How to Use twist.nix to Build Emacs Packages from Source

twist.nix is a Nix library that converts Emacs package declarations into reproducible, source-based Nix derivations by fetching upstream code and compiling it in a sandboxed environment.

The emacs-twist/twist.nix repository provides a Nix-native solution for managing Emacs configurations entirely from source. Unlike traditional Emacs package managers that rely on pre-built archives, twist.nix fetches package source code directly from Git repositories and ELPA archives, then compiles them inside pure Nix derivations. This approach guarantees reproducible builds while giving you complete control over package versions and build parameters.

twist.nix Architecture Overview

Core Entry Point (pkgs/emacs/default.nix)

The library's main interface resides in pkgs/emacs/default.nix. This file accepts a configuration attribute set containing your registries, lock directory, init files, and overrides. It produces an attribute scope containing elispPackages (a map of package derivations), emacsWrapper (the final Emacs executable), and generateLockDir (a script for pinning dependencies).

The entry point orchestrates dependency resolution by recursively walking packageRequires headers from each package's metadata. It automatically excludes built-in Emacs libraries from the dependency graph unless explicitly included in initialLibraries.

Package Builder (pkgs/emacs/build/default.nix)

Individual package compilation happens in pkgs/emacs/build/default.nix. This derivation:

  • Copies source files respecting the files attribute map
  • Runs byte-compilation via emacs --batch -f batch-byte-compile
  • Optionally executes native compilation ahead-of-time when nativeComp && nativeCompileAhead is enabled
  • Generates autoloads using batch-update-autoloads
  • Installs compiled Lisp files to $out/share/emacs/site-lisp
  • Builds Info manuals to a separate info output when documentation exists

Registry Enumeration

