Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

import, refactor, improve & yarn support #2

Merged
merged 21 commits into from
Nov 20, 2018
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,26 @@
# nix-npm-buildpackage
Build nix packages that use npm/yarn packages.
## Description

nix-npm-buildpackage - build nix packages that use npm/yarn packages

You can use `buildNpmPackage`/`buildYarnPackage` to:
* use a `packages-lock.json`/`yarn.lock` file to:
- download the dependencies to the nix store
- build an offline npm/yarn cache that uses those
* build a nix package from the npm/yarn package

## Examples

```nix
{ pkgs ? import <nixpkgs> {} }:
let
bp = pkgs.callPackage .../nix-npm-buildpackage {};
in ...
```

```nix
bp.buildNpmPackage { src = ./.; }
```

```nix
bp.buildYarnPackage { src = ./.; }
```
179 changes: 179 additions & 0 deletions default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
{ stdenvNoCC, writeShellScriptBin, writeText, runCommand,
stdenv, fetchurl, makeWrapper, nodejs-10_x, yarn2nix, yarn }:
with stdenv.lib; let
inherit (builtins) fromJSON toJSON split removeAttrs;

_nodejs = nodejs-10_x;
_yarn = yarn.override { nodejs = _nodejs; };

depsToFetches = deps: concatMap depToFetch (attrValues deps);

depFetchOwn = { resolved, integrity, name ? null, ... }:
let
ssri = split "-" integrity;
hashType = head ssri;
hash = elemAt ssri 2;
bname = baseNameOf resolved;
fname = if hasSuffix ".tgz" bname || hasSuffix ".tar.gz" bname
then bname else bname + ".tgz";
in nameValuePair resolved {
inherit name bname;
path = fetchurl { name = fname; url = resolved; "${hashType}" = hash; };
};

depToFetch = args @ { resolved ? null, dependencies ? {}, ... }:
(optional (resolved != null) (depFetchOwn args)) ++ (depsToFetches dependencies);

cacheInput = oFile: iFile: writeText oFile (toJSON (listToAttrs (depToFetch iFile)));

patchShebangs = writeShellScriptBin "patchShebangs.sh" ''
set -e
source ${stdenvNoCC}/setup
patchShebangs "$@"
'';

shellWrap = writeShellScriptBin "npm-shell-wrap.sh" ''
set -e
if [ ! -e .shebangs_patched ]; then
${patchShebangs}/bin/patchShebangs.sh .
touch .shebangs_patched
fi
exec bash "$@"
'';

npmInfo = src: rec {
pkgJson = src + "/package.json";
info = fromJSON (readFile pkgJson);
name = "${info.name}-${info.version}";
};

npmCmd = "${_nodejs}/bin/npm";
npmAlias = ''npm() { ${npmCmd} "$@" $npmFlags; }'';
npmModules = "${_nodejs}/lib/node_modules/npm/node_modules";

yarnCmd = "${_yarn}/bin/yarn";
yarnAlias = ''yarn() { ${yarnCmd} $yarnFlags "$@"; }'';

npmFlagsYarn = [ "--offline" "--script-shell=${shellWrap}/bin/npm-shell-wrap.sh" ];
npmFlagsNpm = [ "--cache=./npm-cache" "--nodedir=${_nodejs}" ] + npmFlagsYarn;

commonEnv = {
XDG_CONFIG_DIRS = ".";
NO_UPDATE_NOTIFIER = true;
installJavascript = true;
};

commonBuildInputs = [ _nodejs makeWrapper ]; # TODO: git?

