diff options
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rw-r--r-- | LICENSE | 13 | ||||
-rw-r--r-- | README.md | 85 | ||||
-rwxr-xr-x | cmd/main.js | 173 | ||||
-rw-r--r-- | lib/template.js | 89 | ||||
-rw-r--r-- | package.json | 24 |
6 files changed, 389 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b3bb1aa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +0.1.0 (2019-10-02) +================== + + * Initial version. + @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..731814e --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +Npm4nix +======= + +Npm4nix converts Node.js packages into a single Nix build expression +for the [Npm.nix](https://github.com/ip1981/npm.nix) project. Npm4nix is +inspired by [Cabal2nix](https://github.com/NixOS/cabal2nix). Unfortunately, +[npm2nix](https://github.com/NixOS/npm2nix) is already taken :) Thus, +`npm4nix`. It supports local directories and tarballs (e. g. `*.tgz`), +remote tarballs, remote [Git](https://git-scm.com/) respositories. + + +Requirements +============ + +Npm4nix is written in JavaScript with minimum number of depednecies +and should be executed by [Node.js](https://nodejs.org). +Npm4nix invokes [curl](https://curl.haxx.se/), Git, and +[nix-hash](https://nixos.org/nix/manual/#sec-nix-hash), so these tools +should be installed. + + +Example +======= + + +``` +$ node cmd/main.js https://github.com/substack/node-mkdirp.git > mkdirp.nix +executing git clone 'https://github.com/substack/node-mkdirp.git' '/tmp/npm4nix-Feo4WM' +Cloning into '/tmp/npm4nix-Feo4WM'... +executing git -C '/tmp/npm4nix-Feo4WM' rev-parse HEAD +executing nix-hash --base32 --type sha256 '/tmp/npm4nix-Feo4WM' + +$ cat mkdirp.nix +{ fetchgit, buildNpmPackage, minimist, mock-fs, tap }: + +buildNpmPackage { + pname = "mkdirp"; + version = "0.5.1"; + src = fetchgit { + url = "https://github.com/substack/node-mkdirp.git"; + rev = "f2003bbcffa80f8c9744579fabab1212fc84545a"; + sha256 = "0qc3l6571aknhlmzcyaah3plmf852cl160jihy3l4b05j25qv45a"; + }; + + meta = { + description = "Recursively mkdir, like `mkdir -p`"; + homepage = ""; + license = "MIT"; + }; + + npmInputs = [ + minimist mock-fs tap + ]; +} +``` + +``` +$ node cmd/main.js https://github.com/cowboy/javascript-sync-async-foreach/archive/v0.1.3.tar.gz > async-foreach.nix +executing curl -LsSf -o '/tmp/npm4nix-zwGeOX/v0.1.3.tar.gz' 'https://github.com/cowboy/javascript-sync-async-foreach/archive/v0.1.3.tar.gz' +executing nix-hash --flat --base32 --type sha256 '/tmp/npm4nix-zwGeOX/v0.1.3.tar.gz' +executing tar -xOf '/tmp/npm4nix-zwGeOX/v0.1.3.tar.gz' --wildcards '*/package.json' + +$ cat async-foreach.nix +{ fetchurl, buildNpmPackage }: + +buildNpmPackage { + pname = "async-foreach"; + version = "0.1.3"; + src = fetchurl { + url = "https://github.com/cowboy/javascript-sync-async-foreach/archive/v0.1.3.tar.gz"; + sha256 = "1b7h2fgj6rndkviyx1hl0mh72d60a2b2f1sl86ndk8vdvr6mxmj3"; + }; + + meta = { + description = "An optionally-asynchronous forEach with an interesting interface."; + homepage = "http://github.com/cowboy/javascript-sync-async-foreach"; + license = ""; + }; + + npmInputs = [ + + ]; +} + +``` diff --git a/cmd/main.js b/cmd/main.js new file mode 100755 index 0000000..b71bbed --- /dev/null +++ b/cmd/main.js @@ -0,0 +1,173 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const process = require('process'); +const { + execSync +} = require('child_process'); + +const render = require('../lib/template'); + +var url = null, + revision = null; + +function log(msg) { + console.error(msg); +} + +function printHelp() { + console.log(` +npm4nix converts NPM packages into build instructions for Nix. + +Usage: npm4nix [options] URL + +Options: + -r, --revision revision to fetching from Git (tag, branch, hash) + -h, --help show this help screen + +URL can be: + - a local directory with package.json + - a local archive (tarball) + - a remote Git URL + - a remote archive (tarball) + +Examples: + + $ npm4nix -r 0.5.1 https://github.com/substack/node-mkdirp.git > mkdirp.nix + $ npm4nix . > mypkg.nix + `); +} + +function exec(cmd) { + log(`executing ${cmd}`); + return execSync(cmd, { + encoding: 'utf-8' + }).trim(); +} + +function getSHA256(p) { + var flat = ''; + if (fs.statSync(p).isFile()) { + flat = ' --flat'; + } + + return exec(`nix-hash${flat} --base32 --type sha256 '${p}'`); +} + +function mkdtemp() { + const tmpdir = os.tmpdir(); + return fs.mkdtempSync(`${tmpdir}${path.sep}npm4nix-`); +} + +function rmTree(dir) { + fs.readdirSync(dir).forEach(function(entry, index) { + const p = path.join(dir, entry); + if (fs.lstatSync(p).isDirectory()) { + rmTree(p); + } else { + fs.unlinkSync(p); + } + }); + fs.rmdirSync(dir); +}; + +function readPackage(p) { + var pkgFile = path.join(p, 'package.json'); + return JSON.parse(fs.readFileSync(pkgFile)); +} + +function isLocal(p) { + return fs.existsSync(p); +} + +function processLocal(p) { + console.log(render(p, readPackage(p))); +} + +function isArchive(url) { + return url.match(new RegExp('(https?://)?.+/[^/]+\.t(ar\.)?(gz|bz2|xz)', 'i')); +} + +function processArchive(url) { + const dir = mkdtemp(); + const file = path.join(dir, path.basename(url)); + var sha256, pkg; + try { + exec(`curl -LsSf -o '${file}' '${url}'`); + sha256 = getSHA256(file); + pkg = JSON.parse(exec(`tar -xOf '${file}' --wildcards '*/package.json'`)); + } finally { + rmTree(dir); + } + console.log(render({ + fetch: 'fetchurl', + url: url, + sha256: sha256 + }, pkg)); +} + +function isGit(url) { + return url.match(new RegExp('(.*https://)?(github|gitlab)\.com/[^/]+/[^/]+(\.git)?$', 'i')) || + url.match(new RegExp('(.*https://)?bitbucket\.org/[^/]+/[^/]+(\.git)?$', 'i')) || + url.match(new RegExp('^git(\\+https)?://.+', 'i')); +} + +function processGit(url) { + const dir = mkdtemp(); + var rev, sha256, pkg; + try { + exec(`git clone '${url}' '${dir}'`); + if (revision !== null) { + exec(`git -C '${dir}' checkout '${revision}'`); + } + rev = exec(`git -C '${dir}' rev-parse HEAD`); + rmTree(path.join(dir, '.git')); + sha256 = getSHA256(dir); + pkg = readPackage(dir); + } finally { + rmTree(dir); + } + console.log(render({ + fetch: 'fetchgit', + url: url, + rev: rev, + sha256: sha256 + }, pkg)); +} + + +const args = process.argv.slice(2); + +for (var i = 0; i < args.length; i++) { + switch (args[i]) { + case '-r': + case '--revision': + revision = args[++i]; + break; + case '-h': + case '--help': + printHelp(); + process.exit(); + break; + default: + url = args[i]; + } +} + +if (url === null) { + log('Missing URL. Add --help for usage info'); + process.exit(1); +} + +if (isLocal(url)) { + processLocal(url); +} else if (isGit(url)) { + processGit(url); +} else if (isArchive(url)) { + processArchive(url); +} else { + log(`unsupported URL: '${url}'`); + process.exit(1); +} diff --git a/lib/template.js b/lib/template.js new file mode 100644 index 0000000..ce05473 --- /dev/null +++ b/lib/template.js @@ -0,0 +1,89 @@ +function normalize(name) { + return name.replace('/', '-').replace('@', '').replace('.', '-'); +} + +function fmt(words, maxLine, glue1, glue2) { + var line = []; + var lines = [] + var lineLength = 0; + + for (var i = 0; i < words.length; i++) { + const itemLength = glue1.length + words[i].length + if (itemLength + lineLength <= maxLine || line.length === 0) { + line.push(words[i]); + lineLength += itemLength; + } else { + lines.push(line); + line = [words[i]]; + lineLength = itemLength; + } + } + + lines.push(line); + + lines = lines.map((l) => { + return l.join(glue1); + }); + + return lines.join(glue2); +} + + +function render(src, pkg) { + const deps = pkg.dependencies || {}; + const devDeps = pkg.devDependencies || {}; + const npmInputs = Object.keys({...deps, + ...devDeps + }).sort().map(normalize); + const args = ['buildNpmPackage', ...npmInputs]; + + var source = ''; + + if ('string' === typeof src) { + if (!src.startsWith('/') && !src.startsWith('./') && !src.startsWith('../')) { + src = `./${src}`; + } + source = ` src = ${src};`; + } else { + args.unshift(src.fetch); + switch (src.fetch) { + case 'fetchurl': + source = `\ + src = ${src.fetch} { + url = "${src.url}"; + sha256 = "${src.sha256}"; + };`; + break; + case 'fetchgit': + source = `\ + src = ${src.fetch} { + url = "${src.url}"; + rev = "${src.rev}"; + sha256 = "${src.sha256}"; + };`; + break; + } + } + + return `\ +{ ${fmt(args, 90, ', ', '\n, ')} }: + +buildNpmPackage { + pname = "${pkg.name}"; + version = "${pkg.version}"; +${source} + + meta = { + description = "${pkg.description || ''}"; + homepage = "${pkg.homepage || ''}"; + license = "${pkg.license || ''}"; + }; + + npmInputs = [ + ${fmt(npmInputs, 80, ' ', '\n ')} + ]; +} + `; +} + +module.exports = render; diff --git a/package.json b/package.json new file mode 100644 index 0000000..43b9be3 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "npm4nix", + "version": "0.1.0", + "description": "", + "scripts": { + }, + "bin": { + "npm4nix": "cmd/main.js" + }, + "files": [ + "cmd/*.js", + "lib/*.js" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/ip1981/npm4nix.git" + }, + "author": "Igor Pashev", + "license": "WTFPL", + "bugs": { + "url": "https://github.com/ip1981/npm4nix/issues" + }, + "homepage": "https://github.com/ip1981/npm4nix#readme" +} |