diff --git a/documentation/specs/dotnet-run-for-maui.md b/documentation/specs/dotnet-run-for-maui.md
index 33fd374d8b67..d66d1d67f4a2 100644
--- a/documentation/specs/dotnet-run-for-maui.md
+++ b/documentation/specs/dotnet-run-for-maui.md
@@ -86,6 +86,7 @@ to subsequent build, deploy, and run steps._
* `build`: unchanged, but is passed `-p:Device` and optionally `-p:RuntimeIdentifier`
if the selected device provided a `%(RuntimeIdentifier)` metadata value.
+ Environment variables from `-e` are passed as `@(RuntimeEnvironmentVariable)` items.
* `deploy`
@@ -96,12 +97,15 @@ to subsequent build, deploy, and run steps._
`-p:Device` global MSBuild property, and optionally `-p:RuntimeIdentifier`
if the selected device provided a `%(RuntimeIdentifier)` metadata value.
+ * Environment variables from `-e` are passed as `@(RuntimeEnvironmentVariable)` items.
+
* This step needs to run, even with `--no-build`, as you may have
selected a different device.
-* `ComputeRunArguments`: unchanged, but is passed `-p:Device` and optionally
- `-p:RuntimeIdentifier` if the selected device provided a `%(RuntimeIdentifier)`
- metadata value.
+* `ComputeRunArguments`: unchanged, but is passed `-p:Device` and
+ optionally `-p:RuntimeIdentifier` if the selected device provided a
+ `%(RuntimeIdentifier)` metadata value. Environment variables from
+ `-e` are passed as `@(RuntimeEnvironmentVariable)` items.
* `run`: unchanged. `ComputeRunArguments` should have set a valid
`$(RunCommand)` and `$(RunArguments)` using the value supplied by
@@ -146,6 +150,89 @@ A new `--device` switch will:
* The iOS and Android workloads will know how to interpret `$(Device)`
to select an appropriate device, emulator, or simulator.
+## Environment Variables
+
+The `dotnet run` command supports passing environment variables via the
+`-e` or `--environment` option:
+
+```dotnetcli
+dotnet run -e FOO=BAR -e ANOTHER=VALUE
+```
+
+These environment variables are:
+
+1. **Passed to the running application** - as process environment
+ variables when the app is launched.
+
+2. **Passed to MSBuild during build, deploy, and ComputeRunArguments** -
+ as `@(RuntimeEnvironmentVariable)` items that workloads can consume.
+ **This behavior is opt-in**: projects must declare the `RuntimeEnvironmentVariableSupport`
+ project capability to receive these items.
+
+```xml
+
+
+
+
+```
+
+This allows workloads (iOS, Android, etc.) to access environment
+variables during the `build`, `DeployToDevice`, and `ComputeRunArguments` target execution.
+
+### Opting In
+
+To receive environment variables as MSBuild items, projects must opt in by declaring
+the `RuntimeEnvironmentVariableSupport` project capability:
+
+```xml
+
+
+
+```
+
+Mobile workloads (iOS, Android, etc.) should declare this capability in their SDK targets
+so that all projects using those workloads automatically opt in.
+
+Workloads can consume these items in their MSBuild targets:
+
+```xml
+
+
+
+
+```
+
+### Implementation Details
+
+For the **build step**, which uses out-of-process MSBuild via `dotnet build`,
+environment variables are injected by creating a temporary `.props` file.
+The file is created in the project's `$(IntermediateOutputPath)` directory
+(e.g., `obj/Debug/net11.0-android/dotnet-run-env.props`). The path is
+obtained from the project evaluation performed during target framework and
+device selection. If `IntermediateOutputPath` is not available, the file
+falls back to the `obj/` directory.
+
+The file is passed to MSBuild via the `CustomBeforeMicrosoftCommonProps` property,
+ensuring the items are available early in evaluation.
+The temporary file is automatically deleted after the build completes.
+
+The generated props file looks like:
+
+```xml
+
+
+
+
+
+
+```
+
+For the **deploy step** (`DeployToDevice` target) and
+**ComputeRunArguments target**, which use in-process MSBuild,
+environment variables are added directly as
+`@(RuntimeEnvironmentVariable)` items to the `ProjectInstance` before
+invoking the target.
+
## Binary Logs for Device Selection
When using `-bl` with `dotnet run`, all MSBuild operations are logged to a single
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs
index 598d1dfe3483..ee1f84a8a888 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs
@@ -33,10 +33,26 @@ public static class Constants
public const string DeployToDevice = nameof(DeployToDevice);
public const string CoreCompile = nameof(CoreCompile);
+ // MSBuild items
+ internal const string RuntimeEnvironmentVariable = nameof(RuntimeEnvironmentVariable);
+
// MSBuild item metadata
public const string Identity = nameof(Identity);
public const string FullPath = nameof(FullPath);
+ // MSBuild properties
+ public const string CustomBeforeMicrosoftCommonProps = nameof(CustomBeforeMicrosoftCommonProps);
+ public const string IntermediateOutputPath = nameof(IntermediateOutputPath);
+
+ // MSBuild items for project capabilities
+ public const string ProjectCapability = nameof(ProjectCapability);
+
+ ///
+ /// Project capability that workloads declare to opt in to receiving environment variables as MSBuild items.
+ /// When present, 'dotnet run -e' will pass environment variables as @(RuntimeEnvironmentVariable) items.
+ ///
+ public const string RuntimeEnvironmentVariableSupport = nameof(RuntimeEnvironmentVariableSupport);
+
// MSBuild CLI flags
///
diff --git a/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs
new file mode 100644
index 000000000000..7c51ec53f605
--- /dev/null
+++ b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs
@@ -0,0 +1,147 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.ObjectModel;
+using System.Xml;
+using Microsoft.Build.Execution;
+using Microsoft.DotNet.Cli.Utils;
+
+namespace Microsoft.DotNet.Cli.Commands.Run;
+
+///
+/// Provides utilities for passing environment variables to MSBuild as items.
+/// Environment variables specified via dotnet run -e NAME=VALUE are passed
+/// as <RuntimeEnvironmentVariable Include="NAME" Value="VALUE" /> items.
+///
+internal static class EnvironmentVariablesToMSBuild
+{
+ private const string PropsFileName = "dotnet-run-env.props";
+
+ ///
+ /// Adds environment variables as MSBuild items to a ProjectInstance.
+ /// Use this for in-process MSBuild operations (e.g., DeployToDevice target).
+ ///
+ /// The MSBuild project instance to add items to.
+ /// The environment variables to add.
+ public static void AddAsItems(ProjectInstance projectInstance, IReadOnlyDictionary environmentVariables)
+ {
+ foreach (var (name, value) in environmentVariables)
+ {
+ projectInstance.AddItem(Constants.RuntimeEnvironmentVariable, name, new Dictionary
+ {
+ ["Value"] = value
+ });
+ }
+ }
+
+ ///
+ /// Creates a temporary .props file containing environment variables as MSBuild items.
+ /// Use this for out-of-process MSBuild operations where you need to inject items via
+ /// CustomBeforeMicrosoftCommonProps property.
+ ///
+ /// The full path to the project file. If null or empty, returns null.
+ /// The environment variables to include.
+ ///
+ /// Optional intermediate output path where the file will be created.
+ /// If null or empty, defaults to "obj" subdirectory of the project directory.
+ ///
+ /// The full path to the created props file, or null if no environment variables were specified or projectFilePath is null.
+ public static string? CreatePropsFile(string? projectFilePath, IReadOnlyDictionary environmentVariables, string? intermediateOutputPath = null)
+ {
+ if (string.IsNullOrEmpty(projectFilePath) || environmentVariables.Count == 0)
+ {
+ return null;
+ }
+
+ string projectDirectory = Path.GetDirectoryName(projectFilePath) ?? "";
+
+ // Normalize path separators - MSBuild may return paths with backslashes on non-Windows
+ string normalized = intermediateOutputPath?.Replace('\\', Path.DirectorySeparatorChar) ?? "";
+ string objDir = string.IsNullOrEmpty(normalized)
+ ? Path.Combine(projectDirectory, Constants.ObjDirectoryName)
+ : Path.IsPathRooted(normalized)
+ ? normalized
+ : Path.Combine(projectDirectory, normalized);
+ Directory.CreateDirectory(objDir);
+
+ // Ensure we return a full path for MSBuild property usage
+ string propsFilePath = Path.GetFullPath(Path.Combine(objDir, PropsFileName));
+ using (var stream = File.Create(propsFilePath))
+ {
+ WritePropsFileContent(stream, environmentVariables);
+ }
+
+ return propsFilePath;
+ }
+
+ ///
+ /// Deletes the temporary environment variables props file if it exists.
+ ///
+ /// The path to the props file to delete.
+ public static void DeletePropsFile(string? propsFilePath)
+ {
+ if (propsFilePath is not null && File.Exists(propsFilePath))
+ {
+ try
+ {
+ File.Delete(propsFilePath);
+ }
+ catch (Exception ex)
+ {
+ // Best effort cleanup - don't fail the build if we can't delete the temp file
+ Reporter.Verbose.WriteLine($"Failed to delete temporary props file '{propsFilePath}': {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Adds the props file property to the MSBuild arguments.
+ /// This uses CustomBeforeMicrosoftCommonProps to inject the props file early in evaluation.
+ ///
+ /// The base MSBuild arguments.
+ /// The path to the props file (from ).
+ /// The MSBuild arguments with the props file property added, or the original args if propsFilePath is null.
+ public static MSBuildArgs AddPropsFileToArgs(MSBuildArgs msbuildArgs, string? propsFilePath)
+ {
+ if (propsFilePath is null)
+ {
+ return msbuildArgs;
+ }
+
+ // Add the props file via CustomBeforeMicrosoftCommonProps.
+ // This ensures the items are available early in evaluation, similar to how we add items
+ // directly to ProjectInstance for in-process target invocations.
+ var additionalProperties = new ReadOnlyDictionary(new Dictionary
+ {
+ [Constants.CustomBeforeMicrosoftCommonProps] = propsFilePath
+ });
+
+ return msbuildArgs.CloneWithAdditionalProperties(additionalProperties);
+ }
+
+ ///
+ /// Writes the content of the .props file containing environment variables as items.
+ ///
+ private static void WritePropsFileContent(Stream stream, IReadOnlyDictionary environmentVariables)
+ {
+ using var writer = XmlWriter.Create(stream, new XmlWriterSettings
+ {
+ OmitXmlDeclaration = true,
+ Indent = true
+ });
+
+ writer.WriteStartElement("Project");
+ writer.WriteStartElement("ItemGroup");
+
+ foreach (var (name, value) in environmentVariables)
+ {
+ writer.WriteStartElement(Constants.RuntimeEnvironmentVariable);
+ writer.WriteAttributeString("Include", name);
+ writer.WriteAttributeString("Value", value);
+ writer.WriteEndElement();
+ }
+
+ writer.WriteEndElement(); // ItemGroup
+ writer.WriteEndElement(); // Project
+ }
+}
diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs
index 66e3e05af9ae..92ce418923a1 100644
--- a/src/Cli/dotnet/Commands/Run/RunCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs
@@ -156,7 +156,7 @@ public int Execute()
{
// Pre-run evaluation: Handle target framework and device selection for project-based scenarios
using var selector = ProjectFileFullPath is not null
- ? new RunCommandSelector(ProjectFileFullPath, Interactive, MSBuildArgs, logger)
+ ? new RunCommandSelector(ProjectFileFullPath, Interactive, MSBuildArgs, EnvironmentVariables, logger)
: null;
if (selector is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(selector))
{
@@ -186,7 +186,7 @@ public int Execute()
Reporter.Output.WriteLine(CliCommandStrings.RunCommandBuilding);
}
- EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out projectBuilder);
+ EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out projectBuilder, selector?.IntermediateOutputPath, selector?.HasRuntimeEnvironmentVariableSupport ?? false);
}
else if (EntryPointFileFullPath is not null && launchProfileParseResult.Profile is not ExecutableLaunchProfile)
{
@@ -472,7 +472,7 @@ internal LaunchProfileParseResult ReadLaunchProfileSettings()
return LaunchSettings.ReadProfileSettingsFromFile(launchSettingsPath, LaunchProfile);
}
- private void EnsureProjectIsBuilt(out Func? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? projectBuilder)
+ private void EnsureProjectIsBuilt(out Func? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? projectBuilder, string? intermediateOutputPath, bool hasRuntimeEnvironmentVariableSupport)
{
int buildResult;
if (EntryPointFileFullPath is not null)
@@ -489,11 +489,28 @@ private void EnsureProjectIsBuilt(out Func?
projectFactory = null;
cachedRunProperties = null;
projectBuilder = null;
- buildResult = new RestoringCommand(
- MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]),
- NoRestore || _restoreDoneForDeviceSelection,
- advertiseWorkloadUpdates: false
- ).Execute();
+
+ // Create temporary props file for environment variables only if the project has opted in.
+ // This avoids invalidating incremental builds for projects that don't consume the items.
+ // Use IntermediateOutputPath from earlier project evaluation (via RunCommandSelector), defaulting to "obj" if not available.
+ string? envPropsFile = hasRuntimeEnvironmentVariableSupport
+ ? EnvironmentVariablesToMSBuild.CreatePropsFile(ProjectFileFullPath, EnvironmentVariables, intermediateOutputPath)
+ : null;
+ try
+ {
+ var buildArgs = MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]);
+ buildArgs = EnvironmentVariablesToMSBuild.AddPropsFileToArgs(buildArgs, envPropsFile);
+ buildResult = new RestoringCommand(
+ buildArgs,
+ NoRestore || _restoreDoneForDeviceSelection,
+ advertiseWorkloadUpdates: false
+ ).Execute();
+ }
+ finally
+ {
+ // Clean up temporary props file
+ EnvironmentVariablesToMSBuild.DeletePropsFile(envPropsFile);
+ }
}
if (buildResult != 0)
@@ -575,7 +592,7 @@ private ICommand GetTargetCommandForProject(ProjectLaunchProfile? launchSettings
var project = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger);
ValidatePreconditions(project);
- InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs);
+ InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs, EnvironmentVariables);
var runProperties = RunProperties.FromProject(project).WithApplicationArguments(ApplicationArgs);
command = CreateCommandFromRunProperties(runProperties);
@@ -663,8 +680,15 @@ static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, s
return command;
}
- static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs)
+ static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs, IReadOnlyDictionary environmentVariables)
{
+ // Only add environment variables as MSBuild items if the project has opted in via capability
+ if (project.GetItems(Constants.ProjectCapability)
+ .Any(item => string.Equals(item.EvaluatedInclude, Constants.RuntimeEnvironmentVariableSupport, StringComparison.OrdinalIgnoreCase)))
+ {
+ EnvironmentVariablesToMSBuild.AddAsItems(project, environmentVariables);
+ }
+
List loggersForBuild = [
CommonRunHelpers.GetConsoleLogger(
buildArgs.CloneWithExplicitArgs([$"--verbosity:{LoggerVerbosity.Quiet.ToString().ToLowerInvariant()}", ..buildArgs.OtherMSBuildArgs])
diff --git a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs
index b2016b765bf4..982fa2c65e93 100644
--- a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs
+++ b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs
@@ -28,6 +28,7 @@ internal sealed class RunCommandSelector : IDisposable
private readonly FacadeLogger? _binaryLogger;
private readonly bool _isInteractive;
private readonly MSBuildArgs _msbuildArgs;
+ private readonly IReadOnlyDictionary _environmentVariables;
private ProjectCollection? _collection;
private Microsoft.Build.Evaluation.Project? _project;
@@ -38,20 +39,58 @@ internal sealed class RunCommandSelector : IDisposable
///
public bool HasValidProject { get; private set; }
+ ///
+ /// Gets the IntermediateOutputPath property from the evaluated project.
+ /// This will evaluate the project if it hasn't been evaluated yet.
+ /// Returns null if the project cannot be evaluated or the property is not set.
+ ///
+ public string? IntermediateOutputPath
+ {
+ get
+ {
+ if (OpenProjectIfNeeded(out var projectInstance))
+ {
+ return projectInstance.GetPropertyValue(Constants.IntermediateOutputPath);
+ }
+ return null;
+ }
+ }
+
+ ///
+ /// Gets whether the project has opted in to receiving environment variables as MSBuild items.
+ /// When true, 'dotnet run -e' will pass environment variables as @(RuntimeEnvironmentVariable) items
+ /// via CustomBeforeMicrosoftCommonProps.
+ ///
+ public bool HasRuntimeEnvironmentVariableSupport
+ {
+ get
+ {
+ if (OpenProjectIfNeeded(out var projectInstance))
+ {
+ return projectInstance.GetItems(Constants.ProjectCapability)
+ .Any(item => string.Equals(item.EvaluatedInclude, Constants.RuntimeEnvironmentVariableSupport, StringComparison.OrdinalIgnoreCase));
+ }
+ return false;
+ }
+ }
+
/// Path to the project file to evaluate
/// Whether to prompt the user for selections
/// MSBuild arguments containing properties and verbosity settings
+ /// Environment variables to pass to MSBuild targets as items
/// Optional binary logger for MSBuild operations. The logger will not be disposed by this class.
public RunCommandSelector(
string projectFilePath,
bool isInteractive,
MSBuildArgs msbuildArgs,
+ IReadOnlyDictionary environmentVariables,
FacadeLogger? binaryLogger = null)
{
_projectFilePath = projectFilePath;
_globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs);
_isInteractive = isInteractive;
_msbuildArgs = msbuildArgs;
+ _environmentVariables = environmentVariables;
_binaryLogger = binaryLogger;
}
@@ -488,6 +527,12 @@ public bool TryDeployToDevice()
return true;
}
+ // Add environment variables as items before building the target, only if opted in
+ if (HasRuntimeEnvironmentVariableSupport)
+ {
+ EnvironmentVariablesToMSBuild.AddAsItems(projectInstance, _environmentVariables);
+ }
+
// Build the DeployToDevice target
var buildResult = projectInstance.Build(
targets: [Constants.DeployToDevice],
diff --git a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj
index 04ce32c6db1d..8c8991768558 100644
--- a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj
+++ b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj
@@ -3,8 +3,22 @@
Exe
net9.0;$(CurrentTargetFramework)
+
+ false
+
+
+
+
+
+
+
+
+
@@ -49,9 +63,25 @@
+
+
+
+
+
+
+
+
+
+
-
+
+
+
diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs
index c2c1948b12a5..68ac24240d3e 100644
--- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs
+++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs
@@ -376,4 +376,135 @@ public void ItPassesRuntimeIdentifierToDeployToDeviceTarget()
.And.HaveStdOutContaining($"Device: {deviceId}")
.And.HaveStdOutContaining($"RuntimeIdentifier: {rid}");
}
+
+ [Fact]
+ public void ItPassesEnvironmentVariablesToTargets()
+ {
+ var testInstance = _testAssetsManager.CopyTestAsset("DotnetRunDevices", identifier: "EnvVarTargets")
+ .WithSource();
+
+ string deviceId = "test-device-1";
+ string buildBinlogPath = Path.Combine(testInstance.Path, "msbuild.binlog");
+ string runBinlogPath = Path.Combine(testInstance.Path, "msbuild-dotnet-run.binlog");
+
+ var result = new DotnetCommand(Log, "run")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute("--framework", ToolsetInfo.CurrentTargetFramework, "--device", deviceId,
+ "-e", "FOO=BAR", "-e", "ANOTHER=VALUE",
+ "-bl");
+
+ result.Should().Pass();
+
+ // Verify the binlog files were created
+ File.Exists(buildBinlogPath).Should().BeTrue("the build binlog file should be created");
+ File.Exists(runBinlogPath).Should().BeTrue("the run binlog file should be created");
+
+ // Verify environment variables were passed to Build target (out-of-process build)
+ AssertTargetInBinlog(buildBinlogPath, "_LogRuntimeEnvironmentVariableDuringBuild",
+ targets =>
+ {
+ targets.Should().NotBeEmpty("_LogRuntimeEnvironmentVariableDuringBuild target should have executed");
+ var messages = targets.First().FindChildrenRecursive();
+ var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("Build: RuntimeEnvironmentVariable=") == true);
+ envVarMessage.Should().NotBeNull("the Build target should have logged the environment variables");
+ envVarMessage.Text.Should().Contain("FOO=BAR").And.Contain("ANOTHER=VALUE");
+ });
+
+ // Verify environment variables were passed to ComputeRunArguments target (in-process)
+ AssertTargetInBinlog(runBinlogPath, "_LogRuntimeEnvironmentVariableDuringComputeRunArguments",
+ targets =>
+ {
+ targets.Should().NotBeEmpty("_LogRuntimeEnvironmentVariableDuringComputeRunArguments target should have executed");
+ var messages = targets.First().FindChildrenRecursive();
+ var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("ComputeRunArguments: RuntimeEnvironmentVariable=") == true);
+ envVarMessage.Should().NotBeNull("the ComputeRunArguments target should have logged the environment variables");
+ envVarMessage.Text.Should().Contain("FOO=BAR").And.Contain("ANOTHER=VALUE");
+ });
+
+ // Verify environment variables were passed to DeployToDevice target (in-process)
+ AssertTargetInBinlog(runBinlogPath, "DeployToDevice",
+ targets =>
+ {
+ targets.Should().NotBeEmpty("DeployToDevice target should have executed");
+ var messages = targets.First().FindChildrenRecursive();
+ var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("DeployToDevice: RuntimeEnvironmentVariable=") == true);
+ envVarMessage.Should().NotBeNull("the DeployToDevice target should have logged the environment variables");
+ envVarMessage.Text.Should().Contain("FOO=BAR").And.Contain("ANOTHER=VALUE");
+ });
+
+ // Verify the props file was created in the correct IntermediateOutputPath location
+ string tempPropsFile = Path.Combine(testInstance.Path, "obj", "Debug", ToolsetInfo.CurrentTargetFramework, "dotnet-run-env.props");
+ var build = BinaryLog.ReadBuild(buildBinlogPath);
+ var propsFile = build.SourceFiles?.FirstOrDefault(f => f.FullPath.EndsWith("dotnet-run-env.props", StringComparison.OrdinalIgnoreCase));
+ propsFile.Should().NotBeNull("dotnet-run-env.props should be embedded in the binlog");
+ propsFile.FullPath.Should().Be(tempPropsFile, "the props file should be in the IntermediateOutputPath");
+ File.Exists(tempPropsFile).Should().BeFalse("the temporary props file should be deleted after build");
+ }
+
+ [Fact]
+ public void ItDoesNotPassEnvironmentVariablesToTargetsWithoutOptIn()
+ {
+ var testInstance = _testAssetsManager.CopyTestAsset("DotnetRunDevices", identifier: "EnvVarNoOptIn")
+ .WithSource();
+
+ string deviceId = "test-device-1";
+ string buildBinlogPath = Path.Combine(testInstance.Path, "msbuild.binlog");
+ string runBinlogPath = Path.Combine(testInstance.Path, "msbuild-dotnet-run.binlog");
+
+ // Run with EnableRuntimeEnvironmentVariableSupport=false to opt out of the capability
+ var result = new DotnetCommand(Log, "run")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute("--framework", ToolsetInfo.CurrentTargetFramework, "--device", deviceId,
+ "-e", "FOO=BAR", "-e", "ANOTHER=VALUE",
+ "-p:EnableRuntimeEnvironmentVariableSupport=false",
+ "-bl");
+
+ result.Should().Pass();
+
+ // Verify the binlog files were created
+ File.Exists(buildBinlogPath).Should().BeTrue("the build binlog file should be created");
+ File.Exists(runBinlogPath).Should().BeTrue("the run binlog file should be created");
+
+ // Verify _LogRuntimeEnvironmentVariableDuringBuild target did NOT execute (condition failed due to no items)
+ AssertTargetInBinlog(buildBinlogPath, "_LogRuntimeEnvironmentVariableDuringBuild",
+ targets =>
+ {
+ // The target should either not execute, or execute with no environment variable message
+ if (targets.Any())
+ {
+ var messages = targets.First().FindChildrenRecursive();
+ var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("Build: RuntimeEnvironmentVariable=") == true);
+ envVarMessage.Should().BeNull("the Build target should NOT have logged the environment variables when not opted in");
+ }
+ });
+
+ // Verify _LogRuntimeEnvironmentVariableDuringComputeRunArguments target did NOT log env vars
+ AssertTargetInBinlog(runBinlogPath, "_LogRuntimeEnvironmentVariableDuringComputeRunArguments",
+ targets =>
+ {
+ if (targets.Any())
+ {
+ var messages = targets.First().FindChildrenRecursive();
+ var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("ComputeRunArguments: RuntimeEnvironmentVariable=") == true);
+ envVarMessage.Should().BeNull("the ComputeRunArguments target should NOT have logged the environment variables when not opted in");
+ }
+ });
+
+ // Verify DeployToDevice target did NOT log actual env var values
+ AssertTargetInBinlog(runBinlogPath, "DeployToDevice",
+ targets =>
+ {
+ targets.Should().NotBeEmpty("DeployToDevice target should have executed");
+ var messages = targets.First().FindChildrenRecursive();
+ // The message may appear (target has no condition) but should NOT contain actual env var values
+ var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("FOO=BAR") == true || m.Text?.Contains("ANOTHER=VALUE") == true);
+ envVarMessage.Should().BeNull("the DeployToDevice target should NOT have logged the actual environment variable values when not opted in");
+ });
+
+ // Verify no props file was created (since opt-in is false)
+ string tempPropsFile = Path.Combine(testInstance.Path, "obj", "Debug", ToolsetInfo.CurrentTargetFramework, "dotnet-run-env.props");
+ var build = BinaryLog.ReadBuild(buildBinlogPath);
+ var propsFile = build.SourceFiles?.FirstOrDefault(f => f.FullPath.EndsWith("dotnet-run-env.props", StringComparison.OrdinalIgnoreCase));
+ propsFile.Should().BeNull("dotnet-run-env.props should NOT be created when not opted in");
+ }
}