xref: /aosp_15_r20/external/bazelbuild-rules_rust/cargo/private/cargo_build_script.bzl (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1"""Rules for Cargo build scripts (`build.rs` files)"""
2
3load("@bazel_skylib//lib:paths.bzl", "paths")
4load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
5load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
6load("@rules_cc//cc:action_names.bzl", "ACTION_NAMES")
7load("//rust:defs.bzl", "rust_common")
8load("//rust:rust_common.bzl", "BuildInfo", "DepInfo")
9
10# buildifier: disable=bzl-visibility
11load(
12    "//rust/private:rustc.bzl",
13    "get_compilation_mode_opts",
14    "get_linker_and_args",
15)
16
17# buildifier: disable=bzl-visibility
18load(
19    "//rust/private:utils.bzl",
20    "dedent",
21    "expand_dict_value_locations",
22    "find_cc_toolchain",
23    "find_toolchain",
24    _name_to_crate_name = "name_to_crate_name",
25)
26
27# Reexport for cargo_build_script_wrapper.bzl
28name_to_crate_name = _name_to_crate_name
29
30def get_cc_compile_args_and_env(cc_toolchain, feature_configuration):
31    """Gather cc environment variables from the given `cc_toolchain`
32
33    Args:
34        cc_toolchain (cc_toolchain): The current rule's `cc_toolchain`.
35        feature_configuration (FeatureConfiguration): Class used to construct command lines from CROSSTOOL features.
36
37    Returns:
38        tuple: A tuple of the following items:
39            - (sequence): A flattened C command line flags for given action.
40            - (sequence): A flattened CXX command line flags for given action.
41            - (dict): C environment variables to be set for given action.
42    """
43    compile_variables = cc_common.create_compile_variables(
44        feature_configuration = feature_configuration,
45        cc_toolchain = cc_toolchain,
46    )
47    cc_c_args = cc_common.get_memory_inefficient_command_line(
48        feature_configuration = feature_configuration,
49        action_name = ACTION_NAMES.c_compile,
50        variables = compile_variables,
51    )
52    cc_cxx_args = cc_common.get_memory_inefficient_command_line(
53        feature_configuration = feature_configuration,
54        action_name = ACTION_NAMES.cpp_compile,
55        variables = compile_variables,
56    )
57    cc_env = cc_common.get_environment_variables(
58        feature_configuration = feature_configuration,
59        action_name = ACTION_NAMES.c_compile,
60        variables = compile_variables,
61    )
62    return cc_c_args, cc_cxx_args, cc_env
63
64def _pwd_flags_sysroot(args):
65    """Prefix execroot-relative paths of known arguments with ${pwd}.
66
67    Args:
68        args (list): List of tool arguments.
69
70    Returns:
71        list: The modified argument list.
72    """
73    res = []
74    for arg in args:
75        s, opt, path = arg.partition("--sysroot=")
76        if s == "" and not paths.is_absolute(path):
77            res.append("{}${{pwd}}/{}".format(opt, path))
78        else:
79            res.append(arg)
80    return res
81
82def _pwd_flags_isystem(args):
83    """Prefix execroot-relative paths of known arguments with ${pwd}.
84
85    Args:
86        args (list): List of tool arguments.
87
88    Returns:
89        list: The modified argument list.
90    """
91    res = []
92    fix_next_arg = False
93    for arg in args:
94        if fix_next_arg and not paths.is_absolute(arg):
95            res.append("${{pwd}}/{}".format(arg))
96        else:
97            res.append(arg)
98
99        fix_next_arg = arg == "-isystem"
100
101    return res
102
103def _pwd_flags(args):
104    return _pwd_flags_isystem(_pwd_flags_sysroot(args))
105
106def _feature_enabled(ctx, feature_name, default = False):
107    """Check if a feature is enabled.
108
109    If the feature is explicitly enabled or disabled, return accordingly.
110
111    In the case where the feature is not explicitly enabled or disabled, return the default value.
112
113    Args:
114        ctx: The context object.
115        feature_name: The name of the feature.
116        default: The default value to return if the feature is not explicitly enabled or disabled.
117
118    Returns:
119        Boolean defining whether the feature is enabled.
120    """
121    if feature_name in ctx.disabled_features:
122        return False
123
124    if feature_name in ctx.features:
125        return True
126
127    return default
128
129def _cargo_build_script_impl(ctx):
130    """The implementation for the `cargo_build_script` rule.
131
132    Args:
133        ctx (ctx): The rules context object
134
135    Returns:
136        list: A list containing a BuildInfo provider
137    """
138    script = ctx.executable.script
139    toolchain = find_toolchain(ctx)
140    out_dir = ctx.actions.declare_directory(ctx.label.name + ".out_dir")
141    env_out = ctx.actions.declare_file(ctx.label.name + ".env")
142    dep_env_out = ctx.actions.declare_file(ctx.label.name + ".depenv")
143    flags_out = ctx.actions.declare_file(ctx.label.name + ".flags")
144    link_flags = ctx.actions.declare_file(ctx.label.name + ".linkflags")
145    link_search_paths = ctx.actions.declare_file(ctx.label.name + ".linksearchpaths")  # rustc-link-search, propagated from transitive dependencies
146    manifest_dir = "%s.runfiles/%s/%s" % (script.path, ctx.label.workspace_name or ctx.workspace_name, ctx.label.package)
147    compilation_mode_opt_level = get_compilation_mode_opts(ctx, toolchain).opt_level
148
149    streams = struct(
150        stdout = ctx.actions.declare_file(ctx.label.name + ".stdout.log"),
151        stderr = ctx.actions.declare_file(ctx.label.name + ".stderr.log"),
152    )
153
154    pkg_name = ctx.attr.pkg_name
155    if pkg_name == "":
156        pkg_name = name_to_pkg_name(ctx.label.name)
157
158    toolchain_tools = [toolchain.all_files]
159
160    cc_toolchain = find_cpp_toolchain(ctx)
161
162    # Start with the default shell env, which contains any --action_env
163    # settings passed in on the command line.
164    env = dict(ctx.configuration.default_shell_env)
165
166    env.update({
167        "CARGO_CRATE_NAME": name_to_crate_name(pkg_name),
168        "CARGO_MANIFEST_DIR": manifest_dir,
169        "CARGO_PKG_NAME": pkg_name,
170        "HOST": toolchain.exec_triple.str,
171        "NUM_JOBS": "1",
172        "OPT_LEVEL": compilation_mode_opt_level,
173        "RUSTC": toolchain.rustc.path,
174        "TARGET": toolchain.target_flag_value,
175        # OUT_DIR is set by the runner itself, rather than on the action.
176    })
177
178    # This isn't exactly right, but Bazel doesn't have exact views of "debug" and "release", so...
179    env.update({
180        "DEBUG": {"dbg": "true", "fastbuild": "true", "opt": "false"}.get(ctx.var["COMPILATION_MODE"], "true"),
181        "PROFILE": {"dbg": "debug", "fastbuild": "debug", "opt": "release"}.get(ctx.var["COMPILATION_MODE"], "unknown"),
182    })
183
184    if ctx.attr.version:
185        version = ctx.attr.version.split("+")[0].split(".")
186        patch = version[2].split("-") if len(version) > 2 else [""]
187        env["CARGO_PKG_VERSION_MAJOR"] = version[0]
188        env["CARGO_PKG_VERSION_MINOR"] = version[1] if len(version) > 1 else ""
189        env["CARGO_PKG_VERSION_PATCH"] = patch[0]
190        env["CARGO_PKG_VERSION_PRE"] = patch[1] if len(patch) > 1 else ""
191        env["CARGO_PKG_VERSION"] = ctx.attr.version
192
193    # Pull in env vars which may be required for the cc_toolchain to work (e.g. on OSX, the SDK version).
194    # We hope that the linker env is sufficient for the whole cc_toolchain.
195    cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
196    linker, link_args, linker_env = get_linker_and_args(ctx, ctx.attr, "bin", cc_toolchain, feature_configuration, None)
197    env.update(**linker_env)
198    env["LD"] = linker
199    env["LDFLAGS"] = " ".join(_pwd_flags(link_args))
200
201    # MSVC requires INCLUDE to be set
202    cc_c_args, cc_cxx_args, cc_env = get_cc_compile_args_and_env(cc_toolchain, feature_configuration)
203    include = cc_env.get("INCLUDE")
204    if include:
205        env["INCLUDE"] = include
206
207    if cc_toolchain:
208        toolchain_tools.append(cc_toolchain.all_files)
209
210        env["CC"] = cc_common.get_tool_for_action(
211            feature_configuration = feature_configuration,
212            action_name = ACTION_NAMES.c_compile,
213        )
214        env["CXX"] = cc_common.get_tool_for_action(
215            feature_configuration = feature_configuration,
216            action_name = ACTION_NAMES.cpp_compile,
217        )
218        env["AR"] = cc_common.get_tool_for_action(
219            feature_configuration = feature_configuration,
220            action_name = ACTION_NAMES.cpp_link_static_library,
221        )
222
223        # Populate CFLAGS and CXXFLAGS that cc-rs relies on when building from source, in particular
224        # to determine the deployment target when building for apple platforms (`macosx-version-min`
225        # for example, itself derived from the `macos_minimum_os` Bazel argument).
226        env["CFLAGS"] = " ".join(_pwd_flags(cc_c_args))
227        env["CXXFLAGS"] = " ".join(_pwd_flags(cc_cxx_args))
228
229    # Inform build scripts of rustc flags
230    # https://github.com/rust-lang/cargo/issues/9600
231    env["CARGO_ENCODED_RUSTFLAGS"] = "\\x1f".join([
232        # Allow build scripts to locate the generated sysroot
233        "--sysroot=${{pwd}}/{}".format(toolchain.sysroot),
234    ] + ctx.attr.rustc_flags)
235
236    for f in ctx.attr.crate_features:
237        env["CARGO_FEATURE_" + f.upper().replace("-", "_")] = "1"
238
239    links = ctx.attr.links or ""
240    if links:
241        env["CARGO_MANIFEST_LINKS"] = links
242
243    # Add environment variables from the Rust toolchain.
244    env.update(toolchain.env)
245
246    # Gather data from the `toolchains` attribute.
247    for target in ctx.attr.toolchains:
248        if DefaultInfo in target:
249            toolchain_tools.extend([
250                target[DefaultInfo].files,
251                target[DefaultInfo].default_runfiles.files,
252            ])
253        if platform_common.ToolchainInfo in target:
254            all_files = getattr(target[platform_common.ToolchainInfo], "all_files", depset([]))
255            if type(all_files) == "list":
256                all_files = depset(all_files)
257            toolchain_tools.append(all_files)
258        if platform_common.TemplateVariableInfo in target:
259            variables = getattr(target[platform_common.TemplateVariableInfo], "variables", depset([]))
260            env.update(variables)
261
262    _merge_env_dict(env, expand_dict_value_locations(
263        ctx,
264        ctx.attr.build_script_env,
265        getattr(ctx.attr, "data", []) +
266        getattr(ctx.attr, "compile_data", []) +
267        getattr(ctx.attr, "tools", []),
268    ))
269
270    tools = depset(
271        direct = [
272            script,
273            ctx.executable._cargo_build_script_runner,
274        ] + ctx.files.data + ctx.files.tools + ([toolchain.target_json] if toolchain.target_json else []),
275        transitive = toolchain_tools,
276    )
277
278    # dep_env_file contains additional environment variables coming from
279    # direct dependency sys-crates' build scripts. These need to be made
280    # available to the current crate build script.
281    # See https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages
282    # for details.
283    args = ctx.actions.args()
284    args.add(script)
285    args.add(links)
286    args.add(out_dir.path)
287    args.add(env_out)
288    args.add(flags_out)
289    args.add(link_flags)
290    args.add(link_search_paths)
291    args.add(dep_env_out)
292    args.add(streams.stdout)
293    args.add(streams.stderr)
294    args.add(ctx.attr.rundir)
295
296    build_script_inputs = []
297    for dep in ctx.attr.link_deps:
298        if rust_common.dep_info in dep and dep[rust_common.dep_info].dep_env:
299            dep_env_file = dep[rust_common.dep_info].dep_env
300            args.add(dep_env_file.path)
301            build_script_inputs.append(dep_env_file)
302            for dep_build_info in dep[rust_common.dep_info].transitive_build_infos.to_list():
303                build_script_inputs.append(dep_build_info.out_dir)
304
305    for dep in ctx.attr.deps:
306        for dep_build_info in dep[rust_common.dep_info].transitive_build_infos.to_list():
307            build_script_inputs.append(dep_build_info.out_dir)
308
309    experimental_symlink_execroot = ctx.attr._experimental_symlink_execroot[BuildSettingInfo].value or \
310                                    _feature_enabled(ctx, "symlink-exec-root")
311
312    if experimental_symlink_execroot:
313        env["RULES_RUST_SYMLINK_EXEC_ROOT"] = "1"
314
315    ctx.actions.run(
316        executable = ctx.executable._cargo_build_script_runner,
317        arguments = [args],
318        outputs = [out_dir, env_out, flags_out, link_flags, link_search_paths, dep_env_out, streams.stdout, streams.stderr],
319        tools = tools,
320        inputs = build_script_inputs,
321        mnemonic = "CargoBuildScriptRun",
322        progress_message = "Running Cargo build script {}".format(pkg_name),
323        env = env,
324        toolchain = None,
325        # Set use_default_shell_env so that $PATH is set, as tools like cmake may want to probe $PATH for helper tools.
326        use_default_shell_env = True,
327    )
328
329    return [
330        # Although this isn't used anywhere, without this, `bazel build`'ing
331        # the cargo_build_script label won't actually run the build script
332        # since bazel is lazy.
333        DefaultInfo(files = depset([out_dir])),
334        BuildInfo(
335            out_dir = out_dir,
336            rustc_env = env_out,
337            dep_env = dep_env_out,
338            flags = flags_out,
339            linker_flags = link_flags,
340            link_search_paths = link_search_paths,
341            compile_data = depset([]),
342        ),
343        OutputGroupInfo(
344            streams = depset([streams.stdout, streams.stderr]),
345            out_dir = depset([out_dir]),
346        ),
347    ]
348
349cargo_build_script = rule(
350    doc = (
351        "A rule for running a crate's `build.rs` files to generate build information " +
352        "which is then used to determine how to compile said crate."
353    ),
354    implementation = _cargo_build_script_impl,
355    attrs = {
356        "build_script_env": attr.string_dict(
357            doc = "Environment variables for build scripts.",
358        ),
359        "crate_features": attr.string_list(
360            doc = "The list of rust features that the build script should consider activated.",
361        ),
362        "data": attr.label_list(
363            doc = "Data required by the build script.",
364            allow_files = True,
365        ),
366        "deps": attr.label_list(
367            doc = "The Rust build-dependencies of the crate",
368            providers = [rust_common.dep_info],
369            cfg = "exec",
370        ),
371        "link_deps": attr.label_list(
372            doc = dedent("""\
373                The subset of the Rust (normal) dependencies of the crate that
374                have the links attribute and therefore provide environment
375                variables to this build script.
376            """),
377            providers = [rust_common.dep_info],
378        ),
379        "links": attr.string(
380            doc = "The name of the native library this crate links against.",
381        ),
382        "pkg_name": attr.string(
383            doc = "The name of package being compiled, if not derived from `name`.",
384        ),
385        "rundir": attr.string(
386            default = "",
387            doc = dedent("""\
388                A directory to cd to before the cargo_build_script is run. This should be a path relative to the exec root.
389
390                The default behaviour (and the behaviour if rundir is set to the empty string) is to change to the relative path corresponding to the cargo manifest directory, which replicates the normal behaviour of cargo so it is easy to write compatible build scripts.
391
392                If set to `.`, the cargo build script will run in the exec root.
393            """),
394        ),
395        "rustc_flags": attr.string_list(
396            doc = dedent("""\
397                List of compiler flags passed to `rustc`.
398
399                These strings are subject to Make variable expansion for predefined
400                source/output path variables like `$location`, `$execpath`, and
401                `$rootpath`. This expansion is useful if you wish to pass a generated
402                file of arguments to rustc: `@$(location //package:target)`.
403            """),
404        ),
405        # The source of truth will be the `cargo_build_script` macro until stardoc
406        # implements documentation inheritence. See https://github.com/bazelbuild/stardoc/issues/27
407        "script": attr.label(
408            doc = "The binary script to run, generally a `rust_binary` target.",
409            executable = True,
410            allow_files = True,
411            mandatory = True,
412            cfg = "exec",
413        ),
414        "tools": attr.label_list(
415            doc = "Tools required by the build script.",
416            allow_files = True,
417            cfg = "exec",
418        ),
419        "version": attr.string(
420            doc = "The semantic version (semver) of the crate",
421        ),
422        "_cargo_build_script_runner": attr.label(
423            executable = True,
424            allow_files = True,
425            default = Label("//cargo/cargo_build_script_runner:cargo_build_script_runner"),
426            cfg = "exec",
427        ),
428        "_cc_toolchain": attr.label(
429            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
430        ),
431        "_experimental_symlink_execroot": attr.label(
432            default = Label("//cargo/settings:experimental_symlink_execroot"),
433        ),
434    },
435    fragments = ["cpp"],
436    toolchains = [
437        str(Label("//rust:toolchain_type")),
438        "@bazel_tools//tools/cpp:toolchain_type",
439    ],
440)
441
442def _merge_env_dict(prefix_dict, suffix_dict):
443    """Merges suffix_dict into prefix_dict, appending rather than replacing certain env vars."""
444    for key in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]:
445        if key in prefix_dict and key in suffix_dict and prefix_dict[key]:
446            prefix_dict[key] += " " + suffix_dict.pop(key)
447    prefix_dict.update(suffix_dict)
448
449def name_to_pkg_name(name):
450    """Sanitize the name of cargo_build_script targets.
451
452    Args:
453        name (str): The name value pass to the `cargo_build_script` wrapper.
454
455    Returns:
456        str: A cleaned up name for a build script target.
457    """
458    if name.endswith("_bs"):
459        return name[:-len("_bs")]
460    return name
461
462def _cargo_dep_env_implementation(ctx):
463    empty_file = ctx.actions.declare_file(ctx.label.name + ".empty_file")
464    empty_dir = ctx.actions.declare_directory(ctx.label.name + ".empty_dir")
465    ctx.actions.write(
466        output = empty_file,
467        content = "",
468    )
469    ctx.actions.run(
470        outputs = [empty_dir],
471        executable = "true",
472    )
473
474    build_infos = []
475    out_dir = ctx.file.out_dir
476    if out_dir:
477        if not out_dir.is_directory:
478            fail("out_dir must be a directory artifact")
479
480        # BuildInfos in this list are collected up for all transitive cargo_build_script
481        # dependencies. This is important for any flags set in `dep_env` which reference this
482        # `out_dir`.
483        #
484        # TLDR: This BuildInfo propagates up build script dependencies.
485        build_infos.append(BuildInfo(
486            dep_env = empty_file,
487            flags = empty_file,
488            linker_flags = empty_file,
489            link_search_paths = empty_file,
490            out_dir = out_dir,
491            rustc_env = empty_file,
492            compile_data = depset([]),
493        ))
494    return [
495        DefaultInfo(files = depset(ctx.files.src)),
496        # Parts of this BuildInfo is used when building all transitive dependencies
497        # (cargo_build_script and otherwise), alongside the DepInfo. This is how other rules
498        # identify this one as a valid dependency, but we don't otherwise have a use for it.
499        #
500        # TLDR: This BuildInfo propagates up normal (non build script) depenencies.
501        #
502        # In the future, we could consider setting rustc_env here, and also propagating dep_dir
503        # so files in it can be referenced there.
504        BuildInfo(
505            dep_env = empty_file,
506            flags = empty_file,
507            linker_flags = empty_file,
508            link_search_paths = empty_file,
509            out_dir = None,
510            rustc_env = empty_file,
511            compile_data = depset([]),
512        ),
513        # Information here is used directly by dependencies, and it is an error to have more than
514        # one dependency which sets this. This is the main way to specify information from build
515        # scripts, which is what we're looking to do.
516        DepInfo(
517            dep_env = ctx.file.src,
518            direct_crates = depset(),
519            link_search_path_files = depset(),
520            transitive_build_infos = depset(direct = build_infos),
521            transitive_crate_outputs = depset(),
522            transitive_crates = depset(),
523            transitive_noncrates = depset(),
524        ),
525    ]
526
527cargo_dep_env = rule(
528    implementation = _cargo_dep_env_implementation,
529    doc = (
530        "A rule for generating variables for dependent `cargo_build_script`s " +
531        "without a build script. This is useful for using Bazel rules instead " +
532        "of a build script, while also generating configuration information " +
533        "for build scripts which depend on this crate."
534    ),
535    attrs = {
536        "out_dir": attr.label(
537            doc = dedent("""\
538                Folder containing additional inputs when building all direct dependencies.
539
540                This has the same effect as a `cargo_build_script` which prints
541                puts files into `$OUT_DIR`, but without requiring a build script.
542            """),
543            allow_single_file = True,
544            mandatory = False,
545        ),
546        "src": attr.label(
547            doc = dedent("""\
548                File containing additional environment variables to set for build scripts of direct dependencies.
549
550                This has the same effect as a `cargo_build_script` which prints
551                `cargo:VAR=VALUE` lines, but without requiring a build script.
552
553                This files should  contain a single variable per line, of format
554                `NAME=value`, and newlines may be included in a value by ending a
555                line with a trailing back-slash (`\\\\`).
556            """),
557            allow_single_file = True,
558            mandatory = True,
559        ),
560    },
561)
562