chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const electronRoot = path.join(__dirname, "..");
|
||||
const nextjsDir = path.join(electronRoot, "..", "servers", "nextjs");
|
||||
const outDir = path.join(electronRoot, "resources", "nextjs");
|
||||
const nextBuildDir = path.join(nextjsDir, ".next-build");
|
||||
const standaloneDir = path.join(nextBuildDir, "standalone");
|
||||
|
||||
function rm(p) {
|
||||
fs.rmSync(p, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function cpDir(src, dest) {
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.cpSync(src, dest, { recursive: true });
|
||||
}
|
||||
|
||||
console.log("Running Next.js production build (BUILD_TARGET=electron)…");
|
||||
|
||||
rm(outDir);
|
||||
rm(nextBuildDir);
|
||||
|
||||
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const build = spawnSync(npmCmd, ["run", "build"], {
|
||||
cwd: nextjsDir,
|
||||
env: { ...process.env, BUILD_TARGET: "electron" },
|
||||
stdio: "inherit",
|
||||
// Windows: cmd is required to run npm.cmd; without shell, spawnSync can throw EINVAL.
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
if (build.error) {
|
||||
console.error(build.error);
|
||||
process.exit(1);
|
||||
}
|
||||
if (build.status !== 0) {
|
||||
process.exit(build.status ?? 1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(standaloneDir)) {
|
||||
console.error("Expected standalone output at:", standaloneDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.join(outDir, ".next-build"), { recursive: true });
|
||||
|
||||
for (const name of fs.readdirSync(standaloneDir)) {
|
||||
fs.cpSync(
|
||||
path.join(standaloneDir, name),
|
||||
path.join(outDir, name),
|
||||
{ recursive: true }
|
||||
);
|
||||
}
|
||||
|
||||
// Next.js 16 standalone traces the app under servers/nextjs/; the server process
|
||||
// runs from that directory, so static assets and public files must live beside
|
||||
// server.js — not only at the bundle root (older Next versions used a flatter layout).
|
||||
const nestedStandaloneDir = path.join(outDir, "servers", "nextjs");
|
||||
|
||||
const staticSrc = path.join(nextBuildDir, "static");
|
||||
const staticDestinations = [
|
||||
path.join(outDir, ".next-build", "static"),
|
||||
path.join(nestedStandaloneDir, ".next-build", "static"),
|
||||
];
|
||||
if (fs.existsSync(staticSrc)) {
|
||||
for (const staticDest of staticDestinations) {
|
||||
cpDir(staticSrc, staticDest);
|
||||
}
|
||||
} else {
|
||||
console.error("Expected Next.js static output at:", staticSrc);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const publicDir = path.join(nextjsDir, "public");
|
||||
if (fs.existsSync(publicDir)) {
|
||||
cpDir(publicDir, path.join(outDir, "public"));
|
||||
if (fs.existsSync(nestedStandaloneDir)) {
|
||||
cpDir(publicDir, path.join(nestedStandaloneDir, "public"));
|
||||
}
|
||||
}
|
||||
|
||||
const templatesSrc = path.join(nextjsDir, "app", "presentation-templates");
|
||||
const templatesDest = path.join(outDir, "presentation-templates");
|
||||
if (fs.existsSync(templatesSrc)) {
|
||||
cpDir(templatesSrc, templatesDest);
|
||||
}
|
||||
|
||||
console.log("Next.js bundle copied to:", outDir);
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require("typescript");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
const appDistDir = path.join(repoRoot, "app_dist");
|
||||
|
||||
const rootBuildFiles = ["build.js"].map((file) => path.join(repoRoot, file));
|
||||
|
||||
const scriptsDir = path.join(repoRoot, "scripts");
|
||||
const scriptBuildFiles = walkFiles(
|
||||
scriptsDir,
|
||||
(file) => file.endsWith(".cjs") || file.endsWith(".js"),
|
||||
);
|
||||
|
||||
function walkFiles(dir, predicate, results = []) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return results;
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkFiles(fullPath, predicate, results);
|
||||
} else if (predicate(fullPath)) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
const generatedMainFiles = walkFiles(appDistDir, (file) => file.endsWith(".js"));
|
||||
|
||||
if (generatedMainFiles.length === 0) {
|
||||
console.error(
|
||||
"No generated Electron main files found in app_dist. Run `npm run build:ts` first.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = [
|
||||
...generatedMainFiles,
|
||||
...rootBuildFiles.filter((file) => fs.existsSync(file)),
|
||||
...scriptBuildFiles,
|
||||
];
|
||||
|
||||
const program = ts.createProgram(files, {
|
||||
allowJs: true,
|
||||
checkJs: true,
|
||||
noEmit: true,
|
||||
skipLibCheck: true,
|
||||
strict: false,
|
||||
noImplicitAny: false,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
moduleResolution: ts.ModuleResolutionKind.Node10,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
});
|
||||
|
||||
const undefinedNameDiagnostics = ts
|
||||
.getPreEmitDiagnostics(program)
|
||||
.filter((diagnostic) => diagnostic.code === 2304);
|
||||
|
||||
if (undefinedNameDiagnostics.length > 0) {
|
||||
const host = {
|
||||
getCanonicalFileName: (fileName) => fileName,
|
||||
getCurrentDirectory: () => repoRoot,
|
||||
getNewLine: () => "\n",
|
||||
};
|
||||
|
||||
console.error(
|
||||
ts.formatDiagnosticsWithColorAndContext(undefinedNameDiagnostics, host),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`No undefined names found in ${files.length} Electron main JS files.`);
|
||||
@@ -0,0 +1,35 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const electronRoot = path.join(__dirname, "..");
|
||||
const fastapiDir = path.join(electronRoot, "..", "servers", "fastapi");
|
||||
const resourcesFastapiDir = path.join(electronRoot, "resources", "fastapi");
|
||||
|
||||
const sources = [
|
||||
{ name: "static", src: path.join(fastapiDir, "static"), dest: path.join(resourcesFastapiDir, "static") },
|
||||
{ name: "assets", src: path.join(fastapiDir, "assets"), dest: path.join(resourcesFastapiDir, "assets") },
|
||||
];
|
||||
|
||||
function copyDir(src, dest) {
|
||||
if (!fs.existsSync(src)) {
|
||||
throw new Error(`Source directory not found: ${src}`);
|
||||
}
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
fs.cpSync(src, dest, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function main() {
|
||||
fs.mkdirSync(resourcesFastapiDir, { recursive: true });
|
||||
|
||||
for (const { name, src, dest } of sources) {
|
||||
console.log(`[fastapi-assets] Copying ${name} -> ${dest}`);
|
||||
copyDir(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`[fastapi-assets] ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const electronRoot = path.join(__dirname, "..");
|
||||
const fastapiDir = path.join(electronRoot, "..", "servers", "fastapi");
|
||||
const uvCmd = process.platform === "win32" ? "uv.exe" : "uv";
|
||||
const requiredModel = process.env.MEM0_SPACY_MODEL || "en_core_web_sm";
|
||||
const strictMode =
|
||||
(process.env.MEM0_SPACY_STRICT || "").trim().toLowerCase() === "true";
|
||||
const venvDir = path.join(fastapiDir, ".venv");
|
||||
|
||||
function runUv(args, description) {
|
||||
const result = spawnSync(uvCmd, args, {
|
||||
cwd: fastapiDir,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`${description} failed: ${result.error.message}`);
|
||||
}
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
function runUvPython(args, description) {
|
||||
return runUv(["run", "python", ...args], description);
|
||||
}
|
||||
|
||||
function ensureVenv() {
|
||||
if (fs.existsSync(venvDir)) {
|
||||
return true;
|
||||
}
|
||||
console.log(`[spacy-setup] Creating uv venv at ${venvDir}`);
|
||||
return runUv(["venv"], "uv venv");
|
||||
}
|
||||
|
||||
function hasModelInstalled() {
|
||||
return runUvPython(
|
||||
["-c", `import spacy; spacy.load("${requiredModel}")`],
|
||||
`spaCy model check (${requiredModel})`,
|
||||
);
|
||||
}
|
||||
|
||||
function installModel() {
|
||||
return runUvPython(
|
||||
["-m", "spacy", "download", requiredModel],
|
||||
`spaCy model install (${requiredModel})`,
|
||||
);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!ensureVenv()) {
|
||||
throw new Error("Failed to create uv virtual environment");
|
||||
}
|
||||
console.log(`[spacy-setup] Checking spaCy model: ${requiredModel}`);
|
||||
if (hasModelInstalled()) {
|
||||
console.log(
|
||||
`[spacy-setup] spaCy model already available: ${requiredModel}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[spacy-setup] Installing spaCy model: ${requiredModel}`);
|
||||
const installed = installModel();
|
||||
if (installed && hasModelInstalled()) {
|
||||
console.log(`[spacy-setup] spaCy model installed: ${requiredModel}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
`[spacy-setup] Could not install spaCy model (${requiredModel}). ` +
|
||||
"Mem0 will self-disable at runtime if this dependency is unavailable.";
|
||||
|
||||
if (strictMode) {
|
||||
throw new Error(message);
|
||||
}
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`[spacy-setup] ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const electronRoot = path.join(__dirname, "..");
|
||||
const pkg = JSON.parse(
|
||||
fs.readFileSync(path.join(electronRoot, "package.json"), "utf8"),
|
||||
);
|
||||
let existing = {};
|
||||
try {
|
||||
existing = JSON.parse(
|
||||
fs.readFileSync(path.join(electronRoot, "version.json"), "utf8"),
|
||||
);
|
||||
} catch (_) {}
|
||||
|
||||
const version = pkg.version;
|
||||
|
||||
const update = {
|
||||
version,
|
||||
message: process.env.UPDATE_MESSAGE || existing.message || "",
|
||||
downloads: {
|
||||
linux: `https://github.com/presenton/presenton/releases/download/electron-v${version}/Presenton-${version}.deb`,
|
||||
mac: `https://github.com/presenton/presenton/releases/download/electron-v${version}/Presenton-${version}.dmg`,
|
||||
windows: `https://github.com/presenton/presenton/releases/download/electron-v${version}/Presenton-${version}.exe`,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(electronRoot, "version.json"),
|
||||
JSON.stringify(update, null, 2),
|
||||
);
|
||||
|
||||
console.log("version.json generated");
|
||||
@@ -0,0 +1,466 @@
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
const {
|
||||
Browser,
|
||||
computeExecutablePath,
|
||||
detectBrowserPlatform,
|
||||
install,
|
||||
} = require("@puppeteer/browsers");
|
||||
|
||||
const buildId = (process.env.EXPORT_CHROME_BUILD_ID || "146.0.7680.76").trim();
|
||||
const cacheDir = path.join(__dirname, "..", "resources", "chromium");
|
||||
const manifestPath = path.join(cacheDir, "presenton-runtime.json");
|
||||
const windowsRequiredRuntimeFiles = [
|
||||
"chrome.dll",
|
||||
"chrome_100_percent.pak",
|
||||
"chrome_200_percent.pak",
|
||||
"icudtl.dat",
|
||||
"resources.pak",
|
||||
"v8_context_snapshot.bin",
|
||||
path.join("locales", "en-US.pak"),
|
||||
];
|
||||
|
||||
function getRevisionDir(platform) {
|
||||
return path.join(cacheDir, Browser.CHROME, `${platform}-${buildId}`);
|
||||
}
|
||||
|
||||
function fileLooksPresent(filePath) {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
return stat.isFile() && stat.size > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateWindowsRuntimeLayout(executablePath) {
|
||||
if (!fileLooksPresent(executablePath)) {
|
||||
return { ok: false, reason: `Chromium executable is missing: ${executablePath}` };
|
||||
}
|
||||
|
||||
const chromeDir = path.dirname(executablePath);
|
||||
const missingFiles = windowsRequiredRuntimeFiles.filter(
|
||||
(fileName) => !fileLooksPresent(path.join(chromeDir, fileName))
|
||||
);
|
||||
if (missingFiles.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `Chromium runtime layout is incomplete. Missing: ${missingFiles.join(", ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function runtimeLooksComplete(executablePath) {
|
||||
if (!fs.existsSync(executablePath)) {
|
||||
return false;
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
return macChromiumBundleLooksCodeSignReady(executablePath);
|
||||
}
|
||||
if (process.platform !== "win32") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validateWindowsRuntimeLayout(executablePath).ok;
|
||||
}
|
||||
|
||||
function removeProbeProfile(profileDir) {
|
||||
try {
|
||||
fs.rmSync(profileDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 10,
|
||||
retryDelay: 100,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[Chromium] Could not remove temporary probe profile ${profileDir}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateExecutable(executablePath) {
|
||||
if (process.platform === "win32") {
|
||||
// Windows Chrome startup can be slow or session-dependent during packaging.
|
||||
// Runtime export still probes launchability, so the build validates the bundle.
|
||||
return validateWindowsRuntimeLayout(executablePath);
|
||||
}
|
||||
|
||||
if (!runtimeLooksComplete(executablePath)) {
|
||||
return { ok: false, reason: "Chromium runtime layout is incomplete." };
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
const appBundlePath = findAppBundle(executablePath);
|
||||
const result = spawnSync(
|
||||
"codesign",
|
||||
["--verify", "--deep", "--strict", appBundlePath],
|
||||
{
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
const detail = result.error?.message || result.stderr || `status=${result.status}`;
|
||||
return { ok: false, reason: detail.trim() };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
const result = spawnSync(executablePath, ["--version"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
const detail = result.error?.message || result.stderr || `status=${result.status}`;
|
||||
return { ok: false, reason: detail.trim() };
|
||||
}
|
||||
if (!/chrome|chromium/i.test(result.stdout || "")) {
|
||||
return { ok: false, reason: "Chromium version probe produced unexpected output." };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const profileDir = fs.mkdtempSync(path.join(os.tmpdir(), "presenton-chromium-probe-"));
|
||||
try {
|
||||
const result = spawnSync(
|
||||
executablePath,
|
||||
[
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-extensions",
|
||||
"--disable-crash-reporter",
|
||||
"--no-first-run",
|
||||
"--no-sandbox",
|
||||
"--password-store=basic",
|
||||
"--use-mock-keychain",
|
||||
`--user-data-dir=${profileDir}`,
|
||||
"--dump-dom",
|
||||
"about:blank",
|
||||
],
|
||||
{
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf8",
|
||||
timeout: 15000,
|
||||
windowsHide: process.platform === "win32",
|
||||
},
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
const detail = result.error?.message || result.stderr || `status=${result.status}`;
|
||||
return { ok: false, reason: detail.trim() };
|
||||
}
|
||||
if (!(result.stdout || "").toLowerCase().includes("<html")) {
|
||||
return { ok: false, reason: "Chromium probe did not produce HTML output." };
|
||||
}
|
||||
return { ok: true };
|
||||
} finally {
|
||||
removeProbeProfile(profileDir);
|
||||
}
|
||||
}
|
||||
|
||||
function writeManifest(platform, executablePath) {
|
||||
fs.writeFileSync(
|
||||
manifestPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
browser: Browser.CHROME,
|
||||
buildId,
|
||||
platform,
|
||||
nodePlatform: process.platform,
|
||||
arch: process.arch,
|
||||
executable: path.relative(cacheDir, executablePath),
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function findAppBundle(executablePath) {
|
||||
let current = path.dirname(executablePath);
|
||||
while (true) {
|
||||
if (current.endsWith(".app")) {
|
||||
return current;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
return null;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function isSymlink(filePath) {
|
||||
try {
|
||||
return fs.lstatSync(filePath).isSymbolicLink();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function macChromiumFrameworkPath(appBundlePath) {
|
||||
return path.join(
|
||||
appBundlePath,
|
||||
"Contents",
|
||||
"Frameworks",
|
||||
"Google Chrome for Testing Framework.framework",
|
||||
);
|
||||
}
|
||||
|
||||
function macFrameworkLayoutLooksValid(frameworkPath) {
|
||||
if (!fs.existsSync(frameworkPath)) {
|
||||
return false;
|
||||
}
|
||||
const entries = fs.readdirSync(frameworkPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "Versions") {
|
||||
continue;
|
||||
}
|
||||
if (!isSymlink(path.join(frameworkPath, entry.name))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return isSymlink(path.join(frameworkPath, "Versions", "Current"));
|
||||
}
|
||||
|
||||
function macChromiumBundleLooksCodeSignReady(executablePath) {
|
||||
const appBundlePath = findAppBundle(executablePath);
|
||||
if (!appBundlePath) {
|
||||
return false;
|
||||
}
|
||||
return macFrameworkLayoutLooksValid(macChromiumFrameworkPath(appBundlePath));
|
||||
}
|
||||
|
||||
function canonicalizeFrameworkSymlinkTargets(frameworkPath) {
|
||||
if (!fs.existsSync(frameworkPath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const versionsPath = path.join(frameworkPath, "Versions");
|
||||
const currentPath = path.join(versionsPath, "Current");
|
||||
if (!fs.existsSync(versionsPath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let rewritten = 0;
|
||||
|
||||
const versionEntries = fs
|
||||
.readdirSync(versionsPath, { withFileTypes: true })
|
||||
.filter((entry) => entry.name !== "Current" && entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
const currentTarget =
|
||||
isSymlink(currentPath) &&
|
||||
fs.existsSync(path.resolve(versionsPath, fs.readlinkSync(currentPath)))
|
||||
? fs.readlinkSync(currentPath)
|
||||
: versionEntries[0];
|
||||
|
||||
if (!currentTarget) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!isSymlink(currentPath) || fs.readlinkSync(currentPath) !== currentTarget) {
|
||||
fs.rmSync(currentPath, { recursive: true, force: true });
|
||||
fs.symlinkSync(currentTarget, currentPath);
|
||||
rewritten += 1;
|
||||
}
|
||||
|
||||
const topLevelSymlinks = [
|
||||
"Google Chrome for Testing Framework",
|
||||
"Helpers",
|
||||
"Libraries",
|
||||
"Resources",
|
||||
];
|
||||
for (const name of topLevelSymlinks) {
|
||||
const linkPath = path.join(frameworkPath, name);
|
||||
const linkTarget = ["Versions", "Current", name].join("/");
|
||||
if (!fs.existsSync(path.resolve(frameworkPath, linkTarget))) {
|
||||
continue;
|
||||
}
|
||||
if (isSymlink(linkPath) && fs.readlinkSync(linkPath) === linkTarget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.rmSync(linkPath, { recursive: true, force: true });
|
||||
fs.symlinkSync(linkTarget, linkPath);
|
||||
rewritten += 1;
|
||||
}
|
||||
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
function canonicalizeMacFrameworkSymlinks(rootDir) {
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const stack = [rootDir];
|
||||
let rewritten = 0;
|
||||
while (stack.length) {
|
||||
const current = stack.pop();
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.name === "Google Chrome for Testing Framework.framework") {
|
||||
rewritten += canonicalizeFrameworkSymlinkTargets(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (entry.name.endsWith(".framework")) {
|
||||
continue;
|
||||
}
|
||||
stack.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
function normalizeMacBundleForPackaging(executablePath) {
|
||||
const appBundlePath = findAppBundle(executablePath);
|
||||
if (!appBundlePath || !fs.existsSync(appBundlePath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const frameworkPath = macChromiumFrameworkPath(appBundlePath);
|
||||
const rewritten = canonicalizeFrameworkSymlinkTargets(frameworkPath);
|
||||
if (rewritten > 0) {
|
||||
console.log(
|
||||
`[Chromium] Canonicalized ${rewritten} framework symlinks for App Store packaging.`,
|
||||
);
|
||||
}
|
||||
adHocSignMacBundle(appBundlePath);
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
function adHocSignMacBundle(appBundlePath) {
|
||||
if (process.platform !== "darwin") {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
"codesign",
|
||||
["--force", "--deep", "--sign", "-", "--timestamp=none", appBundlePath],
|
||||
{
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to re-sign normalized Chromium bundle: ${(result.stderr || result.stdout || "").trim()}`,
|
||||
);
|
||||
}
|
||||
console.log(`[Chromium] Re-signed normalized macOS bundle: ${appBundlePath}`);
|
||||
}
|
||||
|
||||
function normalizeBundledMacChromiumForPackaging(rootDir = cacheDir) {
|
||||
const rewritten = canonicalizeMacFrameworkSymlinks(rootDir);
|
||||
if (rewritten > 0) {
|
||||
console.log(
|
||||
`[Chromium] Canonicalized ${rewritten} bundled macOS framework symlinks before packaging.`,
|
||||
);
|
||||
}
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
function removeIncompleteRuntime(platform, executablePath) {
|
||||
if (validateExecutable(executablePath).ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const revisionDir = getRevisionDir(platform);
|
||||
if (!fs.existsSync(revisionDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Chromium] Removing incomplete runtime before download: ${revisionDir}`
|
||||
);
|
||||
fs.rmSync(revisionDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.env.SKIP_BUNDLED_CHROMIUM === "1") {
|
||||
console.log("[Chromium] SKIP_BUNDLED_CHROMIUM=1; leaving runtime unbundled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const platform = detectBrowserPlatform();
|
||||
if (!platform) {
|
||||
throw new Error(`Unsupported platform for bundled Chromium: ${process.platform}-${process.arch}`);
|
||||
}
|
||||
|
||||
const options = {
|
||||
browser: Browser.CHROME,
|
||||
buildId,
|
||||
cacheDir,
|
||||
platform,
|
||||
};
|
||||
const executablePath = computeExecutablePath(options);
|
||||
if (runtimeLooksComplete(executablePath)) {
|
||||
if (!validateExecutable(executablePath).ok) {
|
||||
removeIncompleteRuntime(platform, executablePath);
|
||||
} else {
|
||||
normalizeMacBundleForPackaging(executablePath);
|
||||
if (!validateExecutable(executablePath).ok) {
|
||||
removeIncompleteRuntime(platform, executablePath);
|
||||
} else {
|
||||
writeManifest(platform, executablePath);
|
||||
console.log(`[Chromium] Bundled runtime already exists: ${executablePath}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validateExecutable(executablePath).ok) {
|
||||
writeManifest(platform, executablePath);
|
||||
return;
|
||||
}
|
||||
|
||||
removeIncompleteRuntime(platform, executablePath);
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
console.log(`[Chromium] Downloading Chrome for Testing ${buildId} into ${cacheDir}`);
|
||||
await install({
|
||||
...options,
|
||||
downloadProgressCallback(downloadedBytes, totalBytes) {
|
||||
if (totalBytes <= 0) return;
|
||||
const percent = Math.floor((downloadedBytes / totalBytes) * 100);
|
||||
process.stdout.write(`\r[Chromium] ${percent}%`);
|
||||
},
|
||||
});
|
||||
process.stdout.write("\n");
|
||||
|
||||
normalizeMacBundleForPackaging(executablePath);
|
||||
const validation = validateExecutable(executablePath);
|
||||
if (!validation.ok) {
|
||||
throw new Error(
|
||||
`Chromium install finished, but the launch probe failed: ${validation.reason}\n${executablePath}`,
|
||||
);
|
||||
}
|
||||
writeManifest(platform, executablePath);
|
||||
console.log(`[Chromium] Bundled runtime ready: ${executablePath}`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
adHocSignMacBundle,
|
||||
canonicalizeMacFrameworkSymlinks,
|
||||
normalizeBundledMacChromiumForPackaging,
|
||||
normalizeMacBundleForPackaging,
|
||||
};
|
||||
@@ -0,0 +1,703 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const https = require("https");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { path7za } = require("7zip-bin");
|
||||
|
||||
const VERSION = process.env.IMAGEMAGICK_VERSION || "7.1.2-18";
|
||||
const PLATFORM = process.platform;
|
||||
const ARCH = process.arch;
|
||||
const TARGET_DIR = path.join(__dirname, "..", "resources", "imagemagick", `${PLATFORM}-${ARCH}`);
|
||||
const CACHE_DIR = path.join(__dirname, "..", ".cache", "imagemagick", VERSION);
|
||||
const MANIFEST_NAME = "presenton-runtime.json";
|
||||
|
||||
function log(message) {
|
||||
console.log(`[imagemagick] ${message}`);
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(`[imagemagick] ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
...options,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
fail(`${command} ${args.join(" ")} failed with code ${result.status}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function capture(command, args, options = {}) {
|
||||
return spawnSync(command, args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf8",
|
||||
windowsHide: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function runtimeEnv(binaryPath) {
|
||||
const homeDir = fs.statSync(binaryPath).isFile()
|
||||
? path.dirname(binaryPath)
|
||||
: binaryPath;
|
||||
const tempDir = process.env.TEMP || process.env.TMPDIR || os.tmpdir() || homeDir;
|
||||
return {
|
||||
...process.env,
|
||||
MAGICK_HOME: path.basename(homeDir).toLowerCase() === "bin"
|
||||
? path.dirname(homeDir)
|
||||
: homeDir,
|
||||
MAGICK_CONFIGURE_PATH: path.basename(homeDir).toLowerCase() === "bin"
|
||||
? path.dirname(homeDir)
|
||||
: homeDir,
|
||||
MAGICK_TEMPORARY_PATH: tempDir,
|
||||
MAGICK_OCL_DEVICE: "OFF",
|
||||
APPIMAGE_EXTRACT_AND_RUN: "1",
|
||||
};
|
||||
}
|
||||
|
||||
function versionOutput(binaryPath) {
|
||||
const result = spawnSync(binaryPath, ["-version"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf8",
|
||||
timeout: 120000,
|
||||
env: runtimeEnv(binaryPath),
|
||||
windowsHide: true,
|
||||
});
|
||||
if (result.status !== 0 || result.signal) {
|
||||
const reason = result.error?.message
|
||||
|| (result.stderr || "").trim()
|
||||
|| (result.signal ? `terminated by signal ${result.signal}` : `exit ${result.status}`);
|
||||
return { ok: false, reason };
|
||||
}
|
||||
const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
||||
return { ok: output.toLowerCase().includes("imagemagick"), output };
|
||||
}
|
||||
|
||||
function readManifest(targetDir) {
|
||||
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeManifest(targetDir, manifest) {
|
||||
fs.writeFileSync(
|
||||
path.join(targetDir, MANIFEST_NAME),
|
||||
JSON.stringify(
|
||||
{
|
||||
version: VERSION,
|
||||
platform: PLATFORM,
|
||||
arch: ARCH,
|
||||
createdAt: new Date().toISOString(),
|
||||
...manifest,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function validateRuntime(targetDir) {
|
||||
const manifest = readManifest(targetDir);
|
||||
const binary = manifest?.binary || (PLATFORM === "win32" ? "magick.exe" : "bin/magick");
|
||||
const binaryPath = path.join(targetDir, binary);
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
return null;
|
||||
}
|
||||
if (PLATFORM !== "win32") {
|
||||
try {
|
||||
fs.chmodSync(binaryPath, 0o755);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const result = versionOutput(binaryPath);
|
||||
if (!result.ok) {
|
||||
log(`Runtime validation failed for ${binaryPath}: ${result.reason}`);
|
||||
return null;
|
||||
}
|
||||
return { binaryPath, output: result.output };
|
||||
}
|
||||
|
||||
function archiveName() {
|
||||
if (PLATFORM !== "win32") {
|
||||
return null;
|
||||
}
|
||||
const archName = ARCH === "x64" ? "x64" : ARCH === "arm64" ? "arm64" : ARCH === "ia32" ? "x86" : null;
|
||||
if (!archName) {
|
||||
fail(`No bundled ImageMagick Windows asset configured for ${PLATFORM}-${ARCH}`);
|
||||
}
|
||||
return `ImageMagick-${VERSION}-portable-Q16-${archName}.7z`;
|
||||
}
|
||||
|
||||
function linuxAppImageArch() {
|
||||
if (PLATFORM !== "linux") {
|
||||
return null;
|
||||
}
|
||||
if (ARCH !== "x64") {
|
||||
fail(`No bundled ImageMagick Linux AppImage asset configured for ${PLATFORM}-${ARCH}`);
|
||||
}
|
||||
return "x86_64";
|
||||
}
|
||||
|
||||
function linuxDefaultAppImageName() {
|
||||
return `ImageMagick-${VERSION}-gcc-${linuxAppImageArch()}.AppImage`;
|
||||
}
|
||||
|
||||
function parseAssetNameFromUrl(urlValue) {
|
||||
try {
|
||||
const pathname = new URL(urlValue).pathname;
|
||||
const name = pathname.split("/").filter(Boolean).pop();
|
||||
return name || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadUrl(assetName) {
|
||||
if (process.env.IMAGEMAGICK_DOWNLOAD_URL) {
|
||||
return process.env.IMAGEMAGICK_DOWNLOAD_URL;
|
||||
}
|
||||
return `https://github.com/ImageMagick/ImageMagick/releases/download/${VERSION}/${assetName}`;
|
||||
}
|
||||
|
||||
function downloadFile(url, destination) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = https.get(
|
||||
url,
|
||||
{ headers: { "User-Agent": "Presenton ImageMagick runtime fetcher" } },
|
||||
(response) => {
|
||||
if ([301, 302, 303, 307, 308].includes(response.statusCode || 0)) {
|
||||
const location = response.headers.location;
|
||||
if (!location) {
|
||||
reject(new Error(`Redirect from ${url} did not include Location`));
|
||||
return;
|
||||
}
|
||||
response.resume();
|
||||
downloadFile(new URL(location, url).toString(), destination).then(resolve, reject);
|
||||
return;
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`));
|
||||
response.resume();
|
||||
return;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
const file = fs.createWriteStream(destination);
|
||||
response.pipe(file);
|
||||
file.on("finish", () => file.close(resolve));
|
||||
file.on("error", reject);
|
||||
},
|
||||
);
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchJson(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = https.get(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": "Presenton ImageMagick runtime fetcher",
|
||||
Accept: "application/vnd.github+json",
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if ([301, 302, 303, 307, 308].includes(response.statusCode || 0)) {
|
||||
const location = response.headers.location;
|
||||
if (!location) {
|
||||
reject(new Error(`Redirect from ${url} did not include Location`));
|
||||
return;
|
||||
}
|
||||
response.resume();
|
||||
fetchJson(new URL(location, url).toString()).then(resolve, reject);
|
||||
return;
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Request failed with HTTP ${response.statusCode}: ${url}`));
|
||||
response.resume();
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
response.on("end", () => {
|
||||
try {
|
||||
const body = Buffer.concat(chunks).toString("utf8");
|
||||
resolve(JSON.parse(body));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
response.on("error", reject);
|
||||
},
|
||||
);
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function uniqueNonEmpty(values) {
|
||||
return values.filter(Boolean).filter((value, index, all) => all.indexOf(value) === index);
|
||||
}
|
||||
|
||||
async function linuxAppImageCandidates() {
|
||||
const configuredAsset = process.env.IMAGEMAGICK_LINUX_ASSET_NAME?.trim();
|
||||
if (configuredAsset) {
|
||||
return [configuredAsset];
|
||||
}
|
||||
|
||||
const arch = linuxAppImageArch();
|
||||
const downloadOverride = process.env.IMAGEMAGICK_DOWNLOAD_URL?.trim();
|
||||
if (downloadOverride) {
|
||||
return [parseAssetNameFromUrl(downloadOverride) || linuxDefaultAppImageName()];
|
||||
}
|
||||
|
||||
const fallbackName = linuxDefaultAppImageName();
|
||||
const candidates = [fallbackName];
|
||||
try {
|
||||
const release = await fetchJson(`https://api.github.com/repos/ImageMagick/ImageMagick/releases/tags/${VERSION}`);
|
||||
const assets = Array.isArray(release?.assets) ? release.assets : [];
|
||||
const appImages = assets
|
||||
.map((asset) => asset?.name)
|
||||
.filter((name) => typeof name === "string" && name.endsWith(`-${arch}.AppImage`));
|
||||
|
||||
const scored = appImages
|
||||
.map((name) => ({
|
||||
name,
|
||||
score: name === fallbackName
|
||||
? 100
|
||||
: name.includes(`-gcc-${arch}.AppImage`)
|
||||
? 90
|
||||
: name.includes(`-clang-${arch}.AppImage`)
|
||||
? 80
|
||||
: 70,
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map((entry) => entry.name);
|
||||
|
||||
candidates.push(...scored);
|
||||
} catch (error) {
|
||||
log(`Could not resolve Linux AppImage assets from release metadata: ${error?.message || error}`);
|
||||
}
|
||||
|
||||
return uniqueNonEmpty(candidates);
|
||||
}
|
||||
|
||||
function findMagickDir(root) {
|
||||
const stack = [root];
|
||||
while (stack.length) {
|
||||
const current = stack.pop();
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
if (entries.some((entry) => entry.isFile() && entry.name.toLowerCase() === "magick.exe")) {
|
||||
return current;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(path.join(current, entry.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function prepareWindows() {
|
||||
if (!path7za || !fs.existsSync(path7za)) {
|
||||
fail("7zip-bin is unavailable; run npm install before preparing ImageMagick.");
|
||||
}
|
||||
|
||||
const assetName = archiveName();
|
||||
const archivePath = path.join(CACHE_DIR, assetName);
|
||||
const extractDir = path.join(CACHE_DIR, "extract");
|
||||
const tempTarget = `${TARGET_DIR}.tmp`;
|
||||
|
||||
if (!fs.existsSync(archivePath)) {
|
||||
const url = downloadUrl(assetName);
|
||||
log(`Downloading ${url}`);
|
||||
await downloadFile(url, archivePath);
|
||||
} else {
|
||||
log(`Using cached archive: ${archivePath}`);
|
||||
}
|
||||
|
||||
fs.rmSync(extractDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
log(`Extracting ${archivePath}`);
|
||||
run(path7za, ["x", archivePath, `-o${extractDir}`, "-y"]);
|
||||
|
||||
const magickDir = findMagickDir(extractDir);
|
||||
if (!magickDir) {
|
||||
fail("Extracted archive did not contain magick.exe");
|
||||
}
|
||||
|
||||
fs.rmSync(tempTarget, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.dirname(tempTarget), { recursive: true });
|
||||
fs.cpSync(magickDir, tempTarget, { recursive: true });
|
||||
writeManifest(tempTarget, {
|
||||
kind: "windows-portable",
|
||||
binary: "magick.exe",
|
||||
source: downloadUrl(assetName),
|
||||
});
|
||||
|
||||
if (!validateRuntime(tempTarget)) {
|
||||
fail(`Prepared runtime failed validation at ${tempTarget}`);
|
||||
}
|
||||
|
||||
fs.rmSync(TARGET_DIR, { recursive: true, force: true });
|
||||
fs.renameSync(tempTarget, TARGET_DIR);
|
||||
log(`Prepared ${path.join(TARGET_DIR, "magick.exe")}`);
|
||||
}
|
||||
|
||||
async function prepareLinux() {
|
||||
const candidates = await linuxAppImageCandidates();
|
||||
const tempTarget = `${TARGET_DIR}.tmp`;
|
||||
let assetName = null;
|
||||
let appImagePath = null;
|
||||
let lastDownloadError = null;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const candidatePath = path.join(CACHE_DIR, candidate);
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
log(`Using cached AppImage: ${candidatePath}`);
|
||||
assetName = candidate;
|
||||
appImagePath = candidatePath;
|
||||
break;
|
||||
}
|
||||
|
||||
const url = downloadUrl(candidate);
|
||||
log(`Downloading ${url}`);
|
||||
try {
|
||||
await downloadFile(url, candidatePath);
|
||||
assetName = candidate;
|
||||
appImagePath = candidatePath;
|
||||
break;
|
||||
} catch (error) {
|
||||
lastDownloadError = error;
|
||||
const message = String(error?.message || error);
|
||||
if (message.includes("HTTP 404")) {
|
||||
log(`ImageMagick asset not found (${candidate}); trying next candidate.`);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!assetName || !appImagePath) {
|
||||
const reason = lastDownloadError?.message || lastDownloadError || "no candidate succeeded";
|
||||
fail(`Could not fetch Linux ImageMagick AppImage: ${reason}`);
|
||||
}
|
||||
|
||||
fs.rmSync(tempTarget, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.join(tempTarget, "bin"), { recursive: true });
|
||||
|
||||
const runtimeAppImage = path.join(tempTarget, assetName);
|
||||
fs.copyFileSync(appImagePath, runtimeAppImage);
|
||||
fs.chmodSync(runtimeAppImage, 0o755);
|
||||
|
||||
const wrapperPath = path.join(tempTarget, "bin", "magick");
|
||||
fs.writeFileSync(
|
||||
wrapperPath,
|
||||
[
|
||||
"#!/usr/bin/env sh",
|
||||
"set -eu",
|
||||
'DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"',
|
||||
"export APPIMAGE_EXTRACT_AND_RUN=${APPIMAGE_EXTRACT_AND_RUN:-1}",
|
||||
'exec "$DIR/' + assetName + '" "$@"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
fs.chmodSync(wrapperPath, 0o755);
|
||||
writeManifest(tempTarget, {
|
||||
kind: "linux-appimage",
|
||||
binary: "bin/magick",
|
||||
appImage: assetName,
|
||||
source: downloadUrl(assetName),
|
||||
});
|
||||
|
||||
if (!validateRuntime(tempTarget)) {
|
||||
fail(`Prepared runtime failed validation at ${tempTarget}`);
|
||||
}
|
||||
|
||||
fs.rmSync(TARGET_DIR, { recursive: true, force: true });
|
||||
fs.renameSync(tempTarget, TARGET_DIR);
|
||||
log(`Prepared ${path.join(TARGET_DIR, "bin", "magick")}`);
|
||||
}
|
||||
|
||||
function resolveCommandPath(command) {
|
||||
if (path.isAbsolute(command)) {
|
||||
return fs.existsSync(command) ? command : null;
|
||||
}
|
||||
const result = capture("which", [command]);
|
||||
const resolved = (result.stdout || "").trim().split(/\r?\n/).filter(Boolean)[0];
|
||||
return result.status === 0 && resolved ? resolved : null;
|
||||
}
|
||||
|
||||
function resolveMacPrefixFromMagickBinary(magickPath) {
|
||||
if (!magickPath || !fs.existsSync(magickPath)) {
|
||||
return null;
|
||||
}
|
||||
const realMagick = fs.realpathSync(magickPath);
|
||||
const binDir = path.dirname(realMagick);
|
||||
const prefix = path.dirname(binDir);
|
||||
return fs.existsSync(path.join(prefix, "bin", "magick")) ? prefix : null;
|
||||
}
|
||||
|
||||
function listBrewCandidates() {
|
||||
return [resolveCommandPath("brew"), "/opt/homebrew/bin/brew", "/usr/local/bin/brew"]
|
||||
.filter(Boolean)
|
||||
.filter((candidate, index, all) => all.indexOf(candidate) === index)
|
||||
.filter((candidate) => fs.existsSync(candidate));
|
||||
}
|
||||
|
||||
function resolveMacPrefixFromBrew() {
|
||||
const brewCandidates = listBrewCandidates();
|
||||
const formulas = ["imagemagick", "imagemagick@6"];
|
||||
|
||||
for (const brew of brewCandidates) {
|
||||
for (const formula of formulas) {
|
||||
const result = capture(brew, ["--prefix", formula]);
|
||||
const prefix = (result.stdout || "").trim();
|
||||
if (result.status !== 0 || !prefix) {
|
||||
continue;
|
||||
}
|
||||
if (fs.existsSync(path.join(prefix, "bin", "magick"))) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureMacImageMagickWithBrew() {
|
||||
for (const brew of listBrewCandidates()) {
|
||||
log(`ImageMagick not found; trying to install with Homebrew (${brew} install imagemagick).`);
|
||||
const install = spawnSync(brew, ["install", "imagemagick"], {
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
if (install.status === 0) {
|
||||
return true;
|
||||
}
|
||||
const reason = install.error?.message || `exit ${install.status}`;
|
||||
log(`Homebrew installation attempt failed via ${brew}: ${reason}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveMacSourcePrefix() {
|
||||
const configured = process.env.IMAGEMAGICK_VENDOR_DIR?.trim();
|
||||
if (configured) {
|
||||
return path.resolve(configured);
|
||||
}
|
||||
|
||||
const pathMagickPrefix = resolveMacPrefixFromMagickBinary(resolveCommandPath("magick"));
|
||||
if (pathMagickPrefix) {
|
||||
return pathMagickPrefix;
|
||||
}
|
||||
|
||||
for (const magickPath of ["/opt/homebrew/bin/magick", "/usr/local/bin/magick", "/opt/local/bin/magick"]) {
|
||||
const prefix = resolveMacPrefixFromMagickBinary(magickPath);
|
||||
if (prefix) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
const brewPrefix = resolveMacPrefixFromBrew();
|
||||
if (brewPrefix) {
|
||||
return brewPrefix;
|
||||
}
|
||||
|
||||
for (const optPrefix of ["/opt/homebrew/opt/imagemagick", "/usr/local/opt/imagemagick"]) {
|
||||
if (fs.existsSync(path.join(optPrefix, "bin", "magick"))) {
|
||||
return optPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
if (ensureMacImageMagickWithBrew()) {
|
||||
const installedPrefix = resolveMacPrefixFromMagickBinary(resolveCommandPath("magick"))
|
||||
|| resolveMacPrefixFromBrew();
|
||||
if (installedPrefix) {
|
||||
return installedPrefix;
|
||||
}
|
||||
log("Homebrew install succeeded but ImageMagick prefix was not auto-detected; continuing with fallback checks.");
|
||||
}
|
||||
|
||||
fail("Could not find a macOS ImageMagick runtime to vendor. Install ImageMagick (brew install imagemagick) and rerun, or set IMAGEMAGICK_VENDOR_DIR.");
|
||||
}
|
||||
|
||||
function parseOtoolDeps(filePath) {
|
||||
const result = capture("otool", ["-L", filePath]);
|
||||
if (result.status !== 0) {
|
||||
fail(`otool -L failed for ${filePath}: ${result.stderr || result.status}`);
|
||||
}
|
||||
return (result.stdout || "")
|
||||
.split(/\r?\n/)
|
||||
.slice(1)
|
||||
.map((line) => line.trim().split(/\s+/)[0])
|
||||
.filter(Boolean)
|
||||
.filter((dep) => dep.startsWith("/"))
|
||||
.filter((dep) => !dep.startsWith("/System/Library/") && !dep.startsWith("/usr/lib/"));
|
||||
}
|
||||
|
||||
function isMachOFile(filePath) {
|
||||
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||
return false;
|
||||
}
|
||||
const result = capture("file", [filePath]);
|
||||
return result.status === 0 && /Mach-O/.test(result.stdout || "");
|
||||
}
|
||||
|
||||
function walkFiles(rootDir) {
|
||||
const stack = [rootDir];
|
||||
const files = [];
|
||||
while (stack.length) {
|
||||
const current = stack.pop();
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function relinkMacDylibs(targetDir, mainExecutable) {
|
||||
for (const tool of ["otool", "install_name_tool", "file"]) {
|
||||
if (!resolveCommandPath(tool)) {
|
||||
fail(`macOS runtime vendoring requires ${tool}.`);
|
||||
}
|
||||
}
|
||||
|
||||
const libDir = path.join(targetDir, "lib");
|
||||
fs.mkdirSync(libDir, { recursive: true });
|
||||
|
||||
const queue = [mainExecutable];
|
||||
const visited = new Set();
|
||||
const copiedBySource = new Map();
|
||||
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
if (visited.has(current) || !fs.existsSync(current) || !isMachOFile(current)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(current);
|
||||
|
||||
const deps = parseOtoolDeps(current);
|
||||
for (const dep of deps) {
|
||||
const depBase = path.basename(dep);
|
||||
let vendored = copiedBySource.get(dep);
|
||||
if (!vendored) {
|
||||
vendored = path.join(libDir, depBase);
|
||||
if (!fs.existsSync(vendored)) {
|
||||
fs.copyFileSync(dep, vendored);
|
||||
fs.chmodSync(vendored, 0o755);
|
||||
}
|
||||
copiedBySource.set(dep, vendored);
|
||||
queue.push(vendored);
|
||||
}
|
||||
|
||||
const replacement = current === mainExecutable
|
||||
? `@executable_path/../lib/${depBase}`
|
||||
: `@loader_path/${depBase}`;
|
||||
run("install_name_tool", ["-change", dep, replacement, current]);
|
||||
}
|
||||
|
||||
if (current !== mainExecutable) {
|
||||
run("install_name_tool", ["-id", `@loader_path/${path.basename(current)}`, current]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function adHocSignMacRuntime(targetDir, mainExecutable) {
|
||||
if (!resolveCommandPath("codesign")) {
|
||||
fail("macOS runtime vendoring requires codesign.");
|
||||
}
|
||||
|
||||
const machOFiles = walkFiles(targetDir).filter(isMachOFile);
|
||||
const signOrder = machOFiles
|
||||
.filter((filePath) => filePath !== mainExecutable)
|
||||
.concat(machOFiles.includes(mainExecutable) ? [mainExecutable] : []);
|
||||
|
||||
log(`Ad-hoc signing ${signOrder.length} Mach-O files for macOS runtime.`);
|
||||
for (const filePath of signOrder) {
|
||||
run("codesign", ["--force", "--sign", "-", "--timestamp=none", filePath]);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareMacOS() {
|
||||
const sourcePrefix = resolveMacSourcePrefix();
|
||||
const sourceMagick = path.join(sourcePrefix, "bin", "magick");
|
||||
if (!fs.existsSync(sourceMagick)) {
|
||||
fail(`macOS ImageMagick prefix does not contain bin/magick: ${sourcePrefix}`);
|
||||
}
|
||||
|
||||
const tempTarget = `${TARGET_DIR}.tmp`;
|
||||
fs.rmSync(tempTarget, { recursive: true, force: true });
|
||||
fs.mkdirSync(tempTarget, { recursive: true });
|
||||
fs.cpSync(sourcePrefix, tempTarget, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter(source) {
|
||||
const base = path.basename(source);
|
||||
return base !== ".brew" && base !== "INSTALL_RECEIPT.json";
|
||||
},
|
||||
});
|
||||
|
||||
const targetMagick = path.join(tempTarget, "bin", "magick");
|
||||
fs.chmodSync(targetMagick, 0o755);
|
||||
relinkMacDylibs(tempTarget, targetMagick);
|
||||
adHocSignMacRuntime(tempTarget, targetMagick);
|
||||
writeManifest(tempTarget, {
|
||||
kind: "macos-vendored",
|
||||
binary: "bin/magick",
|
||||
source: sourcePrefix,
|
||||
});
|
||||
|
||||
if (!validateRuntime(tempTarget)) {
|
||||
fail(`Prepared runtime failed validation at ${tempTarget}`);
|
||||
}
|
||||
|
||||
fs.rmSync(TARGET_DIR, { recursive: true, force: true });
|
||||
fs.renameSync(tempTarget, TARGET_DIR);
|
||||
log(`Prepared ${path.join(TARGET_DIR, "bin", "magick")}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const existing = validateRuntime(TARGET_DIR);
|
||||
if (existing) {
|
||||
log(`Existing runtime OK: ${existing.binaryPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PLATFORM === "win32") {
|
||||
await prepareWindows();
|
||||
return;
|
||||
}
|
||||
if (PLATFORM === "linux") {
|
||||
await prepareLinux();
|
||||
return;
|
||||
}
|
||||
if (PLATFORM === "darwin") {
|
||||
await prepareMacOS();
|
||||
return;
|
||||
}
|
||||
|
||||
fail(`Unsupported platform for bundled ImageMagick: ${PLATFORM}-${ARCH}`);
|
||||
}
|
||||
|
||||
main().catch((error) => fail(error?.stack || error?.message || String(error)));
|
||||
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
ELECTRON_DIR = Path(__file__).resolve().parent.parent
|
||||
REPO_ROOT = ELECTRON_DIR.parent
|
||||
FASTAPI_DIR = REPO_ROOT / "servers" / "fastapi"
|
||||
NEXT_DIR = REPO_ROOT / "servers" / "nextjs"
|
||||
NOTICE_PATH = REPO_ROOT / "NOTICE"
|
||||
|
||||
PY_LICENSE_CANDIDATES = [
|
||||
"LICENSE",
|
||||
"LICENSE.txt",
|
||||
"LICENSE.md",
|
||||
"LICENCE",
|
||||
"COPYING",
|
||||
"COPYING.txt",
|
||||
"NOTICE",
|
||||
"NOTICE.txt",
|
||||
]
|
||||
|
||||
NODE_LICENSE_CANDIDATES = [
|
||||
"LICENSE",
|
||||
"LICENSE.txt",
|
||||
"LICENSE.md",
|
||||
"LICENCE",
|
||||
"LICENCE.txt",
|
||||
"COPYING",
|
||||
"COPYING.txt",
|
||||
"NOTICE",
|
||||
"NOTICE.txt",
|
||||
]
|
||||
|
||||
|
||||
def read_text_safe(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8", errors="replace").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def parse_rfc822_metadata(text: str) -> Dict[str, str]:
|
||||
data: Dict[str, str] = {}
|
||||
key: Optional[str] = None
|
||||
for raw_line in text.splitlines():
|
||||
if not raw_line:
|
||||
key = None
|
||||
continue
|
||||
if raw_line[0] in " \t" and key:
|
||||
data[key] += "\n" + raw_line.strip()
|
||||
continue
|
||||
if ":" in raw_line:
|
||||
k, v = raw_line.split(":", 1)
|
||||
key = k.strip()
|
||||
data[key] = v.strip()
|
||||
return data
|
||||
|
||||
|
||||
def find_python_site_packages(venv_dir: Path) -> Optional[Path]:
|
||||
# Linux/mac
|
||||
lib_dir = venv_dir / "lib"
|
||||
if lib_dir.exists():
|
||||
for child in lib_dir.iterdir():
|
||||
if child.is_dir() and child.name.startswith("python"):
|
||||
sp = child / "site-packages"
|
||||
if sp.exists():
|
||||
return sp
|
||||
# Windows
|
||||
sp = venv_dir / "Lib" / "site-packages"
|
||||
if sp.exists():
|
||||
return sp
|
||||
return None
|
||||
|
||||
|
||||
def detect_python_venv() -> Optional[Path]:
|
||||
env_path = os.environ.get("NOTICE_PYTHON_VENV")
|
||||
if env_path:
|
||||
v = Path(env_path)
|
||||
if v.exists():
|
||||
return v
|
||||
default = FASTAPI_DIR / ".venv"
|
||||
if default.exists():
|
||||
return default
|
||||
active = os.environ.get("VIRTUAL_ENV")
|
||||
if active and FASTAPI_DIR.as_posix() in Path(active).as_posix():
|
||||
return Path(active)
|
||||
return None
|
||||
|
||||
|
||||
def scan_python_packages(site_packages_dir: Path) -> List[Dict[str, str]]:
|
||||
entries: List[Dict[str, str]] = []
|
||||
dist_infos = sorted(site_packages_dir.glob("*.dist-info"))
|
||||
for dist in dist_infos:
|
||||
metadata_path = dist / "METADATA"
|
||||
if not metadata_path.exists():
|
||||
continue
|
||||
meta = parse_rfc822_metadata(read_text_safe(metadata_path))
|
||||
name = meta.get("Name", "").strip()
|
||||
version = meta.get("Version", "").strip()
|
||||
license_name = meta.get("License", "").strip()
|
||||
if not name:
|
||||
# Fallback to folder name pattern
|
||||
# e.g., requests-2.32.3.dist-info
|
||||
base = dist.name[:-10]
|
||||
if "-" in base:
|
||||
parts = base.rsplit("-", 1)
|
||||
if len(parts) == 2:
|
||||
name = parts[0]
|
||||
version = version or parts[1]
|
||||
author = meta.get("Author", meta.get("Maintainer", meta.get("Author-email", ""))).strip()
|
||||
|
||||
# License text candidates inside dist-info
|
||||
license_text = ""
|
||||
for cand in PY_LICENSE_CANDIDATES:
|
||||
p = dist / cand
|
||||
if p.exists():
|
||||
license_text = read_text_safe(p)
|
||||
if license_text:
|
||||
break
|
||||
|
||||
# Search via RECORD for license files elsewhere
|
||||
if not license_text:
|
||||
record = dist / "RECORD"
|
||||
if record.exists():
|
||||
for line in read_text_safe(record).splitlines():
|
||||
path_part = line.split(",", 1)[0]
|
||||
lower = path_part.lower()
|
||||
if any(token in lower for token in ["license", "licence", "copying", "notice"]):
|
||||
target = site_packages_dir / path_part
|
||||
if target.exists():
|
||||
license_text = read_text_safe(target)
|
||||
if license_text:
|
||||
break
|
||||
|
||||
# As last resort, embed the License: field content
|
||||
if not license_text and license_name:
|
||||
license_text = f"License field from METADATA:\n{license_name}"
|
||||
|
||||
entries.append({
|
||||
"name": name or dist.name,
|
||||
"version": version,
|
||||
"license": license_name,
|
||||
"author": author,
|
||||
"license_text": license_text,
|
||||
})
|
||||
|
||||
# Sort by name for stability
|
||||
entries.sort(key=lambda e: (e["name"].lower(), e["version"]))
|
||||
return entries
|
||||
|
||||
|
||||
def find_license_file_in_dir(base_dir: Path, depth_limit: int = 2) -> Optional[Path]:
|
||||
# First, try immediate candidates
|
||||
for cand in NODE_LICENSE_CANDIDATES:
|
||||
p = base_dir / cand
|
||||
if p.exists():
|
||||
return p
|
||||
# case-insensitive check
|
||||
for child in base_dir.iterdir():
|
||||
if child.is_file() and child.name.lower() == cand.lower():
|
||||
return child
|
||||
|
||||
# Recursive limited-depth scan excluding nested node_modules
|
||||
def walk(dir_path: Path, depth: int) -> Optional[Path]:
|
||||
if depth > depth_limit:
|
||||
return None
|
||||
try:
|
||||
it = list(dir_path.iterdir())
|
||||
except Exception:
|
||||
return None
|
||||
for child in it:
|
||||
name_lower = child.name.lower()
|
||||
if child.is_dir():
|
||||
if child.name == "node_modules" or child.name.startswith('.'):
|
||||
continue
|
||||
found = walk(child, depth + 1)
|
||||
if found:
|
||||
return found
|
||||
else:
|
||||
if any(tok in name_lower for tok in ["license", "licence", "copying", "notice"]):
|
||||
return child
|
||||
return None
|
||||
|
||||
return walk(base_dir, 0)
|
||||
|
||||
|
||||
def scan_node_modules(node_modules_dir: Path) -> List[Dict[str, str]]:
|
||||
entries: List[Dict[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def visit_pkg(pkg_dir: Path):
|
||||
pkg_json = pkg_dir / "package.json"
|
||||
if not pkg_json.exists():
|
||||
return
|
||||
try:
|
||||
data = json.loads(read_text_safe(pkg_json) or "{}")
|
||||
except Exception:
|
||||
return
|
||||
name = data.get("name") or pkg_dir.name
|
||||
version = str(data.get("version") or "")
|
||||
key = f"{name}@{version}"
|
||||
if key in seen:
|
||||
return
|
||||
seen.add(key)
|
||||
|
||||
license_name = ""
|
||||
lic_field = data.get("license")
|
||||
if isinstance(lic_field, str):
|
||||
license_name = lic_field
|
||||
elif isinstance(lic_field, dict):
|
||||
license_name = lic_field.get("type", "")
|
||||
elif isinstance(data.get("licenses"), list):
|
||||
license_name = ", ".join([str(x.get("type", "")) for x in data["licenses"] if isinstance(x, dict)])
|
||||
|
||||
author = ""
|
||||
a = data.get("author")
|
||||
if isinstance(a, str):
|
||||
author = a
|
||||
elif isinstance(a, dict):
|
||||
author = a.get("name", "")
|
||||
|
||||
license_text = ""
|
||||
lic_file = find_license_file_in_dir(pkg_dir, depth_limit=2)
|
||||
if lic_file:
|
||||
license_text = read_text_safe(lic_file)
|
||||
|
||||
entries.append({
|
||||
"name": name,
|
||||
"version": version,
|
||||
"license": license_name,
|
||||
"author": author,
|
||||
"license_text": license_text,
|
||||
})
|
||||
|
||||
def walk_node_modules(base: Path):
|
||||
if not base.exists():
|
||||
return
|
||||
for entry in base.iterdir():
|
||||
if not entry.is_dir():
|
||||
continue
|
||||
if entry.name == ".bin":
|
||||
continue
|
||||
if entry.name.startswith("@"): # scoped packages
|
||||
for scoped in entry.iterdir():
|
||||
if scoped.is_dir():
|
||||
visit_pkg(scoped)
|
||||
# nested node_modules inside the package
|
||||
nested = scoped / "node_modules"
|
||||
walk_node_modules(nested)
|
||||
continue
|
||||
visit_pkg(entry)
|
||||
nested = entry / "node_modules"
|
||||
walk_node_modules(nested)
|
||||
|
||||
walk_node_modules(node_modules_dir)
|
||||
# Sort by package name
|
||||
entries.sort(key=lambda e: (e["name"].lower(), e["version"]))
|
||||
return entries
|
||||
|
||||
|
||||
def format_section(title: str, entries: List[Dict[str, str]]) -> str:
|
||||
header = [
|
||||
"-------------------------------------",
|
||||
title,
|
||||
"-------------------------------------",
|
||||
"",
|
||||
]
|
||||
lines: List[str] = ["\n".join(header)]
|
||||
for e in entries:
|
||||
block = [
|
||||
e.get("name", "").strip(),
|
||||
e.get("version", "").strip(),
|
||||
e.get("license", "").strip(),
|
||||
e.get("author", "").strip(),
|
||||
"",
|
||||
(e.get("license_text", "") or "LICENSE TEXT NOT FOUND").strip(),
|
||||
"",
|
||||
"",
|
||||
]
|
||||
lines.append("\n".join(block))
|
||||
return "".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
# Optional CLI overrides
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Rebuild NOTICE from installed packages")
|
||||
parser.add_argument("--python-venv", dest="python_venv", default=None, help="Path to Python venv to scan")
|
||||
parser.add_argument("--node-modules", dest="node_modules", default=None, help="Path to node_modules to scan")
|
||||
args = parser.parse_args()
|
||||
python_entries: List[Dict[str, str]] = []
|
||||
node_entries: List[Dict[str, str]] = []
|
||||
|
||||
# Python scan
|
||||
venv = Path(args.python_venv) if args.python_venv else detect_python_venv()
|
||||
if venv:
|
||||
sp = find_python_site_packages(venv)
|
||||
if sp and sp.exists():
|
||||
python_entries = scan_python_packages(sp)
|
||||
else:
|
||||
print(f"Warning: site-packages not found under {venv}", file=sys.stderr)
|
||||
else:
|
||||
print("Warning: Python venv not found. Set NOTICE_PYTHON_VENV or create servers/fastapi/.venv", file=sys.stderr)
|
||||
|
||||
# Node scan
|
||||
node_modules_dir = Path(args.node_modules or os.environ.get("NOTICE_NODE_MODULES") or (NEXT_DIR / "node_modules"))
|
||||
if node_modules_dir.exists():
|
||||
node_entries = scan_node_modules(node_modules_dir)
|
||||
else:
|
||||
print(f"Warning: node_modules not found at {node_modules_dir}", file=sys.stderr)
|
||||
|
||||
# Build NOTICE content
|
||||
parts: List[str] = []
|
||||
if python_entries:
|
||||
parts.append(format_section("PYTHON PACKAGES", python_entries))
|
||||
if node_entries:
|
||||
parts.append(format_section("NODE PACKAGES", node_entries))
|
||||
if not parts:
|
||||
print("Error: No sections generated. Ensure .venv and node_modules exist.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
content = "\n".join(parts)
|
||||
NOTICE_PATH.write_text(content, encoding="utf-8")
|
||||
print("NOTICE rebuilt from installed packages")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
const electronRoot = path.join(__dirname, "..");
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(electronRoot, "package.json"), "utf8"),
|
||||
);
|
||||
|
||||
const targetRoot = path.join(electronRoot, "resources", "export");
|
||||
const targetPyDir = path.join(targetRoot, "py");
|
||||
const targetIndex = path.join(targetRoot, "index.js");
|
||||
const cacheDir = path.join(electronRoot, ".cache", "export-runtime");
|
||||
const exportRepoBase = "https://github.com/presenton/presenton-export/releases/download";
|
||||
const exportVersion = packageJson.exportVersion || "v0.1.0";
|
||||
|
||||
const cliArgs = new Set(process.argv.slice(2));
|
||||
const forceDownload = cliArgs.has("--force");
|
||||
const checkOnly = cliArgs.has("--check-only");
|
||||
|
||||
async function getTargetVersion() {
|
||||
const requestedVersion = process.env.EXPORT_RUNTIME_VERSION || exportVersion;
|
||||
if (requestedVersion !== "latest") {
|
||||
return requestedVersion;
|
||||
}
|
||||
|
||||
const apiUrl = "https://api.github.com/repos/presenton/presenton-export/releases/latest";
|
||||
const latest = await requestJson(apiUrl);
|
||||
if (!latest.tag_name) {
|
||||
throw new Error(`Could not resolve latest release tag from ${apiUrl}`);
|
||||
}
|
||||
|
||||
return latest.tag_name;
|
||||
}
|
||||
|
||||
function getPlatformAssetName() {
|
||||
const platformArch = `${process.platform}-${process.arch}`;
|
||||
if (platformArch === "linux-arm64") return "export-Linux-ARM64.zip";
|
||||
if (platformArch === "linux-x64") return "export-Linux-X64.zip";
|
||||
if (platformArch === "darwin-arm64") return "export-macOS-ARM64.zip";
|
||||
if (platformArch === "darwin-x64") return "export-macOS-X64.zip";
|
||||
if (platformArch === "win32-x64") return "export-Windows-X64.zip";
|
||||
|
||||
throw new Error(
|
||||
`Unsupported export runtime platform: ${platformArch}. Supported: linux-arm64, linux-x64, darwin-arm64, darwin-x64, win32-x64`
|
||||
);
|
||||
}
|
||||
|
||||
function getConverterCandidates() {
|
||||
const platformAliases = {
|
||||
linux: ["linux"],
|
||||
darwin: ["darwin", "macos", "mac"],
|
||||
win32: ["win32", "windows", "win"],
|
||||
};
|
||||
const archAliases = {
|
||||
x64: ["x64", "amd64"],
|
||||
arm64: ["arm64", "aarch64"],
|
||||
};
|
||||
|
||||
const candidates = [];
|
||||
const platforms = platformAliases[process.platform] || [process.platform];
|
||||
const archs = archAliases[process.arch] || [process.arch];
|
||||
const windows = process.platform === "win32";
|
||||
|
||||
for (const p of platforms) {
|
||||
for (const a of archs) {
|
||||
candidates.push(path.join(targetPyDir, `convert-${p}-${a}`));
|
||||
candidates.push(path.join(targetPyDir, `convert-${p}-${a}.exe`));
|
||||
}
|
||||
candidates.push(path.join(targetPyDir, `convert-${p}`));
|
||||
candidates.push(path.join(targetPyDir, `convert-${p}.exe`));
|
||||
}
|
||||
|
||||
if (windows) {
|
||||
candidates.push(path.join(targetPyDir, "convert.exe"));
|
||||
}
|
||||
candidates.push(path.join(targetPyDir, "convert"));
|
||||
|
||||
return [...new Set(candidates)];
|
||||
}
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function chmodIfPossible(filePath) {
|
||||
if (process.platform !== "win32") {
|
||||
fs.chmodSync(filePath, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
function detectBinaryFormat(filePath) {
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
try {
|
||||
const header = Buffer.alloc(4);
|
||||
fs.readSync(fd, header, 0, 4, 0);
|
||||
|
||||
if (header[0] === 0x7f && header[1] === 0x45 && header[2] === 0x4c && header[3] === 0x46) {
|
||||
return "elf";
|
||||
}
|
||||
|
||||
if (header[0] === 0x4d && header[1] === 0x5a) {
|
||||
return "pe";
|
||||
}
|
||||
|
||||
const magic = header.readUInt32BE(0);
|
||||
if (
|
||||
magic === 0xfeedface ||
|
||||
magic === 0xcefaedfe ||
|
||||
magic === 0xfeedfacf ||
|
||||
magic === 0xcffaedfe ||
|
||||
magic === 0xcafebabe ||
|
||||
magic === 0xbebafeca
|
||||
) {
|
||||
return "mach-o";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
function isFormatCompatible(format) {
|
||||
if (process.platform === "darwin") return format === "mach-o";
|
||||
if (process.platform === "linux") return format === "elf";
|
||||
if (process.platform === "win32") return format === "pe";
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateExistingRuntime() {
|
||||
if (!fs.existsSync(targetIndex)) {
|
||||
return { ok: false, reason: `Missing runtime bundle: ${targetIndex}` };
|
||||
}
|
||||
|
||||
const converterCandidates = getConverterCandidates();
|
||||
const converterPath = converterCandidates.find((candidate) => fs.existsSync(candidate));
|
||||
|
||||
if (!converterPath) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: [
|
||||
"No converter binary found in electron/resources/export/py.",
|
||||
"Expected one of:",
|
||||
...converterCandidates.map((candidate) => ` - ${candidate}`),
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
const binaryFormat = detectBinaryFormat(converterPath);
|
||||
if (!isFormatCompatible(binaryFormat)) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: [
|
||||
`Converter binary is not valid for ${process.platform}/${process.arch}.`,
|
||||
`Selected converter: ${converterPath}`,
|
||||
`Detected format: ${binaryFormat}`,
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
chmodIfPossible(converterPath);
|
||||
return { ok: true, converterPath };
|
||||
}
|
||||
|
||||
function patchHtmlToImageRuntime() {
|
||||
if (!fs.existsSync(targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const original = fs.readFileSync(targetIndex, "utf8");
|
||||
let patched = original.replace(
|
||||
'await C.setContent(a.html,{waitUntil:"networkidle0",timeout:12e4})',
|
||||
'await C.setContent(a.html,{waitUntil:"domcontentloaded",timeout:12e4})',
|
||||
);
|
||||
patched = patched.replace(
|
||||
'catch(C){throw C instanceof ig?C:new ig("Failed to render HTML to image",500)}',
|
||||
'catch(C){console.error("[html-to-image]",C);throw C instanceof ig?C:new ig("Failed to render HTML to image",500)}',
|
||||
);
|
||||
|
||||
if (patched === original) {
|
||||
return false;
|
||||
}
|
||||
fs.writeFileSync(targetIndex, patched);
|
||||
console.log("[export-runtime] Patched HTML-to-image readiness and error logging.");
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasExportDirectoryContent() {
|
||||
if (!fs.existsSync(targetRoot)) return false;
|
||||
return fs.readdirSync(targetRoot).length > 0;
|
||||
}
|
||||
|
||||
function request(url) {
|
||||
const client = url.startsWith("https:") ? https : http;
|
||||
return client;
|
||||
}
|
||||
|
||||
function requestJson(url, redirects = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = request(url);
|
||||
const req = client.get(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": "presenton-export-runtime-sync",
|
||||
Accept: "application/vnd.github+json",
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
||||
if (redirects <= 0) {
|
||||
reject(new Error(`Too many redirects for JSON request: ${url}`));
|
||||
return;
|
||||
}
|
||||
requestJson(res.headers.location, redirects - 1).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
reject(new Error(`Failed to fetch ${url}. HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
let payload = "";
|
||||
res.setEncoding("utf8");
|
||||
res.on("data", (chunk) => {
|
||||
payload += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(payload));
|
||||
} catch (error) {
|
||||
reject(new Error(`Invalid JSON received from ${url}: ${error.message}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function downloadFile(url, outputPath, redirects = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = request(url);
|
||||
const req = client.get(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": "presenton-export-runtime-sync",
|
||||
Accept: "application/octet-stream",
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
||||
if (redirects <= 0) {
|
||||
reject(new Error(`Too many redirects while downloading ${url}`));
|
||||
return;
|
||||
}
|
||||
downloadFile(res.headers.location, outputPath, redirects - 1).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
reject(new Error(`Failed to download ${url}. HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
ensureDir(path.dirname(outputPath));
|
||||
const fileStream = fs.createWriteStream(outputPath);
|
||||
res.pipe(fileStream);
|
||||
fileStream.on("finish", () => {
|
||||
fileStream.close(resolve);
|
||||
});
|
||||
fileStream.on("error", reject);
|
||||
}
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFileWithRetries(url, outputPath, attempts = 4) {
|
||||
let lastError;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
if (i > 0) {
|
||||
const delay = 1500 * Math.pow(2, i - 1);
|
||||
console.log(`[export-runtime] Retrying download (attempt ${i + 1}/${attempts}) after ${delay}ms…`);
|
||||
await sleep(delay);
|
||||
}
|
||||
try {
|
||||
fs.unlinkSync(outputPath);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
await downloadFile(url, outputPath);
|
||||
const st = fs.statSync(outputPath);
|
||||
if (st.size < 512) {
|
||||
throw new Error(`Downloaded file is too small (${st.size} bytes); likely corrupt or HTML error page`);
|
||||
}
|
||||
const magic = Buffer.alloc(4);
|
||||
const fd = fs.openSync(outputPath, "r");
|
||||
try {
|
||||
fs.readSync(fd, magic, 0, 4, 0);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
if (magic[0] !== 0x50 || magic[1] !== 0x4b) {
|
||||
throw new Error("Downloaded file is not a ZIP (missing PK header); delete cache and retry");
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
function unzipArchive(zipPath, destDir) {
|
||||
ensureDir(destDir);
|
||||
if (process.platform === "win32") {
|
||||
const psQuote = (p) => p.replace(/'/g, "''");
|
||||
execFileSync(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
`Expand-Archive -LiteralPath '${psQuote(zipPath)}' -DestinationPath '${psQuote(destDir)}' -Force`,
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
execFileSync("unzip", ["-o", zipPath, "-d", destDir], { stdio: "inherit" });
|
||||
}
|
||||
|
||||
function hasRuntimeLayout(dir) {
|
||||
const indexPath = path.join(dir, "index.js");
|
||||
if (!fs.existsSync(indexPath)) return false;
|
||||
|
||||
const pyPath = path.join(dir, "py");
|
||||
if (fs.existsSync(pyPath)) {
|
||||
try {
|
||||
return fs.statSync(pyPath).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Windows release zips are often flat: index.js + convert-*.exe (no py/ yet).
|
||||
try {
|
||||
return fs.readdirSync(dir).some((name) => {
|
||||
if (!/^convert/i.test(name)) return false;
|
||||
const p = path.join(dir, name);
|
||||
try {
|
||||
return fs.statSync(p).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Flat Windows bundles ship convert-*.exe next to index.js; runtime expects py/. */
|
||||
function ensurePyConverterLayout(root) {
|
||||
const pyDir = path.join(root, "py");
|
||||
let needMoveFromRoot = false;
|
||||
|
||||
if (fs.existsSync(pyDir)) {
|
||||
try {
|
||||
if (fs.statSync(pyDir).isDirectory()) {
|
||||
const inner = fs.readdirSync(pyDir);
|
||||
const hasBin = inner.some(
|
||||
(n) =>
|
||||
n === "convert" ||
|
||||
n === "convert.exe" ||
|
||||
/^convert-/i.test(n)
|
||||
);
|
||||
if (hasBin) return;
|
||||
needMoveFromRoot = true;
|
||||
}
|
||||
} catch {
|
||||
needMoveFromRoot = true;
|
||||
}
|
||||
} else {
|
||||
needMoveFromRoot = true;
|
||||
}
|
||||
|
||||
if (!needMoveFromRoot) return;
|
||||
|
||||
fs.mkdirSync(pyDir, { recursive: true });
|
||||
const names = fs.readdirSync(root, { withFileTypes: true });
|
||||
for (const ent of names) {
|
||||
if (!ent.isFile()) continue;
|
||||
const base = ent.name;
|
||||
if (!/^convert/i.test(base)) continue;
|
||||
const from = path.join(root, base);
|
||||
const to = path.join(pyDir, base);
|
||||
fs.renameSync(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
function describeExtractTree(extractDir, maxEntries = 30) {
|
||||
const lines = [];
|
||||
function walk(dir, prefix, depth) {
|
||||
if (depth > 3 || lines.length >= maxEntries) return;
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (lines.length >= maxEntries) break;
|
||||
const p = path.join(dir, e.name);
|
||||
lines.push(`${prefix}${e.name}${e.isDirectory() ? "/" : ""}`);
|
||||
if (e.isDirectory()) walk(p, `${prefix} `, depth + 1);
|
||||
}
|
||||
}
|
||||
walk(extractDir, "", 0);
|
||||
return lines.length ? lines.join("\n") : "(empty)";
|
||||
}
|
||||
|
||||
function resolveExtractedRoot(extractDir) {
|
||||
if (hasRuntimeLayout(extractDir)) {
|
||||
return extractDir;
|
||||
}
|
||||
|
||||
const queue = [{ dir: extractDir, depth: 0 }];
|
||||
const maxDepth = 8;
|
||||
while (queue.length > 0) {
|
||||
const { dir, depth } = queue.shift();
|
||||
if (depth >= maxDepth) continue;
|
||||
let children;
|
||||
try {
|
||||
children = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of children) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const candidate = path.join(dir, entry.name);
|
||||
if (hasRuntimeLayout(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
queue.push({ dir: candidate, depth: depth + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
const hint = describeExtractTree(extractDir);
|
||||
throw new Error(
|
||||
`Unable to locate export runtime root under ${extractDir}\n` +
|
||||
`Expected a folder containing index.js and a py/ directory. Extracted layout (partial):\n${hint}`
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadAndInstallRuntime() {
|
||||
const tag = await getTargetVersion();
|
||||
const assetName = getPlatformAssetName();
|
||||
const downloadUrl = `${exportRepoBase}/${tag}/${assetName}`;
|
||||
|
||||
ensureDir(cacheDir);
|
||||
const zipPath = path.join(cacheDir, assetName);
|
||||
const extractDir = path.join(cacheDir, `extract-${Date.now()}`);
|
||||
|
||||
console.log(`[export-runtime] Downloading ${downloadUrl}`);
|
||||
try {
|
||||
await downloadFileWithRetries(downloadUrl, zipPath);
|
||||
} catch (err) {
|
||||
try {
|
||||
fs.unlinkSync(zipPath);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log(`[export-runtime] Extracting ${zipPath}`);
|
||||
try {
|
||||
unzipArchive(zipPath, extractDir);
|
||||
const sourceRoot = resolveExtractedRoot(extractDir);
|
||||
ensurePyConverterLayout(sourceRoot);
|
||||
fs.rmSync(targetRoot, { recursive: true, force: true });
|
||||
ensureDir(targetRoot);
|
||||
fs.cpSync(sourceRoot, targetRoot, { recursive: true, force: true });
|
||||
} finally {
|
||||
fs.rmSync(extractDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
return { tag, downloadUrl };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const existing = validateExistingRuntime();
|
||||
|
||||
if (checkOnly) {
|
||||
if (!existing.ok) {
|
||||
throw new Error(existing.reason);
|
||||
}
|
||||
console.log("[export-runtime] Existing runtime is valid.");
|
||||
console.log(` - ${targetIndex}`);
|
||||
console.log(` - ${existing.converterPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing.ok && !forceDownload) {
|
||||
patchHtmlToImageRuntime();
|
||||
console.log("[export-runtime] Using existing runtime artifacts:");
|
||||
console.log(` - ${targetIndex}`);
|
||||
console.log(` - ${existing.converterPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existing.ok && hasExportDirectoryContent()) {
|
||||
console.log("[export-runtime] Existing export directory is invalid, re-syncing package.");
|
||||
}
|
||||
|
||||
const { tag, downloadUrl } = await downloadAndInstallRuntime();
|
||||
patchHtmlToImageRuntime();
|
||||
const installed = validateExistingRuntime();
|
||||
if (!installed.ok) {
|
||||
throw new Error(installed.reason);
|
||||
}
|
||||
|
||||
console.log("[export-runtime] Runtime synced successfully:");
|
||||
console.log(` - release: ${tag}`);
|
||||
console.log(` - url: ${downloadUrl}`);
|
||||
console.log(` - ${targetIndex}`);
|
||||
console.log(` - ${installed.converterPath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[export-runtime] ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
const appDistDir = path.join(repoRoot, "app_dist");
|
||||
const packageJson = require(path.join(repoRoot, "package.json"));
|
||||
|
||||
function walkFiles(dir, predicate, results = []) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return results;
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkFiles(fullPath, predicate, results);
|
||||
} else if (predicate(fullPath)) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function resolveSentryCli() {
|
||||
if (process.env.SENTRY_CLI_BIN) {
|
||||
return process.env.SENTRY_CLI_BIN;
|
||||
}
|
||||
|
||||
const localBinary = path.join(
|
||||
repoRoot,
|
||||
"node_modules",
|
||||
".bin",
|
||||
process.platform === "win32" ? "sentry-cli.cmd" : "sentry-cli",
|
||||
);
|
||||
|
||||
if (fs.existsSync(localBinary)) {
|
||||
return localBinary;
|
||||
}
|
||||
|
||||
return "sentry-cli";
|
||||
}
|
||||
|
||||
function run(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Failed to run ${command}: ${result.error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
const sourceMaps = walkFiles(appDistDir, (file) => file.endsWith(".js.map"));
|
||||
|
||||
if (sourceMaps.length === 0) {
|
||||
console.error(
|
||||
"No Electron sourcemaps found in app_dist. Run `npm run build:ts` before uploading.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const release =
|
||||
process.env.SENTRY_RELEASE || `presenton-electron@${packageJson.version}`;
|
||||
const urlPrefix = process.env.SENTRY_URL_PREFIX || "app:///app_dist";
|
||||
const sentryCli = resolveSentryCli();
|
||||
|
||||
const globalArgs = [];
|
||||
if (process.env.SENTRY_ORG) {
|
||||
globalArgs.push("--org", process.env.SENTRY_ORG);
|
||||
}
|
||||
if (process.env.SENTRY_PROJECT) {
|
||||
globalArgs.push("--project", process.env.SENTRY_PROJECT);
|
||||
}
|
||||
|
||||
const uploadArgs = [
|
||||
...globalArgs,
|
||||
"sourcemaps",
|
||||
"upload",
|
||||
appDistDir,
|
||||
"--release",
|
||||
release,
|
||||
"--url-prefix",
|
||||
urlPrefix,
|
||||
"--rewrite",
|
||||
"--validate",
|
||||
];
|
||||
|
||||
if (process.env.SENTRY_DIST) {
|
||||
uploadArgs.push("--dist", process.env.SENTRY_DIST);
|
||||
}
|
||||
|
||||
run(sentryCli, uploadArgs);
|
||||
Reference in New Issue
Block a user