xref: /aosp_15_r20/external/bazelbuild-rules_rust/crate_universe/private/generate_utils.bzl (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1"""Utilities directly related to the `generate` step of `cargo-bazel`."""
2
3load(":common_utils.bzl", "CARGO_BAZEL_DEBUG", "CARGO_BAZEL_ISOLATED", "REPIN_ALLOWLIST_ENV_VAR", "REPIN_ENV_VARS", "cargo_environ", "execute", "parse_alias_rule")
4
5CARGO_BAZEL_GENERATOR_SHA256 = "CARGO_BAZEL_GENERATOR_SHA256"
6CARGO_BAZEL_GENERATOR_URL = "CARGO_BAZEL_GENERATOR_URL"
7
8GENERATOR_ENV_VARS = [
9    CARGO_BAZEL_GENERATOR_URL,
10    CARGO_BAZEL_GENERATOR_SHA256,
11]
12
13CRATES_REPOSITORY_ENVIRON = GENERATOR_ENV_VARS + REPIN_ENV_VARS + [
14    REPIN_ALLOWLIST_ENV_VAR,
15    CARGO_BAZEL_ISOLATED,
16    CARGO_BAZEL_DEBUG,
17]
18
19def get_generator(repository_ctx, host_triple):
20    """Query network resources to locate a `cargo-bazel` binary
21
22    Args:
23        repository_ctx (repository_ctx): The rule's context object.
24        host_triple (string): A string representing the host triple
25
26    Returns:
27        tuple(path, dict): The path to a `cargo-bazel` binary and the host sha256 pairing.
28            The pairing (dict) may be `None` if there is no need to update the attribute
29    """
30    use_environ = False
31    for var in GENERATOR_ENV_VARS:
32        if var in repository_ctx.os.environ:
33            use_environ = True
34
35    output = repository_ctx.path("cargo-bazel.exe" if "win" in repository_ctx.os.name else "cargo-bazel")
36
37    # The `generator` attribute is the next highest priority behind
38    # environment variables. We check those first before deciding to
39    # use an explicitly provided variable.
40    if not use_environ and repository_ctx.attr.generator:
41        generator = repository_ctx.path(Label(repository_ctx.attr.generator))
42
43        # Resolve a few levels of symlinks to ensure we're accessing the direct binary
44        for _ in range(1, 100):
45            real_generator = generator.realpath
46            if real_generator == generator:
47                break
48            generator = real_generator
49        return generator, None
50
51    # The environment variable will take precedence if set
52    if use_environ:
53        generator_sha256 = repository_ctx.os.environ.get(CARGO_BAZEL_GENERATOR_SHA256)
54        generator_url = repository_ctx.os.environ.get(CARGO_BAZEL_GENERATOR_URL)
55    else:
56        generator_sha256 = repository_ctx.attr.generator_sha256s.get(host_triple)
57        generator_url = repository_ctx.attr.generator_urls.get(host_triple)
58
59    if not generator_url:
60        fail((
61            "No generator URL was found either in the `CARGO_BAZEL_GENERATOR_URL` " +
62            "environment variable or for the `{}` triple in the `generator_urls` attribute"
63        ).format(host_triple))
64
65    # Download the file into place
66    if generator_sha256:
67        repository_ctx.download(
68            output = output,
69            url = generator_url,
70            sha256 = generator_sha256,
71            executable = True,
72        )
73        return output, None
74
75    result = repository_ctx.download(
76        output = output,
77        url = generator_url,
78        executable = True,
79    )
80
81    return output, {host_triple: result.sha256}
82
83def render_config(
84        build_file_template = "//:BUILD.{name}-{version}.bazel",
85        crate_label_template = "@{repository}__{name}-{version}//:{target}",
86        crate_repository_template = "{repository}__{name}-{version}",
87        crates_module_template = "//:{file}",
88        default_alias_rule = "alias",
89        default_package_name = None,
90        generate_target_compatible_with = True,
91        platforms_template = "@rules_rust//rust/platform:{triple}",
92        regen_command = None,
93        vendor_mode = None,
94        generate_rules_license_metadata = False):
95    """Various settings used to configure rendered outputs
96
97    The template parameters each support a select number of format keys. A description of each key
98    can be found below where the supported keys for each template can be found in the parameter docs
99
100    | key | definition |
101    | --- | --- |
102    | `name` | The name of the crate. Eg `tokio` |
103    | `repository` | The rendered repository name for the crate. Directly relates to `crate_repository_template`. |
104    | `triple` | A platform triple. Eg `x86_64-unknown-linux-gnu` |
105    | `version` | The crate version. Eg `1.2.3` |
106    | `target` | The library or binary target of the crate |
107    | `file` | The basename of a file |
108
109    Args:
110        build_file_template (str, optional): The base template to use for BUILD file names. The available format keys
111            are [`{name}`, {version}`].
112        crate_label_template (str, optional): The base template to use for crate labels. The available format keys
113            are [`{repository}`, `{name}`, `{version}`, `{target}`].
114        crate_repository_template (str, optional): The base template to use for Crate label repository names. The
115            available format keys are [`{repository}`, `{name}`, `{version}`].
116        crates_module_template (str, optional): The pattern to use for the `defs.bzl` and `BUILD.bazel`
117            file names used for the crates module. The available format keys are [`{file}`].
118        default_alias_rule (str, option): Alias rule to use when generating aliases for all crates.  Acceptable values
119            are 'alias', 'dbg'/'fastbuild'/'opt' (transitions each crate's `compilation_mode`)  or a string
120            representing a rule in the form '<label to .bzl>:<rule>' that takes a single label parameter 'actual'.
121            See '@crate_index//:alias_rules.bzl' for an example.
122        default_package_name (str, optional): The default package name to use in the rendered macros. This affects the
123            auto package detection of things like `all_crate_deps`.
124        generate_target_compatible_with (bool, optional):  Whether to generate `target_compatible_with` annotations on
125            the generated BUILD files.  This catches a `target_triple`being targeted that isn't declared in
126            `supported_platform_triples`.
127        platforms_template (str, optional): The base template to use for platform names.
128            See [platforms documentation](https://docs.bazel.build/versions/main/platforms.html). The available format
129            keys are [`{triple}`].
130        regen_command (str, optional): An optional command to demonstrate how generated files should be regenerated.
131        vendor_mode (str, optional): An optional configuration for rendirng content to be rendered into repositories.
132        generate_rules_license_metadata (bool, optional): Whether to generate rules license metedata
133
134    Returns:
135        string: A json encoded struct to match the Rust `config::RenderConfig` struct
136    """
137    return json.encode(struct(
138        build_file_template = build_file_template,
139        crate_label_template = crate_label_template,
140        crate_repository_template = crate_repository_template,
141        crates_module_template = crates_module_template,
142        default_alias_rule = parse_alias_rule(default_alias_rule),
143        default_package_name = default_package_name,
144        generate_target_compatible_with = generate_target_compatible_with,
145        platforms_template = platforms_template,
146        regen_command = regen_command,
147        vendor_mode = vendor_mode,
148        generate_rules_license_metadata = generate_rules_license_metadata,
149    ))
150
151def _crate_id(name, version):
152    """Creates a `cargo_bazel::config::CrateId`.
153
154    Args:
155        name (str): The name of the crate
156        version (str): The crate's version
157
158    Returns:
159        str: A serialized representation of a CrateId
160    """
161    return "{} {}".format(name, version)
162
163def collect_crate_annotations(annotations, repository_name):
164    """Deserialize and sanitize crate annotations.
165
166    Args:
167        annotations (dict): A mapping of crate names to lists of serialized annotations
168        repository_name (str): The name of the repository that owns the annotations
169
170    Returns:
171        dict: A mapping of `cargo_bazel::config::CrateId` to sets of annotations
172    """
173    annotations = {name: [json.decode(a) for a in annotation] for name, annotation in annotations.items()}
174    crate_annotations = {}
175    for name, annotation in annotations.items():
176        for (version, data) in annotation:
177            if name == "*" and version != "*":
178                fail(
179                    "Wildcard crate names must have wildcard crate versions. " +
180                    "Please update the `annotations` attribute of the {} crates_repository".format(
181                        repository_name,
182                    ),
183                )
184            id = _crate_id(name, version)
185            if id in crate_annotations:
186                fail("Found duplicate entries for {}".format(id))
187
188            crate_annotations.update({id: data})
189    return crate_annotations
190
191def _read_cargo_config(repository_ctx):
192    if repository_ctx.attr.cargo_config:
193        config = repository_ctx.path(repository_ctx.attr.cargo_config)
194        return repository_ctx.read(config)
195    return None
196
197def _update_render_config(config, repository_name):
198    """Add the repository name to the render config
199
200    Args:
201        config (dict): A `render_config` struct
202        repository_name (str): The name of the repository that owns the config
203
204    Returns:
205        struct: An updated `render_config`.
206    """
207
208    # Add the repository name as it's very relevant to rendering.
209    config.update({"repository_name": repository_name})
210
211    return struct(**config)
212
213def _get_render_config(repository_ctx):
214    if repository_ctx.attr.render_config:
215        config = dict(json.decode(repository_ctx.attr.render_config))
216    else:
217        config = dict(json.decode(render_config()))
218
219    if not config.get("regen_command"):
220        config["regen_command"] = "bazel sync --only={}".format(
221            repository_ctx.name,
222        )
223
224    return config
225
226def compile_config(
227        crate_annotations,
228        generate_binaries,
229        generate_build_scripts,
230        generate_target_compatible_with,
231        cargo_config,
232        render_config,
233        supported_platform_triples,
234        repository_name,
235        repository_ctx = None):
236    """Create a config file for generating crate targets
237
238    [cargo_config]: https://doc.rust-lang.org/cargo/reference/config.html
239
240    Args:
241        crate_annotations (dict): Extra settings to apply to crates. See
242            `crates_repository.annotations` or `crates_vendor.annotations`.
243        generate_binaries (bool): Whether to generate `rust_binary` targets for all bins.
244        generate_build_scripts (bool): Whether or not to globally disable build scripts.
245        generate_target_compatible_with (bool): DEPRECATED: Moved to `render_config`.
246        cargo_config (str): The optional contents of a [Cargo config][cargo_config].
247        render_config (dict): The deserialized dict of the `render_config` function.
248        supported_platform_triples (list): A list of platform triples
249        repository_name (str): The name of the repository being generated
250        repository_ctx (repository_ctx, optional): A repository context object used for enabling
251            certain functionality.
252
253    Returns:
254        struct: A struct matching a `cargo_bazel::config::Config`.
255    """
256    annotations = collect_crate_annotations(crate_annotations, repository_name)
257
258    # Load additive build files if any have been provided.
259    unexpected = []
260    for name, data in annotations.items():
261        f = data.pop("additive_build_file", None)
262        if f and not repository_ctx:
263            unexpected.append(name)
264            f = None
265        content = [x for x in [
266            data.pop("additive_build_file_content", None),
267            repository_ctx.read(Label(f)) if f else None,
268        ] if x]
269        if content:
270            data.update({"additive_build_file_content": "\n".join(content)})
271
272    if unexpected:
273        fail("The following annotations use `additive_build_file` which is not supported for {}: {}".format(repository_name, unexpected))
274
275    # Deprecated: Apply `generate_target_compatible_with` to `render_config`.
276    if not generate_target_compatible_with:
277        # buildifier: disable=print
278        print("DEPRECATED: 'generate_target_compatible_with' has been moved to 'render_config'")
279        render_config.update({"generate_target_compatible_with": False})
280
281    config = struct(
282        generate_binaries = generate_binaries,
283        generate_build_scripts = generate_build_scripts,
284        annotations = annotations,
285        cargo_config = cargo_config,
286        rendering = _update_render_config(
287            config = render_config,
288            repository_name = repository_name,
289        ),
290        supported_platform_triples = supported_platform_triples,
291    )
292
293    return config
294
295def generate_config(repository_ctx):
296    """Generate a config file from various attributes passed to the rule.
297
298    Args:
299        repository_ctx (repository_ctx): The rule's context object.
300
301    Returns:
302        struct: A struct containing the path to a config and it's contents
303    """
304
305    config = compile_config(
306        crate_annotations = repository_ctx.attr.annotations,
307        generate_binaries = repository_ctx.attr.generate_binaries,
308        generate_build_scripts = repository_ctx.attr.generate_build_scripts,
309        generate_target_compatible_with = repository_ctx.attr.generate_target_compatible_with,
310        cargo_config = _read_cargo_config(repository_ctx),
311        render_config = _get_render_config(repository_ctx),
312        supported_platform_triples = repository_ctx.attr.supported_platform_triples,
313        repository_name = repository_ctx.name,
314        repository_ctx = repository_ctx,
315    )
316
317    config_path = repository_ctx.path("cargo-bazel.json")
318    repository_ctx.file(
319        config_path,
320        json.encode_indent(config, indent = " " * 4),
321    )
322
323    return config_path
324
325def get_lockfiles(repository_ctx):
326    """_summary_
327
328    Args:
329        repository_ctx (repository_ctx): The rule's context object.
330
331    Returns:
332        struct: _description_
333    """
334    return struct(
335        cargo = repository_ctx.path(repository_ctx.attr.cargo_lockfile),
336        bazel = repository_ctx.path(repository_ctx.attr.lockfile) if repository_ctx.attr.lockfile else None,
337    )
338
339def determine_repin(repository_ctx, generator, lockfile_path, config, splicing_manifest, cargo, rustc, repin_instructions = None):
340    """Use the `cargo-bazel` binary to determine whether or not dpeendencies need to be re-pinned
341
342    Args:
343        repository_ctx (repository_ctx): The rule's context object.
344        generator (path): The path to a `cargo-bazel` binary.
345        config (path): The path to a `cargo-bazel` config file. See `generate_config`.
346        splicing_manifest (path): The path to a `cargo-bazel` splicing manifest. See `create_splicing_manifest`
347        lockfile_path (path): The path to a "lock" file for reproducible outputs.
348        cargo (path): The path to a Cargo binary.
349        rustc (path): The path to a Rustc binary.
350        repin_instructions (optional string): Instructions to re-pin dependencies in your repository. Will be shown when re-pinning is required.
351
352    Returns:
353        bool: True if dependencies need to be re-pinned
354    """
355
356    # If a repin environment variable is set, always repin
357    for var in REPIN_ENV_VARS:
358        if var in repository_ctx.os.environ and repository_ctx.os.environ[var].lower() not in ["false", "no", "0", "off"]:
359            # If a repin allowlist is present only force repin if name is in list
360            if REPIN_ALLOWLIST_ENV_VAR in repository_ctx.os.environ:
361                indices_to_repin = repository_ctx.os.environ[REPIN_ALLOWLIST_ENV_VAR].split(",")
362                if repository_ctx.name in indices_to_repin:
363                    return True
364            else:
365                return True
366
367    # If a deterministic lockfile was not added then always repin
368    if not lockfile_path:
369        return True
370
371    # Run the binary to check if a repin is needed
372    args = [
373        generator,
374        "query",
375        "--lockfile",
376        lockfile_path,
377        "--config",
378        config,
379        "--splicing-manifest",
380        splicing_manifest,
381        "--cargo",
382        cargo,
383        "--rustc",
384        rustc,
385    ]
386
387    env = {
388        "CARGO": str(cargo),
389        "RUSTC": str(rustc),
390        "RUST_BACKTRACE": "full",
391    }
392
393    # Add any Cargo environment variables to the `cargo-bazel` execution
394    env.update(cargo_environ(repository_ctx))
395
396    result = execute(
397        repository_ctx = repository_ctx,
398        args = args,
399        env = env,
400        allow_fail = True,
401    )
402
403    # If it was determined repinning should occur but there was no
404    # flag indicating repinning was requested, an error is raised
405    # since repinning should be an explicit action
406    if result.return_code:
407        if repin_instructions:
408            msg = ("\n".join([
409                result.stderr,
410                "The current `lockfile` is out of date for '{}'.".format(repository_ctx.name),
411                repin_instructions,
412            ]))
413        else:
414            msg = ("\n".join([
415                result.stderr,
416                (
417                    "The current `lockfile` is out of date for '{}'. Please re-run " +
418                    "bazel using `CARGO_BAZEL_REPIN=true` if this is expected " +
419                    "and the lockfile should be updated."
420                ).format(repository_ctx.name),
421            ]))
422        fail(msg)
423
424    return False
425
426def execute_generator(
427        repository_ctx,
428        lockfile_path,
429        cargo_lockfile_path,
430        generator,
431        config,
432        splicing_manifest,
433        repository_dir,
434        cargo,
435        rustc,
436        metadata = None):
437    """Execute the `cargo-bazel` binary to produce `BUILD` and `.bzl` files.
438
439    Args:
440        repository_ctx (repository_ctx): The rule's context object.
441        lockfile_path (path): The path to a "lock" file (file used for reproducible renderings).
442        cargo_lockfile_path (path): The path to a "Cargo.lock" file within the root workspace.
443        generator (path): The path to a `cargo-bazel` binary.
444        config (path): The path to a `cargo-bazel` config file.
445        splicing_manifest (path): The path to a `cargo-bazel` splicing manifest. See `create_splicing_manifest`
446        repository_dir (path): The output path for the Bazel module and BUILD files.
447        cargo (path): The path of a Cargo binary.
448        rustc (path): The path of a Rustc binary.
449        metadata (path, optional): The path to a Cargo metadata json file. If this is set, it indicates to
450            the generator that repinning is required. This file must be adjacent to a `Cargo.toml` and
451            `Cargo.lock` file.
452
453    Returns:
454        struct: The results of `repository_ctx.execute`.
455    """
456    repository_ctx.report_progress("Generating crate BUILD files.")
457
458    args = [
459        generator,
460        "generate",
461        "--cargo-lockfile",
462        cargo_lockfile_path,
463        "--config",
464        config,
465        "--splicing-manifest",
466        splicing_manifest,
467        "--repository-dir",
468        repository_dir,
469        "--cargo",
470        cargo,
471        "--rustc",
472        rustc,
473    ]
474
475    if lockfile_path:
476        args.extend([
477            "--lockfile",
478            lockfile_path,
479        ])
480
481    env = {
482        "RUST_BACKTRACE": "full",
483    }
484
485    # Some components are not required unless re-pinning is enabled
486    if metadata:
487        args.extend([
488            "--repin",
489            "--metadata",
490            metadata,
491        ])
492        env.update({
493            "CARGO": str(cargo),
494            "RUSTC": str(rustc),
495        })
496
497    # Add any Cargo environment variables to the `cargo-bazel` execution
498    env.update(cargo_environ(repository_ctx))
499
500    result = execute(
501        repository_ctx = repository_ctx,
502        args = args,
503        env = env,
504    )
505
506    return result
507