From 345b8b9d58798a074a5712f501702c96dbe1562f Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:15:37 -0700 Subject: [PATCH 1/9] [EngSys] Add opt-in Rust native wheel build support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ac28761-49fd-4c09-9885-101a72746ec0 --- doc/dev/packaging.md | 53 +++++ eng/pipelines/templates/jobs/ci.yml | 39 +++- .../templates/stages/archetype-sdk-client.yml | 16 ++ .../steps/build-package-artifacts.yml | 53 ++++- .../steps/install-msrust-toolchain.yml | 89 ++++++++ .../steps/resolve-build-platforms.yml | 3 + eng/tools/azure-sdk-tools/ci_tools/build.py | 8 +- .../ci_tools/parsing/parse_functions.py | 15 ++ .../tests/test_build_interactions.py | 83 ++++++- .../tests/test_native_build_pipeline.py | 203 ++++++++++++++++++ .../tests/test_parse_functionality.py | 51 +++++ 11 files changed, 603 insertions(+), 10 deletions(-) create mode 100644 eng/pipelines/templates/steps/install-msrust-toolchain.yml create mode 100644 eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py diff --git a/doc/dev/packaging.md b/doc/dev/packaging.md index 3e6a8ee8031d..3849530b394c 100644 --- a/doc/dev/packaging.md +++ b/doc/dev/packaging.md @@ -4,6 +4,59 @@ > **Note:** This document covers legacy packaging using `setup.py`. New packages should use `pyproject.toml` instead. See the [`sdk/template/azure-template`](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/template/azure-template) for the current package template. +## Native wheel builds in CI + +`sdk_build` uses `cibuildwheel` when a package declares setuptools `ext_modules` +or a `[tool.cibuildwheel]` table in `pyproject.toml`. The latter supports +non-setuptools backends, including Maturin/PyO3. The package owns its wheel +matrix, backend requirements, and native build hooks. + +Set `signBinaries: true` on the package's artifact entry in the service `ci.yml` +to enable Windows and macOS wheel builds and the existing binary-signing release +path. The platform resolver uses this artifact property, not package names. + +Packages using the Microsoft Rust toolchain can opt in through +`archetype-sdk-client.yml`: + +```yaml +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: myservice + InstallMsRustToolchain: true + MsRustWorkingDirectory: $(Build.SourcesDirectory)/sdk/myservice/azure-myservice + MsRustToolchainFeed: https://pkgs.dev.azure.com/azure-sdk/_packaging//nuget/v3/index.json + MsRustAdditionalTargets: aarch64-pc-windows-msvc + Artifacts: + - name: azure-myservice + safeName: azuremyservice + signBinaries: true +``` + +Replace `` with an approved organization-local feed that +provides the internal Rust toolchain. No prototype test feed is selected by +default. The working directory must contain `rust-toolchain.toml`. +The installer refuses to overwrite an existing Cargo configuration in its +`CARGO_HOME`; provide a clean build environment. +Rust installation is disabled by default and applies only to wheel-build jobs. +Opted-in jobs have a 240-minute timeout and a 210-minute package-build timeout; +other jobs keep their existing limits. + +Linux wheels build inside manylinux containers. The package's `before-all` hook +must install the internal toolchain and configure Cargo source replacement +inside each container. The pipeline forwards `PIP_INDEX_URL`, +`MSRUSTUP_ACCESS_TOKEN`, `MSRUSTUP_FEED_URL`, and the +`CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_*` index, token, and credential-provider +variables. Credentials stay in the build environment, not checked-in files. +On Windows, `PYTHON_ARM64_LIB_DIR` points to the pre-provisioned ARM64 Python +import libraries; a package can use it as `PYO3_CROSS_LIB_DIR` for cross-compilation. + +This opt-in does not provision Rust for source-installing test, documentation, +or analysis jobs. Prepare those jobs separately before enabling a Rust package, +and keep real source-distribution testing. Packages with a static +`[project].version` also need version synchronization with `_version.py` for +nightly builds. No package is activated by adding this shared support. + This article describes the recommendations for defining namespace packaging to release a package inside the `azure` namespace. Being inside the `azure` namespace means that a service `myservice` can be imported using: ```python import azure.myservice diff --git a/eng/pipelines/templates/jobs/ci.yml b/eng/pipelines/templates/jobs/ci.yml index 1e0254e4cbc8..067737406e02 100644 --- a/eng/pipelines/templates/jobs/ci.yml +++ b/eng/pipelines/templates/jobs/ci.yml @@ -61,10 +61,25 @@ parameters: - name: EnvVars type: object default: {} + - name: InstallMsRustToolchain + type: boolean + default: false + - name: MsRustWorkingDirectory + type: string + default: '' + - name: MsRustToolchainFeed + type: string + default: '' + - name: MsRustAdditionalTargets + type: string + default: '' jobs: - job: 'Build_Linux' - timeoutInMinutes: 90 + ${{ if parameters.InstallMsRustToolchain }}: + timeoutInMinutes: 240 + ${{ else }}: + timeoutInMinutes: 90 pool: name: $(LINUXPOOL) @@ -88,9 +103,16 @@ jobs: ArtifactSuffix: linux BuildTargetingString: ${{ parameters.BuildTargetingString }} ExcludePaths: ${{parameters.ExcludePaths}} + InstallMsRustToolchain: ${{ parameters.InstallMsRustToolchain }} + MsRustWorkingDirectory: ${{ parameters.MsRustWorkingDirectory }} + MsRustToolchainFeed: ${{ parameters.MsRustToolchainFeed }} + MsRustAdditionalTargets: ${{ parameters.MsRustAdditionalTargets }} - job: 'Build_Windows' - timeoutInMinutes: 90 + ${{ if parameters.InstallMsRustToolchain }}: + timeoutInMinutes: 240 + ${{ else }}: + timeoutInMinutes: 90 pool: name: $(WINDOWSPOOL) @@ -107,9 +129,16 @@ jobs: ArtifactSuffix: windows BuildTargetingString: ${{ parameters.BuildTargetingString }} ExcludePaths: ${{parameters.ExcludePaths}} + InstallMsRustToolchain: ${{ parameters.InstallMsRustToolchain }} + MsRustWorkingDirectory: ${{ parameters.MsRustWorkingDirectory }} + MsRustToolchainFeed: ${{ parameters.MsRustToolchainFeed }} + MsRustAdditionalTargets: ${{ parameters.MsRustAdditionalTargets }} - job: 'Build_MacOS' - timeoutInMinutes: 90 + ${{ if parameters.InstallMsRustToolchain }}: + timeoutInMinutes: 240 + ${{ else }}: + timeoutInMinutes: 90 pool: name: $(MACPOOL) @@ -133,6 +162,10 @@ jobs: ArtifactSuffix: mac BuildTargetingString: ${{ parameters.BuildTargetingString }} ExcludePaths: ${{parameters.ExcludePaths}} + InstallMsRustToolchain: ${{ parameters.InstallMsRustToolchain }} + MsRustWorkingDirectory: ${{ parameters.MsRustWorkingDirectory }} + MsRustToolchainFeed: ${{ parameters.MsRustToolchainFeed }} + MsRustAdditionalTargets: ${{ parameters.MsRustAdditionalTargets }} - job: 'Build_Extended' displayName: Build Extended diff --git a/eng/pipelines/templates/stages/archetype-sdk-client.yml b/eng/pipelines/templates/stages/archetype-sdk-client.yml index 28b8ded76654..5ca6149585e7 100644 --- a/eng/pipelines/templates/stages/archetype-sdk-client.yml +++ b/eng/pipelines/templates/stages/archetype-sdk-client.yml @@ -87,6 +87,18 @@ parameters: - name: EnvVars type: object default: {} + - name: InstallMsRustToolchain + type: boolean + default: false + - name: MsRustWorkingDirectory + type: string + default: '' + - name: MsRustToolchainFeed + type: string + default: '' + - name: MsRustAdditionalTargets + type: string + default: '' extends: template: /eng/pipelines/templates/stages/1es-redirect.yml @@ -132,6 +144,10 @@ extends: BuildDocs: ${{ parameters.BuildDocs }} DevFeedName: ${{ parameters.DevFeedName }} EnvVars: ${{ parameters.EnvVars }} + InstallMsRustToolchain: ${{ parameters.InstallMsRustToolchain }} + MsRustWorkingDirectory: ${{ parameters.MsRustWorkingDirectory }} + MsRustToolchainFeed: ${{ parameters.MsRustToolchainFeed }} + MsRustAdditionalTargets: ${{ parameters.MsRustAdditionalTargets }} MatrixConfigs: - ${{ each config in parameters.MatrixConfigs }}: - ${{ config }} diff --git a/eng/pipelines/templates/steps/build-package-artifacts.yml b/eng/pipelines/templates/steps/build-package-artifacts.yml index 982426287a10..8ad53eb96dc5 100644 --- a/eng/pipelines/templates/steps/build-package-artifacts.yml +++ b/eng/pipelines/templates/steps/build-package-artifacts.yml @@ -23,6 +23,18 @@ parameters: - name: ExcludePaths type: object default: [] + - name: InstallMsRustToolchain + type: boolean + default: false + - name: MsRustWorkingDirectory + type: string + default: '' + - name: MsRustToolchainFeed + type: string + default: '' + - name: MsRustAdditionalTargets + type: string + default: '' steps: - ${{ if endsWith(variables['Build.Repository.Name'], '-pr') }}: @@ -118,6 +130,15 @@ steps: displayName: 'Install QEMU Dependencies' condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - ${{ if parameters.InstallMsRustToolchain }}: + - template: /eng/pipelines/templates/steps/install-msrust-toolchain.yml + parameters: + ToolchainFeed: ${{ parameters.MsRustToolchainFeed }} + WorkingDirectory: ${{ parameters.MsRustWorkingDirectory }} + ${{ if eq(parameters.ArtifactSuffix, 'windows') }}: + AdditionalTargets: ${{ parameters.MsRustAdditionalTargets }} + Condition: and(succeeded(), or(eq(variables['ENABLE_EXTENSION_BUILD'], 'true'), eq('${{ parameters.ArtifactSuffix }}', 'linux'))) + # Windows: cibuildwheel fetches CPython from api.nuget.org, which is blocked. # Pre-provision the interpreters from the Azure DevOps NuGet feed into the # cibuildwheel cache so it skips the blocked download. @@ -144,26 +165,52 @@ steps: exit 1 } Write-Host "##vso[task.setvariable variable=CIBW_CACHE_PATH]$cache" + + if ($env:INSTALL_MS_RUST_TOOLCHAIN -eq 'true') { + $arm64Libs = Join-Path $nugetCpython "pythonarm64.$version\tools\libs" + if (-not (Test-Path $arm64Libs)) { + throw "Expected ARM64 Python import libraries at $arm64Libs" + } + Write-Host "##vso[task.setvariable variable=PYTHON_ARM64_LIB_DIR]$arm64Libs" + } displayName: 'Pre-provision CPython for cibuildwheel' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['ENABLE_EXTENSION_BUILD'], 'true')) + env: + INSTALL_MS_RUST_TOOLCHAIN: ${{ format('{0}', parameters.InstallMsRustToolchain) }} # Linux: forward PIP_INDEX_URL into the build container so pip fetches build # dependencies from the Azure DevOps feed instead of PyPI. Using # CIBW_ENVIRONMENT_PASS_LINUX (not CIBW_ENVIRONMENT) preserves the package's # own [tool.cibuildwheel].environment (CFLAGS) setting. - pwsh: | - Write-Host "##vso[task.setvariable variable=CIBW_ENVIRONMENT_PASS_LINUX]PIP_INDEX_URL" - displayName: 'Forward PIP_INDEX_URL to cibuildwheel' + $passVariables = @('PIP_INDEX_URL') + if ($env:RUST_ENVIRONMENT_PASS) { + $passVariables += $env:RUST_ENVIRONMENT_PASS.Split(' ') + } + Write-Host "##vso[task.setvariable variable=CIBW_ENVIRONMENT_PASS_LINUX]$($passVariables -join ' ')" + displayName: 'Forward build environment to cibuildwheel' condition: and(succeeded(), or(eq(variables['ENABLE_EXTENSION_BUILD'], 'true'), eq('${{ parameters.ArtifactSuffix }}', 'linux'))) + env: + ${{ if parameters.InstallMsRustToolchain }}: + RUST_ENVIRONMENT_PASS: MSRUSTUP_ACCESS_TOKEN MSRUSTUP_FEED_URL CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_CREDENTIAL_PROVIDER - pwsh: | which python sdk_build -d "$(Build.ArtifactStagingDirectory)" "$(TargetingString)" --inactive --service="${{ parameters.ServiceDirectory }}" displayName: 'Generate Packages' condition: and(succeeded(), or(eq(variables['ENABLE_EXTENSION_BUILD'], 'true'), eq('${{ parameters.ArtifactSuffix }}', 'linux'))) - timeoutInMinutes: 80 + ${{ if parameters.InstallMsRustToolchain }}: + timeoutInMinutes: 210 + ${{ else }}: + timeoutInMinutes: 80 env: CIBW_BUILD_VERBOSITY: 3 + LOGLEVEL: $(SDK_BUILD_LOGLEVEL) + ${{ if parameters.InstallMsRustToolchain }}: + MSRUSTUP_ACCESS_TOKEN: $(System.AccessToken) + MSRUSTUP_FEED_URL: $(MSRUSTUP_FEED_URL) + CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN: $(CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN) + CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_CREDENTIAL_PROVIDER: $(CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_CREDENTIAL_PROVIDER) - pwsh: | which python diff --git a/eng/pipelines/templates/steps/install-msrust-toolchain.yml b/eng/pipelines/templates/steps/install-msrust-toolchain.yml new file mode 100644 index 000000000000..8c7195b1a792 --- /dev/null +++ b/eng/pipelines/templates/steps/install-msrust-toolchain.yml @@ -0,0 +1,89 @@ +parameters: + - name: ToolchainFeed + type: string + default: '' + - name: WorkingDirectory + type: string + default: '' + - name: AdditionalTargets + type: string + default: '' + - name: CratesIoFeed + type: string + default: 'sparse+https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-rust-public~force-auth/Cargo/index/' + - name: Condition + type: string + default: succeeded() + +steps: + - pwsh: | + $ErrorActionPreference = 'Stop' + if (-not $env:RUST_TOOLCHAIN_FEED) { + throw 'An organization-local Rust toolchain feed must be configured.' + } + if (-not $env:RUST_WORKING_DIRECTORY) { + throw 'The directory containing rust-toolchain.toml must be configured.' + } + if (-not (Test-Path (Join-Path $env:RUST_WORKING_DIRECTORY 'rust-toolchain.toml') -PathType Leaf)) { + throw "No rust-toolchain.toml found in $env:RUST_WORKING_DIRECTORY" + } + displayName: 'Validate Rust build configuration' + condition: ${{ parameters.Condition }} + env: + RUST_TOOLCHAIN_FEED: ${{ parameters.ToolchainFeed }} + RUST_WORKING_DIRECTORY: ${{ parameters.WorkingDirectory }} + + - task: RustInstaller@1 + displayName: 'Install internal Rust toolchain' + condition: ${{ parameters.Condition }} + inputs: + toolchainFeed: ${{ parameters.ToolchainFeed }} + workingDirectory: ${{ parameters.WorkingDirectory }} + ${{ if ne(parameters.AdditionalTargets, '') }}: + additionalTargets: ${{ parameters.AdditionalTargets }} + + - pwsh: | + $ErrorActionPreference = 'Stop' + Write-Host "##vso[task.setvariable variable=MSRUSTUP_FEED_URL]$env:RUST_TOOLCHAIN_FEED" + $cargoHome = $env:CARGO_HOME + if (-not $cargoHome) { + throw 'RustInstaller did not define CARGO_HOME.' + } + New-Item -ItemType Directory -Force -Path $cargoHome | Out-Null + $configPath = Join-Path $cargoHome 'config.toml' + if (Test-Path $configPath) { + throw "Refusing to overwrite an existing Cargo configuration: $configPath" + } + + @" + [registries] + azure-sdk-for-rust-public = { index = "$env:CARGO_FEED_URL" } + + [source.crates-io] + replace-with = "azure-sdk-for-rust-public" + + [net] + git-fetch-with-cli = true + "@ | Set-Content -Path $configPath -Encoding utf8 + + Write-Host "##vso[task.setvariable variable=CargoConfigPath]$configPath" + # Linux containers cannot see the host config. Package hooks must write source replacement there. + Write-Host "##vso[task.setvariable variable=CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX]$env:CARGO_FEED_URL" + displayName: 'Configure Rust build feeds' + condition: ${{ parameters.Condition }} + env: + RUST_TOOLCHAIN_FEED: ${{ parameters.ToolchainFeed }} + CARGO_FEED_URL: ${{ parameters.CratesIoFeed }} + + - task: NuGetAuthenticate@1 + displayName: 'Acquire an Azure Artifacts token' + condition: ${{ parameters.Condition }} + + # CargoAuthenticate needs the NuGet token for builds where System.AccessToken is unavailable. + - task: CargoAuthenticate@0 + displayName: 'Authenticate cargo to the Azure Artifacts feed' + condition: ${{ parameters.Condition }} + inputs: + configFile: $(CargoConfigPath) + env: + SYSTEM_ACCESSTOKEN: $(VSS_NUGET_ACCESSTOKEN) diff --git a/eng/pipelines/templates/steps/resolve-build-platforms.yml b/eng/pipelines/templates/steps/resolve-build-platforms.yml index 2de46faf2585..095b6398abf3 100644 --- a/eng/pipelines/templates/steps/resolve-build-platforms.yml +++ b/eng/pipelines/templates/steps/resolve-build-platforms.yml @@ -16,6 +16,7 @@ steps: # variable carries only package names and the decision below needs an artifact property. - pwsh: | $binaryPackages = @() + $logLevel = "INFO" foreach ($packageInfoPath in (Get-ChildItem -Recurse -Force "${{ parameters.PackagePropertiesFolder }}/*.json")) { $packageInfo = Get-Content -Raw -Path $packageInfoPath.FullName | ConvertFrom-Json @@ -28,8 +29,10 @@ steps: if ($binaryPackages) { Write-Host "Targeting package(s) with compiled binaries ($($binaryPackages -join ', ')), enabling extension build." Write-Host "##vso[task.setvariable variable=ENABLE_EXTENSION_BUILD]true" + $logLevel = "DEBUG" } else { Write-Host "No targeted package declares compiled binaries, building on linux only." } + Write-Host "##vso[task.setvariable variable=SDK_BUILD_LOGLEVEL]$logLevel" displayName: Check extension package presence diff --git a/eng/tools/azure-sdk-tools/ci_tools/build.py b/eng/tools/azure-sdk-tools/ci_tools/build.py index 44177c20c026..ca79acf986b3 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/build.py +++ b/eng/tools/azure-sdk-tools/ci_tools/build.py @@ -223,7 +223,7 @@ def create_package( """ Builds a wheel and/or sdist file given a setup.py, pyproject.toml, or directory containing either. - For packages with compiled extensions (ext_modules): + For packages with compiled extensions (ext_modules or a [tool.cibuildwheel] table): - setup.py: uses cibuildwheel to build platform-specific wheels - pyproject.toml: uses cibuildwheel to build platform-specific wheels (respects [tool.cibuildwheel] config) @@ -237,10 +237,12 @@ def create_package( setup_parsed = ParsedSetup.from_path(setup_directory_or_file) should_log_build_output = logger.getEffectiveLevel() <= logging.DEBUG + # Backends such as Maturin compile extensions without declaring setuptools ext_modules. + is_compiled = bool(setup_parsed.ext_modules) or setup_parsed.uses_cibuildwheel if setup_parsed.is_pyproject: # when building with pyproject, check if package has compiled extensions - if enable_wheel and setup_parsed.ext_modules: + if enable_wheel and is_compiled: # Use cibuildwheel for compiled extensions (respects [tool.cibuildwheel] config) run_logged( [sys.executable, "-m", "cibuildwheel", "--output-dir", dist], @@ -289,7 +291,7 @@ def create_package( ) else: if enable_wheel: - if setup_parsed.ext_modules: + if is_compiled: run_logged( [sys.executable, "-m", "cibuildwheel", "--output-dir", dist], cwd=setup_parsed.folder, diff --git a/eng/tools/azure-sdk-tools/ci_tools/parsing/parse_functions.py b/eng/tools/azure-sdk-tools/ci_tools/parsing/parse_functions.py index 2830ee15ae70..48c7619b7f7c 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/parsing/parse_functions.py +++ b/eng/tools/azure-sdk-tools/ci_tools/parsing/parse_functions.py @@ -308,6 +308,7 @@ def __init__( self.is_pyproject = self.setup_filename.endswith(".toml") self.folder = os.path.dirname(self.setup_filename) + self.uses_cibuildwheel: bool = has_cibuildwheel_config(self.folder) @classmethod def from_path(cls, parse_directory_or_file: str): @@ -828,6 +829,20 @@ def parse_setup( return result +def has_cibuildwheel_config(folder: str) -> bool: + """Return whether the package opts into cibuildwheel, including non-setuptools backends.""" + pyproject_filename = os.path.join(folder, "pyproject.toml") + if not os.path.exists(pyproject_filename): + return False + + config = get_value_from_dict(get_pyproject_dict(pyproject_filename), "tool.cibuildwheel") + if config is None: + return False + if not isinstance(config, dict): + raise ValueError(f"Expected a [tool.cibuildwheel] table in {pyproject_filename}") + return True + + def get_pyproject_dict(pyproject_file: str) -> Dict[str, Any]: """ Given a pyproject.toml file, returns a dictionary of a target section. Defaults to `project` section. diff --git a/eng/tools/azure-sdk-tools/tests/test_build_interactions.py b/eng/tools/azure-sdk-tools/tests/test_build_interactions.py index 7bc1546390e8..d21a6c0f1fec 100644 --- a/eng/tools/azure-sdk-tools/tests/test_build_interactions.py +++ b/eng/tools/azure-sdk-tools/tests/test_build_interactions.py @@ -1,6 +1,10 @@ import os, tempfile, shutil +import subprocess +import sys -from ci_tools.build import discover_targeted_packages, build_packages, build +import pytest + +from ci_tools.build import discover_targeted_packages, build_packages, build, create_package repo_root = os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") integration_folder = os.path.join(os.path.dirname(__file__), "integration") @@ -38,3 +42,80 @@ def test_venv_helpers_importable(): from ci_tools.functions import get_venv_call as f_get_venv_call assert f_get_venv_call is get_venv_call + + +@pytest.fixture +def build_commands(monkeypatch): + calls = [] + monkeypatch.setattr("ci_tools.build.run_logged", lambda command, **kwargs: calls.append((command, kwargs))) + monkeypatch.setattr("ci_tools.build.get_pip_list_output", lambda executable: {}) + return calls + + +@pytest.mark.parametrize( + "scenario,cibuildwheel_config,compiled", + [ + ("pyproject_project_def", "", False), + ("pyproject_project_def_with_extension", "", True), + ("setup_py_project_def", "", False), + ("pyproject_project_def", '\n[tool.cibuildwheel]\nbuild = "cp310-*"\n', True), + ("setup_py_project_def", '\n[tool.cibuildwheel]\nbuild = "cp310-*"\n', True), + ], +) +@pytest.mark.parametrize("enable_sdist", [False, True]) +def test_create_package_build_routing(tmp_path, build_commands, scenario, cibuildwheel_config, compiled, enable_sdist): + package = tmp_path / "package" + shutil.copytree(os.path.join(integration_folder, "scenarios", scenario), package) + if cibuildwheel_config: + pyproject = package / "pyproject.toml" + contents = pyproject.read_text() if pyproject.exists() else "" + contents = contents.replace("setuptools.build_meta", "maturin") + pyproject.write_text(contents + cibuildwheel_config) + + destination = tmp_path / "dist" + create_package(str(package), str(destination), enable_sdist=enable_sdist) + + commands = [command for command, _ in build_commands] + cibuildwheel_command = [sys.executable, "-m", "cibuildwheel", "--output-dir", str(destination)] + assert (cibuildwheel_command in commands) is compiled + assert all(options["cwd"] == str(package) for _, options in build_commands) + assert all(options["check"] for command, options in build_commands if "pip" not in command) + if compiled and scenario.startswith("pyproject"): + assert commands == [cibuildwheel_command] + ( + [[sys.executable, "-m", "build", "-s", "-o", str(destination)]] if enable_sdist else [] + ) + elif not compiled and scenario.startswith("pyproject"): + assert commands[-1] == [ + sys.executable, + "-m", + "build", + "-nsw" if enable_sdist else "-nw", + "-o", + str(destination), + ] + elif not compiled: + assert commands[0] == [sys.executable, "setup.py", "bdist_wheel", "-d", str(destination)] + + +def test_sdist_only_does_not_invoke_cibuildwheel(tmp_path, build_commands): + package = tmp_path / "package" + shutil.copytree(os.path.join(integration_folder, "scenarios", "pyproject_project_def"), package) + with (package / "pyproject.toml").open("a") as stream: + stream.write('\n[tool.cibuildwheel]\nbuild = "cp310-*"\n') + + create_package(str(package), str(tmp_path / "dist"), enable_wheel=False) + + assert all("cibuildwheel" not in command for command, _ in build_commands) + assert "-ns" in build_commands[-1][0] + + +def test_cibuildwheel_failure_propagates(tmp_path, monkeypatch): + package = tmp_path / "package" + shutil.copytree(os.path.join(integration_folder, "scenarios", "pyproject_project_def_with_extension"), package) + + def fail_build(command, **kwargs): + raise subprocess.CalledProcessError(1, command) + + monkeypatch.setattr("ci_tools.build.run_logged", fail_build) + with pytest.raises(subprocess.CalledProcessError): + create_package(str(package), str(tmp_path / "dist")) diff --git a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py new file mode 100644 index 000000000000..ec61a689de47 --- /dev/null +++ b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py @@ -0,0 +1,203 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import json +import os +from pathlib import Path +import re +import shutil +import subprocess + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[4] +TEMPLATES = REPO_ROOT / "eng" / "pipelines" / "templates" +RUST_GATE = "${{ if parameters.InstallMsRustToolchain }}" +RUST_PARAMETERS = ( + "InstallMsRustToolchain", + "MsRustWorkingDirectory", + "MsRustToolchainFeed", + "MsRustAdditionalTargets", +) + + +def load_template(relative_path): + return yaml.safe_load((TEMPLATES / relative_path).read_text()) + + +def run_powershell(script, **environment): + powershell = shutil.which("pwsh") + if not powershell: + pytest.skip("PowerShell is required to execute pipeline scripts") + return subprocess.run( + [powershell, "-NoProfile", "-NonInteractive", "-Command", script], + env={**os.environ, **environment}, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + + +def test_rust_support_is_disabled_by_default_and_forwarded(): + archetype = load_template("stages/archetype-sdk-client.yml") + jobs = load_template("jobs/ci.yml") + build = load_template("steps/build-package-artifacts.yml") + for template in (archetype, jobs, build): + defaults = { + parameter["name"]: parameter["default"] + for parameter in template["parameters"] + if parameter["name"] in RUST_PARAMETERS + } + assert defaults["InstallMsRustToolchain"] is False + assert all(defaults[name] == "" for name in RUST_PARAMETERS[1:]) + + build_stage = next(stage for stage in archetype["extends"]["parameters"]["stages"] if stage.get("stage") == "Build") + forwarded = build_stage["jobs"][0]["parameters"] + assert all(forwarded[name] == "${{ parameters." + name + " }}" for name in RUST_PARAMETERS) + + for job in jobs["jobs"]: + if job.get("job") not in ("Build_Linux", "Build_Windows", "Build_MacOS"): + continue + assert job[RUST_GATE]["timeoutInMinutes"] == 240 + assert job["${{ else }}"]["timeoutInMinutes"] == 90 + forwarded = job["steps"][0]["parameters"] + assert all(forwarded[name] == "${{ parameters." + name + " }}" for name in RUST_PARAMETERS) + + +def test_rust_credentials_and_target_setup_are_opt_in(): + build = load_template("steps/build-package-artifacts.yml") + install = next(step[RUST_GATE][0] for step in build["steps"] if RUST_GATE in step) + assert install["template"].endswith("install-msrust-toolchain.yml") + assert install["parameters"]["ToolchainFeed"] == "${{ parameters.MsRustToolchainFeed }}" + assert install["parameters"]["${{ if eq(parameters.ArtifactSuffix, 'windows') }}"] == { + "AdditionalTargets": "${{ parameters.MsRustAdditionalTargets }}" + } + + generate = next(step for step in build["steps"] if step.get("displayName") == "Generate Packages") + assert generate[RUST_GATE]["timeoutInMinutes"] == 210 + assert generate["${{ else }}"]["timeoutInMinutes"] == 80 + assert generate["env"]["LOGLEVEL"] == "$(SDK_BUILD_LOGLEVEL)" + assert generate["env"][RUST_GATE]["MSRUSTUP_ACCESS_TOKEN"] == "$(System.AccessToken)" + assert "MSRUSTUP_ACCESS_TOKEN" not in generate["env"] + + installer = load_template("steps/install-msrust-toolchain.yml") + assert all(step["condition"] == "${{ parameters.Condition }}" for step in installer["steps"]) + cargo_auth = next(step for step in installer["steps"] if step.get("task") == "CargoAuthenticate@0") + assert cargo_auth["env"] == {"SYSTEM_ACCESSTOKEN": "$(VSS_NUGET_ACCESSTOKEN)"} + + provision = next( + step for step in build["steps"] if step.get("displayName") == "Pre-provision CPython for cibuildwheel" + ) + assert provision["env"]["INSTALL_MS_RUST_TOOLCHAIN"] == "${{ format('{0}', parameters.InstallMsRustToolchain) }}" + + +@pytest.mark.parametrize("sign_binaries", [False, True]) +def test_platform_selection_uses_artifact_opt_in(tmp_path, sign_binaries): + package_info = {"Name": "example-native-package", "ArtifactDetails": {"signBinaries": sign_binaries}} + (tmp_path / "example-native-package.json").write_text(json.dumps(package_info)) + resolver = load_template("steps/resolve-build-platforms.yml")["steps"][0]["pwsh"] + resolver = resolver.replace("${{ parameters.PackagePropertiesFolder }}", str(tmp_path)) + + result = run_powershell(resolver) + + assert result.returncode == 0, result.stderr + assert ("variable=ENABLE_EXTENSION_BUILD]true" in result.stdout) is sign_binaries + assert f"variable=SDK_BUILD_LOGLEVEL]{'DEBUG' if sign_binaries else 'INFO'}" in result.stdout + + +@pytest.mark.parametrize("enabled", [False, True]) +def test_container_forwarding_preserves_non_rust_builds(enabled): + build = load_template("steps/build-package-artifacts.yml") + forwarding = next( + step for step in build["steps"] if step.get("displayName") == "Forward build environment to cibuildwheel" + ) + extra = forwarding["env"][RUST_GATE]["RUST_ENVIRONMENT_PASS"] if enabled else "" + + result = run_powershell(forwarding["pwsh"], RUST_ENVIRONMENT_PASS=extra) + + assert result.returncode == 0, result.stderr + assert "variable=CIBW_ENVIRONMENT_PASS_LINUX]PIP_INDEX_URL" in result.stdout + assert ("MSRUSTUP_ACCESS_TOKEN" in result.stdout) is enabled + assert ("CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN" in result.stdout) is enabled + + +@pytest.mark.parametrize("missing", ["feed", "directory", "toolchain"]) +def test_rust_installation_requires_explicit_configuration(tmp_path, missing): + validation = load_template("steps/install-msrust-toolchain.yml")["steps"][0]["pwsh"] + if missing != "toolchain": + (tmp_path / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "ms-prod-1.97"\n') + result = run_powershell( + validation, + RUST_TOOLCHAIN_FEED=( + "" if missing == "feed" else "https://pkgs.dev.azure.com/azure-sdk/_packaging/example/nuget/v3/index.json" + ), + RUST_WORKING_DIRECTORY="" if missing == "directory" else str(tmp_path), + ) + assert result.returncode != 0 + + +def test_rust_installation_accepts_explicit_configuration(tmp_path): + validation = load_template("steps/install-msrust-toolchain.yml")["steps"][0]["pwsh"] + (tmp_path / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "ms-prod-1.97"\n') + result = run_powershell( + validation, + RUST_TOOLCHAIN_FEED="https://pkgs.dev.azure.com/azure-sdk/_packaging/example/nuget/v3/index.json", + RUST_WORKING_DIRECTORY=str(tmp_path), + ) + assert result.returncode == 0, result.stderr + + +def test_cargo_feed_configuration_does_not_overwrite_existing_config(tmp_path): + configure = load_template("steps/install-msrust-toolchain.yml")["steps"][2]["pwsh"] + feed = "sparse+https://pkgs.dev.azure.com/azure-sdk/public/_packaging/example/Cargo/index/" + result = run_powershell( + configure, CARGO_HOME=str(tmp_path), CARGO_FEED_URL=feed, RUST_TOOLCHAIN_FEED="example-feed" + ) + assert result.returncode == 0, result.stderr + config = (tmp_path / "config.toml").read_text() + assert feed in config + assert 'replace-with = "azure-sdk-for-rust-public"' in config + assert "token" not in config + assert "variable=CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX]" + feed in result.stdout + + result = run_powershell( + configure, CARGO_HOME=str(tmp_path), CARGO_FEED_URL=feed, RUST_TOOLCHAIN_FEED="example-feed" + ) + assert result.returncode != 0 + assert (tmp_path / "config.toml").read_text() == config + + +def test_build_powershell_scripts_parse(): + for template_path in ( + "steps/install-msrust-toolchain.yml", + "steps/build-package-artifacts.yml", + "steps/resolve-build-platforms.yml", + ): + template = load_template(template_path) + for step in template["steps"]: + if "pwsh" not in step: + continue + # Azure expands template expressions and the command-valued PIP_EXE macro before parsing. + script = re.sub(r"\$\{\{.*?\}\}", "example", step["pwsh"]) + script = script.replace("$(PIP_EXE)", "python -m pip") + result = run_powershell( + """ + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseInput( + $env:SCRIPT, [ref]$tokens, [ref]$errors + ) | Out-Null + if ($errors.Count) { + $errors | ForEach-Object { Write-Error $_.Message } + exit 1 + } + """, + SCRIPT=script, + ) + assert result.returncode == 0, f"{template_path}: {step['displayName']}: {result.stderr}" diff --git a/eng/tools/azure-sdk-tools/tests/test_parse_functionality.py b/eng/tools/azure-sdk-tools/tests/test_parse_functionality.py index 1405850a0285..705261fde8ec 100644 --- a/eng/tools/azure-sdk-tools/tests/test_parse_functionality.py +++ b/eng/tools/azure-sdk-tools/tests/test_parse_functionality.py @@ -1,6 +1,8 @@ from ci_tools.parsing import parse_require, ParsedSetup +from ci_tools.parsing.parse_functions import has_cibuildwheel_config from packaging.specifiers import SpecifierSet import os +import shutil from unittest.mock import patch import pytest @@ -333,3 +335,52 @@ def test_namespace_discovery_with_substantial_content(): assert result == "test.module" finally: os.unlink(temp_file) + + +@pytest.mark.parametrize( + "config", + [ + "\n[tool.cibuildwheel]\n", + '\n[tool.cibuildwheel]\nbuild = "cp310-*"\n', + '\n[tool.cibuildwheel.linux]\narchs = ["x86_64", "aarch64"]\n', + ], +) +def test_cibuildwheel_config_detected_without_ext_modules(tmp_path, config): + package = tmp_path / "package" + shutil.copytree(pyproject_scenario, package) + with (package / "pyproject.toml").open("a") as stream: + stream.write(config) + + parsed = ParsedSetup.from_path(str(package)) + assert parsed.ext_modules == [] + assert parsed.uses_cibuildwheel is True + assert has_cibuildwheel_config(str(package)) is True + + +@pytest.mark.parametrize("scenario", [pyproject_scenario, setup_project_scenario]) +def test_cibuildwheel_config_absent(scenario): + assert has_cibuildwheel_config(scenario) is False + assert ParsedSetup.from_path(scenario).uses_cibuildwheel is False + + +def test_cibuildwheel_config_detected_for_legacy_setup(tmp_path): + package = tmp_path / "package" + shutil.copytree(setup_project_scenario, package) + (package / "pyproject.toml").write_text('[tool.cibuildwheel]\nbuild = "cp310-*"\n') + + parsed = ParsedSetup.from_path(str(package)) + assert parsed.is_pyproject is False + assert parsed.uses_cibuildwheel is True + + +def test_invalid_cibuildwheel_toml_is_not_ignored(tmp_path): + (tmp_path / "pyproject.toml").write_text("[tool.cibuildwheel\n") + with pytest.raises(ValueError): + has_cibuildwheel_config(str(tmp_path)) + + +@pytest.mark.parametrize("value", ["false", '"not-a-table"']) +def test_cibuildwheel_config_requires_a_table(tmp_path, value): + (tmp_path / "pyproject.toml").write_text(f"[tool]\ncibuildwheel = {value}\n") + with pytest.raises(ValueError, match=r"Expected a \[tool.cibuildwheel\] table"): + has_cibuildwheel_config(str(tmp_path)) From 567b558875ecffd89b3f71ca1dfae6ff5211505b Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:22:13 -0700 Subject: [PATCH 2/9] [EngSys] Fix spelling exceptions for native build documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ac28761-49fd-4c09-9885-101a72746ec0 --- doc/dev/packaging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dev/packaging.md b/doc/dev/packaging.md index 3849530b394c..0b5af63bc917 100644 --- a/doc/dev/packaging.md +++ b/doc/dev/packaging.md @@ -1,6 +1,6 @@ # Azure packaging -[comment]: # ( cspell:ignore myservice ) +[comment]: # ( cspell:ignore myservice azuremyservice msvc MSRUSTUP ) > **Note:** This document covers legacy packaging using `setup.py`. New packages should use `pyproject.toml` instead. See the [`sdk/template/azure-template`](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/template/azure-template) for the current package template. From b4accfc89df26c3d3f40c1f6c3cf60b995fd88df Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:17:01 -0700 Subject: [PATCH 3/9] [EngSys] Clarify native build pipeline requirements Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ac28761-49fd-4c09-9885-101a72746ec0 --- eng/pipelines/templates/jobs/ci.yml | 1 + .../templates/stages/archetype-sdk-client.yml | 4 ++++ .../templates/steps/build-package-artifacts.yml | 7 +++++++ .../templates/steps/install-msrust-toolchain.yml | 13 +++++++++++-- .../templates/steps/resolve-build-platforms.yml | 2 ++ eng/tools/azure-sdk-tools/ci_tools/build.py | 1 + 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/templates/jobs/ci.yml b/eng/pipelines/templates/jobs/ci.yml index 067737406e02..1beadd69cec0 100644 --- a/eng/pipelines/templates/jobs/ci.yml +++ b/eng/pipelines/templates/jobs/ci.yml @@ -76,6 +76,7 @@ parameters: jobs: - job: 'Build_Linux' + # Native multi-target builds need more time, especially ARM64 under Linux emulation. ${{ if parameters.InstallMsRustToolchain }}: timeoutInMinutes: 240 ${{ else }}: diff --git a/eng/pipelines/templates/stages/archetype-sdk-client.yml b/eng/pipelines/templates/stages/archetype-sdk-client.yml index 5ca6149585e7..63f1a8a263ac 100644 --- a/eng/pipelines/templates/stages/archetype-sdk-client.yml +++ b/eng/pipelines/templates/stages/archetype-sdk-client.yml @@ -87,15 +87,19 @@ parameters: - name: EnvVars type: object default: {} + # Rust setup applies only to wheel jobs, not source-installing test or documentation jobs. - name: InstallMsRustToolchain type: boolean default: false + # Package directory containing rust-toolchain.toml. - name: MsRustWorkingDirectory type: string default: '' + # Required when Rust is enabled; no shared toolchain feed is selected by default. - name: MsRustToolchainFeed type: string default: '' + # Space-separated Windows target triples; keep aligned with the package's wheel recipe. - name: MsRustAdditionalTargets type: string default: '' diff --git a/eng/pipelines/templates/steps/build-package-artifacts.yml b/eng/pipelines/templates/steps/build-package-artifacts.yml index 8ad53eb96dc5..cc0794dce664 100644 --- a/eng/pipelines/templates/steps/build-package-artifacts.yml +++ b/eng/pipelines/templates/steps/build-package-artifacts.yml @@ -167,6 +167,7 @@ steps: Write-Host "##vso[task.setvariable variable=CIBW_CACHE_PATH]$cache" if ($env:INSTALL_MS_RUST_TOOLCHAIN -eq 'true') { + # Packages use this path as PYO3_CROSS_LIB_DIR when cross-compiling Windows ARM64 wheels. $arm64Libs = Join-Path $nugetCpython "pythonarm64.$version\tools\libs" if (-not (Test-Path $arm64Libs)) { throw "Expected ARM64 Python import libraries at $arm64Libs" @@ -182,6 +183,8 @@ steps: # dependencies from the Azure DevOps feed instead of PyPI. Using # CIBW_ENVIRONMENT_PASS_LINUX (not CIBW_ENVIRONMENT) preserves the package's # own [tool.cibuildwheel].environment (CFLAGS) setting. + # This replaces, rather than merges with, the package's environment-pass list. + # Include every variable required by its Linux build hooks. - pwsh: | $passVariables = @('PIP_INDEX_URL') if ($env:RUST_ENVIRONMENT_PASS) { @@ -205,10 +208,14 @@ steps: timeoutInMinutes: 80 env: CIBW_BUILD_VERBOSITY: 3 + # The platform resolver selects DEBUG to stream native build output. LOGLEVEL: $(SDK_BUILD_LOGLEVEL) ${{ if parameters.InstallMsRustToolchain }}: + # Map secret tokens explicitly for container feed access. Package hooks can read them; + # enforce source-trust and credential permissions through external ADO controls. MSRUSTUP_ACCESS_TOKEN: $(System.AccessToken) MSRUSTUP_FEED_URL: $(MSRUSTUP_FEED_URL) + # CargoAuthenticate exports the registry token and credential-provider variables. CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN: $(CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN) CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_CREDENTIAL_PROVIDER: $(CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_CREDENTIAL_PROVIDER) diff --git a/eng/pipelines/templates/steps/install-msrust-toolchain.yml b/eng/pipelines/templates/steps/install-msrust-toolchain.yml index 8c7195b1a792..d2158e5a9eb4 100644 --- a/eng/pipelines/templates/steps/install-msrust-toolchain.yml +++ b/eng/pipelines/templates/steps/install-msrust-toolchain.yml @@ -1,13 +1,17 @@ parameters: + # Use an organization-local mirror; the job token cannot authenticate directly to DevDiv. + # See https://aka.ms/rustinado. - name: ToolchainFeed type: string default: '' + # Directory containing rust-toolchain.toml, which selects the compiler version. - name: WorkingDirectory type: string default: '' - name: AdditionalTargets type: string default: '' + # Mirror crates.io for network-isolated builds; ~force-auth makes Cargo authenticate upstream requests. - name: CratesIoFeed type: string default: 'sparse+https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-rust-public~force-auth/Cargo/index/' @@ -33,6 +37,7 @@ steps: RUST_TOOLCHAIN_FEED: ${{ parameters.ToolchainFeed }} RUST_WORKING_DIRECTORY: ${{ parameters.WorkingDirectory }} + # Installs the host toolchain; Linux build containers bootstrap their own. - task: RustInstaller@1 displayName: 'Install internal Rust toolchain' condition: ${{ parameters.Condition }} @@ -42,6 +47,7 @@ steps: ${{ if ne(parameters.AdditionalTargets, '') }}: additionalTargets: ${{ parameters.AdditionalTargets }} + # Configure Cargo after RustInstaller defines CARGO_HOME. - pwsh: | $ErrorActionPreference = 'Stop' Write-Host "##vso[task.setvariable variable=MSRUSTUP_FEED_URL]$env:RUST_TOOLCHAIN_FEED" @@ -55,6 +61,8 @@ steps: throw "Refusing to overwrite an existing Cargo configuration: $configPath" } + # Keep the registry name aligned with the forwarded CARGO_REGISTRIES_* variables. + # Use the Git CLI so dependency fetches honor the agent's Git configuration. @" [registries] azure-sdk-for-rust-public = { index = "$env:CARGO_FEED_URL" } @@ -67,7 +75,8 @@ steps: "@ | Set-Content -Path $configPath -Encoding utf8 Write-Host "##vso[task.setvariable variable=CargoConfigPath]$configPath" - # Linux containers cannot see the host config. Package hooks must write source replacement there. + # Linux containers cannot see this config; the registry environment variable does not replace sources. + # Package hooks must write their own Cargo source replacement. Write-Host "##vso[task.setvariable variable=CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX]$env:CARGO_FEED_URL" displayName: 'Configure Rust build feeds' condition: ${{ parameters.Condition }} @@ -79,7 +88,7 @@ steps: displayName: 'Acquire an Azure Artifacts token' condition: ${{ parameters.Condition }} - # CargoAuthenticate needs the NuGet token for builds where System.AccessToken is unavailable. + # CargoAuthenticate reads SYSTEM_ACCESSTOKEN; use the token acquired by NuGetAuthenticate. - task: CargoAuthenticate@0 displayName: 'Authenticate cargo to the Azure Artifacts feed' condition: ${{ parameters.Condition }} diff --git a/eng/pipelines/templates/steps/resolve-build-platforms.yml b/eng/pipelines/templates/steps/resolve-build-platforms.yml index 095b6398abf3..5955fe572274 100644 --- a/eng/pipelines/templates/steps/resolve-build-platforms.yml +++ b/eng/pipelines/templates/steps/resolve-build-platforms.yml @@ -29,10 +29,12 @@ steps: if ($binaryPackages) { Write-Host "Targeting package(s) with compiled binaries ($($binaryPackages -join ', ')), enabling extension build." Write-Host "##vso[task.setvariable variable=ENABLE_EXTENSION_BUILD]true" + # DEBUG streams long native builds instead of buffering output until a failure. $logLevel = "DEBUG" } else { Write-Host "No targeted package declares compiled binaries, building on linux only." } + # Always define the variable used by the build step's LOGLEVEL mapping. Write-Host "##vso[task.setvariable variable=SDK_BUILD_LOGLEVEL]$logLevel" displayName: Check extension package presence diff --git a/eng/tools/azure-sdk-tools/ci_tools/build.py b/eng/tools/azure-sdk-tools/ci_tools/build.py index ca79acf986b3..e90fdee4d697 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/build.py +++ b/eng/tools/azure-sdk-tools/ci_tools/build.py @@ -238,6 +238,7 @@ def create_package( should_log_build_output = logger.getEffectiveLevel() <= logging.DEBUG # Backends such as Maturin compile extensions without declaring setuptools ext_modules. + # Honor their cibuildwheel matrix instead of building only a host wheel. is_compiled = bool(setup_parsed.ext_modules) or setup_parsed.uses_cibuildwheel if setup_parsed.is_pyproject: From 3240bd1a3926666bb355cdf84987735dc4454d10 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:26:14 -0700 Subject: [PATCH 4/9] Minor doc update --- doc/dev/packaging.md | 106 +++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/doc/dev/packaging.md b/doc/dev/packaging.md index 0b5af63bc917..35e43999e680 100644 --- a/doc/dev/packaging.md +++ b/doc/dev/packaging.md @@ -4,59 +4,6 @@ > **Note:** This document covers legacy packaging using `setup.py`. New packages should use `pyproject.toml` instead. See the [`sdk/template/azure-template`](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/template/azure-template) for the current package template. -## Native wheel builds in CI - -`sdk_build` uses `cibuildwheel` when a package declares setuptools `ext_modules` -or a `[tool.cibuildwheel]` table in `pyproject.toml`. The latter supports -non-setuptools backends, including Maturin/PyO3. The package owns its wheel -matrix, backend requirements, and native build hooks. - -Set `signBinaries: true` on the package's artifact entry in the service `ci.yml` -to enable Windows and macOS wheel builds and the existing binary-signing release -path. The platform resolver uses this artifact property, not package names. - -Packages using the Microsoft Rust toolchain can opt in through -`archetype-sdk-client.yml`: - -```yaml -extends: - template: /eng/pipelines/templates/stages/archetype-sdk-client.yml - parameters: - ServiceDirectory: myservice - InstallMsRustToolchain: true - MsRustWorkingDirectory: $(Build.SourcesDirectory)/sdk/myservice/azure-myservice - MsRustToolchainFeed: https://pkgs.dev.azure.com/azure-sdk/_packaging//nuget/v3/index.json - MsRustAdditionalTargets: aarch64-pc-windows-msvc - Artifacts: - - name: azure-myservice - safeName: azuremyservice - signBinaries: true -``` - -Replace `` with an approved organization-local feed that -provides the internal Rust toolchain. No prototype test feed is selected by -default. The working directory must contain `rust-toolchain.toml`. -The installer refuses to overwrite an existing Cargo configuration in its -`CARGO_HOME`; provide a clean build environment. -Rust installation is disabled by default and applies only to wheel-build jobs. -Opted-in jobs have a 240-minute timeout and a 210-minute package-build timeout; -other jobs keep their existing limits. - -Linux wheels build inside manylinux containers. The package's `before-all` hook -must install the internal toolchain and configure Cargo source replacement -inside each container. The pipeline forwards `PIP_INDEX_URL`, -`MSRUSTUP_ACCESS_TOKEN`, `MSRUSTUP_FEED_URL`, and the -`CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_*` index, token, and credential-provider -variables. Credentials stay in the build environment, not checked-in files. -On Windows, `PYTHON_ARM64_LIB_DIR` points to the pre-provisioned ARM64 Python -import libraries; a package can use it as `PYO3_CROSS_LIB_DIR` for cross-compilation. - -This opt-in does not provision Rust for source-installing test, documentation, -or analysis jobs. Prepare those jobs separately before enabling a Rust package, -and keep real source-distribution testing. Packages with a static -`[project].version` also need version synchronization with `_version.py` for -nightly builds. No package is activated by adding this shared support. - This article describes the recommendations for defining namespace packaging to release a package inside the `azure` namespace. Being inside the `azure` namespace means that a service `myservice` can be imported using: ```python import azure.myservice @@ -198,6 +145,59 @@ Since the package is Python 3 only, do NOT make this wheel universal. This usual - wheel file name suffix is `py3-none-any`, and NOT `py2.py3-none-any`. - sdist must contain a `azure/__init__.py` file that declares `azure` as a namespace package using the `pkgutil` syntax +# Native wheel builds in CI + +`sdk_build` uses `cibuildwheel` when a package declares setuptools `ext_modules` +or a `[tool.cibuildwheel]` table in `pyproject.toml`. The latter supports +non-setuptools backends, including Maturin/PyO3. The package owns its wheel +matrix, backend requirements, and native build hooks. + +Set `signBinaries: true` on the package's artifact entry in the service `ci.yml` +to enable Windows and macOS wheel builds and the existing binary-signing release +path. The platform resolver uses this artifact property, not package names. + +Packages using the Microsoft Rust toolchain can opt in through +`archetype-sdk-client.yml`: + +```yaml +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: myservice + InstallMsRustToolchain: true + MsRustWorkingDirectory: $(Build.SourcesDirectory)/sdk/myservice/azure-myservice + MsRustToolchainFeed: https://pkgs.dev.azure.com/azure-sdk/_packaging//nuget/v3/index.json + MsRustAdditionalTargets: aarch64-pc-windows-msvc + Artifacts: + - name: azure-myservice + safeName: azuremyservice + signBinaries: true +``` + +Replace `` with an approved organization-local feed that +provides the internal Rust toolchain. No prototype test feed is selected by +default. The working directory must contain `rust-toolchain.toml`. +The installer refuses to overwrite an existing Cargo configuration in its +`CARGO_HOME`; provide a clean build environment. +Rust installation is disabled by default and applies only to wheel-build jobs. +Opted-in jobs have a 240-minute timeout and a 210-minute package-build timeout; +other jobs keep their existing limits. + +Linux wheels build inside manylinux containers. The package's `before-all` hook +must install the internal toolchain and configure Cargo source replacement +inside each container. The pipeline forwards `PIP_INDEX_URL`, +`MSRUSTUP_ACCESS_TOKEN`, `MSRUSTUP_FEED_URL`, and the +`CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_*` index, token, and credential-provider +variables. Credentials stay in the build environment, not checked-in files. +On Windows, `PYTHON_ARM64_LIB_DIR` points to the pre-provisioned ARM64 Python +import libraries; a package can use it as `PYO3_CROSS_LIB_DIR` for cross-compilation. + +This opt-in does not provision Rust for source-installing test, documentation, +or analysis jobs. Prepare those jobs separately before enabling a Rust package, +and keep real source-distribution testing. Packages with a static +`[project].version` also need version synchronization with `_version.py` for +nightly builds. No package is activated by adding this shared support. + # I already have a package that supports Python 2, can I get short version on how to udpate to Python 3 only? - Remove "universal" from setup.cfg, or completely remove the file if it was the only option From 5034d121df97524707f04411306779d7656d5040 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:29:55 -0700 Subject: [PATCH 5/9] keep fallback --- .../steps/install-msrust-toolchain.yml | 2 +- .../tests/test_native_build_pipeline.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/templates/steps/install-msrust-toolchain.yml b/eng/pipelines/templates/steps/install-msrust-toolchain.yml index d2158e5a9eb4..dbff0541e538 100644 --- a/eng/pipelines/templates/steps/install-msrust-toolchain.yml +++ b/eng/pipelines/templates/steps/install-msrust-toolchain.yml @@ -53,7 +53,7 @@ steps: Write-Host "##vso[task.setvariable variable=MSRUSTUP_FEED_URL]$env:RUST_TOOLCHAIN_FEED" $cargoHome = $env:CARGO_HOME if (-not $cargoHome) { - throw 'RustInstaller did not define CARGO_HOME.' + $cargoHome = [System.IO.Path]::Combine($HOME, '.cargo') } New-Item -ItemType Directory -Force -Path $cargoHome | Out-Null $configPath = Join-Path $cargoHome 'config.toml' diff --git a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py index ec61a689de47..8f28434007d7 100644 --- a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py +++ b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py @@ -173,6 +173,24 @@ def test_cargo_feed_configuration_does_not_overwrite_existing_config(tmp_path): assert (tmp_path / "config.toml").read_text() == config +def test_cargo_feed_configuration_uses_default_cargo_home(tmp_path): + configure = load_template("steps/install-msrust-toolchain.yml")["steps"][2]["pwsh"] + feed = "sparse+https://pkgs.dev.azure.com/azure-sdk/public/_packaging/example/Cargo/index/" + + result = run_powershell( + configure, + CARGO_HOME="", + CARGO_FEED_URL=feed, + HOME=str(tmp_path), + RUST_TOOLCHAIN_FEED="example-feed", + ) + + assert result.returncode == 0, result.stderr + config = tmp_path / ".cargo" / "config.toml" + assert config.is_file() + assert feed in config.read_text() + + def test_build_powershell_scripts_parse(): for template_path in ( "steps/install-msrust-toolchain.yml", From aaf2e33b4e9535b5e3319568982b868b3c1c37d7 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:45:27 -0700 Subject: [PATCH 6/9] add azure-template-three --- eng/pipelines/pullrequest.yml | 2 + eng/pipelines/templates/jobs/ci.yml | 4 + .../steps/build-package-artifacts.yml | 8 + ...clude-RustTemplateFixtureFromGenericPR.ps1 | 26 + .../ci_tools/versioning/version_shared.py | 30 ++ .../tests/test_native_build_pipeline.py | 453 +++++++++++++++++- .../azure-sdk-tools/tests/test_versioning.py | 53 +- sdk/template/azure-template-three/.gitignore | 3 + .../azure-template-three/CHANGELOG.md | 9 + sdk/template/azure-template-three/Cargo.lock | 212 ++++++++ sdk/template/azure-template-three/Cargo.toml | 17 + sdk/template/azure-template-three/LICENSE | 21 + sdk/template/azure-template-three/README.md | 80 ++++ .../azure/template/three/__init__.py | 9 + .../azure/template/three/_rust.pyi | 4 + .../azure/template/three/_version.py | 7 + .../azure-template-three/dev_requirements.txt | 2 + .../azure-template-three/pyproject.toml | 102 ++++ .../azure-template-three/rust-toolchain.toml | 3 + .../scripts/configure-cargo-feed.sh | 35 ++ .../scripts/install-msrustup.sh | 49 ++ .../azure-template-three/sdk_packaging.toml | 2 + sdk/template/azure-template-three/src/lib.rs | 15 + .../azure-template-three/tests/test_rust.py | 29 ++ sdk/template/ci.yml | 9 + sdk/template/tests.yml | 1 + 26 files changed, 1183 insertions(+), 2 deletions(-) create mode 100644 eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 create mode 100644 sdk/template/azure-template-three/.gitignore create mode 100644 sdk/template/azure-template-three/CHANGELOG.md create mode 100644 sdk/template/azure-template-three/Cargo.lock create mode 100644 sdk/template/azure-template-three/Cargo.toml create mode 100644 sdk/template/azure-template-three/LICENSE create mode 100644 sdk/template/azure-template-three/README.md create mode 100644 sdk/template/azure-template-three/azure/template/three/__init__.py create mode 100644 sdk/template/azure-template-three/azure/template/three/_rust.pyi create mode 100644 sdk/template/azure-template-three/azure/template/three/_version.py create mode 100644 sdk/template/azure-template-three/dev_requirements.txt create mode 100644 sdk/template/azure-template-three/pyproject.toml create mode 100644 sdk/template/azure-template-three/rust-toolchain.toml create mode 100644 sdk/template/azure-template-three/scripts/configure-cargo-feed.sh create mode 100644 sdk/template/azure-template-three/scripts/install-msrustup.sh create mode 100644 sdk/template/azure-template-three/sdk_packaging.toml create mode 100644 sdk/template/azure-template-three/src/lib.rs create mode 100644 sdk/template/azure-template-three/tests/test_rust.py diff --git a/eng/pipelines/pullrequest.yml b/eng/pipelines/pullrequest.yml index 41a3275a0477..1ec1d5985583 100644 --- a/eng/pipelines/pullrequest.yml +++ b/eng/pipelines/pullrequest.yml @@ -33,3 +33,5 @@ extends: # As such, the pullrequest pipeline itself will not invoke the cosmos tests, but the triggered `python - cosmos - ci` public build WILL. ExcludePaths: - sdk/cosmos/ + # Template CI validates the Rust fixture; generic PR builds do not enable its Rust toolchain setup. + - sdk/template/azure-template-three/ diff --git a/eng/pipelines/templates/jobs/ci.yml b/eng/pipelines/templates/jobs/ci.yml index 1beadd69cec0..8c7e185c7d7e 100644 --- a/eng/pipelines/templates/jobs/ci.yml +++ b/eng/pipelines/templates/jobs/ci.yml @@ -343,6 +343,10 @@ jobs: parameters: ServiceDirectory: ${{parameters.ServiceDirectory}} ExcludePaths: ${{parameters.ExcludePaths}} + - pwsh: | + ./eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 ` + -PackagePropertiesFolder "$(Build.ArtifactStagingDirectory)/PackageInfo" + displayName: Exclude Rust template fixture from generic PR validation CloudConfig: Cloud: Public AdditionalParameters: diff --git a/eng/pipelines/templates/steps/build-package-artifacts.yml b/eng/pipelines/templates/steps/build-package-artifacts.yml index cc0794dce664..7aa210c783d9 100644 --- a/eng/pipelines/templates/steps/build-package-artifacts.yml +++ b/eng/pipelines/templates/steps/build-package-artifacts.yml @@ -98,6 +98,14 @@ steps: ServiceDirectory: ${{ parameters.ServiceDirectory }} ExcludePaths: ${{ parameters.ExcludePaths }} + # Template CI validates the Rust fixture; generic PR builds do not enable + # its Rust toolchain setup. + - ${{ if eq(parameters.ServiceDirectory, 'auto') }}: + - pwsh: | + ./eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 ` + -PackagePropertiesFolder "$(Build.ArtifactStagingDirectory)/PackageInfo" + displayName: Exclude Rust template fixture from generic PR validation + - template: /eng/pipelines/templates/steps/resolve-package-targeting.yml parameters: BuildTargetingString: ${{ parameters.BuildTargetingString }} diff --git a/eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 b/eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 new file mode 100644 index 000000000000..d8bcc49decc4 --- /dev/null +++ b/eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +param ( + [string] $PackagePropertiesFolder = '' +) + +$ErrorActionPreference = 'Stop' + +if ($PackagePropertiesFolder -and (Test-Path $PackagePropertiesFolder)) { + Get-ChildItem -Path $PackagePropertiesFolder -Recurse -File -Filter 'azure-template-three.json' | + ForEach-Object { + Write-Host 'Generic PR builds do not enable the Rust toolchain setup; excluding azure-template-three, which template CI validates.' + Remove-Item -LiteralPath $_.FullName + } +} + +# Matrix jobs can already have TargetingString set and skip normal targeting resolution. +if ($env:TARGETINGSTRING) { + $targets = @($env:TARGETINGSTRING.Split(',') | ForEach-Object { $_.Trim() } | + Where-Object { $_ -and $_ -ne 'azure-template-three' }) + if ($targets.Count -eq 0) { + $targets = @('null') + } + Write-Host "##vso[task.setvariable variable=TargetingString;]$($targets -join ',')" +} diff --git a/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py b/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py index cc579312e6ea..6508c58ffc68 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py +++ b/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py @@ -102,6 +102,36 @@ def set_version_py(setup_path, new_version): version_py_file.write(replaced_version_contents) + pyproject_location = path.join(path.dirname(setup_path), "pyproject.toml") + if path.isfile(pyproject_location): + with open(pyproject_location, "r", newline="") as pyproject_file: + pyproject_contents = pyproject_file.read() + + project_section = re.search(r"(?m)^\[project\][^\r\n]*(?:\r?\n|$)", pyproject_contents) + if project_section: + next_section = re.search(r"(?m)^\[", pyproject_contents[project_section.end() :]) + project_end = project_section.end() + next_section.start() if next_section else len(pyproject_contents) + project_contents = pyproject_contents[project_section.end() : project_end] + version_pattern = re.compile( + r"(?m)^(\s*version\s*=\s*)([\"'])([^\"'\r\n]+)(\2)([^\r\n]*)(\r?\n|$)" + ) + replaced_project_contents, replacements = version_pattern.subn( + lambda match: ( + f"{match.group(1)}{match.group(2)}{new_version}" + f"{match.group(4)}{match.group(5)}{match.group(6)}" + ), + project_contents, + count=1, + ) + if replacements: + updated_contents = ( + pyproject_contents[: project_section.end()] + + replaced_project_contents + + pyproject_contents[project_end:] + ) + with open(pyproject_location, "w", newline="") as pyproject_file: + pyproject_file.write(updated_contents) + # Get classification for PyPI (https://pypi.org/classifiers/) def get_classification(version): diff --git a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py index 8f28434007d7..1f2bc4b5c971 100644 --- a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py +++ b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py @@ -10,14 +10,26 @@ import re import shutil import subprocess +import sys import pytest import yaml +from ci_tools.build import create_package +from ci_tools.parsing import ParsedSetup +from ci_tools.parsing.parse_functions import get_pyproject_dict + REPO_ROOT = Path(__file__).resolve().parents[4] TEMPLATES = REPO_ROOT / "eng" / "pipelines" / "templates" +RUST_FIXTURE = REPO_ROOT / "sdk" / "template" / "azure-template-three" +GENERIC_PR_EXCLUSION_SCRIPT = ( + REPO_ROOT / "eng" / "scripts" / "Exclude-RustTemplateFixtureFromGenericPR.ps1" +) RUST_GATE = "${{ if parameters.InstallMsRustToolchain }}" +RUST_TOOLCHAIN_FEED = ( + "https://pkgs.dev.azure.com/azure-sdk/_packaging/azure-sdk-for-rust-org-test/nuget/v3/index.json" +) RUST_PARAMETERS = ( "InstallMsRustToolchain", "MsRustWorkingDirectory", @@ -55,7 +67,14 @@ def test_rust_support_is_disabled_by_default_and_forwarded(): if parameter["name"] in RUST_PARAMETERS } assert defaults["InstallMsRustToolchain"] is False - assert all(defaults[name] == "" for name in RUST_PARAMETERS[1:]) + assert defaults["MsRustWorkingDirectory"] == "" + assert defaults["MsRustToolchainFeed"] == "" + assert defaults["MsRustAdditionalTargets"] == "" + + installer = load_template("steps/install-msrust-toolchain.yml") + assert next( + parameter["default"] for parameter in installer["parameters"] if parameter["name"] == "ToolchainFeed" + ) == "" build_stage = next(stage for stage in archetype["extends"]["parameters"]["stages"] if stage.get("stage") == "Build") forwarded = build_stage["jobs"][0]["parameters"] @@ -219,3 +238,435 @@ def test_build_powershell_scripts_parse(): SCRIPT=script, ) assert result.returncode == 0, f"{template_path}: {step['displayName']}: {result.stderr}" + + +def test_rust_template_is_always_on_and_uses_cosmos_prototype_settings(): + pipeline = yaml.safe_load((REPO_ROOT / "sdk" / "template" / "ci.yml").read_text()) + assert all(parameter["name"] != "EnableRustFixture" for parameter in pipeline["parameters"]) + parameters = pipeline["extends"]["parameters"] + assert "BuildTargetingString" not in parameters + assert parameters["InstallMsRustToolchain"] is True + assert parameters["MsRustWorkingDirectory"] == ( + "$(Build.SourcesDirectory)/sdk/template/azure-template-three" + ) + assert parameters["MsRustToolchainFeed"] == RUST_TOOLCHAIN_FEED + assert parameters["MsRustAdditionalTargets"] == "aarch64-pc-windows-msvc" + assert "variables" not in pipeline + artifact_entries = parameters["Artifacts"] + assert [artifact["name"] for artifact in artifact_entries] == [ + "azure-template", + "azure-template-two", + "azure-template-three", + ] + rust = artifact_entries[2] + assert rust["name"] == "azure-template-three" + assert rust["signBinaries"] is True + assert "skipPublishDevFeed" not in rust + + pullrequest = yaml.safe_load((REPO_ROOT / "eng" / "pipelines" / "pullrequest.yml").read_text()) + assert "sdk/template/azure-template-three/" in pullrequest["extends"]["parameters"]["ExcludePaths"] + live = yaml.safe_load((REPO_ROOT / "sdk" / "template" / "tests.yml").read_text()) + assert live["extends"]["parameters"]["BuildTargetingString"] == "azure-template|azure-template-two" + + +@pytest.mark.parametrize( + "targeting,expected", + [ + ("azure-*", ["azure-template", "azure-template-two"]), + ("azure-template*", ["azure-template", "azure-template-two"]), + ("azure-template-three", []), + ("azure-template-two", ["azure-template-two"]), + ("missing-*", []), + ], +) +def test_generic_pr_package_targeting_excludes_rust_fixture(tmp_path, targeting, expected): + for name in ("azure-template", "azure-template-two", "azure-template-three"): + (tmp_path / f"{name}.json").write_text(json.dumps({"IncludedForValidation": False})) + script = REPO_ROOT / "eng" / "scripts" / "resolve-package-info.ps1" + result = run_powershell( + """ + $ErrorActionPreference = 'Stop' + & $env:FILTER_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO + & $env:RESOLVE_SCRIPT -ParameterTargetingStringSetting $env:TARGETS -PackagePropertiesFolder $env:PACKAGE_INFO + """, + FILTER_SCRIPT=str(GENERIC_PR_EXCLUSION_SCRIPT), + RESOLVE_SCRIPT=str(script), + TARGETS=targeting, + PACKAGE_INFO=str(tmp_path), + BUILDTARGETINGSTRING="", + TARGETINGSTRING="", + ) + assert result.returncode == 0, result.stderr + assert sorted(path.stem for path in tmp_path.glob("*.json")) == expected + output = next(line for line in result.stdout.splitlines() if "variable=TargetingString;]" in line) + targets = output.split("]", 1)[1] + assert sorted(targets.split(",")) == (expected or ["null"]) + + +def run_generic_pr_exclusion(tmp_path, **environment): + return run_powershell( + """ + $ErrorView = 'NormalView' + & $env:FILTER_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO + """, + **{ + "FILTER_SCRIPT": str(GENERIC_PR_EXCLUSION_SCRIPT), + "PACKAGE_INFO": str(tmp_path), + "TARGETINGSTRING": "", + **environment, + }, + ) + + +@pytest.mark.parametrize( + "targets,expected", + [ + ("azure-template,azure-template-three,azure-template-two", "azure-template,azure-template-two"), + (" azure-template-three , azure-cosmos ", "azure-cosmos"), + ("azure-template-three", "null"), + ("azure-cosmos", "azure-cosmos"), + ], +) +def test_generic_pr_exclusion_applies_to_preselected_matrix_batches(tmp_path, targets, expected): + for name in ("azure-template", "azure-template-two", "azure-template-three", "azure-cosmos"): + (tmp_path / f"{name}.json").write_text("{}") + result = run_generic_pr_exclusion(tmp_path, TARGETINGSTRING=targets) + assert result.returncode == 0, result.stderr + assert f"variable=TargetingString;]{expected}" in result.stdout + assert not (tmp_path / "azure-template-three.json").exists() + assert (tmp_path / "azure-cosmos.json").exists() + +def test_generic_pr_exclusion_precedes_target_resolution_and_matrix_generation(): + targeting = load_template("steps/resolve-package-targeting.yml")["steps"] + assert all("Filter-RustTemplateFixture.ps1" not in step.get("pwsh", "") for step in targeting) + + build = load_template("steps/build-package-artifacts.yml")["steps"] + filter_index = next( + index + for index, step in enumerate(build) + if "${{ if eq(parameters.ServiceDirectory, 'auto') }}" in step + ) + filter_step = build[filter_index]["${{ if eq(parameters.ServiceDirectory, 'auto') }}"][0] + assert "Exclude-RustTemplateFixtureFromGenericPR.ps1" in filter_step["pwsh"] + resolve_index = next( + index + for index, step in enumerate(build) + if step.get("template", "").endswith("/steps/resolve-package-targeting.yml") + ) + assert filter_index < resolve_index + + jobs = load_template("jobs/ci.yml")["jobs"] + matrix = next(job for job in jobs if job.get("template", "").endswith("/jobs/generate-job-matrix.yml")) + steps = matrix["parameters"]["${{ if eq(parameters.ServiceDirectory, 'auto') }}"]["PreGenerationSteps"] + assert steps[-2]["template"].endswith("/steps/save-package-properties.yml") + assert "Exclude-RustTemplateFixtureFromGenericPR.ps1" in steps[-1]["pwsh"] + assert '-PackagePropertiesFolder "$(Build.ArtifactStagingDirectory)/PackageInfo"' in steps[-1]["pwsh"] + assert "condition" not in steps[-1] + + +@pytest.mark.parametrize("indirect", [False, True]) +def test_pr_matrix_batches_exclude_rust_fixture(tmp_path, indirect): + package_info = tmp_path / "PackageInfo" + package_info.mkdir() + for name in ("azure-template", "azure-template-two", "azure-template-three", "azure-cosmos"): + (package_info / f"{name}.json").write_text( + json.dumps( + { + "ArtifactName": name, + "IncludedForValidation": indirect, + "CIParameters": {"CIMatrixConfigs": None}, + } + ) + ) + config = tmp_path / "matrix.json" + archetype = load_template("stages/archetype-sdk-client.yml") + config.write_text( + json.dumps( + next(parameter["default"] for parameter in archetype["parameters"] if parameter["name"] == "MatrixConfigs") + ) + ) + result = run_powershell( + """ + $ErrorActionPreference = 'Stop' + & $env:FILTER_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO + & $env:MATRIX_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO ` + -PRMatrixFile $env:MATRIX_CONFIG -PRMatrixSetting TargetingString ` + -Filters 'container=^$', 'SupportedClouds=^$|Public', 'Pool=.*Linux.*Pool$' -CI + """, + FILTER_SCRIPT=str(GENERIC_PR_EXCLUSION_SCRIPT), + MATRIX_SCRIPT=str(REPO_ROOT / "eng" / "common" / "scripts" / "job-matrix" / "Create-PrJobMatrix.ps1"), + PACKAGE_INFO=str(package_info), + MATRIX_CONFIG=str(config), + TARGETINGSTRING="", + BUILD_REASON="PullRequest", + SETDEVVERSION="false", + LINUXVMIMAGE="ubuntu-24.04", + LINUXPOOL="AzureSDKLinuxPool", + WINDOWSVMIMAGE="windows-2022", + WINDOWSPOOL="AzureSDKWindowsPool", + MACVMIMAGE="macOS-15", + MACPOOL="AzureSDKMacPool", + ) + assert result.returncode == 0, result.stderr + output = next( + line + for line in result.stdout.splitlines() + if line.startswith("##vso[task.setVariable variable=matrix;isOutput=true]") + ) + matrix = json.loads(output.split("]", 1)[1]) + assert matrix + targets = {name for job in matrix.values() for name in job["TargetingString"].split(",")} + assert targets == {"azure-template", "azure-template-two", "azure-cosmos"} + + +def run_pr_fixture_discovery(tmp_path, changed, include_cosmos=False): + diff = tmp_path / "diff.json" + diff.write_text( + json.dumps( + { + "ChangedFiles": changed, + "ExcludePaths": ["sdk/template/azure-template-three/", "sdk/cosmos/"], + } + ) + ) + source = (REPO_ROOT / "eng" / "common" / "scripts" / "Package-Properties.ps1").read_text() + helpers = source[source.index("function Get-TriggerPaths(") : source.index("function Get-AllPkgProperties (")] + package_info = tmp_path / "PackageInfo" + package_info.mkdir() + return run_powershell( + """ + $ErrorActionPreference = 'Stop' + $RepoRoot = $env:REPO_ROOT + function LogInfo($message) {} + function Get-AllPkgProperties { + foreach ($name in @('azure-template', 'azure-template-two', 'azure-template-three')) { + [PSCustomObject]@{ + Name = $name + DirectoryPath = Join-Path $RepoRoot "sdk/template/$name" + ServiceDirectory = 'template' + ArtifactDetails = @{ triggeringPaths = @('sdk/template/ci.yml') } + AdditionalValidationPackages = @( + if ($env:INCLUDE_COSMOS -eq 'true' -and $name -eq 'azure-template-two') { + 'sdk/cosmos/azure-cosmos' + } + ) + IncludedForValidation = $false + } + } + if ($env:INCLUDE_COSMOS -eq 'true') { + [PSCustomObject]@{ + Name = 'azure-cosmos' + DirectoryPath = Join-Path $RepoRoot 'sdk/cosmos/azure-cosmos' + ServiceDirectory = 'cosmos' + ArtifactDetails = @{ triggeringPaths = @('sdk/cosmos/ci.yml') } + AdditionalValidationPackages = @() + IncludedForValidation = $false + } + } + } + """ + + helpers + + """ + $packages = @(Get-PrPkgProperties $env:PR_DIFF) + $names = @($packages | Select-Object -ExpandProperty Name | Sort-Object) + Write-Host "SelectedPackages:$($names -join ',')" + foreach ($package in $packages) { + $package | ConvertTo-Json -Depth 10 | Set-Content ( + Join-Path $env:PACKAGE_INFO "$($package.Name).json" + ) + } + & $env:FILTER_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO + $retained = @(Get-ChildItem $env:PACKAGE_INFO -Filter '*.json' | + Select-Object -ExpandProperty BaseName | Sort-Object) + Write-Host "RetainedPackages:$($retained -join ',')" + """, + REPO_ROOT=str(REPO_ROOT), + PR_DIFF=str(diff), + PACKAGE_INFO=str(package_info), + FILTER_SCRIPT=str(GENERIC_PR_EXCLUSION_SCRIPT), + INCLUDE_COSMOS=str(include_cosmos), + TARGETINGSTRING="", + ) + + +@pytest.mark.parametrize("changed", [["sdk/template/ci.yml"], ["eng/scripts/resolve-package-info.ps1"]]) +def test_generic_pr_exclusion_filters_service_changes_and_fallback(tmp_path, changed): + result = run_pr_fixture_discovery(tmp_path, changed) + assert result.returncode == 0, result.stderr + assert "SelectedPackages:azure-template,azure-template-three,azure-template-two" in result.stdout + assert "RetainedPackages:azure-template,azure-template-two" in result.stdout + + +def test_pr_discovery_preserves_excluded_package_dependency_validation(tmp_path): + result = run_pr_fixture_discovery(tmp_path, ["sdk/template/azure-template-two/README.md"], include_cosmos=True) + assert result.returncode == 0, result.stderr + assert "SelectedPackages:azure-cosmos,azure-template-two" in result.stdout + assert "RetainedPackages:azure-cosmos,azure-template-two" in result.stdout + cosmos = json.loads((tmp_path / "PackageInfo" / "azure-cosmos.json").read_text()) + assert cosmos["IncludedForValidation"] is True + + +def test_rust_fixture_routes_through_cibuildwheel_without_setuptools_extensions(tmp_path, monkeypatch): + package = ParsedSetup.from_path(str(RUST_FIXTURE)) + assert package.name == "azure-template-three" + assert package.version == "1.0.0b1" + assert package.namespace == "azure.template.three" + assert package.uses_cibuildwheel + assert not package.ext_modules + commands = [] + monkeypatch.setattr("ci_tools.build.run_logged", lambda command, **kwargs: commands.append(command)) + create_package(str(RUST_FIXTURE), str(tmp_path)) + assert commands == [ + [sys.executable, "-m", "cibuildwheel", "--output-dir", str(tmp_path)], + [sys.executable, "-m", "build", "-s", "-o", str(tmp_path)], + ] + + +def test_rust_fixture_matrix_and_container_contract(): + config = get_pyproject_dict(str(RUST_FIXTURE / "pyproject.toml")) + assert config["build-system"] == {"requires": ["maturin==1.15.0"], "build-backend": "maturin"} + assert config["tool"]["maturin"]["locked"] is True + wheel = config["tool"]["cibuildwheel"] + assert wheel["linux"]["archs"] == ["x86_64", "aarch64"] + assert wheel["macos"]["archs"] == ["arm64"] + assert wheel["windows"]["archs"] == ["AMD64", "ARM64"] + build = load_template("steps/build-package-artifacts.yml") + forwarding = next( + step for step in build["steps"] if step.get("displayName") == "Forward build environment to cibuildwheel" + ) + variables = {"PIP_INDEX_URL", *forwarding["env"][RUST_GATE]["RUST_ENVIRONMENT_PASS"].split()} + assert set(wheel["linux"]["environment-pass"]) == variables + assert "scripts/install-msrustup.sh" in wheel["linux"]["before-all"] + assert "scripts/configure-cargo-feed.sh" in wheel["linux"]["before-all"] + assert wheel["overrides"] == [ + { + "select": "*-win_arm64", + "environment": { + "CARGO_BUILD_TARGET": "aarch64-pc-windows-msvc", + "PYO3_CROSS_LIB_DIR": "$PYTHON_ARM64_LIB_DIR", + }, + } + ] + toolchain = get_pyproject_dict(str(RUST_FIXTURE / "rust-toolchain.toml")) + assert toolchain["toolchain"] == {"channel": "ms-prod-1.97", "profile": "minimal"} + + +def run_cargo_configuration(tmp_path, index="", token=""): + shell = shutil.which("sh") + if not shell: + pytest.skip("A POSIX shell is required for Linux container configuration tests") + return subprocess.run( + [shell, str(RUST_FIXTURE / "scripts" / "configure-cargo-feed.sh")], + env={ + **os.environ, + "CARGO_HOME": str(tmp_path), + "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX": index, + "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN": token, + }, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + + +def test_container_cargo_configuration_keeps_credentials_out_of_files(tmp_path): + index = "sparse+https://example.test/Cargo/index/" + result = run_cargo_configuration(tmp_path, index, "fixture-test-token") + assert result.returncode == 0, result.stderr + config = (tmp_path / "config.toml").read_text() + assert index in config + assert 'replace-with = "azure-sdk-for-rust-public"' in config + assert "fixture-test-token" not in config + result.stdout + result.stderr + result = run_cargo_configuration(tmp_path, index, "fixture-test-token") + assert result.returncode != 0 + assert (tmp_path / "config.toml").read_text() == config + + +def test_container_cargo_configuration_requires_token_when_index_is_set(tmp_path): + result = run_cargo_configuration(tmp_path, "sparse+https://example.test/Cargo/index/") + assert result.returncode != 0 + assert "requires an authentication token" in result.stderr + assert not (tmp_path / "config.toml").exists() + + +def test_container_cargo_configuration_preserves_local_crates_io(tmp_path): + existing = tmp_path / "config.toml" + existing.write_text("[net]\noffline = true\n") + result = run_cargo_configuration(tmp_path) + assert result.returncode == 0, result.stderr + assert existing.read_text() == "[net]\noffline = true\n" + + +@pytest.mark.parametrize( + "architecture,failure,expected", + [ + ("x86_64", "", 0), + ("aarch64", "", 0), + ("x86_64", "index", 1), + ("unsupported", "", 1), + ], +) +def test_container_msrustup_bootstrap_downloads_and_surfaces_failures(tmp_path, architecture, failure, expected): + if os.name == "nt" or not shutil.which("jq"): + pytest.skip("The Linux bootstrap requires POSIX executables and jq") + tools = tmp_path / "bin" + tools.mkdir() + commands = { + "uname": '#!/bin/sh\nif [ "$1" = "-s" ]; then echo Linux; else echo "$FIXTURE_ARCH"; fi\n', + "unzip": '#!/bin/sh\nprintf "#!/bin/sh\\nexit 0\\n" > msrustup\n', + "curl": """#!/bin/sh +while [ "$#" -gt 0 ]; do + case "$1" in + -H) [ "$2" = "Authorization: Bearer fixture-test-token" ] || exit 9; shift 2 ;; + --retry) shift 2 ;; + -o) : > "$2"; shift 2 ;; + -*) shift ;; + *) url="$1"; shift ;; + esac +done +case "$url" in + https://example.test/feed) + printf '{"resources":[{"@type":"PackageBaseAddress/3.0.0","@id":"https://example.test/packages/"}]}' + ;; + */index.json) + [ "$FIXTURE_FAILURE" != "index" ] || exit 1 + printf '{"versions":["7.0.0"]}' + ;; + *.nupkg) printf '%s\\n' "$url" > "$FIXTURE_REQUEST" ;; + *) exit 8 ;; +esac +""", + } + for name, content in commands.items(): + executable = tools / name + executable.write_text(content) + executable.chmod(0o755) + request = tmp_path / "request.txt" + result = subprocess.run( + ["sh", str(RUST_FIXTURE / "scripts" / "install-msrustup.sh")], + cwd=tmp_path, + env={ + **os.environ, + "PATH": f"{tools}{os.pathsep}{os.environ['PATH']}", + "MSRUSTUP_FEED_URL": "https://example.test/feed", + "MSRUSTUP_ACCESS_TOKEN": "fixture-test-token", + "MSRUSTUP_PAT": "", + "FIXTURE_ARCH": architecture, + "FIXTURE_FAILURE": failure, + "FIXTURE_REQUEST": str(request), + }, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + assert result.returncode == expected, result.stderr + assert "fixture-test-token" not in result.stdout + result.stderr + assert not (tmp_path / "msrustup.zip").exists() + if expected == 0: + package = f"rust.msrustup-{architecture}-unknown-linux-gnu" + assert request.read_text().strip() == f"https://example.test/packages/{package}/7.0.0/{package}.7.0.0.nupkg" + assert os.access(tmp_path / "msrustup", os.X_OK) + else: + assert not (tmp_path / "msrustup").exists() diff --git a/eng/tools/azure-sdk-tools/tests/test_versioning.py b/eng/tools/azure-sdk-tools/tests/test_versioning.py index cd355ed80163..c3614a2f2e4c 100644 --- a/eng/tools/azure-sdk-tools/tests/test_versioning.py +++ b/eng/tools/azure-sdk-tools/tests/test_versioning.py @@ -7,7 +7,7 @@ from ci_tools.parsing import ParsedSetup from ci_tools.versioning.version_set_dev import set_dev_version -from ci_tools.versioning.version_shared import get_classification, DEV_STATUS_REGEX +from ci_tools.versioning.version_shared import get_classification, DEV_STATUS_REGEX, set_version_py integration_folder = os.path.join(os.path.dirname(__file__), "integration") setup_folder = os.path.join(integration_folder, "scenarios", "setup_py_project_def") @@ -92,3 +92,54 @@ def test_set_dev_version_on_setup(): # Expect version to have 'a' suffix with zero-padded build id assert parsed_after.version == "1.0.0a20250813001" assert "Development Status :: 4 - Beta" in parsed_after.classifiers + + +def test_set_version_synchronizes_static_pyproject_and_runtime_version(tmp_path): + package_dir = tmp_path / "static-version-package" + version_dir = package_dir / "src" + version_dir.mkdir(parents=True) + pyproject = package_dir / "pyproject.toml" + pyproject.write_text( + """[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "static-version-package" +# Keep this comment and the original formatting. +version = "1.0.0b1" # Distribution version +requires-python = ">=3.10" +dependencies = [] + +[tool.example] +version = "unchanged" +""" + ) + version_file = version_dir / "_version.py" + version_file.write_text('VERSION = "1.0.0b1"\n') + set_version_py(str(pyproject), "1.0.0b12345") + assert pyproject.read_text() == """[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "static-version-package" +# Keep this comment and the original formatting. +version = "1.0.0b12345" # Distribution version +requires-python = ">=3.10" +dependencies = [] + +[tool.example] +version = "unchanged" +""" + assert version_file.read_text() == 'VERSION = "1.0.0b12345"\n' + assert ParsedSetup.from_path(str(package_dir)).version == "1.0.0b12345" + + +def test_set_version_preserves_dynamic_pyproject_version_configuration(tmp_path): + shutil.copytree(pyproject_folder, tmp_path / "package") + pyproject = tmp_path / "package/pyproject.toml" + original = pyproject.read_bytes() + set_version_py(str(pyproject), "1.0.0b12345") + assert pyproject.read_bytes() == original + assert ParsedSetup.from_path(str(pyproject.parent)).version == "1.0.0b12345" diff --git a/sdk/template/azure-template-three/.gitignore b/sdk/template/azure-template-three/.gitignore new file mode 100644 index 000000000000..7bccacabaf39 --- /dev/null +++ b/sdk/template/azure-template-three/.gitignore @@ -0,0 +1,3 @@ +/target/ +/azure/template/three/*.so +/azure/template/three/*.pyd diff --git a/sdk/template/azure-template-three/CHANGELOG.md b/sdk/template/azure-template-three/CHANGELOG.md new file mode 100644 index 000000000000..493ceb49f54c --- /dev/null +++ b/sdk/template/azure-template-three/CHANGELOG.md @@ -0,0 +1,9 @@ +# Release History + +## 1.0.0b1 (Unreleased) + +### Features Added + +- Added a minimal Rust extension to exercise Maturin wheel builds, Microsoft + Rust toolchain installation, Cargo feed authentication, and Linux container + setup in opt-in template CI runs. diff --git a/sdk/template/azure-template-three/Cargo.lock b/sdk/template/azure-template-three/Cargo.lock new file mode 100644 index 000000000000..848cc7083d8e --- /dev/null +++ b/sdk/template/azure-template-three/Cargo.lock @@ -0,0 +1,212 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "azure_template_three" +version = "1.0.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +dependencies = [ + "once_cell", + "python3-dll-a", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "python3-dll-a" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d80ba7540edb18890d444c5aa8e1f1f99b1bdf26fb26ae383135325f4a36042b" +dependencies = [ + "cc", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/sdk/template/azure-template-three/Cargo.toml b/sdk/template/azure-template-three/Cargo.toml new file mode 100644 index 000000000000..7ad9e2869c82 --- /dev/null +++ b/sdk/template/azure-template-three/Cargo.toml @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[package] +name = "azure_template_three" +version = "1.0.0" +edition = "2021" +rust-version = "1.75" +license = "MIT" +publish = false + +[lib] +name = "_rust" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.22", features = ["extension-module", "abi3-py310", "generate-import-lib"] } diff --git a/sdk/template/azure-template-three/LICENSE b/sdk/template/azure-template-three/LICENSE new file mode 100644 index 000000000000..b2f52a2bad4e --- /dev/null +++ b/sdk/template/azure-template-three/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/template/azure-template-three/README.md b/sdk/template/azure-template-three/README.md new file mode 100644 index 000000000000..b6ad6244f5e1 --- /dev/null +++ b/sdk/template/azure-template-three/README.md @@ -0,0 +1,80 @@ +# Azure Rust Template Package client library for Python + +**This package is an engineering-system test fixture, not a service SDK.** + +`azure-template-three` complements the pure-Python `azure-template` and the +setuptools/C-extension `azure-template-two`. It builds one PyO3 function with +Maturin, without setuptools `ext_modules`, to exercise the Rust native-wheel +path. + +```python +from azure.template.three import _rust + +assert _rust.is_true() is True +``` + +## Run template CI + +Queue the template CI pipeline for the desired branch. Every run builds all +three fixtures and installs the Microsoft Rust toolchain for +`azure-template-three`. The Rust fixture follows the normal signing and release +workflow. `ReleaseToDevOpsOnly` can direct the release to the existing staging +feed instead of PyPI. + +Template CI validates this fixture. Generic PR validation excludes it because +that pipeline does not enable its Rust toolchain setup. Shared PR +discovery and dependency validation otherwise remain unchanged. +The separate template live-test pipeline continues to target the Python and C +fixtures because its source-installing jobs do not provision Microsoft Rust. + +The feed and tool versions use: + +- Toolchain: `ms-prod-1.97`, selected by `rust-toolchain.toml`. +- Toolchain feed: `https://pkgs.dev.azure.com/azure-sdk/_packaging/azure-sdk-for-rust-org-test/nuget/v3/index.json`. +- Build backend: Maturin `1.15.0`. +- Binding: PyO3 `0.22`, with the Python 3.10 stable ABI. + +The wheel matrix covers Linux x86_64/aarch64, macOS arm64, and Windows +AMD64/ARM64. Linux containers bootstrap their own Microsoft Rust toolchain +and configure the authenticated Cargo mirror. Windows ARM64 cross-compilation +uses the ARM64 Python import libraries provisioned by the shared build job. +Installed-wheel tests call the Rust function and verify that the module is +native. Windows ARM64 import testing still requires an ARM64 runner; producing +a cross-compiled wheel on an x64 agent does not prove that it loads. + +Toolchain/feed credentials are supplied by the pipeline, never checked in. +Use a trusted Azure DevOps run with access to the prototype feed. Local checks +cannot establish that the internal toolchain or authenticated downloads work. +PR runs do not sign binaries. Internal manual runs can exercise the existing +signing and release stages. + +## Build locally + +With Microsoft Rust installed and authenticated: + +```bash +python -m build sdk/template/azure-template-three +python -m pip install sdk/template/azure-template-three/dist/*.whl +python -m pytest sdk/template/azure-template-three/tests +``` + +For a local compile/import check using public Rust instead of Microsoft Rust: + +```bash +RUSTUP_TOOLCHAIN=stable python -m build sdk/template/azure-template-three +``` + +This does not exercise the CI toolchain feed, manylinux bootstrap, or credentials. + +## Validation boundaries + +Ordinary source-installing analysis, documentation, and test jobs remain +disabled, as in the C fixture. + +Windows ARM64 wheels are cross-built and signed, but their runtime import still +needs a Windows ARM64 runner. Linux aarch64 build tests use the existing QEMU +setup. + +# Contributing + +If you encounter any bugs or have suggestions, please file an issue in the [Issues]() section of the project. diff --git a/sdk/template/azure-template-three/azure/template/three/__init__.py b/sdk/template/azure-template-three/azure/template/three/__init__.py new file mode 100644 index 000000000000..c960807c06fd --- /dev/null +++ b/sdk/template/azure-template-three/azure/template/three/__init__.py @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from ._version import VERSION + +__version__ = VERSION diff --git a/sdk/template/azure-template-three/azure/template/three/_rust.pyi b/sdk/template/azure-template-three/azure/template/three/_rust.pyi new file mode 100644 index 000000000000..672767dae5cb --- /dev/null +++ b/sdk/template/azure-template-three/azure/template/three/_rust.pyi @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def is_true() -> bool: ... diff --git a/sdk/template/azure-template-three/azure/template/three/_version.py b/sdk/template/azure-template-three/azure/template/three/_version.py new file mode 100644 index 000000000000..e5b9bca3a368 --- /dev/null +++ b/sdk/template/azure-template-three/azure/template/three/_version.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +VERSION = "1.0.0b1" diff --git a/sdk/template/azure-template-three/dev_requirements.txt b/sdk/template/azure-template-three/dev_requirements.txt new file mode 100644 index 000000000000..190d2e5520b2 --- /dev/null +++ b/sdk/template/azure-template-three/dev_requirements.txt @@ -0,0 +1,2 @@ +-e ../../../eng/tools/azure-sdk-tools +pytest diff --git a/sdk/template/azure-template-three/pyproject.toml b/sdk/template/azure-template-three/pyproject.toml new file mode 100644 index 000000000000..e7fa483657a7 --- /dev/null +++ b/sdk/template/azure-template-three/pyproject.toml @@ -0,0 +1,102 @@ +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- + +[build-system] +requires = ["maturin==1.15.0"] +build-backend = "maturin" + +[project] +name = "azure-template-three" +version = "1.0.0b1" +description = "Microsoft Azure Template Package (Rust) Client Library for Python" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "Microsoft Corporation", email = "azuresdkengsysadmins@microsoft.com" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dependencies = [] + +[project.urls] +"Bug Reports" = "https://github.com/Azure/azure-sdk-for-python/issues" +repository = "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/template/azure-template-three" + +[tool.maturin] +locked = true +python-source = "." +module-name = "azure.template.three._rust" +include = [ + { path = "rust-toolchain.toml", format = "sdist" }, + { path = "scripts/*.sh", format = "sdist" }, + { path = "tests/*.py", format = "sdist" }, + { path = "LICENSE", format = "sdist" }, + { path = "CHANGELOG.md", format = "sdist" }, +] +exclude = [ + "**/__pycache__/**", + "**/*.py[co]", + "azure/template/three/_rust.*.so", + "azure/template/three/_rust.*.pyd", +] + +# The fixture validates native wheels. Source-installing jobs do not yet provision Microsoft Rust. +[tool.azure-sdk-build] +mypy = false +pyright = false +type_check_samples = false +verifytypes = false +pylint = false +sphinx = false +verifysdist = false +verifywhl = false +sdist = false +breaking = false +suppressed_skip_warnings = ["*"] + +[tool.azure-sdk-conda] +in_bundle = false + +[tool.cibuildwheel] +build = ["cp310-*"] +skip = ["*-musllinux*"] +test-requires = "pytest" +test-command = "pytest {project}/tests" + +[tool.cibuildwheel.macos] +archs = ["arm64"] +environment = { PATH = "$HOME/.msrustup/multiplexers/bin:$PATH" } + +[tool.cibuildwheel.windows] +archs = ["AMD64", "ARM64"] + +[tool.cibuildwheel.linux] +archs = ["x86_64", "aarch64"] +manylinux-x86_64-image = "manylinux_2_28" +manylinux-aarch64-image = "manylinux_2_28" +environment-pass = [ + "PIP_INDEX_URL", + "MSRUSTUP_ACCESS_TOKEN", + "MSRUSTUP_FEED_URL", + "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX", + "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN", + "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_CREDENTIAL_PROVIDER", +] +environment = { MSRUSTUP_HOME = "/msrustup", CARGO_HOME = "/cargo", PATH = "/msrustup/multiplexers/bin:$PATH", CARGO_NET_GIT_FETCH_WITH_CLI = "true" } +before-all = "dnf install -y jq && mkdir -p $MSRUSTUP_HOME/bootstrap && cd $MSRUSTUP_HOME/bootstrap && sh /project/scripts/install-msrustup.sh && cd /project && $MSRUSTUP_HOME/bootstrap/msrustup toolchain install && sh /project/scripts/configure-cargo-feed.sh" + +[[tool.cibuildwheel.overrides]] +select = "*-win_arm64" +environment = { CARGO_BUILD_TARGET = "aarch64-pc-windows-msvc", PYO3_CROSS_LIB_DIR = "$PYTHON_ARM64_LIB_DIR" } diff --git a/sdk/template/azure-template-three/rust-toolchain.toml b/sdk/template/azure-template-three/rust-toolchain.toml new file mode 100644 index 000000000000..28651ef3888b --- /dev/null +++ b/sdk/template/azure-template-three/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "ms-prod-1.97" +profile = "minimal" diff --git a/sdk/template/azure-template-three/scripts/configure-cargo-feed.sh b/sdk/template/azure-template-three/scripts/configure-cargo-feed.sh new file mode 100644 index 000000000000..9e0386e6f670 --- /dev/null +++ b/sdk/template/azure-template-three/scripts/configure-cargo-feed.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +set -eu + +index="${CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX:-}" +if [ -z "$index" ]; then + echo "No Cargo registry index supplied; using crates.io." + exit 0 +fi +if [ -z "${CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN:-}" ]; then + echo "The configured Cargo feed requires an authentication token." >&2 + exit 1 +fi + +cargo_home="${CARGO_HOME:-$HOME/.cargo}" +mkdir -p "$cargo_home" +config="$cargo_home/config.toml" +if [ -e "$config" ]; then + echo "Refusing to overwrite the Cargo configuration: $config" >&2 + exit 1 +fi + +cat > "$config" </dev/null 2>&1 || { + echo "install-msrustup requires $tool." >&2 + exit 1 + } +done + +: "${MSRUSTUP_FEED_URL:?The pipeline must supply the Microsoft Rust toolchain feed.}" +if [ -z "${MSRUSTUP_ACCESS_TOKEN:-}" ] && [ -z "${MSRUSTUP_PAT:-}" ]; then + echo "MSRUSTUP_ACCESS_TOKEN or MSRUSTUP_PAT must be set." >&2 + exit 1 +fi + +do_curl() { + if [ -n "${MSRUSTUP_ACCESS_TOKEN:-}" ]; then + curl -fsSL --retry 5 -H "Authorization: Bearer $MSRUSTUP_ACCESS_TOKEN" "$@" + else + curl -fsSL --retry 5 -u ":$MSRUSTUP_PAT" "$@" + fi +} + +case "$(uname -s)" in + Linux) target_rest="-unknown-linux-gnu" ;; + *) echo "This bootstrap is only used by Linux wheel containers." >&2; exit 1 ;; +esac +case "$(uname -m)" in + x86_64) target_arch="x86_64" ;; + aarch64 | arm64) target_arch="aarch64" ;; + *) echo "Unsupported Microsoft Rust bootstrap architecture." >&2; exit 1 ;; +esac + +package="rust.msrustup-${target_arch}${target_rest}" +response=$(do_curl "$MSRUSTUP_FEED_URL") +base=$(printf '%s' "$response" | jq -er '.resources[] | select(."@type" == "PackageBaseAddress/3.0.0") | .["@id"]') +response=$(do_curl "${base}${package}/index.json") +version=$(printf '%s' "$response" | jq -er '.versions[0]') +latest="${base}${package}/${version}/${package}.${version}.nupkg" + +trap 'rm -f msrustup.zip' EXIT +echo "Installing msrustup ${version} for ${target_arch}${target_rest}." +do_curl "$latest" -o msrustup.zip +unzip -jqo msrustup.zip tools/msrustup +chmod +x msrustup diff --git a/sdk/template/azure-template-three/sdk_packaging.toml b/sdk/template/azure-template-three/sdk_packaging.toml new file mode 100644 index 000000000000..901bc8ccbfa6 --- /dev/null +++ b/sdk/template/azure-template-three/sdk_packaging.toml @@ -0,0 +1,2 @@ +[packaging] +auto_update = false diff --git a/sdk/template/azure-template-three/src/lib.rs b/sdk/template/azure-template-three/src/lib.rs new file mode 100644 index 000000000000..971eb3c5db62 --- /dev/null +++ b/sdk/template/azure-template-three/src/lib.rs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use pyo3::prelude::*; + +#[pyfunction] +fn is_true() -> bool { + true +} + +#[pymodule] +fn _rust(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_function(wrap_pyfunction!(is_true, module)?)?; + Ok(()) +} diff --git a/sdk/template/azure-template-three/tests/test_rust.py b/sdk/template/azure-template-three/tests/test_rust.py new file mode 100644 index 000000000000..968e472f3307 --- /dev/null +++ b/sdk/template/azure-template-three/tests/test_rust.py @@ -0,0 +1,29 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from importlib.machinery import EXTENSION_SUFFIXES +from importlib.metadata import version + +import pytest + +from azure.template.three import __version__, _rust + + +def test_is_true_returns_true(): + assert _rust.is_true() is True + + +def test_is_true_takes_no_arguments(): + with pytest.raises(TypeError): + _rust.is_true(1) + + +def test_extension_is_native(): + assert any(_rust.__file__.endswith(suffix) for suffix in EXTENSION_SUFFIXES) + + +def test_runtime_version_matches_distribution(): + assert __version__ == version("azure-template-three") diff --git a/sdk/template/ci.yml b/sdk/template/ci.yml index 404932379483..c96ad001b485 100644 --- a/sdk/template/ci.yml +++ b/sdk/template/ci.yml @@ -58,6 +58,10 @@ extends: oneESTemplateTag: ${{ parameters.oneESTemplateTag }} ServiceDirectory: template TestProxy: true + InstallMsRustToolchain: true + MsRustWorkingDirectory: $(Build.SourcesDirectory)/sdk/template/azure-template-three + MsRustToolchainFeed: https://pkgs.dev.azure.com/azure-sdk/_packaging/azure-sdk-for-rust-org-test/nuget/v3/index.json + MsRustAdditionalTargets: aarch64-pc-windows-msvc Artifacts: - name: azure-template safeName: azuretemplate @@ -73,3 +77,8 @@ extends: # that ships one wheel per platform. skipPublishDocGithubIo: true skipPublishDocMs: true + - name: azure-template-three + safeName: azuretemplatethree + signBinaries: true + skipPublishDocGithubIo: true + skipPublishDocMs: true diff --git a/sdk/template/tests.yml b/sdk/template/tests.yml index b9da355db231..808a3b59fe1c 100644 --- a/sdk/template/tests.yml +++ b/sdk/template/tests.yml @@ -4,6 +4,7 @@ extends: template: /eng/pipelines/templates/stages/archetype-sdk-tests.yml parameters: ServiceDirectory: template + BuildTargetingString: azure-template|azure-template-two MatrixReplace: - TestSamples=.*/true EnvVars: From e6fb68aee53b227b4b7109c0003161d4e987918d Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:50:27 -0700 Subject: [PATCH 7/9] black --- .../ci_tools/versioning/version_shared.py | 7 ++---- .../tests/test_native_build_pipeline.py | 24 +++++++------------ .../azure-sdk-tools/tests/test_versioning.py | 5 +++- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py b/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py index 6508c58ffc68..06bfdeffd8b3 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py +++ b/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py @@ -112,13 +112,10 @@ def set_version_py(setup_path, new_version): next_section = re.search(r"(?m)^\[", pyproject_contents[project_section.end() :]) project_end = project_section.end() + next_section.start() if next_section else len(pyproject_contents) project_contents = pyproject_contents[project_section.end() : project_end] - version_pattern = re.compile( - r"(?m)^(\s*version\s*=\s*)([\"'])([^\"'\r\n]+)(\2)([^\r\n]*)(\r?\n|$)" - ) + version_pattern = re.compile(r"(?m)^(\s*version\s*=\s*)([\"'])([^\"'\r\n]+)(\2)([^\r\n]*)(\r?\n|$)") replaced_project_contents, replacements = version_pattern.subn( lambda match: ( - f"{match.group(1)}{match.group(2)}{new_version}" - f"{match.group(4)}{match.group(5)}{match.group(6)}" + f"{match.group(1)}{match.group(2)}{new_version}" f"{match.group(4)}{match.group(5)}{match.group(6)}" ), project_contents, count=1, diff --git a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py index 1f2bc4b5c971..4161d09ee539 100644 --- a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py +++ b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py @@ -23,13 +23,9 @@ REPO_ROOT = Path(__file__).resolve().parents[4] TEMPLATES = REPO_ROOT / "eng" / "pipelines" / "templates" RUST_FIXTURE = REPO_ROOT / "sdk" / "template" / "azure-template-three" -GENERIC_PR_EXCLUSION_SCRIPT = ( - REPO_ROOT / "eng" / "scripts" / "Exclude-RustTemplateFixtureFromGenericPR.ps1" -) +GENERIC_PR_EXCLUSION_SCRIPT = REPO_ROOT / "eng" / "scripts" / "Exclude-RustTemplateFixtureFromGenericPR.ps1" RUST_GATE = "${{ if parameters.InstallMsRustToolchain }}" -RUST_TOOLCHAIN_FEED = ( - "https://pkgs.dev.azure.com/azure-sdk/_packaging/azure-sdk-for-rust-org-test/nuget/v3/index.json" -) +RUST_TOOLCHAIN_FEED = "https://pkgs.dev.azure.com/azure-sdk/_packaging/azure-sdk-for-rust-org-test/nuget/v3/index.json" RUST_PARAMETERS = ( "InstallMsRustToolchain", "MsRustWorkingDirectory", @@ -72,9 +68,10 @@ def test_rust_support_is_disabled_by_default_and_forwarded(): assert defaults["MsRustAdditionalTargets"] == "" installer = load_template("steps/install-msrust-toolchain.yml") - assert next( - parameter["default"] for parameter in installer["parameters"] if parameter["name"] == "ToolchainFeed" - ) == "" + assert ( + next(parameter["default"] for parameter in installer["parameters"] if parameter["name"] == "ToolchainFeed") + == "" + ) build_stage = next(stage for stage in archetype["extends"]["parameters"]["stages"] if stage.get("stage") == "Build") forwarded = build_stage["jobs"][0]["parameters"] @@ -246,9 +243,7 @@ def test_rust_template_is_always_on_and_uses_cosmos_prototype_settings(): parameters = pipeline["extends"]["parameters"] assert "BuildTargetingString" not in parameters assert parameters["InstallMsRustToolchain"] is True - assert parameters["MsRustWorkingDirectory"] == ( - "$(Build.SourcesDirectory)/sdk/template/azure-template-three" - ) + assert parameters["MsRustWorkingDirectory"] == ("$(Build.SourcesDirectory)/sdk/template/azure-template-three") assert parameters["MsRustToolchainFeed"] == RUST_TOOLCHAIN_FEED assert parameters["MsRustAdditionalTargets"] == "aarch64-pc-windows-msvc" assert "variables" not in pipeline @@ -336,15 +331,14 @@ def test_generic_pr_exclusion_applies_to_preselected_matrix_batches(tmp_path, ta assert not (tmp_path / "azure-template-three.json").exists() assert (tmp_path / "azure-cosmos.json").exists() + def test_generic_pr_exclusion_precedes_target_resolution_and_matrix_generation(): targeting = load_template("steps/resolve-package-targeting.yml")["steps"] assert all("Filter-RustTemplateFixture.ps1" not in step.get("pwsh", "") for step in targeting) build = load_template("steps/build-package-artifacts.yml")["steps"] filter_index = next( - index - for index, step in enumerate(build) - if "${{ if eq(parameters.ServiceDirectory, 'auto') }}" in step + index for index, step in enumerate(build) if "${{ if eq(parameters.ServiceDirectory, 'auto') }}" in step ) filter_step = build[filter_index]["${{ if eq(parameters.ServiceDirectory, 'auto') }}"][0] assert "Exclude-RustTemplateFixtureFromGenericPR.ps1" in filter_step["pwsh"] diff --git a/eng/tools/azure-sdk-tools/tests/test_versioning.py b/eng/tools/azure-sdk-tools/tests/test_versioning.py index c3614a2f2e4c..f675474e5e01 100644 --- a/eng/tools/azure-sdk-tools/tests/test_versioning.py +++ b/eng/tools/azure-sdk-tools/tests/test_versioning.py @@ -118,7 +118,9 @@ def test_set_version_synchronizes_static_pyproject_and_runtime_version(tmp_path) version_file = version_dir / "_version.py" version_file.write_text('VERSION = "1.0.0b1"\n') set_version_py(str(pyproject), "1.0.0b12345") - assert pyproject.read_text() == """[build-system] + assert ( + pyproject.read_text() + == """[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" @@ -132,6 +134,7 @@ def test_set_version_synchronizes_static_pyproject_and_runtime_version(tmp_path) [tool.example] version = "unchanged" """ + ) assert version_file.read_text() == 'VERSION = "1.0.0b12345"\n' assert ParsedSetup.from_path(str(package_dir)).version == "1.0.0b12345" From 8e1984e034ac7012bdf4f92744d8e757ea7784ee Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:35:50 -0700 Subject: [PATCH 8/9] Revert azure-template-three fixture onboarding Revert aaf2e33b4e9535b5e3319568982b868b3c1c37d7 and its formatting follow-up e6fb68aee53b227b4b7109c0003161d4e987918d. Remove the Rust fixture, generic PR exclusions, and fixture-driven version synchronization while preserving the original opt-in Rust build support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8323733d-4446-4291-afcc-33c49f5afa86 --- eng/pipelines/pullrequest.yml | 2 - eng/pipelines/templates/jobs/ci.yml | 4 - .../steps/build-package-artifacts.yml | 8 - ...clude-RustTemplateFixtureFromGenericPR.ps1 | 26 - .../ci_tools/versioning/version_shared.py | 27 -- .../tests/test_native_build_pipeline.py | 447 +----------------- .../azure-sdk-tools/tests/test_versioning.py | 56 +-- sdk/template/azure-template-three/.gitignore | 3 - .../azure-template-three/CHANGELOG.md | 9 - sdk/template/azure-template-three/Cargo.lock | 212 --------- sdk/template/azure-template-three/Cargo.toml | 17 - sdk/template/azure-template-three/LICENSE | 21 - sdk/template/azure-template-three/README.md | 80 ---- .../azure/template/three/__init__.py | 9 - .../azure/template/three/_rust.pyi | 4 - .../azure/template/three/_version.py | 7 - .../azure-template-three/dev_requirements.txt | 2 - .../azure-template-three/pyproject.toml | 102 ---- .../azure-template-three/rust-toolchain.toml | 3 - .../scripts/configure-cargo-feed.sh | 35 -- .../scripts/install-msrustup.sh | 49 -- .../azure-template-three/sdk_packaging.toml | 2 - sdk/template/azure-template-three/src/lib.rs | 15 - .../azure-template-three/tests/test_rust.py | 29 -- sdk/template/ci.yml | 9 - sdk/template/tests.yml | 1 - 26 files changed, 2 insertions(+), 1177 deletions(-) delete mode 100644 eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 delete mode 100644 sdk/template/azure-template-three/.gitignore delete mode 100644 sdk/template/azure-template-three/CHANGELOG.md delete mode 100644 sdk/template/azure-template-three/Cargo.lock delete mode 100644 sdk/template/azure-template-three/Cargo.toml delete mode 100644 sdk/template/azure-template-three/LICENSE delete mode 100644 sdk/template/azure-template-three/README.md delete mode 100644 sdk/template/azure-template-three/azure/template/three/__init__.py delete mode 100644 sdk/template/azure-template-three/azure/template/three/_rust.pyi delete mode 100644 sdk/template/azure-template-three/azure/template/three/_version.py delete mode 100644 sdk/template/azure-template-three/dev_requirements.txt delete mode 100644 sdk/template/azure-template-three/pyproject.toml delete mode 100644 sdk/template/azure-template-three/rust-toolchain.toml delete mode 100644 sdk/template/azure-template-three/scripts/configure-cargo-feed.sh delete mode 100644 sdk/template/azure-template-three/scripts/install-msrustup.sh delete mode 100644 sdk/template/azure-template-three/sdk_packaging.toml delete mode 100644 sdk/template/azure-template-three/src/lib.rs delete mode 100644 sdk/template/azure-template-three/tests/test_rust.py diff --git a/eng/pipelines/pullrequest.yml b/eng/pipelines/pullrequest.yml index 1ec1d5985583..41a3275a0477 100644 --- a/eng/pipelines/pullrequest.yml +++ b/eng/pipelines/pullrequest.yml @@ -33,5 +33,3 @@ extends: # As such, the pullrequest pipeline itself will not invoke the cosmos tests, but the triggered `python - cosmos - ci` public build WILL. ExcludePaths: - sdk/cosmos/ - # Template CI validates the Rust fixture; generic PR builds do not enable its Rust toolchain setup. - - sdk/template/azure-template-three/ diff --git a/eng/pipelines/templates/jobs/ci.yml b/eng/pipelines/templates/jobs/ci.yml index 8c7e185c7d7e..1beadd69cec0 100644 --- a/eng/pipelines/templates/jobs/ci.yml +++ b/eng/pipelines/templates/jobs/ci.yml @@ -343,10 +343,6 @@ jobs: parameters: ServiceDirectory: ${{parameters.ServiceDirectory}} ExcludePaths: ${{parameters.ExcludePaths}} - - pwsh: | - ./eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 ` - -PackagePropertiesFolder "$(Build.ArtifactStagingDirectory)/PackageInfo" - displayName: Exclude Rust template fixture from generic PR validation CloudConfig: Cloud: Public AdditionalParameters: diff --git a/eng/pipelines/templates/steps/build-package-artifacts.yml b/eng/pipelines/templates/steps/build-package-artifacts.yml index 7aa210c783d9..cc0794dce664 100644 --- a/eng/pipelines/templates/steps/build-package-artifacts.yml +++ b/eng/pipelines/templates/steps/build-package-artifacts.yml @@ -98,14 +98,6 @@ steps: ServiceDirectory: ${{ parameters.ServiceDirectory }} ExcludePaths: ${{ parameters.ExcludePaths }} - # Template CI validates the Rust fixture; generic PR builds do not enable - # its Rust toolchain setup. - - ${{ if eq(parameters.ServiceDirectory, 'auto') }}: - - pwsh: | - ./eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 ` - -PackagePropertiesFolder "$(Build.ArtifactStagingDirectory)/PackageInfo" - displayName: Exclude Rust template fixture from generic PR validation - - template: /eng/pipelines/templates/steps/resolve-package-targeting.yml parameters: BuildTargetingString: ${{ parameters.BuildTargetingString }} diff --git a/eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 b/eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 deleted file mode 100644 index d8bcc49decc4..000000000000 --- a/eng/scripts/Exclude-RustTemplateFixtureFromGenericPR.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -param ( - [string] $PackagePropertiesFolder = '' -) - -$ErrorActionPreference = 'Stop' - -if ($PackagePropertiesFolder -and (Test-Path $PackagePropertiesFolder)) { - Get-ChildItem -Path $PackagePropertiesFolder -Recurse -File -Filter 'azure-template-three.json' | - ForEach-Object { - Write-Host 'Generic PR builds do not enable the Rust toolchain setup; excluding azure-template-three, which template CI validates.' - Remove-Item -LiteralPath $_.FullName - } -} - -# Matrix jobs can already have TargetingString set and skip normal targeting resolution. -if ($env:TARGETINGSTRING) { - $targets = @($env:TARGETINGSTRING.Split(',') | ForEach-Object { $_.Trim() } | - Where-Object { $_ -and $_ -ne 'azure-template-three' }) - if ($targets.Count -eq 0) { - $targets = @('null') - } - Write-Host "##vso[task.setvariable variable=TargetingString;]$($targets -join ',')" -} diff --git a/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py b/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py index 06bfdeffd8b3..cc579312e6ea 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py +++ b/eng/tools/azure-sdk-tools/ci_tools/versioning/version_shared.py @@ -102,33 +102,6 @@ def set_version_py(setup_path, new_version): version_py_file.write(replaced_version_contents) - pyproject_location = path.join(path.dirname(setup_path), "pyproject.toml") - if path.isfile(pyproject_location): - with open(pyproject_location, "r", newline="") as pyproject_file: - pyproject_contents = pyproject_file.read() - - project_section = re.search(r"(?m)^\[project\][^\r\n]*(?:\r?\n|$)", pyproject_contents) - if project_section: - next_section = re.search(r"(?m)^\[", pyproject_contents[project_section.end() :]) - project_end = project_section.end() + next_section.start() if next_section else len(pyproject_contents) - project_contents = pyproject_contents[project_section.end() : project_end] - version_pattern = re.compile(r"(?m)^(\s*version\s*=\s*)([\"'])([^\"'\r\n]+)(\2)([^\r\n]*)(\r?\n|$)") - replaced_project_contents, replacements = version_pattern.subn( - lambda match: ( - f"{match.group(1)}{match.group(2)}{new_version}" f"{match.group(4)}{match.group(5)}{match.group(6)}" - ), - project_contents, - count=1, - ) - if replacements: - updated_contents = ( - pyproject_contents[: project_section.end()] - + replaced_project_contents - + pyproject_contents[project_end:] - ) - with open(pyproject_location, "w", newline="") as pyproject_file: - pyproject_file.write(updated_contents) - # Get classification for PyPI (https://pypi.org/classifiers/) def get_classification(version): diff --git a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py index 4161d09ee539..8f28434007d7 100644 --- a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py +++ b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py @@ -10,22 +10,14 @@ import re import shutil import subprocess -import sys import pytest import yaml -from ci_tools.build import create_package -from ci_tools.parsing import ParsedSetup -from ci_tools.parsing.parse_functions import get_pyproject_dict - REPO_ROOT = Path(__file__).resolve().parents[4] TEMPLATES = REPO_ROOT / "eng" / "pipelines" / "templates" -RUST_FIXTURE = REPO_ROOT / "sdk" / "template" / "azure-template-three" -GENERIC_PR_EXCLUSION_SCRIPT = REPO_ROOT / "eng" / "scripts" / "Exclude-RustTemplateFixtureFromGenericPR.ps1" RUST_GATE = "${{ if parameters.InstallMsRustToolchain }}" -RUST_TOOLCHAIN_FEED = "https://pkgs.dev.azure.com/azure-sdk/_packaging/azure-sdk-for-rust-org-test/nuget/v3/index.json" RUST_PARAMETERS = ( "InstallMsRustToolchain", "MsRustWorkingDirectory", @@ -63,15 +55,7 @@ def test_rust_support_is_disabled_by_default_and_forwarded(): if parameter["name"] in RUST_PARAMETERS } assert defaults["InstallMsRustToolchain"] is False - assert defaults["MsRustWorkingDirectory"] == "" - assert defaults["MsRustToolchainFeed"] == "" - assert defaults["MsRustAdditionalTargets"] == "" - - installer = load_template("steps/install-msrust-toolchain.yml") - assert ( - next(parameter["default"] for parameter in installer["parameters"] if parameter["name"] == "ToolchainFeed") - == "" - ) + assert all(defaults[name] == "" for name in RUST_PARAMETERS[1:]) build_stage = next(stage for stage in archetype["extends"]["parameters"]["stages"] if stage.get("stage") == "Build") forwarded = build_stage["jobs"][0]["parameters"] @@ -235,432 +219,3 @@ def test_build_powershell_scripts_parse(): SCRIPT=script, ) assert result.returncode == 0, f"{template_path}: {step['displayName']}: {result.stderr}" - - -def test_rust_template_is_always_on_and_uses_cosmos_prototype_settings(): - pipeline = yaml.safe_load((REPO_ROOT / "sdk" / "template" / "ci.yml").read_text()) - assert all(parameter["name"] != "EnableRustFixture" for parameter in pipeline["parameters"]) - parameters = pipeline["extends"]["parameters"] - assert "BuildTargetingString" not in parameters - assert parameters["InstallMsRustToolchain"] is True - assert parameters["MsRustWorkingDirectory"] == ("$(Build.SourcesDirectory)/sdk/template/azure-template-three") - assert parameters["MsRustToolchainFeed"] == RUST_TOOLCHAIN_FEED - assert parameters["MsRustAdditionalTargets"] == "aarch64-pc-windows-msvc" - assert "variables" not in pipeline - artifact_entries = parameters["Artifacts"] - assert [artifact["name"] for artifact in artifact_entries] == [ - "azure-template", - "azure-template-two", - "azure-template-three", - ] - rust = artifact_entries[2] - assert rust["name"] == "azure-template-three" - assert rust["signBinaries"] is True - assert "skipPublishDevFeed" not in rust - - pullrequest = yaml.safe_load((REPO_ROOT / "eng" / "pipelines" / "pullrequest.yml").read_text()) - assert "sdk/template/azure-template-three/" in pullrequest["extends"]["parameters"]["ExcludePaths"] - live = yaml.safe_load((REPO_ROOT / "sdk" / "template" / "tests.yml").read_text()) - assert live["extends"]["parameters"]["BuildTargetingString"] == "azure-template|azure-template-two" - - -@pytest.mark.parametrize( - "targeting,expected", - [ - ("azure-*", ["azure-template", "azure-template-two"]), - ("azure-template*", ["azure-template", "azure-template-two"]), - ("azure-template-three", []), - ("azure-template-two", ["azure-template-two"]), - ("missing-*", []), - ], -) -def test_generic_pr_package_targeting_excludes_rust_fixture(tmp_path, targeting, expected): - for name in ("azure-template", "azure-template-two", "azure-template-three"): - (tmp_path / f"{name}.json").write_text(json.dumps({"IncludedForValidation": False})) - script = REPO_ROOT / "eng" / "scripts" / "resolve-package-info.ps1" - result = run_powershell( - """ - $ErrorActionPreference = 'Stop' - & $env:FILTER_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO - & $env:RESOLVE_SCRIPT -ParameterTargetingStringSetting $env:TARGETS -PackagePropertiesFolder $env:PACKAGE_INFO - """, - FILTER_SCRIPT=str(GENERIC_PR_EXCLUSION_SCRIPT), - RESOLVE_SCRIPT=str(script), - TARGETS=targeting, - PACKAGE_INFO=str(tmp_path), - BUILDTARGETINGSTRING="", - TARGETINGSTRING="", - ) - assert result.returncode == 0, result.stderr - assert sorted(path.stem for path in tmp_path.glob("*.json")) == expected - output = next(line for line in result.stdout.splitlines() if "variable=TargetingString;]" in line) - targets = output.split("]", 1)[1] - assert sorted(targets.split(",")) == (expected or ["null"]) - - -def run_generic_pr_exclusion(tmp_path, **environment): - return run_powershell( - """ - $ErrorView = 'NormalView' - & $env:FILTER_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO - """, - **{ - "FILTER_SCRIPT": str(GENERIC_PR_EXCLUSION_SCRIPT), - "PACKAGE_INFO": str(tmp_path), - "TARGETINGSTRING": "", - **environment, - }, - ) - - -@pytest.mark.parametrize( - "targets,expected", - [ - ("azure-template,azure-template-three,azure-template-two", "azure-template,azure-template-two"), - (" azure-template-three , azure-cosmos ", "azure-cosmos"), - ("azure-template-three", "null"), - ("azure-cosmos", "azure-cosmos"), - ], -) -def test_generic_pr_exclusion_applies_to_preselected_matrix_batches(tmp_path, targets, expected): - for name in ("azure-template", "azure-template-two", "azure-template-three", "azure-cosmos"): - (tmp_path / f"{name}.json").write_text("{}") - result = run_generic_pr_exclusion(tmp_path, TARGETINGSTRING=targets) - assert result.returncode == 0, result.stderr - assert f"variable=TargetingString;]{expected}" in result.stdout - assert not (tmp_path / "azure-template-three.json").exists() - assert (tmp_path / "azure-cosmos.json").exists() - - -def test_generic_pr_exclusion_precedes_target_resolution_and_matrix_generation(): - targeting = load_template("steps/resolve-package-targeting.yml")["steps"] - assert all("Filter-RustTemplateFixture.ps1" not in step.get("pwsh", "") for step in targeting) - - build = load_template("steps/build-package-artifacts.yml")["steps"] - filter_index = next( - index for index, step in enumerate(build) if "${{ if eq(parameters.ServiceDirectory, 'auto') }}" in step - ) - filter_step = build[filter_index]["${{ if eq(parameters.ServiceDirectory, 'auto') }}"][0] - assert "Exclude-RustTemplateFixtureFromGenericPR.ps1" in filter_step["pwsh"] - resolve_index = next( - index - for index, step in enumerate(build) - if step.get("template", "").endswith("/steps/resolve-package-targeting.yml") - ) - assert filter_index < resolve_index - - jobs = load_template("jobs/ci.yml")["jobs"] - matrix = next(job for job in jobs if job.get("template", "").endswith("/jobs/generate-job-matrix.yml")) - steps = matrix["parameters"]["${{ if eq(parameters.ServiceDirectory, 'auto') }}"]["PreGenerationSteps"] - assert steps[-2]["template"].endswith("/steps/save-package-properties.yml") - assert "Exclude-RustTemplateFixtureFromGenericPR.ps1" in steps[-1]["pwsh"] - assert '-PackagePropertiesFolder "$(Build.ArtifactStagingDirectory)/PackageInfo"' in steps[-1]["pwsh"] - assert "condition" not in steps[-1] - - -@pytest.mark.parametrize("indirect", [False, True]) -def test_pr_matrix_batches_exclude_rust_fixture(tmp_path, indirect): - package_info = tmp_path / "PackageInfo" - package_info.mkdir() - for name in ("azure-template", "azure-template-two", "azure-template-three", "azure-cosmos"): - (package_info / f"{name}.json").write_text( - json.dumps( - { - "ArtifactName": name, - "IncludedForValidation": indirect, - "CIParameters": {"CIMatrixConfigs": None}, - } - ) - ) - config = tmp_path / "matrix.json" - archetype = load_template("stages/archetype-sdk-client.yml") - config.write_text( - json.dumps( - next(parameter["default"] for parameter in archetype["parameters"] if parameter["name"] == "MatrixConfigs") - ) - ) - result = run_powershell( - """ - $ErrorActionPreference = 'Stop' - & $env:FILTER_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO - & $env:MATRIX_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO ` - -PRMatrixFile $env:MATRIX_CONFIG -PRMatrixSetting TargetingString ` - -Filters 'container=^$', 'SupportedClouds=^$|Public', 'Pool=.*Linux.*Pool$' -CI - """, - FILTER_SCRIPT=str(GENERIC_PR_EXCLUSION_SCRIPT), - MATRIX_SCRIPT=str(REPO_ROOT / "eng" / "common" / "scripts" / "job-matrix" / "Create-PrJobMatrix.ps1"), - PACKAGE_INFO=str(package_info), - MATRIX_CONFIG=str(config), - TARGETINGSTRING="", - BUILD_REASON="PullRequest", - SETDEVVERSION="false", - LINUXVMIMAGE="ubuntu-24.04", - LINUXPOOL="AzureSDKLinuxPool", - WINDOWSVMIMAGE="windows-2022", - WINDOWSPOOL="AzureSDKWindowsPool", - MACVMIMAGE="macOS-15", - MACPOOL="AzureSDKMacPool", - ) - assert result.returncode == 0, result.stderr - output = next( - line - for line in result.stdout.splitlines() - if line.startswith("##vso[task.setVariable variable=matrix;isOutput=true]") - ) - matrix = json.loads(output.split("]", 1)[1]) - assert matrix - targets = {name for job in matrix.values() for name in job["TargetingString"].split(",")} - assert targets == {"azure-template", "azure-template-two", "azure-cosmos"} - - -def run_pr_fixture_discovery(tmp_path, changed, include_cosmos=False): - diff = tmp_path / "diff.json" - diff.write_text( - json.dumps( - { - "ChangedFiles": changed, - "ExcludePaths": ["sdk/template/azure-template-three/", "sdk/cosmos/"], - } - ) - ) - source = (REPO_ROOT / "eng" / "common" / "scripts" / "Package-Properties.ps1").read_text() - helpers = source[source.index("function Get-TriggerPaths(") : source.index("function Get-AllPkgProperties (")] - package_info = tmp_path / "PackageInfo" - package_info.mkdir() - return run_powershell( - """ - $ErrorActionPreference = 'Stop' - $RepoRoot = $env:REPO_ROOT - function LogInfo($message) {} - function Get-AllPkgProperties { - foreach ($name in @('azure-template', 'azure-template-two', 'azure-template-three')) { - [PSCustomObject]@{ - Name = $name - DirectoryPath = Join-Path $RepoRoot "sdk/template/$name" - ServiceDirectory = 'template' - ArtifactDetails = @{ triggeringPaths = @('sdk/template/ci.yml') } - AdditionalValidationPackages = @( - if ($env:INCLUDE_COSMOS -eq 'true' -and $name -eq 'azure-template-two') { - 'sdk/cosmos/azure-cosmos' - } - ) - IncludedForValidation = $false - } - } - if ($env:INCLUDE_COSMOS -eq 'true') { - [PSCustomObject]@{ - Name = 'azure-cosmos' - DirectoryPath = Join-Path $RepoRoot 'sdk/cosmos/azure-cosmos' - ServiceDirectory = 'cosmos' - ArtifactDetails = @{ triggeringPaths = @('sdk/cosmos/ci.yml') } - AdditionalValidationPackages = @() - IncludedForValidation = $false - } - } - } - """ - + helpers - + """ - $packages = @(Get-PrPkgProperties $env:PR_DIFF) - $names = @($packages | Select-Object -ExpandProperty Name | Sort-Object) - Write-Host "SelectedPackages:$($names -join ',')" - foreach ($package in $packages) { - $package | ConvertTo-Json -Depth 10 | Set-Content ( - Join-Path $env:PACKAGE_INFO "$($package.Name).json" - ) - } - & $env:FILTER_SCRIPT -PackagePropertiesFolder $env:PACKAGE_INFO - $retained = @(Get-ChildItem $env:PACKAGE_INFO -Filter '*.json' | - Select-Object -ExpandProperty BaseName | Sort-Object) - Write-Host "RetainedPackages:$($retained -join ',')" - """, - REPO_ROOT=str(REPO_ROOT), - PR_DIFF=str(diff), - PACKAGE_INFO=str(package_info), - FILTER_SCRIPT=str(GENERIC_PR_EXCLUSION_SCRIPT), - INCLUDE_COSMOS=str(include_cosmos), - TARGETINGSTRING="", - ) - - -@pytest.mark.parametrize("changed", [["sdk/template/ci.yml"], ["eng/scripts/resolve-package-info.ps1"]]) -def test_generic_pr_exclusion_filters_service_changes_and_fallback(tmp_path, changed): - result = run_pr_fixture_discovery(tmp_path, changed) - assert result.returncode == 0, result.stderr - assert "SelectedPackages:azure-template,azure-template-three,azure-template-two" in result.stdout - assert "RetainedPackages:azure-template,azure-template-two" in result.stdout - - -def test_pr_discovery_preserves_excluded_package_dependency_validation(tmp_path): - result = run_pr_fixture_discovery(tmp_path, ["sdk/template/azure-template-two/README.md"], include_cosmos=True) - assert result.returncode == 0, result.stderr - assert "SelectedPackages:azure-cosmos,azure-template-two" in result.stdout - assert "RetainedPackages:azure-cosmos,azure-template-two" in result.stdout - cosmos = json.loads((tmp_path / "PackageInfo" / "azure-cosmos.json").read_text()) - assert cosmos["IncludedForValidation"] is True - - -def test_rust_fixture_routes_through_cibuildwheel_without_setuptools_extensions(tmp_path, monkeypatch): - package = ParsedSetup.from_path(str(RUST_FIXTURE)) - assert package.name == "azure-template-three" - assert package.version == "1.0.0b1" - assert package.namespace == "azure.template.three" - assert package.uses_cibuildwheel - assert not package.ext_modules - commands = [] - monkeypatch.setattr("ci_tools.build.run_logged", lambda command, **kwargs: commands.append(command)) - create_package(str(RUST_FIXTURE), str(tmp_path)) - assert commands == [ - [sys.executable, "-m", "cibuildwheel", "--output-dir", str(tmp_path)], - [sys.executable, "-m", "build", "-s", "-o", str(tmp_path)], - ] - - -def test_rust_fixture_matrix_and_container_contract(): - config = get_pyproject_dict(str(RUST_FIXTURE / "pyproject.toml")) - assert config["build-system"] == {"requires": ["maturin==1.15.0"], "build-backend": "maturin"} - assert config["tool"]["maturin"]["locked"] is True - wheel = config["tool"]["cibuildwheel"] - assert wheel["linux"]["archs"] == ["x86_64", "aarch64"] - assert wheel["macos"]["archs"] == ["arm64"] - assert wheel["windows"]["archs"] == ["AMD64", "ARM64"] - build = load_template("steps/build-package-artifacts.yml") - forwarding = next( - step for step in build["steps"] if step.get("displayName") == "Forward build environment to cibuildwheel" - ) - variables = {"PIP_INDEX_URL", *forwarding["env"][RUST_GATE]["RUST_ENVIRONMENT_PASS"].split()} - assert set(wheel["linux"]["environment-pass"]) == variables - assert "scripts/install-msrustup.sh" in wheel["linux"]["before-all"] - assert "scripts/configure-cargo-feed.sh" in wheel["linux"]["before-all"] - assert wheel["overrides"] == [ - { - "select": "*-win_arm64", - "environment": { - "CARGO_BUILD_TARGET": "aarch64-pc-windows-msvc", - "PYO3_CROSS_LIB_DIR": "$PYTHON_ARM64_LIB_DIR", - }, - } - ] - toolchain = get_pyproject_dict(str(RUST_FIXTURE / "rust-toolchain.toml")) - assert toolchain["toolchain"] == {"channel": "ms-prod-1.97", "profile": "minimal"} - - -def run_cargo_configuration(tmp_path, index="", token=""): - shell = shutil.which("sh") - if not shell: - pytest.skip("A POSIX shell is required for Linux container configuration tests") - return subprocess.run( - [shell, str(RUST_FIXTURE / "scripts" / "configure-cargo-feed.sh")], - env={ - **os.environ, - "CARGO_HOME": str(tmp_path), - "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX": index, - "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN": token, - }, - capture_output=True, - text=True, - timeout=30, - check=False, - ) - - -def test_container_cargo_configuration_keeps_credentials_out_of_files(tmp_path): - index = "sparse+https://example.test/Cargo/index/" - result = run_cargo_configuration(tmp_path, index, "fixture-test-token") - assert result.returncode == 0, result.stderr - config = (tmp_path / "config.toml").read_text() - assert index in config - assert 'replace-with = "azure-sdk-for-rust-public"' in config - assert "fixture-test-token" not in config + result.stdout + result.stderr - result = run_cargo_configuration(tmp_path, index, "fixture-test-token") - assert result.returncode != 0 - assert (tmp_path / "config.toml").read_text() == config - - -def test_container_cargo_configuration_requires_token_when_index_is_set(tmp_path): - result = run_cargo_configuration(tmp_path, "sparse+https://example.test/Cargo/index/") - assert result.returncode != 0 - assert "requires an authentication token" in result.stderr - assert not (tmp_path / "config.toml").exists() - - -def test_container_cargo_configuration_preserves_local_crates_io(tmp_path): - existing = tmp_path / "config.toml" - existing.write_text("[net]\noffline = true\n") - result = run_cargo_configuration(tmp_path) - assert result.returncode == 0, result.stderr - assert existing.read_text() == "[net]\noffline = true\n" - - -@pytest.mark.parametrize( - "architecture,failure,expected", - [ - ("x86_64", "", 0), - ("aarch64", "", 0), - ("x86_64", "index", 1), - ("unsupported", "", 1), - ], -) -def test_container_msrustup_bootstrap_downloads_and_surfaces_failures(tmp_path, architecture, failure, expected): - if os.name == "nt" or not shutil.which("jq"): - pytest.skip("The Linux bootstrap requires POSIX executables and jq") - tools = tmp_path / "bin" - tools.mkdir() - commands = { - "uname": '#!/bin/sh\nif [ "$1" = "-s" ]; then echo Linux; else echo "$FIXTURE_ARCH"; fi\n', - "unzip": '#!/bin/sh\nprintf "#!/bin/sh\\nexit 0\\n" > msrustup\n', - "curl": """#!/bin/sh -while [ "$#" -gt 0 ]; do - case "$1" in - -H) [ "$2" = "Authorization: Bearer fixture-test-token" ] || exit 9; shift 2 ;; - --retry) shift 2 ;; - -o) : > "$2"; shift 2 ;; - -*) shift ;; - *) url="$1"; shift ;; - esac -done -case "$url" in - https://example.test/feed) - printf '{"resources":[{"@type":"PackageBaseAddress/3.0.0","@id":"https://example.test/packages/"}]}' - ;; - */index.json) - [ "$FIXTURE_FAILURE" != "index" ] || exit 1 - printf '{"versions":["7.0.0"]}' - ;; - *.nupkg) printf '%s\\n' "$url" > "$FIXTURE_REQUEST" ;; - *) exit 8 ;; -esac -""", - } - for name, content in commands.items(): - executable = tools / name - executable.write_text(content) - executable.chmod(0o755) - request = tmp_path / "request.txt" - result = subprocess.run( - ["sh", str(RUST_FIXTURE / "scripts" / "install-msrustup.sh")], - cwd=tmp_path, - env={ - **os.environ, - "PATH": f"{tools}{os.pathsep}{os.environ['PATH']}", - "MSRUSTUP_FEED_URL": "https://example.test/feed", - "MSRUSTUP_ACCESS_TOKEN": "fixture-test-token", - "MSRUSTUP_PAT": "", - "FIXTURE_ARCH": architecture, - "FIXTURE_FAILURE": failure, - "FIXTURE_REQUEST": str(request), - }, - capture_output=True, - text=True, - timeout=30, - check=False, - ) - assert result.returncode == expected, result.stderr - assert "fixture-test-token" not in result.stdout + result.stderr - assert not (tmp_path / "msrustup.zip").exists() - if expected == 0: - package = f"rust.msrustup-{architecture}-unknown-linux-gnu" - assert request.read_text().strip() == f"https://example.test/packages/{package}/7.0.0/{package}.7.0.0.nupkg" - assert os.access(tmp_path / "msrustup", os.X_OK) - else: - assert not (tmp_path / "msrustup").exists() diff --git a/eng/tools/azure-sdk-tools/tests/test_versioning.py b/eng/tools/azure-sdk-tools/tests/test_versioning.py index f675474e5e01..cd355ed80163 100644 --- a/eng/tools/azure-sdk-tools/tests/test_versioning.py +++ b/eng/tools/azure-sdk-tools/tests/test_versioning.py @@ -7,7 +7,7 @@ from ci_tools.parsing import ParsedSetup from ci_tools.versioning.version_set_dev import set_dev_version -from ci_tools.versioning.version_shared import get_classification, DEV_STATUS_REGEX, set_version_py +from ci_tools.versioning.version_shared import get_classification, DEV_STATUS_REGEX integration_folder = os.path.join(os.path.dirname(__file__), "integration") setup_folder = os.path.join(integration_folder, "scenarios", "setup_py_project_def") @@ -92,57 +92,3 @@ def test_set_dev_version_on_setup(): # Expect version to have 'a' suffix with zero-padded build id assert parsed_after.version == "1.0.0a20250813001" assert "Development Status :: 4 - Beta" in parsed_after.classifiers - - -def test_set_version_synchronizes_static_pyproject_and_runtime_version(tmp_path): - package_dir = tmp_path / "static-version-package" - version_dir = package_dir / "src" - version_dir.mkdir(parents=True) - pyproject = package_dir / "pyproject.toml" - pyproject.write_text( - """[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - -[project] -name = "static-version-package" -# Keep this comment and the original formatting. -version = "1.0.0b1" # Distribution version -requires-python = ">=3.10" -dependencies = [] - -[tool.example] -version = "unchanged" -""" - ) - version_file = version_dir / "_version.py" - version_file.write_text('VERSION = "1.0.0b1"\n') - set_version_py(str(pyproject), "1.0.0b12345") - assert ( - pyproject.read_text() - == """[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - -[project] -name = "static-version-package" -# Keep this comment and the original formatting. -version = "1.0.0b12345" # Distribution version -requires-python = ">=3.10" -dependencies = [] - -[tool.example] -version = "unchanged" -""" - ) - assert version_file.read_text() == 'VERSION = "1.0.0b12345"\n' - assert ParsedSetup.from_path(str(package_dir)).version == "1.0.0b12345" - - -def test_set_version_preserves_dynamic_pyproject_version_configuration(tmp_path): - shutil.copytree(pyproject_folder, tmp_path / "package") - pyproject = tmp_path / "package/pyproject.toml" - original = pyproject.read_bytes() - set_version_py(str(pyproject), "1.0.0b12345") - assert pyproject.read_bytes() == original - assert ParsedSetup.from_path(str(pyproject.parent)).version == "1.0.0b12345" diff --git a/sdk/template/azure-template-three/.gitignore b/sdk/template/azure-template-three/.gitignore deleted file mode 100644 index 7bccacabaf39..000000000000 --- a/sdk/template/azure-template-three/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target/ -/azure/template/three/*.so -/azure/template/three/*.pyd diff --git a/sdk/template/azure-template-three/CHANGELOG.md b/sdk/template/azure-template-three/CHANGELOG.md deleted file mode 100644 index 493ceb49f54c..000000000000 --- a/sdk/template/azure-template-three/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Release History - -## 1.0.0b1 (Unreleased) - -### Features Added - -- Added a minimal Rust extension to exercise Maturin wheel builds, Microsoft - Rust toolchain installation, Cargo feed authentication, and Linux container - setup in opt-in template CI runs. diff --git a/sdk/template/azure-template-three/Cargo.lock b/sdk/template/azure-template-three/Cargo.lock deleted file mode 100644 index 848cc7083d8e..000000000000 --- a/sdk/template/azure-template-three/Cargo.lock +++ /dev/null @@ -1,212 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "azure_template_three" -version = "1.0.0" -dependencies = [ - "pyo3", -] - -[[package]] -name = "cc" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" - -[[package]] -name = "find-msvc-tools" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "indoc" -version = "2.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" -dependencies = [ - "rustversion", -] - -[[package]] -name = "libc" -version = "0.2.190" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pyo3" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" -dependencies = [ - "once_cell", - "python3-dll-a", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn", -] - -[[package]] -name = "python3-dll-a" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d80ba7540edb18890d444c5aa8e1f1f99b1bdf26fb26ae383135325f4a36042b" -dependencies = [ - "cc", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "unicode-ident" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/sdk/template/azure-template-three/Cargo.toml b/sdk/template/azure-template-three/Cargo.toml deleted file mode 100644 index 7ad9e2869c82..000000000000 --- a/sdk/template/azure-template-three/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -[package] -name = "azure_template_three" -version = "1.0.0" -edition = "2021" -rust-version = "1.75" -license = "MIT" -publish = false - -[lib] -name = "_rust" -crate-type = ["cdylib"] - -[dependencies] -pyo3 = { version = "0.22", features = ["extension-module", "abi3-py310", "generate-import-lib"] } diff --git a/sdk/template/azure-template-three/LICENSE b/sdk/template/azure-template-three/LICENSE deleted file mode 100644 index b2f52a2bad4e..000000000000 --- a/sdk/template/azure-template-three/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Copyright (c) Microsoft Corporation. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/sdk/template/azure-template-three/README.md b/sdk/template/azure-template-three/README.md deleted file mode 100644 index b6ad6244f5e1..000000000000 --- a/sdk/template/azure-template-three/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Azure Rust Template Package client library for Python - -**This package is an engineering-system test fixture, not a service SDK.** - -`azure-template-three` complements the pure-Python `azure-template` and the -setuptools/C-extension `azure-template-two`. It builds one PyO3 function with -Maturin, without setuptools `ext_modules`, to exercise the Rust native-wheel -path. - -```python -from azure.template.three import _rust - -assert _rust.is_true() is True -``` - -## Run template CI - -Queue the template CI pipeline for the desired branch. Every run builds all -three fixtures and installs the Microsoft Rust toolchain for -`azure-template-three`. The Rust fixture follows the normal signing and release -workflow. `ReleaseToDevOpsOnly` can direct the release to the existing staging -feed instead of PyPI. - -Template CI validates this fixture. Generic PR validation excludes it because -that pipeline does not enable its Rust toolchain setup. Shared PR -discovery and dependency validation otherwise remain unchanged. -The separate template live-test pipeline continues to target the Python and C -fixtures because its source-installing jobs do not provision Microsoft Rust. - -The feed and tool versions use: - -- Toolchain: `ms-prod-1.97`, selected by `rust-toolchain.toml`. -- Toolchain feed: `https://pkgs.dev.azure.com/azure-sdk/_packaging/azure-sdk-for-rust-org-test/nuget/v3/index.json`. -- Build backend: Maturin `1.15.0`. -- Binding: PyO3 `0.22`, with the Python 3.10 stable ABI. - -The wheel matrix covers Linux x86_64/aarch64, macOS arm64, and Windows -AMD64/ARM64. Linux containers bootstrap their own Microsoft Rust toolchain -and configure the authenticated Cargo mirror. Windows ARM64 cross-compilation -uses the ARM64 Python import libraries provisioned by the shared build job. -Installed-wheel tests call the Rust function and verify that the module is -native. Windows ARM64 import testing still requires an ARM64 runner; producing -a cross-compiled wheel on an x64 agent does not prove that it loads. - -Toolchain/feed credentials are supplied by the pipeline, never checked in. -Use a trusted Azure DevOps run with access to the prototype feed. Local checks -cannot establish that the internal toolchain or authenticated downloads work. -PR runs do not sign binaries. Internal manual runs can exercise the existing -signing and release stages. - -## Build locally - -With Microsoft Rust installed and authenticated: - -```bash -python -m build sdk/template/azure-template-three -python -m pip install sdk/template/azure-template-three/dist/*.whl -python -m pytest sdk/template/azure-template-three/tests -``` - -For a local compile/import check using public Rust instead of Microsoft Rust: - -```bash -RUSTUP_TOOLCHAIN=stable python -m build sdk/template/azure-template-three -``` - -This does not exercise the CI toolchain feed, manylinux bootstrap, or credentials. - -## Validation boundaries - -Ordinary source-installing analysis, documentation, and test jobs remain -disabled, as in the C fixture. - -Windows ARM64 wheels are cross-built and signed, but their runtime import still -needs a Windows ARM64 runner. Linux aarch64 build tests use the existing QEMU -setup. - -# Contributing - -If you encounter any bugs or have suggestions, please file an issue in the [Issues]() section of the project. diff --git a/sdk/template/azure-template-three/azure/template/three/__init__.py b/sdk/template/azure-template-three/azure/template/three/__init__.py deleted file mode 100644 index c960807c06fd..000000000000 --- a/sdk/template/azure-template-three/azure/template/three/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - -from ._version import VERSION - -__version__ = VERSION diff --git a/sdk/template/azure-template-three/azure/template/three/_rust.pyi b/sdk/template/azure-template-three/azure/template/three/_rust.pyi deleted file mode 100644 index 672767dae5cb..000000000000 --- a/sdk/template/azure-template-three/azure/template/three/_rust.pyi +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -def is_true() -> bool: ... diff --git a/sdk/template/azure-template-three/azure/template/three/_version.py b/sdk/template/azure-template-three/azure/template/three/_version.py deleted file mode 100644 index e5b9bca3a368..000000000000 --- a/sdk/template/azure-template-three/azure/template/three/_version.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - -VERSION = "1.0.0b1" diff --git a/sdk/template/azure-template-three/dev_requirements.txt b/sdk/template/azure-template-three/dev_requirements.txt deleted file mode 100644 index 190d2e5520b2..000000000000 --- a/sdk/template/azure-template-three/dev_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ --e ../../../eng/tools/azure-sdk-tools -pytest diff --git a/sdk/template/azure-template-three/pyproject.toml b/sdk/template/azure-template-three/pyproject.toml deleted file mode 100644 index e7fa483657a7..000000000000 --- a/sdk/template/azure-template-three/pyproject.toml +++ /dev/null @@ -1,102 +0,0 @@ -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------- - -[build-system] -requires = ["maturin==1.15.0"] -build-backend = "maturin" - -[project] -name = "azure-template-three" -version = "1.0.0b1" -description = "Microsoft Azure Template Package (Rust) Client Library for Python" -readme = "README.md" -requires-python = ">=3.10" -license = { text = "MIT" } -authors = [ - { name = "Microsoft Corporation", email = "azuresdkengsysadmins@microsoft.com" }, -] -classifiers = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", -] -dependencies = [] - -[project.urls] -"Bug Reports" = "https://github.com/Azure/azure-sdk-for-python/issues" -repository = "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/template/azure-template-three" - -[tool.maturin] -locked = true -python-source = "." -module-name = "azure.template.three._rust" -include = [ - { path = "rust-toolchain.toml", format = "sdist" }, - { path = "scripts/*.sh", format = "sdist" }, - { path = "tests/*.py", format = "sdist" }, - { path = "LICENSE", format = "sdist" }, - { path = "CHANGELOG.md", format = "sdist" }, -] -exclude = [ - "**/__pycache__/**", - "**/*.py[co]", - "azure/template/three/_rust.*.so", - "azure/template/three/_rust.*.pyd", -] - -# The fixture validates native wheels. Source-installing jobs do not yet provision Microsoft Rust. -[tool.azure-sdk-build] -mypy = false -pyright = false -type_check_samples = false -verifytypes = false -pylint = false -sphinx = false -verifysdist = false -verifywhl = false -sdist = false -breaking = false -suppressed_skip_warnings = ["*"] - -[tool.azure-sdk-conda] -in_bundle = false - -[tool.cibuildwheel] -build = ["cp310-*"] -skip = ["*-musllinux*"] -test-requires = "pytest" -test-command = "pytest {project}/tests" - -[tool.cibuildwheel.macos] -archs = ["arm64"] -environment = { PATH = "$HOME/.msrustup/multiplexers/bin:$PATH" } - -[tool.cibuildwheel.windows] -archs = ["AMD64", "ARM64"] - -[tool.cibuildwheel.linux] -archs = ["x86_64", "aarch64"] -manylinux-x86_64-image = "manylinux_2_28" -manylinux-aarch64-image = "manylinux_2_28" -environment-pass = [ - "PIP_INDEX_URL", - "MSRUSTUP_ACCESS_TOKEN", - "MSRUSTUP_FEED_URL", - "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX", - "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN", - "CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_CREDENTIAL_PROVIDER", -] -environment = { MSRUSTUP_HOME = "/msrustup", CARGO_HOME = "/cargo", PATH = "/msrustup/multiplexers/bin:$PATH", CARGO_NET_GIT_FETCH_WITH_CLI = "true" } -before-all = "dnf install -y jq && mkdir -p $MSRUSTUP_HOME/bootstrap && cd $MSRUSTUP_HOME/bootstrap && sh /project/scripts/install-msrustup.sh && cd /project && $MSRUSTUP_HOME/bootstrap/msrustup toolchain install && sh /project/scripts/configure-cargo-feed.sh" - -[[tool.cibuildwheel.overrides]] -select = "*-win_arm64" -environment = { CARGO_BUILD_TARGET = "aarch64-pc-windows-msvc", PYO3_CROSS_LIB_DIR = "$PYTHON_ARM64_LIB_DIR" } diff --git a/sdk/template/azure-template-three/rust-toolchain.toml b/sdk/template/azure-template-three/rust-toolchain.toml deleted file mode 100644 index 28651ef3888b..000000000000 --- a/sdk/template/azure-template-three/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "ms-prod-1.97" -profile = "minimal" diff --git a/sdk/template/azure-template-three/scripts/configure-cargo-feed.sh b/sdk/template/azure-template-three/scripts/configure-cargo-feed.sh deleted file mode 100644 index 9e0386e6f670..000000000000 --- a/sdk/template/azure-template-three/scripts/configure-cargo-feed.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/sh -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -set -eu - -index="${CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_INDEX:-}" -if [ -z "$index" ]; then - echo "No Cargo registry index supplied; using crates.io." - exit 0 -fi -if [ -z "${CARGO_REGISTRIES_AZURE_SDK_FOR_RUST_PUBLIC_TOKEN:-}" ]; then - echo "The configured Cargo feed requires an authentication token." >&2 - exit 1 -fi - -cargo_home="${CARGO_HOME:-$HOME/.cargo}" -mkdir -p "$cargo_home" -config="$cargo_home/config.toml" -if [ -e "$config" ]; then - echo "Refusing to overwrite the Cargo configuration: $config" >&2 - exit 1 -fi - -cat > "$config" </dev/null 2>&1 || { - echo "install-msrustup requires $tool." >&2 - exit 1 - } -done - -: "${MSRUSTUP_FEED_URL:?The pipeline must supply the Microsoft Rust toolchain feed.}" -if [ -z "${MSRUSTUP_ACCESS_TOKEN:-}" ] && [ -z "${MSRUSTUP_PAT:-}" ]; then - echo "MSRUSTUP_ACCESS_TOKEN or MSRUSTUP_PAT must be set." >&2 - exit 1 -fi - -do_curl() { - if [ -n "${MSRUSTUP_ACCESS_TOKEN:-}" ]; then - curl -fsSL --retry 5 -H "Authorization: Bearer $MSRUSTUP_ACCESS_TOKEN" "$@" - else - curl -fsSL --retry 5 -u ":$MSRUSTUP_PAT" "$@" - fi -} - -case "$(uname -s)" in - Linux) target_rest="-unknown-linux-gnu" ;; - *) echo "This bootstrap is only used by Linux wheel containers." >&2; exit 1 ;; -esac -case "$(uname -m)" in - x86_64) target_arch="x86_64" ;; - aarch64 | arm64) target_arch="aarch64" ;; - *) echo "Unsupported Microsoft Rust bootstrap architecture." >&2; exit 1 ;; -esac - -package="rust.msrustup-${target_arch}${target_rest}" -response=$(do_curl "$MSRUSTUP_FEED_URL") -base=$(printf '%s' "$response" | jq -er '.resources[] | select(."@type" == "PackageBaseAddress/3.0.0") | .["@id"]') -response=$(do_curl "${base}${package}/index.json") -version=$(printf '%s' "$response" | jq -er '.versions[0]') -latest="${base}${package}/${version}/${package}.${version}.nupkg" - -trap 'rm -f msrustup.zip' EXIT -echo "Installing msrustup ${version} for ${target_arch}${target_rest}." -do_curl "$latest" -o msrustup.zip -unzip -jqo msrustup.zip tools/msrustup -chmod +x msrustup diff --git a/sdk/template/azure-template-three/sdk_packaging.toml b/sdk/template/azure-template-three/sdk_packaging.toml deleted file mode 100644 index 901bc8ccbfa6..000000000000 --- a/sdk/template/azure-template-three/sdk_packaging.toml +++ /dev/null @@ -1,2 +0,0 @@ -[packaging] -auto_update = false diff --git a/sdk/template/azure-template-three/src/lib.rs b/sdk/template/azure-template-three/src/lib.rs deleted file mode 100644 index 971eb3c5db62..000000000000 --- a/sdk/template/azure-template-three/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -use pyo3::prelude::*; - -#[pyfunction] -fn is_true() -> bool { - true -} - -#[pymodule] -fn _rust(module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add_function(wrap_pyfunction!(is_true, module)?)?; - Ok(()) -} diff --git a/sdk/template/azure-template-three/tests/test_rust.py b/sdk/template/azure-template-three/tests/test_rust.py deleted file mode 100644 index 968e472f3307..000000000000 --- a/sdk/template/azure-template-three/tests/test_rust.py +++ /dev/null @@ -1,29 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- - -from importlib.machinery import EXTENSION_SUFFIXES -from importlib.metadata import version - -import pytest - -from azure.template.three import __version__, _rust - - -def test_is_true_returns_true(): - assert _rust.is_true() is True - - -def test_is_true_takes_no_arguments(): - with pytest.raises(TypeError): - _rust.is_true(1) - - -def test_extension_is_native(): - assert any(_rust.__file__.endswith(suffix) for suffix in EXTENSION_SUFFIXES) - - -def test_runtime_version_matches_distribution(): - assert __version__ == version("azure-template-three") diff --git a/sdk/template/ci.yml b/sdk/template/ci.yml index c96ad001b485..404932379483 100644 --- a/sdk/template/ci.yml +++ b/sdk/template/ci.yml @@ -58,10 +58,6 @@ extends: oneESTemplateTag: ${{ parameters.oneESTemplateTag }} ServiceDirectory: template TestProxy: true - InstallMsRustToolchain: true - MsRustWorkingDirectory: $(Build.SourcesDirectory)/sdk/template/azure-template-three - MsRustToolchainFeed: https://pkgs.dev.azure.com/azure-sdk/_packaging/azure-sdk-for-rust-org-test/nuget/v3/index.json - MsRustAdditionalTargets: aarch64-pc-windows-msvc Artifacts: - name: azure-template safeName: azuretemplate @@ -77,8 +73,3 @@ extends: # that ships one wheel per platform. skipPublishDocGithubIo: true skipPublishDocMs: true - - name: azure-template-three - safeName: azuretemplatethree - signBinaries: true - skipPublishDocGithubIo: true - skipPublishDocMs: true diff --git a/sdk/template/tests.yml b/sdk/template/tests.yml index 808a3b59fe1c..b9da355db231 100644 --- a/sdk/template/tests.yml +++ b/sdk/template/tests.yml @@ -4,7 +4,6 @@ extends: template: /eng/pipelines/templates/stages/archetype-sdk-tests.yml parameters: ServiceDirectory: template - BuildTargetingString: azure-template|azure-template-two MatrixReplace: - TestSamples=.*/true EnvVars: From 328c7d90a8764499a17ac4cdee37602daa1bf2e5 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:59:34 -0700 Subject: [PATCH 9/9] only install toolchain when needed, handle is compiled independently --- doc/dev/packaging.md | 7 +++- .../steps/build-package-artifacts.yml | 2 +- eng/tools/azure-sdk-tools/ci_tools/build.py | 19 +++++---- .../tests/test_build_interactions.py | 41 +++++++++++++------ .../tests/test_native_build_pipeline.py | 1 + 5 files changed, 46 insertions(+), 24 deletions(-) diff --git a/doc/dev/packaging.md b/doc/dev/packaging.md index 35e43999e680..125f34b79c13 100644 --- a/doc/dev/packaging.md +++ b/doc/dev/packaging.md @@ -152,6 +152,10 @@ or a `[tool.cibuildwheel]` table in `pyproject.toml`. The latter supports non-setuptools backends, including Maturin/PyO3. The package owns its wheel matrix, backend requirements, and native build hooks. +For these packages, `sdk_build_package --package_type sdist` builds only the +source distribution with build isolation, which installs the backend declared +in `[build-system].requires` without invoking `cibuildwheel`. + Set `signBinaries: true` on the package's artifact entry in the service `ci.yml` to enable Windows and macOS wheel builds and the existing binary-signing release path. The platform resolver uses this artifact property, not package names. @@ -179,7 +183,8 @@ provides the internal Rust toolchain. No prototype test feed is selected by default. The working directory must contain `rust-toolchain.toml`. The installer refuses to overwrite an existing Cargo configuration in its `CARGO_HOME`; provide a clean build environment. -Rust installation is disabled by default and applies only to wheel-build jobs. +Rust installation is disabled by default and applies only to wheel-build jobs +that target an artifact with `signBinaries: true`, including on Linux. Opted-in jobs have a 240-minute timeout and a 210-minute package-build timeout; other jobs keep their existing limits. diff --git a/eng/pipelines/templates/steps/build-package-artifacts.yml b/eng/pipelines/templates/steps/build-package-artifacts.yml index cc0794dce664..f764bc0cc17a 100644 --- a/eng/pipelines/templates/steps/build-package-artifacts.yml +++ b/eng/pipelines/templates/steps/build-package-artifacts.yml @@ -137,7 +137,7 @@ steps: WorkingDirectory: ${{ parameters.MsRustWorkingDirectory }} ${{ if eq(parameters.ArtifactSuffix, 'windows') }}: AdditionalTargets: ${{ parameters.MsRustAdditionalTargets }} - Condition: and(succeeded(), or(eq(variables['ENABLE_EXTENSION_BUILD'], 'true'), eq('${{ parameters.ArtifactSuffix }}', 'linux'))) + Condition: and(succeeded(), eq(variables['ENABLE_EXTENSION_BUILD'], 'true')) # Windows: cibuildwheel fetches CPython from api.nuget.org, which is blocked. # Pre-provision the interpreters from the Azure DevOps NuGet feed into the diff --git a/eng/tools/azure-sdk-tools/ci_tools/build.py b/eng/tools/azure-sdk-tools/ci_tools/build.py index e90fdee4d697..5d35e53a7263 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/build.py +++ b/eng/tools/azure-sdk-tools/ci_tools/build.py @@ -243,16 +243,17 @@ def create_package( if setup_parsed.is_pyproject: # when building with pyproject, check if package has compiled extensions - if enable_wheel and is_compiled: - # Use cibuildwheel for compiled extensions (respects [tool.cibuildwheel] config) - run_logged( - [sys.executable, "-m", "cibuildwheel", "--output-dir", dist], - cwd=setup_parsed.folder, - check=True, - should_stream_to_console=should_log_build_output, - ) + if is_compiled: + if enable_wheel: + # Use cibuildwheel for compiled extensions (respects [tool.cibuildwheel] config) + run_logged( + [sys.executable, "-m", "cibuildwheel", "--output-dir", dist], + cwd=setup_parsed.folder, + check=True, + should_stream_to_console=should_log_build_output, + ) if enable_sdist: - # Build sdist separately with python -m build + # Build isolation installs package-specific backends such as Maturin. run_logged( [sys.executable, "-m", "build", "-s", "-o", dist], cwd=setup_parsed.folder, diff --git a/eng/tools/azure-sdk-tools/tests/test_build_interactions.py b/eng/tools/azure-sdk-tools/tests/test_build_interactions.py index d21a6c0f1fec..950e9fae9141 100644 --- a/eng/tools/azure-sdk-tools/tests/test_build_interactions.py +++ b/eng/tools/azure-sdk-tools/tests/test_build_interactions.py @@ -62,8 +62,10 @@ def build_commands(monkeypatch): ("setup_py_project_def", '\n[tool.cibuildwheel]\nbuild = "cp310-*"\n', True), ], ) -@pytest.mark.parametrize("enable_sdist", [False, True]) -def test_create_package_build_routing(tmp_path, build_commands, scenario, cibuildwheel_config, compiled, enable_sdist): +@pytest.mark.parametrize("enable_wheel,enable_sdist", [(True, False), (True, True), (False, True)]) +def test_create_package_build_routing( + tmp_path, build_commands, scenario, cibuildwheel_config, compiled, enable_wheel, enable_sdist +): package = tmp_path / "package" shutil.copytree(os.path.join(integration_folder, "scenarios", scenario), package) if cibuildwheel_config: @@ -73,40 +75,53 @@ def test_create_package_build_routing(tmp_path, build_commands, scenario, cibuil pyproject.write_text(contents + cibuildwheel_config) destination = tmp_path / "dist" - create_package(str(package), str(destination), enable_sdist=enable_sdist) + create_package(str(package), str(destination), enable_wheel=enable_wheel, enable_sdist=enable_sdist) commands = [command for command, _ in build_commands] cibuildwheel_command = [sys.executable, "-m", "cibuildwheel", "--output-dir", str(destination)] - assert (cibuildwheel_command in commands) is compiled + assert (cibuildwheel_command in commands) is (compiled and enable_wheel) assert all(options["cwd"] == str(package) for _, options in build_commands) assert all(options["check"] for command, options in build_commands if "pip" not in command) if compiled and scenario.startswith("pyproject"): - assert commands == [cibuildwheel_command] + ( + assert commands == ([cibuildwheel_command] if enable_wheel else []) + ( [[sys.executable, "-m", "build", "-s", "-o", str(destination)]] if enable_sdist else [] ) elif not compiled and scenario.startswith("pyproject"): + build_flags = "-nsw" if enable_wheel and enable_sdist else "-nw" if enable_wheel else "-ns" assert commands[-1] == [ sys.executable, "-m", "build", - "-nsw" if enable_sdist else "-nw", + build_flags, "-o", str(destination), ] - elif not compiled: - assert commands[0] == [sys.executable, "setup.py", "bdist_wheel", "-d", str(destination)] + else: + expected_commands = [] + if enable_wheel: + expected_commands.append( + cibuildwheel_command + if compiled + else [sys.executable, "setup.py", "bdist_wheel", "-d", str(destination)] + ) + if enable_sdist: + expected_commands.append([sys.executable, "setup.py", "sdist", "-d", str(destination)]) + assert commands == expected_commands def test_sdist_only_does_not_invoke_cibuildwheel(tmp_path, build_commands): package = tmp_path / "package" shutil.copytree(os.path.join(integration_folder, "scenarios", "pyproject_project_def"), package) - with (package / "pyproject.toml").open("a") as stream: - stream.write('\n[tool.cibuildwheel]\nbuild = "cp310-*"\n') + pyproject = package / "pyproject.toml" + contents = pyproject.read_text().replace("setuptools.build_meta", "maturin") + contents = contents.replace('requires = ["setuptools>=61.0.0", "wheel"]', 'requires = ["maturin"]') + pyproject.write_text(contents + '\n[tool.cibuildwheel]\nbuild = "cp310-*"\n') - create_package(str(package), str(tmp_path / "dist"), enable_wheel=False) + destination = tmp_path / "dist" + create_package(str(package), str(destination), enable_wheel=False) - assert all("cibuildwheel" not in command for command, _ in build_commands) - assert "-ns" in build_commands[-1][0] + assert [command for command, _ in build_commands] == [[sys.executable, "-m", "build", "-s", "-o", str(destination)]] + assert build_commands[0][1]["check"] is True def test_cibuildwheel_failure_propagates(tmp_path, monkeypatch): diff --git a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py index 8f28434007d7..dcdbe788b7dd 100644 --- a/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py +++ b/eng/tools/azure-sdk-tools/tests/test_native_build_pipeline.py @@ -74,6 +74,7 @@ def test_rust_credentials_and_target_setup_are_opt_in(): build = load_template("steps/build-package-artifacts.yml") install = next(step[RUST_GATE][0] for step in build["steps"] if RUST_GATE in step) assert install["template"].endswith("install-msrust-toolchain.yml") + assert install["parameters"]["Condition"] == "and(succeeded(), eq(variables['ENABLE_EXTENSION_BUILD'], 'true'))" assert install["parameters"]["ToolchainFeed"] == "${{ parameters.MsRustToolchainFeed }}" assert install["parameters"]["${{ if eq(parameters.ArtifactSuffix, 'windows') }}"] == { "AdditionalTargets": "${{ parameters.MsRustAdditionalTargets }}"