harbor-rs
harbor-rs is reusable Rust cross-compilation and toolchain infrastructure for Nix flakes. It packages the common setup needed for Rust workspaces that target Linux, Windows, and macOS without forcing each project to rebuild the same Nix plumbing.
The library exposes:
mkToolchainfor Rust toolchains withcranemkCargoConfigfor generated.cargo/config.tomlmkCrossfor MinGW and optional osxcross environmentsmkDevShellandmkDevShellsfor consistent development shellsmkGpuRenderPinfor GPU/driver identity checks in visual snapshot testsmkAppImageandmkFlatpakManifestfor packaging outputsmkAdapterandmkAtticPushfor binary cache integration
Start with Installation if you want to consume the flake in another project, or jump to Quick Start for a minimal cross-compiling shell.
Source repository: https://github.com/caniko/harbor-rs
Installation
Use harbor-rs as a flake input and follow its pinned shared dependencies from your project.
Requirements
- Nix with flakes enabled
rust-overlayapplied to thepkgsyou pass intomkToolchain- nix-direnv, if you use the recommended
.envrc - A macOS SDK only if you want Darwin cross-compilation through osxcross
Add the flake input
Pin a published trunk revision. Follow only inputs Harbor actually exposes:
{
inputs = {
harbor-rs.url = "git+https://github.com/caniko/harbor-rs.git?ref=trunk&rev=a5b436d24e175042c76d90fc4dbd31e92f7e1721";
nixpkgs.follows = "harbor-rs/nixpkgs";
rust-overlay.follows = "harbor-rs/rust-overlay";
crane.follows = "harbor-rs/crane";
};
}
Ready-made starting points:
nix flake init -t git+https://github.com/caniko/harbor-rs.git
nix flake init -t git+https://github.com/caniko/harbor-rs.git#bevy
For a working configuration after the input is added, continue to Quick Start.
Quick Start
The fastest path is to create a toolchain, generate cross helpers, then feed both into mkDevShells.
{
outputs = { self, nixpkgs, harbor-rs, flake-utils, rust-overlay, ... }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
toolchain = harbor-rs.lib.mkToolchain { inherit pkgs; };
cross = harbor-rs.lib.mkCross { inherit pkgs system; };
in {
devShells = harbor-rs.lib.mkDevShells {
inherit pkgs cross;
inherit (toolchain) craneLib;
};
});
}
This produces four shells:
nix developfor native worknix develop .#windowsfor MinGW cross buildsnix develop .#macosfor osxcross, when a macOS SDK is configurednix develop .#crossfor both Windows and macOS helpers together
If you need more control over the generated shell environment, read mkDevShell and mkDevShells. If you need macOS SDK setup, read macOS SDK Initialization.
mkToolchain
mkToolchain creates the Rust toolchain and craneLib used by the rest of the flake.
Parameters
pkgs(required): nixpkgs withrust-overlayappliedtoolchainProfile: optional harbor-rs-owned pin, either"stable"or"nightly". Stable is currently pinned to Rust1.98.1; nightly uses the repository’s checked-inrust-toolchain.toml(currentlynightly-2026-09-15). Omitting it preserves the legacy channel/date behavior.toolchainFile: optional path to a standardrust-toolchain.toml; when set, its channel, components, and targets are authoritativechannel:"nightly"or"stable"; defaults to"nightly"date:"latest"or a pinned date such as"2025-12-01"; only used withouttoolchainFileextensions: extra Rust components to install. Defaults to["rust-src" "rustfmt" "rustc-codegen-cranelift-preview" "llvm-tools-preview"].llvm-tools-previewprovidesllvm-cov/llvm-profdata, whichcargo-llvm-cov-based coverage CI requires.withRustAnalyzer: whether to includerust-analyzerin the toolchain extensions; defaults totruecrossTargets: list of target triples to include in the toolchaincache.enable: explicitly enable the host-backed compiler cache; defaults tofalse
Returns
mkToolchain returns an attribute set with:
rustToolchaincraneLibbuildCache: the compiler-cache policy when enabled, otherwisenullcargoConfig: matchingmkCargoConfigoutput, inherited automatically by harbor-rs dev shells using thiscraneLibcrossTargets
Path-patched crates and buildDepsOnly
The returned craneLib wraps Crane’s buildPackage and buildDepsOnly for workspaces that use [patch.crates-io] with local path entries.
Crane’s dependency-only phase builds a dummy source tree for path crates. That is unsafe when a registry dependency compiles against a patched local crate, because the dependency may see the dummy crate API instead of the real patched API.
For these workspaces, craneLib.buildPackage automatically disables implicit dependency artifact reuse by passing cargoArtifacts = null unless the caller already provided cargoArtifacts.
Direct craneLib.buildDepsOnly calls fail with an harbor-rs error naming the path patches. Prefer:
craneLib.buildPackage (commonArgs // {
cargoArtifacts = null;
})
If a workspace is known to tolerate dummy path patches, pass rsHarborAllowPathPatchBuildDepsOnly = true to buildDepsOnly.
Path-patch detection reads Cargo.toml during evaluation. Direct Nix paths and
lib.cleanSourceWith sources are handled automatically. If src is a generated
derivation output or an undeclared plain-string path, provide the manifest
explicitly to avoid import-from-derivation and store-state-dependent evaluation:
craneLib.buildPackage {
src = generatedSource;
rsHarborCargoTomlContents = builtins.readFile ./Cargo.toml;
# ...
}
The harbor-rs-only argument is removed before the remaining arguments are passed to Crane.
Example
toolchain = harbor-rs.lib.mkToolchain {
inherit pkgs;
channel = "stable";
date = "2026-04-01";
};
Most downstream projects only need craneLib from the result. mkDevShell and mkDevShells inherit its matching Cargo configuration automatically.
Compiler caching is host infrastructure rather than a portable toolchain default. Hosts with a managed cache transport can opt in explicitly:
toolchain = harbor-rs.lib.mkToolchain {
inherit pkgs;
cache.enable = true;
};
Optional fleet profiles
The profiles are opt-in. A project that wants harbor-rs to control its Rust version can select a profile and reuse the returned Cargo configuration:
toolchain = harbor-rs.lib.mkToolchain {
inherit pkgs;
toolchainProfile = "stable";
};
cargoConfig = toolchain.cargoConfig;
The selected profile is also inherited by mkCrossPackages for non-native
outputs unless toolchainArgs is supplied explicitly. Updating the profile
manifest in harbor-rs then updates every consumer when it refreshes its
harbor-rs input, without requiring per-project version edits.
Projects with a checked-in Rust toolchain file can consume it directly:
toolchain = harbor-rs.lib.mkToolchain {
inherit pkgs;
toolchainFile = ./rust-toolchain.toml;
};
In file mode, channel and date must be omitted. Explicit extensions and
crossTargets are added to the components and targets declared by the file.
mkCargoConfig
mkCargoConfig generates a .cargo/config.toml tuned for fast local builds and cross-target linker configuration.
What it configures
moldfor Linux linker acceleration when enabledlld-compatible Rust target configuration where appropriate- nightly-only optimizations such as shared generics and parallel frontend work
- optional Cranelift code generation for projects that have verified compatibility
- dev-profile defaults that reduce debug info and peak link memory
- extra target sections for the Rust triples you care about
Parameters
pkgs(required)channel:"nightly"or"stable". This is nottoolchainProfile; that argument belongs tomkToolchain. Reusetoolchain.cargoConfigunless you need a different Cargo file than the selected toolchain.crossTargets: target triples to emit configuration forenableMoldenableCranelift(default:false)enableShareGenericsenableParallelFrontendenableDevProfileOptsdevCodegenUnitsextraConfig: raw TOML appended to the generated file
enableCranelift (default: false)
Cranelift is opt-in because it does not yet support LLVM’s \x01 no-mangle
marker used by some generated C and C++ FFI bindings. Affected projects can
compile successfully but fail to link with unresolved symbols. See
rustc_codegen_cranelift#1689.
Nightly projects that have verified their dependency graph can enable it:
enableCranelift = true;
The generated dev profile then uses Cranelift for workspace crates and LLVM for dependencies.
enableDevProfileOpts (default: true)
When enabled, mkCargoConfig writes a [profile.dev] section that reduces peak codegen and link memory. These settings are intended as default wins:
debug = "line-tables-only"keeps line tables for backtraces while dropping full DWARF.split-debuginfo = "unpacked"puts remaining debug info in side files on platforms that support it; Windows treats this as a no-op.[profile.dev.package."*"] debug = falsedrops dependency debuginfo; workspace crates keep line-table backtraces.
This flag does not set codegen-units. That is a separate tradeoff controlled by devCodegenUnits.
devCodegenUnits (default: null)
Optional positive integer. When set to N, mkCargoConfig writes codegen-units = <N> under [profile.dev]. When null, no codegen-units line is emitted and Cargo’s dev default (256) stands.
This is a tradeoff, not a pure win. Lower codegen-units values such as 32 or 16 make each rustc invocation handle larger codegen units. That can improve wall-clock time for some workspace crates, but it can also increase per-process peak RSS. Set a value only after measuring the project that will consume the config.
Cargo profile precedence means keys in a project’s Cargo.toml [profile.dev] override the same keys from this generated .cargo/config.toml. Set enableDevProfileOpts = false and devCodegenUnits = null if a project needs Cargo’s full default dev profile.
Both parameters are pure .cargo/config.toml annotations. They do not add binaries to the build environment.
Returns
configText: rendered TOMLconfigPath: store path to the generated config file
Example
cargoConfig = harbor-rs.lib.mkCargoConfig {
inherit pkgs;
extraConfig = ''
[alias]
xtask = "run -p xtask --"
'';
};
Pass the result into mkDevShell or mkDevShells and harbor-rs will install
it into a hash-specific directory under the user cache. That directory is
exported as CARGO_HOME only when the caller has not already selected one, so
the project tree stays free of generated configuration.
mkCross
mkCross assembles the cross-compilation helpers for Windows and macOS.
Windows support
MinGW toolchain components are exposed through the returned environment and can be enabled in dev shells without extra project-specific setup.
aarch64 Linux support
The returned linuxAarch64 record lifts the aarch64-unknown-linux-gnu cross
boilerplate out of consumer flakes:
linuxAarch64.cc: thepkgsCross.aarch64-multiplatformstdenvcclinuxAarch64.pkgsCross: the fullpkgsCross.aarch64-multiplatformpackage set (handy forbuildInputssuch as crossopenssl/wayland)linuxAarch64.env: a ready-to-merge attrset withCARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER,CC_aarch64_unknown_linux_gnu, andCXX_aarch64_unknown_linux_gnu
Merge linuxAarch64.env into a crane args set (along with
CARGO_BUILD_TARGET = "aarch64-unknown-linux-gnu" and
PKG_CONFIG_ALLOW_CROSS = "1") to cross-build for ARM64 Linux, or let
mkCrossPackages wire it for you.
macOS support
osxcross is optional and turns on when one of these inputs is available:
macosSdk: an already realized SDK referencemacosSdkStorePath: explicit SDK store path, normally injected by host configurationmacosSdkOutputHash: the SDK’s recursive output hash; pass it together withmacosSdkStorePathso the SDK becomes a sandbox-mounted build input (see the warning below)sdkArchive: a local SDK archive pathmacosSdkEnvPath: explicit local discovery pathMACOS_SDK: opt-in local discovery fallback whenenableImpureMacosSdkEnv = true
Only one of macosSdkStorePath, sdkArchive, or macosSdk can be provided at a time.
Resolution precedence is macosSdk, then macosSdkStorePath, then sdkArchive, then macosSdkEnvPath.
Pass
macosSdkOutputHashwithmacosSdkStorePath. A bare store path carries no Nix string context, so undersandbox = trueit is never bind-mounted and osxcross-clang fails withcannot find macOS SDK— with no eval-time error. The hash lets harbor-rs reconstruct the SDK as a content-addressed fixed-output derivation that resolves to the same path with context, making it a real sandbox input. See macOS SDK Initialization.
Ad-hoc signing of darwin outputs works out of the box: the osxcross toolchain provides an unprefixed codesign_allocate on PATH, so sigtool-style codesign in a postInstall needs no CODESIGN_ALLOCATE wiring. See Ad-hoc signing darwin binaries.
macosSdkEnvPath and the opt-in MACOS_SDK fallback accept a direct MacOSX<version>.sdk directory, a parent directory containing that SDK, or a supported SDK archive. Directory inputs are validated for the SDK settings file, TargetConditionals.h, and the required CoreFoundation/SystemConfiguration frameworks before osxcross is invoked.
Example
cross = harbor-rs.lib.mkCross {
inherit pkgs system;
};
See macOS SDK Initialization for the recommended way to create and inject macosSdkStorePath from host configuration.
mkCrossPackages
mkCrossPackages builds a single Rust workspace for several targets at once,
lifting the per-target crane boilerplate (separate cargoArtifacts, the right
CARGO_BUILD_TARGET, and the matching cross env) out of consumer flakes. It is
the reusable form of the hand-rolled cross matrix that projects such as
rs-modde previously kept inline.
For NixOS consumers that need an explicit build/host contract, use
mkCrossPackageOutputs. It preserves the flat package set and adds the stable
namespace crossPackages.<build-system>.<host-system>:
let
outputs = harbor-rs.lib.mkCrossPackageOutputs {
buildSystem = "x86_64-linux";
hostSystem = "aarch64-linux";
inherit pkgs craneLib cross commonArgs;
pname = "my-service";
targets = ["aarch64-linux"];
};
in {
packages = outputs.packages;
crossPackages = outputs.crossPackages;
}
The helper does not make an existing host-native derivation cross-compilable;
the selected target must still be produced by mkCrossPackages with a real
cross toolchain.
Signature
harbor-rs.lib.mkCrossPackages {
pkgs; # native build-system pkgs with rust-overlay applied
craneLib; # native craneLib from harbor-rs.lib.mkToolchain
cross; # result of harbor-rs.lib.mkCross
pname; # base package name, e.g. "modde"
commonArgs; # base crane args shared across targets; MUST include `src`
targets ? [ "native" ]; # subset of the supported target names
targetArgs ? {}; # optional per-target extra crane args
toolchainArgs ? {}; # mkToolchain args for non-native targets
}
It returns an attrset of derivations keyed by output attribute name:
| target name | output attr name | builder |
|---|---|---|
native | ${pname} | native craneLib.buildPackage |
aarch64-linux | ${pname}-aarch64-linux | craneLib built from pkgsCross.aarch64-multiplatform |
windows | ${pname}-windows | native craneLib + cross.windowsEnv + MinGW |
darwin-x86_64 | ${pname}-darwin-x86_64 | cross.osxcrossRustHelpers.mkCrossBuilder |
darwin-aarch64 | ${pname}-darwin-aarch64 | cross.osxcrossRustHelpers.mkCrossBuilder |
Only the requested targets’ attrs are returned.
Merge order
For each target, args are merged as:
commonArgs(must includesrc)- the target’s
pnameand cross env (CARGO_BUILD_TARGET, linker/CC vars,PKG_CONFIG_ALLOW_CROSS, …) targetArgs.<target>last, so consumers can inject project dependencies (buildInputs,nativeBuildInputs,postInstall,cargoBuildExtraArgs,doCheck, …) and override anything above.
toolchainArgs is applied when constructing the non-native Rust toolchain. Use
it to preserve a project’s pinned channel/date or target list, for example
toolchainArgs = { channel = "nightly"; date = "2026-02-28"; };.
Darwin fallback
The darwin-x86_64 and darwin-aarch64 targets require osxcross with a realized
macOS SDK. When cross.osxcrossRustHelpers == null (no SDK configured), those
outputs fall back to a runCommand that exits 1 at build time with a clear
message, so the attribute still evaluates and the flake stays usable on hosts
without an SDK.
System C libraries on darwin
Unlike the windows and aarch64-linux targets — where nixpkgs offers a ready
cross package set (pkgs.pkgsCross.mingwW64, cross.linuxAarch64.pkgsCross) to
populate buildInputs — the osxcross darwin targets get no system C libraries
by default, and there is no convenient pkgsCross.*-darwin set to pull a cross
openssl (or similar) from. So any crate that links a system C library through a
-sys build script fails on darwin:
warning: openssl-sys@…: Could not find directory of OpenSSL installation
error: failed to run custom build command for `openssl-sys`
The system library `openssl` required by crate `openssl-sys` was not found.
The trap is Cargo feature unification: the offending -sys crate is usually
pulled in transitively and only on cfg(unix) (which includes macOS), so the
break shows up on darwin while native, Windows, and Linux builds stay green. For
example, git2’s default https + ssh features pull openssl-sys and
libssh2-sys — you may not use them, but if any workspace member or dependency
enables them, the unified darwin build links them and fails. Diagnose with
cargo tree -i openssl-sys against a darwin target
(--target aarch64-apple-darwin) to see who pulls it.
Pick the lightest mitigation that fits:
- Drop the default feature that pulls it. If you only need a subset (e.g.
git2for local repository operations), setgit2 = { version = "…", default-features = false }in the workspace-rootCargo.toml. This removesopenssl-sys/libssh2-sysfrom every target, not just darwin, and is usually the cleanest fix. Watch for other deps re-enabling the feature — unification will bring it back. - Vendor the C library. Enable the crate’s
vendoredfeature (e.g.openssl’svendored,libgit2-sys’svendored-openssl) so the library is compiled from source with the cross toolchain instead of looked up as a system library. - Provide a cross-built library. Build the library for the darwin target and
hand it to the build via
targetArgs.darwin-*.buildInputsplus the matchingOPENSSL_DIR/PKG_CONFIG_*env. This is the most work — osxcross is not a nixpkgspkgsCross, so there is no off-the-shelf cross package — and is rarely worth it when (1) or (2) apply.
Example
let
toolchain = harbor-rs.lib.mkToolchain { inherit pkgs; };
cross = harbor-rs.lib.mkCross { inherit pkgs system; };
commonArgs = {
inherit src;
version = "1.0.0";
strictDeps = true;
};
crossPkgs = harbor-rs.lib.mkCrossPackages {
inherit pkgs cross commonArgs;
inherit (toolchain) craneLib;
pname = "my-app";
targets = [ "native" "aarch64-linux" "windows" "darwin-aarch64" ];
targetArgs = {
native.buildInputs = [ pkgs.openssl ];
aarch64-linux.buildInputs = [ cross.linuxAarch64.pkgsCross.openssl ];
windows.buildInputs = with pkgs.pkgsCross.mingwW64; [ openssl windows.pthreads ];
};
};
in {
packages = crossPkgs // { default = crossPkgs."my-app"; };
}
See mkCross for the cross toolchain inputs (windowsEnv,
linuxAarch64, osxcrossRustHelpers) that this helper consumes.
Binary release archives
The v1-compatible mkBinaryRelease packages one or more already-built binaries into a
deterministic, static MUSL archive. The archive contains bin/ and a
manifest.json describing the package name, version, Nix system, Rust target,
and exact binary list. mkReleaseBinaryPackage consumes an unpacked archive
from a locked flake = false input and validates that contract before
installing the binaries. Keep this schema when existing consumers already
consume v1 archives; new multi-format release wiring should use the generic
v2 constructors below.
The producer and consumer are deliberately separate. A project keeps its source-built package for development and exposes the prebuilt package as an explicit opt-in output:
let
release = harbor-rs.lib.mkBinaryRelease {
inherit pkgs;
pname = "my-tool";
version = "1.2.3";
artifacts = {
x86_64-linux-musl = {
package = packages.my-tool-x86_64-linux-musl;
system = "x86_64-linux";
rustTarget = "x86_64-unknown-linux-musl";
binaries = ["my-tool"];
};
aarch64-linux-musl = {
package = packages.my-tool-aarch64-linux-musl;
system = "aarch64-linux";
rustTarget = "aarch64-unknown-linux-musl";
binaries = ["my-tool"];
};
};
};
in
{
packages.release-bundle = release.releaseBundle;
}
A consumer pins each supported release asset as a non-flake input, verifies the published Simit checksum signature before changing its lock file, then selects the matching input:
inputs.my-tool-bin-x86_64.url =
"https://codeberg.org/example/my-tool/releases/download/1.2.3/my-tool-1.2.3-x86_64-linux-musl.tar.gz";
inputs.my-tool-bin-x86_64.flake = false;
packages.my-tool-prebuilt = harbor-rs.lib.mkReleaseBinaryPackage {
inherit pkgs;
pname = "my-tool";
version = "1.2.3";
sources.x86_64-linux = inputs.my-tool-bin-x86_64;
binaries = ["my-tool"];
};
The helper fails closed when the current system has no matching input or the
archive manifest is inconsistent. The release workflow is generated by
simit init release; it builds the bundle on atlas-nix-trusted, publishes
the archives to Codeberg, and signs the checksum manifest with the committed
keys/minisign.pub trust root.
Before the first tag, provision the generated workflow’s required secrets:
CODEBERG_TOKEN at user scope, and a dedicated harbor-rs
MINISIGN_SECRET_KEY/MINISIGN_PASSWORD pair at repository scope. The public
key in keys/minisign.pub must be the matching key; do not reuse another
project’s signing key. Check the contract with
simit release secrets contract --json and the Canix declaration with
canix release secrets check --project /path/to/harbor-rs.
Portable native applications
Applications that cannot satisfy the static ELF contract can use the pinned
nix-bundle backend:
let
release = harbor-rs.lib.mkPortableBinaryRelease {
inherit pkgs;
pname = "my-app";
version = "1.2.3";
artifacts.x86_64-linux.entries.my-app.package = packages.my-app;
};
in {
packages.release-bundle = release.releaseBundle;
}
The resulting archive contains a self-extracting executable and a v2
manifest.json with format = "nix-bundle". Consumers use
mkPortableReleaseBinaryPackage with a locked flake = false archive. The
bundle remains dependent on the host kernel and hardware interfaces (for
example GPU drivers or PipeWire), which must be smoke-tested before switching
the production module.
Generic release bundles
Projects with more than one release format can use the format-neutral
constructors. mkReleaseArtifact exposes one flat file, mkReleaseArchive
stages named files into a deterministic tar.gz or zip, and
mkReleaseBundle combines those outputs and writes one versioned
*-release-manifest.json. The configured bundle set is the explicit input
consumed by Simit’s [release.artifacts].nix_bundle_attrs list and must
produce exactly one versioned manifest; project-specific
build_commands remain additive for formats that need extra signing or
installer work.
let
archive = harbor-rs.lib.mkReleaseArchive {
inherit pkgs;
pname = "my-tool";
version = "1.2.3";
name = "my-tool-1.2.3-x86_64-linux.tar.gz";
package = packages.my-tool;
entries = {"bin/my-tool" = "bin/my-tool";};
system = "x86_64-linux";
rustTarget = "x86_64-unknown-linux-gnu";
};
in
{
packages.release-bundle = harbor-rs.lib.mkReleaseBundle {
inherit pkgs;
pname = "my-tool";
version = "1.2.3";
artifacts = {inherit archive;};
};
}
mkDevShell, mkDocsShell, and mkDevShells
mkDevShell builds one development shell. mkDocsShell builds a dedicated docs/tooling shell with the same base toolchain wiring but with Windows and macOS cross environment variables disabled by default. mkDevShells builds the default four-shell layout used by most downstream workspaces. All generated shells include cargo-sweep and the shared native build tools before project-specific packages are appended.
mkDevShell
Use mkDevShell when you want a single environment with precise control over Windows and macOS helpers.
Important parameters:
pkgscraneLibcrossenableWindowsEnvenableOsxcrossEnvpkgConfigDepspackagesextraEnvextraShellHookcheckscargoConfig: optional override; defaults to the configuration attached bymkToolchain
mkProjectCliShellTools
Use mkProjectCliShellTools when a project wants its flake-built CLI available in direnv or nix develop:
projectCli = harbor-rs.lib.mkProjectCliShellTools {
inherit pkgs;
package = self'.packages.my-cli;
commandName = "my-cli";
hint = "my-cli dev shell - run `my-cli --help`";
versionCheck.expected = version;
};
Append projectCli.packages to the shell packages and projectCli.shellHook to the shell hook. The hook fails if command -v resolves to a different binary than the package output, which prevents stale tools earlier on PATH from shadowing the current flake build.
mkDevShells
mkDevShells wraps mkDevShell and returns:
defaultwindowsmacoscross
That layout works well for workspaces where some crates only need native tools while others need MinGW or osxcross.
mkDocsShell
Use mkDocsShell for CI and local workflows that only need mdBook documentation:
nix develop .#docs -c cargo doc --no-deps --all-featuresnix develop .#docs -c mdbook serve docs
The Plinth-powered project site is intentionally isolated from the reusable
harbor-rs flake so consumers cannot create a dependency cycle. Run its
dedicated nested flake when needed:
nix develop ./site#docs
nix build ./site#site
Example
devShells =
(harbor-rs.lib.mkDevShells {
inherit pkgs cross;
inherit (toolchain) craneLib;
packages = with pkgs; [ just vulkan-loader ];
pkgConfigDeps = with pkgs; [ wayland libxkbcommon udev alsa-lib ];
extraEnv = {
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.vulkan-loader ];
};
extraShellHook = ''
echo "Welcome to my project!"
'';
};
})
// {
docs = harbor-rs.lib.mkDocsShell {
inherit pkgs cross;
inherit (toolchain) craneLib;
packages = with pkgs; [ mdbook ];
};
};
The shell installs the selected configuration in a hash-specific directory
under the user cache and uses it as CARGO_HOME only when the caller has not
already selected one. Pass cargoConfig explicitly only to override the
configuration attached to craneLib by mkToolchain. Prefer
toolchain.cargoConfig over reconstructing the same file with mkCargoConfig.
Activation
Harbor produces shell packages, environment variables, and hooks. It does not activate a repository. Each project chooses its toolchain and direnv policy.
This .envrc pattern requires nix-direnv. Register source watches before
use flake. Disable stale-cache fallback so a failed rebuild is not reported
as success. Do not update lockfiles from direnv.
watch_file flake.nix flake.lock
nix_direnv_disallow_fallback
use flake . --no-update-lock-file || return 1
A nested checkout that must not inherit a parent Harbor shell needs its own
.envrc: either use flake . for that project, or a no-op file such as
true. An absent .envrc inherits the parent.
Harbor’s Cargo hook:
- installs generated config only into Harbor’s hashed cache directory
- leaves an existing
CARGO_HOMEunchanged and reports that generated config is not activated - fails the shell when that install cannot complete
checks.mkDevShells-cargo-home covers repeated, concurrent, user-owned, and
failed-install cases. lib.devShellTests.mkCheck covers executable
availability. Real direnv transitions remain a consumer check.
mkGpuRenderPin
mkGpuRenderPin returns a devShell-ready GPU render profile for projects whose
visual snapshots depend on a stable renderer and driver. It is intended for test
harnesses that compare pixels or structural image similarity and need to fail
fast when a developer is using a different GPU stack than the baseline author.
Usage
visualTestGpuPin = harbor-rs.lib.mkGpuRenderPin {
inherit pkgs;
profile = "mesa-radv";
};
devShells = harbor-rs.lib.mkDevShells {
inherit pkgs cross;
inherit (toolchain) craneLib;
packages = with pkgs; [ just ] ++ visualTestGpuPin.packages;
extraEnv = visualTestGpuPin.env // {
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
pkgs.vulkan-loader
pkgs.libxkbcommon
];
};
};
The consuming test framework should read the exported RS_HARBOR_GPU_*
variables and compare them with the renderer identity reported by the graphics
API it actually initialized.
Parameters
| Param | Default | Description |
|---|---|---|
pkgs | required | nixpkgs package set used to provide the graphics stack |
profile | "mesa-radv" | Named render profile |
expected | {} | Extra or overriding expected identity fields. Attributes with null values are ignored |
Returns: { env, packages }
Profiles
mesa-radv
Pins visual rendering to Vulkan on Mesa RADV:
RS_HARBOR_GPU_PIN_PROFILE=mesa-radvRS_HARBOR_GPU_EXPECTED_BACKEND=VulkanRS_HARBOR_GPU_EXPECTED_VENDOR=0x1002RS_HARBOR_GPU_EXPECTED_DRIVER_CONTAINS=radvWGPU_BACKEND=vulkanVK_DRIVER_FILES=<pkgs.mesa>/share/vulkan/icd.d/radeon_icd.<system>.json
The profile intentionally checks the AMD vendor and RADV driver family without pinning a specific GPU device ID. That keeps one baseline usable across AMD machines while still rejecting NVIDIA, Intel, lavapipe, llvmpipe, Metal, DX12, and GL renderers for that baseline.
Supported RADV ICD names are currently defined for x86_64-linux,
i686-linux, and aarch64-linux.
Expected Identity Contract
Consumers may use these environment variables:
RS_HARBOR_GPU_PIN_PROFILERS_HARBOR_GPU_EXPECTED_BACKENDRS_HARBOR_GPU_EXPECTED_VENDORRS_HARBOR_GPU_EXPECTED_DEVICERS_HARBOR_GPU_EXPECTED_DEVICE_TYPERS_HARBOR_GPU_EXPECTED_NAME_CONTAINSRS_HARBOR_GPU_EXPECTED_DRIVER_CONTAINSRS_HARBOR_GPU_EXPECTED_DRIVER_INFO_CONTAINS
mkGpuRenderPin sets only the fields that are meaningful for the selected
profile. Downstream projects can add stricter checks with expected, for
example:
harbor-rs.lib.mkGpuRenderPin {
inherit pkgs;
profile = "mesa-radv";
expected.RS_HARBOR_GPU_EXPECTED_DEVICE_TYPE = "DiscreteGpu";
}
harbor-ci
harbor-ci provides a small, reproducible Cargo quality gate for Rust
workspaces. It reads [workspace.metadata.harbor-ci] (or the equivalent
package metadata) from Cargo.toml and runs the selected profile:
harbor-ci fast
harbor-ci default
harbor-ci full --report target/harbor-ci.json
The metadata supports package selection and workspace exclusions:
[workspace.metadata.harbor-ci]
test-runner = "nextest"
exclude = ["integration-tests"]
nextest-args = ["--profile", "ci", "-E", "not binary(intheavy_*)"]
exclude is emitted as Cargo’s repeated --exclude option for workspace Cargo
stages and cannot be combined with packages. The formatting stage remains
workspace-wide because rustfmt does not support Cargo package exclusions.
nextest-args is passed as individual arguments to the nextest stage and
requires test-runner = "nextest"; it is never parsed as shell text.
Project-specific feature matrices, service provisioning, and compile-only
checks should remain in the project’s own CI command. harbor-ci is intended
to cover the uniform Cargo gate, not become a second workflow language.
mkAppImage
mkAppImage wraps a built Linux executable into a self-contained AppImage.
Notes
- Linux only
- Opt-in: you must add
nix-appimageto your own flake inputs - The result is a derivation that emits a
.AppImagefile
Example
packages.appimage = harbor-rs.lib.mkAppImage {
inherit system nix-appimage;
program = "${myPackage}/bin/my-app";
};
Use this when you already have a reproducible Nix build and want a portable desktop artifact on top of it.
mkFlatpakManifest
mkFlatpakManifest generates a Flatpak manifest JSON file for packaging a pre-built application with flatpak-builder.
Parameters
pkgsappIdpnamedesktopFileiconruntimeruntimeVersionsdksdkExtensionsfinishArgs
Example
packages.flatpak-manifest = (harbor-rs.lib.mkFlatpakManifest {
inherit pkgs;
appId = "com.example.MyApp";
pname = "my-app";
desktopFile = ''
[Desktop Entry]
Type=Application
Name=My App
Exec=my-app
Icon=com.example.MyApp
Categories=Utility;
'';
}).manifestPath;
This is useful when your project already builds the binary in Nix, but you want Flatpak metadata generated from the same source of truth.
mkCoprSpec
mkCoprSpec generates a Fedora RPM .spec for packaging via COPR, Fedora’s community build service. harbor-rs only produces the spec — uploading or building it stays in CI (or a shell), where the COPR credentials live.
The defaults assume the “binary-shipping” workflow: a binary already built in Nix is bundled as the Source0 tarball, then installed to %{_bindir} by the generated %install section.
Parameters
pkgsnameversion— must not contain-(RPM forbids it inVersion:)release— defaults to1%{?dist}summarylicenseurlsources— list ofSource*lines; defaults toSource0: %{name}-%{version}.tar.gzbuildArch— e.g."x86_64"or"noarch"; omitted by defaultbuildRequiresrequiresdescription— defaults tosummaryprep,build,install,files— override the generated section bodiesdesktopFile,icon,appId— ifappId(reverse-DNS) is set, the default%installand%filesadd${appId}.desktopand a 256x256 hicolor iconchangelog— list of{ date; author; version; entries; }coprMakefile— whentrue, also emit a.copr/Makefilefor COPR’s custom-build SCM methodextraSections— appended raw before%changelog
Example
packages.copr-spec = (harbor-rs.lib.mkCoprSpec {
inherit pkgs;
name = "my-app";
version = "1.0.0";
summary = "My example application";
license = "MIT";
url = "https://example.com/my-app";
appId = "com.example.MyApp";
desktopFile = ''
[Desktop Entry]
Type=Application
Name=My App
Exec=my-app
Icon=com.example.MyApp
Categories=Utility;
'';
changelog = [
{
date = "Tue May 19 2026";
author = "Can <can@example.com>";
version = "1.0.0-1";
entries = ["Initial COPR release"];
}
];
}).specPath;
Workflow
nix build .#my-app
nix build .#copr-spec
tar czf my-app-1.0.0.tar.gz -C result/bin my-app
copr-cli build my-project ./result
For a pre-built binary, you usually want to disable COPR’s debuginfo extraction. Pass it through extraSections:
extraSections = "%global debug_package %{nil}";
If you’d rather have COPR drive the build itself from your git repo, set coprMakefile = true and commit the generated .copr/Makefile plus the .spec — then point a COPR “Custom” build at the repo.
mkHomebrewFormula
mkHomebrewFormula generates a Homebrew formula (.rb) for packaging pre-built binary release archives. At Nix evaluation time, use the ":no_check" placeholder when the archives do not exist yet. After the release archives are built, use harbor-rs brew bump to compute real sha256 sums and update the tap.
The helper renders OS and CPU-specific downloads as nested Homebrew platform blocks, such as on_macos do with on_arm do or on_intel do inside it. A sha256 value of ":no_check" is emitted as Homebrew’s sha256 :no_check symbol for workflows that fill checksums later.
Parameters
pkgsname— Homebrew formula name, e.g.moddeversion— release version without a leadingvdescription— one-line description, 80 characters or fewerhomepage— HTTPS project URLlicense— SPDX license identifierplatforms— attrset keyed bydarwin_arm,darwin_intel,linux_arm, orlinux_intel; each value is{ url; sha256; }dependencies— list of Homebrew formula dependenciesbinaries— list of binaries installed intobincaveats— optional post-install messagetestBlock— optional Ruby body fortest doextraRubyBody— optional raw Ruby appended to the formula body
Example
packages.homebrew-formula = (harbor-rs.lib.mkHomebrewFormula {
inherit pkgs;
name = "my-app";
version = "1.0.0";
description = "My example application";
homepage = "https://example.com/my-app";
license = "MIT";
platforms = {
darwin_arm = {
url = "https://example.com/releases/my-app-1.0.0-aarch64-darwin.tar.gz";
sha256 = ":no_check";
};
darwin_intel = {
url = "https://example.com/releases/my-app-1.0.0-x86_64-darwin.tar.gz";
sha256 = ":no_check";
};
};
dependencies = ["openssl@3"];
binaries = ["my-app"];
testBlock = ''system "#{bin}/my-app", "--version"'';
}).formulaPath;
The returned attrset also includes formulaText for inspection or for downstream tools that need to rewrite placeholders before publishing.
Homebrew CLI
harbor-rs brew bump renders a Homebrew formula from release metadata and archive files that already exist on disk. It computes sha256 sums from the local archive paths and writes a ready-to-commit formula to a tap repository.
The archive URL is embedded in the formula, while the archive path is only used for hashing. The URL must point at the same bytes as the local path; otherwise brew install will fail with a checksum mismatch.
Example
harbor-rs brew bump \
--tap ../homebrew-tap \
--name my-app \
--version 1.0.0 \
--description "My example application" \
--homepage https://example.com/my-app \
--license MIT \
--depends openssl@3 \
--binary my-app \
--archive darwin_arm=https://example.com/releases/my-app-1.0.0-aarch64-darwin.tar.gz,dist/my-app-darwin-arm.tar.gz \
--archive linux_intel=https://example.com/releases/my-app-1.0.0-x86_64-linux.tar.gz,dist/my-app-linux-intel.tar.gz
Use --stdout instead of --tap to print the formula for review or piping:
harbor-rs brew bump --stdout \
--name my-app \
--version 1.0.0 \
--description "My example application" \
--homepage https://example.com/my-app \
--license MIT \
--archive linux_intel=https://example.com/my-app.tar.gz,dist/my-app.tar.gz
Use --push to run git add, git commit, and git push in the tap repository. This mode is default-off, requires --tap, and aborts if the tap has dirty files other than Formula/<name>.rb. Git credentials are the caller’s responsibility.
Dioxus packages
harbor-rs.lib.mkDioxusWebPackage and
harbor-rs.lib.mkDioxusFullstackPackage provide the shared Nix mechanics for
Dioxus 0.7 applications. They vendor Cargo dependencies, resolve the exact
wasm-bindgen-cli version recorded in Cargo.lock, add the native linker
tools needed by fullstack builds, and run the Dioxus CLI offline.
Web package
Use the web builder when the product already owns the server process or static asset routing:
harbor-rs.lib.mkDioxusWebPackage {
inherit pkgs craneLib rustToolchain;
src = filteredSource;
cargoLock = ./Cargo.lock;
pname = "my-app-dioxus";
package = "my-app";
wasmBindgenCli = exactWasmBindgenCli;
webFeatures = [ "web" ];
wasmSplit = true;
installSubdir = "share/my-app/dioxus";
}
The derivation installs the generated Dioxus public/ tree below
installSubdir (including index.html, hashed JavaScript, and WASM assets).
The product can copy that tree into its own static directory and apply its own
compression or cache policy.
Release plans pass --debug-symbols=false explicitly. Dioxus 0.7 otherwise
retains DWARF even for --release, increasing the browser artifact and making
some Binaryen releases abort while re-emitting debug information. Non-release
profiles keep symbols by default; set debugSymbols explicitly when a product
needs the opposite policy.
The builder also turns an ignored wasm-opt failure back into a failed Nix
build, preventing an unoptimized fallback bundle from reaching a release.
Fullstack package
Use the fullstack builder when Dioxus owns the deployable server executable:
harbor-rs.lib.mkDioxusFullstackPackage {
inherit pkgs craneLib rustToolchain;
src = filteredSource;
cargoLock = ./Cargo.lock;
pname = "my-app";
package = "my-app";
wasmBindgenCli = exactWasmBindgenCli;
webFeatures = [ "web" ];
serverFeatures = [ "server" ];
publicSubdir = "share/my-app/public";
}
The output contains bin/my-app and bin/my-app-unwrapped, plus the generated
public tree. The wrapper sets DIOXUS_PUBLIC_PATH to the packaged public path;
set wrapServer = false when the product supplies its own process wrapper.
Externally built servers
Some applications own an Axum or other server executable that embeds Dioxus SSR while building the browser bundle separately. Add the asset linker to that server derivation’s native build inputs and run it before wrapping or stripping the executable:
let
dioxusAssetLinker = harbor-rs.lib.mkDioxusAssetLinker {
inherit pkgs;
dioxusCli = pkgs.dioxus-cli;
};
in
craneLib.buildPackage {
nativeBuildInputs = [ dioxusAssetLinker ];
postInstall = ''
dioxus-link-assets \
"$out/bin/my-server" \
"$out/share/my-app/public/assets"
'';
}
This runs Dioxus’ own dx tools assets command, which copies declared assets
and patches their hashed paths into the executable. Crane consumers must call
it in the derivation’s postInstall, before reference-removal hooks sanitize
vendored source paths.
Toolchain and feature policy
mkDioxusBuildPlan is available for consumers that need to inspect or compose
the command shape. Fullstack plans build the client with @client and the
server with @server --server, allowing independent Cargo feature and target
arguments. resolveWasmBindgenCli fails early when the lockfile version has no
exact nixpkgs package; callers may pass a custom derivation with the matching
.version instead. mkDioxusPackage is retained as a compatibility alias for
the web builder during the migration window.
Android APK packages
Android SDK, APK, and Maven-cache helpers live in harbor-android.
harbor-rs.lib re-exports mkAndroidSdk, mkAndroidDevShell, mkAndroidApk,
mkAndroidApkDevBuilder, mkAndroidFlavorTable, and findLocalMavenCache for
one migration release. New consumers should take harbor-android directly.
Keep mkToolchain here and pass rustToolchain into the Android helpers.
mkGradlePackage and fetchMavenCache also stay in harbor-rs.
Package Tests
harbor-rs exposes package build and package-test plans through harbor-meta
so downstream flakes can describe package verification consistently before each
package format has a runnable backend.
The hierarchy is:
- artifact builders for package outputs
- generic package test plans for any package artifact
- Windows package test plans for PowerShell-based installers
- runner builders for local test environments
- Chocolatey Vagrant plans backed by the community Chocolatey test environment
Existing package helpers expose an artifactBuilder field on their result where
they have a concrete output: mkAppImage, mkDebPackage, mkCoprSpec,
mkFlatpakManifest, mkHomebrewFormula, mkScoopManifest, mkChocoPackage,
mkAndroidApk, mkAndroidApkDevBuilder, and mkTrunkPackage. Only Chocolatey
has a runnable VM helper today. Other package formats can still emit normalized
plans with unsupportedRunnerReason so release tooling can surface the missing
local backend explicitly.
Chocolatey
let
choco = harbor-rs.lib.mkChocoPackage {
inherit pkgs;
id = "my-app";
version = "1.0.0";
description = "Example Windows CLI";
homepage = "https://example.com";
license = "MIT";
licenseUrl = "https://example.com/LICENSE";
authors = ["Example"];
architectures.x64 = {
url = "https://example.com/my-app-1.0.0.zip";
sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
};
};
chocoTest = harbor-rs.lib.mkChocoTestEnvironment {
inherit pkgs;
chocoPackage = choco;
verifyPowerShell = [''
my-app --version
''];
};
in {
packages.choco = choco.nupkgPath;
apps.choco-test = chocoTest.app;
}
mkChocoPackage exposes artifactBuilder, with output pointing at the
generated .nupkg. mkChocoTestEnvironment carries that builder into its test
plan and returns a runnerBuilder for the generated Vagrant environment.
apps.choco-test writes a local .package-test/<package> directory, stages the
.nupkg under packages/, writes a generated Vagrantfile, and then runs the
Chocolatey install inside the Vagrant VM.
The generated Vagrantfile uses the chocolatey/test-environment box, syncs
packages to C:\packages, and defaults to verifier-like VirtualBox settings:
headless, 4 CPUs, 6144 MiB RAM, and disabled clipboard/drag-and-drop.
Generic Plans
Use mkPackageArtifactBuilder and mkPackageTestPlan when a package format
does not yet have a runner:
let
deb = harbor-rs.lib.mkDebPackage {
inherit pkgs;
packageName = "my-app";
version = "1.0.0";
arch = "amd64";
maintainer = "Example <example@example.com>";
description = "Example CLI";
files = [
{
source = "${self.packages.${system}.my-app}/bin/my-app";
target = "/usr/bin/my-app";
mode = "0755";
}
];
};
in harbor-rs.lib.mkPackageTestPlan {
inherit pkgs;
kind = "debian";
artifactBuilder = deb.artifactBuilder;
installCommand = "apt install ./my-app.deb";
unsupportedRunnerReason = "No generic Debian VM runner is implemented yet.";
}
Use mkPackageArtifactBuilder directly when a downstream packaging helper is not
implemented in harbor-rs yet but still has a concrete output path.
Steam Runtime Tools
mkSteamRuntimeTools provides generic helpers for Rust games that ship native
Linux builds on Steam and want explicit dynamic-linking audits.
The helpers deliberately avoid app-specific policy. Your project still owns Steam app IDs, depot layout, release branches, crash handlers, symbols, and the actual Cargo or engine build command.
Basic Usage
steamRuntimeTools = harbor-rs.lib.mkSteamRuntimeTools {
inherit pkgs;
};
The default runtime is Steam Linux Runtime 3.0 sniper:
steamRuntimeTools.image
# "registry.gitlab.steamos.cloud/steamrt/sniper/sdk"
You can select another known runtime or provide a custom OCI image:
steamRuntimeTools = harbor-rs.lib.mkSteamRuntimeTools {
inherit pkgs;
runtime = "scout";
};
steamRuntimeTools = harbor-rs.lib.mkSteamRuntimeTools {
inherit pkgs;
runtime = "my-runtime";
customImage = "registry.example.com/my/steam-sdk:latest";
};
Flake Apps
Expose the tools as apps so just, CI, and release scripts can call them:
apps = {
steam-runtime-exec = {
type = "app";
program = "${steamRuntimeTools.steamRuntimeExec}/bin/steam-runtime-exec";
};
audit-elf-runtime-deps = {
type = "app";
program = "${steamRuntimeTools.auditElfRuntimeDeps}/bin/audit-elf-runtime-deps";
};
};
Running Commands In The SDK Container
steam-runtime-exec runs a command in the selected SDK image, mounting the
current working directory at the same path:
nix run .#steam-runtime-exec -- -- cargo build --release
The wrapper does not install Rust, Nix, engine dependencies, or Steamworks SDK content. Projects with custom toolchains should either bake those into their own image or use the wrapper for runtime validation commands.
For Nix-built audit tools, mount /nix/store read-only:
nix run .#steam-runtime-exec -- --mount-nix-store -- \
/nix/store/.../bin/audit-elf-runtime-deps dist/linux
Dependency Audits
The audit scripts are intentionally allowlist-driven:
nix run .#audit-elf-runtime-deps -- \
--require-origin-rpath \
--allow-needed-regex '^(lib(c|m|dl|pthread|gcc_s|stdc\+\+|steam_api).*)\.so(\..*)?$' \
dist/linux
Windows and macOS use the same pattern:
nix run .#audit-windows-runtime-deps -- \
--allow-dll-regex '^(KERNEL32|USER32|ADVAPI32|WS2_32|steam_api64)\.dll$' \
dist/windows
nix run .#audit-darwin-runtime-deps -- \
--allow-dylib-regex '^(@executable_path|@rpath|/usr/lib/|/System/Library/)' \
dist/macos
For Linux Steam release candidates, run the ELF audit inside the same Steam Runtime container that the Steamworks launch option selects.
mkAdapter
mkAdapter builds a typed harbor-adapter value that describes how a downstream project should talk to an Attic binary cache.
Example
harborAdapter = harbor-rs.lib.mkAdapter {
attic = {
endpoint = "https://cache.example.com";
cache = "main";
};
};
The adapter includes:
- cache endpoint
- cache name
- token environment variable, defaulting to
ATTIC_TOKEN - a small type marker so
mkAtticPushcan validate what it receives
Use mkAtticPush to turn the adapter into a runnable Nix app.
mkAtticPush
mkAtticPush creates a Nix app that pushes store paths to Attic using an ephemeral credential file. It can publish explicit paths, optionally including their closures, and every recursively locked source path returned by nix flake archive.
Example
apps.push-cache = harbor-rs.lib.mkAtticPush {
inherit pkgs;
adapter = infra.harborAdapter;
paths = [ self.packages.${system}.default ];
};
apps.push-flake-inputs = harbor-rs.lib.mkAtticPush {
inherit pkgs;
adapter = infra.harborAdapter;
flake = ".";
};
Run it with:
nix run .#push-cache
nix run .#push-flake-inputs
The generated script expects the token environment variable described by the adapter, defaulting to ATTIC_TOKEN. It never runs attic login; its mode-0600 Attic configuration is removed on exit. Set HARBOR_ATTIC_MANIFEST to retain the sorted path manifest outside the temporary directory.
Bevy Template
harbor-rs exports a bevy flake template for new game projects.
Initialize from the template
nix flake init -t git+https://github.com/caniko/harbor-rs.git#bevy
What the template includes
mkToolchain,mkCross, andmkCargoConfigwired into the project flakemkDevShellsfor native, Windows, macOS, and combined cross shells- commented examples for
mkAppImageandmkFlatpakManifest - Bevy-specific shell dependencies in
nix/dev-shells.nix
If you want to understand the generated shell behavior in more detail, read mkDevShell and mkDevShells.
macOS SDK Initialization
For version-controlled project flakes, harbor-rs supports an explicit macosSdkStorePath, but that path is host-specific. Reusable flakes should accept it from host configuration instead of committing it directly. harbor-rs owns SDK discovery and validation; osxcross receives only the resolved SDK reference.
Initialize once per host
nix run harbor-rs#init-macos-sdk -- /host/local/MacOSX26.1.sdk.tar.xz 26.1
The command realizes the archive, validates the SDK root, and prints:
- the host-specific store path for host configuration
- the resolved SDK root
- the recursive hash used for the fixed-output rebuild
Validation requires:
SDKSettings.jsonusr/include/TargetConditionals.hSystem/Library/FrameworksSystemConfiguration.frameworkCoreFoundation.framework
Inject the printed store path
canix.development.macosSdk.storePath = "/nix/store/<host-sdk>-macosx-sdk-26.1";
canix.development.macosSdk.sdkVersion = "26.1";
Host wrappers can then pass the value to mkCross as macosSdkStorePath. Standalone project flakes should keep it null so native and non-macOS cross outputs evaluate everywhere. On another machine, either run the same initialization flow with that host’s local archive or fetch the realized store path from your binary cache.
Always pass macosSdkOutputHash too (the sandbox gotcha)
A bare macosSdkStorePath is a string with no Nix string context, so on its own it is not a build input. Under sandbox = true the daemon never bind-mounts it and osxcross-clang fails inside the sandboxed build with cannot find macOS SDK, even though the path is valid in the store. Passing only macosSdkStorePath silently produces this broken state — there is no error at eval time.
Always pass macosSdkOutputHash alongside it (the recursive hash printed by init-macos-sdk):
cross = harbor-rs.lib.mkCross {
inherit pkgs system;
macosSdkStorePath = "/nix/store/<host-sdk>-macosx-sdk-26.1";
macosSdkOutputHash = "sha256-…"; # recursive hash from init-macos-sdk
osxSdkVersion = "26.1";
};
With the hash, harbor-rs reconstructs the SDK’s fixed-output derivation. Its output path is fully determined by (name, outputHash, recursive sha256), so it resolves to the identical store path — but now with context, making the SDK a real, sandbox-mounted dependency. harbor-rs asserts the reconstructed path equals macosSdkStorePath, so a hash/version mismatch fails loudly at eval instead of silently. Already-realized paths are used directly (no rebuild); a missing, unsubstitutable SDK fails early with a clear message.
Because the reconstruction is a recursive-SHA256 fixed-output derivation, its output is content-addressed and signature-exempt: any substituter serving it (e.g. a private Attic cache) can provide it without a trusted signature. The substituter URL must still be in the daemon’s effective substituters list, though — a flake nixConfig.extra-substituters only takes effect with --accept-flake-config or for a trusted user.
Ad-hoc signing darwin binaries
The osxcross toolchain installs an unprefixed codesign_allocate on PATH (next to the arch-prefixed <arch>-apple-<target>-codesign_allocate), mirroring macOS’s /usr/bin/codesign_allocate. Tools that ad-hoc sign Mach-O outputs — e.g. sigtool’s codesign --sign - --force in a postInstall — spawn codesign_allocate by bare name. With the osxcross toolchain in nativeBuildInputs (as mkCrossPackages and the darwin cross-builders arrange), that bare-name spawn resolves on PATH, so no CODESIGN_ALLOCATE wiring is needed. Sign in postInstall, before fixupPhase’s strip leaves the signature in place.
Local discovery with MACOS_SDK
For local impure workflows, mkCross can read MACOS_SDK when enableImpureMacosSdkEnv = true. The value may be:
- a direct SDK root, such as
/path/to/MacOSX26.1.sdk - a parent directory containing
MacOSX26.1.sdk - a supported SDK archive, such as
/path/to/MacOSX26.1.sdk.tar.xz
Explicit arguments take precedence over MACOS_SDK: macosSdk, macosSdkStorePath, sdkArchive, then the environment value.