/ toolchains / rust_crate.bzl
rust_crate.bzl
1 # toolchains/rust_crate.bzl 2 # 3 # Fetch and build crates from crates.io 4 # 5 # Simple model: 6 # 1. http_archive fetches the crate tarball 7 # 2. rust_crate compiles it with proper flags 8 # 3. Dependencies are just deps=[] 9 # 10 # Example: 11 # crates.io_crate( 12 # name = "serde", 13 # version = "1.0.228", 14 # sha256 = "...", 15 # features = ["derive"], 16 # deps = [":serde_derive"], 17 # ) 18 19 load("@straylight_prelude//http_archive.bzl", "http_archive") 20 21 # Provider for crate outputs 22 RustCrateInfo = provider(fields = [ 23 "rlib", # Compiled .rlib 24 "rmeta", # Metadata for pipelining 25 "crate_name", # Crate name (underscores) 26 "edition", # Rust edition 27 "features", # Enabled features 28 "is_proc_macro", # Is this a proc-macro crate? 29 "transitive_deps", # List of all transitive rlib artifacts (for -L paths) 30 ]) 31 32 def _crate_url(name: str, version: str) -> str: 33 """Get crates.io download URL.""" 34 return "https://static.crates.io/crates/{}/{}/download".format(name, version) 35 36 def _rust_crate_impl(ctx: AnalysisContext) -> list[Provider]: 37 """Build a crate from crates.io.""" 38 39 rustc = read_root_config("rust", "rustc", "rustc") 40 41 # Crate name with underscores (Rust convention) 42 crate_name = ctx.attrs.crate_name or ctx.attrs.name.replace("-", "_") 43 44 # Output files 45 if ctx.attrs.proc_macro: 46 # Proc macros are .so files 47 out = ctx.actions.declare_output("lib{}.so".format(crate_name)) 48 crate_type = "proc-macro" 49 else: 50 out = ctx.actions.declare_output("lib{}.rlib".format(crate_name)) 51 crate_type = "rlib" 52 53 cmd = cmd_args([rustc]) 54 55 # Crate type and name 56 cmd.add("--crate-type", crate_type) 57 cmd.add("--crate-name", crate_name) 58 59 # Edition 60 cmd.add("--edition", ctx.attrs.edition) 61 62 # Output path 63 cmd.add("-o", out.as_output()) 64 65 # Optimization 66 cmd.add("-O") 67 68 # Allow warnings (crates often have minor issues) 69 cmd.add("-Awarnings") 70 71 # Proc-macro crates need access to the proc_macro crate from sysroot 72 if ctx.attrs.proc_macro: 73 cmd.add("--extern", "proc_macro") 74 75 # Features 76 for feature in ctx.attrs.features: 77 cmd.add("--cfg", 'feature="{}"'.format(feature)) 78 79 # Cfg flags 80 for cfg in ctx.attrs.cfg: 81 cmd.add("--cfg", cfg) 82 83 # Rustc flags 84 for flag in ctx.attrs.rustc_flags: 85 cmd.add(flag) 86 87 # Collect transitive deps for propagation (include our own output) 88 transitive_deps = [] 89 90 # Collect dependency search paths and externs 91 for dep in ctx.attrs.deps: 92 if RustCrateInfo in dep: 93 info = dep[RustCrateInfo] 94 cmd.add(cmd_args("--extern", cmd_args(info.crate_name, "=", info.rlib, delimiter = ""))) 95 # Add the directory to -L so this dep can be found 96 cmd.add(cmd_args(info.rlib, format = "-Ldependency={}", parent = 1)) 97 # Collect this dep's rlib for transitive propagation 98 transitive_deps.append(info.rlib) 99 # Also add all transitive deps' -L paths 100 for trans_rlib in info.transitive_deps: 101 cmd.add(cmd_args(trans_rlib, format = "-Ldependency={}", parent = 1)) 102 transitive_deps.append(trans_rlib) 103 104 # Source directory from http_archive 105 src_dir = ctx.attrs.src[DefaultInfo].default_outputs[0] 106 107 # Crate root - path relative to extracted archive 108 crate_root = ctx.attrs.crate_root or "src/lib.rs" 109 cmd.add(cmd_args(src_dir, format = "{{}}/{}".format(crate_root))) 110 111 # Build env dict for Cargo-like environment variables 112 env = {} 113 for key, val in ctx.attrs.env.items(): 114 env[key] = val 115 116 # Handle generated files (for build.rs output simulation) 117 if ctx.attrs.generated_files: 118 generated_outputs = [] 119 for filename, content in ctx.attrs.generated_files.items(): 120 gen_file = ctx.actions.declare_output("generated/{}".format(filename)) 121 ctx.actions.write(gen_file, content) 122 generated_outputs.append(gen_file) 123 # Set OUT_DIR to the absolute directory containing generated files 124 # The project root for Buck2 in this setup 125 project_root = read_root_config("project", "root", "/home/b7r6/src/straylight/aleph") 126 env["OUT_DIR"] = cmd_args(generated_outputs[0], parent = 1, format = project_root + "/{}") 127 cmd.add(cmd_args(hidden = generated_outputs)) 128 129 ctx.actions.run(cmd, category = "rustc", identifier = crate_name, env = env) 130 131 return [ 132 DefaultInfo(default_output = out), 133 RustCrateInfo( 134 rlib = out, 135 rmeta = out, # Use rlib as rmeta for simplicity 136 crate_name = crate_name, 137 edition = ctx.attrs.edition, 138 features = ctx.attrs.features, 139 is_proc_macro = ctx.attrs.proc_macro, 140 transitive_deps = transitive_deps, 141 ), 142 ] 143 144 rust_crate = rule( 145 impl = _rust_crate_impl, 146 attrs = { 147 "src": attrs.dep(), # http_archive target 148 "crate_name": attrs.option(attrs.string(), default = None), 149 "crate_root": attrs.option(attrs.string(), default = None), 150 "edition": attrs.string(default = "2021"), 151 "features": attrs.list(attrs.string(), default = []), 152 "cfg": attrs.list(attrs.string(), default = []), 153 "deps": attrs.list(attrs.dep(), default = []), 154 "proc_macro": attrs.bool(default = False), 155 "rustc_flags": attrs.list(attrs.string(), default = []), 156 "env": attrs.dict(attrs.string(), attrs.string(), default = {}), 157 "generated_files": attrs.dict(attrs.string(), attrs.string(), default = {}), 158 }, 159 ) 160 161 # Convenience macro to fetch and build a crate from crates.io 162 def crates_io( 163 name: str, 164 version: str, 165 sha256: str, 166 features: list[str] = [], 167 deps: list[str] = [], 168 proc_macro: bool = False, 169 edition: str = "2021", 170 crate_root: str | None = None, 171 rustc_flags: list[str] = [], 172 pkg_name: str | None = None, 173 crate_name: str | None = None, 174 env: dict[str, str] = {}, 175 generated_files: dict[str, str] = {}, 176 visibility: list[str] = ["PUBLIC"]): 177 """ 178 Fetch and build a crate from crates.io. 179 180 Args: 181 name: Buck target name 182 version: Crate version 183 sha256: SHA256 of the crate tarball 184 pkg_name: Package name on crates.io (defaults to name) 185 crate_name: Crate name for rustc --extern (defaults to name with - replaced by _) 186 env: Extra environment variables to set during build 187 188 Example: 189 crates_io( 190 name = "serde", 191 version = "1.0.228", 192 sha256 = "abc123...", 193 features = ["derive"], 194 deps = [":serde_derive"], 195 ) 196 """ 197 198 # pkg_name is the crates.io package name, defaults to target name 199 pkg = pkg_name or name 200 201 archive_name = "{}-{}.crate".format(name, version) 202 203 # Fetch the crate 204 http_archive( 205 name = archive_name, 206 urls = [_crate_url(pkg, version)], 207 sha256 = sha256, 208 strip_prefix = "{}-{}".format(pkg, version), 209 ) 210 211 # Parse version for Cargo-like env vars 212 version_parts = version.split(".") 213 major = version_parts[0] if len(version_parts) > 0 else "0" 214 minor = version_parts[1] if len(version_parts) > 1 else "0" 215 patch_full = version_parts[2] if len(version_parts) > 2 else "0" 216 # Handle pre-release suffixes like "1.0.25-alpha" 217 patch = patch_full.split("-")[0].split("+")[0] 218 219 # Generate Cargo-like environment variables 220 cargo_env = { 221 "CARGO_PKG_NAME": pkg, 222 "CARGO_PKG_VERSION": version, 223 "CARGO_PKG_VERSION_MAJOR": major, 224 "CARGO_PKG_VERSION_MINOR": minor, 225 "CARGO_PKG_VERSION_PATCH": patch, 226 } 227 # Merge with user-provided env (user env takes precedence) 228 for k, v in env.items(): 229 cargo_env[k] = v 230 231 # Build it 232 rust_crate( 233 name = name, 234 src = ":{}".format(archive_name), 235 crate_name = crate_name, 236 edition = edition, 237 features = features, 238 deps = deps, 239 proc_macro = proc_macro, 240 crate_root = crate_root, 241 rustc_flags = rustc_flags, 242 env = cargo_env, 243 generated_files = generated_files, 244 visibility = visibility, 245 )