diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 3b3990d6a1..c51dedf5bc 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -15,9 +15,6 @@ default_windows_targets: &default_windows_targets - "-//test/proto/..." - "-//tools/rust_analyzer/..." - "-//test/rustfmt/..." - # rust_doc_test targets are currently broken on windows - # see: https://github.com/bazelbuild/rules_rust/issues/887 - - "-//test/chained_direct_deps:mod3_doc_test" tasks: ubuntu2004: build_targets: *default_linux_targets @@ -33,8 +30,6 @@ tasks: - "//..." - "//test/..." - "-//test/conflicting_deps:conflicting_deps_test" - # rust_doc_test is likely not fully sandboxed - - "-//test/chained_direct_deps:mod3_doc_test" macos: build_targets: *default_macos_targets test_targets: *default_macos_targets @@ -64,8 +59,6 @@ tasks: - "..." - "//test/..." - "-//test/conflicting_deps:conflicting_deps_test" - # rust_doc_test is likely not fully sandboxed - - "-//test/chained_direct_deps:mod3_doc_test" build_flags: *aspects_flags rbe_ubuntu1604_rolling_with_aspects: name: RBE Rolling Bazel Version With Aspects @@ -79,8 +72,6 @@ tasks: - "..." - "//test/..." - "-//test/conflicting_deps:conflicting_deps_test" - # rust_doc_test is likely not fully sandboxed - - "-//test/chained_direct_deps:mod3_doc_test" build_flags: *aspects_flags soft_fail: yes bazel: "rolling" @@ -209,9 +200,6 @@ tasks: - "//..." # TODO: This requires an updated `rules_foreign_cc` - "-//sys/..." - # rust_doc_test is likely not fully sandboxed - - "-//fibonacci:fibonacci_doc_test" - - "-//hello_lib:hello_lib_doc_test" # See https://github.com/bazelbuild/bazel/issues/9987 - "-//ffi/rust_calling_c:matrix_dylib_test" # The bindgen rules currently do not work on RBE @@ -255,10 +243,6 @@ tasks: - "-//proto/..." # The wasm rules do not work on windows - "-//wasm/..." - # rust_doc_test targets are currently broken on windows - # see: https://github.com/bazelbuild/rules_rust/issues/887 - - "-//hello_lib:hello_lib_doc_test" - - "-//fibonacci:fibonacci_doc_test" build_targets: *windows_targets test_targets: *windows_targets crate_universe_examples_ubuntu2004: diff --git a/docs/flatten.md b/docs/flatten.md index d2fc695f64..305f98ff22 100644 --- a/docs/flatten.md +++ b/docs/flatten.md @@ -546,7 +546,7 @@ Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation t | Name | Description | Type | Mandatory | Default | | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | -| crate | The label of the target to generate code documentation for.

rust_doc_test can generate HTML code documentation for the source files of rust_library or rust_binary targets. | Label | optional | None | +| crate | The label of the target to generate code documentation for. rust_doc_test can generate HTML code documentation for the source files of rust_library or rust_binary targets. | Label | optional | None | | dep | __deprecated__: use crate | Label | optional | None | diff --git a/docs/rust_doc.md b/docs/rust_doc.md index ac3db5d155..54918932c5 100644 --- a/docs/rust_doc.md +++ b/docs/rust_doc.md @@ -115,7 +115,7 @@ Running `bazel test //hello_lib:hello_lib_doc_test` will run all documentation t | Name | Description | Type | Mandatory | Default | | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | -| crate | The label of the target to generate code documentation for.

