UNPKG

app-builder-lib

Version:
290 lines 13.1 kB
"use strict"; 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