diff --git a/.azure/pipelines/perf-build.yml b/.azure/pipelines/perf-build.yml new file mode 100644 index 000000000000..eefd5c44bff7 --- /dev/null +++ b/.azure/pipelines/perf-build.yml @@ -0,0 +1,329 @@ +# +# perf-build.yml +# +# Azure DevOps pipeline that builds every commit on `main` (and any other +# branches listed in the trigger) and uploads per-RID ASP.NET Core runtime +# packs to the Build Cache Service (BCS) so that dotnet/crank can resolve +# Microsoft.AspNetCore.App runtime binaries by commit SHA for +# performance-regression bisection. +# +# Modeled on dotnet/runtime's `eng/pipelines/performance/perf-build.yml`, +# including its three-stage shape: +# +# * RegisterBuild — runs in parallel with build; writes a tiny per-config +# buildInfo.json marker to BCS at +# builds/aspnetcore/buildArtifacts/{sha}/{configKey}/buildInfo.json. +# * build — produces per-RID Microsoft.AspNetCore.App.Runtime +# pipeline artifacts. +# * UploadArtifacts — depends on both; downloads each pipeline artifact +# and uploads it to +# builds/aspnetcore/buildArtifacts/{sha}/{configKey}/{archive}. +# +# The RegisterBuild and UploadArtifacts stages delegate to the shared +# dotnet/performance templates (`register-build-jobs.yml@performance` and +# `upload-build-artifacts-jobs.yml@performance`) — the same templates the +# runtime perf-build calls. Those templates were generalized in +# dotnet/performance to take an explicit `repoName` so non-runtime tenants +# (us) land under `builds/aspnetcore/...` instead of `builds/runtime/...`. +# +# This pipeline does NOT extend `1ES.Official.PipelineTemplate.yml`. +# Per-commit perf-build artifacts go to a private BCS cache (not redistributed +# / signed), so SDL/PoliCheck/TSA scans add no security value here. Matches +# runtime's perf-build which also runs as a plain (non-1ES) pipeline against +# the same `.NET Performance` Azure service connection. +# +# Atomicity contract: the MissingBuildsTrigger Azure Function only indexes +# Azure DevOps builds with overall Status == Succeeded. All stages run with +# the default `continueOnError: false`; any failure sinks the overall build +# to Failed and the Function skips indexing for that SHA. Operator retries +# the whole build green. The atomic unit for indexing is the AzDO build, +# not the per-config job. +# +# Output layout in BCS: +# +# builds/aspnetcore/buildArtifacts/{sha}/{configKey}/buildInfo.json +# builds/aspnetcore/buildArtifacts/{sha}/{configKey}/{archiveFile} +# +# Archive contents (lowercase directory names are a load-bearing contract +# — the future aspnetcore-overlay crank PR will FindDirectory on exactly +# `microsoft.aspnetcore.app.runtime.{rid}`): +# +# microsoft.aspnetcore.app.runtime.{rid}/ +# Release/ +# runtimes/{rid}/ +# lib/net{X}.0/Microsoft.AspNetCore.*.dll +# native/aspnetcorev2*.dll (Windows only — ANCM/IIS bits) +# + +# Build every individual commit (no batching). The MissingBuildsTrigger +# Azure Function then indexes succeeded builds into +# builds/aspnetcore/latest/{branch}/latestBuilds.json on its 8h tick. +trigger: + batch: false + branches: + include: + - main + +# No PR validation in v1. This pipeline only produces meaningful output +# under internal+IndividualCI (real commits to mirrored branches). PR +# triggering is left for a follow-up once the security model around the +# `.NET Performance` service connection is settled. +pr: none + +variables: +- name: _TeamName + value: AspNetCore + +# Build-script arguments mirrored from ci.yml. We deliberately omit +# anything related to signing, installers, helix, or maestro publishing +# because none of those contribute to the per-RID runtime pack we ship to +# BCS. +- name: _BuildArgs + value: /p:TeamName=$(_TeamName) + /p:OfficialBuildId=$(Build.BuildNumber) + /p:SkipTestBuild=true +- name: _InternalRuntimeDownloadArgs + value: -RuntimeSourceFeed https://ci.dot.net/internal + -RuntimeSourceFeedKey $(dotnetbuilds-internal-container-read-token-base64) + /p:DotNetAssetRootAccessTokenSuffix='$(dotnetbuilds-internal-container-read-token-base64)' + +- template: /eng/common/templates-official/variables/pool-providers.yml@self + +resources: + repositories: + # dotnet/performance hosts the shared BCS upload templates that runtime's + # perf-build.yml also consumes. We reference its dnceng AzDO mirror + # (internal/dotnet-performance) so the templates resolve at pipeline + # compile time. + - repository: performance + type: git + name: internal/dotnet-performance + +stages: + +# ============================================================================ +# RegisterBuild stage +# +# Writes a per-configKey buildInfo.json marker to BCS at: +# $web/builds/aspnetcore/buildArtifacts/{sha}/{configKey}/buildInfo.json +# +# Runs in parallel with the build stage (dependsOn: []) — the marker only +# encodes $(Build.BuildId), it doesn't need any build output. Mirrors +# dotnet/runtime's perf-build.yml structure so dashboards / cleanup tooling +# that reads buildInfo.json work uniformly across both repos. +# +# Same gate as UploadArtifacts (internal + IndividualCI/Manual). +# ============================================================================ +- ${{ if and(ne(variables['System.TeamProject'], 'public'), or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'Manual'))) }}: + - stage: RegisterBuild + displayName: 'Register Build' + dependsOn: [] + jobs: + - template: /eng/pipelines/register-build-jobs.yml@performance + parameters: + performanceRepoAlias: performance + repoName: aspnetcore + buildType: + - aspnetcore_x64_linux + - aspnetcore_arm64_linux + - aspnetcore_x64_windows + - aspnetcore_arm64_windows + - aspnetcore_x86_windows + +- stage: Build + displayName: Build + jobs: + + # ===================================================================== + # Windows multi-arch build job + # + # Mirrors ci.yml's Windows_build pattern: build.cmd is invoked three + # times in a single agent — x64 first (with native), then x86 and + # arm64 with -noBuildNative because they cross-pack against the x64 + # native bits. Each invocation emits its own per-RID + # Microsoft.AspNetCore.App.Runtime.win-{arch}.{ver}.nupkg into the + # shared artifacts/packages/Release/Shipping/ directory. + # + # The afterBuild step delegates to the shared + # .azure/pipelines/tools/pack-bcs-archives.ps1 script (the single, + # canonical find-and-zip recipe used by every build job here), which + # unpacks each nupkg into the lowercase directory layout the future + # aspnetcore-overlay crank PR expects. It is invoked via `pwsh` (not + # Windows PowerShell 5.1) because 5.1's Compress-Archive writes + # backslash path separators, which would corrupt the archive for crank. + # ===================================================================== + - template: .azure/pipelines/jobs/default-build.yml@self + parameters: + jobName: Windows_build + jobDisplayName: 'Build: Windows x64/x86/arm64 (perf-build)' + agentOs: Windows + installNodeJs: false + + steps: + - script: ./eng/build.cmd + -ci + -prepareMachine + -nativeToolsOnMachine + -arch x64 + -pack + -all + -noBuildJava + /p:OnlyPackPlatformSpecificPackages=true + $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) + /bl:artifacts/log/Release/Build.x64.binlog + env: + MSBUILDUSESERVER: "1" + displayName: Build x64 + + - script: ./eng/build.cmd + -ci + -prepareMachine + -arch x86 + -pack + -all + -noBuildJava + -noBuildNative + /p:OnlyPackPlatformSpecificPackages=true + $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) + -ExcludeCIBinaryLog + env: + MSBUILDUSESERVER: "1" + displayName: Build x86 + + - script: ./eng/build.cmd + -ci + -prepareMachine + -arch arm64 + -pack + -noBuildJava + -noBuildNative + /p:OnlyPackPlatformSpecificPackages=true + $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) + /bl:artifacts/log/Release/Build.arm64.binlog + env: + MSBUILDUSESERVER: "1" + displayName: Build ARM64 + + afterBuild: + - pwsh: | + & "$(Build.SourcesDirectory)/.azure/pipelines/tools/pack-bcs-archives.ps1" -Rids win-x64,win-x86,win-arm64 -Format zip + displayName: 'Pack BCS archives (win-x64 / win-x86 / win-arm64)' + + artifacts: + - name: BuildArtifacts_windows_x64_Release_aspnetcore + path: $(Build.ArtifactStagingDirectory)/bcs/BuildArtifacts_windows_x64_Release_aspnetcore + - name: BuildArtifacts_windows_x86_Release_aspnetcore + path: $(Build.ArtifactStagingDirectory)/bcs/BuildArtifacts_windows_x86_Release_aspnetcore + - name: BuildArtifacts_windows_arm64_Release_aspnetcore + path: $(Build.ArtifactStagingDirectory)/bcs/BuildArtifacts_windows_arm64_Release_aspnetcore + - name: Windows_perf_build_Logs_Attempt_$(System.JobAttempt) + path: artifacts/log/ + publishOnError: true + includeForks: true + + # ===================================================================== + # Linux x64 build job + # ===================================================================== + - template: .azure/pipelines/jobs/default-build.yml@self + parameters: + jobName: Linux_x64_build + jobDisplayName: 'Build: Linux x64 (perf-build)' + agentOs: Linux + use1ESUbuntu: true + installNodeJs: false + buildArgs: + --arch x64 + --pack + --all + --no-build-java + -p:OnlyPackPlatformSpecificPackages=true + $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) + afterBuild: + - pwsh: | + & "$(Build.SourcesDirectory)/.azure/pipelines/tools/pack-bcs-archives.ps1" -Rids linux-x64 -Format targz + displayName: 'Pack BCS archive (linux-x64)' + + artifacts: + - name: BuildArtifacts_linux_x64_Release_aspnetcore + path: $(Build.ArtifactStagingDirectory)/bcs/BuildArtifacts_linux_x64_Release_aspnetcore + - name: Linux_x64_perf_build_Logs_Attempt_$(System.JobAttempt) + path: artifacts/log/ + publishOnError: true + includeForks: true + + # ===================================================================== + # Linux arm64 build job (cross-build from x64 host) + # ===================================================================== + - template: .azure/pipelines/jobs/default-build.yml@self + parameters: + jobName: Linux_arm64_build + jobDisplayName: 'Build: Linux ARM64 (perf-build)' + agentOs: Linux + use1ESUbuntu: true + installNodeJs: false + buildArgs: + --arch arm64 + --pack + --all + --no-build-java + -p:OnlyPackPlatformSpecificPackages=true + $(_BuildArgs) + $(_InternalRuntimeDownloadArgs) + afterBuild: + - pwsh: | + & "$(Build.SourcesDirectory)/.azure/pipelines/tools/pack-bcs-archives.ps1" -Rids linux-arm64 -Format targz + displayName: 'Pack BCS archive (linux-arm64)' + + artifacts: + - name: BuildArtifacts_linux_arm64_Release_aspnetcore + path: $(Build.ArtifactStagingDirectory)/bcs/BuildArtifacts_linux_arm64_Release_aspnetcore + - name: Linux_arm64_perf_build_Logs_Attempt_$(System.JobAttempt) + path: artifacts/log/ + publishOnError: true + includeForks: true + +# ========================================================================== +# ============================================================================ +# UploadArtifacts stage +# +# Delegates to dotnet/performance's `upload-build-artifacts-jobs.yml` +# dispatcher (the same template runtime's perf-build.yml calls), which +# fans out into per-configKey `templates/upload-build-artifacts-job.yml` +# invocations that each: +# +# 1. Download the matching pipeline artifact from the Build stage. +# 2. `az storage blob upload` it to: +# $web/builds/aspnetcore/buildArtifacts/{sha}/{configKey}/{file} +# +# Depends on both build and RegisterBuild so the upload only proceeds once +# both the archives and the buildInfo.json markers are in flight. `condition: +# succeeded()` means any build / RegisterBuild failure sinks the overall +# AzDO build to Failed, and the MissingBuildsTrigger Function skips +# indexing for this SHA (whole-pipeline-indexing contract). +# +# Gated to internal + (IndividualCI | Manual). IndividualCI covers the +# steady-state per-commit flow; Manual covers the runbook's "queue one +# seed run" step. +# ============================================================================ +- ${{ if and(ne(variables['System.TeamProject'], 'public'), or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'Manual'))) }}: + - stage: UploadArtifacts + displayName: 'Upload Artifacts to BCS' + dependsOn: + - Build + - RegisterBuild + condition: succeeded() + jobs: + - template: /eng/pipelines/upload-build-artifacts-jobs.yml@performance + parameters: + performanceRepoAlias: performance + repoName: aspnetcore + buildType: + - aspnetcore_x64_linux + - aspnetcore_arm64_linux + - aspnetcore_x64_windows + - aspnetcore_arm64_windows + - aspnetcore_x86_windows diff --git a/.azure/pipelines/tools/pack-bcs-archives.ps1 b/.azure/pipelines/tools/pack-bcs-archives.ps1 new file mode 100644 index 000000000000..6a943bf96fcd --- /dev/null +++ b/.azure/pipelines/tools/pack-bcs-archives.ps1 @@ -0,0 +1,187 @@ +<# +.SYNOPSIS + Packs per-RID Microsoft.AspNetCore.App runtime packs into Build Cache + Service (BCS) archives for the perf-build pipeline. + +.DESCRIPTION + This is the single, canonical "find the runtime-pack nupkg and zip it into + the BCS archive layout" recipe used by every job in perf-build.yml (Windows + x64/x86/arm64, Linux x64, Linux arm64). The build jobs invoke it from their + `afterBuild` hook with the RID(s) they produced and the archive format that + matches their platform convention (.zip on Windows, .tar.gz on Linux). + + For each RID it: + 1. Locates artifacts/packages/Release/Shipping/Microsoft.AspNetCore.App.Runtime.{rid}.*.nupkg + (excluding the *.symbols.nupkg). + 2. Extracts it into the LOWERCASE archive root + microsoft.aspnetcore.app.runtime.{rid}/Release/ -- this directory name + is a load-bearing contract for the future dotnet/crank aspnetcore + overlay path, which mirrors BuildCacheClient's + FindDirectory(extractDir, "microsoft.aspnetcore.app.runtime.{rid}"). + 3. Validates the managed Microsoft.AspNetCore.*.dll assemblies landed under + runtimes/{rid}/lib and strips any stray *.pdb / *.dbg files. + 4. Compresses the lowercase root directory into the per-config archive. + 5. Asserts the archive's single root entry matches the contract. + +.PARAMETER Rids + One or more .NET RIDs to pack (e.g. win-x64, win-x86, win-arm64, linux-x64, + linux-arm64). os/arch and the BCS naming are derived from each RID. + +.PARAMETER Format + Archive format: 'zip' (Windows convention) or 'targz' (Linux convention). + +.PARAMETER ShippingDir + Directory containing the runtime-pack nupkgs. Defaults to + $(Build.SourcesDirectory)/artifacts/packages/Release/Shipping so the + pipeline can call this with no path arguments; override it for local runs. + +.PARAMETER StagingRoot + Directory under which the per-config archive staging folders are created. + Defaults to $(Build.ArtifactStagingDirectory)/bcs. + +.EXAMPLE + ./pack-bcs-archives.ps1 -Rids win-x64,win-x86,win-arm64 -Format zip + +.EXAMPLE + ./pack-bcs-archives.ps1 -Rids linux-x64 -Format targz +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string[]]$Rids, + + [Parameter(Mandatory = $true)] + [ValidateSet('zip', 'targz')] + [string]$Format, + + [string]$ShippingDir = (Join-Path $env:BUILD_SOURCESDIRECTORY 'artifacts/packages/Release/Shipping'), + + [string]$StagingRoot = (Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY 'bcs') +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +Add-Type -AssemblyName System.IO.Compression.FileSystem + +function Get-RidParts { + param([string]$Rid) + + $dash = $Rid.IndexOf('-') + if ($dash -lt 1) { + throw "RID '$Rid' is not in the expected '-' form." + } + + $osToken = $Rid.Substring(0, $dash) + $arch = $Rid.Substring($dash + 1) + $os = switch ($osToken) { + 'win' { 'windows' } + 'linux' { 'linux' } + 'osx' { 'osx' } + default { throw "Unsupported RID os token '$osToken' in RID '$Rid'." } + } + + return [pscustomobject]@{ Os = $os; Arch = $arch } +} + +if (-not (Test-Path $ShippingDir)) { + throw "Shipping directory '$ShippingDir' does not exist." +} + +New-Item -ItemType Directory -Force -Path $StagingRoot | Out-Null + +foreach ($rid in $Rids) { + $parts = Get-RidParts -Rid $rid + $os = $parts.Os + $arch = $parts.Arch + $configKey = "aspnetcore_${arch}_${os}" + $artifactName = "BuildArtifacts_${os}_${arch}_Release_aspnetcore" + $ext = if ($Format -eq 'zip') { 'zip' } else { 'tar.gz' } + $archiveFile = "${artifactName}.${ext}" + + Write-Host '' + Write-Host "=== Packing $configKey (rid=$rid, format=$Format) ===" + + $pattern = "Microsoft.AspNetCore.App.Runtime.$rid.*.nupkg" + $nupkg = Get-ChildItem -Path $ShippingDir -Filter $pattern -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notmatch '\.symbols\.nupkg$' } | + Sort-Object Name | + Select-Object -First 1 + if (-not $nupkg) { + throw "Could not find runtime pack nupkg matching '$pattern' under '$ShippingDir'." + } + Write-Host "Found nupkg: $($nupkg.FullName)" + + # microsoft.aspnetcore.app.runtime.{rid}/Release/ <-- archive root (lowercase!) + $stageDir = Join-Path $StagingRoot $artifactName + $payloadDir = Join-Path $stageDir "microsoft.aspnetcore.app.runtime.$rid" + $releaseDir = Join-Path $payloadDir 'Release' + if (Test-Path $payloadDir) { + Remove-Item -LiteralPath $payloadDir -Recurse -Force + } + New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null + + # Extract the nupkg (it's a zip) into Release/. Using the .NET API keeps the + # extraction identical on Windows and Linux agents (no unzip/Expand-Archive split). + [System.IO.Compression.ZipFile]::ExtractToDirectory($nupkg.FullName, $releaseDir) + + $runtimesDir = Join-Path $releaseDir "runtimes/$rid" + if (-not (Test-Path $runtimesDir)) { + throw "Extracted runtime pack is missing expected directory '$runtimesDir'." + } + + # Sanity-check that managed assemblies landed where we expect. + $libMatches = @(Get-ChildItem -Path (Join-Path $runtimesDir 'lib') -Recurse -Filter 'Microsoft.AspNetCore.*.dll' -ErrorAction SilentlyContinue) + if ($libMatches.Count -eq 0) { + throw "Extracted runtime pack for $rid is missing managed Microsoft.AspNetCore.*.dll under 'runtimes/$rid/lib'." + } + Write-Host "Validated managed lib dir contains $($libMatches.Count) Microsoft.AspNetCore.*.dll(s)." + + # Defensive: strip debug-symbol files that occasionally tag along. + Get-ChildItem -Path $runtimesDir -Recurse -Include '*.pdb', '*.dbg' -ErrorAction SilentlyContinue | + ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force } + + $archivePath = Join-Path $stageDir $archiveFile + if (Test-Path $archivePath) { + Remove-Item -LiteralPath $archivePath -Force + } + + $expectedRoot = "microsoft.aspnetcore.app.runtime.$rid/" + + if ($Format -eq 'zip') { + # Zip the lowercase root directory so the archive contents start with + # `microsoft.aspnetcore.app.runtime.{rid}/...`. + Compress-Archive -Path $payloadDir -DestinationPath $archivePath -CompressionLevel Optimal -Force + + # Assert the archive root matches the load-bearing contract. + $zip = [System.IO.Compression.ZipFile]::OpenRead($archivePath) + try { + $rootEntries = @($zip.Entries | ForEach-Object { ($_.FullName -split '/')[0] } | Sort-Object -Unique) + if ($rootEntries.Count -ne 1 -or "$($rootEntries[0])/" -ne $expectedRoot) { + throw "Archive '$archivePath' has root entries [$($rootEntries -join ', ')] but expected exactly '$expectedRoot' (load-bearing contract for future crank PR)." + } + } + finally { + $zip.Dispose() + } + } + else { + # Tar the lowercase root directory (so the archive contents start with + # `microsoft.aspnetcore.app.runtime.{rid}/...`). + & tar -czf $archivePath -C $stageDir "microsoft.aspnetcore.app.runtime.$rid" + if ($LASTEXITCODE -ne 0) { + throw "tar of '$payloadDir' failed (exit $LASTEXITCODE)." + } + + # Assert the archive root matches the load-bearing contract. + $firstEntry = (& tar -tzf $archivePath | Select-Object -First 1) + if (-not $firstEntry.StartsWith($expectedRoot)) { + throw "Archive '$archivePath' first entry '$firstEntry' does not start with '$expectedRoot' (load-bearing contract for future crank PR)." + } + } + + # Drop the working payload directory so only the archive ends up in the published pipeline artifact. + Remove-Item -LiteralPath $payloadDir -Recurse -Force + + $size = (Get-Item $archivePath).Length + Write-Host "Produced archive: $archivePath ($size bytes)" +}