diff --git a/README.md b/README.md index 35acb0a16..dcdf905c1 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/lib/build.js b/lib/build.js index 1074fa3b5..be97d66dd 100644 --- a/lib/build.js +++ b/lib/build.js @@ -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'); @@ -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); }); }); @@ -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) { diff --git a/lib/clean.js b/lib/clean.js index a1289f68a..d8436de57 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -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) { diff --git a/lib/configure.js b/lib/configure.js index 1ea56642b..a6e343820 100644 --- a/lib/configure.js +++ b/lib/configure.js @@ -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'); @@ -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); }); } diff --git a/lib/install.js b/lib/install.js index b35516135..ddefe67b1 100644 --- a/lib/install.js +++ b/lib/install.js @@ -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'; @@ -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); } @@ -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'); @@ -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); } diff --git a/lib/node-pre-gyp.js b/lib/node-pre-gyp.js index feb963fa0..3cf04c3f5 100644 --- a/lib/node-pre-gyp.js +++ b/lib/node-pre-gyp.js @@ -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; @@ -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) { diff --git a/lib/package.js b/lib/package.js index 7880394f7..f31ae632c 100644 --- a/lib/package.js +++ b/lib/package.js @@ -8,6 +8,8 @@ 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'); @@ -15,7 +17,8 @@ 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) { diff --git a/lib/pre-binding.js b/lib/pre-binding.js index 326a486c9..bc69d5ed4 100644 --- a/lib/pre-binding.js +++ b/lib/pre-binding.js @@ -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'); @@ -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; }; diff --git a/lib/publish.js b/lib/publish.js index d666b0131..376e39848 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -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'); @@ -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) { diff --git a/lib/rebuild.js b/lib/rebuild.js index 48a7b74b4..1e3a885c3 100644 --- a/lib/rebuild.js +++ b/lib/rebuild.js @@ -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); } diff --git a/lib/reinstall.js b/lib/reinstall.js index ed65d54e0..52e17aea2 100644 --- a/lib/reinstall.js +++ b/lib/reinstall.js @@ -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); } diff --git a/lib/reveal.js b/lib/reveal.js index e6d00eb34..13d2f725a 100644 --- a/lib/reveal.js +++ b/lib/reveal.js @@ -6,6 +6,7 @@ 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; @@ -13,12 +14,13 @@ function unix_paths(key, 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; diff --git a/lib/testbinary.js b/lib/testbinary.js index ff9542e55..453987c38 100644 --- a/lib/testbinary.js +++ b/lib/testbinary.js @@ -9,6 +9,7 @@ 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) { @@ -16,7 +17,8 @@ function testbinary(gyp, argv, callback) { 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' && diff --git a/lib/testpackage.js b/lib/testpackage.js index 9e5d0e026..9091bc32c 100644 --- a/lib/testpackage.js +++ b/lib/testpackage.js @@ -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) { diff --git a/lib/unpublish.js b/lib/unpublish.js index 43f8e66d2..94c93dd8e 100644 --- a/lib/unpublish.js +++ b/lib/unpublish.js @@ -7,6 +7,7 @@ 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"}); @@ -14,7 +15,8 @@ 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); diff --git a/lib/util/handle_gyp_opts.js b/lib/util/handle_gyp_opts.js index 39fe1a28e..afe7279d7 100644 --- a/lib/util/handle_gyp_opts.js +++ b/lib/util/handle_gyp_opts.js @@ -4,6 +4,7 @@ module.exports = exports = handle_gyp_opts; var fs = require('fs'); var versioning = require('./versioning.js'); +var napi = require('./napi.js'); /* @@ -44,6 +45,9 @@ var share_with_node_gyp = [ 'module', 'module_name', 'module_path', + 'napi_version', + 'node_abi_napi', + 'napi_build_version' ]; function handle_gyp_opts(gyp, argv, callback) { @@ -51,13 +55,15 @@ function handle_gyp_opts(gyp, argv, callback) { // Collect node-pre-gyp specific variables to pass to node-gyp var node_pre_gyp_options = []; // generate custom node-pre-gyp versioning info - var opts = versioning.evaluate(JSON.parse(fs.readFileSync('./package.json')), gyp.opts); + var napi_build_version = napi.get_napi_build_version_from_command_args(argv); + var opts = versioning.evaluate(JSON.parse(fs.readFileSync('./package.json')), gyp.opts, napi_build_version); share_with_node_gyp.forEach(function(key) { var val = opts[key]; if (val) { node_pre_gyp_options.push('--' + key + '=' + val); } else { - return callback(new Error("Option " + key + " required but not found by node-pre-gyp")); + if (key !== 'napi_version' && key !== 'node_abi_napi' && key !== 'napi_build_version') + return callback(new Error("Option " + key + " required but not found by node-pre-gyp")); } }); diff --git a/lib/util/napi.js b/lib/util/napi.js new file mode 100644 index 000000000..b74b94f42 --- /dev/null +++ b/lib/util/napi.js @@ -0,0 +1,156 @@ +"use strict"; + +var fs = require('fs'); +var rm = require('rimraf'); + +module.exports = exports; + +var versionArray = process.version + .substr(1) + .replace(/-.*$/, '') + .split('.') + .map(function(item) { + return +item; + }); + +var napi_multiple_commands = [ + 'build', + 'clean', + 'configure', + 'package', + 'publish', + 'reveal', + 'testbinary', + 'testpackage', + 'unpublish' +]; + +var napi_build_version_tag = 'napi_build_version='; + +module.exports.get_napi_version = function() { + // returns the non-zero numeric napi version or undefined if napi is not supported. + var version = process.versions.napi; // can be undefined + if (!version) { // this code should never need to be updated + if (versionArray[0] === 9 && versionArray[1] >= 3) version = 2; // 9.3.0+ + else if (versionArray[0] === 8) version = 1; // 8.0.0+ + } + return version; +}; + +module.exports.get_napi_version_as_string = function() { + // returns the napi version as a string or an empty string if napi is not supported. + var version = module.exports.get_napi_version(); + return version ? ''+version : ''; +}; + +module.exports.validate_package_json = function(package_json) { // return err + var binary = package_json.binary; + var module_path_ok = binary.module_path && binary.module_path.indexOf('{napi_build_version}') !== -1; + var remote_path_ok = binary.remote_path && binary.remote_path.indexOf('{napi_build_version}') !== -1; + var package_name_ok = binary.package_name && binary.package_name.indexOf('{napi_build_version}') !== -1; + var napi_build_versions = module.exports.get_napi_build_versions(package_json); + + if (napi_build_versions) { + napi_build_versions.forEach(function(napi_build_version){ + if (!(parseInt(napi_build_version,10) === napi_build_version && napi_build_version > 0)) { + throw new Error("All values specified in napi_versions must be positive integers."); + } + }); + } + + if (napi_build_versions && (!module_path_ok || (!remote_path_ok && !package_name_ok))) { + throw new Error("When napi_versions is specified; module_path and either remote_path or " + + "package_name must contain the substitution string '{napi_build_version}`."); + } + + if ((module_path_ok || remote_path_ok || package_name_ok) && !napi_build_versions) { + throw new Error("When the substitution string '{napi_build_version}` is specified in " + + "module_path, remote_path, or package_name; napi_versions must also be specified."); + } + + if (napi_build_versions && !module.exports.get_best_napi_build_version(package_json)) { + throw new Error( + 'The N-API version of this Node instance is ' + module.exports.get_napi_version() + '. ' + + 'This module supports N-API version(s) ' + module.exports.get_napi_build_versions(package_json) + '. ' + + 'This Node instance cannot run this module.'); + } + +}; + +module.exports.expand_commands = function(package_json, commands) { + var expanded_commands = []; + var napi_build_versions = module.exports.get_napi_build_versions(package_json); + commands.forEach(function(command){ + if (napi_build_versions && command.name === 'install') { + var napi_build_version = module.exports.get_best_napi_build_version(package_json); + var args = napi_build_version ? [ napi_build_version_tag+napi_build_version ] : [ ]; + expanded_commands.push ({ name: command.name, args: args }); + } else if (napi_build_versions && napi_multiple_commands.includes(command.name)) { + napi_build_versions.forEach(function(napi_build_version){ + var args = command.args.slice(); + args.push (napi_build_version_tag+napi_build_version); + expanded_commands.push ({ name: command.name, args: args }); + }); + } else { + expanded_commands.push (command); + } + }); + return expanded_commands; +}; + +module.exports.get_napi_build_versions = function(package_json) { + var napi_build_versions = []; + if (package_json.binary.napi_versions) { // remove duplicates + package_json.binary.napi_versions.forEach(function(napi_version) { + if (!napi_build_versions.includes(napi_version)) napi_build_versions.push(napi_version); + }); + } + return napi_build_versions.length ? napi_build_versions : undefined; +}; + +module.exports.get_command_arg = function(napi_build_version) { + return napi_build_version_tag + napi_build_version; +}; + +module.exports.get_napi_build_version_from_command_args = function(command_args) { + for (var i = 0; i < command_args.length; i++) { + var arg = command_args[i]; + if (arg.indexOf(napi_build_version_tag) === 0) { + return parseInt(arg.substr(napi_build_version_tag.length),10); + } + } + return undefined; +}; + +module.exports.swap_build_dir_out = function(napi_build_version) { + if (napi_build_version) { + rm.sync(module.exports.get_build_dir(napi_build_version)); + fs.renameSync('build', module.exports.get_build_dir(napi_build_version)); + } +}; + +module.exports.swap_build_dir_in = function(napi_build_version) { + if (napi_build_version) { + rm.sync('build'); + fs.renameSync(module.exports.get_build_dir(napi_build_version), 'build'); + } +}; + +module.exports.get_build_dir = function(napi_build_version) { + return 'build-tmp-napi-v'+napi_build_version; +}; + +module.exports.get_best_napi_build_version = function(package_json) { + var best_napi_build_version = 0; + var napi_build_versions = module.exports.get_napi_build_versions (package_json); + if (napi_build_versions) { + var our_napi_version = module.exports.get_napi_version(); + napi_build_versions.forEach(function(napi_build_version){ + if (napi_build_version > best_napi_build_version && + napi_build_version <= our_napi_version) { + best_napi_build_version = napi_build_version; + } + }); + } + return best_napi_build_version === 0 ? undefined : best_napi_build_version; +}; diff --git a/lib/util/versioning.js b/lib/util/versioning.js index 0fd570310..caf814828 100644 --- a/lib/util/versioning.js +++ b/lib/util/versioning.js @@ -6,6 +6,7 @@ var path = require('path'); var semver = require('semver'); var url = require('url'); var detect_libc = require('detect-libc'); +var napi = require('./napi.js'); var abi_crosswalk; @@ -225,6 +226,7 @@ function validate_config(package_json) { throw new Error("'host' protocol ("+protocol+") is invalid - only 'https:' is accepted"); } } + napi.validate_package_json(package_json); } module.exports.validate_config = validate_config; @@ -272,7 +274,7 @@ module.exports.get_process_runtime = get_process_runtime; var default_package_name = '{module_name}-v{version}-{node_abi}-{platform}-{arch}.tar.gz'; var default_remote_path = ''; -module.exports.evaluate = function(package_json,options) { +module.exports.evaluate = function(package_json,options,napi_build_version) { options = options || {}; validate_config(package_json); var v = package_json.version; @@ -291,6 +293,9 @@ module.exports.evaluate = function(package_json,options) { patch: module_version.patch, runtime: runtime, node_abi: get_runtime_abi(runtime,options.target), + node_abi_napi: napi.get_napi_version() ? 'napi' : get_runtime_abi(runtime,options.target), + napi_version: napi.get_napi_version(), // non-zero numeric, undefined if unsupported + napi_build_version: napi_build_version, // undefined if not specified target: options.target || '', platform: options.target_platform || process.platform, target_platform: options.target_platform || process.platform, diff --git a/test/app7/.gitignore b/test/app7/.gitignore new file mode 100644 index 000000000..f6a050319 --- /dev/null +++ b/test/app7/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +build/ +lib/binding/ +node_modules +npm-debug.log \ No newline at end of file diff --git a/test/app7/README.md b/test/app7/README.md new file mode 100644 index 000000000..f47278a39 --- /dev/null +++ b/test/app7/README.md @@ -0,0 +1,3 @@ +# Test app + +Demonstrates using N-API to return a JavaScript string object. diff --git a/test/app7/app7.cc b/test/app7/app7.cc new file mode 100644 index 000000000..6e6510f7c --- /dev/null +++ b/test/app7/app7.cc @@ -0,0 +1,24 @@ +// https://github.com/nodejs/abi-stable-node-addon-examples/blob/master/1_hello_world/napi/hello.cc +#include +#include + +napi_value Method(napi_env env, napi_callback_info info) { + napi_status status; + napi_value world; + status = napi_create_string_utf8(env, "world", 5, &world); + assert(status == napi_ok); + return world; +} + +#define DECLARE_NAPI_METHOD(name, func) \ + { name, 0, func, 0, 0, 0, napi_default, 0 } + +napi_value Init(napi_env env, napi_value exports) { + napi_status status; + napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method); + status = napi_define_properties(env, exports, 1, &desc); + assert(status == napi_ok); + return exports; +} + +NAPI_MODULE(hello, Init) diff --git a/test/app7/binding.gyp b/test/app7/binding.gyp new file mode 100644 index 000000000..1ef2bc571 --- /dev/null +++ b/test/app7/binding.gyp @@ -0,0 +1,13 @@ +{ + "targets": [ + { + "target_name": "<(module_name)", + "sources": [ "<(module_name).cc" ], + 'product_dir': '<(module_path)', + "xcode_settings": { + "MACOSX_DEPLOYMENT_TARGET":"10.9", + "CLANG_CXX_LIBRARY": "libc++" + } + } + ] +} diff --git a/test/app7/index.js b/test/app7/index.js new file mode 100644 index 000000000..24d008da3 --- /dev/null +++ b/test/app7/index.js @@ -0,0 +1,6 @@ +var binary = require('node-pre-gyp'); +var path = require('path') +var binding_path = binary.find(path.resolve(path.join(__dirname,'./package.json'))); +var binding = require(binding_path); + +require('assert').equal(binding.hello(),"world"); \ No newline at end of file diff --git a/test/app7/package.json b/test/app7/package.json new file mode 100644 index 000000000..fa6d0bbf2 --- /dev/null +++ b/test/app7/package.json @@ -0,0 +1,26 @@ +{ + "name": "node-pre-gyp-test-app7", + "author": "Jim Schlight ", + "description": "node-pre-gyp napi test", + "repository": { + "type": "git", + "url": "git://github.com/mapbox/node-pre-gyp.git" + }, + "license": "BSD-3-Clause", + "version": "0.1.0", + "main": "./index.js", + "dependencies": { + "node-pre-gyp": "0.6.x" + }, + "binary": { + "module_name": "app7", + "module_path": "./lib/binding/napi-v{napi_build_version}", + "remote_path": "./{module_name}/v{version}/{configuration}/", + "package_name": "{module_name}-v{version}-{platform}-{arch}-napi-v{napi_build_version}.tar.gz", + "host": "https://node-pre-gyp-tests.s3-us-west-1.amazonaws.com", + "napi_versions": [1,2] + }, + "scripts": { + "install": "node-pre-gyp install --fallback-to-build" + } +} diff --git a/test/build.test.js b/test/build.test.js index 2352544ea..3da462314 100644 --- a/test/build.test.js +++ b/test/build.test.js @@ -7,6 +7,7 @@ var fs = require('fs'); var rm = require('rimraf'); var path = require('path'); var getPrevious = require('./target_version.util.js'); +var napi = require ('../lib/util/napi.js'); // The list of different sample apps that we use to test var apps = [ @@ -29,6 +30,10 @@ var apps = [ { 'name': 'app4', 'args': '' + }, + { + 'name': 'app7', + 'args': '' } ]; @@ -106,6 +111,8 @@ test(app.name + ' passes --dist-url down to node-gyp via npm ' + app.args, funct apps.forEach(function(app) { + if (app.name === 'app7' && !napi.get_napi_version()) return; + // clear out entire binding directory // to ensure no stale builds. This is needed // because "node-pre-gyp clean" only removes @@ -172,6 +179,9 @@ apps.forEach(function(app) { run('node-pre-gyp', 'reveal', 'module_path --silent', app, {}, function(err,stdout,stderr) { t.ifError(err); var module_path = stdout.trim(); + if (module_path.indexOf('\n') !== -1) { // take just the first line + module_path = module_path.substr(0,module_path.indexOf('\n')); + } t.stringContains(module_path,app.name); t.ok(existsSync(module_path),'is valid path to existing binary: '+ module_path); var module_binary = path.join(module_path,app.name+'.node'); @@ -226,6 +236,9 @@ apps.forEach(function(app) { run('node-pre-gyp', 'reveal', 'package_name', app, {}, function(err,stdout,stderr) { t.ifError(err); var package_name = stdout.trim(); + if (package_name.indexOf('\n') !== -1) { // take just the first line + package_name = package_name.substr(0,package_name.indexOf('\n')); + } run('node-pre-gyp', 'info', '', app, {}, function(err,stdout,stderr) { t.ifError(err); t.stringContains(stdout,package_name);