2016-01-07 13:47:26 +00:00
{ stdenv , fetchurl , nodejs , python , utillinux , runCommand }:
let
# Function that generates a TGZ file from a NPM project
buildNodeSourceDist =
{ name , version , src }:
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
stdenv . mkDerivation {
name = " n o d e - t a r b a l l - ${ name } - ${ version } " ;
inherit src ;
buildInputs = [ nodejs ] ;
buildPhase = ''
export HOME = $ TMPDIR
tgzFile = $ ( npm pack )
'' ;
installPhase = ''
mkdir - p $ out/tarballs
mv $ tgzFile $ out/tarballs
mkdir - p $ out/nix-support
echo " f i l e s o u r c e - d i s t $ o u t / t a r b a l l s / $ t g z F i l e " > > $ out/nix-support/hydra-build-products
'' ;
} ;
# We must run semver to determine whether a provided dependency conforms to a certain version range
semver = buildNodePackage {
name = " s e m v e r " ;
version = " 5 . 0 . 3 " ;
src = fetchurl {
url = http://registry.npmjs.org/semver/-/semver-5.0.3.tgz ;
sha1 = " 7 7 4 6 6 d e 5 8 9 c d 5 d 3 c 9 5 f 1 3 8 a a 7 8 b c 5 6 9 a 3 c b 5 d 2 7 a " ;
} ;
} { } ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Function that produces a deployed NPM package in the Nix store
buildNodePackage =
{ name , version , src , dependencies ? { } , buildInputs ? [ ] , production ? true , npmFlags ? " " , meta ? { } , linkDependencies ? false }:
{ providedDependencies ? { } }:
let
# Generate and import a Nix expression that determines which dependencies
# are required and which are not required (and must be shimmed).
#
# It uses the semver utility to check whether a version range matches any
# of the provided dependencies.
2016-05-04 10:08:35 +00:00
analysedDependencies =
2016-01-07 13:47:26 +00:00
if dependencies = = { } then { }
else
import ( stdenv . mkDerivation {
name = " ${ name } - ${ version } - a n a l y s e d D e p e n d e n c i e s . n i x " ;
buildInputs = [ semver ] ;
buildCommand = ''
cat > $ out < < EOF
{
$ { stdenv . lib . concatMapStrings ( dependencyName :
let
dependency = builtins . getAttr dependencyName dependencies ;
versionSpecs = builtins . attrNames dependency ;
in
stdenv . lib . concatMapStrings ( versionSpec :
if builtins . hasAttr dependencyName providedDependencies # Search for any provided dependencies that match the required version spec. If one matches, the dependency should not be included
then
let
providedDependency = builtins . getAttr dependencyName providedDependencies ;
versions = builtins . attrNames providedDependency ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# If there is a version range match, add the dependency to
# the set of shimmed dependencies.
# Otherwise, it is a required dependency.
in
''
$ ( latestVersion = $ ( semver - r ' $ { versionSpec } ' $ { stdenv . lib . concatMapStrings ( version : " ' ${ version } ' " ) versions } | tail -1 | tr - d ' \ n' )
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
if semver - r ' $ { versionSpec } ' $ { stdenv . lib . concatMapStrings ( version : " ' ${ version } ' " ) versions } > /dev/null
then
echo " s h i m m e d D e p e n d e n c i e s . \" ${ dependencyName } \" . \" $ l a t e s t V e r s i o n \" = t r u e ; "
else
echo ' requiredDependencies . " ${ dependencyName } " . " ${ versionSpec } " = true ; '
fi )
''
else # If a dependency is not provided by an includer, we must always include it ourselves
" r e q u i r e d D e p e n d e n c i e s . \" ${ dependencyName } \" . \" ${ versionSpec } \" = t r u e ; \n "
) versionSpecs
) ( builtins . attrNames dependencies ) }
}
EOF
'' ;
} ) ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
requiredDependencies = analysedDependencies . requiredDependencies or { } ;
shimmedDependencies = analysedDependencies . shimmedDependencies or { } ;
# Extract the Node.js source code which is used to compile packages with native bindings
nodeSources = runCommand " n o d e - s o u r c e s " { } ''
tar - - no-same-owner - - no-same-permissions - xf $ { nodejs . src }
mv node- * $ out
'' ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Compose dependency information that this package must propagate to its
# dependencies, so that provided dependencies are not included a second time.
# This prevents cycles and wildcard version mismatches.
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
propagatedProvidedDependencies =
( stdenv . lib . mapAttrs ( dependencyName : dependency :
builtins . listToAttrs ( map ( versionSpec :
{ name = dependency . " ${ versionSpec } " . version ;
value = true ;
}
) ( builtins . attrNames dependency ) )
) dependencies ) //
providedDependencies //
{ " ${ name } " . " ${ version } " = true ; } ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Create a node_modules folder containing all required dependencies of the
# package
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
nodeDependencies = stdenv . mkDerivation {
name = " n o d e - d e p e n d e n c i e s - ${ name } - ${ version } " ;
inherit src ;
buildCommand = ''
mkdir - p $ out/lib/node_modules
cd $ out/lib/node_modules
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Create copies of (or symlinks to) the dependencies that must be deployed in this package's private node_modules folder.
# This package's private dependencies are NPM packages that have not been provided by any of the includers.
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
$ { stdenv . lib . concatMapStrings ( requiredDependencyName :
stdenv . lib . concatMapStrings ( versionSpec :
let
dependency = dependencies . " ${ requiredDependencyName } " . " ${ versionSpec } " . pkg {
providedDependencies = propagatedProvidedDependencies ;
} ;
in
''
depPath = $ ( echo $ { dependency } /lib/node_modules /* )
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
$ { if linkDependencies then ''
ln - s $ depPath .
'' e l s e ''
cp - r $ depPath .
'' }
''
) ( builtins . attrNames ( requiredDependencies . " ${ requiredDependencyName } " ) )
) ( builtins . attrNames requiredDependencies ) }
'' ;
} ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Deploy the Node package with some tricks
self = stdenv . lib . makeOverridable stdenv . mkDerivation {
inherit src meta ;
dontStrip = true ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
name = " n o d e - ${ name } - ${ version } " ;
buildInputs = [ nodejs python ] ++ stdenv . lib . optional ( stdenv . isLinux ) utillinux ++ buildInputs ;
2016-05-04 10:08:35 +00:00
dontBuild = true ;
2016-01-07 13:47:26 +00:00
installPhase = ''
# Move the contents of the tarball into the output folder
mkdir - p " $ o u t / l i b / n o d e _ m o d u l e s / ${ name } "
mv * " $ o u t / l i b / n o d e _ m o d u l e s / ${ name } "
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Enter the target directory
cd " $ o u t / l i b / n o d e _ m o d u l e s / ${ name } "
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Patch the shebangs of the bundled modules. For "regular" dependencies
# this is step is not required, because it has already been done by the generic builder.
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
if [ - d node_modules ]
then
patchShebangs node_modules
fi
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Copy the required dependencies
mkdir - p node_modules
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
$ { stdenv . lib . optionalString ( requiredDependencies != { } ) ''
for i in $ { nodeDependencies } /lib/node_modules /*
do
if [ ! - d " n o d e _ m o d u l e s / $ ( b a s e n a m e $ i ) " ]
then
cp - a $ i node_modules
fi
done
'' }
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Create shims for the packages that have been provided by earlier includers to allow the NPM install operation to still succeed
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
$ { stdenv . lib . concatMapStrings ( shimmedDependencyName :
stdenv . lib . concatMapStrings ( versionSpec :
''
mkdir - p node_modules / $ { shimmedDependencyName }
cat > node_modules / $ { shimmedDependencyName } /package.json < < EOF
{
" n a m e " : " ${ shimmedDependencyName } " ,
" v e r s i o n " : " ${ versionSpec } "
}
EOF
''
) ( builtins . attrNames ( shimmedDependencies . " ${ shimmedDependencyName } " ) )
) ( builtins . attrNames shimmedDependencies ) }
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Ignore npm-shrinkwrap.json for now. Ideally, it should be supported as well
rm - f npm-shrinkwrap . json
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# 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:
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
(
cat < < EOF
var fs = require ( ' fs' ) ;
var url = require ( ' url' ) ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
/*
* Replaces an impure version specification by *
* /
function replaceImpureVersionSpec ( versionSpec ) {
var parsedUrl = url . parse ( versionSpec ) ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
if ( versionSpec = = " l a t e s t " || versionSpec = = " u n s t a b l e " ||
versionSpec . substr ( 0 , 2 ) = = " . . " || dependency . substr ( 0 , 2 ) = = " . / " || dependency . substr ( 0 , 2 ) = = " ~ / " || dependency . substr ( 0 , 1 ) = = ' / ' )
return ' * ' ;
else if ( parsedUrl . protocol = = " g i t : " || parsedUrl . protocol = = " g i t + s s h : " || parsedUrl . protocol = = " g i t + h t t p : " || parsedUrl . protocol = = " g i t + h t t p s : " ||
parsedUrl . protocol = = " h t t p : " || parsedUrl . protocol = = " h t t p s : " )
return ' * ' ;
else
return versionSpec ;
}
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
var packageObj = JSON . parse ( fs . readFileSync ( ' ./package.json ' ) ) ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
/* R e p l a c e d e p e n d e n c i e s */
if ( packageObj . dependencies != = undefined ) {
for ( var dependency in packageObj . dependencies ) {
var versionSpec = packageObj . dependencies [ dependency ] ;
packageObj . dependencies [ dependency ] = replaceImpureVersionSpec ( versionSpec ) ;
}
}
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
/* R e p l a c e d e v e l o p m e n t d e p e n d e n c i e s */
if ( packageObj . devDependencies != = undefined ) {
for ( var dependency in packageObj . devDependencies ) {
var versionSpec = packageObj . devDependencies [ dependency ] ;
packageObj . devDependencies [ dependency ] = replaceImpureVersionSpec ( versionSpec ) ;
}
}
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
/* R e p l a c e o p t i o n a l d e p e n d e n c i e s */
if ( packageObj . optionalDependencies != = undefined ) {
for ( var dependency in packageObj . optionalDependencies ) {
var versionSpec = packageObj . optionalDependencies [ dependency ] ;
packageObj . optionalDependencies [ dependency ] = replaceImpureVersionSpec ( versionSpec ) ;
}
}
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
/* W r i t e t h e f i x e d J S O N f i l e */
fs . writeFileSync ( " p a c k a g e . j s o n " , JSON . stringify ( packageObj ) ) ;
EOF
) | node
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Deploy the Node.js package by running npm install. Since the dependencies have been symlinked, it should not attempt to install them again,
# which is good, because we want to make it Nix's responsibility. If it needs to install any dependencies anyway (e.g. because the dependency
# parameters are incomplete/incorrect), it fails.
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
export HOME = $ TMPDIR
npm - - registry http://www.example.com - - nodedir = $ { nodeSources } $ { npmFlags } $ { stdenv . lib . optionalString production " - - p r o d u c t i o n " } install
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# After deployment of the NPM package, we must remove the shims again
$ { stdenv . lib . concatMapStrings ( shimmedDependencyName :
''
rm node_modules / $ { shimmedDependencyName } /package.json
rmdir node_modules / $ { shimmedDependencyName }
''
) ( builtins . attrNames shimmedDependencies ) }
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# It makes no sense to keep an empty node_modules folder around, so delete it if this is the case
if [ - d node_modules ]
then
rmdir - - ignore-fail-on-non-empty node_modules
fi
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Create symlink to the deployed executable folder, if applicable
if [ - d " $ o u t / l i b / n o d e _ m o d u l e s / . b i n " ]
then
ln - s $ out/lib/node_modules/.bin $ out/bin
fi
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
# Create symlinks to the deployed manual page folders, if applicable
if [ - d " $ o u t / l i b / n o d e _ m o d u l e s / ${ name } / m a n " ]
then
mkdir - p $ out/share
for dir in " $ o u t / l i b / n o d e _ m o d u l e s / ${ name } / m a n / " *
do
mkdir - p $ out/share/man / $ ( basename " $ d i r " )
for page in " $ d i r " /*
do
ln - s $ page $ out/share/man / $ ( basename " $ d i r " )
done
done
fi
'' ;
2016-05-04 10:08:35 +00:00
2016-01-07 13:47:26 +00:00
shellHook = stdenv . lib . optionalString ( requiredDependencies != { } ) ''
export NODE_PATH = $ { nodeDependencies } /lib/node_modules
'' ;
} ;
in
self ;
in
{ inherit buildNodeSourceDist buildNodePackage ; }