524 lines
No EOL
28 KiB
JavaScript
524 lines
No EOL
28 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getCacheDirectory = getCacheDirectory;
|
|
exports.extractArchive = extractArchive;
|
|
exports.downloadBuilderToolset = downloadBuilderToolset;
|
|
exports.downloadElectronArtifactZip = downloadElectronArtifactZip;
|
|
exports.downloadElectronArtifact = downloadElectronArtifact;
|
|
exports.getBinariesMirrorUrl = getBinariesMirrorUrl;
|
|
const get = require("@electron/get");
|
|
const get_1 = require("@electron/get");
|
|
const builder_util_1 = require("builder-util");
|
|
const _7zip_1 = require("../toolsets/7zip");
|
|
const multiProgress_1 = require("electron-publish/out/multiProgress");
|
|
const fs_1 = require("fs");
|
|
const fs = require("fs/promises");
|
|
const crypto = require("crypto");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const lockfile = require("proper-lockfile");
|
|
const promises_1 = require("stream/promises");
|
|
const tar = require("tar");
|
|
const unzipper = require("unzipper");
|
|
const builder_util_runtime_1 = require("builder-util-runtime");
|
|
const cacheState_1 = require("./cacheState");
|
|
function hashUrlSafe(input, length = 6) {
|
|
let hash = 5381;
|
|
for (let i = 0; i < input.length; i++) {
|
|
hash = ((hash << 5) + hash) ^ input.charCodeAt(i);
|
|
}
|
|
hash >>>= 0;
|
|
const out = hash.toString(36);
|
|
return out.length >= length ? out.slice(0, length) : out.padStart(length, "0");
|
|
}
|
|
function getCacheDirectory(options) {
|
|
var _a, _b, _c, _d, _e;
|
|
const { isAvoidSystemOnWindows = true, allowEnvVarOverride } = options;
|
|
const env = (_a = process.env.ELECTRON_BUILDER_CACHE) === null || _a === void 0 ? void 0 : _a.trim();
|
|
if (allowEnvVarOverride && env && path.parse(env).root) {
|
|
return env;
|
|
}
|
|
const appName = "electron-builder";
|
|
const platform = os.platform();
|
|
const homeDir = os.homedir();
|
|
if (platform === "darwin") {
|
|
return path.join(homeDir, "Library", "Caches", appName);
|
|
}
|
|
if (platform === "win32") {
|
|
const localAppData = (_b = process.env.LOCALAPPDATA) === null || _b === void 0 ? void 0 : _b.trim();
|
|
const username = (_d = (_c = process.env.USERNAME) === null || _c === void 0 ? void 0 : _c.trim()) === null || _d === void 0 ? void 0 : _d.toLowerCase();
|
|
// https://github.com/electron-userland/electron-builder/issues/1164
|
|
const isSystemUser = isAvoidSystemOnWindows && (((_e = localAppData === null || localAppData === void 0 ? void 0 : localAppData.toLowerCase()) === null || _e === void 0 ? void 0 : _e.includes("\\windows\\system32\\")) || username === "system");
|
|
if (!localAppData || isSystemUser) {
|
|
return path.join(os.tmpdir(), `${appName}-cache`);
|
|
}
|
|
return path.join(localAppData, appName, "Cache");
|
|
}
|
|
const xdgCache = process.env.XDG_CACHE_HOME;
|
|
return xdgCache && path.parse(xdgCache).root ? path.join(xdgCache, appName) : path.join(homeDir, ".cache", appName);
|
|
}
|
|
function resolveCacheMode() {
|
|
var _a;
|
|
const varName = "ELECTRON_DOWNLOAD_CACHE_MODE";
|
|
const cacheOverride = (_a = process.env[varName]) === null || _a === void 0 ? void 0 : _a.trim();
|
|
if (cacheOverride && Number(cacheOverride) in get_1.ElectronDownloadCacheMode) {
|
|
const mode = Number(cacheOverride);
|
|
builder_util_1.log.debug({ mode }, `cache mode overridden via env var ${varName}`);
|
|
return mode;
|
|
}
|
|
return get_1.ElectronDownloadCacheMode.ReadWrite;
|
|
}
|
|
async function extractZipStreaming(file, dir) {
|
|
var _a;
|
|
// Pass 1: read central directory once to collect Unix modes (one seek to EOF)
|
|
const zipDir = await unzipper.Open.file(file);
|
|
const entryModes = new Map();
|
|
for (const entry of zipDir.files) {
|
|
const mode = (entry.externalFileAttributes >> 16) & 0xffff;
|
|
if (mode > 0) {
|
|
entryModes.set(entry.path, mode);
|
|
}
|
|
}
|
|
const isSymlink = (mode) => (mode & 0o170000) === 0o120000;
|
|
// Pass 2: stream from byte 0 — no per-entry seeks, no Docker hang
|
|
const entries = (0, fs_1.createReadStream)(file).pipe(unzipper.Parse({ forceStream: true }));
|
|
for await (const entry of entries) {
|
|
const destPath = path.resolve(dir, entry.path);
|
|
if (!destPath.startsWith(dir + path.sep) && destPath !== dir) {
|
|
throw new Error(`Path traversal blocked: ${entry.path}`);
|
|
}
|
|
const mode = (_a = entryModes.get(entry.path)) !== null && _a !== void 0 ? _a : 0;
|
|
if (mode > 0 && isSymlink(mode)) {
|
|
const target = (await entry.buffer()).toString();
|
|
if (path.isAbsolute(target)) {
|
|
throw new Error(`Absolute symlink target blocked: ${target}`);
|
|
}
|
|
const resolvedTarget = path.resolve(path.dirname(destPath), target);
|
|
if (!resolvedTarget.startsWith(dir + path.sep) && resolvedTarget !== dir) {
|
|
throw new Error(`Symlink target escapes extraction dir: ${target}`);
|
|
}
|
|
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
await fs.symlink(target, destPath);
|
|
}
|
|
else if (entry.type === "Directory") {
|
|
await fs.mkdir(destPath, { recursive: true });
|
|
entry.autodrain();
|
|
}
|
|
else {
|
|
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
await (0, promises_1.pipeline)(entry, (0, fs_1.createWriteStream)(destPath));
|
|
if (mode > 0) {
|
|
await fs.chmod(destPath, mode & 0o7777);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
async function extractArchive(file, dir) {
|
|
const tmpDir = `${dir}.tmp`;
|
|
await fs.mkdir(tmpDir, { recursive: true });
|
|
const release = await lockfile.lock(tmpDir, {
|
|
// 100 retries (not 15) so concurrent callers wait out a slow first extraction instead of failing
|
|
// with ELOCKED; the update heartbeat keeps an in-progress holder's lock fresh against `stale`.
|
|
retries: { retries: 100, minTimeout: 1000, maxTimeout: 5000 },
|
|
stale: 120000, // Increased from 60s to allow long-running extractions
|
|
});
|
|
try {
|
|
// rm + mkdir happen AFTER acquiring the lock so no concurrent caller can clear our tmpDir mid-extraction
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
await fs.mkdir(tmpDir, { recursive: true });
|
|
// Guard against the transient window in @electron/get's non-atomic putFileInCache (remove → move).
|
|
// A concurrent worker may have deleted and not yet replaced the source archive; wait briefly for it.
|
|
for (let i = 0; !(await (0, builder_util_1.exists)(file)); i++) {
|
|
if (i >= 4) {
|
|
throw Object.assign(new Error(`Source archive not found after retries: ${file}`), { code: "ENOENT", path: file });
|
|
}
|
|
builder_util_1.log.warn({ file, attempt: i + 1 }, "source archive transiently missing, retrying");
|
|
await new Promise(r => setTimeout(r, 300 * (i + 1)));
|
|
}
|
|
if (file.endsWith(".tar.gz") || file.endsWith(".tgz")) {
|
|
await tar.extract({ file, cwd: tmpDir, strip: 1 });
|
|
}
|
|
else if (file.endsWith(".tar.xz") || file.endsWith(".txz")) {
|
|
// node-tar cannot decompress xz, so use 7za to turn the .tar.xz into a .tar, then extract that tar.
|
|
const cmd7za = await (0, _7zip_1.getPath7za)();
|
|
const xzOutDir = `${tmpDir}.xz`;
|
|
await fs.rm(xzOutDir, { recursive: true, force: true });
|
|
await fs.mkdir(xzOutDir, { recursive: true });
|
|
try {
|
|
await (0, builder_util_1.exec)(cmd7za, ["x", "-bd", file, (0, builder_util_1.to7zaOutputSwitch)((0, builder_util_1.sanitizeDirPath)(xzOutDir)), "-y"]);
|
|
const innerTar = (await fs.readdir(xzOutDir)).find(f => f.endsWith(".tar"));
|
|
if (innerTar == null) {
|
|
throw new Error(`xz decompression of ${path.basename(file)} produced no .tar archive`);
|
|
}
|
|
await tar.extract({ file: path.join(xzOutDir, innerTar), cwd: tmpDir, strip: 1 });
|
|
}
|
|
finally {
|
|
await fs.rm(xzOutDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
else if (file.endsWith(".zip")) {
|
|
await extractZipStreaming(file, tmpDir);
|
|
}
|
|
else if (file.endsWith(".7z")) {
|
|
const cmd7za = await (0, _7zip_1.getPath7za)();
|
|
try {
|
|
await (0, builder_util_1.exec)(cmd7za, ["x", "-bd", file, (0, builder_util_1.to7zaOutputSwitch)((0, builder_util_1.sanitizeDirPath)(tmpDir)), "-y"]);
|
|
}
|
|
catch (e) {
|
|
// Check if extraction actually failed or just had benign warnings
|
|
const files = await fs.readdir(tmpDir);
|
|
if (files.length === 0) {
|
|
builder_util_1.log.warn({ file, tmpDir, error: e.message }, "7z extraction produced no output");
|
|
throw new Error(`7z extraction failed for ${file}: ${e.message}`);
|
|
}
|
|
// If files were extracted despite the error, log and continue
|
|
builder_util_1.log.warn({ error: e.message, filesExtracted: files.length }, "7z reported error but extracted files");
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Unsupported archive format: ${path.basename(file)}`);
|
|
}
|
|
// Verify extraction produced files
|
|
const extractedFiles = await fs.readdir(tmpDir);
|
|
if (extractedFiles.length === 0) {
|
|
throw new Error(`Extraction of ${path.basename(file)} produced no files`);
|
|
}
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
await fs.rename(tmpDir, dir);
|
|
}
|
|
finally {
|
|
await release().catch(err => builder_util_1.log.warn({ err }, "failed to release lockfile"));
|
|
}
|
|
}
|
|
async function downloadArtifactToFile(config, label) {
|
|
var _a, _b, _c, _d;
|
|
// Serialize concurrent downloads of the same artifact across vitest workers to prevent @electron/get's
|
|
// non-atomic putFileInCache (remove + move) from racing with a concurrent reader.
|
|
const artifactLockKey = crypto
|
|
.createHash("sha256")
|
|
.update(JSON.stringify({ v: config.version, p: config.platform, a: config.arch, n: config.artifactName }))
|
|
.digest("hex")
|
|
.slice(0, 20);
|
|
const artifactLockPath = path.join(os.tmpdir(), `eb-dl-${artifactLockKey}.lock`);
|
|
// This lock is taken when the artifact is not yet in @electron/get's download cache. 100 retries
|
|
// matches the other toolset locks; stale (10min) + proper-lockfile's update heartbeat keep a live
|
|
// downloader's lock fresh so the longer wait never falsely steals an in-progress download.
|
|
const releaseArtifactLock = await lockfile.lock(artifactLockPath, {
|
|
retries: { retries: 100, minTimeout: 500, maxTimeout: 5000 },
|
|
stale: 600000,
|
|
realpath: false,
|
|
});
|
|
let lastLoggedMilestone = -1;
|
|
const state = { bar: undefined };
|
|
const downloadOptions = {
|
|
timeout: { request: 10 * 60 * 1000 }, // prevent indefinite hang on stalled connections
|
|
...config.downloadOptions,
|
|
agent: (_b = (_a = config.downloadOptions) === null || _a === void 0 ? void 0 : _a.agent) !== null && _b !== void 0 ? _b : (0, builder_util_1.buildGotProxyAgent)(),
|
|
getProgressCallback: info => {
|
|
// @electron/get passes downloadOptions (including this callback) to its internal
|
|
// SHASUMS256.txt validation download. That file is tiny (<1 MB) and fires at 100%
|
|
// immediately, producing a spurious bar even when the artifact itself is cached.
|
|
// Skip progress display for any download whose total size is known and small.
|
|
if (info.total && info.total < 1000000) {
|
|
return Promise.resolve();
|
|
}
|
|
if (!state.bar && process.stdout.isTTY) {
|
|
builder_util_1.log.info({ label }, "downloading");
|
|
state.bar = new multiProgress_1.MultiProgress().createBar(`${" ".repeat(builder_util_1.PADDING + 2)}[:bar] :percent | ${label}`, { total: 100 });
|
|
}
|
|
const pct = info.percent != null ? Math.floor(info.percent * 100) : 0;
|
|
if (state.bar) {
|
|
state.bar.update(pct);
|
|
}
|
|
else {
|
|
// log every 25% milestone for non-TTY environments (e.g. CI logs) to provide some visibility into download progress without overwhelming logs with too many updates
|
|
const percentCompleted = info.transferred && info.total ? Math.floor((info.transferred / info.total) * 100) : Math.floor(pct / 25) * 25;
|
|
if (percentCompleted > lastLoggedMilestone && percentCompleted > 0) {
|
|
lastLoggedMilestone = percentCompleted;
|
|
builder_util_1.log.info({ label, progress: `${percentCompleted}%` }, percentCompleted !== 100 ? "downloading" : "downloaded");
|
|
}
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
};
|
|
const configWithProgress = { ...config, downloadOptions };
|
|
try {
|
|
let filePath;
|
|
try {
|
|
filePath = await (0, builder_util_runtime_1.retry)(() => get.downloadArtifact(configWithProgress), {
|
|
retries: 3,
|
|
interval: 2000,
|
|
backoff: 2000,
|
|
shouldRetry: (e) => {
|
|
var _a;
|
|
if (e instanceof builder_util_runtime_1.HttpError) {
|
|
return e.isServerError();
|
|
}
|
|
if (typeof ((_a = e === null || e === void 0 ? void 0 : e.response) === null || _a === void 0 ? void 0 : _a.statusCode) === "number") {
|
|
return e.response.statusCode >= 500;
|
|
}
|
|
return typeof (e === null || e === void 0 ? void 0 : e.code) === "string" && ["ENOTFOUND", "ETIMEDOUT", "ECONNRESET", "EPIPE", "ENOENT"].includes(e.code);
|
|
},
|
|
});
|
|
}
|
|
catch (err) {
|
|
if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string" && err.message.includes("dest already exists")) {
|
|
filePath = await get.downloadArtifact(configWithProgress);
|
|
}
|
|
else {
|
|
throw err;
|
|
}
|
|
}
|
|
if (!(await (0, builder_util_1.exists)(filePath))) {
|
|
builder_util_1.log.warn({ filePath, label }, "cached artifact missing from disk; retrying with cache write");
|
|
filePath = await get.downloadArtifact({ ...configWithProgress, cacheMode: get_1.ElectronDownloadCacheMode.WriteOnly });
|
|
}
|
|
if (!state.bar && lastLoggedMilestone === -1) {
|
|
builder_util_1.log.info({ label }, "using cached artifact");
|
|
}
|
|
return filePath;
|
|
}
|
|
finally {
|
|
(_c = state.bar) === null || _c === void 0 ? void 0 : _c.update(100);
|
|
(_d = state.bar) === null || _d === void 0 ? void 0 : _d.terminate();
|
|
await releaseArtifactLock().catch(err => builder_util_1.log.warn({ err }, "failed to release artifact download lock"));
|
|
}
|
|
}
|
|
/**
|
|
* Checks electron-builder's own archive cache for a previously downloaded archive.
|
|
* Validates the SHA-256 checksum when one is known. Returns the cached path on hit,
|
|
* null on miss or checksum mismatch (mismatch also deletes the stale file).
|
|
*/
|
|
async function resolveFromArchiveCache(archiveCachePath, label, expectedSha256) {
|
|
if (!(await (0, builder_util_1.exists)(archiveCachePath))) {
|
|
return null;
|
|
}
|
|
if (expectedSha256) {
|
|
const hash = await new Promise((resolve, reject) => {
|
|
const h = crypto.createHash("sha256");
|
|
const s = (0, fs_1.createReadStream)(archiveCachePath);
|
|
s.on("error", reject);
|
|
s.on("data", (chunk) => h.update(chunk));
|
|
s.on("end", () => resolve(h.digest("hex")));
|
|
});
|
|
if (hash !== expectedSha256) {
|
|
builder_util_1.log.warn({ file: label, archiveCachePath }, "cached archive checksum mismatch — removing and re-downloading");
|
|
await fs.rm(archiveCachePath).catch(() => { });
|
|
return null;
|
|
}
|
|
}
|
|
builder_util_1.log.debug({ file: label, archiveCachePath }, "using cached archive — skipping download");
|
|
return archiveCachePath;
|
|
}
|
|
/**
|
|
* Copies a freshly downloaded archive into electron-builder's own archive cache so that
|
|
* future builds (including offline environments) can skip the network request entirely.
|
|
*/
|
|
async function persistToArchiveCache(sourcePath, archiveCachePath) {
|
|
try {
|
|
await fs.mkdir(path.dirname(archiveCachePath), { recursive: true });
|
|
await fs.copyFile(sourcePath, archiveCachePath);
|
|
}
|
|
catch (err) {
|
|
builder_util_1.log.warn({ err, archiveCachePath }, "failed to persist archive to cache — will re-download next time");
|
|
}
|
|
}
|
|
/**
|
|
* Core download + extract engine. Handles lockfile, cache-hit short-circuit,
|
|
* progress bar, extraction (.zip or .tar.gz), and completion marker.
|
|
* Both public download functions delegate here after building their respective configs.
|
|
*/
|
|
async function downloadAndExtract(config, extractDir, label, archiveCachePath) {
|
|
var _a;
|
|
await fs.mkdir(extractDir, { recursive: true });
|
|
// Pre-lock fast path: only short-circuit for a definitively complete and valid cache.
|
|
// Do NOT clean up here — concurrent processes could race to delete under each other.
|
|
const stateData = await (0, cacheState_1.readCacheStateFile)(extractDir);
|
|
if ((stateData === null || stateData === void 0 ? void 0 : stateData.state) === cacheState_1.CacheState.complete) {
|
|
const isValid = await (0, cacheState_1.validateCacheDirectory)(extractDir, stateData.fileCount);
|
|
if (isValid) {
|
|
builder_util_1.log.debug({ file: label, path: extractDir }, "using cached artifact - cache valid");
|
|
return extractDir;
|
|
}
|
|
}
|
|
// Be patient: when many targets/builds contend on the same toolset cache concurrently, all but the
|
|
// first wait here while the winner downloads+extracts (a large bundle can take >1min). 15 retries
|
|
// (~67s) is too few and waiters fail with ELOCKED before the winner finishes; 100 matches the
|
|
// patience of withToolsetLock. proper-lockfile's update heartbeat keeps a live holder's lock fresh,
|
|
// so increasing waiter retries never falsely steals an in-progress extraction.
|
|
const release = await lockfile.lock(extractDir, {
|
|
retries: { retries: 100, minTimeout: 1000, maxTimeout: 5000 },
|
|
stale: 120000,
|
|
});
|
|
let downloadedFile = null;
|
|
try {
|
|
// Re-check after acquiring lock: another worker may have completed while we waited.
|
|
const stateDataAfterLock = await (0, cacheState_1.readCacheStateFile)(extractDir);
|
|
if ((stateDataAfterLock === null || stateDataAfterLock === void 0 ? void 0 : stateDataAfterLock.state) === cacheState_1.CacheState.complete) {
|
|
const isValid = await (0, cacheState_1.validateCacheDirectory)(extractDir, stateDataAfterLock.fileCount);
|
|
if (isValid) {
|
|
builder_util_1.log.debug({ file: label, path: extractDir }, "using cached artifact - skipping download/extract");
|
|
return extractDir;
|
|
}
|
|
builder_util_1.log.warn({ extractDir }, "Cache marked complete but files invalid, clearing");
|
|
}
|
|
else if ((stateDataAfterLock === null || stateDataAfterLock === void 0 ? void 0 : stateDataAfterLock.state) === cacheState_1.CacheState.corrupted) {
|
|
builder_util_1.log.warn({ extractDir }, "Corrupted cache detected, cleaning up and re-extracting");
|
|
}
|
|
// Cleanup inside the lock — skip lock files since we currently hold them
|
|
await (0, cacheState_1.cleanupCacheDirectory)(extractDir, { skipLockFiles: true });
|
|
await fs.mkdir(extractDir, { recursive: true });
|
|
// Check electron-builder's own archive cache before touching @electron/get.
|
|
// The cache lives alongside the extract dir: <cacheDir>/<releaseName>/<filename>.
|
|
// This lets repeated builds (or offline environments) skip the download entirely once
|
|
// the archive has been fetched at least once.
|
|
if (archiveCachePath) {
|
|
downloadedFile = await resolveFromArchiveCache(archiveCachePath, label, (_a = config.checksums) === null || _a === void 0 ? void 0 : _a[label]);
|
|
}
|
|
if (!downloadedFile) {
|
|
builder_util_1.log.debug({ file: label }, "downloading");
|
|
downloadedFile = await downloadArtifactToFile(config, label);
|
|
if (!downloadedFile) {
|
|
throw new Error(`Failed to download artifact: ${label}`);
|
|
}
|
|
// Persist the downloaded archive so future builds (and offline environments) can
|
|
// skip the network request entirely.
|
|
if (archiveCachePath) {
|
|
await persistToArchiveCache(downloadedFile, archiveCachePath);
|
|
}
|
|
}
|
|
await (0, cacheState_1.writeCacheState)(extractDir, cacheState_1.CacheState.downloaded);
|
|
// Mark extracting so stale-lock detection can recover a crashed mid-extraction process
|
|
await (0, cacheState_1.writeCacheState)(extractDir, cacheState_1.CacheState.extracting);
|
|
// Extract while holding the lock to prevent concurrent interference
|
|
await extractArchive(downloadedFile, extractDir);
|
|
// Compute metadata with a full recursive walk now that tmpDir has been renamed to extractDir
|
|
const { fileCount, extractedSize } = await (0, cacheState_1.computeCacheMetadata)(extractDir);
|
|
// Write complete state with metadata BEFORE releasing the lock; throwOnError=true so a
|
|
// failed write (disk full, permissions) propagates instead of silently producing a no-op cache
|
|
await (0, cacheState_1.writeCacheState)(extractDir, cacheState_1.CacheState.complete, { fileCount, extractedSize }, true);
|
|
}
|
|
finally {
|
|
await release().catch(err => builder_util_1.log.warn({ err }, "failed to release lockfile"));
|
|
}
|
|
return extractDir;
|
|
}
|
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
/**
|
|
* Downloads a generic artifact (.tar.gz or .zip) from a GitHub release.
|
|
* Used for electron-builder-binaries tools (appimage, etc.).
|
|
*/
|
|
async function downloadBuilderToolset(options) {
|
|
const { releaseName, filenameWithExt, checksums, githubOrgRepo = "electron-userland/electron-builder-binaries", overrideUrl } = options;
|
|
if (/[/\\]|\.\./.test(filenameWithExt)) {
|
|
throw new Error(`downloadBuilderToolset: unsafe filenameWithExt "${filenameWithExt}" — must be a plain filename with no path separators or traversal sequences`);
|
|
}
|
|
const baseUrl = getBinariesMirrorUrl(githubOrgRepo);
|
|
const fullUrl = overrideUrl ? `${overrideUrl}/${filenameWithExt}` : `${baseUrl}${releaseName}/${filenameWithExt}`;
|
|
const suffix = hashUrlSafe(fullUrl, 5);
|
|
const folderName = `${filenameWithExt.replace(/\.(tar\.gz|tgz|tar\.xz|txz|zip|7z)$/, "")}-${suffix}`;
|
|
const extractDir = path.join(getCacheDirectory({ allowEnvVarOverride: true }), releaseName, folderName);
|
|
// Use resolveAssetURL so @electron/get's ELECTRON_MIRROR env var check cannot override
|
|
// the builder-binaries URL we've already resolved (see getArtifactRemoteURL in @electron/get).
|
|
const mirrorOptions = {
|
|
resolveAssetURL: async () => Promise.resolve(fullUrl),
|
|
};
|
|
// Predictable archive cache: <cacheDir>/<releaseName>/<filename>, next to the extract dir.
|
|
// downloadAndExtract checks here before touching @electron/get and persists the archive here
|
|
// after every successful download, so subsequent builds never need a network round-trip.
|
|
const archiveCachePath = path.join(getCacheDirectory({ allowEnvVarOverride: true }), releaseName, filenameWithExt);
|
|
const config = {
|
|
version: "9.9.9", // must be >1.3.2 to bypass @electron/get validation shortcut
|
|
artifactName: filenameWithExt,
|
|
cacheRoot: path.resolve(getCacheDirectory({ allowEnvVarOverride: true }), "downloads"),
|
|
cacheMode: resolveCacheMode(),
|
|
...(checksums != null ? { checksums } : { unsafelyDisableChecksums: true }),
|
|
mirrorOptions,
|
|
isGeneric: true,
|
|
};
|
|
return downloadAndExtract(config, extractDir, filenameWithExt, archiveCachePath);
|
|
}
|
|
// Keys present in ElectronGetOptions but absent from ElectronDownloadOptions.
|
|
// Used to discriminate between the two config shapes at runtime.
|
|
const ELECTRON_GET_EXCLUSIVE_KEYS = ["mirrorOptions", "force", "unsafelyDisableChecksums"];
|
|
function isElectronGetOptions(dl) {
|
|
return ELECTRON_GET_EXCLUSIVE_KEYS.some(k => Object.hasOwnProperty.call(dl, k));
|
|
}
|
|
/**
|
|
* Downloads and extracts an electron platform artifact (e.g. ffmpeg) using @electron/get.
|
|
* Deduplicates concurrent calls for the same artifact within the same process.
|
|
*/
|
|
function buildElectronArtifactConfig(options) {
|
|
const { electronDownload, arch, version, platformName: platform, artifactName, cacheDir: cacheRoot } = options;
|
|
let artifactConfig = { cacheRoot, platform, arch, version, artifactName };
|
|
if (electronDownload != null) {
|
|
if (isElectronGetOptions(electronDownload)) {
|
|
const { mirrorOptions, ...rest } = electronDownload;
|
|
artifactConfig = { ...artifactConfig, ...rest, cacheRoot, mirrorOptions };
|
|
}
|
|
else {
|
|
const { mirror, customDir, cache, customFilename, isVerifyChecksum, strictSSL, platform: overridePlatform, arch: overrideArch } = electronDownload;
|
|
// strictSSL: false disables TLS certificate validation for all
|
|
// electron/tool downloads. This option exists only for air-gapped or
|
|
// self-signed-cert environments; ensure your build network is fully trusted.
|
|
if (strictSSL === false) {
|
|
builder_util_1.log.warn({ option: "electronDownload.strictSSL" }, "strictSSL is false — TLS certificate validation is DISABLED for Electron downloads. Only use this option in a trusted, isolated build environment.");
|
|
}
|
|
artifactConfig = {
|
|
...artifactConfig,
|
|
unsafelyDisableChecksums: isVerifyChecksum === false,
|
|
cacheRoot: cache !== null && cache !== void 0 ? cache : cacheRoot,
|
|
mirrorOptions: {
|
|
mirror: mirror || undefined,
|
|
customDir: customDir || undefined,
|
|
customFilename: customFilename || undefined,
|
|
},
|
|
...(strictSSL === false ? { downloadOptions: { https: { rejectUnauthorized: false } } } : {}),
|
|
};
|
|
if (overridePlatform != null) {
|
|
artifactConfig.platform = overridePlatform;
|
|
}
|
|
if (overrideArch != null) {
|
|
artifactConfig.arch = overrideArch;
|
|
}
|
|
}
|
|
}
|
|
artifactConfig.cacheMode = resolveCacheMode();
|
|
return artifactConfig;
|
|
}
|
|
/**
|
|
* Downloads the electron artifact zip via @electron/get (with caching) and returns the zip file path.
|
|
* Use when you need to extract the zip yourself (e.g. directly to appOutDir to preserve empty dirs and symlinks).
|
|
*/
|
|
function downloadElectronArtifactZip(options) {
|
|
const config = buildElectronArtifactConfig(options);
|
|
return downloadArtifactToFile(config, config.artifactName);
|
|
}
|
|
async function downloadElectronArtifact(options) {
|
|
const { arch, version, platformName: platform, artifactName } = options;
|
|
const artifactConfig = buildElectronArtifactConfig(options);
|
|
const suffix = hashUrlSafe(JSON.stringify(artifactConfig), 5);
|
|
const folderName = `${artifactName}-v${version}-${platform}-${arch}-${suffix}`;
|
|
const extractDir = path.join(getCacheDirectory({ allowEnvVarOverride: true }), `${artifactName}-v${version}`, folderName);
|
|
return downloadAndExtract(artifactConfig, extractDir, artifactName);
|
|
}
|
|
/**
|
|
* Get the binaries mirror URL from environment variables.
|
|
* Supports various npm config formats and falls back to GitHub.
|
|
*/
|
|
function getBinariesMirrorUrl(githubOrgRepo) {
|
|
const allowHttp = process.env["ELECTRON_BUILDER_BINARIES_ALLOW_HTTP"] === "true";
|
|
for (const envVar of [
|
|
"NPM_CONFIG_ELECTRON_BUILDER_BINARIES_MIRROR",
|
|
"npm_config_electron_builder_binaries_mirror",
|
|
"npm_package_config_electron_builder_binaries_mirror",
|
|
"ELECTRON_BUILDER_BINARIES_MIRROR",
|
|
]) {
|
|
const url = (0, builder_util_1.parseValidEnvVarUrl)(envVar, allowHttp);
|
|
if (url) {
|
|
return url.endsWith("/") ? url : `${url}/`;
|
|
}
|
|
}
|
|
return `https://github.com/${githubOrgRepo}/releases/download/`;
|
|
}
|
|
//# sourceMappingURL=electronGet.js.map
|