diff --git a/documentation/Property-tracking-capabilities.md b/documentation/Property-tracking-capabilities.md new file mode 100644 index 00000000000..a2cef4e2543 --- /dev/null +++ b/documentation/Property-tracking-capabilities.md @@ -0,0 +1,49 @@ +# MSBuild's property tracking capabilities + +MSBuild Property Tracking is a built-in diagnostic feature that tracks property value changes during the build process. +By default, this feature is opted out due to performance considerations. + +## Property Tracking Coverage + +The implementation tracks properties in the following scenarios: + +1. Properties set via command-line arguments (e.g. using `/p:` switches) + +2. Properties defined based on environment variables and used by MSBuild + +3. Properties set as target outputs + - Tracks changes when properties are modified by target execution + +4. Properties set as task outputs + - Monitors property modifications resulting from task execution + +5. Properties defined in XML during evaluation + - Provides exact location information for properties defined in project files + - Includes line and column information from the source XML + - Reports on property modifications + +## Event Types and Message Formatting + +The feature implements specialized event handling for three scenarios: + +1. `PropertyReassignmentEventArgs` + - Triggered when a property value is changed + `set MsBuildLogPropertyTracking=1` + +2. `PropertyInitialValueSetEventArgs` + - Triggered when a property is first initialized + `set MsBuildLogPropertyTracking=2` + +3. `EnvironmentVariableRead` + - Tracks when environment variables are read + `set MsBuildLogPropertyTracking=4` + +4. `UninitializedPropertyReadEventArgs` + - Triggered when attempting to read a property that hasn't been initialized + `set MsBuildLogPropertyTracking=8` + +5. None + - Disables all property tracking + `set MsBuildLogPropertyTracking=0` + +If you want to enable all these events reporting, enable it by `set MsBuildLogPropertyTracking=15`. \ No newline at end of file diff --git a/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs b/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs index 707587dd7b8..2eef5fde018 100644 --- a/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs +++ b/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs @@ -881,8 +881,11 @@ public void RoundTripPropertyReassignmentEventArgs() propertyName: "a", previousValue: "b", newValue: "c", - location: "d", - message: "Property reassignment: $(a)=\"c\" (previous value: \"b\") at d", + location: null, + file: "file.cs", + line: 10, + column: 20, + message: "Property reassignment: $(a)=\"c\" (previous value: \"b\") at file.cs (10,20)", helpKeyword: "e", senderName: "f"); @@ -900,8 +903,8 @@ public void RoundTripPropertyReassignmentEventArgs() public void UninitializedPropertyReadEventArgs() { var args = new UninitializedPropertyReadEventArgs( - propertyName: Guid.NewGuid().ToString(), - message: Guid.NewGuid().ToString(), + propertyName: "a", + message: "Read uninitialized property \"a\"", helpKeyword: Guid.NewGuid().ToString(), senderName: Guid.NewGuid().ToString()); @@ -916,17 +919,22 @@ public void UninitializedPropertyReadEventArgs() public void PropertyInitialValueEventArgs() { var args = new PropertyInitialValueSetEventArgs( - propertyName: Guid.NewGuid().ToString(), - propertyValue: Guid.NewGuid().ToString(), - propertySource: Guid.NewGuid().ToString(), - message: Guid.NewGuid().ToString(), + propertyName: "a", + propertyValue: "b", + propertySource: null, + file: "file.cs", + line: 10, + column: 20, + message: "Property initial value: $(a)=\"b\" Source: file.cs (10,20)", helpKeyword: Guid.NewGuid().ToString(), senderName: Guid.NewGuid().ToString()); Roundtrip(args, e => e.PropertyName, e => e.PropertyValue, - e => e.PropertySource, + e => e.File, + e => e.LineNumber.ToString(), + e => e.ColumnNumber.ToString(), e => e.Message, e => e.HelpKeyword, e => e.SenderName); diff --git a/src/Build.UnitTests/Evaluation/Evaluator_Tests.cs b/src/Build.UnitTests/Evaluation/Evaluator_Tests.cs index 4f9b2b14f92..8f53177e30f 100644 --- a/src/Build.UnitTests/Evaluation/Evaluator_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Evaluator_Tests.cs @@ -4708,7 +4708,7 @@ public void VerifyPropertyTrackingLoggingDefault() // Having just environment variables defined should default to nothing being logged except one environment variable read. VerifyPropertyTrackingLoggingScenario( null, - logger => + (logger, _) => { logger .AllBuildEvents @@ -4740,7 +4740,7 @@ public void VerifyPropertyTrackingLoggingPropertyReassignment() { VerifyPropertyTrackingLoggingScenario( "1", - logger => + (logger, _) => { logger .AllBuildEvents @@ -4771,7 +4771,7 @@ public void VerifyPropertyTrackingLoggingNone() { this.VerifyPropertyTrackingLoggingScenario( "0", - logger => + (logger, _) => { logger .AllBuildEvents @@ -4803,7 +4803,7 @@ public void VerifyPropertyTrackingLoggingPropertyInitialValue() { this.VerifyPropertyTrackingLoggingScenario( "2", - logger => + (logger, projectPath) => { logger .AllBuildEvents @@ -4829,11 +4829,11 @@ public void VerifyPropertyTrackingLoggingPropertyInitialValue() // Verify logging of property initial values. propertyInitialValueMap.ShouldContainKey("Prop"); - propertyInitialValueMap["Prop"].PropertySource.ShouldBe("Xml"); + propertyInitialValueMap["Prop"].File.ShouldBe(projectPath); propertyInitialValueMap["Prop"].PropertyValue.ShouldBe(string.Empty); propertyInitialValueMap.ShouldContainKey("EnvVar"); - propertyInitialValueMap["EnvVar"].PropertySource.ShouldBe("Xml"); + propertyInitialValueMap["EnvVar"].File.ShouldBe(projectPath); propertyInitialValueMap["EnvVar"].PropertyValue.ShouldBe("It's also Defined!"); propertyInitialValueMap.ShouldContainKey("DEFINED_ENVIRONMENT_VARIABLE"); @@ -4841,11 +4841,11 @@ public void VerifyPropertyTrackingLoggingPropertyInitialValue() propertyInitialValueMap["DEFINED_ENVIRONMENT_VARIABLE"].PropertyValue.ShouldBe("It's Defined!"); propertyInitialValueMap.ShouldContainKey("NotEnvVarRead"); - propertyInitialValueMap["NotEnvVarRead"].PropertySource.ShouldBe("Xml"); + propertyInitialValueMap["NotEnvVarRead"].File.ShouldBe(projectPath); propertyInitialValueMap["NotEnvVarRead"].PropertyValue.ShouldBe("Overwritten!"); propertyInitialValueMap.ShouldContainKey("Prop2"); - propertyInitialValueMap["Prop2"].PropertySource.ShouldBe("Xml"); + propertyInitialValueMap["Prop2"].File.ShouldBe(projectPath); propertyInitialValueMap["Prop2"].PropertyValue.ShouldBe("Value1"); }); } @@ -4855,7 +4855,7 @@ public void VerifyPropertyTrackingLoggingEnvironmentVariableRead() { this.VerifyPropertyTrackingLoggingScenario( "4", - logger => + (logger, _) => { logger .AllBuildEvents @@ -4889,7 +4889,7 @@ public void VerifyPropertyTrackingLoggingUninitializedPropertyRead() { this.VerifyPropertyTrackingLoggingScenario( "8", - logger => + (logger, _) => { logger .AllBuildEvents @@ -4920,7 +4920,7 @@ public void VerifyPropertyTrackingLoggingAll() { this.VerifyPropertyTrackingLoggingScenario( "15", - logger => + (logger, projectPath) => { logger .AllBuildEvents @@ -4949,11 +4949,11 @@ public void VerifyPropertyTrackingLoggingAll() // Verify logging of property initial values. propertyInitialValueMap.ShouldContainKey("Prop"); - propertyInitialValueMap["Prop"].PropertySource.ShouldBe("Xml"); + propertyInitialValueMap["Prop"].File.ShouldBe(projectPath); propertyInitialValueMap["Prop"].PropertyValue.ShouldBe(string.Empty); propertyInitialValueMap.ShouldContainKey("EnvVar"); - propertyInitialValueMap["EnvVar"].PropertySource.ShouldBe("Xml"); + propertyInitialValueMap["EnvVar"].File.ShouldBe(projectPath); propertyInitialValueMap["EnvVar"].PropertyValue.ShouldBe("It's also Defined!"); propertyInitialValueMap.ShouldContainKey("DEFINED_ENVIRONMENT_VARIABLE"); @@ -4961,11 +4961,11 @@ public void VerifyPropertyTrackingLoggingAll() propertyInitialValueMap["DEFINED_ENVIRONMENT_VARIABLE"].PropertyValue.ShouldBe("It's Defined!"); propertyInitialValueMap.ShouldContainKey("NotEnvVarRead"); - propertyInitialValueMap["NotEnvVarRead"].PropertySource.ShouldBe("Xml"); + propertyInitialValueMap["NotEnvVarRead"].File.ShouldBe(projectPath); propertyInitialValueMap["NotEnvVarRead"].PropertyValue.ShouldBe("Overwritten!"); propertyInitialValueMap.ShouldContainKey("Prop2"); - propertyInitialValueMap["Prop2"].PropertySource.ShouldBe("Xml"); + propertyInitialValueMap["Prop2"].File.ShouldBe(projectPath); propertyInitialValueMap["Prop2"].PropertyValue.ShouldBe("Value1"); }); } @@ -4987,7 +4987,7 @@ public void VerifyGetTypeEvaluationBlocked() new Project(XmlReader.Create(new StringReader(projectContents)), null, "Fake", fakeProjectCollection)); } - private void VerifyPropertyTrackingLoggingScenario(string envVarValue, Action loggerEvaluatorAction) + private void VerifyPropertyTrackingLoggingScenario(string envVarValue, Action loggerEvaluatorAction) { // The default is that only reassignments are logged. @@ -5026,7 +5026,7 @@ private void VerifyPropertyTrackingLoggingScenario(string envVarValue, Action private PropertyDictionary _globalProperties = new PropertyDictionary(); + /// + /// Properties passed from the command line (e.g. by using /p:). + /// + private ICollection _propertiesFromCommandLine; + /// /// The loggers. /// @@ -250,6 +255,7 @@ public BuildParameters(ProjectCollection projectCollection) _defaultToolsVersion = projectCollection.DefaultToolsVersion; _globalProperties = new PropertyDictionary(projectCollection.GlobalPropertiesCollection); + _propertiesFromCommandLine = projectCollection.PropertiesFromCommandLine; } /// @@ -279,6 +285,7 @@ internal BuildParameters(BuildParameters other, bool resetEnvironment = false) _environmentProperties = other._environmentProperties != null ? new PropertyDictionary(other._environmentProperties) : null; _forwardingLoggers = other._forwardingLoggers != null ? new List(other._forwardingLoggers) : null; _globalProperties = other._globalProperties != null ? new PropertyDictionary(other._globalProperties) : null; + _propertiesFromCommandLine = other._propertiesFromCommandLine != null ? new HashSet(other._propertiesFromCommandLine, StringComparer.OrdinalIgnoreCase) : null; HostServices = other.HostServices; _loggers = other._loggers != null ? new List(other._loggers) : null; _maxNodeCount = other._maxNodeCount; @@ -332,6 +339,10 @@ public bool UseSynchronousLogging set => _useSynchronousLogging = value; } + /// + /// Properties passed from the command line (e.g. by using /p:). + /// + public ICollection PropertiesFromCommandLine => _propertiesFromCommandLine; /// /// Indicates whether to emit a default error if a task returns false without logging an error. diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/PropertyGroupIntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/PropertyGroupIntrinsicTask.cs index 02ca6a1dab8..5bd3a49c331 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/PropertyGroupIntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/PropertyGroupIntrinsicTask.cs @@ -25,6 +25,8 @@ internal class PropertyGroupIntrinsicTask : IntrinsicTask /// private ProjectPropertyGroupTaskInstance _taskInstance; + private readonly PropertyTrackingSetting _propertyTrackingSettings; + /// /// Create a new PropertyGroup task. /// @@ -36,6 +38,7 @@ public PropertyGroupIntrinsicTask(ProjectPropertyGroupTaskInstance taskInstance, : base(loggingContext, projectInstance, logTaskInputs) { _taskInstance = taskInstance; + _propertyTrackingSettings = (PropertyTrackingSetting)Traits.Instance.LogPropertyTracking; } /// @@ -85,6 +88,14 @@ internal override void ExecuteTask(Lookup lookup) string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(property.Value, ExpanderOptions.ExpandAll, property.Location); bucket.Expander.PropertiesUseTracker.CheckPreexistingUndefinedUsage(property, evaluatedValue, LoggingContext); + PropertyTrackingUtils.LogPropertyAssignment( + _propertyTrackingSettings, + property.Name, + evaluatedValue, + property.Location, + Project.GetProperty(property.Name)?.EvaluatedValue ?? null, + LoggingContext); + if (LogTaskInputs && !LoggingContext.LoggingService.OnlyLogCriticalEvents) { LoggingContext.LogComment(MessageImportance.Low, "PropertyGroupLogMessage", property.Name, evaluatedValue); diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 0e4c160336f..261f52fcb76 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -156,6 +156,8 @@ internal class TaskExecutionHost : IDisposable /// private readonly Dictionary _intrinsicTasks = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly PropertyTrackingSetting _propertyTrackingSettings; + /// /// Constructor /// @@ -172,6 +174,8 @@ internal TaskExecutionHost(IBuildComponentHost host) { LogTaskInputs = Traits.Instance.EscapeHatches.LogTaskInputs; } + + _propertyTrackingSettings = (PropertyTrackingSetting)Traits.Instance.LogPropertyTracking; } /// @@ -1582,6 +1586,14 @@ private void GatherArrayStringAndValueOutputs(bool outputTargetIsItem, string ou } } + PropertyTrackingUtils.LogPropertyAssignment( + _propertyTrackingSettings, + outputTargetName, + outputString, + parameterLocation, + _projectInstance.GetProperty(outputTargetName)?.EvaluatedValue ?? null, + _taskLoggingContext); + _batchBucket.Lookup.SetProperty(ProjectPropertyInstance.Create(outputTargetName, outputString, parameterLocation, _projectInstance.IsImmutable)); } } diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index 4a868767aea..1348a7cffcc 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -3721,6 +3721,7 @@ private void Reevaluate( loadSettings, ProjectCollection.MaxNodeCount, ProjectCollection.EnvironmentProperties, + ProjectCollection.PropertiesFromCommandLine, loggingServiceForEvaluation, new ProjectItemFactory(Owner), ProjectCollection, @@ -4437,7 +4438,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, LoggingContext loggingContext, bool isEnvironmentVariable = false) + public ProjectProperty SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, LoggingContext loggingContext, bool isEnvironmentVariable = false, bool isCommandLineProperty = false) { ProjectProperty property = ProjectProperty.Create(Project, name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved, loggingContext); Properties.Set(property); diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 031e31f1e2e..9973e5d3d80 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -497,6 +497,11 @@ public static string DisplayVersion } } + /// + /// Properties passed from the command line (e.g. by using /p:). + /// + public ICollection PropertiesFromCommandLine { get; set; } + /// /// The default tools version of this project collection. Projects use this tools version if they /// aren't otherwise told what tools version to use. diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index daf8d0ea71b..e285a948aa4 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -12,7 +12,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Components.Logging; using Microsoft.Build.BackEnd.Components.RequestBuilder; -using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.BackEnd.SdkResolution; using Microsoft.Build.Collections; using Microsoft.Build.Construction; @@ -158,6 +157,11 @@ internal class Evaluator /// private readonly PropertyDictionary _environmentProperties; + /// + /// Properties passed from the command line (e.g. by using /p:). + /// + private readonly ICollection _propertiesFromCommandLine; + /// /// The cache to consult for any imports that need loading. /// @@ -201,6 +205,7 @@ private Evaluator( ProjectLoadSettings loadSettings, int maxNodeCount, PropertyDictionary environmentProperties, + ICollection propertiesFromCommandLine, IItemFactory itemFactory, IToolsetProvider toolsetProvider, IDirectoryCacheFactory directoryCacheFactory, @@ -253,6 +258,7 @@ private Evaluator( _loadSettings = loadSettings; _maxNodeCount = maxNodeCount; _environmentProperties = environmentProperties; + _propertiesFromCommandLine = propertiesFromCommandLine ?? []; _itemFactory = itemFactory; _projectRootElementCache = projectRootElementCache; _sdkResolverService = sdkResolverService; @@ -301,6 +307,7 @@ internal static void Evaluate( ProjectLoadSettings loadSettings, int maxNodeCount, PropertyDictionary environmentProperties, + ICollection propertiesFromCommandLine, ILoggingService loggingService, IItemFactory itemFactory, IToolsetProvider toolsetProvider, @@ -321,6 +328,7 @@ internal static void Evaluate( loadSettings, maxNodeCount, environmentProperties, + propertiesFromCommandLine, itemFactory, toolsetProvider, directoryCacheFactory, @@ -1240,7 +1248,7 @@ private void AddToolsetProperties() } /// - /// Put all the global properties into our property bag + /// Put all the global properties into our property bag. /// private int AddGlobalProperties() { @@ -1251,7 +1259,13 @@ private int AddGlobalProperties() foreach (ProjectPropertyInstance globalProperty in _data.GlobalPropertiesDictionary) { - _data.SetProperty(globalProperty.Name, ((IProperty)globalProperty).EvaluatedValueEscaped, true /* IS global property */, false /* may NOT be a reserved name */, loggingContext: _evaluationLoggingContext); + _ = _data.SetProperty( + globalProperty.Name, + ((IProperty)globalProperty).EvaluatedValueEscaped, + isGlobalProperty: true /* it is a global property, but it comes from command line and is tracked separately */, + mayBeReserved: false /* may NOT be a reserved name */, + loggingContext: _evaluationLoggingContext, + isCommandLineProperty: _propertiesFromCommandLine.Contains(globalProperty.Name) /* IS coming from command line argument */); } return _data.GlobalPropertiesDictionary.Count; diff --git a/src/Build/Evaluation/IEvaluatorData.cs b/src/Build/Evaluation/IEvaluatorData.cs index 212f446d70f..82ad36d6650 100644 --- a/src/Build/Evaluation/IEvaluatorData.cs +++ b/src/Build/Evaluation/IEvaluatorData.cs @@ -268,7 +268,7 @@ List EvaluatedItemElements /// /// Sets a property which does not come from the Xml. /// - P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, BackEnd.Logging.LoggingContext loggingContext, bool isEnvironmentVariable = false); + P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, BackEnd.Logging.LoggingContext loggingContext, bool isEnvironmentVariable = false, bool isCommandLineProperty = 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 5d749befd8d..b4685fa40d4 100644 --- a/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs +++ b/src/Build/Evaluation/LazyItemEvaluator.EvaluatorData.cs @@ -311,9 +311,9 @@ public P SetProperty(ProjectPropertyElement propertyElement, string evaluatedVal return _wrappedData.SetProperty(propertyElement, evaluatedValueEscaped, loggingContext); } - public P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, LoggingContext loggingContext, bool isEnvironmentVariable = false) + public P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, LoggingContext loggingContext, bool isEnvironmentVariable = false, bool isCommandLineProperty = false) { - return _wrappedData.SetProperty(name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved, loggingContext: loggingContext); + return _wrappedData.SetProperty(name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved, loggingContext: loggingContext, isCommandLineProperty); } } } diff --git a/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs b/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs index 1e9861f742b..c7b8ae0d6ad 100644 --- a/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs +++ b/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs @@ -87,16 +87,23 @@ public P GetProperty(string name, int startIndex, int endIndex) /// /// Sets a property which does not come from the Xml. /// - public P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, LoggingContext loggingContext, bool isEnvironmentVariable = false) + public P SetProperty( + string name, + string evaluatedValueEscaped, + bool isGlobalProperty, + bool mayBeReserved, + LoggingContext loggingContext, + bool isEnvironmentVariable = false, + bool isCommandLineProperty = false) { P? originalProperty = _wrapped.GetProperty(name); - P newProperty = _wrapped.SetProperty(name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved, _evaluationLoggingContext, isEnvironmentVariable); + P newProperty = _wrapped.SetProperty(name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved, _evaluationLoggingContext, isEnvironmentVariable, isCommandLineProperty); this.TrackPropertyWrite( originalProperty, newProperty, null, - this.DeterminePropertySource(isGlobalProperty, mayBeReserved, isEnvironmentVariable), + this.DeterminePropertySource(isGlobalProperty, mayBeReserved, isEnvironmentVariable, isCommandLineProperty), loggingContext); return newProperty; @@ -168,13 +175,10 @@ public P SetProperty(ProjectPropertyElement propertyElement, string evaluatedVal #region Private Methods... private bool IsPropertyReadTrackingRequested - => IsEnvironmentVariableReadTrackingRequested || - (_settings & PropertyTrackingSetting.UninitializedPropertyRead) == - PropertyTrackingSetting.UninitializedPropertyRead; + => IsEnvironmentVariableReadTrackingRequested + || PropertyTrackingUtils.IsPropertyTrackingEnabled(_settings, PropertyTrackingSetting.UninitializedPropertyRead); - private bool IsEnvironmentVariableReadTrackingRequested - => (_settings & PropertyTrackingSetting.EnvironmentVariableRead) == - PropertyTrackingSetting.EnvironmentVariableRead; + private bool IsEnvironmentVariableReadTrackingRequested => PropertyTrackingUtils.IsPropertyTrackingEnabled(_settings, PropertyTrackingSetting.EnvironmentVariableRead); /// /// Logic containing what to do when a property is read. @@ -209,7 +213,7 @@ private void TrackPropertyRead(string name, P property) /// The name of the environment variable read. private void TrackEnvironmentVariableRead(string name) { - if ((_settings & PropertyTrackingSetting.EnvironmentVariableRead) != PropertyTrackingSetting.EnvironmentVariableRead) + if (!IsEnvironmentVariableReadTrackingRequested) { return; } @@ -231,15 +235,15 @@ private void TrackEnvironmentVariableRead(string name) /// The name of the uninitialized property read. private void TrackUninitializedPropertyRead(string name) { - if ((_settings & PropertyTrackingSetting.UninitializedPropertyRead) != PropertyTrackingSetting.UninitializedPropertyRead) + if (!PropertyTrackingUtils.IsPropertyTrackingEnabled(_settings, PropertyTrackingSetting.UninitializedPropertyRead)) { return; } - var args = new UninitializedPropertyReadEventArgs( - name, - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("UninitializedPropertyRead", name)); - args.BuildEventContext = _evaluationLoggingContext.BuildEventContext; + var args = new UninitializedPropertyReadEventArgs(name, message: null) + { + BuildEventContext = _evaluationLoggingContext.BuildEventContext, + }; _evaluationLoggingContext.LogBuildEvent(args); } @@ -258,12 +262,12 @@ private void TrackPropertyWrite( if (predecessor == null) { // If this property had no previous value, then track an initial value. - TrackPropertyInitialValueSet(property, source); + TrackPropertyInitialValueSet(property, source, location); } else { // There was a previous value, and it might have been changed. Track that. - TrackPropertyReassignment(predecessor, property, location?.LocationString); + TrackPropertyReassignment(predecessor, property, location); } // If this property was an environment variable but no longer is, track it. @@ -278,19 +282,25 @@ private void TrackPropertyWrite( /// /// The property being set. /// The source of the property. - private void TrackPropertyInitialValueSet(P property, PropertySource source) + /// The exact location of the property. Can be null if comes not form xml. + private void TrackPropertyInitialValueSet(P property, PropertySource source, IElementLocation? location) { - if ((_settings & PropertyTrackingSetting.PropertyInitialValueSet) != PropertyTrackingSetting.PropertyInitialValueSet) + if (!PropertyTrackingUtils.IsPropertyTrackingEnabled(_settings, PropertyTrackingSetting.PropertyInitialValueSet)) { return; } var args = new PropertyInitialValueSetEventArgs( - property.Name, - property.EvaluatedValue, - source.ToString(), - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("PropertyAssignment", property.Name, property.EvaluatedValue, source)); - args.BuildEventContext = _evaluationLoggingContext.BuildEventContext; + property.Name, + property.EvaluatedValue, + + // If the property is from XML, we don't need property source since a full location is available. + location == null ? GetPropertySourceName(source) : string.Empty, + location?.File, + location?.Line ?? 0, + location?.Column ?? 0, + message: null) + { BuildEventContext = _evaluationLoggingContext.BuildEventContext, }; _evaluationLoggingContext.LogBuildEvent(args); } @@ -301,7 +311,7 @@ private void TrackPropertyInitialValueSet(P property, PropertySource source) /// The property's preceding state. Null if none. /// The property's current state. /// The location of this property's reassignment. - private void TrackPropertyReassignment(P? predecessor, P property, string? location) + private void TrackPropertyReassignment(P? predecessor, P property, IElementLocation? location) { if (MSBuildNameIgnoreCaseComparer.Default.Equals(property.Name, "MSBuildAllProjects")) { @@ -317,17 +327,17 @@ private void TrackPropertyReassignment(P? predecessor, P property, string? locat return; } - // Either we want to specifically track property reassignments - // or we do not want to track nothing - in which case the prop reassignment is enabled by default. - if ((_settings & PropertyTrackingSetting.PropertyReassignment) == PropertyTrackingSetting.PropertyReassignment || - (_settings == 0 && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10))) + if (PropertyTrackingUtils.IsPropertyReassignmentEnabled(_settings)) { var args = new PropertyReassignmentEventArgs( - property.Name, - oldValue, - newValue, - location, - message: null) + property.Name, + oldValue, + newValue, + location: null, + location?.File, + location?.Line ?? 0, + location?.Column ?? 0, + message: null) { BuildEventContext = _evaluationLoggingContext.BuildEventContext, }; _evaluationLoggingContext.LogBuildEvent(args); @@ -347,7 +357,7 @@ private void TrackPropertyReassignment(P? predecessor, P property, string? locat /// /// Determines the source of a property given the variables SetProperty arguments provided. This logic follows what's in . /// - private PropertySource DeterminePropertySource(bool isGlobalProperty, bool mayBeReserved, bool isEnvironmentVariable) + private PropertySource DeterminePropertySource(bool isGlobalProperty, bool mayBeReserved, bool isEnvironmentVariable, bool isCommandLineProperty) { if (isEnvironmentVariable) { @@ -356,7 +366,7 @@ private PropertySource DeterminePropertySource(bool isGlobalProperty, bool mayBe if (isGlobalProperty) { - return PropertySource.Global; + return isCommandLineProperty ? PropertySource.CommandLine : PropertySource.Global; } return mayBeReserved ? PropertySource.BuiltIn : PropertySource.Toolset; @@ -373,20 +383,107 @@ private enum PropertySource BuiltIn, Global, Toolset, - EnvironmentVariable + EnvironmentVariable, + CommandLine, } - [Flags] - private enum PropertyTrackingSetting + private static string GetPropertySourceName(PropertySource source) => source switch { - None = 0, + PropertySource.Xml => "Xml", + PropertySource.BuiltIn => "BuiltIn", + PropertySource.Global => "Global", + PropertySource.Toolset => "Toolset", + PropertySource.EnvironmentVariable => "EnvironmentVariable", + PropertySource.CommandLine => "CommandLine", + _ => throw new ArgumentOutOfRangeException(nameof(source), source, null) + }; + } + + [Flags] + internal enum PropertyTrackingSetting + { + None = 0, + + PropertyReassignment = 1, + PropertyInitialValueSet = 1 << 1, + EnvironmentVariableRead = 1 << 2, + UninitializedPropertyRead = 1 << 3, + + All = PropertyReassignment | PropertyInitialValueSet | EnvironmentVariableRead | UninitializedPropertyRead + } - PropertyReassignment = 1, - PropertyInitialValueSet = 1 << 1, - EnvironmentVariableRead = 1 << 2, - UninitializedPropertyRead = 1 << 3, + internal class PropertyTrackingUtils + { + /// + /// Determines if a specific property tracking setting is enabled within the provided settings configuration. + /// + /// The combined property tracking settings value to check against. + /// The specific tracking setting to verify. + /// true if the specified tracking setting is enabled in the settings configuration. + internal static bool IsPropertyTrackingEnabled(PropertyTrackingSetting settings, PropertyTrackingSetting currentTrackingSetting) => (settings & currentTrackingSetting) == currentTrackingSetting; - All = PropertyReassignment | PropertyInitialValueSet | EnvironmentVariableRead | UninitializedPropertyRead + // Either we want to specifically track property reassignments + // or we do not want to track nothing - in which case the prop reassignment is enabled by default. + internal static bool IsPropertyReassignmentEnabled(PropertyTrackingSetting currentTrackingSetting) => IsPropertyTrackingEnabled(currentTrackingSetting, PropertyTrackingSetting.PropertyReassignment) + || (currentTrackingSetting == PropertyTrackingSetting.None && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10)); + + /// + /// Logs property assignment information during execution, providing detailed tracking of property value changes. + /// This internal method handles two scenarios: + /// 1. Initial property value assignment (when previousPropertyValue is null) + /// 2. Property value reassignment (when previousPropertyValue has a value) + /// If property tracking is disabled (PropertyTrackingSetting.None), no logging occurs. + /// + /// Controls what types of property assignments should be tracked. + /// Name of the property being modified. + /// New value being assigned to the property. + /// Source location information (file, line, column). + /// Previous value of the property, null if this is initial assignment. + /// Context for logging build events. + internal static void LogPropertyAssignment( + PropertyTrackingSetting settings, + string propertyName, + string propertyValue, + IElementLocation location, + string? previousPropertyValue, + LoggingContext loggingContext) + { + if (previousPropertyValue == null) + { + if (IsPropertyTrackingEnabled(settings, PropertyTrackingSetting.PropertyInitialValueSet)) + { + var args = new PropertyInitialValueSetEventArgs( + propertyName, + propertyValue, + propertySource: string.Empty, + location.File, + location.Line, + location.Column, + message: null) { BuildEventContext = loggingContext.BuildEventContext }; + + loggingContext.LogBuildEvent(args); + } + } + else + { + if (IsPropertyReassignmentEnabled(settings)) + { + if (propertyValue != previousPropertyValue) + { + var args = new PropertyReassignmentEventArgs( + propertyName, + previousPropertyValue, + propertyValue, + location: null, + location.File, + location.Line, + location.Column, + message: null) { BuildEventContext = loggingContext.BuildEventContext, }; + + loggingContext.LogBuildEvent(args); + } + } + } } } } diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index e5eba2b291e..938fcc5cba9 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -1791,7 +1791,7 @@ IItemDefinition IEvaluatorData - ProjectPropertyInstance IEvaluatorData.SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, LoggingContext loggingContext, bool isEnvironmentVariable) + ProjectPropertyInstance IEvaluatorData.SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved, LoggingContext loggingContext, bool isEnvironmentVariable, bool isCommandLineProperty) { // Mutability not verified as this is being populated during evaluation ProjectPropertyInstance property = ProjectPropertyInstance.Create(name, evaluatedValueEscaped, mayBeReserved, _isImmutable, isEnvironmentVariable, loggingContext); @@ -3175,6 +3175,7 @@ private void Initialize( projectLoadSettings ?? buildParameters.ProjectLoadSettings, /* Use override ProjectLoadSettings if specified */ buildParameters.MaxNodeCount, buildParameters.EnvironmentPropertiesInternal, + buildParameters.PropertiesFromCommandLine, loggingService, new ProjectItemInstanceFactory(this), buildParameters.ToolsetProvider, diff --git a/src/Build/Logging/BinaryLogger/BinaryLogger.cs b/src/Build/Logging/BinaryLogger/BinaryLogger.cs index be4eaa2288d..adadde7eb8b 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogger.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogger.cs @@ -80,6 +80,8 @@ public sealed class BinaryLogger : ILogger // BuildCheckTracingEvent, BuildCheckAcquisitionEvent, BuildSubmissionStartedEvent // version 24: // - new record kind: BuildCanceledEventArgs + // version 25: + // - add extra information to PropertyInitialValueSetEventArgs and PropertyReassignmentEventArgs and change message formatting logic. // MAKE SURE YOU KEEP BuildEventArgsWriter AND StructuredLogViewer.BuildEventArgsWriter IN SYNC WITH THE CHANGES ABOVE. // Both components must stay in sync to avoid issues with logging or event handling in the products. @@ -90,7 +92,7 @@ public sealed class BinaryLogger : ILogger // The current version of the binary log representation. // Changes with each update of the binary log format. - internal const int FileFormatVersion = 24; + internal const int FileFormatVersion = 25; // The minimum version of the binary log reader that can read log of above version. // This should be changed only when the binary log format is changed in a way that would prevent it from being diff --git a/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs b/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs index 0b97024c472..4b48d9a8592 100644 --- a/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs +++ b/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs @@ -1177,6 +1177,9 @@ private BuildEventArgs ReadPropertyReassignmentEventArgs() previousValue, newValue, location, + fields.File, + fields.LineNumber, + fields.ColumnNumber, fields.Message, fields.HelpKeyword, fields.SenderName, @@ -1193,7 +1196,7 @@ private BuildEventArgs ReadUninitializedPropertyReadEventArgs() var e = new UninitializedPropertyReadEventArgs( propertyName, - fields.Message, + message: null, fields.HelpKeyword, fields.SenderName, fields.Importance); @@ -1214,10 +1217,14 @@ private BuildEventArgs ReadPropertyInitialValueSetEventArgs() propertyName, propertyValue, propertySource, + fields.File, + fields.LineNumber, + fields.ColumnNumber, fields.Message, fields.HelpKeyword, fields.SenderName, fields.Importance); + SetCommonFields(e, fields); return e; diff --git a/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs b/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs index dba52023339..fc44128aba0 100644 --- a/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs +++ b/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs @@ -572,19 +572,21 @@ private BinaryLogRecordKind Write(PropertyReassignmentEventArgs e) WriteDeduplicatedString(e.PreviousValue); WriteDeduplicatedString(e.NewValue); WriteDeduplicatedString(e.Location); + return BinaryLogRecordKind.PropertyReassignment; } private BinaryLogRecordKind Write(UninitializedPropertyReadEventArgs e) { - WriteMessageFields(e, writeImportance: true); + WriteMessageFields(e, writeMessage: false, writeImportance: true); WriteDeduplicatedString(e.PropertyName); + return BinaryLogRecordKind.UninitializedPropertyRead; } private BinaryLogRecordKind Write(PropertyInitialValueSetEventArgs e) { - WriteMessageFields(e, writeImportance: true); + WriteMessageFields(e, writeMessage: false, writeImportance: true); WriteDeduplicatedString(e.PropertyName); WriteDeduplicatedString(e.PropertyValue); WriteDeduplicatedString(e.PropertySource); diff --git a/src/Framework/PropertyInitialValueSetEventArgs.cs b/src/Framework/PropertyInitialValueSetEventArgs.cs index 318755fcde3..fe1e52023b6 100644 --- a/src/Framework/PropertyInitialValueSetEventArgs.cs +++ b/src/Framework/PropertyInitialValueSetEventArgs.cs @@ -37,13 +37,45 @@ public PropertyInitialValueSetEventArgs( string message, string helpKeyword = null, string senderName = null, - MessageImportance importance = MessageImportance.Low) : base(message, helpKeyword, senderName, importance) + MessageImportance importance = MessageImportance.Low) + : base(message, helpKeyword, senderName, importance) { this.PropertyName = propertyName; this.PropertyValue = propertyValue; this.PropertySource = propertySource; } + /// + /// Creates an instance of the class. + /// + /// The name of the property. + /// The value of the property. + /// The source of the property. + /// The file associated with the event. + /// The line number (0 if not applicable). + /// The column number (0 if not applicable). + /// The message of the property. + /// The help keyword. + /// The sender name of the event. + /// The importance of the message. + public PropertyInitialValueSetEventArgs( + string propertyName, + string propertyValue, + string propertySource, + string file, + int line, + int column, + string message, + string helpKeyword = null, + string senderName = null, + MessageImportance importance = MessageImportance.Low) + : base(subcategory: null, code: null, file: file, lineNumber: line, columnNumber: column, 0, 0, message, helpKeyword, senderName, importance) + { + PropertyName = propertyName; + PropertyValue = propertyValue; + PropertySource = propertySource; + } + /// /// The name of the property. /// @@ -59,6 +91,20 @@ public PropertyInitialValueSetEventArgs( /// public string PropertySource { get; set; } + public override string Message + { + get + { + if (RawMessage == null) + { + string formattedLocation = File == null ? PropertySource : $"{File} ({LineNumber},{ColumnNumber})"; + RawMessage = FormatResourceStringIgnoreCodeAndKeyword("PropertyAssignment", PropertyName, PropertyValue, formattedLocation); + } + + return RawMessage; + } + } + internal override void WriteToStream(BinaryWriter writer) { base.WriteToStream(writer); diff --git a/src/Framework/PropertyReassignmentEventArgs.cs b/src/Framework/PropertyReassignmentEventArgs.cs index 29ec2935e0b..d7477ee8caa 100644 --- a/src/Framework/PropertyReassignmentEventArgs.cs +++ b/src/Framework/PropertyReassignmentEventArgs.cs @@ -41,12 +41,47 @@ public PropertyReassignmentEventArgs( string message, string helpKeyword = null, string senderName = null, - MessageImportance importance = MessageImportance.Low) : base(message, helpKeyword, senderName, importance) + MessageImportance importance = MessageImportance.Low) + : base(message, helpKeyword, senderName, importance) { - this.PropertyName = propertyName; - this.PreviousValue = previousValue; - this.NewValue = newValue; - this.Location = location; + PropertyName = propertyName; + PreviousValue = previousValue; + NewValue = newValue; + Location = location; + } + + /// + /// Creates an instance of the class. + /// + /// The name of the property whose value was reassigned. + /// The previous value of the reassigned property. + /// The new value of the reassigned property. + /// The property location (XML, command line, etc). + /// The file associated with the event. + /// The line number (0 if not applicable). + /// The column number (0 if not applicable). + /// The message of the property. + /// The help keyword. + /// The sender name of the event. + /// The importance of the message. + public PropertyReassignmentEventArgs( + string propertyName, + string previousValue, + string newValue, + string location, + string file, + int line, + int column, + string message, + string helpKeyword = null, + string senderName = null, + MessageImportance importance = MessageImportance.Low) + : base(subcategory: null, code: null, file: file, lineNumber: line, columnNumber: column, 0, 0, message, helpKeyword, senderName, importance) + { + PropertyName = propertyName; + PreviousValue = previousValue; + NewValue = newValue; + Location = location; } /// @@ -75,7 +110,8 @@ public override string Message { if (RawMessage == null) { - RawMessage = FormatResourceStringIgnoreCodeAndKeyword("PropertyReassignment", PropertyName, NewValue, PreviousValue, Location); + string formattedLocation = File == null ? Location : $"{File} ({LineNumber},{ColumnNumber})"; + RawMessage = FormatResourceStringIgnoreCodeAndKeyword("PropertyReassignment", PropertyName, NewValue, PreviousValue, formattedLocation); } return RawMessage; diff --git a/src/Framework/UninitializedPropertyReadEventArgs.cs b/src/Framework/UninitializedPropertyReadEventArgs.cs index 781c8c33bc8..7980bdb5485 100644 --- a/src/Framework/UninitializedPropertyReadEventArgs.cs +++ b/src/Framework/UninitializedPropertyReadEventArgs.cs @@ -51,11 +51,25 @@ internal override void WriteToStream(BinaryWriter writer) writer.WriteOptionalString(PropertyName); } + internal override void CreateFromStream(BinaryReader reader, int version) { base.CreateFromStream(reader, version); PropertyName = reader.ReadOptionalString(); } + + public override string Message + { + get + { + if (RawMessage == null) + { + RawMessage = FormatResourceStringIgnoreCodeAndKeyword("UninitializedPropertyRead", PropertyName); + } + + return RawMessage; + } + } } } diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index aeddef7aba4..c12cd96ec57 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -835,6 +835,9 @@ public static ExitType Execute( { using (ProjectCollection collection = new(globalProperties, loggers, ToolsetDefinitionLocations.Default)) { + // globalProperties collection contains values only from CommandLine at this stage populated by ProcessCommandLineSwitches + collection.PropertiesFromCommandLine = [.. globalProperties.Keys]; + Project project = collection.LoadProject(projectFile, globalProperties, toolsVersion); if (getResultOutputFile.Length == 0) @@ -1393,6 +1396,9 @@ internal static bool BuildProject( useAsynchronousLogging: true, reuseProjectRootElementCache: s_isServerNode); + // globalProperties collection contains values only from CommandLine at this stage populated by ProcessCommandLineSwitches + projectCollection.PropertiesFromCommandLine = [.. globalProperties.Keys]; + if (toolsVersion != null && !projectCollection.ContainsToolset(toolsVersion)) { ThrowInvalidToolsVersionInitializationException(projectCollection.Toolsets, toolsVersion);