xref: /aosp_15_r20/external/bazelbuild-rules_rust/cargo/private/cargo_bootstrap.bzl (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1"""The `cargo_bootstrap` rule is used for bootstrapping cargo binaries in a repository rule."""
2
3load("//cargo/private:cargo_utils.bzl", "get_rust_tools")
4load("//rust:defs.bzl", "rust_common")
5load("//rust/platform:triple.bzl", "get_host_triple")
6
7_CARGO_BUILD_MODES = [
8    "release",
9    "debug",
10]
11
12_FAIL_MESSAGE = """\
13Process exited with code '{code}'
14# ARGV ########################################################################
15{argv}
16
17# STDOUT ######################################################################
18{stdout}
19
20# STDERR ######################################################################
21{stderr}
22"""
23
24def cargo_bootstrap(
25        repository_ctx,
26        cargo_bin,
27        rustc_bin,
28        binary,
29        cargo_manifest,
30        environment = {},
31        quiet = False,
32        build_mode = "release",
33        target_dir = None,
34        timeout = 600):
35    """A function for bootstrapping a cargo binary within a repository rule
36
37    Args:
38        repository_ctx (repository_ctx): The rule's context object.
39        cargo_bin (path): The path to a Cargo binary.
40        rustc_bin (path): The path to a Rustc binary.
41        binary (str): The binary to build (the `--bin` parameter for Cargo).
42        cargo_manifest (path): The path to a Cargo manifest (Cargo.toml file).
43        environment (dict): Environment variables to use during execution.
44        quiet (bool, optional): Whether or not to print output from the Cargo command.
45        build_mode (str, optional): The build mode to use
46        target_dir (path, optional): The directory in which to produce build outputs
47            (Cargo's --target-dir argument).
48        timeout (int, optional): Maximum duration of the Cargo build command in seconds,
49
50    Returns:
51        path: The path of the built binary within the target directory
52    """
53
54    if not target_dir:
55        target_dir = repository_ctx.path(".")
56
57    args = [
58        cargo_bin,
59        "build",
60        "--bin",
61        binary,
62        "--locked",
63        "--target-dir",
64        target_dir,
65        "--manifest-path",
66        cargo_manifest,
67    ]
68
69    if build_mode not in _CARGO_BUILD_MODES:
70        fail("'{}' is not a supported build mode. Use one of {}".format(build_mode, _CARGO_BUILD_MODES))
71
72    if build_mode == "release":
73        args.append("--release")
74
75    env = dict({
76        "RUSTC": str(rustc_bin),
77    }.items() + environment.items())
78
79    repository_ctx.report_progress("Cargo Bootstrapping {}".format(binary))
80    result = repository_ctx.execute(
81        args,
82        environment = env,
83        quiet = quiet,
84        timeout = timeout,
85    )
86
87    if result.return_code != 0:
88        fail(_FAIL_MESSAGE.format(
89            code = result.return_code,
90            argv = args,
91            stdout = result.stdout,
92            stderr = result.stderr,
93        ))
94
95    extension = ""
96    if "win" in repository_ctx.os.name:
97        extension = ".exe"
98
99    binary_path = "{}/{}{}".format(
100        build_mode,
101        binary,
102        extension,
103    )
104
105    if not repository_ctx.path(binary_path).exists:
106        fail("Failed to produce binary at {}".format(binary_path))
107
108    return binary_path
109
110_BUILD_FILE_CONTENT = """\
111load("@rules_rust//rust:defs.bzl", "rust_binary")
112
113package(default_visibility = ["//visibility:public"])
114
115exports_files([
116    "{binary_name}",
117    "{binary}"
118])
119
120alias(
121    name = "binary",
122    actual = "{binary}",
123)
124
125rust_binary(
126    name = "install",
127    rustc_env = {{
128        "RULES_RUST_CARGO_BOOTSTRAP_BINARY": "$(rootpath {binary})"
129    }},
130    data = [
131        "{binary}",
132    ],
133    srcs = [
134        "@rules_rust//cargo/bootstrap:bootstrap_installer.rs"
135    ],
136)
137"""
138
139def _collect_environ(repository_ctx, host_triple):
140    """Gather environment varialbes to use from the current rule context
141
142    Args:
143        repository_ctx (repository_ctx): The rule's context object.
144        host_triple (str): A string of the current host triple
145
146    Returns:
147        dict: A map of environment variables
148    """
149    env_vars = dict(json.decode(repository_ctx.attr.env.get(host_triple, "{}")))
150
151    # Gather the path for each label and ensure it exists
152    env_labels = dict(json.decode(repository_ctx.attr.env_label.get(host_triple, "{}")))
153    env_labels = {key: repository_ctx.path(Label(value)) for (key, value) in env_labels.items()}
154    for key in env_labels:
155        if not env_labels[key].exists:
156            fail("File for key '{}' does not exist: {}", key, env_labels[key])
157    env_labels = {key: str(value) for (key, value) in env_labels.items()}
158
159    return dict(env_vars.items() + env_labels.items())
160
161def _detect_changes(repository_ctx):
162    """Inspect files that are considered inputs to the build for changes
163
164    Args:
165        repository_ctx (repository_ctx): The rule's context object.
166    """
167    # Simply generating a `path` object consideres the file as 'tracked' or
168    # 'consumed' which means changes to it will trigger rebuilds
169
170    for src in repository_ctx.attr.srcs:
171        repository_ctx.path(src)
172
173    repository_ctx.path(repository_ctx.attr.cargo_lockfile)
174    repository_ctx.path(repository_ctx.attr.cargo_toml)
175
176def _cargo_bootstrap_repository_impl(repository_ctx):
177    # Pretend to Bazel that this rule's input files have been used, so that it will re-run the rule if they change.
178    _detect_changes(repository_ctx)
179
180    # Expects something like `1.56.0`, or `nightly/2021-09-08`.
181    version, _, iso_date = repository_ctx.attr.version.partition("/")
182    if iso_date:
183        channel = version
184        version = iso_date
185    else:
186        channel = "stable"
187
188    host_triple = get_host_triple(repository_ctx)
189    cargo_template = repository_ctx.attr.rust_toolchain_cargo_template
190    rustc_template = repository_ctx.attr.rust_toolchain_rustc_template
191
192    tools = get_rust_tools(
193        cargo_template = cargo_template,
194        rustc_template = rustc_template,
195        host_triple = host_triple,
196        channel = channel,
197        version = version,
198    )
199
200    binary_name = repository_ctx.attr.binary or repository_ctx.name
201
202    # In addition to platform specific environment variables, a common set (indicated by `*`) will always
203    # be gathered.
204    environment = dict(_collect_environ(repository_ctx, "*").items() + _collect_environ(repository_ctx, host_triple.str).items())
205
206    built_binary = cargo_bootstrap(
207        repository_ctx = repository_ctx,
208        cargo_bin = repository_ctx.path(tools.cargo),
209        rustc_bin = repository_ctx.path(tools.rustc),
210        binary = binary_name,
211        cargo_manifest = repository_ctx.path(repository_ctx.attr.cargo_toml),
212        build_mode = repository_ctx.attr.build_mode,
213        environment = environment,
214        timeout = repository_ctx.attr.timeout,
215    )
216
217    # Create a symlink so that the binary can be accesed via it's target name
218    repository_ctx.symlink(built_binary, binary_name)
219
220    repository_ctx.file("BUILD.bazel", _BUILD_FILE_CONTENT.format(
221        binary_name = binary_name,
222        binary = built_binary,
223    ))
224
225cargo_bootstrap_repository = repository_rule(
226    doc = "A rule for bootstrapping a Rust binary using [Cargo](https://doc.rust-lang.org/cargo/)",
227    implementation = _cargo_bootstrap_repository_impl,
228    attrs = {
229        "binary": attr.string(
230            doc = "The binary to build (the `--bin` parameter for Cargo). If left empty, the repository name will be used.",
231        ),
232        "build_mode": attr.string(
233            doc = "The build mode the binary should be built with",
234            values = [
235                "debug",
236                "release",
237            ],
238            default = "release",
239        ),
240        "cargo_lockfile": attr.label(
241            doc = "The lockfile of the crate_universe resolver",
242            allow_single_file = ["Cargo.lock"],
243            mandatory = True,
244        ),
245        "cargo_toml": attr.label(
246            doc = "The path of the crate_universe resolver manifest (`Cargo.toml` file)",
247            allow_single_file = ["Cargo.toml"],
248            mandatory = True,
249        ),
250        "env": attr.string_dict(
251            doc = (
252                "A mapping of platform triple to a set of environment variables. See " +
253                "[cargo_env](#cargo_env) for usage details. Additionally, the platform triple `*` applies to all platforms."
254            ),
255        ),
256        "env_label": attr.string_dict(
257            doc = (
258                "A mapping of platform triple to a set of environment variables. This " +
259                "attribute differs from `env` in that all variables passed here must be " +
260                "fully qualified labels of files. See [cargo_env](#cargo_env) for usage details. " +
261                "Additionally, the platform triple `*` applies to all platforms."
262            ),
263        ),
264        "rust_toolchain_cargo_template": attr.string(
265            doc = (
266                "The template to use for finding the host `cargo` binary. `{version}` (eg. '1.53.0'), " +
267                "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " +
268                "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " +
269                "replaced in the string if present."
270            ),
271            default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}",
272        ),
273        "rust_toolchain_rustc_template": attr.string(
274            doc = (
275                "The template to use for finding the host `rustc` binary. `{version}` (eg. '1.53.0'), " +
276                "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " +
277                "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " +
278                "replaced in the string if present."
279            ),
280            default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}",
281        ),
282        "srcs": attr.label_list(
283            doc = "Souce files of the crate to build. Passing source files here can be used to trigger rebuilds when changes are made",
284            allow_files = True,
285        ),
286        "timeout": attr.int(
287            doc = "Maximum duration of the Cargo build command in seconds",
288            default = 600,
289        ),
290        "version": attr.string(
291            doc = "The version of Rust the currently registered toolchain is using. Eg. `1.56.0`, or `nightly/2021-09-08`",
292            default = rust_common.default_version,
293        ),
294    },
295)
296
297def cargo_env(env):
298    """A helper for generating platform specific environment variables
299
300    ```python
301    load("@rules_rust//rust:defs.bzl", "rust_common")
302    load("@rules_rust//cargo:defs.bzl", "cargo_bootstrap_repository", "cargo_env")
303
304    cargo_bootstrap_repository(
305        name = "bootstrapped_bin",
306        cargo_lockfile = "//:Cargo.lock",
307        cargo_toml = "//:Cargo.toml",
308        srcs = ["//:resolver_srcs"],
309        version = rust_common.default_version,
310        binary = "my-crate-binary",
311        env = {
312            "x86_64-unknown-linux-gnu": cargo_env({
313                "FOO": "BAR",
314            }),
315        },
316        env_label = {
317            "aarch64-unknown-linux-musl": cargo_env({
318                "DOC": "//:README.md",
319            }),
320        }
321    )
322    ```
323
324    Args:
325        env (dict): A map of environment variables
326
327    Returns:
328        str: A json encoded string of the environment variables
329    """
330    return json.encode(dict(env))
331