(zilch lang rust): document

Change-Id: I6a6a6964c8aaff8d5f3e18bc5c7486746b5a2952
This commit is contained in:
puck 2025-06-23 12:22:20 +00:00
parent ae774da043
commit 0340f6e830
10 changed files with 597 additions and 82 deletions

View file

@ -0,0 +1,179 @@
= 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"
;; . #<store path /0fnqikhh1rkw991hd37lzxhk13w5zzcs770jvn629d9mrs0zb6ax
;; (ca~ /nix/store/x1g5mjj7py2r263ma5314phfpcwa4gg7-rustc-bin-hello.drv!dep-info)>)
;; ("link"
;; . #<store path /05lmqcyk0kazll2i1jcp7299pfgxnxj0hw32hllxkb1iqxs4fqbm
;; (ca~ /nix/store/x1g5mjj7py2r263ma5314phfpcwa4gg7-rustc-bin-hello.drv!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 `<resolved-package>`
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.

View file

@ -0,0 +1,63 @@
= zilch-cli-rust(1)
Puck Meerburg
v0.0.1
:doctype: manpage
:manmanual: ZILCH-CLI-RUST
:mansource: ZILCH
:page-pagination: prev
== Name
zilch-cli-rust - builds a Rust crate using Zilch
== Synopsis
*zilch-cli-rust* [_OPTION_]... [_TARGET_]...
== Description
This command uses Zilch to build one or more Rust crate targets
entirely inside Nix, using content-addressed derivations.
This program requires a Rust crate consisting of a `Cargo.toml` and
with a valid and up-to-date `Cargo.lock`.
== Options
*-h*::
*--help*::
Print a help message
*-j* _COUNT_::
*--max-jobs* _COUNT_::
The maximum amount of builds to run. Defaults to the amount of cores.
*-v*::
*--verbose*::
Increase the verbosity configured in the Nix daemon. Can be specified
multiple times.
*-L*::
*--print-build-logs*::
Print derivation logs as they come in.
*-m* _DIR_::
*--crate-dir* _DIR_::
The directory to use as crate. This is also the crate that will be
used to find viable targets to build.
*-r* _DIR_::
*--replace* _DIR_::
Add the crate in _DIR_ to the possible dependencies, prioritizing it
over any crate in the `Cargo.lock`. Can be specified multiple times.
*-z* _PATH_::
*--overrides* _PATH_::
Read build script overrides from this file. By default, a stock set
of overrides is used, aimed at some crates. If an empty string is
used for _PATH_, this default set of overrides is not used. Can be
specified multiple times.
*--debug*::
Crash on the first error, rather than continuing to build the next
target.

View file

@ -0,0 +1,76 @@
= Usage
:page-pagination: next
Using `zilch-cli-rust`, you can compile Rust code incrementally crate-by-crate:
[,console]
----
$ git clone https://github.com/BurntSushi/ripgrep
$ zilch-cli-rust --crate-dir ripgrep/ <1>
ripgrep rg bin /nix/store/pxvqbn65lv2f4r3x1cszv5pr5l5mjwh9-rustc-bin-rg-link
grep grep lib not a binary
grep-cli grep_cli lib not a binary
globset globset lib not a binary
$ zilch-cli-rust --crate-dir ripgrep/ rg <2>
ripgrep rg bin /nix/store/pxvqbn65lv2f4r3x1cszv5pr5l5mjwh9-rustc-bin-rg-link
----
<1> Building all binary targets in a crate
<2> Building a specifically targeted target in a crate
Right now, the daemon has to be able to build `x86_64-linux` derivations, and
only the `x86_64-unknown-linux-gnu` Rust target is supported.
== Replacing dependencies
As part of Zilch, it's also possible to quickly build a crate with one of its
(transitive) dependencies replaced:
[,console]
----
$ git clone https://github.com/BurntSushi/bstr
$ zilch-cli-rust --crate-dir ripgrep/ --replace bstr/ rg
ripgrep rg bin /nix/store/6rf4r69sp8ijg2xrdglbnx8smphfg6fr-rustc-bin-rg-link
----
After editing a file any of the replaced dependencies, if this crate is used in
the final build, Zilch will apply early-cutoff where possible.
[IMPORTANT]
.Limitations in dependency selection
====
Zilch will never select dependencies that aren't found in the lockfile of the
primary crate. If there is no lockfile, or the dependencies have changed, it is
necessary to manually run `cargo generate-lockfile` to make sure all
dependencies can be found. See xref:./library.adoc#limits[The resolver's limitations] for more info.
====
== Build script overrides
Many Rust projects use build scripts that require external dependencies. To add
support for these, you can use a JSON file.
This file consists of a JSON object, where the key is the name of a crate, and
the value is an object with the following keys:
- `buildScript`: Overrides to apply when executing the build script for this
crate
- `buildScriptDependency`: Overrides to apply when executing the build script
for any crate that depends on this crate.
- `rustc`: Environment variables to add to the `rustc` invocation for this
crate.
Each of these keys contains a map of key to a Nix expression evaluated inside
`nixpkgs`. If multiple overrides apply to a single crate, each value is
concatenated, separated with a `:`. Zilch's runner supports `_zilch_pkgconfig`
as an environment variable as special case to add both `lib/pkgconfig` and
`share/pkgconfig`, as well as the paths inside all the `propagated-build-inputs`
of each store path, making `pkg-config` work. See
https://puck.moe/git/zilch/tree/cli/overrides.json[the default overrides] for
more examples on real-life crates.
If this mechanism is not expressive enough, it's possible to use Zilch's Rust
support directly, as a Scheme library.