UNPKG

app-builder-lib

Version:
524 lines 28.9 kB
"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