# unpack the .tgz into output directory and add npm wrapper
# TODO: "cd $out" vs NIX_NPM_BUILDPACKAGE_OUT=$out?
untarAndWrap = name: cmds: ''
mkdir -p $out/bin
tar xzvf ./${name}.tgz -C $out --strip-components=1
if [ "$installJavascript" = "1" ]; then
cp -R node_modules $out/
${ concatStringsSep ";" (map (cmd:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is every anything but npm start --scripts-prepend-node-path=true .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ever*. I symlinked npm so it could run the npm start script, which is somewhat trivial to do (doesn't differ between npm and yarn, afaik)

''makeWrapper ${cmd} $out/bin/${baseNameOf cmd} --run "cd $out"''
) cmds) }
fi
'';
in {
buildNpmPackage = args @ {
src, npmBuild ? "npm ci", npmBuildMore ? "",
buildInputs ? [], npmFlags ? [], ...
}:
let
info = npmInfo src;
lock = fromJSON (readFile (src + "/package-lock.json"));
in stdenv.mkDerivation ({
inherit (info) name;
inherit (info.info) version;

preBuildPhases = [ "npmCachePhase" ];
preInstallPhases = [ "npmPackPhase" ];

npmCachePhase = ''
addToSearchPath NODE_PATH ${npmModules} # pacote
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this add to the final npm build search path as well?

node ${./mknpmcache.js} ${cacheInput "npm-cache-input.json" lock}
'';

buildPhase = ''
${npmAlias}
runHook preBuild
${npmBuild}
${npmBuildMore}
runHook postBuild
'';

# make a package .tgz (no way around it)
npmPackPhase = ''
${npmAlias}
npm prune --production
npm pack --ignore-scripts
'';

installPhase = untarAndWrap info.name [npmCmd];
} // commonEnv // args // {
buildInputs = commonBuildInputs ++ buildInputs;
npmFlags = npmFlagsNpm ++ npmFlags;
});

buildYarnPackage = args @ {
src, yarnBuild ? "yarn", yarnBuildMore ? "", integreties ? {},
buildInputs ? [], yarnFlags ? [], npmFlags ? [], ...
}:
let
info = npmInfo src;
deps = { dependencies = fromJSON (readFile yarnJson); };
yarnIntFile = writeText "integreties.json" (toJSON integreties);
yarnLock = src + "/yarn.lock";
yarnJson = runCommand "yarn.json" {} ''
set -e
addToSearchPath NODE_PATH ${npmModules} # ssri
addToSearchPath NODE_PATH ${yarn2nix.node_modules} # @yarnpkg/lockfile
yorickvP marked this conversation as resolved.
Show resolved Hide resolved
${_nodejs}/bin/node ${./mkyarnjson.js} ${yarnLock} ${yarnIntFile} > $out
'';
in stdenv.mkDerivation ({
inherit (info) name;
inherit (info.info) version;

preBuildPhases = [ "yarnConfigPhase" "yarnCachePhase" ];
preInstallPhases = [ "yarnPackPhase" ];

# TODO
obfusk marked this conversation as resolved.
Show resolved Hide resolved
yarnConfigPhase = ''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put this in preConfigurePhases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one.

cat <<-END >> .yarnrc

yarn-offline-mirror "$PWD/yarn-cache"
script-shell "${shellWrap}/bin/npm-shell-wrap.sh"
nodedir "${_nodejs}"
END
'';

yarnCachePhase = ''
mkdir -p yarn-cache
node ${./mkyarncache.js} ${cacheInput "yarn-cache-input.json" deps}
'';

buildPhase = ''
${npmAlias}
${yarnAlias}
runHook preBuild
${yarnBuild}
${yarnBuildMore}
runHook postBuild
'';

# TODO: install --production?
yorickvP marked this conversation as resolved.
Show resolved Hide resolved
yarnPackPhase = ''
${yarnAlias}
yarn pack --ignore-scripts --filename ${info.name}.tgz
'';

installPhase = untarAndWrap info.name [npmCmd yarnCmd];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there's anything here that you can't do with npm.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. I'd say it depends on the project. And how well npm works for yarn packages. For the vault ui you'd need neither npm nor yarn after building I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script running is fairly trivial and the code might even be shared between npm and yarn.

} // commonEnv // removeAttrs args [ "integreties" ] // {
buildInputs = [ _yarn ] ++ commonBuildInputs ++ buildInputs;
yarnFlags = [ "--offline" "--frozen-lockfile" "--non-interactive" ] ++ yarnFlags;
npmFlags = npmFlagsYarn ++ npmFlags;
});
}
49 changes: 49 additions & 0 deletions mknpmcache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const assert = require("assert")
const fs = require("fs")
const pacote = require("pacote")

