Skip to content

Commit 6101545

Browse files
dkelmerCopybara-Service
authored andcommitted
repository_ctx.download{_and_extract} return a struct.
The methods currently return a struct with an entry for "sha256". If a sha was provided in the rule and it was the correct sha, that sha will be returned. If no sha was provided, the calculated sha will be returned. Going forward, removing entries from the returned struct will be subject to proper deprecation periods, but additions can happen at any time. Repository rule writers should not depend on the absence of entries in the struct. RELNOTES[NEW]: repository_ctx.download and repository_ctx.download_and_extract now return a struct. PiperOrigin-RevId: 220474155
1 parent 795bd48 commit 6101545

File tree

3 files changed

+133
-35
lines changed

3 files changed

+133
-35
lines changed

src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
import com.google.common.annotations.VisibleForTesting;
1818
import com.google.common.base.Optional;
19+
import com.google.common.base.Strings;
1920
import com.google.common.collect.ImmutableList;
2021
import com.google.common.collect.ImmutableMap;
2122
import com.google.devtools.build.lib.actions.FileValue;
2223
import com.google.devtools.build.lib.bazel.debug.WorkspaceRuleEvent;
2324
import com.google.devtools.build.lib.bazel.repository.DecompressorDescriptor;
2425
import com.google.devtools.build.lib.bazel.repository.DecompressorValue;
26+
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
2527
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
2628
import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
2729
import com.google.devtools.build.lib.bazel.repository.downloader.HttpUtils;
@@ -332,7 +334,7 @@ private SkylarkPath findCommandOnPath(String program) throws IOException {
332334
}
333335

