Nix Build System
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.
Two-Layer Architecture
emacs.nix
The core build expression. It takes pkgs.emacs-git (or pkgs.emacs-macport for the macport variant) from nix-community/emacs-overlay and calls .override { ... } with every upstream build flag exposed as a file-level argument. The overlay is added in flake.nix/devenv.nix because Emacs 31 is not yet shipped in nixpkgs.
Supported:
- Source variants:
mainline (emacs-overlay’s emacs-git = current master, default), git (alias for mainline), unstable (emacs-unstable, latest tagged release), macport (emacs-macport, jdtsmith/emacs-mac fork), igc (emacs-igc, the feature/igc3 Memory Pool System incremental GC branch).
- GUI toolkits: GTK3, pgtk (pure GTK for Wayland), NS (Cocoa/NeXTstep on macOS), Motif, Athena, X11, or no GUI at all (
noGui = true).
- Compilation: native compilation (libgccjit AOT, default when the build platform can execute the host), compressed install, C sources for
find-function-C-source, srcRepo (run autoreconf on git-based sources).
- Image formats: WebP (default), Cairo (X11 default), optionally ImageMagick.
- Libraries: tree-sitter, SQLite3, Jansson (off — Emacs 30+ ships JSON), dbus, selinux, gpm, ALSA, ACL, mailutils, systemd, GLib networking.
- Darwin patches: optional
system-appearance, round-undecorated-frame, and fix-window-role patches fetched from nix-giant/nix-darwin-emacs, applied via overrideAttrs.
Cache-parity invariant
emacs.nix is written so that every argument default matches the corresponding default in emacs-overlay’s emacs-git derivation (which mirrors upstream nixpkgs’ make-emacs.nix). As long as that holds,
import ./emacs.nix {} # mainline
import ./emacs.nix { noGui = true; } # any standard override
produces the exact store path of pkgs.emacs-git(.override { ... }), so the nix-community.cachix.org (and project jylhis) binary caches hit and nothing recompiles from source. Only the unstable / igc / macport variants and the Darwin patch flags are expected to 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.nixpkgs.locked;
ov = lock.nodes.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) ]; };
in (import ./emacs.nix {}).outPath == pkgs.emacs-git.outPath'
emacs-jylhis.nix
The fork build expression. It builds the pinned github:jylhis/emacs Meson branch and stays separate from emacs.nix so the default cache-parity path remains unchanged. In the flake, the fork source comes from the non-flake input jylhis-emacs; standalone nix-build emacs-jylhis.nix falls back to the pinned rev / hash arguments in the file.
The overlay exposes both bare and full fork-backed packages:
jylhisEmacs / packages.${system}.jylhis-emacs
jylhisEmacsPackages / packages.${system}.jylhis-emacs-packages
The full fork-backed package set is still experimental because the Meson fork has previously crashed while byte-compiling downstream Emacs packages. Use just build-jylhis-full to exercise that path explicitly.
default.nix
The distribution layer. It imports emacs.nix (forwarding every argument it does not consume itself) and, when withTreeSitterGrammars is true (default), wraps the result with emacsPackagesFor emacs |> withPackages (epkgs: [ epkgs.treesit-grammars.with-all-grammars ]). The resulting Emacs loads all ~275 grammars out of the box; early-init.el wires them in via TREE_SITTER_DIR / treesit-extra-load-path.
Key Build Options
| Option | Default | Description |
|---|
variant | "mainline" | Emacs source variant — mainline / git / unstable / macport / igc |
withTreeSitterGrammars | true | (default.nix) include all tree-sitter grammars |
noGui | false | Terminal only — --without-x --without-ns |
withPgtk | false | Pure GTK (Wayland) — --with-pgtk |
withGTK3 | withPgtk && !noGui | GTK3 toolkit — --with-x-toolkit=gtk3 |
withNativeCompilation | auto | libgccjit AOT compilation |
withTreeSitter | true | Built-in tree-sitter support |
withSystemd | Linux | --with-systemd (journal support) |
withSystemAppearancePatch | false | (Darwin) add ns-system-appearance hooks |
withRoundUndecoratedFramePatch | false | (Darwin) rounded borderless frames |
withFixWindowRolePatch | false | (Darwin, Emacs 30 only) fix NSAccessibility role for tiling WMs |
rev / hash | null | Pin a specific commit for git / unstable / igc / macport variants |
See emacs.nix for the complete argument list and defaults.
IGC Variant
The igc variant builds Emacs’s feature/igc3 branch, which replaces the default mark-and-sweep garbage collector with the Memory Pool System. emacs.nix adds --with-mps=yes to configureFlags and pkgs.mps to buildInputs when variant = "igc", so the only manual step is providing the source hash on first build.
Git Variants
For git, unstable, and igc, emacs.nix fetches from https://git.savannah.gnu.org/git/emacs.git via fetchgit. The first build reports the correct hash; re-run with --argstr hash "sha256-..." (or edit the gitMeta attrset). postPatch substitutes the recorded 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, git builds automatically get --enable-check-lisp-object-type added to avoid segfaults.
Consumer Flake Backend Selection
Home Manager, NixOS, and nix-darwin consumers choose the fork with:
services.jotain.emacsBackend = "jylhis";
The default remains "mainline". Use "custom" only when also setting services.jotain.package.
Downstream flakes can replace the pinned fork input:
inputs.jotain.url = "github:jylhis/jotain";
inputs.my-emacs-fork = {
url = "github:jylhis/emacs/my-branch-or-rev";
flake = false;
};
inputs.jotain.inputs.jylhis-emacs.follows = "my-emacs-fork";
Last modified on May 25, 2026