251 lines
8.1 KiB
JavaScript
251 lines
8.1 KiB
JavaScript
import { normalize, join } from 'path'
|
|
import Bluebird from 'bluebird'
|
|
import { Buffer } from 'buffer'
|
|
import { createHash } from 'crypto'
|
|
import { createReadStream } from 'fs'
|
|
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'
|
|
|
|
const isBsd = Boolean(~process.platform.indexOf('bsd'))
|
|
const make = isWindows ? 'vcbuild.bat' : isBsd ? 'gmake' : 'make'
|
|
const configure = isWindows ? 'configure' : './configure'
|
|
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) => {
|
|
if (!size || value.length >= size) {
|
|
return value
|
|
}
|
|
return value + ' '.repeat(size - value.length)
|
|
}
|
|
|
|
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')
|
|
|
|
if (python) {
|
|
if (isWindows) {
|
|
this.env.PATH = '"' + dequote(normalize(python)) + '";' + this.env.PATH
|
|
} else {
|
|
this.env.PYTHON = python
|
|
}
|
|
}
|
|
|
|
this.readFileAsync = async (file) => {
|
|
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' }, () => '')
|
|
}
|
|
this.files.push(cachedFile)
|
|
}
|
|
return cachedFile
|
|
}
|
|
this.writeFileAsync = (file, contents) => writeFileAsync(join(this.src, file), contents)
|
|
this.replaceInFileAsync = async (file, ...replacements) => {
|
|
const entry = await this.readFileAsync(file)
|
|
entry.contents = entry.contents.replace(...replacements)
|
|
}
|
|
this.setFileContentsAsync = async (file, contents) => {
|
|
const entry = await this.readFileAsync(file)
|
|
entry.contents = contents
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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`)
|
|
}
|
|
return padding
|
|
}
|
|
|
|
_getNodeExecutableLocation (target) {
|
|
if (target) {
|
|
return join(this.options.temp, target)
|
|
}
|
|
return this.nodeSrcBinPath
|
|
}
|
|
|
|
_runBuildCommandAsync (command, args) {
|
|
return new Bluebird((resolve, reject) => {
|
|
spawn(command, args, {
|
|
cwd: this.src,
|
|
env: this.env,
|
|
stdio: 'ignore'
|
|
})
|
|
.once('error', reject)
|
|
.once('close', resolve)
|
|
})
|
|
}
|
|
|
|
_configureAsync () {
|
|
return this._runBuildCommandAsync(
|
|
this.env.PYTHON || 'python',
|
|
[configure, ...this.options.configure]
|
|
)
|
|
}
|
|
|
|
async _buildAsync () {
|
|
this.compileStep.log(`Configuring node build: ${this.options.configure}`)
|
|
await this._configureAsync()
|
|
const buildOptions = isWindows ? this.options.vcBuild : this.options.make
|
|
this.compileStep.log(`Compiling Node with arguments: ${buildOptions}`)
|
|
await this._runBuildCommandAsync(make, buildOptions)
|
|
return createReadStream(this._getNodeExecutableLocation())
|
|
}
|
|
|
|
_fetchPrebuiltBinaryAsync () {
|
|
return this._buildAsync()
|
|
}
|
|
|
|
_getPayload (header) {
|
|
return this._serializeHeader(header) + this.input + '/**' + this.resources.bundle + `**/`
|
|
}
|
|
|
|
_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 header = {
|
|
version: padLeft(0, 32),
|
|
resources: this.resources.index,
|
|
contentSize: zeros,
|
|
paddingSize: zeros,
|
|
resourceOffset: zeros,
|
|
binaryOffset: zeros
|
|
}
|
|
const serializedHeader = this._serializeHeader(header)
|
|
header.contentSize = padLeft(Buffer.byteLength(this._getPayload(header)))
|
|
header.paddingSize = padLeft(this._findPaddingSize(+header.contentSize))
|
|
header.resourceOffset = padLeft(Buffer.byteLength(serializedHeader + this.input + '/**'))
|
|
header.version = createHash('md5').update(version + header.paddingSize).digest('hex')
|
|
return header
|
|
}
|
|
|
|
async _getExistingBinaryHeaderAsync (target) {
|
|
const filename = this._getNodeExecutableLocation(target)
|
|
const existingBinary = await pathExistsAsync(filename)
|
|
if (existingBinary) {
|
|
return this._extractHeaderAsync(filename)
|
|
}
|
|
return null
|
|
}
|
|
|
|
_extractHeaderAsync (path) {
|
|
const binary = createReadStream(path)
|
|
const haystack = new Needle(needle)
|
|
let needles = 0
|
|
let stackCache = []
|
|
return new Promise((resolve, reject) => {
|
|
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('needle', () => {
|
|
if (++needles === 2) {
|
|
resolve(JSON.parse(Buffer.concat(stackCache).toString()))
|
|
binary.close()
|
|
haystack.end()
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
_serializeHeader (header) {
|
|
return `/**${marker}${JSON.stringify(header)}${marker}**/process.__nexe=${JSON.stringify(header)};`
|
|
}
|
|
|
|
async setMainModule (compiler, next) {
|
|
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')
|
|
let target = this.options.targets.slice().shift()
|
|
let prebuiltBinary = null
|
|
const header = this._generateHeader()
|
|
target = target && `${target}-${header.version.slice(0, 6)}`
|
|
step.log(`Scanning existing binary...`)
|
|
const existingHeader = await this._getExistingBinaryHeaderAsync(target)
|
|
if (existingHeader && existingHeader.version === header.version) {
|
|
const location = this._getNodeExecutableLocation(target)
|
|
step.log(`Source already built: ${location}`)
|
|
prebuiltBinary = createReadStream(location)
|
|
}
|
|
if (target) {
|
|
throw new Error('\nNot Implemented, use --build during beta')
|
|
// prebuiltBinary = await this._fetchPrebuiltBinaryAsync(target)
|
|
}
|
|
if (!prebuiltBinary) {
|
|
prebuiltBinary = await this._buildAsync()
|
|
step.log('Node binary compiled')
|
|
}
|
|
return this._assembleDeliverable(
|
|
header,
|
|
prebuiltBinary
|
|
)
|
|
}
|
|
|
|
_assembleDeliverable (header, binary) {
|
|
const haystack = new Needle(Buffer.concat([Buffer.from('/**'), needle]))
|
|
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 => {
|
|
if (!needles) {
|
|
currentStackSize += x.length
|
|
artifact.push(x)
|
|
}
|
|
if (needles === 1 && !+header.binaryOffset) {
|
|
header.binaryOffset = padLeft(currentStackSize)
|
|
const content = Buffer.from(inflate(this._getPayload(header), +header.paddingSize) + tail)
|
|
artifact.push(content)
|
|
}
|
|
if (needles > 2) {
|
|
artifact.push(x)
|
|
}
|
|
})
|
|
return artifact
|
|
}
|
|
}
|