diff --git a/electron/app/ipc/export_handlers.ts b/electron/app/ipc/export_handlers.ts index 44eefe2b..74f41231 100644 --- a/electron/app/ipc/export_handlers.ts +++ b/electron/app/ipc/export_handlers.ts @@ -8,6 +8,8 @@ import { v4 as uuidv4 } from 'uuid'; import { spawn } from "child_process"; import { getPuppeteerExecutablePath } from "../utils/puppeteer-check"; +type BinaryFormat = "elf" | "mach-o" | "pe" | "unknown"; + export function setupExportHandlers() { ipcMain.handle("file-downloaded", async (_, filePath: string): Promise => { const fileName = path.basename(filePath); @@ -38,7 +40,7 @@ export function setupExportHandlers() { await fs.promises.writeFile(exportTaskPath, JSON.stringify(exportTask)); const exportScriptPath = path.join(baseDir, "resources", "export", "index.js"); - const pythonModulePath = path.join(baseDir, "resources", "export", "py", "convert"); + const pythonModulePath = await resolveConverterPath(baseDir); const puppeteerExecutablePath = await getPuppeteerExecutablePath(); console.log("[Export] Spawning export task with config:", { exportAs, @@ -125,6 +127,94 @@ export function setupExportHandlers() { } +async function resolveConverterPath(currentBaseDir: string): Promise { + const pyDir = path.join(currentBaseDir, "resources", "export", "py"); + const extension = process.platform === "win32" ? ".exe" : ""; + const converterCandidates = [ + path.join(pyDir, `convert-${process.platform}-${process.arch}${extension}`), + path.join(pyDir, `convert-${process.platform}${extension}`), + ...(process.platform === "win32" + ? [path.join(pyDir, "convert.exe"), path.join(pyDir, "convert")] + : [path.join(pyDir, "convert")]), + ]; + + const converterPath = await findFirstExistingPath(converterCandidates); + if (!converterPath) { + throw new Error( + [ + "No converter binary found for export.", + "Expected one of:", + ...converterCandidates.map((candidate) => ` - ${candidate}`), + ].join("\n") + ); + } + + const format = await detectBinaryFormat(converterPath); + if (!isBinaryFormatCompatible(format)) { + throw new Error( + [ + `Converter binary is not valid for ${process.platform}/${process.arch}.`, + `Selected converter: ${converterPath}`, + `Detected format: ${format}`, + "Please bundle a platform-correct converter binary (for example convert-darwin-arm64 or convert-darwin-x64).", + ].join("\n") + ); + } + + return converterPath; +} + +async function findFirstExistingPath(paths: string[]): Promise { + for (const candidate of paths) { + try { + await fs.promises.access(candidate, fs.constants.F_OK); + return candidate; + } catch { + continue; + } + } + return null; +} + +async function detectBinaryFormat(binaryPath: string): Promise { + const fd = await fs.promises.open(binaryPath, "r"); + try { + const header = Buffer.alloc(4); + await fd.read(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 { + await fd.close(); + } +} + +function isBinaryFormatCompatible(format: BinaryFormat): boolean { + 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 resolveExportedFilePath(responseData: any): string | null { if (responseData?.path && typeof responseData.path === "string") { return path.isAbsolute(responseData.path) diff --git a/electron/build.js b/electron/build.js index 2713f319..c422c2db 100644 --- a/electron/build.js +++ b/electron/build.js @@ -16,7 +16,12 @@ const afterPack = async (context) => { "resources" ) const fastapiPath = path.join(resourcesRoot, "fastapi", "fastapi") - const convertPath = path.join(resourcesRoot, "export", "py", "convert") + const exportPyDir = path.join(resourcesRoot, "export", "py") + const converterCandidates = [ + `convert-${process.platform}-${process.arch}`, + `convert-${process.platform}`, + "convert", + ] console.log("Setting executable permissions for FastAPI binary...") console.log("FastAPI path:", fastapiPath) @@ -29,13 +34,17 @@ const afterPack = async (context) => { } console.log("Setting executable permissions for export converter binary...") - console.log("Converter path:", convertPath) - - if (fs.existsSync(convertPath)) { - fs.chmodSync(convertPath, 0o755) - console.log("✓ Execute permissions set for converter") - } else { - console.warn("⚠ Converter binary not found at:", convertPath) + let converterFound = false + for (const candidate of converterCandidates) { + const candidatePath = path.join(exportPyDir, candidate) + if (fs.existsSync(candidatePath)) { + fs.chmodSync(candidatePath, 0o755) + console.log("✓ Execute permissions set for converter:", candidatePath) + converterFound = true + } + } + if (!converterFound) { + console.warn("⚠ No converter binary found in:", exportPyDir) } const fastapiDir = path.join(resourcesRoot, "fastapi") @@ -43,7 +52,6 @@ const afterPack = async (context) => { console.log("FastAPI directory contents:", fs.readdirSync(fastapiDir)) } - const exportPyDir = path.join(resourcesRoot, "export", "py") if (fs.existsSync(exportPyDir)) { console.log("Export py directory contents:", fs.readdirSync(exportPyDir)) } diff --git a/electron/sync_export_runtime.js b/electron/sync_export_runtime.js index 0e0637f8..2e84294b 100644 --- a/electron/sync_export_runtime.js +++ b/electron/sync_export_runtime.js @@ -3,7 +3,23 @@ const path = require("path"); const targetRoot = path.join(__dirname, "resources", "export"); const targetPyDir = path.join(targetRoot, "py"); const targetIndex = path.join(targetRoot, "index.js"); -const targetConvert = path.join(targetPyDir, "convert"); +const targetConvertDefault = path.join(targetPyDir, "convert"); + +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]; + + return [tagged, platformOnly, ...legacy]; +} function ensureExists(filePath, label) { if (!fs.existsSync(filePath)) { @@ -17,20 +33,81 @@ function chmodIfPossible(filePath) { } } +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 main() { ensureExists( targetIndex, "Committed runtime JS bundle (electron/resources/export/index.js)" ); - ensureExists( - targetConvert, - "Committed runtime converter binary (electron/resources/export/py/convert)" - ); - chmodIfPossible(targetConvert); + + const converterCandidates = getConverterCandidates(); + const converterPath = converterCandidates.find((candidate) => fs.existsSync(candidate)); + + if (!converterPath) { + throw new Error( + [ + "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)) { + throw new Error( + [ + `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") + ); + } + + chmodIfPossible(converterPath); console.log("[export-runtime] Using committed runtime artifacts:"); console.log(` - ${targetIndex}`); - console.log(` - ${targetConvert}`); + console.log(` - ${converterPath}`); } main();