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