| File: Watch\BuildEvaluator.cs | Web Access |
| Project: src\sdk\src\Dotnet.Watch\dotnet-watch\dotnet-watch.csproj (dotnet-watch) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; internal class BuildEvaluator { // File types that require an MSBuild re-evaluation private static readonly string[] s_msBuildFileExtensions = new[] { ".csproj", ".props", ".targets", ".fsproj", ".vbproj", ".vcxproj", }; private static readonly int[] s_msBuildFileExtensionHashes = s_msBuildFileExtensions .Select(e => e.GetHashCode(StringComparison.OrdinalIgnoreCase)) .ToArray(); private readonly MSBuildFileSetFactory _fileSetFactory; private readonly DotNetWatchContext _context; private List<(string fileName, DateTime lastWriteTimeUtc)>? _msbuildFileTimestamps; // result of the last evaluation, or null if no evaluation has been performed yet. private MSBuildFileSetFactory.EvaluationResult? _evaluationResult; public bool RequiresRevaluation { get; set; } public BuildEvaluator(DotNetWatchContext context) { Debug.Assert(context.MainProjectOptions != null); _context = context; _fileSetFactory = CreateMSBuildFileSetFactory(); } private ProjectOptions MainProjectOptions => _context.MainProjectOptions ?? throw new InvalidOperationException(); protected virtual MSBuildFileSetFactory CreateMSBuildFileSetFactory() { // file-based programs only supported in Hot Reload mode: Debug.Assert(MainProjectOptions.Representation.PhysicalPath != null); return new( MainProjectOptions.Representation.PhysicalPath, MainProjectOptions.TargetFramework, _context.BuildArguments, _context.ProcessRunner, _context.BuildLogger, _context.Options, _context.EnvironmentOptions); } public IReadOnlyList<string> GetProcessArguments(int iteration) { var noRestore = false; if (!_context.EnvironmentOptions.SuppressMSBuildIncrementalism && iteration > 0 && MainProjectOptions.IsCodeExecutionCommand) { if (RequiresRevaluation) { _context.Logger.LogDebug("Cannot use --no-restore since msbuild project files have changed."); } else { _context.Logger.LogDebug("Modifying command to use --no-restore"); noRestore = true; } } var arguments = new List<string>() { MainProjectOptions.Command }; if (noRestore) { arguments.Add("--no-restore"); } if (MainProjectOptions.TargetFramework != null) { arguments.Add("--framework"); arguments.Add(MainProjectOptions.TargetFramework); } if (MainProjectOptions.Device != null) { arguments.Add("--device"); arguments.Add(MainProjectOptions.Device); } arguments.AddRange(MainProjectOptions.CommandArguments); return arguments; } public async ValueTask<MSBuildFileSetFactory.EvaluationResult> EvaluateAsync(ChangedFile? changedFile, CancellationToken cancellationToken) { if (_context.EnvironmentOptions.SuppressMSBuildIncrementalism) { RequiresRevaluation = true; return _evaluationResult = await CreateEvaluationResult(cancellationToken); } if (_evaluationResult == null || RequiresMSBuildRevaluation(changedFile?.Item)) { RequiresRevaluation = true; } if (RequiresRevaluation) { _context.Logger.LogDebug("Evaluating dotnet-watch file set."); var result = await CreateEvaluationResult(cancellationToken); _msbuildFileTimestamps = GetMSBuildFileTimeStamps(result); return _evaluationResult = result; } Debug.Assert(_evaluationResult != null); return _evaluationResult; } private async ValueTask<MSBuildFileSetFactory.EvaluationResult> CreateEvaluationResult(CancellationToken cancellationToken) { while (true) { cancellationToken.ThrowIfCancellationRequested(); var result = await _fileSetFactory.TryCreateAsync(requireProjectGraph: true, cancellationToken); if (result != null) { return result; } await FileWatcher.WaitForFileChangeAsync( [_fileSetFactory.RootProjectFile], _context.Logger, _context.EnvironmentOptions, startedWatching: () => _context.Logger.Log(MessageDescriptor.FixBuildError), cancellationToken); } } private bool RequiresMSBuildRevaluation(FileItem? changedFile) { Debug.Assert(_msbuildFileTimestamps != null); if (changedFile != null && IsMsBuildFileExtension(changedFile.Value.FilePath)) { return true; } // The filewatcher may miss changes to files. For msbuild files, we can verify that they haven't been modified // since the previous iteration. // We do not have a way to identify renames or new additions that the file watcher did not pick up, // without performing an evaluation. We will start off by keeping it simple and comparing the timestamps // of known MSBuild files from previous run. This should cover the vast majority of cases. foreach (var (file, lastWriteTimeUtc) in _msbuildFileTimestamps) { if (GetLastWriteTimeUtcSafely(file) != lastWriteTimeUtc) { _context.Logger.LogDebug("Re-evaluation needed due to changes in '{Path}'.", file); return true; } } return false; } private List<(string fileName, DateTime lastModifiedUtc)> GetMSBuildFileTimeStamps(MSBuildFileSetFactory.EvaluationResult result) { var msbuildFiles = new List<(string fileName, DateTime lastModifiedUtc)>(); foreach (var (filePath, _) in result.Files) { if (!string.IsNullOrEmpty(filePath) && IsMsBuildFileExtension(filePath)) { msbuildFiles.Add((filePath, GetLastWriteTimeUtcSafely(filePath))); } } return msbuildFiles; } protected virtual DateTime GetLastWriteTimeUtcSafely(string file) { try { return File.GetLastWriteTimeUtc(file); } catch { return DateTime.UtcNow; } } private static bool IsMsBuildFileExtension(string fileName) { var extension = Path.GetExtension(fileName.AsSpan()); var hashCode = string.GetHashCode(extension, StringComparison.OrdinalIgnoreCase); for (var i = 0; i < s_msBuildFileExtensionHashes.Length; i++) { if (s_msBuildFileExtensionHashes[i] == hashCode && extension.Equals(s_msBuildFileExtensions[i], StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } }