A repository that builds images can also build a Rust project it has checked out, without packaging it first.
The project's committed Cargo.lock pins everything: Buck fetches each registry crate against the SHA-256
the lock records, and each git dependency as a fetch of exactly the locked commit. cargo itself
runs offline inside a box, and cargo-auditable writes the
crate graph into each binary so the image's SBOM reports those crates beside its packages.
examples/image-rust-project is a complete worked example: a builder box, two projects checked out
beside it, and an image carrying the resulting binaries.
load("@tine//cargo:defs.bzl", "cargo")
cargo.package(
name = "hello",
binaries = ["hello-cli"],
box = ":rust.box",
)- The project's own
Cargo.lockis picked out ofsrc; nothing declares it separately. A dynamic action reads it once it has been built and declares the fetches it names from there, so the lock can be a source file or an artifact another target produced, and the rule takes the same shape either way. srcdefaults to<name>: the checkout is expected in a directory named after the target. Passsrcexplicitly when it is called something else. Nothing needs to be added inside the checkout, i.e. a pristine clone of a project that commits itsCargo.lockworks.- Use
git.fetch()for a project that is not committed to this repository, and pass its work tree assrc. - The outermost
Cargo.lockinsrcmarks the workspace cargo builds in; a lock below it, such as a vendored project's, is never cargo's and is left alone. Two locks side by side name no single workspace, so narrowsrcthen. binariesnames what to take out oftarget/<triple>/release, and at least one is required: a crate that produces no binary has nothing an image could install. These are cargo binary names, which need not match the package. Each becomes a sub-target (:hello[hello-cli]), and together they are the target's default outputs. A name that the build does not produce fails the action. A name that is empty,.or.., contains a slash or a backslash, or is listed twice fails analysis.boxis the build environment, declared by the consumer because only the consumer knows what its projects link against.
The binaries are ordinary artifacts, so an image installs one with an image.ops.copy() operation:
image.archive(
name = "demo",
package_manager = ":image.package-manager",
packages = ["glibc"],
ops = [image.ops.copy(":hello[hello-cli]", "/usr/bin/hello-cli")],
format = "tar",
)box.new(
name = "rust.box",
packages = ["cargo", "gcc", "python3", "rust"],
release = "catalog//:fedora.rawhide.release",
)cargoandrustare the toolchain, andgcclinks.python3runs the in-box driver, because tine's drivers execute through the box's own interpreter.- Add whatever the project links against. A crate wrapping a C library needs that library's
-develpackage, and on Fedora anything usingpkg-configalso needsrpm, because/usr/bin/pkg-configis a wrapper that shells out torpm --eval. - The box is a build environment. It never becomes part of an image, and the toolchain is not shipped.
A crate that lives in git is pinned by the commit alone: every git source in the lock, transitive dependencies included, becomes a fetch of exactly the locked commit, which the hash itself verifies. Nothing else has to be pinned or committed, and the manifest needs no particular spelling of the dependency.
Git dependencies are not vendored: cargo builds each one inside its own fetched repository, so workspace inheritance and path dependencies between sibling crates behave exactly as in an online build, and cargo itself verifies that the repository holds the commit the lock names.
How the crates are pinned, fetched and vendored is described under "Rust source builds" in architecture.md.
- A project is one cache unit, and reruns are incremental for a
tine mount. Any change to its sources reruns a single action for the whole project, but cargo's build directory survives between runs, so that action recompiles only what changed.buck cleanis what forces a build from scratch. - A fetched or committed project keeps nothing between runs. Its build directory never reaches the cache, only the final build targets.
- Only crates.io registry sources work. Another registry is rejected with the package named, rather
than guessed at. A
Cargo.lockolder than version 3 is rejected too: it records no per-package checksums. - A project must commit its
Cargo.lock, even when it has no dependencies. This is good practice for a binary crate anyway, for ensuring reproducibility and moving surprise failures from unrelated PRs to dependabot/renovate ones. The build passes--locked, so a lock that no longer agrees withCargo.tomlfails the build instead of quietly resolving something else. The build also reads aCargo.lockthat git does not track. A local build then passes where a build from a fresh clone fails. For a project fromgit.fetch(), fetch a revision that contains the lock. - The checkout must live in the consuming repository. A Buck package can only glob its own cell, so a project registered as a separate workspace cell would have to carry a build file.
- A checkout's own
.cargo/config.tomlis refused, even one that.gitignoreexcludes. Cargo would read it ahead of the configuration the build writes; keep it out ofsrc. A.cargo/config.tomlor a workspaceCargo.tomlelsewhere in the consuming repository does not affect the build. - A build script cannot write beside its sources. The build mounts the sources read-only, so a
build.rsthat writes outsideOUT_DIRfails with a read-only file system error (EROFS). Cargo's documentation says that a build script should write only intoOUT_DIR. The vendored crates.io dependencies are read-only too, so update or patch a dependency whose build script writes into its sources. A git dependency is checked out into a writable directory, so its build script can write beside its sources. - A local
cargo buildinside the checkout needstarget/in.gitignore.tine initwrites it when it creates the.gitignore. If your project already had a.gitignore, addtarget/yourself. Buck reads every file that Git does not ignore as a source. While Git does not ignoretarget/,cargo.package()fails and names the directory. The build skips an ignoredtarget/when it looks for the project'sCargo.lock. - A cold daemon wants the network even when every crate is already cached, because Buck asks the registry for sizes the lock does not record.
- Two projects sharing a crate download it twice. Each project owns its downloads, and content-based paths dedupe only within a target, so the copies are separate. A shared crate pool, along the lines of the package pool a repository owns, would fix it.
cargo-auditableis a pinned upstream binary, not built from source, because building it from crates.io would need the mechanism it exists to serve. It is trusted like the pinnedbuck2,python3andsyft.