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 splciing 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 splicier 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 dict(splicing_config.items() + splicing_manifest_content.items()) 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 splciing 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 repo_dir = repository_ctx.path(".") 98 99 splicing_manifest = repository_ctx.path("{}/splicing_manifest.json".format(repo_dir)) 100 101 data = compile_splicing_manifest( 102 splicing_config = config, 103 manifests = manifests, 104 cargo_config_path = cargo_config, 105 packages = repository_ctx.attr.packages, 106 ) 107 108 # Serialize information required for splicing 109 repository_ctx.file( 110 splicing_manifest, 111 json.encode_indent( 112 data, 113 indent = " " * 4, 114 ), 115 ) 116 117 return splicing_manifest 118 119def splice_workspace_manifest(repository_ctx, generator, cargo_lockfile, splicing_manifest, config_path, cargo, rustc): 120 """Splice together a Cargo workspace from various other manifests and package definitions 121 122 Args: 123 repository_ctx (repository_ctx): The rule's context object. 124 generator (path): The `cargo-bazel` binary. 125 cargo_lockfile (path): The path to a "Cargo.lock" file. 126 splicing_manifest (path): The path to a splicing manifest. 127 config_path: The path to the config file (containing `cargo_bazel::config::Config`.) 128 cargo (path): The path to a Cargo binary. 129 rustc (path): The Path to a Rustc binary. 130 131 Returns: 132 path: The path to a Cargo metadata json file found in the spliced workspace root. 133 """ 134 repository_ctx.report_progress("Splicing Cargo workspace.") 135 repo_dir = repository_ctx.path(".") 136 137 splicing_output_dir = repository_ctx.path("splicing-output") 138 139 # Generate a workspace root which contains all workspace members 140 arguments = [ 141 generator, 142 "splice", 143 "--output-dir", 144 splicing_output_dir, 145 "--splicing-manifest", 146 splicing_manifest, 147 "--config", 148 config_path, 149 "--cargo", 150 cargo, 151 "--rustc", 152 rustc, 153 "--cargo-lockfile", 154 cargo_lockfile, 155 ] 156 157 # Optionally set the splicing workspace directory to somewhere within the repository directory 158 # to improve the debugging experience. 159 if CARGO_BAZEL_DEBUG in repository_ctx.os.environ: 160 arguments.extend([ 161 "--workspace-dir", 162 repository_ctx.path("{}/splicing-workspace".format(repo_dir)), 163 ]) 164 165 env = { 166 "CARGO": str(cargo), 167 "RUSTC": str(rustc), 168 "RUST_BACKTRACE": "full", 169 } 170 171 # Ensure the short hand repin variable is set to the full name. 172 if REPIN in repository_ctx.os.environ and CARGO_BAZEL_REPIN not in repository_ctx.os.environ: 173 env.update({CARGO_BAZEL_REPIN: repository_ctx.os.environ[REPIN]}) 174 175 # Add any Cargo environment variables to the `cargo-bazel` execution 176 env.update(cargo_environ(repository_ctx)) 177 178 execute( 179 repository_ctx = repository_ctx, 180 args = arguments, 181 env = env, 182 ) 183 184 # This file must have been produced by the execution above. 185 spliced_lockfile = repository_ctx.path("{}/Cargo.lock".format(splicing_output_dir)) 186 if not spliced_lockfile.exists: 187 fail("Lockfile file does not exist: {}".format(spliced_lockfile)) 188 spliced_metadata = repository_ctx.path("{}/metadata.json".format(splicing_output_dir)) 189 if not spliced_metadata.exists: 190 fail("Metadata file does not exist: {}".format(spliced_metadata)) 191 192 return spliced_metadata 193