diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 6268e40851..7e6af7849d 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -359,6 +359,8 @@ tasks: test_targets: *default_linux_targets coverage_targets: *default_linux_targets post_shell_commands: *coverage_validation_post_shell_commands + run_targets: + - //test:build_script_path_mapping_test_binary linux_docs: name: Docs platform: ubuntu2204 diff --git a/cargo/private/BUILD.bazel b/cargo/private/BUILD.bazel index 1c66efaffc..a2e8b6bc05 100644 --- a/cargo/private/BUILD.bazel +++ b/cargo/private/BUILD.bazel @@ -1,6 +1,24 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("//rust:defs.bzl", "rust_binary") +config_setting( + name = "compilation_mode_dbg", + values = {"compilation_mode": "dbg"}, + visibility = ["//visibility:public"], +) + +config_setting( + name = "compilation_mode_fastbuild", + values = {"compilation_mode": "fastbuild"}, + visibility = ["//visibility:public"], +) + +config_setting( + name = "compilation_mode_opt", + values = {"compilation_mode": "opt"}, + visibility = ["//visibility:public"], +) + rust_binary( name = "copy_file", srcs = ["copy_file.rs"], @@ -10,9 +28,9 @@ rust_binary( bzl_library( name = "bzl_lib", + srcs = glob(["**/*.bzl"]), + visibility = ["//visibility:public"], deps = [ "//rust:bzl_lib", ], - srcs = glob(["**/*.bzl"]), - visibility = ["//visibility:public"], ) diff --git a/cargo/private/cargo_build_script.bzl b/cargo/private/cargo_build_script.bzl index b4665cbe3a..304b8ab4b7 100644 --- a/cargo/private/cargo_build_script.bzl +++ b/cargo/private/cargo_build_script.bzl @@ -5,7 +5,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("@rules_cc//cc:action_names.bzl", "ACTION_NAMES") load("@rules_cc//cc/common:cc_common.bzl", "cc_common") load("//rust:defs.bzl", "rust_common") -load("//rust:rust_common.bzl", "BuildInfo", "CrateGroupInfo", "DepInfo") +load("//rust:rust_common.bzl", "BuildInfo", "DepInfo") # buildifier: disable=bzl-visibility load( @@ -356,6 +356,7 @@ def _cargo_build_script_impl(ctx): list: A list containing a BuildInfo provider """ script = ctx.executable.script + script_target = ctx.attr.script[0] if type(ctx.attr.script) == "list" else ctx.attr.script toolchain = find_toolchain(ctx) out_dir = ctx.actions.declare_directory(ctx.label.name + ".out_dir") env_out = ctx.actions.declare_file(ctx.label.name + ".env") @@ -382,7 +383,7 @@ def _cargo_build_script_impl(ctx): # https://github.com/bazelbuild/bazel/issues/15486 runfiles_dir, runfiles_inputs, runfiles_args = _create_runfiles_dir( ctx = ctx, - script = ctx.attr.script, + script = script_target, data_runfiles = ctx.attr.data_runfiles, retain_list = ctx.attr._cargo_manifest_dir_filename_suffixes_to_retain[BuildSettingInfo].value, workspace_name = workspace_name, @@ -594,20 +595,8 @@ def _cargo_build_script_impl(ctx): for dep_build_info in dep[rust_common.dep_info].transitive_build_infos.to_list(): build_script_inputs.append(dep_build_info.out_dir) - for dep in ctx.attr.deps: - dep_infos = [] - if DepInfo in dep: - dep_infos = [dep[DepInfo]] - else: - dep_infos = [ - dep_variant_info.dep_info - for dep_variant_info in dep[CrateGroupInfo].dep_variant_infos.to_list() - if dep_variant_info.dep_info - ] - - for dep_info in dep_infos: - for dep_build_info in dep_info.transitive_build_infos.to_list(): - build_script_inputs.append(dep_build_info.out_dir) + for dep_build_info in script_target[DepInfo].transitive_build_infos.to_list(): + build_script_inputs.append(dep_build_info.out_dir) experimental_symlink_execroot = ctx.attr._experimental_symlink_execroot[BuildSettingInfo].value or \ _feature_enabled(ctx, "symlink-exec-root") @@ -632,7 +621,7 @@ def _cargo_build_script_impl(ctx): runfiles_dir, ] + extra_output, tools = [ - ctx.attr.script[DefaultInfo].files_to_run, + script_target[DefaultInfo].files_to_run, tools, ], inputs = depset(build_script_inputs, transitive = [runfiles_inputs]), @@ -663,6 +652,27 @@ def _cargo_build_script_impl(ctx): ), ] +_COMPILATION_MODE = "//command_line_option:compilation_mode" + +def _cargo_build_script_compilation_mode_transition_impl(_settings, attr): + return { + _COMPILATION_MODE: attr.target_compilation_mode, + } + +_cargo_build_script_compilation_mode_transition = transition( + implementation = _cargo_build_script_compilation_mode_transition_impl, + inputs = [], + outputs = [_COMPILATION_MODE], +) + +def _get_cargo_build_script_cfg(): + build_script_cfg = config.exec() + if hasattr(build_script_cfg, "and_then"): + build_script_cfg = build_script_cfg.and_then(_cargo_build_script_compilation_mode_transition) + return build_script_cfg + +_cargo_build_script_cfg = _get_cargo_build_script_cfg() + cargo_build_script = rule( doc = ( "A rule for running a crate's `build.rs` files to generate build information " + @@ -709,11 +719,6 @@ cargo_build_script = rule( cfg = "target", executable = True, ), - "deps": attr.label_list( - doc = "The Rust build-dependencies of the crate", - providers = [[DepInfo], [CrateGroupInfo]], - cfg = "exec", - ), "link_deps": attr.label_list( doc = dedent("""\ The subset of the Rust (normal) dependencies of the crate that @@ -741,14 +746,19 @@ cargo_build_script = rule( doc = "The binary script to run, generally a `rust_binary` target.", executable = True, mandatory = True, - cfg = "exec", - providers = [rust_common.crate_info], + cfg = _cargo_build_script_cfg, + providers = [rust_common.crate_info, DepInfo], ), "tools": attr.label_list( doc = "Tools required by the build script.", allow_files = True, cfg = "exec", ), + "target_compilation_mode": attr.string( + doc = "The parent target's compilation mode.", + mandatory = True, + values = ["dbg", "fastbuild", "opt"], + ), "use_default_shell_env": attr.int( doc = dedent("""\ Whether or not to include the default shell environment for the build diff --git a/cargo/private/cargo_build_script_wrapper.bzl b/cargo/private/cargo_build_script_wrapper.bzl index 995323b7c1..5200ba12c2 100644 --- a/cargo/private/cargo_build_script_wrapper.bzl +++ b/cargo/private/cargo_build_script_wrapper.bzl @@ -9,6 +9,12 @@ load( ) load("//rust:defs.bzl", "rust_binary") +_TARGET_COMPILATION_MODE = select({ + str(Label("//cargo/private:compilation_mode_dbg")): "dbg", + str(Label("//cargo/private:compilation_mode_fastbuild")): "fastbuild", + str(Label("//cargo/private:compilation_mode_opt")): "opt", +}) + def cargo_build_script( *, name, @@ -213,6 +219,7 @@ def cargo_build_script( data_runfiles = ":{}-".format(name), data = data, tools = tools, + target_compilation_mode = _TARGET_COMPILATION_MODE, crate_features = crate_features, version = version, allow_build_script_to_detect_nonhermetic_paths = allow_build_script_to_detect_nonhermetic_paths, @@ -220,7 +227,6 @@ def cargo_build_script( build_script_env_files = build_script_env_files, use_default_shell_env = sanitized_use_default_shell_env, links = links, - deps = deps, link_deps = link_deps, rustc_flags = rustc_flags, visibility = visibility, diff --git a/cargo/tests/unit/build_script_deps/build_script_deps.bzl b/cargo/tests/unit/build_script_deps/build_script_deps.bzl index a445ae54ee..a6b0f8b9d7 100644 --- a/cargo/tests/unit/build_script_deps/build_script_deps.bzl +++ b/cargo/tests/unit/build_script_deps/build_script_deps.bzl @@ -12,13 +12,19 @@ DepActionsInfo = provider( def _collect_dep_actions_aspect_impl(target, ctx): actions = [] actions.extend(target.actions) - for dep in ctx.rule.attr.deps: - actions.extend(dep[DepActionsInfo].actions) + attr_name = "script" if hasattr(ctx.rule.attr, "script") else "deps" + if hasattr(ctx.rule.attr, attr_name): + deps = getattr(ctx.rule.attr, attr_name) + if type(deps) != "list": + deps = [deps] + for dep in deps: + if DepActionsInfo in dep: + actions.extend(dep[DepActionsInfo].actions) return [DepActionsInfo(actions = actions)] collect_dep_actions_aspect = aspect( implementation = _collect_dep_actions_aspect_impl, - attr_aspects = ["deps"], + attr_aspects = ["deps", "script"], ) def _outputs_contain(outputs, substring): @@ -47,6 +53,14 @@ def _build_script_deps_test_impl(ctx): ("-exec-" in rlib_output.path) or ("-exec/bin/" in rlib_output.path), "Expected rlib output to be in an exec configuration, but got: {}".format(rlib_output.path), ) + if hasattr(config.exec(), "and_then"): + asserts.true( + env, + "--codegen=opt-level=0" in build_script_deps_action.argv, + "Expected build script dependencies to use fastbuild compilation mode, but got: {}".format( + build_script_deps_action.argv, + ), + ) return analysistest.end(env) build_script_deps_test = analysistest.make( @@ -78,6 +92,12 @@ def build_script_test_suite(name): target_under_test = ":build_script_deps_in_exec_mode", ) + if hasattr(config.exec(), "and_then"): + native.filegroup( + name = "transition_and_then_supported", + tags = ["manual"], + ) + native.test_suite( name = name, tests = ["build_script_deps_in_exec_mode_test"], diff --git a/test/BUILD.bazel b/test/BUILD.bazel index a4ac9507e4..231564d6a1 100644 --- a/test/BUILD.bazel +++ b/test/BUILD.bazel @@ -1,5 +1,11 @@ load("@rules_shell//shell:sh_binary.bzl", "sh_binary") +sh_binary( + name = "build_script_path_mapping_test_binary", + srcs = ["build_script_path_mapping_test.sh"], + data = ["build_script_path_mapping_test.py"], +) + sh_binary( name = "query_test_binary", srcs = ["query_test_binary.sh"], diff --git a/test/build_script_path_mapping_test.py b/test/build_script_path_mapping_test.py new file mode 100644 index 0000000000..f736ce23f7 --- /dev/null +++ b/test/build_script_path_mapping_test.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 + +import json +import sys + + +CRATE_NAME_ARGUMENT = "--crate-name=dep_of_a_build_script" + + +def path_for_fragment(path_fragments, fragment_id): + labels = [] + while fragment_id: + fragment = path_fragments[fragment_id] + labels.append(fragment["label"]) + fragment_id = fragment.get("parentId") + return "/".join(reversed(labels)) + + +def artifact_paths(action_graph, artifact_ids): + artifacts = {artifact["id"]: artifact for artifact in action_graph["artifacts"]} + path_fragments = { + fragment["id"]: fragment + for fragment in action_graph["pathFragments"] + } + return sorted( + path_for_fragment(path_fragments, artifacts[artifact_id]["pathFragmentId"]) + for artifact_id in artifact_ids + ) + + +def strip_config_segment(path): + segments = path.split("/") + if len(segments) > 2 and segments[0] in ("bazel-out", "blaze-out"): + segments[1] = "cfg" + return "/".join(segments) + + +def input_paths(action_graph, dep_set_ids): + dep_sets = {dep_set["id"]: dep_set for dep_set in action_graph["depSetOfFiles"]} + artifact_ids = set() + pending = list(dep_set_ids) + while pending: + dep_set = dep_sets[pending.pop()] + artifact_ids.update(dep_set.get("directArtifactIds", [])) + pending.extend(dep_set.get("transitiveDepSetIds", [])) + return artifact_paths(action_graph, artifact_ids) + + +def normalized_action(action_graph, action): + # aquery's actionKey preserves the unmodified configuration segments. Compare + # the fields used to construct the path-mapped spawn instead. + return { + "arguments": action.get("arguments", []), + "environmentVariables": sorted( + action.get("environmentVariables", []), + key=lambda variable: (variable["key"], variable.get("value", "")), + ), + "executionInfo": sorted( + action.get("executionInfo", []), + key=lambda entry: (entry["key"], entry.get("value", "")), + ), + "executionPlatform": action.get("executionPlatform"), + "inputs": sorted( + strip_config_segment(path) + for path in input_paths(action_graph, action.get("inputDepSetIds", [])) + ), + "outputs": sorted( + strip_config_segment(path) + for path in artifact_paths(action_graph, action.get("outputIds", [])) + ), + "paramFiles": action.get("paramFiles", []), + } + + +def process_wrapper_inputs(action_graph, action): + return [ + path + for path in input_paths(action_graph, action.get("inputDepSetIds", [])) + if path.endswith("/util/process_wrapper/process_wrapper") + ] + + +def fail(message, details=None): + if details is not None: + message += "\n" + json.dumps(details, indent=2, sort_keys=True) + raise AssertionError(message) + + +def main(): + action_graph = json.load(sys.stdin) + crate_actions = [ + action + for action in action_graph.get("actions", []) + if CRATE_NAME_ARGUMENT in action.get("arguments", []) + ] + # rust_binary adds each dependency to deps and proc_macro_deps so filter_deps + # can select the dependency with the required provider. The proc_macro_deps + # action remains in aquery but is not an input to the build script binary. + actions = [ + action + for action in crate_actions + if "--codegen=opt-level=0" in action.get("arguments", []) + ] + if len(actions) != 2: + fail("Expected two fastbuild dep_of_a_build_script Rustc actions", crate_actions) + + configuration_ids = {action.get("configurationId") for action in actions} + if len(configuration_ids) != 2: + fail("Expected target and exec configurations", actions) + + normalized_actions = [ + normalized_action(action_graph, action) + for action in actions + ] + execution_info = normalized_actions[0]["executionInfo"] + if not any(entry["key"] == "supports-path-mapping" for entry in execution_info): + fail("Expected supports-path-mapping execution info", execution_info) + + if normalized_actions[0] != normalized_actions[1]: + fail( + "Expected identical path-mapped target and build-script dependency spawns", + normalized_actions, + ) + + process_wrappers = [ + process_wrapper_inputs(action_graph, action) + for action in actions + ] + if process_wrappers[0] != process_wrappers[1]: + fail( + "Expected target and build-script dependencies to use the same process_wrapper", + process_wrappers, + ) + + +if __name__ == "__main__": + main() diff --git a/test/build_script_path_mapping_test.sh b/test/build_script_path_mapping_test.sh new file mode 100755 index 0000000000..2f3ef08657 --- /dev/null +++ b/test/build_script_path_mapping_test.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +cd "${BUILD_WORKSPACE_DIRECTORY}" + +bazel_command=("${BAZEL:-bazel}") +if [[ -n "${BAZEL_OUTPUT_BASE:-}" ]]; then + bazel_command+=("--output_base=${BAZEL_OUTPUT_BASE}") +fi + +if ! "${bazel_command[@]}" query //cargo/tests/unit/build_script_deps:transition_and_then_supported >/dev/null 2>&1; then + exit 0 +fi + +"${bazel_command[@]}" aquery \ + --compilation_mode=fastbuild \ + --experimental_output_paths=strip \ + --include_commandline \ + --include_param_files \ + --output=jsonproto \ + 'mnemonic("Rustc", deps(set(//cargo/tests/unit/build_script_deps:dep_of_a_build_script //cargo/tests/unit/build_script_deps:build_script_deps_in_exec_mode)))' \ + | python3 test/build_script_path_mapping_test.py