blob: 91e8c6e3133fc4732d6fae7bce815abf54413bf4 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001"""Utilities directly related to the `generate` step of `cargo-bazel`."""
2
3load(":common_utils.bzl", "CARGO_BAZEL_ISOLATED", "cargo_environ", "execute")
4
5CARGO_BAZEL_GENERATOR_SHA256 = "CARGO_BAZEL_GENERATOR_SHA256"
6CARGO_BAZEL_GENERATOR_URL = "CARGO_BAZEL_GENERATOR_URL"
7CARGO_BAZEL_REPIN = "CARGO_BAZEL_REPIN"
8REPIN = "REPIN"
9
10GENERATOR_ENV_VARS = [
11 CARGO_BAZEL_GENERATOR_URL,
12 CARGO_BAZEL_GENERATOR_SHA256,
13]
14
15REPIN_ENV_VARS = [
16 REPIN,
17 CARGO_BAZEL_REPIN,
18]
19
20CRATES_REPOSITORY_ENVIRON = GENERATOR_ENV_VARS + REPIN_ENV_VARS + [
21 CARGO_BAZEL_ISOLATED,
22]
23
24def get_generator(repository_ctx, host_triple):
25 """Query network resources to locate a `cargo-bazel` binary
26
27 Args:
28 repository_ctx (repository_ctx): The rule's context object.
29 host_triple (string): A string representing the host triple
30
31 Returns:
32 tuple(path, dict): The path to a `cargo-bazel` binary and the host sha256 pairing.
33 The pairing (dict) may be `None` if there is no need to update the attribute
34 """
35 use_environ = False
36 for var in GENERATOR_ENV_VARS:
37 if var in repository_ctx.os.environ:
38 use_environ = True
39
40 output = repository_ctx.path("cargo-bazel.exe" if "win" in repository_ctx.os.name else "cargo-bazel")
41
42 # The `generator` attribute is the next highest priority behind
43 # environment variables. We check those first before deciding to
44 # use an explicitly provided variable.
45 if not use_environ and repository_ctx.attr.generator:
46 generator = repository_ctx.path(Label(repository_ctx.attr.generator))
47
48 # Resolve a few levels of symlinks to ensure we're accessing the direct binary
49 for _ in range(1, 100):
50 real_generator = generator.realpath
51 if real_generator == generator:
52 break
53 generator = real_generator
54 return generator, None
55
56 # The environment variable will take precedence if set
57 if use_environ:
58 generator_sha256 = repository_ctx.os.environ.get(CARGO_BAZEL_GENERATOR_SHA256)
59 generator_url = repository_ctx.os.environ.get(CARGO_BAZEL_GENERATOR_URL)
60 else:
61 generator_sha256 = repository_ctx.attr.generator_sha256s.get(host_triple)
62 generator_url = repository_ctx.attr.generator_urls.get(host_triple)
63
64 if not generator_url:
65 fail((
66 "No generator URL was found either in the `CARGO_BAZEL_GENERATOR_URL` " +
67 "environment variable or for the `{}` triple in the `generator_urls` attribute"
68 ).format(host_triple))
69
70 # Download the file into place
71 if generator_sha256:
72 repository_ctx.download(
73 output = output,
74 url = generator_url,
75 sha256 = generator_sha256,
76 executable = True,
77 )
78 return output, None
79
80 result = repository_ctx.download(
81 output = output,
82 url = generator_url,
83 executable = True,
84 )
85
86 return output, {host_triple: result.sha256}
87
88def render_config(
89 build_file_template = "//:BUILD.{name}-{version}.bazel",
90 crate_label_template = "@{repository}__{name}-{version}//:{target}",
91 crate_repository_template = "{repository}__{name}-{version}",
92 crates_module_template = "//:{file}",
93 default_package_name = None,
94 platforms_template = "@rules_rust//rust/platform:{triple}",
95 vendor_mode = None):
96 """Various settings used to configure rendered outputs
97
98 The template parameters each support a select number of format keys. A description of each key
99 can be found below where the supported keys for each template can be found in the parameter docs
100
101 | key | definition |
102 | --- | --- |
103 | `name` | The name of the crate. Eg `tokio` |
104 | `repository` | The rendered repository name for the crate. Directly relates to `crate_repository_template`. |
105 | `triple` | A platform triple. Eg `x86_64-unknown-linux-gnu` |
106 | `version` | The crate version. Eg `1.2.3` |
107 | `target` | The library or binary target of the crate |
108 | `file` | The basename of a file |
109
110 Args:
111 build_file_template (str, optional): The base template to use for BUILD file names. The available format keys
112 are [`{name}`, {version}`].
113 crate_label_template (str, optional): The base template to use for crate labels. The available format keys
114 are [`{repository}`, `{name}`, `{version}`, `{target}`].
115 crate_repository_template (str, optional): The base template to use for Crate label repository names. The
116 available format keys are [`{repository}`, `{name}`, `{version}`].
117 crates_module_template (str, optional): The pattern to use for the `defs.bzl` and `BUILD.bazel`
118 file names used for the crates module. The available format keys are [`{file}`].
119 default_package_name (str, optional): The default package name to in the rendered macros. This affects the
120 auto package detection of things like `all_crate_deps`.
121 platforms_template (str, optional): The base template to use for platform names.
122 See [platforms documentation](https://docs.bazel.build/versions/main/platforms.html). The available format
123 keys are [`{triple}`].
124 vendor_mode (str, optional): An optional configuration for rendirng content to be rendered into repositories.
125
126 Returns:
127 string: A json encoded struct to match the Rust `config::RenderConfig` struct
128 """
129 return json.encode(struct(
130 build_file_template = build_file_template,
131 crate_label_template = crate_label_template,
132 crate_repository_template = crate_repository_template,
133 crates_module_template = crates_module_template,
134 default_package_name = default_package_name,
135 platforms_template = platforms_template,
136 vendor_mode = vendor_mode,
137 ))
138
139def _crate_id(name, version):
140 """Creates a `cargo_bazel::config::CrateId`.
141
142 Args:
143 name (str): The name of the crate
144 version (str): The crate's version
145
146 Returns:
147 str: A serialized representation of a CrateId
148 """
149 return "{} {}".format(name, version)
150
151def collect_crate_annotations(annotations, repository_name):
152 """Deserialize and sanitize crate annotations.
153
154 Args:
155 annotations (dict): A mapping of crate names to lists of serialized annotations
156 repository_name (str): The name of the repository that owns the annotations
157
158 Returns:
159 dict: A mapping of `cargo_bazel::config::CrateId` to sets of annotations
160 """
161 annotations = {name: [json.decode(a) for a in annotation] for name, annotation in annotations.items()}
162 crate_annotations = {}
163 for name, annotation in annotations.items():
164 for (version, data) in annotation:
165 if name == "*" and version != "*":
166 fail(
167 "Wildcard crate names must have wildcard crate versions. " +
168 "Please update the `annotations` attribute of the {} crates_repository".format(
169 repository_name,
170 ),
171 )
172 id = _crate_id(name, version)
173 if id in crate_annotations:
174 fail("Found duplicate entries for {}".format(id))
175
176 crate_annotations.update({id: data})
177 return crate_annotations
178
179def _read_cargo_config(repository_ctx):
180 if repository_ctx.attr.cargo_config:
181 config = repository_ctx.path(repository_ctx.attr.cargo_config)
182 return repository_ctx.read(config)
183 return None
184
185def _get_render_config(repository_ctx):
186 if repository_ctx.attr.render_config:
187 config = dict(json.decode(repository_ctx.attr.render_config))
188 else:
189 config = dict(json.decode(render_config()))
190
191 # Add the repository name as it's very relevant to rendering.
192 config.update({"repository_name": repository_ctx.name})
193
194 return struct(**config)
195
196def generate_config(repository_ctx):
197 """Generate a config file from various attributes passed to the rule.
198
199 Args:
200 repository_ctx (repository_ctx): The rule's context object.
201
202 Returns:
203 struct: A struct containing the path to a config and it's contents
204 """
205 annotations = collect_crate_annotations(repository_ctx.attr.annotations, repository_ctx.name)
206
207 # Load additive build files if any have been provided.
208 content = list()
209 for data in annotations.values():
210 additive_build_file_content = data.pop("additive_build_file_content", None)
211 if additive_build_file_content:
212 content.append(additive_build_file_content)
213 additive_build_file = data.pop("additive_build_file", None)
214 if additive_build_file:
215 file_path = repository_ctx.path(Label(additive_build_file))
216 content.append(repository_ctx.read(file_path))
217 data.update({"additive_build_file_content": "\n".join(content) if content else None})
218
219 config = struct(
220 generate_build_scripts = repository_ctx.attr.generate_build_scripts,
221 annotations = annotations,
222 cargo_config = _read_cargo_config(repository_ctx),
223 rendering = _get_render_config(repository_ctx),
224 supported_platform_triples = repository_ctx.attr.supported_platform_triples,
225 )
226
227 config_path = repository_ctx.path("cargo-bazel.json")
228 repository_ctx.file(
229 config_path,
230 json.encode_indent(config, indent = " " * 4),
231 )
232
233 # This was originally written to return a struct and not just the config path
234 # so splicing can have access to some rendering information embedded in the config
235 # If splicing should no longer need that info, it'd be simpler to just return a `path`.
236 return struct(
237 path = config_path,
238 info = config,
239 )
240
241def get_lockfile(repository_ctx):
242 """Locate the lockfile and identify the it's type (Cargo or Bazel).
243
244 Args:
245 repository_ctx (repository_ctx): The rule's context object.
246
247 Returns:
248 struct: The path to the lockfile as well as it's type
249 """
250 if repository_ctx.attr.lockfile_kind == "auto":
251 if str(repository_ctx.attr.lockfile).endswith("Cargo.lock"):
252 kind = "cargo"
253 else:
254 kind = "bazel"
255 else:
256 kind = repository_ctx.attr.lockfile_kind
257
258 return struct(
259 path = repository_ctx.path(repository_ctx.attr.lockfile),
260 kind = kind,
261 )
262
263def determine_repin(repository_ctx, generator, lockfile_path, lockfile_kind, config, splicing_manifest, cargo, rustc):
264 """Use the `cargo-bazel` binary to determine whether or not dpeendencies need to be re-pinned
265
266 Args:
267 repository_ctx (repository_ctx): The rule's context object.
268 generator (path): The path to a `cargo-bazel` binary.
269 config (path): The path to a `cargo-bazel` config file. See `generate_config`.
270 splicing_manifest (path): The path to a `cargo-bazel` splicing manifest. See `create_splicing_manifest`
271 lockfile_path (path): The path to a "lock" file for reproducible outputs.
272 lockfile_kind (str): The type of lock file represented by `lockfile_path`
273 cargo (path): The path to a Cargo binary.
274 rustc (path): The path to a Rustc binary.
275
276 Returns:
277 bool: True if dependencies need to be re-pinned
278 """
279
280 # If a repin environment variable is set, always repin
281 for var in REPIN_ENV_VARS:
282 if repository_ctx.os.environ.get(var, "").lower() in ["true", "yes", "1", "on"]:
283 return True
284
285 # Cargo lockfiles should always be repinned.
286 if lockfile_kind == "cargo":
287 return True
288
289 # Run the binary to check if a repin is needed
290 args = [
291 generator,
292 "query",
293 "--lockfile",
294 lockfile_path,
295 "--config",
296 config,
297 "--splicing-manifest",
298 splicing_manifest,
299 "--cargo",
300 cargo,
301 "--rustc",
302 rustc,
303 ]
304
305 env = {
306 "CARGO": str(cargo),
307 "RUSTC": str(rustc),
308 "RUST_BACKTRACE": "full",
309 }
310
311 # Add any Cargo environment variables to the `cargo-bazel` execution
312 env.update(cargo_environ(repository_ctx))
313
314 result = execute(
315 repository_ctx = repository_ctx,
316 args = args,
317 env = env,
318 )
319
320 # If it was determined repinning should occur but there was no
321 # flag indicating repinning was requested, an error is raised
322 # since repinning should be an explicit action
323 if result.stdout.strip().lower() == "repin":
324 # buildifier: disable=print
325 print(result.stderr)
326 fail((
327 "The current `lockfile` is out of date for '{}'. Please re-run " +
328 "bazel using `CARGO_BAZEL_REPIN=true` if this is expected " +
329 "and the lockfile should be updated."
330 ).format(repository_ctx.name))
331
332 return False
333
334def execute_generator(
335 repository_ctx,
336 lockfile_path,
337 lockfile_kind,
338 generator,
339 config,
340 splicing_manifest,
341 repository_dir,
342 cargo,
343 rustc,
344 repin = False,
345 metadata = None):
346 """Execute the `cargo-bazel` binary to produce `BUILD` and `.bzl` files.
347
348 Args:
349 repository_ctx (repository_ctx): The rule's context object.
350 lockfile_path (path): The path to a "lock" file (file used for reproducible renderings).
351 lockfile_kind (str): The type of lockfile given (Cargo or Bazel).
352 generator (path): The path to a `cargo-bazel` binary.
353 config (path): The path to a `cargo-bazel` config file.
354 splicing_manifest (path): The path to a `cargo-bazel` splicing manifest. See `create_splicing_manifest`
355 repository_dir (path): The output path for the Bazel module and BUILD files.
356 cargo (path): The path of a Cargo binary.
357 rustc (path): The path of a Rustc binary.
358 repin (bool, optional): Whether or not to repin dependencies
359 metadata (path, optional): The path to a Cargo metadata json file.
360
361 Returns:
362 struct: The results of `repository_ctx.execute`.
363 """
364 repository_ctx.report_progress("Generating crate BUILD files.")
365
366 args = [
367 generator,
368 "generate",
369 "--lockfile",
370 lockfile_path,
371 "--lockfile-kind",
372 lockfile_kind,
373 "--config",
374 config,
375 "--splicing-manifest",
376 splicing_manifest,
377 "--repository-dir",
378 repository_dir,
379 "--cargo",
380 cargo,
381 "--rustc",
382 rustc,
383 ]
384
385 env = {
386 "RUST_BACKTRACE": "full",
387 }
388
389 # Some components are not required unless re-pinning is enabled
390 if repin:
391 args.extend([
392 "--repin",
393 "--metadata",
394 metadata,
395 ])
396 env.update({
397 "CARGO": str(cargo),
398 "RUSTC": str(rustc),
399 })
400
401 # Add any Cargo environment variables to the `cargo-bazel` execution
402 env.update(cargo_environ(repository_ctx))
403
404 result = execute(
405 repository_ctx = repository_ctx,
406 args = args,
407 env = env,
408 )
409
410 return result