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
filesattribute map - Runs byte-compilation via
emacs --batch -f batch-byte-compile - Optionally executes native compilation ahead-of-time when
nativeComp && nativeCompileAheadis enabled - Generates autoloads using
batch-update-autoloads - Installs compiled Lisp files to
$out/share/emacs/site-lisp - Builds Info manuals to a separate
infooutput 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 instanceemacsPackage: Which Emacs derivation to use (e.g.,pkgs.emacsPgtkGcc)registries: List of package sourceslockDir: Directory path for lock filesinitFiles: 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.nixandpkgs/emacs/build/default.nix. - The library resolves dependencies by parsing
packageRequiresheaders and supports multiple registry types including MELPA, ELPA, and Git modules. - Configuration requires defining
registries,lockDir, and optionallyinitFilesandinputOverrides. - The
generateLockDirscript creates reproducible lock files capturing exact package revisions. - Final output
emacsWrapperbundles 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →