From 4b981a693554a345c46b64bbc67f3955a3fcc83e Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 17 Jun 2026 16:31:10 -0500 Subject: [PATCH 1/2] [build] skip DownloadOneFileWithRetry on no-op builds A no-op build binlog showed `DownloadOneFileWithRetry` running 17 times for a total of ~20s on a 77s no-op build: every invocation re-hashed an already-cached file (e.g. xamarin-android-toolchain-L_18.1.6-8.0.0-1.7z, build-tools_r36_macosx.zip) just to confirm the SHA-256 still matches. The Hash MSBuild task showed maxDurationMs=2005 per call across 32 invocations. Nothing changes on a no-op build, so this work was pure waste. Add a per-file stamp file (`/..stamp`, or `.stamp` when no hash is supplied) that gets touched once the download and SHA verification succeed, and gate the target with: Inputs=`$(MSBuildThisFileFullPath)` Outputs=`$(_DownloadPath);$(_DownloadStamp)` so MSBuild''s incremental engine skips the target - and the expensive re-hash - on no-op builds. Self-heal is preserved: * Cached file deleted -> output missing -> target re-runs. * `_DownloadSha256` updated in a caller -> stamp file name changes -> new stamp output missing -> target re-runs and re-verifies. * This .targets file itself modified -> Inputs newer -> target re-runs. The Touch lands after the SHA Error gate so a mismatch still fails the build with the existing message and never persists a stale stamp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../scripts/DownloadFileWithRetry.targets | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/build-tools/scripts/DownloadFileWithRetry.targets b/build-tools/scripts/DownloadFileWithRetry.targets index 93021e1ff30..08eab96181e 100644 --- a/build-tools/scripts/DownloadFileWithRetry.targets +++ b/build-tools/scripts/DownloadFileWithRetry.targets @@ -49,6 +49,18 @@ whose contents drove the expected hash) so the next build re-fetches them. + Incremental no-op gate: + A per-file stamp file next to the cached download + (`$(_DownloadFolder)\$(_DownloadFileName).$(_DownloadSha256).stamp`, + or `.stamp` when no hash is supplied) is touched once the download + and SHA verification succeed. The target's `Inputs`/`Outputs` then + lets MSBuild skip re-running it - and re-hashing the cached file - + on no-op builds. The expected SHA is encoded in the stamp file + name so changing `_DownloadSha256` in a caller invalidates the + cached stamp and forces re-verification. Listing the cached file + itself in `Outputs` preserves self-heal: if it disappears between + builds the target still runs and re-fetches. + Usage pattern. The `_DownloadFile` item group must be built inside the caller's target body (so the items are visible to ``). Each item carries its per-file parameters as `AdditionalProperties` metadata, @@ -81,13 +93,19 @@ $(MSBuildThisFileFullPath) <_DownloadRetries>5 <_DownloadRetryDelayMilliseconds>5000 + + <_DownloadPath>$(_DownloadFolder)\$(_DownloadFileName) + <_DownloadStamp Condition=" '$(_DownloadSha256)' != '' ">$(_DownloadPath).$(_DownloadSha256).stamp + <_DownloadStamp Condition=" '$(_DownloadSha256)' == '' ">$(_DownloadPath).stamp - - - <_DownloadPath>$(_DownloadFolder)\$(_DownloadFileName) - - + + From c53f211a1e89eaf7bcfc3f8516c22e97aa807fe1 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 17 Jun 2026 17:35:28 -0500 Subject: [PATCH 2/2] [build] only stamp file in Outputs; add self-heal helper Listing the cached file in `Outputs` alongside the stamp defeats the no-op gate when cache files restored from CI caches keep their original old timestamps: Inputs.max (this targets file) > Outputs.min (the cache file from a year ago) -> target re-runs and re-hashes every build. Drop the cache file from `Outputs` and add `_PrepareDownloadOneFileWithRetry` as `DependsOnTargets`. It always runs (no Inputs/Outputs of its own) and deletes the stamp when the cached file is gone, so MSBuild re-runs DownloadOneFileWithRetry with a now-incomplete output set. Verified end-to-end against `Xamarin.Android.sln`: BEFORE: 18 invocations, 22.53s of GetFileHash work on a no-op build (matches the user's reference msbuild.binlog at 20.08s) AFTER : 18 invocations, 1ms total - ALL skipped via Inputs/Outputs. Build wall: 101.94s -> 62.39s (~40s saved). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../scripts/DownloadFileWithRetry.targets | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/build-tools/scripts/DownloadFileWithRetry.targets b/build-tools/scripts/DownloadFileWithRetry.targets index 08eab96181e..ccf78712d5c 100644 --- a/build-tools/scripts/DownloadFileWithRetry.targets +++ b/build-tools/scripts/DownloadFileWithRetry.targets @@ -57,9 +57,14 @@ lets MSBuild skip re-running it - and re-hashing the cached file - on no-op builds. The expected SHA is encoded in the stamp file name so changing `_DownloadSha256` in a caller invalidates the - cached stamp and forces re-verification. Listing the cached file - itself in `Outputs` preserves self-heal: if it disappears between - builds the target still runs and re-fetches. + cached stamp and forces re-verification. Self-heal for a missing + cached file is preserved by `_PrepareDownloadOneFileWithRetry`, + which always runs and drops the stamp if the cached file is gone - + so this target's `Outputs` becomes incomplete and MSBuild forces + it to re-run. Only the stamp is listed in `Outputs`: including the + cached file there would defeat the gate, because cache files + restored from CI caches keep their original old timestamps and + would always look out of date relative to this targets file. Usage pattern. The `_DownloadFile` item group must be built inside the caller's target body (so the items are visible to ``). Each @@ -104,8 +109,9 @@ + Outputs="$(_DownloadStamp)"> + + + +