Content
#+title: Nelix — Elisp DSL package manager backed by the Nix store
#+author: zawatton
#+date: 2026-04-27
[[https://github.com/zawatton/nelix/actions/workflows/test.yml][file:https://github.com/zawatton/nelix/actions/workflows/test.yml/badge.svg]]
[[https://github.com/zawatton/nelix/actions/workflows/smoke.yml][file:https://github.com/zawatton/nelix/actions/workflows/smoke.yml/badge.svg]]
* What is Nelix?
*Nelix* is a package manager you configure in /Emacs Lisp/, backed by
the Nix store. It began as =anvil-pkg=; in 2026-06 it was fully
renamed to =nelix= — the old =anvil-pkg-*= feature and symbol names
were removed (no compatibility aliases). The public =pkg-*= API and
the =nelix-*= entry points are the supported surface.
#+begin_quote
The same idea as GNU Guix (Scheme over the Nix store) — but in Emacs
Lisp, integrated with the anvil + NeLisp tool ecosystem so that AI
agents can install anything by writing one Lisp form.
#+end_quote
Minimal Emacs setup:
#+begin_src elisp
(add-to-list 'load-path "/path/to/nelix")
(require 'nelix)
(require 'nelix-dsl)
(nelix-install "ripgrep")
#+end_src
* Status
*Phase 1+2+3+4-A+4-B+4-C+4-D+4-E+4-F+4-G+4-H SHIPPED + REAL-NIX VERIFIED; Phase 5 compat groundwork local-verified; Phase 6 (uninstall / upgrade / info) + Phase 7 (=pkg-pin= / =pkg-doctor= + node / haskell build systems) SHIPPED* (2026-06-13) —
Full DSL stack on top of the Nix store with multiple fetcher /
build-system support, an =emacs-package= backend that round-trips
Elisp libraries through =trivialBuild= / =melpaBuild= with optional
=:native-comp=, an opt-in =:async= install path, automatic
=:depends-on= derivation from =Package-Requires= for github / tarball /
git sources with cross-session caching in =nelix-state=, profile
generation rollback (=pkg-list-generations= / =pkg-rollback= /
=pkg-rollback-package= / =pkg-history=), portable
=nix profile install= dispatch, MELPA recipe auto-synthesis via =postUnpack= with optional
upstream MELPA recipe lookup, a one-shot =async-installer=
importer that auto-populates =:depends-on= from local clones, and
*private repo support* via env-var credentials
(=GITHUB_TOKEN= / =GITLAB_TOKEN= / =CODEBERG_TOKEN=) wired through
the L18 pre-fetch, git HTTPS clone, and =nix --option
extra-access-tokens= paths.
- ERT 221/221 PASS (mocked, runs without a nix binary).
- /Smoke tests/ run on every push:
- =make nix-check= runs in =smoke.yml= and verifies the repository
flake itself. The flake provides a pinned =nix develop=
environment and a Nix-packaged =make check= derivation.
- =make smoke-render= runs in =test.yml= and renders all examples
discovered under =examples/*.el= without requiring a =nix= binary,
then checks that each generated =flake.nix= exposes the expected
flake output shape. It fails if no examples are present or if an
example registers no packages; this is the quick guard against
stale example DSL.
- =make smoke-pairs-check= validates the no-Nix smoke metadata:
eval / build pair lists must be non-empty, use
=example-file:nix-attr= form, and reference existing example
files whose rendered flakes expose the requested attr.
- =make smoke-eval= renders four examples
(=stdenv-hello=, =rust-ripgrep=, =python-black=, =go-hugo=)
and runs =nix flake check --no-build= on each
(=smoke.yml=, Determinate Nix on =ubuntu-latest=; locally
verified with Nix 2.34.7 on 2026-06-08); an empty, malformed, or
stale smoke-eval pair list is treated as an error before invoking
Nix.
- =make smoke-build= (local-only) actually =nix build= s the
cheap subset (=gnu-hello= + =black=) end-to-end; locally verified
with Nix 2.34.7 on 2026-06-08. An empty, malformed, or stale
smoke-build pair list is treated as an error before invoking Nix.
- =make smoke-nelisp= (local-only) loads
=nelix-compat.el= under a real standalone NeLisp binary and
checks the narrow pure fallback surface that the current CLI can
exercise. When local NeLisp package sources are discoverable, it
also loads the real =nelisp-json.el= package and verifies compat
JSON parsing through that backend. When the local
=nelisp-emacs-compat= dependency sources are discoverable, it loads
those too and verifies compat buffer create / live / empty-read
dispatch through the real =nelisp-ec= backend. It also forces the
no-backend NeLisp runtime branch and verifies async / text HTTP /
binary HTTP unsupported-runtime signals under the real standalone
evaluator. It also verifies explicit hook dispatch for sync
process, async process, env lookup, PATH lookup, text HTTP, and
binary HTTP under that same standalone evaluator. Native process /
HTTP source probing is kept in =make smoke-nelisp-capabilities=.
- =make smoke-nelisp-capabilities= (local-only) prints the current
standalone NeLisp capability snapshot used to explain native
backend gaps. With local NeLisp sources discoverable, it loads
=nelisp-stdlib-eval-special.el= first, the local ERT shim, then
the native process / network / HTTP package sources. Locally this
reports present =ert= / =ert-shim= / =cl-letf= / =cl-defun=,
executable process and text HTTP lower primitives, loaded
=nelisp-call-process= / =nelisp-make-process= /
=nelisp-http-get= / =nelisp-http-fetch= definitions, and
=:native-async-lower-primitive t= /
=:native-text-http-lower-primitive t=.
- =make smoke-nelisp-suite-readiness= (local-only) audits whether
the standalone image is ready to run the full nelix ERT suite.
The current local image reports =:suite-ready t= with no blockers.
- =make smoke-nelisp-suite-loadability= (local-only) loads the
runtime sources and each ERT file in a fresh standalone NeLisp
process with the local ERT shim in registration-only mode. This
proves the current CLI can parse and register every test file in
NeLisp runtime mode, and asserts the registered total is 311 tests.
- =make smoke-nelisp-suite= (local-only) is the final standalone
suite command. It runs the standalone-executable ERT subset under
NeLisp and currently passes 130 tests.
- =make smoke-nelisp-local= (local-only) is the aggregate NeLisp
gate. It runs the compat smoke, public Nelix smoke, capability
audit, suite readiness audit, suite loadability probe, and the
standalone-executable NeLisp ERT suite.
- Real source / cargo / vendor hashes ship in the four smoke-built
recipes, so those examples need no =sha256-PLACEHOLDER= swap before
install. The remaining examples are shape / private-repo examples
and still mark user-specific hashes with placeholders.
- Generated =flake.nix= validated by Nix as well-formed
on every push (matrix: Emacs 30.1 × Linux).
Public Elisp API uses the short =pkg-= prefix:
- =pkg-install= / =pkg-search= / =pkg-list= — Phase 1
- =pkg-define= macro — Phase 2
- =github-fetch= / =git-fetch= sources, =rust= / =python= / =go=
build systems — Phase 3
- =emacs-package= build-system + post-install =load-path= augment +
=:require= keyword — Phase 4-A
- =:format= ("trivial" / "melpa") + =:native-comp= toggle on
=emacs-package=, =:async= keyword on =pkg-install= with
=:on-success= / =:on-error= callbacks, and
=nelix-import-async-installer= migration utility — Phase 4-B
- =:no-auto-deps= keyword on =pkg-install= (opt-out of pre-fetch),
=pkg-list-generations= / =pkg-rollback= / =pkg-history= — Phase 4-C
- =nelix-compat-make-process-async= /
=nelix-compat-http-get= portability primitives (Emacs path
shipped; Phase 5 NeLisp hooks / auto-detect groundwork landed) —
Phase 4-C / Phase 5 groundwork
- =nelix-state= namespaced KV with TTL (deps cache + Nix-version
cache + generations mirror persist across Emacs sessions),
=pkg-clear-cache= scope arg, =:melpa-synth= / =:melpa-recipe= /
=:melpa-files= keywords on =emacs-package=, L18 pre-fetch
extended to =url-fetch= (tarball header scrape) and =git-fetch=
(shallow clone scrape), =pkg-rollback-package= per-package
rollback — Phase 4-D
- =nelix-emacs-fetch-melpa-recipe= public helper +
=nelix-emacs-melpa-upstream-fetch= defcustom for opt-in
upstream MELPA recipe lookup (canonical recipe wins over local
synth when the defcustom is non-nil and a recipe exists at
=melpa/melpa@master/recipes/<pname>=); upgraded =:melpa-files=
default from =("*.el")= to MELPA's full
=package-build-default-files-spec= so subdir / .el.in / .info
layouts work without manual configuration — Phase 4-E
- =pkg-install= multi-package dispatch: =(pkg-install '(magit dash
transient))= or =(pkg-install '(magit "ripgrep"))= renders the
flake once and invokes =nix profile install= once with all
flakerefs (atomic across the bulk via Nix's profile transaction).
Mixed registry symbols + nixpkgs strings supported. Async
callbacks receive =:names NAMES= instead of =:name NAME=.
=:require= rejected with list NAME (ambiguous) — Phase 4-F
- =pkg-uninstall= (=nix profile remove=), =pkg-upgrade-plan=
(read-only target / pin / missing report before upgrading),
=pkg-upgrade= (=nix profile upgrade= — all via the portable =.*=
matcher or a single element), and =pkg-info= (read-only merged
installed-profile + =nix search= metadata) — Phase 6
- =pkg-pin= / =pkg-unpin= / =pkg-list-pins= (hold packages back from a
bulk =pkg-upgrade=, pins persisted in =nelix-state=; a pinned name
passed explicitly to =pkg-upgrade= errors until unpinned),
=pkg-doctor= (read-only environment health report: nix version,
profile dir, installed count, anvil-server, state file), and the
=node= (=pkgs.buildNpmPackage=, requires =:npm-deps-hash=) + =haskell=
(=pkgs.haskellPackages.mkDerivation=) build systems — Phase 7
- =nelix-manifest.el= desired-state workflow:
=nelix-manifest-load=, =nelix-validate=, =nelix-apply=, =nelix-audit=,
=nelix-prune-plan=, =nelix-sync=, =nelix-lock-read=,
=nelix-lock-write=, =nelix-lock-validate=, =nelix-lock-diff=,
=nelix-lock-check=, and manifest-aware =nelix-upgrade-plan=. This
is the first Nix/Guix-like manifest
layer for managing Emacs and Linux package lists from one file.
Lock files are version 2 and record combined manifest/import digests,
per-file digest rows, backend/system metadata, and resolved package
rows so generated import drift is detected by =nelix-lock-check= and
=nelix-audit=.
- =nelix-backend.el= / =nelix-store.el= / =nelix-registry.el= native
backend: backend capabilities and policy selection, Nix-free
=nelix-native-audit=, native store/profile generations, staged profile
activation, rollback, profile prune/remove, and local or packaged registry
list/search/show with fixture recipes. This is the portable base for
Windows/macOS/Nix-free operation.
- =nelix-fetch.el= / =nelix-builder.el= native install path:
hash-verified local/file/HTTP/Git source fetch, GitHub release URL
construction, ELPA archive URL resolution, exact Git rev archive
export, deterministic local directory hashing, tar/zip extraction,
local file/tree copy install, executable bit handling,
POSIX shell shim rendering, Windows =.cmd= shim rendering, native
store metadata writing, and profile generation creation for simple
=:install (:type unpack ...)= and =:install (:type copy ...)= recipes, plus
=:install (:type emacs-lisp ...)= recipes. Emacs Lisp native installs
record profile =load-path= metadata and can be activated with
=nelix-profile-activate-emacs=. Runtime installs record
=:runtime-bins= and can be activated with
=nelix-profile-activate-runtime=, which regenerates profile-local
shims under =active/bin= and emits =path.sh= on POSIX systems or
=path.cmd= on Windows.
- Manifest backend dispatch now lets =nelix-apply= and =nelix-audit=
choose a backend from policy. The default keeps Nix when available
and falls back to =nelix-native= when Nix is absent. Native
=nelix-sync :prune t= now removes unmanaged entries by creating a
new profile generation; =nelix-store-gc= only deletes store paths
unreachable from every profile generation, preserving rollback.
- Native =nelix-backend-upgrade-plan= compares current native profile
entries against local registry recipes, reporting upgrade
candidates, pins, missing direct targets, missing registry recipes,
and up-to-date entries without mutating profiles.
- =bin/nelix= and =scripts/nelix-cli.el= provide the initial command
line wrapper for manifest operations: =validate=, =apply=, =audit=, =sync
[--prune]=, =prune-plan=, =lock=, =lock validate=, =lock diff=,
=lock migrate=, =upgrade-plan=, =outdated=, =upgrade=, =list=,
=rollback=, and =doctor=.
=--json= emits
normalized JSON for agent use. The wrapper supports Emacs batch mode
and standalone NeLisp direct mode via =--runtime nelisp= or
=NELIX_RUNTIME=nelisp=. =validate= is read-only and avoids profile
process/Nix access, so it is the first standalone NeLisp check for
generated manifests. Manifest =audit=, =plan=, =upgrade-plan=, and
=list= use the AOT cache fast lane by default under =--runtime nelisp=;
run =nelix aot-cache MANIFEST= to prebuild the cache, or set
=NELIX_NELISP_AOT=0= to debug the slower direct standalone path.
=apply --locked= and =sync --locked=
refuse mutation unless the lock file matches the manifest and every
imported file, and the lock package rows match the selected
backend/profile/system and current manifest targets. Missing
installs in locked mode use the lock row =:target= values. Native
locked installs reconstruct a transient recipe from the lock row
source/install/dependency metadata and do not consult the mutable
registry after lock generation. Mutating commands include the
target profile path in their result.
Manifest-wide =nelix upgrade
MANIFEST= now mutates the selected backend by applying the
read-only =nelix-outdated= rows; pinned, missing, current, and
unsupported backend rows remain report-only.
- =nelix-outdated= and =nelix outdated [MANIFEST|NAME] [--backend
BACKEND]= aggregate read-only upgrade candidates across available
upgrade-plan backends. Unsupported but available OS package
backends, such as =apt= before a backend-specific planner exists, are
reported under =:skipped= instead of failing the whole report.
- =nelix-substitute.el= adds native substitute metadata and
materialization: descriptors require name/version/system/source/hash,
can be written under the substitute metadata root, listed, read back,
verified against local payload fixtures, and installed into the
native store/profile. Portable tar/zip payloads unpack through the
native builder, while true NAR payloads restore through
=nix-store --restore=. It also includes a precomputed Nix substitute
bridge for maintainer-exported cache/store/nar metadata, Nix
=.narinfo= parsing, Nix valid-path fingerprint generation for
signature input, local trusted-signer policy reports, OpenSSL-backed
Nix Ed25519 verification for narinfo signatures, and OpenSSL
RSA-SHA256 verification for signed substitute descriptors. It also
exposes a pluggable cryptographic verifier registry and an
OpenSSL-free =nelix-sha256-digest= descriptor verifier for native
bootstrap integrity checks; public-key Ed25519/RSA verification
remains OpenSSL-backed until NeLisp provides native crypto.
- Debian packaging now has local publication gates:
=make apt-repo-gate= builds =elpa-nelix=, verifies the =.deb= with
lintian/content checks, generates an APT repository under
=../nelix-apt-repo=, and verifies =Packages=, =Packages.gz=,
=Release=, and =sources.list=. =make apt-signed-repo-gate= signs
that repository with a temporary GPG key, writes =InRelease=,
=Release.gpg=, =nelix-archive-keyring.gpg=, and
=sources.list.signed=, then verifies the signatures with =gpgv=.
=make apt-http-gate= copies a sanitized static publication tree to
=../nelix-apt-public=, rejects accidental GPG secret material, serves
it over local HTTP, runs isolated =apt-get update=, downloads
=elpa-nelix= through HTTP, and verifies the downloaded =.deb=.
=make publication-url-check APT_PUBLIC_URL=https://...
FEDORA_PUBLIC_URL=https://...= rejects placeholder or insecure public
repository URLs before any public publication work starts.
=make publication-preflight APT_PUBLIC_URL=https://...
FEDORA_PUBLIC_URL=https://...= runs that URL guard, then verifies both
static publication trees against their final public URLs before upload.
=make publication-public-smoke= runs the same URL guard before checking
those published URLs after upload.
=make apt-public-url-smoke APT_PUBLIC_URL=https://...= runs the same
rootless isolated APT download check against a real published HTTPS
repository URL.
Fedora packaging has =make fedora-container-gate=, which builds
=nelix= / =emacs-nelix= RPMs in =fedora:latest=, runs =rpmlint=,
creates a local =createrepo_c= repository, installs both packages
with =dnf= from that repository, and verifies =nelix --help= plus
Emacs loadability after install. =make fedora-public-url-smoke
FEDORA_PUBLIC_URL=https://...= installs from a published dnf
repository URL on a Fedora host or disposable Fedora container.
- Phase 5 groundwork: =nelix-compat= exposes NeLisp backend
hooks for synchronous process calls, async process spawn, text HTTP
GET, binary HTTP GET, env lookup, and PATH lookup
so the real NeLisp runtime can plug in native implementations
without changing call sites. When hooks are unset,
=nelisp-make-process=, =nelisp-http-get=,
=nelisp-http-fetch=, and =nelisp-http-get-binary= are auto-detected
when loaded or available
through the package-split noerror require probe, including NeLisp
images where these may be present without =nelisp-call-process=.
Detection also refreshes lazily after load and probes
=nelisp-process= / =nelisp-network= / =nelisp-http= once before
falling back. Binary
HTTP uses a native binary backend when available, then falls back to
=curl=; otherwise the existing unsupported-runtime signals are
preserved. File / env / PATH helpers also lazy-require
=nelisp-runtime=, =nelisp-emacs-compat=, and
=nelisp-emacs-compat-fileio= so package-split NeLisp images can
load state and helper I/O backends after =nelix= itself.
Text HTTP can use either the low-level =nelisp-http-get= backend or
the higher-level =nelisp-http-fetch= package, then falls back through
the same =curl= adapter used by binary HTTP when neither native text
backend is present. Auto-detected native HTTP backends that are
loadable but return status 0 because their lower runtime primitives
are absent also retry through =curl= when that fallback is available.
Explicit backend hook returns are validated against their documented
plist shapes before nelix consumes them.
Async process metadata, status, and exit-code access now go through
compat wrappers so future NeLisp process wraps do not have to mimic
Emacs =process-get= / =process-put= directly.
Async stderr buffer creation, reading, liveness probing, and cleanup
also go through compat wrappers, so the async path no longer depends
on raw Emacs buffer APIs at its call sites.
A local =make smoke-nelisp= target now verifies that the compat
layer loads under a real NeLisp standalone binary and that explicit
sync process / async / HTTP hooks dispatch there.
- =nelix-state= serializes through
=nelix-compat-json-serialize=, so the persistent JSON cache no
longer depends directly on Emacs-only =json-serialize= when running
on NeLisp.
The public Elisp API is the short =pkg-*= surface (plus the =nelix-*=
facade entry points); the historical =anvil-pkg-*= names were removed
in the 2026-06 rename and are not aliased.
Phase 5 focuses on the NeLisp async backend that makes
=nelix-compat-make-process-async= work outside Emacs and a
NeLisp-native HTTP backend. The compat layer already probes and
auto-detects NeLisp process / text HTTP / binary HTTP primitives and
uses =curl= as the HTTP fallback when no native text / binary backend
is installed and a process backend can run =curl=.
The remaining Phase 5 completion blockers are NeLisp runtime /
Layer-2 ownership, not nelix DSL ownership. nelix owns the
compat contracts, hook variables, auto-detection, fallback behavior,
and readiness tests. NeLisp owns the executable lower primitives:
subprocess spawn / wait / pipe semantics for process backends, and a
standalone URL / HTTP transport for text HTTP backends.
* Why?
** AI agents need a single DSL for tool installation
Claude Code (and similar agents) already speak anvil's MCP tools
fluently. Adding =pkg-install= to that surface means an agent can
provision its own dependencies — install =ripgrep=, =jq=, language
servers, anything in nixpkgs — by emitting one Elisp form, without
shelling out to a different package manager per OS.
** Nix solves the hard problems; we add the DSL
Nix already handles dependency closures, content-addressed storage,
sandboxed builds, and binary caches. =nelix= does *not* re-implement
any of that. It provides:
1. An Elisp DSL (=pkg-define=, =pkg-install=, ...) that
feels native to Emacs / NeLisp users.
2. A wrapper that translates that DSL to Nix expressions / =nix profile=
commands.
3. A fallback path for Git-host packages /not/ in nixpkgs (private
repos, in-development MCP servers), inspired by =async-installer=.
** Plain Elisp, no macro DSL forced on you
Like async-installer, =nelix= prefers =setq= / =defun= /
=pkg-define= over a baroque =use-package=-style macro. Reading
your config should not require learning a new macro language.
* Design overview
#+begin_src text
┌─────────────────────────────────────────────────────────────┐
│ Elisp DSL │
│ (pkg-install "ripgrep") │
│ (pkg-define my-tool :src (github "...") :build ...) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ nelix core (this repo) │
│ - DSL parser / form → backend dispatch │
│ - manifest / generation tracking │
│ - MCP tool surface: pkg-install / search / list ... │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Nix backend (primary) │ │ Git backend (fallback) │
│ nix profile install ... │ │ inspired by async-installer│
│ nix-eval / flake / nixpkgs │ │ for non-nixpkgs repos │
└─────────────────────────────┘ └─────────────────────────────┘
#+end_src
* Phase plan
| Phase | Scope | Status |
|-------+------------------------------------------------------------------------+----------------|
| 1 | =nix profile= shell-out wrapper. 3 MCP tools. =install/search/list= | *SHIPPED* |
| 2 | =pkg-define= DSL macro + flake.nix generation + symbol install | *SHIPPED* |
| 3 | =github-fetch= / =git-fetch= sources + =rust= / =python= / =go= bs | *SHIPPED* |
| 4-A | =emacs-package= build-system + post-install =load-path= + =:require= | *SHIPPED* |
| 4-B | =:format= / =:native-comp= / =:async= callbacks + importer (L15 def'd) | *SHIPPED* |
| 4-C | L18 pre-fetch + L19 rollback + L20 Nix 2.34 + L22 compat async | *SHIPPED* |
| 4-D | melpaBuild recipe synthesis + url/git L18 + per-pkg rollback + state | *SHIPPED* |
| 4-E | MELPA upstream recipe fetch (opt-in) + better :melpa-files default | *SHIPPED* |
| 4-F | pkg-install multi-package dispatch (atomic bulk install) | *SHIPPED* |
| 4-G | private repo: env-var credentials → HTTP / git / nix CLI auth | *SHIPPED* |
| CI | GitHub Actions — Emacs 30 × Linux, =make lint= + =make test= + render gate | *SHIPPED* |
| 4-H | Real-Nix smoke (=make smoke-eval=) + cargoSha256→cargoHash renderer fix | *SHIPPED* |
| 5+ | NeLisp async backend + HTTP backend (compat hooks + smoke landed) | research |
| 6 | =pkg-uninstall= / =pkg-upgrade-plan= / =pkg-upgrade= / =pkg-info= | *SHIPPED* |
| 7 | =pkg-pin= / =pkg-doctor= + =node= / =haskell= build systems | *SHIPPED* |
| M1-M4 | Nelix manifest apply/audit/sync/prune/lock v2 + locked apply/sync | *SHIPPED* |
| M5 | Initial =nelix= CLI wrapper with =--json= and Debian command payload | *SHIPPED* |
| M6 | Cross-backend read-only =nelix-outdated= / =nelix outdated= | *SHIPPED* |
| N1/N2/N6 | Native backend protocol, store/profile metadata, local registry | *SHIPPED* |
| N3/N4 | Hash-verified native fetch and unpack builder | *SHIPPED* |
| N5 | Native Emacs Lisp builder and profile load-path activation | *SHIPPED* |
| N7 | Manifest backend dispatch and Nix-absent native fallback | *SHIPPED* |
| N8 | Native + precomputed Nix substitute materialize/install + signature policy | *SHIPPED* |
See =docs/design/01-overview.org= for the full Phase 1 contract.
* Install
#+begin_src shell
git clone https://github.com/zawatton/nelix ~/nelix
#+end_src
In your Emacs init:
#+begin_src elisp
(add-to-list 'load-path "~/nelix")
(require 'nelix)
(require 'nelix-dsl) ; Phase 2 — pkg-define macro
;; To register pkg-* MCP tools (requires anvil.el loaded):
(nelix-core-enable)
#+end_src
Command-line wrapper from a checkout:
#+begin_src shell
bin/nelix --help
bin/nelix --json doctor
bin/nelix audit ~/.config/nelix/manifest.el
bin/nelix sync ~/.config/nelix/manifest.el --prune
bin/nelix --runtime nelisp --json version
#+end_src
Local install:
#+begin_src shell
make install prefix=$HOME/.local
~/.local/bin/nelix --help
#+end_src
** Examples
#+begin_src elisp
;; Phase 1 — install a package straight from nixpkgs
(pkg-install "ripgrep")
(pkg-search "rust")
(pkg-list)
;; Phase 2 — declare a custom package and install by symbol
(pkg-define my-rg
(version "13.0.0")
(source (url-fetch "https://github.com/BurntSushi/ripgrep/archive/13.0.0.tar.gz"
:sha256 "sha256-..."))
(build-system stdenv)
(inputs (list pkg-config openssl))
(install-phase "make install PREFIX=$out"))
(pkg-install 'my-rg) ; goes through generated flake.nix
;; Phase 3 — github-fetch + rust build-system
(pkg-define my-rust-tool
(version "1.0.0")
(source (github-fetch :owner "user" :repo "tool"
:rev "v1.0.0" :sha256 "sha256-..."))
(build-system (rust :cargo-sha256 "sha256-..."))
(inputs (list openssl)))
;; Phase 3 — git-fetch + go build-system (vendored deps)
(pkg-define my-go-tool
(version "0.3.0" )
(source (git-fetch :url "https://example.com/tool.git"
:rev "v0.3.0" :sha256 "sha256-..."))
(build-system (go))) ; vendorHash = null (vendored)
;; Phase 4-A — emacs-package build-system + post-install require
(pkg-define dash-test
(version "2.20.0")
(source (github-fetch :owner "magnars" :repo "dash.el"
:rev "2.20.0" :sha256 "sha256-..."))
(build-system emacs-package))
(pkg-install 'dash-test :require 'dash)
;; Phase 4-B — :format ("melpa") + :native-comp + explicit deps
(pkg-define magit
(version "3.3.0")
(source (github-fetch :owner "magit" :repo "magit"
:rev "v3.3.0" :sha256 "sha256-..."))
(build-system (emacs-package :format "melpa" :native-comp t))
(depends-on (list dash transient with-editor)))
;; Phase 4-B — :async install with sentinel callbacks
(pkg-install 'magit
:async t
:require 'magit
:on-success (lambda (result)
(message "magit ready: %S" result))
:on-error (lambda (err)
(message "magit install failed: %S" err)))
;; → returns the live process object; sentinel routes the exit
;; status through your callback chain.
;; Phase 4-B — one-shot migration from async-installer-git-list
(require 'nelix-import)
(nelix-import-async-installer
:var 'async-installer-git-list
:emit (expand-file-name "imported-pkgs.el" user-emacs-directory))
;; → writes pkg-define forms; load the file from your init when ready.
;; Phase 4-C — auto-derive depends-on (L18 pre-fetch from
;; raw.githubusercontent.com). No explicit (depends-on ...) needed
;; for github-fetch sources; result cached for 30 days per session.
(pkg-define magit
(version "3.3.0")
(source (github-fetch :owner "magit" :repo "magit"
:rev "v3.3.0" :sha256 "sha256-..."))
(build-system (emacs-package :format "melpa")))
(pkg-install 'magit)
;; → pre-fetches magit-pkg.el, derives (dash transient with-editor compat),
;; renders flake.nix, installs. Use :no-auto-deps t to opt out.
;; Phase 4-C — profile generation rollback
(pkg-list-generations)
;; → ((:id 5 :date "..." :packages (ripgrep magit) :active t)
;; (:id 4 :date "..." :packages (ripgrep) :active nil))
(pkg-rollback) ; pop one generation
(pkg-rollback 3) ; jump to generation 3
(pkg-history 'magit) ; → events for magit only
;; Phase 4-C — importer auto-populates depends-on from local clones
(nelix-import-async-installer
:var 'async-installer-git-list
:emit (expand-file-name "imported-pkgs.el" user-emacs-directory)
:scrape-deps t) ; default; reads ~/.emacs.d/external-packages/<pkg>
; or falls through to L18's HTTP path.
;; Phase 4-D — :format "melpa" auto-synthesises a recipe via postUnpack
;; so upstream repos without recipes/<pname> still build under melpaBuild.
(pkg-define helm
(version "3.9.7")
(source (github-fetch :owner "emacs-helm" :repo "helm"
:rev "v3.9.7" :sha256 "sha256-..."))
(build-system (emacs-package
:format "melpa"
:melpa-synth 'auto ; auto / force / never
:melpa-files '("*.el" "lisp/*.el"))))
;; Override the synth completely with a verbatim recipe:
(pkg-define helm-custom
(version "3.9.7")
(source (github-fetch :owner "emacs-helm" :repo "helm"
:rev "v3.9.7" :sha256 "sha256-..."))
(build-system (emacs-package
:format "melpa"
:melpa-recipe "(helm :fetcher git :url \"https://github.com/emacs-helm/helm.git\" :files (\"*.el\" \"lisp/*.el\"))")))
;; Phase 4-D — auto-derive depends-on now covers url-fetch + git-fetch
(pkg-define dash
(version "2.20.0")
(source (url-fetch :url "https://example.com/dash-2.20.0.tar.gz"
:sha256 "sha256-..."))
(build-system (emacs-package :format "trivial")))
(pkg-install 'dash)
;; → tarball downloaded, foo-pkg.el extracted, deps cached by sha256.
(pkg-define internal
(version "1.0")
(source (git-fetch :url "https://git.example.com/internal.el.git"
:rev "v1.0" :sha256 "sha256-..."))
(build-system (emacs-package :format "trivial")))
(pkg-install 'internal)
;; → shallow git clone, deps scraped, tmpdir cleaned, deps cached by url@rev.
;; Phase 4-D — per-package rollback (drops one entry, keeps the rest)
(pkg-rollback-package 'magit)
;; → re-renders flake.nix from the registry minus magit, runs
;; `nix profile install'. Other installed packages stay.
;; Phase 4-D — clear caches by scope (default = all)
(pkg-clear-cache) ; all nelix-state namespaces
(pkg-clear-cache 'deps) ; just the Package-Requires lookup cache
(pkg-clear-cache 'nix-version) ; just the Nix-version detection cache
(pkg-clear-cache 'generations) ; just the profile generations mirror
;; Phase 4-E — opt-in MELPA upstream recipe fetch. When enabled,
;; :melpa-synth 'auto first tries
;; raw.githubusercontent.com/melpa/melpa/master/recipes/<pname>;
;; on hit it emits the canonical recipe verbatim instead of the local
;; synth fallback. Cached per package for 7 days in nelix-state.
(setq nelix-emacs-melpa-upstream-fetch t)
(pkg-define magit
(version "3.3.0")
(source (github-fetch :owner "magit" :repo "magit"
:rev "v3.3.0" :sha256 "sha256-..."))
(build-system (emacs-package :format "melpa")))
;; → upstream MELPA recipe used when present; falls back to synth on miss.
;; Phase 4-E — :melpa-files default upgraded to MELPA's canonical
;; package-build-default-files-spec. When :melpa-files is omitted, the
;; synth carries ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
;; "doc/dir" "doc/*.info" ... "lisp/*.el" "lisp/*.el.in" ... :exclude
;; tests / .dir-locals.el). Override only when you need a tighter list.
(pkg-define dash
(version "2.20.0")
(source (github-fetch :owner "magnars" :repo "dash.el"
:rev "2.20.0" :sha256 "sha256-..."))
(build-system (emacs-package :format "melpa")))
;; → synth recipe now covers lisp/ subdir + .el.in + .info layouts.
;; Phase 4-F — multi-package install (atomic bulk).
;; pkg-install dispatches a single nix profile install invocation
;; with all flakerefs. Atomic: success or none.
(pkg-install '(magit dash transient)) ; 3 registry symbols
(pkg-install '("ripgrep" "fd" "hyperfine")) ; 3 nixpkgs strings
(pkg-install '(magit "ripgrep")) ; mixed
;; Async with multi-install — :on-success receives :names NAMES, not :name:
(pkg-install '(magit dash) :async t
:on-success (lambda (r)
(message "installed %S" (plist-get r :names))))
;; :require is rejected with a list (which symbol to require?):
;; (pkg-install '(magit dash) :require 'magit)
;; → nelix-error
;; Phase 4-G — private repo support via env-var credentials.
;; Export a token before invoking pkg-install; the DSL form is
;; identical to a public-repo recipe. No :auth keyword, no
;; on-disk credential storage.
;;
;; $ export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
(pkg-define my-private-tool
(version "0.1.0")
(source (github-fetch :owner "your-org"
:repo "private-tool"
:rev "v0.1.0"
:sha256 "sha256-..."))
(build-system (rust :cargo-sha256 "sha256-...")))
(pkg-install 'my-private-tool)
;; nelix auto-injects:
;; - L18 raw.githubusercontent.com Authorization: Bearer header
;; - git -c http.<host>/.extraheader=Authorization: Bearer ...
;; - nix --option extra-access-tokens "github.com=$TOKEN"
;; Default credential alist covers github.com + raw + api +
;; codeload + objects subdomains; gitlab.com (GITLAB_TOKEN);
;; codeberg.org (CODEBERG_TOKEN). See examples/private-github.el.
#+end_src
Package install is the Elisp =pkg-install= API; the =bin/nelix= CLI
covers the manifest workflow (=validate= / =apply= / =audit= / =sync= …).
An =anvil.el=-side =anvil pkg ...= dispatch may also surface these.
** Examples directory
=examples/= ships copy-pasteable =pkg-define= recipes for every
supported source + build-system combination:
| File | Source | Build system |
|-------------------------------+--------------+-----------------------|
| =examples/stdenv-hello.el= | url-fetch | stdenv |
| =examples/rust-ripgrep.el= | github-fetch | rust |
| =examples/python-black.el= | github-fetch | python (pyproject) |
| =examples/go-hugo.el= | github-fetch | go |
| =examples/emacs-trivial-dash.el= | github-fetch | emacs-package trivial |
| =examples/emacs-melpa-magit.el= | github-fetch | emacs-package melpa |
| =examples/multi-install.el= | (mixed) | rust × 3 |
| =examples/private-github.el= | github / git | rust / go |
Each file is self-contained; load with =M-x load-file= (or
=(load-file ...)= from your init) and invoke =(pkg-install
'<name>)=. Replace the =sha256-PLACEHOLDER= literals before
installing — see =examples/README.org= for the workflow.
** Security notes (Phase 4-G credentials)
Private repository access is opt-in and entirely env-var-driven.
There is no DSL =:auth= keyword and *no* on-disk credential
storage — nelix never writes tokens to =nelix-state= or
to any of its own log lines.
- Default credential map (=nelix-compat-credential-env-alist=):
| Host | Env vars (first non-empty wins) |
|-----------------------------------------------------+---------------------------------|
| github.com / raw.githubusercontent.com / api.github.com / codeload.github.com / objects.githubusercontent.com | =GITHUB_TOKEN=, =GH_TOKEN= |
| gitlab.com | =GITLAB_TOKEN= |
| codeberg.org | =CODEBERG_TOKEN= |
- Add custom hosts (e.g. corporate GitHub Enterprise) by
=add-to-list='ing into =nelix-compat-credential-env-alist=
before invoking =pkg-install=.
- Tokens *are* visible to =ps aux= for the duration of =nix= /
=git= subprocesses — both tools accept the credential on the
CLI and nelix cannot suppress that. On a single-user
machine this is rarely an issue; on a shared host consider
Nix daemon =access-tokens.conf= or a =GIT_ASKPASS= shim
instead (out of scope for this phase).
- Logging through =nelix-compat-mask-credentials= replaces
=Bearer .*=, =extra-access-tokens "host=tok"=, and
=x-access-token:tok@= patterns with =***= before any
=message= / =lwarn= / debug-print call.
** Run the test suite
#+begin_src shell
nix develop # optional: enter the pinned development environment
nix flake check # run the Nix-packaged no-Nix local gate
make check # local no-Nix gate: lint + test + smoke-pairs-check + smoke-render
make verify-local # full local gate: check + repository flake + real Nix + NeLisp + whitespace
make check-whitespace # git diff --check
make nix-check # same top-level Nix flake check via Makefile
make test # 221 ERT tests, no nix binary required (all mocked)
make compile # byte-compile runtime source/scripts, warnings-as-errors
make compile-tests # byte-compile ERT files, warnings-as-errors
make check-declare # check declarations in runtime source/scripts + ERT files
make lint # byte-compile source/scripts/tests + check-declare
make smoke-pairs-check # validate smoke example pair metadata; no nix required
make smoke-render # render every non-empty example + flake shape check; no nix required
make smoke-eval # real Nix evaluator check for real-hash examples
make smoke-build # local real Nix build for cheap examples
make smoke-nelisp # load compat layer under local standalone NeLisp
make smoke-nelisp-capabilities # print local NeLisp backend capability snapshot
make smoke-nelix-cli-nelisp # run bin/nelix through standalone NeLisp
make smoke-nelisp-suite-readiness # audit real NeLisp full-suite readiness
make smoke-nelisp-suite-loadability # load each ERT file under real NeLisp
make smoke-nelisp-suite # run real NeLisp standalone-executable suite
make smoke-nelisp-local # aggregate local NeLisp gate
#+end_src
The Makefile auto-detects =/nix/var/nix/profiles/default/bin/nix= when
=nix= is not on =PATH= and passes =NIX_CONFIG='experimental-features =
nix-command flakes'= for the smoke targets.
* Hermeticity tiers (source builds)
The native source-build executor (=:install (:type build ...)=, no Nix) runs
build phases at one of three *hermeticity tiers*, selected per build. The
default is Tier 1 and is cross-platform; Tier 2 is an *opt-in, Linux-only*
kernel sandbox (design docs 31 and 32).
| Tier | Isolation | Network | Reproducible | Platforms |
|------+-----------------------------------------+----------+--------------+-----------|
| 0 | plain build dir | open | no | Linux/macOS |
| 1 | env-scrub + rlimit + deterministic env | open | no | Linux/macOS (default) |
| 2 | kernel namespace sandbox (bubblewrap) | *denied* | yes (same host) | *Linux only* |
*Tier 2* (the =nelix-sandbox= optional module) runs the whole build inside a
=bwrap= namespace: only the declared input closure is visible (read-only),
=$out= is writable, and the network namespace is unshared so the build is
*offline*. Source is fetched + =sha256=-verified on the host first
(fixed-output, like Nix); a hash mismatch is rejected loudly. Two builds of
the same recipe on the same host produce a byte-identical binary. It is
loaded lazily and adds *zero* cost to Tier 0/1 builds.
*macOS / Windows stay at Tier 1.* Kernel namespaces are Linux-only, so
=:hermeticity 'tier2= is unavailable off Linux (and on Linux hosts without
=bwrap= or unprivileged user namespaces). In those cases the build does NOT
silently downgrade — it fails loudly with a clear instruction to re-run at
=:hermeticity 'tier1=. Requirements for Tier 2: Linux, =bubblewrap=
installed, and unprivileged user namespaces enabled.
* Requirements (planned)
- Emacs 29+ (anvil runtime requirement)
- Nix 2.18+ with flakes enabled (Phase 1 backend)
- =anvil.el= loaded
* Naming
=nelix= began inside anvil's sub-module pattern (=anvil-http=,
=anvil-state=, =anvil-defs=, =anvil-org-index=, ...) as =anvil-pkg=, and
in 2026-06 was promoted to the standalone =nelix= brand: modules
=nelix-core= / =nelix-compat= / =nelix-state= / =nelix-import= /
=nelix-emacs= / =nelix-dsl=, the =nelix-error= contract, and the
=bin/nelix= CLI. The public =pkg-*= Elisp API is unchanged. An
=anvil.el=-side =anvil pkg ...= dispatch can still surface the CLI so
the user keeps one binary in =$PATH=.
* License
GPL-3.0-or-later. Same as =anvil.el= and =NeLisp=.
Connection Info
You Might Also Like
everything-claude-code
Complete Claude Code configuration collection - agents, skills, hooks,...
markitdown
MarkItDown-MCP is a lightweight server for converting URIs to Markdown.
cc-switch
All-in-One Assistant for Claude Code, Codex & Gemini CLI across platforms.
servers
Model Context Protocol Servers
servers
Model Context Protocol Servers
Time
A Model Context Protocol server for time and timezone conversions.