The pkgs/emacs/data/inventory/*.nix modules convert abstract registry entries into concrete package definitions. The enumerateConcretePackageSet function processes MELPA recipes, ELPA archives, and Git-based registries to extract source locations (src), Lisp file lists, and metadata.

Step-by-Step: Building Emacs Packages from Source with twist.nix

1. Create the Configuration Expression

Start by creating a Nix expression that imports twist.nix. The repository provides a reference implementation in test/twist.nix. Your configuration must specify:

  • pkgs: The Nixpkgs instance
  • emacsPackage: Which Emacs derivation to use (e.g., pkgs.emacsPgtkGcc)
  • registries: List of package sources
  • lockDir: Directory path for lock files
  • initFiles: Optional list of Emacs init files to parse for package declarations

2. Configure Package Registries

Define registries as a list of attribute sets describing where to find packages. According to the source code in pkgs/emacs/default.nix, each registry requires a type field (such as melpa, elpa, gitmodules, or archive-contents) and a path pointing to the local checkout.

registries = [
  { type = "elpa"; path = inputs.gnu-elpa.outPath + "/elpa-packages"; }
  { name = "melpa"; type = "melpa"; path = inputs.melpa.outPath + "/recipes"; }
  { type = "gitmodules"; name = "emacsmirror"; path = inputs.epkgs.outPath + "/.gitmodules"; }
];

Each entry points to a Git tree supplied via flake inputs. The library reads MELPA recipes, ELPA package directories, or .gitmodules files to construct the package inventory.

3. Override Package Sources

Use inputOverrides to substitute upstream sources or modify package files. This attribute set accepts package names mapped to functions that transform the package configuration.

inputOverrides = {
  tramp = _: _: {
    origin = {
      type = "github";
      owner = "emacs-straight";
      repo = "tramp";
      ref = "master";
    };
  };
  bbdb = _: super: {
    files = builtins.removeAttrs super.files [ "bbdb-notmuch.el" "bbdb-vm.el" ];
  };
};

The first argument provides the final package set, the second provides the previous definition, allowing you to override origin (source) or files (which source files to include).

4. Generate Lock Files

Run the lock generation script to pin exact package revisions:

nix run .#generateLockDir

This executes the script produced by default.nix → generateLockDir, writing flake.lock (Git revisions) and archive.lock (archive-based packages) to your configured lockDir. Commit these files to version control for reproducible builds across machines.

5. Build the Emacs Wrapper

Compile all packages and create the final Emacs executable:

nix build .#emacsWrapper

The resulting derivation contains an emacs binary with all elispPackages bundled in emacsWithPackages style. The wrapper includes compiled Lisp files from the source repositories in its load path.

Complete Configuration Example

Here is a minimal working configuration based on test/twist.nix:

{ pkgs, emacsPackage, inputs, initialLibraries ? null }:
{
  inherit pkgs;
  inherit emacsPackage;
  initFiles = [ ./init.el ];
  lockDir = ./lock;

  registries = [
    { type = "elpa"; path = inputs.gnu-elpa.outPath + "/elpa-packages"; }
    { name = "melpa"; type = "melpa"; path = inputs.melpa.outPath + "/recipes"; }
    { type = "elpa"; path = inputs.nongnu.outPath + "/elpa-packages"; }
    { name = "gnu";
      type = "archive-contents";
      path = inputs.gnu-elpa-archive.outPath;
      base-url = "https://raw.githubusercontent.com/d12frosted/elpa-mirror/master/gnu/"; }
    { name = "emacsmirror";
      type = "gitmodules";
      path = inputs.epkgs.outPath + "/.gitmodules"; }
  ];

  inputOverrides = {
    bbdb = _: super: {
      files = builtins.removeAttrs super.files [ "bbdb-notmuch.el" "bbdb-vm.el" "bbdb-vm-aux.el" ];
    };
    tramp = _: _: {
      origin = { type = "github"; owner = "emacs-straight"; repo = "tramp"; ref = "master"; };
    };
  };

  postCommandOnGeneratingLockDir = ''
    echo "Lock files generated in ${./lock}"
  '';
}

This configuration imports packages from GNU ELPA, MELPA, NonGNU ELPA, and Emacsmirror, while overriding specific package sources and pruning unnecessary files from the BBDB package.

Integration with Home Manager

Deploy your twist.nix configuration via Home Manager using the emacsWrapper output:

{ pkgs, ... }:

let
  twist = import ./twist.nix {
    inherit pkgs;
    emacsPackage = pkgs.emacsPgtkGcc;
    lockDir = ./my-lock;
    registries = [
      { type = "melpa"; path = inputs.melpa.outPath + "/recipes"; }
      { type = "elpa"; path = inputs.gnu-elpa.outPath + "/elpa-packages"; }
    ];
    initFiles = [ ./init.el ];
  };
in {
  home.packages = [ twist.emacsWrapper ];
}

The emacsWrapper provides a ready-to-use Emacs installation that includes all compiled packages from your lock directory.

Summary

  • twist.nix builds Emacs packages from source using pure Nix derivations defined in pkgs/emacs/default.nix and pkgs/emacs/build/default.nix.
  • The library resolves dependencies by parsing packageRequires headers and supports multiple registry types including MELPA, ELPA, and Git modules.
  • Configuration requires defining registries, lockDir, and optionally initFiles and inputOverrides.
  • The generateLockDir script creates reproducible lock files capturing exact package revisions.
  • Final output emacsWrapper bundles all compiled packages into a single Emacs executable suitable for Home Manager or NixOS deployment.

Frequently Asked Questions

What package registries does twist.nix support?

twist.nix supports MELPA (type = "melpa"), ELPA (type = "elpa"), Emacsmirror via gitmodules (type = "gitmodules"), and archive-contents (type = "archive-contents"). The registry types are implemented in pkgs/emacs/data/inventory/*.nix, which parse recipe files or package directories to extract source locations and metadata.

How does twist.nix handle package dependencies?

The library computes allDependencies by recursively walking the packageRequires fields derived from each package's Package-Requires header. It converts these requirements into Nix attribute references and automatically excludes built-in Emacs libraries unless explicitly listed in initialLibraries. The dependency graph is resolved during evaluation in pkgs/emacs/default.nix.

Can I use twist.nix with native compilation?

Yes. When you provide an Emacs package with native compilation support (such as pkgs.emacsPgtkGcc), the builder in pkgs/emacs/build/default.nix checks the nativeComp && nativeCompileAhead condition. If true, it compiles Emacs Lisp files to native code ahead of time using the appropriate Emacs batch commands.

Where does twist.nix install compiled Emacs Lisp files?

The builder installs compiled files to $out/share/emacs/site-lisp within each package's derivation. The final emacsWrapper aggregates these paths into the Emacs load path. Info manuals are stored in a separate info output when available in the package source.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →