versioning.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. 'use strict';
  2. module.exports = exports;
  3. const path = require('path');
  4. const semver = require('semver');
  5. const url = require('url');
  6. const detect_libc = require('detect-libc');
  7. const napi = require('./napi.js');
  8. let abi_crosswalk;
  9. // This is used for unit testing to provide a fake
  10. // ABI crosswalk that emulates one that is not updated
  11. // for the current version
  12. if (process.env.NODE_PRE_GYP_ABI_CROSSWALK) {
  13. abi_crosswalk = require(process.env.NODE_PRE_GYP_ABI_CROSSWALK);
  14. } else {
  15. abi_crosswalk = require('./abi_crosswalk.json');
  16. }
  17. const major_versions = {};
  18. Object.keys(abi_crosswalk).forEach((v) => {
  19. const major = v.split('.')[0];
  20. if (!major_versions[major]) {
  21. major_versions[major] = v;
  22. }
  23. });
  24. function get_electron_abi(runtime, target_version) {
  25. if (!runtime) {
  26. throw new Error('get_electron_abi requires valid runtime arg');
  27. }
  28. if (typeof target_version === 'undefined') {
  29. // erroneous CLI call
  30. throw new Error('Empty target version is not supported if electron is the target.');
  31. }
  32. // Electron guarantees that patch version update won't break native modules.
  33. const sem_ver = semver.parse(target_version);
  34. return runtime + '-v' + sem_ver.major + '.' + sem_ver.minor;
  35. }
  36. module.exports.get_electron_abi = get_electron_abi;
  37. function get_node_webkit_abi(runtime, target_version) {
  38. if (!runtime) {
  39. throw new Error('get_node_webkit_abi requires valid runtime arg');
  40. }
  41. if (typeof target_version === 'undefined') {
  42. // erroneous CLI call
  43. throw new Error('Empty target version is not supported if node-webkit is the target.');
  44. }
  45. return runtime + '-v' + target_version;
  46. }
  47. module.exports.get_node_webkit_abi = get_node_webkit_abi;
  48. function get_node_abi(runtime, versions) {
  49. if (!runtime) {
  50. throw new Error('get_node_abi requires valid runtime arg');
  51. }
  52. if (!versions) {
  53. throw new Error('get_node_abi requires valid process.versions object');
  54. }
  55. const sem_ver = semver.parse(versions.node);
  56. if (sem_ver.major === 0 && sem_ver.minor % 2) { // odd series
  57. // https://github.com/mapbox/node-pre-gyp/issues/124
  58. return runtime + '-v' + versions.node;
  59. } else {
  60. // process.versions.modules added in >= v0.10.4 and v0.11.7
  61. // https://github.com/joyent/node/commit/ccabd4a6fa8a6eb79d29bc3bbe9fe2b6531c2d8e
  62. return versions.modules ? runtime + '-v' + (+versions.modules) :
  63. 'v8-' + versions.v8.split('.').slice(0, 2).join('.');
  64. }
  65. }
  66. module.exports.get_node_abi = get_node_abi;
  67. function get_runtime_abi(runtime, target_version) {
  68. if (!runtime) {
  69. throw new Error('get_runtime_abi requires valid runtime arg');
  70. }
  71. if (runtime === 'node-webkit') {
  72. return get_node_webkit_abi(runtime, target_version || process.versions['node-webkit']);
  73. } else if (runtime === 'electron') {
  74. return get_electron_abi(runtime, target_version || process.versions.electron);
  75. } else {
  76. if (runtime !== 'node') {
  77. throw new Error("Unknown Runtime: '" + runtime + "'");
  78. }
  79. if (!target_version) {
  80. return get_node_abi(runtime, process.versions);
  81. } else {
  82. let cross_obj;
  83. // abi_crosswalk generated with ./scripts/abi_crosswalk.js
  84. if (abi_crosswalk[target_version]) {
  85. cross_obj = abi_crosswalk[target_version];
  86. } else {
  87. const target_parts = target_version.split('.').map((i) => { return +i; });
  88. if (target_parts.length !== 3) { // parse failed
  89. throw new Error('Unknown target version: ' + target_version);
  90. }
  91. /*
  92. The below code tries to infer the last known ABI compatible version
  93. that we have recorded in the abi_crosswalk.json when an exact match
  94. is not possible. The reasons for this to exist are complicated:
  95. - We support passing --target to be able to allow developers to package binaries for versions of node
  96. that are not the same one as they are running. This might also be used in combination with the
  97. --target_arch or --target_platform flags to also package binaries for alternative platforms
  98. - When --target is passed we can't therefore determine the ABI (process.versions.modules) from the node
  99. version that is running in memory
  100. - So, therefore node-pre-gyp keeps an "ABI crosswalk" (lib/util/abi_crosswalk.json) to be able to look
  101. this info up for all versions
  102. - But we cannot easily predict what the future ABI will be for released versions
  103. - And node-pre-gyp needs to be a `bundledDependency` in apps that depend on it in order to work correctly
  104. by being fully available at install time.
  105. - So, the speed of node releases and the bundled nature of node-pre-gyp mean that a new node-pre-gyp release
  106. need to happen for every node.js/io.js/node-webkit/nw.js/atom-shell/etc release that might come online if
  107. you want the `--target` flag to keep working for the latest version
  108. - Which is impractical ^^
  109. - Hence the below code guesses about future ABI to make the need to update node-pre-gyp less demanding.
  110. In practice then you can have a dependency of your app like `node-sqlite3` that bundles a `node-pre-gyp` that
  111. only knows about node v0.10.33 in the `abi_crosswalk.json` but target node v0.10.34 (which is assumed to be
  112. ABI compatible with v0.10.33).
  113. TODO: use semver module instead of custom version parsing
  114. */
  115. const major = target_parts[0];
  116. let minor = target_parts[1];
  117. let patch = target_parts[2];
  118. // io.js: yeah if node.js ever releases 1.x this will break
  119. // but that is unlikely to happen: https://github.com/iojs/io.js/pull/253#issuecomment-69432616
  120. if (major === 1) {
  121. // look for last release that is the same major version
  122. // e.g. we assume io.js 1.x is ABI compatible with >= 1.0.0
  123. while (true) {
  124. if (minor > 0) --minor;
  125. if (patch > 0) --patch;
  126. const new_iojs_target = '' + major + '.' + minor + '.' + patch;
  127. if (abi_crosswalk[new_iojs_target]) {
  128. cross_obj = abi_crosswalk[new_iojs_target];
  129. console.log('Warning: node-pre-gyp could not find exact match for ' + target_version);
  130. console.log('Warning: but node-pre-gyp successfully choose ' + new_iojs_target + ' as ABI compatible target');
  131. break;
  132. }
  133. if (minor === 0 && patch === 0) {
  134. break;
  135. }
  136. }
  137. } else if (major >= 2) {
  138. // look for last release that is the same major version
  139. if (major_versions[major]) {
  140. cross_obj = abi_crosswalk[major_versions[major]];
  141. console.log('Warning: node-pre-gyp could not find exact match for ' + target_version);
  142. console.log('Warning: but node-pre-gyp successfully choose ' + major_versions[major] + ' as ABI compatible target');
  143. }
  144. } else if (major === 0) { // node.js
  145. if (target_parts[1] % 2 === 0) { // for stable/even node.js series
  146. // look for the last release that is the same minor release
  147. // e.g. we assume node 0.10.x is ABI compatible with >= 0.10.0
  148. while (--patch > 0) {
  149. const new_node_target = '' + major + '.' + minor + '.' + patch;
  150. if (abi_crosswalk[new_node_target]) {
  151. cross_obj = abi_crosswalk[new_node_target];
  152. console.log('Warning: node-pre-gyp could not find exact match for ' + target_version);
  153. console.log('Warning: but node-pre-gyp successfully choose ' + new_node_target + ' as ABI compatible target');
  154. break;
  155. }
  156. }
  157. }
  158. }
  159. }
  160. if (!cross_obj) {
  161. throw new Error('Unsupported target version: ' + target_version);
  162. }
  163. // emulate process.versions
  164. const versions_obj = {
  165. node: target_version,
  166. v8: cross_obj.v8 + '.0',
  167. // abi_crosswalk uses 1 for node versions lacking process.versions.modules
  168. // process.versions.modules added in >= v0.10.4 and v0.11.7
  169. modules: cross_obj.node_abi > 1 ? cross_obj.node_abi : undefined
  170. };
  171. return get_node_abi(runtime, versions_obj);
  172. }
  173. }
  174. }
  175. module.exports.get_runtime_abi = get_runtime_abi;
  176. const required_parameters = [
  177. 'module_name',
  178. 'module_path',
  179. 'host'
  180. ];
  181. function validate_config(package_json, opts) {
  182. const msg = package_json.name + ' package.json is not node-pre-gyp ready:\n';
  183. const missing = [];
  184. if (!package_json.main) {
  185. missing.push('main');
  186. }
  187. if (!package_json.version) {
  188. missing.push('version');
  189. }
  190. if (!package_json.name) {
  191. missing.push('name');
  192. }
  193. if (!package_json.binary) {
  194. missing.push('binary');
  195. }
  196. const o = package_json.binary;
  197. if (o) {
  198. required_parameters.forEach((p) => {
  199. if (!o[p] || typeof o[p] !== 'string') {
  200. missing.push('binary.' + p);
  201. }
  202. });
  203. }
  204. if (missing.length >= 1) {
  205. throw new Error(msg + 'package.json must declare these properties: \n' + missing.join('\n'));
  206. }
  207. if (o) {
  208. // enforce https over http
  209. const protocol = url.parse(o.host).protocol;
  210. if (protocol === 'http:') {
  211. throw new Error("'host' protocol (" + protocol + ") is invalid - only 'https:' is accepted");
  212. }
  213. }
  214. napi.validate_package_json(package_json, opts);
  215. }
  216. module.exports.validate_config = validate_config;
  217. function eval_template(template, opts) {
  218. Object.keys(opts).forEach((key) => {
  219. const pattern = '{' + key + '}';
  220. while (template.indexOf(pattern) > -1) {
  221. template = template.replace(pattern, opts[key]);
  222. }
  223. });
  224. return template;
  225. }
  226. // url.resolve needs single trailing slash
  227. // to behave correctly, otherwise a double slash
  228. // may end up in the url which breaks requests
  229. // and a lacking slash may not lead to proper joining
  230. function fix_slashes(pathname) {
  231. if (pathname.slice(-1) !== '/') {
  232. return pathname + '/';
  233. }
  234. return pathname;
  235. }
  236. // remove double slashes
  237. // note: path.normalize will not work because
  238. // it will convert forward to back slashes
  239. function drop_double_slashes(pathname) {
  240. return pathname.replace(/\/\//g, '/');
  241. }
  242. function get_process_runtime(versions) {
  243. let runtime = 'node';
  244. if (versions['node-webkit']) {
  245. runtime = 'node-webkit';
  246. } else if (versions.electron) {
  247. runtime = 'electron';
  248. }
  249. return runtime;
  250. }
  251. module.exports.get_process_runtime = get_process_runtime;
  252. const default_package_name = '{module_name}-v{version}-{node_abi}-{platform}-{arch}.tar.gz';
  253. const default_remote_path = '';
  254. module.exports.evaluate = function(package_json, options, napi_build_version) {
  255. options = options || {};
  256. validate_config(package_json, options); // options is a suitable substitute for opts in this case
  257. const v = package_json.version;
  258. const module_version = semver.parse(v);
  259. const runtime = options.runtime || get_process_runtime(process.versions);
  260. const opts = {
  261. name: package_json.name,
  262. configuration: options.debug ? 'Debug' : 'Release',
  263. debug: options.debug,
  264. module_name: package_json.binary.module_name,
  265. version: module_version.version,
  266. prerelease: module_version.prerelease.length ? module_version.prerelease.join('.') : '',
  267. build: module_version.build.length ? module_version.build.join('.') : '',
  268. major: module_version.major,
  269. minor: module_version.minor,
  270. patch: module_version.patch,
  271. runtime: runtime,
  272. node_abi: get_runtime_abi(runtime, options.target),
  273. node_abi_napi: napi.get_napi_version(options.target) ? 'napi' : get_runtime_abi(runtime, options.target),
  274. napi_version: napi.get_napi_version(options.target), // non-zero numeric, undefined if unsupported
  275. napi_build_version: napi_build_version || '',
  276. node_napi_label: napi_build_version ? 'napi-v' + napi_build_version : get_runtime_abi(runtime, options.target),
  277. target: options.target || '',
  278. platform: options.target_platform || process.platform,
  279. target_platform: options.target_platform || process.platform,
  280. arch: options.target_arch || process.arch,
  281. target_arch: options.target_arch || process.arch,
  282. libc: options.target_libc || detect_libc.family || 'unknown',
  283. module_main: package_json.main,
  284. toolset: options.toolset || '', // address https://github.com/mapbox/node-pre-gyp/issues/119
  285. bucket: package_json.binary.bucket,
  286. region: package_json.binary.region,
  287. s3ForcePathStyle: package_json.binary.s3ForcePathStyle || false
  288. };
  289. // support host mirror with npm config `--{module_name}_binary_host_mirror`
  290. // e.g.: https://github.com/node-inspector/v8-profiler/blob/master/package.json#L25
  291. // > npm install v8-profiler --profiler_binary_host_mirror=https://npm.taobao.org/mirrors/node-inspector/
  292. const validModuleName = opts.module_name.replace('-', '_');
  293. const host = process.env['npm_config_' + validModuleName + '_binary_host_mirror'] || package_json.binary.host;
  294. opts.host = fix_slashes(eval_template(host, opts));
  295. opts.module_path = eval_template(package_json.binary.module_path, opts);
  296. // now we resolve the module_path to ensure it is absolute so that binding.gyp variables work predictably
  297. if (options.module_root) {
  298. // resolve relative to known module root: works for pre-binding require
  299. opts.module_path = path.join(options.module_root, opts.module_path);
  300. } else {
  301. // resolve relative to current working directory: works for node-pre-gyp commands
  302. opts.module_path = path.resolve(opts.module_path);
  303. }
  304. opts.module = path.join(opts.module_path, opts.module_name + '.node');
  305. opts.remote_path = package_json.binary.remote_path ? drop_double_slashes(fix_slashes(eval_template(package_json.binary.remote_path, opts))) : default_remote_path;
  306. const package_name = package_json.binary.package_name ? package_json.binary.package_name : default_package_name;
  307. opts.package_name = eval_template(package_name, opts);
  308. opts.staged_tarball = path.join('build/stage', opts.remote_path, opts.package_name);
  309. opts.hosted_path = url.resolve(opts.host, opts.remote_path);
  310. opts.hosted_tarball = url.resolve(opts.hosted_path, opts.package_name);
  311. return opts;
  312. };