nixpkgs/pkgs/development/web/nodejs/build-node-package.nix

334 lines
11 KiB
Nix

{ stdenv, runCommand, nodejs, neededNatives}:
{
name, version ? "", src,
# by default name of nodejs interpreter e.g. "nodejs-${name}"
namePrefix ? nodejs.interpreterName + "-",
# Node package name
pkgName ?
if version != "" then stdenv.lib.removeSuffix "-${version}" name else
(builtins.parseDrvName name).name,
# List or attribute set of dependencies
deps ? {},
# List or attribute set of peer depencies
peerDependencies ? {},
# List or attribute set of optional dependencies
optionalDependencies ? {},
# List of optional dependencies to skip
skipOptionalDependencies ? [],
# Whether package is binary or library
bin ? false,
# Additional flags passed to npm install
flags ? "",
# Command to be run before shell hook
preShellHook ? "",
# Command to be run after shell hook
postShellHook ? "",
# Same as https://docs.npmjs.com/files/package.json#os
os ? [],
# Same as https://docs.npmjs.com/files/package.json#cpu
cpu ? [],
# Attribute set of already resolved deps (internal),
# for avoiding infinite recursion
resolvedDeps ? {},
...
} @ args:
with stdenv.lib;
let
self = let
sources = runCommand "node-sources" {} ''
tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
mv $(find . -type d -mindepth 1 -maxdepth 1) $out
'';
platforms = if os == [] then nodejs.meta.platforms else
fold (entry: platforms:
let
filterPlatforms =
stdenv.lib.platforms.${removePrefix "!" entry} or [];
in
# Ignore unknown platforms
if filterPlatforms == [] then platforms
else
if hasPrefix "!" entry then
subtractLists (intersectLists filterPlatforms nodejs.meta.platforms) platforms
else
platforms ++ (intersectLists filterPlatforms nodejs.meta.platforms)
) [] os;
mapDependencies = deps: f: rec {
# 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
# Also override with already resolved dependencies
requiredDeps = mapAttrs (name: dep:
dep.override {
resolvedDeps = resolvedDeps // { "${name}" = self; };
}
) (filterAttrs f (removeAttrs attrDeps (attrNames resolvedDeps)));
# Recursive dependencies that we want to avoid with shim creation
recursiveDeps = filterAttrs f (removeAttrs attrDeps (attrNames requiredDeps));
};
_dependencies = mapDependencies deps (name: dep:
dep.pkgName != pkgName);
_optionalDependencies = mapDependencies optionalDependencies (name: dep:
(builtins.tryEval dep).success &&
!(elem dep.pkgName skipOptionalDependencies)
);
_peerDependencies = mapDependencies peerDependencies (name: dep:
dep.pkgName != pkgName);
requiredDependencies =
_dependencies.requiredDeps //
_optionalDependencies.requiredDeps //
_peerDependencies.requiredDeps;
recursiveDependencies =
_dependencies.recursiveDeps //
_optionalDependencies.recursiveDeps //
_peerDependencies.recursiveDeps;
patchShebangs = dir: ''
node=`type -p node`
coffee=`type -p coffee || true`
find -L ${dir} -type f -print0 | xargs -0 grep -Il . | \
xargs 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/
''
) (attrValues requiredDependencies)}
# 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 recursiveDependencies)}
)
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
GZIP=-1 tar -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 --fetch-retries 0 ${flags})
runHook postBuild
'';
installPhase = ''
runHook preInstall
(
cd $HOME
# Remove shims
${concatMapStrings (dep: ''
rm node_modules/${dep.pkgName}/package.json
rmdir node_modules/${dep.pkgName}
'') (attrValues recursiveDependencies)}
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
# Move peer dependencies to node_modules
${concatMapStrings (dep: ''
mv node_modules/${dep.pkgName} $out/lib/node_modules
'') (attrValues _peerDependencies.requiredDeps)}
# 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/
'') (attrValues requiredDependencies)}
${postShellHook}
'';
# Stipping does not make a lot of sense in node packages
dontStrip = true;
meta = {
inherit platforms;
maintainers = [ stdenv.lib.maintainers.offline ];
};
passthru.pkgName = pkgName;
} // (filterAttrs (n: v: all (k: n != k) ["deps" "resolvedDeps" "optionalDependencies"]) args) // {
name = namePrefix + name;
# Run the node setup hook when this package is a build input
propagatedNativeBuildInputs = (args.propagatedNativeBuildInputs or []) ++ [ nodejs ];
nativeBuildInputs =
(args.nativeBuildInputs or []) ++ neededNatives ++
(attrValues requiredDependencies);
# Expose list of recursive dependencies upstream, up to the package that
# caused recursive dependency
recursiveDeps =
(flatten (
map (dep: remove name dep.recursiveDeps) (attrValues requiredDependencies)
)) ++
(attrNames recursiveDependencies);
});
in self