/**************************************************************************** ** ** 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 { static PunisherQueue _BuildQueue; static PunisherQueue BuildQueue => StaticThreadSafeInit(() => _BuildQueue, () => _BuildQueue = new PunisherQueue( 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()); EnvDTE.Project Project { get; set; } UnconfiguredProject UnconfiguredProject { get; set; } ConfiguredProject ConfiguredProject { get; set; } Dictionary Properties { get; set; } List Targets { get; set; } LoggerVerbosity LoggerVerbosity { get; set; } static Task BuildDispatcher { get; set; } public static void StartBuild( EnvDTE.Project project, string configName, Dictionary properties, IEnumerable 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 properties, IEnumerable 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( 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")); } } } }