Merge pull request #1 from RecuencoJones/bugfix/add-windows-ext-replace-opt

ADD win_ext to URL template variables
This commit is contained in:
David Recuenco
2019-03-25 20:51:51 +01:00
committed by GitHub
20 changed files with 793 additions and 394 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea
node_modules
coverage
bin

4
.travis.yml Normal file
View File

@@ -0,0 +1,4 @@
language: node_js
node_js:
- "8"
- "10"

View File

@@ -1,16 +1,16 @@
## Go NPM
### Distribute cross-platform Go binaries via NPM
Applications written in Golang are portable - you can easily cross-compile binaries that work on Windows, Mac, and Linux. But how do you distribute the binaries to customers? When you publish new releases, how do they update the binary?
Applications written in Golang are portable - you can easily cross-compile binaries that work on Windows, Mac, and Linux. But how do you distribute the binaries to customers? When you publish new releases, how do they update the binary?
**Use NPM to distribute cross-platform Go binaries**
## Kidding me! Why NPM?
* **Cross-platform**: NPM is the only popular package manager that works cross-platform.
* **Lower barier to entry**: Most developers have NPM installed already.
* **Cross-platform**: NPM is the only popular package manager that works cross-platform.
* **Lower barier to entry**: Most developers have NPM installed already.
* **Pain free publishing**: It just takes one command to publish - `npm publish`
* **Dead simple install & update story**: `npm install/update -g your-awesome-app`
* **Adds $PATH**: NPM will automatically add your binary location to $PATH and generate .cmd file for Windows. Your app just works after installation!
* **Adds $PATH**: NPM will automatically add your binary location to $PATH and generate .cmd file for Windows. Your app just works after installation!
## Okay, tell me how?
### 1. Publish your binaries
@@ -76,7 +76,7 @@ You need to tell `go-npm` where to download the binaries from, and where to inst
"url": "https://github.com/user/my-go-package/releases/download/v{{version}}/myGoPackage_{{version}}_{{platform}}_{{arch}}.tar.gz"
```
* *name*: Name of the command users will use to run your binary.
* *name*: Name of the command users will use to run your binary.
* *path*: Temporary path where binaries will be downloaded to
* *url*: HTTP Web server where binaries are hosted.
@@ -84,6 +84,7 @@ Following variables are available to customize the URL:
* `{{version}}`: Version number read from `package.json` file. When you publish your package to NPM, it will use this version number. Ex: 0.0.1
* `{{platform}}`: `$GOOS` value for the platform
* `{{arch}}`: `$GOARCH` value for the architecture
* `{{ win_ext }}`: optional `.exe` extension for windows assets.
If you use `goreleaser` to publish your modules, it will automatically set the right architecture & platform in your URL.
@@ -106,4 +107,4 @@ To Update:
---
With ❤️ to the community by [Sanath Kumar Ramesh](http://twitter.com/sanathkr_)
With ❤️ to the community by [Sanath Kumar Ramesh](http://twitter.com/sanathkr_)

View File

@@ -0,0 +1,91 @@
const { EventEmitter } = require('events');
const request = require('request');
const common = require('../../src/common');
const install = require('../../src/actions/install');
const move = require('../../src/assets/move');
const untar = require('../../src/assets/untar');
const verifyAndPlaceCallback = require('../../src/assets/binary');
jest.mock('fs');
jest.mock('mkdirp');
jest.mock('request');
jest.mock('../../src/common');
jest.mock('../../src/assets/move');
jest.mock('../../src/assets/untar');
jest.mock('../../src/assets/binary');
describe('install()', () => {
let callback, requestEvents;
beforeEach(() => {
callback = jest.fn();
requestEvents = new EventEmitter();
});
it('should call callback with error if package.json did not return value' , () => {
common.parsePackageJson.mockReturnValueOnce(undefined);
install(callback);
expect(callback).toHaveBeenCalledWith('Invalid inputs');
});
it('should call callback with error on request error', () => {
request.mockReturnValueOnce(requestEvents);
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
install(callback);
requestEvents.emit('error');
expect(callback).toHaveBeenCalledWith('Error downloading from URL: http://url');
});
it('should call callback with error on response with status code different than 200', () => {
request.mockReturnValueOnce(requestEvents);
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
install(callback);
requestEvents.emit('response', { statusCode: 404 });
expect(callback).toHaveBeenCalledWith('Error downloading binary. HTTP Status Code: 404');
});
it('should pick move strategy if url is an uncompressed binary', () => {
request.mockReturnValueOnce(requestEvents);
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
install(callback);
requestEvents.emit('response', { statusCode: 200 });
expect(move).toHaveBeenCalled();
});
it('should pick untar strategy if url ends with .tar.gz', () => {
request.mockReturnValueOnce(requestEvents);
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url.tar.gz' });
install(callback);
requestEvents.emit('response', { statusCode: 200 });
expect(untar).toHaveBeenCalled();
});
it('should call verifyAndPlaceCallback on success', () => {
request.mockReturnValueOnce(requestEvents);
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url', binName: 'command', binPath: './bin' });
move.mockImplementationOnce(({ onSuccess }) => onSuccess());
install(callback);
requestEvents.emit('response', { statusCode: 200 });
expect(verifyAndPlaceCallback).toHaveBeenCalledWith('command', './bin', callback);
});
});

View File

@@ -0,0 +1,59 @@
const fs = require('fs');
const common = require('../../src/common');
const uninstall = require('../../src/actions/uninstall');
jest.mock('fs');
jest.mock('../../src/common');
describe('uninstall()', () => {
let callback;
beforeEach(() => {
callback = jest.fn();
common.parsePackageJson.mockReturnValueOnce({ binName: 'command' });
});
it('should call callback with error if binary not found', () => {
const error = new Error();
common.getInstallationPath.mockImplementationOnce((cb) => cb(error));
uninstall(callback);
expect(callback).toHaveBeenCalledWith(error);
});
it('should call unlinkSync with binary and installation path', () => {
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
uninstall(callback);
expect(fs.unlinkSync).toHaveBeenCalledWith('bin/command');
});
it('should call callback on success', () => {
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
uninstall(callback);
expect(callback).toHaveBeenCalledWith(null);
});
it('should call callback regardless of errors on unlink', () => {
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
fs.unlinkSync.mockImplementationOnce(() => {
throw new Error();
});
uninstall(callback);
expect(callback).toHaveBeenCalledWith(null);
});
});

View File

@@ -0,0 +1,51 @@
const fs = require('fs');
const common = require('../../src/common');
const verifyAndPlaceBinary = require('../../src/assets/binary');
jest.mock('fs');
jest.mock('../../src/common');
describe('verifyAndPlaceBinary()', () => {
let callback;
beforeEach(() => {
callback = jest.fn();
});
it('should call callback with error if binary downloaded differs from config', () => {
fs.existsSync.mockReturnValueOnce(false);
verifyAndPlaceBinary('command', './bin', callback);
expect(callback).toHaveBeenCalledWith('Downloaded binary does not contain the binary specified in configuration - command');
});
it('should call callback with error if installation path cannot be found', () => {
const error = new Error();
fs.existsSync.mockReturnValueOnce(true);
common.getInstallationPath.mockImplementationOnce((cb) => cb(error));
verifyAndPlaceBinary('command', './bin', callback);
expect(callback).toHaveBeenCalledWith(error);
});
it('should call callback with null on success', () => {
fs.existsSync.mockReturnValueOnce(true);
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, '/usr/local/bin'));
verifyAndPlaceBinary('command', './bin', callback);
expect(callback).toHaveBeenCalledWith(null);
});
it('should move the binary to installation directory', () => {
fs.existsSync.mockReturnValueOnce(true);
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, '/usr/local/bin'));
verifyAndPlaceBinary('command', './bin', callback);
expect(fs.renameSync).toHaveBeenCalledWith('bin/command', '/usr/local/bin/command');
});
});

View File

@@ -0,0 +1,49 @@
const { EventEmitter } = require('events');
const fs = require('fs');
const move = require('../../src/assets/move');
jest.mock('fs');
describe('move()', () => {
let streamEvents, pipe, onSuccess, onError;
beforeEach(() => {
streamEvents = new EventEmitter();
pipe = jest.fn();
onSuccess = jest.fn();
onError = jest.fn();
createWriteStream = jest.fn();
fs.createWriteStream.mockReturnValueOnce(streamEvents);
});
it('should download resource to given binPath', () => {
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
expect(fs.createWriteStream).toHaveBeenCalledWith('bin/command');
});
it('should call onSuccess on stream closed', () => {
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
streamEvents.emit('close');
expect(onSuccess).toHaveBeenCalled();
});
it('should call onError with error on write stream error', () => {
const error = new Error();
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
streamEvents.emit('error', error);
expect(onError).toHaveBeenCalledWith(error);
});
});

View File

@@ -0,0 +1,65 @@
const { EventEmitter } = require('events');
const zlib = require('zlib');
const tar = require('tar');
const untar = require('../../src/assets/untar');
jest.mock('zlib');
jest.mock('tar', () => ({
Extract: jest.fn()
}));
describe('untar()', () => {
let ungzEvents, untarEvents, pipe, onSuccess, onError;
beforeEach(() => {
ungzEvents = new EventEmitter();
untarEvents = new EventEmitter();
pipe = jest.fn();
onSuccess = jest.fn();
onError = jest.fn();
pipe.mockReturnValueOnce({ pipe });
tar.Extract.mockReturnValueOnce(untarEvents);
zlib.createGunzip.mockReturnValueOnce(ungzEvents);
});
it('should download resource and untar to given binPath', () => {
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
expect(tar.Extract).toHaveBeenCalledWith({ path: './bin' });
});
it('should call onSuccess on untar end', () => {
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
untarEvents.emit('end');
expect(onSuccess).toHaveBeenCalled();
});
it('should call onError with error on ungz error', () => {
const error = new Error();
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
ungzEvents.emit('error', error);
expect(onError).toHaveBeenCalledWith(error);
});
it('should call onError with error on untar error', () => {
const error = new Error();
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
untarEvents.emit('error', error);
expect(onError).toHaveBeenCalledWith(error);
});
});

40
__test__/cli.spec.js Normal file
View File

@@ -0,0 +1,40 @@
const cli = require('../src/cli');
const install = require('../src/actions/install');
jest.mock('../src/actions/install');
describe('cli()', () => {
let exit;
beforeEach(() => {
exit = jest.fn();
});
it('should exit with error if not enough args are supplied', () => {
cli({ argv: [], exit });
expect(exit).toHaveBeenCalledWith(1);
});
it('should exit with error if command does not exist', () => {
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'command' ], exit });
expect(exit).toHaveBeenCalledWith(1);
});
it('should exit with error if command returns error', () => {
install.mockImplementationOnce((cb) => cb(new Error()));
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'install' ], exit });
expect(exit).toHaveBeenCalledWith(1);
});
it('should exit with success if command runs fine', () => {
install.mockImplementationOnce((cb) => cb(null));
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'install' ], exit });
expect(exit).toHaveBeenCalledWith(0);
});
});

123
__test__/common.spec.js Normal file
View File

@@ -0,0 +1,123 @@
const fs = require('fs');
const childProcess = require('child_process');
const common = require('../src/common');
jest.mock('fs');
jest.mock('child_process');
jest.mock('mkdirp');
describe('common', () => {
describe('getInstallationPath()', () => {
let callback, env;
beforeEach(() => {
callback = jest.fn();
env = { ...process.env };
});
afterEach(() => {
process.env = env;
});
it('should get binaries path from `npm bin`', () => {
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(null, '/usr/local/bin'));
common.getInstallationPath(callback);
expect(callback).toHaveBeenCalledWith(null, '/usr/local/bin');
});
it('should get binaries path from env', () => {
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(new Error()));
process.env.npm_config_prefix = '/usr/local';
common.getInstallationPath(callback);
expect(callback).toHaveBeenCalledWith(null, '/usr/local/bin');
});
it('should call callback with error if binaries path is not found', () => {
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(new Error()));
process.env.npm_config_prefix = undefined;
common.getInstallationPath(callback);
expect(callback).toHaveBeenCalledWith(new Error('Error finding binary installation directory'));
});
});
describe('parsePackageJson()', () => {
let _process;
beforeEach(() => {
_process = { ...global.process };
});
afterEach(() => {
global.process = _process;
});
describe('validation', () => {
it('should return if architecture is unsupported', () => {
process.arch = 'mips';
expect(common.parsePackageJson()).toBeUndefined();
});
it('should return if platform is unsupported', () => {
process.platform = 'amiga';
expect(common.parsePackageJson()).toBeUndefined();
});
it('should return if package.json does not exist', () => {
fs.existsSync.mockReturnValueOnce(false);
expect(common.parsePackageJson()).toBeUndefined();
});
});
describe('variable replacement', () => {
it('should append .exe extension on windows platform', () => {
fs.existsSync.mockReturnValueOnce(true);
fs.readFileSync.mockReturnValueOnce(JSON.stringify({
version: '1.0.0',
goBinary: {
name: 'command',
path: './bin',
url: 'https://github.com/foo/bar/releases/v{{version}}/assets/command{{win_ext}}'
}
}));
process.platform = 'win32';
expect(common.parsePackageJson()).toMatchObject({
binName: 'command.exe',
url: 'https://github.com/foo/bar/releases/v1.0.0/assets/command.exe'
});
});
it('should not append .exe extension on platform different than windows', () => {
fs.existsSync.mockReturnValueOnce(true);
fs.readFileSync.mockReturnValueOnce(JSON.stringify({
version: '1.0.0',
goBinary: {
name: 'command',
path: './bin',
url: 'https://github.com/foo/bar/releases/v{{version}}/assets/command{{win_ext}}'
}
}));
process.platform = 'darwin';
expect(common.parsePackageJson()).toMatchObject({
binName: 'command',
url: 'https://github.com/foo/bar/releases/v1.0.0/assets/command'
});
});
});
});
});

View File

@@ -1,127 +0,0 @@
const rewire = require('rewire');
const EventEmitter = require('events').EventEmitter;
describe('go-npm', function() {
let mod;
beforeEach(function() {
mod = rewire('../src/index.js');
});
describe('Resource handling strategies', function() {
describe('untarStrategy()', function() {
let untarStrategy, ungzEvents, untarEvents, pipe, callback, zlib, createGunzip, tar;
beforeEach(function() {
untarStrategy = mod.__get__('untarStrategy');
zlib = mod.__get__('zlib');
tar = mod.__get__('tar');
ungzEvents = new EventEmitter();
untarEvents = new EventEmitter();
createGunzip = jest.fn();
pipe = jest.fn();
callback = jest.fn();
pipe.mockReturnValueOnce({ pipe });
createGunzip.mockReturnValueOnce(ungzEvents);
jest.spyOn(tar, 'Extract').mockReturnValueOnce(untarEvents);
// jest.spyOn not working on read-only properties
Object.defineProperty(zlib, 'createGunzip', { value: createGunzip });
});
it('should download resource and untar to given binPath', function() {
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
expect(tar.Extract).toHaveBeenCalledWith({ path: './bin' });
});
it('should call verifyAndPlaceBinary on untar end', function() {
const verifyAndPlaceBinary = jest.fn();
mod.__set__('verifyAndPlaceBinary', verifyAndPlaceBinary);
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
untarEvents.emit('end');
expect(verifyAndPlaceBinary).toHaveBeenCalledWith('command', './bin', callback);
});
it('should call callback with error on ungz error', function() {
const error = new Error();
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
ungzEvents.emit('error', error);
expect(callback).toHaveBeenCalledWith(error);
});
it('should call callback with error on untar error', function() {
const error = new Error();
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
untarEvents.emit('error', error);
expect(callback).toHaveBeenCalledWith(error);
});
});
describe('moveStrategy()', function() {
let moveStrategy, streamEvents, pipe, callback, fs;
beforeEach(function() {
moveStrategy = mod.__get__('moveStrategy');
fs = mod.__get__('fs');
streamEvents = new EventEmitter();
pipe = jest.fn();
callback = jest.fn();
jest.spyOn(fs, 'createWriteStream').mockReturnValueOnce(streamEvents);
});
it('should download resource to given binPath', function() {
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
expect(fs.createWriteStream).toHaveBeenCalledWith('bin/command');
});
it('should call verifyAndPlaceBinary on stream closed', function() {
const verifyAndPlaceBinary = jest.fn();
mod.__set__('verifyAndPlaceBinary', verifyAndPlaceBinary);
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
streamEvents.emit('close');
expect(verifyAndPlaceBinary).toHaveBeenCalledWith('command', './bin', callback);
});
it('should call callback with error on write stream error', function() {
const error = new Error();
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
streamEvents.emit('error', error);
expect(callback).toHaveBeenCalledWith(error);
});
});
});
});

View File

@@ -26,7 +26,6 @@
"babel-cli": "^6.24.1",
"babel-core": "^6.25.0",
"babel-preset-es2015": "^6.24.1",
"jest": "^24.5.0",
"rewire": "^4.0.1"
"jest": "^24.5.0"
}
}

52
src/actions/install.js Normal file
View File

@@ -0,0 +1,52 @@
const mkdirp = require('mkdirp');
const request = require('request');
const { parsePackageJson } = require('../common');
const verifyAndPlaceBinary = require('../assets/binary');
/**
* Select a resource handling strategy based on given options.
*/
function getStrategy({ url }) {
if (url.endsWith('.tar.gz')) {
return require('../assets/untar');
} else {
return require('../assets/move');
}
}
/**
* Reads the configuration from application's package.json,
* validates properties, downloads the binary, untars, and stores at
* ./bin in the package's root. NPM already has support to install binary files
* specific locations when invoked with "npm install -g"
*
* See: https://docs.npmjs.com/files/package.json#bin
*/
function install(callback) {
const opts = parsePackageJson();
if (!opts) return callback('Invalid inputs');
mkdirp.sync(opts.binPath);
console.log('Downloading from URL: ' + opts.url);
const req = request({ uri: opts.url });
req.on('error', () => callback('Error downloading from URL: ' + opts.url));
req.on('response', (res) => {
if (res.statusCode !== 200) return callback('Error downloading binary. HTTP Status Code: ' + res.statusCode);
const strategy = getStrategy(opts);
strategy({
opts,
req,
onSuccess: () => verifyAndPlaceBinary(opts.binName, opts.binPath, callback),
onError: callback
});
});
}
module.exports = install;

24
src/actions/uninstall.js Normal file
View File

@@ -0,0 +1,24 @@
const { join } = require('path');
const { unlinkSync } = require('fs');
const { parsePackageJson, getInstallationPath } = require('../common');
function uninstall(callback) {
const { binName } = parsePackageJson();
getInstallationPath((err, installationPath) => {
if (err) {
return callback(err);
}
try {
unlinkSync(join(installationPath, binName));
} catch(ex) {
// Ignore errors when deleting the file.
}
return callback(null);
});
}
module.exports = uninstall;

25
src/assets/binary.js Normal file
View File

@@ -0,0 +1,25 @@
const { join } = require('path');
const { existsSync, renameSync, chmodSync } = require('fs');
const { getInstallationPath } = require('../common');
function verifyAndPlaceBinary(binName, binPath, callback) {
if (!existsSync(join(binPath, binName))) {
return callback(`Downloaded binary does not contain the binary specified in configuration - ${binName}`);
}
getInstallationPath((err, installationPath) => {
if (err) {
return callback(err);
}
// Move the binary file and make sure it is executable
renameSync(join(binPath, binName), join(installationPath, binName));
chmodSync(join(installationPath, binName), '755');
console.log('Placed binary on', join(installationPath, binName));
callback(null);
});
}
module.exports = verifyAndPlaceBinary;

17
src/assets/move.js Normal file
View File

@@ -0,0 +1,17 @@
const { join } = require('path');
const { createWriteStream } = require('fs');
/**
* Move strategy for binary resources without compression.
*/
function move({ opts, req, onSuccess, onError }) {
const stream = createWriteStream(join(opts.binPath, opts.binName));
stream.on('error', onError);
stream.on('close', onSuccess);
req.pipe(stream);
}
module.exports = move;

25
src/assets/untar.js Normal file
View File

@@ -0,0 +1,25 @@
const tar = require('tar');
const zlib = require('zlib');
/**
* Unzip strategy for resources using `.tar.gz`.
*
* First we will Un-GZip, then we will untar. So once untar is completed,
* binary is downloaded into `binPath`. Verify the binary and call it good.
*/
function untar({ opts, req, onSuccess, onError }) {
const ungz = zlib.createGunzip();
const untar = tar.Extract({ path: opts.binPath });
ungz.on('error', onError);
untar.on('error', onError);
// First we will Un-GZip, then we will untar. So once untar is completed,
// binary is downloaded into `binPath`. Verify the binary and call it good
untar.on('end', onSuccess);
req.pipe(ungz).pipe(untar);
}
module.exports = untar;

28
src/cli.js Normal file
View File

@@ -0,0 +1,28 @@
const actions = {
install: (callback) => require('./actions/install')(callback),
uninstall: (callback) => require('./actions/uninstall')(callback)
};
// Parse command line arguments and call the right action
module.exports = ({ argv, exit }) => {
if (argv && argv.length > 2) {
const cmd = argv[2];
if (!actions[cmd]) {
console.log('Invalid command to go-npm. `install` and `uninstall` are the only supported commands');
exit(1);
} else {
actions[cmd]((err) => {
if (err) {
console.error(err);
exit(1);
} else {
exit(0);
}
});
}
} else {
console.log('No command supplied. `install` and `uninstall` are the only supported commands');
exit(1);
}
};

130
src/common.js Normal file
View File

@@ -0,0 +1,130 @@
const { join } = require('path');
const { exec } = require('child_process');
const { existsSync, readFileSync } = require('fs');
const mkdirp = require('mkdirp');
// Mapping from Node's `process.arch` to Golang's `$GOARCH`
const ARCH_MAPPING = {
ia32: '386',
x64: 'amd64',
arm: 'arm'
};
// Mapping between Node's `process.platform` to Golang's
const PLATFORM_MAPPING = {
darwin: 'darwin',
linux: 'linux',
win32: 'windows',
freebsd: 'freebsd'
};
function getInstallationPath(callback) {
// `npm bin` will output the path where binary files should be installed
exec('npm bin', (err, stdout, stderr) => {
let dir = null;
if (err || stderr || !stdout || stdout.length === 0) {
// We couldn't infer path from `npm bin`. Let's try to get it from
// Environment variables set by NPM when it runs.
// npm_config_prefix points to NPM's installation directory where `bin` folder is available
// Ex: /Users/foo/.nvm/versions/node/v4.3.0
const env = process.env;
if (env && env.npm_config_prefix) {
dir = join(env.npm_config_prefix, 'bin');
} else {
return callback(new Error('Error finding binary installation directory'));
}
} else {
dir = stdout.trim();
}
mkdirp.sync(dir);
callback(null, dir);
});
}
function validateConfiguration({ version, goBinary }) {
if (!version) {
return "'version' property must be specified";
}
if (!goBinary || typeof (goBinary) !== 'object') {
return "'goBinary' property must be defined and be an object";
}
if (!goBinary.name) {
return "'name' property is necessary";
}
if (!goBinary.path) {
return "'path' property is necessary";
}
if (!goBinary.url) {
return "'url' property is required";
}
}
function parsePackageJson() {
if (!(process.arch in ARCH_MAPPING)) {
console.error('Installation is not supported for this architecture: ' + process.arch);
return;
}
if (!(process.platform in PLATFORM_MAPPING)) {
console.error('Installation is not supported for this platform: ' + process.platform);
return
}
const packageJsonPath = join('.', 'package.json');
if (!existsSync(packageJsonPath)) {
console.error('Unable to find package.json. ' +
'Please run this script at root of the package you want to be installed');
return
}
const packageJson = JSON.parse(readFileSync(packageJsonPath));
const error = validateConfiguration(packageJson);
if (error && error.length > 0) {
console.error('Invalid package.json: ' + error);
return
}
// We have validated the config. It exists in all its glory
const binPath = packageJson.goBinary.path;
let binName = packageJson.goBinary.name;
let url = packageJson.goBinary.url;
let version = packageJson.version;
if (version[0] === 'v') version = version.substr(1); // strip the 'v' if necessary v0.0.1 => 0.0.1
// Binary name on Windows has .exe suffix
if (process.platform === 'win32') {
binName += '.exe'
url = url.replace(/{{win_ext}}/g, '.exe');
} else {
url = url.replace(/{{win_ext}}/g, '');
}
// Interpolate variables in URL, if necessary
url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]);
url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
url = url.replace(/{{version}}/g, version);
url = url.replace(/{{bin_name}}/g, binName);
return {
binName,
binPath,
url,
version
};
}
module.exports = { parsePackageJson, getInstallationPath };

View File

@@ -1,261 +1,3 @@
#!/usr/bin/env node
"use strict"
const request = require('request'),
path = require('path'),
tar = require('tar'),
zlib = require('zlib'),
mkdirp = require('mkdirp'),
fs = require('fs'),
exec = require('child_process').exec;
// Mapping from Node's `process.arch` to Golang's `$GOARCH`
const ARCH_MAPPING = {
"ia32": "386",
"x64": "amd64",
"arm": "arm"
};
// Mapping between Node's `process.platform` to Golang's
const PLATFORM_MAPPING = {
"darwin": "darwin",
"linux": "linux",
"win32": "windows",
"freebsd": "freebsd"
};
function getInstallationPath(callback) {
// `npm bin` will output the path where binary files should be installed
exec("npm bin", function(err, stdout, stderr) {
let dir = null;
if (err || stderr || !stdout || stdout.length === 0) {
// We couldn't infer path from `npm bin`. Let's try to get it from
// Environment variables set by NPM when it runs.
// npm_config_prefix points to NPM's installation directory where `bin` folder is available
// Ex: /Users/foo/.nvm/versions/node/v4.3.0
let env = process.env;
if (env && env.npm_config_prefix) {
dir = path.join(env.npm_config_prefix, "bin");
}
} else {
dir = stdout.trim();
}
mkdirp.sync(dir);
callback(null, dir);
});
}
function verifyAndPlaceBinary(binName, binPath, callback) {
if (!fs.existsSync(path.join(binPath, binName))) return callback(`Downloaded binary does not contain the binary specified in configuration - ${binName}`);
getInstallationPath(function(err, installationPath) {
if (err) return callback("Error getting binary installation path from `npm bin`");
// Move the binary file and make sure it is executable
fs.renameSync(path.join(binPath, binName), path.join(installationPath, binName));
fs.chmodSync(path.join(installationPath, binName), "755");
console.log("Placed binary on", path.join(installationPath, binName));
callback(null);
});
}
function validateConfiguration(packageJson) {
if (!packageJson.version) {
return "'version' property must be specified";
}
if (!packageJson.goBinary || typeof(packageJson.goBinary) !== "object") {
return "'goBinary' property must be defined and be an object";
}
if (!packageJson.goBinary.name) {
return "'name' property is necessary";
}
if (!packageJson.goBinary.path) {
return "'path' property is necessary";
}
if (!packageJson.goBinary.url) {
return "'url' property is required";
}
// if (!packageJson.bin || typeof(packageJson.bin) !== "object") {
// return "'bin' property of package.json must be defined and be an object";
// }
}
function parsePackageJson() {
if (!(process.arch in ARCH_MAPPING)) {
console.error("Installation is not supported for this architecture: " + process.arch);
return;
}
if (!(process.platform in PLATFORM_MAPPING)) {
console.error("Installation is not supported for this platform: " + process.platform);
return
}
const packageJsonPath = path.join(".", "package.json");
if (!fs.existsSync(packageJsonPath)) {
console.error("Unable to find package.json. " +
"Please run this script at root of the package you want to be installed");
return
}
let packageJson = JSON.parse(fs.readFileSync(packageJsonPath));
let error = validateConfiguration(packageJson);
if (error && error.length > 0) {
console.error("Invalid package.json: " + error);
return
}
// We have validated the config. It exists in all its glory
let binName = packageJson.goBinary.name;
let binPath = packageJson.goBinary.path;
let url = packageJson.goBinary.url;
let version = packageJson.version;
if (version[0] === 'v') version = version.substr(1); // strip the 'v' if necessary v0.0.1 => 0.0.1
// Binary name on Windows has .exe suffix
if (process.platform === "win32") {
binName += ".exe"
}
// Interpolate variables in URL, if necessary
url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]);
url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
url = url.replace(/{{version}}/g, version);
url = url.replace(/{{bin_name}}/g, binName);
return {
binName: binName,
binPath: binPath,
url: url,
version: version
}
}
/**
* Unzip strategy for resources using `.tar.gz`.
*
* First we will Un-GZip, then we will untar. So once untar is completed,
* binary is downloaded into `binPath`. Verify the binary and call it good.
*/
function untarStrategy(opts, req, callback) {
const ungz = zlib.createGunzip();
const untar = tar.Extract({path: opts.binPath});
ungz.on('error', callback);
untar.on('error', callback);
// First we will Un-GZip, then we will untar. So once untar is completed,
// binary is downloaded into `binPath`. Verify the binary and call it good
untar.on('end', verifyAndPlaceBinary.bind(null, opts.binName, opts.binPath, callback));
req.pipe(ungz).pipe(untar);
}
/**
* Move strategy for binary resources without compression.
*/
function moveStrategy(opts, req, callback) {
const stream = fs.createWriteStream(path.join(opts.binPath, opts.binName));
stream.on('error', callback);
stream.on('close', verifyAndPlaceBinary.bind(null, opts.binName, opts.binPath, callback));
req.pipe(stream);
}
/**
* Select a resource handling strategy based on given options.
*/
function getStrategy(opts) {
if (opts.url.endsWith('.tar.gz')) {
return untarStrategy;
} else {
return moveStrategy;
}
}
/**
* Reads the configuration from application's package.json,
* validates properties, downloads the binary, untars, and stores at
* ./bin in the package's root. NPM already has support to install binary files
* specific locations when invoked with "npm install -g"
*
* See: https://docs.npmjs.com/files/package.json#bin
*/
const INVALID_INPUT = "Invalid inputs";
function install(callback) {
const opts = parsePackageJson();
if (!opts) return callback(INVALID_INPUT);
const strategy = getStrategy(opts);
mkdirp.sync(opts.binPath);
console.log("Downloading from URL: " + opts.url);
let req = request({uri: opts.url});
req.on('error', callback.bind(null, "Error downloading from URL: " + opts.url));
req.on('response', function(res) {
if (res.statusCode !== 200) return callback("Error downloading binary. HTTP Status Code: " + res.statusCode);
strategy(opts, req, callback);
});
}
function uninstall(callback) {
let opts = parsePackageJson();
getInstallationPath(function(err, installationPath) {
if (err) callback("Error finding binary installation directory");
try {
fs.unlinkSync(path.join(installationPath, opts.binName));
} catch(ex) {
// Ignore errors when deleting the file.
}
return callback(null);
});
}
// Parse command line arguments and call the right method
let actions = {
"install": install,
"uninstall": uninstall
};
let argv = process.argv;
if (argv && argv.length > 2) {
let cmd = process.argv[2];
if (!actions[cmd]) {
console.log("Invalid command to go-npm. `install` and `uninstall` are the only supported commands");
process.exit(1);
}
actions[cmd](function(err) {
if (err) {
console.error(err);
process.exit(1);
} else {
process.exit(0);
}
});
}
require('./cli')(process);