nix-doom-emacs-unstraightened/package.nix
Marien Zwart ad3ddef70b
Switch to melpaBuild for generated packages
trivialBuild results in files installed directly into site-lisp/,
without package metadata. This breaks ELPA packages whose dependencies
we've replaced: package.el refuses to activate the ELPA package because
as far as it knows its dependency is not installed.

melpaBuild seems the easiest way of generating an activatable package
that works with packages we leave untouched.
2024-03-04 19:19:51 +11:00

203 lines
9.2 KiB
Nix

{
/* Your init.el. */
doomInitFile ? null,
/* Your packages.el. */
doomPrivateModule ? null,
/* Doom source tree. */
doomSource,
/* Emacs package to build against. */
emacs,
/* Whether to enable all default dependencies. Primarily useful for CI /
testing. */
full ? false,
callPackages,
emacsPackagesFor,
lib,
linkFarm,
runCommand,
runtimeShell,
writeText,
}:
let
inherit (lib) optional optionalAttrs optionalString;
# 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).
initialDoomDir = linkFarm "minimal-doom-dir" (
[{ name = "cli.el"; path = ./cli.el; }]
++ optional (doomInitFile != null) { name = "init.el"; path = doomInitFile; }
++ optional (doomPrivateModule != null) { 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 = initialDoomDir;
};
} ''
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.
spell-fu = "https://codeberg.org/ideasman42/emacs-spell-fu.git";
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";
visual-fill-column = "https://codeberg.org/joostkremers/visual-fill-column.git";
};
doomEmacsPackages = (emacsPackagesFor emacs).overrideScope (
eself: esuper:
let
customPackages = callPackages ./elisp-packages.nix { inherit emacs esuper eself; };
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 origEPkg
else
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";
# 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;
# Nix requires that we set version. Inherit it from the
# original if available: package.el currently does not check
# versions of dependencies but there's a fixme for that in the
# code...
version = origEPkg.version or "1";
# melpa2nix requires we set this.
commit = p.pin;
meta = {
description = "trivial build for doom-emacs";
};
# Just enough to make melpa2nix work.
# TODO: pass "files" through, drop postUnpack hack below?
recipe = writeText "generated-recipe"
"(${name} :fetcher github :repo \"marienz/made-up\")";
buildInputs = (map (name: eself.${name}) reqlist);
} // optionalAttrs (p ? recipe.files && p.recipe.files != { defaults = "*"; }) {
# HACK: files can contain :defaults, which splices in defaults.
# If files starts with :defaults, the entire thing gets
# misinterpreted as a proplist when exported to json.
# This currently only happens for `(:defaults "*")`, which we can
# safely ignore (skipping a few excludes).
postUnpack = ''
filteredSrc=$PWD/filteredSrc
mkdir $filteredSrc
pushd $sourceRoot
cp -r ${builtins.toString p.recipe.files} $filteredSrc
popd
sourceRoot=$filteredSrc
'';
}));
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; }
else epkg;
in
lib.mapAttrs makePackage doomPackageSet
);
emacsWithPackages = doomEmacsPackages.emacsWithPackages (epkgs: (map (p: epkgs.${p}) (builtins.attrNames doomPackageSet)));
in
emacsWithPackages