packages feed

nixfromnpm-0.10.1: nix-libs/nodeLib/buildNodePackage.nix

{
  # System packages.
  pkgs,
  # Derivation for nodejs and npm.
  nodejs,
  # Which version of npm to use.
  npm ? nodejs,
  # List of required native build inputs.
  neededNatives,
  # Self-reference for overriding purposes.
  buildNodePackage,
}:

let
  inherit (pkgs) stdenv;
  inherit (pkgs.lib) showVal;
  # This expression builds the raw C headers and source files for the base
  # node.js installation. Node packages which use the C API for node need to
  # link against these files and use the headers.
  nodejsSources = pkgs.runCommand "node-sources" {} ''
    tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
    mv $(find . -type d -mindepth 1 -maxdepth 1) $out
  '';

  # The path within $out/lib to find a package. If the package does not
  # have a namespace, it will simply be in `node_modules`, and otherwise it
  # will appear in `node_modules/@namespace`.
  modulePath = pkg:
    if !(builtins.hasAttr "namespace" pkg)
    then throw ''
      Package dependency does not appear to be a node package: ${showVal pkg}.
      Use "buildInputs" or "propagatedBuildInputs" for non-node dependencies,
      instead of "deps".
    ''
    else if pkg.namespace == null then "node_modules"
    else "node_modules/@${pkg.namespace}";

  # The path to the package within its modulePath. Just appending the name
  # of the package.
  pathInModulePath = pkg: "${modulePath pkg}/${pkg.basicName}";

  # By default, when checking we'll run npm test.
  defaultCheckPhase = ''
    runHook preCheck
    npm test
    runHook postCheck
  '';
in

{
  # The name of the package. If it's a private package with a namespace,
  # this should not contain the namespace.
  name,

  # Used for private packages. Indicated in the name field of the
  # package.json, e.g. if the name in that file is "@mynamespace/mypackage",
  # then the namespace is 'mynamespace' and the name is 'mypackage'. Public
  # packages will not need this.
  namespace ? null,

  # Version of the package. This should follow the semver standard, although
  # we don't explicitly enforce that in this function.
  version,

  # Source of the package; can be a tarball or a folder on the filesystem.
  src,

  # The suffix to the name as it appears in `nix-env -q` and the nix store. By
  # default, the name of nodejs interpreter e.g:
  # "<package name>-<package version>-nodejs-<nodejs version>"
  nameSuffix ? "-${nodejs.name}",

  # If there's a namespace, by default it will be prepended to the package
  # name. Otherwise, a prefix can be given explicitly.
  namePrefix ? (if namespace == null then "" else "${namespace}-"),

  # List or attribute set of (runtime) dependencies.
  deps ? {},

  # List or attribute set of peer dependencies. See:
  # https://nodejs.org/en/blog/npm/peer-dependencies/
  peerDependencies ? {},

  # List or attribute set of optional dependencies.
  optionalDependencies ? {},

  # List of optional dependencies to skip. List of strings, where a string
  # should contain the `name` of the derivation to skip (not a version or
  # namespace).
  skipOptionalDependencies ? [],

  # List or set of development dependencies (or null). These will only be
  # installed when `installDevDependencies` is true, which is provided by
  # the `.env` attribute.
  devDependencies ? null,

  # If true and devDependencies are defined, the package will only be
  # installed contingent on successfully running tests.
  doCheck ? false,

  # Test command.
  checkPhase ? defaultCheckPhase,

  # Additional flags passed to npm install. A list of strings.
  extraNpmFlags ? [],

  # Same as https://docs.npmjs.com/files/package.json#os
  os ? [],

  # Attribute set of already resolved dependencies, for avoiding infinite
  # recursion. Used internally (don't use this argument explicitly).
  resolvedDeps ? {},

  # Build inputs to propagate in addition to nodejs and non-dev dependencies.
  propagatedBuildInputs ? [],

  # Build inputs in addition to npm and dev dependencies.
  buildInputs ? [],

  # Whether to strip debugging symbols from binaries and patch ELF executables.
  # These should both probably be true but can be overridden.
  # Doc for details: https://nixos.org/wiki/NixPkgs_Standard_Environment.
  dontStrip ? true, dontPatchELF ? true,

  # Optional attributes to pass through to downstream derivations.
  passthru ? {},

  # A set of dependencies to patch.
  patchDependencies ? {},

  # Any remaining flags are passed through to mkDerivation.
  ...
} @ args:

