Compare commits

..

8 Commits

Author SHA1 Message Date
calebboyd 34c51064f4 feat: build-target options 2017-09-01 00:28:10 -05:00
calebboyd be48e5a2f3 docs: update readme with new usage 2017-08-31 13:59:01 -05:00
calebboyd 0340c01b36 fix: don't call next twice 2017-08-31 10:02:12 -05:00
calebboyd e62cd19b42 feat: targets 2017-08-31 08:46:50 -05:00
calebboyd fc99386199 chore: options, and resource normalization 2017-08-31 01:15:51 -05:00
calebboyd f949bf3040 feat: initial build 2017-08-30 15:32:26 -05:00
calebboyd 0a1e747ca3 chore: inject source-map-support 2017-08-26 23:34:03 -05:00
calebboyd 91aa014b7d feat: initial fuse-box bundling support 2017-08-25 23:23:57 -05:00
51 changed files with 4477 additions and 6192 deletions
-2
View File
@@ -4,8 +4,6 @@ coverage
node_modules
*.log
*.exe
*.bin
!winsw.exe
.idea
.nexe
lib
+7 -12
View File
@@ -1,12 +1,7 @@
language: node_js
before_install:
- sudo apt-get -qq update
- sudo apt-get install -y g++-multilib
node_js:
- '6'
script: npm run asset-compile
env:
global:
- secure: LyC6djYqPfPKz5gHILES9JmSRxQ+ntKOcoaZNM5WID0+gogUoXZBhcmmtvq9RnMpFZN3pYujY6LB5iy7QS2seGjg+feyewNKI30yIK6N9ulJyZZKmrJVDdRx4wvl8YVTkttjcJFkanDGa6zRvFfFDKx/iIdcWLEpnqR/WO1ppf8=
notifications:
email: false
language: node_js
node_js:
- '6'
script: npm run nexe-build
env:
global:
secure: LyC6djYqPfPKz5gHILES9JmSRxQ+ntKOcoaZNM5WID0+gogUoXZBhcmmtvq9RnMpFZN3pYujY6LB5iy7QS2seGjg+feyewNKI30yIK6N9ulJyZZKmrJVDdRx4wvl8YVTkttjcJFkanDGa6zRvFfFDKx/iIdcWLEpnqR/WO1ppf8=
-1
View File
@@ -3,7 +3,6 @@
"prettier.semi": false,
"prettier.singleQuote": true,
"prettier.typescriptEnable": ["typescript"],
"editor.trimAutoWhitespace": true,
"files.exclude": {
"**/.git": true,
"**/.svn": true,
+46 -92
View File
@@ -5,18 +5,18 @@
<a href="https://www.npmjs.com/package/nexe"><img src="https://img.shields.io/npm/dt/nexe.svg" alt="Downloads"></a>
<a href="https://www.npmjs.com/package/nexe"><img src="https://img.shields.io/npm/v/nexe.svg" alt="Version"></a>
<a href="https://www.npmjs.com/package/nexe"><img src="https://img.shields.io/npm/l/nexe.svg" alt="License"></a>
<a href="https://gitter.im/nexe/nexe?utm_source=badge&utm_medium=badge"><img src="https://badges.gitter.im/nexe/nexe.svg"></a>
</p>
<p align="center"><code>npm i nexe -g</code></p>
<p align="center"><code>npm i nexe@beta -g</code></p>
<p align="center">Nexe is a command-line utility that compiles your Node.js application into a single executable file.</p>
<p align="center">
<img src="https://user-images.githubusercontent.com/5818726/30999006-df7e0ae0-a497-11e7-96db-9ce87ae67b34.gif"/>
<img src="https://cloud.githubusercontent.com/assets/5818726/26533446/ce19ee5a-43de-11e7-9540-caf7ebd93370.gif"/>
</p>
## Motivation and Features
- Supports production ready builds
- Self contained applications
- Ability to run multiple applications with *different* node.js runtimes.
- Distribute binaries without needing node / npm.
@@ -28,42 +28,40 @@
## Usage
- Application entrypoint:
- Existing application bundle:
`nexe my-app.js`
`nexe my-app-bundle.js -o my-app`
- stdin interface
`rollup -c | nexe --resource "./public/**/*" -o my-app.exe`
`rollup -c | nexe --resource ./public/**/* -o my-app.exe`
For more CLI options see: `nexe --help`
### Examples
## Including Additional Resources
- `nexe server.js -r public/**/*.html`
- `nexe my-bundle.js --no-bundle -o app.exe`
- `nexe --build`
- `nexe -t x86-8.0.0`
## Resources
Additional files or resources can be added to the binary by passing `-r "glob/pattern/**/*"`. These included files can be read in the application by using `fs.readFile` or `fs.readFileSync`.
Additional resources can be added to the binary by passing `-r glob/pattern/**/*`. These included files can be read in the application by using `fs.readFile` or `fs.readFileSync`
## Compiling Node
By default `nexe` will attempt to download a pre-built executable. However, It may be unavailable ([github releases](https://github.com/nexe/nexe/releases))
or you may want to customize what is built. See `nexe --help` for a list of options available when passing the [`--build`](#build-boolean) option. You will also need to ensure your environment is setup to [build node](https://github.com/nodejs/node/blob/master/BUILDING.md). Note: the `python` binary in your path should be an acceptable version of python 2. eg. Systems that have python2 will need to create a [symlink](https://github.com/nexe/nexe/issues/354#issuecomment-319874486).
By default `nexe` will attempt to download a pre-built executable. However, some users may want to customize the way node is built, either by changing the flags, providing a different icon, or different executable details. To build node, use the `build` option/flag
Nexe also exposes its patching pipeline to the user. This allows the application of simple patching of node sources prior to compilation.
## Node.js API
Using Nexe Programatically
#### Example
```javascript
const { compile } = require('nexe')
compile({
input: './my-app.js',
build: true, //required to use patches
output: './my-app.exe',
build: true, //builds node
patches: [
async (compiler, next) => {
await compiler.setFileContentsAsync(
@@ -77,65 +75,60 @@ compile({
console.log('success')
})
```
## NexeOptions
### `options: object`
### `options`
- #### `build: boolean`
- Build node from source (required in beta)
- #### `input: string`
- Input bundle file path
- default: stdin or the current directory's main file (package.json)
- #### `output: string`
- Output executable file path
- default: same as `name` with an OS specific extension.
- #### `target: string | object`
- An object or string describing platform-arch-version. e.g. `'windows-ia32-6.10.3'`
- each segment is optional, and will be merged with the current environment
- Examples: ([full list](https://github.com/nexe/nexe/releases))
- `'win32-x86-8.6.0`
- `{ platform: 'alpine' }`
- `darwin-8.6.0`
- `linux-x64`
- `macos-8.4.0`
See [test/target.spec.ts](test/target.spec.ts)
- If the [`build`](#build-boolean) flag is set, the platform portion of the target is ignored.
- default: `process`
- #### `target: string`
- Dash seperated platform-architecture-version. e.g. `'win32-ia32-6.10.3'`
- default: `[process.platform, process.arch, process.version.slice(1)].join('-')`
- #### `bundle: string | boolean`
- If a string is provided it must be a valid relative module path
and should provide an export with the following signature:
```typescript
export function createBundle (options: NexeOptions): Promise<string>
export function createBundle (
filename: string,
options: { name: string, minify: any, cwd: string }
): Promise<string>`
```
- default: true
- default: true, uses the internal fuse-box configuration
- #### `name: string`
- Module friendly name of the application
- default: basename of the input file, or `nexe_${Date.now()}`
- #### `cwd: string`
- Directory nexe will operate on as though it is the cwd
- default: process.cwd()
- #### `build: boolean`
- Build node from source, passing this flag tells nexe to download and build from source. Subsequently using this flag will cause nexe to use the previously built binary. To rebuild, first add [`--clean`](#clean-boolean)
- #### `version: string`
- The Node version you're building for
- default: `process.version.slice(1)`
- #### `python: string`
- On Linux this is the path pointing to your python2 executable
- On Windows this is the directory where `python` can be accessed
- default: `null`
- #### `flags: string[]`
- #### `flags: Array<string>`
- Array of node runtime flags to build node with.
- Example: `['--expose-gc']`
- default: `[]`
- #### `configure: string[]`
- #### `configure: Array<string>`
- Array of arguments for the node build configure step
- Example: `['--with-dtrace', '--dest-cpu=x64']`
- default: `[]`
- #### `make: string[]`
- #### `make: Array<string>`
- Array of arguments for the node build make step, on windows this step recieves options for vcBuild.bat
- default: `[]` or `['nosign', 'release']` for non windows systems
- #### `vcBuild: string[]`
- #### `make: Array<string>`
- Alias for `make` option
- #### `snapshot: string`
- path to a file to be used as the warmup snapshot for the build
- default: `null`
- #### `resources: string[]`
- #### `resources: Array<string>`
- Array of globs with files to include in the build
- Example: `['./public/**/*']`
- default: `[]`
@@ -147,45 +140,27 @@ compile({
- Path to a user provided icon to be used (Windows only).
- #### `rc: object`
- Settings for patching the [node.rc](https://github.com/nodejs/node/blob/master/src/res/node.rc) configuration file (Windows only).
- Example:
```javascript
{
CompanyName: "ACME Corp",
PRODUCTVERSION: "17,3,0,0",
FILEVERSION: "1,2,3,4"
...
}
```
- Example: `{ CompanyName: "ACME Corp" }`
- default: `{}`
- #### `clean: boolean`
- If included, nexe will remove temporary files for the accompanying configuration and exit
- If included, nexe will remove temporary files for accompanying configuration and exit
- #### `enableNodeCli: boolean`
- Enable the original Node CLI (will prevent application cli from working)
- default: `false`
- #### `fakeArgv: boolean`
- fake the entry point file name (`process.argv[1]`). If nexe was used with stdin this will be `'[stdin]'`.
- #### `ghToken: string`
- Provide a Github Token for accessing nexe releases
- This is usually needed in CI environments
- default: `process.env.GITHUB_TOKEN`
- #### `sourceUrl: string`
- Provide an alternate url for the node source code
- Note: temporary files will still be created for this under the specified version
- Provide an alternate url for the node source. Should be a `.tar.gz`
- #### `loglevel: string`
- Set the loglevel, info, silent, or verbose
- default: `'info'`
- #### `patches: NexePatch[]`
- #### `patches: Array<NexePatch>`
- Userland patches for patching or modifying node source
- default: `[]`
- #### `plugins: NexePatch[]`
- Userland plugins for modifying nexe executable behavior
- default: `[]`
### `NexePatch: (compiler: NexeCompiler, next: () => Promise<void>) => Promise<void>`
Patches and Plugins are just a middleware functions that take two arguments, the `compiler`, and `next`. The compiler is described below, and `next` ensures that the pipeline continues. Its invocation should always be awaited or returned to ensure correct behavior. Patches also require that [`--build`](#build-boolean) be set, while plugins do not.
A patch is just a middleware function that takes two arguments, the `compiler`, and `next`. The compiler is described below, and `next` ensures that the pipeline continues. Its invocation should always be awaited or returned to ensure correct behavior.
For examples, see the built in patches: [src/patches](src/patches).
For examples, see the built in patches: [src/patches](src/patches)
### `NexeCompiler`
@@ -195,9 +170,7 @@ For examples, see the built in patches: [src/patches](src/patches).
- Quickly perform a replace in a file within the downloaded Node.js source. The rest arguments are passed along to `String.prototype.replace`
- `readFileAsync(filename: string): Promise<NexeFile>`
- Access (or create) a file within the downloaded Node.js source.
- `addResource(filename: string, contents: Buffer): void`
- Add a resource to the nexe bundle
- `files: NexeFile[]`
- `files: Array<NexeFile>`
- The cache of the currently read, modified, or created files within the downloaded Node.js source.
#### `NexeFile`
@@ -207,31 +180,12 @@ For examples, see the built in patches: [src/patches](src/patches).
Any modifications made to `NexeFile#contents` will be maintained in the cache _without_ the need to explicitly write them back out, e.g. using `NexeCompiler#setFileContentsAsync`.
## Native Modules
### Native Modules
Any `.node` binding can be used with nexe. These library files will be bundled and written alongside the resulting binary at runtime. Currently, nexe supports modules loaded directly (`.node` files) and those loaded with the `'bindings'` module. It does not yet support modules using `node-pre-gyp#find`.
Nexe has a plugin built for use with [fuse-box](http://fuse-box.org) > 2.2.1. This plugin currently supports modules that require `.node` files and those that use the `bindings` module.
Take a look at the [example](examples/native-build/build.js)
Its important to note: unless your native module conditionally loads each platform binary. Nexe builds with native modules will be platform specific. Eg. You will no longer be able to use cross platform builds.
Nexe builds with native modules will need to target the same version, platform and architecture as the platform hosting the module. At least until [N-API](https://github.com/nodejs/abi-stable-node) is fully propegated
TODO
- [ ] Implement support for `node-pre-gyp#find`.
## Contributing
Building
```
$ git clone git@github.com:nexe/nexe.git
$ cd nexe
$ yarn
```
Testing
```
$ npm test
```
- [ ] Implement support `node-pre-gyp#find`.
## Maintainers
+5 -2
View File
@@ -5,13 +5,16 @@ environment:
secure: tR7HXYv3x97q90uvPrjUxKZh7R1+uqKct0HdEcqTbsg=
GITHUB_TOKEN:
secure: 3wuEGJsppSKFvdwhp3jprUA9Zkc3WINlc/9oFXjhAR6J05lYMXP7Fl7lXTKpkGZx
branches:
except:
- master
install:
- ps: Install-Product node 6
- yarn
- npm i
build: off
test: off
build_script:
- yarn run asset-compile
- npm run nexe-build
artifacts:
- path: .nexe\**\node.exe
cache:
+1 -7
View File
@@ -1,9 +1,3 @@
dependencies:
override:
- export NVS_HOME="$HOME/.nvs" && git clone https://github.com/jasongin/nvs "$NVS_HOME" && . "$NVS_HOME/nvs.sh" install
- nvs add lts && nvs link lts && nvs use default
- npm i yarn -g
- yarn
test:
override:
- yarn run asset-compile
- npm run nexe-build
+21 -12
View File
@@ -1,13 +1,22 @@
const nexe = require('../..')
nexe.compile({
output: 'native-build',
silent: true,
native: {
zmq: {
additionalFiles: [
'../../windows/lib/x64/libzmq-v100-mt-4_0_4.dll'
]
}
}
const { FuseBox } = require('fuse-box')
const { NativeModulePlugin } = require('../../lib/bundling')
const fuse = FuseBox.init({
homeDir: './',
cache: false,
log: true,
debug: true,
output: '$name.js',
plugins: [
new NativeModulePlugin({
zmq: {
additionalFiles: [
'../../windows/lib/x64/libzmq-v100-mt-4_0_4.dll'
]
}
})
]
})
fuse.bundle('app')
.target('server')
.instructions(`> index.js`)
fuse.run()
+1
View File
@@ -8,6 +8,7 @@ setInterval(function(){
console.log('sending a multipart message envelope')
pub.send(['kitty cats', 'meow!'])
}, 2500)
console.log('global', global.require.main)
var sub = zmq.socket('sub')
sub.connect('tcp://127.0.0.1:3000')
sub.subscribe('kitty cats')
+5 -3
View File
@@ -4,13 +4,15 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "node build"
"bundle": "node build",
"build-windows": "nexe -i app.js -o app.exe"
},
"author": "",
"license": "ISC",
"dependencies": {
"sqlite3": "^3.1.10",
"zmq": "^2.15.3"
},
"devDependencies": {}
"devDependencies": {
"fuse-box": "^2.2.1-beta.7"
}
}
+7 -16
View File
@@ -1,19 +1,10 @@
#!/usr/bin/env node
const options = require('./lib/options')
const nexe = require('./lib/nexe')
module.exports = nexe
if (require.main === module) {
//fast path for help/version
const argv = options.argv
const eol = require('os').EOL
const showHelp = argv.help || argv._.some(x => x === 'help')
const showVersion = argv.version || argv._.some(x => x === 'version')
if (showHelp || showVersion) {
process.stderr.write(showHelp ? options.help : options.version + eol)
} else {
const nexe = require('./lib/nexe')
nexe.compile(argv).catch((e) => {
process.stderr.write(eol + e.stack)
})
}
} else {
module.exports = require('./lib/nexe')
nexe.compile(nexe.argv).catch((e) => {
process.stderr.write(e.stack, () => process.exit(1))
})
}
+3830
View File
File diff suppressed because it is too large Load Diff
+11 -17
View File
@@ -2,21 +2,18 @@
"name": "nexe",
"description": "Create a single executable out of your Node.js application",
"license": "MIT",
"version": "2.0.0-rc.21",
"version": "2.0.0-rc.1",
"contributors": [
"Craig Condon <craig.j.condon@gmail.com> (http://crcn.io)",
"Jared Allard <jaredallard@outlook.com>",
"Caleb Boyd <caleb.boyd@hotmail.com>"
],
"scripts": {
"asset-compile": "ts-node tasks/asset-compile",
"test": "mocha",
"fmt": "prettier --parser typescript --no-semi --print-width 100 --single-quote --write \"{src,plugins,tasks}/**/*.ts\"",
"prepare": "npm run build",
"prepublishOnly": "npm test",
"prebuild": "rimraf lib && npm run fmt",
"build": "tsc --declaration",
"postbuild": "ts-node tasks/post-build"
"nexe-build": "ts-node tasks/build",
"prebuild": "rimraf lib && npm run lint",
"prepublish": "npm run build",
"lint": "prettier --parser typescript --no-semi --print-width 100 --single-quote --write \"src/**/*.ts\"",
"build": "tsc --declaration"
},
"repository": {
"type": "git",
@@ -26,7 +23,6 @@
"lib",
"source-map-support.js"
],
"typings": "lib/nexe.d.ts",
"main": "index.js",
"bin": {
"nexe": "index.js"
@@ -38,7 +34,7 @@
"app-builder": "^5.1.0",
"chalk": "^1.1.3",
"download": "^6.2.0",
"fuse-box": "^2.3.4-beta.11",
"fuse-box": "^2.2.1",
"globby": "^6.1.0",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
@@ -48,20 +44,18 @@
"uglify-js": "3.0.28"
},
"devDependencies": {
"@types/chai": "^4.0.4",
"@types/bluebird": "^3.5.8",
"@types/chalk": "^0.4.31",
"@types/execa": "^0.7.0",
"@types/execa": "0.7.0",
"@types/globby": "^0.6.0",
"@types/minimist": "^1.2.0",
"@types/mkdirp": "^0.3.29",
"@types/mocha": "^2.2.42",
"@types/ora": "^0.3.31",
"@types/pify": "0.0.28",
"@types/rimraf": "0.0.28",
"chai": "^4.1.2",
"execa": "^0.8.0",
"execa": "0.8.0",
"got": "^7.1.0",
"mocha": "^3.5.0",
"mocha": "^3.2.0",
"prettier": "^1.6.1",
"ts-node": "3.3.0",
"typescript": "^2.4.1"
-24
View File
@@ -1,24 +0,0 @@
{
"name": "nexe-daemon",
"version": "1.0.0",
"description": "Plugin for nexe to create installable services",
"main": "lib/index.js",
"scripts": {
"clean": "rimraf *.js",
"build": "tsc",
"postpublish": "npm run clean"
},
"files": [
"lib",
"winsw.exe"
],
"author": "Caleb Boyd",
"license": "MIT",
"devDependencies": {
"rimraf": "^2.6.2",
"typescript": "^2.5.2"
},
"dependencies": {
"nexe": "^2.0.0-rc.11"
}
}
-48
View File
@@ -1,48 +0,0 @@
import { NexeCompiler, NexeOptions } from 'nexe'
import { readFileSync } from 'fs'
import { join } from 'path'
interface NexeDaemonOptions {
id: string
name: string
description: string
executable: string
}
type DaemonOptions = NexeOptions & { daemon: { windows: NexeDaemonOptions } }
function renderWinswConfig(options: any) {
return (
'<configuration>\r\n' +
`${Object.keys(options).reduce((config: string, element: string) => {
return (config += `<${element}>${options[element]}</${element}>\r\n`)
}, '')}</configuration>\r\n`
)
}
export default function daemon(compiler: NexeCompiler<DaemonOptions>, next: () => Promise<void>) {
if (compiler.target.platform !== 'windows') {
return next()
}
compiler.addResource(
'./nexe/plugin/daemon/winsw.exe',
readFileSync(require.resolve('../winsw.exe'))
)
const name = compiler.options.name,
options = (compiler.options.daemon && compiler.options.daemon.windows) || {},
defaults: NexeDaemonOptions = {
id: name,
name,
description: name,
executable: '%BASE%\\' + compiler.output
}
compiler.addResource(
'./nexe/plugin/daemon/winsw.xml',
Buffer.from(renderWinswConfig(Object.assign(defaults, options)))
)
compiler.addResource('./nexe/plugin/daemon/app.js', Buffer.from(compiler.input))
compiler.input = '{{replace:plugins/nexe-daemon/lib/nexe-daemon.js}}'
return next()
}
-66
View File
@@ -1,66 +0,0 @@
const installCommands = ['-i', '--install']
const installIndex = process.argv.findIndex(x => installCommands.includes(x))
import * as path from 'path'
import * as fs from 'fs'
import * as cp from 'child_process'
function mkdirp(r: string, t?: any): any {
;(t = t || null), (r = path.resolve(r))
try {
fs.mkdirSync(r), (t = t || r)
} catch (c) {
if ('ENOENT' === c.code) (t = mkdirp(path.dirname(r), t)), mkdirp(r, t)
else {
var i
try {
i = fs.statSync(r)
} catch (r) {
throw c
}
if (!i.isDirectory()) throw c
}
}
return t
}
if (~installIndex) {
console.log('DIRECTORY FOUND')
const directory = process.argv[installIndex + 1]
if (directory) {
mkdirp(path.dirname(directory))
const filename = path.join(directory, path.basename(process.execPath))
const readStream = fs.createReadStream(process.execPath)
const writeStream = fs.createWriteStream(filename)
const onError = function() {
readStream.removeAllListeners()
writeStream.removeAllListeners()
//TODO
}
readStream
.on('error', onError)
.pipe(writeStream)
.on('error', onError)
.on('close', () => {
const winsw = filename.replace('.exe', '-service.exe')
fs.writeFileSync(winsw, fs.readFileSync('./nexe/plugin/daemon/winsw.exe'))
fs.writeFileSync(
filename.replace('.exe', '-service.xml'),
fs.readFileSync('./nexe/plugin/daemon/winsw.xml')
)
installService(winsw)
})
}
} else {
console.log('DIRECTORY NOT FOUND')
require('./nexe/plugin/daemon/app.js')
}
function installService(filename: string) {
cp.exec(filename + ' install', error => {
if (error && !error.message.includes('already exists')) {
throw error
}
})
}
-13
View File
@@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [ "es2016" ],
"outDir": "lib",
"strict": true
},
"include": [
"src/**/*.ts"
],
"exclude": []
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
+73 -4
View File
@@ -3,6 +3,75 @@ import * as path from 'path'
import * as child from 'child_process'
import { createHash } from 'crypto'
export interface ExtractNodeModuleOptions {
[key: string]: { additionalFiles: string[] } | true
}
function hashName(name: string | Buffer) {
return createHash('md5')
.update(name)
.digest('hex')
.toString()
.slice(0, 8)
}
export function embedDotNode(
options: ExtractNodeModuleOptions,
file: { contents: string; absPath: string }
) {
const contents = fs.readFileSync(file.absPath)
const module = Object.keys(options).find(x =>
Boolean(~file.absPath.indexOf(path.join('node_modules', x)))
)!
const bindingName = path.basename(file.absPath)
const settings = options[module]
const moduleDir = hashName(contents)
file.contents = `
var fs=require('fs');var path=require('path');var binding='${contents.toString(
'base64'
)}';function mkdirp(r,t){t=t||null,r=path.resolve(r);try{fs.mkdirSync(r),t=t||r}catch(c){if("ENOENT"===c.code)t=mkdirp(path.dirname(r),t),mkdirp(r,t);else{var i;try{i=fs.statSync(r)}catch(r){throw c}if(!i.isDirectory())throw c}}return t};`
if (settings === true) {
file.contents += `
mkdirp('${moduleDir}');
var bindingPath = path.join(process.cwd(), '${moduleDir}', '${bindingName}')
require('fs').writeFileSync(bindingPath, Buffer.from(binding, 'base64'))
process.dlopen(module, bindingPath)
`.trim()
return
}
let depth = 0
settings.additionalFiles.forEach(file => {
let ownDepth = 0
file.split('/').forEach(x => x === '..' && ownDepth++)
depth = ownDepth > depth ? ownDepth : depth
})
let segments = [moduleDir]
while (depth--) {
segments.push(hashName(moduleDir + depth))
}
segments.push(bindingName)
file.contents += `
var cwd = process.cwd()
var bindingFileParts = ${JSON.stringify(segments)};
var bindingFile = path.join.apply(path, [cwd].concat(bindingFileParts));
mkdirp(path.dirname(bindingFile));
fs.writeFileSync(bindingFile, Buffer.from(binding, 'base64'));
${settings.additionalFiles.reduce((code, filename, i) => {
const contents = fs.readFileSync(path.join(path.dirname(file.absPath), filename))
return (code += `
var file${i} = '${contents.toString('base64')}';
var filePath${i} = path.join(cwd, bindingFileParts[0], '${filename.split('../').join('')}');
mkdirp(path.dirname(filePath${i}));
fs.writeFileSync(filePath${i}, Buffer.from(file${i}, 'base64'));
`)
}, '')};
process.dlopen(module, bindingFile)
`
}
function findNativeModulePath(filePath: string, bindingsArg: string) {
const dirname = path.dirname(filePath)
const tempFile = Math.random() * 100 + '.js'
@@ -36,13 +105,13 @@ export class BindingsRewrite {
public nativeModulePaths: string[] = []
public rewrite = false
isRequire(node: any, moduleName: string) {
return node.callee.name === 'require' && node.arguments[0].value === moduleName
isRequireBindings(node: any) {
return node.callee.name === 'require' && node.arguments[0].value === 'bindings'
}
onNode(absolutePath: string, node: any, parent: any) {
if (node.type === 'CallExpression') {
if (this.isRequire(node, 'bindings') && parent.type === 'VariableDeclarator') {
if (this.isRequireBindings(node) && parent.type === 'VariableDeclarator') {
/**
* const loadBindings = require('bindings');
* -> const loadBindings = String('');
@@ -54,7 +123,7 @@ export class BindingsRewrite {
return
}
if (this.isRequire(node, 'bindings') && parent.type === 'CallExpression') {
if (this.isRequireBindings(node) && parent.type === 'CallExpression') {
/**
*const bindings = require('bindings')('native-module')....
* -> const bindings = require('./path/to/native/module.node').....
-72
View File
@@ -1,72 +0,0 @@
import { createHash } from 'crypto'
import * as path from 'path'
import * as fs from 'fs'
export interface EmbedNodeModuleOptions {
[key: string]: { additionalFiles: string[] } | true
}
function hashName(name: string | Buffer) {
return createHash('md5')
.update(name)
.digest('hex')
.toString()
.slice(0, 8)
}
export function embedDotNode(
options: EmbedNodeModuleOptions,
file: { contents: string; absPath: string }
) {
const contents = fs.readFileSync(file.absPath)
const modulePathParts = file.absPath.split(path.sep).reverse()
const module = modulePathParts[modulePathParts.findIndex(x => x === 'node_modules') - 1]
const bindingName = path.basename(file.absPath)
const settings = options[module]
const moduleDir = hashName(contents)
file.contents = `
var fs=require('fs');var path=require('path');var binding='${contents.toString(
'base64'
)}';function mkdirp(r,t){t=t||null,r=path.resolve(r);try{fs.mkdirSync(r),t=t||r}catch(c){if("ENOENT"===c.code)t=mkdirp(path.dirname(r),t),mkdirp(r,t);else{var i;try{i=fs.statSync(r)}catch(r){throw c}if(!i.isDirectory())throw c}}return t};`
if (!settings || settings === true) {
file.contents += `
mkdirp('${moduleDir}');
var bindingPath = path.join(process.cwd(), '${moduleDir}', '${bindingName}')
require('fs').writeFileSync(bindingPath, Buffer.from(binding, 'base64'))
process.dlopen(module, bindingPath)
`.trim()
return {}
}
let depth = 0
settings.additionalFiles.forEach(file => {
let ownDepth = 0
file.split('/').forEach(x => x === '..' && ownDepth++)
depth = ownDepth > depth ? ownDepth : depth
})
let segments = [moduleDir]
while (depth--) {
segments.push(hashName(moduleDir + depth))
}
segments.push(bindingName)
file.contents += `
var cwd = process.cwd()
var bindingFileParts = ${JSON.stringify(segments)};
var bindingFile = path.join.apply(path, [cwd].concat(bindingFileParts));
mkdirp(path.dirname(bindingFile));
fs.writeFileSync(bindingFile, Buffer.from(binding, 'base64'));
${settings.additionalFiles.reduce((code, filename, i) => {
const contents = fs.readFileSync(path.join(path.dirname(file.absPath), filename))
return (code += `
var file${i} = '${contents.toString('base64')}';
var filePath${i} = path.join(cwd, bindingFileParts[0], '${filename.split('../').join('')}');
mkdirp(path.dirname(filePath${i}));
fs.writeFileSync(filePath${i}, Buffer.from(file${i}, 'base64'));
`)
}, '')};
process.dlopen(module, bindingFile)
`
}
+8 -8
View File
@@ -1,5 +1,4 @@
import { BindingsRewrite } from './bindings-rewrite'
import { embedDotNode, EmbedNodeModuleOptions } from './embed-node'
import { BindingsRewrite, embedDotNode, ExtractNodeModuleOptions } from './bindings-rewrite'
export interface FuseBoxFile {
info: { absPath: string }
@@ -9,7 +8,6 @@ export interface FuseBoxFile {
dependencies: string[]
requiresRegeneration: boolean
}
consume(): void
loadContents(): void
makeAnalysis(
parsingOptions?: any,
@@ -22,16 +20,19 @@ export interface FuseBoxFile {
): void
}
export default function(options: EmbedNodeModuleOptions = {}) {
export default function(options: ExtractNodeModuleOptions) {
return new NativeModulePlugin(options)
}
export class NativeModulePlugin {
public test = /node_modules.*(\.js|\.node)$|\.node$/
public test: RegExp
public limit2Project = false
private modules: (keyof EmbedNodeModuleOptions)[]
private modules: (keyof ExtractNodeModuleOptions)[]
constructor(public options = {}) {}
constructor(public options: ExtractNodeModuleOptions) {
this.options = options
this.test = new RegExp(`node_modules\/(${Object.keys(options).join('|')}).*\.js|\.node$`)
}
init(context: any) {
context.allowExtension('.node')
@@ -46,7 +47,6 @@ export class NativeModulePlugin {
}
const bindingsRewrite = new BindingsRewrite()
file.makeAnalysis(null, {
plugins: [
{
+57
View File
@@ -0,0 +1,57 @@
import { NexeCompiler } from '../compiler'
import { FuseBox, JSONPlugin, CSSPlugin, HTMLPlugin, QuantumPlugin } from 'fuse-box'
import { readFileAsync } from '../util'
//import NativeModulePlugin from './fuse-native-module-plugin'
function createBundle(filename: string, options: { name: string; minify: any; cwd: string }) {
const plugins: any = [JSONPlugin(), CSSPlugin(), HTMLPlugin()]
if (options.minify) {
plugins.push(
QuantumPlugin({
target: 'server',
uglify: true,
bakeApiIntoBundle: options.name
})
)
}
const fuse = FuseBox.init({
cache: false,
log: Boolean(process.env.NEXE_BUNDLE_DEBUG) || false,
homeDir: options.cwd,
sourceMaps: false,
writeBundles: false,
output: '$name.js',
target: 'server',
plugins
})
fuse.bundle(options.name).instructions(`> ${filename}`)
return fuse.run().then(x => {
let output = ''
x.bundles.forEach(y => (output = y.context.output.lastPrimaryOutput.content!.toString()))
return output
})
}
export default async function bundle(compiler: NexeCompiler, next: any) {
if (!compiler.options.bundle) {
compiler.input = await readFileAsync(compiler.options.input, 'utf-8')
return next()
}
if (compiler.options.empty || !compiler.options.input) {
compiler.input = ''
return next()
}
let producer = createBundle
if (typeof compiler.options.bundle === 'string') {
producer = require(compiler.options.bundle).createBundle
}
compiler.input = await producer(compiler.options.input, {
cwd: compiler.options.cwd,
name: compiler.options.name,
minify: compiler.options.compress
})
return next()
}
-1
View File
@@ -1 +0,0 @@
//TODO
+70 -126
View File
@@ -4,13 +4,11 @@ import { createHash } from 'crypto'
import { createReadStream } from 'fs'
import { Readable } from 'stream'
import { spawn } from 'child_process'
import { Logger, LogStep } from './logger'
import { readFileAsync, writeFileAsync, pathExistsAsync, dequote, isWindows, bound } from './util'
import { NexeOptions, version } from './options'
import { Logger } from './logger'
import { readFileAsync, writeFileAsync, pathExistsAsync, dequote, isWindows } from './util'
import { NexeOptions, nexeVersion } from './options'
import { NexeTarget } from './target'
import download = require('download')
import { getLatestGitRelease } from './releases'
import { IncomingMessage } from 'http'
import { getLatestGitRelease, storeAsset } from './releases'
const isBsd = Boolean(~process.platform.indexOf('bsd'))
const make = isWindows ? 'vcbuild.bat' : isBsd ? 'gmake' : 'make'
@@ -22,40 +20,38 @@ export interface NexeFile {
contents: string
}
export { NexeOptions }
interface NexeHeader {
resources: { [key: string]: number[] }
version: string
}
export class NexeCompiler<T extends NexeOptions = NexeOptions> {
export class NexeCompiler {
private start = Date.now()
private env = { ...process.env }
private compileStep: LogStep
private compileStep: { modify: Function; log: Function }
public log = new Logger(this.options.loglevel)
public src: string
public src = join(this.options.temp, this.options.version)
public files: NexeFile[] = []
public shims: string[] = []
public input: string
public bundledInput?: string
public targets: NexeTarget[]
public target: NexeTarget
public resources: { bundle: Buffer; index: { [key: string]: number[] } } = {
public output: string | null
public resources: { bundle: string; index: { [key: string]: number[] } } = {
index: {},
bundle: Buffer.from('')
bundle: ''
}
public output = this.options.output
private nodeSrcBinPath: string
constructor(public options: T) {
public readFileAsync: (file: string) => Promise<NexeFile>
public writeFileAsync: (file: string, contents: Buffer | string) => Promise<void>
public replaceInFileAsync: (file: string, replacer: any, replaceValue: string) => Promise<void>
public setFileContentsAsync: (file: string, contents: string | Buffer) => Promise<void>
private nodeSrcBinPath = isWindows
? join(this.src, 'Release', 'node.exe')
: join(this.src, 'out', 'Release', 'node')
constructor(public options: NexeOptions) {
const { python } = (this.options = options)
this.targets = options.targets as NexeTarget[]
this.target = this.targets[0]
this.src = join(this.options.temp, this.target.version)
this.nodeSrcBinPath = isWindows
? join(this.src, 'Release', 'node.exe')
: join(this.src, 'out', 'Release', 'node')
this.log.step('nexe ' + version, 'info')
this.log.step('nexe ' + nexeVersion, 'info')
if (python) {
if (isWindows) {
this.env.PATH = '"' + dequote(normalize(python)) + '";' + this.env.PATH
@@ -63,65 +59,41 @@ export class NexeCompiler<T extends NexeOptions = NexeOptions> {
this.env.PYTHON = python
}
}
}
@bound
addResource(file: string, contents: Buffer) {
const { resources } = this
resources.index[file] = [resources.bundle.byteLength, contents.byteLength]
resources.bundle = Buffer.concat([resources.bundle, contents])
}
@bound
async readFileAsync(file: string) {
this.assertBuild()
let cachedFile = this.files.find(x => normalize(x.filename) === normalize(file))
if (!cachedFile) {
const absPath = join(this.src, file)
cachedFile = {
absPath,
filename: file,
contents: await readFileAsync(absPath, 'utf-8').catch(x => {
if (x.code === 'ENOENT') return ''
throw x
})
this.readFileAsync = async (file: string) => {
let cachedFile = this.files.find(x => normalize(x.filename) === normalize(file))
if (!cachedFile) {
const absPath = join(this.src, file)
cachedFile = {
absPath,
filename: file,
contents: await readFileAsync(absPath, 'utf-8').catch(x => {
if (x.code === 'ENOENT') return ''
throw x
})
}
this.files.push(cachedFile)
}
this.files.push(cachedFile)
return cachedFile
}
this.writeFileAsync = (file, contents) => writeFileAsync(join(this.src, file), contents)
this.replaceInFileAsync = async (file, replace: string | RegExp, value: string) => {
const entry = await this.readFileAsync(file)
entry.contents = entry.contents.replace(replace, value)
}
this.setFileContentsAsync = async (file: string, contents: string) => {
const entry = await this.readFileAsync(file)
entry.contents = contents
}
return cachedFile
}
@bound
writeFileAsync(file: string, contents: string | Buffer) {
this.assertBuild()
return writeFileAsync(join(this.src, file), contents)
}
@bound
async replaceInFileAsync(file: string, replace: string | RegExp, value: string) {
const entry = await this.readFileAsync(file)
entry.contents = entry.contents.replace(replace, value)
}
@bound
async setFileContentsAsync(file: string, contents: string) {
const entry = await this.readFileAsync(file)
entry.contents = contents
}
quit() {
const time = Date.now() - this.start
this.log.write(`Finished in ${time / 1000}s`)
this.log.write(`Finsihed in ${time / 1000}s`)
return this.log.flush()
}
assertBuild() {
if (!this.options.build) {
throw new Error('This feature is only available with `--build`')
}
}
public getNodeExecutableLocation(target?: NexeTarget) {
private _getNodeExecutableLocation(target?: NexeTarget) {
if (target) {
return join(this.options.temp, target.toString())
}
@@ -129,31 +101,14 @@ export class NexeCompiler<T extends NexeOptions = NexeOptions> {
}
private _runBuildCommandAsync(command: string, args: string[]) {
if (this.log.verbose) {
this.compileStep.pause()
}
return new Promise((resolve, reject) => {
spawn(command, args, {
cwd: this.src,
env: this.env,
stdio: this.log.verbose ? 'inherit' : 'ignore'
stdio: 'ignore'
})
.once('error', (e: Error) => {
if (this.log.verbose) {
this.compileStep.resume()
}
reject(e)
})
.once('close', (code: number) => {
if (this.log.verbose) {
this.compileStep.resume()
}
if (code != 0) {
const error = `${command} ${args.join(' ')} exited with code: ${code}`
reject(new Error(error))
}
resolve()
})
.once('error', reject)
.once('close', resolve)
})
}
@@ -176,41 +131,23 @@ export class NexeCompiler<T extends NexeOptions = NexeOptions> {
`Compiling Node${buildOptions.length ? ' with arguments: ' + buildOptions : '...'}`
)
await this._runBuildCommandAsync(make, buildOptions)
return createReadStream(this.getNodeExecutableLocation())
return createReadStream(this._getNodeExecutableLocation())
}
private async _fetchPrebuiltBinaryAsync(target: NexeTarget) {
const downloadOptions: any = {
headers: {
'User-Agent': 'nexe (https://www.npmjs.com/package/nexe)'
}
}
if (this.options.ghToken) {
downloadOptions.headers.Authorization = 'token ' + this.options.ghToken
}
const githubRelease = await getLatestGitRelease(downloadOptions)
const githubRelease = await getLatestGitRelease()
const assetName = target.toString()
const asset = githubRelease.assets.find(x => x.name === assetName)
if (!asset) {
throw new Error(`${assetName} not available, create it using the --build flag`)
throw new Error(`${assetName} not available, create one using --build`)
}
const filename = this.getNodeExecutableLocation(target)
await download(asset.browser_download_url, dirname(filename), downloadOptions).on(
'response',
(res: IncomingMessage) => {
const total = +res.headers['content-length']!
let current = 0
res.on('data', data => {
current += data.length
this.compileStep.modify(`Downloading...${(current / total * 100).toFixed()}%`)
})
}
)
const filename = this._getNodeExecutableLocation(target)
await storeAsset(asset, dirname(filename))
return createReadStream(filename)
}
getHeader() {
private _generateHeader() {
const version =
['configure', 'vcBuild', 'make'].reduce((a, c) => {
return (a += (this.options as any)[c]
@@ -224,26 +161,33 @@ export class NexeCompiler<T extends NexeOptions = NexeOptions> {
.update(version)
.digest('hex')
}
const serializedHeader = this._serializeHeader(header)
return header
}
private _serializeHeader(header: NexeHeader) {
return `process.__nexe=${JSON.stringify(header)};`
}
async compileAsync(target: NexeTarget) {
const step = (this.compileStep = this.log.step('Compiling result'))
const build = this.options.build
const location = this.getNodeExecutableLocation(build ? undefined : target)
const location = this._getNodeExecutableLocation(target)
let binary = (await pathExistsAsync(location)) ? createReadStream(location) : null
if (!build && !binary) {
step.modify('Fetching prebuilt binary')
const header = this._generateHeader()
if (target && !build) {
binary = await this._fetchPrebuiltBinaryAsync(target)
}
if (!binary) {
binary = await this._buildAsync()
step.log('Node binary compiled')
}
return this._assembleDeliverable(binary)
return this._assembleDeliverable(header, binary)
}
private _assembleDeliverable(binary: NodeJS.ReadableStream) {
private _assembleDeliverable(header: NexeHeader, binary: NodeJS.ReadableStream) {
if (this.options.empty) {
return binary
}
@@ -252,12 +196,12 @@ export class NexeCompiler<T extends NexeOptions = NexeOptions> {
artifact.push(chunk)
})
binary.on('close', () => {
const content = [this.shims.join(''), this.input].join(';')
const content = this._serializeHeader(header) + this.input
artifact.push(content)
artifact.push(this.resources.bundle)
const lengths = Buffer.from(Array(16))
lengths.writeDoubleLE(Buffer.byteLength(content), 0)
lengths.writeDoubleLE(this.resources.bundle.byteLength, 8)
lengths.writeDoubleLE(Buffer.byteLength(this.resources.bundle), 8)
artifact.push(Buffer.concat([Buffer.from('<nexe~~sentinel>'), lengths]))
artifact.push(null)
})
+3 -7
View File
@@ -6,12 +6,10 @@ const frameLength = 120
export interface LogStep {
modify(text: string, color?: string): void
log(text: string, color?: string): void
pause(): void
resume(): void
}
export class Logger {
public verbose: boolean
private verbose: boolean
private silent: boolean
private ora: any
private modify: Function
@@ -50,7 +48,7 @@ export class Logger {
step(text: string, method: string = 'succeed'): LogStep {
if (this.silent) {
return { modify() {}, log() {}, pause() {}, resume() {} }
return { modify() {}, log() {} }
}
if (!this.ora.id) {
this.ora.start().text = text
@@ -64,9 +62,7 @@ export class Logger {
return {
modify: this.modify,
log: this.verbose ? this.write : this.modify,
pause: () => this.ora.stopAndPersist(),
resume: () => this.ora.start()
log: this.verbose ? this.write : this.modify
} as LogStep
}
}
+15 -15
View File
@@ -1,40 +1,40 @@
import { compose, Middleware } from 'app-builder'
import { compose, PromiseConfig, Middleware } from 'app-builder'
import resource from './steps/resource'
import { NexeCompiler } from './compiler'
import { argv, version, help, normalizeOptions, NexeOptions, NexePatch } from './options'
import {
argv,
nexeVersion as version,
normalizeOptionsAsync,
NexeOptions,
NexePatch
} from './options'
import cli from './steps/cli'
import bundle from './steps/bundle'
import bundle from './bundling/fuse'
import download from './steps/download'
import shim from './steps/shim'
import artifacts from './steps/artifacts'
import patches from './patches'
import { rimrafAsync } from './util'
import { NexeTarget } from './target'
async function compile(
compilerOptions?: Partial<NexeOptions>,
callback?: (err: Error | null) => void
) {
const options = normalizeOptions(compilerOptions)
const options = await normalizeOptionsAsync(compilerOptions)
const compiler = new NexeCompiler(options)
const build = compiler.options.build
if (options.clean) {
let path = compiler.src
if (!options.build) {
path = compiler.getNodeExecutableLocation(compiler.options.targets[0] as NexeTarget)
}
const step = compiler.log.step('Cleaning up nexe build artifacts...')
step.log(`Deleting contents at: ${path}`)
await rimrafAsync(path)
step.log(`Deleted contents at: ${path}`)
step.log(`Deleting directory and contents at: ${compiler.src}`)
await rimrafAsync(compiler.src)
step.log(`Deleted directory and contents at: ${compiler.src}`)
return compiler.quit()
}
const buildSteps = build
? [download, artifacts, ...patches, ...(options.patches as NexePatch[])]
: []
const nexe = compose(resource, bundle, cli, buildSteps, shim, options.plugins as NexePatch[])
const nexe = compose(resource, bundle, cli, buildSteps)
return callback
? void nexe(compiler).then(
() => callback && callback(null),
@@ -46,4 +46,4 @@ async function compile(
: nexe(compiler)
}
export { argv, compile, version, NexeCompiler, NexeOptions, help }
export { argv, compile, version }
+87 -117
View File
@@ -1,12 +1,11 @@
import * as parseArgv from 'minimist'
import { NexeCompiler } from './compiler'
import { isWindows, padRight } from './util'
import { basename, extname, join, isAbsolute, relative, dirname, resolve } from 'path'
import { isWindows } from './util'
import { basename, extname, join, isAbsolute, relative } from 'path'
import { getTarget, NexeTarget } from './target'
import { EOL, homedir } from 'os'
import * as c from 'chalk'
import { EOL } from 'os'
export const version = '{{replace:0}}'
export const nexeVersion = '2.0.0-rc.1'
export interface NexePatch {
(compiler: NexeCompiler, next: () => Promise<void>): Promise<void>
@@ -19,6 +18,7 @@ export interface NexeOptions {
targets: (string | NexeTarget)[]
name: string
cwd: string
version: string
flags: string[]
configure: string[]
vcBuild: string[]
@@ -30,20 +30,14 @@ export interface NexeOptions {
enableNodeCli: boolean
bundle: boolean | string
patches: (string | NexePatch)[]
plugins: (string | NexePatch)[]
native: any
empty: boolean
ghToken: string
sourceUrl?: string
enableStdIn?: boolean
python?: string
loglevel: 'info' | 'silent' | 'verbose'
silent?: boolean
fakeArgv?: boolean
verbose?: boolean
info?: boolean
ico?: string
debugBundle?: boolean
warmup?: string
compress?: boolean
clean?: boolean
@@ -53,7 +47,12 @@ export interface NexeOptions {
downloadOptions: any
}
function padRight(str: string, l: number) {
return (str + ' '.repeat(l)).substr(0, l)
}
const defaults = {
version: process.version.slice(1),
flags: [],
cwd: process.cwd(),
configure: [],
@@ -64,68 +63,56 @@ const defaults = {
compress: false,
build: false,
bundle: true,
patches: [],
plugins: []
patches: []
}
const alias = {
i: 'input',
o: 'output',
v: 'version',
t: 'target',
b: 'build',
n: 'name',
v: 'version',
r: 'resource',
a: 'resource',
p: 'python',
f: 'flag',
c: 'configure',
m: 'make',
s: 'snapshot',
h: 'help',
l: 'loglevel',
'fake-argv': 'fakeArgv',
'gh-token': 'ghToken',
l: 'loglevel'
}
const argv = parseArgv(process.argv, { alias, default: { ...defaults, enableStdIn: true } })
const g = c.gray
let help = `
${c.bold('nexe <entry-file> [options]')}
const argv = parseArgv(process.argv, { alias, default: defaults })
const help =
`
nexe --help CLI OPTIONS
${c.underline.bold('Options:')}
-b --build -- build from source
-i --input =index.js -- application entry point
-o --output =my-app.exe -- path to output file
-t --target =win32-x64-6.10.3 -- *target a prebuilt binary
-n --name =my-app -- main app module name
-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 step
-m --make ="--loglevel" -- *pass arguments to the make/build step
-s --snapshot =/path/to/snapshot -- build with warmup snapshot
-r --resource =./paths/**/* -- *embed file bytes within the binary
--bundle =./path/to/config -- pass a module path that exports nexeBundle
--temp =./path/to/temp -- nexe temp files (for downloads and source builds)
--no-bundle -- set when input is already bundled
--cwd -- set the current working directory for the command
--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
-i --input -- application entry point
-o --output -- path to output file
-t --target -- node version description
-n --name -- main app module name
-r --resource -- *embed files (glob) within the binary
--plugin -- extend nexe runtime behavior
-* variable key name * option can be used more than once`.trim() + EOL
${c.underline.bold('Building from source:')}
-b --build -- build from source
-p --python -- python2 (as python) executable path
-f --flag -- *v8 flags to include during compilation
-c --configure -- *arguments to the configure step
-m --make -- *arguments to the make/build step
--snapshot -- path to a warmup snapshot
--ico -- file name for alternate icon file (windows)
--rc-* -- populate rc file options (windows)
--sourceUrl -- pass an alternate source (node.tar.gz) url
--enableNodeCli -- enable node cli enforcement (blocks app cli)
${c.underline.bold('Other options:')}
--bundle -- custom bundling module with 'createBundle' export
--temp -- temp file storage default '~/.nexe'
--cwd -- set the current working directory for the command
--fake-argv -- fake argv[1] with entry file
--clean -- force download of sources
--silent -- disable logging
--verbose -- set logging to verbose
-* variable key name * option can be used more than once`.trim()
help = EOL + help + EOL
function flatten(...args: any[]): string[] {
function flattenFilter(...args: any[]): string[] {
return ([] as string[]).concat(...args).filter(x => x)
}
@@ -153,7 +140,7 @@ function tryResolveMainFileName(cwd: string) {
filename = basename(file).replace(extname(file), '')
} catch (_) {}
return filename ? filename : 'nexe_' + Date.now()
return !filename || filename === 'index' ? 'nexe_' + Date.now() : filename
}
function extractLogLevel(options: NexeOptions) {
@@ -163,101 +150,84 @@ function extractLogLevel(options: NexeOptions) {
return 'info'
}
function isName(name: string) {
return name && name !== 'index'
}
function extractName(options: NexeOptions) {
let name = options.name
if (!isName(name) && typeof options.input === 'string') {
if (!name && typeof options.input === 'string') {
name = basename(options.input).replace(extname(options.input), '')
}
if (!isName(name)) {
name = tryResolveMainFileName(options.cwd)
}
if (!isName(name) && basename(options.cwd)) {
name = basename(options.cwd)
}
name = name || tryResolveMainFileName(options.cwd)
return name.replace(/\.exe$/, '')
}
function isEntryFile(filename?: string): filename is string {
return Boolean(
filename && !isAbsolute(filename) && filename !== 'node' && /\.(tsx?|jsx?)$/.test(filename)
)
function isEntryFile(filename: string) {
return filename && !isAbsolute(filename) && filename !== 'node' && /\.(tsx?|jsx?)$/.test(filename)
}
export function resolveEntry(input: string, cwd: string, maybeEntry?: string) {
function findInput(input: string, cwd: string) {
const maybeInput = argv._.slice().pop() || ''
if (input) {
return resolve(cwd, input)
return input
}
if (isEntryFile(maybeEntry)) {
return resolve(cwd, maybeEntry)
}
if (!process.stdin.isTTY) {
return ''
if (isEntryFile(maybeInput)) {
return maybeInput
}
try {
return require.resolve(cwd)
const main = require.resolve(cwd)
return './' + relative(cwd, main)
} catch (e) {
throw new Error('No entry file found')
void e
}
return ''
}
function normalizeOptions(input?: Partial<NexeOptions>): NexeOptions {
function normalizeOptionsAsync(input?: Partial<NexeOptions>): Promise<NexeOptions | never> {
if (argv.help || argv._.some((x: string) => x === 'version') || argv.version === true) {
return new Promise(() => {
process.stderr.write(argv.help ? help : nexeVersion + EOL, () => process.exit(0))
})
}
const options = Object.assign({}, defaults, input) as NexeOptions
const opts = options as any
const cwd = (options.cwd = resolve(options.cwd))
options.temp = options.temp
? resolve(cwd, options.temp)
: process.env.NEXE_TEMP || join(homedir(), '.nexe')
const maybeEntry = input === argv ? argv._[argv._.length - 1] : undefined
options.input = resolveEntry(options.input, cwd, maybeEntry)
options.name = extractName(options)
options.loglevel = extractLogLevel(options)
options.flags = flatten(opts.flag, options.flags)
options.targets = flatten(opts.target, options.targets).map(getTarget)
if (!options.targets.length) {
options.targets.push(getTarget())
}
options.ghToken = options.ghToken || process.env.GITHUB_TOKEN || ''
options.make = flatten(options.vcBuild, options.make)
options.configure = flatten(options.configure)
options.resources = flatten(opts.resource, options.resources)
options.rc = options.rc || extractCliMap(/^rc-.*/, options)
options.output =
(options.targets[0] as NexeTarget).platform === 'windows'
? `${(options.output || options.name).replace(/\.exe$/, '')}.exe`
: `${options.output || options.name}`
options.output = resolve(cwd, options.output)
if (options.build) {
options.temp = process.env.NEXE_TEMP || join(options.cwd, '.nexe')
options.name = extractName(options)
options.input = findInput(options.input, options.cwd)
options.loglevel = extractLogLevel(options)
options.flags = flattenFilter(opts.flag, options.flags)
options.targets = flattenFilter(opts.target, options.targets)
options.make = flattenFilter(options.vcBuild, options.make)
options.configure = flattenFilter(options.configure)
options.resources = flattenFilter(opts.resource, options.resources)
options.rc = options.rc || extractCliMap(/^rc-.*/, options)
options.targets = options.targets.map(getTarget)
if (options.build && options.targets.length) {
const { arch } = options.targets[0] as NexeTarget
if (isWindows) {
options.make = Array.from(new Set(options.make.concat(arch)))
options.make = Array.from(new Set(options.make.concat([arch])))
} else {
options.configure = Array.from(new Set(options.configure.concat([`--dest-cpu=${arch}`])))
}
}
const requireDefault = (x: string) => {
if (!options.targets.length) {
options.targets = [getTarget(options.version)]
}
options.patches = options.patches.map(x => {
if (typeof x === 'string') {
return require(x).default
}
return x
}
options.plugins = flatten(opts.plugin, options.plugins).map(requireDefault)
options.patches = flatten(opts.patch, options.patches).map(requireDefault)
})
Object.keys(alias)
.filter(k => k !== 'rc')
.forEach(x => delete opts[x])
return options
return Promise.resolve(options)
}
export { argv, normalizeOptions, help }
export { argv, normalizeOptionsAsync }
-53
View File
@@ -1,53 +0,0 @@
const fs = require('fs')
const fd = fs.openSync(process.execPath, 'r')
const stat = fs.statSync(process.execPath)
const footer = Buffer.from(Array(32))
fs.readSync(fd, footer, 0, 32, stat.size - 32)
if (!footer.slice(0, 16).equals(Buffer.from('<nexe~~sentinel>'))) {
throw 'Invalid Nexe binary'
}
const contentSize = footer.readDoubleLE(16)
const resourceSize = footer.readDoubleLE(24)
const contentStart = stat.size - 32 - resourceSize - contentSize
const resourceStart = contentStart + contentSize
Object.defineProperty(
process,
'__nexe',
(function() {
let nexeHeader: any = null
return {
get: function() {
return nexeHeader
},
set: function(value: any) {
if (nexeHeader) {
throw new Error('__nexe cannot be reconfigured')
}
nexeHeader = Object.assign({}, value, {
layout: {
stat,
contentSize,
contentStart,
resourceSize,
resourceStart
}
})
Object.freeze(nexeHeader)
},
enumerable: false,
configurable: false
}
})()
)
const contentBuffer = Buffer.from(Array(contentSize))
fs.readSync(fd, contentBuffer, 0, contentSize, contentStart)
fs.closeSync(fd)
const Module = require('module')
process.mainModule = new Module(process.execPath, null)
process.mainModule!.loaded = true
;(process.mainModule as any)._compile(contentBuffer.toString(), process.execPath)
+1 -1
View File
@@ -1,7 +1,7 @@
import { NexeCompiler } from '../compiler'
export default async function buildFixes(compiler: NexeCompiler, next: () => Promise<void>) {
if (!compiler.target.version.startsWith('8.2')) {
if (!compiler.options.version.startsWith('8.2')) {
return next()
}
+1 -1
View File
@@ -5,7 +5,7 @@ import cli from './disable-node-cli'
import flags from './flags'
import ico from './ico'
import rc from './node-rc'
import { NexeCompiler, NexeOptions } from '../compiler'
import { NexeCompiler } from '../compiler'
const patches = [gyp, nexePatches, buildFixes, cli, flags, ico, rc]
+1 -9
View File
@@ -12,15 +12,7 @@ export default async function nodeRc(compiler: NexeCompiler, next: () => Promise
let value = options[key]
const isVar = /^[A-Z_]+$/.test(value)
value = isVar ? value : `"${value}"`
file.contents = file.contents.replace(
new RegExp(`VALUE "${key}",*`),
`VALUE "${key}", ${value}`
)
})
;['PRODUCTVERSION', 'FILEVERSION'].forEach(x => {
if (options[x]) {
file.contents = file.contents.replace(new RegExp(x + ' .*$', 'm'), `${x} ${options[x]}`)
}
file.contents.replace(new RegExp(`VALUE "${key}",*`), `VALUE "${key}", ${value}`)
})
return next()
+116 -1
View File
@@ -5,7 +5,122 @@ import { join } from 'path'
export default async function main(compiler: NexeCompiler, next: () => Promise<void>) {
await compiler.setFileContentsAsync(
'lib/_third_party_main.js',
'{{replace:lib/patches/boot-nexe.js}}'
`
const fs = require('fs')
const path = require('path')
const Buffer = require('buffer').Buffer
const isString = x => typeof x === 'string' || x instanceof String
const fd = fs.openSync(process.execPath, 'r')
const size = fs.statSync(process.execPath).size
const footer = Buffer.from(Array(32))
fs.readSync(fd, footer, 0, 32, size - 32)
if (!footer.slice(0, 16).equals(Buffer.from('<nexe~~sentinel>'))) {
throw 'Invalid Nexe binary'
}
const contentSize = footer.readDoubleLE(16)
const resourceSize = footer.readDoubleLE(24)
const contentStart = size - 32 - resourceSize - contentSize
const resourceStart = contentStart + contentSize
Object.defineProperty(process, '__nexe', (function () {
let nexeHeader = null
return {
get: function () {
return nexeHeader
},
set: function (value) {
if (nexeHeader) {
throw new Error('__nexe cannot be reconfigured')
}
nexeHeader = value
Object.freeze(nexeHeader)
},
enumerable: false,
configurable: false
}
})());
if (resourceSize) {
const originalReadFile = fs.readFile
const originalReadFileSync = fs.readFileSync
let setupManifest = () => {
const manifest = process.__nexe && process.__nexe.resources
if (!manifest) {
return
}
Object.keys(manifest).forEach((key) => {
const absolutePath = path.resolve(key)
if (!manifest[absolutePath]) {
manifest[absolutePath] = manifest[key]
}
const normalizedPath = path.normalize(key)
if (!manifest[normalizedPath]) {
manifest[normalizedPath] = manifest[key]
}
})
normalizeManifest = () => {}
};
fs.readFile = function readFile (file, options, callback) {
setupManifest()
const manifest = process.__nexe
const entry = manifest && manifest.resources[file]
if (!manifest || !entry || !isString(file)) {
return originalReadFile.apply(fs, arguments)
}
const [offset, length] = entry
const resourceOffset = resourceStart + offset
const encoding = isString(options) ? options : null
callback = typeof options === 'function' ? options : callback
fs.open(process.execPath, 'r', function (err, fd) {
if (err) return callback(err, null)
fs.read(fd, Buffer.alloc(length), 0, length, resourceOffset, function (error, bytesRead, buffer) {
if (error) {
return fs.close(fd, function () {
callback(error, null)
})
}
fs.close(fd, function (err) {
if (err) {
return callback(err, buffer)
}
const result = Buffer.from(buffer.toString(), 'base64')
callback(err, encoding ? result.toString(encoding) : result)
})
})
})
}
fs.readFileSync = function readFileSync (file, options) {
setupManifest()
const manifest = process.__nexe
const entry = manifest && manifest.resources[file]
if (!manifest || !entry || !isString(file)) {
return originalReadFileSync.apply(fs, arguments)
}
const [offset, length] = entry
const resourceOffset = resourceStart + offset
const encoding = isString(options) ? options : null
const fd = fs.openSync(process.execPath, 'r')
const result = Buffer.alloc(length)
fs.readSync(fd, result, 0, length, resourceOffset)
fs.closeSync(fd)
const contents = Buffer.from(result.toString(), 'base64')
return encoding ? contents.toString(encoding) : contents
}
}
const contentBuffer = Buffer.from(Array(contentSize));
fs.readSync(fd, contentBuffer, 0, contentSize, contentStart);
fs.closeSync(fd);
const Module = require('module');
process.mainModule = new Module(process.execPath, null);
process.mainModule.loaded = true;
process.mainModule._compile(contentBuffer.toString(), process.execPath);
`.trim()
)
return next()
}
+6
View File
@@ -32,6 +32,7 @@ async function getJson<T>(url: string, options?: any) {
return JSON.parse((await got(url, options)).body) as T
}
//TODO only build the latest of each major...?
function isBuildableVersion(version: string) {
const major = +version.split('.')[0]
return !~[0, 1, 2, 3, 4, 5, 7].indexOf(major) || version === '4.8.4'
@@ -41,6 +42,11 @@ export function getLatestGitRelease(options?: any) {
return getJson<GitRelease>('https://api.github.com/repos/nexe/nexe/releases/latest', options)
}
export async function storeAsset(asset: GitAsset, dest: string, headers?: any) {
const options = headers ? { headers } : undefined
await download(asset.browser_download_url, dest, options as any)
}
export async function getUnBuiltReleases(options?: any) {
const nodeReleases = await getJson<NodeRelease[]>(
'https://nodejs.org/download/release/index.json'
-63
View File
@@ -1,63 +0,0 @@
import { NexeCompiler } from '../compiler'
import { readFileAsync, writeFileAsync } from '../util'
import { resolve, relative } from 'path'
import NativeModulePlugin from '../bundling/fuse-native-module-plugin'
import { NexeOptions } from '../options'
function createBundle(options: NexeOptions) {
const { FuseBox, JSONPlugin, CSSPlugin, HTMLPlugin, QuantumPlugin } = require('fuse-box')
const plugins: any = [JSONPlugin(), CSSPlugin(), HTMLPlugin(), NativeModulePlugin(options.native)]
if (options.compress) {
plugins.push(
QuantumPlugin({
target: 'server',
uglify: true,
bakeApiIntoBundle: options.name
})
)
}
const cwd = options.cwd
const fuse = FuseBox.init({
cache: false,
log: Boolean(process.env.NEXE_BUNDLE_LOG) || false,
homeDir: cwd,
sourceMaps: false,
writeBundles: false,
output: '$name.js',
target: 'server',
plugins
})
const input = relative(cwd, resolve(cwd, options.input)).replace(/\\/g, '/')
fuse.bundle(options.name).instructions(`> ${input}`)
return fuse.run().then((x: any) => {
let output = ''
x.bundles.forEach((y: any) => (output = y.context.output.lastPrimaryOutput.content.toString()))
return output
})
}
export default async function bundle(compiler: NexeCompiler, next: any) {
const { bundle, cwd, empty, input } = compiler.options
if (!bundle) {
compiler.input = await readFileAsync(resolve(cwd, input), 'utf-8')
return next()
}
if (compiler.options.empty || !compiler.options.input) {
compiler.input = ''
return next()
}
let producer = createBundle
if (typeof compiler.options.bundle === 'string') {
producer = require(resolve(cwd, bundle)).createBundle
}
compiler.input = await producer(compiler.options)
if ('string' === typeof compiler.options.debugBundle) {
await writeFileAsync(resolve(cwd, compiler.options.debugBundle), compiler.input)
}
return next()
}
+22 -28
View File
@@ -1,22 +1,19 @@
import { normalize, relative } from 'path'
import { normalize } from 'path'
import { Readable } from 'stream'
import { createWriteStream, chmodSync, statSync } from 'fs'
import { createWriteStream, chmodSync } from 'fs'
import { readFileAsync, dequote, isWindows } from '../util'
import { NexeCompiler } from '../compiler'
import { NexeTarget } from '../target'
function getStdIn(stdin: NodeJS.ReadStream): Promise<string> {
function readStreamAsync(stream: NodeJS.ReadableStream): PromiseLike<string> {
return new Promise(resolve => {
let out = ''
stdin
.setEncoding('utf8')
.on('readable', () => {
let current
while ((current = stdin.read())) {
out += current
}
})
.on('end', () => resolve(out))
let input = ''
stream.setEncoding('utf-8')
stream.on('data', (x: string) => {
input += x
})
stream.once('end', () => resolve(dequote(input)))
stream.resume && stream.resume()
})
}
@@ -35,15 +32,20 @@ function getStdIn(stdin: NodeJS.ReadStream): Promise<string> {
* @param {*} next
*/
export default async function cli(compiler: NexeCompiler, next: () => Promise<void>) {
const { log } = compiler
let stdInUsed = false
if (!process.stdin.isTTY && compiler.options.enableStdIn) {
stdInUsed = true
compiler.input = dequote(await getStdIn(process.stdin))
const { output } = compiler.options
const { log, input: bundledInput } = compiler
if (!process.stdin.isTTY) {
log.step('Using stdin as input')
compiler.input = await readStreamAsync(process.stdin)
}
await next()
compiler.output = isWindows
? `${(output || compiler.options.name).replace(/\.exe$/, '')}.exe`
: `${output || compiler.options.name}`
const target = compiler.options.targets.shift() as NexeTarget
const deliverable = await compiler.compileAsync(target)
@@ -56,16 +58,8 @@ export default async function cli(compiler: NexeCompiler, next: () => Promise<vo
if (e) {
reject(e)
} else if (compiler.output) {
const output = compiler.output
const mode = statSync(output).mode | 0o111
chmodSync(output, mode.toString(8).slice(-3))
const inputFile = relative(process.cwd(), compiler.options.input)
const outputFile = relative(process.cwd(), output)
step.log(
`Entry: '${stdInUsed
? compiler.options.empty ? '[empty]' : '[stdin]'
: inputFile}' written to: ${outputFile}`
)
chmodSync(compiler.output, '755')
step.log(`Executable written to: ${compiler.output}`)
resolve(compiler.quit())
}
})
+2 -3
View File
@@ -3,7 +3,6 @@ import { pathExistsAsync } from '../util'
import { LogStep } from '../logger'
import { IncomingMessage } from 'http'
import { NexeCompiler } from '../compiler'
import { NexeTarget } from '../target'
function fetchNodeSourceAsync(dest: string, url: string, step: LogStep, options = {}) {
const setText = (p: number) => step.modify(`Downloading Node: ${p.toFixed()}%...`)
@@ -28,8 +27,8 @@ function fetchNodeSourceAsync(dest: string, url: string, step: LogStep, options
* @param {*} next
*/
export default async function downloadNode(compiler: NexeCompiler, next: () => Promise<void>) {
const { src, log, targets: [{ version }] } = compiler
const { sourceUrl, downloadOptions } = compiler.options
const { src, log } = compiler
const { version, sourceUrl, downloadOptions } = compiler.options
const url = sourceUrl || `https://nodejs.org/dist/v${version}/node-v${version}.tar.gz`
const step = log.step(`Downloading Node.js source from: ${url}`)
if (await pathExistsAsync(src)) {
+9 -2
View File
@@ -18,8 +18,15 @@ export default async function resource(compiler: NexeCompiler, next: () => Promi
count++
step.log(`Including file: ${file}`)
const contents = await readFileAsync(file)
compiler.addResource(file, contents)
const commentSafeContents = contents.toString('base64')
resources.index[file] = [
Buffer.byteLength(resources.bundle),
Buffer.byteLength(commentSafeContents)
]
resources.bundle += commentSafeContents
})
step.log(`Included ${count} file(s). ${(resources.bundle.byteLength / 1e6).toFixed(3)} MB`)
step.log(
`Included ${count} file(s). ${(Buffer.byteLength(resources.bundle) / 1e6).toFixed(3)} MB`
)
return next()
}
-188
View File
@@ -1,188 +0,0 @@
import { Stats } from 'fs'
import { ok } from 'assert'
import { dirname, resolve, normalize, basename } from 'path'
const binary = (process as any).__nexe as NexeBinary
ok(binary)
const manifest = binary.resources
const directories: { [key: string]: { [key: string]: boolean } } = {}
const isString = (x: any): x is string => typeof x === 'string' || x instanceof String
const isNotFile = () => false
const isNotDirectory = isNotFile
const isFile = () => true
const isDirectory = isFile
if (Object.keys(manifest).length) {
const fs = require('fs')
const originalReadFile = fs.readFile
const originalReadFileSync = fs.readFileSync
const originalReaddir = fs.readdir
const originalReaddirSync = fs.readdirSync
const originalStatSync = fs.statSync
const originalStat = fs.stat
const resourceStart = binary.layout.resourceStart
const statTime = function() {
const stat = binary.layout.stat
return {
dev: 0,
ino: 0,
nlink: 0,
rdev: 0,
uid: 123,
gid: 500,
blksize: 4096,
blocks: 0,
atime: new Date(stat.atime),
atimeMs: stat.atime.getTime(),
mtime: new Date(stat.mtime),
mtimeMs: stat.mtime.getTime(),
ctime: new Date(stat.ctime),
ctimMs: stat.ctime.getTime(),
birthtime: new Date(stat.birthtime),
birthtimeMs: stat.birthtime.getTime()
}
}
const createStat = function(directoryExtensions: any, fileExtensions?: any) {
if (!fileExtensions) {
return Object.assign({}, binary.layout.stat, directoryExtensions, { size: 0 }, statTime())
}
const size = directoryExtensions[1]
return Object.assign({}, binary.layout.stat, fileExtensions, { size }, statTime())
}
const ownStat = function(path: string | Buffer) {
const key = resolve(path)
if (directories[key]) {
return createStat({ isDirectory, isFile: isNotFile })
}
if (manifest[key]) {
return createStat(manifest[key], { isFile, isDirectory: isNotDirectory })
}
}
let setupManifest = () => {
Object.keys(manifest).forEach(key => {
const absolutePath = resolve(key)
const dirPath = dirname(absolutePath)
directories[dirPath] = directories[dirPath] || {}
directories[dirPath][basename(absolutePath)] = true
if (!manifest[absolutePath]) {
manifest[absolutePath] = manifest[key]
}
const normalizedPath = normalize(key)
if (!manifest[normalizedPath]) {
manifest[normalizedPath] = manifest[key]
}
})
setupManifest = () => {}
}
//naive patches intended to work for most use cases
var nfs = {
readdir: function readdir(path: string | Buffer, options: any, callback: any) {
setupManifest()
path = path.toString()
if ('function' === typeof options) {
callback = options
options = { encoding: 'utf8' }
}
const dir = directories[resolve(path)]
if (dir) {
process.nextTick(() => {
callback(null, Object.keys(dir))
})
} else {
return originalReaddir.apply(fs, arguments)
}
},
readdirSync: function readdirSync(path: string | Buffer, options: any) {
setupManifest()
path = path.toString()
const dir = directories[resolve(path)]
if (dir) {
return Object.keys(dir)
}
return originalReaddirSync.apply(fs, arguments)
},
readFile: function readFile(file: any, options: any, callback: any) {
setupManifest()
const entry = manifest[file]
if (!entry || !isString(file)) {
return originalReadFile.apply(fs, arguments)
}
const [offset, length] = entry
const resourceOffset = resourceStart + offset
const encoding = isString(options) ? options : null
callback = typeof options === 'function' ? options : callback
fs.open(process.execPath, 'r', function(err: Error, fd: number) {
if (err) return callback(err, null)
fs.read(fd, Buffer.alloc(length), 0, length, resourceOffset, function(
error: Error,
bytesRead: number,
result: Buffer
) {
if (error) {
return fs.close(fd, function() {
callback(error, null)
})
}
fs.close(fd, function(err: Error) {
if (err) {
return callback(err, result)
}
callback(err, encoding ? result.toString(encoding) : result)
})
})
})
},
readFileSync: function readFileSync(file: any, options: any) {
setupManifest()
const entry = manifest[file]
if (!entry || !isString(file)) {
return originalReadFileSync.apply(fs, arguments)
}
const [offset, length] = entry
const resourceOffset = resourceStart + offset
const encoding = isString(options) ? options : null
const fd = fs.openSync(process.execPath, 'r')
const result = Buffer.alloc(length)
fs.readSync(fd, result, 0, length, resourceOffset)
fs.closeSync(fd)
return encoding ? result.toString(encoding) : result
},
statSync: function statSync(path: string | Buffer) {
const stat = ownStat(path)
if (stat) {
return stat
}
return originalStatSync.apply(fs, arguments)
},
stat: function stat(path: string | Buffer, callback: any) {
const stat = ownStat(path)
if (stat) {
process.nextTick(() => {
callback(null, stat)
})
} else {
return originalStat.apply(fs, arguments)
}
}
}
Object.assign(fs, nfs)
}
interface NexeBinary {
resources: { [key: string]: number[] }
version: string
layout: {
stat: Stats
contentSize: number
contentStart: number
resourceSize: number
resourceStart: number
}
}
-37
View File
@@ -1,37 +0,0 @@
import { NexeCompiler } from '../compiler'
function wrap(code: string) {
return '!(function () {' + code + '})();'
}
export default function(compiler: NexeCompiler, next: () => Promise<void>) {
compiler.shims.push(wrap(compiler.getHeader()))
compiler.shims.push(
wrap(`
if (process.argv[1] && process.env.NODE_UNIQUE_ID) {
const cluster = require('cluster')
cluster._setupWorker()
delete process.env.NODE_UNIQUE_ID
}
`)
)
if (compiler.options.resources.length) {
compiler.shims.push(wrap('{{replace:lib/steps/shim-fs.js}}'))
}
//compiler.shims.push(wrap('{/{replace:lib/steps/shim-require.js}}'))
if (compiler.options.fakeArgv !== false) {
const nty = !process.stdin.isTTY
const input = nty ? '[stdin]' : JSON.stringify(compiler.options.input)
compiler.shims.push(
wrap(`
var r = require('path').resolve;
process.argv.splice(1,0, ${nty ? `'${input}'` : `r(${input})`});`)
)
}
return next()
}
+6 -9
View File
@@ -8,15 +8,14 @@ export { platforms, architectures }
export interface NexeTarget {
version: string
platform: NodePlatform | string
arch: NodeArch | string
platform: NodePlatform
arch: NodeArch
}
//TODO bsd
const prettyPlatform: { [key: string]: NodePlatform } = {
win32: 'windows',
windows: 'windows',
win: 'windows',
darwin: 'mac',
macos: 'mac',
mac: 'mac',
@@ -28,7 +27,6 @@ const prettyPlatform: { [key: string]: NodePlatform } = {
//TODO arm
const prettyArch: { [key: string]: NodeArch } = {
x86: 'x86',
amd64: 'x64',
ia32: 'x86',
x32: 'x86',
x64: 'x64'
@@ -38,7 +36,7 @@ function isVersion(x: string) {
if (!x) {
return false
}
return /^[\d]+$/.test(x.replace(/v|\.|\s+/g, ''))
return /\d/.test(x.replace(/v|\./g, ''))
}
function isPlatform(x: string): x is NodePlatform {
@@ -63,9 +61,8 @@ export function targetsEqual(a: NexeTarget, b: NexeTarget) {
return a.arch === b.arch && a.platform === b.platform && a.version === b.version
}
export function getTarget(target: string | Partial<NexeTarget> = ''): NexeTarget {
const currentArch = process.arch
let arch = currentArch in prettyArch ? prettyArch[process.arch] : process.arch as NodeArch,
export function getTarget(target: string | NexeTarget = ''): NexeTarget {
let arch = process.arch as NodeArch,
platform = prettyPlatform[process.platform],
version = process.version.slice(1)
@@ -78,7 +75,7 @@ export function getTarget(target: string | Partial<NexeTarget> = ''): NexeTarget
.split('-')
.forEach(x => {
if (isVersion(x)) {
version = x.replace(/v/g, '')
version = x
}
if (isPlatform(x)) {
platform = prettyPlatform[x]
-22
View File
@@ -22,26 +22,6 @@ function falseOnEnoent(e: any) {
throw e
}
function padRight(str: string, l: number) {
return (str + ' '.repeat(l)).substr(0, l)
}
const bound: MethodDecorator = function bound<T>(
target: Object,
propertyKey: string,
descriptor: TypedPropertyDescriptor<T>
) {
const configurable = true
return {
configurable,
get(this: T) {
const value = (descriptor.value as any).bind(this)
Object.defineProperty(this, propertyKey, { configurable, value, writable: true })
return value
}
}
}
function dequote(input: string) {
input = input.trim()
const singleQuote = input.startsWith("'") && input.endsWith("'")
@@ -77,8 +57,6 @@ function isDirectoryAsync(path: string) {
export {
dequote,
padRight,
bound,
isWindows,
rimrafAsync,
statAsync,
+24 -24
View File
@@ -1,9 +1,12 @@
import * as nexe from '../lib/nexe'
import { getUnBuiltReleases, getLatestGitRelease } from '../lib/releases'
import * as nexe from '../src/nexe'
import {
getUnBuiltReleases,
getLatestGitRelease
} from '../src/releases'
import * as ci from './ci'
import { runAlpineBuild } from './docker'
import { getTarget } from '../lib/target'
import { pathExistsAsync, statAsync, readFileAsync, execFileAsync } from '../lib/util'
import { getTarget } from '../src/target'
import { pathExistsAsync, statAsync, readFileAsync, execFileAsync } from '../src/util'
import got = require('got')
const env = process.env,
@@ -12,14 +15,10 @@ const env = process.env,
isLinux = Boolean(env.TRAVIS),
isWindows = Boolean(env.APPVEYOR),
isMac = Boolean(env.CIRCLECI),
isPullRequest =
Boolean(env.CIRCLE_PR_NUMBER) ||
Boolean(env.APPVEYOR_PULL_REQUEST_NUMBER) ||
Boolean(env.TRAVIS_PULL_REQUEST_BRANCH),
headers = {
Authorization: 'token ' + env.GITHUB_TOKEN,
'User-Agent': 'nexe (https://www.npmjs.com/package/nexe)'
}
isPullRequest = Boolean(env.CIRCLE_PR_NUMBER)
|| Boolean(env.APPVEYOR_PULL_REQUEST_NUMBER)
|| Boolean(env.TRAVIS_PULL_REQUEST_BRANCH),
headers = { 'Authorization': 'token ' + env.GITHUB_TOKEN }
if (require.main === module) {
if (!isPullRequest) {
@@ -30,7 +29,7 @@ if (require.main === module) {
}
}
async function build() {
async function build () {
if (isScheduled) {
const releases = await getUnBuiltReleases({ headers })
if (!releases.length) {
@@ -52,23 +51,25 @@ async function build() {
}
if (env.NEXE_VERSION) {
const target = getTarget(env.NEXE_VERSION),
const
target = getTarget(env.NEXE_VERSION),
output = isWindows ? './out.exe' : './out',
options = {
empty: true,
build: true,
target,
target, //FIXME
output
}
const stop = keepalive()
if (target.platform === 'alpine') {
await runAlpineBuild(target)
await runAlpineBuild(target, nexe.version.split('.')[0])
} else {
await nexe.compile(options)
await nexe.compile(options)
}
stop()
if (await pathExistsAsync(output)) {
await assertNexeBinary(output)
const gitRelease = await getLatestGitRelease({ headers })
@@ -76,9 +77,8 @@ async function build() {
query: { name: target.toString() },
body: await readFileAsync(output),
headers: {
Authorization: 'token ' + env.GITHUB_TOKEN,
'Content-Type': 'application/octet-stream',
'User-Agent': 'nexe (https://www.npmjs.com/package/nexe)'
'Authorization': 'token ' + env.GITHUB_TOKEN,
'Content-Type': 'application/octet-stream'
}
})
console.log(target + ' uploaded.')
@@ -86,12 +86,12 @@ async function build() {
}
}
function keepalive() {
function keepalive () {
const keepalive = setInterval(() => console.log('Building...'), 300 * 1000)
return () => clearInterval(keepalive)
}
function assertNexeBinary(file: string) {
function assertNexeBinary (file: string) {
return execFileAsync(file).catch(e => {
if (e && e.stack && e.stack.includes('Invalid Nexe binary')) {
return
+20 -21
View File
@@ -1,21 +1,8 @@
import { NexeTarget } from '../lib/releases'
import { NexeTarget } from '../src/releases'
import got = require('got')
import * as assert from 'assert'
const { env } = process
export function triggerMacBuild(release: NexeTarget, branch: string) {
assert.ok(env.CIRCLE_TOKEN)
const circle = `https://circleci.com/api/v1.1/project/github/nexe/nexe/tree/${branch}?circle-token=${env.CIRCLE_TOKEN}`
return got(circle, {
json: true,
body: {
build_parameters: {
NEXE_VERSION: release.toString()
}
}
})
}
export function triggerDockerBuild(release: NexeTarget, branch: string) {
assert.ok(env.TRAVIS_TOKEN)
const travis = `https://api.travis-ci.org/repo/nexe%2Fnexe/requests`
@@ -27,23 +14,35 @@ export function triggerDockerBuild(release: NexeTarget, branch: string) {
config: {
merge_mode: 'deep_merge',
env: {
//use matrix so that secure global variable is merged
matrix: {
NEXE_VERSION: release.toString()
}
NEXE_VERSION: release
}
}
}
},
headers: {
'Travis-API-Version': '3',
Authorization: `token ${env.TRAVIS_TOKEN}`
'Authorization': `token ${env.TRAVIS_TOKEN}`
}
})
}
export function triggerWindowsBuild(release: NexeTarget) {
export function triggerMacBuild (release: NexeTarget, branch: string) {
assert.ok(env.CIRCLE_TOKEN)
const circle = `https://circleci.com/api/v1.1/project/github/nexe/nexe/tree/${branch}?circle-token=${env.CIRCLE_TOKEN}`
return got(circle, {
json: true,
body: {
build_parameters: {
NEXE_VERSION: release.toString()
}
}
})
}
export function triggerWindowsBuild (release: NexeTarget) {
const hasVersion = 'NEXE_VERSION' in env
env.NEXE_VERSION = hasVersion ? env.NEXE_VERSION!.trim() : release.toString()
env.NEXE_VERSION = hasVersion
? env.NEXE_VERSION!.trim()
: release.toString()
return Promise.resolve()
}
+22 -49
View File
@@ -1,59 +1,32 @@
import { NexeTarget } from '../lib/target'
import { writeFileAsync, readFileAsync } from '../lib/util'
import { spawn } from 'child_process'
import got = require('got')
import execa = require('execa')
import { appendFileSync } from 'fs'
import { NexeTarget } from '../src/target'
import { writeFileAsync } from '../src/util'
import exec = require('execa')
function alpine(target: NexeTarget) {
function alpine (target: NexeTarget, nexeVersion: string) {
const version = target.version
return `
FROM ${target.arch === 'x64' ? '' : 'i386/'}alpine:3.4
FROM i386/alpine:3.6
RUN apk add --no-cache curl make gcc g++ binutils-gold python linux-headers paxctl libgcc libstdc++ git vim tar gzip wget
ENV NODE_VERSION=${target.version}
ENV NEXE_VERSION=beta
WORKDIR /
RUN curl -sSL https://nodejs.org/dist/v\${NODE_VERSION}/node-v\${NODE_VERSION}.tar.gz | tar -xz && \
mkdir /nexe_temp && mv /node-v\${NODE_VERSION} /nexe_temp/\${NODE_VERSION} && \
cd /nexe_temp/\${NODE_VERSION} && \
RUN curl -sSL https://nodejs.org/dist/v${version}/node-v${version}.tar.gz | tar -xz && \
cd /node-v${version} && \
./configure --prefix=/usr --fully-static && \
make && make install && \
make -j$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1) && \
make install && \
paxctl -cm /usr/bin/node
RUN rm /nexe_temp/\${NODE_VERSION}/out/Release/node && \
npm install -g nexe@\${NEXE_VERSION} && \
nexe --build --empty --temp /nexe_temp -c="--fully-static" -o out
`.trim()
RUN mkdir nexe_temp && mv node-v${version} nexe_temp/${version}
RUN npm i nexe@beta -g
ENV NEXE_TEMP=/nexe_temp
RUN nexe --empty -c="--fully-static" -o /nexe-out`.trim()
}
export async function runAlpineBuild(target: NexeTarget) {
await writeFileAsync('Dockerfile', alpine(target))
const outFilename = 'nexe-alpine-build-log.txt'
await writeFileAsync(outFilename, '')
let output: any = []
try {
output.push(await execa.shell(`docker build -t nexe-alpine .`))
output.push(await execa.shell(`docker run -d --name nexe nexe-alpine sh`))
output.push(await execa.shell(`docker cp nexe:/out out`))
output.push(await execa.shell(`docker rm nexe`))
} catch (e) {
console.log('Error running docker', e)
} finally {
output.forEach((x: any) => {
appendFileSync(outFilename, x.stderr)
appendFileSync(outFilename, x.stdout)
})
await got(
`https://transfer.sh/${Math.random()
.toString(36)
.substring(2)}.txt`,
{
body: await readFileAsync(outFilename),
method: 'PUT'
}
)
.then(x => console.log('Posted docker log: ', x.body))
.catch(e => console.log('Error posting log', e))
}
export async function runAlpineBuild (target: NexeTarget, nexeVersion: string) {
await writeFileAsync('Dockerfile', alpine(target, nexeVersion))
const option = { stido: 'inherit' } as any
await exec('docker', ['build', '-t', 'nexe-alpine', '.'], option)
await exec('docker', ['run', '--name', 'nexe', 'nexe-alpine'], option)
await exec('docker', ['cp', 'nexe:/nexe-out', 'out'], option)
await exec('docker', ['rm', 'nexe'], option)
}
-28
View File
@@ -1,28 +0,0 @@
import { writeFileSync, readFileSync } from 'fs'
/**
* post build step to insert code files into code files.
* '{{replace:path/to/file}}' => "file contents"
* And the package.json version.
*/
//inject('plugins/nexe-daemon/lib/index.js')
inject('lib/patches/third-party-main.js')
inject('lib/steps/shim.js')
inject('lib/options.js', JSON.stringify(require('../package.json').version))
function inject(filename: string, ...replacements: string[]) {
let contents = readFileSync(filename, 'utf8')
contents = contents.replace(/('{{(.*)}}')/g, (substring: string, ...matches: string[]) => {
if (!matches || !matches[1]) {
return substring
}
const [replace, file] = matches[1].split(':')
if (replace !== 'replace') {
return substring
}
console.log('Replacing: ', substring)
return replacements[+file] ? replacements[+file] : JSON.stringify(readFileSync(file, 'utf8'))
})
writeFileSync(filename, contents)
console.log(`Wrote: ${filename}`)
}
-3
View File
@@ -1,3 +0,0 @@
{
"main": "entry-file.js"
}
-4
View File
@@ -1,4 +0,0 @@
./test/**/*.spec.ts
--check-leaks
--require ts-node/register
--recursive
-48
View File
@@ -1,48 +0,0 @@
import { normalizeOptions } from '../src/options'
import { expect } from 'chai'
import * as path from 'path'
const ext = process.platform === 'win32' ? '.exe' : ''
describe('options', () => {
describe('cwd', () => {
it('should use process.cwd() if nothing is provided', () => {
const options = normalizeOptions()
expect(options.cwd).to.equal(process.cwd())
})
it('should use the main module in a package directory', () => {
const options = normalizeOptions()
expect(options.input).to.equal(path.resolve('./index.js'))
})
it('should resolve pathed options against cwd', () => {
const cwd = '/a/b/c'
const options = normalizeOptions({
cwd,
input: '123.js',
output: 'abc',
temp: './d'
})
expect(options.temp).to.equal(path.resolve(cwd, './d'))
expect(options.input).to.equal(path.resolve(cwd, '123.js'))
expect(options.output).to.equal(path.resolve(cwd, `abc${ext}`))
})
})
describe('output', () => {
it('should work', () => {
const options = normalizeOptions({
output: './some-output'
})
expect(options.output).to.equal(path.resolve(`./some-output${ext}`))
})
it('should default to the input file name if not index', () => {
const options = normalizeOptions({
input: 'src/folder/app.js'
})
expect(options.output).to.equal(path.resolve(`./app${ext}`))
})
it('should default to the folder/project name if filename is index', () => {
const options = normalizeOptions()
expect(options.output).to.equal(path.resolve(`./nexe${ext}`))
})
})
})
-30
View File
@@ -1,30 +0,0 @@
import { padRight, isWindows } from '../src/util'
import { expect } from 'chai'
import { blue as b } from 'chalk'
import { getTarget, NexeTarget } from '../src/target'
const arch = process.arch === 'ia32' ? 'x86' : process.arch
describe('Targets', () => {
[
['win-ia32-6.11.2', 'windows-x86-6.11.2'],
[{ version: '6.11.2', platform: 'win', arch: 'ia32' }, 'windows-x86-6.11.2'],
['win32-x64-6.11.2', 'windows-x64-6.11.2'],
['win-amd64-6.11.2', 'windows-x64-6.11.2'],
['darwin-x64-v8.4.0', 'mac-x64-8.4.0'],
['macos-x64-v8.4.0', 'mac-x64-8.4.0'],
['static-x86-6.10.3', 'alpine-x86-6.10.3'],
['linux-x32', `linux-x86-${process.version.slice(1)}`],
['alpine-notsupported-6.10.3', `alpine-${arch}-6.10.3`],
['not-a-thing', getTarget(process).toString()]
].forEach(([input, expected]) => {
it(`should accept: ${padRight(JSON.stringify(input), 53)} ${b('->')} ${expected}`, () => {
expect(getTarget(input).toString()).to.equal(expected)
})
})
it ('should stringify and toString', () => {
expect(JSON.stringify(getTarget(process))).to.equal(`"${getTarget(process)}"`)
})
})
-1
View File
@@ -1,7 +1,6 @@
{
"compilerOptions": {
"target":"es5",
"experimentalDecorators": true,
"lib": ["es2017"],
"module": "commonjs",
"outDir": "./lib",
-2628
View File
File diff suppressed because it is too large Load Diff