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

N-API Support #345

Merged
merged 11 commits into from
Mar 10, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
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