chore: typescript and native module bundles

This commit is contained in:
calebboyd
2017-07-15 01:08:23 -05:00
parent ce2c4cefd0
commit 548e6454ef
39 changed files with 697 additions and 1256 deletions
-9
View File
@@ -1,9 +0,0 @@
{
"presets": [
["env", {
"targets": {
"node": "4.0"
}
}]
]
}
-1
View File
@@ -2,7 +2,6 @@
temp/
coverage
node_modules
examples
*.log
*.exe
.idea
+1 -1
View File
@@ -1,5 +1,5 @@
{
"recommendations": [
"chenxsan.vscode-standardjs"
"esbenp.prettier-vscode"
]
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"standard.enable": true
"typescript.tsdk": "./node_modules/typescript/lib"
}
+13 -8
View File
@@ -84,6 +84,8 @@ nexe.compile({
### `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)
@@ -124,12 +126,6 @@ nexe.compile({
- Array of globs with files to include in the build
- Example: `['./public/**/*']`
- default: `[]`
- `bundle: boolean | string | object`
- `boolean`: indicating whether integrated bundling should be tried
- `string`: filepath to user-written webpack configuration
- `object`: user provided webpack configuration
- Example: `'./webpack.config.js'`
- default: `false`
- `temp: string`
- Path to use for storing nexe's build files
- Override in the env with `NEXE_TEMP`
@@ -162,10 +158,10 @@ nexe.compile({
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.
### `NexeCompiler`
For examples, see the built in patches: [src/patches](src/patches)
### `NexeCompiler`
- `setFileContentsAsync(filename: string, contents: string): Promise<void>`
- Quickly set a file's contents within the downloaded Node.js source.
- `replaceInFileAsync(filename: string, ...replaceArgs): Promise<void>`
@@ -181,6 +177,15 @@ For examples, see the built in patches: [src/patches](src/patches)
Any modifications made to `SourceFile#contents` will be maintained in the cache _without_ the need to explicitly write them back out, e.g. using `NexeCompiler#setFileContentsAsync`.
## Bundling
Bundling in nexe has been decoupled from the compiler pipeline. While in beta it is completely seperate. Any bundler will always work with nexe as long a node-compatible bundle is created.
### Native Modules
Nexe has a plugin built for use with [fuse-box](http://fuse-box.org). This plugin currently supports modules that require `.node` files and those that use the `bindings` module.
Future plans are in place to support `node-pre-gyp#find`. Take a look at the [example](examples/native-build/build.js)
## Security
A common use case for Nexe is production deployment. When distributing executables it is important to [sign](https://en.wikipedia.org/wiki/Code_signing) them before distributing. Nexe was designed specifically to not mangle the binary it produces, this allows the checksum and signature of the size and location offsets to be maintained through the code signing process.
+23
View File
@@ -0,0 +1,23 @@
const { FuseBox } = require('fuse-box')
const nexe = '../..'
const { NativeModulePlugin } = require(nexe + '/lib/bundling/fuse')
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('electron')
.instructions(`> index.js`)
fuse.run()
+19
View File
@@ -0,0 +1,19 @@
var zmq = require('zmq')
var pub = zmq.socket('pub')
pub.bindSync('tcp://127.0.0.1:3000')
console.log('Publisher bound to port 3000')
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')
console.log('Subscriber connected to port 3000')
sub.on('message', function(topic, message) {
console.log('received a message related to:', topic.toString(), 'containing message:', message.toString())
})
+16
View File
@@ -0,0 +1,16 @@
{
"name": "native-build",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"bundle": "node build",
"build": "nexe -i app.js -o app.exe"
},
"author": "",
"license": "ISC",
"dependencies": {
"fuse-box": "^2.2.0",
"zmq": "^2.15.3"
}
}
+1 -1
View File
@@ -5,6 +5,6 @@ module.exports = nexe
if (require.main === module) {
nexe.compile(nexe.argv).catch((e) => {
process.stderr.write(e.stack, () => process.exit(1))
process.stderr.write(e.message, () => process.exit(1))
})
}
+9 -7
View File
@@ -11,9 +11,7 @@
"scripts": {
"prebuild": "npm run clean",
"clean": "rimraf lib",
"build": "babel src -d lib --copy-files",
"lint": "standard index.js ./src/**/* --fix",
"test": "npm run build && mocha"
"build": "prettier --parser typescript --no-semi --print-width 100 --single-quote --write \"src/**/*.ts\" && tsc --declaration"
},
"repository": {
"type": "git",
@@ -44,10 +42,14 @@
"tar": "^3.1.3"
},
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-core": "^6.24.1",
"babel-preset-env": "^1.3.3",
"@types/bluebird": "^3.5.8",
"@types/chalk": "^0.4.31",
"@types/mkdirp": "^0.3.29",
"@types/ora": "^0.3.31",
"@types/rimraf": "0.0.28",
"mocha": "^3.2.0",
"standard": "^10.0.1"
"prettier": "^1.4.4",
"standard": "^10.0.1",
"typescript": "^2.4.1"
}
}
+22 -21
View File
@@ -1,29 +1,30 @@
import { join, dirname } from 'path'
import { readdir, unlink } from 'fs'
import { readFileAsync, writeFileAsync, isDirectoryAsync } from './util'
import Bluebird from 'bluebird'
import mkdirp from 'mkdirp'
import { promisify, map } from 'bluebird'
import * as mkdirp from 'mkdirp'
import { NexeCompiler } from './compiler'
const { promisify, map } = Bluebird
const mkdirpAsync = promisify(mkdirp)
const unlinkAsync = promisify(unlink)
const unlinkAsync = (promisify(unlink) as any) as (path: string) => PromiseLike<void>
const readdirAsync = promisify(readdir)
function readDirAsync (dir) {
return readdirAsync(dir).map((file) => {
const path = join(dir, file)
return isDirectoryAsync(path).then(x => x ? readDirAsync(path) : path)
}).reduce((a, b) => a.concat(b), [])
function readDirAsync(dir: string): PromiseLike<string[]> {
return readdirAsync(dir)
.map((file: string) => {
const path = join(dir, file)
return isDirectoryAsync(path).then((x: boolean) => (x ? readDirAsync(path) : path as any))
})
.reduce((a: string[], b: string[] | string) => a.concat(b), [])
}
function maybeReadFileContentsAsync (file) {
return readFileAsync(file, 'utf-8')
.catch(e => {
if (e.code === 'ENOENT') {
return ''
}
throw e
})
function maybeReadFileContentsAsync(file: string) {
return readFileAsync(file, 'utf-8').catch(e => {
if (e.code === 'ENOENT') {
return ''
}
throw e
})
}
/**
@@ -36,20 +37,20 @@ function maybeReadFileContentsAsync (file) {
* - Finally, The patched files are written into source.
*
*/
export default async function artifacts (compiler, next) {
export default async function artifacts(compiler: NexeCompiler, next: () => Promise<void>) {
const { src } = compiler
const temp = join(src, 'nexe')
await mkdirpAsync(temp)
const tmpFiles = await readDirAsync(temp)
await map(tmpFiles, async (path) => {
return compiler.writeFileAsync(path.replace(temp, ''), await readFileAsync(path))
await map(tmpFiles, async path => {
return compiler.writeFileAsync(path.replace(temp, ''), await readFileAsync(path, 'utf-8'))
})
await next()
await map(tmpFiles, x => unlinkAsync(x))
return map(compiler.files, async (file) => {
return map(compiler.files, async file => {
const sourceFile = join(src, file.filename)
const tempFile = join(temp, file.filename)
const fileContents = await maybeReadFileContentsAsync(sourceFile)
-15
View File
@@ -1,15 +0,0 @@
import {
FuseBox,
CSSPlugin,
JSONPlugin,
HTMLPlugin } from 'fuse-box'
import { fromCallback } from 'bluebird'
import * as path from 'path'
export default async function bundle (compiler, next) {
let options = compiler.options.bundle
if (!options) {
return next()
}
}
@@ -2,11 +2,13 @@ import * as fs from 'fs'
import * as path from 'path'
import * as child from 'child_process'
function findNativeModulePath (filePath, bindingsArg) {
function findNativeModulePath(filePath: string, bindingsArg: string) {
const dirname = path.dirname(filePath)
const tempFile = Math.random() * 100 + '.js'
const tempFilePath = path.join(dirname, tempFile)
fs.writeFileSync(tempFilePath, `
fs.writeFileSync(
tempFilePath,
`
var bindings = require('bindings');
var Module = require('module');
var originalRequire = Module.prototype.require;
@@ -16,8 +18,9 @@ function findNativeModulePath (filePath, bindingsArg) {
return mod
};
bindings('${bindingsArg}')
`)
//using exec because it can be done syncronously
`
)
//using exec because it can be done sync
const nativeFileName = child.execSync('node ' + tempFile, { cwd: dirname }).toString()
fs.unlinkSync(tempFilePath)
const relativePath = './' + path.relative(dirname, nativeFileName).replace(/\\/g, '/')
@@ -25,20 +28,18 @@ function findNativeModulePath (filePath, bindingsArg) {
}
/**
* Traverse all nodes in a file and evaluate usages of the bindings module
* handles two common cases
*/
export class BindingsRewrite {
private bindingsIdNodes: any[] = []
public nativeModulePaths: string[] = []
public rewrite = false
constructor () {
this.bindingsIdNodes = []
this.nativeModulePaths = []
this.rewrite = false
}
isRequireBindings (node) {
isRequireBindings(node: any) {
return node.callee.name === 'require' && node.arguments[0].value === 'bindings'
}
onNode (absolutePath, node, parent) {
onNode(absolutePath: string, node: any, parent: any) {
if (node.type === 'CallExpression') {
if (this.isRequireBindings(node) && parent.type === 'VariableDeclarator') {
/**
+132
View File
@@ -0,0 +1,132 @@
import { BindingsRewrite } from './bindings-rewrite'
import { createHash } from 'crypto'
import { readFileSync } from 'fs'
import { dirname, join, basename } from 'path'
function hashName(name: string | Buffer) {
return createHash('md5').update(name).digest('hex').toString().slice(0, 8)
}
export interface FuseBoxFile {
info: { absPath: string }
contents: string
analysis: {
dependencies: string[]
requiresRegeneration: boolean
}
loadContents(): void
makeAnalysis(
parsingOptions?: any,
traversalPlugin?: {
plugins: Array<{
onNode(file: FuseBoxFile, node: any, parent: any): void
onEnd(file: FuseBoxFile): void
}>
}
): void
}
export interface NativeModulePluginOptions {
[key: string]:
| {
additionalFiles: string[]
}
| true
}
export class NativeModulePlugin {
public test: RegExp
public limit2Project = false
private modules: (keyof NativeModulePluginOptions)[]
constructor(public options: NativeModulePluginOptions) {
this.modules = Object.keys(options)
this.test = new RegExp(`node_modules\/(${Object.keys(options).join('|')}).*\.js|\.node$`)
}
init(context: any) {
context.allowExtension('.node')
}
transform(file: FuseBoxFile) {
file.loadContents()
if (file.info.absPath.endsWith('.node')) {
const contents = readFileSync(file.info.absPath)
const module = this.modules.find(x =>
Boolean(~file.info.absPath.indexOf(join('node_modules', x)))
)!
const bindingName = basename(file.info.absPath)
const settings = this.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 = readFileSync(join(dirname(file.info.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)
`
return
}
const bindingsRewrite = new BindingsRewrite()
file.makeAnalysis(null, {
plugins: [
{
onNode(file, node, parent) {
bindingsRewrite.onNode(file.info.absPath, node, parent)
},
onEnd(file) {
if (bindingsRewrite.rewrite) {
const index = file.analysis.dependencies.indexOf('bindings')
if (~index) {
file.analysis.dependencies.splice(index, 1)
}
file.analysis.dependencies.push(...bindingsRewrite.nativeModulePaths)
file.analysis.requiresRegeneration = true
}
}
}
]
})
}
}
-30
View File
@@ -1,30 +0,0 @@
import { BindingsRewrite } from './bindings-rewrite'
export class NativeModulePlugin {
constructor (...moduleNames) {
this.test = new RegExp(`node_modules\/(${moduleNames.join('|')}).*\.js`)
this.limit2project = false
}
transform (file) {
const bindingsRewrite = new BindingsRewrite()
file.loadContents()
file.makeAnalysis(null, {
plugins: [{
onNode(file, node, parent) {
bindingsRewrite.onNode(file.info.absPath, node, parent)
},
onEnd(file) {
if (bindingsRewrite.rewrite) {
const index = file.analysis.dependencies.indexOf('bindings')
if (~index) {
file.analysis.dependencies.splice(index, 1)
}
file.analysis.dependencies.push(...bindingsRewrite.nativeModulePaths)
file.analysis.requiresRegeneration = true
}
}
}]
})
}
}
+20 -22
View File
@@ -1,13 +1,17 @@
import { normalize } from 'path'
import Bluebird from 'bluebird'
import * as Bluebird from 'bluebird'
import { Readable } from 'stream'
import { createWriteStream, chmodSync } from 'fs'
import { readFileAsync, dequote, isWindows } from './util'
import { NexeCompiler } from './compiler'
function readStreamAsync (stream) {
return new Bluebird((resolve) => {
function readStreamAsync(stream: NodeJS.ReadStream): PromiseLike<string> {
return new Bluebird(resolve => {
let input = ''
stream.setEncoding('utf-8')
stream.on('data', x => { input += x })
stream.on('data', (x: string) => {
input += x
})
stream.once('end', () => resolve(dequote(input)))
stream.resume && stream.resume()
})
@@ -27,35 +31,28 @@ function readStreamAsync (stream) {
* @param {*} compiler
* @param {*} next
*/
export default async function cli (compiler, next) {
export default async function cli(compiler: NexeCompiler, next: () => Promise<void>) {
const { input, output } = compiler.options
const { log } = compiler
const bundled = Boolean(compiler.input)
if (bundled) {
log.step('Using bundled input as the main module')
await next()
} else if (!input && !process.stdin.isTTY) {
if (!input && !process.stdin.isTTY) {
log.step('Using stdin as input')
compiler.input = await readStreamAsync(process.stdin)
} else if (input) {
log.step(`Using input file as the main module: ${input}`)
compiler.input = await readFileAsync(normalize(input))
compiler.input = await readFileAsync(normalize(input), 'utf-8')
} else if (!compiler.options.empty) {
const bundle = require.resolve(process.cwd())
log.step('Using the cwd\'s main file as the main module')
compiler.input = await readFileAsync(bundle)
log.step("Using the cwd's main file as the main module")
compiler.input = await readFileAsync(bundle, 'utf-8')
} else {
log.step('Using empty input as the main module')
compiler.input = ''
}
if (!bundled) {
await next()
}
await next()
const shouldPipeOutput = Boolean(!output && !process.stdout.isTTY)
const outputName = output ||
`${compiler.options.name}${isWindows ? '.exe' : ''}`
const outputName = output || `${compiler.options.name}${isWindows ? '.exe' : ''}`
compiler.output = shouldPipeOutput ? null : outputName
const deliverable = await compiler.compileAsync()
@@ -67,14 +64,15 @@ export default async function cli (compiler, next) {
log.step('Writing binary to stdout')
deliverable.pipe(process.stdout).once('error', reject)
resolve()
} else {
} else if (compiler.output) {
const step = log.step('Writing result to file')
deliverable.pipe(createWriteStream(normalize(compiler.output)))
deliverable
.pipe(createWriteStream(normalize(compiler.output)))
.on('error', reject)
.once('close', e => {
.once('close', (e: Error) => {
if (e) {
reject(e)
} else {
} else if (compiler.output) {
chmodSync(compiler.output, '755')
step.log(`Executable written to: ${compiler.output}`)
resolve(compiler.quit())
+97 -63
View File
@@ -1,5 +1,5 @@
import { normalize, join } from 'path'
import Bluebird from 'bluebird'
import * as Bluebird from 'bluebird'
import { Buffer } from 'buffer'
import { createHash } from 'crypto'
import { createReadStream } from 'fs'
@@ -7,13 +7,8 @@ import { Readable } from 'stream'
import { spawn } from 'child_process'
import { Stream as Needle } from 'nigel'
import { Logger } from './logger'
import {
readFileAsync,
writeFileAsync,
pathExistsAsync,
dequote,
isWindows
} from './util'
import { readFileAsync, writeFileAsync, pathExistsAsync, dequote, isWindows } from './util'
import { NexeOptions } from './options'
const isBsd = Boolean(~process.platform.indexOf('bsd'))
const make = isWindows ? 'vcbuild.bat' : isBsd ? 'gmake' : 'make'
@@ -22,25 +17,56 @@ const marker = Buffer.from('<nexe~sentinel>').toString('hex')
const tail = `\n//${marker}`
const needle = Buffer.from(marker)
const fixedIntegerLength = 10
const padLeft = (x, l = fixedIntegerLength, c = '0') => (c.repeat(l) + x).slice(-l)
const inflate = (value, size) => {
const padLeft = (x: string | number, l = fixedIntegerLength, c = '0') => (c.repeat(l) + x).slice(-l)
const inflate = (value: string, size: number) => {
if (!size || value.length >= size) {
return value
}
return value + ' '.repeat(size - value.length)
}
export interface NexeFile {
filename: string
contents: string
}
interface NexeHeader {
/**
* Zero padded number indicating the extra size available in the binary
*/
paddingSize: string
/**
*
*/
binaryOffset: string
version: string
}
export class NexeCompiler {
constructor (options) {
this.start = Date.now()
const { python } = this.options = options
this.log = new Logger(options.loglevel)
this.src = join(options.temp, options.version)
this.env = Object.assign({}, process.env)
this.files = []
this.nodeSrcBinPath = isWindows
? join(this.src, 'Release', 'node.exe')
: join(this.src, 'out', 'Release', 'node')
private start = Date.now()
private env = { ...process.env }
private compileStep: { modify: Function; log: Function }
public log = new Logger(this.options.loglevel)
public src = join(this.options.temp, this.options.version)
public files: NexeFile[] = []
public input: string
public output: string | null
public resources: { bundle: string; index: { [key: string]: number[] } } = {
index: {},
bundle: ''
}
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)
if (python) {
if (isWindows) {
@@ -50,74 +76,80 @@ export class NexeCompiler {
}
}
this.readFileAsync = async (file) => {
this.readFileAsync = async (file: string) => {
let cachedFile = this.files.find(x => normalize(x.filename) === normalize(file))
if (!cachedFile) {
cachedFile = {
filename: file,
contents: await readFileAsync(join(this.src, file), 'utf-8')
.catch({ code: 'ENOENT' }, () => '')
contents: await readFileAsync(join(this.src, file), 'utf-8').catch(
{ code: 'ENOENT' },
() => ''
)
}
this.files.push(cachedFile)
}
return cachedFile
}
this.writeFileAsync = (file, contents) => writeFileAsync(join(this.src, file), contents)
this.replaceInFileAsync = async (file, ...replacements) => {
this.replaceInFileAsync = async (file, replace: string | RegExp, value: string) => {
const entry = await this.readFileAsync(file)
entry.contents = entry.contents.replace(...replacements)
entry.contents = entry.contents.replace(replace, value)
}
this.setFileContentsAsync = async (file, contents) => {
this.setFileContentsAsync = async (file: string, contents: string) => {
const entry = await this.readFileAsync(file)
entry.contents = contents
}
}
quit (code = 0) {
quit(code = 0) {
const time = Date.now() - this.start
this.log.write(`Finsihed in ${time / 1000}s`)
return this.log.flush().then(x => process.exit(code))
}
_findPaddingSize (size, override = this.options.padding) {
private _findPaddingSize(size: number, override = this.options.padding) {
if (override === 0) {
return 0
}
size = override > size ? override : size
const padding = [3, 6, 9, 16, 25, 40].map(x => x * 1e6).find(p => size <= p)
if (!padding) {
throw new Error(`No prebuilt target large enough (${(size / 1024).toFixed(2)}Mb).\nUse the --build flag and build for the current platform`)
throw new Error(
`No prebuilt target large enough (${(size / 1024).toFixed(
2
)}Mb).\nUse the --build flag and build for the current platform`
)
}
return padding
}
_getNodeExecutableLocation (target) {
private _getNodeExecutableLocation(target?: string | null) {
if (target) {
return join(this.options.temp, target)
}
return this.nodeSrcBinPath
}
_runBuildCommandAsync (command, args) {
private _runBuildCommandAsync(command: string, args: string[]) {
return new Bluebird((resolve, reject) => {
spawn(command, args, {
cwd: this.src,
env: this.env,
stdio: 'ignore'
})
.once('error', reject)
.once('close', resolve)
.once('error', reject)
.once('close', resolve)
})
}
_configureAsync () {
return this._runBuildCommandAsync(
this.env.PYTHON || 'python',
[configure, ...this.options.configure]
)
private _configureAsync() {
return this._runBuildCommandAsync(this.env.PYTHON || 'python', [
configure,
...this.options.configure
])
}
async _buildAsync () {
private async _buildAsync() {
this.compileStep.log(`Configuring node build: ${this.options.configure}`)
await this._configureAsync()
const buildOptions = isWindows ? this.options.vcBuild : this.options.make
@@ -126,19 +158,20 @@ export class NexeCompiler {
return createReadStream(this._getNodeExecutableLocation())
}
_fetchPrebuiltBinaryAsync () {
private _fetchPrebuiltBinaryAsync() {
return this._buildAsync()
}
_getPayload (header) {
private _getPayload(header: NexeHeader) {
return this._serializeHeader(header) + this.input + '/**' + this.resources.bundle + `**/`
}
_generateHeader () {
private _generateHeader() {
const zeros = padLeft(0)
const version = ['configure', 'vcBuild', 'make'].reduce((a, c) => {
return (a += this.options[c].slice().sort().join())
}, '') + this.options.enableNodeCli
const version =
['configure', 'vcBuild', 'make'].reduce((a, c) => {
return (a += (this.options as any)[c].slice().sort().join())
}, '') + this.options.enableNodeCli
const header = {
version: padLeft(0, 32),
resources: this.resources.index,
@@ -155,7 +188,7 @@ export class NexeCompiler {
return header
}
async _getExistingBinaryHeaderAsync (target) {
private async _getExistingBinaryHeaderAsync(target: string | undefined | null) {
const filename = this._getNodeExecutableLocation(target)
const existingBinary = await pathExistsAsync(filename)
if (existingBinary) {
@@ -164,16 +197,18 @@ export class NexeCompiler {
return null
}
_extractHeaderAsync (path) {
private _extractHeaderAsync(path: string): Promise<NexeHeader> {
const binary = createReadStream(path)
const haystack = new Needle(needle)
let needles = 0
let stackCache = []
let stackCache: Buffer[] = []
return new Promise((resolve, reject) => {
binary.on('error', reject).pipe(haystack)
binary
.on('error', reject)
.pipe(haystack)
.on('error', reject)
.on('close', () => reject(new Error(`Binary: ${path} is not compatible with nexe`)))
.on('haystack', x => needles && stackCache.push(x))
.on('haystack', (x: Buffer) => needles && stackCache.push(x))
.on('needle', () => {
if (++needles === 2) {
resolve(JSON.parse(Buffer.concat(stackCache).toString()))
@@ -184,19 +219,21 @@ export class NexeCompiler {
})
}
_serializeHeader (header) {
return `/**${marker}${JSON.stringify(header)}${marker}**/process.__nexe=${JSON.stringify(header)};`
private _serializeHeader(header: NexeHeader) {
return `/**${marker}${JSON.stringify(header)}${marker}**/process.__nexe=${JSON.stringify(
header
)};`
}
async setMainModule (compiler, next) {
async setMainModule(compiler: NexeCompiler, next: () => Promise<void>) {
await next()
const header = compiler._generateHeader()
const contents = inflate(this._getPayload(header), +header.paddingSize) + tail
return compiler.setFileContentsAsync(`lib/${compiler.options.name}.js`, contents)
}
async compileAsync () {
const step = this.compileStep = this.log.step('Compiling result')
async compileAsync() {
const step = (this.compileStep = this.log.step('Compiling result'))
let target = this.options.targets.slice().shift()
let prebuiltBinary = null
const header = this._generateHeader()
@@ -209,29 +246,26 @@ export class NexeCompiler {
prebuiltBinary = createReadStream(location)
}
if (target) {
throw new Error('\nNot Implemented, use --build during beta')
throw new Error('\nNot Implemented, use --build during beta\n')
// prebuiltBinary = await this._fetchPrebuiltBinaryAsync(target)
}
if (!prebuiltBinary) {
prebuiltBinary = await this._buildAsync()
step.log('Node binary compiled')
}
return this._assembleDeliverable(
header,
prebuiltBinary
)
return this._assembleDeliverable(header, prebuiltBinary)
}
_assembleDeliverable (header, binary) {
private _assembleDeliverable(header: NexeHeader, binary: NodeJS.ReadableStream) {
const haystack = new Needle(Buffer.concat([Buffer.from('/**'), needle]))
const artifact = new Readable({ read () {} })
const artifact = new Readable({ read() {} })
let needles = 0
let currentStackSize = 0
binary.pipe(haystack)
haystack
.on('close', () => artifact.push(null))
.on('needle', () => ++needles && haystack.needle(needle))
.on('haystack', x => {
.on('haystack', (x: Buffer) => {
if (!needles) {
currentStackSize += x.length
artifact.push(x)
+10 -9
View File
@@ -1,23 +1,24 @@
import download from 'download'
import download = require('download')
import { isDirectoryAsync } from './util'
import { LogStep } from './logger'
import { IncomingMessage } from 'http'
import { NexeCompiler } from './compiler'
function fetchNodeSourceAsync (cwd, url, step, options = {}) {
const setText = (p) => step.modify(`Downloading Node: ${p.toFixed()}%...`)
function fetchNodeSourceAsync(cwd: string, url: string, step: LogStep, options = {}) {
const setText = (p: number) => step.modify(`Downloading Node: ${p.toFixed()}%...`)
return download(url, cwd, Object.assign(options, { extract: true, strip: 1 }))
.on('response', res => {
.on('response', (res: IncomingMessage) => {
const total = +res.headers['content-length']
let current = 0
res.on('data', data => {
current += data.length
setText((current / total) * 100)
setText(current / total * 100)
if (current === total) {
step.log('Extracting Node...')
}
})
})
.then(
() => step.log(`Node source extracted to: ${cwd}`)
)
.then(() => step.log(`Node source extracted to: ${cwd}`))
}
/**
@@ -25,7 +26,7 @@ function fetchNodeSourceAsync (cwd, url, step, options = {}) {
* @param {*} compiler
* @param {*} next
*/
export default async function downloadNode (compiler, next) {
export default async function downloadNode(compiler: NexeCompiler, next: () => Promise<void>) {
const { src, log } = compiler
const { version, sourceUrl, downloadOptions } = compiler.options
const url = sourceUrl || `https://nodejs.org/dist/v${version}/node-v${version}.tar.gz`
+24 -12
View File
@@ -1,42 +1,54 @@
import colors from 'chalk'
import ora from 'ora'
import colors = require('chalk')
import ora = require('ora')
const frameLength = 120
export interface LogStep {
modify(text: string, color?: string): void
log(text: string, color?: string): void
}
export class Logger {
constructor (level) {
private verbose: boolean
private silent: boolean
private ora: any
private modify: Function
public write: (text: string, color?: string) => void
constructor(level: 'verbose' | 'silent' | 'info') {
this.verbose = level === 'verbose'
this.silent = level === 'silent'
if (!this.silent) {
this.ora = ora('Starting...', {
this.ora = ora({
text: 'Starting...',
color: 'blue',
spinner: 'dots'
})
this.ora.stop()
}
const noop = () => {}
this.modify = this.slient ? noop : this._modify.bind(this)
this.modify = this.silent ? noop : this._modify.bind(this)
this.write = this.silent ? noop : this._write.bind(this)
}
flush () {
flush() {
!this.silent && this.ora.succeed()
return new Promise(resolve => setTimeout(resolve, frameLength))
}
_write (update, color = 'green') {
this.ora.succeed().text = colors[color](update)
_write(update: string, color = 'green') {
this.ora.succeed().text = (colors as any)[color](update)
this.ora.start()
}
_modify (update, color = this.ora.color) {
_modify(update: string, color = this.ora.color) {
this.ora.text = update
this.ora.color = color
}
step (text) {
step(text: string): LogStep {
if (this.silent) {
return { modify () {}, log () {} }
return { modify() {}, log() {} }
}
if (!this.ora.id) {
this.ora.start().text = text
@@ -48,6 +60,6 @@ export class Logger {
return {
modify: this.modify,
log: this.verbose ? this.write : this.modify
}
} as LogStep
}
}
+7 -18
View File
@@ -1,18 +1,17 @@
import { compose, PromiseConfig } from 'app-builder'
import bundle from './bundle'
import { compose, PromiseConfig, Middleware } from 'app-builder'
import resource from './resource'
import { NexeCompiler } from './compiler'
import { argv, normalizeOptionsAsync } from './options'
import { argv, normalizeOptionsAsync, NexeOptions } from './options'
import cli from './cli'
import download from './download'
import artifacts from './artifacts'
import patches from './patches'
import { rimrafAsync } from './util'
import Bluebird from 'bluebird'
import * as Bluebird from 'bluebird'
PromiseConfig.constructor = Bluebird
async function compile (compilerOptions, callback) {
async function compile(compilerOptions: NexeOptions, callback?: (err: Error | null) => void) {
const options = await normalizeOptionsAsync(compilerOptions)
const compiler = new NexeCompiler(options)
const build = compiler.options.build
@@ -25,19 +24,9 @@ async function compile (compilerOptions, callback) {
return compiler.quit()
}
const nexe = compose(...[
resource,
bundle,
cli,
build && download,
build && artifacts,
build && patches,
build && options.patches
].filter(x => x))
const buildSteps = build ? [download, artifacts, ...patches, ...options.patches] : []
const nexe = compose(resource, cli, buildSteps)
return nexe(compiler).asCallback(callback)
}
export {
argv,
compile
}
export { argv, compile }
+89 -34
View File
@@ -1,9 +1,74 @@
import parseArgv from 'minimist'
import * as parseArgv from 'minimist'
import { NexeCompiler } from './compiler'
import { basename, extname, join } from 'path'
import Bluebird from 'bluebird'
import * as Bluebird from 'bluebird'
import { EOL } from 'os'
function padRight (str, l) {
export interface NexePatch {
(compiler: NexeCompiler, next: () => Promise<void>): Promise<void>
}
export interface NexeOptions {
build: boolean
/**
* Entrypoint filepath
*/
input: string
output: string
targets: string[]
/**
* Build name, Used for executable, and stacktraces
*/
name: string
/**
* The node version to be built
*/
version: string
python?: string
/**
* Node flags e.g. "--expose-gc" baked into the executable
*/
flags: string[]
/**
* Pass configuration options to node build configure script
*/
configure: string[]
/**
* Pass make options to node make script
*/
make: string[]
vcBuild: string[]
snapshot?: string
/**
* Array of glob strings describing resources to pull into the bundle
*/
resources: string[]
/**
* Temporary directory where nexe artifacts will be cached
* TODO dot folder in home dir
*/
temp: string
ico?: string
rc: { [key: string]: string }
/**
* Causes nexe to remove all temporary files for current configuration
*/
clean: boolean
enableNodeCli: boolean
sourceUrl?: string
loglevel: 'info' | 'silent' | 'verbose'
silent?: boolean
verbose?: boolean
info?: boolean
padding: number
patches: NexePatch[]
empty: boolean
warmup?: string
downloadOptions?: any
}
function padRight(str: string, l: number) {
return (str + ' '.repeat(l)).substr(0, l)
}
const defaults = {
@@ -14,8 +79,6 @@ const defaults = {
make: [],
targets: [],
vcBuild: ['nosign', 'release'],
bundle: false,
build: false,
enableNodeCli: false,
patches: []
}
@@ -33,7 +96,6 @@ const alias = {
m: 'make',
vc: 'vcBuild',
s: 'snapshot',
b: 'bundle',
cli: 'enableNodeCli',
h: 'help',
l: 'loglevel'
@@ -54,7 +116,6 @@ nexe --help CLI OPTIONS
-vc --vcBuild =x64 -- *pass arguments to vcbuild.bat
-s --snapshot =/path/to/snapshot -- build with warmup snapshot
-r --resource =./paths/**/* -- *embed file bytes within the binary
-b --bundle =webpack-config.js -- use default configuration or provide custom webpack config
--temp =./path/to/temp -- nexe temp files (for downloads and source builds)
--ico -- file name for alternate icon file (windows)
--rc-* -- populate rc file options (windows)
@@ -66,8 +127,8 @@ nexe --help CLI OPTIONS
-* variable key name * option can be used more than once`.trim()
function flattenFilter (...args) {
return [].concat(...args).filter(x => x)
function flattenFilter(...args: any[]): string[] {
return ([] as string[]).concat(...args).filter(x => x)
}
/**
@@ -76,9 +137,10 @@ function flattenFilter (...args) {
* @param {*} match
* @param {*} options
*/
function extractCliMap (match, options) {
return Object.keys(options).filter(x => match.test(x))
.reduce((map, option) => {
function extractCliMap(match: RegExp, options: any) {
return Object.keys(options)
.filter(x => match.test(x))
.reduce((map: { [key: string]: string }, option: keyof NexeOptions) => {
const key = option.split('-')[1]
map[key] = options[option]
delete options[option]
@@ -86,24 +148,24 @@ function extractCliMap (match, options) {
}, {})
}
function tryResolveMainFileName () {
function tryResolveMainFileName() {
let filename
try {
const file = require.resolve(process.cwd())
filename = basename(file).replace(extname(file), '')
} catch (_) {}
return !filename || filename === 'index' ? ('nexe_' + Date.now()) : filename
return !filename || filename === 'index' ? 'nexe_' + Date.now() : filename
}
function extractLogLevel (options) {
function extractLogLevel(options: NexeOptions) {
if (options.loglevel) return options.loglevel
if (options.silent) return 'silent'
if (options.verbose) return 'verbose'
return 'info'
}
function extractName (options) {
function extractName(options: NexeOptions) {
let name = options.name
if (typeof options.input === 'string' && !name) {
name = basename(options.input).replace(extname(options.input), '')
@@ -112,25 +174,23 @@ function extractName (options) {
return name.replace(/\.exe$/, '')
}
function normalizeOptionsAsync (input) {
function normalizeOptionsAsync(input: Partial<NexeOptions>) {
if (argv.help || argv._.some(x => x === 'version')) {
return Bluebird.fromCallback(cb => process.stderr.write(
argv.help ? help : '2.0.0-beta.1' + EOL,
() => cb(null, process.exit(0))
))
process.stderr.write(argv.help ? help : '2.0.0-beta.1' + EOL, () => process.exit(0))
}
const options = Object.assign({}, defaults, input)
delete options._
const options = Object.assign({}, defaults, input) as NexeOptions
const opts = options as any
delete opts._
options.loglevel = extractLogLevel(options)
options.name = extractName(options)
options.flags = flattenFilter(options.flag, options.flags)
options.targets = flattenFilter(options.target, options.targets)
options.flags = flattenFilter(opts.flag, options.flags)
options.targets = flattenFilter(opts.target, options.targets)
options.make = flattenFilter(options.make)
options.vcBuild = flattenFilter(options.vcBuild)
options.resources = flattenFilter(options.resource, options.resources)
options.resources = flattenFilter(opts.resource, options.resources)
options.rc = options.rc || extractCliMap(/^rc-.*/, options)
options.build = true // FIXME
if (options.build || options.padding === 0) {
options.targets = []
options.build = true
@@ -139,14 +199,9 @@ function normalizeOptionsAsync (input) {
options.targets = [defaultTarget]
}
Object.keys(alias)
.filter(k => k !== 'rc')
.forEach(x => delete options[x])
Object.keys(alias).filter(k => k !== 'rc').forEach(x => delete opts[x])
return Promise.resolve(options)
}
export {
argv,
normalizeOptionsAsync
}
export { argv, normalizeOptionsAsync }
-14
View File
@@ -1,14 +0,0 @@
export default async function disableNodeCli (compiler, next) {
if (compiler.options.enableNodeCli) {
return next()
}
const nodeccMarker = "argv[index][0] == '-'"
await compiler.replaceInFileAsync(
'src/node.cc',
nodeccMarker,
nodeccMarker.replace('-', ']')
)
return next()
}
+12
View File
@@ -0,0 +1,12 @@
import { NexeCompiler } from '../compiler'
export default async function disableNodeCli(compiler: NexeCompiler, next: () => Promise<void>) {
if (compiler.options.enableNodeCli) {
return next()
}
const nodeccMarker = "argv[index][0] == '-'"
await compiler.replaceInFileAsync('src/node.cc', nodeccMarker, nodeccMarker.replace('-', ']'))
return next()
}
@@ -1,4 +1,6 @@
export default async function flags (compiler, next) {
import { NexeCompiler } from '../compiler'
export default async function flags(compiler: NexeCompiler, next: () => Promise<void>) {
const nodeflags = compiler.options.flags
if (!nodeflags.length) {
return next()
-12
View File
@@ -1,12 +0,0 @@
export default async function nodeGyp ({ files, replaceInFileAsync }, next) {
await next()
const nodeGypMarker = "'lib/fs.js',"
await replaceInFileAsync('node.gyp', nodeGypMarker, `
${nodeGypMarker}
${files
.filter(x => x.filename.startsWith('lib'))
.map(x => `'${x.filename}'`)
.toString()},
`.trim())
}
+18
View File
@@ -0,0 +1,18 @@
import { NexeCompiler } from '../compiler'
export default async function nodeGyp(
{ files, replaceInFileAsync }: NexeCompiler,
next: () => Promise<void>
) {
await next()
const nodeGypMarker = "'lib/fs.js',"
await replaceInFileAsync(
'node.gyp',
nodeGypMarker,
`
${nodeGypMarker}
${files.filter(x => x.filename.startsWith('lib')).map(x => `'${x.filename}'`).toString()},
`.trim()
)
}
-14
View File
@@ -1,14 +0,0 @@
import { normalize } from 'path'
import { readFileAsync } from '../util'
export default async function ico (compiler, next) {
const iconFile = compiler.options.ico
if (!iconFile) {
return next()
}
await compiler.setFileContentsAsync(
'src/res/node.ico',
await readFileAsync(normalize(iconFile))
)
return next()
}
+12
View File
@@ -0,0 +1,12 @@
import { normalize } from 'path'
import { readFileAsync } from '../util'
import { NexeCompiler } from '../compiler'
export default async function ico(compiler: NexeCompiler, next: () => Promise<void>) {
const iconFile = compiler.options.ico
if (!iconFile) {
return next()
}
await compiler.setFileContentsAsync('src/res/node.ico', await readFileAsync(normalize(iconFile)))
return next()
}
@@ -4,10 +4,11 @@ import cli from './disable-node-cli'
import flags from './flags'
import ico from './ico'
import rc from './node-rc'
import { NexeCompiler } from '../compiler'
const patches = [
gyp,
(compiler, next) => compiler.setMainModule(compiler, next),
(compiler: NexeCompiler, next: () => Promise<void>) => compiler.setMainModule(compiler, next),
nexePatches,
cli,
flags,
@@ -1,4 +1,6 @@
export default async function nodeRc (compiler, next) {
import { NexeCompiler } from '../compiler'
export default async function nodeRc(compiler: NexeCompiler, next: () => Promise<void>) {
const options = compiler.options.rc
if (!options) {
return next()
@@ -6,7 +8,7 @@ export default async function nodeRc (compiler, next) {
const file = await compiler.readFileAsync('src/res/node.rc')
Object.keys(options).forEach((key) => {
Object.keys(options).forEach(key => {
let value = options[key]
const isVar = /^[A-Z_]+$/.test(value)
value = isVar ? value : `"${value}"`
-14
View File
@@ -1,14 +0,0 @@
export default async function snapshot (compiler, next) {
const snapshotFile = compiler.options.snapshot
if (!snapshotFile) {
return next()
}
await compiler.replaceInFileAsync(
'configure',
'def configure_v8(o):',
`def configure_v8(o):\n o['variables']['embed_script'] = '${snapshotFile}'\n o['variables']['warmup_script'] = '${snapshotFile}'`
)
return next()
}
+19
View File
@@ -0,0 +1,19 @@
import { NexeCompiler } from '../compiler'
export default async function snapshot(compiler: NexeCompiler, next: () => Promise<void>) {
const snapshotFile = compiler.options.snapshot
const warmupScript = compiler.options.warmup
if (!snapshotFile) {
return next()
}
await compiler.replaceInFileAsync(
'configure',
'def configure_v8(o):',
`def configure_v8(o):\n o['variables']['embed_script'] = '${snapshotFile}'\n o['variables']['warmup_script'] = '${warmupScript ||
snapshotFile}'`
)
return next()
}
@@ -1,5 +1,9 @@
export default async function main (compiler, next) {
await compiler.setFileContentsAsync('lib/_third_party_main.js', `
import { NexeCompiler } from '../compiler'
export default async function main(compiler: NexeCompiler, next: () => Promise<void>) {
await compiler.setFileContentsAsync(
'lib/_third_party_main.js',
`
const fs = require('fs')
const Buffer = require('buffer').Buffer
const isString = x => typeof x === 'string' || x instanceof String
+8 -8
View File
@@ -1,20 +1,18 @@
import { each } from 'bluebird'
import { readFileAsync, isDirectoryAsync } from './util'
import { Buffer } from 'buffer'
import globs from 'globby'
import * as globs from 'globby'
import { NexeCompiler } from './compiler'
export default async function resource (compiler, next) {
const resources = compiler.resources = {
index: {},
bundle: ''
}
export default async function resource(compiler: NexeCompiler, next: () => Promise<void>) {
const resources = compiler.resources
if (!compiler.options.resources.length) {
return next()
}
const step = compiler.log.step('Bundling Resources...')
let count = 0
await each(globs(compiler.options.resources), async (file) => {
await each(globs(compiler.options.resources), async file => {
if (await isDirectoryAsync(file)) {
return
}
@@ -28,6 +26,8 @@ export default async function resource (compiler, next) {
]
resources.bundle += commentSafeContents
})
step.log(`Included ${count} file(s). ${(Buffer.byteLength(resources.bundle) / 1e6).toFixed(3)} MB`)
step.log(
`Included ${count} file(s). ${(Buffer.byteLength(resources.bundle) / 1e6).toFixed(3)} MB`
)
return next()
}
+35
View File
@@ -0,0 +1,35 @@
declare module 'nigel' {
import { Writable } from 'stream'
interface Needle {
value: Buffer
lastPos: number
last: number
length: number
badCharShift: Buffer
}
export function compile(needle: Buffer): Needle
export function horspool(haystack: Buffer, needle: Needle, start: number): number
export function all(haystack: Buffer, needle: Needle, pos: number): number[]
export class Stream extends Writable {
constructor(needle: Buffer)
needle(needle: Buffer): void
flush(): void
}
}
declare module 'download' {
import { Duplex } from 'stream'
interface DownloadOptions {
extract?: boolean
strip?: number
filename?: string
proxy?: string
}
function download(
url: string,
destination?: string | DownloadOptions,
options?: DownloadOptions
): PromiseLike<Buffer> & Duplex
export = download
}
-45
View File
@@ -1,45 +0,0 @@
import { readFile, writeFile, stat } from 'fs'
import Bluebird from 'bluebird'
import rimraf from 'rimraf'
const rimrafAsync = Bluebird.promisify(rimraf)
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 = Bluebird.promisify(readFile)
const writeFileAsync = Bluebird.promisify(writeFile)
const statAsync = Bluebird.promisify(stat)
const isWindows = process.platform === 'win32'
function pathExistsAsync (path) {
return statAsync(path).then(x => true, err => {
if (err.code !== 'ENOENT') {
throw err
}
return false
})
}
function isDirectoryAsync (path) {
return statAsync(path)
.then(x => x.isDirectory())
.catch({ code: 'ENOENT' }, () => false)
}
export {
dequote,
isWindows,
rimrafAsync,
readFileAsync,
pathExistsAsync,
isDirectoryAsync,
writeFileAsync
}
+47
View File
@@ -0,0 +1,47 @@
import { readFile, writeFile, stat } from 'fs'
import * as Bluebird from 'bluebird'
import rimraf = require('rimraf')
const { promisify } = Bluebird
const rimrafAsync = (promisify(rimraf) as any) as (path: string) => Bluebird<void>
function dequote(input: string) {
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
}
export interface ReadFileAsync {
(path: string): Bluebird<Buffer>
(path: string, encoding: string): Bluebird<string>
}
const readFileAsync = (promisify(readFile) as any) as ReadFileAsync
const writeFileAsync = (promisify(writeFile) as any) as (
path: string,
contents: string | Buffer
) => Promise<void>
const statAsync = promisify(stat)
const isWindows = process.platform === 'win32'
function pathExistsAsync(path: string) {
return statAsync(path).then(x => true).catch({ code: 'ENOENT' }, () => false)
}
function isDirectoryAsync(path: string) {
return statAsync(path).then(x => x.isDirectory()).catch({ code: 'ENOENT' }, () => false)
}
export {
dequote,
isWindows,
rimrafAsync,
readFileAsync,
pathExistsAsync,
isDirectoryAsync,
writeFileAsync
}
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target":"es5",
"lib": ["es2017"],
"module": "commonjs",
"outDir": "./lib",
"strict": true
},
"include":[
"src/**/*.ts"
]
}
+22 -879
View File
File diff suppressed because it is too large Load Diff