| /****************************************************************************  | 
| **  | 
| ** Copyright (C) 2021 The Qt Company Ltd.  | 
| ** Contact: https://www.qt.io/licensing/  | 
| **  | 
| ** This file is part of the Qt VS Tools.  | 
| **  | 
| ** $QT_BEGIN_LICENSE:GPL-EXCEPT$  | 
| ** Commercial License Usage  | 
| ** Licensees holding valid commercial Qt licenses may use this file in  | 
| ** accordance with the commercial license agreement provided with the  | 
| ** Software or, alternatively, in accordance with the terms contained in  | 
| ** a written agreement between you and The Qt Company. For licensing terms  | 
| ** and conditions see https://www.qt.io/terms-conditions. For further  | 
| ** information use the contact form at https://www.qt.io/contact-us.  | 
| **  | 
| ** GNU General Public License Usage  | 
| ** Alternatively, this file may be used under the terms of the GNU  | 
| ** General Public License version 3 as published by the Free Software  | 
| ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT  | 
| ** included in the packaging of this file. Please review the following  | 
| ** information to ensure the GNU General Public License requirements will  | 
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html.  | 
| **  | 
| ** $QT_END_LICENSE$  | 
| **  | 
| ****************************************************************************/  | 
|   | 
| using System;  | 
| using System.Collections.Generic;  | 
| using System.Collections.Immutable;  | 
| using System.IO;  | 
| using System.Linq;  | 
| using System.Text;  | 
| using System.Threading.Tasks;  | 
| using Microsoft.Build.Evaluation;  | 
| using Microsoft.Build.Execution;  | 
| using Microsoft.Build.Framework;  | 
| using Microsoft.VisualStudio.ProjectSystem;  | 
| using Microsoft.VisualStudio.TaskStatusCenter;  | 
| using Microsoft.VisualStudio.Threading;  | 
| using Microsoft.VisualStudio.VCProjectEngine;  | 
| using EnvDTE;  | 
|   | 
| namespace QtVsTools.QtMsBuild  | 
| {  | 
|     using Core;  | 
|     using VisualStudio;  | 
|     using Thread = System.Threading.Thread;  | 
|   | 
|     class QtProjectBuild : Concurrent<QtProjectBuild>  | 
|     {  | 
|         static PunisherQueue<QtProjectBuild> _BuildQueue;  | 
|         static PunisherQueue<QtProjectBuild> BuildQueue =>  | 
|             StaticThreadSafeInit(() => _BuildQueue,  | 
|                 () => _BuildQueue = new PunisherQueue<QtProjectBuild>(  | 
|                     getItemKey: (QtProjectBuild build) =>  | 
|                     {  | 
|                         return build.ConfiguredProject;  | 
|                     })  | 
|                 );  | 
|   | 
|         static ConcurrentStopwatch _RequestTimer;  | 
|         static ConcurrentStopwatch RequestTimer =>  | 
|             StaticThreadSafeInit(() => _RequestTimer, () => _RequestTimer = new ConcurrentStopwatch());  | 
|   | 
|         static IVsTaskStatusCenterService _StatusCenter;  | 
|         static IVsTaskStatusCenterService StatusCenter => StaticThreadSafeInit(() => _StatusCenter,  | 
|                 () => _StatusCenter = VsServiceProvider  | 
|                     .GetService<SVsTaskStatusCenterService, IVsTaskStatusCenterService>());  | 
|   | 
|         EnvDTE.Project Project { get; set; }  | 
|         UnconfiguredProject UnconfiguredProject { get; set; }  | 
|         ConfiguredProject ConfiguredProject { get; set; }  | 
|         Dictionary<string, string> Properties { get; set; }  | 
|         List<string> Targets { get; set; }  | 
|         LoggerVerbosity LoggerVerbosity { get; set; }  | 
|   | 
|         static Task BuildDispatcher { get; set; }  | 
|   | 
|         public static void StartBuild(  | 
|             EnvDTE.Project project,  | 
|             string configName,  | 
|             Dictionary<string, string> properties,  | 
|             IEnumerable<string> targets,  | 
|             LoggerVerbosity verbosity = LoggerVerbosity.Quiet)  | 
|         {  | 
|             if (project == null)  | 
|                 throw new ArgumentException("Project cannot be null.");  | 
|             if (configName == null)  | 
|                 throw new ArgumentException("Configuration name cannot be null.");  | 
|   | 
|             Task.Run(() => StartBuildAsync(project, configName, properties, targets, verbosity));  | 
|         }  | 
|   | 
|         public static async Task StartBuildAsync(  | 
|             EnvDTE.Project project,  | 
|             string configName,  | 
|             Dictionary<string, string> properties,  | 
|             IEnumerable<string> targets,  | 
|             LoggerVerbosity verbosity)  | 
|         {  | 
|             if (project == null)  | 
|                 throw new ArgumentException("Project cannot be null.");  | 
|             if (configName == null)  | 
|                 throw new ArgumentException("Configuration name cannot be null.");  | 
|   | 
|             RequestTimer.Restart();  | 
|             if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) {  | 
|                 Messages.Print(string.Format(  | 
|                 "{0:HH:mm:ss.FFF} QtProjectBuild({1}): Request [{2}] {3}",  | 
|                 DateTime.Now, Thread.CurrentThread.ManagedThreadId,  | 
|                 configName, project.FullName));  | 
|             }  | 
|   | 
|             var tracker = QtProjectTracker.Get(project);  | 
|             await tracker.Initialized;  | 
|   | 
|             var knownConfigs = await tracker.UnconfiguredProject.Services  | 
|                 .ProjectConfigurationsService.GetKnownProjectConfigurationsAsync();  | 
|   | 
|             ConfiguredProject configuredProject = null;  | 
|             foreach (var config in knownConfigs) {  | 
|                 var configProject = await tracker.UnconfiguredProject  | 
|                     .LoadConfiguredProjectAsync(config);  | 
|                 if (configProject.ProjectConfiguration.Name == configName) {  | 
|                     configuredProject = configProject;  | 
|                     break;  | 
|                 }  | 
|             }  | 
|             if (configuredProject == null)  | 
|                 throw new ArgumentException(string.Format("Unknown configuration '{0}'.", configName));  | 
|   | 
|             BuildQueue.Enqueue(new QtProjectBuild()  | 
|             {  | 
|                 Project = project,  | 
|                 UnconfiguredProject = tracker.UnconfiguredProject,  | 
|                 ConfiguredProject = configuredProject,  | 
|                 Properties = properties?.ToDictionary(x => x.Key, x => x.Value),  | 
|                 Targets = targets?.ToList(),  | 
|                 LoggerVerbosity = verbosity  | 
|             });  | 
|             StaticThreadSafeInit(() => BuildDispatcher,  | 
|                 () => BuildDispatcher = Task.Run(BuildDispatcherLoopAsync))  | 
|                 .Forget();  | 
|         }  | 
|   | 
|         public static void Reset()  | 
|         {  | 
|             BuildQueue.Clear();  | 
|         }  | 
|   | 
|         static async Task BuildDispatcherLoopAsync()  | 
|         {  | 
|             ITaskHandler2 dispatchStatus = null;  | 
|             while (!QtVsToolsPackage.Instance.Zombied) {  | 
|                 while (BuildQueue.IsEmpty || RequestTimer.ElapsedMilliseconds < 1000) {  | 
|                     if (BuildQueue.IsEmpty && dispatchStatus != null) {  | 
|                         dispatchStatus.Dismiss();  | 
|                         dispatchStatus = null;  | 
|                     }  | 
|                     await Task.Delay(100);  | 
|                 }  | 
|                 QtProjectBuild buildRequest;  | 
|                 if (BuildQueue.TryDequeue(out buildRequest)) {  | 
|                     if (dispatchStatus == null) {  | 
|                         dispatchStatus = StatusCenter.PreRegister(  | 
|                             new TaskHandlerOptions  | 
|                             {  | 
|                                 Title = "Qt VS Tools",  | 
|                             },  | 
|                             new TaskProgressData  | 
|                             {  | 
|                                 ProgressText = string.Format(  | 
|                                     "Refreshing IntelliSense data, {0} project(s) remaining...",  | 
|                                     BuildQueue.Count),  | 
|                                 CanBeCanceled = true  | 
|                             })  | 
|                             as ITaskHandler2;  | 
|                         dispatchStatus.RegisterTask(new Task(() =>  | 
|                             throw new InvalidOperationException()));  | 
|                     } else {  | 
|                         dispatchStatus.Progress.Report(  | 
|                             new TaskProgressData  | 
|                             {  | 
|                                 ProgressText = string.Format(  | 
|                                     "Refreshing IntelliSense data, {0} project(s) remaining...",  | 
|                                     BuildQueue.Count),  | 
|                                 CanBeCanceled = true,  | 
|                             });  | 
|                     }  | 
|                     await buildRequest.BuildAsync();  | 
|                 }  | 
|                 if (BuildQueue.IsEmpty  | 
|                     || dispatchStatus?.UserCancellation.IsCancellationRequested == true) {  | 
|                     if (dispatchStatus != null) {  | 
|                         dispatchStatus.Dismiss();  | 
|                         dispatchStatus = null;  | 
|                     }  | 
|                     Reset();  | 
|                 }  | 
|             }  | 
|         }  | 
|   | 
|         async Task BuildAsync()  | 
|         {  | 
|             if (LoggerVerbosity != LoggerVerbosity.Quiet) {  | 
|                 Messages.Print(clear: !QtVsToolsPackage.Instance.Options.BuildDebugInformation, activate: true,  | 
|                     text: string.Format(  | 
| @"== {0}: starting build...  | 
|   * Properties: {1}  | 
|   * Targets: {2}  | 
| ",  | 
|                     /*{0}*/ Project.Name,  | 
|                     /*{1}*/ string.Join("", Properties  | 
|                         .Select(property => string.Format(@"  | 
|         {0} = {1}",     /*{0}*/ property.Key, /*{1}*/ property.Value))),  | 
|                     /*{2}*/ string.Join(";", Targets)));  | 
|             }  | 
|   | 
|             var lockService = UnconfiguredProject.ProjectService.Services.ProjectLockService;  | 
|   | 
|             bool ok = false;  | 
|             try {  | 
|                 ProjectWriteLockReleaser writeAccess;  | 
|                 var timer = ConcurrentStopwatch.StartNew();  | 
|                 while (timer.IsRunning) {  | 
|                     try {  | 
|                         writeAccess = await lockService.WriteLockAsync();  | 
|                         timer.Stop();  | 
|                     } catch (InvalidOperationException) {  | 
|                         if (timer.ElapsedMilliseconds >= 5000)  | 
|                             throw;  | 
|                         using (var readAccess = await lockService.ReadLockAsync())  | 
|                             await readAccess.ReleaseAsync();  | 
|                     }  | 
|                 }  | 
|   | 
|                 using (writeAccess) {  | 
|                     var msBuildProject = await writeAccess.GetProjectAsync(ConfiguredProject);  | 
|   | 
|                     var solutionPath = QtProjectTracker.SolutionPath;  | 
|                     var configProps = new Dictionary<string, string>(  | 
|                         ConfiguredProject.ProjectConfiguration.Dimensions.ToImmutableDictionary())  | 
|                     {  | 
|                         { "SolutionPath", solutionPath },  | 
|                         { "SolutionFileName", Path.GetFileName(solutionPath) },  | 
|                         { "SolutionName", Path.GetFileNameWithoutExtension(solutionPath) },  | 
|                         { "SolutionExt", Path.GetExtension(solutionPath) },  | 
|                         { "SolutionDir", Path.GetDirectoryName(solutionPath).TrimEnd('\\') + '\\'  }  | 
|                     };  | 
|   | 
|                     foreach (var property in Properties)  | 
|                         configProps[property.Key] = property.Value;  | 
|   | 
|                     var projectInstance = new ProjectInstance(msBuildProject.Xml,  | 
|                         configProps, null, new ProjectCollection());  | 
|   | 
|                     var loggerVerbosity = LoggerVerbosity;  | 
|                     if (QtVsToolsPackage.Instance.Options.BuildDebugInformation)  | 
|                         loggerVerbosity = QtVsToolsPackage.Instance.Options.BuildLoggerVerbosity;  | 
|                     var buildParams = new BuildParameters()  | 
|                     {  | 
|                         Loggers = (loggerVerbosity != LoggerVerbosity.Quiet)  | 
|                                 ? new[] { new QtProjectLogger() { Verbosity = loggerVerbosity } }  | 
|                                 : null  | 
|                     };  | 
|   | 
|                     var buildRequest = new BuildRequestData(projectInstance,  | 
|                         Targets.ToArray(),  | 
|                         hostServices: null,  | 
|                         flags: BuildRequestDataFlags.ProvideProjectStateAfterBuild);  | 
|   | 
|                     if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) {  | 
|                         Messages.Print(string.Format(  | 
|                             "{0:HH:mm:ss.FFF} QtProjectBuild({1}): Build [{2}] {3}",  | 
|                             DateTime.Now, Thread.CurrentThread.ManagedThreadId,  | 
|                             ConfiguredProject.ProjectConfiguration.Name,  | 
|                             UnconfiguredProject.FullPath));  | 
|                         Messages.Print("=== Targets");  | 
|                         foreach (var target in buildRequest.TargetNames)  | 
|                             Messages.Print(string.Format("    {0}", target));  | 
|                         Messages.Print("=== Properties");  | 
|                         foreach (var property in Properties) {  | 
|                             Messages.Print(string.Format("    {0}={1}",  | 
|                                 property.Key, property.Value));  | 
|                         }  | 
|                     }  | 
|   | 
|                     BuildResult result = null;  | 
|                     while (result == null) {  | 
|                         try {  | 
|                             result = BuildManager.DefaultBuildManager.Build(  | 
|                                 buildParams, buildRequest);  | 
|                         } catch (InvalidOperationException) {  | 
|                             if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) {  | 
|                                 Messages.Print(string.Format(  | 
|                                     "{0:HH:mm:ss.FFF} QtProjectBuild({1}): [{2}] "  | 
|                                     + "Warning: Another build is in progress; waiting...",  | 
|                                     DateTime.Now,  | 
|                                     Thread.CurrentThread.ManagedThreadId,  | 
|                                     ConfiguredProject.ProjectConfiguration.Name));  | 
|                             }  | 
|                             await Task.Delay(3000);  | 
|                         }  | 
|                     }  | 
|   | 
|                     if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) {  | 
|                         string resMsg;  | 
|                         StringBuilder resInfo = new StringBuilder();  | 
|                         if (result?.OverallResult == BuildResultCode.Success) {  | 
|                             resMsg = "Build ok";  | 
|                         } else {  | 
|                             resMsg = "Build FAIL";  | 
|                             if (result == null) {  | 
|                                 resInfo.AppendLine("####### Build returned 'null'");  | 
|                             } else {  | 
|                                 resInfo.AppendLine("####### Build returned 'Failure' code");  | 
|                                 if (result.ResultsByTarget != null) {  | 
|                                     foreach (var tr in result.ResultsByTarget) {  | 
|                                         var res = tr.Value;  | 
|                                         if (res.ResultCode != TargetResultCode.Failure)  | 
|                                             continue;  | 
|                                         resInfo.AppendFormat("### Target '{0}' FAIL\r\n", tr.Key);  | 
|                                         if (res.Items != null && res.Items.Length > 0) {  | 
|                                             resInfo.AppendFormat(  | 
|                                                 "Items: {0}\r\n", string.Join(", ", res.Items  | 
|                                                     .Select(it => it.ItemSpec)));  | 
|                                         }  | 
|                                         var e = tr.Value?.Exception;  | 
|                                         if (e != null) {  | 
|                                             resInfo.AppendFormat(  | 
|                                                 "Exception: {0}\r\nStacktrace:\r\n{1}\r\n",  | 
|                                                 e.Message, e.StackTrace);  | 
|                                         }  | 
|                                     }  | 
|                                 }  | 
|                             }  | 
|                         }  | 
|                         Messages.Print(string.Format(  | 
|                             "{0:HH:mm:ss.FFF} QtProjectBuild({1}): [{2}] {3}\r\n{4}",  | 
|                             DateTime.Now, Thread.CurrentThread.ManagedThreadId,  | 
|                             ConfiguredProject.ProjectConfiguration.Name,  | 
|                             resMsg, resInfo.ToString()));  | 
|                     }  | 
|   | 
|                     if (result == null  | 
|                         || result.ResultsByTarget == null  | 
|                         || result.OverallResult != BuildResultCode.Success) {  | 
|                         Messages.Print(string.Format("{0}: background build FAILED!",  | 
|                                 Path.GetFileName(UnconfiguredProject.FullPath)));  | 
|                     } else {  | 
|                         var checkResults = result.ResultsByTarget  | 
|                             .Where(x => Targets.Contains(x.Key))  | 
|                             .Select(x => x.Value);  | 
|                         ok = checkResults.Any()  | 
|                             && checkResults.All(x => x.ResultCode == TargetResultCode.Success);  | 
|                         if (ok)  | 
|                             msBuildProject.MarkDirty();  | 
|                     }  | 
|                     await writeAccess.ReleaseAsync();  | 
|                 }  | 
|   | 
|                 if (ok) {  | 
|                     var vcProject = Project.Object as VCProject;  | 
|                     var vcConfigs = vcProject.Configurations as IVCCollection;  | 
|                     var vcConfig = vcConfigs.Item(ConfiguredProject.ProjectConfiguration.Name) as VCConfiguration;  | 
|                     var props = vcConfig.Rules.Item("QtRule10_Settings") as IVCRulePropertyStorage;  | 
|                     props.SetPropertyValue("QtLastBackgroundBuild", DateTime.UtcNow.ToString("o"));  | 
|                 }  | 
|             } catch (Exception e) {  | 
|                 Messages.Print(string.Format("{0}: background build ERROR: {1}",  | 
|                         Path.GetFileName(UnconfiguredProject.FullPath), e.Message));  | 
|             }  | 
|   | 
|             if (LoggerVerbosity != LoggerVerbosity.Quiet) {  | 
|                 Messages.Print(string.Format(  | 
| @"  | 
| == {0}: build {1}",  | 
|                     Project.Name, ok ? "successful" : "ERROR"));  | 
|             }  | 
|         }  | 
|     }  | 
| }  |