node-env.nix 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. # This file originates from node2nix
  2. {lib, stdenv, nodejs, python2, pkgs, libtool, runCommand, writeTextFile, writeShellScript}:
  3. let
  4. # Workaround to cope with utillinux in Nixpkgs 20.09 and util-linux in Nixpkgs master
  5. utillinux = if pkgs ? utillinux then pkgs.utillinux else pkgs.util-linux;
  6. python = if nodejs ? python then nodejs.python else python2;
  7. # Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
  8. tarWrapper = runCommand "tarWrapper" {} ''
  9. mkdir -p $out/bin
  10. cat > $out/bin/tar <<EOF
  11. #! ${stdenv.shell} -e
  12. $(type -p tar) "\$@" --warning=no-unknown-keyword --delay-directory-restore
  13. EOF
  14. chmod +x $out/bin/tar
  15. '';
  16. # Function that generates a TGZ file from a NPM project
  17. buildNodeSourceDist =
  18. { name, version, src, ... }:
  19. stdenv.mkDerivation {
  20. name = "node-tarball-${name}-${version}";
  21. inherit src;
  22. buildInputs = [ nodejs ];
  23. buildPhase = ''
  24. export HOME=$TMPDIR
  25. tgzFile=$(npm pack | tail -n 1) # Hooks to the pack command will add output (https://docs.npmjs.com/misc/scripts)
  26. '';
  27. installPhase = ''
  28. mkdir -p $out/tarballs
  29. mv $tgzFile $out/tarballs
  30. mkdir -p $out/nix-support
  31. echo "file source-dist $out/tarballs/$tgzFile" >> $out/nix-support/hydra-build-products
  32. '';
  33. };
  34. # Common shell logic
  35. installPackage = writeShellScript "install-package" ''
  36. installPackage() {
  37. local packageName=$1 src=$2
  38. local strippedName
  39. local DIR=$PWD
  40. cd $TMPDIR
  41. unpackFile $src
  42. # Make the base dir in which the target dependency resides first
  43. mkdir -p "$(dirname "$DIR/$packageName")"
  44. if [ -f "$src" ]
  45. then
  46. # Figure out what directory has been unpacked
  47. packageDir="$(find . -maxdepth 1 -type d | tail -1)"
  48. # Restore write permissions to make building work
  49. find "$packageDir" -type d -exec chmod u+x {} \;
  50. chmod -R u+w "$packageDir"
  51. # Move the extracted tarball into the output folder
  52. mv "$packageDir" "$DIR/$packageName"
  53. elif [ -d "$src" ]
  54. then
  55. # Get a stripped name (without hash) of the source directory.
  56. # On old nixpkgs it's already set internally.
  57. if [ -z "$strippedName" ]
  58. then
  59. strippedName="$(stripHash $src)"
  60. fi
  61. # Restore write permissions to make building work
  62. chmod -R u+w "$strippedName"
  63. # Move the extracted directory into the output folder
  64. mv "$strippedName" "$DIR/$packageName"
  65. fi
  66. # Change to the package directory to install dependencies
  67. cd "$DIR/$packageName"
  68. }
  69. '';
  70. # Bundle the dependencies of the package
  71. #
  72. # Only include dependencies if they don't exist. They may also be bundled in the package.
  73. includeDependencies = {dependencies}:
  74. lib.optionalString (dependencies != []) (
  75. ''
  76. mkdir -p node_modules
  77. cd node_modules
  78. ''
  79. + (lib.concatMapStrings (dependency:
  80. ''
  81. if [ ! -e "${dependency.packageName}" ]; then
  82. ${composePackage dependency}
  83. fi
  84. ''
  85. ) dependencies)
  86. + ''
  87. cd ..
  88. ''
  89. );
  90. # Recursively composes the dependencies of a package
  91. composePackage = { name, packageName, src, dependencies ? [], ... }@args:
  92. builtins.addErrorContext "while evaluating node package '${packageName}'" ''
  93. installPackage "${packageName}" "${src}"
  94. ${includeDependencies { inherit dependencies; }}
  95. cd ..
  96. ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
  97. '';
  98. pinpointDependencies = {dependencies, production}:
  99. let
  100. pinpointDependenciesFromPackageJSON = writeTextFile {
  101. name = "pinpointDependencies.js";
  102. text = ''
  103. var fs = require('fs');
  104. var path = require('path');
  105. function resolveDependencyVersion(location, name) {
  106. if(location == process.env['NIX_STORE']) {
  107. return null;
  108. } else {
  109. var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json");
  110. if(fs.existsSync(dependencyPackageJSON)) {
  111. var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON));
  112. if(dependencyPackageObj.name == name) {
  113. return dependencyPackageObj.version;
  114. }
  115. } else {
  116. return resolveDependencyVersion(path.resolve(location, ".."), name);
  117. }
  118. }
  119. }
  120. function replaceDependencies(dependencies) {
  121. if(typeof dependencies == "object" && dependencies !== null) {
  122. for(var dependency in dependencies) {
  123. var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency);
  124. if(resolvedVersion === null) {
  125. process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n");
  126. } else {
  127. dependencies[dependency] = resolvedVersion;
  128. }
  129. }
  130. }
  131. }
  132. /* Read the package.json configuration */
  133. var packageObj = JSON.parse(fs.readFileSync('./package.json'));
  134. /* Pinpoint all dependencies */
  135. replaceDependencies(packageObj.dependencies);
  136. if(process.argv[2] == "development") {
  137. replaceDependencies(packageObj.devDependencies);
  138. }
  139. replaceDependencies(packageObj.optionalDependencies);
  140. /* Write the fixed package.json file */
  141. fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2));
  142. '';
  143. };
  144. in
  145. ''
  146. node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"}
  147. ${lib.optionalString (dependencies != [])
  148. ''
  149. if [ -d node_modules ]
  150. then
  151. cd node_modules
  152. ${lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies}
  153. cd ..
  154. fi
  155. ''}
  156. '';
  157. # Recursively traverses all dependencies of a package and pinpoints all
  158. # dependencies in the package.json file to the versions that are actually
  159. # being used.
  160. pinpointDependenciesOfPackage = { packageName, dependencies ? [], production ? true, ... }@args:
  161. ''
  162. if [ -d "${packageName}" ]
  163. then
  164. cd "${packageName}"
  165. ${pinpointDependencies { inherit dependencies production; }}
  166. cd ..
  167. ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
  168. fi
  169. '';
  170. # Extract the Node.js source code which is used to compile packages with
  171. # native bindings
  172. nodeSources = runCommand "node-sources" {} ''
  173. tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
  174. mv node-* $out
  175. '';
  176. # Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty)
  177. addIntegrityFieldsScript = writeTextFile {
  178. name = "addintegrityfields.js";
  179. text = ''
  180. var fs = require('fs');
  181. var path = require('path');
  182. function augmentDependencies(baseDir, dependencies) {
  183. for(var dependencyName in dependencies) {
  184. var dependency = dependencies[dependencyName];
  185. // Open package.json and augment metadata fields
  186. var packageJSONDir = path.join(baseDir, "node_modules", dependencyName);
  187. var packageJSONPath = path.join(packageJSONDir, "package.json");
  188. if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored
  189. console.log("Adding metadata fields to: "+packageJSONPath);
  190. var packageObj = JSON.parse(fs.readFileSync(packageJSONPath));
  191. if(dependency.integrity) {
  192. packageObj["_integrity"] = dependency.integrity;
  193. } else {
  194. packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads.
  195. }
  196. if(dependency.resolved) {
  197. packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided
  198. } else {
  199. packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories.
  200. }
  201. if(dependency.from !== undefined) { // Adopt from property if one has been provided
  202. packageObj["_from"] = dependency.from;
  203. }
  204. fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2));
  205. }
  206. // Augment transitive dependencies
  207. if(dependency.dependencies !== undefined) {
  208. augmentDependencies(packageJSONDir, dependency.dependencies);
  209. }
  210. }
  211. }
  212. if(fs.existsSync("./package-lock.json")) {
  213. var packageLock = JSON.parse(fs.readFileSync("./package-lock.json"));
  214. if(![1, 2].includes(packageLock.lockfileVersion)) {
  215. process.stderr.write("Sorry, I only understand lock file versions 1 and 2!\n");
  216. process.exit(1);
  217. }
  218. if(packageLock.dependencies !== undefined) {
  219. augmentDependencies(".", packageLock.dependencies);
  220. }
  221. }
  222. '';
  223. };
  224. # Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes
  225. reconstructPackageLock = writeTextFile {
  226. name = "addintegrityfields.js";
  227. text = ''
  228. var fs = require('fs');
  229. var path = require('path');
  230. var packageObj = JSON.parse(fs.readFileSync("package.json"));
  231. var lockObj = {
  232. name: packageObj.name,
  233. version: packageObj.version,
  234. lockfileVersion: 1,
  235. requires: true,
  236. dependencies: {}
  237. };
  238. function augmentPackageJSON(filePath, dependencies) {
  239. var packageJSON = path.join(filePath, "package.json");
  240. if(fs.existsSync(packageJSON)) {
  241. var packageObj = JSON.parse(fs.readFileSync(packageJSON));
  242. dependencies[packageObj.name] = {
  243. version: packageObj.version,
  244. integrity: "sha1-000000000000000000000000000=",
  245. dependencies: {}
  246. };
  247. processDependencies(path.join(filePath, "node_modules"), dependencies[packageObj.name].dependencies);
  248. }
  249. }
  250. function processDependencies(dir, dependencies) {
  251. if(fs.existsSync(dir)) {
  252. var files = fs.readdirSync(dir);
  253. files.forEach(function(entry) {
  254. var filePath = path.join(dir, entry);
  255. var stats = fs.statSync(filePath);
  256. if(stats.isDirectory()) {
  257. if(entry.substr(0, 1) == "@") {
  258. // When we encounter a namespace folder, augment all packages belonging to the scope
  259. var pkgFiles = fs.readdirSync(filePath);
  260. pkgFiles.forEach(function(entry) {
  261. if(stats.isDirectory()) {
  262. var pkgFilePath = path.join(filePath, entry);
  263. augmentPackageJSON(pkgFilePath, dependencies);
  264. }
  265. });
  266. } else {
  267. augmentPackageJSON(filePath, dependencies);
  268. }
  269. }
  270. });
  271. }
  272. }
  273. processDependencies("node_modules", lockObj.dependencies);
  274. fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2));
  275. '';
  276. };
  277. prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}:
  278. let
  279. forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com";
  280. in
  281. ''
  282. # Pinpoint the versions of all dependencies to the ones that are actually being used
  283. echo "pinpointing versions of dependencies..."
  284. source $pinpointDependenciesScriptPath
  285. # Patch the shebangs of the bundled modules to prevent them from
  286. # calling executables outside the Nix store as much as possible
  287. patchShebangs .
  288. # Deploy the Node.js package by running npm install. Since the
  289. # dependencies have been provided already by ourselves, it should not
  290. # attempt to install them again, which is good, because we want to make
  291. # it Nix's responsibility. If it needs to install any dependencies
  292. # anyway (e.g. because the dependency parameters are
  293. # incomplete/incorrect), it fails.
  294. #
  295. # The other responsibilities of NPM are kept -- version checks, build
  296. # steps, postprocessing etc.
  297. export HOME=$TMPDIR
  298. cd "${packageName}"
  299. runHook preRebuild
  300. ${lib.optionalString bypassCache ''
  301. ${lib.optionalString reconstructLock ''
  302. if [ -f package-lock.json ]
  303. then
  304. echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!"
  305. echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!"
  306. rm package-lock.json
  307. else
  308. echo "No package-lock.json file found, reconstructing..."
  309. fi
  310. node ${reconstructPackageLock}
  311. ''}
  312. node ${addIntegrityFieldsScript}
  313. ''}
  314. npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} rebuild
  315. if [ "''${dontNpmInstall-}" != "1" ]
  316. then
  317. # NPM tries to download packages even when they already exist if npm-shrinkwrap is used.
  318. rm -f npm-shrinkwrap.json
  319. npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} install
  320. fi
  321. '';
  322. # Builds and composes an NPM package including all its dependencies
  323. buildNodePackage =
  324. { name
  325. , packageName
  326. , version ? null
  327. , dependencies ? []
  328. , buildInputs ? []
  329. , production ? true
  330. , npmFlags ? ""
  331. , dontNpmInstall ? false
  332. , bypassCache ? false
  333. , reconstructLock ? false
  334. , preRebuild ? ""
  335. , dontStrip ? true
  336. , unpackPhase ? "true"
  337. , buildPhase ? "true"
  338. , meta ? {}
  339. , ... }@args:
  340. let
  341. extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" "meta" ];
  342. in
  343. stdenv.mkDerivation ({
  344. name = "${name}${if version == null then "" else "-${version}"}";
  345. buildInputs = [ tarWrapper python nodejs ]
  346. ++ lib.optional (stdenv.isLinux) utillinux
  347. ++ lib.optional (stdenv.isDarwin) libtool
  348. ++ buildInputs;
  349. inherit nodejs;
  350. inherit dontStrip; # Stripping may fail a build for some package deployments
  351. inherit dontNpmInstall preRebuild unpackPhase buildPhase;
  352. compositionScript = composePackage args;
  353. pinpointDependenciesScript = pinpointDependenciesOfPackage args;
  354. passAsFile = [ "compositionScript" "pinpointDependenciesScript" ];
  355. installPhase = ''
  356. source ${installPackage}
  357. # Create and enter a root node_modules/ folder
  358. mkdir -p $out/lib/node_modules
  359. cd $out/lib/node_modules
  360. # Compose the package and all its dependencies
  361. source $compositionScriptPath
  362. ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
  363. # Create symlink to the deployed executable folder, if applicable
  364. if [ -d "$out/lib/node_modules/.bin" ]
  365. then
  366. ln -s $out/lib/node_modules/.bin $out/bin
  367. # Patch the shebang lines of all the executables
  368. ls $out/bin/* | while read i
  369. do
  370. file="$(readlink -f "$i")"
  371. chmod u+rwx "$file"
  372. patchShebangs "$file"
  373. done
  374. fi
  375. # Create symlinks to the deployed manual page folders, if applicable
  376. if [ -d "$out/lib/node_modules/${packageName}/man" ]
  377. then
  378. mkdir -p $out/share
  379. for dir in "$out/lib/node_modules/${packageName}/man/"*
  380. do
  381. mkdir -p $out/share/man/$(basename "$dir")
  382. for page in "$dir"/*
  383. do
  384. ln -s $page $out/share/man/$(basename "$dir")
  385. done
  386. done
  387. fi
  388. # Run post install hook, if provided
  389. runHook postInstall
  390. '';
  391. meta = {
  392. # default to Node.js' platforms
  393. platforms = nodejs.meta.platforms;
  394. } // meta;
  395. } // extraArgs);
  396. # Builds a node environment (a node_modules folder and a set of binaries)
  397. buildNodeDependencies =
  398. { name
  399. , packageName
  400. , version ? null
  401. , src
  402. , dependencies ? []
  403. , buildInputs ? []
  404. , production ? true
  405. , npmFlags ? ""
  406. , dontNpmInstall ? false
  407. , bypassCache ? false
  408. , reconstructLock ? false
  409. , dontStrip ? true
  410. , unpackPhase ? "true"
  411. , buildPhase ? "true"
  412. , ... }@args:
  413. let
  414. extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ];
  415. in
  416. stdenv.mkDerivation ({
  417. name = "node-dependencies-${name}${if version == null then "" else "-${version}"}";
  418. buildInputs = [ tarWrapper python nodejs ]
  419. ++ lib.optional (stdenv.isLinux) utillinux
  420. ++ lib.optional (stdenv.isDarwin) libtool
  421. ++ buildInputs;
  422. inherit dontStrip; # Stripping may fail a build for some package deployments
  423. inherit dontNpmInstall unpackPhase buildPhase;
  424. includeScript = includeDependencies { inherit dependencies; };
  425. pinpointDependenciesScript = pinpointDependenciesOfPackage args;
  426. passAsFile = [ "includeScript" "pinpointDependenciesScript" ];
  427. installPhase = ''
  428. source ${installPackage}
  429. mkdir -p $out/${packageName}
  430. cd $out/${packageName}
  431. source $includeScriptPath
  432. # Create fake package.json to make the npm commands work properly
  433. cp ${src}/package.json .
  434. chmod 644 package.json
  435. ${lib.optionalString bypassCache ''
  436. if [ -f ${src}/package-lock.json ]
  437. then
  438. cp ${src}/package-lock.json .
  439. chmod 644 package-lock.json
  440. fi
  441. ''}
  442. # Go to the parent folder to make sure that all packages are pinpointed
  443. cd ..
  444. ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
  445. ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
  446. # Expose the executables that were installed
  447. cd ..
  448. ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
  449. mv ${packageName} lib
  450. ln -s $out/lib/node_modules/.bin $out/bin
  451. '';
  452. } // extraArgs);
  453. # Builds a development shell
  454. buildNodeShell =
  455. { name
  456. , packageName
  457. , version ? null
  458. , src
  459. , dependencies ? []
  460. , buildInputs ? []
  461. , production ? true
  462. , npmFlags ? ""
  463. , dontNpmInstall ? false
  464. , bypassCache ? false
  465. , reconstructLock ? false
  466. , dontStrip ? true
  467. , unpackPhase ? "true"
  468. , buildPhase ? "true"
  469. , ... }@args:
  470. let
  471. nodeDependencies = buildNodeDependencies args;
  472. extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "unpackPhase" "buildPhase" ];
  473. in
  474. stdenv.mkDerivation ({
  475. name = "node-shell-${name}${if version == null then "" else "-${version}"}";
  476. buildInputs = [ python nodejs ] ++ lib.optional (stdenv.isLinux) utillinux ++ buildInputs;
  477. buildCommand = ''
  478. mkdir -p $out/bin
  479. cat > $out/bin/shell <<EOF
  480. #! ${stdenv.shell} -e
  481. $shellHook
  482. exec ${stdenv.shell}
  483. EOF
  484. chmod +x $out/bin/shell
  485. '';
  486. # Provide the dependencies in a development shell through the NODE_PATH environment variable
  487. inherit nodeDependencies;
  488. shellHook = lib.optionalString (dependencies != []) ''
  489. export NODE_PATH=${nodeDependencies}/lib/node_modules
  490. export PATH="${nodeDependencies}/bin:$PATH"
  491. '';
  492. } // extraArgs);
  493. in
  494. {
  495. buildNodeSourceDist = lib.makeOverridable buildNodeSourceDist;
  496. buildNodePackage = lib.makeOverridable buildNodePackage;
  497. buildNodeDependencies = lib.makeOverridable buildNodeDependencies;
  498. buildNodeShell = lib.makeOverridable buildNodeShell;
  499. }