| File: Commands\Run\EnvironmentVariablesToMSBuild.cs | Web Access |
| Project: src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet) |
// 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; /// <summary> /// Provides utilities for passing environment variables to MSBuild as items. /// Environment variables specified via <c>dotnet run -e NAME=VALUE</c> are passed /// as <c><RuntimeEnvironmentVariable Include="NAME" Value="VALUE" /></c> items. /// </summary> internal static class EnvironmentVariablesToMSBuild { private const string ValueMetadataName = "Value"; /// <summary> /// Determines whether the project has opted in to receiving environment variables as MSBuild items /// by declaring the <see cref="Constants.RuntimeEnvironmentVariableSupport"/> project capability. /// </summary> /// <param name="projectInstance">The MSBuild project instance to inspect.</param> /// <returns><see langword="true"/> if the project declares the capability; otherwise <see langword="false"/>.</returns> public static bool HasRuntimeEnvironmentVariableSupport(ProjectInstance projectInstance) { return projectInstance.GetItems(Constants.ProjectCapability) .Any(item => string.Equals(item.EvaluatedInclude, Constants.RuntimeEnvironmentVariableSupport, StringComparison.OrdinalIgnoreCase)); } /// <summary> /// Adds environment variables as MSBuild items to a ProjectInstance. /// Use this for in-process MSBuild operations (e.g., DeployToDevice target). /// </summary> /// <param name="projectInstance">The MSBuild project instance to add items to.</param> /// <param name="environmentVariables">The environment variables to add.</param> public static void AddAsItems(ProjectInstance projectInstance, IReadOnlyDictionary<string, string> environmentVariables) { foreach (var (name, value) in environmentVariables) { projectInstance.AddItem(Constants.RuntimeEnvironmentVariable, name, new Dictionary<string, string> { [ValueMetadataName] = value }); } } /// <summary> /// Reads the current <c>@(RuntimeEnvironmentVariable)</c> items from a ProjectInstance into a dictionary. /// This captures any additions or modifications made by MSBuild targets (e.g., <c>ComputeRunArguments</c>) /// so they can be applied to the launched process. /// </summary> /// <param name="projectInstance">The MSBuild project instance to read items from.</param> /// <returns>A dictionary mapping environment variable names to their values.</returns> public static IReadOnlyDictionary<string, string> ReadFromItems(ProjectInstance projectInstance) { // Always use an ordinal (case-sensitive) comparer. Although environment variable names // are case-insensitive on Windows, the target (e.g. device/emulator) the app actually // runs on may differ from the OS running 'dotnet run' - for example, building an Android // project on Windows. Switching the comparer based on RuntimeInformation of the current // process would therefore be incorrect, so we preserve every @(RuntimeEnvironmentVariable) // item exactly as MSBuild produced it and leave any case handling to the OS that // ultimately receives the variables. var result = new Dictionary<string, string>(StringComparer.Ordinal); foreach (var item in projectInstance.GetItems(Constants.RuntimeEnvironmentVariable)) { result[item.EvaluatedInclude] = item.GetMetadataValue(ValueMetadataName); } return result; } /// <summary> /// 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 /// <c>CustomBeforeMicrosoftCommonProps</c> property. /// </summary> /// <param name="projectFilePath">The full path to the project file. If null or empty, returns null.</param> /// <param name="environmentVariables">The environment variables to include.</param> /// <param name="intermediateOutputPath"> /// Optional intermediate output path where the file will be created. /// If null or empty, defaults to "obj" subdirectory of the project directory. /// </param> /// <param name="propsFileName">The name of the generated props file.</param> /// <returns>The full path to the created props file, or null if no environment variables were specified or projectFilePath is null.</returns> public static string? CreatePropsFile( string? projectFilePath, IReadOnlyDictionary<string, string> environmentVariables, string propsFileName, 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; } /// <summary> /// Deletes the temporary environment variables props file if it exists. /// </summary> /// <param name="propsFilePath">The path to the props file to delete.</param> 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}"); } } } /// <summary> /// Adds the props file property to the MSBuild arguments. /// This uses <c>CustomBeforeMicrosoftCommonProps</c> to inject the props file early in evaluation. /// </summary> /// <param name="msbuildArgs">The base MSBuild arguments.</param> /// <param name="propsFilePath">The path to the props file (from <see cref="CreatePropsFile"/>).</param> /// <returns>The MSBuild arguments with the props file property added, or the original args if propsFilePath is null.</returns> 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<string, string>(new Dictionary<string, string> { [Constants.CustomBeforeMicrosoftCommonProps] = propsFilePath }); return msbuildArgs.CloneWithAdditionalProperties(additionalProperties); } /// <summary> /// Writes the content of the .props file containing environment variables as items. /// </summary> private static void WritePropsFileContent(Stream stream, IReadOnlyDictionary<string, string> 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(ValueMetadataName, value); writer.WriteEndElement(); } writer.WriteEndElement(); // ItemGroup writer.WriteEndElement(); // Project } }