forked from adnelson/nix-node-packages
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuildNodePackage.nix
More file actions
597 lines (518 loc) · 23.3 KB
/
buildNodePackage.nix
File metadata and controls
597 lines (518 loc) · 23.3 KB
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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
{
# System packages.
pkgs,
# Derivation for nodejs and npm.
nodejs,
# Which version of npm to use.
npm ? nodejs,
# Self-reference for overriding purposes.
buildNodePackage,
# Provides xcode binaries to OSX builds (for native packages).
xcode-wrapper,
# Scripts that we use during the npm builds.
node-build-tools,
# C header files for node libraries
nodejsSources,
}:
let
inherit (pkgs) stdenv python2 file darwin;
inherit (pkgs.lib) showVal optional foldl;
inherit (stdenv.lib) fold removePrefix hasPrefix subtractLists flip isList
intersectLists isAttrs listToAttrs nameValuePair hasAttr
mapAttrs filterAttrs attrNames elem concatMapStrings
attrValues concatStringsSep optionalString filter
optionalAttrs optionals;
# Join a list of strings with newlines, filtering out empty lines.
joinLines = strings: concatStringsSep "\n" (filter (s: s != "") strings);
# Map a function and concatenate with newlines.
concatMapLines = list: func: joinLines (map func list);
# Create a tar wrapper that filters all the 'Ignoring unknown
# extended header keyword' noise
#
# Cribbed from nixpkgs/pkgs/development/node-packages/node-env.nix
tarWrapper = pkgs.runCommand "tarWrapper" {} ''
mkdir -p $out/bin
cat > $out/bin/tar <<EOF
#! ${pkgs.stdenv.shell} -e
$(type -p tar) "\$@" --warning=no-unknown-keyword
EOF
chmod +x $out/bin/tar
'';
# Checks a derivation's structure; if it doesn't have certain attributes then
# it isn't a node package and we error. Otherwise return the package.
verifyNodePackage = pkg:
if !builtins.hasAttr "namespace" pkg
then throw ''
Package dependency does not appear to be a node package: ${showVal pkg}.
Use "buildInputs" or "propagatedBuildInputs" for non-node dependencies,
instead of "deps".
''
else pkg;
# By default, when checking we'll run npm test.
defaultCheckPhase = ''
runHook preCheck
npm test
runHook postCheck
'';
# Convert a list of derivations into an attribute set. Ensure
# everything in the set is a node package. Keys of the set will be
# package names, and values will be the packages.
toAttrSet = obj:
if isAttrs obj then obj
else if obj == null then {}
else listToAttrs (map (x: nameValuePair x.name x)
(map verifyNodePackage obj));
in
{
# The name of the package. If it's a private package with a namespace,
# this should not contain the namespace.
name,
# Used for private packages. Indicated in the name field of the
# package.json, e.g. if the name in that file is "@mynamespace/mypackage",
# then the namespace is 'mynamespace' and the name is 'mypackage'. Public
# packages will not need this.
namespace ? null,
# Version of the package. This should follow the semver standard, although
# we don't explicitly enforce that in this function.
version,
# Source of the package; can be a tarball or a folder on the filesystem.
src,
# The suffix to the name as it appears in `nix-env -q` and the nix store. By
# default, the name of nodejs interpreter e.g:
# "<package name>-<package version>-nodejs-<nodejs version>"
nameSuffix ? "-${nodejs.name}",
# If there's a namespace, by default it will be prepended to the package
# name. Otherwise, a prefix can be given explicitly.
namePrefix ? (if namespace == null then "" else "=${namespace}=-"),
# List of (runtime) dependencies.
deps ? [],
# List of runtime dependencies which are circular, meaning that the
# package being defined here occurs somewhere in its own dependency
# tree.
circularDependencies ? [],
# List of optional dependencies.
optionalDependencies ? [],
# List or set of development dependencies (or null). These will only be
# installed when `includeDevDependencies` is true, which is provided by
# the `.env` attribute.
devDependencies ? null,
# If true and devDependencies are defined, the package will only be
# installed contingent on successfully running tests.
doCheck ? false,
# If true, devDependencies will be added to the packages to the
# build environment. By default, this is true whenever doCheck is true.
includeDevDependencies ? doCheck,
# Bash command to run package tests.
checkPhase ? defaultCheckPhase,
# Build inputs to propagate in addition to nodejs and non-dev dependencies.
propagatedBuildInputs ? [],
# Build inputs in addition to npm and dev dependencies.
buildInputs ? [],
# Whether to strip debugging symbols from binaries.
# This normally shouldn't be necessary but it can be enabled if desired.
# Doc for details: https://nixos.org/wiki/NixPkgs_Standard_Environment.
dontStrip ? true,
# Optional attributes to pass through to downstream derivations.
passthru ? {},
# A set of dependencies to patch, changing the version given in the
# package.json. Keys are dependency names, values are new
# versions. Alternatively, a value can be `null`, which will have
# the effect of removing the dependency from the package.json.
patchDependencies ? {},
# We attempt to automatically remove dev dependencies from the node_modules
# folder prior to copying to the nix store. If this isn't desired (for
# example, custom behavior is needed), then set this to true.
skipDevDependencyCleanup ? false,
# Indicates the package is broken. Not super user-friendly but
# better than nothing.
isBroken ? false,
# Metadata about the package.
meta ? {},
# Build step
buildStep ? "execute-install-scripts",
# Overrides to the arguments to mkDerivation. This can be used to
# set custom values for the arguments that buildNodePackage would
# set, so it's only necessary for a certain set of keys (everything
# else can just be passed in directly).
derivationOverrides ? {},
# Any remaining flags are passed through to mkDerivation.
...
} @ args:
let
# The package name as it appears in the package.json. This contains a
# namespace if there is one, so it will be a distinct identifier for
# different packages.
fullName = if namespace == null then name else "@${namespace}/${name}";
# The package name with a version appended. This should be unique amongst
# all packages.
uniqueName = "${fullName}@${version}";
in
# Dev dependencies are required to be installed to run unit tests for
# nearly all packages. Therefore we require that they be installed in
# order to enable tests.
if doCheck && (devDependencies == null)
then throw ("${uniqueName}: Can't run tests because devDependencies have " +
"not been defined. You can pass in `devDependencies = [];` if " +
"there are no dev dependencies.")
else if includeDevDependencies && (devDependencies == null)
then throw ("${uniqueName}: Can't include dev dependencies since they have " +
"not been defined. You can pass in `devDependencies = [];` if " +
"there are no dev dependencies.")
else if isBroken
then throw "${uniqueName}: listed as broken, see definition for details"
else
let
# Types of npm dependencies as they appear as keys in a package.json file.
dependencyTypes = ["dependencies" "devDependencies" "optionalDependencies"];
# These arguments are intended as directives to this function and not
# to be passed through to mkDerivation. They are removed below.
attrsToRemove = ["deps" "flags" "isBroken"
"passthru" "doCheck" "includeDevDependencies" "version"
"namespace" "skipDevDependencyCleanup" "patchDependencies"
"circularDependencies" "derivationOverrides"] ++ dependencyTypes;
# We create a `self` object for self-referential expressions. It
# bottoms out in a call to `mkDerivation` at the end.
self = let
# Set of normal dependencies.
_dependencies = toAttrSet deps;
# Set of circular dependencies.
_circularDependencies = toAttrSet circularDependencies;
# Since optional dependencies are optional, ignore the ones that fail
tryOrNull = p: let r = builtins.tryEval "${p}"; in if r.success then p else null;
_optionalDependencies =
if isList optionalDependencies
then toAttrSet (filter (x: x != null) (map tryOrNull optionalDependencies))
else if isAttrs optionalDependencies
then toAttrSet (filterAttrs (_: x: x != null) (mapAttrs (_: tryOrNull) optionalDependencies))
else toAttrSet optionalDependencies;
# Dev dependencies will only be included if requested.
_devDependencies = if !includeDevDependencies then {}
else toAttrSet devDependencies;
# Dependencies we need to propagate, meaning they need to be
# available to the package at runtime. We don't include the
# circular dependencies here, even though they might be needed at
# runtime, because we have a "special way" of building them.
runtimeDependencies = _dependencies // _optionalDependencies;
# Names of packages to keep when cleaning up dev dependencies. We
# put them in a dictionary for fast lookup, but the values are
# just null.
packagesToRetain = mapAttrs (_: _: null) (
runtimeDependencies // _circularDependencies);
# Required dependencies are those that we haven't filtered yet.
requiredDependencies = _devDependencies // runtimeDependencies;
patchPhase = joinLines [
"runHook prePatch"
"patchShebangs $PWD >/dev/null"
# Ensure that the package name matches what is in the package.json.
"check-package-json checkPackageName ${fullName}"
# Remove any impure dependencies from the package.json (see script
# for details). Apply patches in patchDependencies arguments.
"patch-dependencies"
# We do not handle shrinkwraps yet
"rm -fv npm-shrinkwrap.json"
(args.patchPhase or "")
"runHook postPatch"
];
# Computes the "circular closure" of a package.
# See ./circular_dependencies.md for details.
circularClosure =
# Packages we've already seen.
seenPackages:
# Name of package we're inspecting.
package:
if hasAttr package.name seenPackages
# We've completed the cycle; stop here.
then seenPackages
else let
# Add package to seen.
seen = seenPackages // {"${package.name}" = package;};
# Recur on circular dependencies.
closure = map (circularClosure seen)
(attrValues package.circularDependencies);
in
# Combine the results into a single set.
foldl (a: b: a // b) self.circularDependencies closure;
# Compute any cycles. Remove 'self' from the dependency closure.
circularDepClosure = removeAttrs (circularClosure {} self) [self.name];
# Turn the closure into a list of all circular dependencies.
circulars = attrValues circularDepClosure;
# All of the transitive dependencies (non-circular) of the
# circular packages.
transCircularDeps =
foldl (a: b: a // b) {} (map (p: p.runtimeDependencies) circulars);
configurePhase =
let
# Symlink dependencies for node modules.
link = dep: ''
if ! [[ -e node_modules/${dep.fullName} ]]; then
ln -sv ${dep.fullPath} ${dep.modulePath}
if [[ -d ${dep}/bin ]]; then
find -L ${dep}/bin -maxdepth 1 -type f -executable \
| while read exec_file; do
echo "Symlinking $exec_file binary to node_modules/.bin"
mkdir -p node_modules/.bin
ln -s $exec_file node_modules/.bin/$(basename $exec_file)
done
fi
fi
'';
in concatStringsSep "\n" (
["runHook preConfigure"] ++
(flip map (attrValues requiredDependencies) (dep:
# Create symlinks (or copies) of all of the required dependencies.
''
mkdir -p ${dep.modulePath}
${link dep}
'')) ++
["runHook postConfigure"] ++
(optional (circulars != []) (let
in concatStringsSep "\n" [
# Extract all of the circular dependencies' tarballs.
(concatMapLines circulars (dep: ''
echo Satisfying ${dep.fullName}, circular dependency \
of ${self.fullName}
mkdir -p node_modules
if [[ ! -d node_modules/${dep.fullName} ]]; then
tar xf ${dep.src}
if [[ ! -d package ]]; then
echo "Expected ${dep.src} to be a tarball containing a" \
"'package' directory. Don't know how to handle this :("
exit 1
fi
mkdir -p node_modules/${dep.fullName}
cp -rP package/. node_modules/${dep.fullName}
rm -rf ./package
fi
''))
# Symlink all of the transitive dependencies of the circular packages.
(concatMapLines (attrValues transCircularDeps) link)
# Create a temporary symlink to the current package directory,
# so that node knows that the dependency is satisfied when
# checking the recursive dependencies (grumble grumble).
"ln -s $PWD node_modules/${self.fullName}"
]
)));
buildPhase = concatStringsSep "\n" [
"runHook preBuild"
# Previous NODE_PATH should be empty, but it might have been set
# in the custom derivation steps.
"export NODE_PATH=$PWD/node_modules:$NODE_PATH"
"check-package-json checkDependencies"
buildStep
# If we have any circular dependencies, they will need to reference
# the current package at runtime. Make a symlink into the node modules
# folder which points at where the package will live in $out.
(optionalString (circulars != []) ''
rm node_modules/${self.fullName}
ln -s $out/lib/node_modules/${self.fullName} \
node_modules/${self.fullName}
'')
"runHook postBuild"
];
installPhase = ''
runHook preInstall
# Ensure that the main entry point appears post-build.
check-package-json checkMainEntryPoint
# Install the package that we just built.
mkdir -p $out/lib/${self.modulePath}
# Remove all of the dev dependencies which do not appear in other
# dependency sets.
${if skipDevDependencyCleanup then "" else
flip concatMapStrings (attrValues _devDependencies) (dep:
let
rm = dep:
if !hasAttr dep.name packagesToRetain
then ''
# Remove the dependency from node modules
rm -rfv node_modules/${dep.fullName}
# Remove any binaries it generated from node_modules/.bin
if [[ -d ${dep}/bin ]]; then
find -L ${dep}/bin -maxdepth 1 -type f -executable \
| while read exec_file; do
rm -fv node_modules/.bin/$(basename $exec_file)
done
fi
''
else ''
echo "Retaining ${dep.basicName} since it " \
"appears in the set of dependencies to propagate"
'';
in
rm dep)}
# Copy the folder that was created for this path to $out/lib.
cp -r $PWD $out/lib/node_modules/${self.fullName}
# Remove the node_modules subfolder from there, and instead put things
# in $PWD/node_modules into that folder.
if [ -e "$out/lib/node_modules/${self.fullName}/man" ]; then
echo -n "Linking manpages... "
NUM_MAN_PAGES=0
mkdir -p $out/share
for dir in $out/lib/node_modules/${self.fullName}/man/*; do #*/
mkdir -p $out/share/man/$(basename "$dir")
for page in $dir/*; do #*/
ln -s $page $out/share/man/$(basename "$dir")
NUM_MAN_PAGES=$(($NUM_MAN_PAGES + 1))
done
done
echo "linked $NUM_MAN_PAGES man pages."
fi
# Install binaries using the `bin` object in the package.json
install-binaries
runHook postInstall
'';
# These are the arguments that we will pass to `stdenv.mkDerivation`.
mkDerivationArgs = removeAttrs args attrsToRemove // {
inherit
buildPhase
checkPhase
configurePhase
doCheck
dontStrip
fullName
installPhase
meta
patchPhase
nodejsSources
src;
patchDependencies = builtins.toJSON patchDependencies;
NO_DEV_DEPENDENCIES = !includeDevDependencies;
# Tell mkDerivation to run `setVariables` prior to other phases.
prePhases = ["setVariables"];
# Define some environment variables that we will use in the build.
setVariables = ''
# In case this was set by an upstream derivation.
unset NODE_PATH
# This creates a string for this package which is unique but
# deterministic. We can use it to create temporary directories
# and URLs and be confident there will be no collisions.
HASHEDNAME=$(echo "$propagatedNativeBuildInputs $name" \
| md5sum | awk '{print $1}')
export HASHEDNAME
# This appends the package name and version to the hash string
# we defined above, so that it is more human-readable.
export UNIQNAME="''${HASHEDNAME:0:10}-${name}-${version}"
# Add gyp to the path in case it's needed
export PATH=${nodejs}/lib/node_modules/npm/bin/node-gyp-bin:$PATH
'';
shellHook = ''
runHook preShellHook
runHook setVariables
export PATH=${npm}/bin:${nodejs}/bin:$(pwd)/node_modules/.bin:$PATH
rm -rf $TMPDIR/$UNIQNAME
mkdir -p $TMPDIR/$UNIQNAME
(
cd $TMPDIR/$UNIQNAME
eval "$configurePhase"
)
echo "Installed $fullName dependencies in temporary directory" \
"$TMPDIR/$UNIQNAME"
export PATH=$TMPDIR/$UNIQNAME/node_modules/.bin:$PATH
NODE_MODULES=$TMPDIR/$UNIQNAME/node_modules
export NODE_PATH=$NODE_MODULES:$NODE_PATH
# Check if the current directory contains the package.json for
# this package.
py_cmd='import json; print(json.load(open("package.json"))["name"])'
if [[ -e package.json ]] && \
[[ $(python -c "$py_cmd" 2>/dev/null) == "$fullName" ]]; then
IN_PACKAGE_DIR=true
# If we're in the package directory, symlink it into the
# temporary node modules folder we're building and then
# attempt to import it. Issue a warning if we're not
# successful.
echo "Symlinking current directory into node modules folder..."
mkdir -pv $(dirname $NODE_MODULES/$fullName)
ln -sv $(pwd) $NODE_MODULES/$fullName
# Symlink the node modules folder to whatever has been built.
# Don't do this if there is a node_modules directory because this
# could break current directory state. However, issue a warning in
# this case.
if [[ -e node_modules ]] && [[ ! -L node_modules ]]; then
echo "Warning: node_modules exists but is not a symlink." >&2
echo "You can remove it (rm -r node_modules) and re-enter the" >&2
echo 'shell, or run `ln -sf $NODE_MODULES node_modules`' >&2
else
rm -fv node_modules
ln -sfv $NODE_MODULES node_modules
fi
else
echo >&2
echo "WARNING:" >&2
echo "You are not in the directory for $fullName, so the shell"\
"hook can't symlink the local source code into the temporary"\
"node_modules directory. This will probably prevent you from"\
"using $fullName in a node REPL or running its code." >&2
echo "You might be able to do something manually to"\
"set this up. For example if this package's source is a "\
"tarball, running these commands might work:" >&2
echo >&2
echo ' $ tar -xf $src' >&2
echo ' $ ln -s $PWD/package $NODE_MODULES/$fullName' >&2
echo >&2
fi
runHook postShellHook
'';
# Propagate pieces of information about the package so that downstream
# packages can reflect on them.
passthru = (passthru // {
inherit uniqueName fullName namespace version runtimeDependencies;
circularDependencies = _circularDependencies;
# The basic name is the name without namespace or version, in contrast
# to the fullName which might have a namespace attached, or the
# uniqueName which has a version attached.
basicName = name;
# The path within $out/lib to find the package. If the package does not
# have a namespace, it will simply be in `node_modules`, and otherwise
# it will appear in `node_modules/@namespace`.
modulePath = if namespace == null then "node_modules"
else "node_modules/@${namespace}";
# The full path to the package's code (i.e. folder containing
# package.json) within the nix store.
fullPath = "${self}/lib/node_modules/${self.fullName}";
# The `env` attribute is meant to be used with `nix-shell` (although
# that's not required). It will build the package with its dev
# dependencies. This means that the package must have dev dependencies
# defined, or it will error.
env = buildNodePackage (args // {includeDevDependencies = true;});
# An 'overrideNodePackage' attribute, which will call
# `buildNodePackage` with new arguments produced by the given
# arg-override function. The function consumes the original
# argument set.
#
# N.B: the legacy behavior of accepting a set is preserved but
# the preferred usage-pattern is to supply a function that
# discards its argument; e.g:
#
# overrideNodePackage (_: { ... })
#
# We don't use the name `override` because this will get stomped on
# if the derivation is the result of a `callPackage` application.
overrideNodePackage = newArgs:
if builtins.isFunction newArgs
then buildNodePackage (args // (newArgs args))
else buildNodePackage (args // newArgs);
});
} // {
name = if namePrefix == null then throw "Name prefix is null"
else if name == null then throw "Name is null"
else if version == null then throw "Version of ${name} is null"
else if nameSuffix == null then throw "Name suffix is null"
else "${namePrefix}${name}-${version}${nameSuffix}";
# Propagate the runtime dependencies, any non-nodejs dependencies,
# and nodejs itself.
propagatedBuildInputs = propagatedBuildInputs ++
attrValues runtimeDependencies ++
[nodejs];
# Give as buildInputs npm, python, dev dependencies (if any) and
# additional specified build inputs. In addition, on darwin we
# provide XCode, since node-gyp will use it, and on linux we add
# utillinux.
buildInputs = [ tarWrapper npm python2 file node-build-tools ] ++
attrValues _devDependencies ++
buildInputs ++
(optional stdenv.isLinux pkgs.utillinux) ++
(optionals stdenv.isDarwin [darwin.cctools xcode-wrapper]);
} // optionalAttrs stdenv.isLinux {
LOCALE_ARCHIVE = "${pkgs.glibcLocales}/lib/locale/locale-archive";
} // derivationOverrides;
in stdenv.mkDerivation mkDerivationArgs;
in self