[ci] Cache Android toolchain downloads + bump DownloadFile retries - #11618
Merged
jonathanpeppers merged 2 commits intoJun 10, 2026
Conversation
Builds frequently fail on `dl.google.com` DNS / TCP errors during the
initial download of the Android toolchain (commandlinetools, build-tools,
emulator, system images, m2repository, docs, source, etc.) on dnceng-public
agents. Example failure:
error MSB3923: Failed to download file
"https://dl.google.com/android/repository/source-36_r01.zip".
nodename nor servname provided, or not known (dl.google.com:443)
Two changes here:
1. Add a Pipelines `Cache@2` step for `$(AndroidToolchainCacheDirectory)`
(a.k.a. `$HOME/android-archives`), modeled exactly on the existing
`cache-gradle.yaml` template. Wired into all three of
build-{linux,macos,windows}-steps.yaml.
Cache safety: every download under this directory is verified against
a hardcoded SHA-256 by the corresponding MSBuild target
(androidsdk/openjdk/binutils/aapt2/bundletool). A stale / corrupt
cached entry is detected and re-downloaded - cache hits cannot mask
a version bump.
Cache key is `Configuration.props` + each download `*.targets` file,
so any version or hash change invalidates the cache. The `restoreKeys`
fallback (`Agent.OS` only) still recovers most bytes when only a
subset of components changes.
2. Bump `Retries` from 3 to 5 and add `RetryDelayMilliseconds="5000"`
on every `DownloadFile` call. This makes the first (cold-cache) and
scheduled runs (which warm the shared cache) more resilient to
transient `dl.google.com` / `aka.ms` / `github.com` flakes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves CI reliability for dotnet/android by reducing sensitivity to transient network/DNS failures when downloading the Android toolchain, and by reusing previously downloaded archives across ephemeral agents.
Changes:
- Add an Azure Pipelines cache for
$(AndroidToolchainCacheDirectory)(default~/android-archives) and wire it into Linux/macOS/Windows build step templates. - Increase
DownloadFileretries from 3 → 5 and add a 5s retry delay across toolchain download targets (Android SDK packages, aapt2/build-tools, OpenJDK, binutils, bundletool). - Introduce a cache key strategy based on
Configuration.propsand the download targets so cache invalidates on version/hash changes.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/openjdk/openjdk.targets | Increase download retries and add retry delay for OpenJDK archive + hash file downloads. |
| src/bundletool/bundletool.targets | Increase download retries and add retry delay for bundletool jar download. |
| src/binutils/binutils.targets | Increase download retries and add retry delay for binutils archive download. |
| src/androidsdk/androidsdk.targets | Increase download retries and add retry delay for Android SDK package downloads. |
| src/aapt2/aapt2.targets | Increase download retries and add retry delay for build-tools (aapt2) downloads. |
| build-tools/automation/yaml-templates/cache-android-archives.yaml | New template to cache Android toolchain archives directory between pipeline runs. |
| build-tools/automation/yaml-templates/build-windows-steps.yaml | Wire android-archives cache template into Windows build steps. |
| build-tools/automation/yaml-templates/build-macos-steps.yaml | Wire android-archives cache template into macOS build steps. |
| build-tools/automation/yaml-templates/build-linux-steps.yaml | Wire android-archives cache template into Linux build steps. |
* src/bundletool/bundletool.targets: delete the cached jar when the SHA-256 does not match `$(XABundleToolHash)`, matching the pattern used in androidsdk/binutils/aapt2/openjdk targets. Without this, a corrupt jar persisted in the new pipeline cache could repeatedly break future builds. * cache-android-archives.yaml: reword the cache-safety comment to accurately describe verification - most targets compare against a hash hardcoded in Configuration.props, but openjdk verifies against a downloaded .sha256sum.txt from Microsoft. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival
approved these changes
Jun 10, 2026
simonrozsival
added a commit
that referenced
this pull request
Jul 7, 2026
## Summary This is another incremental hardening PR for the recurring `install OpenJDK and accept Android SDK licenses` CI failure where large Android SDK archives from `dl.google.com` fail with: ```text MSB3923: Failed to download file "https://dl.google.com/android/repository/...". The response ended prematurely. (ResponseEnded) ``` The specific recent failure already had the newer retry/backoff behavior: the log shows retries waiting 30s, 60s, 90s, and 120s before ultimately exhausting `x86_64-29_r08-darwin.zip`. So this PR does **not** replace the existing download retry work; it builds on it and covers the next failure mode. ## Prior work this builds on - [#11348](#11348) moved OpenJDK installation into an MSBuild NoTargets project. - [#11440](#11440) moved Android SDK/NDK component downloads into `src/androidsdk/androidsdk.targets`, which is what this setup step builds. - [#11618](#11618) added the shared `android-archives` pipeline cache for build jobs and bumped the built-in `DownloadFile` retries. - [#11647](#11647) added the shared `DownloadFileWithRetry` wrapper because `ResponseEnded` is a top-level `HttpIOException` that MSBuild's built-in `DownloadFile` retry logic does not catch. - [#11693](#11693) made `DownloadOneFileWithRetry` skip cleanly on no-op builds using per-file stamps, which makes whole-step retries less wasteful once some archives already succeeded. - [#11817](#11817) added escalating outer backoff between retry attempts, so transient CDN hiccups get several minutes to recover instead of burning through attempts immediately. ## Remaining gap Those PRs made individual downloads much more resilient, but the failing path is the **test environment setup** template, not the main build template: - `build-{linux,macos,windows}-steps.yaml` already uses `cache-android-archives.yaml`. - `setup-test-environment-steps.yaml` did **not** use that cache before running `src/androidsdk/androidsdk.csproj`. - The `install OpenJDK and accept Android SDK licenses` `run-dotnet-preview.yaml` invocation also had `retryCountOnTaskFailure: 0` through the template default. That means a macOS test setup job could still cold-download several large archives concurrently and fail the entire job if one archive exhausted the per-file retry/backoff loop. ## Change This PR hardens that remaining test-setup path by: - adding a `condition` parameter to `cache-android-archives.yaml`, preserving the existing default behavior for build jobs - invoking the Android archive cache from `setup-test-environment-steps.yaml` for macOS test setup before SDK downloads run - setting `retryCountOnTaskFailure: 2` on the `install OpenJDK and accept Android SDK licenses` step The cache is intentionally limited to macOS test setup for now. Linux archive caching already has a separate disk-pressure concern tracked by [#11837](#11837), so this PR avoids expanding Linux cache usage while still targeting the observed macOS `ResponseEnded` failure. ## Why step retry helps The per-file retry wrapper is still the first line of defense. The task-level retry is a second line of defense for the case where a specific archive exhausts all per-file attempts. On a second task attempt, successfully downloaded archives should be reused/skipped by the existing cache/stamp logic, so the retry mostly focuses on the archive that failed rather than restarting all work from scratch. ## Validation - `git diff --check`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
PR builds frequently fail on
dl.google.comDNS / TCP errors during the initial download of the Android toolchain (commandlinetools, build-tools, emulator, system images, m2repository, docs, source, etc.) on dnceng-public agents. Example from a recent build:This costs the entire ~4 hour pipeline run for one transient DNS hiccup.
Changes
1. Cache
$(AndroidToolchainCacheDirectory)between runsNew
build-tools/automation/yaml-templates/cache-android-archives.yamltemplate, modeled exactly on the existingcache-gradle.yaml. Wired into all three ofbuild-{linux,macos,windows}-steps.yamlright next to the existing Gradle cache step.Cache safety. Every download under
$HOME/android-archivesis verified after download by the corresponding MSBuild target (androidsdk/openjdk/binutils/aapt2/bundletool). Most targets check the downloaded file's SHA-256 against a hash hardcoded inConfiguration.props;openjdkinstead verifies against Microsoft's published.sha256sum.txt. In all cases a corrupt or stale archive is deleted, the build is failed, and the next run re-downloads it fresh — so cache hits cannot mask a version bump.Cache key:
Configuration.props+ each download*.targetsfile. Any version or hash change invalidates the cache. TherestoreKeysfallback (Agent.OSonly) still recovers most bytes when only a subset of components changes.Cache size. ~2 GB physical / ~10.7 GB raw per OS, comfortably within Azure Pipelines' per-key limit.
2. Bump
DownloadFileretriesRetries="3"→Retries="5"and addedRetryDelayMilliseconds="5000"on everyDownloadFileinvocation. This helps the first (cold-cache) and scheduled runs — which are responsible for warming the shared cache — better tolerate transient flakes ondl.google.com/aka.ms/github.com.3. Delete corrupt
bundletool.jaron SHA-256 mismatchbundletool.targetspreviously errored on hash mismatch but left the corrupt jar in place. With the new pipeline cache, that bad jar could be persisted and repeatedly break future builds. Now matches the pattern used inandroidsdk/binutils/aapt2/openjdk.Verification — what we actually save
Measured against build #1456864 (first run with the cache, so all three OSes are a cache miss — they still downloaded everything; the cache save for the next build is what completed successfully):
00:37:00→00:38:33)SkipUnchangedFiles=true")$HOME)So the wall-clock savings are modest: ~1.5–2 min per Linux build on a cache hit, near-zero on macOS / Windows.
The bigger value of this PR is reliability, not throughput. dnceng-public Linux agents are ephemeral and download ~2 GB from
dl.google.comon every build today. The previous motivating failure (build #1456555) lost a 4-hour run to a single DNS resolution error on one of those downloads. Cache hits avoid the request entirely; the bumped retries help the cold-cache and scheduled runs that have to repopulate the cache.