Merge pull request #2619 from cdmihai/cdmihai/globChangeDetectionRelMerge
Cache inputs for globbing change detection
This commit is contained in:
commit
91fb67835d
13 changed files with 541 additions and 167 deletions
12
TestAssets/TestProjects/TestSimpleIncrementalApp/Program2.cs
Normal file
12
TestAssets/TestProjects/TestSimpleIncrementalApp/Program2.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace ConsoleApplication
|
||||
{
|
||||
public class Program2
|
||||
{
|
||||
public static void Foo(string[] args)
|
||||
{
|
||||
Console.WriteLine("Hello World!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
// 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.IO;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.PlatformAbstractions;
|
||||
|
||||
namespace Microsoft.DotNet.Cli.Utils
|
||||
{
|
||||
|
@ -12,5 +14,16 @@ namespace Microsoft.DotNet.Cli.Utils
|
|||
/// The CLI ships with a .version file that stores the commit information and CLI version
|
||||
/// </summary>
|
||||
public static string VersionFile => Path.GetFullPath(Path.Combine(typeof(DotnetFiles).GetTypeInfo().Assembly.Location, "..", ".version"));
|
||||
|
||||
/// <summary>
|
||||
/// Reads the version file and adds runtime specific information
|
||||
/// </summary>
|
||||
public static string ReadAndInterpretVersionFile()
|
||||
{
|
||||
var content = File.ReadAllText(DotnetFiles.VersionFile);
|
||||
content += Environment.NewLine;
|
||||
content += PlatformServices.Default.Runtime.GetRuntimeIdentifier();
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,12 +37,14 @@ namespace Microsoft.DotNet.Cli.Utils
|
|||
{
|
||||
Output = new Reporter(AnsiConsole.GetOutput());
|
||||
Error = new Reporter(AnsiConsole.GetError());
|
||||
Verbose = CommandContext.IsVerbose() ?
|
||||
Verbose = IsVerbose ?
|
||||
new Reporter(AnsiConsole.GetOutput()) :
|
||||
NullReporter;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsVerbose => CommandContext.IsVerbose();
|
||||
|
||||
public void WriteLine(string message)
|
||||
{
|
||||
lock (_lock)
|
||||
|
|
|
@ -37,6 +37,12 @@ namespace Microsoft.DotNet.Cli.Compiler.Common
|
|||
return Path.Combine(intermediatePath, ".SDKVersion");
|
||||
}
|
||||
|
||||
public static string IncrementalCacheFile(this ProjectContext context, string configuration, string buildBasePath, string outputPath)
|
||||
{
|
||||
var intermediatePath = context.GetOutputPaths(configuration, buildBasePath, outputPath).IntermediateOutputDirectoryPath;
|
||||
return Path.Combine(intermediatePath, ".IncrementalCache");
|
||||
}
|
||||
|
||||
// used in incremental compilation for the key file
|
||||
public static CommonCompilerOptions ResolveCompilationOptions(this ProjectContext context, string configuration)
|
||||
{
|
||||
|
|
|
@ -11,6 +11,9 @@ namespace Microsoft.DotNet.TestFramework
|
|||
{
|
||||
public class TestInstance
|
||||
{
|
||||
// made tolower because the rest of the class works with normalized tolower strings
|
||||
private static readonly IEnumerable<string> BuildArtifactBlackList = new List<string>() {".IncrementalCache", ".SDKVersion"}.Select(s => s.ToLower()).ToArray();
|
||||
|
||||
private string _testDestination;
|
||||
private string _testAssetRoot;
|
||||
|
||||
|
@ -105,8 +108,13 @@ namespace Microsoft.DotNet.TestFramework
|
|||
.Where(file =>
|
||||
{
|
||||
file = file.ToLower();
|
||||
return file.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")
|
||||
|
||||
var isArtifact = file.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")
|
||||
|| file.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}");
|
||||
|
||||
var isBlackListed = BuildArtifactBlackList.Any(b => file.Contains(b));
|
||||
|
||||
return isArtifact && !isBlackListed;
|
||||
});
|
||||
|
||||
foreach (string binFile in binFiles)
|
||||
|
|
|
@ -2,18 +2,43 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Build
|
||||
{
|
||||
internal struct CompilerIO
|
||||
internal class CompilerIO
|
||||
{
|
||||
public readonly List<string> Inputs;
|
||||
public readonly List<string> Outputs;
|
||||
public readonly IEnumerable<string> Inputs;
|
||||
public readonly IEnumerable<string> Outputs;
|
||||
|
||||
public CompilerIO(List<string> inputs, List<string> outputs)
|
||||
public CompilerIO(IEnumerable<string> inputs, IEnumerable<string> outputs)
|
||||
{
|
||||
Inputs = inputs;
|
||||
Outputs = outputs;
|
||||
}
|
||||
|
||||
public DiffResult DiffInputs(CompilerIO other)
|
||||
{
|
||||
var myInputSet = new HashSet<string>(Inputs);
|
||||
var otherInputSet = new HashSet<string>(other.Inputs);
|
||||
|
||||
var additions = myInputSet.Except(otherInputSet);
|
||||
var deletions = otherInputSet.Except(myInputSet);
|
||||
|
||||
return new DiffResult(additions, deletions);
|
||||
}
|
||||
|
||||
internal class DiffResult
|
||||
{
|
||||
public IEnumerable<string> Additions { get; private set; }
|
||||
public IEnumerable<string> Deletions { get; private set; }
|
||||
|
||||
public DiffResult(IEnumerable<string> additions, IEnumerable<string> deletions)
|
||||
{
|
||||
Additions = additions;
|
||||
Deletions = deletions;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -19,6 +20,7 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
private readonly string _buildBasePath;
|
||||
private readonly IList<string> _runtimes;
|
||||
private readonly WorkspaceContext _workspace;
|
||||
private readonly ConcurrentDictionary<ProjectContextIdentity, CompilerIO> _cache;
|
||||
|
||||
public CompilerIOManager(string configuration,
|
||||
string outputPath,
|
||||
|
@ -31,49 +33,36 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
_buildBasePath = buildBasePath;
|
||||
_runtimes = runtimes.ToList();
|
||||
_workspace = workspace;
|
||||
|
||||
_cache = new ConcurrentDictionary<ProjectContextIdentity, CompilerIO>();
|
||||
}
|
||||
|
||||
public 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($"Project {project.GetDisplayName()} will be compiled because expected {itemsType} are missing.");
|
||||
|
||||
foreach (var missing in missingItems)
|
||||
{
|
||||
Reporter.Verbose.WriteLine($" {missing}");
|
||||
}
|
||||
|
||||
Reporter.Verbose.WriteLine(); ;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 CompilerIO GetCompileIO(ProjectGraphNode graphNode)
|
||||
{
|
||||
return _cache.GetOrAdd(graphNode.ProjectContext.Identity, i => ComputeIO(graphNode));
|
||||
}
|
||||
|
||||
private CompilerIO ComputeIO(ProjectGraphNode graphNode)
|
||||
{
|
||||
var inputs = new List<string>();
|
||||
var outputs = new List<string>();
|
||||
|
||||
var isRootProject = graphNode.IsRoot;
|
||||
var project = graphNode.ProjectContext;
|
||||
|
||||
var compilerIO = new CompilerIO(new List<string>(), new List<string>());
|
||||
var calculator = project.GetOutputPaths(_configuration, _buildBasePath, _outputPath);
|
||||
var binariesOutputPath = calculator.CompilationOutputPath;
|
||||
|
||||
// input: project.json
|
||||
compilerIO.Inputs.Add(project.ProjectFile.ProjectFilePath);
|
||||
inputs.Add(project.ProjectFile.ProjectFilePath);
|
||||
|
||||
// input: lock file; find when dependencies change
|
||||
AddLockFile(project, compilerIO);
|
||||
AddLockFile(project, inputs);
|
||||
|
||||
// input: source files
|
||||
compilerIO.Inputs.AddRange(CompilerUtil.GetCompilationSources(project));
|
||||
inputs.AddRange(CompilerUtil.GetCompilationSources(project));
|
||||
|
||||
var allOutputPath = new HashSet<string>(calculator.CompilationFiles.All());
|
||||
if (isRootProject && project.ProjectFile.HasRuntimeOutput(_configuration))
|
||||
|
@ -88,23 +77,22 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
// output: compiler outputs
|
||||
foreach (var path in allOutputPath)
|
||||
{
|
||||
compilerIO.Outputs.Add(path);
|
||||
outputs.Add(path);
|
||||
}
|
||||
|
||||
// input compilation options files
|
||||
AddCompilationOptions(project, _configuration, compilerIO);
|
||||
AddCompilationOptions(project, _configuration, inputs);
|
||||
|
||||
// input / output: resources with culture
|
||||
AddNonCultureResources(project, calculator.IntermediateOutputDirectoryPath, compilerIO);
|
||||
AddNonCultureResources(project, calculator.IntermediateOutputDirectoryPath, inputs, outputs);
|
||||
|
||||
// input / output: resources without culture
|
||||
AddCultureResources(project, binariesOutputPath, compilerIO);
|
||||
AddCultureResources(project, binariesOutputPath, inputs, outputs);
|
||||
|
||||
return compilerIO;
|
||||
return new CompilerIO(inputs, outputs);
|
||||
}
|
||||
|
||||
|
||||
private static void AddLockFile(ProjectContext project, CompilerIO compilerIO)
|
||||
private static void AddLockFile(ProjectContext project, List<string> inputs)
|
||||
{
|
||||
if (project.LockFile == null)
|
||||
{
|
||||
|
@ -113,48 +101,48 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
compilerIO.Inputs.Add(project.LockFile.LockFilePath);
|
||||
inputs.Add(project.LockFile.LockFilePath);
|
||||
|
||||
if (project.LockFile.ExportFile != null)
|
||||
{
|
||||
compilerIO.Inputs.Add(project.LockFile.ExportFile.ExportFilePath);
|
||||
inputs.Add(project.LockFile.ExportFile.ExportFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void AddCompilationOptions(ProjectContext project, string config, CompilerIO compilerIO)
|
||||
private static void AddCompilationOptions(ProjectContext project, string config, List<string> inputs)
|
||||
{
|
||||
var compilerOptions = project.ResolveCompilationOptions(config);
|
||||
|
||||
// input: key file
|
||||
if (compilerOptions.KeyFile != null)
|
||||
{
|
||||
compilerIO.Inputs.Add(compilerOptions.KeyFile);
|
||||
inputs.Add(compilerOptions.KeyFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddNonCultureResources(ProjectContext project, string intermediaryOutputPath, CompilerIO compilerIO)
|
||||
private static void AddNonCultureResources(ProjectContext project, string intermediaryOutputPath, List<string> inputs, IList<string> outputs)
|
||||
{
|
||||
foreach (var resourceIO in CompilerUtil.GetNonCultureResources(project.ProjectFile, intermediaryOutputPath))
|
||||
{
|
||||
compilerIO.Inputs.Add(resourceIO.InputFile);
|
||||
inputs.Add(resourceIO.InputFile);
|
||||
|
||||
if (resourceIO.OutputFile != null)
|
||||
{
|
||||
compilerIO.Outputs.Add(resourceIO.OutputFile);
|
||||
outputs.Add(resourceIO.OutputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCultureResources(ProjectContext project, string outputPath, CompilerIO compilerIO)
|
||||
private static void AddCultureResources(ProjectContext project, string outputPath, List<string> inputs, List<string> outputs)
|
||||
{
|
||||
foreach (var cultureResourceIO in CompilerUtil.GetCultureResources(project.ProjectFile, outputPath))
|
||||
{
|
||||
compilerIO.Inputs.AddRange(cultureResourceIO.InputFileToMetadata.Keys);
|
||||
inputs.AddRange(cultureResourceIO.InputFileToMetadata.Keys);
|
||||
|
||||
if (cultureResourceIO.OutputFile != null)
|
||||
{
|
||||
compilerIO.Outputs.Add(cultureResourceIO.OutputFile);
|
||||
outputs.Add(cultureResourceIO.OutputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,14 +21,17 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
private readonly CompilerIOManager _compilerIOManager;
|
||||
private readonly ScriptRunner _scriptRunner;
|
||||
private readonly DotNetCommandFactory _commandFactory;
|
||||
private readonly IncrementalManager _incrementalManager;
|
||||
|
||||
public DotNetProjectBuilder(BuildCommandApp args)
|
||||
{
|
||||
_args = args;
|
||||
|
||||
_preconditionManager = new IncrementalPreconditionManager(
|
||||
args.ShouldPrintIncrementalPreconditions,
|
||||
args.ShouldNotUseIncrementality,
|
||||
args.ShouldSkipDependencies);
|
||||
|
||||
_compilerIOManager = new CompilerIOManager(
|
||||
args.ConfigValue,
|
||||
args.OutputValue,
|
||||
|
@ -36,7 +39,19 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
args.GetRuntimes(),
|
||||
args.Workspace
|
||||
);
|
||||
|
||||
_incrementalManager = new IncrementalManager(
|
||||
this,
|
||||
_compilerIOManager,
|
||||
_preconditionManager,
|
||||
_args.ShouldSkipDependencies,
|
||||
_args.ConfigValue,
|
||||
_args.BuildBasePathValue,
|
||||
_args.OutputValue
|
||||
);
|
||||
|
||||
_scriptRunner = new ScriptRunner();
|
||||
|
||||
_commandFactory = new DotNetCommandFactory();
|
||||
}
|
||||
|
||||
|
@ -52,7 +67,7 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
|
||||
string content = ComputeCurrentVersionFileData();
|
||||
string content = DotnetFiles.ReadAndInterpretVersionFile();
|
||||
|
||||
File.WriteAllText(projectVersionFile, content);
|
||||
}
|
||||
|
@ -62,14 +77,6 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
}
|
||||
}
|
||||
|
||||
private static string ComputeCurrentVersionFileData()
|
||||
{
|
||||
var content = File.ReadAllText(DotnetFiles.VersionFile);
|
||||
content += Environment.NewLine;
|
||||
content += PlatformServices.Default.Runtime.GetRuntimeIdentifier();
|
||||
return content;
|
||||
}
|
||||
|
||||
private void PrintSummary(ProjectGraphNode projectNode, 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
|
||||
|
@ -83,17 +90,6 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
Reporter.Output.WriteLine();
|
||||
}
|
||||
|
||||
private void CreateOutputDirectories()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_args.OutputValue))
|
||||
{
|
||||
Directory.CreateDirectory(_args.OutputValue);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(_args.BuildBasePathValue))
|
||||
{
|
||||
Directory.CreateDirectory(_args.BuildBasePathValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyCompilationOutput(OutputPaths outputPaths)
|
||||
{
|
||||
|
@ -151,118 +147,47 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
finally
|
||||
{
|
||||
StampProjectWithSDKVersion(projectNode.ProjectContext);
|
||||
_incrementalManager.CacheIncrementalState(projectNode);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ProjectSkiped(ProjectGraphNode projectNode)
|
||||
{
|
||||
StampProjectWithSDKVersion(projectNode.ProjectContext);
|
||||
}
|
||||
|
||||
private bool CLIChangedSinceLastCompilation(ProjectContext project)
|
||||
{
|
||||
var currentVersionFile = DotnetFiles.VersionFile;
|
||||
var versionFileFromLastCompile = project.GetSDKVersionFile(_args.ConfigValue, _args.BuildBasePathValue, _args.OutputValue);
|
||||
|
||||
if (!File.Exists(currentVersionFile))
|
||||
{
|
||||
// this CLI does not have a version file; cannot tell if CLI changed
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(versionFileFromLastCompile))
|
||||
{
|
||||
// this is the first compilation; cannot tell if CLI changed
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentContent = ComputeCurrentVersionFileData();
|
||||
|
||||
var versionsAreEqual = string.Equals(currentContent, File.ReadAllText(versionFileFromLastCompile), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return !versionsAreEqual;
|
||||
_incrementalManager.CacheIncrementalState(projectNode);
|
||||
}
|
||||
|
||||
protected override bool NeedsRebuilding(ProjectGraphNode graphNode)
|
||||
{
|
||||
var project = graphNode.ProjectContext;
|
||||
if (_args.ShouldNotUseIncrementality)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!_args.ShouldSkipDependencies &&
|
||||
graphNode.Dependencies.Any(d => GetCompilationResult(d) != CompilationResult.IncrementalSkip))
|
||||
{
|
||||
Reporter.Output.WriteLine($"Project {project.GetDisplayName()} will be compiled because some of it's dependencies changed");
|
||||
return true;
|
||||
}
|
||||
var preconditions = _preconditionManager.GetIncrementalPreconditions(graphNode);
|
||||
if (preconditions.PreconditionsDetected())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var result = _incrementalManager.NeedsRebuilding(graphNode);
|
||||
|
||||
if (CLIChangedSinceLastCompilation(project))
|
||||
PrintIncrementalResult(graphNode.ProjectContext.GetDisplayName(), result);
|
||||
|
||||
return result.NeedsRebuilding;
|
||||
}
|
||||
|
||||
private void PrintIncrementalResult(string projectName, IncrementalResult result)
|
||||
{
|
||||
if (result.NeedsRebuilding)
|
||||
{
|
||||
Reporter.Output.WriteLine($"Project {project.GetDisplayName()} will be compiled because the version or bitness of the CLI changed since the last build");
|
||||
return true;
|
||||
Reporter.Output.WriteLine($"Project {projectName} will be compiled because {result.Reason}");
|
||||
PrintIncrementalItems(result);
|
||||
}
|
||||
|
||||
var compilerIO = _compilerIOManager.GetCompileIO(graphNode);
|
||||
|
||||
// rebuild if empty inputs / outputs
|
||||
if (!(compilerIO.Outputs.Any() && compilerIO.Inputs.Any()))
|
||||
else
|
||||
{
|
||||
Reporter.Output.WriteLine($"Project {project.GetDisplayName()} will be compiled because it either has empty inputs or outputs");
|
||||
return true;
|
||||
Reporter.Output.WriteLine($"Project {projectName} was previously compiled. Skipping compilation.");
|
||||
}
|
||||
}
|
||||
|
||||
//rebuild if missing inputs / outputs
|
||||
if (_compilerIOManager.AnyMissingIO(project, compilerIO.Outputs, "outputs") || _compilerIOManager.AnyMissingIO(project, compilerIO.Inputs, "inputs"))
|
||||
private static void PrintIncrementalItems(IncrementalResult result)
|
||||
{
|
||||
if (Reporter.IsVerbose)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// find the output with the earliest write time
|
||||
var minOutputPath = compilerIO.Outputs.First();
|
||||
var minDateUtc = File.GetLastWriteTimeUtc(minOutputPath);
|
||||
|
||||
foreach (var outputPath in compilerIO.Outputs)
|
||||
{
|
||||
if (File.GetLastWriteTimeUtc(outputPath) >= minDateUtc)
|
||||
foreach (var item in result.Items)
|
||||
{
|
||||
continue;
|
||||
Reporter.Verbose.WriteLine($"\t{item}");
|
||||
}
|
||||
|
||||
minDateUtc = File.GetLastWriteTimeUtc(outputPath);
|
||||
minOutputPath = outputPath;
|
||||
}
|
||||
|
||||
// find inputs that are older than the earliest output
|
||||
var newInputs = compilerIO.Inputs.FindAll(p => File.GetLastWriteTimeUtc(p) >= minDateUtc);
|
||||
|
||||
if (!newInputs.Any())
|
||||
{
|
||||
Reporter.Output.WriteLine($"Project {project.GetDisplayName()} was previously compiled. Skipping compilation.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Reporter.Output.WriteLine($"Project {project.GetDisplayName()} will be compiled because some of its inputs were newer than its oldest output.");
|
||||
Reporter.Verbose.WriteLine();
|
||||
Reporter.Verbose.WriteLine($" Oldest output item:");
|
||||
Reporter.Verbose.WriteLine($" {minDateUtc.ToLocalTime()}: {minOutputPath}");
|
||||
Reporter.Verbose.WriteLine();
|
||||
|
||||
Reporter.Verbose.WriteLine($" Inputs newer than the oldest output item:");
|
||||
|
||||
foreach (var newInput in newInputs)
|
||||
{
|
||||
Reporter.Verbose.WriteLine($" {File.GetLastWriteTime(newInput)}: {newInput}");
|
||||
}
|
||||
|
||||
Reporter.Verbose.WriteLine();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
92
src/dotnet/commands/dotnet-build/IncrementalCache.cs
Normal file
92
src/dotnet/commands/dotnet-build/IncrementalCache.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
// 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 Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Build
|
||||
{
|
||||
internal class IncrementalCache
|
||||
{
|
||||
private const string InputsKeyName = "inputs";
|
||||
private const string OutputsKeyNane = "outputs";
|
||||
|
||||
public CompilerIO CompilerIO { get; }
|
||||
|
||||
public IncrementalCache(CompilerIO compilerIO)
|
||||
{
|
||||
CompilerIO = compilerIO;
|
||||
}
|
||||
|
||||
public void WriteToFile(string cacheFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
CreatePathIfAbsent(cacheFile);
|
||||
|
||||
using (var streamWriter = new StreamWriter(new FileStream(cacheFile, FileMode.Create, FileAccess.Write, FileShare.None)))
|
||||
{
|
||||
var rootObject = new JObject();
|
||||
rootObject[InputsKeyName] = new JArray(CompilerIO.Inputs);
|
||||
rootObject[OutputsKeyNane] = new JArray(CompilerIO.Outputs);
|
||||
|
||||
JsonSerializer.Create().Serialize(streamWriter, rootObject);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidDataException($"Could not write the incremental cache file: {cacheFile}", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreatePathIfAbsent(string filePath)
|
||||
{
|
||||
var parentDir = Path.GetDirectoryName(filePath);
|
||||
|
||||
if (!Directory.Exists(parentDir))
|
||||
{
|
||||
Directory.CreateDirectory(parentDir);
|
||||
}
|
||||
}
|
||||
|
||||
public static IncrementalCache ReadFromFile(string cacheFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var streamReader = new StreamReader(new FileStream(cacheFile, FileMode.Open, FileAccess.Read, FileShare.Read)))
|
||||
{
|
||||
var jObject = JObject.Parse(streamReader.ReadToEnd());
|
||||
|
||||
if (jObject == null)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
var inputs = ReadArray<string>(jObject, InputsKeyName);
|
||||
var outputs = ReadArray<string>(jObject, OutputsKeyNane);
|
||||
|
||||
return new IncrementalCache(new CompilerIO(inputs, outputs));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidDataException($"Could not read the incremental cache file: {cacheFile}", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<T> ReadArray<T>(JObject jObject, string keyName)
|
||||
{
|
||||
var array = jObject.Value<JToken>(keyName)?.Values<T>();
|
||||
|
||||
if (array == null)
|
||||
{
|
||||
throw new InvalidDataException($"Could not read key {keyName}");
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
||||
}
|
207
src/dotnet/commands/dotnet-build/IncrementalManager.cs
Normal file
207
src/dotnet/commands/dotnet-build/IncrementalManager.cs
Normal file
|
@ -0,0 +1,207 @@
|
|||
// 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 System.Linq;
|
||||
using Microsoft.DotNet.Cli.Compiler.Common;
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
using Microsoft.DotNet.ProjectModel;
|
||||
using Microsoft.DotNet.ProjectModel.Utilities;
|
||||
using Microsoft.DotNet.Tools.Compiler;
|
||||
using Microsoft.Extensions.PlatformAbstractions;
|
||||
using Microsoft.DotNet.ProjectModel.Compilation;
|
||||
using NuGet.Protocol.Core.Types;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Build
|
||||
{
|
||||
internal class IncrementalManager
|
||||
{
|
||||
private readonly ProjectBuilder _projectBuilder;
|
||||
private readonly CompilerIOManager _compilerIoManager;
|
||||
private readonly IncrementalPreconditionManager _preconditionManager;
|
||||
private readonly bool _shouldSkipDependencies;
|
||||
private readonly string _configuration;
|
||||
private readonly string _buildBasePath;
|
||||
private readonly string _outputPath;
|
||||
|
||||
public IncrementalManager(
|
||||
ProjectBuilder projectBuilder,
|
||||
CompilerIOManager compilerIOManager,
|
||||
IncrementalPreconditionManager incrementalPreconditionManager,
|
||||
bool shouldSkipDependencies,
|
||||
string configuration,
|
||||
string buildBasePath,
|
||||
string outputPath)
|
||||
{
|
||||
_projectBuilder = projectBuilder;
|
||||
_compilerIoManager = compilerIOManager;
|
||||
_preconditionManager = incrementalPreconditionManager;
|
||||
_shouldSkipDependencies = shouldSkipDependencies;
|
||||
_configuration = configuration;
|
||||
_buildBasePath = buildBasePath;
|
||||
_outputPath = outputPath;
|
||||
}
|
||||
|
||||
public IncrementalResult NeedsRebuilding(ProjectGraphNode graphNode)
|
||||
{
|
||||
if (!_shouldSkipDependencies &&
|
||||
graphNode.Dependencies.Any(d => _projectBuilder.GetCompilationResult(d) != CompilationResult.IncrementalSkip))
|
||||
{
|
||||
return new IncrementalResult("dependencies changed");
|
||||
}
|
||||
|
||||
var preconditions = _preconditionManager.GetIncrementalPreconditions(graphNode);
|
||||
if (preconditions.PreconditionsDetected())
|
||||
{
|
||||
return new IncrementalResult($"project is not safe for incremental compilation. Use {BuildCommandApp.BuildProfileFlag} flag for more information.");
|
||||
}
|
||||
|
||||
var compilerIO = _compilerIoManager.GetCompileIO(graphNode);
|
||||
|
||||
var result = CLIChanged(graphNode);
|
||||
if (result.NeedsRebuilding)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = InputItemsChanged(graphNode, compilerIO);
|
||||
if (result.NeedsRebuilding)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = TimestampsChanged(compilerIO);
|
||||
if (result.NeedsRebuilding)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return IncrementalResult.DoesNotNeedRebuild;
|
||||
}
|
||||
|
||||
private IncrementalResult CLIChanged(ProjectGraphNode graphNode)
|
||||
{
|
||||
var currentVersionFile = DotnetFiles.VersionFile;
|
||||
var versionFileFromLastCompile = graphNode.ProjectContext.GetSDKVersionFile(_configuration, _buildBasePath, _outputPath);
|
||||
|
||||
if (!File.Exists(currentVersionFile))
|
||||
{
|
||||
// this CLI does not have a version file; cannot tell if CLI changed
|
||||
return IncrementalResult.DoesNotNeedRebuild;
|
||||
}
|
||||
|
||||
if (!File.Exists(versionFileFromLastCompile))
|
||||
{
|
||||
// this is the first compilation; cannot tell if CLI changed
|
||||
return IncrementalResult.DoesNotNeedRebuild;
|
||||
}
|
||||
|
||||
var currentContent = DotnetFiles.ReadAndInterpretVersionFile();
|
||||
|
||||
var versionsAreEqual = string.Equals(currentContent, File.ReadAllText(versionFileFromLastCompile), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return versionsAreEqual
|
||||
? IncrementalResult.DoesNotNeedRebuild
|
||||
: new IncrementalResult("the version or bitness of the CLI changed since the last build");
|
||||
}
|
||||
|
||||
private IncrementalResult InputItemsChanged(ProjectGraphNode graphNode, CompilerIO compilerIO)
|
||||
{
|
||||
// check empty inputs / outputs
|
||||
if (!compilerIO.Inputs.Any())
|
||||
{
|
||||
return new IncrementalResult("the project has no inputs");
|
||||
}
|
||||
|
||||
if (!compilerIO.Outputs.Any())
|
||||
{
|
||||
return new IncrementalResult("the project has no outputs");
|
||||
}
|
||||
|
||||
// check non existent items
|
||||
var result = CheckMissingIO(compilerIO.Inputs, "inputs");
|
||||
if (result.NeedsRebuilding)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = CheckMissingIO(compilerIO.Outputs, "outputs");
|
||||
if (result.NeedsRebuilding)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return CheckInputGlobChanges(graphNode, compilerIO);
|
||||
}
|
||||
|
||||
private IncrementalResult CheckInputGlobChanges(ProjectGraphNode graphNode, CompilerIO compilerIO)
|
||||
{
|
||||
// check cache against input glob pattern changes
|
||||
var incrementalCacheFile = graphNode.ProjectContext.IncrementalCacheFile(_configuration, _buildBasePath, _outputPath);
|
||||
|
||||
if (!File.Exists(incrementalCacheFile))
|
||||
{
|
||||
// cache is not present (first compilation); can't determine if globs changed; cache will be generated after build processes project
|
||||
return IncrementalResult.DoesNotNeedRebuild;
|
||||
}
|
||||
|
||||
var incrementalCache = IncrementalCache.ReadFromFile(incrementalCacheFile);
|
||||
|
||||
var diffResult = compilerIO.DiffInputs(incrementalCache.CompilerIO);
|
||||
|
||||
if (diffResult.Deletions.Any())
|
||||
{
|
||||
return new IncrementalResult("Input items removed from last build", diffResult.Deletions);
|
||||
}
|
||||
|
||||
if (diffResult.Additions.Any())
|
||||
{
|
||||
return new IncrementalResult("Input items added from last build", diffResult.Additions);
|
||||
}
|
||||
|
||||
return IncrementalResult.DoesNotNeedRebuild;
|
||||
}
|
||||
|
||||
private IncrementalResult CheckMissingIO(IEnumerable<string> items, string itemsType)
|
||||
{
|
||||
var missingItems = items.Where(i => !File.Exists(i)).ToList();
|
||||
|
||||
return missingItems.Any()
|
||||
? new IncrementalResult($"expected {itemsType} are missing", missingItems)
|
||||
: IncrementalResult.DoesNotNeedRebuild;
|
||||
}
|
||||
|
||||
private IncrementalResult TimestampsChanged(CompilerIO compilerIO)
|
||||
{
|
||||
// find the output with the earliest write time
|
||||
var minDateUtc = DateTime.MaxValue;
|
||||
|
||||
foreach (var outputPath in compilerIO.Outputs)
|
||||
{
|
||||
var lastWriteTimeUtc = File.GetLastWriteTimeUtc(outputPath);
|
||||
|
||||
if (lastWriteTimeUtc < minDateUtc)
|
||||
{
|
||||
minDateUtc = lastWriteTimeUtc;
|
||||
}
|
||||
}
|
||||
|
||||
// find inputs that are older than the earliest output
|
||||
var newInputs = compilerIO.Inputs.Where(p => File.GetLastWriteTimeUtc(p) >= minDateUtc);
|
||||
|
||||
return newInputs.Any()
|
||||
? new IncrementalResult("inputs were modified", newInputs)
|
||||
: IncrementalResult.DoesNotNeedRebuild;
|
||||
}
|
||||
|
||||
public void CacheIncrementalState(ProjectGraphNode graphNode)
|
||||
{
|
||||
var incrementalCacheFile = graphNode.ProjectContext.IncrementalCacheFile(_configuration, _buildBasePath, _outputPath);
|
||||
|
||||
var incrementalCache = new IncrementalCache(_compilerIoManager.GetCompileIO(graphNode));
|
||||
incrementalCache.WriteToFile(incrementalCacheFile);
|
||||
}
|
||||
}
|
||||
}
|
34
src/dotnet/commands/dotnet-build/IncrementalResult.cs
Normal file
34
src/dotnet/commands/dotnet-build/IncrementalResult.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Build
|
||||
{
|
||||
internal class IncrementalResult
|
||||
{
|
||||
public static readonly IncrementalResult DoesNotNeedRebuild = new IncrementalResult(false, "", Enumerable.Empty<string>());
|
||||
|
||||
public bool NeedsRebuilding { get; }
|
||||
public string Reason { get; }
|
||||
public IEnumerable<string> Items { get; }
|
||||
|
||||
private IncrementalResult(bool needsRebuilding, string reason, IEnumerable<string> items)
|
||||
{
|
||||
NeedsRebuilding = needsRebuilding;
|
||||
Reason = reason;
|
||||
Items = items;
|
||||
}
|
||||
|
||||
public IncrementalResult(string reason)
|
||||
: this(true, reason, Enumerable.Empty<string>())
|
||||
{
|
||||
}
|
||||
|
||||
public IncrementalResult(string reason, IEnumerable<string> items)
|
||||
: this(true, reason, items)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ namespace Microsoft.DotNet.Tools.Build
|
|||
}
|
||||
}
|
||||
|
||||
protected CompilationResult? GetCompilationResult(ProjectGraphNode projectNode)
|
||||
public CompilationResult? GetCompilationResult(ProjectGraphNode projectNode)
|
||||
{
|
||||
CompilationResult result;
|
||||
if (_compilationResults.TryGetValue(projectNode.ProjectContext.Identity, out result))
|
||||
|
|
|
@ -38,6 +38,7 @@ namespace Microsoft.DotNet.Tools.Builder.Tests
|
|||
buildResult.Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
buildResult = BuildProject(noIncremental: true);
|
||||
buildResult.Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
Assert.Contains("[Forced Unsafe]", buildResult.StdOut);
|
||||
}
|
||||
|
||||
|
@ -85,12 +86,12 @@ namespace Microsoft.DotNet.Tools.Builder.Tests
|
|||
CreateTestInstance();
|
||||
BuildProject().Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
//change version file
|
||||
// change version file
|
||||
var versionFile = Path.Combine(GetIntermediaryOutputPath(), ".SDKVersion");
|
||||
File.Exists(versionFile).Should().BeTrue();
|
||||
File.AppendAllText(versionFile, "text");
|
||||
|
||||
//assert rebuilt
|
||||
// assert rebuilt
|
||||
BuildProject().Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
}
|
||||
|
||||
|
@ -100,19 +101,80 @@ namespace Microsoft.DotNet.Tools.Builder.Tests
|
|||
CreateTestInstance();
|
||||
BuildProject().Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
//delete version file
|
||||
// delete version file
|
||||
var versionFile = Path.Combine(GetIntermediaryOutputPath(), ".SDKVersion");
|
||||
File.Exists(versionFile).Should().BeTrue();
|
||||
File.Delete(versionFile);
|
||||
File.Exists(versionFile).Should().BeFalse();
|
||||
|
||||
//assert build skipped due to no version file
|
||||
// assert build skipped due to no version file
|
||||
BuildProject().Should().HaveSkippedProjectCompilation(MainProject, _appFrameworkFullName);
|
||||
|
||||
//the version file should have been regenerated during the build, even if compilation got skipped
|
||||
// the version file should have been regenerated during the build, even if compilation got skipped
|
||||
File.Exists(versionFile).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestRebuildDeletedSource()
|
||||
{
|
||||
CreateTestInstance();
|
||||
BuildProject().Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
var sourceFile = Path.Combine(GetProjectDirectory(MainProject), "Program2.cs");
|
||||
File.Delete(sourceFile);
|
||||
Assert.False(File.Exists(sourceFile));
|
||||
|
||||
// second build; should get rebuilt since we deleted a source file
|
||||
BuildProject().Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
// third build; incremental cache should have been regenerated and project skipped
|
||||
BuildProject().Should().HaveSkippedProjectCompilation(MainProject, _appFrameworkFullName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestRebuildRenamedSource()
|
||||
{
|
||||
CreateTestInstance();
|
||||
var buildResult = BuildProject();
|
||||
buildResult.Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
var sourceFile = Path.Combine(GetProjectDirectory(MainProject), "Program2.cs");
|
||||
var destinationFile = Path.Combine(Path.GetDirectoryName(sourceFile), "ProgramNew.cs");
|
||||
File.Move(sourceFile, destinationFile);
|
||||
Assert.False(File.Exists(sourceFile));
|
||||
Assert.True(File.Exists(destinationFile));
|
||||
|
||||
// second build; should get rebuilt since we renamed a source file
|
||||
buildResult = BuildProject();
|
||||
buildResult.Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
// third build; incremental cache should have been regenerated and project skipped
|
||||
BuildProject().Should().HaveSkippedProjectCompilation(MainProject, _appFrameworkFullName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestRebuildDeletedSourceAfterCliChanged()
|
||||
{
|
||||
CreateTestInstance();
|
||||
BuildProject().Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
// change version file
|
||||
var versionFile = Path.Combine(GetIntermediaryOutputPath(), ".SDKVersion");
|
||||
File.Exists(versionFile).Should().BeTrue();
|
||||
File.AppendAllText(versionFile, "text");
|
||||
|
||||
// delete a source file
|
||||
var sourceFile = Path.Combine(GetProjectDirectory(MainProject), "Program2.cs");
|
||||
File.Delete(sourceFile);
|
||||
Assert.False(File.Exists(sourceFile));
|
||||
|
||||
// should get rebuilt since we changed version file and deleted source file
|
||||
BuildProject().Should().HaveCompiledProject(MainProject, _appFrameworkFullName);
|
||||
|
||||
// third build; incremental cache should have been regenerated and project skipped
|
||||
BuildProject().Should().HaveSkippedProjectCompilation(MainProject, _appFrameworkFullName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestRebuildChangedLockFile()
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue