345 lines
9.4 KiB
TypeScript
345 lines
9.4 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
|
|
} from "./util";
|
|
import { NexeOptions, version } from "./options";
|
|
import { NexeTarget } from "./target";
|
|
import { Readable, Transform } from "stream";
|
|
import MultiStream = require("multistream");
|
|
import { Bundle, toStream } from "./fs/bundle";
|
|
import { File } from "resolve-dependencies";
|
|
|
|
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 = "";
|
|
/**
|
|
* 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,
|
|
shell: true,
|
|
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])));
|
|
}
|
|
});
|
|
}
|
|
}
|