Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion doc/dev/packaging.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -145,6 +145,64 @@ 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.

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.

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/<approved-rust-feed>/nuget/v3/index.json
MsRustAdditionalTargets: aarch64-pc-windows-msvc
Artifacts:
- name: azure-myservice
safeName: azuremyservice
signBinaries: true
```

Replace `<approved-rust-feed>` 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
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.

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
Expand Down
40 changes: 37 additions & 3 deletions eng/pipelines/templates/jobs/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,26 @@ 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
# Native multi-target builds need more time, especially ARM64 under Linux emulation.
${{ if parameters.InstallMsRustToolchain }}:
timeoutInMinutes: 240
${{ else }}:
timeoutInMinutes: 90

pool:
name: $(LINUXPOOL)
Expand All @@ -88,9 +104,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)
Expand All @@ -107,9 +130,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)
Expand All @@ -133,6 +163,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
Expand Down
20 changes: 20 additions & 0 deletions eng/pipelines/templates/stages/archetype-sdk-client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ 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: ''

extends:
template: /eng/pipelines/templates/stages/1es-redirect.yml
Expand Down Expand Up @@ -132,6 +148,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 }}
Expand Down
60 changes: 57 additions & 3 deletions eng/pipelines/templates/steps/build-package-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}:
Expand Down Expand Up @@ -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(), 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
# cibuildwheel cache so it skips the blocked download.
Expand All @@ -144,26 +165,59 @@ steps:
exit 1
}
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"
}
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.
# This replaces, rather than merges with, the package's environment-pass list.
# Include every variable required by its Linux build hooks.
- 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
# 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)
Comment thread
JennyPng marked this conversation as resolved.
Resolved
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)

- pwsh: |
which python
Expand Down
98 changes: 98 additions & 0 deletions eng/pipelines/templates/steps/install-msrust-toolchain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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/'
- 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 }}

# Installs the host toolchain; Linux build containers bootstrap their own.
- 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 }}

# Configure Cargo after RustInstaller defines CARGO_HOME.
- pwsh: |
$ErrorActionPreference = 'Stop'
Write-Host "##vso[task.setvariable variable=MSRUSTUP_FEED_URL]$env:RUST_TOOLCHAIN_FEED"
$cargoHome = $env:CARGO_HOME
if (-not $cargoHome) {
$cargoHome = [System.IO.Path]::Combine($HOME, '.cargo')
}
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"
}

# 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" }

[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 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 }}
env:
RUST_TOOLCHAIN_FEED: ${{ parameters.ToolchainFeed }}
CARGO_FEED_URL: ${{ parameters.CratesIoFeed }}

- task: NuGetAuthenticate@1
displayName: 'Acquire an Azure Artifacts token'
condition: ${{ parameters.Condition }}

# CargoAuthenticate reads SYSTEM_ACCESSTOKEN; use the token acquired by NuGetAuthenticate.
- task: CargoAuthenticate@0
displayName: 'Authenticate cargo to the Azure Artifacts feed'
condition: ${{ parameters.Condition }}
inputs:
configFile: $(CargoConfigPath)
env:
SYSTEM_ACCESSTOKEN: $(VSS_NUGET_ACCESSTOKEN)
5 changes: 5 additions & 0 deletions eng/pipelines/templates/steps/resolve-build-platforms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,8 +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
Loading
Loading