diff --git a/README.md b/README.md index e9585b1..26aa71a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ In addition to all arguments accepted by `stdenv.mkDerivation`, the `mkPnpmPacka | `script` | The npm script that is executed | `build` | | `distDir` | The directory that should be copied to the output | `dist` | | `installInPlace` | Run `pnpm install` in the source directory instead of a separate derivation | `false` | +| `installEnv` | Environment variables that should be present during `pnpm install` | `{}` | +| `noDevDependencies` | Only download and install `dependencies`, not `devDependencies` | `false` | +| `extraNodeModuleSources` | Additional files that should be available during `pnpm install` | `[]` | | `copyPnpmStore` | Copy the pnpm store into the build directory instead of linking it | `true` | | `copyNodeModules` | Copy the `node_modules` into the build directory instead of linking it | `false` | -| `extraNodeModuleSources` | Additional files that should be available during `pnpm install` | `[]` | | `extraBuildInputs` | Additional entries for `nativeBuildInputs` | `[]` | diff --git a/derivation.nix b/derivation.nix index b90d7e3..d193469 100644 --- a/derivation.nix +++ b/derivation.nix @@ -2,9 +2,7 @@ , stdenv , nodejs , pkg-config -, yq , callPackage -, writeShellScriptBin , writeText , runCommand , ... @@ -27,9 +25,11 @@ in , script ? "build" , distDir ? "dist" , installInPlace ? false + , installEnv ? { } + , noDevDependencies ? false + , extraNodeModuleSources ? [ ] , copyPnpmStore ? true , copyNodeModules ? false - , extraNodeModuleSources ? [ ] , extraBuildInputs ? [ ] , nodejs ? nodePkg , pnpm ? nodejs.pkgs.pnpm @@ -78,76 +78,86 @@ in installPhase = '' runHook preInstall - mv ${distDir} $out + ${if distDir == "." then "cp -r" else "mv"} ${distDir} $out runHook postInstall ''; - passthru = { - inherit attrs; - - patchedLockfile = patchLockfile pnpmLockYaml; - patchedLockfileYaml = writeText "pnpm-lock.yaml" (toJSON passthru.patchedLockfile); - - pnpmStore = runCommand "${name}-pnpm-store" - { - nativeBuildInputs = [ nodejs pnpm ]; - } '' - mkdir -p $out - - store=$(pnpm store path) - mkdir -p $(dirname $store) - ln -s $out $(pnpm store path) - - pnpm store add ${concatStringsSep " " (unique (dependencyTarballs { inherit registry; lockfile = pnpmLockYaml; }))} - ''; - - nodeModules = stdenv.mkDerivation { - name = "${name}-node-modules"; + passthru = + let + processResult = processLockfile { inherit registry noDevDependencies; lockfile = pnpmLockYaml; }; + in + { + inherit attrs; - inherit nativeBuildInputs; + patchedLockfile = processResult.patchedLockfile; + patchedLockfileYaml = writeText "pnpm-lock.yaml" (toJSON passthru.patchedLockfile); - unpackPhase = concatStringsSep "\n" - ( - map - (v: - let - nv = if isAttrs v then v else { name = "."; value = v; }; - in - "cp -vr ${nv.value} ${nv.name}" - ) - ([ - { name = "package.json"; value = packageJSON; } - { name = "pnpm-lock.yaml"; value = passthru.patchedLockfileYaml; } - ] ++ extraNodeModuleSources) - ); - - buildPhase = '' - export HOME=$NIX_BUILD_TOP # Some packages need a writable HOME + pnpmStore = runCommand "${name}-pnpm-store" + { + nativeBuildInputs = [ nodejs pnpm ]; + } '' + mkdir -p $out store=$(pnpm store path) mkdir -p $(dirname $store) + ln -s $out $(pnpm store path) - cp -f ${passthru.patchedLockfileYaml} pnpm-lock.yaml - - # solve pnpm: EACCES: permission denied, copyfile '/build/.pnpm-store - ${if !copyPnpmStore - then "ln -s" - else "cp -RL" - } ${passthru.pnpmStore} $(pnpm store path) - - ${lib.optionalString copyPnpmStore "chmod -R +w $(pnpm store path)"} - - pnpm install --frozen-lockfile --offline + pnpm store add ${concatStringsSep " " (unique processResult.dependencyTarballs)} ''; - installPhase = '' - cp -r node_modules/. $out - ''; + nodeModules = stdenv.mkDerivation { + name = "${name}-node-modules"; + + inherit nativeBuildInputs; + + unpackPhase = concatStringsSep "\n" + ( + map + (v: + let + nv = if isAttrs v then v else { name = "."; value = v; }; + in + "cp -vr ${nv.value} ${nv.name}" + ) + ([ + { name = "package.json"; value = packageJSON; } + { name = "pnpm-lock.yaml"; value = passthru.patchedLockfileYaml; } + ] ++ extraNodeModuleSources) + ); + + buildPhase = '' + export HOME=$NIX_BUILD_TOP # Some packages need a writable HOME + + store=$(pnpm store path) + mkdir -p $(dirname $store) + + cp -f ${passthru.patchedLockfileYaml} pnpm-lock.yaml + + # solve pnpm: EACCES: permission denied, copyfile '/build/.pnpm-store + ${if !copyPnpmStore + then "ln -s" + else "cp -RL" + } ${passthru.pnpmStore} $(pnpm store path) + + ${lib.optionalString copyPnpmStore "chmod -R +w $(pnpm store path)"} + + ${concatStringsSep "\n" ( + mapAttrsToList + (n: v: ''export ${n}="${v}"'') + installEnv + )} + + pnpm install ${optionalString noDevDependencies "--prod "}--frozen-lockfile --offline + ''; + + installPhase = '' + cp -r node_modules/. $out + ''; + }; }; - }; }) - (attrs // { extraNodeModuleSources = null; }) + (attrs // { extraNodeModuleSources = null; installEnv = null; }) ); } diff --git a/lockfile.nix b/lockfile.nix index b44924a..88b8235 100644 --- a/lockfile.nix +++ b/lockfile.nix @@ -6,81 +6,118 @@ }: with lib; -let - splitVersion = name: splitString "@" (head (splitString "(" name)); - getVersion = name: last (splitVersion name); - withoutVersion = name: concatStringsSep "@" (init (splitVersion name)); - gitTarball = n: v: - let - repo = - if ((v.resolution.type or "") == "git") - then - fetchGit - { - url = v.resolution.repo; - rev = v.resolution.commit; - shallow = true; - } - else - let - split = splitString "/" v.id; - in - fetchGit { - url = "https://${concatStringsSep "/" (init split)}.git"; - rev = (last split); - shallow = true; - }; - in - # runCommand (last (init (traceValSeq (splitString "/" (traceValSeq (withoutVersion (traceValSeq n))))))) { } '' - runCommand "${last (init (splitString "/" (head (splitString "(" n))))}.tgz" { } '' - tar -czf $out -C ${repo} . - ''; -in rec { parseLockfile = lockfile: builtins.fromJSON (readFile (runCommand "toJSON" { } "${remarshal}/bin/yaml2json ${lockfile} $out")); - dependencyTarballs = { registry, lockfile }: - unique ( - mapAttrsToList - (n: v: - if hasPrefix "/" n then - let - name = withoutVersion n; - baseName = last (splitString "/" (withoutVersion n)); - version = getVersion n; - in - fetchurl ( - { - url = v.resolution.tarball or "${registry}/${name}/-/${baseName}-${version}.tgz"; - } // ( - if hasPrefix "sha1-" v.resolution.integrity then - { sha1 = v.resolution.integrity; } - else - { sha512 = v.resolution.integrity; } - ) - ) - else - gitTarball n v - ) - (parseLockfile lockfile).packages - ); - - patchLockfile = lockfile: + processLockfile = { registry, lockfile, noDevDependencies }: let - orig = parseLockfile lockfile; - in - orig // { - packages = mapAttrs - (n: v: - if hasPrefix "/" n - then v - else v // { - resolution.tarball = "file:${gitTarball n v}"; + splitVersion = name: splitString "@" (head (splitString "(" name)); + getVersion = name: last (splitVersion name); + withoutVersion = name: concatStringsSep "@" (init (splitVersion name)); + switch = options: + if ((length options) == 0) + then throw "No matching case found!" + else + if ((head options).case or true) + then (head options).result + else switch (tail options); + mkTarball = pkg: contents: + runCommand "${last (init (splitString "/" (head (splitString "(" pkg))))}.tgz" { } '' + tar -czf $out -C ${contents} . + ''; + findTarball = n: v: + switch [ + { + case = (v.resolution.type or "") == "git"; + result = + mkTarball n ( + fetchGit { + url = v.resolution.repo; + rev = v.resolution.commit; + shallow = true; + } + ); + } + { + case = hasAttrByPath [ "resolution" "tarball" ] v && hasAttrByPath [ "resolution" "integrity" ] v; + result = fetchurl { + url = v.resolution.tarball; + ${head (splitString "-" v.resolution.integrity)} = v.resolution.integrity; + }; } - ) - orig.packages; + { + case = hasPrefix "https://codeload.github.com" (v.resolution.tarball or ""); + result = + let + m = strings.match "https://codeload.github.com/([^/]+)/([^/]+)/tar\\.gz/([a-f0-9]+)" v.resolution.tarball; + in + mkTarball n ( + fetchGit { + url = "https://github.com/${elemAt m 0}/${elemAt m 1}"; + rev = (elemAt m 2); + shallow = true; + } + ); + } + { + case = (v ? id); + result = + let + split = splitString "/" v.id; + in + mkTarball n ( + fetchGit { + url = "https://${concatStringsSep "/" (init split)}.git"; + rev = (last split); + shallow = true; + } + ); + } + { + case = hasPrefix "/" n; + result = + let + name = withoutVersion n; + baseName = last (splitString "/" (withoutVersion n)); + version = getVersion n; + in + fetchurl { + url = "${registry}/${name}/-/${baseName}-${version}.tgz"; + ${head (splitString "-" v.resolution.integrity)} = v.resolution.integrity; + }; + } + ]; + in + { + dependencyTarballs = + unique ( + mapAttrsToList + findTarball + (filterAttrs + (n: v: !noDevDependencies || !(v.dev or false)) + (parseLockfile lockfile).packages + ) + ); + + patchedLockfile = + let + orig = parseLockfile lockfile; + in + orig // { + packages = mapAttrs + (n: v: + v // ( + if noDevDependencies && (v.dev or false) + then { resolution = { }; } + else { + resolution.tarball = "file:${findTarball n v}"; + } + ) + ) + orig.packages; + }; }; }