Third cl for verbose workspaces (ability to log certain potentially non-hermetic events that happen as part of repository rules).
Adding logging for download and download_and_extract
In the future:
- Add more event types
- Allowing to specify log file rather than dumping to INFO
- Log levels, full or alerts only
RELNOTES: None
PiperOrigin-RevId: 209789459
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleEvent.java b/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleEvent.java
index 38c9da3..2ff1bb5 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleEvent.java
@@ -16,6 +16,8 @@
import com.google.devtools.build.lib.bazel.debug.proto.WorkspaceLogProtos;
import com.google.devtools.build.lib.events.ExtendedEventHandler.ProgressLike;
import com.google.devtools.build.lib.events.Location;
+import java.net.URL;
+import java.util.List;
import java.util.Map;
/** An event to record events happening during workspace rule resolution */
@@ -32,8 +34,6 @@
/**
* Creates a new WorkspaceRuleEvent for an execution event.
- *
- * <p>Note: we will add more granular information as needed.
*/
public static WorkspaceRuleEvent newExecuteEvent(
Iterable<Object> args,
@@ -73,6 +73,66 @@
return new WorkspaceRuleEvent(result.build());
}
+ /** Creates a new WorkspaceRuleEvent for a download event. */
+ public static WorkspaceRuleEvent newDownloadEvent(
+ List<URL> urls,
+ String output,
+ String sha256,
+ Boolean executable,
+ String ruleLabel,
+ Location location) {
+ WorkspaceLogProtos.DownloadEvent.Builder e =
+ WorkspaceLogProtos.DownloadEvent.newBuilder()
+ .setOutput(output)
+ .setSha256(sha256)
+ .setExecutable(executable);
+ for (URL u : urls) {
+ e.addUrl(u.toString());
+ }
+
+ WorkspaceLogProtos.WorkspaceEvent.Builder result =
+ WorkspaceLogProtos.WorkspaceEvent.newBuilder();
+ result = result.setDownloadEvent(e.build());
+ if (location != null) {
+ result = result.setLocation(location.print());
+ }
+ if (ruleLabel != null) {
+ result = result.setRule(ruleLabel);
+ }
+ return new WorkspaceRuleEvent(result.build());
+ }
+
+ /** Creates a new WorkspaceRuleEvent for a download event. */
+ public static WorkspaceRuleEvent newDownloadAndExtractEvent(
+ List<URL> urls,
+ String output,
+ String sha256,
+ String type,
+ String stripPrefix,
+ String ruleLabel,
+ Location location) {
+ WorkspaceLogProtos.DownloadAndExtractEvent.Builder e =
+ WorkspaceLogProtos.DownloadAndExtractEvent.newBuilder()
+ .setOutput(output)
+ .setSha256(sha256)
+ .setType(type)
+ .setStripPrefix(stripPrefix);
+ for (URL u : urls) {
+ e.addUrl(u.toString());
+ }
+
+ WorkspaceLogProtos.WorkspaceEvent.Builder result =
+ WorkspaceLogProtos.WorkspaceEvent.newBuilder();
+ result = result.setDownloadAndExtractEvent(e.build());
+ if (location != null) {
+ result = result.setLocation(location.print());
+ }
+ if (ruleLabel != null) {
+ result = result.setRule(ruleLabel);
+ }
+ return new WorkspaceRuleEvent(result.build());
+ }
+
/*
* @return a message to log for this event
*/
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/debug/workspace_log.proto b/src/main/java/com/google/devtools/build/lib/bazel/debug/workspace_log.proto
index 1dea56a..edab4fa 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/debug/workspace_log.proto
+++ b/src/main/java/com/google/devtools/build/lib/bazel/debug/workspace_log.proto
@@ -38,6 +38,31 @@
string output_directory = 6;
}
+// Information on "Download" event in repository_ctx.
+message DownloadEvent {
+ // Url to download from. If multiple, treated as mirrors
+ repeated string url = 1;
+ // Output file
+ string output = 2;
+ // sha256, if speficied
+ string sha256 = 3;
+ // whether to make the resulting file executable
+ bool executable = 4;
+}
+
+message DownloadAndExtractEvent {
+ // Url(s) to download from
+ repeated string url = 1;
+ // Output file
+ string output = 2;
+ // sha256, if specified
+ string sha256 = 3;
+ // Archive type, if specified. Otherwise, inferred from URL.
+ string type = 4;
+ // A directory prefix to strip from extracted files.
+ string strip_prefix = 5;
+}
+
message WorkspaceEvent {
// Location in the code (.bzl file) where the event originates.
string location = 1;
@@ -47,5 +72,7 @@
oneof event {
ExecuteEvent execute_event = 3;
+ DownloadEvent download_event = 4;
+ DownloadAndExtractEvent download_and_extract_event = 5;
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
index a618eae..0aaebe1 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
@@ -298,11 +298,16 @@
}
@Override
- public void download(Object url, Object output, String sha256, Boolean executable)
+ public void download(
+ Object url, Object output, String sha256, Boolean executable, Location location)
throws RepositoryFunctionException, EvalException, InterruptedException {
validateSha256(sha256);
List<URL> urls = getUrls(url);
SkylarkPath outputPath = getPath("download()", output);
+ WorkspaceRuleEvent w =
+ WorkspaceRuleEvent.newDownloadEvent(
+ urls, output.toString(), sha256, executable, rule.getLabel().toString(), location);
+ env.getListener().post(w);
try {
checkInOutputDirectory(outputPath);
makeDirectories(outputPath.getPath());
@@ -326,11 +331,22 @@
@Override
public void downloadAndExtract(
- Object url, Object output, String sha256, String type, String stripPrefix)
+ Object url, Object output, String sha256, String type, String stripPrefix, Location location)
throws RepositoryFunctionException, InterruptedException, EvalException {
validateSha256(sha256);
List<URL> urls = getUrls(url);
+ WorkspaceRuleEvent w =
+ WorkspaceRuleEvent.newDownloadAndExtractEvent(
+ urls,
+ output.toString(),
+ sha256,
+ type,
+ stripPrefix,
+ rule.getLabel().toString(),
+ location);
+ env.getListener().post(w);
+
// Download to outputDirectory and delete it after extraction
SkylarkPath outputPath = getPath("download_and_extract()", output);
checkInOutputDirectory(outputPath);
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/repository/SkylarkRepositoryContextApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/repository/SkylarkRepositoryContextApi.java
index 698f307..900fe0a 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/repository/SkylarkRepositoryContextApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/repository/SkylarkRepositoryContextApi.java
@@ -251,120 +251,114 @@
public RepositoryPathApi<?> which(String program) throws EvalException;
@SkylarkCallable(
- name = "download",
- doc = "Download a file to the output path for the provided url.",
- parameters = {
- @Param(
- name = "url",
- allowedTypes = {
- @ParamType(type = String.class),
- @ParamType(type = Iterable.class, generic1 = String.class),
- },
- named = true,
- doc = "List of mirror URLs referencing the same file."
- ),
- @Param(
- name = "output",
- allowedTypes = {
- @ParamType(type = String.class),
- @ParamType(type = Label.class),
- @ParamType(type = RepositoryPathApi.class)
- },
- defaultValue = "''",
- named = true,
- doc = "path to the output file, relative to the repository directory."
- ),
- @Param(
- name = "sha256",
- type = String.class,
- defaultValue = "''",
- named = true,
- doc =
- "the expected SHA-256 hash of the file downloaded."
- + " This must match the SHA-256 hash of the file downloaded. It is a security risk"
- + " to omit the SHA-256 as remote files can change. At best omitting this field"
- + " will make your build non-hermetic. It is optional to make development easier"
- + " but should be set before shipping."
- ),
- @Param(
- name = "executable",
- type = Boolean.class,
- defaultValue = "False",
- named = true,
- doc = "set the executable flag on the created file, false by default."
- ),
- }
- )
- public void download(Object url, Object output, String sha256, Boolean executable)
+ name = "download",
+ doc = "Download a file to the output path for the provided url.",
+ useLocation = true,
+ parameters = {
+ @Param(
+ name = "url",
+ allowedTypes = {
+ @ParamType(type = String.class),
+ @ParamType(type = Iterable.class, generic1 = String.class),
+ },
+ named = true,
+ doc = "List of mirror URLs referencing the same file."),
+ @Param(
+ name = "output",
+ allowedTypes = {
+ @ParamType(type = String.class),
+ @ParamType(type = Label.class),
+ @ParamType(type = RepositoryPathApi.class)
+ },
+ defaultValue = "''",
+ named = true,
+ doc = "path to the output file, relative to the repository directory."),
+ @Param(
+ name = "sha256",
+ type = String.class,
+ defaultValue = "''",
+ named = true,
+ doc =
+ "the expected SHA-256 hash of the file downloaded."
+ + " This must match the SHA-256 hash of the file downloaded. It is a security"
+ + " risk to omit the SHA-256 as remote files can change. At best omitting this"
+ + " field will make your build non-hermetic. It is optional to make"
+ + " development easier but should be set before shipping."),
+ @Param(
+ name = "executable",
+ type = Boolean.class,
+ defaultValue = "False",
+ named = true,
+ doc = "set the executable flag on the created file, false by default."),
+ })
+ public void download(
+ Object url, Object output, String sha256, Boolean executable, Location location)
throws RepositoryFunctionExceptionT, EvalException, InterruptedException;
@SkylarkCallable(
- name = "download_and_extract",
- doc = "Download a file to the output path for the provided url, and extract it.",
- parameters = {
- @Param(
- name = "url",
- allowedTypes = {
- @ParamType(type = String.class),
- @ParamType(type = Iterable.class, generic1 = String.class),
- },
- named = true,
- doc = "List of mirror URLs referencing the same file."
- ),
- @Param(
- name = "output",
- allowedTypes = {
- @ParamType(type = String.class),
- @ParamType(type = Label.class),
- @ParamType(type = RepositoryPathApi.class)
- },
- defaultValue = "''",
- named = true,
- doc =
- "path to the directory where the archive will be unpacked,"
- + " relative to the repository directory."
- ),
- @Param(
- name = "sha256",
- type = String.class,
- defaultValue = "''",
- named = true,
- doc =
- "the expected SHA-256 hash of the file downloaded."
- + " This must match the SHA-256 hash of the file downloaded. It is a security risk"
- + " to omit the SHA-256 as remote files can change. At best omitting this field"
- + " will make your build non-hermetic. It is optional to make development easier"
- + " but should be set before shipping."
- + " If provided, the repository cache will first be checked for a file with the"
- + " given hash; a download will only be attempted, if the file was not found in the"
- + " cache. After a successful download, the file will be added to the cache."
- ),
- @Param(
- name = "type",
- type = String.class,
- defaultValue = "''",
- named = true,
- doc =
- "the archive type of the downloaded file."
- + " By default, the archive type is determined from the file extension of the URL."
- + " If the file has no extension, you can explicitly specify either \"zip\","
- + " \"jar\", \"war\", \"tar.gz\", \"tgz\", \"tar.bz2\", or \"tar.xz\" here."
- ),
- @Param(
- name = "stripPrefix",
- type = String.class,
- defaultValue = "''",
- named = true,
- doc =
- "a directory prefix to strip from the extracted files."
- + "\nMany archives contain a top-level directory that contains all files in the"
- + " archive. Instead of needing to specify this prefix over and over in the"
- + " <code>build_file</code>, this field can be used to strip it from extracted"
- + " files."
- ),
- }
- )
+ name = "download_and_extract",
+ doc = "Download a file to the output path for the provided url, and extract it.",
+ useLocation = true,
+ parameters = {
+ @Param(
+ name = "url",
+ allowedTypes = {
+ @ParamType(type = String.class),
+ @ParamType(type = Iterable.class, generic1 = String.class),
+ },
+ named = true,
+ doc = "List of mirror URLs referencing the same file."),
+ @Param(
+ name = "output",
+ allowedTypes = {
+ @ParamType(type = String.class),
+ @ParamType(type = Label.class),
+ @ParamType(type = RepositoryPathApi.class)
+ },
+ defaultValue = "''",
+ named = true,
+ doc =
+ "path to the directory where the archive will be unpacked,"
+ + " relative to the repository directory."),
+ @Param(
+ name = "sha256",
+ type = String.class,
+ defaultValue = "''",
+ named = true,
+ doc =
+ "the expected SHA-256 hash of the file downloaded."
+ + " This must match the SHA-256 hash of the file downloaded. It is a security"
+ + " risk to omit the SHA-256 as remote files can change. At best omitting this"
+ + " field will make your build non-hermetic. It is optional to make"
+ + " development easier but should be set before shipping."
+ + " If provided, the repository cache will first be checked for a file with the"
+ + " given hash; a download will only be attempted, if the file was not found"
+ + " in the cache. After a successful download, the file will be added to the"
+ + " cache."),
+ @Param(
+ name = "type",
+ type = String.class,
+ defaultValue = "''",
+ named = true,
+ doc =
+ "the archive type of the downloaded file."
+ + " By default, the archive type is determined from the file extension of the"
+ + " URL. If the file has no extension, you can explicitly specify either"
+ + " \"zip\", \"jar\", \"war\", \"tar.gz\", \"tgz\", \"tar.bz2\", or \"tar.xz\""
+ + " here."),
+ @Param(
+ name = "stripPrefix",
+ type = String.class,
+ defaultValue = "''",
+ named = true,
+ doc =
+ "a directory prefix to strip from the extracted files."
+ + "\nMany archives contain a top-level directory that contains all files in the"
+ + " archive. Instead of needing to specify this prefix over and over in the"
+ + " <code>build_file</code>, this field can be used to strip it from extracted"
+ + " files."),
+ })
public void downloadAndExtract(
- Object url, Object output, String sha256, String type, String stripPrefix)
+ Object url, Object output, String sha256, String type, String stripPrefix, Location location)
throws RepositoryFunctionExceptionT, InterruptedException, EvalException;
}
diff --git a/src/test/shell/bazel/bazel_workspaces_test.sh b/src/test/shell/bazel/bazel_workspaces_test.sh
index 75ebc78..efad56e 100755
--- a/src/test/shell/bazel/bazel_workspaces_test.sh
+++ b/src/test/shell/bazel/bazel_workspaces_test.sh
@@ -20,15 +20,17 @@
source "${CURRENT_DIR}/../integration_test_setup.sh" \
|| { echo "integration_test_setup.sh not found!" >&2; exit 1; }
+source "${CURRENT_DIR}/remote_helpers.sh" \
+ || { echo "remote_helpers.sh not found!" >&2; exit 1; }
function test_execute() {
create_new_workspace
- cat > BUILD <<EOF
+ cat > BUILD <<'EOF'
genrule(
name="test",
srcs=["@repo//:t.txt"],
outs=["out.txt"],
- cmd="echo Result > \$(location out.txt)"
+ cmd="echo Result > $(location out.txt)"
)
EOF
cat >> repos.bzl <<EOF
@@ -58,7 +60,7 @@
# Cached executions are not replayed
bazel build //:test --experimental_workspace_rules_logging=yes &> output || fail "could not build //:test"
- cat output &> $TEST_log
+ cat output >> $TEST_log
executes=`grep "location: .*repos.bzl:2:3" output | wc -l`
if [ "$executes" -ne "0" ]
then
@@ -68,12 +70,12 @@
function test_reexecute() {
create_new_workspace
- cat > BUILD <<EOF
+ cat > BUILD <<'EOF'
genrule(
name="test",
srcs=["@repo//:t.txt"],
outs=["out.txt"],
- cmd="echo Result > \$(location out.txt)"
+ cmd="echo Result > $(location out.txt)"
)
EOF
cat >> repos.bzl <<EOF
@@ -114,4 +116,131 @@
fi
}
+# Sets up a workspace with the given commands inserted into the repository rule
+# that will be executed when doing bazel build //:test
+function set_workspace_command() {
+ create_new_workspace
+ cat > BUILD <<'EOF'
+genrule(
+ name="test",
+ srcs=["@repo//:t.txt"],
+ outs=["out.txt"],
+ cmd="echo Result > $(location out.txt)"
+)
+EOF
+ cat >> repos.bzl <<EOF
+def _executeMe(repository_ctx):
+ $1
+ build_contents = "package(default_visibility = ['//visibility:public'])\n\n"
+ build_contents += "exports_files([\"t.txt\"])\n"
+ repository_ctx.file("BUILD", build_contents, False)
+ repository_ctx.file("t.txt", "HELLO!\n", False)
+
+ex_repo = repository_rule(
+ implementation = _executeMe,
+ local = True,
+)
+EOF
+ cat >> WORKSPACE <<EOF
+load("//:repos.bzl", "ex_repo")
+ex_repo(name = "repo")
+EOF
+}
+
+# Ensure details of the specific functions are present
+function test_execute2() {
+ set_workspace_command 'repository_ctx.execute(["echo", "test_contents"], 21, {"Arg1": "Val1"}, True)'
+
+ bazel build //:test --experimental_workspace_rules_logging=yes &> ${TEST_log} || fail "could not build //:test\n"
+ expect_log "location: .*repos.bzl:2:3"
+ expect_log "arguments: \"echo\""
+ expect_log "arguments: \"test_contents\""
+ expect_log "timeout_seconds: 21"
+ expect_log "quiet: true"
+ expect_log "key: \"Arg1\""
+ expect_log "value: \"Val1\""
+ expect_log "rule: \"//external:repo\""
+}
+
+
+function test_download() {
+ # Prepare HTTP server with Python
+ local server_dir="${TEST_TMPDIR}/server_dir"
+ mkdir -p "${server_dir}"
+ local file="${server_dir}/file.txt"
+ echo "file contents here" > "${file}"
+ file_sha256="$(sha256sum "${file}" | head -c 64)"
+
+ # Start HTTP server with Python
+ startup_server "${server_dir}"
+
+ set_workspace_command "repository_ctx.download(\"http://localhost:${fileserver_port}/file.txt\", \"file.txt\", \"${file_sha256}\")"
+
+ bazel build //:test --experimental_workspace_rules_logging=yes &> ${TEST_log} && shutdown_server || fail "could not build //:test\n"
+ expect_log "location: .*repos.bzl:2:3"
+ expect_log "rule: \"//external:repo\""
+ expect_log "download_event"
+ expect_log "url: \"http://localhost:${fileserver_port}/file.txt\""
+ expect_log "output: \"file.txt\""
+ expect_log "sha256: \"${file_sha256}\""
+}
+
+function test_download_multiple() {
+ # Prepare HTTP server with Python
+ local server_dir="${TEST_TMPDIR}/server_dir"
+ mkdir -p "${server_dir}"
+ local file2="${server_dir}/file2.txt"
+ echo "second contents here" > "${file2}"
+
+ # Start HTTP server with Python
+ startup_server "${server_dir}"
+
+ set_workspace_command "repository_ctx.download([\"http://localhost:${fileserver_port}/file1.txt\",\"http://localhost:${fileserver_port}/file2.txt\"], \"out_for_list.txt\")"
+
+ bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log && shutdown_server || fail "could not build //:test\n"
+ expect_log "location: .*repos.bzl:2:3"
+ expect_log "rule: \"//external:repo\""
+ expect_log "download_event"
+ expect_log "url: \"http://localhost:${fileserver_port}/file1.txt\""
+ expect_log "url: \"http://localhost:${fileserver_port}/file2.txt\""
+ expect_log "output: \"out_for_list.txt\""
+}
+
+function test_download_and_extract() {
+ # Prepare HTTP server with Python
+ local server_dir="${TEST_TMPDIR}/server_dir"
+ mkdir -p "${server_dir}"
+ local file_prefix="${server_dir}/download_and_extract"
+
+ pushd ${TEST_TMPDIR}
+ echo "This is one file" > server_dir/download_and_extract.txt
+ zip -r server_dir/download_and_extract.zip server_dir
+ file_sha256="$(sha256sum server_dir/download_and_extract.zip | head -c 64)"
+ popd
+
+ # Start HTTP server with Python
+ startup_server "${server_dir}"
+
+ set_workspace_command "repository_ctx.download_and_extract(\"http://localhost:${fileserver_port}/download_and_extract.zip\", \"out_dir\", \"${file_sha256}\", \"zip\", \"server_dir/\")"
+
+ bazel build //:test --experimental_workspace_rules_logging=yes &> ${TEST_log} && shutdown_server || fail "could not build //:test\n"
+
+ expect_log "location: .*repos.bzl:2:3"
+ expect_log "rule: \"//external:repo\""
+ expect_log "download_and_extract_event"
+ expect_log "url: \"http://localhost:${fileserver_port}/download_and_extract.zip\""
+ expect_log "output: \"out_dir\""
+ expect_log "sha256: \"${file_sha256}\""
+ expect_log "type: \"zip\""
+ expect_log "strip_prefix: \"server_dir/\""
+}
+
+function tear_down() {
+ shutdown_server
+ if [ -d "${TEST_TMPDIR}/server_dir" ]; then
+ rm -fr "${TEST_TMPDIR}/server_dir"
+ fi
+ true
+}
+
run_suite "workspaces_tests"