1"""Utilities directly related to the `splicing` step of `cargo-bazel`.""" 2 3load(":common_utils.bzl", "CARGO_BAZEL_DEBUG", "CARGO_BAZEL_REPIN", "REPIN", "cargo_environ", "execute") 4 5def splicing_config(resolver_version = "2"): 6 """Various settings used to configure Cargo manifest splicing behavior. 7 8 [rv]: https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions 9 10 Args: 11 resolver_version (str, optional): The [resolver version][rv] to use in generated Cargo 12 manifests. This flag is **only** used when splicing a manifest from direct package 13 definitions. See `crates_repository::packages`. 14 15 Returns: 16 str: A json encoded string of the parameters provided 17 """ 18 return json.encode(struct( 19 resolver_version = resolver_version, 20 )) 21 22def kebab_case_keys(data): 23 """Ensure the key value of the data given are kebab-case 24 25 Args: 26 data (dict): A deserialized json blob 27 28 Returns: 29 dict: The same `data` but with kebab-case keys 30 """ 31 return { 32 key.lower().replace("_", "-"): val 33 for (key, val) in data.items() 34 } 35 36def compile_splicing_manifest(splicing_config, manifests, cargo_config_path, packages): 37 """Produce a manifest containing required components for splicing a new Cargo workspace 38 39 [cargo_config]: https://doc.rust-lang.org/cargo/reference/config.html 40 [cargo_toml]: https://doc.rust-lang.org/cargo/reference/manifest.html 41 42 Args: 43 splicing_config (dict): A deserialized `splicing_config` 44 manifests (dict): A mapping of paths to Bazel labels which represent [Cargo manifests][cargo_toml]. 45 cargo_config_path (str): The absolute path to a [Cargo config][cargo_config]. 46 packages (dict): A set of crates (packages) specifications to depend on 47 48 Returns: 49 dict: A dictionary representation of a `cargo_bazel::splicing::SplicingManifest` 50 """ 51 52 # Deserialize information about direct packges 53 direct_packages_info = { 54 # Ensure the data is using kebab-case as that's what `cargo_toml::DependencyDetail` expects. 55 pkg: kebab_case_keys(dict(json.decode(data))) 56 for (pkg, data) in packages.items() 57 } 58 59 # Auto-generated splicer manifest values 60 splicing_manifest_content = { 61 "cargo_config": cargo_config_path, 62 "direct_packages": direct_packages_info, 63 "manifests": manifests, 64 } 65 66 return splicing_config | splicing_manifest_content 67 68def _no_at_label(label): 69 """Strips leading '@'s for stringified labels in the main repository for backwards-comaptibility reasons.""" 70 s = str(label) 71 if s.startswith("@@//"): 72 return s[2:] 73 if s.startswith("@//"): 74 return s[1:] 75 return s 76 77def create_splicing_manifest(repository_ctx): 78 """Produce a manifest containing required components for splicing a new Cargo workspace 79 80 Args: 81 repository_ctx (repository_ctx): The rule's context object. 82 83 Returns: 84 path: The path to a json encoded manifest 85 """ 86 87 manifests = {str(repository_ctx.path(m)): _no_at_label(m) for m in repository_ctx.attr.manifests} 88 89 if repository_ctx.attr.cargo_config: 90 cargo_config = str(repository_ctx.path(repository_ctx.attr.cargo_config)) 91 else: 92 cargo_config = None 93 94 # Load user configurable splicing settings 95 config = json.decode(repository_ctx.attr.splicing_config or splicing_config()) 96 97 splicing_manifest = repository_ctx.path("splicing_manifest.json") 98 99 data = compile_splicing_manifest( 100 splicing_config = config, 101 manifests = manifests, 102 cargo_config_path = cargo_config, 103 packages = repository_ctx.attr.packages, 104 ) 105 106 # Serialize information required for splicing 107 repository_ctx.file( 108 splicing_manifest, 109 json.encode_indent( 110 data, 111 indent = " " * 4, 112 ), 113 ) 114 115 return splicing_manifest 116 117def splice_workspace_manifest(repository_ctx, generator, cargo_lockfile, splicing_manifest, config_path, cargo, rustc): 118 """Splice together a Cargo workspace from various other manifests and package definitions 119 120 Args: 121 repository_ctx (repository_ctx): The rule's context object. 122 generator (path): The `cargo-bazel` binary. 123 cargo_lockfile (path): The path to a "Cargo.lock" file. 124 splicing_manifest (path): The path to a splicing manifest. 125 config_path: The path to the config file (containing `cargo_bazel::config::Config`.) 126 cargo (path): The path to a Cargo binary. 127 rustc (path): The Path to a Rustc binary. 128 129 Returns: 130 path: The path to a Cargo metadata json file found in the spliced workspace root. 131 """ 132 repository_ctx.report_progress("Splicing Cargo workspace.") 133 134 splicing_output_dir = repository_ctx.path("splicing-output") 135 136 # Generate a workspace root which contains all workspace members 137 arguments = [ 138 generator, 139 "splice", 140 "--output-dir", 141 splicing_output_dir, 142 "--splicing-manifest", 143 splicing_manifest, 144 "--config", 145 config_path, 146 "--cargo", 147 cargo, 148 "--rustc", 149 rustc, 150 "--cargo-lockfile", 151 cargo_lockfile, 152 ] 153 154 # Optionally set the splicing workspace directory to somewhere within the repository directory 155 # to improve the debugging experience. 156 if CARGO_BAZEL_DEBUG in repository_ctx.os.environ: 157 arguments.extend([ 158 "--workspace-dir", 159 repository_ctx.path("splicing-workspace"), 160 ]) 161 162 env = { 163 "CARGO": str(cargo), 164 "RUSTC": str(rustc), 165 "RUST_BACKTRACE": "full", 166 } 167 168 # Ensure the short hand repin variable is set to the full name. 169 if REPIN in repository_ctx.os.environ and CARGO_BAZEL_REPIN not in repository_ctx.os.environ: 170 env["CARGO_BAZEL_REPIN"] = repository_ctx.os.environ[REPIN] 171 172 # Add any Cargo environment variables to the `cargo-bazel` execution 173 env |= cargo_environ(repository_ctx) 174 175 execute( 176 repository_ctx = repository_ctx, 177 args = arguments, 178 env = env, 179 ) 180 181 # This file must have been produced by the execution above. 182 spliced_lockfile = repository_ctx.path(splicing_output_dir.get_child("Cargo.lock")) 183 if not spliced_lockfile.exists: 184 fail("Lockfile file does not exist: " + str(spliced_lockfile)) 185 spliced_metadata = repository_ctx.path(splicing_output_dir.get_child("metadata.json")) 186 if not spliced_metadata.exists: 187 fail("Metadata file does not exist: " + str(spliced_metadata)) 188 189 return spliced_metadata 190