1# This file originates from node2nix
2
3{stdenv, python, nodejs, utillinux, runCommand, writeTextFile}:
4
5let
6 # Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
7 tarWrapper = runCommand "tarWrapper" {} ''
8 mkdir -p $out/bin
9
10 cat > $out/bin/tar <<EOF
11 #! ${stdenv.shell} -e
12 $(type -p tar) "\$@" --warning=no-unknown-keyword
13 EOF
14
15 chmod +x $out/bin/tar
16 '';
17
18 # Function that generates a TGZ file from a NPM project
19 buildNodeSourceDist =
20 { name, version, src, ... }:
21
22 stdenv.mkDerivation {
23 name = "node-tarball-${name}-${version}";
24 inherit src;
25 buildInputs = [ nodejs ];
26 buildPhase = ''
27 export HOME=$TMPDIR
28 tgzFile=$(npm pack)
29 '';
30 installPhase = ''
31 mkdir -p $out/tarballs
32 mv $tgzFile $out/tarballs
33 mkdir -p $out/nix-support
34 echo "file source-dist $out/tarballs/$tgzFile" >> $out/nix-support/hydra-build-products
35 '';
36 };
37
38 includeDependencies = {dependencies}:
39 stdenv.lib.optionalString (dependencies != [])
40 (stdenv.lib.concatMapStrings (dependency:
41 ''
42 # Bundle the dependencies of the package
43 mkdir -p node_modules
44 cd node_modules
45
46 # Only include dependencies if they don't exist. They may also be bundled in the package.
47 if [ ! -e "${dependency.name}" ]
48 then
49 ${composePackage dependency}
50 fi
51
52 cd ..
53 ''
54 ) dependencies);
55
56 # Recursively composes the dependencies of a package
57 composePackage = { name, packageName, src, dependencies ? [], ... }@args:
58 let
59 fixImpureDependencies = writeTextFile {
60 name = "fixDependencies.js";
61 text = ''
62 var fs = require('fs');
63 var url = require('url');
64
65 /*
66 * Replaces an impure version specification by *
67 */
68 function replaceImpureVersionSpec(versionSpec) {
69 var parsedUrl = url.parse(versionSpec);
70
71 if(versionSpec == "latest" || versionSpec == "unstable" ||
72 versionSpec.substr(0, 2) == ".." || dependency.substr(0, 2) == "./" || dependency.substr(0, 2) == "~/" || dependency.substr(0, 1) == '/')
73 return '*';
74 else if(parsedUrl.protocol == "git:" || parsedUrl.protocol == "git+ssh:" || parsedUrl.protocol == "git+http:" || parsedUrl.protocol == "git+https:" ||
75 parsedUrl.protocol == "http:" || parsedUrl.protocol == "https:")
76 return '*';
77 else
78 return versionSpec;
79 }
80
81 var packageObj = JSON.parse(fs.readFileSync('./package.json'));
82
83 /* Replace dependencies */
84 if(packageObj.dependencies !== undefined) {
85 for(var dependency in packageObj.dependencies) {
86 var versionSpec = packageObj.dependencies[dependency];
87 packageObj.dependencies[dependency] = replaceImpureVersionSpec(versionSpec);
88 }
89 }
90
91 /* Replace development dependencies */
92 if(packageObj.devDependencies !== undefined) {
93 for(var dependency in packageObj.devDependencies) {
94 var versionSpec = packageObj.devDependencies[dependency];
95 packageObj.devDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
96 }
97 }
98
99 /* Replace optional dependencies */
100 if(packageObj.optionalDependencies !== undefined) {
101 for(var dependency in packageObj.optionalDependencies) {
102 var versionSpec = packageObj.optionalDependencies[dependency];
103 packageObj.optionalDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
104 }
105 }
106
107 /* Write the fixed JSON file */
108 fs.writeFileSync("package.json", JSON.stringify(packageObj));
109 '';
110 };
111 in
112 ''
113 DIR=$(pwd)
114 cd $TMPDIR
115
116 unpackFile ${src}
117
118 # Make the base dir in which the target dependency resides first
119 mkdir -p "$(dirname "$DIR/${packageName}")"
120
121 if [ -f "${src}" ]
122 then
123 # Figure out what directory has been unpacked
124 packageDir=$(find . -type d -maxdepth 1 | tail -1)
125
126 # Restore write permissions to make building work
127 find "$packageDir" -type d -print0 | xargs -0 chmod u+x
128 chmod -R u+w "$packageDir"
129
130 # Move the extracted tarball into the output folder
131 mv "$packageDir" "$DIR/${packageName}"
132 elif [ -d "${src}" ]
133 then
134 # Restore write permissions to make building work
135 chmod -R u+w $strippedName
136
137 # Move the extracted directory into the output folder
138 mv $strippedName "$DIR/${packageName}"
139 fi
140
141 # Unset the stripped name to not confuse the next unpack step
142 unset strippedName
143
144 # Some version specifiers (latest, unstable, URLs, file paths) force NPM to make remote connections or consult paths outside the Nix store.
145 # The following JavaScript replaces these by * to prevent that
146 cd "$DIR/${packageName}"
147 node ${fixImpureDependencies}
148
149 # Include the dependencies of the package
150 ${includeDependencies { inherit dependencies; }}
151 cd ..
152 ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
153 '';
154
155 # Extract the Node.js source code which is used to compile packages with
156 # native bindings
157 nodeSources = runCommand "node-sources" {} ''
158 tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
159 mv node-* $out
160 '';
161
162 # Builds and composes an NPM package including all its dependencies
163 buildNodePackage = { name, packageName, version, dependencies ? [], production ? true, npmFlags ? "", dontNpmInstall ? false, preRebuild ? "", ... }@args:
164
165 stdenv.lib.makeOverridable stdenv.mkDerivation (builtins.removeAttrs args [ "dependencies" ] // {
166 name = "node-${name}-${version}";
167 buildInputs = [ tarWrapper python nodejs ] ++ stdenv.lib.optional (stdenv.isLinux) utillinux ++ args.buildInputs or [];
168 dontStrip = args.dontStrip or true; # Striping may fail a build for some package deployments
169
170 inherit dontNpmInstall preRebuild;
171
172 unpackPhase = args.unpackPhase or "true";
173
174 buildPhase = args.buildPhase or "true";
175
176 compositionScript = composePackage args;
177 passAsFile = [ "compositionScript" ];
178
179 installPhase = args.installPhase or ''
180 # Create and enter a root node_modules/ folder
181 mkdir -p $out/lib/node_modules
182 cd $out/lib/node_modules
183
184 # Compose the package and all its dependencies
185 source $compositionScriptPath
186
187 # Patch the shebangs of the bundled modules to prevent them from
188 # calling executables outside the Nix store as much as possible
189 patchShebangs .
190
191 # Deploy the Node.js package by running npm install. Since the
192 # dependencies have been provided already by ourselves, it should not
193 # attempt to install them again, which is good, because we want to make
194 # it Nix's responsibility. If it needs to install any dependencies
195 # anyway (e.g. because the dependency parameters are
196 # incomplete/incorrect), it fails.
197 #
198 # The other responsibilities of NPM are kept -- version checks, build
199 # steps, postprocessing etc.
200
201 export HOME=$TMPDIR
202 cd "${packageName}"
203 runHook preRebuild
204 npm --registry http://www.example.com --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} rebuild
205
206 if [ "$dontNpmInstall" != "1" ]
207 then
208 npm --registry http://www.example.com --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} install
209 fi
210
211 # Create symlink to the deployed executable folder, if applicable
212 if [ -d "$out/lib/node_modules/.bin" ]
213 then
214 ln -s $out/lib/node_modules/.bin $out/bin
215 fi
216
217 # Create symlinks to the deployed manual page folders, if applicable
218 if [ -d "$out/lib/node_modules/${packageName}/man" ]
219 then
220 mkdir -p $out/share
221 for dir in "$out/lib/node_modules/${packageName}/man/"*
222 do
223 mkdir -p $out/share/man/$(basename "$dir")
224 for page in "$dir"/*
225 do
226 ln -s $page $out/share/man/$(basename "$dir")
227 done
228 done
229 fi
230 '';
231 });
232
233 # Builds a development shell
234 buildNodeShell = { name, packageName, version, src, dependencies ? [], production ? true, npmFlags ? "", dontNpmInstall ? false, ... }@args:
235 let
236 nodeDependencies = stdenv.mkDerivation {
237 name = "node-dependencies-${name}-${version}";
238
239 buildInputs = [ tarWrapper python nodejs ] ++ stdenv.lib.optional (stdenv.isLinux) utillinux ++ args.buildInputs or [];
240
241 includeScript = includeDependencies { inherit dependencies; };
242 passAsFile = [ "includeScript" ];
243
244 buildCommand = ''
245 mkdir -p $out/lib
246 cd $out/lib
247 source $includeScriptPath
248
249 # Create fake package.json to make the npm commands work properly
250 cat > package.json <<EOF
251 {
252 "name": "${packageName}",
253 "version": "${version}"
254 }
255 EOF
256
257 # Patch the shebangs of the bundled modules to prevent them from
258 # calling executables outside the Nix store as much as possible
259 patchShebangs .
260
261 export HOME=$TMPDIR
262 npm --registry http://www.example.com --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} rebuild
263
264 ${stdenv.lib.optionalString (!dontNpmInstall) ''
265 npm --registry http://www.example.com --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} install
266 ''}
267
268 ln -s $out/lib/node_modules/.bin $out/bin
269 '';
270 };
271 in
272 stdenv.lib.makeOverridable stdenv.mkDerivation {
273 name = "node-shell-${name}-${version}";
274
275 buildInputs = [ python nodejs ] ++ stdenv.lib.optional (stdenv.isLinux) utillinux ++ args.buildInputs or [];
276 buildCommand = ''
277 mkdir -p $out/bin
278 cat > $out/bin/shell <<EOF
279 #! ${stdenv.shell} -e
280 $shellHook
281 exec ${stdenv.shell}
282 EOF
283 chmod +x $out/bin/shell
284 '';
285
286 # Provide the dependencies in a development shell through the NODE_PATH environment variable
287 inherit nodeDependencies;
288 shellHook = stdenv.lib.optionalString (dependencies != []) ''
289 export NODE_PATH=$nodeDependencies/lib/node_modules
290 '';
291 };
292in
293{ inherit buildNodeSourceDist buildNodePackage buildNodeShell; }