diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj index 9fa046c5ac5..756260c60ee 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj @@ -43,6 +43,9 @@ ResolvedAnalyzerReference.xaml + + ResolvedCompilationReference.xaml + ResolvedPackageReference.xaml @@ -91,6 +94,9 @@ UpToDateCheckOutput.xaml + + CopyUpToDateMarker.xaml + Folder.xaml @@ -168,6 +174,10 @@ MSBuild:GenerateRuleSourceFromXaml Designer + + MSBuild:GenerateRuleSourceFromXaml + Designer + MSBuild:GenerateRuleSourceFromXaml Designer @@ -280,6 +290,10 @@ MSBuild:GenerateRuleSourceFromXaml Designer + + MSBuild:GenerateRuleSourceFromXaml + Designer + MSBuild:GenerateRuleSourceFromXaml Designer diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/BuildUpToDateCheck.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/BuildUpToDateCheck.cs index de76b2f7a3f..e689d1d8627 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/BuildUpToDateCheck.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/BuildUpToDateCheck.cs @@ -48,6 +48,7 @@ private void Log(LogLevel level, string message, params object[] values) private const string ResolvedPath = "ResolvedPath"; private const string CopyToOutputDirectory = "CopyToOutputDirectory"; private const string Never = "Never"; + private const string OriginalPath = "OriginalPath"; private static HashSet KnownOutputGroups = new HashSet { @@ -60,13 +61,17 @@ private void Log(LogLevel level, string message, params object[] values) private static ImmutableHashSet ReferenceSchemas => ImmutableHashSet.Empty .Add(ResolvedAnalyzerReference.SchemaName) - .Add(ResolvedAssemblyReference.SchemaName) - .Add(ResolvedCOMReference.SchemaName) - .Add(ResolvedProjectReference.SchemaName); + .Add(ResolvedCompilationReference.SchemaName); + + private static ImmutableHashSet UpToDateSchemas => ImmutableHashSet.Empty + .Add(CopyUpToDateMarker.SchemaName) + .Add(UpToDateCheckInput.SchemaName) + .Add(UpToDateCheckOutput.SchemaName); private static ImmutableHashSet ProjectPropertiesSchemas => ImmutableHashSet.Empty .Add(ConfigurationGeneral.SchemaName) - .Union(ReferenceSchemas); + .Union(ReferenceSchemas) + .Union(UpToDateSchemas); private readonly IProjectSystemOptions _projectSystemOptions; private readonly ConfiguredProject _configuredProject; @@ -79,11 +84,15 @@ private void Log(LogLevel level, string message, params object[] values) private bool _isDisabled = true; private bool _itemsChangedSinceLastCheck = true; private string _msBuildProjectFullPath; + private string _markerFile; private HashSet _imports = new HashSet(); private HashSet _itemTypes = new HashSet(); private Dictionary> _items = new Dictionary>(); - private Dictionary> _references = new Dictionary>(); + private HashSet _customInputs = new HashSet(); private HashSet _customOutputs = new HashSet(); + private HashSet _analyzerReferences = new HashSet(); + private HashSet _compilationReferences = new HashSet(); + private HashSet _copyReferenceInputs = new HashSet(); private Dictionary> _outputGroups = new Dictionary>(); [ImportingConstructor] @@ -117,14 +126,50 @@ private void OnProjectChanged(IProjectSubscriptionUpdate e) _isDisabled = disableFastUpToDateCheckString != null && string.Equals(disableFastUpToDateCheckString, TrueValue, StringComparison.OrdinalIgnoreCase); _msBuildProjectFullPath = e.CurrentState.GetPropertyOrDefault(ConfigurationGeneral.SchemaName, ConfigurationGeneral.MSBuildProjectFullPathProperty, _msBuildProjectFullPath); - foreach (var referenceSchema in ReferenceSchemas) + + if (e.ProjectChanges.TryGetValue(ResolvedAnalyzerReference.SchemaName, out var changes) && + changes.Difference.AnyChanges) { - if (e.ProjectChanges.TryGetValue(referenceSchema, out var changes) && - changes.Difference.AnyChanges) + _analyzerReferences = new HashSet(changes.After.Items.Select(item => item.Value[ResolvedPath])); + } + + if (e.ProjectChanges.TryGetValue(ResolvedCompilationReference.SchemaName, out changes) && + changes.Difference.AnyChanges) + { + _compilationReferences.Clear(); + _copyReferenceInputs.Clear(); + + foreach (var item in changes.After.Items) { - _references[referenceSchema] = new HashSet(changes.After.Items.Select(item => item.Value[ResolvedPath])); + _compilationReferences.Add(item.Value[ResolvedPath]); + if (!string.IsNullOrWhiteSpace(item.Value[CopyUpToDateMarker.SchemaName])) + { + _copyReferenceInputs.Add(item.Value[CopyUpToDateMarker.SchemaName]); + } + if (!string.IsNullOrWhiteSpace(item.Value[OriginalPath])) + { + _copyReferenceInputs.Add(item.Value[OriginalPath]); + } } } + + if (e.ProjectChanges.TryGetValue(UpToDateCheckInput.SchemaName, out var inputs) && + inputs.Difference.AnyChanges) + { + _customInputs = new HashSet(inputs.After.Items.Select(item => item.Value[FullPath])); + } + + if (e.ProjectChanges.TryGetValue(UpToDateCheckOutput.SchemaName, out var outputs) && + outputs.Difference.AnyChanges) + { + _customOutputs = new HashSet(outputs.After.Items.Select(item => item.Value[FullPath])); + } + + if (e.ProjectChanges.TryGetValue(CopyUpToDateMarker.SchemaName, out var upToDateMarkers) && + upToDateMarkers.Difference.AnyChanges) + { + _markerFile = upToDateMarkers.After.Items.Count == 1 ? upToDateMarkers.After.Items.Single().Value[FullPath] : null; + } } private void OnProjectImportsChanged(IProjectImportTreeSnapshot e) @@ -161,12 +206,6 @@ private void OnSourceItemChanged(IProjectSubscriptionUpdate e, IProjectItemSchem _items[itemType.Key] = new HashSet(items); _itemsChangedSinceLastCheck = true; } - - if (e.ProjectChanges.TryGetValue(UpToDateCheckOutput.SchemaName, out var outputs) && - outputs.Difference.AnyChanges) - { - _customOutputs = new HashSet(outputs.After.Items.Select(item => item.Value[FullPath])); - } } private void OnOutputGroupChanged(IImmutableDictionary e) @@ -289,10 +328,9 @@ private HashSet CollectInputs(Logger logger) AddInputs(logger, inputs, pair.Value, pair.Key); } - foreach (var pair in _references) - { - AddInputs(logger, inputs, pair.Value, pair.Key); - } + AddInputs(logger, inputs, _analyzerReferences, ResolvedAnalyzerReference.SchemaName); + AddInputs(logger, inputs, _compilationReferences, ResolvedCompilationReference.SchemaName); + AddInputs(logger, inputs, _customInputs, UpToDateCheckInput.SchemaName); return inputs; } @@ -306,12 +344,12 @@ private HashSet CollectOutputs(Logger logger) AddOutputs(logger, outputs, pair.Value, pair.Key); } - AddOutputs(logger, outputs, _customOutputs, "UpToDateCheckOutput"); + AddOutputs(logger, outputs, _customOutputs, UpToDateCheckOutput.SchemaName); return outputs; } - private (DateTime? time, string path) GetLatestInput(HashSet inputs, IDictionary timestampCache) + private (DateTime? time, string path) GetLatestInput(HashSet inputs, IDictionary timestampCache, bool ignoreMissing = false) { DateTime? latest = DateTime.MinValue; string latestPath = null; @@ -319,7 +357,7 @@ private HashSet CollectOutputs(Logger logger) foreach (var input in inputs) { var time = GetTimestamp(input, timestampCache); - if (latest != null && (time == null || time > latest)) + if (latest != null && (time == null && !ignoreMissing || time > latest)) { latest = time; latestPath = input; @@ -347,6 +385,50 @@ private HashSet CollectOutputs(Logger logger) return (earliest, earliestPath); } + // Reference assembly copy markers are strange. The property is always going to be present on + // references to SDK-based projects, regardless of whether or not those referenced projects + // will actually produce a marker. And an item always will be present in an SDK-based project, + // regardless of whether or not the project produces a marker. So, basically, we only check + // here if the project actually produced a marker and we only check it against references that + // actually produced a marker. + private bool CheckMarkers(Logger logger, IDictionary timestampCache) + { + if (string.IsNullOrWhiteSpace(_markerFile) || !_copyReferenceInputs.Any()) + { + return true; + } + + foreach (var referenceMarkerFile in _copyReferenceInputs) + { + logger.Verbose("Found possible input marker '{0}'.", referenceMarkerFile); + } + + logger.Verbose("Found possible output marker '{0}'.", _markerFile); + + var latestInputMarker = GetLatestInput(_copyReferenceInputs, timestampCache, true); + var outputMarkerTime = GetTimestamp(_markerFile, timestampCache); + + if (latestInputMarker.path != null) + { + logger.Info("Latest write timestamp on input marker is {0} on '{1}'.", latestInputMarker.time.Value, latestInputMarker.path); + } + else + { + logger.Info("No input markers exist, skipping marker check."); + } + + if (outputMarkerTime != null) + { + logger.Info("Write timestamp on output marker is {0} on '{1}'.", outputMarkerTime, _markerFile); + } + else + { + logger.Info("Output marker '{0}' does not exist, skipping marker check.", _markerFile); + } + + return latestInputMarker.path == null || outputMarkerTime == null || outputMarkerTime > latestInputMarker.time; + } + public async Task IsUpToDateAsync(BuildAction buildAction, TextWriter logWriter, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -368,7 +450,7 @@ private HashSet CollectOutputs(Logger logger) if (latestInput.time != null) { - logger.Info("Lastest write timestamp on input is {0} on '{1}'.", latestInput.time.Value, latestInput.path); + logger.Info("Latest write timestamp on input is {0} on '{1}'.", latestInput.time.Value, latestInput.path); } else { @@ -385,7 +467,13 @@ private HashSet CollectOutputs(Logger logger) } // We are up to date if the earliest output write happened after the latest input write - var isUpToDate = latestInput.time != null && earliestOutput.time != null && earliestOutput.time > latestInput.time; + var markersUpToDate = CheckMarkers(logger, timestampCache); + var isUpToDate = + latestInput.time != null + && earliestOutput.time != null + && earliestOutput.time > latestInput.time + && markersUpToDate; + logger.Info("Project is{0} up to date.", (!isUpToDate ? " not" : "")); return isUpToDate; diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/CopyUpToDateMarker.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/CopyUpToDateMarker.cs new file mode 100644 index 00000000000..d5deeec97e8 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/CopyUpToDateMarker.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.VisualStudio.ProjectSystem +{ + [ExcludeFromCodeCoverage] + [SuppressMessage("Style", "IDE0016:Use 'throw' expression")] + partial class CopyUpToDateMarker + { + } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/CopyUpToDateMarker.xaml b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/CopyUpToDateMarker.xaml new file mode 100644 index 00000000000..7666a2d590c --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/CopyUpToDateMarker.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ProjectItemsSchema.xaml b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ProjectItemsSchema.xaml index 1c4600a8474..778f5f27eea 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ProjectItemsSchema.xaml +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ProjectItemsSchema.xaml @@ -58,23 +58,9 @@ - - - - - - - - diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ResolvedCompilationReference.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ResolvedCompilationReference.cs new file mode 100644 index 00000000000..485904ab74e --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ResolvedCompilationReference.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.VisualStudio.ProjectSystem +{ + [ExcludeFromCodeCoverage] + [SuppressMessage("Style", "IDE0016:Use 'throw' expression")] + partial class ResolvedCompilationReference + { + } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ResolvedCompilationReference.xaml b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ResolvedCompilationReference.xaml new file mode 100644 index 00000000000..a01b05d683b --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/ResolvedCompilationReference.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/src/Targets/Microsoft.Managed.DesignTime.targets b/src/Targets/Microsoft.Managed.DesignTime.targets index 874431234d7..6c825e74dde 100644 --- a/src/Targets/Microsoft.Managed.DesignTime.targets +++ b/src/Targets/Microsoft.Managed.DesignTime.targets @@ -135,6 +135,10 @@ File + + File + + File;ProjectSubscriptionService @@ -240,6 +244,10 @@ ProjectSubscriptionService;BrowseObject + + + ProjectSubscriptionService + @@ -266,5 +274,8 @@ + + + \ No newline at end of file