doom-profile-data-dir is for the current profile, naming it "profiles" (plural) made no sense. Also move the loader into its own subdir so it's more obvious what the two init.29.elc files are.
310 lines
13 KiB
Nix
310 lines
13 KiB
Nix
{
|
|
/* DOOMDIR / Doom private directory / module. */
|
|
doomDir ? "/var/empty",
|
|
/* Doom source tree. */
|
|
doomSource,
|
|
/* Emacs package to build against. */
|
|
emacs,
|
|
/* Whether to enable all default dependencies. Primarily useful for CI /
|
|
testing. */
|
|
full ? false,
|
|
/* Name of doom profile to use. */
|
|
profileName ? "nix",
|
|
|
|
callPackages,
|
|
git,
|
|
emacsPackagesFor,
|
|
lib,
|
|
linkFarm,
|
|
runCommand,
|
|
runtimeShell,
|
|
writeText,
|
|
makeBinaryWrapper,
|
|
}:
|
|
let
|
|
inherit (lib) optional optionalAttrs optionalString;
|
|
|
|
doomInitFile = "${doomDir}/init.el";
|
|
doomPrivateModule = "${doomDir}/packages.el";
|
|
|
|
# Step 1: determine which Emacs packages to pull in.
|
|
#
|
|
# Inputs: unpatched Doom, a DOOMDIR with the provided init.el and packages.el.
|
|
# Outputs:
|
|
# - Packages Doom normally loads using Straight (as json)
|
|
# - modified packages.el that claims all packages are system-installed
|
|
#
|
|
# Uses Doom's CLI framework, which does not require anything else is installed
|
|
# (not even straight).
|
|
stage1DoomDir = linkFarm "doom-dir-stage1" (
|
|
[{ name = "cli.el"; path = ./cli1.el; }]
|
|
++ optional (lib.pathExists doomInitFile) { name = "init.el"; path = doomInitFile; }
|
|
++ optional (lib.pathExists doomPrivateModule) { name = "packages.el"; path = doomPrivateModule; }
|
|
);
|
|
# Set DOOMLOCALDIR somewhere harmless to stop Doom from trying to create it
|
|
# somewhere read-only.
|
|
|
|
# (If this step breaks, add DEBUG=1 to make Doom more verbose.)
|
|
|
|
# XXX this may need to be runCommandLocal just in case conditionals an init.el
|
|
# / packages.el evaluate differently on build systems.
|
|
doomIntermediates = runCommand "doom-intermediates"
|
|
{
|
|
env = {
|
|
EMACS = lib.getExe emacs;
|
|
DOOMDIR = stage1DoomDir;
|
|
};
|
|
} ''
|
|
mkdir $out
|
|
export DOOMLOCALDIR=$(mktemp -d)
|
|
${runtimeShell} ${doomSource}/bin/doom dump-for-nix-build \
|
|
${optionalString full "--full"} -o $out
|
|
'';
|
|
|
|
doomPackageSet = lib.importJSON "${doomIntermediates}/packages.json";
|
|
|
|
# URLs for a few packages used by Doom that have straight recipes but are not
|
|
# in nixpkgs.
|
|
extraUrls = {
|
|
# Straight recipe from el-get
|
|
font-lock-ext = "https://github.com/sensorflo/font-lock-ext.git";
|
|
sln-mode = "https://github.com/sensorflo/sln-mode.git";
|
|
# Straight recipe from emacsmirror-mirror
|
|
nose = "https://github.com/emacsattic/nose.git";
|
|
# In nixpkgs, but uses codeberg, for which nixpkgs uses fetchzip.
|
|
# TODO: consider parsing origEPkg.src.url instead.
|
|
tree-sitter-indent = "https://codeberg.org/FelipeLema/tree-sitter-indent.el.git";
|
|
undo-fu = "https://codeberg.org/ideasman42/emacs-undo-fu.git";
|
|
undo-fu-session = "https://codeberg.org/ideasman42/emacs-undo-fu-session.git";
|
|
};
|
|
|
|
# Step 2: override Emacs packages to respect Doom's pins.
|
|
doomEmacsPackages = (emacsPackagesFor emacs).overrideScope (
|
|
eself: esuper:
|
|
let
|
|
customPackages = callPackages ./elisp-packages.nix { inherit emacs esuper eself; };
|
|
# We want to override `version` along with `src` to avoid spurious
|
|
# rebuilds on version bumps in emacs-overlay of packages Doom has
|
|
# pinned.
|
|
#
|
|
# The elisp manual says we need a version `version-to-list` can parse,
|
|
# which means it must start with a number and cannot contain the actual
|
|
# commit ID. We start with a large integer in case package.el starts
|
|
# version-checking dependencies (it currently does not but a comment in
|
|
# the code says it should). Additionally, `(package-version-join
|
|
# (version-to-list v))` must roundtrip to avoid elpa2nix failing with
|
|
# "Package does not untar cleanly".
|
|
snapshotVersion = "9999snapshot";
|
|
|
|
makePackage = name: p:
|
|
assert lib.asserts.assertEachOneOf
|
|
"keys for ${name}"
|
|
(lib.attrNames p)
|
|
[ "modules" "recipe" "pin" "type" ];
|
|
assert (p ? type) -> lib.asserts.assertOneOf
|
|
"type of ${name}"
|
|
p.type
|
|
[ "core" ];
|
|
let
|
|
origEPkg = esuper.${name} or null;
|
|
# We have to specialcase ELPA packages pinned by Doom: Straight mirrors /
|
|
# repackages them. Doom's pins assume that mirror is used (so we have to
|
|
# use it), and replacing the source in nixpkgs's derivation will not work
|
|
# (it assumes it gets a tarball as input).
|
|
|
|
# TODO: check notmuch works correctly without notmuch-version.el
|
|
|
|
isElpa = origEPkg != null && (origEPkg == esuper.elpaPackages.${name} or null || origEPkg == esuper.nongnuPackages.${name} or null);
|
|
epkg =
|
|
customPackages.${name}
|
|
or (if origEPkg == null || (p ? pin && isElpa)
|
|
then
|
|
assert lib.assertMsg
|
|
(isElpa || (p ? recipe && p ? pin) || extraUrls ? ${name})
|
|
"${name}: not in epkgs, not elpa, no recipe or not pinned";
|
|
# Assume we can safely ignore (pre-)build unless we're actually
|
|
# building our own package.
|
|
assert lib.assertMsg (!(p ? recipe.pre-build)) "${name}: pre-build not supported";
|
|
assert lib.assertMsg (!(p ? recipe.build)) "${name}: build not supported";
|
|
# TODO: lift "pin" requirement, if that turns out to be
|
|
# necessary or at least desirable. Requires figuring out why
|
|
# melpa2nix requires `commit`. Not a priority because if it's
|
|
# not in epkgs we'd need a recipe passed in, and it's uncommon
|
|
# for Doom to pass in a recipe without pinning.
|
|
#
|
|
# Doom does currently have unpinned packages with an explicit
|
|
# recipe, but they're in epkgs (popon and flymake-popon) so it
|
|
# should be ok. Users might do this to pull in a custom package
|
|
# they don't care about pinning, though: we may want to support
|
|
# that.
|
|
assert lib.assertMsg (p ? pin)
|
|
"${name}: not in epkgs and not pinned. This is not yet supported.";
|
|
# epkgs.*Build helpers take an attrset, they do not support
|
|
# mkDerivation's fixed-point evaluation (`finalAttrs`).
|
|
# If they did, the buildInputs stuff should use finalAttrs.src.
|
|
|
|
# This uses melpaBuild instead of trivialBuild to end up with
|
|
# something package.el understands as satisfying dependencies.
|
|
# This is necessary if we're replacing a pinned ELPA dependency
|
|
# of an unpinned ELPA package.
|
|
esuper.melpaBuild {
|
|
pname = name;
|
|
# melpaBuild requires we set `version` and `commit` here
|
|
# (leaving `version` unset until overrideAttrs below does not
|
|
# work).
|
|
version = snapshotVersion;
|
|
commit = p.pin;
|
|
meta = {
|
|
description = "trivial build for doom-emacs";
|
|
};
|
|
# Just enough to make melpa2nix work.
|
|
recipe = writeText "generated-recipe" ''
|
|
(${name} :fetcher github :repo "marienz/made-up"
|
|
${optionalString (p ? recipe.files) ":files ${lib.debug.traceValSeq p.recipe.files}"})'';
|
|
buildInputs = (map (name: eself.${name}) reqlist);
|
|
}
|
|
else origEPkg);
|
|
url =
|
|
if (p.recipe.host or "") == "github" && p ? recipe.repo
|
|
then "https://github.com/${p.recipe.repo}"
|
|
else epkg.src.gitRepoUrl
|
|
or (if isElpa then "https://github.com/emacs-straight/${name}"
|
|
else extraUrls.${name}
|
|
or (throw "${name}: cannot derive url from recipe ${p.recipe or "<missing>"}"));
|
|
# Use builtins.fetchGit instead of nixpkgs's fetchFromGitHub because
|
|
# fetchGit allows fetching a specific git commit without a hash.
|
|
# TODO: port to fetchTree once (mostly) stable
|
|
# (in particular the github fetcher may be noticably more efficient)
|
|
src = builtins.fetchGit (
|
|
{
|
|
inherit url;
|
|
rev = p.pin;
|
|
submodules = !(p.recipe.nonrecursive or false);
|
|
# TODO: might need to pull ref from derivation.src if we're not pulling it from p.recipe?
|
|
# Note Doom does have packages with pin + branch (or nonrecursive) set,
|
|
# expecting to inherit the rest of the recipe from Straight.
|
|
} // optionalAttrs (p ? recipe.branch) { ref = p.recipe.branch; }
|
|
// optionalAttrs (p ? recipe.depth) { shallow = p.recipe.depth == 1; }
|
|
);
|
|
# Ignore dependency extraction errors because it fails for repos not
|
|
# containing a "proper" package (no -pkg.el, no file with the right magic
|
|
# header). These seem common enough to be not worth allowlisting.
|
|
reqfile = runCommand "${name}-deps" { } ''
|
|
${lib.getExe emacs} -Q --batch --eval \
|
|
"(progn
|
|
(require 'package)
|
|
(with-temp-buffer
|
|
(setq default-directory \"${src}\")
|
|
(dired-mode)
|
|
(let ((reqs (with-demoted-errors \"Extracting dependencies: %s\" (package-desc-reqs (package-dir-info)))))
|
|
(princ (json-encode (mapcar #'car (seq-remove (lambda (p) (apply #'package-built-in-p p)) reqs)))))))" \
|
|
> $out
|
|
'';
|
|
reqjson = lib.importJSON reqfile;
|
|
# json-encode encodes the empty list as null (nil), not [].
|
|
reqlist = if reqjson == null then [ ] else reqjson;
|
|
in
|
|
if p ? pin
|
|
then epkg.overrideAttrs {
|
|
inherit src;
|
|
version = snapshotVersion;
|
|
}
|
|
else epkg;
|
|
in
|
|
lib.mapAttrs makePackage doomPackageSet
|
|
);
|
|
|
|
# Step 3: Build an emacsWithPackages, pulling all packages from step 1 from
|
|
# the set from step 2.
|
|
emacsWithPackages = doomEmacsPackages.emacsWithPackages (epkgs: (map (p: epkgs.${p}) (builtins.attrNames doomPackageSet)));
|
|
|
|
# Step 4: build a final DOOMDIR with packages.el from Step 1.
|
|
#
|
|
# This is used in three contexts:
|
|
# - To build the Doom profile. So we need our cli commands.
|
|
# - When loading that profile, as the path to the :user module.
|
|
# This path is hardcoded into the profile.
|
|
# - As doom-user-dir at runtime.
|
|
#
|
|
# I would prefer to avoid that last one, but Doom uses doom-user-dir and the
|
|
# path to the :user module interchangeably in several places. Attempting to
|
|
# split them looks not just confusing for the user but error-prone.
|
|
#
|
|
# TODO: symlink farm instead of copy?
|
|
finalDoomDir = runCommand "doom-dir" {} ''
|
|
if [[ -e ${doomDir} ]]; then
|
|
cp -r ${doomDir} $out/
|
|
chmod +w $out
|
|
else
|
|
mkdir $out
|
|
fi
|
|
ln -sf ${doomIntermediates}/packages.el $out/
|
|
ln -sf ${./cli2.el} $out/cli.el
|
|
'';
|
|
|
|
# TODO: maybe do something about custom-file (which Doom sets relative to
|
|
# doom-user-dir, so with that in the store it becomes readonly).
|
|
|
|
# Step 5: build a Doom profile and profile loader using Emacs from step 3 and
|
|
# DOOMDIR from step 4.
|
|
#
|
|
# Create both in the same derivation: we want the path to the generated
|
|
# profile in the loader (so building the loader depends on the profile), but
|
|
# I'm currently using the loader to set up Doom to write the profile to the
|
|
# right place (so building the profile depends on the loader).
|
|
|
|
# XXX runCommandLocal? (See doomIntermediates.)
|
|
doomProfile = runCommand "doom-profile"
|
|
{
|
|
env = {
|
|
EMACS = lib.getExe emacsWithPackages;
|
|
DOOMDIR = finalDoomDir;
|
|
# Enable this to troubleshoot failures at this step.
|
|
#DEBUG = "1";
|
|
};
|
|
# Required to avoid Doom erroring out at startup.
|
|
nativeBuildInputs = [ git ];
|
|
} ''
|
|
mkdir $out
|
|
|
|
echo '
|
|
((${profileName} (user-emacs-directory . "${doomSource}")
|
|
(doom-profile-data-dir . "'$out'/profile")
|
|
("DOOMDIR" . "${finalDoomDir}")))
|
|
' > profiles.el
|
|
|
|
mkdir $out/loader
|
|
export DOOMPROFILELOADFILE=$out/loader/init.el
|
|
export DOOMPROFILELOADPATH=$PWD/profiles.el
|
|
export DOOMLOCALDIR=$(mktemp -d)
|
|
# Prevent error on Emacs shutdown writing empty build cache.
|
|
mkdir $DOOMLOCALDIR/straight
|
|
|
|
${runtimeShell} ${doomSource}/bin/doom build-profile-loader-for-nix-build
|
|
|
|
# With DOOMPROFILE set, doom-state-dir and friends are HOME-relative.
|
|
export HOME=$(mktemp -d)
|
|
export DOOMPROFILE='${profileName}';
|
|
${runtimeShell} ${doomSource}/bin/doom build-profile-for-nix-build
|
|
'';
|
|
|
|
# TODO: test HOME and DOOMPROFILE tmpdirs don't leak into profile init?
|
|
# Currently doesn't happen.
|
|
|
|
# TODO: write a package.el equiv of doom-profile--generate-package-autoloads.
|
|
# Doom already picks up load-path because in cli mode it inits packages.el
|
|
# But during normal startup it suppresses packages.el's auto-activation,
|
|
# which means elpa/*/*-autoloads.el don't load.
|
|
|
|
|
|
pkg = runCommand "doom" {
|
|
nativeBuildInputs = [ makeBinaryWrapper ];
|
|
}
|
|
''
|
|
makeWrapper ${emacsWithPackages}/bin/emacs $out/bin/emacs \
|
|
--set DOOMPROFILELOADFILE ${doomProfile}/loader/init.el \
|
|
--add-flags "--init-directory=${doomSource} --profile ${profileName}"
|
|
'';
|
|
|
|
in
|
|
pkg
|