Skip to content

Commit

Permalink
remove mkYarnPackage
Browse files Browse the repository at this point in the history
The Nix file for mkYarnPackage must be in the nixpkgs repository,
because Hydra does not allow importing Nix files from downloaded paths.
To prevent the nixpkgs version of the Nix code from getting out-of-sync
with this repository, let's remove it here and use nixpkgs as the single
source of truth for the Nix code.
Incompatibilities in the interface can be dealt with by adding an
attribute for the yarn.nix file format version in the future.
  • Loading branch information
Milan Pässler committed May 18, 2020
1 parent 70666fc commit 1c903af
Show file tree
Hide file tree
Showing 11 changed files with 41 additions and 395 deletions.
369 changes: 5 additions & 364 deletions default.nix
Original file line number Diff line number Diff line change
@@ -1,377 +1,18 @@
{ pkgs ? import <nixpkgs> {}
, nodejs ? pkgs.nodejs
, yarn ? pkgs.yarn
, src ? ./.
}:

let
inherit (pkgs) stdenv lib fetchurl linkFarm callPackage git rsync makeWrapper;
inherit (pkgs) stdenv lib callPackage git rsync makeWrapper yarn2nix-moretea;
inherit (yarn2nix-moretea) mkYarnPackage defaultYarnFlags;

