Flake outputs and delivery¶
These are the functions a downstream flake calls once, near the top level,
and then stops thinking about. examples/flake-outputs/ is a worked version
of the whole surface.
A nerima-lisp package normally does not call them one at a time:
mkPackageFlake, at the end of this page, is the org
standard as a single function call and generates all of them together.
Read the individual entries first — the preset's arguments are named after
them, and every escape hatch it offers hands you back the function it was
about to call.
mkExecutable¶
Deliver a standalone binary via asdf:program-op — one code path on every
declared platform, including aarch64-darwin with SBCL.
packages.${system}.my-cli = cl.mkExecutable {
args = {
pname = "my-cli";
lispSystem = "my-app";
version = cl.fromAsdSystem ./my-app.asd;
src = cl.mkLispSource { root = ./.; };
lispDependencies = [ ... ];
};
dynamicSpaceSize = 2048;
imageRequires = [ "sb-cover" ];
};
| Argument | Default | Effect |
|---|---|---|
args |
required | The exact attrset you would pass to lispDerivation, without lispBuildOp — this function sets it |
buildOperation |
"asdf:operate 'asdf:program-op" |
The ASDF operation class invoked via asdf:operate |
programPath |
null |
Where ASDF actually wrote the program, if not the system name |
dynamicSpaceSize |
null |
Megabytes of SBCL dynamic space |
imageRequires |
[ ] |
Implementation modules to (require ...) before the image is dumped |
installSource |
false |
Ship the sources under the delivered image's own prefix — see the contract below |
args.pname controls the installed executable name and may differ from
lispSystem. Exactly one system must be named, via lispSystem or a
single-element lispSystems; anything else is an evaluation error.
args.version, when present, reaches the store path
(…-my-cli-1.0.1) and the derivation's pname/version, so a delivered
binary can be told apart from the previous release's by path, and
mkOverlay can name it without re-parsing anything.
If you are writing a whole package's flake, mkPackageFlake's executable
argument calls this for you from the arguments it already resolved, so the
package's pname, version, src, meta and lispDependencies are spelled
once rather than twice. Reach for mkExecutable directly for a delivery that
argument cannot express — and pass args = ctx.lispDerivationArgs when you do,
rather than restating the package.
What $out contains, and what an image may assume¶
This contract is written down because its absence was a bug: an image that
resolved its own ASDF source root by looking for share/common-lisp/source/
under its installation prefix found nothing, fell back to the build-time
source directory, and died with Failed to find the TRUENAME of
/nix/var/nix/builds/nix-…/src/package.lisp the first time it re-loaded one of
its own systems. Both sides were individually correct and disagreed silently.
$out always contains $out/bin/<pname> — the entry point, and
meta.mainProgram. It is the real executable program-op produced, unless
the delivery has native libraries to resolve at run time, in which case it is
a thin makeWrapper shim around that executable (see
Dependencies and native libraries):
baking a search path into DYLD_LIBRARY_PATH/LD_LIBRARY_PATH has no
representation inside the binary itself.
With installSource = true it also contains:
$out/share/common-lisp/source/<pname>/ args.src, verbatim
$out/share/common-lisp/source/<dep>/ one directory per entry of the
resolved dependency closure
so that one (:tree "<prefix>/share/common-lisp/source/") registry entry
resolves the delivered system and everything it loads. The dependency closure
is installed too, not just the system's own tree: sources that are findable
while their dependencies' are not fail at the same place, one
asdf:load-system later.
A delivered image may therefore assume that share/common-lisp/source/ exists
under the installation prefix of the file it is running out of — the parent of
the directory holding sb-ext:*runtime-pathname* — and nothing more.
What is not promised: $out also holds the whole built tree at its root,
because that is where ASDF wrote the program and the delivery copies the
derivation wholesale. That is an artifact, not an interface —
share/common-lisp/source/ is the only source layout to build on.
installSource defaults to false because it puts the source tree and its whole
dependency closure into the delivered runtime closure, which a binary that
never re-loads a system at run time should not pay for.
programPath exists because ASDF writes a program to the system's
:build-pathname, which is not necessarily $out/bin/<system>. That
ASDF-specific detail stays explicit at the boundary, and the Nix result is
then normalized. If the program is missing or not executable, the build fails
with a message naming the path it looked at, rather than installing nothing.
dynamicSpaceSize and imageRequires act on the Lisp that performs the
dump. They are spelled as SBCL command-line options, so requesting them for
another implementation is an assertion failure. imageRequires has a
preferred alternative: :depends-on ((:require :sb-cover)) in the .asd is
ASDF-native, works everywhere, and keeps a system's dependencies in the
system definition. The Nix option is for the remaining case where the dump
needs a module the system itself does not depend on.
What Nix owns and what the .asd owns¶
The .asd owns everything about what is built: :build-operation,
:build-pathname, :entry-point, and which implementation contribs the
system needs. None of those are Nix options here, and adding them would give
a system two places to disagree about its own entry point.
Nix owns the invocation of the Lisp that performs the dump. Two knobs are not options here:
save-runtime-options-
Not an option because it cannot be turned off.
uiop:dump-imagehardcodes:save-runtime-options twhenever it is dumping an executable, soprogram-opalways saves them. The observable behaviour it buys — the image starts with the heap it was dumped with, and the runtime does not eat the user's arguments — needs nothing further from this module. core compression-
Not an option because it cannot be turned on for
program-op. ASDF'sperformmethod for animage-opcallsdump-imageand never passes:compression, and SBCL exposes no global to change that, so the only route would be monkey-patching an ASDF method at build time.
Darwin, and why no code-signing step is needed¶
mkExecutable drives the same asdf:program-op path on aarch64-darwin as on
every other declared platform. An earlier revision fell back to a plain,
non-executable .core via save-lisp-and-die wrapped in sbcl --core with
makeWrapper on Darwin with SBCL, on the strength of one observation that
program-op had not produced a binary within five minutes. That observation
did not reproduce: rebuilding the exact same lispDerivation invocation this
function uses against a current nixpkgs SBCL on aarch64-darwin produced a
working executable in seconds, every time tried; see
Platform coverage for what is and is not
verified about this now.
No sigtool/codesign step is needed to make the delivered binary runnable.
codesign -dv on a freshly built binary reports
flags=0x20002(adhoc,linker-signed): nixpkgs' SBCL runtime is already
ad-hoc/linker-signed the way every locally linked Mach-O binary is on modern
macOS, and save-lisp-and-die :executable t (what program-op calls on
SBCL) writes the delivered image by appending the dumped heap to a copy of
that already-signed runtime, so the signature travels with it.
mkApp¶
A flake apps.<system>.<name> entry for a derivation that already knows
which of its binaries is the interesting one.
| Argument | Default | Effect |
|---|---|---|
drv |
required | Any derivation with meta.mainProgram, or whose pname is its binary name |
description |
drv.meta.description |
App description |
Every downstream flake spells this out by hand, and every one of them repeats
the program name three times: in program, in meta.mainProgram, and in the
bin/ path. mkExecutable and lispScript both already set
meta.mainProgram, and lib.getExe exists to read it, so the derivation
stays the single source of truth for its own entry point.
mkTestApp¶
nix run .#test — the org-standard entry point that runs the repository's
own run-tests.lisp.
apps.${system}.test = cl.mkTestApp {
pname = "my-app";
src = ./.;
lispDependencies = [ clWeave ];
};
| Argument | Default | Effect |
|---|---|---|
pname |
required | Project name; the runner installs as <pname>-test |
src |
required | The source tree containing runner; its systems shadow anything else on the registry |
runner |
"run-tests.lisp" |
The entry point inside src |
lisp |
pkgs.sbcl |
Implementation to run it with |
lispImplementation |
lib.getName lisp |
Table row for that implementation |
lispDependencies |
[ ] |
Other systems the suite needs — a test framework, typically |
timeoutSeconds |
600 |
SIGTERM deadline |
killAfterSeconds |
30 |
SIGKILL grace period |
description |
"Run the <pname> test suite" |
App description |
This drives the script rather than asdf:test-system, so the plain
sbcl --script run-tests.lisp path a contributor uses in a REPL-less shell is
the same path CI exercises; mkTestCheck covers the
test-system route. It complements the check helpers: same suite, but the
nix run output rather than a pass/fail derivation.
The script runs from the source tree in place, because a run-tests.lisp
locates its own systems relative to *load-truename* — copying it elsewhere
would silently test whichever copy of the project happened to be on the
registry first.
The registry it builds has no trailing :. That is ASDF's spelling of "and
then inherit whatever is configured", which would let a developer's
~/.config/common-lisp make nix run .#test pass locally and fail in CI. An
explicitly exported CL_SOURCE_REGISTRY is still honoured, appended so it
cannot shadow the tree under test.
The timeout -k escalation matters because SBCL defers signals to
safepoints: a tight compiled loop — a runaway backtracking bug is exactly
this — outlives a bare SIGTERM and would otherwise fall through to the
enclosing CI job timeout instead of failing here with an attributable error.
mkOverlay¶
overlays.default — expose this flake's packages in the pkgs namespace
under their own names.
| Argument | Default | Effect |
|---|---|---|
packages |
required | The flake's own self.packages, keyed by system |
names |
[ "default" ] |
Which entries of packages.<system> to publish |
The name a package takes in pkgs is its pname, read off the derivation
rather than spelled a second time: a flake whose packages.default is built
by lispDerivation already named it once, and an overlay that repeats the
name is one rename away from exporting pkgs.cl-weave from a package called
something else.
names is not "all of them" — packages.docs must not become
pkgs.docs.
It reads the system from prev, not final. The attribute names here are
computed from each package's own pname, so evaluating the attrset's key set
forces system, and final.stdenv cannot be forced while the overlay
producing it is still being evaluated. That is an infinite recursion, and it
was reproduced before the comment recording it was written. A hand-written
overlay with literal keys gets away with final because its names are known
without evaluating anything; the moment it grows a second package and starts
generating names, it does not.
mkDevShell¶
A nix develop shell that exports the derivation's own resolved
CL_SOURCE_REGISTRY and prints a short banner.
devShells.${system}.default = cl.mkDevShell {
drv = myApp;
extraPackages = [ pkgs.sbcl.pkgs.alexandria ];
};
lispDerivation's result is already a usable shell — it carries the right
buildInputs. mkDevShell adds the two things a raw derivation cannot:
interactive-only packages that do not become part of the build, and a
banner so nix develop is not silent about what it just set up. Plus the one
thing inputsFrom alone gets wrong.
That last one: lispDerivation exports its search path by having its
shellHook eval "$setAsdfPathPhase", and mkShell copies shellHook text
out of inputsFrom but not the derivation's environment — so
$setAsdfPathPhase arrived empty and the eval silently did nothing. The
banner claimed CL_SOURCE_REGISTRY was set while it was in fact unset and
(asdf:load-system ...) failed. It is now set here from the derivation's own
resolved registryPath: same value, same order, including the literal $PWD
entry that makes the tree you are standing in take precedence, and
prepending any registry the caller had already exported exactly as the build
does.
ASDF_OUTPUT_TRANSLATIONS is left alone. The build sets it to
the identity mapping so fasls land beside their sources and the output stays
reusable as a registry entry; a REPL wants the opposite, because fasls
dropped into the working tree are precisely the artefacts mkLispSource then
has to keep back out of the store. Left unset, ASDF caches under
~/.cache/common-lisp.
The registry it exports is drv's, so drv decides whether
lispCheckDependencies are on it: lispDerivation resolves them only when
doCheck is true and drops them entirely when it is not. Pass
drv = myApp.enableCheck — the same derivation with checks on, which is also
what mkScriptCheck builds a check from — whenever
the shell is meant to run the suite, or the test system will not load in it.
That costs no build: inputsFrom takes a derivation's inputs, not its
output, so nothing realises enableCheck itself; the check dependencies are
pulled in, and the check that runs the suite builds those anyway.
mkPackageFlake's generated devShells.default does exactly this.
An extraPackages Lisp built with pkgs.sbcl.withPackages composes because
mkShell puts packages ahead of everything from inputsFrom on PATH, so
the wrapped Lisp wins over the plain one the derivation pulls in. Nixpkgs
builds that wrapper with
--prefix CL_SOURCE_REGISTRY rather than --set, so its packages prepend to
the registry exported here rather than replacing it.
mkDocsSite¶
A generic mkdocs-material site builder.
packages.${system}.docs = cl.mkDocsSite {
root = ./docs;
pname = "my-app-docs";
version = cl.fromAsdSystem ./my-app.asd;
meta = {
description = "Rendered documentation for my-app";
homepage = "https://github.com/nerima-lisp/my-app";
license = lib.licenses.mit;
};
};
| Argument | Default | Effect |
|---|---|---|
root |
required | The directory mkdocsYmlName lives in |
fileset |
null |
A fileset, when the docs need something outside root |
pname |
"docs-site" |
By convention <project>-docs |
version |
"unstable" |
nixpkgs' spelling of "no meaningful version" |
meta |
{ } |
description / homepage / license |
strict |
true |
Promote broken links and pages missing from the nav to build failures |
mkdocsYmlName |
"mkdocs.yml" |
Config file name inside root |
It does not infer any one project's fileset shape. Pass
fileset, built with lib.fileset.unions like any other Nix project, when
the mkdocs.yml is not itself the root of what the build must see — a repo
whose config sits at docs/mkdocs.yml but whose pages include a file from
the repository root passes root = ./., a fileset naming both, and
mkdocsYmlName = "docs/mkdocs.yml" — and plain root for the common
single-directory case. That one optional argument is what collapses two docs
conventions that grew independently into one function.
Identity is the caller's, not this function's. The site is a package a
project publishes under its own name and version — it appears in
nix flake show, in a store path, and in whatever deploys it — so baking in
docs-site/unstable made every consumer that cared reject the helper and
hand-write the derivation again. version pairs naturally with
fromAsdSystem, so the .asd stays the one
place a release number is edited.
strict defaults to true because mkdocs build --strict turns a broken link
or a page missing from the nav into a build failure. Material bundles its own
assets, so the build needs no network either way. This documentation site is
itself built by this function.
collect¶
Pull a named set of sibling packages out of flake inputs.
lispDependencies = builtins.attrValues (cl.collect {
flakeInputs = inputs;
names = [ "cl-weave" "cl-json-kit" ];
inherit system;
});
The real fix for cross-repository composition is not a bigger helper: once a
sibling repository's own flake exposes its package, it is an ordinary
lispDependencies entry and no bespoke registry-string merging is needed at
all. This is only a naming-convention shorthand for "pull the default package
out of each of these flake inputs by name", so a downstream flake does not
reinvent yet another way to spell that. names must be attribute names
shared by both flakeInputs and each input's own packages.<system>.
mkPackageFlake¶
Every output a nerima-lisp package's flake.nix declares, as one call.
outputs =
{ self, nixpkgs, treefmt-nix, cl-nix-forge, ... }:
cl-nix-forge.lib.x86_64-linux.mkPackageFlake {
inherit self nixpkgs;
pname = "my-app";
asd = ./my-app.asd;
root = ./.;
systems = [ "x86_64-linux" ];
meta = { description = "..."; };
docs = { root = ./docs; };
treefmt = { evalModule = treefmt-nix.lib.evalModule; };
executable = { dynamicSpaceSize = 4096; };
};
The org standard is distributed as a template — one flake.nix copied by
hand into every repository. The copies then drift: at the time this function
was written, three of them spelled the same .asd version extraction three
ways, and one of the three regexes had already diverged. This is that
template as a function call, so the copies converge by construction.
What it generates¶
Exactly the org standard's required-output table, per declared system, and nothing else:
| Output | Built by | Present when |
|---|---|---|
packages.<pname> |
lispDerivation |
always |
packages.default |
alias for <pname> — or the CLI |
always |
packages.docs |
mkDocsSite |
docs != null |
checks.default |
mkScriptCheck over runner |
always |
checks.docs |
asserts the --strict site emitted an index.html |
docs != null |
checks.formatting |
the treefmt gate | treefmt != null |
apps.test |
mkTestApp |
always |
apps.default |
alias for test — or the CLI |
always |
apps.<pname> |
mkApp over the CLI |
executable != null |
devShells.default |
mkDevShell |
always |
formatter |
the same treefmt eval checks.formatting uses |
treefmt != null |
overlays.default |
mkOverlay |
overlayPackages != [ ] |
Two of those rows are decisions rather than plumbing:
packages.<pname> is the ASDF system even when a CLI is delivered,
because that is the entry a downstream lispDependencies needs;
packages.default is what nix build . produces and what the overlay
publishes, which for a package that ships a binary is the binary.
devShells.default is built from the check-enabled derivation. The
documented contributor loop is nix develop then sbcl --script
run-tests.lisp — the same runner checks.default drives — and a shell built
from the doCheck = false derivation has no lispCheckDependencies on its
registry, so a package whose test system depends on a sibling gets a shell
that cannot load its own test system. It costs no extra build:
mkShell's inputsFrom takes a derivation's inputs, not its output.
systems is iterated with lib.genAttrs, not flake-utils: the caller's list
is the whole truth about which platforms exist, so an output for an
undeclared system cannot exist.
Arguments¶
| Argument | Default | Effect |
|---|---|---|
self |
required | The flake's own self; the treefmt gate needs every tracked file |
pname |
required | Names the package, the derivation, the runner check and the docs site |
asd |
required | Path to the .asd. The only place a version comes from |
root |
required | What mkLispSource filters |
systems |
required | Declared platforms, and only those |
lispSystem |
pname |
The ASDF system name, when it differs |
meta |
{ } |
The package's meta; also the docs site's, minus mainProgram |
nixpkgs |
null |
The caller's nixpkgs input; used only by the default pkgsFor |
pkgsFor |
import nixpkgs { … } |
Override to apply config/overlays in one place |
forgeFor |
this library, per system | Override to hand in an instance you already have |
src |
null |
A ready-made source, bypassing mkLispSource entirely |
sourceInclude / sourceExclude |
[ ] |
mkLispSource's escape hatches |
lisp |
ctx: ctx.pkgs.sbcl |
The implementation |
lispDependencies |
_: [ ] |
Sibling packages as built derivations, never registry strings |
lispCheckDependencies |
_: [ ] |
The same, for doCheck only |
packageArgs |
_: { } |
Extra lispDerivation arguments (lispAsdPath, nativeLibraries, …) |
executable |
null |
mkExecutable arguments; delivers a CLI from this package |
runner |
"run-tests.lisp" |
Drives checks.default and apps.test alike |
timeoutSeconds / killAfterSeconds |
600 / 30 |
The runner's limits |
docs |
null |
mkDocsSite arguments; null omits the package and its check |
treefmt |
null |
{ evalModule, module ? <nix-only default> } |
devShellPackages |
_: [ ] |
Interactive-only tools for the generated shell |
overlayPackages |
[ "default" ] |
Which packages the overlay publishes; [ ] omits it |
extraOutputs |
_: { } |
Add-only, per output kind |
overrideOutputs |
_: { } |
Replace-only, per output kind |
root has no default. A flake's self is an attrset carrying an outPath, and lib.fileset
refuses a string-like value for its root, and the failure then surfaces from
inside mkLispSource naming neither self nor the argument that caused it.
The default treefmt module formats Nix and only Nix. That is the standard's
answer, not an oversight: a YAML formatter mangles a GitHub Actions on:
key — it is the boolean true to a YAML 1.1 parser — and reformatting
Markdown churns a whole docs tree for no reviewable gain.
The context record¶
Every argument spelled ctx -> … above receives:
| Field | Meaning |
|---|---|
system, pkgs, cl |
This system, its nixpkgs, this library instantiated for it |
version |
The .asd's, via fromAsdSystem |
src |
The resolved source — mkLispSource's output, or your src |
package |
The built lispDerivation (not available to the arguments it is built from) |
docs, executable |
The built docs site and CLI, or null |
lispDerivationArgs |
The exact attrset handed to lispDerivation |
generated |
This preset's own output set for the system |
lispDerivationArgs is what makes a hand-rolled delivery cheap: it is by
definition what mkExecutable's args wants, so
ctx.cl.mkExecutable { args = ctx.lispDerivationArgs; … } from
overrideOutputs still spells the package's identity once. generated lets
an override wrap what it replaces —
generated.checks.default.overrideAttrs … — instead of rebuilding it.
Two escape hatches, not one¶
extraOutputs and overrideOutputs take the same shape and differ only in
what they assert:
extraOutputssays this name is new. Colliding with a generated name is an evaluation error, so a preset that grows a new generated check later cannot silently shadow a caller's check of the same name — or be shadowed by it, which of the two happening would otherwise depend on//order.overrideOutputssays this name already exists. Naming something that was never generated is an evaluation error, so a typo is a build failure rather than an inert extra output nobody notices until the thing it was meant to replace ships unmodified.
A single overlay-style final: prev: channel cannot express that intent and
therefore cannot check it. Only packages, checks, apps and devShells
may be named: formatter is one value per system with nothing to merge into,
and overlays is not per-system at all, so both are configured by argument
and naming them here is an error that says so.
The preset also refuses arguments that would create a second source of truth
for something it computes. packageArgs may not set pname, version,
src, meta, lispSystem, lispSystems, lisp, lispDependencies,
lispCheckDependencies or doCheck; docs may not set version;
executable may not set args or lispDerivation. Each rejection names the
dedicated argument to use instead — and for version specifically, the
alternative it prevents is exactly the hardcoded version string the org's
conformance check greps for.
examples/org-preset/ exercises all of this, including the rejections.