ca657b5b06
* refactor: compile with esmoudle interop * Rigidify code replacement by using Lodash templates. * Generate zip file bundle. * Use yarn's fslib to implement the fake filesystem. Leverage the work done by yarn, rather than reimplementing from scratch. This means the fake fs is now reading from a zip file in the binary. A few methods need custom implementations as the fake fs is not writable. These were copied and adjusted from the yarn ZipOpenFS. * Add test suite using the built artefact. Smoke tests the different fs methods. Not perfect in any world, but better than nothing. * Fix tsc on Windows - globbing doesn't work there. There's probably a cleaner way to do this... * Bump version. * Dep bumps. * fixup 9d57b32: make fs patch code clearer. * fixup 3771c720: simplify test to just use require directly. --------- Co-authored-by: calebboyd <caleb.boyd@gmail.com>
329 lines
9.2 KiB
TypeScript
329 lines
9.2 KiB
TypeScript
import { delimiter, resolve, normalize, join } from 'path'
|
|
import { Buffer } from 'buffer'
|
|
import { createReadStream, ReadStream } from 'fs'
|
|
import { spawn } from 'child_process'
|
|
import { Logger, LogStep } from './logger'
|
|
import {
|
|
readFileAsync,
|
|
writeFileAsync,
|
|
pathExistsAsync,
|
|
statAsync,
|
|
dequote,
|
|
isWindows,
|
|
bound,
|
|
semverGt,
|
|
wrap,
|
|
} from './util'
|
|
import { NexeOptions, version } from './options'
|
|
import { NexeTarget } from './target'
|
|
import { PassThrough, Readable, Stream, Transform } from 'stream'
|
|
import MultiStream = require('multistream')
|
|
import { Bundle, toStream } from './fs/bundle'
|
|
import { File } from 'resolve-dependencies'
|
|
import { FactoryStream, LazyStream } from 'multistream'
|
|
|
|
const isBsd = Boolean(~process.platform.indexOf('bsd'))
|
|
const make = isWindows ? 'vcbuild.bat' : isBsd ? 'gmake' : 'make'
|
|
const configure = isWindows ? 'configure' : './configure'
|
|
|
|
type StringReplacer = (match: string) => string
|
|
|
|
export interface NexeFile {
|
|
filename: string
|
|
absPath: string
|
|
contents: string | Buffer
|
|
}
|
|
|
|
export { NexeOptions }
|
|
|
|
export class NexeError extends Error {
|
|
constructor(m: string) {
|
|
super(m)
|
|
Object.setPrototypeOf(this, NexeError.prototype)
|
|
}
|
|
}
|
|
|
|
export class NexeCompiler {
|
|
/**
|
|
* Epoch of when compilation started
|
|
*/
|
|
private start = Date.now()
|
|
private compileStep: LogStep | undefined
|
|
public log = new Logger(this.options.loglevel)
|
|
/**
|
|
* Copy of process.env
|
|
*/
|
|
public env = { ...process.env }
|
|
/**
|
|
* Virtual FileSystem
|
|
*/
|
|
public bundle: Bundle
|
|
/**
|
|
* Root directory for the source of the current build
|
|
*/
|
|
public src: string
|
|
/**
|
|
* In memory files that are being manipulated by the compiler
|
|
*/
|
|
public files: NexeFile[] = []
|
|
/**
|
|
* Standalone pieces of code run before the application entrypoint
|
|
*/
|
|
public shims: string[] = []
|
|
/**
|
|
* The last shim (defaults to "require('module').runMain()")
|
|
*/
|
|
public startup: string = ''
|
|
/**
|
|
* The main entrypoint filename for your application - eg. node mainFile.js
|
|
*/
|
|
public entrypoint: string | undefined
|
|
/**
|
|
* Not used
|
|
*/
|
|
public targets: NexeTarget[]
|
|
/**
|
|
* Current target of the compiler
|
|
*/
|
|
public target: NexeTarget
|
|
/**
|
|
* Output filename (-o myapp.exe)
|
|
*/
|
|
public output = this.options.output
|
|
/**
|
|
* Flag to indicate whether or notstdin was used for input
|
|
*/
|
|
public stdinUsed = false
|
|
/**
|
|
* Path to the configure script
|
|
*/
|
|
public configureScript: string
|
|
/**
|
|
* The file path of node binary
|
|
*/
|
|
public nodeSrcBinPath: string
|
|
/**
|
|
* Remote asset path if available
|
|
*/
|
|
public remoteAsset: string
|
|
|
|
constructor(public options: NexeOptions) {
|
|
const { python } = (this.options = options)
|
|
//SOMEDAY iterate over multiple targets with `--outDir`
|
|
this.targets = options.targets as NexeTarget[]
|
|
this.target = this.targets[0]
|
|
if (!/https?\:\/\//.test(options.remote)) {
|
|
throw new NexeError(`Invalid remote URI scheme (must be http or https): ${options.remote}`)
|
|
}
|
|
this.remoteAsset = options.remote + this.target.toString()
|
|
this.src = join(this.options.temp, this.target.version)
|
|
this.configureScript = configure + (semverGt(this.target.version, '10.10.0') ? '.py' : '')
|
|
this.nodeSrcBinPath = isWindows
|
|
? join(this.src, 'Release', 'node.exe')
|
|
: join(this.src, 'out', 'Release', 'node')
|
|
this.log.step('nexe ' + version, 'info')
|
|
this.bundle = new Bundle(options)
|
|
if (isWindows) {
|
|
const originalPath = process.env.PATH
|
|
delete process.env.PATH
|
|
this.env = { ...process.env }
|
|
this.env.PATH = python
|
|
? (this.env.PATH = dequote(normalize(python)) + delimiter + originalPath)
|
|
: originalPath
|
|
process.env.PATH = originalPath
|
|
} else {
|
|
this.env = { ...process.env }
|
|
python && (this.env.PYTHON = python)
|
|
}
|
|
}
|
|
|
|
@bound
|
|
addResource(absoluteFileName: string, content?: Buffer | string | File) {
|
|
return this.bundle.addResource(absoluteFileName, content)
|
|
}
|
|
|
|
@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.files.push(cachedFile)
|
|
}
|
|
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 | StringReplacer) {
|
|
const entry = await this.readFileAsync(file)
|
|
entry.contents = entry.contents.toString().replace(replace, value as any)
|
|
}
|
|
|
|
@bound
|
|
async setFileContentsAsync(file: string, contents: string | Buffer) {
|
|
const entry = await this.readFileAsync(file)
|
|
entry.contents = contents
|
|
}
|
|
|
|
quit(error?: any) {
|
|
const time = Date.now() - this.start
|
|
this.log.write(`Finished in ${time / 1000}s`, error ? 'red' : 'green')
|
|
return this.log.flush()
|
|
}
|
|
|
|
assertBuild() {
|
|
if (!this.options.build) {
|
|
throw new NexeError('This feature is only available with `--build`')
|
|
}
|
|
}
|
|
|
|
public getNodeExecutableLocation(target?: NexeTarget) {
|
|
if (this.options.asset) {
|
|
return resolve(this.options.cwd, this.options.asset)
|
|
}
|
|
if (target) {
|
|
return join(this.options.temp, target.toString())
|
|
}
|
|
return this.nodeSrcBinPath
|
|
}
|
|
|
|
private _runBuildCommandAsync(command: string, args: string[]) {
|
|
if (this.log.verbose) {
|
|
this.compileStep!.pause()
|
|
}
|
|
return new Promise<void>((resolve, reject) => {
|
|
spawn(command, args, {
|
|
cwd: this.src,
|
|
env: this.env,
|
|
stdio: this.log.verbose ? 'inherit' : '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 NexeError(error))
|
|
}
|
|
resolve()
|
|
})
|
|
})
|
|
}
|
|
|
|
private _configureAsync() {
|
|
if (isWindows && semverGt(this.target.version, '10.15.99')) {
|
|
return Promise.resolve()
|
|
}
|
|
return this._runBuildCommandAsync(this.env.PYTHON || 'python', [
|
|
this.configureScript,
|
|
...this.options.configure,
|
|
])
|
|
}
|
|
|
|
public async build(): Promise<ReadStream> {
|
|
this.compileStep!.log(
|
|
`Configuring node build${
|
|
this.options.configure.length ? ': ' + this.options.configure : '...'
|
|
}`
|
|
)
|
|
await this._configureAsync()
|
|
const buildOptions = this.options.make
|
|
this.compileStep!.log(
|
|
`Compiling Node${buildOptions.length ? ' with arguments: ' + buildOptions : '...'}`
|
|
)
|
|
await this._runBuildCommandAsync(make, buildOptions)
|
|
return createReadStream(this.getNodeExecutableLocation())
|
|
}
|
|
|
|
private async _shouldCompileBinaryAsync(binary: NodeJS.ReadableStream | null, location: string) {
|
|
//SOMEDAY combine make/configure/vcBuild/and modified times of included files
|
|
const { snapshot, build } = this.options
|
|
|
|
if (!binary) {
|
|
return true
|
|
}
|
|
|
|
if (build && snapshot != null && (await pathExistsAsync(snapshot))) {
|
|
const snapshotLastModified = (await statAsync(snapshot)).mtimeMs
|
|
const binaryLastModified = (await statAsync(location)).mtimeMs
|
|
return snapshotLastModified > binaryLastModified
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
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)
|
|
let binary = (await pathExistsAsync(location)) ? createReadStream(location) : null
|
|
|
|
if (await this._shouldCompileBinaryAsync(binary, location)) {
|
|
binary = await this.build()
|
|
step.log('Node binary compiled')
|
|
}
|
|
return this._assembleDeliverable(binary!)
|
|
}
|
|
|
|
code() {
|
|
return [this.shims.join(''), this.startup].join(';')
|
|
}
|
|
|
|
private async _assembleDeliverable(binary: Readable) {
|
|
if (!this.options.mangle) {
|
|
return binary
|
|
}
|
|
const launchCode = this.code()
|
|
const codeSize = Buffer.byteLength(launchCode)
|
|
const sentinel = Buffer.from('<nexe~~sentinel>')
|
|
|
|
let vfsSize = 0
|
|
const streams = [
|
|
binary,
|
|
toStream(launchCode),
|
|
this.bundle.toStream().pipe(
|
|
new Transform({
|
|
transform: (chunk, _, cb) => {
|
|
vfsSize || this.bundle.finalize()
|
|
chunk && (vfsSize += chunk.length)
|
|
cb(null, chunk)
|
|
},
|
|
})
|
|
),
|
|
]
|
|
|
|
let done = false
|
|
return new MultiStream((cb) => {
|
|
if (done) cb(null, null)
|
|
else if (streams.length) cb(null, streams.shift() as Readable)
|
|
else {
|
|
done = true
|
|
const trailers = Buffer.alloc(16)
|
|
trailers.writeDoubleLE(codeSize, 0)
|
|
trailers.writeDoubleLE(vfsSize, 8)
|
|
cb(null, toStream(Buffer.concat([sentinel, trailers])))
|
|
}
|
|
})
|
|
}
|
|
}
|