about summary refs log tree commit diff
path: root/pkgs/build-support/emacs/wrapper.nix
blob: ecfcc0cd52c5f7d165478164b682c0514bb9d825 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/*

# 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, makeWrapper, runCommand, gcc }:
self:
let
  inherit (self) emacs;
  withNativeCompilation = emacs.withNativeCompilation or emacs.nativeComp or false;
  withTreeSitter = emacs.withTreeSitter or emacs.treeSitter 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 makeWrapper ];

    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-file "$emacs/share/emacs/site-lisp/site-start.el"))
        (add-to-list 'load-path "$out/share/emacs/site-lisp")
        (add-to-list 'exec-path "$out/bin")
        ${lib.optionalString withNativeCompilation ''
          (add-to-list 'native-comp-eln-load-path "$out/share/emacs/native-lisp/")
        ''}
        ${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
    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
    fi

    mkdir -p $out/share
    # Link icons and desktop files into place
    for dir in applications icons info man emacs; do
      ln -s $emacs/share/$dir $out/share/$dir
    done
  ''