about summary refs log tree commit diff
path: root/nixpkgs/pkgs/build-support/emacs
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/pkgs/build-support/emacs')
-rw-r--r--nixpkgs/pkgs/build-support/emacs/buffer.nix77
-rw-r--r--nixpkgs/pkgs/build-support/emacs/elpa.nix37
-rw-r--r--nixpkgs/pkgs/build-support/emacs/elpa2nix.el33
-rw-r--r--nixpkgs/pkgs/build-support/emacs/emacs-funcs.sh34
-rw-r--r--nixpkgs/pkgs/build-support/emacs/generic.nix90
-rw-r--r--nixpkgs/pkgs/build-support/emacs/melpa.nix125
-rw-r--r--nixpkgs/pkgs/build-support/emacs/melpa2nix.el32
-rw-r--r--nixpkgs/pkgs/build-support/emacs/mk-wrapper-subdirs.el6
-rw-r--r--nixpkgs/pkgs/build-support/emacs/package-build-dont-use-mtime.patch21
-rw-r--r--nixpkgs/pkgs/build-support/emacs/trivial.nix28
-rw-r--r--nixpkgs/pkgs/build-support/emacs/wrapper.nix236
-rw-r--r--nixpkgs/pkgs/build-support/emacs/wrapper.sh53
12 files changed, 772 insertions, 0 deletions
diff --git a/nixpkgs/pkgs/build-support/emacs/buffer.nix b/nixpkgs/pkgs/build-support/emacs/buffer.nix
new file mode 100644
index 000000000000..48a7996916e8
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/buffer.nix
@@ -0,0 +1,77 @@
+# Functions to build elisp files to locally configure emcas buffers.
+# See https://github.com/shlevy/nix-buffer
+
+{ lib, writeText, inherit-local }:
+
+rec {
+  withPackages = pkgs': let
+      pkgs = builtins.filter (x: x != null) pkgs';
+      extras = map (x: x.emacsBufferSetup pkgs) (builtins.filter (builtins.hasAttr "emacsBufferSetup") pkgs);
+    in writeText "dir-locals.el" ''
+      (require 'inherit-local "${inherit-local}/share/emacs/site-lisp/elpa/inherit-local-${inherit-local.version}/inherit-local.elc")
+
+      ; Only set up nixpkgs buffer handling when we have some buffers active
+      (defvar nixpkgs--buffer-count 0)
+      (when (eq nixpkgs--buffer-count 0)
+        (make-variable-buffer-local 'nixpkgs--is-nixpkgs-buffer)
+        ; When generating a new temporary buffer (one whose name starts with a space), do inherit-local inheritance and make it a nixpkgs buffer
+        (defun nixpkgs--around-generate (orig name &optional ibh)
+          (if (and nixpkgs--is-nixpkgs-buffer (eq (aref name 0) ?\s))
+              (let ((buf (funcall orig name ibh)))
+                (progn
+                  (inherit-local-inherit-child buf)
+                  (with-current-buffer buf
+                    (setq nixpkgs--buffer-count (1+ nixpkgs--buffer-count))
+                    (add-hook 'kill-buffer-hook 'nixpkgs--decrement-buffer-count nil t)))
+                buf)
+            (funcall orig name ibh)))
+        (advice-add 'generate-new-buffer :around #'nixpkgs--around-generate)
+        ; When we have no more nixpkgs buffers, tear down the buffer handling
+        (defun nixpkgs--decrement-buffer-count ()
+          (setq nixpkgs--buffer-count (1- nixpkgs--buffer-count))
+          (when (eq nixpkgs--buffer-count 0)
+            (advice-remove 'generate-new-buffer #'nixpkgs--around-generate)
+            (fmakunbound 'nixpkgs--around-generate)
+            (fmakunbound 'nixpkgs--decrement-buffer-count))))
+      (setq nixpkgs--buffer-count (1+ nixpkgs--buffer-count))
+      (add-hook 'kill-buffer-hook 'nixpkgs--decrement-buffer-count nil t)
+
+      ; Add packages to PATH and exec-path
+      (make-local-variable 'process-environment)
+      (put 'process-environment 'permanent-local t)
+      (inherit-local 'process-environment)
+      ; setenv modifies in place, so copy the environment first
+      (setq process-environment (copy-tree process-environment))
+      (setenv "PATH" (concat "${lib.makeSearchPath "bin" pkgs}:" (getenv "PATH")))
+      (inherit-local-permanent exec-path (append '(${builtins.concatStringsSep " " (map (p: "\"${p}/bin\"") pkgs)}) exec-path))
+
+      (inherit-local-permanent eshell-path-env (concat "${lib.makeSearchPath "bin" pkgs}:" (if (boundp 'eshell-path-env) eshell-path-env (getenv "PATH"))))
+
+      (setq nixpkgs--is-nixpkgs-buffer t)
+      (inherit-local 'nixpkgs--is-nixpkgs-buffer)
+
+      ${lib.concatStringsSep "\n" extras}
+    '';
+  # nix-buffer function for a project with a bunch of haskell packages
+  # in one directory
+  haskellMonoRepo = { project-root # The monorepo root
+                    , haskellPackages # The composed haskell packages set that contains all of the packages
+                    }: { root }:
+    let # The haskell paths.
+        haskell-paths = lib.filesystem.haskellPathsInDir project-root;
+        # Find the haskell package that the 'root' is in, if any.
+        haskell-path-parent =
+          let filtered = builtins.filter (name:
+            lib.hasPrefix (toString (project-root + "/${name}")) (toString root)
+          ) (builtins.attrNames haskell-paths);
+          in
+            if filtered == [] then null else builtins.head filtered;
+        # We're in the directory of a haskell package
+        is-haskell-package = haskell-path-parent != null;
+        haskell-package = haskellPackages.${haskell-path-parent};
+        # GHC environment with all needed deps for the haskell package
+        haskell-package-env =
+          builtins.head haskell-package.env.nativeBuildInputs;
+    in
+      lib.optionalAttrs is-haskell-package (withPackages [ haskell-package-env ]);
+}
diff --git a/nixpkgs/pkgs/build-support/emacs/elpa.nix b/nixpkgs/pkgs/build-support/emacs/elpa.nix
new file mode 100644
index 000000000000..a43578fd3936
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/elpa.nix
@@ -0,0 +1,37 @@
+# builder for Emacs packages built for packages.el
+
+{ lib, stdenv, emacs, texinfo, writeText, gcc }:
+
+let
+  handledArgs = [ "files" "fileSpecs" "meta" ];
+  genericBuild = import ./generic.nix { inherit lib stdenv emacs texinfo writeText gcc; };
+
+in
+
+{ pname
+, version
+, src
+, meta ? {}
+, ...
+}@args:
+
+genericBuild ({
+
+  dontUnpack = true;
+
+  installPhase = ''
+    runHook preInstall
+
+    emacs --batch -Q -l ${./elpa2nix.el} \
+        -f elpa2nix-install-package \
+        "$src" "$out/share/emacs/site-lisp/elpa"
+
+    runHook postInstall
+  '';
+
+  meta = {
+    homepage = args.src.meta.homepage or "https://elpa.gnu.org/packages/${pname}.html";
+  } // meta;
+}
+
+// removeAttrs args handledArgs)
diff --git a/nixpkgs/pkgs/build-support/emacs/elpa2nix.el b/nixpkgs/pkgs/build-support/emacs/elpa2nix.el
new file mode 100644
index 000000000000..64587c0fad1a
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/elpa2nix.el
@@ -0,0 +1,33 @@
+(require 'package)
+(package-initialize)
+
+(defun elpa2nix-install-package ()
+  (if (not noninteractive)
+      (error "`elpa2nix-install-package' is to be used only with -batch"))
+  (pcase command-line-args-left
+    (`(,archive ,elpa)
+     (progn (setq package-user-dir elpa)
+            (elpa2nix-install-file archive)))))
+
+(defun elpa2nix-install-from-buffer ()
+  "Install a package from the current buffer."
+  (let ((pkg-desc (if (derived-mode-p 'tar-mode)
+                      (package-tar-file-info)
+                    (package-buffer-info))))
+    ;; Install the package itself.
+    (package-unpack pkg-desc)
+    pkg-desc))
+
+(defun elpa2nix-install-file (file)
+  "Install a package from a file.
+The file can either be a tar file or an Emacs Lisp file."
+  (let ((is-tar (string-match "\\.tar\\'" file)))
+    (with-temp-buffer
+      (if is-tar
+          (insert-file-contents-literally file)
+        (insert-file-contents file))
+      (when is-tar (tar-mode))
+      (elpa2nix-install-from-buffer))))
+
+;; Allow installing package tarfiles larger than 10MB
+(setq large-file-warning-threshold nil)
diff --git a/nixpkgs/pkgs/build-support/emacs/emacs-funcs.sh b/nixpkgs/pkgs/build-support/emacs/emacs-funcs.sh
new file mode 100644
index 000000000000..e1e6a3b62208
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/emacs-funcs.sh
@@ -0,0 +1,34 @@
+addToEmacsLoadPath() {
+  local lispDir="$1"
+  if [[ -d $lispDir && ${EMACSLOADPATH-} != *"$lispDir":* ]] ; then
+    # It turns out, that the trailing : is actually required
+    # see https://www.gnu.org/software/emacs/manual/html_node/elisp/Library-Search.html
+    export EMACSLOADPATH="$lispDir:${EMACSLOADPATH-}"
+  fi
+}
+
+addToEmacsNativeLoadPath() {
+  local nativeDir="$1"
+  if [[ -d $nativeDir && ${EMACSNATIVELOADPATH-} != *"$nativeDir":* ]]; then
+    export EMACSNATIVELOADPATH="$nativeDir:${EMACSNATIVELOADPATH-}"
+  fi
+}
+
+addEmacsVars () {
+  addToEmacsLoadPath "$1/share/emacs/site-lisp"
+
+  if [ -n "${addEmacsNativeLoadPath:-}" ]; then
+    addToEmacsNativeLoadPath "$1/share/emacs/native-lisp"
+  fi
+
+  # Add sub paths to the Emacs load path if it is a directory
+  # containing .el files. This is necessary to build some packages,
+  # e.g., using trivialBuild.
+  for lispDir in \
+      "$1/share/emacs/site-lisp/"* \
+      "$1/share/emacs/site-lisp/elpa/"*; do
+    if [[ -d $lispDir && "$(echo "$lispDir"/*.el)" ]] ; then
+      addToEmacsLoadPath "$lispDir"
+    fi
+  done
+}
diff --git a/nixpkgs/pkgs/build-support/emacs/generic.nix b/nixpkgs/pkgs/build-support/emacs/generic.nix
new file mode 100644
index 000000000000..bdf1cd4e50f3
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/generic.nix
@@ -0,0 +1,90 @@
+# generic builder for Emacs packages
+
+{ lib, stdenv, emacs, texinfo, writeText, gcc, ... }:
+
+let
+  inherit (lib) optionalAttrs getLib;
+  handledArgs = [ "buildInputs" "packageRequires" "meta" ];
+
+  setupHook = writeText "setup-hook.sh" ''
+    source ${./emacs-funcs.sh}
+
+    if [[ ! -v emacsHookDone ]]; then
+      emacsHookDone=1
+
+      # If this is for a wrapper derivation, emacs and the dependencies are all
+      # run-time dependencies. If this is for precompiling packages into bytecode,
+      # emacs is a compile-time dependency of the package.
+      addEnvHooks "$hostOffset" addEmacsVars
+      addEnvHooks "$targetOffset" addEmacsVars
+    fi
+  '';
+
+in
+
+{ pname
+, version
+, buildInputs ? []
+, packageRequires ? []
+, meta ? {}
+, ...
+}@args:
+
+stdenv.mkDerivation (finalAttrs: ({
+  name = "emacs-${pname}-${finalAttrs.version}";
+
+  unpackCmd = ''
+    case "$curSrc" in
+      *.el)
+        # keep original source filename without the hash
+        local filename=$(basename "$curSrc")
+        filename="''${filename:33}"
+        cp $curSrc $filename
+        chmod +w $filename
+        sourceRoot="."
+        ;;
+      *)
+        _defaultUnpack "$curSrc"
+        ;;
+    esac
+  '';
+
+  buildInputs = [emacs texinfo] ++ packageRequires ++ buildInputs;
+  propagatedBuildInputs = packageRequires;
+  propagatedUserEnvPkgs = packageRequires;
+
+  inherit setupHook;
+
+  doCheck = false;
+
+  meta = {
+    broken = false;
+    platforms = emacs.meta.platforms;
+  } // optionalAttrs ((args.src.meta.homepage or "") != "") {
+    homepage = args.src.meta.homepage;
+  } // meta;
+}
+
+// optionalAttrs (emacs.withNativeCompilation or false) {
+
+  LIBRARY_PATH = "${getLib stdenv.cc.libc}/lib";
+
+  nativeBuildInputs = [ gcc ];
+
+  addEmacsNativeLoadPath = true;
+
+  postInstall = ''
+    # Besides adding the output directory to the native load path, make sure
+    # the current package's elisp files are in the load path, otherwise
+    # (require 'file-b) from file-a.el in the same package will fail.
+    mkdir -p $out/share/emacs/native-lisp
+    source ${./emacs-funcs.sh}
+    addEmacsVars "$out"
+
+    find $out/share/emacs -type f -name '*.el' -print0 \
+      | xargs -0 -I {} -n 1 -P $NIX_BUILD_CORES sh -c \
+          "emacs --batch --eval '(setq large-file-warning-threshold nil)' -f batch-native-compile {} || true"
+  '';
+}
+
+// removeAttrs args handledArgs))
diff --git a/nixpkgs/pkgs/build-support/emacs/melpa.nix b/nixpkgs/pkgs/build-support/emacs/melpa.nix
new file mode 100644
index 000000000000..c8f656704918
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/melpa.nix
@@ -0,0 +1,125 @@
+# builder for Emacs packages built for packages.el
+# using MELPA package-build.el
+
+{ lib, stdenv, fetchFromGitHub, emacs, texinfo, writeText, gcc }:
+
+let
+  genericBuild = import ./generic.nix { inherit lib stdenv emacs texinfo writeText gcc; };
+
+  packageBuild = stdenv.mkDerivation {
+    name = "package-build";
+    src = fetchFromGitHub {
+      owner = "melpa";
+      repo = "package-build";
+      rev = "c48aa078c01b4f07b804270c4583a0a58ffea1c0";
+      sha256 = "sha256-MzPj375upIiYXdQR+wWXv3A1zMqbSrZlH0taLuxx/1M=";
+    };
+
+    patches = [ ./package-build-dont-use-mtime.patch ];
+
+    dontConfigure = true;
+    dontBuild = true;
+
+    installPhase = "
+      mkdir -p $out
+      cp -r * $out
+    ";
+  };
+
+in
+
+{ /*
+    pname: Nix package name without special symbols and without version or
+    "emacs-" prefix.
+  */
+  pname
+  /*
+    ename: Original Emacs package name, possibly containing special symbols.
+    Default: pname
+  */
+, ename ? pname
+, version
+  /*
+    commit: Optional package history commit.
+    Default: src.rev or "unknown"
+    This will be written into the generated package but it is not needed during
+    the build process.
+  */
+, commit ? (args.src.rev or "unknown")
+  /*
+    files: Optional recipe property specifying the files used to build the package.
+    If null, do not set it in recipe, keeping the default upstream behaviour.
+    Default: null
+  */
+, files ? null
+  /*
+    recipe: Optional MELPA recipe.
+    Default: a minimally functional recipe
+  */
+, recipe ? (writeText "${pname}-recipe" ''
+    (${ename} :fetcher git :url ""
+              ${lib.optionalString (files != null) ":files ${files}"})
+  '')
+, meta ? {}
+, ...
+}@args:
+
+genericBuild ({
+
+  elpa2nix = ./elpa2nix.el;
+  melpa2nix = ./melpa2nix.el;
+
+  inherit packageBuild commit ename recipe;
+
+  preUnpack = ''
+    mkdir -p "$NIX_BUILD_TOP/recipes"
+    if [ -n "$recipe" ]; then
+      cp "$recipe" "$NIX_BUILD_TOP/recipes/$ename"
+    fi
+
+    ln -s "$packageBuild" "$NIX_BUILD_TOP/package-build"
+
+    mkdir -p "$NIX_BUILD_TOP/packages"
+  '';
+
+  postUnpack = ''
+    mkdir -p "$NIX_BUILD_TOP/working"
+    ln -s "$NIX_BUILD_TOP/$sourceRoot" "$NIX_BUILD_TOP/working/$ename"
+  '';
+
+  buildPhase = ''
+    runHook preBuild
+
+    cd "$NIX_BUILD_TOP"
+
+    emacs --batch -Q \
+        -L "$NIX_BUILD_TOP/package-build" \
+        -l "$melpa2nix" \
+        -f melpa2nix-build-package \
+        $ename $version $commit
+
+    runHook postBuild
+    '';
+
+  installPhase = ''
+    runHook preInstall
+
+    archive="$NIX_BUILD_TOP/packages/$ename-$version.el"
+    if [ ! -f "$archive" ]; then
+        archive="$NIX_BUILD_TOP/packages/$ename-$version.tar"
+    fi
+
+    emacs --batch -Q \
+        -l "$elpa2nix" \
+        -f elpa2nix-install-package \
+        "$archive" "$out/share/emacs/site-lisp/elpa"
+
+    runHook postInstall
+  '';
+
+  meta = {
+    homepage = args.src.meta.homepage or "https://melpa.org/#/${pname}";
+  } // meta;
+}
+
+// removeAttrs args [ "meta" ])
diff --git a/nixpkgs/pkgs/build-support/emacs/melpa2nix.el b/nixpkgs/pkgs/build-support/emacs/melpa2nix.el
new file mode 100644
index 000000000000..3de77dbf5e5c
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/melpa2nix.el
@@ -0,0 +1,32 @@
+(require 'package)
+(package-initialize)
+
+(require 'package-recipe)
+(require 'package-build)
+
+(setq package-build-working-dir (expand-file-name "working/"))
+(setq package-build-archive-dir (expand-file-name "packages/"))
+(setq package-build-recipes-dir (expand-file-name "recipes/"))
+
+;; Allow installing package tarfiles larger than 10MB
+(setq large-file-warning-threshold nil)
+
+(defun melpa2nix-build-package-1 (rcp)
+  (let* ((default-directory (package-recipe--working-tree rcp)))
+    (unwind-protect
+        (let ((files (package-build-expand-files-spec rcp t)))
+          (unless files
+            (error "Unable to find files matching recipe patterns"))
+          (if (> (length files) 1)
+              (package-build--build-multi-file-package rcp files)
+            (package-build--build-single-file-package rcp files))))))
+
+(defun melpa2nix-build-package ()
+  (unless noninteractive
+    (error "`melpa2nix-build-package' is to be used only with -batch"))
+  (pcase command-line-args-left
+    (`(,package ,version ,commit)
+     (let ((recipe (package-recipe-lookup package)))
+       (setf (oref recipe commit) commit)
+       (setf (oref recipe version) version)
+       (melpa2nix-build-package-1 recipe)))))
diff --git a/nixpkgs/pkgs/build-support/emacs/mk-wrapper-subdirs.el b/nixpkgs/pkgs/build-support/emacs/mk-wrapper-subdirs.el
new file mode 100644
index 000000000000..7d30400a5c65
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/mk-wrapper-subdirs.el
@@ -0,0 +1,6 @@
+(defmacro mk-subdirs-expr (path)
+  `(setq load-path
+         (delete-dups (append '(,path)
+                              ',(let ((default-directory path))
+                                  (normal-top-level-add-subdirs-to-load-path))
+                              load-path))))
diff --git a/nixpkgs/pkgs/build-support/emacs/package-build-dont-use-mtime.patch b/nixpkgs/pkgs/build-support/emacs/package-build-dont-use-mtime.patch
new file mode 100644
index 000000000000..1ace7771ea3a
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/package-build-dont-use-mtime.patch
@@ -0,0 +1,21 @@
+diff --git a/package-build.el b/package-build.el
+index 29cdb61..c19be1b 100644
+--- a/package-build.el
++++ b/package-build.el
+@@ -923,7 +923,6 @@ DIRECTORY is a temporary directory that contains the directory
+ that is put in the tarball."
+   (let* ((name (oref rcp name))
+          (version (oref rcp version))
+-         (time (oref rcp time))
+          (tar (expand-file-name (concat name "-" version ".tar")
+                                 package-build-archive-dir))
+          (dir (concat name "-" version)))
+@@ -939,7 +938,7 @@ that is put in the tarball."
+        ;; prevent a reproducible tarball as described at
+        ;; https://reproducible-builds.org/docs/archives.
+        "--sort=name"
+-       (format "--mtime=@%d" time)
++       "--mtime=@0"
+        "--owner=0" "--group=0" "--numeric-owner"
+        "--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime"))
+     (when (and package-build-verbose noninteractive)
diff --git a/nixpkgs/pkgs/build-support/emacs/trivial.nix b/nixpkgs/pkgs/build-support/emacs/trivial.nix
new file mode 100644
index 000000000000..11c28c0133e4
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/trivial.nix
@@ -0,0 +1,28 @@
+# trivial builder for Emacs packages
+
+{ callPackage, lib, ... }@envargs:
+
+args:
+
+callPackage ./generic.nix envargs ({
+  buildPhase = ''
+    runHook preBuild
+
+    emacs -L . --batch -f batch-byte-compile *.el
+
+    runHook postBuild
+  '';
+
+  installPhase = ''
+    runHook preInstall
+
+    LISPDIR=$out/share/emacs/site-lisp
+    install -d $LISPDIR
+    install *.el *.elc $LISPDIR
+    emacs --batch -l package --eval "(package-generate-autoloads \"${args.pname}\" \"$LISPDIR\")"
+
+    runHook postInstall
+  '';
+}
+
+// args)
diff --git a/nixpkgs/pkgs/build-support/emacs/wrapper.nix b/nixpkgs/pkgs/build-support/emacs/wrapper.nix
new file mode 100644
index 000000000000..59a694286d09
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/wrapper.nix
@@ -0,0 +1,236 @@
+/*
+
+# Usage
+
+`emacs.pkgs.withPackages` takes a single argument: a function from a package
+set to a list of packages (the packages that will be available in
+Emacs). For example,
+```
+emacs.pkgs.withPackages (epkgs: [ epkgs.evil epkgs.magit ])
+```
+All the packages in the list should come from the provided package
+set. It is possible to add any package to the list, but the provided
+set is guaranteed to have consistent dependencies and be built with
+the correct version of Emacs.
+
+# Overriding
+
+`emacs.pkgs.withPackages` inherits the package set which contains it, so the
+correct way to override the provided package set is to override the
+set which contains `emacs.pkgs.withPackages`. For example, to override
+`emacs.pkgs.emacs.pkgs.withPackages`,
+```
+let customEmacsPackages =
+      emacs.pkgs.overrideScope (self: super: {
+        # use a custom version of emacs
+        emacs = ...;
+        # use the unstable MELPA version of magit
+        magit = self.melpaPackages.magit;
+      });
+in customEmacsPackages.withPackages (epkgs: [ epkgs.evil epkgs.magit ])
+```
+
+*/
+
+{ lib, lndir, makeBinaryWrapper, runCommand, gcc }:
+self:
+let
+  inherit (self) emacs;
+  withNativeCompilation = emacs.withNativeCompilation or false;
+  withTreeSitter = emacs.withTreeSitter or false;
+in
+packagesFun: # packages explicitly requested by the user
+let
+  explicitRequires =
+    if lib.isFunction packagesFun
+    then packagesFun self
+    else packagesFun;
+in
+runCommand
+  (lib.appendToName "with-packages" emacs).name
+  {
+    inherit emacs explicitRequires;
+    nativeBuildInputs = [ emacs lndir makeBinaryWrapper ];
+
+    preferLocalBuild = true;
+    allowSubstitutes = false;
+
+    # Store all paths we want to add to emacs here, so that we only need to add
+    # one path to the load lists
+    deps = runCommand "emacs-packages-deps"
+      ({
+        inherit explicitRequires lndir emacs;
+        nativeBuildInputs = lib.optional withNativeCompilation gcc;
+      } // lib.optionalAttrs withNativeCompilation {
+        inherit (emacs) LIBRARY_PATH;
+      })
+      ''
+        findInputsOld() {
+          local pkg="$1"; shift
+          local var="$1"; shift
+          local propagatedBuildInputsFiles=("$@")
+
+          # TODO(@Ericson2314): Restore using associative array once Darwin
+          # nix-shell doesn't use impure bash. This should replace the O(n)
+          # case with an O(1) hash map lookup, assuming bash is implemented
+          # well :D.
+          local varSlice="$var[*]"
+          # ''${..-} to hack around old bash empty array problem
+          case "''${!varSlice-}" in
+              *" $pkg "*) return 0 ;;
+          esac
+          unset -v varSlice
+
+          eval "$var"'+=("$pkg")'
+
+          if ! [ -e "$pkg" ]; then
+              echo "build input $pkg does not exist" >&2
+              exit 1
+          fi
+
+          local file
+          for file in "''${propagatedBuildInputsFiles[@]}"; do
+              file="$pkg/nix-support/$file"
+              [[ -f "$file" ]] || continue
+
+              local pkgNext
+              for pkgNext in $(< "$file"); do
+                  findInputsOld "$pkgNext" "$var" "''${propagatedBuildInputsFiles[@]}"
+              done
+          done
+        }
+        mkdir -p $out/bin
+        mkdir -p $out/share/emacs/site-lisp
+        ${lib.optionalString withNativeCompilation ''
+          mkdir -p $out/share/emacs/native-lisp
+        ''}
+        ${lib.optionalString withTreeSitter ''
+          mkdir -p $out/lib
+        ''}
+
+        local requires
+        for pkg in $explicitRequires; do
+          findInputsOld $pkg requires propagated-user-env-packages
+        done
+        # requires now holds all requested packages and their transitive dependencies
+
+        linkPath() {
+          local pkg=$1
+          local origin_path=$2
+          local dest_path=$3
+
+          # Add the path to the search path list, but only if it exists
+          if [[ -d "$pkg/$origin_path" ]]; then
+            $lndir/bin/lndir -silent "$pkg/$origin_path" "$out/$dest_path"
+          fi
+        }
+
+        linkEmacsPackage() {
+          linkPath "$1" "bin" "bin"
+          linkPath "$1" "share/emacs/site-lisp" "share/emacs/site-lisp"
+          ${lib.optionalString withNativeCompilation ''
+            linkPath "$1" "share/emacs/native-lisp" "share/emacs/native-lisp"
+          ''}
+          ${lib.optionalString withTreeSitter ''
+            linkPath "$1" "lib" "lib"
+          ''}
+        }
+
+        # Iterate over the array of inputs (avoiding nix's own interpolation)
+        for pkg in "''${requires[@]}"; do
+          linkEmacsPackage $pkg
+        done
+
+        siteStart="$out/share/emacs/site-lisp/site-start.el"
+        siteStartByteCompiled="$siteStart"c
+        subdirs="$out/share/emacs/site-lisp/subdirs.el"
+        subdirsByteCompiled="$subdirs"c
+
+        # A dependency may have brought the original siteStart or subdirs, delete
+        # it and create our own
+        # Begin the new site-start.el by loading the original, which sets some
+        # NixOS-specific paths. Paths are searched in the reverse of the order
+        # they are specified in, so user and system profile paths are searched last.
+        #
+        # NOTE: Avoid displaying messages early at startup by binding
+        # inhibit-message to t. This would prevent the Emacs GUI from showing up
+        # prematurely. The messages would still be logged to the *Messages*
+        # buffer.
+        rm -f $siteStart $siteStartByteCompiled $subdirs $subdirsByteCompiled
+        cat >"$siteStart" <<EOF
+        (let ((inhibit-message t))
+          (load "$emacs/share/emacs/site-lisp/site-start"))
+        ;; "$out/share/emacs/site-lisp" is added to load-path in wrapper.sh
+        ;; "$out/share/emacs/native-lisp" is added to native-comp-eln-load-path in wrapper.sh
+        (add-to-list 'exec-path "$out/bin")
+        ${lib.optionalString withTreeSitter ''
+          (add-to-list 'treesit-extra-load-path "$out/lib/")
+        ''}
+        EOF
+
+        # Generate a subdirs.el that statically adds all subdirectories to load-path.
+        $emacs/bin/emacs \
+          --batch \
+          --load ${./mk-wrapper-subdirs.el} \
+          --eval "(prin1 (macroexpand-1 '(mk-subdirs-expr \"$out/share/emacs/site-lisp\")))" \
+          > "$subdirs"
+
+        # Byte-compiling improves start-up time only slightly, but costs nothing.
+        $emacs/bin/emacs --batch -f batch-byte-compile "$siteStart" "$subdirs"
+
+        ${lib.optionalString withNativeCompilation ''
+          $emacs/bin/emacs --batch \
+            --eval "(add-to-list 'native-comp-eln-load-path \"$out/share/emacs/native-lisp/\")" \
+            -f batch-native-compile "$siteStart" "$subdirs"
+        ''}
+      '';
+
+    inherit (emacs) meta;
+  }
+  ''
+    mkdir -p "$out/bin"
+
+    # Wrap emacs and friends so they find our site-start.el before the original.
+    for prog in $emacs/bin/*; do # */
+      local progname=$(basename "$prog")
+      rm -f "$out/bin/$progname"
+
+      substitute ${./wrapper.sh} $out/bin/$progname \
+        --subst-var-by bash ${emacs.stdenv.shell} \
+        --subst-var-by wrapperSiteLisp "$deps/share/emacs/site-lisp" \
+        --subst-var-by wrapperSiteLispNative "$deps/share/emacs/native-lisp" \
+        --subst-var prog
+      chmod +x $out/bin/$progname
+      # Create a “NOP” binary wrapper for the pure sake of it becoming a
+      # non-shebang, actual binary. See the makeBinaryWrapper docs for rationale
+      # (summary: it allows you to use emacs as a shebang itself on Darwin,
+      # e.g. #!$ {emacs}/bin/emacs --script)
+      wrapProgramBinary $out/bin/$progname
+    done
+
+    # Wrap MacOS app
+    # this has to pick up resources and metadata
+    # to recognize it as an "app"
+    if [ -d "$emacs/Applications/Emacs.app" ]; then
+      mkdir -p $out/Applications/Emacs.app/Contents/MacOS
+      cp -r $emacs/Applications/Emacs.app/Contents/Info.plist \
+            $emacs/Applications/Emacs.app/Contents/PkgInfo \
+            $emacs/Applications/Emacs.app/Contents/Resources \
+            $out/Applications/Emacs.app/Contents
+
+
+      substitute ${./wrapper.sh} $out/Applications/Emacs.app/Contents/MacOS/Emacs \
+        --subst-var-by bash ${emacs.stdenv.shell} \
+        --subst-var-by wrapperSiteLisp "$deps/share/emacs/site-lisp" \
+        --subst-var-by wrapperSiteLispNative "$deps/share/emacs/native-lisp" \
+        --subst-var-by prog "$emacs/Applications/Emacs.app/Contents/MacOS/Emacs"
+      chmod +x $out/Applications/Emacs.app/Contents/MacOS/Emacs
+      wrapProgramBinary $out/Applications/Emacs.app/Contents/MacOS/Emacs
+    fi
+
+    mkdir -p $out/share
+    # Link icons and desktop files into place
+    for dir in applications icons info man; do
+      ln -s $emacs/share/$dir $out/share/$dir
+    done
+  ''
diff --git a/nixpkgs/pkgs/build-support/emacs/wrapper.sh b/nixpkgs/pkgs/build-support/emacs/wrapper.sh
new file mode 100644
index 000000000000..44762bd4582b
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/emacs/wrapper.sh
@@ -0,0 +1,53 @@
+#!@bash@
+
+IFS=:
+
+newLoadPath=()
+newNativeLoadPath=()
+addedNewLoadPath=
+addedNewNativeLoadPath=
+
+if [[ -n $EMACSLOADPATH ]]
+then
+    while read -rd: entry
+    do
+        if [[ -z $entry && -z $addedNewLoadPath ]]
+        then
+            newLoadPath+=(@wrapperSiteLisp@)
+            addedNewLoadPath=1
+        fi
+        newLoadPath+=("$entry")
+    done <<< "$EMACSLOADPATH:"
+else
+    newLoadPath+=(@wrapperSiteLisp@)
+    newLoadPath+=("")
+fi
+
+# NOTE: Even though we treat EMACSNATIVELOADPATH like EMACSLOADPATH in
+# this wrapper, empty elements in EMACSNATIVELOADPATH have no special
+# meaning for Emacs.  Only non-empty elements in EMACSNATIVELOADPATH
+# will be prepended to native-comp-eln-load-path.
+# https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/startup.el?id=3685387e609753293c4518be75e77c659c3b2d8d#n599
+if [[ -n $EMACSNATIVELOADPATH ]]
+then
+    while read -rd: entry
+    do
+        if [[ -z $entry && -z $addedNewNativeLoadPath ]]
+        then
+            newNativeLoadPath+=(@wrapperSiteLispNative@)
+            addedNewNativeLoadPath=1
+        fi
+        newNativeLoadPath+=("$entry")
+    done <<< "$EMACSNATIVELOADPATH:"
+else
+    newNativeLoadPath+=(@wrapperSiteLispNative@)
+    newNativeLoadPath+=("")
+fi
+
+export EMACSLOADPATH="${newLoadPath[*]}"
+export emacsWithPackages_siteLisp=@wrapperSiteLisp@
+
+export EMACSNATIVELOADPATH="${newNativeLoadPath[*]}"
+export emacsWithPackages_siteLispNative=@wrapperSiteLispNative@
+
+exec @prog@ "$@"