Refactor export runtime synchronization script
- Removed the deprecated convert binary file from the project. - Enhanced the runtime synchronization logic to support dynamic version fetching from GitHub releases. - Improved platform and architecture detection for the converter candidates. - Added functions for downloading, extracting, and validating the export runtime. - Implemented error handling for HTTP requests and file operations. - Updated the main function to handle existing runtime validation and conditional downloading.
This commit is contained in:
parent
3d1932b5a5
commit
0b64a2aedb
4 changed files with 290 additions and 1072 deletions
|
|
@ -2,6 +2,7 @@
|
|||
"name": "presenton",
|
||||
"productName": "Presenton Open Source",
|
||||
"version": "0.6.2-beta",
|
||||
"exportVersion": "v0.1.0",
|
||||
"main": "app_dist/main.js",
|
||||
"description": "Open-Source AI Presentation Generator",
|
||||
"homepage": "https://presenton.ai",
|
||||
|
|
@ -29,12 +30,15 @@
|
|||
"dist": "electron-builder",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"dev": "rm -rf app_dist && tsc && electron .",
|
||||
"setup:env": "npm install && cd servers/fastapi && uv sync && cd ../../servers/nextjs && npm install",
|
||||
"setup:env": "npm install && cd servers/fastapi && uv sync && cd ../../servers/nextjs && npm install && cd ../.. && npm run setup:export-runtime",
|
||||
"install:pyinstaller": "cd servers/fastapi && echo 'pyinstaller already in dependencies'",
|
||||
"build:ts": "rm -rf app_dist && tsc",
|
||||
"build:css": "tailwindcss -i ./resources/ui/assets/css/tailwind.import.css -o ./resources/ui/assets/css/tailwind.css --watch",
|
||||
"build:vectorstore": "cd servers/fastapi && uv run python build_vectorstore.py",
|
||||
"build:export-runtime": "node sync_export_runtime.js",
|
||||
"setup:export-runtime": "node sync_export_runtime.js",
|
||||
"fetch:export-runtime": "node sync_export_runtime.js --force",
|
||||
"fetch:export-runtime:latest": "EXPORT_RUNTIME_VERSION=latest node sync_export_runtime.js --force",
|
||||
"build:nextjs": "rm -rf resources/nextjs && cd servers/nextjs && cross-env BUILD_TARGET=electron npm run build && cp -r .next-build ../../resources/nextjs && cp -r app/presentation-templates ../../resources/nextjs/presentation-templates",
|
||||
"build:fastapi": "rm -rf resources/fastapi && npm run build:vectorstore && (cp ../servers/fastapi/alembic/versions/*.py servers/fastapi/alembic/versions/ 2>/dev/null || true) && cd servers/fastapi && uv run python -m PyInstaller --distpath ../../resources server.spec",
|
||||
"generate:version": "node generate_update.js",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -1,30 +1,83 @@
|
|||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"));
|
||||
|
||||
const targetRoot = path.join(__dirname, "resources", "export");
|
||||
const targetPyDir = path.join(targetRoot, "py");
|
||||
const targetIndex = path.join(targetRoot, "index.js");
|
||||
const targetConvertDefault = path.join(targetPyDir, "convert");
|
||||
const cacheDir = path.join(__dirname, ".cache", "export-runtime");
|
||||
const exportRepoBase = "https://github.com/presenton/presenton-export/releases/download";
|
||||
const exportVersion = packageJson.exportVersion || "v0.1.0";
|
||||
|
||||
function getConverterCandidates() {
|
||||
const tagged = path.join(
|
||||
targetPyDir,
|
||||
`convert-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`
|
||||
);
|
||||
const platformOnly = path.join(
|
||||
targetPyDir,
|
||||
`convert-${process.platform}${process.platform === "win32" ? ".exe" : ""}`
|
||||
);
|
||||
const legacy = process.platform === "win32"
|
||||
? [path.join(targetPyDir, "convert.exe"), targetConvertDefault]
|
||||
: [targetConvertDefault];
|
||||
const cliArgs = new Set(process.argv.slice(2));
|
||||
const forceDownload = cliArgs.has("--force");
|
||||
const checkOnly = cliArgs.has("--check-only");
|
||||
|
||||
return [tagged, platformOnly, ...legacy];
|
||||
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 ensureExists(filePath, label) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`${label} not found at: ${filePath}`);
|
||||
function getPlatformAssetName() {
|
||||
const platformArch = `${process.platform}-${process.arch}`;
|
||||
if (platformArch === "linux-x64") return "export-Linux-X64.zip";
|
||||
if (platformArch === "darwin-arm64") return "export-macOS-ARM64.zip";
|
||||
if (platformArch === "win32-x64") return "export-Windows-X64.zip";
|
||||
|
||||
throw new Error(
|
||||
`Unsupported export runtime platform: ${platformArch}. Supported: linux-x64, darwin-arm64, 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) {
|
||||
|
|
@ -72,42 +125,238 @@ function isFormatCompatible(format) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function main() {
|
||||
ensureExists(
|
||||
targetIndex,
|
||||
"Committed runtime JS bundle (electron/resources/export/index.js)"
|
||||
);
|
||||
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) {
|
||||
throw new Error(
|
||||
[
|
||||
return {
|
||||
ok: false,
|
||||
reason: [
|
||||
"No converter binary found in electron/resources/export/py.",
|
||||
"Expected one of:",
|
||||
...converterCandidates.map((candidate) => ` - ${candidate}`),
|
||||
].join("\n")
|
||||
);
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
const binaryFormat = detectBinaryFormat(converterPath);
|
||||
if (!isFormatCompatible(binaryFormat)) {
|
||||
throw new Error(
|
||||
[
|
||||
return {
|
||||
ok: false,
|
||||
reason: [
|
||||
`Converter binary is not valid for ${process.platform}/${process.arch}.`,
|
||||
`Selected converter: ${converterPath}`,
|
||||
`Detected format: ${binaryFormat}`,
|
||||
"Bundle a platform-correct converter binary (e.g. convert-darwin-arm64, convert-darwin-x64, convert-linux-x64, convert.exe).",
|
||||
].join("\n")
|
||||
);
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
chmodIfPossible(converterPath);
|
||||
|
||||
console.log("[export-runtime] Using committed runtime artifacts:");
|
||||
console.log(` - ${targetIndex}`);
|
||||
console.log(` - ${converterPath}`);
|
||||
return { ok: true, converterPath };
|
||||
}
|
||||
|
||||
main();
|
||||
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 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);
|
||||
});
|
||||
}
|
||||
|
||||
function unzipArchive(zipPath, destDir) {
|
||||
ensureDir(destDir);
|
||||
if (process.platform === "win32") {
|
||||
execFileSync(
|
||||
"powershell",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
`Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`,
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
execFileSync("unzip", ["-o", zipPath, "-d", destDir], { stdio: "inherit" });
|
||||
}
|
||||
|
||||
function resolveExtractedRoot(extractDir) {
|
||||
const directIndex = path.join(extractDir, "index.js");
|
||||
const directPy = path.join(extractDir, "py");
|
||||
if (fs.existsSync(directIndex) && fs.existsSync(directPy)) {
|
||||
return extractDir;
|
||||
}
|
||||
|
||||
const children = fs.readdirSync(extractDir, { withFileTypes: true });
|
||||
for (const entry of children) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const candidate = path.join(extractDir, entry.name);
|
||||
const candidateIndex = path.join(candidate, "index.js");
|
||||
const candidatePy = path.join(candidate, "py");
|
||||
if (fs.existsSync(candidateIndex) && fs.existsSync(candidatePy)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to locate export runtime root under ${extractDir}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
await downloadFile(downloadUrl, zipPath);
|
||||
|
||||
console.log(`[export-runtime] Extracting ${zipPath}`);
|
||||
unzipArchive(zipPath, extractDir);
|
||||
|
||||
const sourceRoot = resolveExtractedRoot(extractDir);
|
||||
fs.rmSync(targetRoot, { recursive: true, force: true });
|
||||
ensureDir(targetRoot);
|
||||
fs.cpSync(sourceRoot, targetRoot, { recursive: true, force: true });
|
||||
|
||||
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) {
|
||||
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();
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue