-
Notifications
You must be signed in to change notification settings - Fork 570
Expand file tree
/
Copy pathDownloadFileWithRetry.targets
More file actions
192 lines (175 loc) · 9.99 KB
/
Copy pathDownloadFileWithRetry.targets
File metadata and controls
192 lines (175 loc) · 9.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<Project>
<!--
Shared download helper. Wraps MSBuild's `<DownloadFile>` task in an outer
retry loop and an optional SHA-256 self-heal step.
Why this exists:
MSBuild's built-in `DownloadFile` task has `Retries` /
`RetryDelayMilliseconds`, but its internal `IsRetriable` check only
retries when a `HttpRequestException` wraps an inner `IOException`.
The `ResponseEnded` failure that flaky CDNs produce is thrown as a
top-level `HttpIOException`, so `Retries` does NOT cover it - the task
errors out on the first mid-stream disconnect. This file's
`DownloadOneFileWithRetry` target wraps `<DownloadFile>` in three
outer attempts using `ContinueOnError="WarnAndContinue"`, so transient
stream failures get up to 3 retries before failing the build.
Why callers dispatch via `<MSBuild>` (and not `<CallTarget>`):
Two MSBuild semantics force this:
1. Items added inside a target body are NOT visible to a target
invoked via `<CallTarget>` from that same body. They ARE visible
to tasks called from the same body, including the `<MSBuild>`
task. Without that, every caller would silently pass an empty
item list and download nothing.
2. A target executes at most once per build context, so a
`<CallTarget>` cannot be used twice (e.g. openjdk's two-phase
hash-then-archive flow). `<MSBuild>` with distinct
`AdditionalProperties` per item creates distinct build requests,
so the same target body runs once per file - and `BuildInParallel`
lets MSBuild fan those requests out across worker nodes when
there is more than one file to download.
Required input properties for `DownloadOneFileWithRetry`:
_DownloadUrl = source URL
_DownloadFolder = destination folder
_DownloadFileName = file name to write inside _DownloadFolder
Optional input properties:
_DownloadSha256 = expected SHA-256 (uppercase hex). When
set, the downloaded file is hashed and
removed/errored if the hash does not
match. If a previously cached file is
present with the wrong hash, it is also
deleted up-front so this run re-downloads
instead of failing.
_DownloadCleanupOnMismatch = semicolon-separated list of additional
files to delete on Sha256 mismatch. Use
for sibling files (e.g. a .sha256sum.txt
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. 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 `<MSBuild>`). Each
item carries its per-file parameters as `AdditionalProperties` metadata,
and the `<MSBuild>` call projects every item onto this targets file so
MSBuild treats each one as a distinct, parallelizable build request:
<ItemGroup>
<_DownloadFile Include="@(_BuildTools)">
<AdditionalProperties>_DownloadUrl=$(BaseUrl)%(Identity);_DownloadFolder=$(Cache);_DownloadFileName=%(Identity);_DownloadSha256=%(Hash)</AdditionalProperties>
</_DownloadFile>
</ItemGroup>
<MSBuild Projects="@(_DownloadFile->'$(DownloadFileWithRetryFile)')"
Targets="DownloadOneFileWithRetry"
BuildInParallel="true" />
For a single file, the same shape works - use the file name as the
Include so each item still has a distinct identity:
<ItemGroup>
<_DownloadFile Include="$(FileName)">
<AdditionalProperties>_DownloadUrl=$(Url);_DownloadFolder=$(Cache);_DownloadFileName=$(FileName);_DownloadSha256=$(Hash)</AdditionalProperties>
</_DownloadFile>
</ItemGroup>
<MSBuild Projects="@(_DownloadFile->'$(DownloadFileWithRetryFile)')"
Targets="DownloadOneFileWithRetry"
BuildInParallel="true" />
-->
<PropertyGroup>
<DownloadFileWithRetryFile>$(MSBuildThisFileFullPath)</DownloadFileWithRetryFile>
<_DownloadRetries>5</_DownloadRetries>
<_DownloadRetryDelayMilliseconds>5000</_DownloadRetryDelayMilliseconds>
<!-- Computed up-front (vs inside the target body) so the per-file
destination and stamp paths are visible to the target's
Inputs/Outputs incremental gate below. Each `<MSBuild>` build
request that calls into this file gets its own evaluated
scope with these AdditionalProperties already set. -->
<_DownloadPath>$(_DownloadFolder)\$(_DownloadFileName)</_DownloadPath>
<_DownloadStamp Condition=" '$(_DownloadSha256)' != '' ">$(_DownloadPath).$(_DownloadSha256).stamp</_DownloadStamp>
<_DownloadStamp Condition=" '$(_DownloadSha256)' == '' ">$(_DownloadPath).stamp</_DownloadStamp>
</PropertyGroup>
<Target Name="DownloadOneFileWithRetry"
DependsOnTargets="_PrepareDownloadOneFileWithRetry"
Inputs="$(MSBuildThisFileFullPath)"
Outputs="$(_DownloadStamp)">
<!-- Self-heal: if a cached file already exists AND we know the expected
hash, verify it up-front. On mismatch, delete (along with any
CleanupOnMismatch siblings) so the download attempts below actually
re-fetch in this same build. Without this, a stale/corrupt cached
file would skip the download (because of the !Exists guard below)
and the final SHA check would fail the build with no recovery. -->
<GetFileHash Condition=" Exists('$(_DownloadPath)') and '$(_DownloadSha256)' != '' "
Files="$(_DownloadPath)"
Algorithm="SHA256">
<Output TaskParameter="Hash" PropertyName="_DownloadExistingSha256" />
</GetFileHash>
<Delete
Condition=" '$(_DownloadExistingSha256)' != '' and '$(_DownloadExistingSha256)' != '$(_DownloadSha256)' "
Files="$(_DownloadPath);$(_DownloadCleanupOnMismatch)"
/>
<!-- Outer retry loop. Each `<DownloadFile>` is gated by `!Exists()`, so
once a download succeeds the remaining attempts no-op. The first
two use `WarnAndContinue` so transient failures don't fail the
build; the third lets the error propagate. -->
<DownloadFile Condition=" !Exists('$(_DownloadPath)') "
SourceUrl="$(_DownloadUrl)"
DestinationFolder="$(_DownloadFolder)"
DestinationFileName="$(_DownloadFileName)"
Retries="$(_DownloadRetries)"
RetryDelayMilliseconds="$(_DownloadRetryDelayMilliseconds)"
ContinueOnError="WarnAndContinue"
/>
<DownloadFile Condition=" !Exists('$(_DownloadPath)') "
SourceUrl="$(_DownloadUrl)"
DestinationFolder="$(_DownloadFolder)"
DestinationFileName="$(_DownloadFileName)"
Retries="$(_DownloadRetries)"
RetryDelayMilliseconds="$(_DownloadRetryDelayMilliseconds)"
ContinueOnError="WarnAndContinue"
/>
<DownloadFile Condition=" !Exists('$(_DownloadPath)') "
SourceUrl="$(_DownloadUrl)"
DestinationFolder="$(_DownloadFolder)"
DestinationFileName="$(_DownloadFileName)"
Retries="$(_DownloadRetries)"
RetryDelayMilliseconds="$(_DownloadRetryDelayMilliseconds)"
/>
<!-- Final SHA-256 verification (covers the just-downloaded case). -->
<GetFileHash Condition=" '$(_DownloadSha256)' != '' "
Files="$(_DownloadPath)"
Algorithm="SHA256">
<Output TaskParameter="Hash" PropertyName="_DownloadActualSha256" />
</GetFileHash>
<Delete
Condition=" '$(_DownloadSha256)' != '' and '$(_DownloadActualSha256)' != '$(_DownloadSha256)' "
Files="$(_DownloadPath);$(_DownloadCleanupOnMismatch)"
/>
<Error
Condition=" '$(_DownloadSha256)' != '' and '$(_DownloadActualSha256)' != '$(_DownloadSha256)' "
Text="SHA256 mismatch for $(_DownloadFileName). Expected: $(_DownloadSha256) Actual: $(_DownloadActualSha256)"
/>
<!-- Record success so subsequent no-op builds can skip this target via
the Inputs/Outputs gate above (and avoid re-hashing the cached
file). Reached only when the SHA check above passed - or when no
hash was supplied to verify against. -->
<Touch Files="$(_DownloadStamp)" AlwaysCreate="True" />
</Target>
<!-- Self-heal helper for the Inputs/Outputs gate on DownloadOneFileWithRetry.
Always runs (no Inputs/Outputs of its own) and is cheap: a single Exists
check per file. If the stamp claims a previous successful validation but
the cached file is gone, we drop the stamp so the parent target sees its
Outputs as incomplete and re-runs to re-fetch. -->
<Target Name="_PrepareDownloadOneFileWithRetry">
<Delete Condition=" Exists('$(_DownloadStamp)') and !Exists('$(_DownloadPath)') "
Files="$(_DownloadStamp)"
/>
</Target>
</Project>