Skip to main content

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 selects a base package per variant — nixpkgs’ default emacs attribute for the default mainline, emacs-overlay’s emacs-git/emacs-unstable/emacs-igc for the git-based variants, and nixpkgs’ emacs-macport alias for macport — and calls .override { ... } with every upstream build flag exposed as a file-level argument. The nix-community/emacs-overlay is added in flake.nix/devenv.nix to supply the git-based variants. Supported:
  • Source variants: mainline (nixpkgs’ default emacs attribute, currently the Emacs 30 release, default), git (emacs-overlay’s emacs-git, current master), unstable (emacs-unstable, latest tagged release), macport (emacs-macport → nixpkgs emacs30-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, dbus, selinux, gpm, ALSA, ACL, mailutils, systemd, GLib networking.
  • Darwin patches: optional system-appearance, round-undecorated-frame, and adjust-ns-init-colors (master/32+ only) 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 upstream nixpkgs’ make-emacs.nix (and the explicit args emacs-overlay passes to its prebuilt attrs). 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(.override { ... }) — and variant = "git"/"unstable"/"igc" the store paths of the matching emacs-overlay attrs — so the Hydra, nix-community.cachix.org, and project jylhis binary caches hit and nothing recompiles from source. Only custom rev pins 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 {
    mainline = (import ./emacs.nix {}).outPath == pkgs.emacs.outPath;
    git      = (import ./emacs.nix { variant = "git"; }).outPath == pkgs.emacs-git.outPath;
    unstable = (import ./emacs.nix { variant = "unstable"; }).outPath == pkgs.emacs-unstable.outPath;
    igc      = (import ./emacs.nix { variant = "igc"; }).outPath == pkgs.emacs-igc.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.

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 — overlay attribute only; intentionally not exposed as a flake packages output
The full fork-backed package set is still experimental because the Meson fork has previously crashed while byte-compiling downstream Emacs packages, so it is not advertised as a buildable flake output. It remains reachable through the Home Manager services.jotain.emacsBackend = "jylhis" option, which consumes the jylhisEmacsPackages overlay attribute directly.

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

OptionDefaultDescription
variant"mainline"Emacs source variant — mainline / git / unstable / macport / igc
withTreeSitterGrammarstrue(default.nix) include all tree-sitter grammars
noGuifalseTerminal only — --without-x --without-ns
withPgtkfalsePure GTK (Wayland) — --with-pgtk
withGTK3withPgtk && !noGuiGTK3 toolkit — --with-x-toolkit=gtk3
withNativeCompilationautolibgccjit AOT compilation
withTreeSittertrueBuilt-in tree-sitter support
withSystemdLinux--with-systemd (journal support)
withSystemAppearancePatchfalse(Darwin) add ns-system-appearance hooks
withRoundUndecoratedFramePatchfalse(Darwin) rounded borderless frames
withFixWindowRolePatchfalse(Darwin, Emacs 30 only) fix NSAccessibility role for tiling WMs
rev / hashnullPin 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. 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 — the overlay-pinned revision is a binary-cache hit.

Git Variants

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.

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";

nix-on-droid

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.

Overriding nixpkgs

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.
Last modified on July 9, 2026