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:
parent
28f01faae5
commit
bedeaaf2dc
10 changed files with 501 additions and 114 deletions
|
@ -7,13 +7,16 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
{
|
||||
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 ForceUnsafeValue => OptionHasValue(ForceUnsafeFlag);
|
||||
|
||||
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 mark the entire build as not safe for incrementality");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
|
@ -14,6 +15,7 @@ using Microsoft.DotNet.Cli.Compiler.Common;
|
|||
|
||||
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
|
||||
// Collects icnremental safety checks and transitively compiles a project context
|
||||
internal class CompileContext
|
||||
|
@ -23,25 +25,25 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
|
||||
private readonly ProjectContext _rootProject;
|
||||
private readonly BuilderCommandApp _args;
|
||||
private readonly Dictionary<string, ProjectDescription> _dependencies;
|
||||
private readonly string _outputPath;
|
||||
private readonly string _intermediateOutputPath;
|
||||
private readonly IncrementalPreconditions _preconditions;
|
||||
private readonly ProjectDependenciesFacade _dependencies;
|
||||
|
||||
public bool IsSafeForIncrementalCompilation => _preconditions.PreconditionsDetected();
|
||||
public bool IsSafeForIncrementalCompilation => !_preconditions.PreconditionsDetected();
|
||||
|
||||
public CompileContext(ProjectContext rootProject, BuilderCommandApp args)
|
||||
{
|
||||
_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
|
||||
// Todo: clone args and mutate the clone so the rest of this class does not have special treatment for output paths
|
||||
_outputPath = _rootProject.GetOutputPath(_args.ConfigValue, _args.OutputValue);
|
||||
_intermediateOutputPath = _rootProject.GetIntermediateOutputPath(_args.ConfigValue, _args.IntermediateValue, _args.OutputValue);
|
||||
_args.OutputValue = _rootProject.GetOutputPath(_args.ConfigValue, _args.OutputValue);
|
||||
_args.IntermediateValue = _rootProject.GetIntermediateOutputPath(_args.ConfigValue, _args.IntermediateValue, _args.OutputValue);
|
||||
|
||||
// Set up dependencies
|
||||
_dependencies = GetProjectDependenciesWithSources(_rootProject, _args.ConfigValue);
|
||||
_dependencies = new ProjectDependenciesFacade(_rootProject, _args.ConfigValue);
|
||||
|
||||
// gather preconditions
|
||||
_preconditions = GatherIncrementalPreconditions();
|
||||
|
@ -52,22 +54,112 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
CreateOutputDirectories();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// compile project
|
||||
var success = InvokeCompileOnRootProject(_outputPath, _intermediateOutputPath);
|
||||
var success = InvokeCompileOnRootProject();
|
||||
|
||||
PrintSummary(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)
|
||||
{
|
||||
// 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
|
||||
|
@ -82,47 +174,26 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
|
||||
private void CreateOutputDirectories()
|
||||
{
|
||||
Directory.CreateDirectory(_outputPath);
|
||||
Directory.CreateDirectory(_intermediateOutputPath);
|
||||
}
|
||||
|
||||
//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;
|
||||
Directory.CreateDirectory(_args.OutputValue);
|
||||
Directory.CreateDirectory(_args.IntermediateValue);
|
||||
}
|
||||
|
||||
private IncrementalPreconditions GatherIncrementalPreconditions()
|
||||
{
|
||||
var preconditions = new IncrementalPreconditions(_args.BuildProfileValue);
|
||||
|
||||
if (_args.ForceUnsafeValue)
|
||||
{
|
||||
preconditions.AddForceUnsafePrecondition();
|
||||
}
|
||||
|
||||
var projectsToCheck = GetProjectsToCheck();
|
||||
|
||||
foreach (var project in projectsToCheck)
|
||||
{
|
||||
CollectScriptPreconditions(project, preconditions);
|
||||
CollectCompilerNamePreconditions(project, preconditions);
|
||||
CheckPathProbing(project, preconditions);
|
||||
CollectCheckPathProbingPreconditions(project, preconditions);
|
||||
}
|
||||
|
||||
return preconditions;
|
||||
|
@ -132,11 +203,11 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
private List<ProjectContext> GetProjectsToCheck()
|
||||
{
|
||||
// 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
|
||||
var dependencyContexts = _dependencies.Select
|
||||
(keyValuePair => ProjectContext.Create(keyValuePair.Value.Path, keyValuePair.Value.TargetFrameworkInfo.FrameworkName));
|
||||
var dependencyContexts = _dependencies.ProjectDependenciesWithSources.Select
|
||||
(keyValuePair => ProjectContext.Create(keyValuePair.Value.Path, keyValuePair.Value.Framework));
|
||||
|
||||
contextsToCheck.AddRange(dependencyContexts);
|
||||
|
||||
|
@ -144,7 +215,7 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
return contextsToCheck;
|
||||
}
|
||||
|
||||
private void CheckPathProbing(ProjectContext project, IncrementalPreconditions preconditions)
|
||||
private void CollectCheckPathProbingPreconditions(ProjectContext project, IncrementalPreconditions preconditions)
|
||||
{
|
||||
var pathCommands = CompilerUtil.GetCommandsInvokedByCompile(project)
|
||||
.Select(commandName => Command.Create(commandName, "", project.TargetFramework))
|
||||
|
@ -160,7 +231,7 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
{
|
||||
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);
|
||||
}
|
||||
|
@ -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",
|
||||
$"--framework {projectDependency.Framework} " +
|
||||
$"--configuration {_args.ConfigValue} " +
|
||||
$"--output \"{outputPath}\" " +
|
||||
$"--temp-output \"{intermediateOutputPath}\" " +
|
||||
$"--output \"{_args.OutputValue}\" " +
|
||||
$"--temp-output \"{_args.IntermediateValue}\" " +
|
||||
(_args.NoHostValue ? "--no-host " : string.Empty) +
|
||||
$"\"{projectDependency.Project.ProjectDirectory}\"")
|
||||
.ForwardStdOut()
|
||||
|
@ -198,14 +269,14 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
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",
|
||||
$"--framework {_rootProject.TargetFramework} " +
|
||||
$"--configuration {_args.ConfigValue} " +
|
||||
$"--output \"{outputPath}\" " +
|
||||
$"--temp-output \"{intermediateOutputPath}\" " +
|
||||
$"--output \"{_args.OutputValue}\" " +
|
||||
$"--temp-output \"{_args.IntermediateValue}\" " +
|
||||
(_args.NoHostValue ? "--no-host " : string.Empty) +
|
||||
//nativeArgs
|
||||
(_args.IsNativeValue ? "--native " : string.Empty) +
|
||||
|
@ -248,6 +319,96 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
{
|
||||
internal class IncrementalPreconditions
|
||||
{
|
||||
private readonly List<string> _preconditions;
|
||||
private readonly ISet<string> _preconditions;
|
||||
private readonly bool _isProfile;
|
||||
|
||||
public IncrementalPreconditions(bool isProfile)
|
||||
{
|
||||
_isProfile = isProfile;
|
||||
_preconditions = new List<string>();
|
||||
_preconditions = new HashSet<string>();
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
return _preconditions.Any();
|
||||
|
@ -70,7 +75,7 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
{
|
||||
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 "";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"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",
|
||||
|
||||
"Microsoft.DotNet.ProjectModel": "1.0.0-*",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.Dnx.Runtime.Common.CommandLine;
|
||||
|
@ -115,7 +114,6 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
IsCppModeValue = _cppModeOption.HasValue();
|
||||
CppCompilerFlagsValue = _cppCompilerFlagsOption.Value();
|
||||
|
||||
|
||||
// Load project contexts for each framework
|
||||
var contexts = _frameworkOption.HasValue() ?
|
||||
_frameworkOption.Values.Select(f => ProjectContext.Create(ProjectPathValue, NuGetFramework.Parse(f))) :
|
||||
|
@ -129,6 +127,11 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
return _app.Execute(args);
|
||||
}
|
||||
|
||||
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){
|
||||
baseClassOptions[optionTemplate] = _app.Option(optionTemplate, descriptino, CommandOptionType.NoValue);
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
using Microsoft.DotNet.ProjectModel;
|
||||
using Microsoft.DotNet.Cli.Compiler.Common;
|
||||
using Microsoft.DotNet.ProjectModel.Compilation;
|
||||
using Microsoft.DotNet.Tools.Common;
|
||||
using NuGet.Frameworks;
|
||||
|
||||
// This class is responsible with defining the arguments for the Compile verb.
|
||||
// It knows how to interpret them and set default values
|
||||
|
@ -113,6 +114,44 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
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)
|
||||
{
|
||||
return new List<string> {ResolveCompilerName(project)};
|
||||
|
|
|
@ -71,7 +71,7 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
|
||||
var compilationOptions = context.ProjectFile.GetCompilerOptions(context.TargetFramework, args.ConfigValue);
|
||||
var managedOutput =
|
||||
GetProjectOutput(context.ProjectFile, context.TargetFramework, args.ConfigValue, outputPath);
|
||||
CompilerUtil.GetCompilationOutput(context.ProjectFile, context.TargetFramework, args.ConfigValue, outputPath);
|
||||
|
||||
var nativeArgs = new List<string>();
|
||||
|
||||
|
@ -199,7 +199,7 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
}
|
||||
|
||||
// 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
|
||||
var compilerArgs = new List<string>()
|
||||
|
@ -208,20 +208,7 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
$"--out:{outputName}"
|
||||
};
|
||||
|
||||
var compilationOptions = context.ProjectFile.GetCompilerOptions(context.TargetFramework, 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 compilationOptions = CompilerUtil.ResolveCompilationOptions(context, args.ConfigValue);
|
||||
|
||||
var references = new List<string>();
|
||||
|
||||
|
@ -239,7 +226,7 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -288,7 +275,7 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
return false;
|
||||
}
|
||||
// Add project source files
|
||||
var sourceFiles = context.ProjectFile.Files.SourceFiles;
|
||||
var sourceFiles = CompilerUtil.GetCompilationSources(context);
|
||||
compilerArgs.AddRange(sourceFiles);
|
||||
|
||||
var compilerName = CompilerUtil.ResolveCompilerName(context);
|
||||
|
@ -367,6 +354,8 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
return PrintSummary(diagnostics, sw, success);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void RunScripts(ProjectContext context, string name, Dictionary<string, string> contextVariables)
|
||||
{
|
||||
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)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
@ -47,6 +48,91 @@ namespace Microsoft.DotNet.Tests.EndToEnd
|
|||
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]
|
||||
[ActiveIssue(712, PlatformID.Windows | PlatformID.OSX | PlatformID.Linux)]
|
||||
public void TestDotnetBuildNativeRyuJit()
|
||||
|
@ -82,6 +168,32 @@ namespace Microsoft.DotNet.Tests.EndToEnd
|
|||
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]
|
||||
public void TestDotnetRun()
|
||||
{
|
||||
|
@ -160,5 +272,16 @@ namespace Microsoft.DotNet.Tests.EndToEnd
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
private string _appDepSDKPath;
|
||||
private bool _nativeCppMode;
|
||||
private string _cppCompilerFlags;
|
||||
private bool _buildProfile;
|
||||
private bool _forceIncrementalUnsafe;
|
||||
|
||||
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(
|
||||
string projectPath,
|
||||
string output="",
|
||||
|
@ -146,7 +168,9 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
string ilcPath="",
|
||||
string appDepSDKPath="",
|
||||
bool nativeCppMode=false,
|
||||
string cppCompilerFlags=""
|
||||
string cppCompilerFlags="",
|
||||
bool buildProfile=true,
|
||||
bool forceIncrementalUnsafe=false
|
||||
)
|
||||
: base("dotnet")
|
||||
{
|
||||
|
@ -165,12 +189,13 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
_appDepSDKPath = appDepSDKPath;
|
||||
_nativeCppMode = nativeCppMode;
|
||||
_cppCompilerFlags = cppCompilerFlags;
|
||||
|
||||
_buildProfile = buildProfile;
|
||||
_forceIncrementalUnsafe = forceIncrementalUnsafe;
|
||||
}
|
||||
|
||||
public override CommandResult Execute(string args = "")
|
||||
{
|
||||
args = $"build {BuildArgs()} {args}";
|
||||
args = $"--verbose build {BuildArgs()} {args}";
|
||||
return base.Execute(args);
|
||||
}
|
||||
|
||||
|
@ -189,7 +214,7 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue