app-builder-lib
Version:
electron-builder lib
290 lines • 13.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsarPackager = void 0;
const builder_util_1 = require("builder-util");
const fs_1 = require("builder-util/out/fs");
const fs_2 = require("fs");
const promises_1 = require("fs/promises");
const path = require("path");
const appFileCopier_1 = require("../util/appFileCopier");
const asar_1 = require("./asar");
const integrity_1 = require("./integrity");
const unpackDetector_1 = require("./unpackDetector");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pickle = require("chromium-pickle-js");
/** @internal */
class AsarPackager {
constructor(src, destination, options, unpackPattern) {
this.src = src;
this.destination = destination;
this.options = options;
this.unpackPattern = unpackPattern;
this.fs = new asar_1.AsarFilesystem(this.src);
this.outFile = path.join(destination, "app.asar");
this.unpackedDest = `${this.outFile}.unpacked`;
}
// sort files to minimize file change (i.e. asar file is not changed dramatically on small change)
async pack(fileSets, packager) {
if (this.options.ordering != null) {
// ordering doesn't support transformed files, but ordering is not used functionality - wait user report to fix it
await order(fileSets[0].files, this.options.ordering, fileSets[0].src);
}
await (0, promises_1.mkdir)(path.dirname(this.outFile), { recursive: true });
const unpackedFileIndexMap = new Map();
const orderedFileSets = [
// Write dependencies first to minimize offset changes to asar header
...fileSets.slice(1),
// Finish with the app files that change most often
fileSets[0],
].map(orderFileSet);
for (const fileSet of orderedFileSets) {
unpackedFileIndexMap.set(fileSet, await this.createPackageFromFiles(fileSet, packager.info));
}
await this.writeAsarFile(orderedFileSets, unpackedFileIndexMap);
}
async createPackageFromFiles(fileSet, packager) {
const metadata = fileSet.metadata;
// search auto unpacked dir
const unpackedDirs = new Set();
const rootForAppFilesWithoutAsar = path.join(this.destination, "app");
if (this.options.smartUnpack !== false) {
await (0, unpackDetector_1.detectUnpackedDirs)(fileSet, unpackedDirs, this.unpackedDest, rootForAppFilesWithoutAsar);
}
const dirToCreateForUnpackedFiles = new Set(unpackedDirs);
const correctDirNodeUnpackedFlag = async (filePathInArchive, dirNode) => {
for (const dir of unpackedDirs) {
if (filePathInArchive.length > dir.length + 2 && filePathInArchive[dir.length] === path.sep && filePathInArchive.startsWith(dir)) {
dirNode.unpacked = true;
unpackedDirs.add(filePathInArchive);
// 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
await (0, promises_1.mkdir)(path.join(this.unpackedDest, filePathInArchive), { recursive: true });
break;
}
}
};
const transformedFiles = fileSet.transformedFiles;
const taskManager = new builder_util_1.AsyncTaskManager(packager.cancellationToken);
const fileCopier = new fs_1.FileCopier();
let currentDirNode = null;
let currentDirPath = null;
const unpackedFileIndexSet = new Set();
for (let i = 0, n = fileSet.files.length; i < n; i++) {
const file = fileSet.files[i];
const stat = metadata.get(file);
if (stat == null) {
continue;
}
const pathInArchive = path.relative(rootForAppFilesWithoutAsar, (0, appFileCopier_1.getDestinationPath)(file, fileSet));
if (stat.isSymbolicLink()) {
const s = stat;
this.fs.getOrCreateNode(pathInArchive).link = s.relativeLink;
s.pathInArchive = pathInArchive;
unpackedFileIndexSet.add(i);
continue;
}
let fileParent = path.dirname(pathInArchive);
if (fileParent === ".") {
fileParent = "";
}
if (currentDirPath !== fileParent) {
if (fileParent.startsWith("..")) {
throw new Error(`Internal error: path must not start with "..": ${fileParent}`);
}
currentDirPath = fileParent;
currentDirNode = this.fs.getOrCreateNode(fileParent);
// do not check for root
if (fileParent !== "" && !currentDirNode.unpacked) {
if (unpackedDirs.has(fileParent)) {
currentDirNode.unpacked = true;
}
else {
await correctDirNodeUnpackedFlag(fileParent, currentDirNode);
}
}
}
const dirNode = currentDirNode;
const newData = transformedFiles == null ? undefined : transformedFiles.get(i);
const isUnpacked = dirNode.unpacked || (this.unpackPattern != null && this.unpackPattern(file, stat));
const integrity = newData === undefined ? await (0, integrity_1.hashFile)(file) : (0, integrity_1.hashFileContents)(newData);
this.fs.addFileNode(file, dirNode, newData == undefined ? stat.size : Buffer.byteLength(newData), isUnpacked, stat, integrity);
if (isUnpacked) {
if (!dirNode.unpacked && !dirToCreateForUnpackedFiles.has(fileParent)) {
dirToCreateForUnpackedFiles.add(fileParent);
await (0, promises_1.mkdir)(path.join(this.unpackedDest, fileParent), { recursive: true });
}
const unpackedFile = path.join(this.unpackedDest, pathInArchive);
taskManager.addTask(copyFileOrData(fileCopier, newData, file, unpackedFile, stat));
if (taskManager.tasks.length > fs_1.MAX_FILE_REQUESTS) {
await taskManager.awaitTasks();
}
unpackedFileIndexSet.add(i);
}
}
if (taskManager.tasks.length > 0) {
await taskManager.awaitTasks();
}
return unpackedFileIndexSet;
}
writeAsarFile(fileSets, unpackedFileIndexMap) {
return new Promise((resolve, reject) => {
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 = (0, fs_2.createWriteStream)(this.outFile);
writeStream.on("error", reject);
writeStream.on("close", resolve);
writeStream.write(sizeBuf);
let fileSetIndex = 0;
let files = fileSets[0].files;
let metadata = fileSets[0].metadata;
let transformedFiles = fileSets[0].transformedFiles;
let unpackedFileIndexSet = unpackedFileIndexMap.get(fileSets[0]);
const w = (index) => {
while (true) {
if (index >= files.length) {
if (++fileSetIndex >= fileSets.length) {
writeStream.end();
return;
}
else {
files = fileSets[fileSetIndex].files;
metadata = fileSets[fileSetIndex].metadata;
transformedFiles = fileSets[fileSetIndex].transformedFiles;
unpackedFileIndexSet = unpackedFileIndexMap.get(fileSets[fileSetIndex]);
index = 0;
}
}
if (!unpackedFileIndexSet.has(index)) {
break;
}
else {
const stat = metadata.get(files[index]);
if (stat != null && stat.isSymbolicLink()) {
(0, fs_2.symlink)(stat.linkRelativeToFile, path.join(this.unpackedDest, stat.pathInArchive), () => w(index + 1));
return;
}
}
index++;
}
const data = transformedFiles == null ? null : transformedFiles.get(index);
const file = files[index];
if (data !== null && data !== undefined) {
writeStream.write(data, () => w(index + 1));
return;
}
// https://github.com/yarnpkg/yarn/pull/3539
const stat = metadata.get(file);
if (stat != null && stat.size < 2 * 1024 * 1024) {
(0, promises_1.readFile)(file)
.then(it => {
writeStream.write(it, () => w(index + 1));
})
.catch((e) => reject(`Cannot read file ${file}: ${e.stack || e}`));
}
else {
const readStream = (0, fs_2.createReadStream)(file);
readStream.on("error", reject);
readStream.once("end", () => w(index + 1));
readStream.on("open", () => {
readStream.pipe(writeStream, {
end: false,
});
});
}
};
writeStream.write(headerBuf, () => w(0));
});
}
}
exports.AsarPackager = AsarPackager;
async function order(filenames, orderingFile, src) {
const orderingFiles = (await (0, promises_1.readFile)(orderingFile, "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 (const file of orderingFiles) {
const pathComponents = file.split(path.sep);
for (const pathComponent of pathComponents) {
ordering.push(path.join(src, pathComponent));
}
}
const sortedFiles = [];
let missing = 0;
const total = filenames.length;
for (const file of ordering) {
if (!sortedFiles.includes(file) && filenames.includes(file)) {
sortedFiles.push(file);
}
}
for (const file of filenames) {
if (!sortedFiles.includes(file)) {
sortedFiles.push(file);
missing += 1;
}
}
builder_util_1.log.info({ coverage: ((total - missing) / total) * 100 }, "ordering files in ASAR archive");
return sortedFiles;
}
function copyFileOrData(fileCopier, data, source, destination, stats) {
if (data == null) {
return fileCopier.copy(source, destination, stats);
}
else {
return (0, promises_1.writeFile)(destination, data);
}
}
function orderFileSet(fileSet) {
const sortedFileEntries = Array.from(fileSet.files.entries());
sortedFileEntries.sort(([, a], [, b]) => {
if (a === b) {
return 0;
}
// Place addons last because their signature change
const isAAddon = a.endsWith(".node");
const isBAddon = b.endsWith(".node");
if (isAAddon && !isBAddon) {
return 1;
}
if (isBAddon && !isAAddon) {
return -1;
}
// Otherwise order by name
return a < b ? -1 : 1;
});
let transformedFiles;
if (fileSet.transformedFiles) {
transformedFiles = new Map();
const indexMap = new Map();
for (const [newIndex, [oldIndex]] of sortedFileEntries.entries()) {
indexMap.set(oldIndex, newIndex);
}
for (const [oldIndex, value] of fileSet.transformedFiles) {
const newIndex = indexMap.get(oldIndex);
if (newIndex === undefined) {
const file = fileSet.files[oldIndex];
throw new Error(`Internal error: ${file} was lost while ordering asar`);
}
transformedFiles.set(newIndex, value);
}
}
const { src, destination, metadata } = fileSet;
return {
src,
destination,
metadata,
files: sortedFileEntries.map(([, file]) => file),
transformedFiles,
};
}
//# sourceMappingURL=asarUtil.js.map
;