diff --git a/pkgs/buildNpmPackage/default.nix b/pkgs/buildNpmPackage/default.nix new file mode 100644 index 0000000..0291e38 --- /dev/null +++ b/pkgs/buildNpmPackage/default.nix @@ -0,0 +1,74 @@ +{ stdenvNoCC, writeShellScriptBin, writeText, stdenv, fetchurl, makeWrapper, nodejs-10_x }: +with stdenv.lib; +let + inherit (builtins) fromJSON toJSON split; + + depsToFetches = deps: concatMap depToFetch (attrValues deps); + depFetchOwn = { resolved, integrity, ... }: let + ssri = split "-" integrity; # standard subresource integrity + hashType = head ssri; + hash = elemAt ssri 2; + in nameValuePair resolved (fetchurl { + url = resolved; + "${hashType}" = hash; + }); + + depToFetch = args @ { resolved ? null, dependencies ? {}, ... }: + (optional (resolved != null) (depFetchOwn args)) ++ (depsToFetches dependencies); + + npmCacheInput = lock: writeText "npm-cache-input.json" (toJSON (listToAttrs (depToFetch lock))); + + patchShebangs = writeShellScriptBin "patchShebangs.sh" '' + set -e + source ${stdenvNoCC}/setup + patchShebangs "$@" + ''; + + shellWrap = writeShellScriptBin "npm-shell-wrap.sh" '' + set -e + if [ ! -e .shebangs_patched ]; then + ${patchShebangs}/bin/patchShebangs.sh . + touch .shebangs_patched + fi + exec bash "$@" + ''; +in +args @ { lockfile, src, buildInputs ? [], npmFlags ? [], ... }: +let + lock = fromJSON (readFile lockfile); +in +stdenv.mkDerivation ({ + inherit (lock) version; + name = "${lock.name}-${lock.version}"; + + XDG_CONFIG_DIRS = "."; + NO_UPDATE_NOTIFIER = true; + preBuildPhases = [ "npmCachePhase" ]; + preInstallPhases = [ "npmPackPhase" ]; + installJavascript = true; + npmCachePhase = '' + node ${./mkcache.js} ${npmCacheInput lock} + ''; + buildPhase = '' + runHook preBuild + npm ci $npmFlags + runHook postBuild + ''; + # make a package .tgz (no way around it) + npmPackPhase = '' + npm prune --production $npmFlags + npm pack --ignore-scripts $npmFlags + ''; + # unpack the .tgz into output directory and add npm wrapper + installPhase = '' + mkdir -p $out/bin + tar xzvf ./${lock.name}-${lock.version}.tgz -C $out --strip-components=1 + if [ "$installJavascript" -eq "1" ]; then + cp -R node_modules $out/ + makeWrapper ${nodejs-10_x}/bin/npm $out/bin/npm --run "cd $out" + fi + ''; +} // args // { + buildInputs = [ nodejs-10_x makeWrapper ] ++ buildInputs; + npmFlags = [ "--cache=./npm-cache" "--offline" "--script-shell=${shellWrap}/bin/npm-shell-wrap.sh" ] ++ npmFlags; + }) diff --git a/pkgs/buildNpmPackage/mkcache.js b/pkgs/buildNpmPackage/mkcache.js new file mode 100644 index 0000000..36ff6b5 --- /dev/null +++ b/pkgs/buildNpmPackage/mkcache.js @@ -0,0 +1,38 @@ +const assert = require('assert') +const fs = require('fs') +const path = require('path') +const {promisify} = require('util') + +// find pacote from npm dependencies +module.paths.push(path.join(process.argv[0], "../../lib/node_modules/npm/node_modules")) +const pacote = require('pacote') + +function traverseDeps(pkg, fn) { + Object.values(pkg.dependencies).forEach(dep => { + if (dep.resolved && dep.integrity) fn(dep) + if (dep.dependencies) traverseDeps(dep, fn) + }) +} + +async function main(lockfile, nix, cache) { + var promises = Object.keys(nix).map(async function(url) { + var tar = nix[url] + const manifest = await pacote.manifest(tar, {offline: true, cache}) + return [url, manifest._integrity] + }) + var hashes = new Map(await Promise.all(promises)) + traverseDeps(lockfile, dep => { + if (dep.integrity.startsWith("sha1-")) { + assert(hashes.has(dep.resolved)) + dep.integrity = hashes.get(dep.resolved) + } + else { + assert(dep.integrity == hashes.get(dep.resolved)) + } + }) + await promisify(fs.writeFile)("./package-lock.json", JSON.stringify(lock, null, 4)) +} +const lock = JSON.parse(fs.readFileSync('./package-lock.json', 'utf8')) +const nix_pkgs = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')) +main(lock, nix_pkgs, './npm-cache/_cacache') + diff --git a/pkgs/default.nix b/pkgs/default.nix index cbcfbb8..619712c 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -16,6 +16,8 @@ in rev = "76dc0f06d21f6063cb7b7d2291b8623da24affa9"; }) {}; + buildNpmPackage = callPackage ./buildNpmPackage {}; + buildMacOSApp = callPackage (fetchGit { url = https://github.com/serokell/nix-macos-app; rev = "192f3c22b4270be84aef9176fdf52a41d0d85b32";