const USAGE = "node mknpmcache.js npm-cache-input.json"

if (process.argv.length != USAGE.split(/\s+/).length) {
console.error("Usage:", USAGE)
process.exit(1)
}

const [nixPkgsFile] = process.argv.slice(2)

const pkgLockFile = "./package-lock.json"
const lock = JSON.parse(fs.readFileSync(pkgLockFile, "utf8"))
const nixPkgs = JSON.parse(fs.readFileSync(nixPkgsFile, "utf8"))

function traverseDeps(pkg, fn) {
Object.values(pkg.dependencies).forEach(dep => {
if (dep.resolved && dep.integrity) fn(dep)
if (dep.dependencies) traverseDeps(dep, fn)
})
}

async function main(lockfile, nix, cache) {
const promises = Object.keys(nix).map(async function (url) {
const tar = nix[url].path
const manifest = await pacote.manifest(tar, { offline: true, cache })
return [url, manifest._integrity]
})
const hashes = new Map(await Promise.all(promises))
traverseDeps(lockfile, dep => {
if (dep.integrity.startsWith("sha1-")) {
assert(hashes.has(dep.resolved))
dep.integrity = hashes.get(dep.resolved)
} else {
assert(dep.integrity == hashes.get(dep.resolved))
}
})
// rewrite lock file to use sha512 hashes from pacote
fs.writeFileSync(pkgLockFile, JSON.stringify(lock, null, 2))
}

process.on("unhandledRejection", error => {
console.log("unhandledRejection", error.message)
process.exit(1)
})

main(lock, nixPkgs, "./npm-cache/_cacache")
23 changes: 23 additions & 0 deletions mkyarncache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const fs = require("fs")
const path = require("path")

const USAGE = "node mkyarncache.js yarn-cache-input.json"

if (process.argv.length != USAGE.split(/\s+/).length) {
console.error("Usage:", USAGE)
process.exit(1)
}

const [nixPkgsFile] = process.argv.slice(2)

const yarnCacheDir = "./yarn-cache"
const nixPkgs = JSON.parse(fs.readFileSync(nixPkgsFile, "utf8"))

function name(dep) {
return dep.name[0] == "@" ? dep.name.split("/")[0] + "-" + dep.bname : dep.bname
}

Object.keys(nixPkgs).forEach(url => {
const dep = nixPkgs[url];
fs.symlinkSync(dep.path, path.join(yarnCacheDir, name(dep)))
})
41 changes: 41 additions & 0 deletions mkyarnjson.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const assert = require("assert")
const fs = require("fs")
const lockfile = require("@yarnpkg/lockfile")
const ssri = require("ssri")

const USAGE = "node mkyarnjson.js yarn.lock integrities.json"

if (process.argv.length != USAGE.split(/\s+/).length) {
console.error("Usage:", USAGE)
process.exit(1)
}

const [yarnLockFile, intFile] = process.argv.slice(2)

const yarnJson = lockfile.parse(fs.readFileSync(yarnLockFile, "utf8")).object
const integrities = JSON.parse(fs.readFileSync(intFile))

function splitNameVsn(key) {
// foo@vsn or @foo/bar@vsn
if (key[0] == "@") {
const [name, vsn] = key.slice(1).split("@")
return ["@"+name, vsn]
} else {
return key.split("@")
}
}

const deps = {}

Object.keys(yarnJson).forEach(key => {
if (key in deps) return
const dep = yarnJson[key]
const [name, vsn] = splitNameVsn(key)
const [url, sha1] = dep.resolved.split("#", 2)
const integrity = dep.integrity || integrities[url] ||
(sha1 && ssri.fromHex(sha1, "sha1").toString())
assert(integrity, "missing integrity for " + JSON.stringify(dep))
deps[key] = { name, resolved: url, integrity }
})

console.log(JSON.stringify(deps, null, 2))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the best way to generate non-log output.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? It's less verbose than process.stdout.write(... + "\n") and does the same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semantics. console.log is for logs ;)