1"""The `cargo_bootstrap` rule is used for bootstrapping cargo binaries in a repository rule.""" 2 3load("//cargo/private:cargo_utils.bzl", "get_rust_tools") 4load("//rust:defs.bzl", "rust_common") 5load("//rust/platform:triple.bzl", "get_host_triple") 6 7_CARGO_BUILD_MODES = [ 8 "release", 9 "debug", 10] 11 12_FAIL_MESSAGE = """\ 13Process exited with code '{code}' 14# ARGV ######################################################################## 15{argv} 16 17# STDOUT ###################################################################### 18{stdout} 19 20# STDERR ###################################################################### 21{stderr} 22""" 23 24def cargo_bootstrap( 25 repository_ctx, 26 cargo_bin, 27 rustc_bin, 28 binary, 29 cargo_manifest, 30 environment = {}, 31 quiet = False, 32 build_mode = "release", 33 target_dir = None, 34 timeout = 600): 35 """A function for bootstrapping a cargo binary within a repository rule 36 37 Args: 38 repository_ctx (repository_ctx): The rule's context object. 39 cargo_bin (path): The path to a Cargo binary. 40 rustc_bin (path): The path to a Rustc binary. 41 binary (str): The binary to build (the `--bin` parameter for Cargo). 42 cargo_manifest (path): The path to a Cargo manifest (Cargo.toml file). 43 environment (dict): Environment variables to use during execution. 44 quiet (bool, optional): Whether or not to print output from the Cargo command. 45 build_mode (str, optional): The build mode to use 46 target_dir (path, optional): The directory in which to produce build outputs 47 (Cargo's --target-dir argument). 48 timeout (int, optional): Maximum duration of the Cargo build command in seconds, 49 50 Returns: 51 path: The path of the built binary within the target directory 52 """ 53 54 if not target_dir: 55 target_dir = repository_ctx.path(".") 56 57 args = [ 58 cargo_bin, 59 "build", 60 "--bin", 61 binary, 62 "--locked", 63 "--target-dir", 64 target_dir, 65 "--manifest-path", 66 cargo_manifest, 67 ] 68 69 if build_mode not in _CARGO_BUILD_MODES: 70 fail("'{}' is not a supported build mode. Use one of {}".format(build_mode, _CARGO_BUILD_MODES)) 71 72 if build_mode == "release": 73 args.append("--release") 74 75 env = dict({ 76 "RUSTC": str(rustc_bin), 77 }.items() + environment.items()) 78 79 repository_ctx.report_progress("Cargo Bootstrapping {}".format(binary)) 80 result = repository_ctx.execute( 81 args, 82 environment = env, 83 quiet = quiet, 84 timeout = timeout, 85 ) 86 87 if result.return_code != 0: 88 fail(_FAIL_MESSAGE.format( 89 code = result.return_code, 90 argv = args, 91 stdout = result.stdout, 92 stderr = result.stderr, 93 )) 94 95 extension = "" 96 if "win" in repository_ctx.os.name: 97 extension = ".exe" 98 99 binary_path = "{}/{}{}".format( 100 build_mode, 101 binary, 102 extension, 103 ) 104 105 if not repository_ctx.path(binary_path).exists: 106 fail("Failed to produce binary at {}".format(binary_path)) 107 108 return binary_path 109 110_BUILD_FILE_CONTENT = """\ 111load("@rules_rust//rust:defs.bzl", "rust_binary") 112 113package(default_visibility = ["//visibility:public"]) 114 115exports_files([ 116 "{binary_name}", 117 "{binary}" 118]) 119 120alias( 121 name = "binary", 122 actual = "{binary}", 123) 124 125rust_binary( 126 name = "install", 127 rustc_env = {{ 128 "RULES_RUST_CARGO_BOOTSTRAP_BINARY": "$(rootpath {binary})" 129 }}, 130 data = [ 131 "{binary}", 132 ], 133 srcs = [ 134 "@rules_rust//cargo/bootstrap:bootstrap_installer.rs" 135 ], 136) 137""" 138 139def _collect_environ(repository_ctx, host_triple): 140 """Gather environment varialbes to use from the current rule context 141 142 Args: 143 repository_ctx (repository_ctx): The rule's context object. 144 host_triple (str): A string of the current host triple 145 146 Returns: 147 dict: A map of environment variables 148 """ 149 env_vars = dict(json.decode(repository_ctx.attr.env.get(host_triple, "{}"))) 150 151 # Gather the path for each label and ensure it exists 152 env_labels = dict(json.decode(repository_ctx.attr.env_label.get(host_triple, "{}"))) 153 env_labels = {key: repository_ctx.path(Label(value)) for (key, value) in env_labels.items()} 154 for key in env_labels: 155 if not env_labels[key].exists: 156 fail("File for key '{}' does not exist: {}", key, env_labels[key]) 157 env_labels = {key: str(value) for (key, value) in env_labels.items()} 158 159 return dict(env_vars.items() + env_labels.items()) 160 161def _detect_changes(repository_ctx): 162 """Inspect files that are considered inputs to the build for changes 163 164 Args: 165 repository_ctx (repository_ctx): The rule's context object. 166 """ 167 # Simply generating a `path` object consideres the file as 'tracked' or 168 # 'consumed' which means changes to it will trigger rebuilds 169 170 for src in repository_ctx.attr.srcs: 171 repository_ctx.path(src) 172 173 repository_ctx.path(repository_ctx.attr.cargo_lockfile) 174 repository_ctx.path(repository_ctx.attr.cargo_toml) 175 176def _cargo_bootstrap_repository_impl(repository_ctx): 177 # Pretend to Bazel that this rule's input files have been used, so that it will re-run the rule if they change. 178 _detect_changes(repository_ctx) 179 180 # Expects something like `1.56.0`, or `nightly/2021-09-08`. 181 version, _, iso_date = repository_ctx.attr.version.partition("/") 182 if iso_date: 183 channel = version 184 version = iso_date 185 else: 186 channel = "stable" 187 188 host_triple = get_host_triple(repository_ctx) 189 cargo_template = repository_ctx.attr.rust_toolchain_cargo_template 190 rustc_template = repository_ctx.attr.rust_toolchain_rustc_template 191 192 tools = get_rust_tools( 193 cargo_template = cargo_template, 194 rustc_template = rustc_template, 195 host_triple = host_triple, 196 channel = channel, 197 version = version, 198 ) 199 200 binary_name = repository_ctx.attr.binary or repository_ctx.name 201 202 # In addition to platform specific environment variables, a common set (indicated by `*`) will always 203 # be gathered. 204 environment = dict(_collect_environ(repository_ctx, "*").items() + _collect_environ(repository_ctx, host_triple.str).items()) 205 206 built_binary = cargo_bootstrap( 207 repository_ctx = repository_ctx, 208 cargo_bin = repository_ctx.path(tools.cargo), 209 rustc_bin = repository_ctx.path(tools.rustc), 210 binary = binary_name, 211 cargo_manifest = repository_ctx.path(repository_ctx.attr.cargo_toml), 212 build_mode = repository_ctx.attr.build_mode, 213 environment = environment, 214 timeout = repository_ctx.attr.timeout, 215 ) 216 217 # Create a symlink so that the binary can be accesed via it's target name 218 repository_ctx.symlink(built_binary, binary_name) 219 220 repository_ctx.file("BUILD.bazel", _BUILD_FILE_CONTENT.format( 221 binary_name = binary_name, 222 binary = built_binary, 223 )) 224 225cargo_bootstrap_repository = repository_rule( 226 doc = "A rule for bootstrapping a Rust binary using [Cargo](https://doc.rust-lang.org/cargo/)", 227 implementation = _cargo_bootstrap_repository_impl, 228 attrs = { 229 "binary": attr.string( 230 doc = "The binary to build (the `--bin` parameter for Cargo). If left empty, the repository name will be used.", 231 ), 232 "build_mode": attr.string( 233 doc = "The build mode the binary should be built with", 234 values = [ 235 "debug", 236 "release", 237 ], 238 default = "release", 239 ), 240 "cargo_lockfile": attr.label( 241 doc = "The lockfile of the crate_universe resolver", 242 allow_single_file = ["Cargo.lock"], 243 mandatory = True, 244 ), 245 "cargo_toml": attr.label( 246 doc = "The path of the crate_universe resolver manifest (`Cargo.toml` file)", 247 allow_single_file = ["Cargo.toml"], 248 mandatory = True, 249 ), 250 "env": attr.string_dict( 251 doc = ( 252 "A mapping of platform triple to a set of environment variables. See " + 253 "[cargo_env](#cargo_env) for usage details. Additionally, the platform triple `*` applies to all platforms." 254 ), 255 ), 256 "env_label": attr.string_dict( 257 doc = ( 258 "A mapping of platform triple to a set of environment variables. This " + 259 "attribute differs from `env` in that all variables passed here must be " + 260 "fully qualified labels of files. See [cargo_env](#cargo_env) for usage details. " + 261 "Additionally, the platform triple `*` applies to all platforms." 262 ), 263 ), 264 "rust_toolchain_cargo_template": attr.string( 265 doc = ( 266 "The template to use for finding the host `cargo` binary. `{version}` (eg. '1.53.0'), " + 267 "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " + 268 "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " + 269 "replaced in the string if present." 270 ), 271 default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}", 272 ), 273 "rust_toolchain_rustc_template": attr.string( 274 doc = ( 275 "The template to use for finding the host `rustc` binary. `{version}` (eg. '1.53.0'), " + 276 "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " + 277 "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " + 278 "replaced in the string if present." 279 ), 280 default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}", 281 ), 282 "srcs": attr.label_list( 283 doc = "Souce files of the crate to build. Passing source files here can be used to trigger rebuilds when changes are made", 284 allow_files = True, 285 ), 286 "timeout": attr.int( 287 doc = "Maximum duration of the Cargo build command in seconds", 288 default = 600, 289 ), 290 "version": attr.string( 291 doc = "The version of Rust the currently registered toolchain is using. Eg. `1.56.0`, or `nightly/2021-09-08`", 292 default = rust_common.default_version, 293 ), 294 }, 295) 296 297def cargo_env(env): 298 """A helper for generating platform specific environment variables 299 300 ```python 301 load("@rules_rust//rust:defs.bzl", "rust_common") 302 load("@rules_rust//cargo:defs.bzl", "cargo_bootstrap_repository", "cargo_env") 303 304 cargo_bootstrap_repository( 305 name = "bootstrapped_bin", 306 cargo_lockfile = "//:Cargo.lock", 307 cargo_toml = "//:Cargo.toml", 308 srcs = ["//:resolver_srcs"], 309 version = rust_common.default_version, 310 binary = "my-crate-binary", 311 env = { 312 "x86_64-unknown-linux-gnu": cargo_env({ 313 "FOO": "BAR", 314 }), 315 }, 316 env_label = { 317 "aarch64-unknown-linux-musl": cargo_env({ 318 "DOC": "//:README.md", 319 }), 320 } 321 ) 322 ``` 323 324 Args: 325 env (dict): A map of environment variables 326 327 Returns: 328 str: A json encoded string of the environment variables 329 """ 330 return json.encode(dict(env)) 331