Skip to content

Commit

Permalink
Merge pull request mapbox#345 from inspiredware/napi-support
Browse files Browse the repository at this point in the history
N-API Support
  • Loading branch information
springmeyer authored Mar 10, 2018
2 parents bc1ff5a + bede6ed commit c50fe56
Show file tree
Hide file tree
Showing 25 changed files with 410 additions and 19 deletions.
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,81 @@ What will happen is this:

If a a binary was not available for a given platform and `--fallback-to-build` was used then `node-gyp rebuild` will be called to try to source compile the module.

## N-API Considerations

[N-API](https://nodejs.org/api/n-api.html#n_api_n_api) is an ABI-stable alternative to previous technologies such as [nan](https://github.com/nodejs/nan) which are tied to a specific Node runtime engine. N-API is Node runtime engine agnostic and guarantees modules created today will continue to run, without changes, into the future.

Using `node-pre-gyp` with N-API projects requires a handful of additional congiguration values and imposes some additional requirements.

The most significant difference is that an N-API module can be coded to target multiple N-API versions. Therefore, an N-API module must declare in its `package.json` file which N-API versions the module is designed to run against. In addition, since multiple builds may be required for a single module, path and file names must be specified in way that avoids naming conflicts.

### The `napi_versions` array property

An N-API modules must declare in its `package.json` file, the N-API versions the module is intended to support. This is accomplished by including an `napi-versions` array property in the `binary` object. For example:

```js
"binary": {
"module_name": "your_module",
"module_path": "your_module_path",
"host": "https://your_bucket.s3-us-west-1.amazonaws.com",
"napi_versions": [1,3]
}
```

If the `napi_versions` array property is *not* present, `node-pre-gyp` operates as it always has. Including the `napi_versions` array property instructs `node-pre-gyp` that this is a N-API module build.

When the `napi_versions` array property is present, `node-pre-gyp` fires off multiple operations, one for each of the N-API versions in the array. In the example above, two operations are initiated, one for N-API version 1 and second for N-API version 3. How this version number is communicated is described next.

### The `napi_build_version` value

For each of the N-API module operations `node-pre-gyp` initiates, it insures that the `napi_build_version` is set appropriately.

This value is of importance in two areas:

1. The C/C++ code which needs to know against which N-API version it should compile.
2. `node-pre-gyp` itself which must assign appropriate path and file names to avoid collisions.

### Defining `NAPI_BUILD_VERSION` for the C/C++ code

The `napi_build_version` value is communicated to the C/C++ code by adding this code to the `binding.gyp` file:

```
"defines": [
"NAPI_BUILD_VERSION=<(napi_build_version)",
]
```

This insures that `NAPI_BUILD_VERSION`, an integer value, is declared appropriately to the C/C++ code for each build.

### Path and file naming requirements in `package.json`

Since `node-pre-gyp` fires off multiple operations for each request, it is essential that path and file names be created in such a way as to avoid collisions. This is accomplished by imposing additional path and file naming requirements.

Specifically, when performing N-API builds, the `{napi_build_version}` text substitution string *must* be present in the `module_path` property. In addition, the `{napi_build_version}` text substitution string *must* be present in either the `remote_path` or `package_name` property. (No problem if it's in both.)

Here's an example:

```js
"binary": {
"module_name": "your_module",
"module_path": "./lib/binding/napi-v{napi_build_version}",
"remote_path": "./{module_name}/v{version}/{configuration}/",
"package_name": "{platform}-{arch}-napi-v{napi_build_version}.tar.gz",
"host": "https://your_bucket.s3-us-west-1.amazonaws.com",
"napi_versions": [1,3]
}
```

### Two additional configuration values

For those who need them in legacy projects, two additional configuration values are available for all builds.

1. `napi_version` If N-API is supported by the currently executing Node instance, this value is the N-API version number supported by Node. If N-API is not supported, this value is an empty string.

2. `node_abi_napi` If the value returned for `napi_version` is non empty, this value is `'napi'`. If the value returned for `napi_version` is empty, this value is the value returned for `node_abi`.

These values are present for use in the `binding.gyp` file and may be used as `{napi_version}` and `{node_abi_napi}` for text substituion in the `package.json` file.

## S3 Hosting

You can host wherever you choose but S3 is cheap, `node-pre-gyp publish` expects it, and S3 can be integrated well with [Travis.ci](http://travis-ci.org) to automate builds for OS X and Ubuntu, and with [Appveyor](http://appveyor.com) to automate builds for Windows. Here is an approach to do this:
Expand Down
8 changes: 8 additions & 0 deletions lib/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = exports = build;

exports.usage = 'Attempts to compile the module by dispatching to node-gyp or nw-gyp';

var napi = require('./util/napi.js');
var compile = require('./util/compile.js');
var handle_gyp_opts = require('./util/handle_gyp_opts.js');
var configure = require('./configure.js');
Expand All @@ -16,7 +17,13 @@ function do_build(gyp,argv,callback) {
concat(['--']).
concat(result.unparsed);
}
if (!err && result.opts.napi_build_version) {
napi.swap_build_dir_in(result.opts.napi_build_version);
}
compile.run_gyp(final_args,result.opts,function(err) {
if (!err && result.opts.napi_build_version) {
napi.swap_build_dir_out(result.opts.napi_build_version);
}
return callback(err);
});
});
Expand All @@ -28,6 +35,7 @@ function build(gyp, argv, callback) {
// We map `node-pre-gyp build` to `node-gyp configure build` so that we do not
// trigger a clean and therefore do not pay the penalty of a full recompile
if (argv.length && (argv.indexOf('rebuild') > -1)) {
argv.shift(); // remove `rebuild`
// here we map `node-pre-gyp rebuild` to `node-gyp rebuild` which internally means
// "clean + configure + build" and triggers a full recompile
compile.run_gyp(['clean'],{},function(err) {
Expand Down
4 changes: 3 additions & 1 deletion lib/clean.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ var fs = require('fs');
var rm = require('rimraf');
var exists = require('fs').exists || require('path').exists;
var versioning = require('./util/versioning.js');
var napi = require('./util/napi.js');

function clean (gyp, argv, callback) {
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
var napi_build_version = napi.get_napi_build_version_from_command_args(argv);
var opts = versioning.evaluate(package_json, gyp.opts, napi_build_version);
var to_delete = opts.module_path;
exists(to_delete, function(found) {
if (found) {
Expand Down
4 changes: 4 additions & 0 deletions lib/configure.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = exports = configure;

exports.usage = 'Attempts to configure node-gyp or nw-gyp build';

var napi = require('./util/napi.js');
var compile = require('./util/compile.js');
var handle_gyp_opts = require('./util/handle_gyp_opts.js');

Expand Down Expand Up @@ -41,6 +42,9 @@ function configure(gyp, argv, callback) {
concat(result.unparsed);
}
compile.run_gyp(['configure'].concat(final_args),result.opts,function(err) {
if (!err && result.opts.napi_build_version) {
napi.swap_build_dir_out(result.opts.napi_build_version);
}
return callback(err);
});
}
Expand Down
7 changes: 5 additions & 2 deletions lib/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var path = require('path');
var log = require('npmlog');
var existsAsync = fs.exists || path.exists;
var versioning = require('./util/versioning.js');
var napi = require('./util/napi.js');
var mkdirp = require('mkdirp');

var npgVersion = 'unknown';
Expand Down Expand Up @@ -121,7 +122,8 @@ function place_binary(from,to,opts,callback) {
}

function do_build(gyp,argv,callback) {
gyp.todo.push( { name: 'build', args: ['rebuild'] } );
var args = ['rebuild'].concat(argv);
gyp.todo.push( { name: 'build', args: args } );
process.nextTick(callback);
}

Expand Down Expand Up @@ -151,6 +153,7 @@ function print_fallback_error(err,opts,package_json) {

function install(gyp, argv, callback) {
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var napi_build_version = napi.get_napi_build_version_from_command_args(argv);
var source_build = gyp.opts['build-from-source'] || gyp.opts.build_from_source;
var update_binary = gyp.opts['update-binary'] || gyp.opts.update_binary;
var should_do_source_build = source_build === package_json.name || (source_build === true || source_build === 'true');
Expand All @@ -171,7 +174,7 @@ function install(gyp, argv, callback) {
}
var opts;
try {
opts = versioning.evaluate(package_json, gyp.opts);
opts = versioning.evaluate(package_json, gyp.opts, napi_build_version);
} catch (err) {
return callback(err);
}
Expand Down
9 changes: 9 additions & 0 deletions lib/node-pre-gyp.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ module.exports = exports;
* Module dependencies.
*/

var fs = require('fs');
var path = require('path');
var nopt = require('nopt');
var log = require('npmlog');
log.disableProgress();
var napi = require('./util/napi.js');

var EE = require('events').EventEmitter;
var inherits = require('util').inherits;
Expand Down Expand Up @@ -128,6 +130,13 @@ proto.parseArgv = function parseOpts (argv) {
commands[commands.length - 1].args = argv.splice(0);
}

// expand commands entries for multiple napi builds
var dir = this.opts.directory;
if (dir == null) dir = process.cwd();
var package_json = JSON.parse(fs.readFileSync(path.join(dir,'package.json')));

this.todo = napi.expand_commands (package_json, commands);

// support for inheriting config env variables from npm
var npm_config_prefix = 'npm_config_';
Object.keys(process.env).forEach(function (name) {
Expand Down
5 changes: 4 additions & 1 deletion lib/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ var fs = require('fs');
var path = require('path');
var log = require('npmlog');
var versioning = require('./util/versioning.js');
var napi = require('./util/napi.js');
var write = require('fs').createWriteStream;
var existsAsync = fs.exists || path.exists;
var mkdirp = require('mkdirp');
var tar = require('tar');

function _package(gyp, argv, callback) {
var packlist = require('npm-packlist');
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
var napi_build_version = napi.get_napi_build_version_from_command_args(argv);
var opts = versioning.evaluate(package_json, gyp.opts, napi_build_version);
var from = opts.module_path;
var binary_module = path.join(from,opts.module_name + '.node');
existsAsync(binary_module,function(found) {
Expand Down
7 changes: 6 additions & 1 deletion lib/pre-binding.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

var versioning = require('../lib/util/versioning.js');
var napi = require('../lib/util/napi.js');
var existsSync = require('fs').existsSync || require('path').existsSync;
var path = require('path');

Expand All @@ -18,8 +19,12 @@ exports.find = function(package_json_path,opts) {
}
var package_json = require(package_json_path);
versioning.validate_config(package_json);
var napi_build_version;
if (napi.get_napi_build_versions (package_json)) {
napi_build_version = napi.get_best_napi_build_version(package_json);
}
opts = opts || {};
if (!opts.module_root) opts.module_root = path.dirname(package_json_path);
var meta = versioning.evaluate(package_json,opts);
var meta = versioning.evaluate(package_json,opts,napi_build_version);
return meta.module;
};
4 changes: 3 additions & 1 deletion lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var fs = require('fs');
var path = require('path');
var log = require('npmlog');
var versioning = require('./util/versioning.js');
var napi = require('./util/napi.js');
var s3_setup = require('./util/s3_setup.js');
var existsAsync = fs.exists || path.exists;
var url = require('url');
Expand All @@ -16,7 +17,8 @@ var config = require('rc')("node_pre_gyp",{acl:"public-read"});
function publish(gyp, argv, callback) {
var AWS = require("aws-sdk");
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
var napi_build_version = napi.get_napi_build_version_from_command_args(argv);
var opts = versioning.evaluate(package_json, gyp.opts, napi_build_version);
var tarball = opts.staged_tarball;
existsAsync(tarball,function(found) {
if (!found) {
Expand Down
16 changes: 12 additions & 4 deletions lib/rebuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ module.exports = exports = rebuild;

exports.usage = 'Runs "clean" and "build" at once';

var fs = require('fs');
var napi = require('./util/napi.js');

function rebuild (gyp, argv, callback) {
gyp.todo.unshift(
{ name: 'clean', args: [] },
{ name: 'build', args: ['rebuild'] }
);
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var commands = [
{ name: 'clean', args: [] },
{ name: 'build', args: ['rebuild'] }
];
commands = napi.expand_commands(package_json, commands);
for (var i = commands.length; i !== 0; i--) {
gyp.todo.unshift(commands[i-1]);
}
process.nextTick(callback);
}
9 changes: 8 additions & 1 deletion lib/reinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ module.exports = exports = rebuild;

exports.usage = 'Runs "clean" and "install" at once';

var fs = require('fs');
var napi = require('./util/napi.js');

function rebuild (gyp, argv, callback) {
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var installArgs = [];
var napi_build_version = napi.get_best_napi_version(package_json);
if (napi_build_version != null) installArgs = [ napi.get_command_arg (napi_build_version) ];
gyp.todo.unshift(
{ name: 'clean', args: [] },
{ name: 'install', args: [] }
{ name: 'install', args: installArgs }
);
process.nextTick(callback);
}
6 changes: 4 additions & 2 deletions lib/reveal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ exports.usage = 'Reveals data on the versioned binary';

var fs = require('fs');
var versioning = require('./util/versioning.js');
var napi = require('./util/napi.js');

function unix_paths(key, val) {
return val && val.replace ? val.replace(/\\/g, '/') : val;
}

function reveal(gyp, argv, callback) {
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
var napi_build_version = napi.get_napi_build_version_from_command_args(argv);
var opts = versioning.evaluate(package_json, gyp.opts, napi_build_version);
var hit = false;
// if a second arg is passed look to see
// if it is a known option
//console.log(JSON.stringify(gyp.opts,null,1))
var remain = gyp.opts.argv.remain.pop();
var remain = gyp.opts.argv.remain[gyp.opts.argv.remain.length-1];
if (remain && opts.hasOwnProperty(remain)) {
console.log(opts[remain].replace(/\\/g, '/'));
hit = true;
Expand Down
4 changes: 3 additions & 1 deletion lib/testbinary.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ var path = require('path');
var log = require('npmlog');
var cp = require('child_process');
var versioning = require('./util/versioning.js');
var napi = require('./util/napi.js');
var path = require('path');

function testbinary(gyp, argv, callback) {
var args = [];
var options = {};
var shell_cmd = process.execPath;
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
var napi_build_version = napi.get_napi_build_version_from_command_args(argv);
var opts = versioning.evaluate(package_json, gyp.opts, napi_build_version);
// skip validation for runtimes we don't explicitly support (like electron)
if (opts.runtime &&
opts.runtime !== 'node-webkit' &&
Expand Down
4 changes: 3 additions & 1 deletion lib/testpackage.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ var path = require('path');
var log = require('npmlog');
var existsAsync = fs.exists || path.exists;
var versioning = require('./util/versioning.js');
var napi = require('./util/napi.js');
var testbinary = require('./testbinary.js');
var tar = require('tar');
var mkdirp = require('mkdirp');

function testpackage(gyp, argv, callback) {
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
var napi_build_version = napi.get_napi_build_version_from_command_args(argv);
var opts = versioning.evaluate(package_json, gyp.opts, napi_build_version);
var tarball = opts.staged_tarball;
existsAsync(tarball, function(found) {
if (!found) {
Expand Down
4 changes: 3 additions & 1 deletion lib/unpublish.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ exports.usage = 'Unpublishes pre-built binary (requires aws-sdk)';
var fs = require('fs');
var log = require('npmlog');
var versioning = require('./util/versioning.js');
var napi = require('./util/napi.js');
var s3_setup = require('./util/s3_setup.js');
var url = require('url');
var config = require('rc')("node_pre_gyp",{acl:"public-read"});

function unpublish(gyp, argv, callback) {
var AWS = require("aws-sdk");
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
var napi_build_version = napi.get_napi_build_version_from_command_args(argv);
var opts = versioning.evaluate(package_json, gyp.opts, napi_build_version);
s3_setup.detect(opts.hosted_path,config);
AWS.config.update(config);
var key_name = url.resolve(config.prefix,opts.package_name);
Expand Down
Loading

0 comments on commit c50fe56

Please sign in to comment.