chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:08:41 +08:00
commit bcc7968ff6
9988 changed files with 1016778 additions and 0 deletions
@@ -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);
+79
View File
@@ -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.`);
+35
View File
@@ -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);
}
+86
View File
@@ -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);
}
+32
View File
@@ -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,
};
+703
View File
@@ -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)));
+334
View File
@@ -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()
+546
View File
@@ -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);
});
+104
View File
@@ -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);