Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions src/Build.UnitTests/Evaluation/SdkResultEvaluation_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,121 @@ public void SdkResolverCanSetEnvVarsThatInfluenceBuild()
instance.GetItems("ExecThingsAsEnvironment").ShouldHaveSingleItem().EvaluatedInclude.ShouldBe("TestEnvVarValue");
}

[Fact]
public void SdkResolverEnvironmentVariablesOverrideAmbientEnvironmentVariables()
{
string originalValue = Environment.GetEnvironmentVariable("SDK_TEST_VAR");
try
{
// Set an ambient environment variable
Environment.SetEnvironmentVariable("SDK_TEST_VAR", "AmbientValue");

var projectOptions = SdkUtilities.CreateProjectOptionsWithResolver(new SdkUtilities.ConfigurableMockSdkResolver((_, _, factory) =>
factory.IndicateSuccess(Path.Combine(_testFolder, "Sdk"), "1.0.0", null, null, null, environmentVariablesToAdd: new Dictionary<string, string>
{
{ "SDK_TEST_VAR", "SdkValue" }
})));

string projectContent = """
<Project Sdk="sdkenvvar">
<PropertyGroup>
<CapturedValue>$(SDK_TEST_VAR)</CapturedValue>
</PropertyGroup>
<Target Name="TestTarget">
<PropertyGroup>
<ExecValue>$(SDK_TEST_VAR)</ExecValue>
</PropertyGroup>
</Target>
</Project>
""";

string projectPath = Path.Combine(_testFolder, "project.proj");
File.WriteAllText(projectPath, projectContent);

string sdkPropsContents = "<Project />";
string sdkPropsPath = Path.Combine(_testFolder, "Sdk", "Sdk.props");
Directory.CreateDirectory(Path.Combine(_testFolder, "Sdk"));
File.WriteAllText(sdkPropsPath, sdkPropsContents);

string sdkTargetsContents = @"<Project />";
string sdkTargetsPath = Path.Combine(_testFolder, "Sdk", "Sdk.targets");
File.WriteAllText(sdkTargetsPath, sdkTargetsContents);

var project = CreateProject(projectPath, projectOptions);
var instance = project.CreateProjectInstance();

_logger.AssertNoErrors();
_logger.AssertNoWarnings();

// The SDK value should override the ambient environment variable
instance.GetPropertyValue("CapturedValue").ShouldBe("SdkValue");

instance.Build(["TestTarget"], [_logger], out var targetOutputs);

instance.GetPropertyValue("ExecValue").ShouldBe("SdkValue");
}
finally
{
Environment.SetEnvironmentVariable("SDK_TEST_VAR", originalValue);
}
}

[Fact]
public void FirstSdkEnvironmentVariableWinsOverSubsequentSdks()
{
var projectOptions = SdkUtilities.CreateProjectOptionsWithResolver(new SdkUtilities.ConfigurableMockSdkResolver((sdkReference, _, factory) =>
{
// First SDK sets the variable
if (sdkReference.Name == "FirstSdk")
{
return factory.IndicateSuccess(Path.Combine(_testFolder, "Sdk1"), "1.0.0", null, null, null, environmentVariablesToAdd: new Dictionary<string, string>
{
{ "SHARED_VAR", "FirstValue" }
});
}
// Second SDK tries to set it, but should be ignored
else if (sdkReference.Name == "SecondSdk")
{
return factory.IndicateSuccess(Path.Combine(_testFolder, "Sdk2"), "1.0.0", null, null, null, environmentVariablesToAdd: new Dictionary<string, string>
{
{ "SHARED_VAR", "SecondValue" }
});
}
return null;
}));

string projectContent = """
<Project>
<Import Project="Sdk.props" Sdk="FirstSdk"/>
<Import Project="Sdk.props" Sdk="SecondSdk"/>
<PropertyGroup>
<CapturedValue>$(SHARED_VAR)</CapturedValue>
</PropertyGroup>
</Project>
""";

string projectPath = Path.Combine(_testFolder, "project.proj");
File.WriteAllText(projectPath, projectContent);

string sdk1PropsContents = "<Project />";
string sdk1PropsPath = Path.Combine(_testFolder, "Sdk1", "Sdk.props");
Directory.CreateDirectory(Path.Combine(_testFolder, "Sdk1"));
File.WriteAllText(sdk1PropsPath, sdk1PropsContents);

string sdk2PropsContents = "<Project />";
string sdk2PropsPath = Path.Combine(_testFolder, "Sdk2", "Sdk.props");
Directory.CreateDirectory(Path.Combine(_testFolder, "Sdk2"));
File.WriteAllText(sdk2PropsPath, sdk2PropsContents);

var project = CreateProject(projectPath, projectOptions);

_logger.AssertNoErrors();
_logger.AssertNoWarnings();

// The first SDK's value should win
project.GetPropertyValue("CapturedValue").ShouldBe("FirstValue");
}

