@git.zone/tsrust
A CLI build tool for Rust projects that follows the same conventions as @git.zone/tsbuild. It detects your rust/ source directory, parses Cargo.toml (including workspaces), runs cargo build --release with a managed target cache, and copies the resulting binaries into a clean dist_rust/ directory at the project root.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
Install globally with pnpm:
pnpm add -g @git.zone/tsrust
Or as a project-level dev dependency:
pnpm add --save-dev @git.zone/tsrust
⚡ No Rust required! If
cargoisn't found on your system,tsrustautomatically downloads and installs a minimal Rust toolchain to/tmp/tsrust_toolchain/. This gives a zero-setup experience. If you already have Rust installed,tsrustuses your system toolchain.
The Convention
tsrust mirrors the directory convention established by tsbuild:
| Tool | Source Directory | Output Directory |
|---|---|---|
tsbuild |
ts/ |
dist_ts/ |
tsrust |
rust/ |
dist_rust/ |
Your Rust code lives in rust/ (or ts_rust/ as fallback), and compiled binaries land in dist_rust/ — ready for packaging, deployment, or further tooling.
Usage
🔨 Build (Default Command)
Simply run tsrust from your project root:
tsrust
This will:
- Detect the Rust toolchain (system
cargo, or auto-install to/tmp/tsrust_toolchain/) - Locate your
rust/directory (containingCargo.toml) - Parse the workspace to discover all
[[bin]]targets - With native notices configured: install the build's missing rustup targets, run the notices check's own
cargo metadata --format-version 1 --locked --filter-platform <triple>online once per notices target so Cargo downloads exactly the packages the check reads, and verify the committed notices offline - Run
cargo build --releasewith full streaming output, for the host's own targets only (see Target Selection), with every local build directory remapped (see Path Remapping) - Store Cargo intermediates in
.nogit/tsrust-targetof the building checkout by default; a linked Git worktree seeds its missing cache once from its canonical checkout (see Target Cache) - Copy each binary to
dist_rust/with executable permissions (chmod 755), replacing only the artifacts this build produced; a host or--targetbuild fails when Cargo produced no binary, so it never leaves an earlier build's artifact in place, and every build fails when a binary still contains a local path that rustc recorded (see Path Remapping) - Write a SHA-256-bound provenance sidecar without changing the binary bytes
- With native notices configured, write the third-party notices of the binary's target beside it
- Report file sizes and total build time
Example output:
Using cargo 1.90.0 (840b83a10 2025-07-30)
Found Rust project at: rust
Detected Cargo workspace
Binary targets: rustproxy
Running: CARGO_TARGET_DIR="/path/to/project/.nogit/tsrust-target" CARGO_BUILD_BUILD_DIR="/path/to/project/.nogit/tsrust-target" CARGO_ENCODED_RUSTFLAGS="--remap-path-prefix=/path/to/project=/workspace\x1f…" cargo build --release --message-format=json-render-diagnostics
Compiling rustproxy v0.1.0
Finished `release` profile [optimized] target(s) in 29.01s
Copied rustproxy (13.4 MB) -> dist_rust/rustproxy
Wrote provenance: @example/rustproxy@1.0.0 abc123def456 (native)
Done in 29.2s
Automatic Rust Toolchain
tsrust provides a zero-setup experience through automatic toolchain management:
- System toolchain detected → uses it as-is (no download, no overhead)
- No system toolchain → checks
/tmp/tsrust_toolchain/for a previously installed bundled toolchain - No ready bundled toolchain → installs a minimal host-qualified stable toolchain with
rustup-init, or cleanly reinstalls a secure but incomplete managed toolchain
The bundled toolchain is stored in /tmp/, so it's cleaned up on reboot. Subsequent runs reuse the existing installation. Before executing it, tsrust requires the root to be a current-user-owned, non-symlink directory, restricts it to mode 0700, verifies that cargo and rustup resolve to current-user-owned executables inside that root that are not group- or world-writable, and binds cargo, rustc, and target-manifest checks to the exact managed stable toolchain for the execution host. A securely resolved installation that fails readiness is removed and freshly installed through rustup under the installation lock. An atomically published current-user-owned installation lock serializes validation and bootstrap. A dead installer fails closed with the exact lock path for operator inspection instead of risking concurrent stale-lock deletion.
Supported platforms for automatic install: Linux (x64, arm64) and macOS (x64, arm64).
🐛 Debug Build
Build with the debug profile instead of release:
tsrust --debug
Binaries are taken from .nogit/tsrust-target/debug/ instead of .nogit/tsrust-target/release/.
🧹 Clean Before Building
Run cargo clean before building to force a full rebuild:
tsrust --clean
Target Selection
Building other architectures is always explicit. A plain tsrust compiles only the configured targets that match the host's operating system and CPU architecture, in every configured C runtime: on an x86_64 Linux host with linux_amd64, linux_arm64 and linux_amd64_musl configured, it builds linux_amd64 and linux_amd64_musl. That is the build for development and tests.
| Command | Builds | dist_rust/ |
|---|---|---|
tsrust |
configured targets of the host's OS and architecture | replaces only the artifacts it built; other targets stay |
tsrust --configured-targets |
every target configured for this build host (targetsByHost entry or targets) |
emptied first, then rebuilt from the complete set |
tsrust --target <t> [--target <t>] |
exactly the named targets | replaces only the artifacts it built; other targets stay |
tsrust without configured targets |
the native host build, unsuffixed | emptied first, as before |
When targets are configured but none matches the host (for example a macOS machine with only Linux targets configured), a plain build fails and names --target and --configured-targets. --target and --configured-targets cannot be combined.
Builds that replace individual artifacts write each binary with its provenance sidecar and third-party notices under the .nogit/tsrust-assembly.lock publication lock: a new copy is staged beside it and renamed over the old binary, and the old sidecar and notices are removed before the new ones are written. Every build takes the same lock. While another build or an assembly holds it, a build waits and reports the holder, bounded like the target cache wait. The lock records its holder's process identity: a machine id (a hash of /etc/machine-id, else /var/lib/dbus/machine-id, else the hostname), the boot id, the PID namespace, the process id, and the process start time from /proc/<pid>/stat. A lock whose holder provably ended is released and taken over, unless an interrupted assembly left its transaction: then the build fails and tsrust assemble recovers the transaction. A holder has ended when the machine restarted since (another boot id), when no process with its process id runs, or when that process id now belongs to a process with another start time. A process id that /proc does not show (hidepid, another user's process) but that is in use counts as running. Network interface changes do not affect the identity. Lock takeover relies on distinct machine ids: a workspace on a network filesystem shared by machines with the same machine id (for example cloned images) is not supported. While another process recovers a dead holder's lock, a build waits for it like for a running holder, and refuses a recovery claim recorded on another machine or in another PID namespace, naming the claim file. A lock recorded on another machine or in another PID namespace of the same kernel (another container) is refused, as tsrust cannot check its process; the error names the lock file to remove once no build runs there. Platforms other than Linux have no boot id or start time: there a holder counts as running while its process id is in use, so a reused process id holds the lock until the wait times out, and the timeout error names the lock file to remove. A lock written by tsrust 1.x is judged as before when its host fingerprint matches this host, by its process id alone, so its timeout error also names the lock file; otherwise the build fails and names the lock file instead of guessing. Artifacts of other targets can stem from an older commit; their sidecars say which, and tsrust verify rejects such a set.
Cross-compile for other OS/architecture combinations using the --target flag:
# Cross-compile for a single target
tsrust --target linux_arm64
# Cross-compile for multiple targets
tsrust --target linux_arm64 --target linux_amd64
# Full Rust triples are also accepted
tsrust --target aarch64-unknown-linux-gnu
Supported friendly target names:
| Friendly name | Rust target triple |
|---|---|
linux_amd64 |
x86_64-unknown-linux-gnu |
linux_arm64 |
aarch64-unknown-linux-gnu |
linux_amd64_musl |
x86_64-unknown-linux-musl |
linux_arm64_musl |
aarch64-unknown-linux-musl |
macos_amd64 |
x86_64-apple-darwin |
macos_arm64 |
aarch64-apple-darwin |
With configured targets or --target, output binaries are named <binname>_<os>_<arch>:
dist_rust/
├── rustproxy_linux_arm64
├── rustproxy_linux_arm64.tsrust-build.json
├── rustproxy_linux_amd64
└── rustproxy_linux_amd64.tsrust-build.json
tsrust automatically installs missing rustup targets via rustup target add when needed. With native notices configured, the build does this for its targets before the notices check. The check reads the rust-src component and every configured musl target, including ones this host does not build, and installs neither: declare them in rust-toolchain.toml (see Native Notices), or it refuses and names the rustup component add rust-src or rustup target add <triple> command to run.
Configuration via .smartconfig.json
Configure the project's target matrix in its .smartconfig.json:
{
"@git.zone/tsrust": {
"targets": ["linux_arm64", "linux_amd64"],
"locked": true,
"targetDir": ".nogit/tsrust-target",
"pruneAfterBuild": false
}
}
The configured targets are the release matrix: tsrust --configured-targets builds all of them that belong to this build host, while a plain tsrust builds only those of the host's own architecture. CLI --target flags determine the selected targets when provided. The complete configuration is still validated first, so malformed legacy or host-specific entries are always rejected.
For builds split across Linux and macOS hosts, select targets by build host:
{
"@git.zone/tsrust": {
"targetsByHost": {
"linux": ["linux_amd64", "linux_arm64"],
"macos": ["macos_amd64", "macos_arm64"]
},
"locked": true
}
}
The configured set of a build host uses this precedence: exact host key, OS-family key, legacy targets, then a native build; a plain tsrust then keeps the entries of the host's architecture. Exact host keys are linux_amd64, linux_arm64, macos_amd64, and macos_arm64; OS-family keys are linux and macos. The selected entries are not merged, so an exact host entry completely replaces its family entry for that host.
locked: true runs Cargo builds with --locked for deterministic dependency resolution. This requires Cargo.lock to exist and remain consistent with the manifests; Cargo fails instead of updating an inconsistent lockfile.
targetDir is optional. It must point to a tsrust-owned path under .nogit/, such as .nogit/tsrust-target or .nogit/tsrust-custom-target. You can also set TSRUST_TARGET_DIR for one-off runs. pruneAfterBuild: true or TSRUST_PRUNE_AFTER_BUILD=true removes the marked managed target cache after binaries have been copied to dist_rust/.
Target Cache
Every checkout keeps its own Cargo target cache: .nogit/tsrust-target, or the targetDir configuration or TSRUST_TARGET_DIR. Checkouts never share one, because Cargo identifies workspace crates by their path relative to the workspace root and judges their freshness by modification times, so a shared cache could link crates compiled from another checkout's source.
A project in a linked Git worktree (git worktree add) with the default location starts from a warm cache anyway. When its cache does not exist yet, the first build seeds it once from the .nogit/tsrust-target of the same project directory in the canonical checkout, found through the Git common directory:
- The canonical cache must carry the tsrust cache marker; otherwise the worktree builds from an empty cache.
- The copy uses reflinks where the filesystem supports them (for example XFS and Btrfs) and a plain copy elsewhere, and preserves modification times.
- Every output of a crate built from local source is removed from the copy: fingerprints, compiled files, build-script directories, incremental and example outputs, and the binaries and libraries Cargo copies into the profile directory. The crates are every package that
cargo metadata --format-version 1(with--lockedwhenlocked: true) reports without a registry or Git source: the workspace members and all path dependencies, including those only other path dependencies declare. Their package and target names are matched in both-and_spellings. Only third-party outputs are reused; Cargo compiles the worktree's own crates from its own source. - The seed holds the canonical cache's lock while it reads, waiting like a build, and is built in a sibling directory that is renamed into place when complete. The leftovers of an interrupted seed are removed by the next build.
An explicit targetDir or TSRUST_TARGET_DIR is never seeded. tsrust clean, default tsrust prune and pruneAfterBuild act on the checkout's own cache. tsrust prune --target <dir> selects exactly one marked cache, including an archive outside the checkout.
The target cache is Cargo's build directory too: tsrust sets CARGO_BUILD_BUILD_DIR to it beside CARGO_TARGET_DIR, so deps/ and Cargo's other intermediates stay in the cache, where the local path check reads them. Cargo therefore ignores a build.build-dir entry in the Cargo configuration files it reads and an inherited CARGO_BUILD_BUILD_DIR variable (Cargo 1.91 and later; earlier versions have no build directory of their own), and a build that finds either prints a warning naming each one:
Warning: tsrust builds in its target directory /home/me/project/.nogit/tsrust-target and makes it Cargo's build directory too, so Cargo ignores build.build-dir in /home/me/project/rust/.cargo/config.toml. tsrust reads the dep-info files Cargo writes there to check each binary for local paths.
On Linux, builds and prune operations that use one cache take turns through a kernel-owned socket guard. A waiting build prints which process holds the cache every 30 seconds and gives up after 30 minutes; set TSRUST_LOCK_WAIT_TIMEOUT (seconds) to wait longer or shorter. The same limit bounds the wait for the dist_rust/ publication lock.
The cache guard, and with it the lock a seed holds on the canonical cache, exists on Linux only. On other platforms, such as macOS, tsrust does not serialize builds of one cache (Cargo's own build-directory lock still serializes concurrent Cargo runs), and a seed copies the canonical cache without waiting for a build in the canonical checkout: do not build there while a worktree's first build seeds from it.
Verify a Release Build
tsrust verify
verify checks that dist_rust/ holds exactly the configured release matrix of the checked-out commit: for every target in the union of targets and targetsByHost and every Cargo binary, <binary>_<target> must exist, be executable and carry a valid provenance sidecar that records this binary and target, the package name and version, the HEAD commit, and a clean worktree. With native notices configured, each binary must also carry the third-party notices of its target, byte for byte as the committed notices project them (verify reads the committed directory and checks it against its manifest.json, without Cargo or a toolchain). It also fails when the worktree is dirty or dist_rust/ holds anything else, including notices of a project without native notices. Every failed check is listed and the command exits with status 1.
Migrating to tsrust 2
A plain tsrust no longer cross-compiles the configured targets; it builds the host's own architecture. Development and test scripts keep calling plain tsrust. The release build must request the matrix explicitly and verify it, because gitzone release runs release.preflight.buildCommand (default pnpm build) in a fresh checkout and publishes exactly what that command leaves in dist_rust/:
{
"scripts": {
"build": "tsbuild tsfolders && tsrust",
"build:release": "tsbuild tsfolders && tsrust --configured-targets && tsrust verify"
}
}
{
"@git.zone/cli": {
"release": {
"preflight": {
"buildCommand": "pnpm run build:release"
}
}
}
}
Projects that release through tsrust matrix build need no change: matrix workers pass explicit --target options. Dockerfiles that emptied targets to force a native build can drop that step; the plain build now writes <binary>_<target> names for the container's architecture. ProvenanceStamper.stamp is removed; ProvenanceStamper.read still reads trailers of tsrust 1.7 and 1.8 binaries.
Build Provenance
Every new build writes <binary>.tsrust-build.json beside the binary. In a Git checkout, the sidecar records the consuming package name and version, exact commit and dirty state, target, build time, and tsrust version. The build time is SOURCE_DATE_EPOCH when that variable is set, so two builds of the same source write identical sidecars, and the current time otherwise; a value that is not a whole number of seconds fails the build before Cargo runs. tsrust matrix build validates the coordinator's SOURCE_DATE_EPOCH before any worker starts and passes it to every local and SSH worker, so all sidecars of a matrix record the same build time. Non-Git builds record gitCommit: "unknown" and omit gitDirty. The sidecar also contains the binary's SHA-256 digest, so provenance reads fail if either file no longer belongs to the pair. The binary itself remains byte-identical to the postprocessed Cargo target-cache binary, preserving Mach-O signatures and other binary-format integrity checks. On Darwin targets, tsrust preserves an existing valid signature and applies an ad-hoc signature only when codesign explicitly identifies the Cargo output as unsigned. Invalid or ambiguous signature states fail the build. Signing happens before the binary is copied and before its provenance digest is written.
Inspect a built artifact with:
tsrust inspect dist_rust/rustproxy_linux_amd64
inspect verifies and reads the sidecar. It retains read compatibility with provenance trailers produced by tsrust 1.7 and 1.8, but new builds no longer append those trailers.
tsrust snapshots the Git commit and complete worktree status before each Cargo build and verifies that neither changed before copying artifacts. Dirty builds remain possible for development and are marked as dirty; strict multi-host assembly rejects them.
Multi-Host Assembly
Each host builds its complete set with tsrust --configured-targets, which empties and refills its own dist_rust/. Copy those host outputs into separate artifact directories, then assemble the complete matrix from the same clean Git checkout and package version:
tsrust assemble .nogit/artifacts/linux .nogit/artifacts/macos
The union of targets and targetsByHost defines the required target matrix. Repeated --target options can define an explicit matrix instead. Assembly always publishes to the tsrust-owned dist_rust/ directory.
With native notices configured, every binary in an artifact directory must carry its third-party notices pair, byte for byte as the committed notices of the checkout project them for its target; the assembly copies the pair with the binary and records its digests. Without native notices, an artifact directory that holds a notices file is refused.
Assembly requires every Cargo binary for every expected target. It rejects missing, duplicate, unexpected, non-executable, symlinked, hash-mismatched, dirty, wrong-commit, wrong-package, and wrong-tsrust-version inputs. Validation and copying happen in a same-filesystem transaction under .nogit/tsrust-assembly/, guarded by the atomically published .nogit/tsrust-assembly.lock. The Git commit, clean worktree, and package identity are checked again immediately before publication. The previous output is retained until the complete staged matrix is ready, and successful publication removes files left by older matrices.
The transaction records its owner's process identity (machine id, boot id, PID namespace, process id and start time, as for the publication lock), owner token, publication phase, and exact digest manifest for staged binaries, provenance sidecars and third-party notices. A separate atomic recovery claim serializes dead-owner recovery. When the owner lock and state are readable and consistent, a later invocation on the owning machine rejects a live process, rolls back an interrupted pre-commit publication after that process exits, and completes publication or cleanup after durable commit intent only after revalidating the manifest. Legacy committed v1 transactions from tsrust 1.9 are validated against the current exact assembly request, upgraded with a manifest, and then completed; incompatible legacy staging fails closed. A committed transaction whose digest manifest a tsrust before 4.6.0 wrote fails closed as well: finish or roll it back with that version, or remove the reported transaction record. A confirmed missing state file is safe to clean because the owner-lock protocol forbids output mutation before initial state persistence; malformed, non-regular, unreadable, owner-inconsistent, or manifest-inconsistent state fails closed. A restart of the owning machine ends its owner, so the next invocation recovers the transaction. A transaction owned by another machine or PID namespace is never taken over automatically because portable filesystem operations cannot fence a paused remote writer; the error names the record to remove once no assembly runs there. If committed cleanup cannot finish immediately, the programmatic result reports cleanupPending: true and the CLI prints a warning; the owning process can retry it immediately.
Native Build Matrix
tsrust matrix automates host-partitioned builds and strict assembly. Committed configuration identifies expected worker architectures and names the environment variables that contain operator-local SSH details:
{
"@git.zone/tsrust": {
"targetsByHost": {
"linux": ["linux_amd64", "linux_arm64"],
"macos": ["macos_amd64", "macos_arm64"]
},
"locked": true,
"matrix": {
"builders": {
"linux_amd64": {
"transport": "local"
},
"macos_arm64": {
"transport": "ssh",
"destinationEnv": "TSRUST_MACOS_SSH",
"temporaryRootEnv": "TSRUST_MACOS_TEMP_ROOT"
}
},
"smokeTestArgs": ["--version"],
"verificationCommand": ["cargo", "test", "--manifest-path", "rust/Cargo.toml", "--workspace", "--locked"],
"forwardEnvironment": ["NPM_TOKEN"]
}
}
}
matrix.verificationCommand is optional. It is an executable followed by literal
arguments, with no shell expansion. It runs once per configured worker in that
worker's exact-commit checkout, after frozen dependency installation and before
the artifact build. Use ['pnpm', 'run', 'verify-native'] for a reviewed project
script that combines Rust and public API qualification. A test failure, timeout,
interruption or tracked-source mutation prevents artifact assembly and preserves
the prior output. Verification inherits the restricted worker environment, pnpm
age policy, shared one-hour worker deadline and owned process-group cleanup;
SSH workers also retain the disconnect watchdog. Arguments are limited to 256
entries and 64 KiB total, with no NUL or line breaks. Keep credentials out of
committed commands and test output.
Successful NativeMatrixBuilder.build() results include verifiedWorkers, the
host keys on which verification completed; it is empty when no command was
configured. matrix check checks capabilities only and does not run verification.
Set the referenced values outside the repository. The SSH destination may be an OpenSSH alias or user@host; the temporary root must be a dedicated absolute path owned by the remote account:
export TSRUST_MACOS_SSH="release-user@mac-builder.example"
export TSRUST_MACOS_TEMP_ROOT="/Users/release-user/.git.zone/tsrust/my-project"
export TSRUST_MATRIX_ALLOWED_ENVIRONMENT="NPM_TOKEN"
Builder keys are limited to linux_amd64, linux_arm64, macos_amd64, and macos_arm64. At most one worker may use transport: "local". Worker target partitions must be non-empty, disjoint, complete, and from the worker's OS family. Matrix execution probes support the six predefined targets linux_amd64, linux_arm64, linux_amd64_musl, linux_arm64_musl, macos_amd64, and macos_arm64. Native GNU targets require cc; non-native GNU probes use their predefined *-linux-gnu-gcc command. Musl probes always use their predefined *-linux-musl-gcc command. Non-native architectures also require executable emulation.
Environment-variable names must match [A-Za-z_][A-Za-z0-9_]*. Committed forwardEnvironment entries are requests, not grants: every requested name must also appear in the operator-owned comma-separated TSRUST_MATRIX_ALLOWED_ENVIRONMENT value or matrix construction fails. Built-in worker and transport names cannot be requested, including PATH, HOME, CI, RUSTUP_HOME, CARGO_HOME, SOURCE_DATE_EPOCH (passed from the coordinator, see Build Provenance), SSH agent variables, and the grant variable itself. Worker lifecycle commands receive only a small execution baseline, synthetic per-run HOME and CARGO_HOME directories, and operator-granted names; unrelated coordinator credentials are omitted. For SSH workers, granted values are made available to the SSH client and admitted into project commands only when the remote login environment supplies them, such as through deliberate OpenSSH SendEnv/AcceptEnv configuration. A worker baseline or granted name the remote login lacks stays unset in project commands, not empty. Values are never placed in the remote command line. SSH destinations are ASCII single aliases or user@host values without embedded options, ports, whitespace, or IPv6 syntax. Put ports, identity files, proxy jumps, host-key policy, credentials, and agent selection in OpenSSH configuration or an SSH agent. Temporary roots use an ASCII path whitelist and must be normalized, space-free, multi-component absolute paths whose physical resolution contains no symlink traversal. When present, smokeTestArgs must be a non-empty array of single-line, NUL-free strings and must not contain secrets.
Matrix builds capture the coordinator repository's effective minimumReleaseAge, minimumReleaseAgeStrict, and minimumReleaseAgeExclude values using individual pnpm config get <key> --json calls. These three nonsecret policy values travel to every local and SSH worker through tSRust-owned PNPM_CONFIG_MINIMUM_RELEASE_AGE, PNPM_CONFIG_MINIMUM_RELEASE_AGE_STRICT, and PNPM_CONFIG_MINIMUM_RELEASE_AGE_EXCLUDE environment variables. These names cannot be requested through forwardEnvironment. Global scope exclusions and repository overrides therefore survive the synthetic worker home. SSH transport shell-quotes the policy values; unlike granted credentials, these nonsecret values are included in the remote command. The coordinator's registry and authentication configuration is never enumerated or copied, and the coordinator process environment remains unchanged; workers see only the registry settings the committed project carries. Values must be valid JSON with a nonnegative integer age, boolean strictness, and a string-array exclusion list; unset values remain unset. The policy is limited to 32 KiB, at most 512 exclusions, and 1,024 characters per exclusion. Each worker reads all three values back before installation and fails if they differ from the captured policy. There is no age-gate bypass or fallback policy.
matrix check validates the coordinator's package-age policy and verifies the exact project-local tsrust package version without executing its binary, the native linker, system Cargo plus rustup with an active default toolchain and readable target manifest or the complete securely resolved bundled pair with cargo, rustc, and target-manifest readiness bound to that worker's exact managed stable toolchain, worker identity, emulation or cross-execution, and remote-root ownership and permissions. It is a capability-only command: it does not require a clean worktree or compare the working configuration with the committed plan. If usable system Cargo is unavailable and the bundled executables are either absent or securely resolved but not ready, it verifies only that curl is available for tsrust bootstrap or clean reinstall; it does not install targets or compile the project, and it proves network access only for a pinned pnpm, as described below. The check creates the dedicated remote root with restrictive permissions when it is absent. Capability and cleanup commands have a five-minute limit. Dependency installation, configured verification, compilation, per-binary smoke execution, bundle upload, and bounded artifact retrieval share each worker's one-hour build limit.
When the committed package.json pins pnpm through packageManager or devEngines.packageManager, matrix check and matrix build also prove, before any worker installs, that every worker can fetch, verify and start that pnpm release as its frozen install will. Each worker runs pnpm --version in a temporary probe project that holds the committed pin, the leading environment document of the committed pnpm-lock.yaml, and the committed pnpm-workspace.yaml and .npmrc, with the same restricted environment, synthetic HOME and CARGO_HOME, and package-age policy as its build. Working-tree edits and untracked files never enter the probe. The worker must report exactly the pinned version, or for a devEngines range the version the lockfile resolves, and must not print pnpm's Cannot use the pnpm version this project pins warning, because pnpm then continues with the release it is running. A worker that already runs the pinned release fetches nothing, as for its install. The probe has a two-minute limit, runs in its own process group, and is removed afterwards. A failure throws MatrixPnpmPinError, which names the worker, the pin and pnpm's error, or no answer within 120 s when the limit expires. A packageManager pin must be an exact version such as pnpm@12.8.2, optionally with its integrity hash.
Before a configured verification command runs, the worker selects usable system Cargo or prepares the bundled toolchain through the exact installed tsrust package. Bundled verification receives its host-specific Rustup selection and executable path while retaining the worker's private HOME and CARGO_HOME. This supports direct cargo, rustc and rustup calls inside verification scripts. Preparation failure prevents verification and artifact assembly; bootstrap and verification remain within the same worker deadline and process cleanup boundary.
Run the capability-only check before release metadata is created:
tsrust matrix check
Build and atomically publish the complete matrix from a clean exact Git commit:
tsrust matrix build
Every worker receives the same exact commit through a streamed Git bundle, installs with pnpm install --frozen-lockfile, revalidates the installed tsrust package version, invokes the project-local tsrust with explicit targets, and optionally executes every binary with smokeTestArgs. With native notices, a worker writes every binary's third-party notices from the committed notices of the exact commit the coordinator verified; the coordinator collects them with the binaries and provenance and the assembly checks them against its own projection. Local and remote project commands run with the restricted worker environment. Local and remote workers build in isolated clones, so an incomplete matrix never replaces an existing dist_rust. SSH workers use /bin/bash on Linux or /bin/zsh on macOS in login mode for builds, normal OpenSSH host verification, architecture and random-owner-marker validation on every lifecycle and transfer connection, locally and remotely byte-bounded non-login SSH streams for artifact retrieval, a heartbeat-bound process-group watchdog, and a unique mode-0700 child below the configured root. Remote owner-validated cleanup is a pre-publication gate: failure preserves the prior dist_rust and reports retained diagnostics. After successful assembly, failure to remove local staging emits a warning but does not reverse the committed publication.
matrix build additionally requires the normalized resolved matrix policy to match committed .smartconfig.json at the captured commit, a clean Git worktree without skip-worktree or assume-unchanged flags, package name and version, a frozen pnpm lockfile, and at least one Cargo binary. Matrix transport rejects Git submodules, tracked symbolic links, checkout filters, and external-content pointer records because those inputs are not self-contained regular files in a plain exact-commit bundle. Source compatibility, index flags, and the clean Git snapshot are revalidated around bundle creation and after project-controlled build and smoke commands. Binaries are limited to 2 GiB each, provenance sidecars to 1 MiB each, third-party notices files to 64 MiB each, and an assembly to 8 GiB total. Worker keys are expected host identities, not network addresses. Keep only the SSH destination and temporary root in the referenced variables; keys and credentials remain in OpenSSH configuration or an agent. Failed workspaces are intentionally retained and are not pruned automatically; each local and remote root admits at most three retained failed runs and then requires the operator to inspect and remove the exact reported paths before retrying.
Matrix workers are trusted execution principals, not sandboxes. A release therefore trusts the reviewed exact source commit, its lockfile-selected dependencies and lifecycle/build scripts, the selected local and SSH hosts, their toolchains and login configuration, and the registries they contact. Environment filtering, owner markers, source checks, bounded transfers, provenance digests, and transactional publication prevent accidental credential inheritance, stale or incomplete inputs, endpoint mix-ups, and partial publication; they do not contain deliberately malicious code already running as the same operating-system user. Use a separately isolated worker account or container when that trust assumption does not hold.
Static Linking
For static Linux binaries, build the musl targets. They are statically linked by default, run on glibc distros (Debian/Ubuntu) and musl distros (Alpine) alike, and contain musl libc, which is MIT-licensed:
{
"@git.zone/tsrust": {
"targets": ["linux_amd64_musl", "linux_arm64_musl"],
"static": true
}
}
static: true (or --static per invocation) makes tsrust verify after the build that every Linux binary in dist_rust/ is statically linked (no PT_INTERP ELF program header — the check is architecture-independent, so cross-compiled binaries are verified too) and fail the build otherwise.
Behavior per target:
*-linux-musl: statically linked by default; no flags are injected.*-linux-gnu:tsrustinjects-C target-feature=+crt-staticinto the Rust flags and links glibc statically.*-apple-darwin: full static linking is not applicable on macOS; the target builds with default linkage.
Prefer musl over static glibc. glibc is licensed under the LGPL-2.1-or-later. A binary that links it statically must be distributed with the means to relink it that LGPL-2.1 section 6 requires: the application's object files or source, so users can relink it against a modified glibc, and the glibc source or a written offer for it. tsrust does not produce that material, and native notices refuse static glibc targets. Use the musl targets for static binaries, or leave GNU targets dynamically linked (without static): a dynamic binary loads the host's glibc at run time and redistributes none of it.
Why tsrust injects the crt-static flag for GNU targets instead of the project setting rustflags in rust/.cargo/config.toml:
tsrustalways builds with an explicit--target, so the flag never applies to host artifacts. A repo-widerustflagsentry also applies to proc-macros and build scripts whenever cargo runs without--target(plaincargo test,cargo check, rust-analyzer) — and rustc cannot build proc-macros with+crt-staticon linux-gnu, breaking those commands.- Keep
rust/.cargo/config.tomlfree ofrustflags— tsrust passes its flags in theCARGO_ENCODED_RUSTFLAGSenvironment variable, which replaces config-filerustflagsand aRUSTFLAGSvariable (cargo does not merge them), and warns about each one it replaces (see Path Remapping).linkerentries (e.g. for aarch64 cross-compilation) are unaffected and should stay.
Allocator
Every Rust binary built with tsrust uses jemalloc as its global allocator, through tikv-jemallocator 0.7.0 with the background_threads feature: background threads return the dirty pages of idle long-running services to the system. The default features stay on. Only binary crates set the allocator; library crates never declare #[global_allocator] or depend on an allocator crate.
In a workspace, declare the dependency once in the root Cargo.toml:
[workspace.dependencies]
tikv-jemallocator = { version = "0.7.0", features = ["background_threads"] }
Each binary crate's Cargo.toml uses it (a single-crate repo writes the full specification here instead of workspace = true):
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = { workspace = true }
and its crate root, src/main.rs, sets it:
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
jemalloc does not support Windows MSVC targets, where the cfg falls back to Rust's system allocator. Every target tsrust builds supports it.
Check. Every build warns about each binary that does not follow this, and still builds it: a binary (a [[bin]] entry, or src/main.rs without one) whose crate has no tikv-jemallocator dependency (also under a renamed key or a target.<cfg> table), or whose crate root declares no #[global_allocator] static of type Jemalloc. The warning names the missing dependency or declaration and the file to add it to. Once the repositories have switched, a later tsrust release turns the warning into an error that fails the build.
Page size on Linux arm64. jemalloc fixes its page size when it is configured and aborts at start ("Unsupported system page size") on a kernel with larger pages. A cross build from x86_64 detects 4 KiB, while Linux aarch64 kernels run with 4, 16 (Raspberry Pi 5, Asahi) or 64 KiB pages. tsrust therefore builds linux_arm64 and linux_arm64_musl, including native builds on a Linux arm64 host, with JEMALLOC_SYS_WITH_LG_PAGE=16 (2^16 = 64 KiB), which runs on all three; the Running: line shows it. A value the caller sets is kept, and every other target keeps the detected page size. Matrix workers build through the project-local tsrust, so they set it too; a caller value reaches a worker only as a granted forwardEnvironment name.
Build requirements. tikv-jemalloc-sys compiles jemalloc from C sources, so every host that builds a binary needs a C compiler for the target, found by the cc crate (native cc, aarch64-linux-gnu-gcc for cross GNU builds, x86_64-linux-musl-gcc or aarch64-linux-musl-gcc for musl, the commands the matrix probes already require), and GNU make. Autoconf is not needed: the crate ships a generated configure. A missing compiler fails the build in tikv-jemalloc-sys with a configure error that names it. A cold build takes about 80 seconds longer per target. musl builds print a warning that background_threads_runtime_support is not supported; background threads still work there, and the warning is harmless.
Notices. Native notices know the jemalloc sources tikv-jemalloc-sys compiles in, so the crate needs no notices.packages entry.
Runtime tuning. The jemalloc symbols are prefixed (_rjem_), so its runtime configuration variable is _RJEM_MALLOC_CONF, not MALLOC_CONF, for example _RJEM_MALLOC_CONF=dirty_decay_ms:1000. Build-time defaults go through JEMALLOC_SYS_WITH_MALLOC_CONF.
Release Profile
Every release build tsrust runs (every build without --debug) compiles with fat link-time optimisation and one codegen unit: tsrust sets CARGO_PROFILE_RELEASE_LTO=fat and CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 in Cargo's environment, and the Running: line shows them. Optimising the whole program at once makes smaller and faster binaries. Debug builds keep Cargo's dev profile. Matrix workers build through the project-local tsrust, so they set them too.
Opt-out. Cargo's environment variables override the manifest and the Cargo configuration, so tsrust leaves each variable out when its key is already chosen:
- the caller set the variable; the value reaches Cargo unchanged (on a matrix worker only as a granted
forwardEnvironmentname); - the repository sets the key under
[profile.release]in the Rust directory'sCargo.toml(the workspace root) or its.cargo/config.toml(or.cargo/config).
The check is per key: a repository that measured fat LTO as slower for its binary sets, for example, lto = "thin" and still gets one codegen unit.
[profile.release]
lto = "thin"
Build-time cost. The final link optimises every crate together on one codegen unit, so a release build takes longer. Measured on NoSQLDB's musl engine (diagnostic build, two runs): the binary went from 22.9 MB to 15.6 MB, resident memory at a small workload fell by about 2.1 MB, and the release build takes about 6 minutes at -j2. A small crate (serde, serde_json, sha2) built cold at -j2 in 13 seconds instead of 9 to 11, and its binary went from 585 KB to 468 KB.
Path Remapping
rustc writes absolute source paths into binaries: panic locations and debug info name the files of the project, of the crates in Cargo's registry and Git checkouts, and of the standard library sources rustup installs. Every build therefore passes --remap-path-prefix for these directories, so a binary names neutral paths instead of the build machine's:
| Local directory | Appears in binaries as |
|---|---|
| the project directory | /workspace |
the Rust directory (rust/ or ts_rust/) |
/workspace/rust |
CARGO_HOME (default ~/.cargo) and the bundled toolchain's Cargo home |
/cargo |
RUSTUP_HOME (default ~/.rustup) and the bundled toolchain's rustup home |
/rustup |
A directory reached through a symbolic link is remapped under both of its names. Binaries of the same source therefore do not depend on the checkout location or the user building them. The flags name the checkout, so Cargo rebuilds the intermediates a linked worktree seeded from its canonical checkout.
After each build tsrust checks every binary for the local paths rustc recorded in Cargo's dep-info files, the <binary>.d file beside the binary and the .d file of every unit of the build: the source files rustc compiled, and the values of the environment variables a crate read with env! or option_env!. The units are those Cargo reports in its compiler-artifact messages (--message-format=json-render-diagnostics; the console still shows Cargo's usual output), the crates it compiled and those it found up to date, including procedural macros; build scripts are left out. A unit's .d file in deps/ is the one whose rules name a file the unit produced, and for the binary, the one whose rules name the file in deps/ that Cargo linked or copied out as the binary. The .d files of crates an earlier build compiled stay in deps/ and are not read, so a crate that is no longer part of the build neither fails nor passes it. A binary that Cargo did not report in this build, and a unit whose .d file is missing, fail the build. A binary that still contains one of these paths inside a remapped directory fails the build, and the error names each path with its crate, for example /home/me/project/rust (env!("CARGO_MANIFEST_DIR") of crate app). Remapping covers only the source file paths rustc writes; a crate that embeds a directory itself, such as through env!("CARGO_MANIFEST_DIR"), must stop doing so. Only whole recorded paths count: a string that merely starts with the project directory, such as /app/index in a project at /app, passes. Paths a build script writes into generated code are not recorded and therefore not checked. A directory that contains a neutral path, such as a project at /workspace itself, is not checked. A host or --target build that fails the check keeps the earlier artifact in dist_rust/; --configured-targets and builds of a project without configured targets clear dist_rust/ first.
For Linux GNU static builds -C target-feature=+crt-static comes first, then the flags of the rustflags setting, then the remapping flags:
{
"@git.zone/tsrust": {
"rustflags": ["--cfg=my_feature"]
}
}
tsrust passes the flags in CARGO_ENCODED_RUSTFLAGS, separated by 0x1f, so a directory with whitespace stays one flag. CARGO_ENCODED_RUSTFLAGS takes precedence over every other source of Rust flags, so Cargo ignores RUSTFLAGS, CARGO_BUILD_RUSTFLAGS, CARGO_TARGET_<TRIPLE>_RUSTFLAGS, an inherited CARGO_ENCODED_RUSTFLAGS, and build.rustflags and target.<triple or cfg>.rustflags in the Cargo configuration files Cargo reads (.cargo/config.toml in the Rust directory and its parent directories, and config.toml in CARGO_HOME). A build that finds any of them prints a warning naming each one:
Warning: tsrust passes its Rust flags in CARGO_ENCODED_RUSTFLAGS, so Cargo ignores RUSTFLAGS, build.rustflags in /home/me/project/rust/.cargo/config.toml. Move the flags this build needs into the "rustflags" setting of @git.zone/tsrust in .smartconfig.json, which applies to every target.
Flags a build needs, such as -C target-cpu=…, belong in the rustflags setting; it applies to every target the build compiles.
Remapping is no longer a setting: remove a remapLocalPaths entry from .smartconfig.json.
Native Notices
A package that ships native binaries must carry the license notices of everything those binaries contain: the Rust crates from Cargo.lock, C libraries that crates compile in, the Rust standard library, and the C runtime. tsrust notices generates them into a committed directory next to your sources, and every build verifies it:
{
"@git.zone/tsrust": {
"targets": ["linux_amd64_musl", "linux_arm64_musl"],
"static": true,
"locked": true,
"notices": {}
}
}
tsrust notices # write native-notices/
tsrust notices --check # verify without writing
Ship the directory with the package by listing it in package.json:
{
"files": ["dist_rust/**/*", "native-notices/**/*"]
}
What the bundle holds (manifest.json records each package's license expression, the texts that cover it, each target's packages and runtime, and the SHA-256 digest of every file):
| Path | Contents |
|---|---|
crates/<name>-<version>/ |
Every license, copyright and notice file each third-party package publishes, copied verbatim. |
crates/<name>-<version>/extra/ |
Reviewed extra texts the project declares for a package in notices.packages.<key>.extraTexts. |
native/<crate>-<version>/<library>-<version>/ |
License files of C/C++ libraries a crate compiles in, as declared in notices.packages or known to tsrust. |
rust/ |
The toolchain's standard-library copyright inventory and license texts, compiler-builtins' license, and (rust/crates/) the license files of standard-library dependencies the inventory omits. |
runtime/ |
musl's copyright and the license of LLVM's libunwind (static musl targets), the glibc LGPL-2.1 text (dynamic GNU targets, glibc itself is not redistributed), and GCC's GPL-3.0 with the Runtime Library Exception (Linux targets). macOS targets link Apple's system libraries, which are not redistributed. |
readme.md |
A summary of the targets, their linkage, and the obligations above. |
Packages are resolved per configured target with cargo metadata --locked --filter-platform <triple>: the normal and build dependencies of the workspace's binary packages, transitively, including procedural macros. Development dependencies and the workspace's own crates are excluded. The Rust runtime comes from the toolchain rust-toolchain.toml selects.
Generation fails instead of guessing when:
- a package declares no license, or an expression that is not valid SPDX (Cargo's legacy
MIT/Apache-2.0form is accepted); - no license file of a package covers its expression: every
ANDterm of at least one alternative needs a recognised license text; - a package has a
linkskey, does not declare the native libraries it compiles, and is not one whose librariestsrustknows (below); - a license file is a symbolic link, or lies below one, that does not resolve to a regular file inside the same Git checkout (Git sources) or registry package (registry sources); links in path and other packages are always refused;
- a Linux GNU target is linked statically (see Static Linking);
- an override names no compiled package, or is no longer needed;
- an extra text is missing or its SHA-256 digest differs from its pin;
- the musl version in the toolchain has no reviewed copyright text in
tsrust; package.jsonfilesdoes not ship the notices directory.
Overrides are keyed by the exact name@version, so a dependency upgrade retires them:
{
"@git.zone/tsrust": {
"notices": {
"directory": "native-notices",
"packages": {
"zstd-sys@2.0.16+zstd.1.5.7": {
"native": [
{
"name": "zstd",
"version": "1.5.7",
"license": "BSD-3-Clause OR GPL-2.0-only",
"files": ["zstd/LICENSE", "zstd/COPYING"]
}
]
},
"crc32c@0.6.8": {
"files": [{ "from": "project", "path": "legal/crc32c-license-mit", "licenses": ["MIT"] }]
},
"ring@0.17.14": {
"extraTexts": [
{
"path": "legal/ring-0.17.14-once-cell-license-mit.txt",
"sha256": "6ee2ed6c77710de911761acd5fc1ad1da00f476beb1a7ef27e78c2d1858deafc",
"kind": "component-license",
"component": "once_cell"
},
{
"path": "legal/ring-0.17.14-source-attributions.txt",
"sha256": "675df5a11ef946e519a267eba5f54ce8125eee0d0d443e614302706db4819459",
"kind": "source-attribution"
}
]
}
},
"runtime": {
"linux_amd64_musl": [
{ "name": "musl-cross", "version": "20260823", "license": "MIT", "files": ["legal/musl-cross-copyright"] }
]
}
}
}
}
packages.<key>.native: C/C++ libraries the crate compiles, with license files relative to the crate root.[]declares that alinkscrate bundles no third-party library. A declaration replaces whattsrustknows about the crate, and one identical to that knowledge fails as no longer needed. A file is a path, or, for a file whose wordingtsrustdoes not recognise, the path with the SPDX identifiers a reviewer found it to satisfy, for example{ "path": "vendor/COPYRIGHT", "licenses": ["BSD-3-Clause"] }; such declarations fail as no longer needed once the component's files cover its license without them.packages.<key>.files: reviewed texts for a package whose own files do not cover its license, either kept in the project ("from": "project") or a crate file whose wordingtsrustdoes not recognise ("from": "crate"), with the SPDX identifiers each text satisfies.packages.<key>.extraTexts: reviewed project texts to ship with a package in addition to the texts that cover its license, for example the license of a component the crate vendors or a supplement reproducing the copyright comments of its sources. Each entry names the project-relativepath, the file's lowercase hexsha256digest, itskind("component-license"or"source-attribution") and, optionally, the vendoredcomponentit applies to. The texts are copied tocrates/<name>-<version>/extra/(rust/crates/…/extra/for standard-library dependencies) and listed with their kind under the package'sextraTextsinmanifest.json. They never count towards covering the package's license, so a package whose own files do not cover it still needsfiles, andfilesthat are no longer needed still fail. Generation and every check refuse a missing text or one whose digest differs from its pin; after reviewing a changed text, update the pin.packages.<key>.license: only for a package that declares no license or one that is not valid SPDX.runtime.<target>: runtime material a custom linker adds, for example the startup files of a musl cross toolchain.
tsrust knows the C libraries these crates compile in, so they need no native entry:
| Crate | Library | License file |
|---|---|---|
tikv-jemalloc-sys |
jemalloc, BSD-2-Clause, at the release in the crate version's build metadata (0.7.1+5.3.1-… → 5.3.1) |
jemalloc/COPYING |
A tikv-jemalloc-sys version without that build metadata still needs its entry.
The build (tsrust) checks the notices before compiling when notices is configured, and refuses targets the notices do not cover. It first installs the rustup targets it builds and then, in the Rust directory and with the same toolchain, runs the check's own metadata command online, cargo metadata --format-version 1 --locked --filter-platform <triple> without --offline, once for every notices target, one after another, so a fresh CI container or developer machine with an empty Cargo cache builds without a manual step; the first failing run fails the build with its command, exit code, Rust directory and Cargo's error. Each run downloads exactly what the offline check then reads for that target: the packages the lockfile activates with default features for that target and for the host, where build scripts and procedural macros run. It does not use cargo fetch, which downloads the optional dependencies of every feature of every workspace member and, for several targets at once, the dependencies a package has on one target when another target uses it, none of which the check reads. Packages only other platforms use, such as cfg(windows) dependencies of a Linux build, and optional dependencies of features that nothing in the default-feature build enables are not downloaded, so a Cargo cache that tsrust notices or an earlier build filled on the same host is enough during a registry outage or offline. Git dependencies are cloned whole whatever the platform, because Cargo needs them to resolve the lockfile. If cargo metadata --offline still fails after the same command succeeded online, the error says so instead of the standalone advice. tsrust matrix check and tsrust matrix build check them once on the coordinator, before any worker starts; tsrust matrix build first runs the same online metadata on the coordinator, once per notices target, and stops before any worker when it fails. tsrust notices --check and the notices check of tsrust matrix check still download nothing. Workers then build with --no-notices-check, which skips only that comparison. After a dependency, toolchain, or target change, run tsrust notices, review the diff, and commit it.
Apart from that build preparation, only tsrust notices uses the network and installs anything: it lets Cargo download missing packages, fetches the standard-library dependencies the toolchain inventory omits (verified against the checksums in the toolchain's library/Cargo.lock), and installs the rust-src component and missing musl targets. tsrust notices --check, the notices check of tsrust matrix check and the check itself in tsrust and tsrust matrix build work offline and install nothing: they run cargo metadata --offline against the local Cargo cache, verify the committed rust/crates/ notices against the toolchain's checksums and the digests in manifest.json and the project's extra texts against their pins, and refuse with the command to run when something is missing (cargo fetch --locked for an empty Cargo cache, rustup component add rust-src, rustup target add <triple>).
The notices read the standard-library sources of the rust-src component and the C runtime of every configured musl target. No check installs them, and the build installs only the targets it builds, never rust-src. Declare them in the project's rust-toolchain.toml, so rustup installs them with the toolchain on every machine:
[toolchain]
channel = "1.95.0"
components = ["rust-src"]
targets = ["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl"]
Without the declaration, run rustup component add rust-src and rustup target add <triple> once per machine, or tsrust notices, which installs them.
Static musl executables contain LLVM's libunwind besides musl: the standard library's unwind crate links the toolchain's self-contained libunwind.a statically whenever crt-static is on, which is the default of the musl targets (library/unwind/src/lib.rs in rust-src). tsrust derives it from the target, so stripped executables are covered, and confirms it with the archive in the toolchain (lib/rustlib/<triple>/lib/self-contained/libunwind.a). The notices record it at the LLVM release rustc -vV reports, under Apache-2.0 WITH LLVM-exception AND (NCSA OR MIT), with the LICENSE.TXT of the libunwind sources rust-src ships (src/llvm-project/libunwind/); a toolchain without either fails generation. Projects no longer need a notices.runtime entry for libunwind on musl targets: remove it and run tsrust notices. GNU targets load glibc and libgcc_s at run time and get neither musl nor libunwind.
Third-party notices beside each binary
With notices configured, every build writes two files beside each binary in dist_rust/, in the notices format of @git.zone/tsbundle (@git.zone/tsbundle/notices), so tools that embed the binary, such as @git.zone/tssea for sidecars of Node.js single executable applications, merge them with the notices of their other parts:
| File | Contents |
|---|---|
<binary>.third-party-notices.json |
{ generator: "@git.zone/tsrust", packages: [{ name, version, license, files: [{ name, origin, suppliedBy?, sha256, text }] }] }, as parseThirdPartyNotices() reads it |
<binary>.third-party-notices.txt |
The same, one section per component, after a header naming the target and its linkage |
They are build output, never committed: tsrust projects them for the binary's target from the committed, verified notices directory, which stays the single source of truth. A build that checks the notices projects the bundle the check verified; a build with --no-notices-check (a matrix worker) reads the committed directory and refuses it unless every file matches the digest manifest.json records, and refuses a target the notices do not cover. Without notices, a build writes none and removes a pair an earlier build left beside a binary it replaces.
Entry names carry a namespace, because npm package names cannot contain a colon:
| Name | Entry | Version |
|---|---|---|
cargo:<crate> |
A Cargo package compiled for the target, including standard-library dependencies the toolchain inventory omits | The crate version |
native:<library> |
A C/C++ library a crate compiles in (cc, build.rs) |
The library version |
rust:std |
The standard library: the toolchain's copyright inventory and license texts (MIT OR Apache-2.0) |
The Rust release |
rust:compiler-builtins |
compiler-builtins, when the toolchain ships its license in rust-src |
The Rust release |
runtime:musl / runtime:libunwind / runtime:glibc / runtime:gcc / runtime:<name> |
The runtime the toolchain supplies for the target's linkage, and runtime components the project declares | musl's version from the toolchain's libc.a; libunwind's LLVM release from rustc -vV; unversioned where the notices record none |
Files are named by their path relative to the component's directory in the notices bundle (license-mit, licenses/mit.txt), and sha256 is the digest manifest.json records for them. Texts the project supplies (files with "from": "project", extraTexts, the files of runtime components) are supplements with suppliedBy: "project", named by their project-relative path. macOS system libraries are not redistributed and get no entry. The notices record the packages of all the workspace's binary packages for each target, so every binary of a target carries the same notices, a superset of what each binary links.
With notices configured, every build refuses static glibc, including builds with --no-notices-check: static/--static on a GNU target, and +crt-static in the tsrust rustflags, RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS, CARGO_BUILD_RUSTFLAGS or CARGO_TARGET_<TRIPLE>_RUSTFLAGS. Static linking configured elsewhere, such as rustflags in .cargo/config.toml, is caught after compiling: a GNU binary without an ELF interpreter is removed from dist_rust/ and the build fails.
🗑️ Clean Only
Remove all build artifacts without rebuilding:
tsrust clean
This runs cargo clean in the Rust directory and deletes the dist_rust/ output directory.
🧹 Prune Rust Target Caches
Report Rust target caches without deleting anything:
tsrust prune
Apply cleanup to marked tsrust-managed target caches:
tsrust prune --apply --days 14 --max-size 5GiB
To inspect exactly one marked target in an archive or evidence copy, without discovering or removing checkout caches, siblings, or other directories:
tsrust prune --target /path/to/archive/cargo-target --days 14 --json
tsrust prune --target /path/to/archive/cargo-target --days 14 --json --apply
--target names the cache directory itself, not a parent to search recursively. Relative
paths resolve against --workspace (the current directory by default). This explicit
selection does not change build targetDir or TSRUST_TARGET_DIR configuration.
--json emits one JSON object on stdout for both preview and apply, with kind: "tsrust.prune",
tool, version, apply, success, error (null on success), and candidates. Every
candidate has path, workspace, bytes, identity (dev, ino, and markerCreatedAt
when marked; null when missing), ageMs (null when unavailable), marked, shouldPrune,
meetsThresholds, reason, truncated, criteria, holderVerdict, and action.
meetsThresholds is true when the age or size threshold selects the cache; marker, safety
and holder checks then decide shouldPrune. A cache the thresholds keep reports
below prune thresholds with the age of its newest change, even when its holders could not be
cleared, and does not refuse an apply. The holder verdict reports state (quiescent,
in-use, or unknown), check (the failing check, null when quiescent: lock for another
tsrust build or prune holding the guard, process for a Rust tool working in the workspace or a
process using the cache, hidden-process for a same-user process that hides its files while the cache changed
within 24 hours, or hides both its files and its command line, scan for an unsafe or incomplete cache scan, inspection for a
failed process inspection), visible holders (PID, kind, and optional process name),
hiddenProcesses (PID and process name), skippedProcessFdDirectories, checkedAt, and
reason, which names the holder PIDs when any. Command lines are never returned. action is report, would-remove, removed, or refused;
a refusal on apply sets success: false, fills error, and exits with code 1. Partial
outcomes, such as an I/O failure after a removal, retain the actual removed actions.
Prune plans retain their thresholds and cache identity. Apply checks the marker, identity,
complete scan, and current age/size again before removal. Symlinks, filesystem boundaries,
unreadable entries, and incomplete scans are report-only. A process using the cache (its working
directory, executable, an open file or a mapping inside it) blocks removal, and so does a running
cargo, rustc or rust-analyzer whose working directory lies inside --workspace or cannot be
read; a Rust tool working elsewhere, also in a directory above the workspace, does not. Apply skips processes whose /proc/<pid>/fd directory returns
EACCES or EPERM, such as non-dumpable SSH and GPG agents, or disappears during the scan
(ENOENT or ESRCH), and reports the number skipped for each cache. A readable process holding
a cache file open still blocks removal, and unexpected inspection errors still fail the prune.
If a process has readable descriptors but the kernel hides its working directory, executable
or mappings, apply reads its command line instead: a command line that names the cache, or one
that cannot be read either, blocks removal. Otherwise the cache must have been unchanged for
24 hours. Errors name the processes by PID and process name, never their command lines.
A process that holds a capability the caller lacks, such as systemd --user with
CAP_WAKE_ALARM, hides all of these, so on most hosts a cache younger than 24 hours is
unknown until it has been unchanged for a day. Linux builds and prune operations share a kernel-owned
socket guard with a bounded, reported wait (see Target Cache); process inspection requires a readable /proc.
Apply acquires all selected cache guards and revalidates the entire plan before the first
removal. TSRUST_LOCK_WAIT_TIMEOUT is a positive number of seconds (default 1800), shared
across the batch's lock acquisition. For a caller with a 240-second budget, use a shorter
limit, allowing time for directory and holder scans and removal:
TSRUST_LOCK_WAIT_TIMEOUT=180 tsrust prune --target /path/to/cache --days 14 --apply --json
Lock contention or timeout refuses the batch before any cache is removed. The limit bounds
lock waits, not the total scan or deletion time. Each directory scan and each process
inspection has its own 30-second bound. Preview reports a held guard without waiting for
the configured timeout. Apply (including pruneAfterBuild) fails closed on other platforms. Dry-run reporting remains available.
Direct Cargo commands do not participate in the tsrust guard: stop other Rust tools before
applying a prune. Unmarked targets remain report-only; conventional targets require an explicit --target.
| Option | Description |
|---|---|
--apply |
Remove selected marked target caches that match the filters |
--days <n> |
Prune marked caches whose newest file is at least n days old; default 14 |
--max-size <size> |
Prune marked caches at or above a size such as 5GiB |
--workspace <path> |
Inspect another workspace path |
--target <dir> |
Inspect exactly this cache, including a marked target outside the checkout |
--json |
Emit the typed preview or apply result as one JSON object |
Without --target, conventional rust/target and ts_rust/target directories remain
report-only, even with a marker; only checkout-managed .nogit/tsrust* targets are eligible.
With --target, the exact directory must have a valid tsrust marker and pass the same
ownership, identity, complete-scan, age/size, and holder checks. Symlink targets and symlink
ancestors are refused. A missing explicit target is reported as an ineligible candidate.
Adopt an Existing Cargo Target
tsrust adopt --target /path/to/unmarked/target --json
tsrust adopt --target /path/to/unmarked/target --json --apply
Adoption is a dry run by default. It requires Cargo's CACHEDIR.TAG signature, a valid
.rustc_info.json compiler record, and at least one build profile with .cargo-lock,
.fingerprint/, and deps/. Cross-compilation profiles beneath target triples are accepted.
Unrelated root entries, incomplete scans, symlinks, ownership mismatches, existing markers
(including invalid or foreign markers), and held or ambiguous targets are refused. Apply
rechecks the directory and its holders under the cache guard, then exclusively creates
.gitzone-tool-cache.json in the named target. It never overwrites a marker or marks siblings.
A newly written marker contributes to the target's age. JSON uses kind: "tsrust.adopt"
and the same envelope and candidate fields, plus shouldAdopt and an action of
would-adopt, adopted, or refused. Refusals exit with code 1.
Prune Library API
Owner tools can consume the same CLI contract through the @git.zone/tsrust/prune export:
import { prune, adopt, createTsrustPrunePlan, applyTsrustPrunePlan } from '@git.zone/tsrust/prune';
const preview = await prune({ workspace: '/path/to/project', target: '/path/to/archive/target', days: 14 });
const applied = await prune({ workspace: '/path/to/project', target: '/path/to/archive/target', days: 14, apply: true });
if (!applied.success) throw new Error(applied.error ?? 'Prune refused');
const adoption = await adopt({ workspace: '/path/to/project', target: '/path/to/cargo/target' });
// Keep a preview and apply it later with identity/eligibility revalidation:
const plan = await createTsrustPrunePlan({ workspace: '/path/to/project', target: '/path/to/archive/target', days: 14 });
await applyTsrustPrunePlan(plan);
prune() defaults to days: 14 and apply: false; adopt() also defaults to preview.
Both return typed result envelopes and emit no progress unless a log callback is supplied.
Invalid options throw; operational apply refusals return success: false. The lower-level
applyTsrustPrunePlan() throws on refusal, and createTsrustPrunePlan() uses the thresholds
explicitly supplied. The module exports ITsrustPruneRunOptions, ITsrustPruneResult,
ITsrustPruneResultCandidate, ITsrustPruneOptions, ITsrustPruneCandidate,
ITargetCacheHolderVerdict, TTargetCacheHolderCheck, TTsrustPruneAction, ITsrustAdoptOptions,
ITsrustAdoptResult, and ITsrustAdoptCandidate.
Project Structure
tsrust expects your project to follow this layout:
my-project/
├── rust/ # 🦀 Your Rust source code
│ ├── Cargo.toml # Root manifest (workspace or single crate)
│ ├── src/
│ │ └── main.rs # (for single-crate projects)
│ └── crates/ # (for workspace projects)
│ ├── my-binary/
│ │ ├── Cargo.toml # Contains [[bin]] targets
│ │ └── src/
│ └── my-lib/
│ ├── Cargo.toml
│ └── src/
├── native-notices/ # ⚖️ Third-party notices (tsrust notices, committed)
├── dist_rust/ # 📦 Output: compiled binaries go here
│ ├── my-binary
│ └── my-binary.tsrust-build.json
├── .nogit/
│ └── tsrust-target/ # 🧹 Managed Cargo target cache
├── ts/ # (your TypeScript code, built by tsbuild)
├── dist_ts/ # (TypeScript output)
└── package.json
Workspace Support
tsrust fully supports Cargo workspaces. It reads the [workspace] section from your root Cargo.toml, iterates through all members, and discovers binary targets from each member crate's Cargo.toml.
Binary target discovery follows Cargo's own rules:
- Explicit
[[bin]]entries → uses thenamefield from each entry - Implicit binary → if no
[[bin]]is declared butsrc/main.rsexists, uses the[package] name - Library-only crates → skipped (no binary output expected)
Fallback Directory
If no rust/ directory is found, tsrust checks for ts_rust/ as a fallback. This supports projects that use the ts_ prefix convention for all source directories.
Programmatic API
tsrust exports its internals for use in other Node.js/TypeScript tools:
import {
ArtifactAssembler,
CargoConfig,
CargoRunner,
FsHelpers,
ProvenanceStore,
NativeMatrixBuilder,
TsRustCli,
configuredAssemblyTargets,
normalizeTargets,
resolveBuildTargets,
resolveTargetCacheLocation,
selectBuildTargets,
} from '@git.zone/tsrust';
// Parse a Cargo workspace
const config = new CargoConfig('/path/to/rust');
const info = await config.parse();
console.log(info.isWorkspace); // true
console.log(info.binTargets); // ['rustproxy']
console.log(info.binaries); // [{ name: 'rustproxy', crateDir: '/path/to/rust/rustproxy', rootSource: '/path/to/rust/rustproxy/src/main.rs' }]
// Run cargo build
const runner = new CargoRunner('/path/to/rust');
const { targetDir } = resolveTargetCacheLocation('/path/to/project');
const result = await runner.build({ debug: false, clean: false, targetDir });
console.log(result.success); // true
console.log(result.exitCode); // 0
// File helpers
await FsHelpers.ensureEmptyDir('/path/to/dist_rust');
await FsHelpers.copyFile(src, dest);
await FsHelpers.makeExecutable(dest);
const size = await FsHelpers.getFileSize(dest);
console.log(FsHelpers.formatFileSize(size)); // "13.4 MB"
Important exported build and artifact APIs:
| API | Purpose |
|---|---|
selectBuildTargets() |
Select the targets and dist_rust/ mode of a standard build: host, configured, explicit, or native. |
resolveBuildTargets() |
Apply CLI, exact-host, OS-family, legacy, and native target precedence. |
resolveHostTargets() |
Keep the configured targets of the host's operating system and architecture. |
findBinariesWithoutJemalloc() / jemallocAllocatorWarning() |
Find the binaries of CargoConfig.parse() that lack tikv-jemallocator as their global allocator, and word the build's warning for each (see Allocator). |
jemallocBuildEnvironment() / jemallocLgPageVariable |
The jemalloc variables tsrust adds to Cargo's environment for a target: JEMALLOC_SYS_WITH_LG_PAGE=16 for Linux arm64 unless the caller set it (see Allocator). |
releaseProfileBuildEnvironment() / releaseProfileSettings |
The release profile variables tsrust adds to Cargo's environment: CARGO_PROFILE_RELEASE_LTO=fat and CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 for a release build, each unless the caller set it or the repository sets its [profile.release] key (see Release Profile). |
resolveTargetCacheLocation() |
Resolve a checkout's own Cargo target cache and, in a linked worktree, the canonical cache that seeds it. |
seedTargetCache() |
Create a missing worktree cache once from the canonical cache, keeping only third-party outputs. |
DistRustPublisher |
Replace or clear dist_rust/ artifacts under the assembly publication lock. |
DistRustVerifier |
Check that dist_rust/ holds exactly the configured matrix of one clean commit. |
getCurrentProcessIdentity() / getProcessIdentityStatus() |
Record the calling process's identity and judge whether a recorded lock owner is alive, dead, or cannot be checked. |
configuredAssemblyTargets() |
Normalize and deduplicate the union required for multi-host assembly. |
normalizeTargets() |
Resolve friendly aliases and reject invalid or colliding target names. |
ProvenanceStore |
Write, hash-verify, and read byte-preserving provenance sidecars. |
resolveBuildTime() |
The build time provenance records: SOURCE_DATE_EPOCH when set, else the current time. |
ArtifactAssembler |
Validate and transactionally publish a complete exact-commit artifact matrix to dist_rust/. |
NativeMatrixBuilder |
Check and build an isolated local/SSH native matrix, then delegate publication to ArtifactAssembler. |
resolveMatrixPlan() / matrixHostKeys |
Validate matrix configuration and inspect its complete disjoint worker schedule. |
MatrixCommandRunner |
Execute bounded matrix subprocesses with process-group, signal, timeout, input, and heartbeat ownership. |
MatrixPnpmPinError |
Thrown when a matrix worker cannot fetch, verify or start the project's pinned pnpm; carries host and pin. |
| Matrix configuration and result types | Type NativeMatrixBuilder, worker transports, resolved plans, and check/build results. |
captureGitSnapshot() / assertGitSnapshotUnchanged() |
Capture and compare Git commit and worktree state around a build. |
ProvenanceStamper |
Read the embedded trailers of tsrust 1.7/1.8 binaries; builds use ProvenanceStore. |
NativeNoticesGenerator / createProjectNoticesGenerator() |
Generate, write, or check the native notices bundle of a project. |
BinaryNoticesProjector |
Project a verified or committed notices bundle onto the per-binary third-party notices of a target. |
thirdPartyNoticesPaths() / IBinaryThirdPartyNotices |
The paths and contents of a binary's third-party notices pair. |
parseSpdxExpression() / classifyLicenseText() |
Expand SPDX expressions into alternatives and recognise license texts. |
knownNativeComponentsFor() |
The C/C++ libraries tsrust knows a crate version compiles in, such as jemalloc in tikv-jemalloc-sys. |
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.