From a5dc1c5578bd7fe67c889eab0466a7f13fc22d53 Mon Sep 17 00:00:00 2001 From: wheelv5 Date: Wed, 17 Jun 2026 10:16:06 -0400 Subject: [PATCH] Cargo build SDK: MSRustup scenario fixes Targeted fixes to MSRustup usage scenarios, avoiding large-scale refactoring. While the public Rust toolchain and MS-internal MSRustup can coexist, build scenarios using MSRustup will only use MSRustup. Previously, even when the SDK detected MSRustup usage, it still attempted to install the public Rust toolchain before MSRustup, which would fail in network-isolated build environments. New tri-state MSBuild property `SkipPublicRustUpInstall` allows for control over the public Rust toolchain install: - (default) => Auto-detect; skip the public Rust toolchain install when MSRustup use is detected. - true => Always skip the public install (explicit opt-in for edge cases). - false => Always run the public install, restoring original behavior. This is the only change which is not backward-compatible. However, the previous behavior was in direct opposition to the use case, so the decision was to have the new default behavior match the scenario requirements, with an available configuration option to revert if needed. The SDK pinned the MSRustup toolchain install to the target triple for the host architecture. That behavior is preserved by default, but new MSBuild property `MsRustupTargets` allows projects to specify which target triples to install (as a semicolon-separated list), to support cross-compilation (by `--target` arguments to `msrustup toolchain install`). E.g. to support both x64/amd64 and arm64: \aarch64-pc-windows-msvc;x86_64-pc-windows-msvc\ The bundled install script is updated to the current upstream version: - The install directory path is no longer an input: the script installs to the working directory. CargoTask now creates the install directory and sets it as the working directory for the script. - The script no longer directly parses `MSRUSTUP_FILE` for authentication. It does accept `MSRUSTUP_PAT`, so CargoTask's existing logic for setting `MSRUSTUP_PAT` from `MSRUSTUP_FILE` is reused. The SDK forced Cargo commands in non-Debug configurations for MSRustup projects to use a profile selection of `--` (expecting e.g. `--release`). That behavior is preserved by default, but new MSBuild property `CargoProfile` causes the SDK to pass `--profile ` to Cargo instead. This enables using custom profiles defined in Cargo.toml without the side effects from using custom Configuration names, e.g.: \release-windows\ The authentication to crate registries for MSRustup projects leverages the environment variables `CARGO_REGISTRIES_{registryName}_TOKEN`. The variable naming requirement is that '-' in the registry name be converted to '_', which was missing from the conversion logic; it has now been added. --- src/Cargo.UnitTests/CargoTest.cs | 8 ++ src/Cargo/CargoTask.cs | 188 ++++++++++++++++++++++++------- src/Cargo/README.md | 37 +++++- src/Cargo/dist/msrustup.ps1 | 54 ++------- src/Cargo/sdk/InstallCargo.proj | 2 +- src/Cargo/sdk/Sdk.props | 19 ++++ src/Cargo/sdk/Sdk.targets | 16 +-- 7 files changed, 231 insertions(+), 93 deletions(-) diff --git a/src/Cargo.UnitTests/CargoTest.cs b/src/Cargo.UnitTests/CargoTest.cs index 6d661415..668f7d92 100644 --- a/src/Cargo.UnitTests/CargoTest.cs +++ b/src/Cargo.UnitTests/CargoTest.cs @@ -190,6 +190,8 @@ public void ProjectsCanDependOnEachOtherProjects() [Theory] [InlineData("AutomaticallyUseReferenceAssemblyPackages", "true", "true")] [InlineData("AutomaticallyUseReferenceAssemblyPackages", null, "false")] + [InlineData("CargoProfile", "release-windows", "release-windows")] + [InlineData("CargoProfile", null, "")] [InlineData("DebugSymbols", "true", "false")] [InlineData("DebugSymbols", null, "false")] [InlineData("DebugType", "Full", "None")] @@ -217,6 +219,12 @@ public void ProjectsCanDependOnEachOtherProjects() [InlineData("SkipCopyFilesMarkedCopyLocal", "false", "false")] [InlineData("SkipCopyFilesMarkedCopyLocal", "true", "true")] [InlineData("SkipCopyFilesMarkedCopyLocal", null, "")] + [InlineData("SkipPublicRustUpInstall", "true", "true")] + [InlineData("SkipPublicRustUpInstall", "false", "false")] + [InlineData("SkipPublicRustUpInstall", null, "")] + [InlineData("MsRustupTargets", "aarch64-pc-windows-msvc", "aarch64-pc-windows-msvc")] + [InlineData("MsRustupTargets", "aarch64-pc-windows-msvc;x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc;x86_64-pc-windows-msvc")] + [InlineData("MsRustupTargets", null, "")] public void PropertiesHaveExpectedValues(string propertyName, string value, string expectedValue) { ProjectCreator.Templates.CargoProject( diff --git a/src/Cargo/CargoTask.cs b/src/Cargo/CargoTask.cs index d5d7a8a1..33099054 100644 --- a/src/Cargo/CargoTask.cs +++ b/src/Cargo/CargoTask.cs @@ -103,6 +103,29 @@ private enum ExitCode /// public string CargoOutputDir { get; set; } = string.Empty; + /// + /// Gets or sets an optional Cargo profile to pass to cargo as "--profile <value>" for MSRustup. + /// When set, this overrides the behavior of deriving the profile from Configuration. + /// + public string CargoProfile { get; set; } = string.Empty; + + /// + /// Gets or sets an optional override for whether to install the public Rust toolchain via rustup-init.exe. + /// + /// "" (default) - Skip the public install iff rust-toolchain.toml selects an "ms-" channel. + /// "true" - Always skip the public rustup-init download and install. + /// "false" - Always run the public rustup-init install, even when MSRustup is detected. + /// + /// + public string SkipPublicRustUpInstall { get; set; } = string.Empty; + + /// + /// Gets or sets an optional semicolon-separated list of additional target triples to install when running "msrustup toolchain install" + /// (e.g. "aarch64-pc-windows-msvc;x86_64-pc-windows-msvc"). Each value is passed to MSRustup as "--target <triple>". + /// Use this to enable cross-compilation. + /// + public string MsRustupTargets { get; set; } = string.Empty; + /// public override bool Execute() { @@ -178,7 +201,7 @@ private async Task ExecuteAsync() foreach (var registry in GetRegistries(Path.Combine(RepoRoot, _cargoConfigFilePath))) { - var registryName = registry.Key.Trim().ToUpper(); + var registryName = registry.Key.Trim().ToUpper().Replace('-', '_'); _cargoRegistries.Add(registryName); var tokenName = $"CARGO_REGISTRIES_{registryName}_TOKEN"; AddOrUpdateEnvVar(tokenName, $"Bearer {val}"); @@ -247,13 +270,7 @@ private async Task CargoRunCommandAsync(string command, string args) if (!string.IsNullOrEmpty(customCargo)) { - bool isDebugConfiguration = true; - if (!Configuration.Equals("debug", StringComparison.InvariantCultureIgnoreCase)) - { - isDebugConfiguration = false; - } - - return await ExecuteProcessAsync(GetCustomToolChainCargoBin() !, $"{command} {args} --offline {(isDebugConfiguration ? string.Empty : "--" + Configuration.ToLowerInvariant())} --config {Path.Combine(RepoRoot, _cargoConfigFilePath)}", ".", _envVars); + return await ExecuteProcessAsync(GetCustomToolChainCargoBin() !, $"{command} {args} --offline {GetMsRustupProfileArgument()} --config {Path.Combine(RepoRoot, _cargoConfigFilePath)}", ".", _envVars); } return ExitCode.Failed; @@ -262,16 +279,44 @@ private async Task CargoRunCommandAsync(string command, string args) return await ExecuteProcessAsync(_cargoPath, $"{command} {args}", ".", _envVars); } + /// + /// For MSRustup, determines the appropriate Cargo profile argument to pass based on the CargoProfile and Configuration properties. + /// + /// The Cargo profile argument string, if needed; else an empty string. + private string GetMsRustupProfileArgument() + { + // Explicit CargoProfile wins over the Configuration-derived value. + if (!string.IsNullOrEmpty(CargoProfile)) + { + return $"--profile {CargoProfile}"; + } + + // No flag for the default (Debug) profile. + if (string.IsNullOrEmpty(Configuration) || Configuration.Equals("debug", StringComparison.InvariantCultureIgnoreCase)) + { + return string.Empty; + } + + // Use the Configuration as a Cargo profile shorthand (e.g. "--release"). + return "--" + Configuration.ToLowerInvariant(); + } + private async Task DownloadAndInstallRust() { try { - bool downloadSuccess = await DownloadRustUpAsync(); + bool skipPublicRustUp = ShouldSkipPublicRustUp(); + if (skipPublicRustUp) + { + Log.LogMessage(MessageImportance.Normal, "Skipping public Rust toolchain install."); + } + + bool downloadSuccess = skipPublicRustUp || await DownloadRustUpAsync(); bool installSuccess = false; if (downloadSuccess) { - installSuccess = await InstallRust(); - if (installSuccess) + installSuccess = await InstallRust(skipPublicRustUp); + if (installSuccess && !skipPublicRustUp) { _shouldCleanRustPath = true; } @@ -543,16 +588,26 @@ private async Task DownloadRustUpAsync() return await VerifyInitHashAsync(); } - private async Task InstallRust() + private async Task InstallRust(bool skipPublicRustUp) { var rootToolchainPath = Path.Combine(StartupProj, _rustToolChainFileName); var useMsRustUp = File.Exists(rootToolchainPath) && IsMSToolChain(rootToolchainPath); var rustUpBinary = useMsRustUp ? _msRustUpBinary : _rustUpBinary; + bool msRustupBinaryExists = File.Exists(_msRustUpBinary); bool msRustupToolChainExists = useMsRustUp && !string.IsNullOrEmpty(GetCustomToolChainCargoBin()); bool cargoPathAndRustPathsExists = Directory.Exists(_cargoHome) && Directory.Exists(_rustUpHome); bool cargoBinaryExists = File.Exists(_cargoPath); - if ((msRustupToolChainExists && cargoPathAndRustPathsExists && useMsRustUp) || cargoPathAndRustPathsExists && cargoBinaryExists && !useMsRustUp) + // Early-return when everything we need is already installed. + if (skipPublicRustUp && useMsRustUp) + { + // MSRustup-only flow: only require MSRustup binary and the requested toolchain. + if (msRustupBinaryExists && msRustupToolChainExists) + { + return true; + } + } + else if ((msRustupToolChainExists && cargoPathAndRustPathsExists && useMsRustUp) || (cargoPathAndRustPathsExists && cargoBinaryExists && !useMsRustUp)) { return true; } @@ -592,7 +647,8 @@ private async Task InstallRust() } } - if ((!cargoBinaryExists && !useMsRustUp) || !cargoPathAndRustPathsExists) + // Public rustup-init step. Skipped when the caller asks us to skip the public toolchain entirely, or MSRustup-only is auto-detected. + if (!skipPublicRustUp && ((!cargoBinaryExists && !useMsRustUp) || !cargoPathAndRustPathsExists)) { Log.LogMessage(MessageImportance.Normal, "Installing Rust"); exitCode = await ExecuteProcessAsync(_rustUpInitBinary, "-y", ".", _envVars); @@ -605,33 +661,10 @@ private async Task InstallRust() Log.LogError("Rust failed to install successfully"); return false; } - - if (useMsRustUp) - { - string? workingDirPart = new DirectoryInfo(BuildEngine.ProjectFileOfTaskNode).Parent?.Parent?.FullName; - - if (Directory.Exists(workingDirPart)) - { - Log.LogMessage(MessageImportance.Normal, "Installing MSRustup"); - string distRootPath = Path.Combine(workingDirPart!, "content\\dist"); - var installationExitCode = await ExecuteProcessAsync("powershell.exe", $".\\msrustup.ps1 '{_msRustUpHome}'", distRootPath, _envVars); - if (installationExitCode == ExitCode.Succeeded) - { - Log.LogMessage(MessageImportance.Normal, "Installed MSRustup successfully"); - } - else - { - Log.LogError("MSRustup failed to installed successfully"); - return false; - } - } - } } if (useMsRustUp) { - Log.LogMessage(MessageImportance.Normal, "Installing custom toolchain"); - if (string.IsNullOrEmpty(_rustUpFile) || !File.Exists(_rustUpFile)) { Log.LogMessage($"MSRUSTUP_FILE environment variable is not set or the file does not exist. Assuming local build."); @@ -642,8 +675,40 @@ private async Task InstallRust() var val = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(File.ReadAllText(_rustUpFile))); AddOrUpdateEnvVar("MSRUSTUP_PAT", val); } + } + + // Install MSRustup itself (independent of the public rustup install) when needed. + if (useMsRustUp && !msRustupBinaryExists) + { + string? workingDirPart = new DirectoryInfo(BuildEngine.ProjectFileOfTaskNode).Parent?.Parent?.FullName; + + if (Directory.Exists(workingDirPart)) + { + Log.LogMessage(MessageImportance.Normal, "Installing MSRustup"); + string scriptPath = Path.Combine(workingDirPart!, "content\\dist", "msrustup.ps1"); + + // The MSRustup script installs the compiler and tools into the working directory. + // Ensure the desired install location exists, then run Powershell with that directory as the working directory. + Directory.CreateDirectory(_msRustUpHome); + + var installationExitCode = await ExecuteProcessAsync(fileName: "powershell.exe", args: $"-File \"{scriptPath}\"", workingDir: _msRustUpHome, envars: _envVars); + if (installationExitCode == ExitCode.Succeeded) + { + Log.LogMessage(MessageImportance.Normal, "Installed MSRustup successfully"); + } + else + { + Log.LogError("MSRustup failed to installed successfully"); + return false; + } + } + } - exitCodeLatest = await ExecuteProcessAsync(rustUpBinary, $"toolchain install {GetToolChainVersion()}", StartupProj, _envVars); + if (useMsRustUp) + { + Log.LogMessage(MessageImportance.Normal, "Installing custom toolchain"); + + exitCodeLatest = await ExecuteProcessAsync(rustUpBinary, $"toolchain install {GetToolChainVersion()}{GetMsRustupTargetArgs()}", StartupProj, _envVars); if (exitCodeLatest == ExitCode.Succeeded) { @@ -655,7 +720,7 @@ private async Task InstallRust() return false; } } - else + else if (!skipPublicRustUp) { exitCodeLatest = await ExecuteProcessAsync(rustUpBinary, "default stable", ".", _envVars); // ensure we have the latest stable version } @@ -663,6 +728,51 @@ private async Task InstallRust() return exitCode == 0 && exitCodeLatest == 0; } + /// + /// Determines whether to skip the public RustUp installation. + /// + /// Whether to skip the public RustUp installation. + private bool ShouldSkipPublicRustUp() + { + // Explicit override wins over auto-detection. + if (bool.TryParse(SkipPublicRustUpInstall, out bool explicitValue)) + { + return explicitValue; + } + + // Auto-detect: skip when MSRustup is detected from rust-toolchain.toml. + return _isMsRustUp; + } + + /// + /// Builds the target arguments to pass to "msrustup toolchain install" from the property. + /// + /// + /// Each target in the semicolon-separated list becomes its own "--target <triple>" argument in the returned string. + /// Returns the empty string when no targets are configured. + /// + private string GetMsRustupTargetArgs() + { + if (string.IsNullOrWhiteSpace(MsRustupTargets)) + { + return string.Empty; + } + + var sb = new System.Text.StringBuilder(); + foreach (var target in MsRustupTargets.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + var trimmed = target.Trim(); + if (trimmed.Length == 0) + { + continue; + } + + sb.Append(" --target ").Append(trimmed); + } + + return sb.ToString(); + } + private async Task VerifyInitHashAsync() { using var sha256 = SHA256.Create(); diff --git a/src/Cargo/README.md b/src/Cargo/README.md index e598aec4..aa9e3e9b 100644 --- a/src/Cargo/README.md +++ b/src/Cargo/README.md @@ -78,8 +78,39 @@ msbuild /t:clearcargocache ### Using MSRustup (Microsoft internal use only) To enable use of MSRustup, you will need to have a rust-toolchain.toml at the root of your repo. The toml file should include a channel specifier that has "ms-" as a prefix, followed by the channel version. ```toml - [toolchain] - channel - ms- + channel = "ms-" ``` - \ No newline at end of file + +#### Optional MSRustup configuration properties + + The SDK exposes a handful of MSBuild properties for advanced scenarios. + +##### `CargoProfile` + +By default the SDK derives the Cargo profile from the MSBuild `Configuration`: `Debug` uses Cargo's default debug profile, and any other configuration is +passed as the `--` value (so `Release` becomes `--release`). + +Set `CargoProfile` to override this and pass `--profile ` to Cargo instead. This is useful when your `Cargo.toml` defines a custom profile +such as `release-windows`. + +##### `SkipPublicRustUpInstall` + +The `InstallCargo` step normally downloads `rustup-init.exe` from `static.rust-lang.org` and runs it before falling through to the MSRustup install path. + +The SDK auto-skips the public step when it detects an `ms-` channel in `rust-toolchain.toml`. You can also force the behavior explicitly via `SkipPublicRustUpInstall`: +- Unset/empty (default): Skip the public install only when an `ms-` channel is detected. +- `true`: Always skip the public rustup-init download and install. +- `false`: Always run the public rustup-init install, even when MSRustup is detected. + +##### `MsRustupTargets` + +A semicolon-separated list of target triples to install when running `msrustup toolchain install`. +Each value becomes a `--target ` argument. Use this to enable cross-compilation. + +```xml + + aarch64-pc-windows-msvc;x86_64-pc-windows-msvc + +``` + diff --git a/src/Cargo/dist/msrustup.ps1 b/src/Cargo/dist/msrustup.ps1 index 21501d79..13273ba8 100644 --- a/src/Cargo/dist/msrustup.ps1 +++ b/src/Cargo/dist/msrustup.ps1 @@ -1,5 +1,5 @@ # Originally from https://aka.ms/install-msrustup.ps1 -# Version 5 +# Version 6 # This script is expected to be copied into any build system that needs to install the internal Rust toolchain, if # that system cannot use an ADO pipeline and the Rust installer pipeline task. # Updates to this script will be avoided if possible, but if it stops working in your environment, please check the above @@ -9,18 +9,8 @@ # Requires MSRUSTUP_ACCESS_TOKEN or MSRUSTUP_PAT environment variables to be set with a token. # See https://aka.ms/rust for more information. -param ( - [string]$destinationDirectory -) - $ErrorActionPreference = "Stop" - # Create directory if it doesn't exist - Write-Host $destinationDirectory - if (-Not (Test-Path $destinationDirectory)) { - New-Item -Path $destinationDirectory -ItemType Directory - } - Switch ([System.Environment]::OSVersion.Platform.ToString()) { "Win32NT" { $target_rest = 'pc-windows-msvc'; Break } "MacOSX" { $target_rest = 'apple-darwin'; Break } @@ -48,54 +38,34 @@ $package = "rust.msrustup-$target_arch-$target_rest" $feed = if (Test-Path env:MSRUSTUP_FEED_URL) { $env:MSRUSTUP_FEED_URL } else { - 'https://mscodehub.pkgs.visualstudio.com/Rust/_packaging/Rust%40Release/nuget/v3/index.json' + 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Rust.Sdk%40Release/nuget/v3/index.json' } # Get authentication token $token = if (Test-Path env:MSRUSTUP_ACCESS_TOKEN) { "Bearer $env:MSRUSTUP_ACCESS_TOKEN" - } elseif (Test-Path env:MSRUSTUP_PAT) { "Basic $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":$($env:MSRUSTUP_PAT)")))" -} elseif (Test-Path env:MSRUSTUP_FILE) { - $location = $env:MSRUSTUP_FILE - if (Test-Path $location) { - $contents = Get-Content $location -Raw - } - $fromBase64 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($contents)) - "Basic $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":$($fromBase64)")))" -} -elseif ((Get-Command "azureauth" -ErrorAction SilentlyContinue) -ne $null) { +} elseif ((Get-Command "azureauth" -ErrorAction SilentlyContinue) -ne $null) { azureauth ado token --output headervalue } else { - $version = '0.9.1' - $env:AZUREAUTH_VERSION = $version - $script = "${env:TEMP}\install.ps1" - $url = "https://raw.githubusercontent.com/AzureAD/microsoft-authentication-cli/$version/install/install.ps1" - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest $url -OutFile $script; if ($?) { &$script | Out-Null }; if ($?) { rm $script } - - $path = "$env:LOCALAPPDATA\Programs\AzureAuth\$version\azureauth.exe" - & $path ado token --output headervalue | Out-String + Write-Error "MSRUSTUP_ACCESS_TOKEN or MSRUSTUP_PAT must be set or azureauth must be present." + exit 1 } $h = @{'Authorization' = "$token"} -try { - # Download latest NuGet package - $response = Invoke-RestMethod -Headers $h $feed - $base = ($response.resources | Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' }).'@id' - $version = (Invoke-RestMethod -Headers $h "$base/$package/index.json").versions[0] - Invoke-WebRequest -Headers $h "${base}${package}/$version/$package.$version.nupkg" -OutFile 'msrustup.zip' -} catch { - Write-Error "Failed to download msrustup package. Please check your access token and feed URL." - exit 1 -} + +# Download latest NuGet package +$response = Invoke-RestMethod -Headers $h $feed +$base = ($response.resources | Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' }).'@id' +$version = (Invoke-RestMethod -Headers $h "$base/$package/index.json").versions[0] +Invoke-WebRequest -Headers $h "${base}${package}/$version/$package.$version.nupkg" -OutFile 'msrustup.zip' try { # Extract archive Expand-Archive 'msrustup.zip' try { - Move-Item .\msrustup\tools\msrustup* $destinationDirectory + Move-Item .\msrustup\tools\msrustup* . } finally { Remove-Item -Recurse 'msrustup' diff --git a/src/Cargo/sdk/InstallCargo.proj b/src/Cargo/sdk/InstallCargo.proj index c7a40c88..b572fe3e 100644 --- a/src/Cargo/sdk/InstallCargo.proj +++ b/src/Cargo/sdk/InstallCargo.proj @@ -3,6 +3,6 @@ - + \ No newline at end of file diff --git a/src/Cargo/sdk/Sdk.props b/src/Cargo/sdk/Sdk.props index 51ea2e6f..a98b8651 100644 --- a/src/Cargo/sdk/Sdk.props +++ b/src/Cargo/sdk/Sdk.props @@ -91,6 +91,25 @@ AzureAuth $(MSBuildProjectDirectory)\bin + + + + + + diff --git a/src/Cargo/sdk/Sdk.targets b/src/Cargo/sdk/Sdk.targets index e8fc3b78..b6de5320 100644 --- a/src/Cargo/sdk/Sdk.targets +++ b/src/Cargo/sdk/Sdk.targets @@ -112,35 +112,35 @@ - + - + - + - + - + - + - +