public void Dispose()
{
_env.Dispose();
Expand Down
21 changes: 21 additions & 0 deletions src/Build/BackEnd/Components/SdkResolution/SdkResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public void Translate(ITranslator translator)
keyTranslator: (ITranslator t, ref string s) => t.Translate(ref s),
valueTranslator: SdkResultTranslationHelpers.Translate,
dictionaryCreator: count => new Dictionary<string, SdkResultItem>(count, StringComparer.OrdinalIgnoreCase));
translator.TranslateDictionary(ref _environmentVariablesToAdd, count => new Dictionary<string, string>(count, StringComparer.OrdinalIgnoreCase));

translator.Translate(ref _sdkReference);
}
Expand All @@ -112,6 +113,7 @@ public override bool Equals(object obj)
_additionalPaths?.Count == result._additionalPaths?.Count &&
_propertiesToAdd?.Count == result._propertiesToAdd?.Count &&
_itemsToAdd?.Count == result._propertiesToAdd?.Count &&
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The equality check is comparing _itemsToAdd?.Count with result._propertiesToAdd?.Count instead of result._itemsToAdd?.Count. This is a copy-paste error that makes the equality comparison incorrect.

Suggested change
_itemsToAdd?.Count == result._propertiesToAdd?.Count &&
_itemsToAdd?.Count == result._itemsToAdd?.Count &&

Copilot uses AI. Check for mistakes.
_environmentVariablesToAdd?.Count == result._environmentVariablesToAdd?.Count &&
EqualityComparer<SdkReference>.Default.Equals(_sdkReference, result._sdkReference))
{
if (_additionalPaths != null)
Expand Down Expand Up @@ -147,6 +149,17 @@ public override bool Equals(object obj)
}
}

if (_environmentVariablesToAdd != null)
{
foreach (var envVarToAdd in _environmentVariablesToAdd)
{
if (result._environmentVariablesToAdd[envVarToAdd.Key] != envVarToAdd.Value)
{
return false;
}
}
}

return true;
}

Expand Down Expand Up @@ -185,6 +198,14 @@ public override int GetHashCode()
hashCode = (hashCode * -1521134295) + itemToAdd.Value.GetHashCode();
}
}
if (_environmentVariablesToAdd != null)
{
foreach (var envVarToAdd in _environmentVariablesToAdd)
{
hashCode = (hashCode * -1521134295) + envVarToAdd.Key.GetHashCode();
hashCode = (hashCode * -1521134295) + envVarToAdd.Value.GetHashCode();
}
}

return hashCode;
}
Expand Down
9 changes: 6 additions & 3 deletions src/Build/Definition/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4466,9 +4466,11 @@ public IItemDefinition<ProjectMetadata> GetItemDefinition(string itemType)
/// <param name="value">Environment variable value.</param>
public void AddSdkResolvedEnvironmentVariable(string name, string value)
{
// If the property has already been set as an environment variable or by another SDK, we do not overwrite it.
if (EnvironmentVariablePropertiesDictionary?.Contains(name) == true
|| SdkResolvedEnvironmentVariablePropertiesDictionary?.Contains(name) == true)
ErrorUtilities.VerifyThrowArgumentLength(name);
ErrorUtilities.VerifyThrowArgumentNull(value);

// If another SDK already set it, we do not overwrite it.
if (SdkResolvedEnvironmentVariablePropertiesDictionary?.Contains(name) == true)
{
return;
}
Expand All @@ -4478,6 +4480,7 @@ public void AddSdkResolvedEnvironmentVariable(string name, string value)
SdkResolvedEnvironmentVariablePropertiesDictionary ??= new();
SdkResolvedEnvironmentVariablePropertiesDictionary.Set(property);

// SDK-resolved environment variables override ambient environment variables.
SetProperty(name, value, isGlobalProperty: false, mayBeReserved: false, loggingContext: null);
}

