diff --git a/Jenkinsfile b/Jenkinsfile index 5fd1dd0..c419f23 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -100,7 +100,7 @@ def prepareMacBuildWorkspace() { hdiutil detach "$vol" -force || true fi done - rm -rf build/stable-macos-*/.dmg-staging build/stable-macos-*/.finalize-dmg-staging 2>/dev/null || true + rm -rf build/stable-macos-*/.dmg-staging build/stable-macos-*/.finalize-dmg-staging build/production-macos-*/.dmg-staging build/production-macos-*/.finalize-dmg-staging 2>/dev/null || true ''' } } diff --git a/bun.lock b/bun.lock index d7402bc..f3e1d5f 100644 --- a/bun.lock +++ b/bun.lock @@ -77,7 +77,7 @@ "@eslint/js": "^9.39.2", "@vitejs/plugin-react": "^5.0.2", "concurrently": "^9.2.1", - "electrobun": "^1.18.1", + "electrobun": "1.18.4-beta.19", "electron": "^38.7.1", "electron-builder": "^26.0.12", "electron-packager": "^17.1.2", @@ -93,6 +93,7 @@ "png-to-ico": "^2.1.8", "prettier": "^3.6.2", "prettier-eslint": "^16.4.2", + "rcedit": "^4.0.1", "rollup-plugin-visualizer": "^6.0.5", "serve": "^14.2.4", "standard": "^17.1.2", @@ -837,6 +838,8 @@ "@tsparticles/updater-wobble": ["@tsparticles/updater-wobble@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-c99Ogy9q4QWO+zsDXol0UnpUwZiY2UucFb8ltuDv9AlbGUeprygoub8jhgT5pEDv+GdzWOJGSgq7rfgv9cHBrg=="], + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "7.29.0", "@babel/types": "7.29.0", "@types/babel__generator": "7.27.0", "@types/babel__template": "7.4.4", "@types/babel__traverse": "7.28.0" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "7.29.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -907,12 +910,18 @@ "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], + "@types/stylis": ["@types/stylis@4.2.7", "", {}, "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA=="], + "@types/three": ["@types/three@0.165.0", "", { "dependencies": { "@tweenjs/tween.js": "~23.1.1", "@types/stats.js": "*", "@types/webxr": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-AJK8JZAFNBF0kBXiAIl5pggYlzAGGA8geVYQXAcPCEDRbyA+oEjkpUBcJJrtNz6IiALwzGexFJGZG2yV3WsYBw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "4.4.3" }, "optionalDependencies": { "typescript": "5.9.3" }, "peerDependencies": { "eslint": "8.57.1" } }, "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ=="], @@ -1307,7 +1316,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electrobun": ["electrobun@1.18.1", "", { "dependencies": { "@babylonjs/core": "^7.45.0", "@types/bun": "^1.3.8", "png-to-ico": "^2.1.8", "proxy-agent": "^6.5.0", "rcedit": "^4.0.1", "three": "^0.165.0" }, "bin": { "electrobun": "bin/electrobun.cjs" } }, "sha512-tgZ+WKGskn/1/Y5i1mpVCCkgRa1O31Tz7MIArBTK44GfPzT43uOq9c/HvxdQGrLeZV8ZvjK1lQrwqSXCgh6tvA=="], + "electrobun": ["electrobun@1.18.4-beta.19", "", { "dependencies": { "@babylonjs/core": "^7.45.0", "@types/bun": "^1.3.8", "@types/three": "^0.165.0", "png-to-ico": "^2.1.8", "proxy-agent": "^6.5.0", "rcedit": "^4.0.1", "three": "^0.165.0" }, "bin": { "electrobun": "bin/electrobun.cjs" } }, "sha512-u8/Zhcn9ml3U0zxWUnCm09mmri6z0gnwjGG3b4K45kaugpbJww5VQ/YkhVzms/tRhyj8MiRY/HOHZppFDtBkTw=="], "electron": ["electron@38.8.6", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-lyBhcVi9QYAZL6FO6r5twAWAjWnYomo3iVDvrb5SJZlq928BGemHOKG0tPIq41NOLaCu9f3XdEEjMkjQPjprRg=="], @@ -1855,6 +1864,8 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "4.1.12", "debug": "4.4.3", "decode-named-character-reference": "1.3.0", "devlop": "1.1.0", "micromark-core-commonmark": "2.0.3", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-chunked": "2.0.1", "micromark-util-combine-extensions": "2.0.1", "micromark-util-decode-numeric-character-reference": "2.0.2", "micromark-util-encode": "2.0.1", "micromark-util-normalize-identifier": "2.0.1", "micromark-util-resolve-all": "2.0.1", "micromark-util-sanitize-uri": "2.0.1", "micromark-util-subtokenize": "2.1.0", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "1.3.0", "devlop": "1.1.0", "micromark-factory-destination": "2.0.1", "micromark-factory-label": "2.0.1", "micromark-factory-space": "2.0.1", "micromark-factory-title": "2.0.1", "micromark-factory-whitespace": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-chunked": "2.0.1", "micromark-util-classify-character": "2.0.1", "micromark-util-html-tag-name": "2.0.1", "micromark-util-normalize-identifier": "2.0.1", "micromark-util-resolve-all": "2.0.1", "micromark-util-subtokenize": "2.1.0", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], diff --git a/electrobun.config.ts b/electrobun.config.ts index 6ab2c6c..35b2241 100644 --- a/electrobun.config.ts +++ b/electrobun.config.ts @@ -1,7 +1,12 @@ import type { ElectrobunConfig } from "electrobun"; import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; -const packageJson = JSON.parse(readFileSync("./package.json", "utf8")); +const rootDir = path.dirname(fileURLToPath(import.meta.url)); +const packageJson = JSON.parse( + readFileSync(path.join(rootDir, "package.json"), "utf8"), +); const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev"; const isStable = buildEnv === "stable"; const canCodesign = Boolean(process.env.ELECTROBUN_DEVELOPER_ID); @@ -29,6 +34,7 @@ export default { exitOnLastWindowClosed: true, }, build: { + mainProcess: "bun", buildFolder: "build", artifactFolder: "app_dist", // macOS WebKit loads views:// assets reliably from a flat app folder; diff --git a/package.json b/package.json index b7746f3..cd5539a 100644 --- a/package.json +++ b/package.json @@ -80,12 +80,12 @@ "description": "3D Printer ERP and Control Software.", "scripts": { "dev": "cross-env NODE_ENV=development vite", - "dev:app": "concurrently \"cross-env NODE_ENV=development bun run dev:renderer\" \"cross-env NODE_ENV=development electrobun dev\"", + "dev:app": "concurrently \"cross-env NODE_ENV=development bun run dev:renderer\" \"cross-env NODE_ENV=development bun scripts/run-hutch-electrobun.mjs dev\"", "dev:renderer": "vite --port 5780 --no-open", "electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5780 && cross-env NODE_ENV=development && electron .", "start": "serve -s build", "build": "vite build", - "build:app": "electrobun build --env=stable", + "build:app": "bun scripts/run-hutch-electrobun.mjs build --env=stable", "build:app:mac": "bun scripts/build-macos.mjs", "dev:electron": "concurrently \"cross-env NODE_ENV=development vite --port 5780 --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5780 NODE_ENV=development electron public/electron.js\"", "build:electron": "vite build && electron-builder", @@ -118,7 +118,7 @@ "@eslint/js": "^9.39.2", "@vitejs/plugin-react": "^5.0.2", "concurrently": "^9.2.1", - "electrobun": "^1.18.1", + "electrobun": "1.18.4-beta.19", "electron": "^38.7.1", "jimp": "^1.6.1", "png-to-ico": "^2.1.8", @@ -134,6 +134,7 @@ "globals": "^15.12.0", "prettier": "^3.6.2", "prettier-eslint": "^16.4.2", + "rcedit": "^4.0.1", "rollup-plugin-visualizer": "^6.0.5", "serve": "^14.2.4", "standard": "^17.1.2", diff --git a/scripts/ensure-hutch.mjs b/scripts/ensure-hutch.mjs new file mode 100644 index 0000000..c660023 --- /dev/null +++ b/scripts/ensure-hutch.mjs @@ -0,0 +1,127 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { get } from "node:https"; +import { homedir, platform, tmpdir } from "node:os"; +import path from "node:path"; +import { + getElectrobunVersion, + hutchIsInstalled, + resolveHutchCommand, +} from "./resolve-hutch-command.mjs"; + +const installerBaseUrl = "https://hutch.blackboard.sh/hutch"; +const maxInstallerBytes = 1024 * 1024; + +function download(url, redirects = 0) { + if (redirects > 5) { + return Promise.reject(new Error("too many installer redirects")); + } + + return new Promise((resolve, reject) => { + const request = get(url, (response) => { + if ( + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location + ) { + response.resume(); + resolve( + download(new URL(response.headers.location, url).href, redirects + 1), + ); + return; + } + + if (response.statusCode !== 200) { + response.resume(); + reject( + new Error(`installer download returned HTTP ${response.statusCode}`), + ); + return; + } + + const chunks = []; + let size = 0; + response.on("data", (chunk) => { + size += chunk.length; + if (size > maxInstallerBytes) { + request.destroy(new Error("installer download exceeded 1 MiB")); + return; + } + chunks.push(chunk); + }); + response.on("end", () => resolve(Buffer.concat(chunks))); + }); + request.on("error", reject); + }); +} + +function checkedSpawn(command, args, options) { + const result = spawnSync(command, args, options); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(`${command} exited with status ${result.status ?? "unknown"}`); + } +} + +async function installHutch(channel) { + const temporary = mkdtempSync(path.join(tmpdir(), "electrobun-hutch-")); + try { + if (platform() === "win32") { + const installer = path.join(temporary, "install.ps1"); + writeFileSync(installer, await download(`${installerBaseUrl}/install.ps1`)); + checkedSpawn( + "powershell.exe", + [ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + installer, + "-Channel", + channel, + ], + { env: process.env, stdio: "inherit" }, + ); + } else { + const installer = path.join(temporary, "install.sh"); + writeFileSync(installer, await download(`${installerBaseUrl}/install.sh`), { + mode: 0o700, + }); + checkedSpawn("sh", [installer, "--channel", channel, "--no-modify-path"], { + env: process.env, + stdio: "inherit", + }); + } + } finally { + rmSync(temporary, { force: true, recursive: true }); + } +} + +const command = resolveHutchCommand(); +if (hutchIsInstalled(command)) { + console.log(`ensure-hutch: ${command} already present`); + process.exit(0); +} + +const version = getElectrobunVersion(); +const channel = version.includes("-") ? "canary" : "production"; + +if (process.env.ELECTROBUN_HUTCH_BINARY) { + console.error(`ensure-hutch: ELECTROBUN_HUTCH_BINARY does not exist: ${command}`); + process.exit(1); +} + +console.error( + `ensure-hutch: installing Hutch ${channel} for electrobun ${version}...`, +); +await installHutch(channel); + +if (!existsSync(command)) { + console.error(`ensure-hutch: Hutch was not installed at ${command}`); + process.exit(1); +} + +console.log(`ensure-hutch: ${command} ready`); diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index 767e6fe..285fd30 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -14,6 +14,8 @@ import { getReleaseArch, getReleaseArtifactName, getReleaseVersion, + getElectrobunOutputEnvName, + getElectrobunPlatformDirCandidates, isBundleCefEnabled } from './release-artifact-utils.mjs' import { @@ -59,13 +61,29 @@ function getBuildRoot() { } const baseName = path.basename(electrobunBuildDir) - if (baseName.startsWith('stable-')) { + if (/^(stable|production|dev|canary)-/.test(baseName)) { return path.dirname(electrobunBuildDir) } return electrobunBuildDir } +function resolvePlatformDir(os, arch) { + const buildRoot = getBuildRoot() + + for (const candidate of getElectrobunPlatformDirCandidates(os, arch, buildEnv)) { + const platformDir = path.join(buildRoot, candidate) + if (existsSync(platformDir)) { + return platformDir + } + } + + return path.join( + buildRoot, + `${getElectrobunOutputEnvName(buildEnv)}-${os}-${arch}` + ) +} + function walkFiles(dir) { const files = [] if (!existsSync(dir)) { @@ -92,7 +110,7 @@ function findByExtension(root, extension) { } function findMacAppBundle(arch) { - const platformDir = path.join(getBuildRoot(), `stable-macos-${arch}`) + const platformDir = resolvePlatformDir('macos', arch) if (!existsSync(platformDir)) { return null } @@ -107,19 +125,19 @@ function findMacAppBundle(arch) { } function findMacDmgSource(arch) { + const platformDir = resolvePlatformDir('macos', arch) + const platformDirName = path.basename(platformDir) + const prefixedArtifact = walkFiles(artifactDir).find( (filePath) => - filePath.includes(`stable-macos-${arch}`) && + filePath.includes(platformDirName) && filePath.toLowerCase().endsWith('.dmg') ) if (prefixedArtifact) { return prefixedArtifact } - const buildDmg = findByExtension( - path.join(getBuildRoot(), `stable-macos-${arch}`), - '.dmg' - ) + const buildDmg = findByExtension(platformDir, '.dmg') if (buildDmg) { return buildDmg } @@ -134,7 +152,7 @@ function findWindowsInstallerFiles() { } for (const entry of readdirSync(buildRoot)) { - if (!entry.startsWith('stable-win-')) { + if (!/^(stable|production|dev|canary)-win-/.test(entry)) { continue } @@ -183,7 +201,9 @@ function cleanStagingArtifacts(keepNames) { if ( entry.includes('stable-macos-') || + entry.includes('production-macos-') || entry.includes('stable-win-') || + entry.includes('production-win-') || entry.endsWith('-update.json') || entry.endsWith('.tar.gz') ) { @@ -455,7 +475,7 @@ async function buildMacDmg(appBundlePath, arch) { } function cleanMacBuildDir(arch) { - const platformDir = path.join(getBuildRoot(), `stable-macos-${arch}`) + const platformDir = resolvePlatformDir('macos', arch) if (existsSync(platformDir)) { rmSync(platformDir, { recursive: true, force: true }) console.log(`Removed build output ${platformDir}`) @@ -494,7 +514,7 @@ function stageWindowsDeeplinkScript(appDir) { } function cleanWinBuildDir(arch) { - const platformDir = path.join(getBuildRoot(), `stable-win-${arch}`) + const platformDir = resolvePlatformDir('win', arch) if (existsSync(platformDir)) { rmSync(platformDir, { recursive: true, force: true }) console.log(`Removed build output ${platformDir}`) diff --git a/scripts/patch-electrobun-src.mjs b/scripts/patch-electrobun-src.mjs index 0656823..2f4a4d8 100644 --- a/scripts/patch-electrobun-src.mjs +++ b/scripts/patch-electrobun-src.mjs @@ -1,350 +1,23 @@ -import { - cpSync, - existsSync, - mkdirSync, - readFileSync, - writeFileSync -} from 'node:fs' +import { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const electrobunDir = path.join(rootDir, 'node_modules/electrobun') -const sharedSrcDir = path.join(electrobunDir, 'src/shared') -const sharedDistDir = path.join(electrobunDir, 'dist/api/shared') +const rceditSrc = path.join(rootDir, 'node_modules/rcedit') +const rceditDest = path.join(electrobunDir, 'node_modules/rcedit') -if (!existsSync(sharedDistDir)) { - console.error('patch-electrobun-src: electrobun dist/api/shared not found') +if (!existsSync(rceditSrc)) { + console.error( + 'patch-electrobun-src: rcedit not found. Install it with `bun add -d rcedit`.' + ) process.exit(1) } -mkdirSync(sharedSrcDir, { recursive: true }) - -for (const fileName of [ - 'cef-version.ts', - 'bun-version.ts', - 'electrobun-version.ts', - 'naming.ts', - 'rpc.ts' -]) { - const source = path.join(sharedDistDir, fileName) - const destination = path.join(sharedSrcDir, fileName) - if (existsSync(source)) { - cpSync(source, destination) - } -} - -const platformPatch = `import { platform, arch } from "os"; - -export type SupportedOS = "macos" | "win" | "linux"; -export type SupportedArch = "arm64" | "x64"; - -const platformName = platform(); -const archName = arch(); - -export const OS: SupportedOS = (() => { - switch (platformName) { - case "win32": - return "win"; - case "darwin": - return "macos"; - case "linux": - return "linux"; - default: - throw new Error(\`Unsupported platform: \${platformName}\`); - } -})(); - -function normalizeForcedArch(value) { - if (value === "arm64" || value === "aarch64") { - return "arm64"; - } - if (value === "x64" || value === "amd64") { - return "x64"; - } - return null; -} - -export const ARCH: SupportedArch = (() => { - const forcedArch = normalizeForcedArch( - process.env.ELECTROBUN_FORCE_ARCH || process.env.ELECTROBUN_TARGET_ARCH, - ); - if (forcedArch) { - return forcedArch; - } - - if (OS === "win") { - return "x64"; - } - - switch (archName) { - case "arm64": - return "arm64"; - case "x64": - return "x64"; - default: - throw new Error(\`Unsupported architecture: \${archName}\`); - } -})(); - -export const HOST_ARCH: SupportedArch = (() => { - if (OS === "win") { - return "x64"; - } - - switch (archName) { - case "arm64": - return "arm64"; - case "x64": - return "x64"; - default: - throw new Error(\`Unsupported architecture: \${archName}\`); - } -})(); - -export function getPlatformOS(): SupportedOS { - return OS; -} - -export function getPlatformArch(): SupportedArch { - return ARCH; -} -` - -writeFileSync(path.join(sharedSrcDir, 'platform.ts'), platformPatch) - -const templatesDir = path.join(electrobunDir, 'src/cli/templates') -mkdirSync(templatesDir, { recursive: true }) -writeFileSync( - path.join(templatesDir, 'embedded.ts'), - `export function getTemplateNames() { - return []; -} - -export function getTemplate(name: string) { - throw new Error(\`Template not available in patched electrobun CLI: \${name}\`); -} -` -) - -const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) { - const candidates = [ - join(projectRoot, "node_modules/rcedit/package.json"), - join(projectRoot, "node_modules/electrobun/node_modules/rcedit/package.json"), - ]; - for (const candidate of candidates) { - if (existsSync(candidate)) { - return candidate; - } - } - throw new Error( - "rcedit not found under " + projectRoot + ". Install rcedit in the project (bun add -d rcedit).", - ); -}` - -const cliPath = path.join(electrobunDir, 'src/cli/index.ts') -let cliSource = readFileSync(cliPath, 'utf8') -if (!cliSource.includes('HOST_ARCH')) { - cliSource = cliSource.replace( - 'import { OS, ARCH } from "../shared/platform";', - 'import { OS, ARCH, HOST_ARCH } from "../shared/platform";' - ) - cliSource = cliSource.replace( - 'const hostPaths = getPlatformPaths(OS, ARCH);', - 'const hostPaths = getPlatformPaths(OS, HOST_ARCH);' - ) -} -if (!cliSource.includes('toolPaths')) { - cliSource = cliSource.replace( - 'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);', - 'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);' - ) - cliSource = cliSource.replaceAll( - 'const zstdPath = targetPaths.ZSTD;', - 'const zstdPath = toolPaths.ZSTD;' - ) - cliSource = cliSource.replaceAll( - 'const bsdiffpath = targetPaths.BSDIFF;', - 'const bsdiffpath = toolPaths.BSDIFF;' - ) - cliSource = cliSource.replace( - 'zigAsarCli = join(targetPaths.BSPATCH).replace("bspatch", "zig-asar");', - 'zigAsarCli = join(toolPaths.BSPATCH).replace("bspatch", "zig-asar");' - ) - writeFileSync(cliPath, cliSource) - cliSource = readFileSync(cliPath, 'utf8') -} -if (!cliSource.includes('hookBunBinary')) { - cliSource = cliSource.replace( - `const hostPaths = getPlatformPaths(OS, HOST_ARCH); - - const result = Bun.spawnSync([hostPaths.BUN_BINARY, hookScript],`, - `const hostPaths = getPlatformPaths(OS, HOST_ARCH); - const hookBunBinary = existsSync(hostPaths.BUN_BINARY) - ? hostPaths.BUN_BINARY - : process.execPath; - - const result = Bun.spawnSync([hookBunBinary, hookScript],` - ) - cliSource = cliSource.replace( - 'console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);', - 'console.error("Tried to run with bun at:", hookBunBinary);' - ) - writeFileSync(cliPath, cliSource) - cliSource = readFileSync(cliPath, 'utf8') -} - -if (!cliSource.includes('resolveProjectRceditPkgPath')) { - cliSource = cliSource.replaceAll( - 'const rceditPkgPath = require.resolve("rcedit/package.json");', - 'const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);' - ) - cliSource = cliSource.replace( - 'function getPlatformPaths(', - `${RCEDIT_RESOLVER_FN} - -function getPlatformPaths(` - ) - writeFileSync(cliPath, cliSource) - cliSource = readFileSync(cliPath, 'utf8') -} else { - cliSource = cliSource.replace( - /function resolveProjectRceditPkgPath\(projectRoot\) \{[\s\S]*?\n\}/m, - RCEDIT_RESOLVER_FN - ) - writeFileSync(cliPath, cliSource) -} - -const electrobunCjsPath = path.join(electrobunDir, 'bin/electrobun.cjs') -let electrobunCjs = readFileSync(electrobunCjsPath, 'utf8') - -if (!electrobunCjs.includes('farmcontrol-use-patched-cli')) { - electrobunCjs = electrobunCjs.replace( - `async function main() { - try { - const args = process.argv.slice(2); - const cliPath = await ensureCliBinary(); - - // Replace this process with the actual CLI - const child = spawn(cliPath, args, { - stdio: 'inherit', - cwd: process.cwd() - });`, - `async function main() { - try { - const args = process.argv.slice(2); - const patchedCliPath = join(electrobunDir, 'src', 'cli', 'index.ts'); - - if (existsSync(patchedCliPath)) { - // farmcontrol-use-patched-cli: bundled electrobun.exe cannot resolve project rcedit - const bunBinary = process.env.BUN_INSTALL - ? join(process.env.BUN_INSTALL, 'bin', 'bun' + binExt) - : 'bun'; - const child = spawn(bunBinary, [patchedCliPath, ...args], { - stdio: 'inherit', - cwd: process.cwd(), - env: process.env, - shell: platform === 'win', - }); - - child.on('exit', (code) => { - process.exit(code || 0); - }); - - child.on('error', (error) => { - console.error('Failed to start electrobun patched CLI:', error.message); - process.exit(1); - }); - - return; - } - - const cliPath = await ensureCliBinary(); - - // Replace this process with the actual CLI - const child = spawn(cliPath, args, { - stdio: 'inherit', - cwd: process.cwd() - });` - ) - writeFileSync(electrobunCjsPath, electrobunCjs) -} - -const rceditSrc = path.join(rootDir, 'node_modules/rcedit') -const rceditDest = path.join(electrobunDir, 'node_modules/rcedit') -if (existsSync(rceditSrc)) { - mkdirSync(path.join(electrobunDir, 'node_modules'), { recursive: true }) - cpSync(rceditSrc, rceditDest, { recursive: true, force: true }) -} - -function patchNativeWrapperPath(nativeTsPath) { - if (!existsSync(nativeTsPath)) { - return - } - - let source = readFileSync(nativeTsPath, 'utf8') - - if (source.includes('function resolveNativeWrapperPath()')) { - return - } - - if (!source.includes('existsSync')) { - if (source.includes('import { dirname, join } from "path";')) { - source = source.replace( - 'import { dirname, join } from "path";', - 'import { existsSync } from "fs";\nimport { dirname, join } from "path";' - ) - } else { - source = source.replace( - 'import { join } from "path";', - 'import { existsSync } from "fs";\nimport { dirname, join } from "path";' - ) - } - } else if (!source.includes('dirname')) { - source = source.replace( - 'import { join } from "path";', - 'import { dirname, join } from "path";' - ) - } - - const helper = ` -function resolveNativeWrapperPath() { -\tconst fileName = \`libNativeWrapper.\${suffix}\`; -\tconst candidates = [ -\t\tjoin(dirname(process.execPath), fileName), -\t\tjoin(process.cwd(), fileName), -\t]; -\tfor (const candidate of candidates) { -\t\tif (existsSync(candidate)) { -\t\t\treturn candidate; -\t\t} -\t} -\treturn candidates[0]!; -} -` - - source = source.replace( - 'export const native = (() => {', - `${helper}\nexport const native = (() => {` - ) - - source = source.replace( - /const nativeWrapperPath = join\((?:dirname\(process\.execPath\)|process\.cwd\(\)), `libNativeWrapper\.\$\{suffix\}`\);/, - 'const nativeWrapperPath = resolveNativeWrapperPath();' - ) - - writeFileSync(nativeTsPath, source) -} - -for (const distDir of ['dist', 'dist-macos-arm64', 'dist-win-x64']) { - patchNativeWrapperPath( - path.join(electrobunDir, distDir, 'api/bun/proc/native.ts') - ) -} +mkdirSync(path.join(electrobunDir, 'node_modules'), { recursive: true }) +cpSync(rceditSrc, rceditDest, { recursive: true, force: true }) const markerPath = path.join(electrobunDir, '.farmcontrol-electrobun-patched') writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`) -console.log( - 'patch-electrobun-src: electrobun src/shared ready for cross-arch builds' -) +console.log('patch-electrobun-src: rcedit ready for electrobun builds') diff --git a/scripts/release-artifact-utils.mjs b/scripts/release-artifact-utils.mjs index 83912ed..db4e03b 100644 --- a/scripts/release-artifact-utils.mjs +++ b/scripts/release-artifact-utils.mjs @@ -26,3 +26,26 @@ export function getReleaseArtifactName(version, arch, ext, options = {}) { const cefSuffix = cef ? "-cef" : ""; return `farmcontrol-${version}-${arch}${cefSuffix}.${ext}`; } + +export function getElectrobunOutputEnvName( + buildEnv = process.env.ELECTROBUN_BUILD_ENV || "stable", +) { + if (buildEnv === "stable") { + return "production"; + } + + return buildEnv; +} + +export function getElectrobunPlatformDirCandidates( + os, + arch, + buildEnv = process.env.ELECTROBUN_BUILD_ENV || "stable", +) { + const outputEnv = getElectrobunOutputEnvName(buildEnv); + return [ + `${outputEnv}-${os}-${arch}`, + `stable-${os}-${arch}`, + `${buildEnv}-${os}-${arch}`, + ]; +} diff --git a/scripts/resolve-hutch-command.mjs b/scripts/resolve-hutch-command.mjs new file mode 100644 index 0000000..6793b7a --- /dev/null +++ b/scripts/resolve-hutch-command.mjs @@ -0,0 +1,35 @@ +import { existsSync, readFileSync } from "node:fs"; +import { homedir, platform } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +function hutchChannelForElectrobunVersion(version) { + return version.includes("-") ? "canary" : "production"; +} + +function hutchBinaryName(channel) { + const base = channel === "canary" ? "hutch-canary" : "hutch"; + return platform() === "win32" ? `${base}.exe` : base; +} + +export function getElectrobunVersion() { + const packageJsonPath = path.join(rootDir, "node_modules/electrobun/package.json"); + return JSON.parse(readFileSync(packageJsonPath, "utf8")).version; +} + +export function resolveHutchCommand(environment = process.env) { + if (environment.ELECTROBUN_HUTCH_BINARY) { + return environment.ELECTROBUN_HUTCH_BINARY; + } + + const channel = hutchChannelForElectrobunVersion(getElectrobunVersion()); + const dashHome = environment.DASH_HOME || path.join(homedir(), ".dash"); + const pathApi = platform() === "win32" ? path.win32 : path.posix; + return pathApi.join(dashHome, "bin", hutchBinaryName(channel)); +} + +export function hutchIsInstalled(command = resolveHutchCommand()) { + return existsSync(command); +} diff --git a/scripts/run-electrobun-build.mjs b/scripts/run-electrobun-build.mjs index 53737d0..d4b1822 100644 --- a/scripts/run-electrobun-build.mjs +++ b/scripts/run-electrobun-build.mjs @@ -2,12 +2,9 @@ import { spawnSync } from "node:child_process"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { getReleaseArch, isBundleCefEnabled } from "./release-artifact-utils.mjs"; +import { resolveHutchCommand } from "./resolve-hutch-command.mjs"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const electrobunCli = path.join( - rootDir, - "node_modules/electrobun/src/cli/index.ts", -); const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "stable"; const targetArch = getReleaseArch( @@ -17,16 +14,21 @@ const targetArch = getReleaseArch( ); const bundleCef = isBundleCefEnabled(); -const patchResult = spawnSync("bun", [path.join(rootDir, "scripts/patch-electrobun-src.mjs")], { - cwd: rootDir, - stdio: "inherit", - env: process.env, -}); +function runScript(scriptName) { + const result = spawnSync("bun", [path.join(rootDir, "scripts", scriptName)], { + cwd: rootDir, + stdio: "inherit", + env: process.env, + }); -if (patchResult.status !== 0) { - process.exit(patchResult.status ?? 1); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } } +runScript("ensure-hutch.mjs"); +runScript("patch-electrobun-src.mjs"); + const macosEffectsResult = spawnSync( "bun", [path.join(rootDir, "scripts/build-macos-effects.mjs")], @@ -82,13 +84,15 @@ const env = { NODE_ENV: process.env.NODE_ENV || "production", }; +const hutchCommand = resolveHutchCommand(env); + console.log( `run-electrobun-build: env=${buildEnv} os=macos arch=${targetArch} cef=${bundleCef} (host ${getReleaseArch(process.arch)})`, ); const result = spawnSync( - "bun", - [electrobunCli, "build", `--env=${buildEnv}`], + hutchCommand, + ["electrobun", "build", `--env=${buildEnv}`], { cwd: rootDir, stdio: "inherit", diff --git a/scripts/run-hutch-electrobun.mjs b/scripts/run-hutch-electrobun.mjs new file mode 100644 index 0000000..6c89f43 --- /dev/null +++ b/scripts/run-hutch-electrobun.mjs @@ -0,0 +1,35 @@ +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { resolveHutchCommand } from "./resolve-hutch-command.mjs"; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const [, , subcommand, ...rest] = process.argv; + +if (!subcommand) { + console.error("usage: run-hutch-electrobun.mjs [...args]"); + process.exit(1); +} + +const ensureHutch = spawnSync( + "bun", + [path.join(rootDir, "scripts/ensure-hutch.mjs")], + { cwd: rootDir, stdio: "inherit", env: process.env }, +); + +if (ensureHutch.status !== 0) { + process.exit(ensureHutch.status ?? 1); +} + +const hutchCommand = resolveHutchCommand(); +const result = spawnSync( + hutchCommand, + ["electrobun", subcommand, ...rest], + { + cwd: rootDir, + stdio: "inherit", + env: process.env, + }, +); + +process.exit(result.status ?? 1);