334336
@Override
335-
public void download(
337+
public StructImpl download(
336338
Object url, Object output, String sha256, Boolean executable, Location location)
337339
throws RepositoryFunctionException, EvalException, InterruptedException {
338340
validateSha256(sha256);
@@ -342,16 +344,18 @@ public void download(
342344
WorkspaceRuleEvent.newDownloadEvent(
343345
urls, output.toString(), sha256, executable, rule.getLabel().toString(), location);
344346
env.getListener().post(w);
347+
Path downloadedPath;
345348
try {
346349
checkInOutputDirectory(outputPath);
347350
makeDirectories(outputPath.getPath());
348-
httpDownloader.download(
349-
urls,
350-
sha256,
351-
Optional.<String>absent(),
352-
outputPath.getPath(),
353-
env.getListener(),
354-
osObject.getEnvironmentVariables());
351+
downloadedPath =
352+
httpDownloader.download(
353+
urls,
354+
sha256,
355+
Optional.<String>absent(),
356+
outputPath.getPath(),
357+
env.getListener(),
358+
osObject.getEnvironmentVariables());
355359
if (executable) {
356360
outputPath.getPath().setExecutable(true);
357361
}
@@ -361,10 +365,21 @@ public void download(
361365
} catch (IOException e) {
362366
throw new RepositoryFunctionException(e, Transience.TRANSIENT);
363367
}
368+
String finalSha256;
369+
try {
370+
finalSha256 = calculateSha256(sha256, downloadedPath);
371+
} catch (IOException e) {
372+
throw new RepositoryFunctionException(
373+
new IOException(
374+
"Couldn't hash downloaded file (" + downloadedPath.getPathString() + ")", e),
375+
Transience.PERSISTENT);
376+
}
377+
SkylarkDict<String, Object> dict = SkylarkDict.of(null, "sha256", finalSha256);
378+
return StructProvider.STRUCT.createStruct(dict, null);
364379
}
365380

366381
@Override
367-
public void downloadAndExtract(
382+
public StructImpl downloadAndExtract(
368383
Object url, Object output, String sha256, String type, String stripPrefix, Location location)
369384
throws RepositoryFunctionException, InterruptedException, EvalException {
370385
validateSha256(sha256);
@@ -410,6 +425,15 @@ public void downloadAndExtract(
410425
.setRepositoryPath(outputPath.getPath())
411426
.setPrefix(stripPrefix)
412427
.build());
428+
String finalSha256 = null;
429+
try {
430+
finalSha256 = calculateSha256(sha256, downloadedPath);
431+
} catch (IOException e) {
432+
throw new RepositoryFunctionException(
433+
new IOException(
434+
"Couldn't hash downloaded file (" + downloadedPath.getPathString() + ")", e),
435+
Transience.PERSISTENT);
436+
}
413437
try {
414438
if (downloadedPath.exists()) {
415439
downloadedPath.delete();
@@ -420,6 +444,16 @@ public void downloadAndExtract(
420444
"Couldn't delete temporary file (" + downloadedPath.getPathString() + ")", e),
421445
Transience.TRANSIENT);
422446
}
447+
SkylarkDict<String, Object> dict = SkylarkDict.of(null, "sha256", finalSha256);
448+
return StructProvider.STRUCT.createStruct(dict, null);
449+
}
450+
451+
private String calculateSha256(String originalSha, Path path) throws IOException {
452+
if (!Strings.isNullOrEmpty(originalSha)) {
453+
// The sha is checked on download, so if we got here, the user provided sha is good
454+
return originalSha;
455+
}
456+
return RepositoryCache.getChecksum(KeyType.SHA256, path);
423457
}
424458

425459
private static void validateSha256(String sha256) throws RepositoryFunctionException {

src/main/java/com/google/devtools/build/lib/skylarkbuildapi/repository/SkylarkRepositoryContextApi.java

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,9 @@ public SkylarkExecutionResultApi execute(
246246

247247
@SkylarkCallable(
248248
name = "download",
249-
doc = "Download a file to the output path for the provided url.",
249+
doc =
250+
"Download a file to the output path for the provided url and return the hash of"
251+
+ " the file.",
250252
useLocation = true,
251253
parameters = {
252254
@Param(
@@ -272,26 +274,27 @@ public SkylarkExecutionResultApi execute(
272274
type = String.class,
273275
defaultValue = "''",
274276
named = true,
275-
doc =
276-
"the expected SHA-256 hash of the file downloaded."
277-
+ " This must match the SHA-256 hash of the file downloaded. It is a security"
278-
+ " risk to omit the SHA-256 as remote files can change. At best omitting this"
279-
+ " field will make your build non-hermetic. It is optional to make"
280-
+ " development easier but should be set before shipping."),
277+
doc = "the expected SHA-256 hash of the file downloaded."
278+
+ " This must match the SHA-256 hash of the file downloaded. It is a security risk"
279+
+ " to omit the SHA-256 as remote files can change. At best omitting this field"
280+
+ " will make your build non-hermetic. It is optional to make development easier"
281+
+ " but should be set before shipping."),
281282
@Param(
282283
name = "executable",
283284
type = Boolean.class,
284285
defaultValue = "False",
285286
named = true,
286287
doc = "set the executable flag on the created file, false by default."),
287288
})
288-
public void download(
289+
public StructApi download(
289290
Object url, Object output, String sha256, Boolean executable, Location location)
290291
throws RepositoryFunctionExceptionT, EvalException, InterruptedException;
291292

292293
@SkylarkCallable(
293294
name = "download_and_extract",
294-
doc = "Download a file to the output path for the provided url, and extract it.",
295+
doc =
296+
"Download a file to the output path for the provided url, extract it, and return the"
297+
+ " hash of the downloaded file.",
295298
useLocation = true,
296299
parameters = {
297300
@Param(
@@ -319,27 +322,23 @@ public void download(
319322
type = String.class,
320323
defaultValue = "''",
321324
named = true,
322-
doc =
323-
"the expected SHA-256 hash of the file downloaded."
324-
+ " This must match the SHA-256 hash of the file downloaded. It is a security"
325-
+ " risk to omit the SHA-256 as remote files can change. At best omitting this"
326-
+ " field will make your build non-hermetic. It is optional to make"
327-
+ " development easier but should be set before shipping."
328-
+ " If provided, the repository cache will first be checked for a file with the"
329-
+ " given hash; a download will only be attempted, if the file was not found"
330-
+ " in the cache. After a successful download, the file will be added to the"
331-
+ " cache."),
325+
doc = "the expected SHA-256 hash of the file downloaded."
326+
+ " This must match the SHA-256 hash of the file downloaded. It is a security risk"
327+
+ " to omit the SHA-256 as remote files can change. At best omitting this field"
328+
+ " will make your build non-hermetic. It is optional to make development easier"
329+
+ " but should be set before shipping."
330+
+ " If provided, the repository cache will first be checked for a file with the"
331+
+ " given hash; a download will only be attempted, if the file was not found in the"
332+
+ " cache. After a successful download, the file will be added to the cache."),
332333
@Param(
333334
name = "type",
334335
type = String.class,
335336
defaultValue = "''",
336337
named = true,
337-
doc =
338-
"the archive type of the downloaded file."
339-
+ " By default, the archive type is determined from the file extension of the"
340-
+ " URL. If the file has no extension, you can explicitly specify either"
341-
+ " \"zip\", \"jar\", \"war\", \"tar.gz\", \"tgz\", \"tar.bz2\", or \"tar.xz\""
342-
+ " here."),
338+
doc = "the archive type of the downloaded file."
339+
+ " By default, the archive type is determined from the file extension of the URL."
340+
+ " If the file has no extension, you can explicitly specify either \"zip\","
341+
+ " \"jar\", \"war\", \"tar.gz\", \"tgz\", \"tar.bz2\", or \"tar.xz\" here."),
343342
@Param(
344343
name = "stripPrefix",
345344
type = String.class,
@@ -352,7 +351,7 @@ public void download(
352351
+ " <code>build_file</code>, this field can be used to strip it from extracted"
353352
+ " files."),
354353
})
355-
public void downloadAndExtract(
354+
public StructApi downloadAndExtract(
356355
Object url, Object output, String sha256, String type, String stripPrefix, Location location)
357356
throws RepositoryFunctionExceptionT, InterruptedException, EvalException;
358357
}

src/test/shell/bazel/skylark_repository_test.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,71 @@ EOF
851851
|| fail "download_executable_file.sh is not executable"
852852
}
853853

854+
function test_skylark_repository_context_downloads_return_struct() {
855+
# Prepare HTTP server with Python
856+
local server_dir="${TEST_TMPDIR}/server_dir"
857+
mkdir -p "${server_dir}"
858+
local download_with_sha256="${server_dir}/download_with_sha256.txt"
859+
local download_no_sha256="${server_dir}/download_no_sha256.txt"
860+
local compressed_with_sha256="${server_dir}/compressed_with_sha256.txt"
861+
local compressed_no_sha256="${server_dir}/compressed_no_sha256.txt"
862+
echo "This is one file" > "${download_no_sha256}"
863+
echo "This is another file" > "${download_with_sha256}"
864+
echo "Compressed file with sha" > "${compressed_with_sha256}"
865+
echo "Compressed file no sha" > "${compressed_no_sha256}"
866+
zip "${compressed_with_sha256}".zip "${compressed_with_sha256}"
867+
zip "${compressed_no_sha256}".zip "${compressed_no_sha256}"
868+
869+
provided_sha256="$(sha256sum "${download_with_sha256}" | head -c 64)"
870+
not_provided_sha256="$(sha256sum "${download_no_sha256}" | head -c 64)"
871+
compressed_provided_sha256="$(sha256sum "${compressed_with_sha256}.zip" | head -c 64)"
872+
compressed_not_provided_sha256="$(sha256sum "${compressed_no_sha256}.zip" | head -c 64)"
873+
874+
# Start HTTP server with Python
875+
startup_server "${server_dir}"
876+
877+
setup_skylark_repository
878+
# Our custom repository rule
879+
cat >test.bzl <<EOF
880+
def _impl(repository_ctx):
881+
no_sha_return = repository_ctx.download(
882+
url = "http://localhost:${fileserver_port}/download_no_sha256.txt",
883+
output = "download_no_sha256.txt")
884+
with_sha_return = repository_ctx.download(
885+
url = "http://localhost:${fileserver_port}/download_with_sha256.txt",
886+
output = "download_with_sha256.txt",
887+
sha256 = "${provided_sha256}")
888+
compressed_no_sha_return = repository_ctx.download_and_extract(
889+
url = "http://localhost:${fileserver_port}/compressed_no_sha256.txt.zip",
890+
output = "compressed_no_sha256.txt.zip")
891+
compressed_with_sha_return = repository_ctx.download_and_extract(
892+
url = "http://localhost:${fileserver_port}/compressed_with_sha256.txt.zip",
893+
output = "compressed_with_sha256.txt.zip",
894+
sha256 = "${compressed_provided_sha256}")
895+
896+
file_content = "no_sha_return " + no_sha_return.sha256 + "\n"
897+
file_content += "with_sha_return " + with_sha_return.sha256 + "\n"
898+
file_content += "compressed_no_sha_return " + compressed_no_sha_return.sha256 + "\n"
899+
file_content += "compressed_with_sha_return " + compressed_with_sha_return.sha256
900+
repository_ctx.file("returned_shas.txt", content = file_content, executable = False)
901+
repository_ctx.file("BUILD") # necessary directories should already created by download function
902+
repo = repository_rule(implementation = _impl, local = False)
903+
EOF
904+
905+
bazel build @foo//:all >& $TEST_log && shutdown_server \
906+
|| fail "Execution of @foo//:all failed"
907+
908+
output_base="$(bazel info output_base)"
909+
grep "no_sha_return $not_provided_sha256" $output_base/external/foo/returned_shas.txt \
910+
|| fail "expected calculated sha256 $not_provided_sha256"
911+
grep "with_sha_return $provided_sha256" $output_base/external/foo/returned_shas.txt \
912+
|| fail "expected provided sha256 $provided_sha256"
913+
grep "compressed_with_sha_return $compressed_provided_sha256" $output_base/external/foo/returned_shas.txt \
914+
|| fail "expected provided sha256 $compressed_provided_sha256"
915+
grep "compressed_no_sha_return $compressed_not_provided_sha256" $output_base/external/foo/returned_shas.txt \
916+
|| fail "expected compressed calculated sha256 $compressed_not_provided_sha256"
917+
}
918+
854919
function test_skylark_repository_download_args() {
855920
# Prepare HTTP server with Python
856921
local server_dir="${TEST_TMPDIR}/server_dir"

0 commit comments

Comments
 (0)