UNPKG

@reforged/maker-appimage

Version:

An AppImage maker implementation for the Electron Forge.

218 lines 9.05 kB
import EventEmitter from "events"; import { existsSync } from "fs"; import { readFile } from "fs/promises"; import { execFileSync, execFile } from "child_process"; import { coerce } from "semver"; ; // FIXME: Library considerations? Should we make for it separate module? export function generateDesktop(desktopEntry, actions) { function toEscapeSeq(string) { if (typeof string === "string") return string .replaceAll(/\\(?!["`trn])/g, "\\\\") .replaceAll("`", "\\`") .replaceAll("\t", "\\t") .replaceAll("\r", "\\r") .replaceAll("\n", "\\n"); return string; } const template = { desktop: [], actions: [] }; let actionsKey = null; template.desktop.push('[Desktop Entry]'); for (const entry of Object.entries(desktopEntry)) if (entry[0] !== "Actions" && entry[1] !== undefined && entry[1] !== null) template.desktop.push(entry.map(v => toEscapeSeq(v)).join('=')); if (actions) for (const [name, record] of Object.entries(actions)) if (/[a-zA-Z]/.test(name)) { actionsKey === null ? actionsKey = name : actionsKey += ";" + name; template.actions.push('\n[Desktop Action ' + name + ']'); for (const entry of Object.entries(record)) if (entry[1] !== undefined && entry[1] !== null) template.actions.push(entry.map(v => toEscapeSeq(v)).join('=')); } if (actionsKey) template.desktop.push("Actions=" + actions); return template.desktop.join('\n') + '\n' + template.actions.join('\n'); } /** * A wrapper for `mksquashfs` binary. * * @returns An event used to watch for `mksquashfs` changes, including the job progress (in percent – as float number). */ export function mkSquashFs(...argv) { let lastProgress = 0, stderrCollector = ""; const event = new EventEmitter(), { PATH, SOURCE_DATE_EPOCH } = process.env, { stderr, stdout } = execFile("mksquashfs", argv, { encoding: "utf-8", windowsHide: true, env: { PATH, SOURCE_DATE_EPOCH } }).once("close", (...args) => event.emit("close", ...args, stderrCollector ? undefined : stderrCollector)).on("error", (error) => event.emit("error", error)); stderr?.on("data", (chunk) => { switch (chunk.constructor) { case String: stderrCollector += chunk; break; default: throw new TypeError(`Unresolved chunk of type '${chunk?.constructor.name ?? typeof chunk}'.`); } }); stdout?.on("data", (chunk) => { if (chunk.constructor !== String) return; const progress = chunk.match(/\] [0-9/]+ ([0-9]+)%/)?.[1]; if (progress === undefined) return; const progInt = parseInt(progress, 10); if (progInt >= 0 && progInt <= 100 && progInt !== lastProgress && event.emit("progress", progInt / 100)) lastProgress = progInt; }); return event; } /** * Returns the version of `mksquashfs` binary, as `SemVer` value. * * Under the hood, it executes `mksquashfs` with `-version`, parses * the `stdout` and tries to coerce it to `SemVer`. */ export function getSquashFsVer() { let output = execFileSync("mksquashfs", ["-version"], { encoding: "utf8", timeout: 3000, maxBuffer: 768, windowsHide: true, env: { PATH: process.env["PATH"] } }).split('\n')[0]; if (output === undefined) throw new TypeError("Unable to parse '-version': first line read error."); output = /(?<=version )[0-9.]+/.exec(output)?.[0]; if (output === undefined) throw new TypeError("Unable to parse '-version': number not found."); output = coerce(output); if (output === null) throw new Error(`Unable to coerce string '${output}' to SemVer.`); return output; } ; /** * Concatenates files and/or buffers into a new buffer. */ export async function joinFiles(...filesAndBuffers) { const buffArr = []; for (const path of filesAndBuffers) // Convert anything to Uint8Array as buffer representation if (path instanceof Object.getPrototypeOf(Uint8Array)) buffArr.push(Promise.resolve(new Uint8Array(path.buffer))); else if (path instanceof ArrayBuffer || path instanceof SharedArrayBuffer) buffArr.push(Promise.resolve(new Uint8Array(path))); else if (existsSync(path)) buffArr.push(readFile(path)); else throw new Error(`Unable to concat '${path}': Invalid path.`); return Promise.all(buffArr) .then(buffArr => { // Concat all buffers into the new ones. const length = buffArr.reduce((p, c) => p + c.length, 0); const result = new Uint8Array(length); let preBuffLen = 0; for (const buff of buffArr) result.set(buff, preBuffLen), preBuffLen = buff.length; return result; }); } /** * Maps Node.js architecture to the AppImage-friendly format. */ export const mapArch = Object.freeze({ x64: "x86_64", ia32: "i686", arm64: "aarch64", armv7l: "armhf" }); /** * A function to validate if the type of any value is like the one in * {@link ImageMetadata} interface. * * @param meta Any value to validate the type of. * @returns Whenever `meta` is an {@link ImageMetadata}-like object. */ function validateImageMetadata(meta) { if (typeof meta !== "object" || meta === null) return false; if (!("type" in meta) || (meta.type !== "PNG" && meta.type !== "SVG")) return false; if (!("width" in meta) || (typeof meta.width !== "number" && meta.width !== null)) return false; if (!("height" in meta) || (typeof meta.height !== "number" && meta.height !== null)) return false; return true; } /** * A function to fetch metadata from buffer in PNG or SVG format. * * @remarks * * For PNGs, it gets required information (like image width or height) * from IHDR header (if it is correct according to spec), otherwise it sets * dimension values to `null`. * * For SVGs, it gets information about the dimensions from `<svg>` tag. If it is * missing, this function will return `null` for `width` and/or `height`. * * This function will also recognize file formats based on *MAGIC* headers – for * SVGs, it looks for existence of `<svg>` tag, for PNGs it looks if file starts * from the specific bytes. * * @param image PNG/SVG/XPM image buffer. */ export function getImageMetadata(image) { const svgMagic = { file: /<svg ?[^>]*>/, width: /<svg (?!width).*.width=["']?(\d+)(?:px)?["']?[^>]*>/, height: /<svg (?!height).*.height=["']?(\d+)(?:px)?["']?[^>]*>/ }; const partialMeta = {}; if (image.readUInt32BE() === 2303741511 /* FileHeader.PNG */) partialMeta["type"] = "PNG"; else if (image.readUInt32BE(2) === 1481657650 /* FileHeader.XPM2 */) partialMeta["type"] = "XPM2"; else if (image.readUInt32BE(3) === 1481657632 /* FileHeader.XPM3 */) partialMeta["type"] = "XPM3"; else if (svgMagic.file.test(image.toString("utf8"))) partialMeta["type"] = "SVG"; else throw Error("Unsupported image format (FreeDesktop spec expects images only of following MIME type: PNG, SVG and XPM)."); switch (partialMeta.type) { // Based on specification by W3C: https://www.w3.org/TR/PNG/ case "PNG": { const prefixIHDR = 4 + image.indexOf("IHDR"); const rawMeta = { width: prefixIHDR === 3 ? null : image.readInt32BE(prefixIHDR), height: prefixIHDR === 3 ? null : image.readInt32BE(prefixIHDR + 4) }; partialMeta["width"] = (rawMeta.width ?? 0) === 0 ? null : rawMeta.width; partialMeta["height"] = (rawMeta.height ?? 0) === 0 ? null : rawMeta.height; break; } case "SVG": { const svgImage = image.toString("utf8"); const rawMeta = { width: parseInt(svgImage.match(svgMagic.width)?.[1] ?? ""), height: parseInt(svgImage.match(svgMagic.height)?.[1] ?? ""), }; partialMeta["width"] = isNaN(rawMeta["width"]) ? null : rawMeta["width"]; partialMeta["height"] = isNaN(rawMeta["height"]) ? null : rawMeta["height"]; break; } default: if (typeof partialMeta["type"] === "string") throw new Error(`Not yet supported image format: '${partialMeta["type"]}'.`); else throw new TypeError(`Invalid type of 'partialMeta.type': '${typeof partialMeta["type"]}' (should be 'string')`); } if (validateImageMetadata(partialMeta)) return partialMeta; throw new TypeError("Malformed function return type! (" + JSON.stringify(partialMeta) + ")."); } //# sourceMappingURL=utils.js.map