rust_doc_test can generate HTML code documentation for the source files of rust_library or rust_binary targets. | Label | optional | None | +| crate | The label of the target to generate code documentation for. rust_doc_test can generate HTML code documentation for the source files of rust_library or rust_binary targets. | Label | optional | None | | dep | __deprecated__: use crate | Label | optional | None | diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index b5cf1d10e0..a53a089e3e 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -460,7 +460,9 @@ def construct_arguments( build_flags_files, emit = ["dep-info", "link"], force_all_deps_direct = False, - stamp = False): + force_link = False, + stamp = False, + remap_path_prefix = "."): """Builds an Args object containing common rustc flags Args: @@ -482,8 +484,10 @@ def construct_arguments( emit (list): Values for the --emit flag to rustc. force_all_deps_direct (bool, optional): Whether to pass the transitive rlibs with --extern to the commandline as opposed to -L. + force_link (bool, optional): Whether to add link flags to the command regardless of `emit`. stamp (bool, optional): Whether or not workspace status stamping is enabled. For more details see https://docs.bazel.build/versions/main/user-manual.html#flag--stamp + remap_path_prefix (str, optional): A value used to remap `${pwd}` to. If set to a falsey value, no prefix will be set. Returns: tuple: A tuple of the following items @@ -576,9 +580,11 @@ def construct_arguments( rustc_flags.add("--codegen=debuginfo=" + compilation_mode.debug_info) # For determinism to help with build distribution and such - rustc_flags.add("--remap-path-prefix=${pwd}=.") + if remap_path_prefix: + rustc_flags.add("--remap-path-prefix=${{pwd}}={}".format(remap_path_prefix)) - rustc_flags.add("--emit=" + ",".join(emit_with_paths)) + if emit: + rustc_flags.add("--emit=" + ",".join(emit_with_paths)) rustc_flags.add("--color=always") rustc_flags.add("--target=" + toolchain.target_flag_value) if hasattr(attr, "crate_features"): @@ -604,11 +610,14 @@ def construct_arguments( add_edition_flags(rustc_flags, crate_info) # Link! - if "link" in emit: + if "link" in emit or force_link: # Rust's built-in linker can handle linking wasm files. We don't want to attempt to use the cc # linker since it won't understand. if toolchain.target_arch != "wasm32": - rpaths = _compute_rpaths(toolchain, output_dir, dep_info) + if output_dir: + rpaths = _compute_rpaths(toolchain, output_dir, dep_info) + else: + rpaths = depset([]) ld, link_args, link_env = get_linker_and_args(ctx, attr, cc_toolchain, feature_configuration, rpaths) env.update(link_env) rustc_flags.add("--codegen=linker=" + ld) diff --git a/rust/private/rustdoc.bzl b/rust/private/rustdoc.bzl index 7a857f5776..798e135b0e 100644 --- a/rust/private/rustdoc.bzl +++ b/rust/private/rustdoc.bzl @@ -15,84 +15,125 @@ """Rules for generating documentation with `rustdoc` for Bazel built crates""" load("//rust/private:common.bzl", "rust_common") -load("//rust/private:rustc.bzl", "add_crate_link_flags", "add_edition_flags") -load("//rust/private:utils.bzl", "dedent", "find_toolchain") +load("//rust/private:rustc.bzl", "collect_deps", "collect_inputs", "construct_arguments") +load("//rust/private:utils.bzl", "dedent", "find_cc_toolchain", "find_toolchain") -def _rust_doc_impl(ctx): - """The implementation of the `rust_doc` rule +def _strip_crate_info_output(crate_info): + """Set the CrateInfo.output to None for a given CrateInfo provider. Args: - ctx (ctx): The rule's context object + crate_info (CrateInfo): A provider + + Returns: + CrateInfo: A modified CrateInfo provider """ + return rust_common.create_crate_info( + name = crate_info.name, + type = crate_info.type, + root = crate_info.root, + srcs = crate_info.srcs, + deps = crate_info.deps, + proc_macro_deps = crate_info.proc_macro_deps, + aliases = crate_info.aliases, + # This crate info should have no output + output = None, + edition = crate_info.edition, + rustc_env = crate_info.rustc_env, + is_test = crate_info.is_test, + compile_data = crate_info.compile_data, + ) - if ctx.attr.crate and ctx.attr.dep: - fail("{} should only use the `crate` attribute. `dep` is deprecated".format( - ctx.label, - )) +def rustdoc_compile_action( + ctx, + toolchain, + crate_info, + output = None, + rustdoc_flags = []): + """Create a struct of information needed for a `rustdoc` compile action based on crate passed to the rustdoc rule. - crate = ctx.attr.crate or ctx.attr.dep - if not crate: - fail("{} is missing the `crate` attribute".format(ctx.label)) + Args: + ctx (ctx): The rule's context object. + toolchain (rust_toolchain): The currently configured `rust_toolchain`. + crate_info (CrateInfo): The provider of the crate passed to a rustdoc rule. + output (File, optional): An optional output a `rustdoc` action is intended to produce. + rustdoc_flags (list, optional): A list of `rustdoc` specific flags. - crate_info = crate[rust_common.crate_info] - dep_info = crate[rust_common.dep_info] + Returns: + struct: A struct of some `ctx.actions.run` arguments. + """ - toolchain = find_toolchain(ctx) + # If an output was provided, ensure it's used in rustdoc arguments + if output: + rustdoc_flags = [ + "--output", + output.path, + ] + rustdoc_flags - rustdoc_inputs = depset( - [c.output for c in dep_info.transitive_crates.to_list()] + - [toolchain.rust_doc], - transitive = [ - crate_info.srcs, - toolchain.rustc_lib.files, - toolchain.rust_lib.files, - ], + cc_toolchain, feature_configuration = find_cc_toolchain(ctx) + + dep_info, build_info, linkstamps = collect_deps( + label = ctx.label, + deps = crate_info.deps, + proc_macro_deps = crate_info.proc_macro_deps, + aliases = crate_info.aliases, ) - output_dir = ctx.actions.declare_directory(ctx.label.name) - args = ctx.actions.args() - args.add(crate_info.root.path) - args.add("--crate-name", crate_info.name) - args.add("--crate-type", crate_info.type) - if crate_info.type == "proc-macro": - args.add("--extern") - args.add("proc_macro") - args.add("--output", output_dir.path) - add_edition_flags(args, crate_info) - - # nb. rustdoc can't do anything with native link flags; we must omit them. - add_crate_link_flags(args, dep_info) - - args.add_all(ctx.files.markdown_css, before_each = "--markdown-css") - if ctx.file.html_in_header: - args.add("--html-in-header", ctx.file.html_in_header) - if ctx.file.html_before_content: - args.add("--html-before-content", ctx.file.html_before_content) - if ctx.file.html_after_content: - args.add("--html-after-content", ctx.file.html_after_content) + compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs = collect_inputs( + ctx = ctx, + file = ctx.file, + files = ctx.files, + linkstamps = linkstamps, + toolchain = toolchain, + cc_toolchain = cc_toolchain, + feature_configuration = feature_configuration, + crate_info = crate_info, + dep_info = dep_info, + build_info = build_info, + ) - ctx.actions.run( - executable = toolchain.rust_doc, - inputs = rustdoc_inputs, - outputs = [output_dir], - arguments = [args], - mnemonic = "Rustdoc", - progress_message = "Generating rustdoc for {} ({} files)".format( - crate_info.name, - len(crate_info.srcs.to_list()), - ), + # Since this crate is not actually producing the output described by the + # given CrateInfo, this attribute needs to be stripped to allow the rest + # of the rustc functionality in `construct_arguments` to avoid generating + # arguments expecting to do so. + rustdoc_crate_info = _strip_crate_info_output(crate_info) + + args, env = construct_arguments( + ctx = ctx, + attr = ctx.attr, + file = ctx.file, + toolchain = toolchain, + tool_path = toolchain.rust_doc.path, + cc_toolchain = cc_toolchain, + feature_configuration = feature_configuration, + crate_info = rustdoc_crate_info, + dep_info = dep_info, + linkstamp_outs = linkstamp_outs, + output_hash = None, + rust_flags = rustdoc_flags, + out_dir = out_dir, + build_env_files = build_env_files, + build_flags_files = build_flags_files, + emit = [], + remap_path_prefix = None, + force_link = True, ) - # This rule does nothing without a single-file output, though the directory should've sufficed. - _zip_action(ctx, output_dir, ctx.outputs.rust_doc_zip) + return struct( + executable = ctx.executable._process_wrapper, + inputs = depset([crate_info.output], transitive = [compile_inputs]), + env = env, + arguments = args.all, + tools = [toolchain.rust_doc], + ) -def _zip_action(ctx, input_dir, output_zip): +def _zip_action(ctx, input_dir, output_zip, crate_label): """Creates an archive of the generated documentation from `rustdoc` Args: ctx (ctx): The `rust_doc` rule's context object input_dir (File): A directory containing the outputs from rustdoc output_zip (File): The location of the output archive containing generated documentation + crate_label (Label): The label of the crate docs are being generated for. """ args = ctx.actions.args() args.add(ctx.executable._zipper) @@ -104,9 +145,70 @@ def _zip_action(ctx, input_dir, output_zip): inputs = [input_dir], outputs = [output_zip], arguments = [args], + mnemonic = "RustdocZip", + progress_message = "Creating RustdocZip for {}".format(crate_label), tools = [ctx.executable._zipper], ) +def _rust_doc_impl(ctx): + """The implementation of the `rust_doc` rule + + Args: + ctx (ctx): The rule's context object + """ + + if ctx.attr.crate and ctx.attr.dep: + fail("{} should only use the `crate` attribute. `dep` is deprecated".format( + ctx.label, + )) + + crate = ctx.attr.crate or ctx.attr.dep + if not crate: + fail("{} is missing the `crate` attribute".format(ctx.label)) + + crate_info = crate[rust_common.crate_info] + dep_info = crate[rust_common.dep_info] + + output_dir = ctx.actions.declare_directory("{}.rustdoc".format(ctx.label.name)) + + # Add the current crate as an extern for the compile action + rustdoc_flags = [ + "--extern", + "{}={}".format(crate_info.name, crate_info.output.path), + ] + + action = rustdoc_compile_action( + ctx = ctx, + toolchain = find_toolchain(ctx), + crate_info = crate_info, + output = output_dir, + rustdoc_flags = rustdoc_flags, + ) + + ctx.actions.run( + mnemonic = "Rustdoc", + progress_message = "Generating Rustdoc for {}".format(crate.label), + outputs = [output_dir], + executable = action.executable, + inputs = action.inputs, + env = action.env, + arguments = action.arguments, + tools = action.tools, + ) + + # This rule does nothing without a single-file output, though the directory should've sufficed. + _zip_action(ctx, output_dir, ctx.outputs.rust_doc_zip, crate.label) + + return [ + DefaultInfo( + files = depset([ctx.outputs.rust_doc_zip]), + ), + OutputGroupInfo( + rustdoc_dir = depset([output_dir]), + rustdoc_zip = depset([ctx.outputs.rust_doc_zip]), + ), + ] + rust_doc = rule( doc = dedent("""\ Generates code documentation. @@ -179,17 +281,31 @@ rust_doc = rule( doc = "CSS files to include via `` in a rendered Markdown file.", allow_files = [".css"], ), + "_cc_toolchain": attr.label( + doc = "In order to use find_cpp_toolchain, you must define the '_cc_toolchain' attribute on your rule or aspect.", + default = "@bazel_tools//tools/cpp:current_cc_toolchain", + ), "_dir_zipper": attr.label( + doc = "A tool that orchestrates the creation of zip archives for rustdoc outputs.", default = Label("//util/dir_zipper"), cfg = "exec", executable = True, ), + "_process_wrapper": attr.label( + doc = "A process wrapper for running rustdoc on all platforms", + default = Label("@rules_rust//util/process_wrapper"), + executable = True, + allow_single_file = True, + cfg = "exec", + ), "_zipper": attr.label( + doc = "A Bazel provided tool for creating archives", default = Label("@bazel_tools//tools/zip:zipper"), cfg = "exec", executable = True, ), }, + fragments = ["cpp"], outputs = { "rust_doc_zip": "%{name}.zip", }, diff --git a/rust/private/rustdoc_test.bzl b/rust/private/rustdoc_test.bzl index 29ca464af7..a6b8ac7a3d 100644 --- a/rust/private/rustdoc_test.bzl +++ b/rust/private/rustdoc_test.bzl @@ -15,7 +15,75 @@ """Rules for performing `rustdoc --test` on Bazel built crates""" load("//rust/private:common.bzl", "rust_common") -load("//rust/private:utils.bzl", "dedent", "find_toolchain", "get_lib_name", "get_preferred_artifact") +load("//rust/private:rustdoc.bzl", "rustdoc_compile_action") +load("//rust/private:toolchain_utils.bzl", "find_sysroot") +load("//rust/private:utils.bzl", "dedent", "find_toolchain") + +def _construct_writer_arguments(ctx, test_runner, action, crate_info, rust_toolchain): + """Construct arguments and environment variables specific to `rustdoc_test_writer`. + + This is largely solving for the fact that tests run from a runfiles directory + where actions run in an execroot. But it also tracks what environment variables + were explicitly added to the action. + + Args: + ctx (ctx): The rule's context object. + test_runner (File): The test_runner output file declared by `rustdoc_test`. + action (struct): Action arguments generated by `rustdoc_compile_action`. + crate_info (CrateInfo): The provider of the crate who's docs are being tested. + rust_toolchain (rust_toolchain): The currently configured `rust_toolchain`. + + Returns: + tuple: A tuple of `rustdoc_test_writer` specific inputs + - Args: Arguments for the test writer + - dict: Required environment variables + """ + + # Set the SYSROOT to the directory of the rust_lib files passed to the toolchain + env = { + "SYSROOT": "${{pwd}}/{}".format(find_sysroot(rust_toolchain)), + } + + writer_args = ctx.actions.args() + + # Track the output path where the test writer should write the test + writer_args.add("--output={}".format(test_runner.path)) + + # Track what environment variables should be written to the test runner + writer_args.add("--action_env=DEVELOPER_DIR") + writer_args.add("--action_env=SDKROOT") + writer_args.add("--action_env=PATHEXT") + writer_args.add("--action_env=SYSROOT") + for var in action.env.keys(): + writer_args.add("--action_env={}".format(var)) + + # Since the test runner will be running from a runfiles directory, the + # paths originally generated for the build action will not map to any + # files. To ensure rustdoc can find the appropriate dependencies, the + # file roots are identified and tracked for each dependency so it can be + # stripped from the test runner. + for dep in crate_info.deps.to_list(): + dep_crate_info = getattr(dep, "crate_info", None) + dep_dep_info = getattr(dep, "dep_info", None) + if dep_crate_info: + root = dep_crate_info.output.root.path + writer_args.add("--strip_substring={}/".format(root)) + if dep_dep_info: + for direct_dep in dep_dep_info.direct_crates.to_list(): + root = direct_dep.dep.output.root.path + writer_args.add("--strip_substring={}/".format(root)) + for transitive_dep in dep_dep_info.transitive_crates.to_list(): + root = transitive_dep.output.root.path + writer_args.add("--strip_substring={}/".format(root)) + + # Indicate that the rustdoc_test args are over. + writer_args.add("--") + + # Prepare for the process runner to ingest the rest of the arguments + # to match the expectations of `rustc_compile_action`. + writer_args.add(ctx.executable._process_wrapper.short_path) + + return (writer_args, action.env) def _rust_doc_test_impl(ctx): """The implementation for the `rust_doc_test` rule @@ -31,180 +99,70 @@ def _rust_doc_test_impl(ctx): ctx.label, )) + toolchain = find_toolchain(ctx) + crate = ctx.attr.crate or ctx.attr.dep if not crate: fail("{} is missing the `crate` attribute".format(ctx.label)) - - toolchain = find_toolchain(ctx) - crate_info = crate[rust_common.crate_info] - dep_info = crate[rust_common.dep_info] - # Construct rustdoc test command, which will be written to a shell script - # to be executed to run the test. - flags = _build_rustdoc_flags(dep_info, crate_info) - if toolchain.os != "windows": - rust_doc_test = _build_rustdoc_test_bash_script(ctx, toolchain, flags, crate_info) + if toolchain.os == "windows": + test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.bat") else: - rust_doc_test = _build_rustdoc_test_batch_script(ctx, toolchain, flags, crate_info) - - # The test script compiles the crate and runs it, so it needs both compile and runtime inputs. - compile_inputs = depset( - [crate_info.output] + - [toolchain.rust_doc] + - [toolchain.rustc] + - toolchain.crosstool_files, - transitive = [ - crate_info.srcs, - dep_info.transitive_libs, - toolchain.rustc_lib.files, - toolchain.rust_lib.files, - ], + test_runner = ctx.actions.declare_file(ctx.label.name + ".rustdoc_test.sh") + + # Add the current crate as an extern for the compile action + rustdoc_flags = [ + "--extern", + "{}={}".format(crate_info.name, crate_info.output.short_path), + "--test", + ] + + action = rustdoc_compile_action( + ctx = ctx, + toolchain = find_toolchain(ctx), + crate_info = crate_info, + rustdoc_flags = rustdoc_flags, ) - return [DefaultInfo( - runfiles = ctx.runfiles( - files = compile_inputs.to_list(), - collect_data = True, - ), - executable = rust_doc_test, - )] - -# TODO: Replace with bazel-skylib's `path.dirname`. This requires addressing some dependency issues or -# generating docs will break. -def _dirname(path_str): - """Returns the path of the direcotry from a unix path. - - Args: - path_str (str): A string representing a unix path - - Returns: - str: The parsed directory name of the provided path - """ - return "/".join(path_str.split("/")[:-1]) - -def _build_rustdoc_flags(dep_info, crate_info): - """Constructs the rustdoc script used to test `crate`. - - Args: - dep_info (DepInfo): The DepInfo provider - crate_info (CrateInfo): The CrateInfo provider - - Returns: - list: A list of rustdoc flags (str) - """ - - d = dep_info - - # nb. Paths must be constructed wrt runfiles, so we construct relative link flags for doctest. - link_flags = [] - link_search_flags = [] - - link_flags.append("--extern=" + crate_info.name + "=" + crate_info.output.short_path) - link_flags += ["--extern=" + c.name + "=" + c.dep.output.short_path for c in d.direct_crates.to_list()] - link_search_flags += ["-Ldependency={}".format(_dirname(c.output.short_path)) for c in d.transitive_crates.to_list()] + tools = action.tools + [ctx.executable._process_wrapper] - # TODO(hlopko): use the more robust logic from rustc.bzl also here, through a reasonable API. - for lib_to_link in dep_info.transitive_noncrates.to_list(): - is_static = bool(lib_to_link.static_library or lib_to_link.pic_static_library) - f = get_preferred_artifact(lib_to_link) - if not is_static: - link_flags.append("-ldylib=" + get_lib_name(f)) - else: - link_flags.append("-lstatic=" + get_lib_name(f)) - link_flags.append("-Lnative={}".format(_dirname(f.short_path))) - link_search_flags.append("-Lnative={}".format(_dirname(f.short_path))) - - if crate_info.type == "proc-macro": - link_flags.extend(["--extern", "proc_macro"]) - - edition_flags = ["--edition={}".format(crate_info.edition)] if crate_info.edition != "2015" else [] - - return link_search_flags + link_flags + edition_flags - -_rustdoc_test_bash_script = """\ -#!/usr/bin/env bash - -set -e; - -{rust_doc} --test \\ - {crate_root} \\ - --crate-name={crate_name} \\ - {flags} -""" - -def _build_rustdoc_test_bash_script(ctx, toolchain, flags, crate_info): - """Generates a helper script for executing a rustdoc test for unix systems - - Args: - ctx (ctx): The `rust_doc_test` rule's context object - toolchain (ToolchainInfo): A rustdoc toolchain - flags (list): A list of rustdoc flags (str) - crate_info (CrateInfo): The CrateInfo provider - - Returns: - File: An executable containing information for a rustdoc test - """ - rust_doc_test = ctx.actions.declare_file( - ctx.label.name + ".sh", + writer_args, env = _construct_writer_arguments( + ctx = ctx, + test_runner = test_runner, + action = action, + crate_info = crate_info, + rust_toolchain = toolchain, ) - ctx.actions.write( - output = rust_doc_test, - content = _rustdoc_test_bash_script.format( - rust_doc = toolchain.rust_doc.short_path, - crate_root = crate_info.root.path, - crate_name = crate_info.name, - # TODO: Should be possible to do this with ctx.actions.Args, but can't seem to get them as a str and into the template. - flags = " \\\n ".join(flags), - ), - is_executable = True, - ) - return rust_doc_test - -_rustdoc_test_batch_script = """\ -{rust_doc} --test ^ - {crate_root} ^ - --crate-name={crate_name} ^ - {flags} -""" -def _build_rustdoc_test_batch_script(ctx, toolchain, flags, crate_info): - """Generates a helper script for executing a rustdoc test for windows systems - - Args: - ctx (ctx): The `rust_doc_test` rule's context object - toolchain (ToolchainInfo): A rustdoc toolchain - flags (list): A list of rustdoc flags (str) - crate_info (CrateInfo): The CrateInfo provider - - Returns: - File: An executable containing information for a rustdoc test - """ - rust_doc_test = ctx.actions.declare_file( - ctx.label.name + ".bat", - ) - ctx.actions.write( - output = rust_doc_test, - content = _rustdoc_test_batch_script.format( - rust_doc = toolchain.rust_doc.short_path.replace("/", "\\"), - crate_root = crate_info.root.path, - crate_name = crate_info.name, - # TODO: Should be possible to do this with ctx.actions.Args, but can't seem to get them as a str and into the template. - flags = " ^\n ".join(flags), - ), - is_executable = True, + # Allow writer environment variables to override those from the action. + action.env.update(env) + + ctx.actions.run( + mnemonic = "RustdocTestWriter", + progress_message = "Generating Rustdoc test runner for {}".format(crate.label), + executable = ctx.executable._test_writer, + inputs = action.inputs, + tools = tools, + arguments = [writer_args] + action.arguments, + env = action.env, + outputs = [test_runner], ) - return rust_doc_test + + return [DefaultInfo( + files = depset([test_runner]), + runfiles = ctx.runfiles(files = tools, transitive_files = action.inputs), + executable = test_runner, + )] rust_doc_test = rule( implementation = _rust_doc_test_impl, attrs = { "crate": attr.label( doc = ( - "The label of the target to generate code documentation for.\n" + - "\n" + - "`rust_doc_test` can generate HTML code documentation for the source files of " + - "`rust_library` or `rust_binary` targets." + "The label of the target to generate code documentation for. " + + "`rust_doc_test` can generate HTML code documentation for the " + + "source files of `rust_library` or `rust_binary` targets." ), providers = [rust_common.crate_info], # TODO: Make this attribute mandatory once `dep` is removed @@ -213,10 +171,34 @@ rust_doc_test = rule( doc = "__deprecated__: use `crate`", providers = [rust_common.crate_info], ), + "_cc_toolchain": attr.label( + doc = ( + "In order to use find_cc_toolchain, your rule has to depend " + + "on C++ toolchain. See @rules_cc//cc:find_cc_toolchain.bzl " + + "docs for details." + ), + default = "@bazel_tools//tools/cpp:current_cc_toolchain", + ), + "_process_wrapper": attr.label( + doc = "A process wrapper for running rustdoc on all platforms", + cfg = "exec", + default = Label("//util/process_wrapper"), + executable = True, + ), + "_test_writer": attr.label( + doc = "A binary used for writing script for use as the test executable.", + cfg = "exec", + default = Label("//tools/rustdoc:rustdoc_test_writer"), + executable = True, + ), }, - executable = True, test = True, - toolchains = [str(Label("//rust:toolchain"))], + fragments = ["cpp"], + host_fragments = ["cpp"], + toolchains = [ + str(Label("//rust:toolchain")), + "@bazel_tools//tools/cpp:toolchain_type", + ], incompatible_use_toolchain_transition = True, doc = dedent("""\ Runs Rust documentation tests. diff --git a/rust/private/toolchain_utils.bzl b/rust/private/toolchain_utils.bzl index f26acf298b..aa98a4311b 100644 --- a/rust/private/toolchain_utils.bzl +++ b/rust/private/toolchain_utils.bzl @@ -1,5 +1,18 @@ """A module defining toolchain utilities""" +def find_sysroot(rust_toolchain): + """Locate the rustc sysroot from the `rust_toolchain` + + Args: + rust_toolchain (rust_toolchain): The currently configured `rust_toolchain`. + + Returns: + str: A path assignable as `SYSROOT` for an action. + """ + sysroot_anchor = rust_toolchain.rust_lib.files.to_list()[0] + directory = sysroot_anchor.path.split(sysroot_anchor.short_path, 1)[0] + return directory.rstrip("/") + def _toolchain_files_impl(ctx): toolchain = ctx.toolchains[str(Label("//rust:toolchain"))] diff --git a/tools/rustdoc/BUILD.bazel b/tools/rustdoc/BUILD.bazel new file mode 100644 index 0000000000..f94175906a --- /dev/null +++ b/tools/rustdoc/BUILD.bazel @@ -0,0 +1,12 @@ +load("//rust:defs.bzl", "rust_binary") + +package(default_visibility = ["//visibility:public"]) + +rust_binary( + name = "rustdoc_test_writer", + srcs = ["rustdoc_test_writer.rs"], + edition = "2018", + deps = [ + "//tools/runfiles", + ], +) diff --git a/tools/rustdoc/rustdoc_test_writer.rs b/tools/rustdoc/rustdoc_test_writer.rs new file mode 100644 index 0000000000..33c4c68e61 --- /dev/null +++ b/tools/rustdoc/rustdoc_test_writer.rs @@ -0,0 +1,205 @@ +//! A utility for writing scripts for use as test executables intended to match the +//! subcommands of Bazel build actions so `rustdoc --test`, which builds and tests +//! code in a single call, can be run as a test target in a hermetic manner. + +use std::cmp::Reverse; +use std::collections::{BTreeSet, HashMap}; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug)] +struct Options { + /// A list of environment variable keys to parse from the build action env. + env_keys: BTreeSet, + + /// A list of substrings to strip from [Options::action_argv]. + strip_substrings: Vec, + + /// The path where the script should be written. + output: PathBuf, + + /// The `argv` of the configured rustdoc build action. + action_argv: Vec, +} + +/// Parse command line arguments +fn parse_args() -> Options { + let args: Vec = env::args().into_iter().collect(); + let (writer_args, action_args) = { + let split = args + .iter() + .position(|arg| arg == "--") + .expect("Unable to find split identifier `--`"); + + // Converting each set into a vector makes them easier to parse in + // the absence of nightly features + let (writer, action) = args.split_at(split); + (writer.to_vec(), action.to_vec()) + }; + + // Remove the leading `--` which is expected to be the first + // item in `action_args` + debug_assert_eq!(action_args[0], "--"); + let action_argv = action_args[1..].to_vec(); + + let output = writer_args + .iter() + .find(|arg| arg.starts_with("--output=")) + .and_then(|arg| arg.splitn(2, '=').last()) + .map(PathBuf::from) + .expect("Missing `--output` argument"); + + let (strip_substring_args, writer_args): (Vec, Vec) = writer_args + .into_iter() + .partition(|arg| arg.starts_with("--strip_substring=")); + + let mut strip_substrings: Vec = strip_substring_args + .into_iter() + .map(|arg| { + arg.splitn(2, '=') + .last() + .expect("--strip_substring arguments must have assignments using `=`") + .to_owned() + }) + .collect(); + + // Strip substrings should always be in reverse order of the length of each + // string so when filtering we know that the longer strings are checked + // first in order to avoid cases where shorter strings might match longer ones. + strip_substrings.sort_by_key(|b| Reverse(b.len())); + strip_substrings.dedup(); + + let env_keys = writer_args + .into_iter() + .filter(|arg| arg.starts_with("--action_env=")) + .map(|arg| { + arg.splitn(2, '=') + .last() + .expect("--env arguments must have assignments using `=`") + .to_owned() + }) + .collect(); + + Options { + env_keys, + strip_substrings, + output, + action_argv, + } +} + +/// Write a unix compatible test runner +fn write_test_runner_unix( + path: &Path, + env: &HashMap, + argv: &[String], + strip_substrings: &[String], +) { + let mut content = vec![ + "#!/usr/bin/env bash".to_owned(), + "".to_owned(), + "exec env - \\".to_owned(), + ]; + + content.extend(env.iter().map(|(key, val)| format!("{}='{}' \\", key, val))); + + let argv_str = argv + .iter() + // Remove any substrings found in the argument + .map(|arg| { + let mut stripped_arg = arg.to_owned(); + strip_substrings + .iter() + .for_each(|substring| stripped_arg = stripped_arg.replace(substring, "")); + stripped_arg + }) + .map(|arg| format!("'{}'", arg)) + .collect::>() + .join(" "); + + content.extend(vec![argv_str, "".to_owned()]); + + fs::write(path, content.join("\n")).expect("Failed to write test runner"); +} + +/// Write a windows compatible test runner +fn write_test_runner_windows( + path: &Path, + env: &HashMap, + argv: &[String], + strip_substrings: &[String], +) { + let env_str = env + .iter() + .map(|(key, val)| format!("$env:{}='{}'", key, val)) + .collect::>() + .join(" ; "); + + let argv_str = argv + .iter() + // Remove any substrings found in the argument + .map(|arg| { + let mut stripped_arg = arg.to_owned(); + strip_substrings + .iter() + .for_each(|substring| stripped_arg = stripped_arg.replace(substring, "")); + stripped_arg + }) + .map(|arg| format!("'{}'", arg)) + .collect::>() + .join(" "); + + let content = vec![ + "@ECHO OFF".to_owned(), + "".to_owned(), + format!("powershell.exe -c \"{} ; & {}\"", env_str, argv_str), + "".to_owned(), + ]; + + fs::write(path, content.join("\n")).expect("Failed to write test runner"); +} + +#[cfg(target_family = "unix")] +fn set_executable(path: &Path) { + use std::os::unix::prelude::PermissionsExt; + + let mut perm = fs::metadata(path) + .expect("Failed to get test runner metadata") + .permissions(); + + perm.set_mode(0o755); + fs::set_permissions(path, perm).expect("Failed to set permissions on test runner"); +} + +#[cfg(target_family = "windows")] +fn set_executable(_path: &Path) { + // Windows determines whether or not a file is executable via the PATHEXT + // environment variable. This function is a no-op for this platform. +} + +fn write_test_runner( + path: &Path, + env: &HashMap, + argv: &[String], + strip_substrings: &[String], +) { + if cfg!(target_family = "unix") { + write_test_runner_unix(path, env, argv, strip_substrings); + } else if cfg!(target_family = "windows") { + write_test_runner_windows(path, env, argv, strip_substrings); + } + + set_executable(path); +} + +fn main() { + let opt = parse_args(); + + let env: HashMap = env::vars() + .into_iter() + .filter(|(key, _)| opt.env_keys.iter().any(|k| k == key)) + .collect(); + + write_test_runner(&opt.output, &env, &opt.action_argv, &opt.strip_substrings); +}