diff --git a/docs/design/tools/illink/task-cache.md b/docs/design/tools/illink/task-cache.md index 55662b0ce75432..54c0a8c59e2fa9 100644 --- a/docs/design/tools/illink/task-cache.md +++ b/docs/design/tools/illink/task-cache.md @@ -82,7 +82,7 @@ internal sealed class ILLinkCache - Purge requires an explicit cache root and UTC cutoff. Only published SHA-256 entry directories in `v1` are considered; delete entries whose last-used time is strictly older than the cutoff and retain equality. Leave staging directories and other layouts alone. Report per-entry maintenance errors and continue, then return nonzero if maintenance failed. - For a missing, malformed, non-UTC, or unreadable marker, report the fallback and use the entry directory's creation time instead. This may purge a recently used entry or retain a stale one if restoring a cache snapshot resets directory creation times. Do not migrate or repair the marker. - Purge must not run during any build using the cache. Do not add purge locks or change reader locking; coordination with active readers/writers remains a separate follow-up, as does protection against manual deletion. -- CI may record build start after restoring a job-private cache, run linking, then purge against that timestamp before uploading the next snapshot. Preserve tool diagnostics but do not fail the build for maintenance errors. Only purge once all users have exited; incremental skips and unreached work in failed builds do not refresh usage. See the [CI recipe](../../../tools/illink/illink-tasks.md#ci-purge-ordering). This does not enable an ILLink cache in CI automatically. +- Only the `CoreLib_Libraries_AnalyzersLinker` CI job opts into ILLink caching through `setup-build-caches.yml`, on Windows and Unix x64/arm64. Roslyn and sccache remain gated by platform. Restore a job-private Azure cache, record build start, run linking and all post-build steps, then purge against that timestamp before uploading the next snapshot. This job explicitly enables `RunILLinkInBuild` and builds the `tools.illink` subset for the maintenance tool; building `ILLink.Tasks` through a library project reference does not build it. Purge only after a successful build; skip maintenance when no versioned cache directory exists and report a missing tool. Preserve tool diagnostics but do not fail the build for maintenance errors. Only purge once all users have exited; incremental skips and unreached work in failed builds do not refresh usage. See the [CI recipe and wiring](../../../tools/illink/illink-tasks.md#ci-purge-ordering). - Attempt best-effort cleanup of the current attempt's unpublished staging directory. Log cleanup I/O/access failures without failing a successful link; do not scan or delete other attempts' staging directories. Correctness must tolerate leftovers after crashes. ## Open follow-ups diff --git a/docs/tools/illink/illink-tasks.md b/docs/tools/illink/illink-tasks.md index c6f092e7fca48e..7e11f70f63b0f0 100644 --- a/docs/tools/illink/illink-tasks.md +++ b/docs/tools/illink/illink-tasks.md @@ -115,8 +115,8 @@ or created by that build: ```sh export ILLINK_CACHE_BUILD_START="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" ``` -3. Run the build and wait for all processes using the cache to finish. -4. Run the tool from the same revision, with maintenance failures reported but nonfatal: +3. Run the build successfully and wait for all processes using the cache to finish. +4. Run the maintenance tool from the same checkout, with maintenance failures reported but nonfatal: ```sh if ! dotnet illink-cache purge \ --cache-directory "$ILLINK_EXPERIMENTAL_CACHE_PATH" \ @@ -131,13 +131,24 @@ If the build and purge run in different CI steps, persist `ILLINK_CACHE_BUILD_ST pipeline variable; a shell export alone does not carry it across steps. This variable is only for the CI recipe, not another task configuration setting. -A failed build can still be followed by purge once all linking has stopped, but entries for -work that was never reached will count as unused. Likewise, incremental builds that skip -linking do not refresh cache usage. Skip purge if the cache may still be in use, including -during cancellation. Do not share a live directory with another job while purging. - -This is usage guidance, not automatic CI enablement. ILLink cache restore/upload wiring -is separate from runtime's Roslyn/csc cache maintenance. +Skip purge after a failed or canceled build. Incremental builds that skip linking do not +refresh cache usage. Do not share a live directory with another job while purging. + +The `CoreLib_Libraries_AnalyzersLinker` job in `global-build.yml` opts into ILLink caching +through `setup-build-caches.yml` on Windows and Unix x64/arm64 and sets `RunILLinkInBuild=true`. +The shared setup also configures Roslyn caching on Windows and Unix x64/arm64, and sccache +on Unix x64/arm64; other jobs retain those platform gates without restoring the ILLink cache. +`setup-illink-cache.yml` restores a branch-scoped Azure +Pipelines snapshot into `$(Pipeline.Workspace)/.illink-cache`, enables the experiment, +and records build start. `global-build-job.yml` runs `purge-illink-cache.yml` after all +post-build steps and before the `Cache@2` post-job save. The linker job explicitly +builds the `tools.illink` subset, which includes the purge tool; building `ILLink.Tasks` +through a library project reference alone does not build it. The purge step reports +missing prerequisites and maintenance failures as warnings, and runs only when ILLink +caching was enabled and the job succeeded. When no versioned cache directory exists, +it skips purge. + +This configuration is separate from runtime's Roslyn/csc and native compiler caches. ## ILLink Task Properties diff --git a/eng/pipelines/common/global-build-job.yml b/eng/pipelines/common/global-build-job.yml index 7ed5a35d0cfbb0..bdf49787d55b4f 100644 --- a/eng/pipelines/common/global-build-job.yml +++ b/eng/pipelines/common/global-build-job.yml @@ -277,3 +277,7 @@ jobs: - template: /eng/pipelines/coreclr/templates/prune-roslyn-cache.yml parameters: osGroup: ${{ parameters.osGroup }} + + - template: /eng/pipelines/coreclr/templates/purge-illink-cache.yml + parameters: + osGroup: ${{ parameters.osGroup }} diff --git a/eng/pipelines/coreclr/templates/purge-illink-cache.yml b/eng/pipelines/coreclr/templates/purge-illink-cache.yml new file mode 100644 index 00000000000000..b2a566635a0599 --- /dev/null +++ b/eng/pipelines/coreclr/templates/purge-illink-cache.yml @@ -0,0 +1,88 @@ +parameters: + osGroup: 'linux' + +steps: + - ${{ if ne(parameters.osGroup, 'windows') }}: + - script: | + set -euo pipefail + + if [[ -z "${ILLINK_EXPERIMENTAL_CACHE_PATH:-}" || + -z "${ILLINK_CACHE_BUILD_START:-}" ]]; then + echo "##vso[task.logissue type=warning]ILLink cache configuration or build-start timestamp is unavailable; skipping purge." + exit 0 + fi + + if [[ ! -e "$ILLINK_EXPERIMENTAL_CACHE_PATH/v1" ]]; then + echo "No ILLink cache entries were created; skipping purge." + exit 0 + fi + + dotnet="$(Build.SourcesDirectory)/.dotnet/dotnet" + if [[ ! -x "$dotnet" ]]; then + echo "##vso[task.logissue type=warning]The build SDK is unavailable; skipping ILLink cache purge." + exit 0 + fi + + tool="" + for candidate in "$(Build.SourcesDirectory)"/artifacts/bin/ILLink.CacheTool/*/net*/dotnet-illink-cache.dll; do + if [[ -f "$candidate" ]]; then + tool="$candidate" + break + fi + done + + if [[ -z "$tool" ]]; then + echo "##vso[task.logissue type=warning]The ILLink cache maintenance tool was not built; skipping purge." + exit 1 + fi + + if ! "$dotnet" "$tool" purge \ + --cache-directory "$ILLINK_EXPERIMENTAL_CACHE_PATH" \ + --before "$ILLINK_CACHE_BUILD_START"; then + echo "##vso[task.logissue type=warning]ILLink cache purge failed; see the tool diagnostics above." + fi + displayName: Purge unused ILLink cache entries + condition: and(succeeded(), eq(variables['ILLINK_EXPERIMENTAL_CACHE'], 'true')) + continueOnError: true + + - ${{ if eq(parameters.osGroup, 'windows') }}: + - pwsh: | + if ([string]::IsNullOrEmpty($env:ILLINK_EXPERIMENTAL_CACHE_PATH) -or + [string]::IsNullOrEmpty($env:ILLINK_CACHE_BUILD_START)) { + Write-Host "##vso[task.logissue type=warning]ILLink cache configuration or build-start timestamp is unavailable; skipping purge." + exit 0 + } + + if (-not (Test-Path -LiteralPath (Join-Path $env:ILLINK_EXPERIMENTAL_CACHE_PATH "v1") -PathType Container)) { + Write-Host "No ILLink cache entries were created; skipping purge." + exit 0 + } + + $dotnet = Join-Path "$(Build.SourcesDirectory)" ".dotnet/dotnet.exe" + if (-not (Test-Path -LiteralPath $dotnet -PathType Leaf)) { + Write-Host "##vso[task.logissue type=warning]The build SDK is unavailable; skipping ILLink cache purge." + exit 0 + } + + $toolRoot = Join-Path "$(Build.SourcesDirectory)" "artifacts/bin/ILLink.CacheTool" + if (-not (Test-Path -LiteralPath $toolRoot -PathType Container)) { + Write-Host "##vso[task.logissue type=warning]The ILLink cache maintenance tool was not built; skipping purge." + exit 1 + } + + $tool = Get-ChildItem -LiteralPath $toolRoot -Filter "dotnet-illink-cache.dll" -File -Recurse | + Sort-Object FullName | Select-Object -First 1 + if ($null -eq $tool) { + Write-Host "##vso[task.logissue type=warning]The ILLink cache maintenance tool was not built; skipping purge." + exit 1 + } + + & $dotnet $tool.FullName purge ` + --cache-directory $env:ILLINK_EXPERIMENTAL_CACHE_PATH ` + --before $env:ILLINK_CACHE_BUILD_START + if ($LASTEXITCODE -ne 0) { + Write-Host "##vso[task.logissue type=warning]ILLink cache purge failed; see the tool diagnostics above." + } + displayName: Purge unused ILLink cache entries + condition: and(succeeded(), eq(variables['ILLINK_EXPERIMENTAL_CACHE'], 'true')) + continueOnError: true diff --git a/eng/pipelines/coreclr/templates/setup-build-caches.yml b/eng/pipelines/coreclr/templates/setup-build-caches.yml index b82d888fac5e98..2370bbb852cd62 100644 --- a/eng/pipelines/coreclr/templates/setup-build-caches.yml +++ b/eng/pipelines/coreclr/templates/setup-build-caches.yml @@ -10,6 +10,7 @@ parameters: platform: '' shouldContinueOnError: false osSubgroup: '' + enableILLinkCache: false # sccache version. Keep the version in sync across platforms so all jobs # cache against the same compiler-launcher behavior. @@ -51,6 +52,14 @@ steps: Write-Host "##vso[task.setvariable variable=ROSLYN_CACHE_BUILD_START]$buildStart" displayName: Configure Roslyn compiler cache + - ${{ if eq(parameters.enableILLinkCache, true) }}: + - template: /eng/pipelines/coreclr/templates/setup-illink-cache.yml + parameters: + osGroup: ${{ parameters.osGroup }} + osSubgroup: ${{ parameters.osSubgroup }} + archType: ${{ parameters.archType }} + nameSuffix: ${{ parameters.nameSuffix }} + # sccache native compiler cache - ${{ if and(or(eq(parameters.osGroup, 'linux'), eq(parameters.osGroup, 'freebsd'), eq(parameters.osGroup, 'openbsd'), eq(parameters.osGroup, 'osx')), or(eq(parameters.archType, 'x64'), eq(parameters.archType, 'arm64'))) }}: # Set up the Azure Pipeline Cache for sccache's local cache directory. diff --git a/eng/pipelines/coreclr/templates/setup-illink-cache.yml b/eng/pipelines/coreclr/templates/setup-illink-cache.yml new file mode 100644 index 00000000000000..a18c73527ec0d2 --- /dev/null +++ b/eng/pipelines/coreclr/templates/setup-illink-cache.yml @@ -0,0 +1,42 @@ +parameters: + archType: 'x64' + osGroup: 'linux' + nameSuffix: '' + osSubgroup: '' + +steps: + - ${{ if or(eq(parameters.osGroup, 'windows'), and(in(parameters.osGroup, 'linux', 'freebsd', 'openbsd', 'osx'), in(parameters.archType, 'x64', 'arm64'))) }}: + # Use the same branch-scoped restore policy as the compiler caches. Each job + # works on its own restored copy, so post-build purge cannot affect another job. + # Keep Debug and Release snapshots separate in rolling builds. + - task: Cache@2 + displayName: ILLink task cache + continueOnError: true + inputs: + key: illink | v1 | ${{ parameters.osGroup }}${{ parameters.osSubgroup }} | ${{ parameters.archType }} | ${{ parameters.nameSuffix }} | "$(buildConfigUpper)" | "$(Build.BuildId)" + path: $(Pipeline.Workspace)/.illink-cache + restoreKeys: | + illink | v1 | ${{ parameters.osGroup }}${{ parameters.osSubgroup }} | ${{ parameters.archType }} | ${{ parameters.nameSuffix }} | "$(buildConfigUpper)" + + - ${{ if ne(parameters.osGroup, 'windows') }}: + - script: | + set -euo pipefail + mkdir -p "$(Pipeline.Workspace)/.illink-cache" + buildStart="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + echo "##vso[task.setvariable variable=ILLINK_EXPERIMENTAL_CACHE_PATH]$(Pipeline.Workspace)/.illink-cache" + echo "##vso[task.setvariable variable=ILLINK_CACHE_BUILD_START]$buildStart" + echo "##vso[task.setvariable variable=ILLINK_EXPERIMENTAL_CACHE]true" + displayName: Configure experimental ILLink task cache + continueOnError: true + + - ${{ if eq(parameters.osGroup, 'windows') }}: + - pwsh: | + $cachePath = Join-Path "$(Pipeline.Workspace)" ".illink-cache" + New-Item -ItemType Directory -Path $cachePath -Force | Out-Null + $buildStart = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + + Write-Host "##vso[task.setvariable variable=ILLINK_EXPERIMENTAL_CACHE_PATH]$cachePath" + Write-Host "##vso[task.setvariable variable=ILLINK_CACHE_BUILD_START]$buildStart" + Write-Host "##vso[task.setvariable variable=ILLINK_EXPERIMENTAL_CACHE]true" + displayName: Configure experimental ILLink task cache + continueOnError: true diff --git a/eng/pipelines/global-build.yml b/eng/pipelines/global-build.yml index f40cd1e3ceabee..5db24664ad6c48 100644 --- a/eng/pipelines/global-build.yml +++ b/eng/pipelines/global-build.yml @@ -93,11 +93,18 @@ extends: - osx_arm64 jobParameters: nameSuffix: CoreLib_Libraries_AnalyzersLinker - buildArgs: -subset clr.corelib+libs -restore -build -pack -c $(_BuildConfig) /p:RunAnalyzersInBuild=true /p:RunILLinkInBuild=true + buildArgs: -subset clr.corelib+libs+tools.illink -restore -build -pack -c $(_BuildConfig) /p:RunAnalyzersInBuild=true /p:RunILLinkInBuild=true timeoutInMinutes: 120 + preBuildSteps: + - template: /eng/pipelines/coreclr/templates/setup-build-caches.yml + parameters: + enableILLinkCache: true + postBuildSteps: + - template: /eng/pipelines/coreclr/templates/build-cache-stats.yml condition: or( eq(stageDependencies.EvaluatePaths.evaluate_paths.outputs['SetPathVars_libraries.containsChange'], true), + eq(stageDependencies.EvaluatePaths.evaluate_paths.outputs['SetPathVars_tools_illink.containsChange'], true), eq(variables['isRollingBuild'], true)) #