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