Compile incrementally

- Clone the args in the CompileContext constructor to bring uniformity
to the way args are accessed

- Compute IO for a project and have it shared between build and compile

- Extract dependency logic into facade

- Add tests for incremental build

- Add precondition checks for compiler IO

add --force-incremental-unsafe flag
This commit is contained in:
Mihai Codoban 2015-12-21 10:42:41 -08:00
parent 28f01faae5
commit bedeaaf2dc
10 changed files with 501 additions and 114 deletions

View file

@ -7,13 +7,16 @@ namespace Microsoft.DotNet.Tools.Build
{ {
internal class BuilderCommandApp : CompilerCommandApp internal class BuilderCommandApp : CompilerCommandApp
{ {
private const string BuildProfileFlag = "--build-profile"; public const string BuildProfileFlag = "--build-profile";
public const string ForceUnsafeFlag = "--force-incremental-unsafe";
public bool BuildProfileValue => OptionHasValue(BuildProfileFlag); public bool BuildProfileValue => OptionHasValue(BuildProfileFlag);
public bool ForceUnsafeValue => OptionHasValue(ForceUnsafeFlag);
public BuilderCommandApp(string name, string fullName, string description) : base(name, fullName, description) 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(BuildProfileFlag, "Set this flag to print the incremental safety checks that prevent incremental compilation");
AddNoValueOption(ForceUnsafeFlag, "Set this flag to mark the entire build as not safe for incrementality");
} }
} }
} }

