Merge pull request #2619 from cdmihai/cdmihai/globChangeDetectionRelMerge

Cache inputs for globbing change detection
This commit is contained in:
Mihai Codoban 2016-04-25 09:32:24 -07:00
commit 91fb67835d
13 changed files with 541 additions and 167 deletions

View file

@ -0,0 +1,12 @@
using System;
namespace ConsoleApplication
{
public class Program2
{
public static void Foo(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;
}
}
}

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

View 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)
{
}
}
}

View file

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

View file

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