compose = f: g: x: f (g x);
id = x: x;
composeAll = builtins.foldl' compose id;
in rec {
# Export yarn again to make it easier to find out which yarn was used.
inherit yarn;

# Re-export pkgs
inherit pkgs;

unlessNull = item: alt:
if item == null then alt else item;

reformatPackageName = pname:
let
# regex adapted from `validate-npm-package-name`
# will produce 3 parts e.g.
# "@someorg/somepackage" -> [ "@someorg/" "someorg" "somepackage" ]
# "somepackage" -> [ null null "somepackage" ]
parts = builtins.tail (builtins.match "^(@([^/]+)/)?([^/]+)$" pname);
# if there is no organisation we need to filter out null values.
non-null = builtins.filter (x: x != null) parts;
in builtins.concatStringsSep "-" non-null;

# https://docs.npmjs.com/files/package.json#license
# TODO: support expression syntax (OR, AND, etc)
spdxLicense = licstr:
if licstr == "UNLICENSED" then
lib.licenses.unfree
else
lib.findFirst
(l: l ? spdxId && l.spdxId == licstr)
{ shortName = licstr; }
(builtins.attrValues lib.licenses);

# Generates the yarn.nix from the yarn.lock file
mkYarnNix = { yarnLock, flags ? [] }:
pkgs.runCommand "yarn.nix" {}
"${yarn2nix}/bin/yarn2nix --lockfile ${yarnLock} --no-patch --builtin-fetchgit ${lib.escapeShellArgs flags} > $out";

# Loads the generated offline cache. This will be used by yarn as
# the package source.
importOfflineCache = yarnNix:
let
pkg = callPackage yarnNix { };
in
pkg.offline_cache;

defaultYarnFlags = [
"--offline"
"--frozen-lockfile"
"--ignore-engines"
"--ignore-scripts"
];

mkYarnModules = {
name, # safe name and version, e.g. testcompany-one-modules-1.0.0
pname, # original name, e.g @testcompany/one
version,
packageJSON,
yarnLock,
yarnNix ? mkYarnNix { inherit yarnLock; },
yarnFlags ? defaultYarnFlags,
pkgConfig ? {},
preBuild ? "",
postBuild ? "",
workspaceDependencies ? [], # List of yarn packages
}:
let
offlineCache = importOfflineCache yarnNix;

extraBuildInputs = (lib.flatten (builtins.map (key:
pkgConfig.${key}.buildInputs or []
) (builtins.attrNames pkgConfig)));

postInstall = (builtins.map (key:
if (pkgConfig.${key} ? postInstall) then
''
for f in $(find -L -path '*/node_modules/${key}' -type d); do
(cd "$f" && (${pkgConfig.${key}.postInstall}))
done
''
else
""
) (builtins.attrNames pkgConfig));

workspaceJSON = pkgs.writeText
"${name}-workspace-package.json"
(builtins.toJSON { private = true; workspaces = ["deps/**"]; }); # scoped packages need second splat

workspaceDependencyLinks = lib.concatMapStringsSep "\n"
(dep: ''
mkdir -p "deps/${dep.pname}"
ln -sf ${dep.packageJSON} "deps/${dep.pname}/package.json"
'')
workspaceDependencies;

in stdenv.mkDerivation {
inherit preBuild postBuild name;
phases = ["configurePhase" "buildPhase"];
buildInputs = [ yarn nodejs git ] ++ extraBuildInputs;

configurePhase = ''
# Yarn writes cache directories etc to $HOME.
export HOME=$PWD/yarn_home
'';

buildPhase = ''
runHook preBuild
mkdir -p "deps/${pname}"
cp ${packageJSON} "deps/${pname}/package.json"
cp ${workspaceJSON} ./package.json
cp ${yarnLock} ./yarn.lock
chmod +w ./yarn.lock
yarn config --offline set yarn-offline-mirror ${offlineCache}
# Do not look up in the registry, but in the offline cache.
${fixup_yarn_lock}/bin/fixup_yarn_lock yarn.lock
${workspaceDependencyLinks}
# Set the nodedir so we can build native packages.
yarn config --offline set nodedir ${nodejs}
yarn install ${lib.escapeShellArgs yarnFlags}
${lib.concatStringsSep "\n" postInstall}
mkdir $out
mv node_modules $out/
mv deps $out/
patchShebangs $out
runHook postBuild
'';
};

# This can be used as a shellHook in mkYarnPackage. It brings the built node_modules into
# the shell-hook environment.
linkNodeModulesHook = ''
if [[ -d node_modules || -L node_modules ]]; then
echo "./node_modules is present. Replacing."
rm -rf node_modules
fi
ln -s "$node_modules" node_modules
'';

mkYarnWorkspace = {
src,
packageJSON ? src + "/package.json",
yarnLock ? src + "/yarn.lock",
packageOverrides ? {},
...
}@attrs:
let
package = lib.importJSON packageJSON;

packageGlobs = package.workspaces;

globElemToRegex = lib.replaceStrings ["*"] [".*"];

# PathGlob -> [PathGlobElem]
splitGlob = lib.splitString "/";

# Path -> [PathGlobElem] -> [Path]
# Note: Only directories are included, everything else is filtered out
expandGlobList = base: globElems:
let
elemRegex = globElemToRegex (lib.head globElems);
rest = lib.tail globElems;
children = lib.attrNames (lib.filterAttrs (name: type: type == "directory") (builtins.readDir base));
matchingChildren = lib.filter (child: builtins.match elemRegex child != null) children;
in if globElems == []
then [ base ]
else lib.concatMap (child: expandGlobList (base+("/"+child)) rest) matchingChildren;

# Path -> PathGlob -> [Path]
expandGlob = base: glob: expandGlobList base (splitGlob glob);

packagePaths = lib.concatMap (expandGlob src) packageGlobs;

packages = lib.listToAttrs (map (src:
let
packageJSON = src + "/package.json";

package = lib.importJSON packageJSON;

allDependencies = lib.foldl (a: b: a // b) {} (map (field: lib.attrByPath [field] {} package) ["dependencies" "devDependencies"]);

# { [name: String] : { pname : String, packageJSON : String, ... } } -> { [pname: String] : version } -> [{ pname : String, packageJSON : String, ... }]
getWorkspaceDependencies = packages: allDependencies:
let
packageList = lib.attrValues packages;
in
composeAll [
(lib.filter (x: x != null))
(lib.mapAttrsToList (pname: _version: lib.findFirst (package: package.pname == pname) null packageList))
] allDependencies;

workspaceDependencies = getWorkspaceDependencies packages allDependencies;

name = reformatPackageName package.name;
in {
inherit name;
value = mkYarnPackage (
builtins.removeAttrs attrs ["packageOverrides"]
// { inherit src packageJSON yarnLock workspaceDependencies; }
// lib.attrByPath [name] {} packageOverrides
);
})
packagePaths
);
in packages;

mkYarnPackage = {
name ? null,
src,
packageJSON ? src + "/package.json",
yarnLock ? src + "/yarn.lock",
yarnNix ? mkYarnNix { inherit yarnLock; },
yarnFlags ? defaultYarnFlags,
yarnPreBuild ? "",
pkgConfig ? {},
extraBuildInputs ? [],
publishBinsFor ? null,
workspaceDependencies ? [], # List of yarnPackages
...
}@attrs:
let
package = lib.importJSON packageJSON;
pname = package.name;
safeName = reformatPackageName pname;
version = package.version or attrs.version;
baseName = unlessNull name "${safeName}-${version}";

workspaceDependenciesTransitive = lib.unique (
(lib.flatten (builtins.map (dep: dep.workspaceDependencies) workspaceDependencies))
++ workspaceDependencies
);

deps = mkYarnModules {
name = "${safeName}-modules-${version}";
preBuild = yarnPreBuild;
workspaceDependencies = workspaceDependenciesTransitive;
inherit packageJSON pname version yarnLock yarnNix yarnFlags pkgConfig;
};

publishBinsFor_ = unlessNull publishBinsFor [pname];

linkDirFunction = ''
linkDirToDirLinks() {
target=$1
if [ ! -f "$target" ]; then
mkdir -p "$target"
elif [ -L "$target" ]; then
local new=$(mktemp -d)
trueSource=$(realpath "$target")
if [ "$(ls $trueSource | wc -l)" -gt 0 ]; then
ln -s $trueSource/* $new/
fi
rm -r "$target"
mv "$new" "$target"
fi
}
'';

workspaceDependencyCopy = lib.concatMapStringsSep "\n"
(dep: ''
# ensure any existing scope directory is not a symlink
linkDirToDirLinks "$(dirname node_modules/${dep.pname})"
mkdir -p "deps/${dep.pname}"
tar -xf "${dep}/tarballs/${dep.name}.tgz" --directory "deps/${dep.pname}" --strip-components=1
if [ ! -e "deps/${dep.pname}/node_modules" ]; then
ln -s "${deps}/deps/${dep.pname}/node_modules" "deps/${dep.pname}/node_modules"
fi
'')
workspaceDependenciesTransitive;

in stdenv.mkDerivation (builtins.removeAttrs attrs ["pkgConfig" "workspaceDependencies"] // {
inherit src pname;

name = baseName;

buildInputs = [ yarn nodejs rsync ] ++ extraBuildInputs;

node_modules = deps + "/node_modules";

configurePhase = attrs.configurePhase or ''
runHook preConfigure
for localDir in npm-packages-offline-cache node_modules; do
if [[ -d $localDir || -L $localDir ]]; then
echo "$localDir dir present. Removing."
rm -rf $localDir
fi
done
# move convent of . to ./deps/${pname}
mv $PWD $NIX_BUILD_TOP/temp
mkdir -p "$PWD/deps/${pname}"
rm -fd "$PWD/deps/${pname}"
mv $NIX_BUILD_TOP/temp "$PWD/deps/${pname}"
cd $PWD
if [ -d ${deps}/deps/${pname}/node_modules ]; then
cp -r ${deps}/deps/${pname}/node_modules "deps/${pname}/node_modules"
chmod -R +w "deps/${pname}/node_modules"
fi
cp -r $node_modules node_modules
chmod -R +w node_modules
${linkDirFunction}
linkDirToDirLinks "$(dirname node_modules/${pname})"
${workspaceDependencyCopy}
# Help yarn commands run in other phases find the package
echo "--cwd deps/${pname}" > .yarnrc
runHook postConfigure
'';

# Replace this phase on frontend packages where only the generated
# files are an interesting output.
installPhase = attrs.installPhase or ''
runHook preInstall
mkdir -p $out/{bin,libexec/${pname}}
mv node_modules $out/libexec/${pname}/node_modules
mv deps $out/libexec/${pname}/deps
node ${./internal/fixup_bin.js} $out/bin $out/libexec/${pname}/node_modules ${lib.concatStringsSep " " publishBinsFor_}
runHook postInstall
'';

doDist = true;

distPhase = attrs.distPhase or ''
# pack command ignores cwd option
rm -f .yarnrc
cd $out/libexec/${pname}/deps/${pname}
mkdir -p $out/tarballs/
yarn pack --offline --ignore-scripts --filename $out/tarballs/${baseName}.tgz
'';

passthru = {
inherit pname package packageJSON deps;
workspaceDependencies = workspaceDependenciesTransitive;
} // (attrs.passthru or {});

meta = {
inherit (nodejs.meta) platforms;
description = packageJSON.description or "";
homepage = packageJSON.homepage or "";
version = packageJSON.version or "";
license = if packageJSON ? license then spdxLicense packageJSON.license else "";
} // (attrs.meta or {});
});

yarn2nix = mkYarnPackage {
src =
let
src = ./.;
inherit src;

mkFilter = { dirsToInclude, filesToInclude, root }: path: type:
let
Expand Down Expand Up @@ -401,7 +42,7 @@ in rec {
yarnFlags = defaultYarnFlags ++ ["--production=true"];

buildPhase = ''
source ${./nix/expectShFunctions.sh}
source ${./expectShFunctions.sh}
expectFilePresent ./node_modules/.yarn-integrity
Expand Down
4 changes: 2 additions & 2 deletions tests/duplicate-pkgs/default.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{ yarn2nix }:
{ mkYarnPackage }:

yarn2nix.mkYarnPackage {
mkYarnPackage {
src = ./.;
buildPhase = ''
source ${../../nix/expectShFunctions.sh}
Expand Down
Loading

0 comments on commit 1c903af

Please sign in to comment.