-
Notifications
You must be signed in to change notification settings - Fork 39
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
Changes from 17 commits
42522f3
2205e62
8b8c0f9
f26dce3
938136d
a3891e1
e43fb1d
7b322b1
4bdaa75
103c648
801f985
c57d014
abb6155
b727599
73265a7
cdd4e36
1f607e5
faaf1af
2c10f7c
66d51b3
32891fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = ./.; } | ||
``` |
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: | ||
''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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = '' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe put this in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
}); | ||
} |
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") |
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))) | ||
}) |
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not the best way to generate non-log output. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? It's less verbose than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Semantics. console.log is for logs ;) |
There was a problem hiding this comment.
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
.There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 thenpm start
script, which is somewhat trivial to do (doesn't differ between npm and yarn, afaik)