/ toolchains / rust.bzl
rust.bzl
  1  # toolchains/rust.bzl
  2  #
  3  # Hermetic Rust toolchain and rules using Nix store paths.
  4  #
  5  # Paths are read from .buckconfig.local [rust] section, generated by `nix develop`.
  6  # No PATH lookup. Just absolute Nix store paths.
  7  #
  8  # Rules:
  9  #   rust_toolchain - toolchain definition
 10  #   rust_binary    - executable
 11  #   rust_library   - rlib
 12  
 13  # NOTE: Use upstream @prelude types for toolchain provider compatibility.
 14  # Our custom rust_binary/rust_library rules below don't use the toolchain
 15  # provider - they read config directly. But if anyone uses prelude rust rules,
 16  # they'll need the upstream provider type.
 17  load("@prelude//rust:rust_toolchain.bzl", "PanicRuntime", "RustToolchainInfo")
 18  
 19  # Import RustCrateInfo so we can handle deps from crates_io()
 20  load("@toolchains//rust_crate.bzl", "RustCrateInfo")
 21  
 22  # Provider for rust library outputs
 23  RustLibraryInfo = provider(fields = [
 24      "rlib",           # The .rlib artifact
 25      "crate_name",     # Crate name for --extern
 26      "transitive_deps", # List of all transitive rlib artifacts (for -L paths)
 27  ])
 28  
 29  def _rust_toolchain_impl(ctx: AnalysisContext) -> list[Provider]:
 30      """
 31      Rust toolchain with paths from .buckconfig.local.
 32  
 33      Reads [rust] section for absolute Nix store paths:
 34        rustc       - Rust compiler
 35        rustdoc     - Documentation generator
 36        clippy      - Linter
 37        cargo       - Package manager (for build scripts)
 38      """
 39  
 40      # Read tool paths from config
 41      rustc = read_root_config("rust", "rustc", "rustc")
 42      rustdoc = read_root_config("rust", "rustdoc", "rustdoc")
 43      clippy_driver = read_root_config("rust", "clippy_driver", "clippy-driver")
 44  
 45      # Target triple
 46      target_triple = read_root_config("rust", "target_triple", "x86_64-unknown-linux-gnu")
 47  
 48      return [
 49          DefaultInfo(),
 50          RustToolchainInfo(
 51              compiler = RunInfo(args = [rustc]),
 52              rustdoc = RunInfo(args = [rustdoc]),
 53              clippy_driver = RunInfo(args = [clippy_driver]),
 54              rustc_target_triple = target_triple,
 55              default_edition = ctx.attrs.default_edition,
 56              panic_runtime = PanicRuntime(ctx.attrs.panic_runtime),
 57              rustc_flags = ctx.attrs.rustc_flags,
 58              rustc_binary_flags = ctx.attrs.rustc_binary_flags,
 59              rustc_test_flags = ctx.attrs.rustc_test_flags,
 60              rustdoc_flags = ctx.attrs.rustdoc_flags,
 61              allow_lints = ctx.attrs.allow_lints,
 62              deny_lints = ctx.attrs.deny_lints,
 63              warn_lints = ctx.attrs.warn_lints,
 64              report_unused_deps = ctx.attrs.report_unused_deps,
 65              doctests = ctx.attrs.doctests,
 66          ),
 67      ]
 68  
 69  rust_toolchain = rule(
 70      impl = _rust_toolchain_impl,
 71      attrs = {
 72          "default_edition": attrs.string(default = "2021"),
 73          "panic_runtime": attrs.string(default = "unwind"),
 74          "rustc_flags": attrs.list(attrs.string(), default = []),
 75          "rustc_binary_flags": attrs.list(attrs.string(), default = []),
 76          "rustc_test_flags": attrs.list(attrs.string(), default = []),
 77          "rustdoc_flags": attrs.list(attrs.string(), default = []),
 78          "allow_lints": attrs.list(attrs.string(), default = []),
 79          "deny_lints": attrs.list(attrs.string(), default = []),
 80          "warn_lints": attrs.list(attrs.string(), default = []),
 81          "report_unused_deps": attrs.bool(default = False),
 82          "doctests": attrs.bool(default = False),
 83      },
 84      is_toolchain_rule = True,
 85  )
 86  
 87  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 88  # rust_binary
 89  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 90  
 91  def _rust_binary_impl(ctx: AnalysisContext) -> list[Provider]:
 92      """
 93      Build a Rust binary.
 94      
 95      Simple: rustc srcs -o output
 96      """
 97      rustc = read_root_config("rust", "rustc", "rustc")
 98      
 99      out = ctx.actions.declare_output(ctx.attrs.name)
