From 0aa8c5fc6cc893dd5aed77397733c2225c682ed6 Mon Sep 17 00:00:00 2001 From: "Yue (Felix) Huang" Date: Tue, 4 Apr 2023 13:02:15 -0700 Subject: [PATCH] Add question flag to "question" the build if it is incremental (#8012) This PR adds "question" switch to msbuild.exe that will error out if a target or a task fails incremental check. Targets will fail if both Inputs and Outputs are present and not skip. Tasks changes are individually modified to support the interface IIncrementalTask, which sets the question boolean. Each task will need to be updated this interface take part. I have started with the following tasks and fixed some of the issues within MSBuild enlistment. And there are more, see the notes below. Tasks updated: ToolTask, Copy, MakeDir, Touch, WriteLinesTofile, RemoveDir, DownloadFile, Move, ZipDirectory, Unzip, GenerateResource, GenerateBindingRedirects. Using question investigate incremental issues is orders of magnitude easier. Reading the logs is simpler and repros are more consistent. In this PR, it includes a few fixes to the common targets which address some issues. Fixes #7348 --- eng/BootStrapMSBuild.targets | 59 ++++--- .../BackEnd/TargetBuilder_Tests.cs | 35 ++++ .../BackEnd/TargetUpToDateChecker_Tests.cs | 2 +- .../BackEnd/TaskBuilder_Tests.cs | 1 - .../BackEnd/BuildManager/BuildParameters.cs | 13 ++ .../Components/RequestBuilder/TargetEntry.cs | 9 +- .../RequestBuilder/TargetUpToDateChecker.cs | 29 +++- .../TaskExecutionHost/TaskExecutionHost.cs | 5 + src/Directory.Build.targets | 2 +- src/Framework/IIncrementalTask.cs | 19 +++ .../MSBuild.Bootstrap.csproj | 4 +- .../CommandLineSwitches_Tests.cs | 1 + src/MSBuild/CommandLineSwitches.cs | 2 + src/MSBuild/Properties/launchSettings.json | 9 + src/MSBuild/Resources/Strings.resx | 17 +- src/MSBuild/Resources/xlf/Strings.cs.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.de.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.es.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.fr.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.it.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.ja.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.ko.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.pl.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.ru.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.tr.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 26 ++- src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 26 ++- src/MSBuild/XMake.cs | 11 ++ src/Tasks.UnitTests/Copy_Tests.cs | 159 ++++++++++++++++++ .../GenerateBindingRedirects_Tests.cs | 5 +- src/Tasks.UnitTests/MakeDir_Tests.cs | 57 +++++++ src/Tasks.UnitTests/RemoveDir_Tests.cs | 32 +++- src/Tasks.UnitTests/Touch_Tests.cs | 75 +++++++++ src/Tasks.UnitTests/Unzip_Tests.cs | 30 +++- src/Tasks.UnitTests/WriteLinesToFile_Tests.cs | 62 +++++++ .../GenerateBindingRedirects.cs | 12 +- .../ResolveAssemblyReference.cs | 10 +- src/Tasks/Copy.cs | 36 +++- src/Tasks/Delete.cs | 23 ++- src/Tasks/DownloadFile.cs | 9 +- src/Tasks/FileIO/WriteLinesToFile.cs | 44 ++++- src/Tasks/GenerateResource.cs | 8 +- src/Tasks/MakeDir.cs | 17 +- .../Microsoft.Common.CurrentVersion.targets | 29 ++-- src/Tasks/Move.cs | 18 +- src/Tasks/RemoveDir.cs | 10 +- src/Tasks/Resources/Strings.resx | 7 + src/Tasks/Resources/xlf/Strings.cs.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.de.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.es.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.fr.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.it.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.ja.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.ko.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.pl.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.ru.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.tr.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 10 ++ src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 10 ++ src/Tasks/Touch.cs | 49 +++++- src/Tasks/Unzip.cs | 9 +- src/Tasks/ZipDirectory.cs | 21 ++- src/Utilities/Resources/Strings.resx | 3 + src/Utilities/Resources/xlf/Strings.cs.xlf | 5 + src/Utilities/Resources/xlf/Strings.de.xlf | 5 + src/Utilities/Resources/xlf/Strings.es.xlf | 5 + src/Utilities/Resources/xlf/Strings.fr.xlf | 5 + src/Utilities/Resources/xlf/Strings.it.xlf | 5 + src/Utilities/Resources/xlf/Strings.ja.xlf | 5 + src/Utilities/Resources/xlf/Strings.ko.xlf | 5 + src/Utilities/Resources/xlf/Strings.pl.xlf | 5 + src/Utilities/Resources/xlf/Strings.pt-BR.xlf | 5 + src/Utilities/Resources/xlf/Strings.ru.xlf | 5 + src/Utilities/Resources/xlf/Strings.tr.xlf | 5 + .../Resources/xlf/Strings.zh-Hans.xlf | 5 + .../Resources/xlf/Strings.zh-Hant.xlf | 5 + src/Utilities/ToolTask.cs | 16 +- 79 files changed, 1352 insertions(+), 140 deletions(-) create mode 100644 src/Framework/IIncrementalTask.cs create mode 100644 src/MSBuild/Properties/launchSettings.json diff --git a/eng/BootStrapMSBuild.targets b/eng/BootStrapMSBuild.targets index 5a6b132e78e..5f09f79ddbc 100644 --- a/eng/BootStrapMSBuild.targets +++ b/eng/BootStrapMSBuild.targets @@ -122,35 +122,45 @@ + DestinationFiles="@(InstalledVersionedExtensions->'$(BootstrapDestination)$(TargetMSBuildToolsVersion)\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + DestinationFiles="@(SdkResolverFiles->'$(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin\SdkResolvers\Microsoft.DotNet.MSBuildSdkResolver\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + DestinationFiles="@(InstalledMicrosoftExtensions->'$(BootstrapDestination)Microsoft\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + Condition="'$(MonoBuild)' != 'true'" + SkipUnchangedFiles="true" /> + Condition="'$(MonoBuild)' == 'true'" + SkipUnchangedFiles="true" /> + DestinationFiles="@(InstalledStaticAnalysisTools -> '$(BootstrapDestination)..\Team Tools\Static Analysis Tools\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + DestinationFiles="@(InstalledNuGetFiles->'$(BootstrapDestination)Microsoft\NuGet\%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + DestinationFolder="$(BootstrapDestination)..\Common7\IDE\CommonExtensions\Microsoft\NuGet\" + SkipUnchangedFiles="true" /> + DestinationFolder="$(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin" + SkipUnchangedFiles="true" /> + DestinationFolder="$(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin\SdkResolvers\Microsoft.Build.NuGetSdkResolver" + SkipUnchangedFiles="true" /> @@ -158,27 +168,36 @@ + DestinationFiles="@(FreshlyBuiltBinaries -> '$(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + + DestinationFiles="@(RoslynBinaries -> '$(BootstrapDestination)15.0\Bin\Roslyn\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> - + - + + DestinationFiles="@(FreshlyBuiltRootProjects -> '$(BootstrapDestination)$(TargetMSBuildToolsVersion)\%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + DestinationFiles="@(FreshlyBuiltProjects -> '$(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + DestinationFiles="@(FreshlyBuiltProjects -> '$(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin\amd64\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> + DestinationFiles="@(FreshlyBuiltProjects -> '$(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin\arm64\%(RecursiveDir)%(Filename)%(Extension)')" + SkipUnchangedFiles="true" /> diff --git a/src/Build.UnitTests/BackEnd/TargetBuilder_Tests.cs b/src/Build.UnitTests/BackEnd/TargetBuilder_Tests.cs index 30eafec7e91..5644861dbc9 100644 --- a/src/Build.UnitTests/BackEnd/TargetBuilder_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TargetBuilder_Tests.cs @@ -236,6 +236,41 @@ Skipping target ""Build"" because all output files are up-to-date with respect t } } + [Fact] + public void TestErrorForSkippedTargetInputsAndOutputs() + { + string projectContents = @" + + + + +"; + + using (var env = TestEnvironment.Create()) + { + var buildParameters = new BuildParameters() + { + Question = true, + }; + + using (var buildSession = new Helpers.BuildManagerSession(env, buildParameters)) + { + var files = env.CreateTestProjectWithFiles(projectContents, new[] { "a.txt", "b.txt", "c.txt" }); + var fileA = new FileInfo(files.CreatedFiles[0]); + var fileB = new FileInfo(files.CreatedFiles[1]); + var fileC = new FileInfo(files.CreatedFiles[2]); + + var now = DateTime.UtcNow; + fileA.LastWriteTimeUtc = now - TimeSpan.FromSeconds(10); + fileB.LastWriteTimeUtc = now + TimeSpan.FromSeconds(10); + fileC.LastWriteTimeUtc = now; + + var result = buildSession.BuildProjectFile(files.ProjectFile); + result.OverallResult.ShouldBe(BuildResultCode.Failure); + } + } + } + /// /// Ensure that skipped targets only infer outputs once /// diff --git a/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs b/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs index 42a4d6545d3..065958278e0 100644 --- a/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs @@ -572,7 +572,7 @@ private DependencyAnalysisResult PerformDependencyAnalysisTestHelper( ItemBucket itemBucket = new ItemBucket(null, null, new Lookup(itemsByName, new PropertyDictionary()), 0); TargetUpToDateChecker analyzer = new TargetUpToDateChecker(p, p.Targets["Build"], _mockHost, BuildEventContext.Invalid); - return analyzer.PerformDependencyAnalysis(itemBucket, out changedTargetInputs, out upToDateTargetInputs); + return analyzer.PerformDependencyAnalysis(itemBucket, false, out changedTargetInputs, out upToDateTargetInputs); } finally { diff --git a/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs b/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs index c91df2ca317..7748a189690 100644 --- a/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs @@ -164,7 +164,6 @@ public void CanceledTasksDoNotLogMSB4181() Loggers = new ILogger[] { logger }, EnableNodeReuse = false }; - ; BuildRequestData data = new BuildRequestData(project.CreateProjectInstance(), new string[] { "test" }, collection.HostServices); manager.BeginBuild(_parameters); diff --git a/src/Build/BackEnd/BuildManager/BuildParameters.cs b/src/Build/BackEnd/BuildManager/BuildParameters.cs index db8405646a1..8d7a8268648 100644 --- a/src/Build/BackEnd/BuildManager/BuildParameters.cs +++ b/src/Build/BackEnd/BuildManager/BuildParameters.cs @@ -205,6 +205,8 @@ public class BuildParameters : ITranslatable /// private bool _logInitialPropertiesAndItems; + private bool _question; + /// /// The settings used to load the project under build /// @@ -303,6 +305,7 @@ internal BuildParameters(BuildParameters other, bool resetEnvironment = false) _outputResultsCacheFile = other._outputResultsCacheFile; DiscardBuildResults = other.DiscardBuildResults; LowPriority = other.LowPriority; + Question = other.Question; ProjectCacheDescriptor = other.ProjectCacheDescriptor; } @@ -808,6 +811,15 @@ public string OutputResultsCacheFile /// public bool LowPriority { get; set; } + /// + /// Gets or sets a value that will error when the build process fails an incremental check. + /// + public bool Question + { + get => _question; + set => _question = value; + } + /// /// Gets or sets the project cache description to use for all or /// in addition to any potential project caches described in each project. @@ -871,6 +883,7 @@ void ITranslatable.Translate(ITranslator translator) translator.Translate(ref _logInitialPropertiesAndItems); translator.TranslateEnum(ref _projectLoadSettings, (int)_projectLoadSettings); translator.Translate(ref _interactive); + translator.Translate(ref _question); translator.TranslateEnum(ref _projectIsolationMode, (int)_projectIsolationMode); // ProjectRootElementCache is not transmitted. diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs index 79f3c4cd0f7..cef0fb834f9 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs @@ -462,7 +462,7 @@ internal async Task ExecuteTarget(ITaskBuilder taskBuilder, BuildRequestEntry re // UNDONE: (Refactor) Refactor TargetUpToDateChecker to take a logging context, not a logging service. MSBuildEventSource.Log.TargetUpToDateStart(); TargetUpToDateChecker dependencyAnalyzer = new TargetUpToDateChecker(requestEntry.RequestConfiguration.Project, _target, targetLoggingContext.LoggingService, targetLoggingContext.BuildEventContext); - DependencyAnalysisResult dependencyResult = dependencyAnalyzer.PerformDependencyAnalysis(bucket, out changedTargetInputs, out upToDateTargetInputs); + DependencyAnalysisResult dependencyResult = dependencyAnalyzer.PerformDependencyAnalysis(bucket, _host.BuildParameters.Question, out changedTargetInputs, out upToDateTargetInputs); MSBuildEventSource.Log.TargetUpToDateStop((int)dependencyResult); switch (dependencyResult) @@ -471,6 +471,13 @@ internal async Task ExecuteTarget(ITaskBuilder taskBuilder, BuildRequestEntry re case DependencyAnalysisResult.FullBuild: case DependencyAnalysisResult.IncrementalBuild: case DependencyAnalysisResult.SkipUpToDate: + if (dependencyResult != DependencyAnalysisResult.SkipUpToDate && _host.BuildParameters.Question && !string.IsNullOrEmpty(_target.Inputs) && !string.IsNullOrEmpty(_target.Outputs)) + { + targetSuccess = false; + aggregateResult = aggregateResult.AggregateResult(new WorkUnitResult(WorkUnitResultCode.Canceled, WorkUnitActionCode.Stop, null)); + break; + } + // Create the lookups used to hold the current set of properties and items lookupForInference = bucket.Lookup; lookupForExecution = bucket.Lookup.Clone(); diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs index 2f444fce909..dcf16e16545 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs @@ -118,6 +118,7 @@ private string TargetOutputSpecification /// incremental build is needed. /// /// + /// /// /// /// @@ -129,6 +130,7 @@ private string TargetOutputSpecification /// internal DependencyAnalysisResult PerformDependencyAnalysis( ItemBucket bucket, + bool question, out ItemDictionary changedTargetInputs, out ItemDictionary upToDateTargetInputs) { @@ -252,7 +254,7 @@ internal DependencyAnalysisResult PerformDependencyAnalysis( } } - LogReasonForBuildingTarget(result); + LogReasonForBuildingTarget(result, question); return result; } @@ -261,15 +263,23 @@ internal DependencyAnalysisResult PerformDependencyAnalysis( /// Does appropriate logging to indicate why this target is being built fully or partially. /// /// - private void LogReasonForBuildingTarget(DependencyAnalysisResult result) + /// + private void LogReasonForBuildingTarget(DependencyAnalysisResult result, bool question) { // Only if we are not logging just critical events should we be logging the details if (!_loggingService.OnlyLogCriticalEvents) { if (result == DependencyAnalysisResult.FullBuild && _dependencyAnalysisDetail.Count > 0) { - // For the full build decision the are three possible outcomes - _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "BuildTargetCompletely", _targetToAnalyze.Name); + if (question) + { + _loggingService.LogError(_buildEventContext, new BuildEventFileInfo(String.Empty), "BuildTargetCompletely", _targetToAnalyze.Name); + } + else + { + // For the full build decision, there are three possible outcomes + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "BuildTargetCompletely", _targetToAnalyze.Name); + } foreach (DependencyAnalysisLogDetail logDetail in _dependencyAnalysisDetail) { @@ -279,8 +289,15 @@ private void LogReasonForBuildingTarget(DependencyAnalysisResult result) } else if (result == DependencyAnalysisResult.IncrementalBuild) { - // For the partial build decision the are three possible outcomes - _loggingService.LogComment(_buildEventContext, MessageImportance.Normal, "BuildTargetPartially", _targetToAnalyze.Name); + if (question) + { + _loggingService.LogError(_buildEventContext, new BuildEventFileInfo(String.Empty), "BuildTargetPartially", _targetToAnalyze.Name); + } + else + { + // For the partial build decision the are three possible outcomes + _loggingService.LogComment(_buildEventContext, MessageImportance.Normal, "BuildTargetPartially", _targetToAnalyze.Name); + } foreach (DependencyAnalysisLogDetail logDetail in _dependencyAnalysisDetail) { string reason = GetIncrementalBuildReason(logDetail); diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index b6157448cf1..69da00e3955 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -369,6 +369,11 @@ bool ITaskExecutionHost.SetTaskParameters(IDictionary - + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToDotNetFrameworkSdkFile('tlbexp.exe')) diff --git a/src/Framework/IIncrementalTask.cs b/src/Framework/IIncrementalTask.cs new file mode 100644 index 00000000000..7a44e93f14b --- /dev/null +++ b/src/Framework/IIncrementalTask.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace Microsoft.Build.Framework +{ + /// + /// Interface for tasks which is supports incrementality. + /// + /// The tasks implementing this interface should return false to stop the build when in is true and task is not fully incremental. Try to provide helpful information to diagnose incremental behavior. + public interface IIncrementalTask + { + /// + /// Set by MSBuild when Question flag is used. + /// + bool FailIfNotIncremental { set; } + } +} diff --git a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj index 5ebb1fa8b8a..9eedf618d79 100644 --- a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj +++ b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj @@ -45,10 +45,10 @@ - + - + diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index ca52d2e3156..3ffb8e5046a 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -1023,6 +1023,7 @@ public void InvalidToolsVersionErrors() isolateProjects: ProjectIsolationMode.False, graphBuildOptions: null, lowPriority: false, + question: false, inputResultsCaches: null, outputResultsCache: null, commandLine: null); diff --git a/src/MSBuild/CommandLineSwitches.cs b/src/MSBuild/CommandLineSwitches.cs index 9a7d968a673..2e5160d0ea9 100644 --- a/src/MSBuild/CommandLineSwitches.cs +++ b/src/MSBuild/CommandLineSwitches.cs @@ -106,6 +106,7 @@ internal enum ParameterizedSwitch InputResultsCaches, OutputResultsCache, LowPriority, + Question, DetailedSummary, NumberOfParameterizedSwitches, } @@ -270,6 +271,7 @@ internal ParameterizedSwitchInfo( new ParameterizedSwitchInfo( new string[] { "inputResultsCaches", "irc" }, ParameterizedSwitch.InputResultsCaches, null, true, null, true, true), new ParameterizedSwitchInfo( new string[] { "outputResultsCache", "orc" }, ParameterizedSwitch.OutputResultsCache, "DuplicateOutputResultsCache", false, null, true, true), new ParameterizedSwitchInfo( new string[] { "lowpriority", "low" }, ParameterizedSwitch.LowPriority, null, false, null, true, false), + new ParameterizedSwitchInfo( new string[] { "question", "q" }, ParameterizedSwitch.Question, null, false, null, true, false), new ParameterizedSwitchInfo( new string[] { "detailedsummary", "ds" }, ParameterizedSwitch.DetailedSummary, null, false, null, true, false), }; diff --git a/src/MSBuild/Properties/launchSettings.json b/src/MSBuild/Properties/launchSettings.json new file mode 100644 index 00000000000..a8c127489fd --- /dev/null +++ b/src/MSBuild/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "MSBuild": { + "commandName": "Project", + "commandLineArgs": "Solution.proj /m /nr:false /bl", + "workingDirectory": "C:\\test\\fastslowTest" + } + } +} \ No newline at end of file diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index b9f33d32577..e923426f326 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -833,8 +833,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -848,6 +847,20 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBUILD : Configuration error MSB1043: The application could not start. {0} diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index 98a88bdeebc..b4fbb8dcedd 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Seznam kódů upozornění, které se nemají považovat za chyby. Pomocí středníku nebo čárky oddělte diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index 9987602025e..30622ac9e89 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Liste der Warnungscodes, die nicht als Fehler behandelt werden sollen. Semikolon oder Komma zum Trennen verwenden diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index eb73ebf0c9d..8d22bc8c809 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Lista de códigos de advertencia para que no se traten como errores. Use un punto y coma o una coma para separar diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index fafd17cc1e2..9cce4115e83 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Liste des codes d’avertissement à traiter non comme des erreurs. Utiliser un point-virgule ou une virgule pour séparer diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 5cf616edf59..a94da3091cc 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -283,8 +304,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -293,7 +313,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Elenco di codici di avviso da non considerare come errori. Usare un punto e virgola o una virgola per separare diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index fd64433fcfb..909ce96e62d 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] エラーとして扱わない警告コードのリスト. セミコロンまたはコンマを使用して、複数の警告コード diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index 4261d2bd6f8..9265420c6e3 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] 오류로 처리하지 않을 경고 코드 목록입니다. 세미콜론이나 쉼표를 사용하여 구분하세요. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 4dc5a69afbd..004ab82ee63 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -283,8 +304,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -293,7 +313,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Lista kodów ostrzeżeń, które mają być traktowane jako błędy. Rozdziel średnik lub przecinek diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index dca3492d709..e2532f03bad 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -280,8 +301,7 @@ arquivo de resposta. - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -290,7 +310,7 @@ arquivo de resposta. Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Lista de códigos de aviso para tratar e não tratar como erros. Use ponto e vírgula ou vírgula para separar diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index a7875856982..320c30cce2f 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -278,8 +299,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -288,7 +308,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Список кодов предупреждений, которые не будут рассматриваться как ошибки. Используйте точку с запятой или запятую для разделения diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index 5f0ac58c172..d5c6710a77d 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] Hata olarak kabul edilmeyen uyarı kodlarının listesi. Ayırmak için noktalı virgül veya virgül kullanın diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 42fee89dc08..35be029857f 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] 不视为错误的警告代码列表. 使用分号或逗号分隔 diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index 09b7f0b0c27..5a5ddb1582c 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -33,6 +33,27 @@ Included response file: {0} + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + -question + (Experimental) Question whether there is any build work. + MSBuild will error out when it detects a target or task + that can be incremental (has inputs and outputs), + but isn't up to date. + (Short form: -q) + + + LOCALIZATION: "MSBuild" should not be localized. + LOCALIZATION: "-question" and "-q" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + MSBuild version {0} for {1} MSBuild version {0} for {1} @@ -279,8 +300,7 @@ - - -warnNotAsError[:code[;code2]] + -warnNotAsError[:code[;code2]] List of warning codes to treats not treat as errors. Use a semicolon or a comma to separate multiple warning codes. Has no effect if the -warnaserror @@ -289,7 +309,7 @@ Example: -warnNotAsError:MSB3026 - + -warnNotAsError[:code[;code2]] 要視為不視為錯誤的警告碼清單。 使用分號或逗號分隔 diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 3183763473c..e082ae28727 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -706,6 +706,7 @@ public static ExitType Execute( bool lowPriority = false; string[] inputResultsCaches = null; string outputResultsCache = null; + bool question = false; GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _); bool buildCanBeInvoked = ProcessCommandLineSwitches( @@ -741,6 +742,7 @@ public static ExitType Execute( ref inputResultsCaches, ref outputResultsCache, ref lowPriority, + ref question, recursing: false, #if FEATURE_GET_COMMANDLINE commandLine); @@ -808,6 +810,7 @@ public static ExitType Execute( isolateProjects, graphBuildOptions, lowPriority, + question, inputResultsCaches, outputResultsCache, commandLine)) @@ -1121,6 +1124,7 @@ internal static bool BuildProject( ProjectIsolationMode isolateProjects, GraphBuildOptions graphBuildOptions, bool lowPriority, + bool question, string[] inputResultsCaches, string outputResultsCache, #if FEATURE_GET_COMMANDLINE @@ -1292,6 +1296,7 @@ internal static bool BuildProject( parameters.ProjectIsolationMode = isolateProjects; parameters.InputResultsCacheFiles = inputResultsCaches; parameters.OutputResultsCacheFile = outputResultsCache; + parameters.Question = question; // Propagate the profiler flag into the project load settings so the evaluator // can pick it up @@ -2215,6 +2220,7 @@ private static bool ProcessCommandLineSwitches( ref string[] inputResultsCaches, ref string outputResultsCache, ref bool lowPriority, + ref bool question, bool recursing, string commandLine) { @@ -2330,6 +2336,7 @@ private static bool ProcessCommandLineSwitches( ref inputResultsCaches, ref outputResultsCache, ref lowPriority, + ref question, recursing: true, commandLine); } @@ -2393,10 +2400,13 @@ private static bool ProcessCommandLineSwitches( graphBuild = ProcessGraphBuildSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.GraphBuild]); } + question = commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Question); + inputResultsCaches = ProcessInputResultsCaches(commandLineSwitches); outputResultsCache = ProcessOutputResultsCache(commandLineSwitches); + // figure out which loggers are going to listen to build events string[][] groupedFileLoggerParameters = commandLineSwitches.GetFileLoggerParameters(); @@ -4045,6 +4055,7 @@ private static void ShowHelpMessage() Console.WriteLine(AssemblyResources.GetString("HelpMessage_OutputCacheFile")); Console.WriteLine(AssemblyResources.GetString("HelpMessage_36_GraphBuildSwitch")); Console.WriteLine(AssemblyResources.GetString("HelpMessage_39_LowPrioritySwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_41_QuestionSwitch")); Console.WriteLine(AssemblyResources.GetString("HelpMessage_7_ResponseFile")); Console.WriteLine(AssemblyResources.GetString("HelpMessage_8_NoAutoResponseSwitch")); Console.WriteLine(AssemblyResources.GetString("HelpMessage_5_NoLogoSwitch")); diff --git a/src/Tasks.UnitTests/Copy_Tests.cs b/src/Tasks.UnitTests/Copy_Tests.cs index 555b200c29f..0832273c79b 100644 --- a/src/Tasks.UnitTests/Copy_Tests.cs +++ b/src/Tasks.UnitTests/Copy_Tests.cs @@ -128,6 +128,165 @@ public void DontCopyOverSameFile() } } + /// + /// Question should not copy any files. + /// + [Fact] + public void QuestionCopyFile() + { + string source = FileUtilities.GetTemporaryFile(); + string destination = FileUtilities.GetTemporaryFile(null, ".tmp", false); + string content = "This is a source file."; + + try + { + using (StreamWriter sw = FileUtilities.OpenWrite(source, true)) + { + sw.Write(content); + } + + ITaskItem sourceItem = new TaskItem(source); + ITaskItem destinationItem = new TaskItem(destination); + ITaskItem[] sourceFiles = { sourceItem }; + ITaskItem[] destinationFiles = { destinationItem }; + + CopyMonitor m = new CopyMonitor(); + Copy t = new Copy + { + RetryDelayMilliseconds = 1, // speed up tests! + BuildEngine = new MockEngine(_testOutputHelper), + SourceFiles = sourceFiles, + DestinationFiles = destinationFiles, + UseHardlinksIfPossible = UseHardLinks, + UseSymboliclinksIfPossible = UseSymbolicLinks, + FailIfNotIncremental = true, + }; + + Assert.False(t.Execute(m.CopyFile, _parallelismThreadCount)); + + // Expect for there to have been no copies. + Assert.Equal(0, m.copyCount); + + Assert.False(FileUtilities.FileExistsNoThrow(destination)); + } + finally + { + File.Delete(source); + } + } + + /// + /// Question copy should not error if copy did no work. + /// + [Fact] + public void QuestionCopyFileSameContent() + { + string source = FileUtilities.GetTemporaryFile(); + string destination = FileUtilities.GetTemporaryFile(); + string content = "This is a source file."; + DateTime testTime = DateTime.Now; + + try + { + using (StreamWriter sw = FileUtilities.OpenWrite(source, true)) + { + sw.Write(content); + } + + using (StreamWriter sw = FileUtilities.OpenWrite(destination, true)) + { + sw.Write(content); + } + + FileInfo sourcefi = new FileInfo(source); + sourcefi.LastWriteTimeUtc = testTime; + + FileInfo destinationfi = new FileInfo(destination); + destinationfi.LastWriteTimeUtc = testTime; + + ITaskItem sourceItem = new TaskItem(source); + ITaskItem destinationItem = new TaskItem(destination); + ITaskItem[] sourceFiles = { sourceItem }; + ITaskItem[] destinationFiles = { destinationItem }; + + CopyMonitor m = new CopyMonitor(); + Copy t = new Copy + { + RetryDelayMilliseconds = 1, // speed up tests! + BuildEngine = new MockEngine(_testOutputHelper), + SourceFiles = sourceFiles, + DestinationFiles = destinationFiles, + UseHardlinksIfPossible = UseHardLinks, + UseSymboliclinksIfPossible = UseSymbolicLinks, + SkipUnchangedFiles = true, + FailIfNotIncremental = true, + }; + Assert.True(t.Execute(m.CopyFile, _parallelismThreadCount)); + + // Expect for there to have been no copies. + Assert.Equal(0, m.copyCount); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(source); + File.Delete(destination); + } + } + + /// + /// Question copy should error if a copy will occur. + /// + [Fact] + public void QuestionCopyFileNotSameContent() + { + string source = FileUtilities.GetTemporaryFile(); + string destination = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = FileUtilities.OpenWrite(source, true)) + { + sw.Write("This is a source file."); + } + + using (StreamWriter sw = FileUtilities.OpenWrite(destination, true)) + { + sw.Write("This is a destination file."); + } + + ITaskItem sourceItem = new TaskItem(source); + ITaskItem destinationItem = new TaskItem(destination); + ITaskItem[] sourceFiles = { sourceItem }; + ITaskItem[] destinationFiles = { destinationItem }; + + CopyMonitor m = new CopyMonitor(); + Copy t = new Copy + { + RetryDelayMilliseconds = 1, // speed up tests! + BuildEngine = new MockEngine(_testOutputHelper), + SourceFiles = sourceFiles, + DestinationFiles = destinationFiles, + UseHardlinksIfPossible = UseHardLinks, + UseSymboliclinksIfPossible = UseSymbolicLinks, + SkipUnchangedFiles = true, + FailIfNotIncremental = true, + }; + + Assert.False(t.Execute(m.CopyFile, _parallelismThreadCount)); + + // Expect for there to have been no copies. + Assert.Equal(0, m.copyCount); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(source); + File.Delete(destination); + } + } + /// /// Unless ignore readonly attributes is set, we should not copy over readonly files. /// diff --git a/src/Tasks.UnitTests/GenerateBindingRedirects_Tests.cs b/src/Tasks.UnitTests/GenerateBindingRedirects_Tests.cs index 927348ec7e1..f80107870cf 100644 --- a/src/Tasks.UnitTests/GenerateBindingRedirects_Tests.cs +++ b/src/Tasks.UnitTests/GenerateBindingRedirects_Tests.cs @@ -289,10 +289,9 @@ public void AppConfigFileNotSavedWhenIdentical() // Verify it ran correctly and that it's still old redirectResults2.ExecuteResult.ShouldBeTrue(); redirectResults2.TargetAppConfigContent.ShouldContain(""); - redirectResults.TargetAppConfigContent.ShouldContain("newVersion=\"40.0.0.0\""); + redirectResults2.TargetAppConfigContent.ShouldContain("newVersion=\"40.0.0.0\""); - File.GetCreationTime(outputAppConfigFile).ShouldBe(oldTimestamp, TimeSpan.FromSeconds(5)); - File.GetLastWriteTime(outputAppConfigFile).ShouldBe(oldTimestamp, TimeSpan.FromSeconds(5)); + File.GetLastWriteTime(outputAppConfigFile).ShouldBeGreaterThan(oldTimestamp); } private BindingRedirectsExecutionResult GenerateBindingRedirects(string appConfigFile, string targetAppConfigFile, diff --git a/src/Tasks.UnitTests/MakeDir_Tests.cs b/src/Tasks.UnitTests/MakeDir_Tests.cs index fd390661eb3..8a50acbd4c3 100644 --- a/src/Tasks.UnitTests/MakeDir_Tests.cs +++ b/src/Tasks.UnitTests/MakeDir_Tests.cs @@ -166,6 +166,63 @@ public void CreateNewDirectory() } } + /// + /// Question Create Directory when a directory is needed to be created should return false. + /// + [Fact] + public void QuestionCreateNewDirectory() + { + string temp = Path.GetTempPath(); + string dir = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A38C"); + + try + { + var dirList = new ITaskItem[] + { + new TaskItem(dir) + }; + MakeDir t = new MakeDir(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.FailIfNotIncremental = true; + t.Directories = dirList; + + bool success = t.Execute(); + + Assert.False(success); + Assert.Single(t.DirectoriesCreated); + Assert.Contains( + String.Format(AssemblyResources.GetString("MakeDir.Comment"), dir), + engine.Log); + + // Actually create the directory + // Note: Need a new task to reset the Log.HasLoggedErrors + engine.Log = ""; + t = new MakeDir(); + t.BuildEngine = engine; + t.Directories = dirList; + success = t.Execute(); + Assert.True(success); + + // Question an existing directory should return true. + engine.Log = ""; + t.FailIfNotIncremental = true; + success = t.Execute(); + Assert.True(success); + + // should still return directory even though it didn't need to be created + Assert.Single(t.DirectoriesCreated); + Assert.Equal(dir, t.DirectoriesCreated[0].ItemSpec); + Assert.DoesNotContain( + String.Format(AssemblyResources.GetString("MakeDir.Comment"), dir), + engine.Log); + } + finally + { + FileUtilities.DeleteWithoutTrailingBackslash(dir); + } + } + /* * Method: FileAlreadyExists * diff --git a/src/Tasks.UnitTests/RemoveDir_Tests.cs b/src/Tasks.UnitTests/RemoveDir_Tests.cs index 08a081023ea..c0dd5b24cc2 100644 --- a/src/Tasks.UnitTests/RemoveDir_Tests.cs +++ b/src/Tasks.UnitTests/RemoveDir_Tests.cs @@ -47,7 +47,6 @@ public void AttributeForwarding() [Fact] public void SimpleDelete() { - using (TestEnvironment env = TestEnvironment.Create(_output)) { List list = new List(); @@ -57,19 +56,36 @@ public void SimpleDelete() list.Add(new TaskItem(env.CreateFolder().Path)); } - RemoveDir t = new RemoveDir(); - - t.Directories = list.ToArray(); - t.BuildEngine = new MockEngine(_output); - - t.Execute().ShouldBeTrue(); + // Question RemoveDir when files exists. + RemoveDir t = new RemoveDir() + { + Directories = list.ToArray(), + BuildEngine = new MockEngine(_output), + FailIfNotIncremental = true, + }; + t.Execute().ShouldBeFalse(); - t.RemovedDirectories.Length.ShouldBe(list.Count); + RemoveDir t2 = new RemoveDir() + { + Directories = list.ToArray(), + BuildEngine = new MockEngine(_output), + }; + t2.Execute().ShouldBeTrue(); + t2.RemovedDirectories.Length.ShouldBe(list.Count); for (int i = 0; i < 20; i++) { Directory.Exists(list[i].ItemSpec).ShouldBeFalse(); } + + // Question again to make sure all files were deleted. + RemoveDir t3 = new RemoveDir() + { + Directories = list.ToArray(), + BuildEngine = new MockEngine(_output), + FailIfNotIncremental = true, + }; + t3.Execute().ShouldBeTrue(); } } diff --git a/src/Tasks.UnitTests/Touch_Tests.cs b/src/Tasks.UnitTests/Touch_Tests.cs index d87d2e5a65d..72117637d4a 100644 --- a/src/Tasks.UnitTests/Touch_Tests.cs +++ b/src/Tasks.UnitTests/Touch_Tests.cs @@ -332,5 +332,80 @@ public void TouchNonExistingDirectoryDoesntExist() Assert.Contains("MSB3371", engine.Log); Assert.Contains(nonexisting_txt, engine.Log); } + + /// + /// Question touch on non-existing file should return false. + /// + [Fact] + public void QuestionTouchNonExisting() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.FailIfNotIncremental = true; + + t.Files = new ITaskItem[] + { + new TaskItem(mynonexisting_txt) + }; + + bool success = Execute(t); + + // Not success because the file doesn't exist + Assert.False(success); + + Assert.Contains( + String.Format(AssemblyResources.GetString("Touch.FileDoesNotExist"), mynonexisting_txt), + engine.Log); + } + + /// + /// Question touch on a non-existing file with AlwaysCreate property should return false. + /// + [Fact] + public void QuestionTouchNonExistingAlwaysCreate() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.FailIfNotIncremental = true; + t.AlwaysCreate = true; + t.Files = new ITaskItem[] + { + new TaskItem(mynonexisting_txt) + }; + + bool success = Execute(t); + + Assert.False(success); + + Assert.Contains( + String.Format(AssemblyResources.GetString("Touch.CreatingFile"), mynonexisting_txt, "AlwaysCreate"), + engine.Log); + } + + /// + /// Question touch should return true and the file is not touched. + /// + [Fact] + public void QuestionTouchExisting() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.FailIfNotIncremental = true; + t.Files = new ITaskItem[] + { + new TaskItem(myexisting_txt) + }; + + bool success = Execute(t); + + Assert.False(success); + + Assert.Contains( + String.Format(AssemblyResources.GetString("Touch.Touching"), myexisting_txt), + engine.Log); + } } } diff --git a/src/Tasks.UnitTests/Unzip_Tests.cs b/src/Tasks.UnitTests/Unzip_Tests.cs index 26de13ddc87..eb2dc83d338 100644 --- a/src/Tasks.UnitTests/Unzip_Tests.cs +++ b/src/Tasks.UnitTests/Unzip_Tests.cs @@ -60,19 +60,45 @@ public void CanUnzip() TransientZipArchive zipArchive = TransientZipArchive.Create(source, testEnvironment.CreateFolder(createFolder: true)); + // Question new task, should be false. Unzip unzip = new Unzip { BuildEngine = _mockEngine, DestinationFolder = new TaskItem(destination.Path), OverwriteReadOnlyFiles = true, SkipUnchangedFiles = false, - SourceFiles = new ITaskItem[] { new TaskItem(zipArchive.Path) } + SourceFiles = new ITaskItem[] { new TaskItem(zipArchive.Path) }, + FailIfNotIncremental = true, }; + unzip.Execute().ShouldBeFalse(() => _mockEngine.Log); + _mockEngine.Log = string.Empty; - unzip.Execute().ShouldBeTrue(() => _mockEngine.Log); + // Run the task. + Unzip unzip2 = new Unzip + { + BuildEngine = _mockEngine, + DestinationFolder = new TaskItem(destination.Path), + OverwriteReadOnlyFiles = true, + SkipUnchangedFiles = false, + SourceFiles = new ITaskItem[] { new TaskItem(zipArchive.Path) }, + FailIfNotIncremental = false, + }; + unzip2.Execute().ShouldBeTrue(() => _mockEngine.Log); _mockEngine.Log.ShouldContain(Path.Combine(destination.Path, "BE78A17D30144B549D21F71D5C633F7D.txt"), () => _mockEngine.Log); _mockEngine.Log.ShouldContain(Path.Combine(destination.Path, "A04FF4B88DF14860B7C73A8E75A4FB76.txt"), () => _mockEngine.Log); + + // Question ran task, should be true + Unzip unzip3 = new Unzip + { + BuildEngine = _mockEngine, + DestinationFolder = new TaskItem(destination.Path), + OverwriteReadOnlyFiles = true, + SkipUnchangedFiles = true, + SourceFiles = new ITaskItem[] { new TaskItem(zipArchive.Path) }, + FailIfNotIncremental = true, + }; + unzip3.Execute().ShouldBeTrue(() => _mockEngine.Log); } } diff --git a/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs b/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs index 893a3cfa3c4..0b3d12f099e 100644 --- a/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs +++ b/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs @@ -176,6 +176,68 @@ public void RedundantParametersAreLogged() engine.AssertLogContainsMessageFromResource(AssemblyResources.GetString, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", file); } + /// + /// Question WriteLines to return false when a write will be required. + /// + [Fact] + public void QuestionWriteLinesWriteOnlyWhenDifferentTest() + { + var file = FileUtilities.GetTemporaryFile(); + try + { + // Write an initial file. + var a = new WriteLinesToFile + { + Overwrite = true, + BuildEngine = new MockEngine(_output), + File = new TaskItem(file), + WriteOnlyWhenDifferent = true, + Lines = new ITaskItem[] { new TaskItem("File contents1") } + }; + + a.Execute().ShouldBeTrue(); + + // Verify contents + var r = new ReadLinesFromFile { File = new TaskItem(file) }; + r.Execute().ShouldBeTrue(); + r.Lines[0].ItemSpec.ShouldBe("File contents1"); + + var writeTime = DateTime.Now.AddHours(-1); + + File.SetLastWriteTime(file, writeTime); + + // Write the same contents to the file, timestamps should match. + var a2 = new WriteLinesToFile + { + Overwrite = true, + BuildEngine = new MockEngine(_output), + File = new TaskItem(file), + WriteOnlyWhenDifferent = true, + Lines = new ITaskItem[] { new TaskItem("File contents1") }, + FailIfNotIncremental = true, + }; + a2.Execute().ShouldBeTrue(); + File.GetLastWriteTime(file).ShouldBe(writeTime, tolerance: TimeSpan.FromSeconds(1)); + + // Write different contents to the file, last write time should differ. + var a3 = new WriteLinesToFile + { + Overwrite = true, + BuildEngine = new MockEngine(_output), + File = new TaskItem(file), + WriteOnlyWhenDifferent = true, + Lines = new ITaskItem[] { new TaskItem("File contents2") }, + FailIfNotIncremental = true, + }; + a3.Execute().ShouldBeFalse(); + File.GetLastWriteTime(file).ShouldBe(writeTime, tolerance: TimeSpan.FromSeconds(1)); + } + finally + { + File.Delete(file); + } + } + /// /// Should create directory structure when target does not exist. /// diff --git a/src/Tasks/AssemblyDependency/GenerateBindingRedirects.cs b/src/Tasks/AssemblyDependency/GenerateBindingRedirects.cs index fbb3c2470cb..19aab4b83e6 100644 --- a/src/Tasks/AssemblyDependency/GenerateBindingRedirects.cs +++ b/src/Tasks/AssemblyDependency/GenerateBindingRedirects.cs @@ -9,6 +9,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; +using System.IO; #nullable disable @@ -103,8 +104,9 @@ public override bool Execute() runtimeNode.Add(redirectNodes); var writeOutput = true; + var outputExists = FileSystems.Default.FileExists(OutputAppConfigFile.ItemSpec); - if (FileSystems.Default.FileExists(OutputAppConfigFile.ItemSpec)) + if (outputExists) { try { @@ -131,11 +133,19 @@ public override bool Execute() if (writeOutput) { + Log.LogMessageFromResources(MessageImportance.Low, "GenerateBindingRedirects.CreatingBindingRedirectionFile", OutputAppConfigFile.ItemSpec); using (var stream = FileUtilities.OpenWrite(OutputAppConfigFile.ItemSpec, false)) { doc.Save(stream); } } + else if (outputExists) + { + // if the file exists and the content is up to date, then touch the output file. + var now = DateTime.Now; + File.SetLastAccessTime(OutputAppConfigFile.ItemSpec, now); + File.SetLastWriteTime(OutputAppConfigFile.ItemSpec, now); + } return !Log.HasLoggedErrors; } diff --git a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs index a3942efd097..afdde42ef14 100644 --- a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs +++ b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs @@ -29,7 +29,7 @@ namespace Microsoft.Build.Tasks /// Given a list of assemblyFiles, determine the closure of all assemblyFiles that /// depend on those assemblyFiles including second and nth-order dependencies too. /// - public class ResolveAssemblyReference : TaskExtension + public class ResolveAssemblyReference : TaskExtension, IIncrementalTask { /// /// key assembly used to trigger inclusion of facade references. @@ -888,6 +888,8 @@ public string[] FullFrameworkFolders } } + public bool FailIfNotIncremental { get; set; } + /// /// This is a list of all primary references resolved to full paths. /// bool CopyLocal - whether the given reference should be copied to the output directory. @@ -2054,6 +2056,12 @@ internal void WriteStateFile() } else if (!String.IsNullOrEmpty(_stateFile) && _cache.IsDirty) { + if (FailIfNotIncremental) + { + Log.LogErrorFromResources("ResolveAssemblyReference.WritingCacheFile", _stateFile); + return; + } + _cache.SerializeCache(_stateFile, Log); } } diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs index 15faca0d420..979c9652291 100644 --- a/src/Tasks/Copy.cs +++ b/src/Tasks/Copy.cs @@ -21,7 +21,7 @@ namespace Microsoft.Build.Tasks /// /// A task that copies files. /// - public class Copy : TaskExtension, ICancelableTask + public class Copy : TaskExtension, IIncrementalTask, ICancelableTask { internal const string AlwaysRetryEnvVar = "MSBUILDALWAYSRETRY"; internal const string AlwaysOverwriteReadOnlyFilesEnvVar = "MSBUILDALWAYSOVERWRITEREADONLYFILES"; @@ -153,6 +153,8 @@ public Copy() /// public bool OverwriteReadOnlyFiles { get; set; } + public bool FailIfNotIncremental { get; set; } + #endregion /// @@ -254,8 +256,16 @@ private void LogDiagnostic(string message, params object[] messageArgs) { if (!FileSystems.Default.DirectoryExists(destinationFolder)) { - Log.LogMessage(MessageImportance.Normal, CreatesDirectory, destinationFolder); - Directory.CreateDirectory(destinationFolder); + if (FailIfNotIncremental) + { + Log.LogError(CreatesDirectory, destinationFolder); + return false; + } + else + { + Log.LogMessage(MessageImportance.Normal, CreatesDirectory, destinationFolder); + Directory.CreateDirectory(destinationFolder); + } } // It's very common for a lot of files to be copied to the same folder. @@ -264,6 +274,14 @@ private void LogDiagnostic(string message, params object[] messageArgs) _directoriesKnownToExist.TryAdd(destinationFolder, true); } + if (FailIfNotIncremental) + { + string sourceFilePath = FileUtilities.GetFullPathNoThrow(sourceFileState.Name); + string destinationFilePath = FileUtilities.GetFullPathNoThrow(destinationFileState.Name); + Log.LogError(FileComment, sourceFilePath, destinationFilePath); + return false; + } + if (OverwriteReadOnlyFiles) { MakeFileWriteable(destinationFileState, true); @@ -733,6 +751,7 @@ private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationF "true"); MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Name, true); } + // We only do the cheap check for identicalness here, we try the more expensive check // of comparing the fullpaths of source and destination to see if they are identical, // in the exception handler lower down. @@ -742,7 +761,16 @@ private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationF StringComparison.OrdinalIgnoreCase)) { MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Name, false); - success = DoCopyWithRetries(sourceFileState, destinationFileState, copyFile); + + if (FailIfNotIncremental) + { + Log.LogError(FileComment, sourceFileState.Name, destinationFileState.Name); + success = false; + } + else + { + success = DoCopyWithRetries(sourceFileState, destinationFileState, copyFile); + } } else { diff --git a/src/Tasks/Delete.cs b/src/Tasks/Delete.cs index bd782dd8bae..0dbe2f78609 100644 --- a/src/Tasks/Delete.cs +++ b/src/Tasks/Delete.cs @@ -17,7 +17,7 @@ namespace Microsoft.Build.Tasks /// /// Delete files from disk. /// - public class Delete : TaskExtension, ICancelableTask + public class Delete : TaskExtension, ICancelableTask, IIncrementalTask { #region Properties @@ -57,6 +57,12 @@ public ITaskItem[] Files #endregion + /// + /// Set question parameter to verify if this is incremental. + /// + /// + public bool FailIfNotIncremental { get; set; } + /// /// Verify that the inputs are correct. /// @@ -115,10 +121,17 @@ public override bool Execute() { if (FileSystems.Default.FileExists(file.ItemSpec)) { - // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessageFromResources(MessageImportance.Normal, "Delete.DeletingFile", file.ItemSpec); - - File.Delete(file.ItemSpec); + if (FailIfNotIncremental) + { + Log.LogErrorFromResources("Delete.DeletingFile", file.ItemSpec); + } + else + { + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessageFromResources(MessageImportance.Normal, "Delete.DeletingFile", file.ItemSpec); + + File.Delete(file.ItemSpec); + } } else { diff --git a/src/Tasks/DownloadFile.cs b/src/Tasks/DownloadFile.cs index 6e5a029f286..8119588a35f 100644 --- a/src/Tasks/DownloadFile.cs +++ b/src/Tasks/DownloadFile.cs @@ -19,7 +19,7 @@ namespace Microsoft.Build.Tasks /// /// Represents a task that can download a file. /// - public sealed class DownloadFile : TaskExtension, ICancelableTask + public sealed class DownloadFile : TaskExtension, ICancelableTask, IIncrementalTask { private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); @@ -66,6 +66,8 @@ public sealed class DownloadFile : TaskExtension, ICancelableTask /// public int Timeout { get; set; } = 100_000; + public bool FailIfNotIncremental { get; set; } + /// /// Gets or sets a to use. This is used by unit tests to mock a connection to a remote server. /// @@ -192,6 +194,11 @@ private async Task DownloadAsync(Uri uri, CancellationToken cancellationToken) return; } + else if (FailIfNotIncremental) + { + Log.LogErrorFromResources("DownloadFile.Downloading", SourceUrl, destinationFile.FullName, response.Content.Headers.ContentLength); + return; + } try { diff --git a/src/Tasks/FileIO/WriteLinesToFile.cs b/src/Tasks/FileIO/WriteLinesToFile.cs index dcc9a4f69fa..7ae0228c8e5 100644 --- a/src/Tasks/FileIO/WriteLinesToFile.cs +++ b/src/Tasks/FileIO/WriteLinesToFile.cs @@ -15,7 +15,7 @@ namespace Microsoft.Build.Tasks /// /// Appends a list of items to a file. One item per line with carriage returns in-between. /// - public class WriteLinesToFile : TaskExtension + public class WriteLinesToFile : TaskExtension, IIncrementalTask { // Default encoding taken from System.IO.WriteAllText() private static readonly Encoding s_defaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); @@ -48,6 +48,14 @@ public class WriteLinesToFile : TaskExtension /// public bool WriteOnlyWhenDifferent { get; set; } + /// + /// Question whether this task is incremental. + /// + /// When question is true, then this task would not write to disk. If CanBeIncremental is true, then error out. + public bool FailIfNotIncremental { get; set; } + + public bool CanBeIncremental => WriteOnlyWhenDifferent; + /// /// Execute the task. /// @@ -61,7 +69,7 @@ public override bool Execute() // do not return if Lines is null, because we may // want to delete the file in that case StringBuilder buffer = new StringBuilder(); - if (Lines != null) + if (Lines != null && (!FailIfNotIncremental || WriteOnlyWhenDifferent)) { foreach (ITaskItem line in Lines) { @@ -108,6 +116,11 @@ public override bool Execute() MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(File.ItemSpec, true); return true; } + else if (FailIfNotIncremental) + { + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorReadingFile", File.ItemSpec); + return false; + } } } } @@ -118,16 +131,33 @@ public override bool Execute() MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(File.ItemSpec, false); } - System.IO.File.WriteAllText(File.ItemSpec, contentsAsString, encoding); + if (FailIfNotIncremental) + { + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorReadingFile", File.ItemSpec); + return false; + } + else + { + System.IO.File.WriteAllText(File.ItemSpec, contentsAsString, encoding); + } } else { - if (WriteOnlyWhenDifferent) + if (FailIfNotIncremental) { - Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", File.ItemSpec); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", File.ItemSpec, string.Empty); + return false; + } + else + { + if (WriteOnlyWhenDifferent) + { + Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", File.ItemSpec); + } + + Directory.CreateDirectory(directoryPath); + System.IO.File.AppendAllText(File.ItemSpec, buffer.ToString(), encoding); } - Directory.CreateDirectory(directoryPath); - System.IO.File.AppendAllText(File.ItemSpec, buffer.ToString(), encoding); } } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) diff --git a/src/Tasks/GenerateResource.cs b/src/Tasks/GenerateResource.cs index ac63dfc4ed9..6ff6c0a5dff 100644 --- a/src/Tasks/GenerateResource.cs +++ b/src/Tasks/GenerateResource.cs @@ -53,7 +53,7 @@ namespace Microsoft.Build.Tasks /// to transform resource files. /// [RequiredRuntime("v2.0")] - public sealed partial class GenerateResource : TaskExtension + public sealed partial class GenerateResource : TaskExtension, IIncrementalTask { #region Fields @@ -552,6 +552,8 @@ public GenerateResource() // do nothing } + public bool FailIfNotIncremental { get; set; } + /// /// Logs a Resgen.exe command line that indicates what parameters were /// passed to this task. Since this task is replacing Resgen, and we used @@ -721,6 +723,10 @@ public override bool Execute() Log.LogMessageFromResources("GenerateResource.NothingOutOfDate"); } + else if (FailIfNotIncremental) + { + Log.LogErrorFromResources("GenerateResource.OutOfDate"); + } else { if (!ComputePathToResGen()) diff --git a/src/Tasks/MakeDir.cs b/src/Tasks/MakeDir.cs index 4b5412534b0..eb7d2ef3281 100644 --- a/src/Tasks/MakeDir.cs +++ b/src/Tasks/MakeDir.cs @@ -14,7 +14,7 @@ namespace Microsoft.Build.Tasks /// /// A task that creates a directory /// - public class MakeDir : TaskExtension + public class MakeDir : TaskExtension, IIncrementalTask { [Required] public ITaskItem[] Directories @@ -31,6 +31,8 @@ public ITaskItem[] Directories [Output] public ITaskItem[] DirectoriesCreated { get; private set; } + public bool FailIfNotIncremental { get; set; } + private ITaskItem[] _directories; #region ITask Members @@ -59,10 +61,17 @@ public override bool Execute() // Only log a message if we actually need to create the folder if (!FileUtilities.DirectoryExistsNoThrow(directory.ItemSpec)) { - // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessageFromResources(MessageImportance.Normal, "MakeDir.Comment", directory.ItemSpec); + if (FailIfNotIncremental) + { + Log.LogErrorFromResources("MakeDir.Comment", directory.ItemSpec); + } + else + { + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessageFromResources(MessageImportance.Normal, "MakeDir.Comment", directory.ItemSpec); - Directory.CreateDirectory(FileUtilities.FixFilePath(directory.ItemSpec)); + Directory.CreateDirectory(FileUtilities.FixFilePath(directory.ItemSpec)); + } } items.Add(directory); diff --git a/src/Tasks/Microsoft.Common.CurrentVersion.targets b/src/Tasks/Microsoft.Common.CurrentVersion.targets index 8233656dfc3..f36e079ea3b 100644 --- a/src/Tasks/Microsoft.Common.CurrentVersion.targets +++ b/src/Tasks/Microsoft.Common.CurrentVersion.targets @@ -1471,6 +1471,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. ResolveNativeReferences; ResolveAssemblyReferences; GenerateBindingRedirects; + GenerateBindingRedirectsUpdateAppConfig; ResolveComReferences; AfterResolveReferences @@ -2466,18 +2467,18 @@ Copyright (C) Microsoft Corporation. All rights reserved. - - + SuggestedRedirects="@(SuggestedBindingRedirects)" /> + + + + Condition="'$(AutoGenerateBindingRedirects)' == 'true' and '$(GenerateBindingRedirectsOutputType)' == 'true'"> - $(_GenerateBindingRedirectsIntermediateAppConfig) + <_NewGenerateBindingRedirectsIntermediateAppConfig Condition="Exists('$(_GenerateBindingRedirectsIntermediateAppConfig)')">true + $(_GenerateBindingRedirectsIntermediateAppConfig) - - + + $(TargetFileName).config - @@ -5673,7 +5673,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. - + @@ -5705,7 +5705,8 @@ Copyright (C) Microsoft Corporation. All rights reserved. + Overwrite="true" + WriteOnlyWhenDifferent="true" /> diff --git a/src/Tasks/Move.cs b/src/Tasks/Move.cs index 42f0e20b968..746ce275be7 100644 --- a/src/Tasks/Move.cs +++ b/src/Tasks/Move.cs @@ -22,7 +22,7 @@ namespace Microsoft.Build.Tasks /// but this could restriction could be lifted as MoveFileEx, /// which is used here, supports it. /// - public class Move : TaskExtension, ICancelableTask + public class Move : TaskExtension, ICancelableTask, IIncrementalTask { /// /// Flags for MoveFileEx. @@ -62,11 +62,19 @@ public class Move : TaskExtension, ICancelableTask public ITaskItem[] DestinationFiles { get; set; } /// - /// Subset that were successfully moved + /// Subset that were successfully moved. /// [Output] public ITaskItem[] MovedFiles { get; private set; } + /// + /// Set question parameter for Move task. + /// + /// Move can be chained A->B->C with location C as the final location. + /// Incrementally, it is hard to question A->B if both files are gone. + /// In short, question will always return false and author should use target inputs/outputs. + public bool FailIfNotIncremental { get; set; } + /// /// Stop and return (in an undefined state) as soon as possible. /// @@ -149,7 +157,7 @@ public override bool Execute() try { - if (MoveFileWithLogging(sourceFile, destinationFile)) + if (!FailIfNotIncremental && MoveFileWithLogging(sourceFile, destinationFile)) { SourceFiles[i].CopyMetadataTo(DestinationFiles[i]); destinationFilesSuccessfullyMoved.Add(DestinationFiles[i]); @@ -175,7 +183,7 @@ public override bool Execute() } /// - /// Makes the provided file writeable if necessary + /// Makes the provided file writeable if necessary. /// private static void MakeWriteableIfReadOnly(string file) { @@ -189,7 +197,7 @@ private static void MakeWriteableIfReadOnly(string file) /// /// Move one file from source to destination. Create the target directory if necessary. /// - /// IO related exceptions + /// IO related exceptions. private bool MoveFileWithLogging( string sourceFile, string destinationFile) diff --git a/src/Tasks/RemoveDir.cs b/src/Tasks/RemoveDir.cs index da78951a376..3e43ca69a80 100644 --- a/src/Tasks/RemoveDir.cs +++ b/src/Tasks/RemoveDir.cs @@ -16,7 +16,7 @@ namespace Microsoft.Build.Tasks /// /// Remove the specified directories. /// - public class RemoveDir : TaskExtension + public class RemoveDir : TaskExtension, IIncrementalTask { //----------------------------------------------------------------------------------- // Property: directory to remove @@ -41,6 +41,8 @@ public ITaskItem[] Directories [Output] public ITaskItem[] RemovedDirectories { get; set; } + public bool FailIfNotIncremental { get; set; } + //----------------------------------------------------------------------------------- // Execute -- this runs the task //----------------------------------------------------------------------------------- @@ -61,6 +63,12 @@ public override bool Execute() if (FileSystems.Default.DirectoryExists(directory.ItemSpec)) { + if (FailIfNotIncremental) + { + Log.LogErrorFromResources("RemoveDir.Removing", directory.ItemSpec); + continue; + } + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.Removing", directory.ItemSpec); diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx index 7fa9c5ef7fc..c9326aeb55c 100644 --- a/src/Tasks/Resources/Strings.resx +++ b/src/Tasks/Resources/Strings.resx @@ -625,6 +625,9 @@ Processing suggested binding redirect on "{0}" with MaxVersion "{1}". + + Creating binding redirection file "{0}". + Tracking logs are not available, minimal rebuild will be disabled. diff --git a/src/Utilities/Resources/xlf/Strings.cs.xlf b/src/Utilities/Resources/xlf/Strings.cs.xlf index a8bfb9f8d72..57b8c235b02 100644 --- a/src/Utilities/Resources/xlf/Strings.cs.xlf +++ b/src/Utilities/Resources/xlf/Strings.cs.xlf @@ -62,6 +62,11 @@ MSB6003: Nepodařilo se spustit spustitelný soubor zadané úlohy {0}. {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: Příkaz {0} byl ukončen s kódem {1}. diff --git a/src/Utilities/Resources/xlf/Strings.de.xlf b/src/Utilities/Resources/xlf/Strings.de.xlf index df6e3dfda00..ac2253af2ef 100644 --- a/src/Utilities/Resources/xlf/Strings.de.xlf +++ b/src/Utilities/Resources/xlf/Strings.de.xlf @@ -62,6 +62,11 @@ MSB6003: Die angegebene ausführbare Datei der Aufgabe "{0}" konnte nicht ausgeführt werden. {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" wurde mit dem Code {1} beendet. diff --git a/src/Utilities/Resources/xlf/Strings.es.xlf b/src/Utilities/Resources/xlf/Strings.es.xlf index 71a0cc32f88..85a458060bd 100644 --- a/src/Utilities/Resources/xlf/Strings.es.xlf +++ b/src/Utilities/Resources/xlf/Strings.es.xlf @@ -62,6 +62,11 @@ MSB6003: No se pudo ejecutar la tarea ejecutable especificada "{0}". {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" salió con el código {1}. diff --git a/src/Utilities/Resources/xlf/Strings.fr.xlf b/src/Utilities/Resources/xlf/Strings.fr.xlf index 2924d87cb1a..4b4e3e2ce99 100644 --- a/src/Utilities/Resources/xlf/Strings.fr.xlf +++ b/src/Utilities/Resources/xlf/Strings.fr.xlf @@ -62,6 +62,11 @@ MSB6003: Impossible d'exécuter la tâche exécutable spécifiée "{0}". {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: Arrêt de "{0}" avec le code {1}. diff --git a/src/Utilities/Resources/xlf/Strings.it.xlf b/src/Utilities/Resources/xlf/Strings.it.xlf index ea4e723c61a..e3adf9c22fb 100644 --- a/src/Utilities/Resources/xlf/Strings.it.xlf +++ b/src/Utilities/Resources/xlf/Strings.it.xlf @@ -62,6 +62,11 @@ MSB6003: non è stato possibile eseguire il file eseguibile "{0}" dell'attività. {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" terminato con il codice {1}. diff --git a/src/Utilities/Resources/xlf/Strings.ja.xlf b/src/Utilities/Resources/xlf/Strings.ja.xlf index 77b2163cf7a..c7e01f40257 100644 --- a/src/Utilities/Resources/xlf/Strings.ja.xlf +++ b/src/Utilities/Resources/xlf/Strings.ja.xlf @@ -62,6 +62,11 @@ MSB6003: 指定されたタスク実行可能ファイル "{0}" を実行できませんでした。{1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" はコード {1} を伴って終了しました。 diff --git a/src/Utilities/Resources/xlf/Strings.ko.xlf b/src/Utilities/Resources/xlf/Strings.ko.xlf index 937d12bd39a..cbc95195c50 100644 --- a/src/Utilities/Resources/xlf/Strings.ko.xlf +++ b/src/Utilities/Resources/xlf/Strings.ko.xlf @@ -62,6 +62,11 @@ MSB6003: 지정한 작업 실행 파일 "{0}"을(를) 실행할 수 없습니다. {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}"이(가) 종료되었습니다(코드: {1}). diff --git a/src/Utilities/Resources/xlf/Strings.pl.xlf b/src/Utilities/Resources/xlf/Strings.pl.xlf index 9cb15be1f03..62554d2c96d 100644 --- a/src/Utilities/Resources/xlf/Strings.pl.xlf +++ b/src/Utilities/Resources/xlf/Strings.pl.xlf @@ -62,6 +62,11 @@ MSB6003: Nie można uruchomić określonego pliku wykonywalnego zadania „{0}”. {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: Polecenie „{0}” zakończone przez kod {1}. diff --git a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf index 19e23f9ecdd..b5a62e5a024 100644 --- a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf @@ -62,6 +62,11 @@ MSB6003: Não foi possível executar a tarefa executável "{0}" especificada. {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" foi encerrado com o código {1}. diff --git a/src/Utilities/Resources/xlf/Strings.ru.xlf b/src/Utilities/Resources/xlf/Strings.ru.xlf index c11d3a50afd..d3f5b246605 100644 --- a/src/Utilities/Resources/xlf/Strings.ru.xlf +++ b/src/Utilities/Resources/xlf/Strings.ru.xlf @@ -62,6 +62,11 @@ MSB6003: Не удалось запустить указанный исполняемый файл задачи "{0}". {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" завершилась с кодом {1}. diff --git a/src/Utilities/Resources/xlf/Strings.tr.xlf b/src/Utilities/Resources/xlf/Strings.tr.xlf index faf2fbb9512..cf7f608206d 100644 --- a/src/Utilities/Resources/xlf/Strings.tr.xlf +++ b/src/Utilities/Resources/xlf/Strings.tr.xlf @@ -62,6 +62,11 @@ MSB6003: Belirtilen "{0}" görev yürütülebilir dosyası çalıştırılamadı. {1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" öğesinden {1} koduyla çıkıldı. diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf index 65e9de502ba..e4a7a1f5090 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf @@ -62,6 +62,11 @@ MSB6003: 指定的任务可执行文件“{0}”未能运行。{1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: “{0}”已退出,代码为 {1}。 diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf index 6d773c29ed9..6f1465f1627 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf @@ -62,6 +62,11 @@ MSB6003: 無法執行指定的工作可執行檔 "{0}"。{1} {StrBegin="MSB6003: "} + + Unable to skip task because it is not up-to-date. + Unable to skip task because it is not up-to-date. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" 以返回碼 {1} 結束。 diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index 2243faedfca..7e74ae41056 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -58,7 +58,7 @@ public enum HostObjectInitializationStatus /// // INTERNAL WARNING: DO NOT USE the Log property in this class! Log points to resources in the task assembly itself, and // we want to use resources from Utilities. Use LogPrivate (for private Utilities resources) and LogShared (for shared MSBuild resources) - public abstract class ToolTask : Task, ICancelableTask + public abstract class ToolTask : Task, IIncrementalTask, ICancelableTask { private static readonly bool s_preserveTempFiles = string.Equals(Environment.GetEnvironmentVariable("MSBUILDPRESERVETOOLTEMPFILES"), "1", StringComparison.Ordinal); @@ -351,7 +351,14 @@ protected virtual void ProcessStarted() { } /// Returns true if task execution is not necessary. Executed after ValidateParameters /// /// - protected virtual bool SkipTaskExecution() => false; + protected virtual bool SkipTaskExecution() { canBeIncremental = false; return false; } + + /// + /// ToolTask is not incremental by default. When a derived class overrides SkipTaskExecution, then Question feature can take into effect. + /// + protected bool canBeIncremental { get; set; } = true; + + public bool FailIfNotIncremental { get; set; } /// /// Returns a string with those switches and other information that can go into a response file. @@ -1324,6 +1331,11 @@ public override bool Execute() // doing any actual work). return true; } + else if (canBeIncremental && FailIfNotIncremental) + { + LogPrivate.LogErrorWithCodeFromResources("ToolTask.NotUpToDate"); + return false; + } string commandLineCommands = GenerateCommandLineCommands(); // If there are response file commands, then we need a response file later.