From 74f5ff78bfb0caae8dff6554b57dab7ca5ea9ca4 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 22 Feb 2024 23:22:44 +0100 Subject: [PATCH] pnpm.fetchDeps: init Signed-off-by: Sefa Eyeoglu --- .../javascript.section.md | 63 ++++++++++++++ .../tools/pnpm/fetch-deps/default.nix | 83 +++++++++++++++++++ .../tools/pnpm/fetch-deps/pnpm-config-hook.sh | 40 +++++++++ pkgs/development/tools/pnpm/generic.nix | 11 ++- 4 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 pkgs/development/tools/pnpm/fetch-deps/default.nix create mode 100644 pkgs/development/tools/pnpm/fetch-deps/pnpm-config-hook.sh diff --git a/doc/languages-frameworks/javascript.section.md b/doc/languages-frameworks/javascript.section.md index f706f92c6691..5421ca0258b1 100644 --- a/doc/languages-frameworks/javascript.section.md +++ b/doc/languages-frameworks/javascript.section.md @@ -310,6 +310,69 @@ See `node2nix` [docs](https://github.com/svanderburg/node2nix) for more info. - `node2nix` has some [bugs](https://github.com/svanderburg/node2nix/issues/238) related to working with lock files from npm distributed with `nodejs_16`. - `node2nix` does not like missing packages from npm. If you see something like `Cannot resolve version: vue-loader-v16@undefined` then you might want to try another tool. The package might have been pulled off of npm. +### pnpm {#javascript-pnpm} + +Pnpm is available as the top-level package `pnpm`. Additionally, there are variants pinned to certain major versions, like `pnpm_8` and `pnpm_9`, which support different sets of lock file versions. + +When packaging an application that includes a `pnpm-lock.yaml`, you need to fetch the pnpm store for that project using a fixed-output-derivation. The functions `pnpm_8.fetchDeps` and `pnpm_9.fetchDeps` can create this pnpm store derivation. In conjunction, the setup hooks `pnpm_8.configHook` and `pnpm_9.configHook` will prepare the build environment to install the prefetched dependencies store. Here is an example for a package that contains a `package.json` and a `pnpm-lock.yaml` files using the above `pnpm_` attributes: + +```nix +{ + stdenv, + nodejs, + # This is pinned as { pnpm = pnpm_9; } + pnpm +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "foo"; + version = "0-unstable-1980-01-01"; + + src = ...; + + nativeBuildInputs = [ + nodejs + pnpm.configHook + ]; + + pnpmDeps = pnpm.fetchDeps { + inherit (finalAttrs) pname version src; + hash = "..."; + }; +}) +``` + +NOTE: It is highly recommended to use a pinned version of pnpm (i.e. `pnpm_8` or `pnpm_9`), to increase future reproducibility. It might also be required to use an older version, if the package needs support for a certain lock file version. + +In case you are patching `package.json` or `pnpm-lock.yaml`, make sure to pass `finalAttrs.patches` to the function as well (i.e. `inherit (finalAttrs) patches`. + +#### Dealing with `sourceRoot` {#javascript-pnpm-sourceRoot} + +If the pnpm project is in a subdirectory, you can just define `sourceRoot` or `setSourceRoot` for `fetchDeps`. Note, that projects using `pnpm-workspace.yaml` are currently not supported, and will probably not work using this approach. +If `sourceRoot` is different between the parent derivation and `fetchDeps`, you will have to set `pnpmRoot` to effectively be the same location as it is in `fetchDeps`. + +Assuming the following directory structure, we can define `sourceRoot` and `pnpmRoot` as follows: + +``` +. +├── frontend +│   ├── ... +│   ├── package.json +│   └── pnpm-lock.yaml +└── ... +``` + +```nix + ... + pnpmDeps = pnpm.fetchDeps { + ... + sourceRoot = "${finalAttrs.src.name}/frontend"; + }; + + # by default the working directory is the extracted source + pnpmRoot = "frontend"; +``` + ### yarn2nix {#javascript-yarn2nix} #### Preparation {#javascript-yarn2nix-preparation} diff --git a/pkgs/development/tools/pnpm/fetch-deps/default.nix b/pkgs/development/tools/pnpm/fetch-deps/default.nix new file mode 100644 index 000000000000..3a74f4cfe389 --- /dev/null +++ b/pkgs/development/tools/pnpm/fetch-deps/default.nix @@ -0,0 +1,83 @@ +{ + stdenvNoCC, + fetchurl, + jq, + moreutils, + cacert, + makeSetupHook, + pnpm, +}: +{ + fetchDeps = + { + src, + hash ? "", + pname, + ... + }@args: + let + args' = builtins.removeAttrs args [ + "hash" + "pname" + ]; + hash' = + if hash != "" then + { outputHash = hash; } + else + { + outputHash = ""; + outputHashAlgo = "sha256"; + }; + in + stdenvNoCC.mkDerivation ( + args' + // { + name = "${pname}-pnpm-deps"; + + nativeBuildInputs = [ + jq + moreutils + pnpm + cacert + ]; + + installPhase = '' + runHook preInstall + + export HOME=$(mktemp -d) + pnpm config set store-dir $out + # Some packages produce platform dependent outputs. We do not want to cache those in the global store + pnpm config set side-effects-cache false + # As we pin pnpm versions, we don't really care about updates + pnpm config set update-notifier false + # pnpm is going to warn us about using --force + # --force allows us to fetch all dependencies including ones that aren't meant for our host platform + pnpm install --frozen-lockfile --ignore-script --force + + runHook postInstall + ''; + + fixupPhase = '' + runHook preFixup + + # Remove timestamp and sort the json files + rm -rf $out/v3/tmp + for f in $(find $out -name "*.json"); do + jq --sort-keys "del(.. | .checkedAt?)" $f | sponge $f + done + + runHook postFixup + ''; + + dontConfigure = true; + dontBuild = true; + outputHashMode = "recursive"; + } + // hash' + ); + + configHook = makeSetupHook { + name = "pnpm-config-hook"; + propagatedBuildInputs = [ pnpm ]; + } ./pnpm-config-hook.sh; +} diff --git a/pkgs/development/tools/pnpm/fetch-deps/pnpm-config-hook.sh b/pkgs/development/tools/pnpm/fetch-deps/pnpm-config-hook.sh new file mode 100644 index 000000000000..fc0c47dbb70b --- /dev/null +++ b/pkgs/development/tools/pnpm/fetch-deps/pnpm-config-hook.sh @@ -0,0 +1,40 @@ +# shellcheck shell=bash + +pnpmConfigHook() { + echo "Executing pnpmConfigHook" + + if [ -n "${pnpmRoot-}" ]; then + pushd "$pnpmRoot" + fi + + if [ -z "${pnpmDeps-}" ]; then + echo "Error: 'pnpmDeps' must be set when using pnpmConfigHook." + exit 1 + fi + + echo "Configuring pnpm store" + + export HOME=$(mktemp -d) + export STORE_PATH=$(mktemp -d) + + cp -Tr "$pnpmDeps" "$STORE_PATH" + chmod -R +w "$STORE_PATH" + + pnpm config set store-dir "$STORE_PATH" + + echo "Installing dependencies" + + pnpm install --offline --frozen-lockfile --ignore-script + + echo "Patching scripts" + + patchShebangs node_modules/{*,.*} + + if [ -n "${pnpmRoot-}" ]; then + popd + fi + + echo "Finished pnpmConfigHook" +} + +postConfigureHooks+=(pnpmConfigHook) diff --git a/pkgs/development/tools/pnpm/generic.nix b/pkgs/development/tools/pnpm/generic.nix index 81fd5c2967dd..c53dfd78f673 100644 --- a/pkgs/development/tools/pnpm/generic.nix +++ b/pkgs/development/tools/pnpm/generic.nix @@ -1,6 +1,7 @@ { lib, stdenvNoCC, + callPackages, fetchurl, nodejs, testers, @@ -8,9 +9,7 @@ version, hash, -}: - -stdenvNoCC.mkDerivation (finalAttrs: { +}: stdenvNoCC.mkDerivation (finalAttrs: { pname = "pnpm"; inherit version; @@ -32,7 +31,11 @@ stdenvNoCC.mkDerivation (finalAttrs: { runHook postInstall ''; - passthru = { + passthru = let + fetchDepsAttrs = callPackages ./fetch-deps { pnpm = finalAttrs.finalPackage; }; + in { + inherit (fetchDepsAttrs) fetchDeps configHook; + tests.version = lib.optionalAttrs withNode ( testers.testVersion { package = finalAttrs.finalPackage; } );