Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame^] | 1 | """Utilities directly related to the `generate` step of `cargo-bazel`.""" |
| 2 | |
| 3 | load(":common_utils.bzl", "CARGO_BAZEL_ISOLATED", "cargo_environ", "execute") |
| 4 | |
| 5 | CARGO_BAZEL_GENERATOR_SHA256 = "CARGO_BAZEL_GENERATOR_SHA256" |
| 6 | CARGO_BAZEL_GENERATOR_URL = "CARGO_BAZEL_GENERATOR_URL" |
| 7 | CARGO_BAZEL_REPIN = "CARGO_BAZEL_REPIN" |
| 8 | REPIN = "REPIN" |
| 9 | |
| 10 | GENERATOR_ENV_VARS = [ |
| 11 | CARGO_BAZEL_GENERATOR_URL, |
| 12 | CARGO_BAZEL_GENERATOR_SHA256, |
| 13 | ] |
| 14 | |
| 15 | REPIN_ENV_VARS = [ |
| 16 | REPIN, |
| 17 | CARGO_BAZEL_REPIN, |
| 18 | ] |
| 19 | |
| 20 | CRATES_REPOSITORY_ENVIRON = GENERATOR_ENV_VARS + REPIN_ENV_VARS + [ |
| 21 | CARGO_BAZEL_ISOLATED, |
| 22 | ] |
| 23 | |
| 24 | def 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 | |
| 88 | def 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 | |
| 139 | def _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 | |
| 151 | def 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 | |
| 179 | def _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 | |
| 185 | def _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 | |
| 196 | def 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 | |
| 241 | def 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 | |
| 263 | def 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 | |
| 334 | def 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 |