/ 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      )