xref: /aosp_15_r20/external/bazelbuild-rules_rust/crate_universe/private/crates_vendor.bzl (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1"""Rules for vendoring Bazel targets into existing workspaces"""
2
3load("//crate_universe/private:generate_utils.bzl", "compile_config", "render_config")
4load("//crate_universe/private:splicing_utils.bzl", "kebab_case_keys", generate_splicing_config = "splicing_config")
5load("//crate_universe/private:urls.bzl", "CARGO_BAZEL_LABEL")
6load("//rust/platform:triple_mappings.bzl", "SUPPORTED_PLATFORM_TRIPLES")
7
8_UNIX_WRAPPER = """\
9#!/usr/bin/env bash
10set -euo pipefail
11export RUNTIME_PWD="$(pwd)"
12if [[ -z "${{BAZEL_REAL:-}}" ]]; then
13    BAZEL_REAL="$(which bazel || echo 'bazel')"
14fi
15
16# The path needs to be preserved to prevent bazel from starting with different
17# startup options (requiring a restart of bazel).
18# If you provide an empty path, bazel starts itself with
19# --default_system_javabase set to the empty string, but if you provide a path,
20# it may set it to a value (eg. "/usr/local/buildtools/java/jdk11").
21exec env - BAZEL_REAL="${{BAZEL_REAL}}" BUILD_WORKSPACE_DIRECTORY="${{BUILD_WORKSPACE_DIRECTORY}}" PATH="${{PATH}}" {env} \\
22"{bin}" {args} "$@"
23"""
24
25_WINDOWS_WRAPPER = """\
26@ECHO OFF
27set RUNTIME_PWD=%CD%
28{env}
29
30call {bin} {args} %@%
31"""
32
33CARGO_BAZEL_GENERATOR_PATH = "CARGO_BAZEL_GENERATOR_PATH"
34
35def _default_render_config():
36    return json.decode(render_config())
37
38def _runfiles_path(file, is_windows):
39    if is_windows:
40        runtime_pwd_var = "%RUNTIME_PWD%"
41    else:
42        runtime_pwd_var = "${RUNTIME_PWD}"
43
44    return "{}/{}".format(runtime_pwd_var, file.short_path)
45
46def _is_windows(ctx):
47    toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain_type")]
48    return toolchain.target_os == "windows"
49
50def _get_output_package(ctx):
51    # Determine output directory
52    if ctx.attr.vendor_path.startswith("/"):
53        output = ctx.attr.vendor_path
54    else:
55        output = "{}/{}".format(
56            ctx.label.package,
57            ctx.attr.vendor_path,
58        )
59    return output.lstrip("/")
60
61def _write_data_file(ctx, name, data):
62    file = ctx.actions.declare_file("{}.{}".format(ctx.label.name, name))
63    ctx.actions.write(
64        output = file,
65        content = data,
66    )
67    return file
68
69def _prepare_manifest_path(target):
70    """Generate manifest paths that are resolvable by `cargo_bazel::SplicingManifest::resolve`
71
72    Args:
73        target (Target): A `crate_vendor.manifest` target
74
75    Returns:
76        str: A string representing the path to a manifest.
77    """
78    files = target[DefaultInfo].files.to_list()
79    if len(files) != 1:
80        fail("The manifest {} hand an unexpected number of files: {}".format(
81            target.label,
82            files,
83        ))
84
85    manifest = files[0]
86
87    if target.label.workspace_root.startswith("external"):
88        # The short path of an external file is expected to start with `../`
89        if not manifest.short_path.startswith("../"):
90            fail("Unexpected shortpath for {}: {}".format(
91                manifest,
92                manifest.short_path,
93            ))
94        return manifest.short_path.replace("../", "${output_base}/external/", 1)
95
96    return "${build_workspace_directory}/" + manifest.short_path
97
98def _write_splicing_manifest(ctx):
99    # Manifests are required to be single files
100    manifests = {_prepare_manifest_path(m): str(m.label) for m in ctx.attr.manifests}
101
102    manifest = _write_data_file(
103        ctx = ctx,
104        name = "cargo-bazel-splicing-manifest.json",
105        data = generate_splicing_manifest(
106            packages = ctx.attr.packages,
107            splicing_config = ctx.attr.splicing_config,
108            cargo_config = ctx.attr.cargo_config,
109            manifests = manifests,
110            manifest_to_path = _prepare_manifest_path,
111        ),
112    )
113
114    is_windows = _is_windows(ctx)
115
116    args = ["--splicing-manifest", _runfiles_path(manifest, is_windows)]
117    runfiles = [manifest] + ctx.files.manifests + ([ctx.file.cargo_config] if ctx.attr.cargo_config else [])
118    return args, runfiles
119
120def generate_splicing_manifest(packages, splicing_config, cargo_config, manifests, manifest_to_path):
121    # Deserialize information about direct packages
122    direct_packages_info = {
123        # Ensure the data is using kebab-case as that's what `cargo_toml::DependencyDetail` expects.
124        pkg: kebab_case_keys(dict(json.decode(data)))
125        for (pkg, data) in packages.items()
126    }
127
128    config = json.decode(splicing_config or generate_splicing_config())
129    splicing_manifest_content = {
130        "cargo_config": str(manifest_to_path(cargo_config)) if cargo_config else None,
131        "direct_packages": direct_packages_info,
132        "manifests": manifests,
133    }
134
135    return json.encode_indent(
136        dict(dict(config).items() + splicing_manifest_content.items()),
137        indent = " " * 4,
138    )
139
140def _write_config_file(ctx):
141    workspace_name = ctx.workspace_name
142    if workspace_name == "__main__" or ctx.workspace_name == "_main":
143        workspace_name = ""
144
145    config = _write_data_file(
146        ctx = ctx,
147        name = "cargo-bazel-config.json",
148        data = generate_config_file(
149            ctx,
150            mode = ctx.attr.mode,
151            annotations = ctx.attr.annotations,
152            generate_binaries = ctx.attr.generate_binaries,
153            generate_build_scripts = ctx.attr.generate_build_scripts,
154            generate_target_compatible_with = ctx.attr.generate_target_compatible_with,
155            supported_platform_triples = ctx.attr.supported_platform_triples,
156            repository_name = ctx.attr.repository_name,
157            output_pkg = _get_output_package(ctx),
158            workspace_name = workspace_name,
159            render_config = dict(json.decode(ctx.attr.render_config)) if ctx.attr.render_config else None,
160        ),
161    )
162
163    is_windows = _is_windows(ctx)
164    args = ["--config", _runfiles_path(config, is_windows)]
165    runfiles = [config] + ctx.files.manifests
166    return args, runfiles
167
168def generate_config_file(
169        ctx,
170        mode,
171        annotations,
172        generate_binaries,
173        generate_build_scripts,
174        generate_target_compatible_with,
175        supported_platform_triples,
176        repository_name,
177        output_pkg,
178        workspace_name,
179        render_config,
180        repository_ctx = None):
181    """Writes the rendering config to cargo-bazel-config.json.
182
183    Args:
184        ctx: The rule's context.
185        mode (str): The vendoring mode.
186        annotations: Any annotations provided.
187        generate_binaries (bool): Whether to generate binaries for the crates.
188        generate_build_scripts (bool): Whether to generate BUILD.bazel files.
189        generate_target_compatible_with (bool): DEPRECATED: Moved to `render_config`.
190        supported_platform_triples (str): The platform triples to support in
191            the generated BUILD.bazel files.
192        repository_name (str): The name of the repository to generate.
193        output_pkg: The path to the package containing the build files.
194        workspace_name (str): The name of the workspace.
195        render_config: The render config to use.
196        repository_ctx (repository_ctx, optional): A repository context object
197            used for enabling certain functionality.
198
199    Returns:
200        file: The cargo-bazel-config.json written.
201    """
202    default_render_config = _default_render_config()
203    if render_config == None:
204        render_config = default_render_config
205
206    if mode == "local":
207        build_file_base_template = "//{}/{{name}}-{{version}}:BUILD.bazel".format(output_pkg)
208        if workspace_name != "":
209            build_file_base_template = "@{}//{}/{{name}}-{{version}}:BUILD.bazel".format(workspace_name, output_pkg)
210        crate_label_template = "//{}/{{name}}-{{version}}:{{target}}".format(
211            output_pkg,
212        )
213    else:
214        build_file_base_template = "//{}:BUILD.{{name}}-{{version}}.bazel".format(output_pkg)
215        if workspace_name != "":
216            build_file_base_template = "@{}//{}:BUILD.{{name}}-{{version}}.bazel".format(workspace_name, output_pkg)
217        crate_label_template = render_config["crate_label_template"]
218
219    # If `workspace_name` is blank (such as when using modules), the `@{}//{}:{{file}}` template would generate
220    # a reference like `Label(@//<stuff>)`. This causes issues if the module doing the `crates_vendor`ing is not the root module.
221    # See: https://github.com/bazelbuild/rules_rust/issues/2661
222    crates_module_template_value = "//{}:{{file}}".format(output_pkg)
223    if workspace_name != "":
224        crates_module_template_value = "@{}//{}:{{file}}".format(
225            workspace_name,
226            output_pkg,
227        )
228
229    updates = {
230        "build_file_template": build_file_base_template,
231        "crate_label_template": crate_label_template,
232        "crates_module_template": crates_module_template_value,
233        "vendor_mode": mode,
234    }
235
236    # "crate_label_template" is explicitly supported above in non-local modes
237    excluded_from_key_check = ["crate_label_template"]
238
239    for key in updates:
240        if (render_config[key] != default_render_config[key]) and key not in excluded_from_key_check:
241            fail("The `crates_vendor.render_config` attribute does not support the `{}` parameter. Please update {} to remove this value.".format(
242                key,
243                ctx.label,
244            ))
245
246    render_config.update(updates)
247
248    # Allow users to override the regen command.
249    if "regen_command" not in render_config or not render_config["regen_command"]:
250        render_config.update({"regen_command": "bazel run {}".format(ctx.label)})
251
252    config_data = compile_config(
253        crate_annotations = annotations,
254        generate_binaries = generate_binaries,
255        generate_build_scripts = generate_build_scripts,
256        generate_target_compatible_with = generate_target_compatible_with,
257        cargo_config = None,
258        render_config = render_config,
259        supported_platform_triples = supported_platform_triples,
260        repository_name = repository_name or ctx.label.name,
261        repository_ctx = repository_ctx,
262    )
263
264    return json.encode_indent(
265        config_data,
266        indent = " " * 4,
267    )
268
269def _crates_vendor_impl(ctx):
270    toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain_type")]
271    is_windows = _is_windows(ctx)
272
273    environ = {
274        "CARGO": _runfiles_path(toolchain.cargo, is_windows),
275        "RUSTC": _runfiles_path(toolchain.rustc, is_windows),
276    }
277
278    args = ["vendor"]
279
280    cargo_bazel_runfiles = []
281
282    # Allow action envs to override the use of the cargo-bazel target.
283    if CARGO_BAZEL_GENERATOR_PATH in ctx.var:
284        bin_path = ctx.var[CARGO_BAZEL_GENERATOR_PATH]
285    elif ctx.executable.cargo_bazel:
286        bin_path = _runfiles_path(ctx.executable.cargo_bazel, is_windows)
287        cargo_bazel_runfiles.append(ctx.executable.cargo_bazel)
288    else:
289        fail("{} is missing either the `cargo_bazel` attribute or the '{}' action env".format(
290            ctx.label,
291            CARGO_BAZEL_GENERATOR_PATH,
292        ))
293
294    # Generate config file
295    config_args, config_runfiles = _write_config_file(ctx)
296    args.extend(config_args)
297    cargo_bazel_runfiles.extend(config_runfiles)
298
299    # Generate splicing manifest
300    splicing_manifest_args, splicing_manifest_runfiles = _write_splicing_manifest(ctx)
301    args.extend(splicing_manifest_args)
302    cargo_bazel_runfiles.extend(splicing_manifest_runfiles)
303
304    # Add an optional `Cargo.lock` file.
305    if ctx.attr.cargo_lockfile:
306        args.extend([
307            "--cargo-lockfile",
308            _runfiles_path(ctx.file.cargo_lockfile, is_windows),
309        ])
310        cargo_bazel_runfiles.extend([ctx.file.cargo_lockfile])
311
312    # Optionally include buildifier
313    if ctx.attr.buildifier:
314        args.extend(["--buildifier", _runfiles_path(ctx.executable.buildifier, is_windows)])
315        cargo_bazel_runfiles.append(ctx.executable.buildifier)
316
317    # Optionally include an explicit `bazel` path
318    if ctx.attr.bazel:
319        args.extend(["--bazel", _runfiles_path(ctx.executable.bazel, is_windows)])
320        cargo_bazel_runfiles.append(ctx.executable.bazel)
321
322    # Determine platform specific settings
323    if is_windows:
324        extension = ".bat"
325        template = _WINDOWS_WRAPPER
326        env_template = "\nset {}={}"
327    else:
328        extension = ".sh"
329        template = _UNIX_WRAPPER
330        env_template = "{}={}"
331
332    # Write the wrapper script
333    runner = ctx.actions.declare_file(ctx.label.name + extension)
334    ctx.actions.write(
335        output = runner,
336        content = template.format(
337            env = " ".join([env_template.format(key, val) for key, val in environ.items()]),
338            bin = bin_path,
339            args = " ".join(args),
340        ),
341        is_executable = True,
342    )
343
344    return DefaultInfo(
345        files = depset([runner]),
346        runfiles = ctx.runfiles(
347            files = cargo_bazel_runfiles,
348            transitive_files = toolchain.all_files,
349        ),
350        executable = runner,
351    )
352
353CRATES_VENDOR_ATTRS = {
354    "annotations": attr.string_list_dict(
355        doc = "Extra settings to apply to crates. See [crate.annotation](#crateannotation).",
356    ),
357    "bazel": attr.label(
358        doc = "The path to a bazel binary used to locate the output_base for the current workspace.",
359        cfg = "exec",
360        executable = True,
361        allow_files = True,
362    ),
363    "buildifier": attr.label(
364        doc = "The path to a [buildifier](https://github.com/bazelbuild/buildtools/blob/5.0.1/buildifier/README.md) binary used to format generated BUILD files.",
365        cfg = "exec",
366        executable = True,
367        allow_files = True,
368        default = Label("//crate_universe/private/vendor:buildifier"),
369    ),
370    "cargo_bazel": attr.label(
371        doc = (
372            "The cargo-bazel binary to use for vendoring. If this attribute is not set, then a " +
373            "`{}` action env will be used.".format(CARGO_BAZEL_GENERATOR_PATH)
374        ),
375        cfg = "exec",
376        executable = True,
377        allow_files = True,
378        default = CARGO_BAZEL_LABEL,
379    ),
380    "cargo_config": attr.label(
381        doc = "A [Cargo configuration](https://doc.rust-lang.org/cargo/reference/config.html) file.",
382        allow_single_file = True,
383    ),
384    "cargo_lockfile": attr.label(
385        doc = "The path to an existing `Cargo.lock` file",
386        allow_single_file = True,
387    ),
388    "generate_binaries": attr.bool(
389        doc = (
390            "Whether to generate `rust_binary` targets for all the binary crates in every package. " +
391            "By default only the `rust_library` targets are generated."
392        ),
393        default = False,
394    ),
395    "generate_build_scripts": attr.bool(
396        doc = (
397            "Whether or not to generate " +
398            "[cargo build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) by default."
399        ),
400        default = True,
401    ),
402    "generate_target_compatible_with": attr.bool(
403        doc = "DEPRECATED: Moved to `render_config`.",
404        default = True,
405    ),
406    "manifests": attr.label_list(
407        doc = "A list of Cargo manifests (`Cargo.toml` files).",
408        allow_files = ["Cargo.toml"],
409    ),
410    "mode": attr.string(
411        doc = (
412            "Flags determining how crates should be vendored. `local` is where crate source and BUILD files are " +
413            "written to the repository. `remote` is where only BUILD files are written and repository rules " +
414            "used to fetch source code."
415        ),
416        values = [
417            "local",
418            "remote",
419        ],
420        default = "remote",
421    ),
422    "packages": attr.string_dict(
423        doc = "A set of crates (packages) specifications to depend on. See [crate.spec](#crate.spec).",
424    ),
425    "render_config": attr.string(
426        doc = (
427            "The configuration flags to use for rendering. Use `//crate_universe:defs.bzl\\%render_config` to " +
428            "generate the value for this field. If unset, the defaults defined there will be used."
429        ),
430    ),
431    "repository_name": attr.string(
432        doc = "The name of the repository to generate for `remote` vendor modes. If unset, the label name will be used",
433    ),
434    "splicing_config": attr.string(
435        doc = (
436            "The configuration flags to use for splicing Cargo maniests. Use `//crate_universe:defs.bzl\\%rsplicing_config` to " +
437            "generate the value for this field. If unset, the defaults defined there will be used."
438        ),
439    ),
440    "supported_platform_triples": attr.string_list(
441        doc = "A set of all platform triples to consider when generating dependencies.",
442        default = SUPPORTED_PLATFORM_TRIPLES,
443    ),
444    "vendor_path": attr.string(
445        doc = "The path to a directory to write files into. Absolute paths will be treated as relative to the workspace root",
446        default = "crates",
447    ),
448}
449
450crates_vendor = rule(
451    implementation = _crates_vendor_impl,
452    doc = """\
453A rule for defining Rust dependencies (crates) and writing targets for them to the current workspace.
454This rule is useful for users whose workspaces are expected to be consumed in other workspaces as the
455rendered `BUILD` files reduce the number of workspace dependencies, allowing for easier loads. This rule
456handles all the same [workflows](#workflows) `crate_universe` rules do.
457
458Example:
459
460Given the following workspace structure:
461
462```text
463[workspace]/
464    WORKSPACE
465    BUILD
466    Cargo.toml
467    3rdparty/
468        BUILD
469    src/
470        main.rs
471```
472
473The following is something that'd be found in `3rdparty/BUILD`:
474
475```python
476load("@rules_rust//crate_universe:defs.bzl", "crates_vendor", "crate")
477
478crates_vendor(
479    name = "crates_vendor",
480    annotations = {
481        "rand": [crate.annotation(
482            default_features = False,
483            features = ["small_rng"],
484        )],
485    },
486    cargo_lockfile = "//:Cargo.Bazel.lock",
487    manifests = ["//:Cargo.toml"],
488    mode = "remote",
489    vendor_path = "crates",
490    tags = ["manual"],
491)
492```
493
494The above creates a target that can be run to write `BUILD` files into the `3rdparty`
495directory next to where the target is defined. To run it, simply call:
496
497```shell
498bazel run //3rdparty:crates_vendor
499```
500
501<a id="#crates_vendor_repinning_updating_dependencies"></a>
502
503### Repinning / Updating Dependencies
504
505Repinning dependencies is controlled by both the `CARGO_BAZEL_REPIN` environment variable or the `--repin`
506flag to the `crates_vendor` binary. To update dependencies, simply add the flag ro your `bazel run` invocation.
507
508```shell
509bazel run //3rdparty:crates_vendor -- --repin
510```
511
512Under the hood, `--repin` will trigger a [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html)
513call against the generated workspace. The following table describes how to control particular values passed to the
514`cargo update` command.
515
516| Value | Cargo command |
517| --- | --- |
518| Any of [`true`, `1`, `yes`, `on`, `workspace`] | `cargo update --workspace` |
519| Any of [`full`, `eager`, `all`] | `cargo update` |
520| `package_name` | `cargo upgrade --package package_name` |
521| `[email protected]` | `cargo upgrade --package package_name --precise 1.2.3` |
522
523""",
524    attrs = CRATES_VENDOR_ATTRS,
525    executable = True,
526    toolchains = ["@rules_rust//rust:toolchain_type"],
527)
528
529def _crates_vendor_remote_repository_impl(repository_ctx):
530    build_file = repository_ctx.path(repository_ctx.attr.build_file)
531    defs_module = repository_ctx.path(repository_ctx.attr.defs_module)
532
533    repository_ctx.file("BUILD.bazel", repository_ctx.read(build_file))
534    repository_ctx.file("defs.bzl", repository_ctx.read(defs_module))
535    repository_ctx.file("crates.bzl", "")
536    repository_ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format(
537        repository_ctx.name,
538    ))
539
540crates_vendor_remote_repository = repository_rule(
541    doc = "Creates a repository paired with `crates_vendor` targets using the `remote` vendor mode.",
542    implementation = _crates_vendor_remote_repository_impl,
543    attrs = {
544        "build_file": attr.label(
545            doc = "The BUILD file to use for the root package",
546            mandatory = True,
547        ),
548        "defs_module": attr.label(
549            doc = "The `defs.bzl` file to use in the repository",
550            mandatory = True,
551        ),
552    },
553)
554