View file

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -14,6 +15,7 @@ using Microsoft.DotNet.Cli.Compiler.Common;
namespace Microsoft.DotNet.Tools.Build namespace Microsoft.DotNet.Tools.Build
{ {
// todo: Convert CompileContext into a DAG of dependencies: if a node needs recompilation, the entire path up to root needs compilation
// Knows how to orchestrate compilation for a ProjectContext // Knows how to orchestrate compilation for a ProjectContext
// Collects icnremental safety checks and transitively compiles a project context // Collects icnremental safety checks and transitively compiles a project context
internal class CompileContext internal class CompileContext
@ -23,27 +25,27 @@ namespace Microsoft.DotNet.Tools.Build
private readonly ProjectContext _rootProject; private readonly ProjectContext _rootProject;
private readonly BuilderCommandApp _args; private readonly BuilderCommandApp _args;
private readonly Dictionary<string, ProjectDescription> _dependencies;
private readonly string _outputPath;
private readonly string _intermediateOutputPath;
private readonly IncrementalPreconditions _preconditions; private readonly IncrementalPreconditions _preconditions;
private readonly ProjectDependenciesFacade _dependencies;
public bool IsSafeForIncrementalCompilation => _preconditions.PreconditionsDetected(); public bool IsSafeForIncrementalCompilation => !_preconditions.PreconditionsDetected();
public CompileContext(ProjectContext rootProject, BuilderCommandApp args) public CompileContext(ProjectContext rootProject, BuilderCommandApp args)
{ {
_rootProject = rootProject; _rootProject = rootProject;
_args = args;
// 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();
// Set up Output Paths. They are unique per each CompileContext // Set up Output Paths. They are unique per each CompileContext
// Todo: clone args and mutate the clone so the rest of this class does not have special treatment for output paths _args.OutputValue = _rootProject.GetOutputPath(_args.ConfigValue, _args.OutputValue);
_outputPath = _rootProject.GetOutputPath(_args.ConfigValue, _args.OutputValue); _args.IntermediateValue = _rootProject.GetIntermediateOutputPath(_args.ConfigValue, _args.IntermediateValue, _args.OutputValue);
_intermediateOutputPath = _rootProject.GetIntermediateOutputPath(_args.ConfigValue, _args.IntermediateValue, _args.OutputValue);
// Set up dependencies // Set up dependencies
_dependencies = GetProjectDependenciesWithSources(_rootProject, _args.ConfigValue); _dependencies = new ProjectDependenciesFacade(_rootProject, _args.ConfigValue);
//gather preconditions // gather preconditions
_preconditions = GatherIncrementalPreconditions(); _preconditions = GatherIncrementalPreconditions();
} }
@ -51,26 +53,116 @@ namespace Microsoft.DotNet.Tools.Build
{ {
CreateOutputDirectories(); CreateOutputDirectories();
//compile dependencies // compile dependencies
foreach (var dependency in Sort(_dependencies)) foreach (var dependency in Sort(_dependencies.ProjectDependenciesWithSources))
{ {
if (!InvokeCompileOnDependency(dependency, _outputPath, _intermediateOutputPath)) if (incremental)
{
var dependencyContext = ProjectContext.Create(dependency.Path, dependency.Framework);
if (!NeedsRebuilding(dependencyContext, new ProjectDependenciesFacade(dependencyContext, _args.ConfigValue)))
{
continue;
}
}
if (!InvokeCompileOnDependency(dependency))
{ {
return false; return false;
} }
} }
//compile project if (incremental && !NeedsRebuilding(_rootProject, _dependencies))
var success = InvokeCompileOnRootProject(_outputPath, _intermediateOutputPath); {
// todo: what if the previous build had errors / warnings and nothing changed? Need to propagate them in case of incremental
return true;
}
// compile project
var success = InvokeCompileOnRootProject();
PrintSummary(success); PrintSummary(success);
return success; return success;
} }
private bool NeedsRebuilding(ProjectContext project, ProjectDependenciesFacade dependencies)
{
var compilerIO = GetCompileIO(project, _args.ConfigValue, _args.OutputValue, _args.IntermediateValue, dependencies);
// rebuild if empty inputs / outputs
if (!(compilerIO.Outputs.Any() && compilerIO.Inputs.Any()))
{
Reporter.Verbose.WriteLine($"\nProject {project.ProjectName()} will be compiled because it either has empty inputs or outputs");
return true;
}
//rebuild if missing inputs / outputs
if (AnyMissingIO(project, compilerIO.Outputs, "outputs") || AnyMissingIO(project, compilerIO.Inputs, "inputs"))
{
return true;
}
// find the output with the earliest write time
var minOutputPath = compilerIO.Outputs.First();
var minDate = File.GetLastWriteTime(minOutputPath);
foreach (var outputPath in compilerIO.Outputs)
{
if (File.GetLastWriteTime(outputPath) >= minDate)
{
continue;
}
minDate = File.GetLastWriteTime(outputPath);
minOutputPath = outputPath;
}
// find inputs that are older than the earliest output
var newInputs = compilerIO.Inputs.FindAll(p => File.GetLastWriteTime(p) > minDate);
if (!newInputs.Any())
{
Reporter.Verbose.WriteLine($"\nSkipped compilation for project {project.ProjectName()}. All the input files were older than the output files.");
return false;
}
Reporter.Verbose.WriteLine($"\nProject {project.ProjectName()} was compiled because some of its inputs were newer than its oldest output:");
Reporter.Verbose.WriteLine($"Oldest output item was written at {minDate} : {minOutputPath}");
Reporter.Verbose.WriteLine($"Inputs newer than the oldest output item:");
foreach (var newInput in newInputs)
{
Reporter.Verbose.WriteLine($"\t{File.GetLastWriteTime(newInput)}\t:\t{newInput}");
}
return true;
}
private static bool AnyMissingIO(ProjectContext project, IEnumerable<string> items, string itemsType)
{
var missingItems = items.Where(i => !File.Exists(i)).ToList();
if (!missingItems.Any())
{
return false;
}
Reporter.Verbose.WriteLine($"\nProject {project.ProjectName()} will be compiled because expected {itemsType} are missing: ");
foreach (var missing in missingItems)
{
Reporter.Verbose.WriteLine($"\t {missing}");
}
Reporter.Verbose.WriteLine();
return true;
}
private void PrintSummary(bool success) private void PrintSummary(bool success)
{ {
//todo: Ideally it's the builder's responsibility for adding the time elapsed. That way we avoid cross cutting display concerns between compile and build for printing time elapsed // todo: Ideally it's the builder's responsibility for adding the time elapsed. That way we avoid cross cutting display concerns between compile and build for printing time elapsed
if (success) if (success)
{ {
Reporter.Output.Write(" " + _preconditions.LogMessage()); Reporter.Output.Write(" " + _preconditions.LogMessage());
@ -82,61 +174,40 @@ namespace Microsoft.DotNet.Tools.Build
private void CreateOutputDirectories() private void CreateOutputDirectories()
{ {
Directory.CreateDirectory(_outputPath); Directory.CreateDirectory(_args.OutputValue);
Directory.CreateDirectory(_intermediateOutputPath); Directory.CreateDirectory(_args.IntermediateValue);
}
//todo make extension of ProjectContext?
//returns map with dependencies: string projectName -> ProjectDescription
private static Dictionary<string, ProjectDescription> GetProjectDependenciesWithSources(ProjectContext projectContext, string configuration)
{
var projects = new Dictionary<string, ProjectDescription>();
// Create the library exporter
var exporter = projectContext.CreateExporter(configuration);
// Gather exports for the project
var dependencies = exporter.GetDependencies().ToList();
// Build project references
foreach (var dependency in dependencies)
{
var projectDependency = dependency.Library as ProjectDescription;
if (projectDependency != null && projectDependency.Project.Files.SourceFiles.Any())
{
projects[projectDependency.Identity.Name] = projectDependency;
}
}
return projects;
} }
private IncrementalPreconditions GatherIncrementalPreconditions() private IncrementalPreconditions GatherIncrementalPreconditions()
{ {
var preconditions = new IncrementalPreconditions(_args.BuildProfileValue); var preconditions = new IncrementalPreconditions(_args.BuildProfileValue);
if (_args.ForceUnsafeValue)
{
preconditions.AddForceUnsafePrecondition();
}
var projectsToCheck = GetProjectsToCheck(); var projectsToCheck = GetProjectsToCheck();
foreach (var project in projectsToCheck) foreach (var project in projectsToCheck)
{ {
CollectScriptPreconditions(project, preconditions); CollectScriptPreconditions(project, preconditions);
CollectCompilerNamePreconditions(project, preconditions); CollectCompilerNamePreconditions(project, preconditions);
CheckPathProbing(project, preconditions); CollectCheckPathProbingPreconditions(project, preconditions);
} }
return preconditions; return preconditions;
} }
//check the entire project tree that needs to be compiled, duplicated for each framework // check the entire project tree that needs to be compiled, duplicated for each framework
private List<ProjectContext> GetProjectsToCheck() private List<ProjectContext> GetProjectsToCheck()
{ {
//include initial root project // include initial root project
var contextsToCheck = new List<ProjectContext>(1 + _dependencies.Count) {_rootProject}; var contextsToCheck = new List<ProjectContext>(1 + _dependencies.ProjectDependenciesWithSources.Count) {_rootProject};
//convert ProjectDescription to ProjectContext // convert ProjectDescription to ProjectContext
var dependencyContexts = _dependencies.Select var dependencyContexts = _dependencies.ProjectDependenciesWithSources.Select
(keyValuePair => ProjectContext.Create(keyValuePair.Value.Path, keyValuePair.Value.TargetFrameworkInfo.FrameworkName)); (keyValuePair => ProjectContext.Create(keyValuePair.Value.Path, keyValuePair.Value.Framework));
contextsToCheck.AddRange(dependencyContexts); contextsToCheck.AddRange(dependencyContexts);
@ -144,7 +215,7 @@ namespace Microsoft.DotNet.Tools.Build
return contextsToCheck; return contextsToCheck;
} }
private void CheckPathProbing(ProjectContext project, IncrementalPreconditions preconditions) private void CollectCheckPathProbingPreconditions(ProjectContext project, IncrementalPreconditions preconditions)
{ {
var pathCommands = CompilerUtil.GetCommandsInvokedByCompile(project) var pathCommands = CompilerUtil.GetCommandsInvokedByCompile(project)
.Select(commandName => Command.Create(commandName, "", project.TargetFramework)) .Select(commandName => Command.Create(commandName, "", project.TargetFramework))
@ -160,7 +231,7 @@ namespace Microsoft.DotNet.Tools.Build
{ {
var projectCompiler = CompilerUtil.ResolveCompilerName(project); var projectCompiler = CompilerUtil.ResolveCompilerName(project);
if (KnownCompilers.Any(knownCompiler => knownCompiler.Equals(projectCompiler, StringComparison.Ordinal))) if (!KnownCompilers.Any(knownCompiler => knownCompiler.Equals(projectCompiler, StringComparison.Ordinal)))
{ {
preconditions.AddUnknownCompilerPrecondition(project.ProjectName(), projectCompiler); preconditions.AddUnknownCompilerPrecondition(project.ProjectName(), projectCompiler);
} }
@ -182,13 +253,13 @@ namespace Microsoft.DotNet.Tools.Build
} }
} }
private bool InvokeCompileOnDependency(ProjectDescription projectDependency, string outputPath, string intermediateOutputPath) private bool InvokeCompileOnDependency(ProjectDescription projectDependency)
{ {
var compileResult = Command.Create("dotnet-compile", var compileResult = Command.Create("dotnet-compile",
$"--framework {projectDependency.Framework} " + $"--framework {projectDependency.Framework} " +
$"--configuration {_args.ConfigValue} " + $"--configuration {_args.ConfigValue} " +
$"--output \"{outputPath}\" " + $"--output \"{_args.OutputValue}\" " +
$"--temp-output \"{intermediateOutputPath}\" " + $"--temp-output \"{_args.IntermediateValue}\" " +
(_args.NoHostValue ? "--no-host " : string.Empty) + (_args.NoHostValue ? "--no-host " : string.Empty) +
$"\"{projectDependency.Project.ProjectDirectory}\"") $"\"{projectDependency.Project.ProjectDirectory}\"")
.ForwardStdOut() .ForwardStdOut()
@ -198,14 +269,14 @@ namespace Microsoft.DotNet.Tools.Build
return compileResult.ExitCode == 0; return compileResult.ExitCode == 0;
} }
private bool InvokeCompileOnRootProject(string outputPath, string intermediateOutputPath) private bool InvokeCompileOnRootProject()
{ {
//todo: add methods to CompilerCommandApp to generate the arg string // todo: add methods to CompilerCommandApp to generate the arg string?
var compileResult = Command.Create("dotnet-compile", var compileResult = Command.Create("dotnet-compile",
$"--framework {_rootProject.TargetFramework} " + $"--framework {_rootProject.TargetFramework} " +
$"--configuration {_args.ConfigValue} " + $"--configuration {_args.ConfigValue} " +
$"--output \"{outputPath}\" " + $"--output \"{_args.OutputValue}\" " +
$"--temp-output \"{intermediateOutputPath}\" " + $"--temp-output \"{_args.IntermediateValue}\" " +
(_args.NoHostValue ? "--no-host " : string.Empty) + (_args.NoHostValue ? "--no-host " : string.Empty) +
//nativeArgs //nativeArgs
(_args.IsNativeValue ? "--native " : string.Empty) + (_args.IsNativeValue ? "--native " : string.Empty) +
@ -248,6 +319,96 @@ namespace Microsoft.DotNet.Tools.Build
outputs.Add(project); outputs.Add(project);
} }
public struct CompilerIO
{
public readonly List<string> Inputs;
public readonly List<string> Outputs;
public CompilerIO(List<string> inputs, List<string> outputs)
{
Inputs = inputs;
Outputs = outputs;
}
}
// computes all the inputs and outputs that would be used in the compilation of a project
// ensures that all paths are files
// ensures no missing inputs
public static CompilerIO GetCompileIO(ProjectContext project, string config, string outputPath, string intermediaryOutputPath, ProjectDependenciesFacade dependencies)
{
var compilerIO = new CompilerIO(new List<string>(), new List<string>());
// input: project.json
compilerIO.Inputs.Add(project.ProjectFile.ProjectFilePath);
// input: source files
compilerIO.Inputs.AddRange(CompilerUtil.GetCompilationSources(project));
// todo: Factor out dependency resolution between Build and Compile. Ideally Build injects the dependencies into Compile
// todo: use lock file insteaf of dependencies. One file vs many
// input: dependencies
AddDependencies(dependencies, compilerIO);
// input: key file
AddKeyFile(project, config, compilerIO);
// output: compiler output
compilerIO.Outputs.Add(CompilerUtil.GetCompilationOutput(project.ProjectFile, project.TargetFramework, config, outputPath));
// input / output: resources without culture
AddCultureResources(project, intermediaryOutputPath, compilerIO);
// input / output: resources with culture
AddNonCultureResources(project, outputPath, compilerIO);
return compilerIO;
}
private static void AddDependencies(ProjectDependenciesFacade dependencies, CompilerIO compilerIO)
{
// add dependency sources that need compilation
compilerIO.Inputs.AddRange(dependencies.ProjectDependenciesWithSources.Values.SelectMany(p => p.Project.Files.SourceFiles));
// add compilation binaries
compilerIO.Inputs.AddRange(dependencies.Dependencies.SelectMany(d => d.CompilationAssemblies.Select(ca => ca.ResolvedPath)));
}
private static void AddKeyFile(ProjectContext project, string config, CompilerIO compilerIO)
{
var keyFile = CompilerUtil.ResolveCompilationOptions(project, config).KeyFile;
if (keyFile != null)
{
compilerIO.Inputs.Add(keyFile);
}
}
private static void AddNonCultureResources(ProjectContext project, string intermediaryOutputPath, CompilerIO compilerIO)
{
foreach (var resourceIO in CompilerUtil.GetNonCultureResources(project.ProjectFile, intermediaryOutputPath))
{
compilerIO.Inputs.Add(resourceIO.InputFile);
if (resourceIO.OutputFile != null)
{
compilerIO.Outputs.Add(resourceIO.OutputFile);
}
}
}
private static void AddCultureResources(ProjectContext project, string outputPath, CompilerIO compilerIO)
{
foreach (var cultureResourceIO in CompilerUtil.GetCultureResources(project.ProjectFile, outputPath))
{
compilerIO.Inputs.AddRange(cultureResourceIO.InputFileToMetadata.Keys);
if (cultureResourceIO.OutputFile != null)
{
compilerIO.Outputs.Add(cultureResourceIO.OutputFile);
}
}
}
} }
} }

View file

@ -10,13 +10,13 @@ namespace Microsoft.DotNet.Tools.Build
{ {
internal class IncrementalPreconditions internal class IncrementalPreconditions
{ {
private readonly List<string> _preconditions; private readonly ISet<string> _preconditions;
private readonly bool _isProfile; private readonly bool _isProfile;
public IncrementalPreconditions(bool isProfile) public IncrementalPreconditions(bool isProfile)
{ {
_isProfile = isProfile; _isProfile = isProfile;
_preconditions = new List<string>(); _preconditions = new HashSet<string>();
} }
public void AddPrePostScriptPrecondition(string projectName, string scriptType) public void AddPrePostScriptPrecondition(string projectName, string scriptType)
@ -34,6 +34,11 @@ namespace Microsoft.DotNet.Tools.Build
_preconditions.Add($"[PATH Probing] Project {projectName} is loading tool \"{commandName}\" from PATH"); _preconditions.Add($"[PATH Probing] Project {projectName} is loading tool \"{commandName}\" from PATH");
} }
public void AddForceUnsafePrecondition()
{
_preconditions.Add($"[Forced Unsafe] The build was marked as unsafe. Remove the {BuilderCommandApp.ForceUnsafeFlag} flag to enable incremental compilation");
}
public bool PreconditionsDetected() public bool PreconditionsDetected()
{ {
return _preconditions.Any(); return _preconditions.Any();
@ -70,7 +75,7 @@ namespace Microsoft.DotNet.Tools.Build
{ {
if (PreconditionsDetected()) if (PreconditionsDetected())
{ {
return _isProfile ? PreconditionsMessage().Yellow() : "(The compilation time can be improved. Run \"dotnet build --profile\" for more information)"; return _isProfile ? PreconditionsMessage().Yellow() : $"(The compilation time can be improved. Run \"dotnet build {BuilderCommandApp.BuildProfileFlag}\" for more information)";
} }
return ""; return "";

View file

@ -0,0 +1,50 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.ProjectModel.Compilation;
namespace Microsoft.DotNet.Tools.Build
{
// facade over the dependencies of a project context
internal class ProjectDependenciesFacade
{
// projectName -> ProjectDescription
public Dictionary<string, ProjectDescription> ProjectDependenciesWithSources { get; }
public List<LibraryExport> Dependencies { get; }
public ProjectDependenciesFacade(ProjectContext rootProject, string configValue)
{
Dependencies = GetProjectDependencies(rootProject, configValue);
ProjectDependenciesWithSources = new Dictionary<string, ProjectDescription>();
// Build project references
foreach (var dependency in Dependencies)
{
var projectDependency = dependency.Library as ProjectDescription;
if (projectDependency != null && projectDependency.Project.Files.SourceFiles.Any())
{
ProjectDependenciesWithSources[projectDependency.Identity.Name] = projectDependency;
}
}
}
// todo make extension of ProjectContext?
private static List<LibraryExport> GetProjectDependencies(ProjectContext projectContext, string configuration)
{
// Create the library exporter
var exporter = projectContext.CreateExporter(configuration);
// Gather exports for the project
var dependencies = exporter.GetDependencies().ToList();
return dependencies;
}
}
}

View file

@ -6,7 +6,7 @@
}, },
"dependencies": { "dependencies": {
"NETStandard.Library": "1.0.0-rc2-23704", "NETStandard.Library": "1.0.0-rc2-23704",
"System.Linq": "4.0.1-rc2-23608", "System.Linq": "4.0.1-rc2-23704",
"System.Reflection.Metadata": "1.1.0", "System.Reflection.Metadata": "1.1.0",
"Microsoft.DotNet.ProjectModel": "1.0.0-*", "Microsoft.DotNet.ProjectModel": "1.0.0-*",

View file

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved. // Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.Dnx.Runtime.Common.CommandLine; using Microsoft.Dnx.Runtime.Common.CommandLine;
@ -10,8 +9,8 @@ using Microsoft.DotNet.ProjectModel;
using NuGet.Frameworks; using NuGet.Frameworks;
using System.Linq; using System.Linq;
//This class is responsible with defining the arguments for the Compile verb. // This class is responsible with defining the arguments for the Compile verb.
//It knows how to interpret them and set default values // It knows how to interpret them and set default values
namespace Microsoft.DotNet.Tools.Compiler namespace Microsoft.DotNet.Tools.Compiler
{ {
public delegate bool OnExecute(List<ProjectContext> contexts, CompilerCommandApp compilerCommand); public delegate bool OnExecute(List<ProjectContext> contexts, CompilerCommandApp compilerCommand);
@ -20,7 +19,7 @@ namespace Microsoft.DotNet.Tools.Compiler
{ {
private readonly CommandLineApplication _app; private readonly CommandLineApplication _app;
//options and arguments for compilation // options and arguments for compilation
private CommandOption _outputOption; private CommandOption _outputOption;
private CommandOption _intermediateOutputOption; private CommandOption _intermediateOutputOption;
private CommandOption _frameworkOption; private CommandOption _frameworkOption;
@ -36,7 +35,7 @@ namespace Microsoft.DotNet.Tools.Compiler
private CommandOption _cppModeOption; private CommandOption _cppModeOption;
private CommandOption _cppCompilerFlagsOption; private CommandOption _cppCompilerFlagsOption;
//resolved values for the options and arguments // resolved values for the options and arguments
public string ProjectPathValue { get; set; } public string ProjectPathValue { get; set; }
public string OutputValue { get; set; } public string OutputValue { get; set; }
public string IntermediateValue { get; set; } public string IntermediateValue { get; set; }
@ -51,7 +50,7 @@ namespace Microsoft.DotNet.Tools.Compiler
public string AppDepSdkPathValue { get; set; } public string AppDepSdkPathValue { get; set; }
public string CppCompilerFlagsValue { get; set; } public string CppCompilerFlagsValue { get; set; }
//workaround: CommandLineApplication is internal therefore I cannot make _app protected so baseclasses can add their own params // workaround: CommandLineApplication is internal therefore I cannot make _app protected so baseclasses can add their own params
private readonly Dictionary<string, CommandOption> baseClassOptions; private readonly Dictionary<string, CommandOption> baseClassOptions;
public CompilerCommandApp(string name, string fullName, string description) public CompilerCommandApp(string name, string fullName, string description)
@ -115,7 +114,6 @@ namespace Microsoft.DotNet.Tools.Compiler
IsCppModeValue = _cppModeOption.HasValue(); IsCppModeValue = _cppModeOption.HasValue();
CppCompilerFlagsValue = _cppCompilerFlagsOption.Value(); CppCompilerFlagsValue = _cppCompilerFlagsOption.Value();
// Load project contexts for each framework // Load project contexts for each framework
var contexts = _frameworkOption.HasValue() ? var contexts = _frameworkOption.HasValue() ?
_frameworkOption.Values.Select(f => ProjectContext.Create(ProjectPathValue, NuGetFramework.Parse(f))) : _frameworkOption.Values.Select(f => ProjectContext.Create(ProjectPathValue, NuGetFramework.Parse(f))) :
@ -129,7 +127,12 @@ namespace Microsoft.DotNet.Tools.Compiler
return _app.Execute(args); return _app.Execute(args);
} }
//CommandOptionType is internal. Cannot pass it as argument. Therefore the method name encodes the option type. public CompilerCommandApp ShallowCopy()
{
return (CompilerCommandApp) MemberwiseClone();
}
// CommandOptionType is internal. Cannot pass it as argument. Therefore the method name encodes the option type.
protected void AddNoValueOption(string optionTemplate, string descriptino){ protected void AddNoValueOption(string optionTemplate, string descriptino){
baseClassOptions[optionTemplate] = _app.Option(optionTemplate, descriptino, CommandOptionType.NoValue); baseClassOptions[optionTemplate] = _app.Option(optionTemplate, descriptino, CommandOptionType.NoValue);
} }

View file

@ -2,18 +2,19 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectModel; using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.Cli.Compiler.Common; using Microsoft.DotNet.Cli.Compiler.Common;
using Microsoft.DotNet.ProjectModel.Compilation; using Microsoft.DotNet.ProjectModel.Compilation;
using Microsoft.DotNet.Tools.Common; using Microsoft.DotNet.Tools.Common;
using NuGet.Frameworks;
//This class is responsible with defining the arguments for the Compile verb. // This class is responsible with defining the arguments for the Compile verb.
//It knows how to interpret them and set default values // It knows how to interpret them and set default values
namespace Microsoft.DotNet.Tools.Compiler namespace Microsoft.DotNet.Tools.Compiler
{ {
@ -32,7 +33,7 @@ namespace Microsoft.DotNet.Tools.Compiler
public readonly string InputFile; public readonly string InputFile;
public readonly string MetadataName; public readonly string MetadataName;
//is non-null only when resgen needs to be invoked (inputFile is .resx) // is non-null only when resgen needs to be invoked (inputFile is .resx)
public readonly string OutputFile; public readonly string OutputFile;
public NonCultureResgenIO(string inputFile, string outputFile, string metadataName) public NonCultureResgenIO(string inputFile, string outputFile, string metadataName)
@ -43,7 +44,7 @@ namespace Microsoft.DotNet.Tools.Compiler
} }
} }
//used in incremental compilation // used in incremental compilation
public static List<NonCultureResgenIO> GetNonCultureResources(Project project, string intermediateOutputPath) public static List<NonCultureResgenIO> GetNonCultureResources(Project project, string intermediateOutputPath)
{ {
return return
@ -70,7 +71,7 @@ namespace Microsoft.DotNet.Tools.Compiler
} }
} }
//used in incremental compilation // used in incremental compilation
public static List<CultureResgenIO> GetCultureResources(Project project, string outputPath) public static List<CultureResgenIO> GetCultureResources(Project project, string outputPath)
{ {
return return
@ -84,7 +85,7 @@ namespace Microsoft.DotNet.Tools.Compiler
).ToList(); ).ToList();
} }
//used in incremental compilation // used in incremental compilation
public static IList<string> GetReferencePathsForCultureResgen(List<LibraryExport> dependencies) public static IList<string> GetReferencePathsForCultureResgen(List<LibraryExport> dependencies)
{ {
return dependencies.SelectMany(libraryExport => libraryExport.CompilationAssemblies).Select(r => r.ResolvedPath).ToList(); return dependencies.SelectMany(libraryExport => libraryExport.CompilationAssemblies).Select(r => r.ResolvedPath).ToList();
@ -99,7 +100,7 @@ namespace Microsoft.DotNet.Tools.Compiler
string resourcePath = resourceFile.Key; string resourcePath = resourceFile.Key;
if (string.IsNullOrEmpty(resourceFile.Value)) if (string.IsNullOrEmpty(resourceFile.Value))
{ {
// No logical name, so use the file name // No logical name, so use the file name
resourceName = ResourceUtility.GetResourceName(root, resourcePath); resourceName = ResourceUtility.GetResourceName(root, resourcePath);
rootNamespace = project.Name; rootNamespace = project.Name;
} }
@ -113,6 +114,44 @@ namespace Microsoft.DotNet.Tools.Compiler
return name; return name;
} }
// used in incremental compilation
public static IEnumerable<string> GetCompilationSources(ProjectContext project) => project.ProjectFile.Files.SourceFiles;
// used in incremental compilation
public static string GetCompilationOutput(Project project, NuGetFramework framework, string configuration, string outputPath)
{
var compilationOptions = project.GetCompilerOptions(framework, configuration);
var outputExtension = ".dll";
if (framework.IsDesktop() && compilationOptions.EmitEntryPoint.GetValueOrDefault())
{
outputExtension = ".exe";
}
return Path.Combine(outputPath, project.Name + outputExtension);
}
// used in incremental compilation for the key file
public static CommonCompilerOptions ResolveCompilationOptions(ProjectContext context, string configuration)
{
var compilationOptions = context.ProjectFile.GetCompilerOptions(context.TargetFramework, configuration);
// Path to strong naming key in environment variable overrides path in project.json
var environmentKeyFile = Environment.GetEnvironmentVariable(EnvironmentNames.StrongNameKeyFile);
if (!string.IsNullOrWhiteSpace(environmentKeyFile))
{
compilationOptions.KeyFile = environmentKeyFile;
}
else if (!string.IsNullOrWhiteSpace(compilationOptions.KeyFile))
{
// Resolve full path to key file
compilationOptions.KeyFile =
Path.GetFullPath(Path.Combine(context.ProjectFile.ProjectDirectory, compilationOptions.KeyFile));
}
return compilationOptions;
}
public static IEnumerable<string> GetCommandsInvokedByCompile(ProjectContext project) public static IEnumerable<string> GetCommandsInvokedByCompile(ProjectContext project)
{ {
return new List<string> {ResolveCompilerName(project)}; return new List<string> {ResolveCompilerName(project)};

View file

@ -71,7 +71,7 @@ namespace Microsoft.DotNet.Tools.Compiler
var compilationOptions = context.ProjectFile.GetCompilerOptions(context.TargetFramework, args.ConfigValue); var compilationOptions = context.ProjectFile.GetCompilerOptions(context.TargetFramework, args.ConfigValue);
var managedOutput = var managedOutput =
GetProjectOutput(context.ProjectFile, context.TargetFramework, args.ConfigValue, outputPath); CompilerUtil.GetCompilationOutput(context.ProjectFile, context.TargetFramework, args.ConfigValue, outputPath);
var nativeArgs = new List<string>(); var nativeArgs = new List<string>();
@ -199,7 +199,7 @@ namespace Microsoft.DotNet.Tools.Compiler
} }
// Get compilation options // Get compilation options
var outputName = GetProjectOutput(context.ProjectFile, context.TargetFramework, args.ConfigValue, outputPath); var outputName = CompilerUtil.GetCompilationOutput(context.ProjectFile, context.TargetFramework, args.ConfigValue, outputPath);
// Assemble args // Assemble args
var compilerArgs = new List<string>() var compilerArgs = new List<string>()
@ -208,20 +208,7 @@ namespace Microsoft.DotNet.Tools.Compiler
$"--out:{outputName}" $"--out:{outputName}"
}; };
var compilationOptions = context.ProjectFile.GetCompilerOptions(context.TargetFramework, args.ConfigValue); var compilationOptions = CompilerUtil.ResolveCompilationOptions(context, args.ConfigValue);
// Path to strong naming key in environment variable overrides path in project.json
var environmentKeyFile = Environment.GetEnvironmentVariable(EnvironmentNames.StrongNameKeyFile);
if (!string.IsNullOrWhiteSpace(environmentKeyFile))
{
compilationOptions.KeyFile = environmentKeyFile;
}
else if (!string.IsNullOrWhiteSpace(compilationOptions.KeyFile))
{
// Resolve full path to key file
compilationOptions.KeyFile = Path.GetFullPath(Path.Combine(context.ProjectFile.ProjectDirectory, compilationOptions.KeyFile));
}
var references = new List<string>(); var references = new List<string>();
@ -239,7 +226,7 @@ namespace Microsoft.DotNet.Tools.Compiler
{ {
if (projectDependency.Project.Files.SourceFiles.Any()) if (projectDependency.Project.Files.SourceFiles.Any())
{ {
var projectOutputPath = GetProjectOutput(projectDependency.Project, projectDependency.Framework, args.ConfigValue, outputPath); var projectOutputPath = CompilerUtil.GetCompilationOutput(projectDependency.Project, projectDependency.Framework, args.ConfigValue, outputPath);
references.Add(projectOutputPath); references.Add(projectOutputPath);
} }
} }
@ -288,7 +275,7 @@ namespace Microsoft.DotNet.Tools.Compiler
return false; return false;
} }
// Add project source files // Add project source files
var sourceFiles = context.ProjectFile.Files.SourceFiles; var sourceFiles = CompilerUtil.GetCompilationSources(context);
compilerArgs.AddRange(sourceFiles); compilerArgs.AddRange(sourceFiles);
var compilerName = CompilerUtil.ResolveCompilerName(context); var compilerName = CompilerUtil.ResolveCompilerName(context);
@ -367,6 +354,8 @@ namespace Microsoft.DotNet.Tools.Compiler
return PrintSummary(diagnostics, sw, success); return PrintSummary(diagnostics, sw, success);
} }
private static void RunScripts(ProjectContext context, string name, Dictionary<string, string> contextVariables) private static void RunScripts(ProjectContext context, string name, Dictionary<string, string> contextVariables)
{ {
foreach (var script in context.ProjectFile.Scripts.GetOrEmpty(name)) foreach (var script in context.ProjectFile.Scripts.GetOrEmpty(name))
@ -378,18 +367,7 @@ namespace Microsoft.DotNet.Tools.Compiler
} }
} }
private static string GetProjectOutput(Project project, NuGetFramework framework, string configuration, string outputPath)
{
var compilationOptions = project.GetCompilerOptions(framework, configuration);
var outputExtension = ".dll";
if (framework.IsDesktop() && compilationOptions.EmitEntryPoint.GetValueOrDefault())
{
outputExtension = ".exe";
}
return Path.Combine(outputPath, project.Name + outputExtension);
}
private static void CopyExport(string outputPath, LibraryExport export) private static void CopyExport(string outputPath, LibraryExport export)

