about summary refs log tree commit diff
path: root/pkgs/development/web/nodejs/build-node-package.nix
blob: e385c0d40c44c9032bf4cf99c1fe086522c69218 (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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
{ stdenv, runCommand, nodejs, neededNatives}:

{
  name, src,

  # Node package name
  pkgName ? (builtins.parseDrvName name).name,

  # List or attribute set of dependencies
  deps ? {},

  # List or attribute set of peer depencies
  peerDependencies ? [],

  # Whether package is binary or library
  bin ? null,

  # Flags passed to npm install
  flags ? [],

  # Command to be run before shell hook
  preShellHook ? "",

  # Command to be run after shell hook
  postShellHook ? "",

  # Attribute set of already resolved deps (internal),
  # for avoiding infinite recursion
  resolvedDeps ? {},

  ...
} @ args:

with stdenv.lib;

let
  npmFlags = concatStringsSep " " (map (v: "--${v}") flags);

  sources = runCommand "node-sources" {} ''
    tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
    mv *node* $out
  '';

  # Convert deps to attribute set
  attrDeps = if isAttrs deps then deps else
    (listToAttrs (map (dep: nameValuePair dep.name dep) deps));

  # All required node modules, without already resolved dependencies
  requiredDeps = removeAttrs attrDeps (attrNames resolvedDeps);

  # Recursive dependencies that we want to avoid with shim creation
  recursiveDeps = removeAttrs attrDeps (attrNames requiredDeps);

  peerDeps = filter (dep: dep.pkgName != pkgName) peerDependencies;

  self = let
    # Pass resolved dependencies to dependencies of this package
    deps = map (
      dep: dep.override {
        resolvedDeps = resolvedDeps // { "${name}" = self; };
      }
    ) (attrValues requiredDeps);

    patchShebangs = dir: ''
        node=`type -p node`
        coffee=`type -p coffee || true`
        find -L ${dir} -type f -print0 | \
        xargs -0 sed --follow-symlinks -i \
            -e 's@#!/usr/bin/env node@#!'"$node"'@' \
            -e 's@#!/usr/bin/env coffee@#!'"$coffee"'@' \
            -e 's@#!/.*/node@#!'"$node"'@' \
            -e 's@#!/.*/coffee@#!'"$coffee"'@' || true
    '';

  in stdenv.mkDerivation ({
    inherit src;

    configurePhase = ''
      runHook preConfigure

      ${patchShebangs "./"}

      # Some version specifiers (latest, unstable, URLs, file paths) force NPM
      # to make remote connections or consult paths outside the Nix store.
      # The following JavaScript replaces these by * to prevent that:
      # Also some packages require a specific npm version because npm may
      # resovle dependencies differently, but npm is not used by Nix for dependency
      # reslution, so these requirements are dropped.

      (
      cat <<EOF
        var fs = require('fs');
        var url = require('url');

        /*
        * Replaces an impure version specification by *
        */
        function replaceImpureVersionSpec(versionSpec) {
            var parsedUrl = url.parse(versionSpec);

            if(versionSpec == "latest" || versionSpec == "unstable" ||
                versionSpec.substr(0, 2) == ".." || dependency.substr(0, 2) == "./" || dependency.substr(0, 2) == "~/" || dependency.substr(0, 1) == '/' || /^[^/]+\/[^/]+$/.test(versionSpec))
                return '*';
            else if(parsedUrl.protocol == "git:" || parsedUrl.protocol == "git+ssh:" || parsedUrl.protocol == "git+http:" || parsedUrl.protocol == "git+https:" ||
                parsedUrl.protocol == "http:" || parsedUrl.protocol == "https:")
                return '*';
            else
                return versionSpec;
        }

        var packageObj = JSON.parse(fs.readFileSync('./package.json'));

        /* Replace dependencies */
        if(packageObj.dependencies !== undefined) {
            for(var dependency in packageObj.dependencies) {
                var versionSpec = packageObj.dependencies[dependency];
                packageObj.dependencies[dependency] = replaceImpureVersionSpec(versionSpec);
            }
        }

        /* Replace development dependencies */
        if(packageObj.devDependencies !== undefined) {
            for(var dependency in packageObj.devDependencies) {
                var versionSpec = packageObj.devDependencies[dependency];
                packageObj.devDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
            }
        }

        /* Replace optional dependencies */
        if(packageObj.optionalDependencies !== undefined) {
            for(var dependency in packageObj.optionalDependencies) {
                var versionSpec = packageObj.optionalDependencies[dependency];
                packageObj.optionalDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
            }
        }

        /* Ignore npm version requirement */
        if(packageObj.engines) {
            delete packageObj.engines.npm;
        }

        /* Write the fixed JSON file */
        fs.writeFileSync("package.json", JSON.stringify(packageObj));
      EOF
      ) | node

      # We do not handle shrinkwraps yet
      rm npm-shrinkwrap.json 2>/dev/null || true

      mkdir build-dir
      (
        cd build-dir
        mkdir node_modules

        # Symlink or copy dependencies for node modules
        # copy is needed if dependency has recursive dependencies,
        # because node can't follow symlinks while resolving recursive deps.
        ${concatMapStrings (dep:
          if dep.recursiveDeps == [] then ''
            ln -sv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
          '' else ''
            cp -R ${dep}/lib/node_modules/${dep.pkgName} node_modules/
          ''
        ) deps}

        # Symlink peer dependencies
        ${concatMapStrings (dep: ''
          ln -sv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
        '') peerDeps}

        # Create shims for recursive dependenceies
        ${concatMapStrings (dep: ''
          mkdir -p node_modules/${dep.pkgName}
          cat > node_modules/${dep.pkgName}/package.json <<EOF
          {
              "name": "${dep.pkgName}",
              "version": "${getVersion dep}"
          }
          EOF
        '') (attrValues recursiveDeps)}
      )

      export HOME=$PWD/build-dir
      runHook postConfigure
    '';

    buildPhase = ''
      runHook preBuild

      # If source was a file, repackage it, so npm pre/post publish hooks are not triggered,
      if [[ -f $src ]]; then
        tar --exclude='build-dir' -czf build-dir/package.tgz ./
        export src=$HOME/package.tgz
      else
        export src=$PWD
      fi

      # Install package
      (cd $HOME && npm --registry http://www.example.com --nodedir=${sources} install $src ${npmFlags})

      runHook postBuild
    '';

    installPhase = ''
      runHook preInstall

      (
        cd $HOME

        # Remove shims
        ${concatMapStrings (dep: ''
          rm node_modules/${dep.pkgName}/package.json
          rmdir node_modules/${dep.pkgName}
        '') (attrValues recursiveDeps)}

        mkdir -p $out/lib/node_modules

        # Install manual
        mv node_modules/${pkgName} $out/lib/node_modules
        rm -fR $out/lib/node_modules/${pkgName}/node_modules
        cp -r node_modules $out/lib/node_modules/${pkgName}/node_modules

        if [ -e "$out/lib/node_modules/${pkgName}/man" ]; then
          mkdir -p $out/share
          for dir in "$out/lib/node_modules/${pkgName}/man/"*; do
            mkdir -p $out/share/man/$(basename "$dir")
            for page in "$dir"/*; do
              ln -sv $page $out/share/man/$(basename "$dir")
            done
          done
        fi

        # Symlink dependencies
        ${concatMapStrings (dep: ''
          mv node_modules/${dep.pkgName} $out/lib/node_modules
        '') peerDeps}

        # Install binaries and patch shebangs
        mv node_modules/.bin $out/lib/node_modules 2>/dev/null || true
        if [ -d "$out/lib/node_modules/.bin" ]; then
          ln -sv $out/lib/node_modules/.bin $out/bin
          ${patchShebangs "$out/lib/node_modules/.bin/*"}
        fi
      )

      runHook postInstall
    '';

    preFixup = ''
      find $out -type f -print0 | xargs -0 sed -i 's|${src}|${src.name}|g'
    '';

    shellHook = ''
      ${preShellHook}
      export PATH=${nodejs}/bin:$(pwd)/node_modules/.bin:$PATH
      mkdir -p node_modules
      ${concatMapStrings (dep: ''
        ln -sfv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
      '') deps}
      ${postShellHook}
    '';

    passthru.pkgName = pkgName;
  } // (filterAttrs (n: v: n != "deps" && n != "resolvedDeps") args) // {
    name = "${
      if bin == true then "bin-" else if bin == false then "node-" else ""
    }${name}";

    # Run the node setup hook when this package is a build input
    propagatedNativeBuildInputs = (args.propagatedNativeBuildInputs or []) ++ [ nodejs ];

    # Make buildNodePackage useful with --run-env
    nativeBuildInputs = (args.nativeBuildInputs or []) ++ deps ++ peerDependencies ++ neededNatives;

    # Expose list of recursive dependencies upstream, up to the package that
    # caused recursive dependency
    recursiveDeps = (flatten (map (d: remove name d.recursiveDeps) deps)) ++ (attrNames recursiveDeps);
  });

in self