Update dependencies and enhance build scripts for Electrobun
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

- Updated `electrobun` dependency to version `1.18.4-beta.19` in `package.json` and `bun.lock`.
- Added `rcedit` and `@tweenjs/tween.js` dependencies to `bun.lock`.
- Modified `Jenkinsfile` to clean up additional staging directories for production builds.
- Refactored `electrobun.config.ts` to dynamically read the `package.json` path.
- Introduced new scripts for managing Hutch installation and execution, improving the build process.
- Enhanced artifact finalization logic to support multiple build environments and streamline platform directory resolution.
This commit is contained in:
Tom Butcher 2026-08-07 21:35:16 +01:00
parent e81bc235a8
commit 6690516be3
11 changed files with 302 additions and 367 deletions

2
Jenkinsfile vendored
View File

@ -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
'''
}
}

View File

@ -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=="],

View File

@ -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;

View File

@ -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",

127
scripts/ensure-hutch.mjs Normal file
View File

@ -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`);

View File

@ -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}`)

View File

@ -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')
)
}
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')

View File

@ -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}`,
];
}

View File

@ -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);
}

View File

@ -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,15 +14,20 @@ const targetArch = getReleaseArch(
);
const bundleCef = isBundleCefEnabled();
const patchResult = spawnSync("bun", [path.join(rootDir, "scripts/patch-electrobun-src.mjs")], {
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",
@ -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",

View File

@ -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 <dev|build|run> [...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);