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 if repository_ctx.attr.version in ("beta", "nightly"): 181 channel = repository_ctx.attr.version 182 version = repository_ctx.attr.iso_date 183 else: 184 channel = "stable" 185 version = repository_ctx.attr.version 186 187 host_triple = get_host_triple(repository_ctx) 188 cargo_template = repository_ctx.attr.rust_toolchain_cargo_template 189 rustc_template = repository_ctx.attr.rust_toolchain_rustc_template 190 191 tools = get_rust_tools( 192 cargo_template = cargo_template, 193 rustc_template = rustc_template, 194 host_triple = host_triple, 195 channel = channel, 196 version = version, 197 ) 198 199 binary_name = repository_ctx.attr.binary or repository_ctx.name 200 201 # In addition to platform specific environment variables, a common set (indicated by `*`) will always 202 # be gathered. 203 environment = dict(_collect_environ(repository_ctx, "*").items() + _collect_environ(repository_ctx, host_triple.str).items()) 204 205 built_binary = cargo_bootstrap( 206 repository_ctx = repository_ctx, 207 cargo_bin = repository_ctx.path(tools.cargo), 208 rustc_bin = repository_ctx.path(tools.rustc), 209 binary = binary_name, 210 cargo_manifest = repository_ctx.path(repository_ctx.attr.cargo_toml), 211 build_mode = repository_ctx.attr.build_mode, 212 environment = environment, 213 timeout = repository_ctx.attr.timeout, 214 ) 215 216 # Create a symlink so that the binary can be accesed via it's target name 217 repository_ctx.symlink(built_binary, binary_name) 218 219 repository_ctx.file("BUILD.bazel", _BUILD_FILE_CONTENT.format( 220 binary_name = binary_name, 221 binary = built_binary, 222 )) 223 224cargo_bootstrap_repository = repository_rule( 225 doc = "A rule for bootstrapping a Rust binary using [Cargo](https://doc.rust-lang.org/cargo/)", 226 implementation = _cargo_bootstrap_repository_impl, 227 attrs = { 228 "binary": attr.string( 229 doc = "The binary to build (the `--bin` parameter for Cargo). If left empty, the repository name will be used.", 230 ), 231 "build_mode": attr.string( 232 doc = "The build mode the binary should be built with", 233 values = [ 234 "debug", 235 "release", 236 ], 237 default = "release", 238 ), 239 "cargo_lockfile": attr.label( 240 doc = "The lockfile of the crate_universe resolver", 241 allow_single_file = ["Cargo.lock"], 242 mandatory = True, 243 ), 244 "cargo_toml": attr.label( 245 doc = "The path of the crate_universe resolver manifest (`Cargo.toml` file)", 246 allow_single_file = ["Cargo.toml"], 247 mandatory = True, 248 ), 249 "env": attr.string_dict( 250 doc = ( 251 "A mapping of platform triple to a set of environment variables. See " + 252 "[cargo_env](#cargo_env) for usage details. Additionally, the platform triple `*` applies to all platforms." 253 ), 254 ), 255 "env_label": attr.string_dict( 256 doc = ( 257 "A mapping of platform triple to a set of environment variables. This " + 258 "attribute differs from `env` in that all variables passed here must be " + 259 "fully qualified labels of files. See [cargo_env](#cargo_env) for usage details. " + 260 "Additionally, the platform triple `*` applies to all platforms." 261 ), 262 ), 263 "iso_date": attr.string( 264 doc = "The iso_date of cargo binary the resolver should use. Note: This can only be set if `version` is `beta` or `nightly`", 265 ), 266 "rust_toolchain_cargo_template": attr.string( 267 doc = ( 268 "The template to use for finding the host `cargo` binary. `{version}` (eg. '1.53.0'), " + 269 "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " + 270 "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " + 271 "replaced in the string if present." 272 ), 273 default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}", 274 ), 275 "rust_toolchain_rustc_template": attr.string( 276 doc = ( 277 "The template to use for finding the host `rustc` binary. `{version}` (eg. '1.53.0'), " + 278 "`{triple}` (eg. 'x86_64-unknown-linux-gnu'), `{arch}` (eg. 'aarch64'), `{vendor}` (eg. 'unknown'), " + 279 "`{system}` (eg. 'darwin'), `{channel}` (eg. 'stable'), and `{tool}` (eg. 'rustc.exe') will be " + 280 "replaced in the string if present." 281 ), 282 default = "@rust_{system}_{arch}__{triple}__{channel}_tools//:bin/{tool}", 283 ), 284 "srcs": attr.label_list( 285 doc = "Souce files of the crate to build. Passing source files here can be used to trigger rebuilds when changes are made", 286 allow_files = True, 287 ), 288 "timeout": attr.int( 289 doc = "Maximum duration of the Cargo build command in seconds", 290 default = 600, 291 ), 292 "version": attr.string( 293 doc = "The version of cargo the resolver should use", 294 default = rust_common.default_version, 295 ), 296 "_cc_toolchain": attr.label( 297 default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), 298 ), 299 }, 300) 301 302def cargo_env(env): 303 """A helper for generating platform specific environment variables 304 305 ```python 306 load("@rules_rust//rust:defs.bzl", "rust_common") 307 load("@rules_rust//cargo:defs.bzl", "cargo_bootstrap_repository", "cargo_env") 308 309 cargo_bootstrap_repository( 310 name = "bootstrapped_bin", 311 cargo_lockfile = "//:Cargo.lock", 312 cargo_toml = "//:Cargo.toml", 313 srcs = ["//:resolver_srcs"], 314 version = rust_common.default_version, 315 binary = "my-crate-binary", 316 env = { 317 "x86_64-unknown-linux-gnu": cargo_env({ 318 "FOO": "BAR", 319 }), 320 }, 321 env_label = { 322 "aarch64-unknown-linux-musl": cargo_env({ 323 "DOC": "//:README.md", 324 }), 325 } 326 ) 327 ``` 328 329 Args: 330 env (dict): A map of environment variables 331 332 Returns: 333 str: A json encoded string of the environment variables 334 """ 335 return json.encode(dict(env)) 336