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