100      
101      cmd = cmd_args([rustc])
102      
103      # Edition
104      cmd.add("--edition", ctx.attrs.edition)
105      
106      # Optimization
107      cmd.add("-O")
108      
109      # Output
110      cmd.add("-o", out.as_output())
111      
112      # Collect deps (rlibs) - handle both RustLibraryInfo and RustCrateInfo
113      for dep in ctx.attrs.deps:
114          if RustLibraryInfo in dep:
115              lib_info = dep[RustLibraryInfo]
116              cmd.add(cmd_args("--extern", cmd_args(lib_info.crate_name, "=", lib_info.rlib, delimiter = "")))
117              # Add search path for RustLibraryInfo deps too
118              cmd.add(cmd_args(lib_info.rlib, format = "-Ldependency={}", parent = 1))
119              # Add search paths for all transitive deps
120              for trans_rlib in lib_info.transitive_deps:
121                  cmd.add(cmd_args(trans_rlib, format = "-Ldependency={}", parent = 1))
122          elif RustCrateInfo in dep:
123              crate_info = dep[RustCrateInfo]
124              cmd.add(cmd_args("--extern", cmd_args(crate_info.crate_name, "=", crate_info.rlib, delimiter = "")))
125              # Add search path for this dep
126              cmd.add(cmd_args(crate_info.rlib, format = "-Ldependency={}", parent = 1))
127              # Add search paths for all transitive deps
128              for trans_rlib in crate_info.transitive_deps:
129                  cmd.add(cmd_args(trans_rlib, format = "-Ldependency={}", parent = 1))
130      
131      # Binary root is the first source file
132      # Other sources are included as hidden deps so they're tracked for rebuilds
133      if ctx.attrs.srcs:
134          cmd.add(ctx.attrs.srcs[0])
135          if len(ctx.attrs.srcs) > 1:
136              cmd.add(cmd_args(hidden = ctx.attrs.srcs[1:]))
137      
138      ctx.actions.run(cmd, category = "rustc")
139      
140      return [
141          DefaultInfo(default_output = out),
142          RunInfo(args = cmd_args(out)),
143      ]
144  
145  rust_binary = rule(
146      impl = _rust_binary_impl,
147      attrs = {
148          "srcs": attrs.list(attrs.source()),
149          "deps": attrs.list(attrs.dep(), default = []),
150          "edition": attrs.string(default = "2021"),
151      },
152  )
153  
154  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
155  # rust_library
156  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
157  
158  def _rust_library_impl(ctx: AnalysisContext) -> list[Provider]:
159      """
160      Build a Rust library (rlib or proc-macro).
161      
162      The first source file is treated as the crate root (lib.rs).
163      Other sources are tracked as dependencies but not passed directly to rustc.
164      """
165      rustc = read_root_config("rust", "rustc", "rustc")
166      
167      crate_name = ctx.attrs.crate_name or ctx.attrs.name
168      
169      # Determine crate type and output extension
170      if ctx.attrs.proc_macro:
171          out = ctx.actions.declare_output("lib{}.so".format(crate_name))
172          crate_type = "proc-macro"
173      else:
174          out = ctx.actions.declare_output("lib{}.rlib".format(crate_name))
175          crate_type = "rlib"
176      
177      cmd = cmd_args([rustc])
178      
179      # Build crate
180      cmd.add("--crate-type", crate_type)
181      cmd.add("--crate-name", crate_name)
182      
183      # Edition
184      cmd.add("--edition", ctx.attrs.edition)
185      
186      # Optimization
187      cmd.add("-O")
188      
189      # Output
190      cmd.add("-o", out.as_output())
191      
192      # Proc-macro crates need access to the proc_macro crate from sysroot
193      if ctx.attrs.proc_macro:
194          cmd.add("--extern", "proc_macro")
195      
196      # Features
197      for feature in ctx.attrs.features:
198          cmd.add("--cfg", 'feature="{}"'.format(feature))
199      
200      # Collect transitive deps for propagation
201      transitive_deps = []
202      
203      # Collect deps - handle both RustLibraryInfo and RustCrateInfo
204      for dep in ctx.attrs.deps:
205          if RustLibraryInfo in dep:
206              lib_info = dep[RustLibraryInfo]
207              cmd.add(cmd_args("--extern", cmd_args(lib_info.crate_name, "=", lib_info.rlib, delimiter = "")))
208              # Add search path for RustLibraryInfo deps too
209              cmd.add(cmd_args(lib_info.rlib, format = "-Ldependency={}", parent = 1))
210              # Collect this dep's rlib for transitive propagation
211              transitive_deps.append(lib_info.rlib)
212              # Also add all of its transitive deps
213              for trans_rlib in lib_info.transitive_deps:
214                  cmd.add(cmd_args(trans_rlib, format = "-Ldependency={}", parent = 1))
215                  transitive_deps.append(trans_rlib)
216          elif RustCrateInfo in dep:
217              crate_info = dep[RustCrateInfo]
218              cmd.add(cmd_args("--extern", cmd_args(crate_info.crate_name, "=", crate_info.rlib, delimiter = "")))
219              # Add search path for this dep
220              cmd.add(cmd_args(crate_info.rlib, format = "-Ldependency={}", parent = 1))
221              # Collect this dep's rlib for transitive propagation
222              transitive_deps.append(crate_info.rlib)
223              # Add search paths for all transitive deps
224              for trans_rlib in crate_info.transitive_deps:
225                  cmd.add(cmd_args(trans_rlib, format = "-Ldependency={}", parent = 1))
226                  transitive_deps.append(trans_rlib)
227      
228      # Crate root is the first source file
229      # Other sources are included as hidden deps so they're tracked for rebuilds
230      if ctx.attrs.srcs:
231          cmd.add(ctx.attrs.srcs[0])
232          if len(ctx.attrs.srcs) > 1:
233              cmd.add(cmd_args(hidden = ctx.attrs.srcs[1:]))
234      
235      ctx.actions.run(cmd, category = "rustc")
236      
237      return [
238          DefaultInfo(default_output = out),
239          RustLibraryInfo(rlib = out, crate_name = crate_name, transitive_deps = transitive_deps),
240      ]
241  
242  rust_library = rule(
243      impl = _rust_library_impl,
244      attrs = {
245          "srcs": attrs.list(attrs.source()),
246          "deps": attrs.list(attrs.dep(), default = []),
247          "edition": attrs.string(default = "2021"),
248          "crate_name": attrs.option(attrs.string(), default = None),
249          "proc_macro": attrs.bool(default = False),
250          "features": attrs.list(attrs.string(), default = []),
251      },
252  )