vrrv-installer-builder
Version:
A complete solution to package and build a ready for distribution Electron app for MacOS, Windows and Linux with “auto update” support out of the box
409 lines (408 loc) • 18.9 kB
JavaScript
"use strict";
const asar_electron_builder_1 = require("asar-electron-builder");
const util_1 = require("./util/util");
const fs_extra_p_1 = require("fs-extra-p");
const bluebird_1 = require("bluebird");
const path = require("path");
const log_1 = require("./util/log");
const minimatch_1 = require("minimatch");
const deepAssign_1 = require("./util/deepAssign");
const isBinaryFile = bluebird_1.Promise.promisify(require("isbinaryfile"));
const pickle = require("chromium-pickle-js");
const Filesystem = require("asar-electron-builder/lib/filesystem");
const UINT64 = require("cuint").UINT64;
//noinspection JSUnusedLocalSymbols
const __awaiter = require("./util/awaiter");
const MAX_FILE_REQUESTS = 32;
const concurrency = { concurrency: MAX_FILE_REQUESTS };
const NODE_MODULES_PATTERN = path.sep + "node_modules" + path.sep;
function walk(dirPath, consumer, filter, addRootToResult) {
return fs_extra_p_1.readdir(dirPath).then(names => bluebird_1.Promise.map(names, name => {
const filePath = dirPath + path.sep + name;
return fs_extra_p_1.lstat(filePath).then(stat => {
if (filter != null && !filter(filePath, stat)) {
return null;
}
if (consumer != null) {
consumer(filePath, stat);
}
if (stat.isDirectory()) {
return walk(filePath, consumer, filter, true);
}
return filePath;
});
}, concurrency)).then(list => {
list.sort((a, b) => {
// files before directories
if (Array.isArray(a) && Array.isArray(b)) {
return 0;
} else if (a == null || Array.isArray(a)) {
return 1;
} else if (b == null || Array.isArray(b)) {
return -1;
} else {
return a.localeCompare(b);
}
});
const result = addRootToResult ? [dirPath] : [];
for (let item of list) {
if (item != null) {
if (Array.isArray(item)) {
result.push.apply(result, item);
} else {
result.push(item);
}
}
}
return result;
});
}
exports.walk = walk;
function createAsarArchive(src, resourcesPath, options, filter) {
return __awaiter(this, void 0, void 0, function* () {
// sort files to minimize file change (i.e. asar file is not changed dramatically on small change)
yield new AsarPackager(src, resourcesPath, options).pack(filter);
});
}
exports.createAsarArchive = createAsarArchive;
function isUnpackDir(path, pattern, rawPattern) {
return path.startsWith(rawPattern) || pattern.match(path);
}
class AsarPackager {
constructor(src, resourcesPath, options) {
this.src = src;
this.resourcesPath = resourcesPath;
this.options = options;
this.toPack = [];
this.fs = new Filesystem(this.src);
this.changedFiles = new Map();
this.outFile = path.join(this.resourcesPath, "app.asar");
}
pack(filter) {
return __awaiter(this, void 0, void 0, function* () {
const metadata = new Map();
const files = yield walk(this.src, (it, stat) => {
metadata.set(it, stat);
}, filter);
yield this.createPackageFromFiles(this.options.ordering == null ? files : yield this.order(files), metadata);
yield this.writeAsarFile();
});
}
getSrcRealPath() {
if (this.srcRealPath == null) {
this.srcRealPath = fs_extra_p_1.realpath(this.src);
}
return this.srcRealPath;
}
detectUnpackedDirs(files, metadata, autoUnpackDirs, createDirPromises, unpackedDest, fileIndexToModulePackageData) {
return __awaiter(this, void 0, void 0, function* () {
const packageJsonStringLength = "package.json".length;
const readPackageJsonPromises = [];
for (let i = 0, n = files.length; i < n; i++) {
const file = files[i];
const index = file.lastIndexOf(NODE_MODULES_PATTERN);
if (index < 0) {
continue;
}
const nextSlashIndex = file.indexOf(path.sep, index + NODE_MODULES_PATTERN.length + 1);
if (nextSlashIndex < 0) {
continue;
}
if (!metadata.get(file).isFile()) {
continue;
}
const nodeModuleDir = file.substring(0, nextSlashIndex);
if (file.length === nodeModuleDir.length + 1 + packageJsonStringLength && file.endsWith("package.json")) {
const promise = fs_extra_p_1.readJson(file);
if (readPackageJsonPromises.length > MAX_FILE_REQUESTS) {
yield bluebird_1.Promise.all(readPackageJsonPromises);
readPackageJsonPromises.length = 0;
}
readPackageJsonPromises.push(promise);
fileIndexToModulePackageData[i] = promise;
}
if (autoUnpackDirs.has(nodeModuleDir)) {
const fileParent = path.dirname(file);
if (fileParent !== nodeModuleDir && !autoUnpackDirs.has(fileParent)) {
autoUnpackDirs.add(fileParent);
createDirPromises.push(fs_extra_p_1.ensureDir(path.join(unpackedDest, path.relative(this.src, fileParent))));
if (createDirPromises.length > MAX_FILE_REQUESTS) {
yield bluebird_1.Promise.all(createDirPromises);
createDirPromises.length = 0;
}
}
continue;
}
const ext = path.extname(file);
let shouldUnpack = false;
if (ext === ".dll" || ext === ".exe") {
shouldUnpack = true;
} else if (ext === "") {
shouldUnpack = yield isBinaryFile(file);
}
if (!shouldUnpack) {
continue;
}
log_1.log(`${ path.relative(this.src, nodeModuleDir) } is not packed into asar archive - contains executable code`);
let fileParent = path.dirname(file);
// create parent dir to be able to copy file later without directory existence check
createDirPromises.push(fs_extra_p_1.ensureDir(path.join(unpackedDest, path.relative(this.src, fileParent))));
if (createDirPromises.length > MAX_FILE_REQUESTS) {
yield bluebird_1.Promise.all(createDirPromises);
createDirPromises.length = 0;
}
while (fileParent !== nodeModuleDir) {
autoUnpackDirs.add(fileParent);
fileParent = path.dirname(fileParent);
}
autoUnpackDirs.add(nodeModuleDir);
}
if (readPackageJsonPromises.length > 0) {
yield bluebird_1.Promise.all(readPackageJsonPromises);
}
if (createDirPromises.length > 0) {
yield bluebird_1.Promise.all(createDirPromises);
createDirPromises.length = 0;
}
});
}
createPackageFromFiles(files, metadata) {
return __awaiter(this, void 0, void 0, function* () {
// search auto unpacked dir
const autoUnpackDirs = new Set();
const createDirPromises = [fs_extra_p_1.ensureDir(path.dirname(this.outFile))];
const unpackedDest = `${ this.outFile }.unpacked`;
const fileIndexToModulePackageData = new Array(files.length);
if (this.options.smartUnpack !== false) {
yield this.detectUnpackedDirs(files, metadata, autoUnpackDirs, createDirPromises, unpackedDest, fileIndexToModulePackageData);
}
const unpackDir = this.options.unpackDir == null ? null : new minimatch_1.Minimatch(this.options.unpackDir);
const unpack = this.options.unpack == null ? null : new minimatch_1.Minimatch(this.options.unpack, {
matchBase: true
});
const copyPromises = [];
const mainPackageJson = path.join(this.src, "package.json");
for (let i = 0, n = files.length; i < n; i++) {
const file = files[i];
const stat = metadata.get(file);
if (stat.isFile()) {
const fileParent = path.dirname(file);
const dirNode = this.fs.searchNodeFromPath(fileParent);
if (dirNode.unpacked && createDirPromises.length > 0) {
yield bluebird_1.Promise.all(createDirPromises);
createDirPromises.length = 0;
}
const packageDataPromise = fileIndexToModulePackageData[i];
let newData = null;
if (packageDataPromise == null) {
if (this.options.extraMetadata != null && file === mainPackageJson) {
newData = JSON.stringify(deepAssign_1.deepAssign((yield fs_extra_p_1.readJson(file)), this.options.extraMetadata), null, 2);
}
} else {
newData = cleanupPackageJson(packageDataPromise.value());
}
const fileSize = newData == null ? stat.size : Buffer.byteLength(newData);
const node = this.fs.searchNodeFromPath(file);
node.size = fileSize;
if (dirNode.unpacked || unpack != null && unpack.match(file)) {
node.unpacked = true;
if (!dirNode.unpacked) {
createDirPromises.push(fs_extra_p_1.ensureDir(path.join(unpackedDest, path.relative(this.src, fileParent))));
yield bluebird_1.Promise.all(createDirPromises);
createDirPromises.length = 0;
}
const unpackedFile = path.join(unpackedDest, path.relative(this.src, file));
copyPromises.push(newData == null ? copyFile(file, unpackedFile, stat) : fs_extra_p_1.writeFile(unpackedFile, newData));
if (copyPromises.length > MAX_FILE_REQUESTS) {
yield bluebird_1.Promise.all(copyPromises);
copyPromises.length = 0;
}
} else {
if (newData != null) {
this.changedFiles.set(file, newData);
}
if (fileSize > 4294967295) {
throw new Error(`${ file }: file size can not be larger than 4.2GB`);
}
node.offset = this.fs.offset.toString();
//noinspection JSBitwiseOperatorUsage
if (process.platform !== "win32" && stat.mode & 0x40) {
node.executable = true;
}
this.toPack.push(file);
this.fs.offset.add(UINT64(fileSize));
}
} else if (stat.isDirectory()) {
let unpacked = false;
if (autoUnpackDirs.has(file)) {
unpacked = true;
} else {
unpacked = unpackDir != null && isUnpackDir(path.relative(this.src, file), unpackDir, this.options.unpackDir);
if (unpacked) {
createDirPromises.push(fs_extra_p_1.ensureDir(path.join(unpackedDest, path.relative(this.src, file))));
} else {
for (let d of autoUnpackDirs) {
if (file.length > d.length + 2 && file[d.length] === path.sep && file.startsWith(d)) {
unpacked = true;
autoUnpackDirs.add(file);
// not all dirs marked as unpacked after first iteration - because node module dir can be marked as unpacked after processing node module dir content
// e.g. node-notifier/example/advanced.js processed, but only on process vendor/terminal-notifier.app module will be marked as unpacked
createDirPromises.push(fs_extra_p_1.ensureDir(path.join(unpackedDest, path.relative(this.src, file))));
break;
}
}
}
}
this.fs.insertDirectory(file, unpacked);
} else if (stat.isSymbolicLink()) {
yield this.addLink(file);
}
}
yield bluebird_1.Promise.all(copyPromises);
});
}
addLink(file) {
return __awaiter(this, void 0, void 0, function* () {
const realFile = yield fs_extra_p_1.realpath(file);
const link = path.relative((yield this.getSrcRealPath()), realFile);
if (link.startsWith("..")) {
throw new Error(realFile + ": file links out of the package");
} else {
this.fs.searchNodeFromPath(file).link = link;
}
});
}
writeAsarFile() {
const headerPickle = pickle.createEmpty();
headerPickle.writeString(JSON.stringify(this.fs.header));
const headerBuf = headerPickle.toBuffer();
const sizePickle = pickle.createEmpty();
sizePickle.writeUInt32(headerBuf.length);
const sizeBuf = sizePickle.toBuffer();
const writeStream = fs_extra_p_1.createWriteStream(this.outFile);
return new bluebird_1.Promise((resolve, reject) => {
writeStream.on("error", reject);
writeStream.once("finish", resolve);
writeStream.write(sizeBuf);
let w;
w = (list, index) => {
if (list.length === index) {
writeStream.end();
return;
}
const file = list[index];
const data = this.changedFiles.get(file);
if (data != null) {
writeStream.write(data, () => w(list, index + 1));
return;
}
const readStream = fs_extra_p_1.createReadStream(file);
readStream.on("error", reject);
readStream.once("end", () => w(list, index + 1));
readStream.pipe(writeStream, {
end: false
});
};
writeStream.write(headerBuf, () => w(this.toPack, 0));
});
}
order(filenames) {
return __awaiter(this, void 0, void 0, function* () {
const orderingFiles = (yield fs_extra_p_1.readFile(this.options.ordering, "utf8")).split("\n").map(line => {
if (line.indexOf(":") !== -1) {
line = line.split(":").pop();
}
line = line.trim();
if (line[0] === "/") {
line = line.slice(1);
}
return line;
});
const ordering = [];
for (let file of orderingFiles) {
let pathComponents = file.split(path.sep);
let str = this.src;
for (let pathComponent of pathComponents) {
str = path.join(str, pathComponent);
ordering.push(str);
}
}
const filenamesSorted = [];
let missing = 0;
const total = filenames.length;
for (let file of ordering) {
if (!(filenamesSorted.indexOf(file) !== -1) && filenames.indexOf(file) !== -1) {
filenamesSorted.push(file);
}
}
for (let file of filenames) {
if (!(filenamesSorted.indexOf(file) !== -1)) {
filenamesSorted.push(file);
missing += 1;
}
}
log_1.log(`Ordering file has ${ (total - missing) / total * 100 }% coverage.`);
return filenamesSorted;
});
}
}
function cleanupPackageJson(data) {
try {
let changed = false;
for (let prop of Object.getOwnPropertyNames(data)) {
if (prop[0] === "_" || prop === "dist" || prop === "gitHead" || prop === "keywords") {
delete data[prop];
changed = true;
}
}
if (changed) {
return JSON.stringify(data, null, 2);
}
} catch (e) {
util_1.debug(e);
}
return null;
}
function checkFileInArchive(asarFile, relativeFile, messagePrefix) {
return __awaiter(this, void 0, void 0, function* () {
function error(text) {
return new Error(`${ messagePrefix } "${ relativeFile }" in the "${ asarFile }" ${ text }`);
}
let stat;
try {
stat = asar_electron_builder_1.statFile(asarFile, relativeFile);
} catch (e) {
const fileStat = yield util_1.statOrNull(asarFile);
if (fileStat == null) {
throw error(`does not exist. Seems like a wrong configuration.`);
}
try {
asar_electron_builder_1.listPackage(asarFile);
} catch (e) {
throw error(`is corrupted: ${ e }`);
}
// asar throws error on access to undefined object (info.link)
stat = null;
}
if (stat == null) {
throw error(`does not exist. Seems like a wrong configuration.`);
}
if (stat.size === 0) {
throw error(`is corrupted: size 0`);
}
});
}
exports.checkFileInArchive = checkFileInArchive;
function copyFile(src, dest, stats) {
return new bluebird_1.Promise(function (resolve, reject) {
const readStream = fs_extra_p_1.createReadStream(src);
const writeStream = fs_extra_p_1.createWriteStream(dest, { mode: stats.mode });
readStream.on("error", reject);
writeStream.on("error", reject);
writeStream.on("open", function () {
readStream.pipe(writeStream);
});
writeStream.once("finish", resolve);
});
}
//# sourceMappingURL=asarUtil.js.map