diff --git a/src/dotnet/commands/dotnet-build/BuilderCommandApp.cs b/src/dotnet/commands/dotnet-build/BuilderCommandApp.cs index e9d3affa2..6f0348404 100644 --- a/src/dotnet/commands/dotnet-build/BuilderCommandApp.cs +++ b/src/dotnet/commands/dotnet-build/BuilderCommandApp.cs @@ -8,15 +8,18 @@ namespace Microsoft.DotNet.Tools.Build internal class BuilderCommandApp : CompilerCommandApp { public const string BuildProfileFlag = "--build-profile"; - public const string ForceUnsafeFlag = "--no-incremental"; + public const string NoIncrementalFlag = "--no-incremental"; + public const string NoDependenciesFlag = "--no-dependencies"; - public bool BuildProfileValue => OptionHasValue(BuildProfileFlag); - public bool ForceUnsafeValue => OptionHasValue(ForceUnsafeFlag); + public bool ShouldPrintIncrementalPreconditions => OptionHasValue(BuildProfileFlag); + public bool ShouldNotUseIncrementality => OptionHasValue(NoIncrementalFlag); + public bool ShouldSkipDependencies => OptionHasValue(NoDependenciesFlag); public BuilderCommandApp(string name, string fullName, string description) : base(name, fullName, description) { AddNoValueOption(BuildProfileFlag, "Set this flag to print the incremental safety checks that prevent incremental compilation"); - AddNoValueOption(ForceUnsafeFlag, "Set this flag to turn off incremental build"); + AddNoValueOption(NoIncrementalFlag, "Set this flag to turn off incremental build"); + AddNoValueOption(NoDependenciesFlag, "Set this flag to ignore project to project references and only build the root project"); } } } \ No newline at end of file diff --git a/src/dotnet/commands/dotnet-build/CompileContext.cs b/src/dotnet/commands/dotnet-build/CompileContext.cs index 0cae3a45b..6c2906386 100644 --- a/src/dotnet/commands/dotnet-build/CompileContext.cs +++ b/src/dotnet/commands/dotnet-build/CompileContext.cs @@ -23,9 +23,9 @@ namespace Microsoft.DotNet.Tools.Build public static readonly string[] KnownCompilers = { "csc", "vbc", "fsc" }; private readonly ProjectContext _rootProject; + private readonly ProjectDependenciesFacade _rootProjectDependencies; private readonly BuilderCommandApp _args; private readonly IncrementalPreconditions _preconditions; - private readonly ProjectDependenciesFacade _dependencies; public bool IsSafeForIncrementalCompilation => !_preconditions.PreconditionsDetected(); @@ -36,12 +36,9 @@ namespace Microsoft.DotNet.Tools.Build // Cleaner to clone the args and mutate the clone than have separate CompileContext fields for mutated args // and then reasoning which ones to get from args and which ones from fields. _args = (BuilderCommandApp)args.ShallowCopy(); - - _args.OutputValue = _args.OutputValue; - _args.BuildBasePathValue = _args.BuildBasePathValue; // Set up dependencies - _dependencies = new ProjectDependenciesFacade(_rootProject, _args.ConfigValue); + _rootProjectDependencies = new ProjectDependenciesFacade(_rootProject, _args.ConfigValue); // gather preconditions _preconditions = GatherIncrementalPreconditions(); @@ -51,17 +48,36 @@ namespace Microsoft.DotNet.Tools.Build { CreateOutputDirectories(); - // compile dependencies - foreach (var dependency in Sort(_dependencies.ProjectDependenciesWithSources)) - { - if (incremental) - { - var dependencyProjectContext = ProjectContext.Create(dependency.Path, dependency.Framework, new[] { _rootProject.RuntimeIdentifier }); + return CompileDendencies(incremental) && CompileRootProject(incremental); + } - if (!NeedsRebuilding(dependencyProjectContext, new ProjectDependenciesFacade(dependencyProjectContext, _args.ConfigValue))) - { - continue; - } + private bool CompileRootProject(bool incremental) + { + if (incremental && !NeedsRebuilding(_rootProject, _rootProjectDependencies)) + { + // todo: what if the previous build had errors / warnings and nothing changed? Need to propagate them in case of incremental + return true; + } + + var success = InvokeCompileOnRootProject(); + + PrintSummary(success); + + return success; + } + + private bool CompileDendencies(bool incremental) + { + if (_args.ShouldSkipDependencies) + { + return true; + } + + foreach (var dependency in Sort(_rootProjectDependencies.ProjectDependenciesWithSources)) + { + if (incremental && !DependencyNeedsRebuilding(dependency)) + { + continue; } if (!InvokeCompileOnDependency(dependency)) @@ -70,28 +86,18 @@ namespace Microsoft.DotNet.Tools.Build } } - if (incremental && !NeedsRebuilding(_rootProject, _dependencies)) - { - // todo: what if the previous build had errors / warnings and nothing changed? Need to propagate them in case of incremental - return true; - } + return true; + } - // compile project - var success = InvokeCompileOnRootProject(); - - PrintSummary(success); - - return success; + private bool DependencyNeedsRebuilding(ProjectDescription dependency) + { + var dependencyProjectContext = ProjectContext.Create(dependency.Path, dependency.Framework, new[] { _rootProject.RuntimeIdentifier }); + return NeedsRebuilding(dependencyProjectContext, new ProjectDependenciesFacade(dependencyProjectContext, _args.ConfigValue)); } private bool NeedsRebuilding(ProjectContext project, ProjectDependenciesFacade dependencies) { - return NeedsRebuilding(project, dependencies, _args.BuildBasePathValue); - } - - private bool NeedsRebuilding(ProjectContext project, ProjectDependenciesFacade dependencies, string baseBuildPath) - { - var compilerIO = GetCompileIO(project, _args.ConfigValue, baseBuildPath, _args.OutputValue, dependencies, project == _rootProject); + var compilerIO = GetCompileIO(project, _args.ConfigValue, _args.BuildBasePathValue, _args.OutputValue, dependencies, project == _rootProject); // rebuild if empty inputs / outputs if (!(compilerIO.Outputs.Any() && compilerIO.Inputs.Any())) @@ -195,9 +201,9 @@ namespace Microsoft.DotNet.Tools.Build private IncrementalPreconditions GatherIncrementalPreconditions() { - var preconditions = new IncrementalPreconditions(_args.BuildProfileValue); + var preconditions = new IncrementalPreconditions(_args.ShouldPrintIncrementalPreconditions); - if (_args.ForceUnsafeValue) + if (_args.ShouldNotUseIncrementality) { preconditions.AddForceUnsafePrecondition(); } @@ -217,11 +223,16 @@ namespace Microsoft.DotNet.Tools.Build // check the entire project tree that needs to be compiled, duplicated for each framework private List GetProjectsToCheck() { + if (_args.ShouldSkipDependencies) + { + return new List(1) { _rootProject }; + } + // include initial root project - var contextsToCheck = new List(1 + _dependencies.ProjectDependenciesWithSources.Count) { _rootProject }; + var contextsToCheck = new List(1 + _rootProjectDependencies.ProjectDependenciesWithSources.Count) { _rootProject }; // convert ProjectDescription to ProjectContext - var dependencyContexts = _dependencies.ProjectDependenciesWithSources.Select + var dependencyContexts = _rootProjectDependencies.ProjectDependenciesWithSources.Select (keyValuePair => ProjectContext.Create(keyValuePair.Value.Path, keyValuePair.Value.Framework)); contextsToCheck.AddRange(dependencyContexts); diff --git a/src/dotnet/commands/dotnet-build/IncrementalPreconditions.cs b/src/dotnet/commands/dotnet-build/IncrementalPreconditions.cs index bba3f4084..fbcd4435a 100644 --- a/src/dotnet/commands/dotnet-build/IncrementalPreconditions.cs +++ b/src/dotnet/commands/dotnet-build/IncrementalPreconditions.cs @@ -36,7 +36,7 @@ namespace Microsoft.DotNet.Tools.Build public void AddForceUnsafePrecondition() { - _preconditions.Add($"[Forced Unsafe] The build was marked as unsafe. Remove the {BuilderCommandApp.ForceUnsafeFlag} flag to enable incremental compilation"); + _preconditions.Add($"[Forced Unsafe] The build was marked as unsafe. Remove the {BuilderCommandApp.NoIncrementalFlag} flag to enable incremental compilation"); } public bool PreconditionsDetected() diff --git a/src/dotnet/commands/dotnet-build/README.md b/src/dotnet/commands/dotnet-build/README.md index 4e7eac3f1..d958d923b 100644 --- a/src/dotnet/commands/dotnet-build/README.md +++ b/src/dotnet/commands/dotnet-build/README.md @@ -33,3 +33,6 @@ Prints out the incremental safety checks that users need to address in order for --no-incremental Marks the build as unsafe for incrementality. This turns off incremental compilation and forces a clean rebuild of the project dependency graph. + +--no-dependencies +Ignore project to project references and only build the root project specified to build. diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/BuildCommand.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/BuildCommand.cs index 4c1b4f2d6..6b6534feb 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/BuildCommand.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/BuildCommand.cs @@ -26,6 +26,7 @@ namespace Microsoft.DotNet.Tools.Test.Utilities private string _cppCompilerFlags; private bool _buildProfile; private bool _noIncremental; + private bool _noDependencies; private string OutputOption { @@ -166,6 +167,16 @@ namespace Microsoft.DotNet.Tools.Test.Utilities } } + private string NoDependencies + { + get + { + return _noDependencies ? + "--no-dependencies" : + ""; + } + } + public BuildCommand( string projectPath, string output="", @@ -181,11 +192,11 @@ namespace Microsoft.DotNet.Tools.Test.Utilities bool nativeCppMode=false, string cppCompilerFlags="", bool buildProfile=true, - bool noIncremental=false + bool noIncremental=false, + bool noDependencies=false ) : base("dotnet") { - _projectPath = projectPath; _project = ProjectReader.GetProject(projectPath); @@ -203,6 +214,7 @@ namespace Microsoft.DotNet.Tools.Test.Utilities _cppCompilerFlags = cppCompilerFlags; _buildProfile = buildProfile; _noIncremental = noIncremental; + _noDependencies = noDependencies; } public override CommandResult Execute(string args = "") @@ -226,7 +238,7 @@ namespace Microsoft.DotNet.Tools.Test.Utilities private string BuildArgs() { - return $"{BuildProfile} {NoIncremental} \"{_projectPath}\" {OutputOption} {BuildBasePathOption} {ConfigurationOption} {FrameworkOption} {NoHostOption} {NativeOption} {ArchitectureOption} {IlcArgsOption} {IlcPathOption} {AppDepSDKPathOption} {NativeCppModeOption} {CppCompilerFlagsOption}"; + return $"{BuildProfile} {NoDependencies} {NoIncremental} \"{_projectPath}\" {OutputOption} {BuildBasePathOption} {ConfigurationOption} {FrameworkOption} {NoHostOption} {NativeOption} {ArchitectureOption} {IlcArgsOption} {IlcPathOption} {AppDepSDKPathOption} {NativeCppModeOption} {CppCompilerFlagsOption}"; } } } diff --git a/test/dotnet-build.Tests/BuildProjectToProjectTests.cs b/test/dotnet-build.Tests/BuildProjectToProjectTests.cs index 75784c4f3..c494f2114 100644 --- a/test/dotnet-build.Tests/BuildProjectToProjectTests.cs +++ b/test/dotnet-build.Tests/BuildProjectToProjectTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using FluentAssertions; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Test.Utilities; using Xunit; @@ -13,7 +14,7 @@ namespace Microsoft.DotNet.Tools.Builder.Tests { public class ProjectToProjectDependenciesIncrementalTest : IncrementalTestBase { - private string[] _projects = new[] { "L0", "L11", "L12", "L21", "L22" }; + private readonly string[] _projects = new[] { "L0", "L11", "L12", "L21", "L22" }; public ProjectToProjectDependenciesIncrementalTest() : base( Path.Combine(AppContext.BaseDirectory, "TestAssets", "TestProjects", "TestProjectToProjectDependencies"), @@ -48,6 +49,38 @@ namespace Microsoft.DotNet.Tools.Builder.Tests AssertRebuilt(result3, expectedRebuiltProjects); } + [Fact] + public void TestNoDependencyFlag() + { + var dependencies = new[] { "L11", "L12", "L21", "L22" }; + + // first clean build; all projects required compilation + var result1 = BuildProject(); + AssertRebuilt(result1, _projects); + + // modify the source code of a leaf dependency + TouchSourcesOfProject("L22"); + + // second build with no dependencies and no incremental; only the root rebuilds + var result2 = BuildProject(noDependencies: true, noIncremental: true); + result2.Should().StdOutMatchPattern("Compiling.*L0.*"); + + AssertResultDoesNotContainStrings(result2, dependencies); + + // third build with no dependencies but incremental; nothing rebuilds + var result3 = BuildProject(noDependencies: true); + result3.Should().HaveSkippedProjectCompilation("L0"); + AssertResultDoesNotContainStrings(result3, dependencies); + } + + private static void AssertResultDoesNotContainStrings(CommandResult commandResult, string[] strings) + { + foreach (var s in strings) + { + commandResult.StdOut.Should().NotContain(s); + } + } + // compute A - B private T[] SetDifference(T[] A, T[] B) { diff --git a/test/dotnet-build.Tests/IncrementalTestBase.cs b/test/dotnet-build.Tests/IncrementalTestBase.cs index 0b5c3f72a..a897d5b69 100644 --- a/test/dotnet-build.Tests/IncrementalTestBase.cs +++ b/test/dotnet-build.Tests/IncrementalTestBase.cs @@ -46,11 +46,11 @@ namespace Microsoft.DotNet.Tools.Builder.Tests File.SetLastWriteTimeUtc(file, DateTime.UtcNow); } - protected CommandResult BuildProject(bool noIncremental = false, bool expectBuildFailure = false) + protected CommandResult BuildProject(bool noDependencies = false, bool noIncremental = false, bool expectBuildFailure = false) { var mainProjectFile = GetProjectFile(MainProject); - var buildCommand = new BuildCommand(mainProjectFile, output: GetBinRoot(), framework: "dnxcore50", noIncremental : noIncremental); + var buildCommand = new BuildCommand(mainProjectFile, output: GetBinRoot(), framework: "dnxcore50", noIncremental : noIncremental, noDependencies : noDependencies); var result = buildCommand.ExecuteWithCapturedOutput(); if (!expectBuildFailure)