= Zilch's Rust support :page-pagination: Zilch supports using the Rust compiler directly from Scheme. This can be used to compile single crates, or to drive the crate support more directly. == Compiling Rust code To compile Rust code, xref:generated:zilch.lang.rust.adoc[`(zilch lang rust)`] provides a `call-rustc` procedure, which handles most of the code necessary to compile Rust code. [,scheme,line-comment=;] ---- (define gcc (cdr (assoc "out" (nixpkgs "gcc")))) ; <1> (define outputs (call-rustc (zfile "fn main() { println!(\"Hello, World!\"); }") ; <2> '() ; <3> crate-type: 'bin crate-name: "hello" edition: "2021" emits: '(dep-info: #t link: #t) ; <4> codegen-flags: (cons "linker" #~,(string-append #$gcc "/bin/cc")))) ; <5> ;; (("dep-info" ;; . #) ;; ("link" ;; . #)) ---- <1> Get a copy of the `out` output for `gcc` in nixpkgs. <2> Use a file defined inline to compile. <3> Don't add any variables to the environment. <4> Make `rustc` only emit the `dep-info` and `link` outputs. <5> Tell `rustc` where to find the linker. The resulting binary is linked dynamically, using the linker passed either as `codegen-flag`, or findable as `cc` on `$PATH`. By default, each `emits:` entry ends up as a separate output; they are all part of the same derivation, though. To compile more than one file, a more involved structure has to be used; Rust expects the file structure to exist on-disk, and `mod` works relative to the directory the current file is in. [,scheme,line-comment=;] ---- (define multi-file (zdir "lib.rs" (zfile "pub mod foo;") "foo.rs" (zfile "pub fn hello(who: String) { println!(\"Hello, {who}\"); }"))) (define outputs (call-rustc #~,(string-append #$multi-file "/lib.rs") ; <1> '() crate-type: 'rlib crate-name: "hello-lib" edition: "2021" emits: '(metadata: #t link: #t))) ---- <1> Import `multi-file` (the directory) to store, append `/lib.rs` to its store path, then use that as the file input to `rustc`. This construction is equivalent to `"${./multi-file}/lib.rs"` in Nix. == Reading Cargo files Cargo files contain a lot of automated detection of paths. For example, a binary can live at `src/bin.rs`, or at `src/bins/foo.rs`. To handle this, Zilch requires the Cargo file be accompanied with a VFS representing the crate. To handle workspaces, parsing a `Cargo.toml` takes a workspace, and returns both the crate _and_ workspace defined in the file, where applicable. This is handled by xref:generated:zilch.lang.rust.cargo.adoc[`parse-cargo-toml`]. Reading a `Cargo.lock` is handled by `parse-lockfile`, which takes in the contents of a lockfile, and parses it into a list of lockfile entries. Each entry in the lockfile can be used in xref:generated:zilch.lang.rust.registry.adoc[`fetch-and-unpack-crate`] to fetch the crate, though this is usually done automatically by the resolver. == The resolver The rust resolver in Zilch handles selecting dependencies from a `Cargo.lock` for one or more Cargo targets. To use it, first parse a lockfile and a `Cargo.toml`: [,scheme,line-comment=;] ---- (define example-vfs (vfs-from-directory "example")) (define lockfile (parse-lockfile (read-file "example/Cargo.lock"))) <1> (define-values (crate workspace) (parse-cargo-toml example-vfs (read-file "example/Cargo.toml") #f)) ---- <1> `read-file` is an example procedure, and isn't part of Zilch (yet). If desired, add more `Cargo.toml` files to prefer over any crates found in the `Cargo.lock`: [,scheme,line-comment=;] ---- (define override-vfs (vfs-from-directory "override-example")) (define-values (override-crate workspace) (parse-cargo-toml override-vfs (read-file "override-example/Cargo.toml") #f)) ---- Then, call the resolver: [,scheme,line-comment=;] ---- (define targets (process-many-with-lockfile (list (cons crate example-vfs) (cons override-crate override-vfs)) lockfile)) ---- The result from `process-many-with-lockfile` is a list of `` records, each representing a Cargo target in the crates that have been processed. [CAUTION] ==== By default, the resolver will set _no_ features on any crate, not even default ones. As exception, if a binary crate is available, it _will_ have the default feature set, which is likely to percolate down to most other crates it depends on. There is currently no way to change this behavior easily. ==== When a target is to be built, it can be used with `build-package`: [,scheme,line-comment=;] ---- (define (build-script-env-override-example crate-name is-dependency) (if (and is-dependency (string=? crate-name "test")) ; <1> (list (cons "PATH" #~,(string-append #$(cdr (assoc "out" (nixpkgs "hello"))) "/bin"))) #f)) (define (compiler-env-override-example crate-name is-dependency) (if (and (not is-dependency) (string=? crate-name "test")) ; <2> '(("HELLO_STRING" . "Hello!")) #f)) (map (lambda (pkg) (build-package pkg build-script-env-override-example compiler-env-overridde-example)) ; <3> targets) ---- <1> Add GNU Hello to the PATH of any build script that depends on the crate `test` <2> If the crate `test` is built, set `HELLO_STRING`. This can be read from Rust using the `env!` macro. <3> Use the overrides when building each crate's targets. [#limits] === Limitations The `(zilch lang rust resolver)` implements most of the logic of a https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions[v1 resolver], which handles selecting dependencies from a set of candidates. The biggest difference between Zilch's resolver and the canonical implemention (in Cargo) is that the resolver in Zilch will never fall back to looking for a candidate inside any configured Cargo registries. If a crate isn't found in the `Cargo.lock` it is considered an error. While this makes using Zilch a tad more cumbersome, it means that Zilch itself is entirely deterministic on the files of the crates it processes.