Repo for the search and displace core module including the interface to select files and search and displace operations to run on them. https://searchanddisplace.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

588 lines
20 KiB

  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.name}" ]; 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
  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}-${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. fi
  368. # Create symlinks to the deployed manual page folders, if applicable
  369. if [ -d "$out/lib/node_modules/${packageName}/man" ]
  370. then
  371. mkdir -p $out/share
  372. for dir in "$out/lib/node_modules/${packageName}/man/"*
  373. do
  374. mkdir -p $out/share/man/$(basename "$dir")
  375. for page in "$dir"/*
  376. do
  377. ln -s $page $out/share/man/$(basename "$dir")
  378. done
  379. done
  380. fi
  381. # Run post install hook, if provided
  382. runHook postInstall
  383. '';
  384. meta = {
  385. # default to Node.js' platforms
  386. platforms = nodejs.meta.platforms;
  387. } // meta;
  388. } // extraArgs);
  389. # Builds a node environment (a node_modules folder and a set of binaries)
  390. buildNodeDependencies =
  391. { name
  392. , packageName
  393. , version
  394. , src
  395. , dependencies ? []
  396. , buildInputs ? []
  397. , production ? true
  398. , npmFlags ? ""
  399. , dontNpmInstall ? false
  400. , bypassCache ? false
  401. , reconstructLock ? false
  402. , dontStrip ? true
  403. , unpackPhase ? "true"
  404. , buildPhase ? "true"
  405. , ... }@args:
  406. let
  407. extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ];
  408. in
  409. stdenv.mkDerivation ({
  410. name = "node-dependencies-${name}-${version}";
  411. buildInputs = [ tarWrapper python nodejs ]
  412. ++ lib.optional (stdenv.isLinux) utillinux
  413. ++ lib.optional (stdenv.isDarwin) libtool
  414. ++ buildInputs;
  415. inherit dontStrip; # Stripping may fail a build for some package deployments
  416. inherit dontNpmInstall unpackPhase buildPhase;
  417. includeScript = includeDependencies { inherit dependencies; };
  418. pinpointDependenciesScript = pinpointDependenciesOfPackage args;
  419. passAsFile = [ "includeScript" "pinpointDependenciesScript" ];
  420. installPhase = ''
  421. source ${installPackage}
  422. mkdir -p $out/${packageName}
  423. cd $out/${packageName}
  424. source $includeScriptPath
  425. # Create fake package.json to make the npm commands work properly
  426. cp ${src}/package.json .
  427. chmod 644 package.json
  428. ${lib.optionalString bypassCache ''
  429. if [ -f ${src}/package-lock.json ]
  430. then
  431. cp ${src}/package-lock.json .
  432. fi
  433. ''}
  434. # Go to the parent folder to make sure that all packages are pinpointed
  435. cd ..
  436. ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
  437. ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
  438. # Expose the executables that were installed
  439. cd ..
  440. ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
  441. mv ${packageName} lib
  442. ln -s $out/lib/node_modules/.bin $out/bin
  443. '';
  444. } // extraArgs);
  445. # Builds a development shell
  446. buildNodeShell =
  447. { name
  448. , packageName
  449. , version
  450. , src
  451. , dependencies ? []
  452. , buildInputs ? []
  453. , production ? true
  454. , npmFlags ? ""
  455. , dontNpmInstall ? false
  456. , bypassCache ? false
  457. , reconstructLock ? false
  458. , dontStrip ? true
  459. , unpackPhase ? "true"
  460. , buildPhase ? "true"
  461. , ... }@args:
  462. let
  463. nodeDependencies = buildNodeDependencies args;
  464. in
  465. stdenv.mkDerivation {
  466. name = "node-shell-${name}-${version}";
  467. buildInputs = [ python nodejs ] ++ lib.optional (stdenv.isLinux) utillinux ++ buildInputs;
  468. buildCommand = ''
  469. mkdir -p $out/bin
  470. cat > $out/bin/shell <<EOF
  471. #! ${stdenv.shell} -e
  472. $shellHook
  473. exec ${stdenv.shell}
  474. EOF
  475. chmod +x $out/bin/shell
  476. '';
  477. # Provide the dependencies in a development shell through the NODE_PATH environment variable
  478. inherit nodeDependencies;
  479. shellHook = lib.optionalString (dependencies != []) ''
  480. export NODE_PATH=${nodeDependencies}/lib/node_modules
  481. export PATH="${nodeDependencies}/bin:$PATH"
  482. '';
  483. };
  484. in
  485. {
  486. buildNodeSourceDist = lib.makeOverridable buildNodeSourceDist;
  487. buildNodePackage = lib.makeOverridable buildNodePackage;
  488. buildNodeDependencies = lib.makeOverridable buildNodeDependencies;
  489. buildNodeShell = lib.makeOverridable buildNodeShell;
  490. }