1/*
2
3# Usage
4
5`emacs.pkgs.withPackages` takes a single argument: a function from a package
6set to a list of packages (the packages that will be available in
7Emacs). For example,
8```
9emacs.pkgs.withPackages (epkgs: [ epkgs.evil epkgs.magit ])
10```
11All the packages in the list should come from the provided package
12set. It is possible to add any package to the list, but the provided
13set is guaranteed to have consistent dependencies and be built with
14the correct version of Emacs.
15
16# Overriding
17
18`emacs.pkgs.withPackages` inherits the package set which contains it, so the
19correct way to override the provided package set is to override the
20set which contains `emacs.pkgs.withPackages`. For example, to override
21`emacs.pkgs.emacs.pkgs.withPackages`,
22```
23let customEmacsPackages =
24 emacs.pkgs.overrideScope' (self: super: {
25 # use a custom version of emacs
26 emacs = ...;
27 # use the unstable MELPA version of magit
28 magit = self.melpaPackages.magit;
29 });
30in customEmacsPackages.withPackages (epkgs: [ epkgs.evil epkgs.magit ])
31```
32
33*/
34
35{ lib, lndir, makeWrapper, runCommand, gcc }: self:
36
37with lib;
38
39let
40
41 inherit (self) emacs;
42
43 nativeComp = emacs.nativeComp or false;
44
45 treeSitter = emacs.treeSitter or false;
46
47in
48
49packagesFun: # packages explicitly requested by the user
50
51let
52 explicitRequires =
53 if lib.isFunction packagesFun
54 then packagesFun self
55 else packagesFun;
56in
57
58runCommand
59 (appendToName "with-packages" emacs).name
60 {
61 nativeBuildInputs = [ emacs lndir makeWrapper ];
62 inherit emacs explicitRequires;
63
64 preferLocalBuild = true;
65 allowSubstitutes = false;
66
67 # Store all paths we want to add to emacs here, so that we only need to add
68 # one path to the load lists
69 deps = runCommand "emacs-packages-deps"
70 ({
71 inherit explicitRequires lndir emacs;
72 nativeBuildInputs = lib.optional nativeComp gcc;
73 } // lib.optionalAttrs nativeComp {
74 inherit (emacs) LIBRARY_PATH;
75 })
76 ''
77 findInputsOld() {
78 local pkg="$1"; shift
79 local var="$1"; shift
80 local propagatedBuildInputsFiles=("$@")
81
82 # TODO(@Ericson2314): Restore using associative array once Darwin
83 # nix-shell doesn't use impure bash. This should replace the O(n)
84 # case with an O(1) hash map lookup, assuming bash is implemented
85 # well :D.
86 local varSlice="$var[*]"
87 # ''${..-} to hack around old bash empty array problem
88 case "''${!varSlice-}" in
89 *" $pkg "*) return 0 ;;
90 esac
91 unset -v varSlice
92
93 eval "$var"'+=("$pkg")'
94
95 if ! [ -e "$pkg" ]; then
96 echo "build input $pkg does not exist" >&2
97 exit 1
98 fi
99
100 local file
101 for file in "''${propagatedBuildInputsFiles[@]}"; do
102 file="$pkg/nix-support/$file"
103 [[ -f "$file" ]] || continue
104
105 local pkgNext
106 for pkgNext in $(< "$file"); do
107 findInputsOld "$pkgNext" "$var" "''${propagatedBuildInputsFiles[@]}"
108 done
109 done
110 }
111 mkdir -p $out/bin
112 mkdir -p $out/share/emacs/site-lisp
113 ${optionalString nativeComp ''
114 mkdir -p $out/share/emacs/native-lisp
115 ''}
116 ${optionalString treeSitter ''
117 mkdir -p $out/lib
118 ''}
119
120 local requires
121 for pkg in $explicitRequires; do
122 findInputsOld $pkg requires propagated-user-env-packages
123 done
124 # requires now holds all requested packages and their transitive dependencies
125
126 linkPath() {
127 local pkg=$1
128 local origin_path=$2
129 local dest_path=$3
130
131 # Add the path to the search path list, but only if it exists
132 if [[ -d "$pkg/$origin_path" ]]; then
133 $lndir/bin/lndir -silent "$pkg/$origin_path" "$out/$dest_path"
134 fi
135 }
136
137 linkEmacsPackage() {
138 linkPath "$1" "bin" "bin"
139 linkPath "$1" "share/emacs/site-lisp" "share/emacs/site-lisp"
140 ${optionalString nativeComp ''
141 linkPath "$1" "share/emacs/native-lisp" "share/emacs/native-lisp"
142 ''}
143 ${optionalString treeSitter ''
144 linkPath "$1" "lib" "lib"
145 ''}
146 }
147
148 # Iterate over the array of inputs (avoiding nix's own interpolation)
149 for pkg in "''${requires[@]}"; do
150 linkEmacsPackage $pkg
151 done
152
153 siteStart="$out/share/emacs/site-lisp/site-start.el"
154 siteStartByteCompiled="$siteStart"c
155 subdirs="$out/share/emacs/site-lisp/subdirs.el"
156 subdirsByteCompiled="$subdirs"c
157
158 # A dependency may have brought the original siteStart or subdirs, delete
159 # it and create our own
160 # Begin the new site-start.el by loading the original, which sets some
161 # NixOS-specific paths. Paths are searched in the reverse of the order
162 # they are specified in, so user and system profile paths are searched last.
163 #
164 # NOTE: Avoid displaying messages early at startup by binding
165 # inhibit-message to t. This would prevent the Emacs GUI from showing up
166 # prematurely. The messages would still be logged to the *Messages*
167 # buffer.
168 rm -f $siteStart $siteStartByteCompiled $subdirs $subdirsByteCompiled
169 cat >"$siteStart" <<EOF
170 (let ((inhibit-message t))
171 (load-file "$emacs/share/emacs/site-lisp/site-start.el"))
172 (add-to-list 'load-path "$out/share/emacs/site-lisp")
173 (add-to-list 'exec-path "$out/bin")
174 ${optionalString nativeComp ''
175 (add-to-list 'native-comp-eln-load-path "$out/share/emacs/native-lisp/")
176 ''}
177 ${optionalString treeSitter ''
178 (add-to-list 'treesit-extra-load-path "$out/lib/")
179 ''}
180 EOF
181
182 # Generate a subdirs.el that statically adds all subdirectories to load-path.
183 $emacs/bin/emacs \
184 --batch \
185 --load ${./mk-wrapper-subdirs.el} \
186 --eval "(prin1 (macroexpand-1 '(mk-subdirs-expr \"$out/share/emacs/site-lisp\")))" \
187 > "$subdirs"
188
189 # Byte-compiling improves start-up time only slightly, but costs nothing.
190 $emacs/bin/emacs --batch -f batch-byte-compile "$siteStart" "$subdirs"
191
192 ${optionalString nativeComp ''
193 $emacs/bin/emacs --batch \
194 --eval "(add-to-list 'native-comp-eln-load-path \"$out/share/emacs/native-lisp/\")" \
195 -f batch-native-compile "$siteStart" "$subdirs"
196 ''}
197 '';
198
199 inherit (emacs) meta;
200 }
201 ''
202 mkdir -p "$out/bin"
203
204 # Wrap emacs and friends so they find our site-start.el before the original.
205 for prog in $emacs/bin/*; do # */
206 local progname=$(basename "$prog")
207 rm -f "$out/bin/$progname"
208
209 substitute ${./wrapper.sh} $out/bin/$progname \
210 --subst-var-by bash ${emacs.stdenv.shell} \
211 --subst-var-by wrapperSiteLisp "$deps/share/emacs/site-lisp" \
212 --subst-var-by wrapperSiteLispNative "$deps/share/emacs/native-lisp:" \
213 --subst-var prog
214 chmod +x $out/bin/$progname
215 done
216
217 # Wrap MacOS app
218 # this has to pick up resources and metadata
219 # to recognize it as an "app"
220 if [ -d "$emacs/Applications/Emacs.app" ]; then
221 mkdir -p $out/Applications/Emacs.app/Contents/MacOS
222 cp -r $emacs/Applications/Emacs.app/Contents/Info.plist \
223 $emacs/Applications/Emacs.app/Contents/PkgInfo \
224 $emacs/Applications/Emacs.app/Contents/Resources \
225 $out/Applications/Emacs.app/Contents
226
227
228 substitute ${./wrapper.sh} $out/Applications/Emacs.app/Contents/MacOS/Emacs \
229 --subst-var-by bash ${emacs.stdenv.shell} \
230 --subst-var-by wrapperSiteLisp "$deps/share/emacs/site-lisp" \
231 --subst-var-by wrapperSiteLispNative "$deps/share/emacs/native-lisp:" \
232 --subst-var-by prog "$emacs/Applications/Emacs.app/Contents/MacOS/Emacs"
233 chmod +x $out/Applications/Emacs.app/Contents/MacOS/Emacs
234 fi
235
236 mkdir -p $out/share
237 # Link icons and desktop files into place
238 for dir in applications icons info man emacs; do
239 ln -s $emacs/share/$dir $out/share/$dir
240 done
241 ''