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
{
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");
}
}
}

View file

@ -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,27 +25,27 @@ 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
// gather preconditions
_preconditions = GatherIncrementalPreconditions();
}
@ -51,26 +53,116 @@ namespace Microsoft.DotNet.Tools.Build
{
CreateOutputDirectories();
//compile dependencies
foreach (var dependency in Sort(_dependencies))
// compile 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;
}
}
//compile project
var success = InvokeCompileOnRootProject(_outputPath, _intermediateOutputPath);
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();
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
// 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)
{
Reporter.Output.Write(" " + _preconditions.LogMessage());
@ -82,61 +174,40 @@ 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;
}
//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()
{
//include initial root project
var contextsToCheck = new List<ProjectContext>(1 + _dependencies.Count) {_rootProject};
// include initial root project
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));
// convert ProjectDescription to ProjectContext
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);
}
}
}
}
}

View file

@ -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 "";

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": {
"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-*",

View file

@ -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;
@ -10,8 +9,8 @@ using Microsoft.DotNet.ProjectModel;
using NuGet.Frameworks;
using System.Linq;
//This class is responsible with defining the arguments for the Compile verb.
//It knows how to interpret them and set default values
// This class is responsible with defining the arguments for the Compile verb.
// It knows how to interpret them and set default values
namespace Microsoft.DotNet.Tools.Compiler
{
public delegate bool OnExecute(List<ProjectContext> contexts, CompilerCommandApp compilerCommand);
@ -20,7 +19,7 @@ namespace Microsoft.DotNet.Tools.Compiler
{
private readonly CommandLineApplication _app;
//options and arguments for compilation
// options and arguments for compilation
private CommandOption _outputOption;
private CommandOption _intermediateOutputOption;
private CommandOption _frameworkOption;
@ -36,7 +35,7 @@ namespace Microsoft.DotNet.Tools.Compiler
private CommandOption _cppModeOption;
private CommandOption _cppCompilerFlagsOption;
//resolved values for the options and arguments
// resolved values for the options and arguments
public string ProjectPathValue { get; set; }
public string OutputValue { get; set; }
public string IntermediateValue { get; set; }
@ -51,7 +50,7 @@ namespace Microsoft.DotNet.Tools.Compiler
public string AppDepSdkPathValue { 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;
public CompilerCommandApp(string name, string fullName, string description)
@ -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,7 +127,12 @@ namespace Microsoft.DotNet.Tools.Compiler
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){
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.
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
// This class is responsible with defining the arguments for the Compile verb.
// It knows how to interpret them and set default values
namespace Microsoft.DotNet.Tools.Compiler
{
@ -32,7 +33,7 @@ namespace Microsoft.DotNet.Tools.Compiler
public readonly string InputFile;
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 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)
{
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)
{
return
@ -84,7 +85,7 @@ namespace Microsoft.DotNet.Tools.Compiler
).ToList();
}
//used in incremental compilation
// used in incremental compilation
public static IList<string> GetReferencePathsForCultureResgen(List<LibraryExport> dependencies)
{
return dependencies.SelectMany(libraryExport => libraryExport.CompilationAssemblies).Select(r => r.ResolvedPath).ToList();
@ -99,7 +100,7 @@ namespace Microsoft.DotNet.Tools.Compiler
string resourcePath = resourceFile.Key;
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);
rootNamespace = project.Name;
}
@ -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)};

View file

@ -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)

View file

@ -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);
}
}
}

View file

@ -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}";
}
}
}