Jotain uses Nix for reproducible Emacs builds with fine-grained control over compile options. The source of nixpkgs is the revision pinned in flake.lock by default; both default.nix and emacs.nix read it directly via fetchTarball, so non-flake nix-build consumers get the same pin. Pass --arg pkgs '<nixpkgs>' or override pkgs to use a different one.
The core build expression. It selects a base package per variant — emacs-overlay’s emacs-git/emacs-unstable/emacs-igc for the git-based variants (unstable is the default), nixpkgs’ default emacs attribute for mainline (the cache-parity canary) — and calls .override { ... } with the supported build flags exposed as file-level arguments. The build matrix is four shipped builds on {x86_64, aarch64} × {Linux, Darwin}: pgtk/Wayland GUI or terminal-only on Linux, patched NS GUI or terminal-only on Darwin; emacs.nix asserts every other GUI configuration away. pgtk honors each backend’s advertised scale (Wayland fractional scale) so a fixed point size tracks the system. When withPgtk = true (the Linux GUI default) the base selection picks the prebuilt *-pgtk sibling (emacs-unstable-pgtk etc.) directly instead of overriding withPgtk on the non-pgtk base, because the two build identical content under different derivation names and the sibling is the one the cache holds. The nix-community/emacs-overlay that supplies the git-based variants is a flake input composed into every overlay consumer in flake.nix; emacs.nix and overlay.nix re-apply it from flake.lock when imported standalone.
Supported:
unstable (emacs-unstable, the Emacs 31.1 release branch; the default), git (emacs-overlay’s emacs-git, current master), igc (emacs-igc, the feature/igc3 Memory Pool System incremental GC branch), mainline (nixpkgs’ default emacs attribute; parity canary, not a flake output).
noGui = true).
find-function-C-source, srcRepo (run autoreconf on git-based sources), opt-in useCcache.
emacs-unstable-pgtk.
system-appearance, round-undecorated-frame, and fix-ns-x-colors applied via overrideAttrs — on by default for the NS GUI, which makes every Darwin GUI build a from-source build. The 31/30 branch patch sets are fetched from nix-giant/nix-darwin-emacs; the master/32+ set (the git/igc variants) from d12frosted/homebrew-emacs-plus, since nix-giant dropped its unstable patch branch in 2026-08.
emacs.nix is written so that every argument default matches the corresponding default in upstream nixpkgs’ make-emacs.nix (and the explicit args emacs-overlay passes to its prebuilt attrs). As long as that holds,
import ./emacs.nix {} # unstable (bare-file default)
import ./emacs.nix { variant = "mainline"; } # nixpkgs' emacs, the parity canary
import ./emacs.nix { noGui = true; } # any standard override
produces the exact store path of the matching base attr. On Linux withPgtk is on by default, so each GUI build maps to the prebuilt *-pgtk sibling (emacs-unstable-pgtk, emacs-git-pgtk, emacs-igc-pgtk, or nixpkgs’ emacs-pgtk for mainline); the terminal-only noGui builds map to their own matching attrs. So the nix-community.cachix.org (git/unstable/igc, including the shipped unstable default), Hydra (mainline), and project jylhis binary caches hit and nothing recompiles from source. Only custom rev pins and the Darwin patch flags diverge; those paths run through overrideAttrs and intentionally bust the cache.
Verify after any change to defaults:
nix-instantiate --eval --strict -E '
let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
n = lock.nodes.${lock.nodes.root.inputs.nixpkgs}.locked;
ov = lock.nodes.${lock.nodes.root.inputs.emacs-overlay}.locked;
nixpkgs = fetchTarball {
url = "https://github.com/${n.owner}/${n.repo}/archive/${n.rev}.tar.gz";
sha256 = n.narHash;
};
overlay = fetchTarball {
url = "https://github.com/${ov.owner}/${ov.repo}/archive/${ov.rev}.tar.gz";
sha256 = ov.narHash;
};
pkgs = import nixpkgs { overlays = [ (import overlay) (import ./overlay.nix) ]; };
in {
# On this x86_64-linux eval every GUI build maps to a *-pgtk sibling
# (pgtk is the Linux GUI default in emacs.nix itself); non-pgtk Linux
# GUIs are asserted away and cannot be evaluated.
default = pkgs.jotainEmacs.outPath == pkgs.emacs-unstable-pgtk.outPath;
bare-default = (import ./emacs.nix {}).outPath == pkgs.emacs-unstable-pgtk.outPath;
mainline-pgtk = (import ./emacs.nix { variant = "mainline"; }).outPath == pkgs.emacs-pgtk.outPath;
git-pgtk = (import ./emacs.nix { variant = "git"; }).outPath == pkgs.emacs-git-pgtk.outPath;
igc-pgtk = (import ./emacs.nix { variant = "igc"; }).outPath == pkgs.emacs-igc-pgtk.outPath;
}'
The override arg set is filtered through lib.intersectAttrs (lib.functionArgs basePackage.override), so only the flags the base make-emacs.nix actually defines are forwarded. This keeps the build evaluating when a downstream flake overrides nixpkgs with an older release (24.05+): arguments that newer make-emacs.nix versions added are dropped rather than throwing "called with unexpected argument". On the pinned unstable every argument is accepted, so the intersection is a no-op and cache parity is unaffected.
The non-flake entry point — a thin flake-compat wrapper, not a distribution layer. It reads the pinned flake-compat rev from flake.lock, evaluates flake.nix, and promotes the current system’s packages to the top level: plain nix-build builds the full distribution (jotainEmacsPackages), nix-build -A emacs the bare Emacs. Variant builds still target emacs.nix directly (e.g. nix-build emacs.nix --arg withPgtk true).
Grammar bundling lives in nix/mk-overlay.nix, not in default.nix: the distribution’s withPackages set includes epkgs.treesit-grammars.with-all-grammars (~275 grammars — a linkFarm over per-grammar derivations, so the full set costs closure size, never build time). Discovery is nixpkgs’ own site-start.el, which sets treesit-extra-load-path to the bundled grammar directory — early-init.el deliberately does no TREE_SITTER_DIR handling (see its comment), and init-prog.el propagates treesit-extra-load-path to async native-comp workers, which run without site files.
legacyPackages.<system>.emacs-packages (nix/emacs-package-set.nix) exposes every Emacs package the configuration bundles — the lisp/ use-package scan with :ensure aliases resolved, the Nix-provided extras (nix/nix-provided-packages.nix), and the full tree-sitter grammar set — each individually buildable:
nix build .#emacs-packages.magit nix build .#emacs-packages.treesit-grammars
Keys are the use-package head names, so dired-async holds the async derivation its :ensure async resolves to, and ghostel is the Elisp-only rebuild from nix/extra-packages.nix. The set lives in legacyPackages so nix flake check never builds it; the eval-only emacs-packages-eval check verifies every declared name resolves to a derivation. The packages mirror packages.default (the GUI-base scope), not emacs-nox.
| Option | Default | Description |
|---|---|---|
variant | "unstable" | Emacs source variant — unstable / git / igc / mainline |
noGui | false | Terminal only — --without-x --without-ns |
withPgtk | isLinux && !noGui | Pure GTK (Wayland) — --with-pgtk; the Linux GUI. Selects the prebuilt *-pgtk sibling for cache parity |
withNS | isDarwin && !noGui | Cocoa/NeXTstep — the macOS GUI |
withNativeCompilation | auto | libgccjit AOT compilation |
withTreeSitter | true | Built-in tree-sitter support |
withSystemd | Linux | --with-systemd (journal support) |
withSystemAppearancePatch | withNS | (Darwin) add ns-system-appearance hooks — on by default for the GUI |
withRoundUndecoratedFramePatch | withNS | (Darwin) rounded borderless frames — on by default for the GUI |
withFixNsXColorsPatch | withNS | (Darwin) refresh x-colors from ns-list-colors at runtime, so the full palette is available instead of the ~62 headless-dump colors — on by default for the GUI |
rev / hash | null | Pin a specific commit for git / unstable / igc variants |
useCcache | false | Build with pkgs.ccacheStdenv (CCACHE_DIR wired via extraConfig; ccacheDir argument, default /var/cache/ccache). Only useful for builds already off the cache-parity path (custom rev, the Darwin GUI, or igc on Darwin); needs a machine-level extra-sandbox-paths exception first |
cpuTune | null | CPU-tuned perf build: appends -O3 -march=<tune> -mtune=<tune> to NIX_CFLAGS_COMPILE (just build-perf passes icelake-client). Off every binary cache by design; only for builds already off the cache-parity path or when a from-source build is accepted |
See emacs.nix for the complete argument list and defaults.
The igc variant builds Emacs’s feature/igc3 branch, which replaces the default mark-and-sweep garbage collector with the Memory Pool System. The base package is emacs-overlay’s emacs-igc, which already carries --with-mps=yes and the mps build input, so no manual steps are needed on Linux — the overlay-pinned revision is a binary-cache hit there. On Darwin, nix-community.cachix.org has no prebuilt emacs-igc, so just build-igc compiles it from source even at the default revision; just build-igc-ccache (useCcache = true) makes repeat local rebuilds on that platform cheaper once ccache is set up (see the useCcache doc comment in emacs.nix).
The git, unstable, and igc variants build the revision pinned inside emacs-overlay (updated daily upstream, advanced here by just update) and are binary-cache hits from nix-community.cachix.org. To pin a different commit, pass --argstr rev "..." — emacs.nix then fetches it from https://git.savannah.gnu.org/git/emacs.git via fetchgit; the first build reports the correct hash to pass back via --argstr hash "sha256-...". In that case postPatch substitutes the pinned revision into lisp/loadup.el so emacs-repository-get-version returns the expected value without a .git directory in the build tree. On aarch64-linux, the overlay’s bases include --enable-check-lisp-object-type to avoid segfaults.
nixOnDroidModules.default (from module-nix-on-droid.nix) installs Jotain on Android via nix-on-droid. Because Android runs headless under proot, the module is a trimmed cousin of module-system.nix: it pkgs.extends the overlay and adds a terminal-only build (jotainEmacsPackagesNoGui, a noGui = true Emacs) plus an emacsclient EDITOR/VISUAL wrapper to environment.packages and environment.sessionVariables. There is no systemd service, launchd agent, fonts.packages, or GUI frame.
flake.nix exposes an example nixOnDroidConfigurations.default (aarch64-linux). It activates only on-device or under aarch64 emulation, so nix flake check does not realise it (CI is x86_64); the module itself is eval-checked on x86_64 via nix-on-droid-module-eval in nix/checks.nix.
A downstream flake can follow a different nixpkgs (release branches 24.05+ through unstable):
inputs.jotain.inputs.nixpkgs.follows = "nixpkgs";
The version-gated override split (see Cache-parity invariant) keeps the modules evaluating and building on older releases. The caveat is Emacs version: 24.05’s pkgs.emacs is Emacs 29 while Jotain’s Elisp targets 30/31, so the build succeeds but runtime behaviour is only guaranteed on the pinned unstable.