• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 packges
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"
208        crate_label_template = "//{}/{{name}}-{{version}}:{{target}}".format(
209            output_pkg,
210        )
211    else:
212        build_file_base_template = "@{}//{}:BUILD.{{name}}-{{version}}.bazel"
213        crate_label_template = render_config["crate_label_template"]
214
215    updates = {
216        "build_file_template": build_file_base_template.format(
217            workspace_name,
218            output_pkg,
219        ),
220        "crate_label_template": crate_label_template,
221        "crates_module_template": "@{}//{}:{{file}}".format(
222            workspace_name,
223            output_pkg,
224        ),
225        "vendor_mode": mode,
226    }
227
228    # "crate_label_template" is explicitly supported above in non-local modes
229    excluded_from_key_check = ["crate_label_template"]
230
231    for key in updates:
232        if (render_config[key] != default_render_config[key]) and key not in excluded_from_key_check:
233            fail("The `crates_vendor.render_config` attribute does not support the `{}` parameter. Please update {} to remove this value.".format(
234                key,
235                ctx.label,
236            ))
237
238    render_config.update(updates)
239
240    # Allow users to override the regen command.
241    if "regen_command" not in render_config or not render_config["regen_command"]:
242        render_config.update({"regen_command": "bazel run {}".format(ctx.label)})
243
244    config_data = compile_config(
245        crate_annotations = annotations,
246        generate_binaries = generate_binaries,
247        generate_build_scripts = generate_build_scripts,
248        generate_target_compatible_with = generate_target_compatible_with,
249        cargo_config = None,
250        render_config = render_config,
251        supported_platform_triples = supported_platform_triples,
252        repository_name = repository_name or ctx.label.name,
253        repository_ctx = repository_ctx,
254    )
255
256    return json.encode_indent(
257        config_data,
258        indent = " " * 4,
259    )
260
261def _crates_vendor_impl(ctx):
262    toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain_type")]
263    is_windows = _is_windows(ctx)
264
265    environ = {
266        "CARGO": _runfiles_path(toolchain.cargo, is_windows),
267        "RUSTC": _runfiles_path(toolchain.rustc, is_windows),
268    }
269
270    args = ["vendor"]
271
272    cargo_bazel_runfiles = []
273
274    # Allow action envs to override the use of the cargo-bazel target.
275    if CARGO_BAZEL_GENERATOR_PATH in ctx.var:
276        bin_path = ctx.var[CARGO_BAZEL_GENERATOR_PATH]
277    elif ctx.executable.cargo_bazel:
278        bin_path = _runfiles_path(ctx.executable.cargo_bazel, is_windows)
279        cargo_bazel_runfiles.append(ctx.executable.cargo_bazel)
280    else:
281        fail("{} is missing either the `cargo_bazel` attribute or the '{}' action env".format(
282            ctx.label,
283            CARGO_BAZEL_GENERATOR_PATH,
284        ))
285
286    # Generate config file
287    config_args, config_runfiles = _write_config_file(ctx)
288    args.extend(config_args)
289    cargo_bazel_runfiles.extend(config_runfiles)
290
291    # Generate splicing manifest
292    splicing_manifest_args, splicing_manifest_runfiles = _write_splicing_manifest(ctx)
293    args.extend(splicing_manifest_args)
294    cargo_bazel_runfiles.extend(splicing_manifest_runfiles)
295
296    # Add an optional `Cargo.lock` file.
297    if ctx.attr.cargo_lockfile:
298        args.extend([
299            "--cargo-lockfile",
300            _runfiles_path(ctx.file.cargo_lockfile, is_windows),
301        ])
302        cargo_bazel_runfiles.extend([ctx.file.cargo_lockfile])
303
304    # Optionally include buildifier
305    if ctx.attr.buildifier:
306        args.extend(["--buildifier", _runfiles_path(ctx.executable.buildifier, is_windows)])
307        cargo_bazel_runfiles.append(ctx.executable.buildifier)
308
309    # Optionally include an explicit `bazel` path
310    if ctx.attr.bazel:
311        args.extend(["--bazel", _runfiles_path(ctx.executable.bazel, is_windows)])
312        cargo_bazel_runfiles.append(ctx.executable.bazel)
313
314    # Determine platform specific settings
315    if is_windows:
316        extension = ".bat"
317        template = _WINDOWS_WRAPPER
318        env_template = "\nset {}={}"
319    else:
320        extension = ".sh"
321        template = _UNIX_WRAPPER
322        env_template = "{}={}"
323
324    # Write the wrapper script
325    runner = ctx.actions.declare_file(ctx.label.name + extension)
326    ctx.actions.write(
327        output = runner,
328        content = template.format(
329            env = " ".join([env_template.format(key, val) for key, val in environ.items()]),
330            bin = bin_path,
331            args = " ".join(args),
332        ),
333        is_executable = True,
334    )
335
336    return DefaultInfo(
337        files = depset([runner]),
338        runfiles = ctx.runfiles(
339            files = cargo_bazel_runfiles,
340            transitive_files = toolchain.all_files,
341        ),
342        executable = runner,
343    )
344
345CRATES_VENDOR_ATTRS = {
346    "annotations": attr.string_list_dict(
347        doc = "Extra settings to apply to crates. See [crate.annotation](#crateannotation).",
348    ),
349    "bazel": attr.label(
350        doc = "The path to a bazel binary used to locate the output_base for the current workspace.",
351        cfg = "exec",
352        executable = True,
353        allow_files = True,
354    ),
355    "buildifier": attr.label(
356        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.",
357        cfg = "exec",
358        executable = True,
359        allow_files = True,
360        default = Label("//crate_universe/private/vendor:buildifier"),
361    ),
362    "cargo_bazel": attr.label(
363        doc = (
364            "The cargo-bazel binary to use for vendoring. If this attribute is not set, then a " +
365            "`{}` action env will be used.".format(CARGO_BAZEL_GENERATOR_PATH)
366        ),
367        cfg = "exec",
368        executable = True,
369        allow_files = True,
370        default = CARGO_BAZEL_LABEL,
371    ),
372    "cargo_config": attr.label(
373        doc = "A [Cargo configuration](https://doc.rust-lang.org/cargo/reference/config.html) file.",
374        allow_single_file = True,
375    ),
376    "cargo_lockfile": attr.label(
377        doc = "The path to an existing `Cargo.lock` file",
378        allow_single_file = True,
379    ),
380    "generate_binaries": attr.bool(
381        doc = (
382            "Whether to generate `rust_binary` targets for all the binary crates in every package. " +
383            "By default only the `rust_library` targets are generated."
384        ),
385        default = False,
386    ),
387    "generate_build_scripts": attr.bool(
388        doc = (
389            "Whether or not to generate " +
390            "[cargo build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) by default."
391        ),
392        default = True,
393    ),
394    "generate_target_compatible_with": attr.bool(
395        doc = "DEPRECATED: Moved to `render_config`.",
396        default = True,
397    ),
398    "manifests": attr.label_list(
399        doc = "A list of Cargo manifests (`Cargo.toml` files).",
400        allow_files = ["Cargo.toml"],
401    ),
402    "mode": attr.string(
403        doc = (
404            "Flags determining how crates should be vendored. `local` is where crate source and BUILD files are " +
405            "written to the repository. `remote` is where only BUILD files are written and repository rules " +
406            "used to fetch source code."
407        ),
408        values = [
409            "local",
410            "remote",
411        ],
412        default = "remote",
413    ),
414    "packages": attr.string_dict(
415        doc = "A set of crates (packages) specifications to depend on. See [crate.spec](#crate.spec).",
416    ),
417    "render_config": attr.string(
418        doc = (
419            "The configuration flags to use for rendering. Use `//crate_universe:defs.bzl\\%render_config` to " +
420            "generate the value for this field. If unset, the defaults defined there will be used."
421        ),
422    ),
423    "repository_name": attr.string(
424        doc = "The name of the repository to generate for `remote` vendor modes. If unset, the label name will be used",
425    ),
426    "splicing_config": attr.string(
427        doc = (
428            "The configuration flags to use for splicing Cargo maniests. Use `//crate_universe:defs.bzl\\%rsplicing_config` to " +
429            "generate the value for this field. If unset, the defaults defined there will be used."
430        ),
431    ),
432    "supported_platform_triples": attr.string_list(
433        doc = "A set of all platform triples to consider when generating dependencies.",
434        default = SUPPORTED_PLATFORM_TRIPLES,
435    ),
436    "vendor_path": attr.string(
437        doc = "The path to a directory to write files into. Absolute paths will be treated as relative to the workspace root",
438        default = "crates",
439    ),
440}
441
442crates_vendor = rule(
443    implementation = _crates_vendor_impl,
444    doc = """\
445A rule for defining Rust dependencies (crates) and writing targets for them to the current workspace.
446This rule is useful for users whose workspaces are expected to be consumed in other workspaces as the
447rendered `BUILD` files reduce the number of workspace dependencies, allowing for easier loads. This rule
448handles all the same [workflows](#workflows) `crate_universe` rules do.
449
450Example:
451
452Given the following workspace structure:
453
454```text
455[workspace]/
456    WORKSPACE
457    BUILD
458    Cargo.toml
459    3rdparty/
460        BUILD
461    src/
462        main.rs
463```
464
465The following is something that'd be found in `3rdparty/BUILD`:
466
467```python
468load("@rules_rust//crate_universe:defs.bzl", "crates_vendor", "crate")
469
470crates_vendor(
471    name = "crates_vendor",
472    annotations = {
473        "rand": [crate.annotation(
474            default_features = False,
475            features = ["small_rng"],
476        )],
477    },
478    cargo_lockfile = "//:Cargo.Bazel.lock",
479    manifests = ["//:Cargo.toml"],
480    mode = "remote",
481    vendor_path = "crates",
482    tags = ["manual"],
483)
484```
485
486The above creates a target that can be run to write `BUILD` files into the `3rdparty`
487directory next to where the target is defined. To run it, simply call:
488
489```shell
490bazel run //3rdparty:crates_vendor
491```
492
493<a id="#crates_vendor_repinning_updating_dependencies"></a>
494
495### Repinning / Updating Dependencies
496
497Repinning dependencies is controlled by both the `CARGO_BAZEL_REPIN` environment variable or the `--repin`
498flag to the `crates_vendor` binary. To update dependencies, simply add the flag ro your `bazel run` invocation.
499
500```shell
501bazel run //3rdparty:crates_vendor -- --repin
502```
503
504Under the hood, `--repin` will trigger a [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html)
505call against the generated workspace. The following table describes how to control particular values passed to the
506`cargo update` command.
507
508| Value | Cargo command |
509| --- | --- |
510| Any of [`true`, `1`, `yes`, `on`, `workspace`] | `cargo update --workspace` |
511| Any of [`full`, `eager`, `all`] | `cargo update` |
512| `package_name` | `cargo upgrade --package package_name` |
513| `package_name@1.2.3` | `cargo upgrade --package package_name --precise 1.2.3` |
514
515""",
516    attrs = CRATES_VENDOR_ATTRS,
517    executable = True,
518    toolchains = ["@rules_rust//rust:toolchain_type"],
519)
520
521def _crates_vendor_remote_repository_impl(repository_ctx):
522    build_file = repository_ctx.path(repository_ctx.attr.build_file)
523    defs_module = repository_ctx.path(repository_ctx.attr.defs_module)
524
525    repository_ctx.file("BUILD.bazel", repository_ctx.read(build_file))
526    repository_ctx.file("defs.bzl", repository_ctx.read(defs_module))
527    repository_ctx.file("crates.bzl", "")
528    repository_ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format(
529        repository_ctx.name,
530    ))
531
532crates_vendor_remote_repository = repository_rule(
533    doc = "Creates a repository paired with `crates_vendor` targets using the `remote` vendor mode.",
534    implementation = _crates_vendor_remote_repository_impl,
535    attrs = {
536        "build_file": attr.label(
537            doc = "The BUILD file to use for the root package",
538            mandatory = True,
539        ),
540        "defs_module": attr.label(
541            doc = "The `defs.bzl` file to use in the repository",
542            mandatory = True,
543        ),
544    },
545)
546