View file

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Xunit; using Xunit;
@ -47,6 +48,91 @@ namespace Microsoft.DotNet.Tests.EndToEnd
TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName()); TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName());
} }
[Fact]
public void TestDotnetIncrementalBuild()
{
TestSetup();
// first build
var buildCommand = new BuildCommand(TestProject, output: OutputDirectory);
buildCommand.Execute().Should().Pass();
TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName());
var latestWriteTimeFirstBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
// second build; should get skipped (incremental because no inputs changed)
buildCommand.Execute().Should().Pass();
TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName());
var latestWriteTimeSecondBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
Assert.Equal(latestWriteTimeFirstBuild, latestWriteTimeSecondBuild);
TouchSourceFileInDirectory(TestDirectory);
// third build; should get compiled because the source file got touched
buildCommand.Execute().Should().Pass();
TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName());
var latestWriteTimeThirdBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
Assert.NotEqual(latestWriteTimeSecondBuild, latestWriteTimeThirdBuild);
}
[Fact]
public void TestDotnetForceIncrementalUnsafe()
{
TestSetup();
// first build
var buildCommand = new BuildCommand(TestProject, output: OutputDirectory);
buildCommand.Execute().Should().Pass();
TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName());
var latestWriteTimeFirstBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
// second build; will get recompiled due to force unsafe flag
buildCommand = new BuildCommand(TestProject, output: OutputDirectory, forceIncrementalUnsafe:true);
buildCommand.Execute().Should().Pass();
TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName());
var latestWriteTimeSecondBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
Assert.NotEqual(latestWriteTimeFirstBuild, latestWriteTimeSecondBuild);
}
[Fact]
public void TestDotnetIncrementalBuildDeleteOutputFile()
{
TestSetup();
// first build
var buildCommand = new BuildCommand(TestProject, output: OutputDirectory);
buildCommand.Execute().Should().Pass();
TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName());
var latestWriteTimeFirstBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
Reporter.Verbose.WriteLine($"Files in {OutputDirectory}");
foreach (var file in Directory.EnumerateFiles(OutputDirectory))
{
Reporter.Verbose.Write($"\t {file}");
}
// delete output files
foreach (var outputFile in Directory.EnumerateFiles(OutputDirectory).Where(f => Path.GetFileName(f).StartsWith(s_testdirName, StringComparison.OrdinalIgnoreCase)))
{
Reporter.Verbose.WriteLine($"Delete {outputFile}");
File.Delete(outputFile);
Assert.False(File.Exists(outputFile));
}
// second build; should get rebuilt since we deleted output items
buildCommand.Execute().Should().Pass();
TestOutputExecutable(OutputDirectory, buildCommand.GetOutputExecutableName());
var latestWriteTimeSecondBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
Assert.NotEqual(latestWriteTimeFirstBuild, latestWriteTimeSecondBuild);
}
[Fact] [Fact]
[ActiveIssue(712, PlatformID.Windows | PlatformID.OSX | PlatformID.Linux)] [ActiveIssue(712, PlatformID.Windows | PlatformID.OSX | PlatformID.Linux)]
public void TestDotnetBuildNativeRyuJit() public void TestDotnetBuildNativeRyuJit()
@ -82,6 +168,32 @@ namespace Microsoft.DotNet.Tests.EndToEnd
TestOutputExecutable(nativeOut, buildCommand.GetOutputExecutableName()); TestOutputExecutable(nativeOut, buildCommand.GetOutputExecutableName());
} }
[Fact]
public void TestDotnetCompileNativeCppIncremental()
{
if (IsCentOS())
{
Console.WriteLine("Skipping native compilation tests on CentOS - https://github.com/dotnet/cli/issues/453");
return;
}
var nativeOut = Path.Combine(OutputDirectory, "native");
// first build
var buildCommand = new BuildCommand(TestProject, output: OutputDirectory, native: true, nativeCppMode: true);
buildCommand.Execute().Should().Pass();
TestOutputExecutable(nativeOut, buildCommand.GetOutputExecutableName());
var latestWriteTimeFirstBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
// second build; should be skipped because nothing changed
buildCommand.Execute().Should().Pass();
TestOutputExecutable(nativeOut, buildCommand.GetOutputExecutableName());
var latestWriteTimeSecondBuild = GetLastWriteTimeOfDirectoryFiles(OutputDirectory);
Assert.Equal(latestWriteTimeFirstBuild, latestWriteTimeSecondBuild);
}
[Fact] [Fact]
public void TestDotnetRun() public void TestDotnetRun()
{ {
@ -160,5 +272,16 @@ namespace Microsoft.DotNet.Tests.EndToEnd
return false; return false;
} }
private static DateTime GetLastWriteTimeOfDirectoryFiles(string outputDirectory)
{
return Directory.EnumerateFiles(outputDirectory).Max(f => File.GetLastWriteTime(f));
}
private static void TouchSourceFileInDirectory(string directory)
{
var csFile = Directory.EnumerateFiles(directory).First(f => Path.GetExtension(f).Equals(".cs"));
File.SetLastWriteTimeUtc(csFile, DateTime.UtcNow);
}
} }
} }

