diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0a252e4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true +[*] +end_of_line = lf +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace=true +insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..19719fe --- /dev/null +++ b/.eslintrc @@ -0,0 +1,146 @@ +{ + "env": { + "node": true, + "es6": true + }, + "rules": { + "comma-dangle": [2,"never"], + "no-cond-assign": [2,"except-parens"], + "no-console": 2, + "no-constant-condition": 2, + "no-control-regex": 2, + "no-debugger": 2, + "no-dupe-keys": 2, + "no-dupe-class-members": 2, + "no-duplicate-imports": 2, + "no-empty": 2, + "no-empty-character-class": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 2, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-negated-in-lhs": 2, + "no-obj-calls": 2, + "no-regex-spaces": 2, + "no-reserved-keys": 0, + "no-sparse-arrays": 2, + "no-unreachable": 2, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + "prefer-const": 2, + "block-scoped-var": 0, + "complexity": 0, + "consistent-return": 2, + "curly": [2,"multi-line"], + "default-case": 0, + "dot-notation": 2, + "eqeqeq": 2, + "guard-for-in": 0, + "no-alert": 2, + "no-caller": 2, + "no-div-regex": 2, + "no-else-return": 2, + "no-eq-null": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-iterator": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-spaces": 2, + "no-multi-str": 0, + "no-native-reassign": 0, + "no-new": 0, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal": 0, + "no-octal-escape": 0, + "no-process-env": 0, + "no-proto": 0, + "no-redeclare": 2, + "no-return-assign": 0, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-throw-literal": 2, + "no-unused-expressions": 0, + "no-void": 0, + "no-warning-comments": [2, { "terms": ["FIXME"]}], + "no-with": 2, + "radix": 2, + "vars-on-top": 0, + "wrap-iife": 2, + "yoda": 0, + "no-catch-shadow": 2, + "no-delete-var": 2, + "no-label-var": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 2, + "no-unused-vars": 2, + "no-use-before-define": 2, + "strict": [2, "never"], + "handle-callback-err": 2, + "no-new-require": 2, + "no-path-concat": 2, + "no-process-exit": 0, + "no-restricted-modules": 0, + "no-sync": 2, + "indent": [2, 2], + "brace-style": [2,"1tbs"], + "camelcase": 0, + "comma-spacing": [2, { "before": false, "after": true }], + "comma-style": 2, + "consistent-this": 0, + "eol-last": 2, + "func-names": 0, + "func-style": 0, + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "linebreak-style": [2, "unix"], + "max-nested-callbacks": [2, 3], + "new-parens": 2, + "no-array-constructor": 2, + "no-inline-comments": 0, + "no-lonely-if": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [2, {"max": 1}], + "no-nested-ternary": 2, + "no-new-object": 2, + "semi-spacing": [2, {"before": false, "after": true}], + "no-spaced-func": 2, + "no-ternary": 0, + "no-trailing-spaces": 2, + "no-underscore-dangle": 0, + "operator-assignment": [2, "always"], + "padded-blocks": 0, + "quote-props": 0, + "quotes": [2, "single", "avoid-escape"], + "semi": [2, "never"], + "sort-vars": 0, + "keyword-spacing": [2, { "before": true, "after": true}], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "always"], + "object-curly-spacing": [2, "always"], + "array-bracket-spacing": [2, "never"], + "computed-property-spacing": [2, "never"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": false, "nonwords": false }], + "spaced-line-comment": 0, + "wrap-regex": 0, + "require-await": 2, + "no-var": 0, + "generator-star": 0 + } +} diff --git a/.gitignore b/.gitignore index 24b5cbf..55b337f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,8 @@ .DS_Store temp/ node_modules -npm-debug.log -test/**/src -test/**/out.nex -test/**/test.nex -test/**/tmp -test/**/node_modules -test/**/*.map -test/src -test/test.log -test-results.xml -src +*.log +*.exe .idea +.nexe + diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 28a18e0..0000000 --- a/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -test -src diff --git a/CHANGELOG.md b/CHANGELOG.md index 699a7fe..41d3f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ * Gave win32 a 100 length progress bar. ### Commits - + * [**2cacd83**] Update README.md (@crcn) * [**0e90ac9**] Update README.md (@crcn) * [**54967d1**] Added Gitter badge (@jaredallard) @@ -36,7 +36,6 @@ ## 2015-02-14, Version v0.3.5, @jaredallard - ### Noteable Changes * Added new flag: `--python ` diff --git a/LICENSE.txt b/LICENSE similarity index 96% rename from LICENSE.txt rename to LICENSE index 53ea90c..4fb8011 100644 --- a/LICENSE.txt +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013 Craig Condon +Copyright (c) 2013-2017 Nexe Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/bin/nexe b/bin/nexe deleted file mode 100755 index 9d12495..0000000 --- a/bin/nexe +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env node - -var path = require('path'), - fs = require('fs'), - nexe = require('../lib'), - _log = require("../lib/log"), - cli = require('yargs'). -usage('Usage: $0 -i [sources] -o [binary]'). -options('i', { - demand: true, - alias: 'input', - desc: 'The entry javascript files', - default: process.cwd() -}). -options('o', { - demand: true, - alias: 'output', - desc: 'The output binary', - default: "out.nex" -}). -options('r', { - alias: 'runtime', - default: 'latest', - description: 'The node.js runtime to use' -}). -options('t', { - alias: 'temp', - default: './tmp/nexe', - description: 'The path to store node.js sources' -}). -options('f', { - alias: 'flags', - description: 'Don\'t parse node and v8 flags, pass through app flags', - default: false -}). -options('d', { - alias: 'debug', - default: false, - description: 'generate and use source maps, large file size' -}). -options('j', { - alias: 'jsFlags', - default: false, - description: 'v8 flags for runtime' -}). -options('v', { - alias: 'version', - description: 'Display version number' -}). -options('p', { - alias: 'python', - description: 'Set path of python to use.', - default: 'python' -}). -options('F', { - alias: 'framework', - description: 'Set the framework to use', - default: 'nodejs' -}); - -var argv = cli.argv; -if (argv.h || argv.help) { - cli.showHelp(); - process.exit(); -} else if (argv.v || argv.version) { - var pkginfo = require('../package.json'); - console.log(pkginfo.version); - process.exit(); -} - -/** - * TODO: Support network shares? - **/ -function toAbsolute(pt) { - //already resolved? - try{ - fs.accessSync(pt,fs.F_OK); - return pt; - }catch(e){} - - if (pt.substr(0, 1) == "/") return pt; // for *nix "/" - if (pt.substr(0, 2) == "\\\\") return pt; // for windows "\\" - if (pt.substr(1, 3) == ":/") return pt; // for windows "c:/" - - // otherwise... - return path.join(process.cwd(), pt); -} - -if (fs.lstatSync(argv.i).isDirectory()) { - opts = nexe.package(path.join(argv.i, "package.json"), argv); - nexe.compile(opts, function(error) { - if (error) { - return console.log(error.message); - } - }); -} else { - /** - * Call the core - **/ - nexe.compile({ - input: require.resolve(toAbsolute(argv.i)), - output: toAbsolute(argv.o), - flags: argv.f, - nodeVersion: argv.r, - python: argv.python, - nodeTempDir: toAbsolute(argv.t), - framework: argv.F - }, - function(error) { - if (error) { - return console.log(error.message); - } - } - ); -} diff --git a/circle.yml b/circle.yml deleted file mode 100644 index ec6cd80..0000000 --- a/circle.yml +++ /dev/null @@ -1,8 +0,0 @@ -machine: - node: - version: 5.7.0 - -test: - override: - - npm test: - timeout: 5400 diff --git a/index.js b/index.js new file mode 100644 index 0000000..9fafb53 --- /dev/null +++ b/index.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node +const nexe = require('.'), + logger = require('./src/logger').logger + +module.exports = nexe + +if (require.main === module) { + nexe.compile(nexe.options).catch((e) => { + logger.error(e.stack, () => process.exit(1)) + }) +} diff --git a/lib/artifacts.js b/lib/artifacts.js new file mode 100644 index 0000000..0ab3bc9 --- /dev/null +++ b/lib/artifacts.js @@ -0,0 +1,99 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _path = require('path'); + +var _fs = require('fs'); + +var _util = require('./util'); + +var _bluebird = require('bluebird'); + +var _mkdirp = require('mkdirp'); + +var _mkdirp2 = _interopRequireDefault(_mkdirp); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +const mkdirpAsync = (0, _bluebird.promisify)(_mkdirp2.default); +const statAsync = (0, _bluebird.promisify)(_fs.stat); +const unlinkAsync = (0, _bluebird.promisify)(_fs.unlink); +const readdirAsync = (0, _bluebird.promisify)(_fs.readdir); + +function readDirAsync(dir) { + return readdirAsync(dir).map(file => { + const path = (0, _path.join)(dir, file); + return statAsync(path).then(s => s.isDirectory() ? readDirAsync(path) : path); + }).reduce((a, b) => a.concat(b), []); +} + +function maybeReadFileContents(file) { + return (0, _util.readFileAsync)(file, 'utf-8').catch(e => { + if (e.code === 'ENOENT') { + return ''; + } + throw e; + }); +} + +/** + * The artifacts step is where source patches are committed, or written as "artifacts" + * Steps: + * - A temporary directory is created in the downloaded source + * - On start, any files in that directory are restored into the source tree + * - After the patch functions have run, the temporary directory is emptied + * - Original versions of sources to be patched are written to the temporary directory + * - Finally, The patched files are written into source. + * + */ + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + const { src } = compiler; + const temp = (0, _path.join)(src, 'nexe'); + yield mkdirpAsync(temp); + const tmpFiles = yield readDirAsync(temp); + + yield (0, _bluebird.map)(tmpFiles, (() => { + var _ref2 = _asyncToGenerator(function* (path) { + return compiler.writeFileAsync(path.replace(temp, ''), (yield (0, _util.readFileAsync)(path))); + }); + + return function (_x3) { + return _ref2.apply(this, arguments); + }; + })()); + + yield next(); + + yield (0, _bluebird.map)(tmpFiles, function (x) { + return unlinkAsync(x); + }); + return (0, _bluebird.map)(compiler.files, (() => { + var _ref3 = _asyncToGenerator(function* (file) { + const sourceFile = (0, _path.join)(src, file.filename); + const tempFile = (0, _path.join)(temp, file.filename); + const fileContents = yield maybeReadFileContents(sourceFile); + + yield mkdirpAsync((0, _path.dirname)(tempFile)); + yield (0, _util.writeFileAsync)(tempFile, fileContents); + yield compiler.writeFileAsync(file.filename, file.contents); + }); + + return function (_x4) { + return _ref3.apply(this, arguments); + }; + })()); + }); + + function artifacts(_x, _x2) { + return _ref.apply(this, arguments); + } + + return artifacts; +})(); \ No newline at end of file diff --git a/lib/bundle.js b/lib/bundle.js deleted file mode 100644 index b8a5670..0000000 --- a/lib/bundle.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (c) 2013 Craig Condon - * Copyright (c) 2015-2016 Jared Allard - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - **/ - -'use strict'; - -let browserify = require('browserify'), - path = require("path"), - spawn = require('child_process').spawn, - insertGlobals = require('insert-module-globals'), - fs = require("fs"), - async = require("async"), - _log = require("./log"); - -/** - * User browserify to create a "packed" file. - * - * @param {string} input - input file - * @param {string} nc - node compiler dir - * @param {array} options - nexe options - * @param {function} complete - next function to call (async) - **/ -function bundle(input, nc, options, complete) { - const bundlePath = path.join(nc, "lib", "nexe.js"); - const mapfile = options.output+'.map'; - let ws = fs.createWriteStream(bundlePath); - - const igv = '__filename,__dirname,_process'; - let insertGlobalVars = { isNexe: true }, - wantedGlobalVars = igv.split(','); - - // parse insertGlobalVars. - Object.keys(insertGlobals.vars).forEach(function (x) { - if (wantedGlobalVars.indexOf(x) === -1) { - insertGlobalVars[x] = undefined; - } - }); - - let paths = [path.join(nc, 'lib')]; - - if(options.browserifyPaths) { - paths = paths.concat(options.browserifyPaths); - } - - - _log('executing browserify via API'); - let bproc = browserify([input], { - debug: options.debug, - commondir: false, - paths: paths, - builtins: false, - insertGlobalVars: insertGlobalVars, - detectGlobals: true, - browserField: false - }); - - if (options.browserifyExcludes && Array.isArray(options.browserifyExcludes)) { - for (let i = 0; i < options.browserifyExcludes.length; i++) { - let lib = options.browserifyExcludes[i]; - _log('Excluding \'%s\' from browserify bundle', lib); - bproc.exclude(lib); - } - } - - // copy the excludes code for requires for now. - if (options.browserifyRequires && Array.isArray(options.browserifyRequires)) { - for (let i = 0; i < options.browserifyRequires.length; i++) { - let lib = options.browserifyRequires[i]; - let name = lib.file || lib; // for object format. - // if `lib` is object then fetch all params without `file` - // otherwise returns empty object - let opts = (lib instanceof Object) && Object.keys(lib) - .filter((key) => key !== 'file') - .reduce((acc, key) => { return acc[key] = lib[key], acc }, {}) - || {}; - - _log('Force including \'%s\' in browserify bundle', name); - bproc.require(lib, opts); - } - } - - if(options.debug) { - bproc.require(require.resolve('source-map-support')) - } - - let bprocbun = bproc.bundle() // bundle - .pipe(ws) // pipe to file - - // error on require errors, still can't contionue. ffs browserify - bprocbun.on('error', function(err) { - _log('error', '[browserify] '+err); - }); - - ws.on('error', function(err) { - console.log(err); - _log('error', 'Failed to save stdout to disk'); - process.exit(1); - }) - - ws.on('close', function() { - var source = fs.readFileSync(bundlePath, 'utf8'); - source = source.replace(/[^\x00-\x7F]/g, ""); - - // write the source modified to nexe.js - fs.writeFile(bundlePath, source, 'utf8', function(err) { - if (err) { - _log('error', 'failed to save source'); - process.exit(1); - } - - complete(); - }); - }); -} - -module.exports = bundle; diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..aea34e1 --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,94 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _path = require('path'); + +var _bluebird = require('bluebird'); + +var _fs = require('fs'); + +var _util = require('./util'); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new _bluebird.Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return _bluebird.Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +const isWindows = process.platform === 'win32'; + +function getStdIn() { + return new _bluebird.Promise(resolve => { + const bundle = []; + process.stdin.setEncoding('utf-8'); + process.stdin.on('data', x => bundle.push(x)); + process.stdin.once('end', () => resolve((0, _util.dequote)(Buffer.concat(bundle).toString()))); + process.stdin.resume(); + }); +} + +/** + * The "cli" step detects whether the process is in a tty. If it is then the input is read into memory. + * Otherwise, it is buffered from stdin. If no input options are passed in the tty, the package.json#main file is used. + * After all the build steps have run, the output (the executable) is written to a file or piped to stdout. + * + * Configuration: + * - compiler.options.input - file path to the input bundle. + * - fallbacks: stdin, package.json#main + * - compiler.options.output - file path to the output executable. + * - fallbacks: stdout, nexe_ + epoch + ext + * + * @param {*} compiler + * @param {*} next + */ + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + const input = compiler.options.input; + const bundled = Boolean(compiler.input); + + if (bundled) { + yield next(); + } else if (!input && !process.stdin.isTTY) { + compiler.log.verbose('Buffering stdin as main module...'); + compiler.input = yield getStdIn(); + } else if (input) { + compiler.log.verbose('Reading input as main module: ' + input); + compiler.input = yield (0, _util.readFileAsync)((0, _path.normalize)(input)); + } else if (!compiler.options.empty) { + compiler.log.verbose('Resolving cwd as main module...'); + compiler.input = yield (0, _util.readFileAsync)(require.resolve(process.cwd())); + } + + if (!bundled) { + yield next(); + } + + const deliverable = yield compiler.getDeliverableAsync(); + + return new _bluebird.Promise(function (resolve, reject) { + deliverable.once('error', reject); + + if (!compiler.options.output && !process.stdout.isTTY) { + compiler.log.verbose('Writing result to stdout...'); + deliverable.pipe(process.stdout).once('error', reject); + resolve(); + } else { + compiler.log.verbose('Writing result to file...'); + const output = compiler.options.output || `${compiler.options.name}${isWindows ? '.exe' : ''}`; + deliverable.pipe((0, _fs.createWriteStream)((0, _path.normalize)(output))).once('error', reject).once('close', function (e) { + if (e) { + reject(e); + } else { + resolve(compiler.log.info('Executable written: ' + output)); + } + }); + } + }); + }); + + function cli(_x, _x2) { + return _ref.apply(this, arguments); + } + + return cli; +})(); \ No newline at end of file diff --git a/lib/compiler.js b/lib/compiler.js new file mode 100644 index 0000000..0e8c718 --- /dev/null +++ b/lib/compiler.js @@ -0,0 +1,111 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.NexeCompiler = undefined; + +var _path = require('path'); + +var _bluebird = require('bluebird'); + +var _fs = require('fs'); + +var _child_process = require('child_process'); + +var _logger = require('./logger'); + +var logger = _interopRequireWildcard(_logger); + +var _util = require('./util'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new _bluebird.Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return _bluebird.Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +const isWindows = process.platform === 'win32'; +const isBsd = Boolean(~process.platform.indexOf('bsd')); +const make = isWindows ? 'vcbuild.bat' : isBsd ? 'gmake' : 'make'; +const configure = isWindows ? 'configure' : './configure'; + +class NexeCompiler { + constructor(options) { + var _this = this; + + this.options = options; + logger.setLevel(options.loglevel); + this.log = logger; + this.python = options.python; + this.configure = options.configure; + this.make = isWindows ? options.vcBuild : options.make; + this.src = (0, _path.join)(options.temp, options.version); + this.env = Object.assign({}, process.env); + this.files = []; + + this.readFileAsync = (() => { + var _ref = _asyncToGenerator(function* (file) { + let cachedFile = _this.files.find(function (x) { + return (0, _path.normalize)(x.filename) === (0, _path.normalize)(file); + }); + if (!cachedFile) { + cachedFile = { + filename: file, + contents: yield (0, _util.readFileAsync)((0, _path.join)(_this.src, file), 'utf-8').catch({ code: 'ENOENT' }, function () { + return ''; + }) + }; + _this.files.push(cachedFile); + } + return cachedFile; + }); + + return function (_x) { + return _ref.apply(this, arguments); + }; + })(); + this.writeFileAsync = (file, contents) => (0, _util.writeFileAsync)((0, _path.join)(this.src, file), contents); + } + + set python(pythonPath) { + if (!pythonPath) { + return; + } + if (isWindows) { + this.env.PATH = this.env.PATH + ';"' + (0, _util.dequote)((0, _path.normalize)(pythonPath)) + '"'; + } else { + this.env.PYTHON = pythonPath; + } + } + + get _deliverableLocation() { + return isWindows ? (0, _path.join)(this.src, 'Release', 'node.exe') : (0, _path.join)(this.src, 'out', 'Release', 'node'); + } + + _runBuildCommandAsync(command, args) { + return new _bluebird.Promise((resolve, reject) => { + (0, _child_process.spawn)(command, args, { + cwd: this.src, + env: this.env, + stdio: 'ignore' + }).once('error', reject).once('close', resolve); + }); + } + + _configureAsync() { + return this._runBuildCommandAsync(this.env.PYTHON || 'python', [configure, ...this.configure]); + } + + buildAsync() { + var _this2 = this; + + return _asyncToGenerator(function* () { + yield _this2._configureAsync(); + return _this2._runBuildCommandAsync(make, _this2.make); + })(); + } + + getDeliverableAsync() { + return _bluebird.Promise.resolve((0, _fs.createReadStream)(this._deliverableLocation)); + } +} +exports.NexeCompiler = NexeCompiler; \ No newline at end of file diff --git a/lib/download.js b/lib/download.js new file mode 100644 index 0000000..45d556a --- /dev/null +++ b/lib/download.js @@ -0,0 +1,84 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = download; + +var _zlib = require('zlib'); + +var _tar = require('tar'); + +var _request = require('request'); + +var _request2 = _interopRequireDefault(_request); + +var _bluebird = require('bluebird'); + +var _fs = require('fs'); + +var _rimraf = require('rimraf'); + +var _rimraf2 = _interopRequireDefault(_rimraf); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const statAsync = (0, _bluebird.promisify)(_fs.stat); +const rimrafAsync = (0, _bluebird.promisify)(_rimraf2.default); + +function progress(req, log, precision = 10) { + const logged = {}; + let length = 0; + let total = 1; + + return req.on('response', res => { + total = res.headers['content-length']; + }).on('data', chunk => { + length += chunk.length; + const percentComplete = +(length / total * 100).toFixed(); + if (percentComplete % precision === 0 && !logged[percentComplete]) { + logged[percentComplete] = true; + log(percentComplete); + } + }); +} + +function fetchNodeSource(path, url, log) { + log.info('Downloading Node: ' + url); + return new _bluebird.Promise((resolve, reject) => { + progress(_request2.default.get(url), pc => { + log.verbose(`Downloading Node: ${pc}%...`); + if (pc === 100) { + log.info('Extracting Node...'); + } + }).on('error', reject).pipe((0, _zlib.createGunzip)().on('error', reject)).pipe((0, _tar.Extract)({ path, strip: 1 })).on('error', reject).on('end', () => resolve(log.info('Extracted to: ' + path))); + }); +} + +function cleanSrc(clean, src, log) { + if (clean === true) { + log.info('Removing source: ' + src); + return rimrafAsync(src).then(() => { + log.info('Source deleted.' + src); + }); + } + return _bluebird.Promise.resolve(); +} + +/** + * Deletes (maybe) and downloads the node source to the configured temporary directory + * @param {*} compiler + * @param {*} next + */ +function download(compiler, next) { + const { src, log } = compiler; + const { version, sourceUrl, clean } = compiler.options; + const url = sourceUrl || `https://nodejs.org/dist/v${version}/node-v${version}.tar.gz`; + + return cleanSrc(clean, src, log).then(() => statAsync(src).then(x => !x.isDirectory() && fetchNodeSource(src, url, log), e => { + if (e.code !== 'ENOENT') { + throw e; + } + return fetchNodeSource(src, url, log); + })).then(next); +} \ No newline at end of file diff --git a/lib/embed.js b/lib/embed.js deleted file mode 100644 index 084d63c..0000000 --- a/lib/embed.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2013 Craig Condon - * Copyright (c) 2015-2016 Jared Allard - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - **/ - -'use strict'; - -let path = require("path"), - fs = require("fs"); - -/** - * Accessort for embed - **/ -const accessor = function(key) { - if (embeddedFiles.hasOwnProperty(key)) { - return new Buffer(embeddedFiles[key], 'base64'); - } else { - //file was not embedded, throw err. - throw new Error('Embedded file not found'); - } -} - -/** - * Embed files. - * - * @param {array} resourceFiles - array of files to embed. - * @param {string} resourceRoot - root of resources. - * @param {function} compelte - callback - **/ -function embed(resourceFiles, resourceRoot, options, complete) { - const encode = function(filePath) { - return fs.readFileSync(filePath).toString('base64'); - } - - resourceFiles = resourceFiles || []; - resourceRoot = resourceRoot || ""; - - if (!Array.isArray(resourceFiles)) { - throw new Error("Bad Argument: resourceFiles is not an array"); - } - - let buffer = "var embeddedFiles = {\n"; - for (let i = 0; i < resourceFiles.length; ++i) { - buffer += JSON.stringify(path.relative(resourceRoot, resourceFiles[i])) + ': "'; - buffer += encode(resourceFiles[i]) + '",\n'; - } - - buffer += "\n};\n\nmodule.exports.keys = function () { return Object.keys(embeddedFiles); }\n\nmodule.exports.get = "; - buffer += accessor.toString(); - complete(null, buffer); -} - -module.exports = embed; diff --git a/lib/exe.js b/lib/exe.js deleted file mode 100644 index 81244e1..0000000 --- a/lib/exe.js +++ /dev/null @@ -1,1052 +0,0 @@ -/** - * Copyright (c) 2013 Craig Condon - * Copyright (c) 2015-2016 Jared Allard - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - **/ - -'use strict'; - -const async = require("async"), - mkdirp = require("mkdirp"), - request = require("request"), - gunzip = require("gunzip-maybe"), - path = require("path"), - fs = require("fs"), - tarstream = require('tar-stream'), - colors = require('colors'), - ncp = require("ncp").ncp, - ProgressBar = require("progress"), - child_process = require("child_process"), - glob = require("glob"), - bundle = require("./bundle"), - embed = require("./embed"), - os = require("os"), - _log = require("./log"), - _monkeypatch = require("./monkeypatch"), - spawn = child_process.spawn; - -const isWin = /^win/.test(process.platform); - -let isPy, - framework, - version; - -/** - * Compiliation process. - */ - -exports.compile = function(options, complete) { - - var nodeCompiler, nexeEntryPath; - - async.waterfall([ - /** - *check relevant options - */ - function checkOpts(next) { - /* failsafe */ - if (options === undefined) { - _log("error", "no options given to .compile()"); - process.exit() - } - - /** - * Have we been given a custom flag for python executable? - **/ - if (options.python !== 'python' && options.python !== "" && options.python !== undefined) { - if (isWin) { - isPy = options.python.replace(/\//gm, "\\"); // use windows file paths, batch is sensitive. - } else { - isPy = options.python; - } - - _log("set python as " + isPy); - } else { - isPy = "python"; - } - - // remove dots - options.framework = options.framework.replace(/\./g, ""); - - // set outter-scope framework variable. - framework = options.framework; - _log("framework => " + framework); - - version = options.nodeVersion; // better framework vc - - // check iojs version - if (framework === "iojs" && version === "latest") { - _log("fetching iojs versions"); - mkdirp(options.nodeTempDir); // make temp dir, probably repetive. - - // create write stream so we have control over events - var output = fs.createWriteStream(path.join(options.nodeTempDir, - "iojs-versions.json")); - - request.get("https://iojs.org/dist/index.json") - .pipe(output); - - output.on('close', function() { - _log("done"); - var f = fs.readFileSync(path.join(options.nodeTempDir, - "iojs-versions.json")); - f = JSON.parse(f); - version = f[0].version.replace("v", ""); - - _log("iojs latest => " + version); - - // continue down along the async road - next(); - }); - } else { - next(); - } - }, - - /** - * first download node - */ - function downloadNode(next) { - _downloadNode(version, options.nodeTempDir, options.nodeConfigureArgs, options.nodeMakeArgs, options.nodeVCBuildArgs, next); - }, - - /** - * Embed Resources into a base64 encoded array. - **/ - function embedResources(nc, next) { - nodeCompiler = nc; - - _log("embedResources %s", options.resourceFiles); - embed(options.resourceFiles, options.resourceRoot, nc, next); - }, - - /** - * Write nexeres.js - **/ - function writeResources(resources, next) { - let resourcePath = path.join(nodeCompiler.dir, "lib", "nexeres.js"); - _log("resource -> %s", resourcePath); - - fs.writeFile(resourcePath, resources, next); - }, - - /** - * Bundle the application into one script - **/ - function combineProject(next) { - _log("bundle %s", options.input); - bundle(options.input, nodeCompiler.dir, options, next); - }, - - /** - * monkeypatch some files so that the nexe.js file is loaded when the app runs - */ - - function monkeyPatchNodeConfig(next) { - _monkeyPatchNodeConfig(nodeCompiler, next, options); - }, - - /** - * monkeypatch node.cc to prevent v8 and node from processing CLI flags - */ - function monkeyPatchNodeCc(next) { - if (options.flags) { - _monkeyPatchMainCc(nodeCompiler, next); - } else { - next(); - } - }, - - function monkeyPatchv8FlagsCc(next) { - if (options.jsFlags) { - return _monkeyPatchv8FlagsCc(nodeCompiler, options, next); - } - - return next(); - }, - - /** - * monkeypatch child_process.js so nexejs knows when it is a forked process - */ - function monkeyPatchChildProc(next) { - _monkeyPatchChildProcess(nodeCompiler, next); - }, - - /** - * If an old compiled executable exists in the Release directory, delete it. - * This lets us see if the build failed by checking the existence of this file later. - */ - - function cleanUpOldExecutable(next) { - fs.unlink(nodeCompiler.releasePath, function(err) { - if (err) { - if (err.code === "ENOENT") { - next(); - } else { - throw err; - } - } else { - next(); - } - }); - }, - - /** - * compile the node application - */ - - function makeExecutable(next) { - if (isWin) { - _log("vcbuild [make stage]"); - } else { - _log("make"); - } - nodeCompiler.make(next); - }, - - /** - * we create the output directory if needed - */ - - function makeOutputDirectory(next) { - mkdirp(path.dirname(options.output), function() { - next(); - }); - }, - - /** - * Verify that the executable was compiled successfully - */ - - function checkThatExecutableExists(next) { - fs.exists(nodeCompiler.releasePath, function(exists) { - if (!exists) { - _log("error", - "The release executable has not been generated. " + - "This indicates a failure in the build process. " + - "There is likely additional information above." - ); - process.exit(1); - } else { - next(); - } - }); - }, - - /** - * Copy the compilied binary to the output specified. - */ - - function copyBinaryToOutput(next) { - _log("cp %s %s", nodeCompiler.releasePath, options.output); - ncp(nodeCompiler.releasePath, options.output, function(err) { - if (err) { - _log("error", "Couldn't copy binary."); - throw err; // dump raw error object - } - _log('copied'); - - next(); - }); - } - ], complete); -} - -/** - * Download a version of node - * - * @param {string} version, version of node to download - * @param {string} directory, where to store the downloaded src - * @param {function} complete, callback - */ - -function _downloadNode(version, directory, nodeConfigureArgs, nodeMakeArgs, nodeVCBuildArgs, complete) { - var nodeFileDir = path.resolve(path.join(directory, framework, version)), // fixes #107, was process.cwd(), + rest. - nodeFilePath = path.resolve(path.join(nodeFileDir, framework + "-" + version + ".tar.gz")); - - - // might already be downloaded, and unzipped - if (_getNodeCompiler(nodeFileDir, nodeConfigureArgs, nodeMakeArgs, nodeVCBuildArgs, complete)) { - return; - } - - - async.waterfall([ - - /** - * first make the directory where the zip file will live - */ - - function makeDirectory(next) { - mkdirp.sync(path.dirname(nodeFilePath)); - next(); - }, - - /** - * download node into the target directory - */ - - function downloadNode(next) { - if (fs.existsSync(nodeFilePath)) return next(); - - var uri = framework; - - if (framework === "node") { - uri = 'nodejs'; // if node, use nodejs uri - } else if (framework === 'nodejs') { - framework = 'node'; // support nodejs, and node, as framework. - } - - var type = global.type; - var url, prefix = "https://" + uri + ".org/dist"; - - if (version === "latest") { - url = prefix + "/" + framework + "-" + version + ".tar.gz"; - } else { - url = prefix + "/v" + version + "/" + framework + "-v" + version + ".tar.gz"; - } - - _log("downloading %s", url); - - var output = fs.createWriteStream(nodeFilePath, { - "flags": "w+" - }); - - // need to set user-agent to bypass some corporate firewalls - var requestOptions = { - url: url, - headers: { - "User-Agent": "Node.js" - } - } - - _logProgress(request(requestOptions)).pipe(output); - - output.on("close", function() { - next(); - }); - }, - - /** - * unzip in the same directory - */ - - function unzipNodeTarball(next) { - var onError = function(err) { - console.log(err.stack); - _log("error", "failed to extract the node source"); - process.exit(1); - } - - if (isWin) { - _log("extracting the node source [node-tar.gz]"); - - // tar-stream method w/ gunzip-maybe - var read = fs.createReadStream(nodeFilePath); - var extract = tarstream.extract() - var basedir = nodeFileDir; - - if (!fs.existsSync(nodeFileDir)) { - fs.mkdirSync(nodeFileDir); - } - - extract.on('entry', function(header, stream, callback) { - // header is the tar header - // stream is the content body (might be an empty stream) - // call next when you are done with this entry - - var absolutepath = path.join(basedir, header.name); - if (header.type === 'directory') { - // handle directories. - // console.log('dir:', header.name); - fs.mkdirSync(absolutepath); - return callback(); - } else if (header.type === 'file') { - // handle files - // console.log('file:', header.name); - } else { - console.log(header.type + ':', header.name); - _log('warn', 'unhandled type in tar extraction, skipping'); - return callback(); - } - - var write = fs.createWriteStream(absolutepath); - - stream.pipe(write); - - write.on('close', function() { - return callback(); - }); - - stream.on('error', function(err) { - return onError(err); - }) - - write.on('error', function(err) { - return onError(err); - }); - - stream.resume() // just auto drain the stream - }) - - extract.on('finish', function() { - _log('extraction finished'); - return next(); - }) - - read.pipe(gunzip()).pipe(extract); - } else { - _log("extracting the node source [native tar]"); - - var cmd = ["tar", "-xf", nodeFilePath, "-C", nodeFileDir]; - _log(cmd.join(" ")); - - var tar = spawn(cmd.shift(), cmd); - tar.stdout.pipe(process.stdout); - tar.stderr.pipe(process.stderr); - - tar.on("close", function() { - return next(); - }); - tar.on("error", onError); - } - }, - - /** - * return the compiler object for the node version - */ - - function(next, type) { - _getNodeCompiler(nodeFileDir, nodeConfigureArgs, nodeMakeArgs, nodeVCBuildArgs, next, type) - }, - - ], complete); -} - -/** - * Get the compilier we will use for whatever platform we may be on and configure - * it. - */ - -function _getNodeCompiler(nodeFileDir, nodeConfigureArgs, nodeMakeArgs, nodeVCBuildArgs, complete, type) { - var dir = _getFirstDirectory(nodeFileDir); - - // standard - var executable = "node.exe"; - var binary = "node"; - - // iojs specifics. - if (framework === "iojs") { - executable = "iojs.exe"; - binary = "iojs"; - } - - if (dir) { - if (isWin) { - complete(null, { - dir: dir, - version: path.basename(nodeFileDir), - releasePath: path.join(dir, "Release", executable), - make: function(next) { - // create a new env with minimal impact on old one - var newEnv = process.env - - if (isPy !== "python") { - // add the dir of the suposed python exe to path - newEnv.path = process.env.PATH + ";" + path.dirname(isPy) - } - - if (!nodeVCBuildArgs || !nodeVCBuildArgs.length) { - nodeVCBuildArgs = [ "nosign" ]; // "release" is already the default config value in VCBuild.bat - } - - // spawn a vcbuild process with our custom enviroment. - var vcbuild = spawn("vcbuild.bat", nodeVCBuildArgs, { - cwd: dir, - env: newEnv - }); - vcbuild.stdout.pipe(process.stdout); - vcbuild.stderr.pipe(process.stderr); - vcbuild.on("close", function() { - next(); - }); - } - }); - } else { - complete(null, { - dir: dir, - version: path.basename(nodeFileDir), - releasePath: path.join(dir, "out", "Release", binary), - make: function(next) { - var cfg = "./configure", - configure; - - var conf = [cfg]; - - /* add all nodeConfigureArgs to the ./configure argument array */ - conf = [conf].concat(nodeConfigureArgs); - - // should work for all use cases now. - configure = spawn(isPy, conf, { - cwd: dir.toString('ascii') - }); - - // local function, move to top eventually - function _loop(dir) { - /* eventually try every python file */ - var pdir = fs.readdirSync(dir); - - pdir.forEach(function(v, i) { - var stat = fs.statSync(dir + "/" + v); - if (stat.isFile()) { - // only process Makefiles and .mk targets. - if (v !== "Makefile" && path.extname(v) !== ".mk") { - return; - } - - _log("patching " + v); - - /* patch the file */ - var py = fs.readFileSync(dir + "/" + v, { - encoding: 'utf8' - }); - py = py.replace(/([a-z]|\/)*python(\w|)/gm, isPy); // this is definently needed - fs.writeFileSync(dir + "/" + v, py, { - encoding: 'utf8' - }); // write to file - - } else if (stat.isDirectory()) { - // must be dir? - // skip tests because we don't need them here - if (v !== "test") { - _loop(dir + "/" + v) - } - } - }); - } - - configure.stdout.pipe(process.stdout); - configure.stderr.pipe(process.stderr); - - // on error - configure.on("error", function(err) { - console.log('Error:', err); - console.log(''); - console.log('Details:'); - console.log('command =', isPy, cfg, conf_args || ''); - console.log('cwd =', dir); - - var configure_path = path.join(dir, 'configure'); - var contains_configure = fs.existsSync(configure_path); - - console.log('cwd contains configure,', (contains_configure ? colors.green('yes') : colors.red('no'))); - - var configure_size = fs.statSync(configure_path).size; - - console.log('configure is non-zero size,', ((configure_size > 0) ? colors.green('yes') : colors.red('no'))); - - _log("error", "failed to launch configure."); - process.exit(1); - }); - - // when it's finished - configure.on("close", function() { - if (isPy !== "python") { - /** - * Originally I thought this only applied to io.js, - * however I soon found out this affects node.js, - * so it is now mainstream. - */ - _log("preparing python"); - - // loop over depends - _loop(dir); - } - - if (nodeMakeArgs === undefined) { - nodeMakeArgs = []; - } - - var platformMake = "make"; - if (os.platform().match(/bsd$/) != null) { - platformMake = "gmake"; - } - - var make = spawn(platformMake, nodeMakeArgs, { - cwd: dir - }); - make.stdout.pipe(process.stdout); - make.stderr.pipe(process.stderr); - make.on("error", function(err) { - console.log(err); - _log("error", "failed to run make."); - process.exit(1); - }) - make.on("close", function() { - next(); - }); - }) - } - }); - } - return true; - } - - return false; -} - -/** - */ - -function _monkeyPatchNodeConfig(compiler, complete, options) { - async.waterfall([ - /** - * monkeypatch the gyp file to include the nexe.js and nexeres.js files - */ - function(next) { - _monkeyPatchGyp(compiler, next) - }, - - /** - * patch the configure file to allow for custom startup snapshots - */ - - function(next) { - _monkeyPatchConfigure(compiler, next, options); - }, - - /** - * monkeypatch main entry point - */ - function(next) { - _monkeyPatchMainJs(compiler, next) - } - ], complete); -} - -/** - * patch the gyp file to allow our custom includes - */ - -function _monkeyPatchGyp(compiler, complete) { - - var gypPath = path.join(compiler.dir, "node.gyp"); - - _monkeypatch( - gypPath, - function(content) { - return ~content.indexOf("nexe.js"); - }, - function(content, next) { - next(null, content.replace("'lib/fs.js',", "'lib/fs.js', 'lib/nexe.js', 'lib/nexeres.js', ")) - }, - complete - ) -} - - -/** - * patch the configure file to allow for custom startup snapshots - */ - -function _monkeyPatchConfigure(compiler, complete, options) { - var configurePath = path.join(compiler.dir, "configure"); - var snapshotPath = options.startupSnapshot; - - if (snapshotPath != null) { - _log('monkey patching configure file'); - snapshotPath = path.join(process.cwd(), snapshotPath); - _monkeypatch( - configurePath, - function(content) { - return ~content.indexOf("embed_script"); - }, - function(content, next) { - next(null, content.replace( - "def configure_v8(o):", - `def configure_v8(o):\n o['variables']['embed_script'] = r'${snapshotPath}'\n o['variables']['warmup_script'] = r'${snapshotPath}'`)); - }, - complete - ) - } - - return complete(); -} - -/** - */ - -function _monkeyPatchMainJs(compiler, complete) { - var mainPath = path.join(compiler.dir, "src", "node.js"); - - if(!fs.existsSync(mainPath)) { - _log('warn', 'src/node.js doesn\'t exist. Trying \'lib/internal/bootstrap_node.js\'') - mainPath = path.join(compiler.dir, "lib/internal", "bootstrap_node.js"); - } - - _monkeypatch( - mainPath, - function(content) { - return ~content.indexOf("nexe"); - }, - function(content, next) { - content = content.replace(/\(function\(process\) \{/, '\ -(function(process) {\n\ - if (process.argv[1] !== "nexe.js") {\n\ - process.argv.splice(1, 0, "nexe.js");\n\ - }\n\ -') - content = content.replace('function startup() {', '\ -function startup() {\n\ - process._eval = NativeModule.getSource("nexe");\n\ -') - next(null, content) - }, - complete - ); -} - -/** - * Make child_process work. - */ -function _monkeyPatchChildProcess(compiler, complete) { - var childProcPath = path.join(compiler.dir, "lib", "child_process.js"); - - _monkeypatch( - childProcPath, - function(content) { - return ~content.indexOf("--child_process"); - }, - function(content, next) { - next(null, content.replace(/return spawn\(/, 'args.unshift("nexe.js", "--child_process");\n return spawn(')); - }, - complete - ); -} - -/** - * Patch node.cc to not check the internal arguments. - */ - -function _monkeyPatchMainCc(compiler, complete) { - let finalContents; - - let mainPath = path.join(compiler.dir, "src", "node.cc"); - let mainC = fs.readFileSync(mainPath, { - encoding: 'utf8' - }); - - // content split, and original start/end - let constant_loc = 1; - let lines = mainC.split('\n'); - let startLine = lines.indexOf(' // TODO use parse opts'); - let endLine = lines.indexOf(' option_end_index = i;'); // pre node 0.11.6 compat - let isPatched = lines.indexOf('// NEXE_PATCH_IGNOREFLAGS'); - - if (isPatched !== -1) { - _log('already patched node.cc'); - return complete(); - } - - /** - * This is the new method of passing the args. Tested on node.js 0.12.5 - * and iojs 2.3.1 - **/ - if (endLine === -1 && startLine === -1) { // only if the pre-0.12.5 failed. - _log("using the after 0.12.5 method of ignoring flags."); - - startLine = lines.indexOf(" while (index < nargs && argv[index][0] == '-') {"); // beginning of the function - endLine = lines.indexOf(' // Copy remaining arguments.'); - endLine--; // space, then it's at the } - - constant_loc = lines.length + 1; - } else { - _log('using 0.10.x > method of ignoring flags'); - lines[endLine] = ' option_end_index = 1;'; - } - - /** - * This is the method for 5.5.0 - **/ - if (endLine === -1 || startLine === -1) { - _log("using the after 5.5.0 method of ignoring flags."); - - startLine = lines.indexOf(" while (index < nargs && argv[index][0] == '-' && !short_circuit) {"); // beginning of the function - endLine = lines.indexOf(' // Copy remaining arguments.'); - endLine--; // space, then it's at the } - - constant_loc = lines.length + 1; - } - - // other versions here. - if (endLine === -1 || startLine === -1) { // failsafe. - _log("error", "Failed to find a way to patch node.cc to ignoreFlags"); - _log("startLine =", startLine, '| endLine =', endLine); - process.exit(1); - } - - // check if it's been done - lines[constant_loc] = '// NEXE_PATCH_IGNOREFLAGS'; - - for (var i = startLine; i < endLine; i++) { - lines[i] = undefined; // set the value to undefined so it's skipped by the join - } - - _log('patched node.cc'); - - finalContents = lines.join('\n'); - - // write the file contents - fs.writeFile(mainPath, finalContents, { - encoding: 'utf8' - }, function(err) { - if (err) { - _log('error', 'failed to write to', mainPath); - return process.exit(1); - } - - return complete(); - }); -} - -/** - * Patch flags.cc from deps/v8/src to use hard-coded flags. - * this function is very closely ready to accept custom injection code. - **/ -function _monkeyPatchv8FlagsCc(compiler, options, complete) { - var mainPath = path.join(compiler.dir, "deps/v8/src", "flags.cc"); - - fs.readFile(mainPath, { - encoding: 'utf8' - }, function(err, contents) { - if (err) { - return _log('error', 'failed to read', mainPath); - } - - // Super simple injection here. Perhaps make it an array at somepoint? - var injection = '\ -const char* nexevargs = "{{args}}";\n\ -int nexevargslen = strlen(nexevargs);\n\ -SetFlagsFromString(nexevargs, nexevargslen);\n\ -'; - - var injectionSplit = injection.split('\n'); - var injectionLength = injectionSplit.length; - var contentsSplit = contents.split('\n'); - var contentsLength = contentsSplit.length; - var lastInjectionLine = injectionSplit[injectionLength - 2]; - - var lineToInjectAfter = contentsSplit.indexOf(' ComputeFlagListHash();'); - var haveWeInjectedBefore = contentsSplit.indexOf(lastInjectionLine); - - var lineInjectDifference = contentsLength - lineToInjectAfter; - - // support for 0.12.x - if (lineToInjectAfter === -1) { - _log('warn', 'Using an expiramental support patch for 0.12.x'); - lineToInjectAfter = contentsSplit.indexOf('#undef FLAG_MODE_DEFINE_IMPLICATIONS'); - } - - // support for 0.10.x - if (lineToInjectAfter === -1) { - _log('warn', '0.12.x patch failed. Trying 0.10.0 patch'); - lineToInjectAfter = contentsSplit.indexOf('#define FLAG_MODE_DEFINE_IMPLICATIONS') + 1; - } - - // this is debug, comment out. - // _log('v8 injection is', injectionLength, 'newlines long'); - // _log('v8 flags source is', contentsLength, 'newlines long'); - - // console.log(finalContents) - - var finalContents, - dontCombine; - - if (lineToInjectAfter !== -1 && haveWeInjectedBefore === -1) { - _log('injecting v8/flags.cc'); - - // super debug - // _log('v8 injection determined by', lastInjectionLine); - // _log('v8 inject after line', lineToInjectAfter); - // _log('v8 inject needs to shift', lineInjectDifference, 'amount of lines by', injectionLength); - - // compute out the amount of space we'll need in this. - var startShiftLine = contentsLength - 1; // minus one to make up for 0 arg line. - var endShiftLine = lineToInjectAfter; - var injectRoom = injectionLength - 1; - - injectionSplit[0] = injectionSplit[0].replace('{{args}}', options.jsFlags); - - for (var i = startShiftLine; i !== endShiftLine; i--) { - contentsSplit[i + injectRoom] = contentsSplit[i]; - contentsSplit[i] = ''; - } - - var injectionPos = 0; - for (var i = 0; i !== injectionLength - 1; i++) { - contentsSplit[(lineToInjectAfter + 1) + injectionPos] = injectionSplit[injectionPos]; - injectionPos++; - } - } else if (lineToInjectAfter !== -1 && haveWeInjectedBefore !== -1) { - _log('re-injecting v8 args'); - - dontCombine = true; - finalContents = contentsSplit.join('\n'); - finalContents = finalContents.replace(/const char\* nexevargs = "[A-Z\-\_]*";/gi, - 'const char* nexevargs = "' + options.jsFlags + '";'); - } else { - _log('error', 'failed to find a suitable injection point for v8 args.', - 'File a bug report with the node version and log.'); - - _log('lineToInjectAfter=' + lineToInjectAfter, 'haveWeInjectedBefore=' + haveWeInjectedBefore); - return process.exit(1); - } - - if (!dontCombine) { - finalContents = contentsSplit.join('\n'); - } - - // write the file contents - fs.writeFile(mainPath, finalContents, { - encoding: 'utf8' - }, function(err) { - if (err) { - _log('error', 'failed to write to', mainPath); - return process.exit(1); - } - - return complete(); - }) - }); -} - -/** - * Get the first directory of a string. - */ - -function _getFirstDirectory(dir) { - var files = glob.sync(dir + "/*"); - - for (var i = files.length; i--;) { - var file = files[i]; - if (fs.statSync(file).isDirectory()) return file; - } - - return false; -} - -/** - * Log the progress of a request object. - */ - -function _logProgress(req) { - - req.on("response", function(resp) { - - var len = parseInt(resp.headers["content-length"], 10), - bar = new ProgressBar("[:bar]", { - complete: "=", - incomplete: " ", - total: len, - width: 100 // just use 100 - }); - - req.on("data", function(chunk) { - bar.tick(chunk.length); - }); - }); - - req.on("error", function(err) { - console.log(err); - _log("error", "failed to download node sources,"); - process.exit(1); - }); - - return req; -} - -/** - * Attempt to parse the package.json for nexe information. - * - * @param {string} path - path to package.json - * @param {object} options - fallback options - * - * @todo implement options overriding package defaults. - * @todo make this much less hackily implemented.... - * - * @return {object} nexe.compile - options object - **/ -exports.package = function(path, options) { - let _package; // scope - - // check if the file exists - if (fs.existsSync(path) === false) { - _log("warn", "no package.json found."); - } else { - _package = require(path); - } - - if(!_package || !_package.nexe) { - _log('error', 'trying to use package.json variables, but not setup to do so!'); - process.exit(1); - } - - // replace ^$ w/ os specific extension on output - if (isWin) { - _package.nexe.output = _package.nexe.output.replace(/\^\$/, '.exe') // exe - } else { - _package.nexe.output = _package.nexe.output.replace(/\^\$/, '') // none - } - - // construct the object - let obj = { - input: (_package.nexe.input || options.i), - output: (_package.nexe.output || options.o), - flags: (_package.nexe.runtime.ignoreFlags || (options.f || false)), - resourceFiles: (_package.nexe.resourceFiles), - nodeVersion: (_package.nexe.runtime.version || options.r), - nodeConfigureArgs: (_package.nexe.runtime.nodeConfigureArgs || []), - nodeMakeArgs: (_package.nexe.runtime.nodeMakeArgs || []), - nodeVCBuildArgs: (_package.nexe.runtime.nodeVCBuildArgs || []), - jsFlags: (_package.nexe.runtime['js-flags'] || options.j), - python: (_package.nexe.python || options.p), - debug: (_package.nexe.debug || options.d), - nodeTempDir: (_package.nexe.temp || options.t), - framework: (_package.nexe.runtime.framework || options.f) - } - - // browserify options - if(_package.nexe.browserify !== undefined) { - obj.browserifyRequires = (_package.nexe.browserify.requires || []); - obj.browserifyExcludes = (_package.nexe.browserify.excludes || []); - obj.browserifyPaths = (_package.nexe.browserify.paths || []); - } - - // TODO: get rid of this crappy code I wrote and make it less painful to read. - Object.keys(_package.nexe).forEach(function(v, i) { - if (v !== "runtime" && v !== 'browserify') { - _log("log", v + " => '" + _package.nexe[v] + "'"); - } - }); - - return obj; -} diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 459e3d1..0000000 --- a/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./exe"); diff --git a/lib/log.js b/lib/log.js deleted file mode 100644 index 75f445f..0000000 --- a/lib/log.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2013 Craig Condon - * Copyright (c) 2015-2016 Jared Allard - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - **/ - -var colors = require('colors'); - -/** - * standard output, takes 3 different types. - * log, error, and warn - * - * @param {any} arguments - Text to output. - * @return undefined - **/ -function _log () { - - var args = Array.prototype.slice.call(arguments, 0), - level = args.shift(); - - if (!~["log", "error", "warn"].indexOf(level)) { - args.unshift(level); - level = "log"; - } - - if(level == "log") { - args[0] = "----> " + args[0]; - } else if(level == "error") { - args[0] = "....> " + colors.red("ERROR: ") + args[0] - } else if(level == "warn") { - args[0] = "....> " + colors.yellow("WARNING: ") + args[0] - } - - console[level].apply(console, args); -} - -// export the log function, for require() -module.exports = _log; diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..637cc61 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,59 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.verbose = exports.error = exports.info = exports.setLevel = undefined; + +var _os = require('os'); + +var _chalk = require('chalk'); + +var _chalk2 = _interopRequireDefault(_chalk); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function isFn(f) { + return typeof f === 'function'; +} + +function logNoop(x, cb) { + return isFn(cb) && process.nextTick(cb); +} + +function safeCb(f) { + return isFn(f) ? f : logNoop; +} + +const logLevels = [['verbose', 'green'], ['info', 'blue'], ['error', 'red']]; + +const logger = logLevels.reduce((logMethods, info) => { + const [level, color] = info; + logMethods[level] = function (output, cb) { + process.stderr.write(_chalk2.default[color](`${_os.EOL}[${level}]: ${output}`), safeCb(cb)); + }; + return logMethods; +}, { + setLevel(level) { + if (level === 'silent') { + logLevels.forEach(([x]) => { + logger[x] = logNoop; + }); + return; + } + process.on('beforeExit', () => { + process.stderr.write(_os.EOL); + }); + if (level === 'info') { + logger.verbose = logNoop; + } + return logger; + } +}); + +const { info, error, verbose, setLevel } = logger; + +exports.setLevel = setLevel; +exports.info = info; +exports.error = error; +exports.verbose = verbose; \ No newline at end of file diff --git a/lib/monkeypatch.js b/lib/monkeypatch.js deleted file mode 100644 index dd26949..0000000 --- a/lib/monkeypatch.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2013 Craig Condon - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - **/ - -var async = require("async"), - fs = require("fs"), - _log = require("./log"); - -/** - * Monkey patch a file. - * - * @param {string} filePath - path to file. - * @param {function} monkeyPatched - function to process contents - * @param {function} processor - TODO: detail what this is - * @param {function} complete - callback - * - * @return undefined - */ -function _monkeypatch (filePath, monkeyPatched, processor, complete) { - - async.waterfall([ - - function read (next) { - fs.readFile(filePath, "utf8", next); - }, - - // TODO - need to parse gyp file - this is a bit hacker - function monkeypatch (content, next) { - - if (monkeyPatched(content)) return complete(); - - _log("monkey patch %s", filePath); - processor(content, next); - }, - - function write (content, next) { - fs.writeFile(filePath, content, "utf8", next); - } - ], complete); -} - -// export the function for require() -module.exports = _monkeypatch; diff --git a/lib/nexe.js b/lib/nexe.js new file mode 100644 index 0000000..ec7f9e2 --- /dev/null +++ b/lib/nexe.js @@ -0,0 +1,85 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.compile = exports.isNexe = exports.argv = undefined; + +let compile = (() => { + var _ref = _asyncToGenerator(function* (compilerOptions, callback) { + const options = yield (0, _options.normalizeOptionsAsync)(compilerOptions); + const compiler = new _compiler.NexeCompiler(options); + + compiler.log.verbose('Compiler options:' + _os.EOL + JSON.stringify(compiler.options, null, 4)); + + const nexe = (0, _appBuilder.compose)(_bundle2.default, _cli2.default, _download2.default, (() => { + var _ref2 = _asyncToGenerator(function* (_, next) { + yield next(); + return compiler.buildAsync(); + }); + + return function (_x3, _x4) { + return _ref2.apply(this, arguments); + }; + })(), _artifacts2.default, _patches2.default, compiler.options.patches); + return nexe(compiler).asCallback(callback); + }); + + return function compile(_x, _x2) { + return _ref.apply(this, arguments); + }; +})(); + +var _appBuilder = require('app-builder'); + +var _bundle = require('./bundle'); + +var _bundle2 = _interopRequireDefault(_bundle); + +var _compiler = require('./compiler'); + +var _options = require('./options'); + +var _cli = require('./cli'); + +var _cli2 = _interopRequireDefault(_cli); + +var _download = require('./download'); + +var _download2 = _interopRequireDefault(_download); + +var _artifacts = require('./artifacts'); + +var _artifacts2 = _interopRequireDefault(_artifacts); + +var _patches = require('./patches'); + +var _patches2 = _interopRequireDefault(_patches); + +var _os = require('os'); + +var _bluebird = require('bluebird'); + +var _logger = require('./logger'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new _bluebird.Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return _bluebird.Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +_appBuilder.PromiseConfig.constructor = _bluebird.Promise; +(0, _bluebird.longStackTraces)(); + +function isNexe(callback) { + return _bluebird.Promise.resolve(Boolean(process.__nexe)).asCallback(callback); +} + +exports.argv = _options.argv; +exports.isNexe = isNexe; +exports.compile = compile; + + +if (require.main === module || process.__nexe) { + compile(_options.argv).catch(e => { + (0, _logger.error)(e.stack, () => process.exit(e.exitCode || 1)); + }); +} \ No newline at end of file diff --git a/lib/options.js b/lib/options.js new file mode 100644 index 0000000..5e5339f --- /dev/null +++ b/lib/options.js @@ -0,0 +1,152 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.normalizeOptionsAsync = exports.argv = undefined; + +var _minimist = require('minimist'); + +var _minimist2 = _interopRequireDefault(_minimist); + +var _path = require('path'); + +var _bluebird = require('bluebird'); + +var _os = require('os'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function padRight(str, l) { + return (str + ' '.repeat(l)).substr(0, l); +} +const defaults = { + temp: process.env.NEXE_TEMP || (0, _path.join)(process.cwd(), '.nexe'), + version: process.version.slice(1), + flags: [], + configure: [], + make: [], + vcBuild: ['nosign', 'release'], + quick: false, + bundle: true, + enableNodeCli: false, + verbose: false, + silent: false, + padding: 2, + patches: [] +}; +const alias = { + i: 'input', + o: 'output', + t: 'temp', + n: 'name', + v: 'version', + p: 'python', + f: 'flag', + c: 'configure', + m: 'make', + vc: 'vcBuild', + q: 'quick', + s: 'snapshot', + b: 'bundle', + cli: 'enableNodeCli', + h: 'help', + l: 'loglevel' +}; +const argv = (0, _minimist2.default)(process.argv, { alias, default: defaults }); +const help = ` +nexe --help CLI OPTIONS + + -i --input =./index.js -- application entry point + -o --output =./nexe.exe -- path to output file + -t --temp =./.nexe -- nexe temp directory (3Gb+) ~ NEXE_TEMP + -n --name =nexe.js -- file name for error reporting at run time + -v --version =${padRight(process.version.slice(1), 23)}-- node version + -p --python =/path/to/python2 -- python executable + -f --flag ="--expose-gc" -- *v8 flags to include during compilation + -c --configure ="--with-dtrace" -- *pass arguments to the configure command + -m --make ="--loglevel" -- *pass arguments to the make command + -vc --vcBuild =x64 -- *pass arguments to vcbuild.bat + -s --snapshot =/path/to/snapshot -- build with warmup snapshot + -b --bundle -- attempt bundling application + --ico -- file name for alternate icon file (windows) + --rc-* -- populate rc file options (windows) + --clean -- force download of sources + --enableNodeCli -- enable node cli enforcement (blocks app cli) + --sourceUrl -- pass an alternate source (node.tar.gz) url + --silent -- disable logging + --verbose -- set logging to verbose + + + -* variable key name * option can be used more than once + + TODO -q --quick =win32-x64-X.X.X -- use prebuilt binary (url, key or path) + TODO --resource-* =/path/to/resource -- *embed file bytes within the binary +`.trim(); + +function flattenFilter(...args) { + return [].concat(...args).filter(x => x); +} + +/** + * Extract keys such as { "rc-CompanyName": "Node.js" } to + * { CompanyName: "Node.js" } + * @param {*} match + * @param {*} options + */ +function extractCliMap(match, options) { + Object.keys(options).filter(x => match.test(x)).reduce((map, option) => { + const key = option.split('-')[1]; + map = map || {}; + map[key] = options[option]; + delete options[option]; + return map; + }, null); +} + +function tryResolveMainFileName() { + let filename = 'nexe'; + try { + const file = require.resolve(process.cwd()); + filename = (0, _path.basename)(file).replace((0, _path.extname)(file), ''); + } catch (_) {} + + return filename === 'index' ? 'nexe_' + Date.now() : filename; +} + +function extractLogLevel(options) { + if (options.loglevel) return options.loglevel; + if (options.silent) return 'silent'; + if (options.verbose) return 'verbose'; + if (options.info) return 'info'; +} + +function extractName(options) { + if (typeof options.input === 'string') { + return options.name || (0, _path.basename)(options.input).replace((0, _path.extname)(options.input), ''); + } + const mainName = tryResolveMainFileName(); + return options.name || mainName; +} + +function normalizeOptionsAsync(input) { + if (argv.help || Boolean(argv._.find(x => x === 'version'))) { + return (0, _bluebird.fromCallback)(cb => process.stderr.write(argv.help ? help : '2.0.0-beta.1' + _os.EOL, () => cb(null, process.exit(1)))); + } + + const options = Object.assign({}, defaults, input); + delete options._; + options.loglevel = extractLogLevel(options); + options.name = extractName(options); + options.flags = flattenFilter(options.flag, options.flags); + options.make = flattenFilter(options.make); + options.vcBuild = flattenFilter(options.vcBuild); + options.rc = options.rc || extractCliMap(/^rc-.*/, options); + options.resources = options.resources || extractCliMap(/^resource-.*/, options); + Object.keys(alias).filter(k => k !== 'rc').forEach(x => delete options[x]); + + return _bluebird.Promise.resolve(options); +} + +exports.argv = argv; +exports.normalizeOptionsAsync = normalizeOptionsAsync; \ No newline at end of file diff --git a/lib/patches/content.js b/lib/patches/content.js new file mode 100644 index 0000000..7903f31 --- /dev/null +++ b/lib/patches/content.js @@ -0,0 +1,36 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _buffer = require('buffer'); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + yield next(); + + const filename = 'lib/' + compiler.options.name + '.js'; + const file = yield compiler.readFileAsync(filename); + const header = '/' + '*'.repeat(19) + 'nexe_'; + const end = '_' + '*'.repeat(19) + '/'; + const padding = Array(compiler.options.padding * 1000000).fill('*').join(''); + + if (!padding) { + file.contents = compiler.input; + return; + } + + file.contents = [compiler.input, '/', padding, '/'].join(''); + + file.contents = header + _buffer.Buffer.byteLength(file.contents) + end + file.contents; + }); + + function content(_x, _x2) { + return _ref.apply(this, arguments); + } + + return content; +})(); \ No newline at end of file diff --git a/lib/patches/disable-node-cli.js b/lib/patches/disable-node-cli.js new file mode 100644 index 0000000..41ce40d --- /dev/null +++ b/lib/patches/disable-node-cli.js @@ -0,0 +1,29 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + if (compiler.options.enableNodeCli) { + return next(); + } + + const nodecc = yield compiler.readFileAsync('src/node.cc'); + console.log(nodecc); + const nodeccMarker = "argv[index][0] == '-'"; + + nodecc.contents = nodecc.contents.replace(nodeccMarker, nodeccMarker.replace('-', ']')); + + return next(); + }); + + function disableNodeCli(_x, _x2) { + return _ref.apply(this, arguments); + } + + return disableNodeCli; +})(); \ No newline at end of file diff --git a/lib/patches/flags.js b/lib/patches/flags.js new file mode 100644 index 0000000..37fbe01 --- /dev/null +++ b/lib/patches/flags.js @@ -0,0 +1,28 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + const nodeflags = compiler.options.flags; + if (!nodeflags.length) { + return next(); + } + + const nodegyp = yield compiler.readFileAsync('node.gyp'); + + nodegyp.contents = nodegyp.contents.replace("'node_v8_options%': ''", `'node_v8_options%': '${nodeflags.join(' ')}'`); + + return next(); + }); + + function flags(_x, _x2) { + return _ref.apply(this, arguments); + } + + return flags; +})(); \ No newline at end of file diff --git a/lib/patches/gyp.js b/lib/patches/gyp.js new file mode 100644 index 0000000..c4bc206 --- /dev/null +++ b/lib/patches/gyp.js @@ -0,0 +1,31 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +exports.default = (() => { + var _ref = _asyncToGenerator(function* ({ files, readFileAsync }, next) { + yield next(); + + const nodegyp = yield readFileAsync('node.gyp'); + const nodeGypMarker = "'lib/fs.js',"; + + nodegyp.contents = nodegyp.contents.replace(nodeGypMarker, ` + ${nodeGypMarker} + ${files.filter(function (x) { + return x.filename.startsWith('lib'); + }).map(function (x) { + return `'${x.filename}'`; + }).toString()}, + `.trim()); + }); + + function nodeGyp(_x, _x2) { + return _ref.apply(this, arguments); + } + + return nodeGyp; +})(); \ No newline at end of file diff --git a/lib/patches/ico.js b/lib/patches/ico.js new file mode 100644 index 0000000..bd53c24 --- /dev/null +++ b/lib/patches/ico.js @@ -0,0 +1,29 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _path = require('path'); + +var _util = require('../util'); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + const iconFile = compiler.options.ico; + if (!iconFile) { + return next(); + } + const file = yield compiler.readFileAsync('src/res/node.ico'); + file.contents = yield (0, _util.readFileAsync)((0, _path.normalize)(iconFile)); + return next(); + }); + + function ico(_x, _x2) { + return _ref.apply(this, arguments); + } + + return ico; +})(); \ No newline at end of file diff --git a/lib/patches/index.js b/lib/patches/index.js new file mode 100644 index 0000000..97bf3ee --- /dev/null +++ b/lib/patches/index.js @@ -0,0 +1,39 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _gyp = require('./gyp'); + +var _gyp2 = _interopRequireDefault(_gyp); + +var _content = require('./content'); + +var _content2 = _interopRequireDefault(_content); + +var _thirdPartyMain = require('./third-party-main'); + +var _thirdPartyMain2 = _interopRequireDefault(_thirdPartyMain); + +var _disableNodeCli = require('./disable-node-cli'); + +var _disableNodeCli2 = _interopRequireDefault(_disableNodeCli); + +var _flags = require('./flags'); + +var _flags2 = _interopRequireDefault(_flags); + +var _ico = require('./ico'); + +var _ico2 = _interopRequireDefault(_ico); + +var _nodeRc = require('./node-rc'); + +var _nodeRc2 = _interopRequireDefault(_nodeRc); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const patches = [_gyp2.default, _content2.default, _thirdPartyMain2.default, _disableNodeCli2.default, _flags2.default, _ico2.default, _nodeRc2.default]; + +exports.default = patches; \ No newline at end of file diff --git a/lib/patches/node-rc.js b/lib/patches/node-rc.js new file mode 100644 index 0000000..988da0f --- /dev/null +++ b/lib/patches/node-rc.js @@ -0,0 +1,34 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + const options = compiler.options.rc; + if (!options) { + return next(); + } + + const file = yield compiler.readFileAsync('src/res/node.rc'); + + Object.keys(options).forEach(function (key) { + let value = options[key]; + const isVar = /^[A-Z_]+$/.test(value); + + value = isVar ? value : `"${value}"`; + file.contents.replace(new RegExp(`VALUE "${key}",*`), `VALUE "${key}", ${value}`); + }); + + return next(); + }); + + function nodeRc(_x, _x2) { + return _ref.apply(this, arguments); + } + + return nodeRc; +})(); \ No newline at end of file diff --git a/lib/patches/snapshot.js b/lib/patches/snapshot.js new file mode 100644 index 0000000..bc0ca9b --- /dev/null +++ b/lib/patches/snapshot.js @@ -0,0 +1,27 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + const snapshotFile = compiler.options.snapshot; + + if (!snapshotFile) { + return next(); + } + + const file = yield compiler.readFileAsync('configure'); + file.contents = file.contents.replace('def configure_v8(o):', `def configure_v8(o):\n o['variables']['embed_script'] = '${snapshotFile}'\n o['variables']['warmup_script'] = '${snapshotFile}'`); + return next(); + }); + + function snapshot(_x, _x2) { + return _ref.apply(this, arguments); + } + + return snapshot; +})(); \ No newline at end of file diff --git a/lib/patches/third-party-main.js b/lib/patches/third-party-main.js new file mode 100644 index 0000000..445b0a4 --- /dev/null +++ b/lib/patches/third-party-main.js @@ -0,0 +1,37 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +exports.default = (() => { + var _ref = _asyncToGenerator(function* (compiler, next) { + const mainFile = yield compiler.readFileAsync('lib/_third_party_main.js'); + mainFile.contents = ` + Object.defineProperty(process, '__nexe', { + value: true, + enumerable: false, + writable: false, + configurable: false + }); + require("${compiler.options.name}"); + `.trim(); + + if (compiler.options.empty === true) { + compiler.options.resources.length = 0; + //eslint-disable-next-line + compiler.input = 'console.log(`nexe-${process.platform}-${process.arch}-${process.version}`)'; + return next(); + } + + return next(); + }); + + function main(_x, _x2) { + return _ref.apply(this, arguments); + } + + return main; +})(); \ No newline at end of file diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..8941b1d --- /dev/null +++ b/lib/util.js @@ -0,0 +1,28 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.dequote = exports.writeFileAsync = exports.readFileAsync = undefined; + +var _fs = require('fs'); + +var _bluebird = require('bluebird'); + +function dequote(input) { + input = input.trim(); + + const singleQuote = input.startsWith('\'') && input.endsWith('\''); + const doubleQuote = input.startsWith('"') && input.endsWith('"'); + if (singleQuote || doubleQuote) { + return input.slice(1).slice(0, -1); + } + return input; +} + +const readFileAsync = (0, _bluebird.promisify)(_fs.readFile); +const writeFileAsync = (0, _bluebird.promisify)(_fs.writeFile); + +exports.readFileAsync = readFileAsync; +exports.writeFileAsync = writeFileAsync; +exports.dequote = dequote; \ No newline at end of file diff --git a/package.json b/package.json index 72ea79b..828a841 100644 --- a/package.json +++ b/package.json @@ -1,55 +1,36 @@ { - "author": "Jared Allard ", "name": "nexe", - "description": "create single executables out of your [node/io].js applications", + "description": "Create a single executable out of your Node.js application", "license": "MIT", - "version": "1.1.2", + "version": "2.0.0-beta.1", "contributors": [ - { - "name": "Criag Condon", - "email": "craig.j.condon@gmail.com", - "url": "http://crcn.io/" - } + "Craig Condon (http://crcn.io)", + "Jared Allard ", + "Caleb Boyd " ], "scripts": { - "test": "echo node: $(node -v) && node_modules/.bin/mocha --reporter mocha-circleci-reporter test/test.js" + "lint": "eslint index.js ./src/**/*", + "test": "npm run lint" }, "repository": { "type": "git", - "url": "git://github.com/jaredallard/nexe.git" + "url": "git://github.com/nexe/nexe.git" }, - "main": "./lib/index.js", - "dependencies": { - "async": "^1.5.2", - "browserify": "^13.0.0", - "colors": "^1.1.2", - "glob": "^7.0.0", - "gunzip-maybe": "^1.3.1", - "insert-module-globals": "^7.0.1", - "mkdirp": "^0.5.1", - "module-deps": "^4.0.5", - "ncp": "^2.0.0", - "progress": "^1.1.8", - "request": "^2.67.0", - "tar-stream": "^1.3.1", - "yargs": "^4.2.0" - }, - "nexe": { - "input": "./bin/nexe", - "output": "nexe^$", - "temp": "src", - "runtime": { - "framework": "nodejs", - "version": "5.5.0", - "ignoreFlags": true - } - }, - "preferGlobal": true, + "main": "src/nexe.js", "bin": { - "nexe": "bin/nexe" + "nexe": "index.js" + }, + "dependencies": { + "app-builder": "^5.1.0", + "bluebird": "^3.5.0", + "chalk": "^1.1.3", + "memory-fs": "^0.4.1", + "minimist": "^1.2.0", + "request": "^2.81.0", + "tar": "^2.2.1", + "webpack": "^2.3.3" }, "devDependencies": { - "mocha": "^2.4.5", - "mocha-circleci-reporter": "0.0.1" + "eslint": "^3.19.0" } } diff --git a/src/artifacts.js b/src/artifacts.js new file mode 100644 index 0000000..1bb528e --- /dev/null +++ b/src/artifacts.js @@ -0,0 +1,58 @@ +const + { join, dirname } = require('path'), + { readdir, stat, readFile, unlink, writeFile } = require('fs'), + { promisify, map, coroutine } = require('bluebird'), + mkdirpAsync = promisify(require('mkdirp')), + statAsync = promisify(stat), + unlinkAsync = promisify(unlink), + readdirAsync = promisify(readdir), + writeAnyFileAsync = promisify(writeFile), + readFileAsync = promisify(readFile), + readDirAsync = (dir) => { + return readdirAsync(dir).map((file) => { + const path = join(dir, file) + return statAsync(path).then(s => s.isDirectory() ? readDirAsync(path) : path) + }).reduce((a, b) => a.concat(b), []) + }, + maybeReadFileContents = (file) => { + return readFileAsync(file, 'utf-8') + .catch(e => { + if (e.code === 'ENOENT') { + return '' + } + throw e + }) + } + +/** + * The artifacts step is where source patches are committed, or written as "artifacts" + * Steps: + * - A temporary directory is created in the downloaded source + * - On start, any files in that directory are restored into the source tree + * - After the patch functions have run, the temporary directory is emptied + * - Original versions of sources to be patched are written to the temporary directory + * - Finally, The patched files are written into source. + * + */ +module.exports.artifacts = function* artifacts ({ files, writeFileAsync, src }, next) { + const temp = join(src, 'nexe') + yield mkdirpAsync(temp) + const tmpFiles = yield readDirAsync(temp) + + yield map(tmpFiles, coroutine(function* (path) { + return writeFileAsync(path.replace(temp, ''), yield readFileAsync(path)) + })) + + yield next() + + yield map(tmpFiles, x => unlinkAsync(x)) + return map(files, coroutine(function* (file) { + const sourceFile = join(src, file.filename), + tempFile = join(temp, file.filename), + fileContents = yield maybeReadFileContents(sourceFile) + + yield mkdirpAsync(dirname(tempFile)) + yield writeAnyFileAsync(tempFile, fileContents) + yield writeFileAsync(file.filename, file.contents) + })) +} diff --git a/src/bundle.js b/src/bundle.js new file mode 100644 index 0000000..b3d31c9 --- /dev/null +++ b/src/bundle.js @@ -0,0 +1,46 @@ +const Mfs = require('memory-fs'), + webpack = require('webpack'), + { fromCallback } = require('bluebird'), + { resolve, join, relative } = require('path'), + isObject = (x) => typeof x === 'object', + isString = (x) => typeof x === 'string' + +function* bundle (compiler, next) { + let bundleConfig = compiler.options.bundle + if (!bundleConfig) { + return next() + } + + const + input = compiler.options.input || require.resolve(process.cwd()), + fs = new Mfs(), + path = resolve('nexe'), + filename = 'virtual-bundle.js' + + if (isString(bundleConfig)) { + bundleConfig === require(relative(process.cwd(), bundleConfig)) + } else if (!isObject(bundleConfig)) { + bundleConfig = { + entry: resolve(input), + target: 'node', + output: { path, filename } + } + } + + const bundler = webpack(bundleConfig) + bundler.outputFileSystem = fs + const stats = yield fromCallback(cb => bundler.run(cb)) + + if (stats.hasErrors()) { + compiler.log.error(stats.toString()) + return null + } + //eslint-disable-next-line no-sync + compiler.input = fs.readFileSync( + join(bundleConfig.output.path, bundleConfig.output.filename) + ) + + return next() +} + +module.exports.bundle = bundle diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..f66b4cf --- /dev/null +++ b/src/cli.js @@ -0,0 +1,88 @@ +const + { normalize } = require('path'), + { Promise, promisify } = require('bluebird'), + { createWriteStream, readFile } = require('fs') + +const readFileAsync = promisify(readFile), + isWindows = process.platform === 'win32' + +function dequoteStdIn (input) { + input = input.trim() + if (input.startsWith('\'') && input.endsWith('\'') || + input.startsWith('"') && input.endsWith('"')) { + return input.slice(1).slice(0, -1) + } + return input +} + +function getStdIn () { + return new Promise((resolve) => { + const bundle = [] + process.stdin.setEncoding('utf-8') + process.stdin.on('data', x => bundle.push(x)) + process.stdin.once('end', () => + resolve(dequoteStdIn(Buffer.concat(bundle).toString())) + ) + process.stdin.resume() + }) +} + +/** + * The "cli" step detects whether the process is in a tty. If it is then the input is read into memory. + * Otherwise, it is buffered from stdin. If no input options are passed in the tty, the package.json#main file is used. + * After all the build steps have run, the output (the executable) is written to a file or piped to stdout. + * + * Configuration: + * - compiler.options.input - file path to the input bundle. + * - fallbacks: stdin, package.json#main + * - compiler.options.output - file path to the output executable. + * - fallbacks: stdout, nexe_ + epoch + ext + * + * @param {*} compiler + * @param {*} next + */ +module.exports.cli = function* cli (compiler, next) { + const input = compiler.options.input, + bundled = Boolean(compiler.input) + + if (bundled) { + yield next() + } else if (!input && !process.stdin.isTTY) { + compiler.log.verbose('Buffering stdin as main module...') + compiler.input = yield getStdIn() + } else if (input) { + compiler.log.verbose('Reading input as main module: ' + input) + compiler.input = yield readFileAsync(normalize(input)) + } else if (!compiler.options.empty) { + compiler.log.verbose('Resolving cwd as main module...') + compiler.input = yield readFileAsync(require.resolve(process.cwd())) + } + + if (!bundled) { + yield next() + } + + const deliverable = yield compiler.getDeliverableAsync() + + return new Promise((resolve, reject) => { + deliverable.once('error', reject) + + if (!compiler.options.output && !process.stdout.isTTY) { + compiler.log.verbose('Writing result to stdout...') + deliverable.pipe(process.stdout).once('error', reject) + resolve() + } else { + compiler.log.verbose('Writing result to file...') + const output = compiler.options.output || `${compiler.options.name}${isWindows ? '.exe' : ''}` + deliverable.pipe(createWriteStream(normalize(output))) + .once('error', reject) + .once('close', e => { + if (e) { + reject(e) + } else { + resolve(compiler.log.info('Executable written: ' + output)) + } + }) + } + }) +} diff --git a/src/compiler.js b/src/compiler.js new file mode 100644 index 0000000..aa1f978 --- /dev/null +++ b/src/compiler.js @@ -0,0 +1,94 @@ +const + { dirname, normalize, join } = require('path'), + { promisify, Promise, coroutine } = require('bluebird'), + { normalizeOptions } = require('./options'), + { readFile, writeFile, createReadStream } = require('fs'), + { spawn } = require('child_process'), + { logger } = require('./logger') + +const + isWindows = process.platform === 'win32', + isBsd = Boolean(~process.platform.indexOf('bsd')), + make = isWindows && 'vcbuild.bat' || + isBsd && 'gmake' || + 'make', + configure = isWindows ? 'configure' : './configure', + readFileAsync = promisify(readFile), + writeFileAsync = promisify(writeFile) + +module.exports.NexeCompiler = class NexeCompiler { + constructor (options) { + options = this.options = normalizeOptions(options) + logger.setLevel(options.loglevel) + this.log = logger + this.python = options.python + this.configure = options.configure + this.make = isWindows ? options.vcBuild : options.make + this.src = join(options.temp, options.version) + this.env = Object.assign({}, process.env) + this.files = [] + + this.readFileAsync = coroutine(function * (file) { + const cachedFile = this.files.find(x => normalize(x.filename) === normalize(file)) + if (cachedFile) { + return Promise.resolve(cachedFile) + } + this.files.push({ + filename: file, + contents: yield readFileAsync(join(this.src, file), 'utf-8').catch(e => { + if (e.code !== 'ENOENT') { + throw e + } + return '' + }) + }) + return this.readFileAsync(file) + }.bind(this)) + this.writeFileAsync = (file, contents) => writeFileAsync(join(this.src, file), contents) + } + + set python (pythonPath) { + if (!pythonPath) { + return + } + if (isWindows) { + this.env.PATH = this.env.PATH + ';' + normalize(dirname(pythonPath)) + } else { + this.env.PYTHON = pythonPath + } + } + + get _deliverableLocation () { + return isWindows + ? join(this.src, 'Release', 'node.exe') + : join(this.src, 'out', 'Release', 'node') + } + + _runBuildCommandAsync (command, args) { + return new Promise((resolve, reject) => { + spawn(command, args, { + cwd: this.src, + env: this.env, + stdio: 'ignore' + }) + .once('error', reject) + .once('close', resolve) + }) + } + + _configureAsync () { + return this._runBuildCommandAsync( + this.env.PYTHON || 'python', + [configure, ...this.configure] + ) + } + + buildAsync () { + return this._configureAsync() + .then(() => this._runBuildCommandAsync(make, this.make)) + } + + getDeliverableAsync () { + return Promise.resolve(createReadStream(this._deliverableLocation)) + } +} diff --git a/src/download.js b/src/download.js new file mode 100644 index 0000000..04d38b6 --- /dev/null +++ b/src/download.js @@ -0,0 +1,75 @@ +const + unZip = require('zlib').createGunzip, + unTar = require('tar').Extract, + request = require('request'), + { Promise, promisify } = require('bluebird'), + statAsync = promisify(require('fs').stat), + rimrafAsync = promisify(require('rimraf')) + +function progress (req, log, precision = 10) { + const logged = {} + let length = 0, + total = 1 + + return req.on('response', (res) => { + total = res.headers['content-length'] + }).on('data', (chunk) => { + length += chunk.length + const percentComplete = +(length / total * 100).toFixed() + if (percentComplete % precision === 0 && !logged[percentComplete]) { + logged[percentComplete] = true + log(percentComplete) + } + }) +} + +function fetchNodeSource (path, url, log) { + log.info('Downloading Node: ' + url) + return new Promise((resolve, reject) => { + progress(request.get(url), (pc) => { + log.verbose(`Downloading Node: ${pc}%...`) + if (pc === 100) { + log.info('Extracting Node...') + } + }).on('error', reject) + .pipe(unZip().on('error', reject)) + .pipe(unTar({ path, strip: 1 })) + .on('error', reject) + .on('end', () => resolve(log.info('Extracted to: ' + path))) + }) +} + +function cleanSrc (clean, src, log) { + if (clean === true) { + log.info('Removing source: ' + src) + return rimrafAsync(src).then(() => { + log.info('Source deleted.' + src) + }) + } + return Promise.resolve() +} + +/** + * Deletes (maybe) and downloads the node source to the configured temporary directory + * @param {*} compiler + * @param {*} next + */ +function download (compiler, next) { + const + { src, log } = compiler, + { version, sourceUrl, clean } = compiler.options, + url = sourceUrl || + `https://nodejs.org/dist/v${version}/node-v${version}.tar.gz` + + return cleanSrc(clean, src, log).then(() => statAsync(src).then( + x => !x.isDirectory() && fetchNodeSource(src, url, log), + e => { + if (e.code !== 'ENOENT') { + throw e + } + return fetchNodeSource(src, url, log) + } + )).then(next) +} + +module.exports.download = download diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000..a946997 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,35 @@ +const + EOL = require('os').EOL, + colors = require('chalk'), + isFn = f => typeof f === 'function', + logNoop = (x, cb) => { + isFn(cb) && process.nextTick(cb) + }, + safeCb = f => isFn(f) ? f : logNoop, + logLevels = [ + ['verbose', 'green'], + ['info', 'blue'], + ['error', 'red'] + ], + logger = logLevels.reduce((logMethods, info) => { + const [level, color] = info + logMethods[level] = function (output, cb) { + process.stderr.write(colors[color](`${EOL}[${level}]: ${output}`), safeCb(cb)) + } + return logMethods + }, { + setLevel (level) { + if (level === 'silent') { + return void logLevels.forEach(([x]) => logger[x] = logNoop) + } + process.on('beforeExit', () => { + process.stderr.write(EOL) + }) + if (level === 'info') { + logger.verbose = logNoop + } + return logger + } + }) + +module.exports.logger = logger diff --git a/src/nexe.js b/src/nexe.js new file mode 100644 index 0000000..f73c28c --- /dev/null +++ b/src/nexe.js @@ -0,0 +1,51 @@ +const + { compose } = require('app-builder'), + { bundle } = require('./bundle'), + { NexeCompiler } = require('./compiler'), + { options } = require('./options'), + { cli } = require('./cli'), + { download } = require('./download'), + { artifacts } = require('./artifacts'), + { patches } = require('./patches'), + { EOL } = require('os'), + { coroutine, Promise } = require('bluebird'), + { logger } = require('./logger') + +function wrap (fns) { + return [].concat(...fns).map(fn => { + return fn.constructor.name === 'GeneratorFunction' + ? coroutine(fn) : fn + }) +} + +function compile (compilerOptions, callback) { + const compiler = new NexeCompiler(compilerOptions) + + compiler.log.verbose('Compiler options:' + + EOL + JSON.stringify(compiler.options, null, 4) + ) + + const nexe = compose(wrap([ + bundle, + cli, + download, + (nexeCompiler, next) => { + return next().then(() => { + return nexeCompiler.buildAsync() + }) + }, + artifacts, + patches, + compiler.options.patches + ])) + return Promise.resolve(nexe(compiler)).asCallback(callback) +} + +module.exports.options = options +module.exports.compile = compile + +if (require.main === module || process.__nexe) { + compile(options).catch((e) => { + logger.error(e.stack, () => process.exit(1)) + }) +} diff --git a/src/options.js b/src/options.js new file mode 100644 index 0000000..b598b6c --- /dev/null +++ b/src/options.js @@ -0,0 +1,136 @@ +const + parseArgv = require('minimist'), + { basename, extname, join } = require('path'), + EOL = require('os').EOL, + padRight = (str, l) => { + while (str.length < l) { + str += ' ' + } + return str.substr(0, l) + }, + defaults = { + temp: process.env.NEXE_TEMP || join(process.cwd(), '.nexe'), + version: process.version.slice(1), + flags: [], + configure: [], + make: [], + vcBuild: ['nosign', 'release'], + quick: false, + bundle: true, + enableNodeCli: false, + verbose: false, + silent: false, + padding: 2, + patches: [] + }, + alias = { + i: 'input', + o: 'output', + t: 'temp', + n: 'name', + v: 'version', + p: 'python', + f: 'flag', + c: 'configure', + m: 'make', + vc: 'vcBuild', + q: 'quick', + s: 'snapshot', + b: 'bundle', + cli: 'enableNodeCli', + h: 'help', + l: 'loglevel' + }, + argv = parseArgv(process.argv, { alias, default: defaults }), + help = ` +nexe --help CLI OPTIONS + + -i --input =./index.js -- application entry point + -o --output =./nexe.exe -- path to output file + -t --temp =./.nexe -- nexe temp directory (3Gb+) ~ NEXE_TEMP + -n --name =nexe.js -- file name for error reporting at run time + -v --version =${padRight(process.version.slice(1), 23)}-- node version + -p --python =/path/to/python2 -- python executable + -f --flag ="--expose-gc" -- *v8 flags to include during compilation + -c --configure ="--with-dtrace" -- *pass arguments to the configure command + -m --make ="--loglevel" -- *pass arguments to the make command + -vc --vcBuild =x64 -- *pass arguments to vcbuild.bat + -s --snapshot =/path/to/snapshot -- build with warmup snapshot + -b --bundle -- attempt bundling application + --ico -- file name for alternate icon file (windows) + --rc-* -- populate rc file options (windows) + --clean -- force download of sources + --enableNodeCli -- enable node cli enforcement (blocks app cli) + --sourceUrl -- pass an alternate source (node.tar.gz) url + --silent -- disable logging + --verbose -- set logging to verbose + + + -* variable key name * option can be used more than once + + TODO -q --quick =win32-x64-X.X.X -- use prebuilt binary (url, key or path) + TODO --resource-* =/path/to/resource -- *embed file bytes within the binary +`.trim() + +function flattenFilter (...args) { + return [].concat(...args).filter(x => x) +} + +/** + * Extract keys such as { "rc-CompanyName": "Node.js" } to + * { CompanyName: "Node.js" } + * @param {*} match + * @param {*} options + */ +function extractCliMap (match, options) { + Object.keys(options).filter(x => match.test(x)) + .reduce((map, option) => { + const key = option.split('-')[1] + map = map || {} + map[key] = options[option] + delete options[option] + return map + }, null) +} + +function extractName (options) { + if (typeof options.input === 'string') { + return options.name + || basename(options.input).replace(extname(options.input), '') + } + const mainName = basename(require.resolve(process.cwd())) + return options.name || mainName.replace(extname(mainName), '') +} + +function normalizeOptions (input) { + if (input.__normalized) { + return input + } + + if (argv.help || Boolean(argv._.find(x => x === 'version'))) { + process.stderr.write( + argv.help ? help : 'next' + EOL, + () => process.exit(0) + ) + } + + const options = Object.assign({}, defaults, input) + delete options._ + delete alias.rc + options.loglevel = options.loglevel + || options.silent && 'silent' + || options.verbose && 'verbose' || 'info' + options.name = extractName(options) + options.flags = flattenFilter(options.flag, options.flags) + options.make = flattenFilter(options.make) + options.vcBuild = flattenFilter(options.vcBuild) + options.rc = options.rc || extractCliMap(/^rc-.*/, options) + options.resources = options.resources || extractCliMap(/^resource-.*/, options) + Object.keys(alias).forEach(x => delete options[x]) + + options.__normalized = true + return options +} + +module.exports.normalizeOptions = normalizeOptions +module.exports.options = normalizeOptions(argv) diff --git a/src/patches/content.js b/src/patches/content.js new file mode 100644 index 0000000..b4733ed --- /dev/null +++ b/src/patches/content.js @@ -0,0 +1,27 @@ +const Buffer = require('buffer').Buffer + +function* content (compiler, next) { + yield next() + + const filename = 'lib/' + compiler.options.name + '.js', + file = yield compiler.readFileAsync(filename), + header = '/' + '*'.repeat(19) + 'nexe_', + end = '_' + '*'.repeat(19) + '/', + padding = Array(compiler.options.padding * 1000000).fill('*').join('') + + if (!padding) { + file.contents = compiler.input + return + } + + file.contents = [ + compiler.input, + '/', + padding, + '/' + ].join('') + + file.contents = header + Buffer.byteLength(file.contents) + end + file.contents +} + +module.exports.content = content diff --git a/src/patches/disable-node-cli.js b/src/patches/disable-node-cli.js new file mode 100644 index 0000000..4382486 --- /dev/null +++ b/src/patches/disable-node-cli.js @@ -0,0 +1,17 @@ +function * disableNodeCli (compiler, next) { + if (compiler.options.enableNodeCli) { + return next() + } + + const nodecc = yield compiler.readFileAsync('src/node.cc'), + nodeccMarker = "argv[index][0] == '-'" + + nodecc.contents = nodecc.contents.replace( + nodeccMarker, + nodeccMarker.replace('-', ']') + ) + + return next() +} + +module.exports.disableNodeCli = disableNodeCli diff --git a/src/patches/flags.js b/src/patches/flags.js new file mode 100644 index 0000000..6145f99 --- /dev/null +++ b/src/patches/flags.js @@ -0,0 +1,14 @@ +module.exports.flags = function* flags (compiler, next) { + const nodeflags = compiler.options.flags + if (!nodeflags.length) { + return next() + } + + const nodegyp = yield compiler.readFileAsync('node.gyp') + + nodegyp.contents = nodegyp.contents.replace( + "'node_v8_options%': ''", + `'node_v8_options%': '${nodeflags.join(' ')}'`) + + return next() +} diff --git a/src/patches/gyp.js b/src/patches/gyp.js new file mode 100644 index 0000000..a6bcfb0 --- /dev/null +++ b/src/patches/gyp.js @@ -0,0 +1,15 @@ +module.exports.nodeGyp = function * nodeGyp ({ files, readFileAsync, download }, next) { + yield next() + + const nodegyp = yield readFileAsync('node.gyp'), + nodeGypMarker = "'lib/fs.js'," + + nodegyp.contents = nodegyp.contents + .replace(nodeGypMarker, ` + ${nodeGypMarker} + ${files + .filter(x => x.filename.startsWith('lib')) + .map(x => `'${x.filename}'`) + .toString()}, + `.trim()) +} diff --git a/src/patches/ico.js b/src/patches/ico.js new file mode 100644 index 0000000..2bfa440 --- /dev/null +++ b/src/patches/ico.js @@ -0,0 +1,16 @@ +const + { readFile } = require('fs'), + { normalize } = require('path'), + { promisify } = require('bluebird') + +const readFileAsync = promisify(readFile) + +module.exports.ico = function * ico (compiler, next) { + const iconFile = compiler.options.ico + if (!iconFile) { + return next() + } + const file = yield compiler.readFileAsync('src/res/node.ico') + file.contents = yield readFileAsync(normalize(iconFile)) + return next() +} diff --git a/src/patches/index.js b/src/patches/index.js new file mode 100644 index 0000000..3f16ac5 --- /dev/null +++ b/src/patches/index.js @@ -0,0 +1,9 @@ +module.exports.patches = [ + require('./gyp').nodeGyp, //upstream, sets manifest + require('./content').content, //upstream, sets main module content + require('./third-party-main').main, //loads main module + require('./disable-node-cli').disableNodeCli, + require('./flags').flags, + require('./ico').ico, + require('./node-rc').nodeRc +] diff --git a/src/patches/node-rc.js b/src/patches/node-rc.js new file mode 100644 index 0000000..bc8993f --- /dev/null +++ b/src/patches/node-rc.js @@ -0,0 +1,21 @@ +function* nodeRc (compiler, next) { + const options = compiler.options.rc + if (!options) { + return next() + } + + const file = yield compiler.readFileAsync('src/res/node.rc') + + Object.keys(options).forEach((key) => { + const value = options[key], + isVar = /^[A-Z_]+$/.test(value) + + value = isVar ? value : `"${value}"` + file.contents.replace(new RegExp(`VALUE "${key}",*`), `VALUE "${key}", ${value}`) + // TODO support keys that are not present? + }) + + return next() +} + +module.exports.nodeRc = nodeRc diff --git a/src/patches/snapshot.js b/src/patches/snapshot.js new file mode 100644 index 0000000..d0e1f1f --- /dev/null +++ b/src/patches/snapshot.js @@ -0,0 +1,15 @@ +module.exports.snapshot = function* snapshot (compiler, next) { + const snapshotFile = compiler.options.snapshot + + if (!snapshotFile) { + return next() + } + + const file = yield compiler.readFileAsync('configure') + file.contents = file.contents + .replace( + 'def configure_v8(o):', + `def configure_v8(o):\n o['variables']['embed_script'] = '${snapshotFile}'\n o['variables']['warmup_script'] = '${snapshotFile}'` + ) + return next() +} diff --git a/src/patches/third-party-main.js b/src/patches/third-party-main.js new file mode 100644 index 0000000..c06318a --- /dev/null +++ b/src/patches/third-party-main.js @@ -0,0 +1,21 @@ +function * main (compiler, next) { + const mainFile = yield compiler.readFileAsync('lib/_third_party_main.js') + mainFile.contents = ` + Object.defineProperty(process, '__nexe', { + value: true, + enumerable: false, + configurable: false + }); + require("${compiler.options.name}"); + `.trim() + + if (compiler.options.empty === true) { + compiler.options.resources.length = 0 + compiler.input = 'console.log(`nexe-${process.platform}-${process.arch}-${process.version}`)' + return next() + } + + return next() +} + +module.exports.main = main