• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    if repository_ctx.attr.version in ("beta", "nightly"):
181        channel = repository_ctx.attr.version
182        version = repository_ctx.attr.iso_date
183    else:
184        channel = "stable"
185        version = repository_ctx.attr.version
186
187    host_triple = get_host_triple(repository_ctx)
188    cargo_template = repository_ctx.attr.rust_toolchain_cargo_template
189    rustc_template = repository_ctx.attr.rust_toolchain_rustc_template
190
191    tools = get_rust_tools(
192        cargo_template = cargo_template,
193        rustc_template = rustc_template,
194        host_triple = host_triple,
195        channel = channel,
196        version = version,
197    )
198
199    binary_name = repository_ctx.attr.binary or repository_ctx.name
200
201    # In addition to platform specific environment variables, a common set (indicated by `*`) will always
202    # be gathered.
203    environment = dict(_collect_environ(repository_ctx, "*").items() + _collect_environ(repository_ctx, host_triple.str).items())
204
205    built_binary = cargo_bootstrap(
206        repository_ctx = repository_ctx,
207        cargo_bin = repository_ctx.path(tools.cargo),
208        rustc_bin = repository_ctx.path(tools.rustc),
209        binary = binary_name,
210        cargo_manifest = repository_ctx.path(repository_ctx.attr.cargo_toml),
211        build_mode = repository_ctx.attr.build_mode,
212        environment = environment,
213        timeout = repository_ctx.attr.timeout,
214    )
215
216    # Create a symlink so that the binary can be accesed via it's target name
217    repository_ctx.symlink(built_binary, binary_name)
218
219    repository_ctx.file("BUILD.bazel", _BUILD_FILE_CONTENT.format(
220        binary_name = binary_name,
221        binary = built_binary,
222    ))
223
224cargo_bootstrap_repository = repository_rule(
225    doc = "A rule for bootstrapping a Rust binary using [Cargo](https://doc.rust-lang.org/cargo/)",
226    implementation = _cargo_bootstrap_repository_impl,
227    attrs = {
228        "binary": attr.string(
229            doc = "The binary to build (the `--bin` parameter for Cargo). If left empty, the repository name will be used.",
230        ),
231        "build_mode": attr.string(
232            doc = "The build mode the binary should be built with",
233            values = [
234                "debug",
235                "release",
236            ],
237            default = "release",
238        ),
239        "cargo_lockfile": attr.label(
240            doc = "The lockfile of the crate_universe resolver",
241            allow_single_file = ["Cargo.lock"],
242            mandatory = True,
243        ),
244        "cargo_toml": attr.label(
245            doc = "The path of the crate_universe resolver manifest (`Cargo.toml` file)",
246            allow_single_file = ["Cargo.toml"],
247            mandatory = True,
248        ),
249        "env": attr.string_dict(
250            doc = (
251                "A mapping of platform triple to a set of environment variables. See " +
252                "[cargo_env](#cargo_env) for usage details. Additionally, the platform triple `*` applies to all platforms."
253            ),
254        ),
255        "env_label": attr.string_dict(
256            doc = (
257                "A mapping of platform triple to a set of environment variables. This " +
258                "attribute differs from `env` in that all variables passed here must be " +
259                "fully qualified labels of files. See [cargo_env](#cargo_env) for usage details. " +
260                "Additionally, the platform triple `*` applies to all platforms."
261            ),
262        ),
263        "iso_date": attr.string(
264            doc = "The iso_date of cargo binary the resolver should use. Note: This can only be set if `version` is `beta` or `nightly`",
265        ),
266        "rust_toolchain_cargo_template": attr.string(
267            doc = (
268                "The template to use for finding the host `cargo` binary. `{version}` (eg. '1.53.0'), " +
269                "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " +
270                "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " +
271                "replaced in the string if present."
272            ),
273            default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}",
274        ),
275        "rust_toolchain_rustc_template": attr.string(
276            doc = (
277                "The template to use for finding the host `rustc` binary. `{version}` (eg. '1.53.0'), " +
278                "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " +
279                "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " +
280                "replaced in the string if present."
281            ),
282            default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}",
283        ),
284        "srcs": attr.label_list(
285            doc = "Souce files of the crate to build. Passing source files here can be used to trigger rebuilds when changes are made",
286            allow_files = True,
287        ),
288        "timeout": attr.int(
289            doc = "Maximum duration of the Cargo build command in seconds",
290            default = 600,
291        ),
292        "version": attr.string(
293            doc = "The version of cargo the resolver should use",
294            default = rust_common.default_version,
295        ),
296        "_cc_toolchain": attr.label(
297            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
298        ),
299    },
300)
301
302def cargo_env(env):
303    """A helper for generating platform specific environment variables
304
305    ```python
306    load("@rules_rust//rust:defs.bzl", "rust_common")
307    load("@rules_rust//cargo:defs.bzl", "cargo_bootstrap_repository", "cargo_env")
308
309    cargo_bootstrap_repository(
310        name = "bootstrapped_bin",
311        cargo_lockfile = "//:Cargo.lock",
312        cargo_toml = "//:Cargo.toml",
313        srcs = ["//:resolver_srcs"],
314        version = rust_common.default_version,
315        binary = "my-crate-binary",
316        env = {
317            "x86_64-unknown-linux-gnu": cargo_env({
318                "FOO": "BAR",
319            }),
320        },
321        env_label = {
322            "aarch64-unknown-linux-musl": cargo_env({
323                "DOC": "//:README.md",
324            }),
325        }
326    )
327    ```
328
329    Args:
330        env (dict): A map of environment variables
331
332    Returns:
333        str: A json encoded string of the environment variables
334    """
335    return json.encode(dict(env))
336