diff options
Diffstat (limited to 'nixpkgs/pkgs/build-support/node/build-npm-package')
5 files changed, 364 insertions, 0 deletions
diff --git a/nixpkgs/pkgs/build-support/node/build-npm-package/default.nix b/nixpkgs/pkgs/build-support/node/build-npm-package/default.nix new file mode 100644 index 000000000000..1c7bf63e8cd6 --- /dev/null +++ b/nixpkgs/pkgs/build-support/node/build-npm-package/default.nix @@ -0,0 +1,88 @@ +{ lib +, stdenv +, fetchNpmDeps +, buildPackages +, nodejs +, darwin +} @ topLevelArgs: + +{ name ? "${args.pname}-${args.version}" +, src ? null +, srcs ? null +, sourceRoot ? null +, prePatch ? "" +, patches ? [ ] +, postPatch ? "" +, nativeBuildInputs ? [ ] +, buildInputs ? [ ] + # The output hash of the dependencies for this project. + # Can be calculated in advance with prefetch-npm-deps. +, npmDepsHash ? "" + # Whether to force the usage of Git dependencies that have install scripts, but not a lockfile. + # Use with care. +, forceGitDeps ? false + # Whether to force allow an empty dependency cache. + # This can be enabled if there are truly no remote dependencies, but generally an empty cache indicates something is wrong. +, forceEmptyCache ? false + # Whether to make the cache writable prior to installing dependencies. + # Don't set this unless npm tries to write to the cache directory, as it can slow down the build. +, makeCacheWritable ? false + # The script to run to build the project. +, npmBuildScript ? "build" + # Flags to pass to all npm commands. +, npmFlags ? [ ] + # Flags to pass to `npm ci`. +, npmInstallFlags ? [ ] + # Flags to pass to `npm rebuild`. +, npmRebuildFlags ? [ ] + # Flags to pass to `npm run ${npmBuildScript}`. +, npmBuildFlags ? [ ] + # Flags to pass to `npm pack`. +, npmPackFlags ? [ ] + # Flags to pass to `npm prune`. +, npmPruneFlags ? npmInstallFlags + # Value for npm `--workspace` flag and directory in which the files to be installed are found. +, npmWorkspace ? null +, nodejs ? topLevelArgs.nodejs +, npmDeps ? fetchNpmDeps { + inherit forceGitDeps forceEmptyCache src srcs sourceRoot prePatch patches postPatch; + name = "${name}-npm-deps"; + hash = npmDepsHash; +} + # Custom npmConfigHook +, npmConfigHook ? null + # Custom npmBuildHook +, npmBuildHook ? null + # Custom npmInstallHook +, npmInstallHook ? null +, ... +} @ args: + +let + # .override {} negates splicing, so we need to use buildPackages explicitly + npmHooks = buildPackages.npmHooks.override { + inherit nodejs; + }; +in +stdenv.mkDerivation (args // { + inherit npmDeps npmBuildScript; + + nativeBuildInputs = nativeBuildInputs + ++ [ + nodejs + # Prefer passed hooks + (if npmConfigHook != null then npmConfigHook else npmHooks.npmConfigHook) + (if npmBuildHook != null then npmBuildHook else npmHooks.npmBuildHook) + (if npmInstallHook != null then npmInstallHook else npmHooks.npmInstallHook) + nodejs.python + ] + ++ lib.optionals stdenv.isDarwin [ darwin.cctools ]; + buildInputs = buildInputs ++ [ nodejs ]; + + strictDeps = true; + + # Stripping takes way too long with the amount of files required by a typical Node.js project. + dontStrip = args.dontStrip or true; + + meta = (args.meta or { }) // { platforms = args.meta.platforms or nodejs.meta.platforms; }; +}) diff --git a/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/default.nix b/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/default.nix new file mode 100644 index 000000000000..36f0319e3d23 --- /dev/null +++ b/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/default.nix @@ -0,0 +1,48 @@ +{ lib +, srcOnly +, makeSetupHook +, makeWrapper +, nodejs +, jq +, prefetch-npm-deps +, diffutils +, installShellFiles +}: + +{ + npmConfigHook = makeSetupHook + { + name = "npm-config-hook"; + substitutions = { + nodeSrc = srcOnly nodejs; + nodeGyp = "${nodejs}/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js"; + + # Specify `diff`, `jq`, and `prefetch-npm-deps` by abspath to ensure that the user's build + # inputs do not cause us to find the wrong binaries. + diff = "${diffutils}/bin/diff"; + jq = "${jq}/bin/jq"; + prefetchNpmDeps = "${prefetch-npm-deps}/bin/prefetch-npm-deps"; + + nodeVersion = nodejs.version; + nodeVersionMajor = lib.versions.major nodejs.version; + }; + } ./npm-config-hook.sh; + + npmBuildHook = makeSetupHook + { + name = "npm-build-hook"; + } ./npm-build-hook.sh; + + npmInstallHook = makeSetupHook + { + name = "npm-install-hook"; + propagatedBuildInputs = [ + installShellFiles + makeWrapper + ]; + substitutions = { + hostNode = "${nodejs}/bin/node"; + jq = "${jq}/bin/jq"; + }; + } ./npm-install-hook.sh; +} diff --git a/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-build-hook.sh b/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-build-hook.sh new file mode 100644 index 000000000000..c341f672363a --- /dev/null +++ b/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-build-hook.sh @@ -0,0 +1,38 @@ +# shellcheck shell=bash + +npmBuildHook() { + echo "Executing npmBuildHook" + + runHook preBuild + + if [ -z "${npmBuildScript-}" ]; then + echo + echo "ERROR: no build script was specified" + echo 'Hint: set `npmBuildScript`, override `buildPhase`, or set `dontNpmBuild = true`.' + echo + + exit 1 + fi + + if ! npm run ${npmWorkspace+--workspace=$npmWorkspace} "$npmBuildScript" $npmBuildFlags "${npmBuildFlagsArray[@]}" $npmFlags "${npmFlagsArray[@]}"; then + echo + echo 'ERROR: `npm build` failed' + echo + echo "Here are a few things you can try, depending on the error:" + echo "1. Make sure your build script ($npmBuildScript) exists" + echo ' If there is none, set `dontNpmBuild = true`.' + echo '2. If the error being thrown is something similar to "error:0308010C:digital envelope routines::unsupported", add `NODE_OPTIONS = "--openssl-legacy-provider"` to your derivation' + echo " See https://github.com/webpack/webpack/issues/14532 for more information." + echo + + exit 1 + fi + + runHook postBuild + + echo "Finished npmBuildHook" +} + +if [ -z "${dontNpmBuild-}" ] && [ -z "${buildPhase-}" ]; then + buildPhase=npmBuildHook +fi diff --git a/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-config-hook.sh b/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-config-hook.sh new file mode 100644 index 000000000000..486b0c2f8372 --- /dev/null +++ b/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-config-hook.sh @@ -0,0 +1,119 @@ +# shellcheck shell=bash + +npmConfigHook() { + echo "Executing npmConfigHook" + + # Use npm patches in the nodejs package + export NIX_NODEJS_BUILDNPMPACKAGE=1 + export prefetchNpmDeps="@prefetchNpmDeps@" + + if [ -n "${npmRoot-}" ]; then + pushd "$npmRoot" + fi + + echo "Configuring npm" + + export HOME="$TMPDIR" + export npm_config_nodedir="@nodeSrc@" + export npm_config_node_gyp="@nodeGyp@" + + if [ -z "${npmDeps-}" ]; then + echo + echo "ERROR: no dependencies were specified" + echo 'Hint: set `npmDeps` if using these hooks individually. If this is happening with `buildNpmPackage`, please open an issue.' + echo + + exit 1 + fi + + local -r cacheLockfile="$npmDeps/package-lock.json" + local -r srcLockfile="$PWD/package-lock.json" + + echo "Validating consistency between $srcLockfile and $cacheLockfile" + + if ! @diff@ "$srcLockfile" "$cacheLockfile"; then + # If the diff failed, first double-check that the file exists, so we can + # give a friendlier error msg. + if ! [ -e "$srcLockfile" ]; then + echo + echo "ERROR: Missing package-lock.json from src. Expected to find it at: $srcLockfile" + echo "Hint: You can copy a vendored package-lock.json file via postPatch." + echo + + exit 1 + fi + + if ! [ -e "$cacheLockfile" ]; then + echo + echo "ERROR: Missing lockfile from cache. Expected to find it at: $cacheLockfile" + echo + + exit 1 + fi + + echo + echo "ERROR: npmDepsHash is out of date" + echo + echo "The package-lock.json in src is not the same as the in $npmDeps." + echo + echo "To fix the issue:" + echo '1. Use `lib.fakeHash` as the npmDepsHash value' + echo "2. Build the derivation and wait for it to fail with a hash mismatch" + echo "3. Copy the 'got: sha256-' value back into the npmDepsHash field" + echo + + exit 1 + fi + + export CACHE_MAP_PATH="$TMP/MEOW" + @prefetchNpmDeps@ --map-cache + + @prefetchNpmDeps@ --fixup-lockfile "$srcLockfile" + + local cachePath + + if [ -z "${makeCacheWritable-}" ]; then + cachePath="$npmDeps" + else + echo "Making cache writable" + cp -r "$npmDeps" "$TMPDIR/cache" + chmod -R 700 "$TMPDIR/cache" + cachePath="$TMPDIR/cache" + fi + + npm config set cache "$cachePath" + npm config set offline true + npm config set progress false + + echo "Installing dependencies" + + if ! npm ci --ignore-scripts $npmInstallFlags "${npmInstallFlagsArray[@]}" $npmFlags "${npmFlagsArray[@]}"; then + echo + echo "ERROR: npm failed to install dependencies" + echo + echo "Here are a few things you can try, depending on the error:" + echo '1. Set `makeCacheWritable = true`' + echo " Note that this won't help if npm is complaining about not being able to write to the logs directory -- look above that for the actual error." + echo '2. Set `npmFlags = [ "--legacy-peer-deps" ]`' + echo + + exit 1 + fi + + patchShebangs node_modules + + npm rebuild $npmRebuildFlags "${npmRebuildFlagsArray[@]}" $npmFlags "${npmFlagsArray[@]}" + + patchShebangs node_modules + + rm "$CACHE_MAP_PATH" + unset CACHE_MAP_PATH + + if [ -n "${npmRoot-}" ]; then + popd + fi + + echo "Finished npmConfigHook" +} + +postPatchHooks+=(npmConfigHook) diff --git a/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-install-hook.sh b/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-install-hook.sh new file mode 100644 index 000000000000..750ed421789f --- /dev/null +++ b/nixpkgs/pkgs/build-support/node/build-npm-package/hooks/npm-install-hook.sh @@ -0,0 +1,71 @@ +# shellcheck shell=bash + +npmInstallHook() { + echo "Executing npmInstallHook" + + runHook preInstall + + local -r packageOut="$out/lib/node_modules/$(@jq@ --raw-output '.name' package.json)" + + # `npm pack` writes to cache so temporarily override it + while IFS= read -r file; do + local dest="$packageOut/$(dirname "$file")" + mkdir -p "$dest" + cp "${npmWorkspace-.}/$file" "$dest" + done < <(@jq@ --raw-output '.[0].files | map(.path | select(. | startswith("node_modules/") | not)) | join("\n")' <<< "$(npm_config_cache="$HOME/.npm" npm pack --json --dry-run --loglevel=warn --no-foreground-scripts ${npmWorkspace+--workspace=$npmWorkspace} $npmPackFlags "${npmPackFlagsArray[@]}" $npmFlags "${npmFlagsArray[@]}")") + + # Based on code from Python's buildPythonPackage wrap.sh script, for + # supporting both the case when makeWrapperArgs is an array and a + # IFS-separated string. + # + # TODO: remove the string branch when __structuredAttrs are used. + if [[ "${makeWrapperArgs+defined}" == "defined" && "$(declare -p makeWrapperArgs)" =~ ^'declare -a makeWrapperArgs=' ]]; then + local -a user_args=("${makeWrapperArgs[@]}") + else + local -a user_args="(${makeWrapperArgs:-})" + fi + while IFS=" " read -ra bin; do + mkdir -p "$out/bin" + makeWrapper @hostNode@ "$out/bin/${bin[0]}" --add-flags "$packageOut/${bin[1]}" "${user_args[@]}" + done < <(@jq@ --raw-output '(.bin | type) as $typ | if $typ == "string" then + .name + " " + .bin + elif $typ == "object" then .bin | to_entries | map(.key + " " + .value) | join("\n") + elif $typ == "null" then empty + else "invalid type " + $typ | halt_error end' "${npmWorkspace-.}/package.json") + + while IFS= read -r man; do + installManPage "$packageOut/$man" + done < <(@jq@ --raw-output '(.man | type) as $typ | if $typ == "string" then .man + elif $typ == "list" then .man | join("\n") + elif $typ == "null" then empty + else "invalid type " + $typ | halt_error end' "${npmWorkspace-.}/package.json") + + local -r nodeModulesPath="$packageOut/node_modules" + + if [ ! -d "$nodeModulesPath" ]; then + if [ -z "${dontNpmPrune-}" ]; then + if ! npm prune --omit=dev --no-save ${npmWorkspace+--workspace=$npmWorkspace} $npmPruneFlags "${npmPruneFlagsArray[@]}" $npmFlags "${npmFlagsArray[@]}"; then + echo + echo + echo "ERROR: npm prune step failed" + echo + echo 'If npm tried to download additional dependencies above, try setting `dontNpmPrune = true`.' + echo + + exit 1 + fi + fi + + find node_modules -maxdepth 1 -type d -empty -delete + + cp -r node_modules "$nodeModulesPath" + fi + + runHook postInstall + + echo "Finished npmInstallHook" +} + +if [ -z "${dontNpmInstall-}" ] && [ -z "${installPhase-}" ]; then + installPhase=npmInstallHook +fi |