View file

@ -23,6 +23,8 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
private string _appDepSDKPath; private string _appDepSDKPath;
private bool _nativeCppMode; private bool _nativeCppMode;
private string _cppCompilerFlags; private string _cppCompilerFlags;
private bool _buildProfile;
private bool _forceIncrementalUnsafe;
private string OutputOption private string OutputOption
{ {
@ -134,6 +136,26 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
} }
} }
private string BuildProfile
{
get
{
return _buildProfile ?
"--build-profile" :
"";
}
}
private string ForceIncrementalUnsafe
{
get
{
return _forceIncrementalUnsafe ?
"--force-incremental-unsafe" :
"";
}
}
public BuildCommand( public BuildCommand(
string projectPath, string projectPath,
string output="", string output="",
@ -146,7 +168,9 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
string ilcPath="", string ilcPath="",
string appDepSDKPath="", string appDepSDKPath="",
bool nativeCppMode=false, bool nativeCppMode=false,
string cppCompilerFlags="" string cppCompilerFlags="",
bool buildProfile=true,
bool forceIncrementalUnsafe=false
) )
: base("dotnet") : base("dotnet")
{ {
@ -165,12 +189,13 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
_appDepSDKPath = appDepSDKPath; _appDepSDKPath = appDepSDKPath;
_nativeCppMode = nativeCppMode; _nativeCppMode = nativeCppMode;
_cppCompilerFlags = cppCompilerFlags; _cppCompilerFlags = cppCompilerFlags;
_buildProfile = buildProfile;
_forceIncrementalUnsafe = forceIncrementalUnsafe;
} }
public override CommandResult Execute(string args = "") public override CommandResult Execute(string args = "")
{ {
args = $"build {BuildArgs()} {args}"; args = $"--verbose build {BuildArgs()} {args}";
return base.Execute(args); return base.Execute(args);
} }
@ -189,7 +214,7 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
private string BuildArgs() private string BuildArgs()
{ {
return $"{_projectPath} {OutputOption} {TempOutputOption} {ConfigurationOption} {NoHostOption} {NativeOption} {ArchitectureOption} {IlcArgsOption} {IlcPathOption} {AppDepSDKPathOption} {NativeCppModeOption} {CppCompilerFlagsOption}"; return $"{BuildProfile} {ForceIncrementalUnsafe} {_projectPath} {OutputOption} {TempOutputOption} {ConfigurationOption} {NoHostOption} {NativeOption} {ArchitectureOption} {IlcArgsOption} {IlcPathOption} {AppDepSDKPathOption} {NativeCppModeOption} {CppCompilerFlagsOption}";
} }
} }
} }