let
  # The package name as it appears in the package.json. This contains a
  # namespace if there is one, so it will be a distinct identifier for
  # different packages.
  packageJsonName = if namespace == null then name
                    else "@${namespace}/${name}";

  # The package name with a version appended. This should be unique amongst
  # all packages.
  uniqueName = "${packageJsonName}@${version}";

in

if doCheck && (devDependencies == null)
then throw ("${uniqueName}: Can't run tests because devDependencies have " +
            "not been defined.")
else

let
  inherit (stdenv.lib) fold removePrefix hasPrefix subtractLists isList flip
                       intersectLists isAttrs listToAttrs nameValuePair
                       mapAttrs filterAttrs attrNames elem concatMapStrings
                       attrValues getVersion flatten remove concatStringsSep;

  dependencyTypes = ["dependencies" "devDependencies" "peerDependencies"
                     "optionalDependencies"];


  # These arguments are intended as directives to this function and not
  # to be passed through to mkDerivation. They are removed below.
  attrsToRemove = ["deps" "resolvedDeps" "flags" "os" "skipOptionalDependencies"
                   "passthru" "doCheck" "installDevDependencies" "version"
                   "namespace" "patchDependencies"] ++ dependencyTypes;

  # We create a `self` object for self-referential expressions. It
  # bottoms out in a call to `mkDerivation` at the end.
  self = let
    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 (if platforms == [] then nodejs.meta.platforms else platforms)
          else
            if hasPrefix "!" entry then
              subtractLists (intersectLists filterPlatforms nodejs.meta.platforms) platforms
            else
              platforms ++ (intersectLists filterPlatforms nodejs.meta.platforms)
      ) [] os;


    strict = x: builtins.seq x x;

    toAttrSet = obj: if isAttrs obj then obj else
      (listToAttrs (map (x: nameValuePair x.name x) obj));

    mapDependencies = deps: filterFunc: let
        attrDeps = toAttrSet deps;
      in rec {
        # All required node modules, without already resolved dependencies
        # Also override with already resolved dependencies
        requiredDeps = strict (mapAttrs (name: dep:
          dep.overrideNodePackage {
            resolvedDeps = resolvedDeps // {"${name}" = self;};
          }
        ) (filterAttrs filterFunc
            (removeAttrs attrDeps (attrNames resolvedDeps))));

        # Recursive dependencies that we want to avoid with shim creation
        recursiveDeps = strict (filterAttrs filterFunc
                          (removeAttrs attrDeps (attrNames requiredDeps)));
      };

    # Filter out self-referential dependencies.
    _dependencies = strict (mapDependencies deps (name: dep: dep.uniqueName != uniqueName));

    # Filter out self-referential peer dependencies.
    _peerDependencies = strict (mapDependencies peerDependencies (name: dep:
      dep.uniqueName != uniqueName));

    # Filter out any optional dependencies which don't build correctly.
    _optionalDependencies = strict (mapDependencies optionalDependencies (name: dep:
      (builtins.tryEval dep).success &&
      !(elem dep.basicName skipOptionalDependencies)
    ));

    # Grab development dependencies if doCheck is true.
    _devDependencies = let
        filterFunc = name: dep: dep.uniqueName != uniqueName;
        depSet = if doCheck then devDependencies else [];
      in
      strict (mapDependencies depSet filterFunc);

    # Depencencies we need to propagate (all except devDependencies)
    propagatedDependencies = strict (
      _dependencies.requiredDeps //
      _optionalDependencies.requiredDeps //
      _peerDependencies.requiredDeps);

    # Required dependencies are those that we haven't filtered yet.
    requiredDependencies =
      strict (_devDependencies.requiredDeps // propagatedDependencies);

    # Recursive dependencies. These are turned into "shims" or fake packages,
    # which allows us to have dependency cycles, something npm allows.
    recursiveDependencies = strict (
      _devDependencies.recursiveDeps //
      _dependencies.recursiveDeps //
      _optionalDependencies.recursiveDeps //
      _peerDependencies.recursiveDeps);

    # Flags that we will pass to `npm install`.
    npmFlags = concatStringsSep " " ([
      # We point the registry at something that doesn't exist. This will
      # mean that NPM will fail if any of the dependencies aren't met, as it
      # will attempt to hit this registry for the missing dependency.
      "--registry=http://notaregistry.$UNIQNAME.derp"
      # These flags make failure fast, as otherwise NPM will spin for a while.
      "--fetch-retry-mintimeout=0" "--fetch-retry-maxtimeout=10" "--fetch-retries=0"
      # This will disable any user-level npm configuration.
      "--userconfig=/dev/null"
      # This flag is used for packages which link against the node headers.
      "--nodedir=${nodejsSources}"
      # This will tell npm not to run pre/post publish hooks
      # "--ignore-scripts"
      ] ++
      # Use the --production flag if we're not running tests; this will make
      # npm skip the dev dependencies.
      (if !doCheck then ["--production"] else []) ++
      # Add any extra headers that the user has passed in.
      extraNpmFlags);

    # A bit of bash to check that variables are set.
    checkSet = vars: concatStringsSep "\n" (flip map vars (var: ''
      [[ -z $${var} ]] && { echo "${var} is not set."; exit 1; }
    ''));

    patchPhase = ''
      runHook prePatch
      patchShebangs $PWD

      # Ensure that the package name matches what is in the package.json.
      node ${./checkPackageJson.js} ${packageJsonName}

      # Remove any impure dependencies from the package.json (see script
      # for details)
      node ${./removeImpureDependencies.js}

      # We do not handle shrinkwraps yet
      rm npm-shrinkwrap.json 2>/dev/null || true

      ${if patchDependencies == {} then "" else ''
        cat <<EOF | python
        import json
        with open("package.json") as f:
            package_json = json.load(f)
        ${flip concatMapStrings (attrNames patchDependencies) (name: let
            version = patchDependencies.${name};
          in flip concatMapStrings dependencyTypes (depType: ''
            if "${name}" in package_json.setdefault("${depType}", {}):
                ${if version == null then ''
                    print("removing ${name} from ${depType}")
                    package_json["${depType}"].pop("${name}", None)
                  '' else ''
                    print("Patching ${depType} ${name} to version ${version}")
                    package_json["dependencies"]["${name}"] = "${version}"
                  ''}
            ''))}
        with open("package.json", "w") as f:
            f.write(json.dumps(package_json))
        EOF
      ''}

      runHook postPatch
    '';

    configurePhase = ''
      runHook preConfigure
      # 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.
      ${
        let
          linkCmd = p: if p.recursiveDeps == [] then "ln -sv" else "cp -r";
          link = dep: ''
            if ! [[ -e ${pathInModulePath dep} ]]; then
              ${linkCmd dep} ${dep}/lib/${pathInModulePath dep} ${modulePath dep}
              if [[ -d ${dep}/bin ]]; then
                find -L ${dep}/bin -maxdepth 1 -type f -executable \
                  | while read exec_file; do
                    echo "Symlinking $exec_file binary to node_modules/.bin"
                    mkdir -p node_modules/.bin
                    ln -s $exec_file node_modules/.bin/$(basename $exec_file)
                done
              fi
            fi
          '';
        in
        flip concatMapStrings (attrValues requiredDependencies) (dep:
        # Create symlinks (or copies) of all of the required dependencies.
        ''
          mkdir -p ${modulePath dep}
          ${link dep}
          ${concatMapStrings link (attrValues dep.peerDependencies)}
        '')}

      # Remove recursive dependencies from package.json
      ${if recursiveDependencies == {} then "" else ''
        python <<EOF
        import json
        with open("package.json") as f:
            package_json = json.load(f)
        dep_names = [${concatStringsSep ", "
                       (map (d: "'${d.packageJsonName}'")
                         (attrValues recursiveDependencies))}]
        for name in dep_names:
            print("Removing recursive dependency on {} from package.json"
                  .format(name))
            for k in ("dependencies", "devDependencies", "peerDependencies",
                      "optionalDependencies"):
                package_json[k] = package_json.setdefault(k, {})
                package_json[k].pop(name, None)
        with open("package.json", "w") as f:
            f.write(json.dumps(package_json))
        EOF
        ''}

      runHook postConfigure
    '';

    buildPhase = ''
      runHook preBuild
      (
        # NPM reads the `HOME` environment variable and fails if it doesn't
        # exist, so set it here.
        export HOME=$PWD
        echo npm install $npmFlags

        npm install $npmFlags || {
          echo "NPM installation of ${name}@${version} failed!"
          echo "Rerunning with verbose logging:"
          npm install . $npmFlags --loglevel=verbose
          if [[ -d node_modules ]]; then
            echo "node_modules contains these files:"
            ls -l node_modules
          fi
          exit 1
        }
      )
      runHook postBuild
    '';

    installPhase = ''
      runHook preInstall

      # Install the package that we just built.
      mkdir -p $out/lib/${modulePath self}

      # Copy the folder that was created for this path to $out/lib.
      cp -r $PWD $out/lib/${pathInModulePath self}

      # Remove the node_modules subfolder from there, and instead put things
      # in $PWD/node_modules into that folder.
      if [ -e "$out/lib/${pathInModulePath self}/man" ]; then
        echo "Linking manpages..."
        mkdir -p $out/share
        for dir in $out/lib/${pathInModulePath self}/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: ''
        mkdir -p ${modulePath dep}
        mv ${pathInModulePath dep} $out/lib/${modulePath dep}
      '') (attrValues _peerDependencies.requiredDeps)}

      # Install binaries using the `bin` object in the package.json
      python ${./installBinaries.py}

      runHook postInstall
    '';

    # These are the arguments that we will pass to `stdenv.mkDerivation`.
    mkDerivationArgs = {
      inherit
        src
        patchPhase
        configurePhase
        buildPhase
        checkPhase
        installPhase
        doCheck;

      # Tell mkDerivation to run `setVariables` prior to other phases.
      prePhases = ["setVariables"];

      # Define some environment variables that we will use in the build.
      setVariables = ''
        export HASHEDNAME=$(echo "$propagatedNativeBuildInputs $name" \
                          | md5sum | awk '{print $1}')
        export UNIQNAME="''${HASHEDNAME:0:10}-${name}-${version}"
        export BUILD_DIR=$TMPDIR/$UNIQNAME-build
        export npmFlags="${npmFlags}"
      '';

      shellHook = ''
        runHook preShellHook
        runHook setVariables
        export PATH=${npm}/bin:${nodejs}/bin:$(pwd)/node_modules/.bin:$PATH
        mkdir -p $TMPDIR/$UNIQNAME
        (
          cd $TMPDIR/$UNIQNAME
          eval "$configurePhase"
        )
        echo "Installed dependencies in $TMPDIR/$UNIQNAME."
        export PATH=$TMPDIR/$UNIQNAME/node_modules/.bin:$PATH
        export NODE_PATH=$TMPDIR/$UNIQNAME/node_modules
        runHook postShellHook
      '';

      inherit dontStrip dontPatchELF;

      meta = {
        inherit platforms;
        maintainers = [ stdenv.lib.maintainers.offline ];
      };

      # Propagate pieces of information about the package so that downstream
      # packages can reflect on them.
      passthru = (passthru // {
        inherit uniqueName packageJsonName namespace version;
        # The basic name is the name without namespace or version.
        basicName = name;
        peerDependencies = _peerDependencies.requiredDeps;

        # Expose a list of recursive dependencies to upstream packages, so that
        # they can be shimmed out.
        recursiveDeps = let
          required = attrValues requiredDependencies;
          recursive = attrNames recursiveDependencies;
        in
          flatten (map (dep: remove name dep.recursiveDeps) required) ++
          recursive;

        # The `env` attribute is meant to be used with `nix-shell` (although
        # that's not required). It will build the package with its dev
        # dependencies. This means that the package must have dev dependencies
        # defined, or it will error.
        env = buildNodePackage (args // {installDevDependencies = true;});

        # An 'overrideNodePackage' attribute, which will call
        # `buildNodePackage` with the given arguments overridden.
        # We don't use the name `override` because this will get stomped on
        # if the derivation is the result of a `callPackage` application.
        overrideNodePackage = newArgs: buildNodePackage (args // newArgs);
      });
    } // (removeAttrs args attrsToRemove) // {
      name = "${namePrefix}${name}-${version}${nameSuffix}";

      # Pass the required dependencies and
      propagatedBuildInputs = propagatedBuildInputs ++
                              attrValues propagatedDependencies ++
                              [nodejs pkgs.tree];


      buildInputs = [npm] ++ buildInputs ++
                    attrValues (_devDependencies.requiredDeps) ++
                    neededNatives;

      # 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 stdenv.mkDerivation mkDerivationArgs;

in self