Expand Down
25 changes: 16 additions & 9 deletions src/Build/Instance/ProjectInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1387,14 +1387,11 @@ PropertyDictionary<ProjectPropertyInstance> IEvaluatorData<ProjectPropertyInstan

public void AddSdkResolvedEnvironmentVariable(string name, string value)
{
// If the property has already been set as an environment variable, we do not overwrite it.
if (_environmentVariableProperties.Contains(name))
{
_loggingContext.LogComment(MessageImportance.Low, "SdkEnvironmentVariableAlreadySet", name, value);
return;
}
ErrorUtilities.VerifyThrowArgumentLength(name);
ErrorUtilities.VerifyThrowArgumentNull(value);

// If another SDK already set it, we do not overwrite it.
else if (_sdkResolvedEnvironmentVariableProperties?.Contains(name) == true)
if (_sdkResolvedEnvironmentVariableProperties?.Contains(name) == true)
{
_loggingContext.LogComment(MessageImportance.Low, "SdkEnvironmentVariableAlreadySetBySdk", name, value);
return;
Expand All @@ -1406,8 +1403,18 @@ public void AddSdkResolvedEnvironmentVariable(string name, string value)

_sdkResolvedEnvironmentVariableProperties.Set(property);

// Only set the local property if it does not already exist, prioritizing regular properties defined in XML.
if (GetProperty(name) is null)
// SDK-resolved environment variables override ambient environment variables.
bool overridingAmbient = _environmentVariableProperties.Contains(name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is null-check needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot ensure that null environment variable names are handled in the Project and ProjectInstance handling

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added null checks for both name and value parameters in AddSdkResolvedEnvironmentVariable methods in ProjectInstance.cs and Project.cs. The validation uses ErrorUtilities.VerifyThrowArgumentLength(name) and ErrorUtilities.VerifyThrowArgumentNull(value) to match the pattern used in other MSBuild property-setting methods. (commit c24c943 updated)

if (overridingAmbient)
{
_environmentVariableProperties.Remove(name);
_loggingContext.LogComment(MessageImportance.Low, "SdkEnvironmentVariableOverridingAmbient", name, value);
}

// Set the property, overriding ambient environment variables but not regular properties defined in XML.
// If we're overriding an ambient variable, or if the property doesn't exist, set it.
// Otherwise, prioritize regular properties defined in XML.
if (overridingAmbient || GetProperty(name) is null)
Comment on lines +1414 to +1417
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic in the conditional at line 1414 needs clarification. When overridingAmbient is true, GetProperty(name) will never return null because the ambient property already exists (it was just removed from _environmentVariableProperties). This means the GetProperty(name) is null check is only meaningful when overridingAmbient is false. Consider simplifying this to make the logic clearer, such as checking GetProperty(name) is null first, then handling the override case separately.

Copilot uses AI. Check for mistakes.
{
((IEvaluatorData<ProjectPropertyInstance, ProjectItemInstance, ProjectMetadataInstance, ProjectItemDefinitionInstance>)this)
.SetProperty(name, value, isGlobalProperty: false, mayBeReserved: false, loggingContext: _loggingContext, isEnvironmentVariable: true, isCommandLineProperty: false);
Expand Down
3 changes: 3 additions & 0 deletions src/Build/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,9 @@ Errors: {3}</value>
<data name="SdkEnvironmentVariableSet" xml:space="preserve">
<value>An SDK attempted set the environment variable "{0}" to "{1}".</value>
</data>
<data name="SdkEnvironmentVariableOverridingAmbient" xml:space="preserve">
<value>An SDK set the environment variable "{0}" to "{1}", overriding the ambient environment variable value.</value>
</data>
<data name="UnrecognizedParentElement" xml:space="preserve">
<value>MSB4189: &lt;{1}&gt; is not a valid child of the &lt;{0}&gt; element.</value>
<comment>{StrBegin="MSB4189: "}</comment>
Expand Down
5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading