diff --git a/ref/Microsoft.Build/net/Microsoft.Build.cs b/ref/Microsoft.Build/net/Microsoft.Build.cs index e52adc709ef..5c8595965da 100644 --- a/ref/Microsoft.Build/net/Microsoft.Build.cs +++ b/ref/Microsoft.Build/net/Microsoft.Build.cs @@ -1095,6 +1095,7 @@ public ProjectInstance(string projectFile, System.Collections.Generic.IDictionar public ProjectInstance(string projectFile, System.Collections.Generic.IDictionary globalProperties, string toolsVersion, string subToolsetVersion, Microsoft.Build.Evaluation.ProjectCollection projectCollection) { } public System.Collections.Generic.List DefaultTargets { get { throw null; } } public string Directory { get { throw null; } } + public System.Collections.Generic.ICollection EnvironmentVariableReads { get { throw null; } } public System.Collections.Generic.List EvaluatedItemElements { get { throw null; } } public int EvaluationId { get { throw null; } set { } } public string FullPath { get { throw null; } } @@ -1109,6 +1110,7 @@ public ProjectInstance(string projectFile, System.Collections.Generic.IDictionar public System.Collections.Generic.IDictionary Targets { get { throw null; } } public string ToolsVersion { get { throw null; } } public bool TranslateEntireState { get { throw null; } set { } } + public System.Collections.Generic.ICollection UninitializedPropertyReads { get { throw null; } } public Microsoft.Build.Execution.ProjectItemInstance AddItem(string itemType, string evaluatedInclude) { throw null; } public Microsoft.Build.Execution.ProjectItemInstance AddItem(string itemType, string evaluatedInclude, System.Collections.Generic.IEnumerable> metadata) { throw null; } public bool Build() { throw null; } @@ -1425,9 +1427,12 @@ public ProjectGraph(string entryProjectFile, Microsoft.Build.Evaluation.ProjectC public ProjectGraph(string entryProjectFile, System.Collections.Generic.IDictionary globalProperties) { } public ProjectGraph(string entryProjectFile, System.Collections.Generic.IDictionary globalProperties, Microsoft.Build.Evaluation.ProjectCollection projectCollection) { } public System.Collections.Generic.IReadOnlyCollection EntryPointNodes { get { throw null; } } + public System.Collections.Generic.IEnumerable EnvironmentVariableReads { get { throw null; } } + public System.Collections.Generic.IEnumerable EnvironmentVariablesImpactingBuild { get { throw null; } } public System.Collections.Generic.IReadOnlyCollection GraphRoots { get { throw null; } } public System.Collections.Generic.IReadOnlyCollection ProjectNodes { get { throw null; } } public System.Collections.Generic.IReadOnlyCollection ProjectNodesTopologicallySorted { get { throw null; } } + public System.Collections.Generic.IEnumerable UninitializedPropertyReads { get { throw null; } } public System.Collections.Generic.IReadOnlyDictionary> GetTargetLists(System.Collections.Generic.ICollection entryProjectTargets) { throw null; } public delegate Microsoft.Build.Execution.ProjectInstance ProjectInstanceFactoryFunc(string projectPath, System.Collections.Generic.Dictionary globalProperties, Microsoft.Build.Evaluation.ProjectCollection projectCollection); } diff --git a/ref/Microsoft.Build/netstandard/Microsoft.Build.cs b/ref/Microsoft.Build/netstandard/Microsoft.Build.cs index e464b7cfc94..af8ca9a6c19 100644 --- a/ref/Microsoft.Build/netstandard/Microsoft.Build.cs +++ b/ref/Microsoft.Build/netstandard/Microsoft.Build.cs @@ -1089,6 +1089,7 @@ public ProjectInstance(string projectFile, System.Collections.Generic.IDictionar public ProjectInstance(string projectFile, System.Collections.Generic.IDictionary globalProperties, string toolsVersion, string subToolsetVersion, Microsoft.Build.Evaluation.ProjectCollection projectCollection) { } public System.Collections.Generic.List DefaultTargets { get { throw null; } } public string Directory { get { throw null; } } + public System.Collections.Generic.ICollection EnvironmentVariableReads { get { throw null; } } public System.Collections.Generic.List EvaluatedItemElements { get { throw null; } } public int EvaluationId { get { throw null; } set { } } public string FullPath { get { throw null; } } @@ -1103,6 +1104,7 @@ public ProjectInstance(string projectFile, System.Collections.Generic.IDictionar public System.Collections.Generic.IDictionary Targets { get { throw null; } } public string ToolsVersion { get { throw null; } } public bool TranslateEntireState { get { throw null; } set { } } + public System.Collections.Generic.ICollection UninitializedPropertyReads { get { throw null; } } public Microsoft.Build.Execution.ProjectItemInstance AddItem(string itemType, string evaluatedInclude) { throw null; } public Microsoft.Build.Execution.ProjectItemInstance AddItem(string itemType, string evaluatedInclude, System.Collections.Generic.IEnumerable> metadata) { throw null; } public bool Build() { throw null; } @@ -1419,9 +1421,12 @@ public ProjectGraph(string entryProjectFile, Microsoft.Build.Evaluation.ProjectC public ProjectGraph(string entryProjectFile, System.Collections.Generic.IDictionary globalProperties) { } public ProjectGraph(string entryProjectFile, System.Collections.Generic.IDictionary globalProperties, Microsoft.Build.Evaluation.ProjectCollection projectCollection) { } public System.Collections.Generic.IReadOnlyCollection EntryPointNodes { get { throw null; } } + public System.Collections.Generic.IEnumerable EnvironmentVariableReads { get { throw null; } } + public System.Collections.Generic.IEnumerable EnvironmentVariablesImpactingBuild { get { throw null; } } public System.Collections.Generic.IReadOnlyCollection GraphRoots { get { throw null; } } public System.Collections.Generic.IReadOnlyCollection ProjectNodes { get { throw null; } } public System.Collections.Generic.IReadOnlyCollection ProjectNodesTopologicallySorted { get { throw null; } } + public System.Collections.Generic.IEnumerable UninitializedPropertyReads { get { throw null; } } public System.Collections.Generic.IReadOnlyDictionary> GetTargetLists(System.Collections.Generic.ICollection entryProjectTargets) { throw null; } public delegate Microsoft.Build.Execution.ProjectInstance ProjectInstanceFactoryFunc(string projectPath, System.Collections.Generic.Dictionary globalProperties, Microsoft.Build.Evaluation.ProjectCollection projectCollection); } diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs index 079258236b7..1ba327e12b9 100644 --- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs +++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs @@ -1847,6 +1847,204 @@ public void InnerBuildsProducedByOuterBuildsCanBeReferencedByOtherInnerBuilds() innerBuild1WithReferenceToInnerBuild2.ProjectReferences.ShouldBeEquivalentTo(new []{outerBuild2, innerBuild2}); } + [Fact] + public void EnvironmentVariablesReadReturnsNullWhenDisabled() + { + using (var env = TestEnvironment.Create()) + { + env.SetEnvironmentVariable("MSBUILDTRACKENVVARREADS", ""); + var projFile = env.CreateFile($"{Guid.NewGuid()}.proj", @" + + + booyah + + "); + + var graph = new ProjectGraph(projFile.Path); + graph.ProjectNodes.ShouldNotBeNull(); + graph.ProjectNodes.Count.ShouldBe(1); + var first = graph.ProjectNodes.First(); + first.ProjectInstance.ShouldNotBeNull(); + first.ProjectInstance.EnvironmentVariableReads.ShouldBeNull(); + } + } + + [Fact] + public void EnvironmentVariablesReadReturnsNotNullWhenEnabled() + { + using (var env = TestEnvironment.Create()) + { + env.SetEnvironmentVariable("MSBUILDTRACKENVVARREADS", "1"); + + var projFile = env.CreateFile($"{Guid.NewGuid()}.proj", @" + + + booyah + + "); + + var graph = new ProjectGraph(projFile.Path); + graph.ProjectNodes.ShouldNotBeNull(); + graph.ProjectNodes.Count.ShouldBe(1); + var first = graph.ProjectNodes.First(); + first.ProjectInstance.ShouldNotBeNull(); + first.ProjectInstance.EnvironmentVariableReads.ShouldNotBeNull(); + first.ProjectInstance.EnvironmentVariableReads.Count.ShouldBe(0); + } + } + + [Fact] + public void EnvironmentVariablesReadReturnsNumActuallyRead() + { + using (var env = TestEnvironment.Create()) + { + env.SetEnvironmentVariable("MSBUILDTRACKENVVARREADS", "1"); + + var projFile = env.CreateFile($"{Guid.NewGuid()}.proj", @" + + + $(Path) + + "); + + var graph = new ProjectGraph(projFile.Path); + graph.ProjectNodes.ShouldNotBeNull(); + graph.ProjectNodes.Count.ShouldBe(1); + var first = graph.ProjectNodes.First(); + first.ProjectInstance.ShouldNotBeNull(); + first.ProjectInstance.EnvironmentVariableReads.ShouldNotBeNull(); + first.ProjectInstance.EnvironmentVariableReads.Count.ShouldBe(1); + first.ProjectInstance.EnvironmentVariableReads.First().ShouldBe("Path"); + } + } + + [Fact] + public void EnvironmentVariablesReadIgnoresOverwrittenEnvVars() + { + using (var env = TestEnvironment.Create()) + { + env.SetEnvironmentVariable("MSBUILDTRACKENVVARREADS", "1"); + env.SetEnvironmentVariable("SomeTestEnvVar", "SomeValue"); + + var projFile = env.CreateFile($"{Guid.NewGuid()}.proj", @" + + + $(Path) + Overwritten! + + "); + + var graph = new ProjectGraph(projFile.Path); + graph.ProjectNodes.ShouldNotBeNull(); + graph.ProjectNodes.Count.ShouldBe(1); + var first = graph.ProjectNodes.First(); + first.ProjectInstance.ShouldNotBeNull(); + first.ProjectInstance.EnvironmentVariableReads.ShouldNotBeNull(); + first.ProjectInstance.EnvironmentVariableReads.Count.ShouldBe(1); // NOT TWO! + first.ProjectInstance.EnvironmentVariableReads.First().ShouldBe("Path"); + } + } + + [Fact] + public void EnvironmentVariablesReadReturnsNumActuallyReadMultiple() + { + using (var env = TestEnvironment.Create()) + { + env.SetEnvironmentVariable("MSBUILDTRACKENVVARREADS", "1"); + + var projFile = env.CreateFile($"{Guid.NewGuid()}.proj", @" + + + $(Path) + $(LocalAppData) + + "); + + var graph = new ProjectGraph(projFile.Path); + graph.ProjectNodes.ShouldNotBeNull(); + graph.ProjectNodes.Count.ShouldBe(1); + var first = graph.ProjectNodes.First(); + first.ProjectInstance.ShouldNotBeNull(); + first.ProjectInstance.EnvironmentVariableReads.ShouldNotBeNull(); + + string readEnvVars = string.Join(", ", first.ProjectInstance.EnvironmentVariableReads); + string allEnvVars = string.Join(", ", first.ProjectInstance.TestEnvironmentalProperties.Select(p => p.Name)); + first.ProjectInstance.EnvironmentVariableReads.Count.ShouldBe(2, $"All: {allEnvVars} | Read: {readEnvVars}"); + } + } + + [Fact] + public void UninitializedVariablesNullWhenDisabled() + { + using (var env = TestEnvironment.Create()) + { + env.SetEnvironmentVariable("MSBUILDTRACKENVVARREADS", ""); + + var projFile = env.CreateFile($"{Guid.NewGuid()}.proj", @" + + + $(This_Should_Not_Exist) + + "); + + var graph = new ProjectGraph(projFile.Path); + graph.ProjectNodes.ShouldNotBeNull(); + graph.ProjectNodes.Count.ShouldBe(1); + var first = graph.ProjectNodes.First(); + first.ProjectInstance.ShouldNotBeNull(); + first.ProjectInstance.UninitializedPropertyReads.ShouldBeNull(); + } + } + + [Fact] + public void UninitializedVariablesWithActualValues() + { + int defaultUninitPropCount = 0; + + using (var env = TestEnvironment.Create()) + { + env.SetEnvironmentVariable("MSBUILDTRACKENVVARREADS", "1"); + + var projFile = env.CreateFile($"{Guid.NewGuid()}.proj", @" + + + + "); + + var graph = new ProjectGraph(projFile.Path); + graph.ProjectNodes.ShouldNotBeNull(); + graph.ProjectNodes.Count.ShouldBe(1); + var first = graph.ProjectNodes.First(); + first.ProjectInstance.ShouldNotBeNull(); + first.ProjectInstance.UninitializedPropertyReads.ShouldNotBeNull(); + + // Determine how many uninitialized variables are present by default. + defaultUninitPropCount = first.ProjectInstance.UninitializedPropertyReads.Count; + } + + using (var env = TestEnvironment.Create()) + { + env.SetEnvironmentVariable("MSBUILDTRACKENVVARREADS", "1"); + + var projFile = env.CreateFile($"{Guid.NewGuid()}.proj", @" + + + + "); + + var graph = new ProjectGraph(projFile.Path); + graph.ProjectNodes.ShouldNotBeNull(); + graph.ProjectNodes.Count.ShouldBe(1); + var first = graph.ProjectNodes.First(); + first.ProjectInstance.ShouldNotBeNull(); + first.ProjectInstance.UninitializedPropertyReads.ShouldNotBeNull(); + int count = first.ProjectInstance.UninitializedPropertyReads.Count; + + first.ProjectInstance.UninitializedPropertyReads.Where(p => p.Equals("This_Should_Not_Exist")).FirstOrDefault().ShouldNotBeNull(); + (count - defaultUninitPropCount).ShouldBe(1); + } + } + public static IEnumerable AllNodesShouldHaveGraphBuildGlobalPropertyData { get diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index 67215eadefd..8c1b9716347 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -3371,7 +3371,7 @@ public IItemDefinition GetItemDefinition(string itemType) /// /// Sets a property which is not derived from Xml. /// - public ProjectProperty SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved) + public ProjectProperty SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, bool isEnvVar = false) { ProjectProperty property = ProjectProperty.Create(Project, name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved); Properties.Set(property); diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index 666338f2406..32fa18b6c1d 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -1256,7 +1256,7 @@ private ICollection

AddEnvironmentProperties() foreach (ProjectPropertyInstance environmentProperty in _environmentProperties) { - P property = _data.SetProperty(environmentProperty.Name, ((IProperty)environmentProperty).EvaluatedValueEscaped, false /* NOT global property */, false /* may NOT be a reserved name */); + P property = _data.SetProperty(environmentProperty.Name, ((IProperty)environmentProperty).EvaluatedValueEscaped, false /* NOT global property */, false /* may NOT be a reserved name */, true /* IS environment variable. */); environmentPropertiesList.Add(property); } diff --git a/src/Build/Evaluation/IEvaluatorData.cs b/src/Build/Evaluation/IEvaluatorData.cs index eb965e2c2ec..ce3e4d2d085 100644 --- a/src/Build/Evaluation/IEvaluatorData.cs +++ b/src/Build/Evaluation/IEvaluatorData.cs @@ -263,7 +263,7 @@ List EvaluatedItemElements ///

/// Sets a property which does not come from the Xml. /// - P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved); + P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, bool isEnvironmentVariable = false); /// /// Sets a property which comes from the Xml. diff --git a/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs b/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs index df3f02f78ac..89fe36b66e1 100644 --- a/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs +++ b/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs @@ -297,7 +297,7 @@ public P SetProperty(ProjectPropertyElement propertyElement, string evaluatedVal return _wrappedData.SetProperty(propertyElement, evaluatedValueEscaped, predecessor); } - public P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved) + public P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, bool isEnvVar = false) { return _wrappedData.SetProperty(name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved); } diff --git a/src/Build/Graph/ProjectGraph.cs b/src/Build/Graph/ProjectGraph.cs index ea6e244bd8e..eed42518fc6 100644 --- a/src/Build/Graph/ProjectGraph.cs +++ b/src/Build/Graph/ProjectGraph.cs @@ -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; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -75,6 +76,63 @@ public delegate ProjectInstance ProjectInstanceFactoryFunc( public IReadOnlyCollection GraphRoots { get; } + /// + /// Gets a distinct enumeration of all of the environment variables reads during evaluation. + /// + public IEnumerable EnvironmentVariableReads + { + get + { + if (!ProjectNodes.Any()) + { + return Enumerable.Empty(); + } + + var distinctReads = new HashSet(); + + foreach (ProjectGraphNode projectNode in ProjectNodes) + { + foreach (string environmentVariableRead in projectNode.ProjectInstance.EnvironmentVariableReads) + { + distinctReads.Add(environmentVariableRead); + } + } + + return distinctReads; + } + } + + /// + /// Get a distinct enumeration of all the uninitialized property reads during evaluation. + /// + public IEnumerable UninitializedPropertyReads + { + get + { + if (!ProjectNodes.Any()) + { + return Enumerable.Empty(); + } + + var distinctUninitializedProperties = new HashSet(); + + foreach (ProjectGraphNode projectNode in ProjectNodes) + { + foreach (string uninitializedPropertyRead in projectNode.ProjectInstance.UninitializedPropertyReads) + { + distinctUninitializedProperties.Add(uninitializedPropertyRead); + } + } + + return distinctUninitializedProperties; + } + } + + /// + /// A list of all potentially impactful environment variables. + /// + public IEnumerable EnvironmentVariablesImpactingBuild => UninitializedPropertyReads.Concat(EnvironmentVariableReads); + /// /// Constructs a graph starting from the given project file, evaluating with the global project collection and no /// global properties. diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index ad7099d9d69..293934a1f39 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -178,6 +178,11 @@ public class ProjectInstance : IPropertyProvider, IItem private bool _translateEntireState; private int _evaluationId = BuildEventContext.InvalidEvaluationId; + // Fields for tracking property usage: + private bool _trackPropertyReads = false; + private HashSet _environmentVariables; + private HashSet _environmentVariableReads; + private HashSet _uninitializedPropertyReads; /// /// Creates a ProjectInstance directly. @@ -818,6 +823,16 @@ public bool IsImmutable get { return _isImmutable; } } + /// + /// The collection of environment variables read. + /// + public ICollection EnvironmentVariableReads => _environmentVariableReads; + + /// + /// The collection of uninitialized variables read. + /// + public ICollection UninitializedPropertyReads => _uninitializedPropertyReads; + /// /// Task classes and locations known to this project. /// This is the project-specific task registry, which is consulted before @@ -1314,11 +1329,28 @@ IItemDefinition IEvaluatorData - ProjectPropertyInstance IEvaluatorData.SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved) + ProjectPropertyInstance IEvaluatorData.SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, bool isEnvironmentVariable) { // Mutability not verified as this is being populated during evaluation ProjectPropertyInstance property = ProjectPropertyInstance.Create(name, evaluatedValueEscaped, mayBeReserved, _isImmutable); _properties.Set(property); + + if (_trackPropertyReads) + { + if (isEnvironmentVariable) + { + // If it's an env var, track it. + _environmentVariables.Add(name); + } + else + { + // If it's NOT an env var but the property has the same name, + // then we're not dependent on the env var any more: remove it. + // Note: Any reads that happened before this removal will still be tracked. + _environmentVariables.Remove(name); + } + } + return property; } @@ -1383,7 +1415,14 @@ void IEvaluatorData @@ -1394,7 +1433,14 @@ public ProjectPropertyInstance GetProperty(string name) [DebuggerStepThrough] ProjectPropertyInstance IPropertyProvider.GetProperty(string name, int startIndex, int endIndex) { - return _properties.GetProperty(name, startIndex, endIndex); + ProjectPropertyInstance prop = _properties.GetProperty(name, startIndex, endIndex); + + if (_trackPropertyReads) + { + this.TrackPropertyRead(name.Substring(startIndex, endIndex-startIndex + 1), prop == null); + } + + return prop; } /// @@ -1416,6 +1462,11 @@ public string GetPropertyValue(string name) ProjectPropertyInstance property = _properties[name]; string value = (property == null) ? String.Empty : property.EvaluatedValue; + if (_trackPropertyReads) + { + this.TrackPropertyRead(name, property == null); + } + return value; } @@ -2477,6 +2528,14 @@ private void Initialize(ProjectRootElement xml, IDictionary glob _hostServices = buildParameters.HostServices; this.ProjectRootElementCache = buildParameters.ProjectRootElementCache; + _trackPropertyReads = Traits.Instance.TrackReadsOnEnvironmentVariables; + if (_trackPropertyReads) + { + _environmentVariables = new HashSet(StringComparer.OrdinalIgnoreCase); + _environmentVariableReads = new HashSet(StringComparer.OrdinalIgnoreCase); + _uninitializedPropertyReads = new HashSet(StringComparer.OrdinalIgnoreCase); + } + this.EvaluatedItemElements = new List(); _explicitToolsVersionSpecified = (explicitToolsVersion != null); @@ -2710,5 +2769,30 @@ private void CreatePropertiesSnapshot(Evaluation.Project.Data data, bool isImmut _properties.Set(instance); } } + + /// + /// Tracks this property read if it's from an environment variable or uninitialized. + /// + /// The name of the property being read. + private void TrackPropertyRead(string name, bool propertyUninitialized) + { + if (string.IsNullOrEmpty(name)) + return; + + if (propertyUninitialized) + { + if (!_uninitializedPropertyReads.Contains(name)) + { + _uninitializedPropertyReads.Add(name); + } + } + else if (_environmentVariables.Contains(name)) + { + if (!_environmentVariableReads.Contains(name)) + { + _environmentVariableReads.Add(name); + } + } + } } } diff --git a/src/Shared/Traits.cs b/src/Shared/Traits.cs index a6a6d80f899..f8531d998d4 100644 --- a/src/Shared/Traits.cs +++ b/src/Shared/Traits.cs @@ -77,6 +77,11 @@ public Traits() /// public readonly bool LogPropertyFunctionsRequiringReflection = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildLogPropertyFunctionsRequiringReflection")); + /// + /// Track the environment variables actually read and used. + /// + public readonly bool TrackReadsOnEnvironmentVariables = Environment.GetEnvironmentVariable("MSBUILDTRACKENVVARREADS") == "1"; + private static int ParseIntFromEnvironmentVariableOrDefault(string environmentVariable, int defaultValue) { return int.TryParse(Environment.GetEnvironmentVariable(environmentVariable), out int result)