nixpkgs/pkgs/development/web/nodejs/build-node-package.nix
Valérian Galliat 25ce27b45e Patch npm packages to ignore npm requirements
Some npm packages strictly require a specific npm version because npm
don't handle some version requirements the same way in 1.0 and 2.0.

However, Nix don't use npm for dependency resolution (this is always
achieved with npm 1.0 criteria by npm2nix), so these requirements turns
out to be pointless and just crashes some package installations.

This patch removes npm requirements so the packages can still be built.

This solves #5787.
2015-02-07 15:17:15 +01:00

280 lines
8.8 KiB
Nix

{ stdenv, runCommand, nodejs, neededNatives}:
{
name, src,
# Node package name
pkgName ? (builtins.parseDrvName name).name,
# List or attribute set of dependencies
deps ? {},
# List or attribute set of peer depencies
peerDependencies ? [],
# Whether package is binary or library
bin ? null,
# Flags passed to npm install
flags ? [],
# Command to be run before shell hook
preShellHook ? "",
# Command to be run after shell hook
postShellHook ? "",
# Attribute set of already resolved deps (internal),
# for avoiding infinite recursion
resolvedDeps ? {},
...
} @ args:
with stdenv.lib;
let
npmFlags = concatStringsSep " " (map (v: "--${v}") flags);
sources = runCommand "node-sources" {} ''
tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
mv *node* $out
'';
# Convert deps to attribute set
attrDeps = if isAttrs deps then deps else
(listToAttrs (map (dep: nameValuePair dep.name dep) deps));
# All required node modules, without already resolved dependencies
requiredDeps = removeAttrs attrDeps (attrNames resolvedDeps);
# Recursive dependencies that we want to avoid with shim creation
recursiveDeps = removeAttrs attrDeps (attrNames requiredDeps);
peerDeps = filter (dep: dep.pkgName != pkgName) peerDependencies;
self = let
# Pass resolved dependencies to dependencies of this package
deps = map (
dep: dep.override {
resolvedDeps = resolvedDeps // { "${name}" = self; };
}
) (attrValues requiredDeps);
patchShebangs = dir: ''
node=`type -p node`
coffee=`type -p coffee || true`
find -L ${dir} -type f -print0 | \
xargs -0 sed --follow-symlinks -i \
-e 's@#!/usr/bin/env node@#!'"$node"'@' \
-e 's@#!/usr/bin/env coffee@#!'"$coffee"'@' \
-e 's@#!/.*/node@#!'"$node"'@' \
-e 's@#!/.*/coffee@#!'"$coffee"'@' || true
'';
in stdenv.mkDerivation ({
inherit src;
configurePhase = ''
runHook preConfigure
${patchShebangs "./"}
# Some version specifiers (latest, unstable, URLs, file paths) force NPM
# to make remote connections or consult paths outside the Nix store.
# The following JavaScript replaces these by * to prevent that:
# Also some packages require a specific npm version because npm may
# resovle dependencies differently, but npm is not used by Nix for dependency
# reslution, so these requirements are dropped.
(
cat <<EOF
var fs = require('fs');
var url = require('url');
/*
* Replaces an impure version specification by *
*/
function replaceImpureVersionSpec(versionSpec) {
var parsedUrl = url.parse(versionSpec);
if(versionSpec == "latest" || versionSpec == "unstable" ||
versionSpec.substr(0, 2) == ".." || dependency.substr(0, 2) == "./" || dependency.substr(0, 2) == "~/" || dependency.substr(0, 1) == '/' || /^[^/]+\/[^/]+$/.test(versionSpec))
return '*';
else if(parsedUrl.protocol == "git:" || parsedUrl.protocol == "git+ssh:" || parsedUrl.protocol == "git+http:" || parsedUrl.protocol == "git+https:" ||
parsedUrl.protocol == "http:" || parsedUrl.protocol == "https:")
return '*';
else
return versionSpec;
}
var packageObj = JSON.parse(fs.readFileSync('./package.json'));
/* Replace dependencies */
if(packageObj.dependencies !== undefined) {
for(var dependency in packageObj.dependencies) {
var versionSpec = packageObj.dependencies[dependency];
packageObj.dependencies[dependency] = replaceImpureVersionSpec(versionSpec);
}
}
/* Replace development dependencies */
if(packageObj.devDependencies !== undefined) {
for(var dependency in packageObj.devDependencies) {
var versionSpec = packageObj.devDependencies[dependency];
packageObj.devDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
}
}
/* Replace optional dependencies */
if(packageObj.optionalDependencies !== undefined) {
for(var dependency in packageObj.optionalDependencies) {
var versionSpec = packageObj.optionalDependencies[dependency];
packageObj.optionalDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
}
}
/* Ignore npm version requirement */
if(packageObj.engines) {
delete packageObj.engines.npm;
}
/* Write the fixed JSON file */
fs.writeFileSync("package.json", JSON.stringify(packageObj));
EOF
) | node
# We do not handle shrinkwraps yet
rm npm-shrinkwrap.json 2>/dev/null || true
mkdir build-dir
(
cd build-dir
mkdir node_modules
# Symlink or copy dependencies for node modules
# copy is needed if dependency has recursive dependencies,
# because node can't follow symlinks while resolving recursive deps.
${concatMapStrings (dep:
if dep.recursiveDeps == [] then ''
ln -sv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
'' else ''
cp -R ${dep}/lib/node_modules/${dep.pkgName} node_modules/
''
) deps}
# Symlink peer dependencies
${concatMapStrings (dep: ''
ln -sv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
'') peerDeps}
# Create shims for recursive dependenceies
${concatMapStrings (dep: ''
mkdir -p node_modules/${dep.pkgName}
cat > node_modules/${dep.pkgName}/package.json <<EOF
{
"name": "${dep.pkgName}",
"version": "${getVersion dep}"
}
EOF
'') (attrValues recursiveDeps)}
)
export HOME=$PWD/build-dir
runHook postConfigure
'';
buildPhase = ''
runHook preBuild
# If source was a file, repackage it, so npm pre/post publish hooks are not triggered,
if [[ -f $src ]]; then
tar --exclude='build-dir' -czf build-dir/package.tgz ./
export src=$HOME/package.tgz
else
export src=$PWD
fi
# Install package
(cd $HOME && npm --registry http://www.example.com --nodedir=${sources} install $src ${npmFlags})
runHook postBuild
'';
installPhase = ''
runHook preInstall
(
cd $HOME
# Remove shims
${concatMapStrings (dep: ''
rm node_modules/${dep.pkgName}/package.json
rmdir node_modules/${dep.pkgName}
'') (attrValues recursiveDeps)}
mkdir -p $out/lib/node_modules
# Install manual
mv node_modules/${pkgName} $out/lib/node_modules
rm -fR $out/lib/node_modules/${pkgName}/node_modules
cp -r node_modules $out/lib/node_modules/${pkgName}/node_modules
if [ -e "$out/lib/node_modules/${pkgName}/man" ]; then
mkdir -p $out/share
for dir in "$out/lib/node_modules/${pkgName}/man/"*; do
mkdir -p $out/share/man/$(basename "$dir")
for page in "$dir"/*; do
ln -sv $page $out/share/man/$(basename "$dir")
done
done
fi
# Symlink dependencies
${concatMapStrings (dep: ''
mv node_modules/${dep.pkgName} $out/lib/node_modules
'') peerDeps}
# Install binaries and patch shebangs
mv node_modules/.bin $out/lib/node_modules 2>/dev/null || true
if [ -d "$out/lib/node_modules/.bin" ]; then
ln -sv $out/lib/node_modules/.bin $out/bin
${patchShebangs "$out/lib/node_modules/.bin/*"}
fi
)
runHook postInstall
'';
preFixup = ''
find $out -type f -print0 | xargs -0 sed -i 's|${src}|${src.name}|g'
'';
shellHook = ''
${preShellHook}
export PATH=${nodejs}/bin:$(pwd)/node_modules/.bin:$PATH
mkdir -p node_modules
${concatMapStrings (dep: ''
ln -sfv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
'') deps}
${postShellHook}
'';
passthru.pkgName = pkgName;
} // (filterAttrs (n: v: n != "deps" && n != "resolvedDeps") args) // {
name = "${
if bin == true then "bin-" else if bin == false then "node-" else ""
}${name}";
# Run the node setup hook when this package is a build input
propagatedNativeBuildInputs = (args.propagatedNativeBuildInputs or []) ++ [ nodejs ];
# Make buildNodePackage useful with --run-env
nativeBuildInputs = (args.nativeBuildInputs or []) ++ deps ++ peerDependencies ++ neededNatives;
# Expose list of recursive dependencies upstream, up to the package that
# caused recursive dependency
recursiveDeps = (flatten (map (d: remove name d.recursiveDeps) deps)) ++ (attrNames recursiveDeps);
});
in self