From 7b6deefd36a5300d3d176af73013e70cbcb4d2f8 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 22 Jan 2026 16:25:37 -0600 Subject: [PATCH 1/8] `dotnet run -e FOO=BAR` passes `@(RuntimeEnvironmentVariable)` Context: https://github.com/dotnet/sdk/issues/52492 In investigating how to pass implement `dotnet-watch` for mobile, we found that we need to make use of: dotnet run -e FOO=BAR Where `FOO=BAR` is passed as an MSBuild item `@(RuntimeEnvironmentVariable)` to the build, `DeployToDevice` target, and `ComputeRunArguments` target: This is straightforward for the in-process MSBuild target invocations, but for the build step, we need to generate a temporary `.props` file and import using `$(CustomBeforeMicrosoftCommonProps)`. In the iOS & Android workloads, we would handle processing the `@(RuntimeEnvironmentVariable)` item to pass the environment variables to the device/emulator when deploying and running the app. I added a test to verify environment variables are passed through correctly to all targets. --- documentation/specs/dotnet-run-for-maui.md | 77 +++++++++- .../Microsoft.DotNet.Cli.Utils/Constants.cs | 7 + .../Run/EnvironmentVariablesToMSBuild.cs | 143 ++++++++++++++++++ src/Cli/dotnet/Commands/Run/RunCommand.cs | 36 +++-- .../dotnet/Commands/Run/RunCommandSelector.cs | 24 +++ .../DotnetRunDevices/DotnetRunDevices.csproj | 18 ++- .../Run/GivenDotnetRunSelectsDevice.cs | 64 ++++++++ 7 files changed, 355 insertions(+), 14 deletions(-) create mode 100644 src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs diff --git a/documentation/specs/dotnet-run-for-maui.md b/documentation/specs/dotnet-run-for-maui.md index 33fd374d8b67..55a04e7a52a7 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,73 @@ 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: + +```xml + + + + +``` + +This allows workloads (iOS, Android, etc.) to access environment +variables during the `build`, `DeployToDevice`, and `ComputeRunArguments` target execution. + +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..aedc96da84fb 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs @@ -33,10 +33,17 @@ 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 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..498bded1e665 --- /dev/null +++ b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs @@ -0,0 +1,143 @@ +// 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) ?? ""; + string objDir = string.IsNullOrEmpty(intermediateOutputPath) + ? Path.Combine(projectDirectory, Constants.ObjDirectoryName) + : Path.IsPathRooted(intermediateOutputPath) + ? intermediateOutputPath + : Path.Combine(projectDirectory, intermediateOutputPath); + Directory.CreateDirectory(objDir); + + string propsFilePath = 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..f1d2bf8f6ad1 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); } 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) { int buildResult; if (EntryPointFileFullPath is not null) @@ -489,11 +489,25 @@ 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 if any are specified + // Use IntermediateOutputPath from earlier project evaluation (via RunCommandSelector), defaulting to "obj" if not available + string? envPropsFile = EnvironmentVariablesToMSBuild.CreatePropsFile(ProjectFileFullPath, EnvironmentVariables, intermediateOutputPath); + 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 +589,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 +677,10 @@ 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) { + 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..b8d630506ffd 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,40 @@ 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; + } + } + /// 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 +509,9 @@ public bool TryDeployToDevice() return true; } + // Add environment variables as items before building the target + 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..50864624feaf 100644 --- a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj +++ b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj @@ -49,9 +49,25 @@ + + + + + + + + + + - + + + diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs index c2c1948b12a5..d1f53ffc381f 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs @@ -376,4 +376,68 @@ 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"); + } } From fcc45c349b308cf9d3a8e0223902ab2dd81f45df Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 27 Jan 2026 11:04:59 -0600 Subject: [PATCH 2/8] Ensure full path for MSBuild props file Updated the props file path generation to use Path.GetFullPath, ensuring that MSBuild receives an absolute path for the generated props file. This improves reliability when referencing the file in build processes. --- src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs index 498bded1e665..c3c2e8b4b441 100644 --- a/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs +++ b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs @@ -61,7 +61,8 @@ public static void AddAsItems(ProjectInstance projectInstance, IReadOnlyDictiona : Path.Combine(projectDirectory, intermediateOutputPath); Directory.CreateDirectory(objDir); - string propsFilePath = Path.Combine(objDir, PropsFileName); + // 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); From 7fb51d2f8a2c0ae79c8c7a9745689765174cf516 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 27 Jan 2026 11:11:14 -0600 Subject: [PATCH 3/8] Make `$(UseRuntimeEnvironmentVariableItems)=true` opt-in --- documentation/specs/dotnet-run-for-maui.md | 18 ++++- .../Microsoft.DotNet.Cli.Utils/Constants.cs | 6 ++ src/Cli/dotnet/Commands/Run/RunCommand.cs | 19 ++++-- .../dotnet/Commands/Run/RunCommandSelector.cs | 24 ++++++- .../DotnetRunDevices/DotnetRunDevices.csproj | 2 + .../Run/GivenDotnetRunSelectsDevice.cs | 67 +++++++++++++++++++ 6 files changed, 127 insertions(+), 9 deletions(-) diff --git a/documentation/specs/dotnet-run-for-maui.md b/documentation/specs/dotnet-run-for-maui.md index 55a04e7a52a7..e386b91fa27a 100644 --- a/documentation/specs/dotnet-run-for-maui.md +++ b/documentation/specs/dotnet-run-for-maui.md @@ -165,7 +165,9 @@ These environment variables are: variables when the app is launched. 2. **Passed to MSBuild during build, deploy, and ComputeRunArguments** - - as `@(RuntimeEnvironmentVariable)` items that workloads can consume: + as `@(RuntimeEnvironmentVariable)` items that workloads can consume. + **This behavior is opt-in**: projects must set `$(UseRuntimeEnvironmentVariableItems)=true` + to receive these items. ```xml @@ -177,6 +179,20 @@ These environment variables are: 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 setting +the `UseRuntimeEnvironmentVariableItems` property: + +```xml + + true + +``` + +Mobile workloads (iOS, Android, etc.) should set this property in their SDK targets +so that all projects using those workloads automatically opt in. + Workloads can consume these items in their MSBuild targets: ```xml diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs index aedc96da84fb..f4058af2561d 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs @@ -44,6 +44,12 @@ public static class Constants public const string CustomBeforeMicrosoftCommonProps = nameof(CustomBeforeMicrosoftCommonProps); public const string IntermediateOutputPath = nameof(IntermediateOutputPath); + /// + /// Property that workloads set to opt in to receiving environment variables as MSBuild items. + /// When true, 'dotnet run -e' will pass environment variables as @(RuntimeEnvironmentVariable) items. + /// + public const string UseRuntimeEnvironmentVariableItems = nameof(UseRuntimeEnvironmentVariableItems); + // MSBuild CLI flags /// diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index f1d2bf8f6ad1..87f9f8fd009d 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -186,7 +186,7 @@ public int Execute() Reporter.Output.WriteLine(CliCommandStrings.RunCommandBuilding); } - EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out projectBuilder, selector?.IntermediateOutputPath); + EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out projectBuilder, selector?.IntermediateOutputPath, selector?.UseRuntimeEnvironmentVariableItems ?? 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, string? intermediateOutputPath) + private void EnsureProjectIsBuilt(out Func? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? projectBuilder, string? intermediateOutputPath, bool useRuntimeEnvironmentVariableItems) { int buildResult; if (EntryPointFileFullPath is not null) @@ -490,9 +490,12 @@ private void EnsureProjectIsBuilt(out Func? cachedRunProperties = null; projectBuilder = null; - // Create temporary props file for environment variables if any are specified - // Use IntermediateOutputPath from earlier project evaluation (via RunCommandSelector), defaulting to "obj" if not available - string? envPropsFile = EnvironmentVariablesToMSBuild.CreatePropsFile(ProjectFileFullPath, EnvironmentVariables, intermediateOutputPath); + // 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 = useRuntimeEnvironmentVariableItems + ? EnvironmentVariablesToMSBuild.CreatePropsFile(ProjectFileFullPath, EnvironmentVariables, intermediateOutputPath) + : null; try { var buildArgs = MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]); @@ -679,7 +682,11 @@ static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, s static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs, IReadOnlyDictionary environmentVariables) { - EnvironmentVariablesToMSBuild.AddAsItems(project, environmentVariables); + // Only add environment variables as MSBuild items if the project has opted in + if (string.Equals(project.GetPropertyValue(Constants.UseRuntimeEnvironmentVariableItems), "true", StringComparison.OrdinalIgnoreCase)) + { + EnvironmentVariablesToMSBuild.AddAsItems(project, environmentVariables); + } List loggersForBuild = [ CommonRunHelpers.GetConsoleLogger( diff --git a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs index b8d630506ffd..3f5dd7c58006 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs @@ -56,6 +56,23 @@ public string? IntermediateOutputPath } } + /// + /// 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 UseRuntimeEnvironmentVariableItems + { + get + { + if (OpenProjectIfNeeded(out var projectInstance)) + { + return string.Equals(projectInstance.GetPropertyValue(Constants.UseRuntimeEnvironmentVariableItems), "true", 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 @@ -509,8 +526,11 @@ public bool TryDeployToDevice() return true; } - // Add environment variables as items before building the target - EnvironmentVariablesToMSBuild.AddAsItems(projectInstance, _environmentVariables); + // Add environment variables as items before building the target, only if opted in + if (UseRuntimeEnvironmentVariableItems) + { + EnvironmentVariablesToMSBuild.AddAsItems(projectInstance, _environmentVariables); + } // Build the DeployToDevice target var buildResult = projectInstance.Build( diff --git a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj index 50864624feaf..b6c0b489b6e3 100644 --- a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj +++ b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj @@ -3,6 +3,8 @@ Exe net9.0;$(CurrentTargetFramework) + + true diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs index d1f53ffc381f..85276756bc64 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs @@ -440,4 +440,71 @@ public void ItPassesEnvironmentVariablesToTargets() 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 UseRuntimeEnvironmentVariableItems=false to simulate no opt-in + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(testInstance.Path) + .Execute("--framework", ToolsetInfo.CurrentTargetFramework, "--device", deviceId, + "-e", "FOO=BAR", "-e", "ANOTHER=VALUE", + "-p:UseRuntimeEnvironmentVariableItems=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"); + } } From 07f2dc513791ea1cbd98f54300c049175b69c495 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 28 Jan 2026 08:39:59 -0600 Subject: [PATCH 4/8] Workaround https://github.com/dotnet/msbuild/issues/12546 --- .../TestProjects/DotnetRunDevices/DotnetRunDevices.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj index b6c0b489b6e3..688cf047dc18 100644 --- a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj +++ b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj @@ -5,6 +5,10 @@ net9.0;$(CurrentTargetFramework) true + + false From 7cd0e6b2fb38876755a0603eb1f0e1cca400f4c0 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 28 Jan 2026 14:43:23 -0600 Subject: [PATCH 5/8] EnableDefaultEmbeddedResourceItems -> EnableDefaultItems --- .../DotnetRunDevices/DotnetRunDevices.csproj | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj index 688cf047dc18..5ddb87b2bd94 100644 --- a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj +++ b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj @@ -5,12 +5,17 @@ net9.0;$(CurrentTargetFramework) true - - false + false + + + + From b73be5aca664d7366fd2e782d8b92a230a280daf Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 29 Jan 2026 10:41:22 -0600 Subject: [PATCH 6/8] Normalize path separators in intermediate output path Replaces backslashes with the system's directory separator in intermediateOutputPath to ensure consistent path handling across platforms. This prevents issues when MSBuild returns paths with Windows-style separators on non-Windows systems. --- .../Commands/Run/EnvironmentVariablesToMSBuild.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs index c3c2e8b4b441..7c51ec53f605 100644 --- a/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs +++ b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs @@ -54,11 +54,14 @@ public static void AddAsItems(ProjectInstance projectInstance, IReadOnlyDictiona } string projectDirectory = Path.GetDirectoryName(projectFilePath) ?? ""; - string objDir = string.IsNullOrEmpty(intermediateOutputPath) + + // 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(intermediateOutputPath) - ? intermediateOutputPath - : Path.Combine(projectDirectory, intermediateOutputPath); + : Path.IsPathRooted(normalized) + ? normalized + : Path.Combine(projectDirectory, normalized); Directory.CreateDirectory(objDir); // Ensure we return a full path for MSBuild property usage From 3f1510553c62044db9613f41f46fd9adfce92a32 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 2 Feb 2026 14:26:58 -0600 Subject: [PATCH 7/8] `` --- documentation/specs/dotnet-run-for-maui.md | 16 ++++++++-------- src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs | 9 ++++++--- src/Cli/dotnet/Commands/Run/RunCommand.cs | 11 ++++++----- .../dotnet/Commands/Run/RunCommandSelector.cs | 7 ++++--- .../DotnetRunDevices/DotnetRunDevices.csproj | 7 +++++-- .../Run/GivenDotnetRunSelectsDevice.cs | 4 ++-- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/documentation/specs/dotnet-run-for-maui.md b/documentation/specs/dotnet-run-for-maui.md index e386b91fa27a..d66d1d67f4a2 100644 --- a/documentation/specs/dotnet-run-for-maui.md +++ b/documentation/specs/dotnet-run-for-maui.md @@ -166,8 +166,8 @@ These environment variables are: 2. **Passed to MSBuild during build, deploy, and ComputeRunArguments** - as `@(RuntimeEnvironmentVariable)` items that workloads can consume. - **This behavior is opt-in**: projects must set `$(UseRuntimeEnvironmentVariableItems)=true` - to receive these items. + **This behavior is opt-in**: projects must declare the `RuntimeEnvironmentVariableSupport` + project capability to receive these items. ```xml @@ -181,16 +181,16 @@ variables during the `build`, `DeployToDevice`, and `ComputeRunArguments` target ### Opting In -To receive environment variables as MSBuild items, projects must opt in by setting -the `UseRuntimeEnvironmentVariableItems` property: +To receive environment variables as MSBuild items, projects must opt in by declaring +the `RuntimeEnvironmentVariableSupport` project capability: ```xml - - true - + + + ``` -Mobile workloads (iOS, Android, etc.) should set this property in their SDK targets +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: diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs index f4058af2561d..ee1f84a8a888 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs @@ -44,11 +44,14 @@ public static class Constants public const string CustomBeforeMicrosoftCommonProps = nameof(CustomBeforeMicrosoftCommonProps); public const string IntermediateOutputPath = nameof(IntermediateOutputPath); + // MSBuild items for project capabilities + public const string ProjectCapability = nameof(ProjectCapability); + /// - /// Property that workloads set to opt in to receiving environment variables as MSBuild items. - /// When true, 'dotnet run -e' will pass environment variables as @(RuntimeEnvironmentVariable) items. + /// 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 UseRuntimeEnvironmentVariableItems = nameof(UseRuntimeEnvironmentVariableItems); + public const string RuntimeEnvironmentVariableSupport = nameof(RuntimeEnvironmentVariableSupport); // MSBuild CLI flags diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 87f9f8fd009d..92ce418923a1 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -186,7 +186,7 @@ public int Execute() Reporter.Output.WriteLine(CliCommandStrings.RunCommandBuilding); } - EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out projectBuilder, selector?.IntermediateOutputPath, selector?.UseRuntimeEnvironmentVariableItems ?? false); + 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, string? intermediateOutputPath, bool useRuntimeEnvironmentVariableItems) + private void EnsureProjectIsBuilt(out Func? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? projectBuilder, string? intermediateOutputPath, bool hasRuntimeEnvironmentVariableSupport) { int buildResult; if (EntryPointFileFullPath is not null) @@ -493,7 +493,7 @@ private void EnsureProjectIsBuilt(out Func? // 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 = useRuntimeEnvironmentVariableItems + string? envPropsFile = hasRuntimeEnvironmentVariableSupport ? EnvironmentVariablesToMSBuild.CreatePropsFile(ProjectFileFullPath, EnvironmentVariables, intermediateOutputPath) : null; try @@ -682,8 +682,9 @@ static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, s 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 - if (string.Equals(project.GetPropertyValue(Constants.UseRuntimeEnvironmentVariableItems), "true", StringComparison.OrdinalIgnoreCase)) + // 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); } diff --git a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs index 3f5dd7c58006..982fa2c65e93 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs @@ -61,13 +61,14 @@ public string? IntermediateOutputPath /// When true, 'dotnet run -e' will pass environment variables as @(RuntimeEnvironmentVariable) items /// via CustomBeforeMicrosoftCommonProps. /// - public bool UseRuntimeEnvironmentVariableItems + public bool HasRuntimeEnvironmentVariableSupport { get { if (OpenProjectIfNeeded(out var projectInstance)) { - return string.Equals(projectInstance.GetPropertyValue(Constants.UseRuntimeEnvironmentVariableItems), "true", StringComparison.OrdinalIgnoreCase); + return projectInstance.GetItems(Constants.ProjectCapability) + .Any(item => string.Equals(item.EvaluatedInclude, Constants.RuntimeEnvironmentVariableSupport, StringComparison.OrdinalIgnoreCase)); } return false; } @@ -527,7 +528,7 @@ public bool TryDeployToDevice() } // Add environment variables as items before building the target, only if opted in - if (UseRuntimeEnvironmentVariableItems) + if (HasRuntimeEnvironmentVariableSupport) { EnvironmentVariablesToMSBuild.AddAsItems(projectInstance, _environmentVariables); } diff --git a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj index 5ddb87b2bd94..8c8991768558 100644 --- a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj +++ b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj @@ -3,8 +3,6 @@ Exe net9.0;$(CurrentTargetFramework) - - true + + + + diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs index 85276756bc64..e6a01ff62fcb 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs @@ -451,12 +451,12 @@ public void ItDoesNotPassEnvironmentVariablesToTargetsWithoutOptIn() string buildBinlogPath = Path.Combine(testInstance.Path, "msbuild.binlog"); string runBinlogPath = Path.Combine(testInstance.Path, "msbuild-dotnet-run.binlog"); - // Run with UseRuntimeEnvironmentVariableItems=false to simulate no opt-in + // Run with EnableRuntimeEnvironmentVariableSupport=false to disable the capability var result = new DotnetCommand(Log, "run") .WithWorkingDirectory(testInstance.Path) .Execute("--framework", ToolsetInfo.CurrentTargetFramework, "--device", deviceId, "-e", "FOO=BAR", "-e", "ANOTHER=VALUE", - "-p:UseRuntimeEnvironmentVariableItems=false", + "-p:EnableRuntimeEnvironmentVariableSupport=false", "-bl"); result.Should().Pass(); From 007dd663d4333d102a265a76e7030918823639d3 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 2 Feb 2026 16:59:53 -0600 Subject: [PATCH 8/8] Update test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../CommandTests/Run/GivenDotnetRunSelectsDevice.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs index e6a01ff62fcb..68ac24240d3e 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs @@ -451,7 +451,7 @@ public void ItDoesNotPassEnvironmentVariablesToTargetsWithoutOptIn() string buildBinlogPath = Path.Combine(testInstance.Path, "msbuild.binlog"); string runBinlogPath = Path.Combine(testInstance.Path, "msbuild-dotnet-run.binlog"); - // Run with EnableRuntimeEnvironmentVariableSupport=false to disable the capability + // 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,