Enhance macOS window effects validation and build process
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

- Introduced functions to validate the macOS dynamic library, ensuring it meets size and architecture requirements.
- Updated build scripts to resolve target architecture dynamically and validate the generated dylib.
- Enhanced the macOS window effects implementation to include traffic light positioning and improved error handling.
- Refactored the dylib path resolution logic to support multiple potential locations for the library.
This commit is contained in:
Tom Butcher 2026-08-02 13:05:28 +01:00
parent 8d9455eaa9
commit a8339aa9f8
5 changed files with 207 additions and 34 deletions

View File

@ -5,7 +5,27 @@ import { fileURLToPath } from "node:url";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const srcFile = path.join(rootDir, "native/macos/window-effects.mm");
const outFile = path.join(rootDir, "src/bun/libMacWindowEffects.dylib");
export const outFile = path.join(rootDir, "src/bun/libMacWindowEffects.dylib");
const minDylibBytes = 10_000;
function normalizeArch(value) {
if (value === "x64" || value === "amd64" || value === "x86_64") {
return "x86_64";
}
if (value === "arm64" || value === "aarch64") {
return "arm64";
}
return null;
}
export function resolveTargetArch() {
return (
normalizeArch(process.env.ELECTROBUN_TARGET_ARCH) ||
normalizeArch(process.env.ELECTROBUN_ARCH) ||
normalizeArch(process.arch) ||
"arm64"
);
}
function createPlaceholder() {
mkdirSync(path.dirname(outFile), { recursive: true });
@ -13,6 +33,49 @@ function createPlaceholder() {
console.log(`build-macos-effects: created placeholder dylib at ${outFile}`);
}
function readDylibArch(dylibPath) {
const lipo = spawnSync("lipo", ["-info", dylibPath], { encoding: "utf8" });
if (lipo.status === 0) {
return lipo.stdout;
}
const file = spawnSync("file", ["-b", dylibPath], { encoding: "utf8" });
return file.stdout || "";
}
export function validateMacosEffectsDylib({
dylibPath = outFile,
expectedArch = resolveTargetArch(),
required = process.platform === "darwin",
} = {}) {
if (!required) {
return true;
}
if (!existsSync(dylibPath)) {
console.error(`build-macos-effects: missing dylib at ${dylibPath}`);
return false;
}
const { size } = statSync(dylibPath);
if (size < minDylibBytes) {
console.error(
`build-macos-effects: dylib at ${dylibPath} is too small (${size} bytes)`,
);
return false;
}
const archInfo = readDylibArch(dylibPath);
if (!archInfo.includes(expectedArch)) {
console.error(
`build-macos-effects: dylib architecture mismatch (expected ${expectedArch}): ${archInfo.trim()}`,
);
return false;
}
return true;
}
if (process.platform !== "darwin") {
createPlaceholder();
process.exit(0);
@ -25,12 +88,16 @@ if (!existsSync(srcFile)) {
mkdirSync(path.dirname(outFile), { recursive: true });
const targetArch = resolveTargetArch();
const result = spawnSync(
"xcrun",
[
"clang++",
"-dynamiclib",
"-fobjc-arc",
"-arch",
targetArch,
"-mmacosx-version-min=11.0",
"-framework",
"Cocoa",
srcFile,
@ -44,5 +111,11 @@ if (result.status !== 0) {
process.exit(result.status ?? 1);
}
if (!validateMacosEffectsDylib({ dylibPath: outFile, expectedArch: targetArch })) {
process.exit(1);
}
const { size } = statSync(outFile);
console.log(`build-macos-effects: built ${outFile} (${size} bytes)`);
console.log(
`build-macos-effects: built ${outFile} (${size} bytes, arch=${targetArch})`,
);

View File

@ -1,7 +1,7 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev";
@ -46,6 +46,21 @@ if (buildMacosEffects.status !== 0) {
process.exit(buildMacosEffects.status ?? 1);
}
if (process.platform === "darwin") {
const { validateMacosEffectsDylib, outFile } = await import(
pathToFileURL(
path.join(rootDir, "scripts/build-macos-effects.mjs"),
).href
);
if (!validateMacosEffectsDylib({ dylibPath: outFile })) {
console.error(
"pre-build: macOS window effects dylib is missing or invalid; blur and traffic lights will not work in packaged builds.",
);
process.exit(1);
}
}
const prepareIcons = spawnSync(
"bun",
[

View File

@ -1,6 +1,6 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import { getReleaseArch } from "./release-artifact-utils.mjs";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@ -36,6 +36,21 @@ if (macosEffectsResult.status !== 0) {
process.exit(macosEffectsResult.status ?? 1);
}
if (process.platform === "darwin") {
const { validateMacosEffectsDylib, outFile } = await import(
pathToFileURL(
path.join(rootDir, "scripts/build-macos-effects.mjs"),
).href
);
if (!validateMacosEffectsDylib({ dylibPath: outFile })) {
console.error(
"run-electrobun-build: macOS window effects dylib is missing or invalid.",
);
process.exit(1);
}
}
const ensureCoreEnv = {
...process.env,
ELECTROBUN_TARGET_ARCH: targetArch,

View File

@ -4,22 +4,38 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
export const MAC_WINDOW_CORNER_RADIUS = 15
export const MAC_TRAFFIC_LIGHT_OFFSET = { x: 14, y: 12 }
const DYLIB_NAME = 'libMacWindowEffects.dylib'
const MIN_DYLIB_BYTES = 10_000
function resolveDylibPath() {
const candidates = new Set()
const moduleDir = path.dirname(fileURLToPath(import.meta.url))
const candidates = [
path.join(moduleDir, '../bun/libMacWindowEffects.dylib'),
path.join(moduleDir, 'libMacWindowEffects.dylib')
]
candidates.add(path.join(moduleDir, DYLIB_NAME))
candidates.add(path.join(moduleDir, '../bun', DYLIB_NAME))
candidates.add(path.join(moduleDir, '../../bun', DYLIB_NAME))
candidates.add(path.join(process.cwd(), 'src/bun', DYLIB_NAME))
for (const execPath of [process.execPath, process.argv0].filter(Boolean)) {
const execDir = path.dirname(execPath)
candidates.add(
path.join(execDir, '../Resources/app/bun', DYLIB_NAME)
)
candidates.add(path.join(execDir, 'Resources/app/bun', DYLIB_NAME))
candidates.add(path.join(execDir, DYLIB_NAME))
}
for (const candidate of candidates) {
if (!existsSync(candidate)) {
const resolved = path.resolve(candidate)
if (!existsSync(resolved)) {
continue
}
try {
if (statSync(candidate).size > 0) {
return candidate
if (statSync(resolved).size >= MIN_DYLIB_BYTES) {
return resolved
}
} catch {
// Ignore unreadable paths and keep searching.
@ -29,21 +45,19 @@ function resolveDylibPath() {
return null
}
export function applyMacOSWindowEffects(mainWindow) {
if (process.platform !== 'darwin' || !mainWindow?.ptr) {
return
}
function loadMacWindowEffectsLibrary() {
const dylibPath = resolveDylibPath()
if (!dylibPath) {
console.warn(
'macOS vibrancy: native effects library not found; using transparent window only.'
)
return
return null
}
try {
const lib = dlopen(dylibPath, {
return {
path: dylibPath,
lib: dlopen(dylibPath, {
enableWindowVibrancy: {
args: [FFIType.ptr, FFIType.f64],
returns: FFIType.bool
@ -55,24 +69,75 @@ export function applyMacOSWindowEffects(mainWindow) {
setWindowCornerRadius: {
args: [FFIType.ptr, FFIType.f64],
returns: FFIType.bool
},
setWindowTrafficLightsPosition: {
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
returns: FFIType.bool
},
setNativeWindowDragRegion: {
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
returns: FFIType.bool
}
})
}
} catch (error) {
console.warn(
`macOS vibrancy: failed to load native effects library (${dylibPath}):`,
error
)
return null
}
}
function applyTrafficLightsPosition(lib, mainWindow, attempt = 0) {
const applied = lib.symbols.setWindowTrafficLightsPosition(
mainWindow.ptr,
MAC_TRAFFIC_LIGHT_OFFSET.x,
MAC_TRAFFIC_LIGHT_OFFSET.y
)
if (applied || attempt >= 10) {
return applied
}
setTimeout(() => applyTrafficLightsPosition(lib, mainWindow, attempt + 1), 50)
return false
}
export function applyMacOSWindowEffects(mainWindow) {
if (process.platform !== 'darwin' || !mainWindow?.ptr) {
return
}
const loaded = loadMacWindowEffectsLibrary()
if (!loaded) {
return
}
const { path: dylibPath, lib } = loaded
try {
const vibrancyEnabled = lib.symbols.enableWindowVibrancy(
mainWindow.ptr,
MAC_WINDOW_CORNER_RADIUS
)
const shadowEnabled = lib.symbols.ensureWindowShadow(mainWindow.ptr)
const trafficLightsScheduled = applyTrafficLightsPosition(lib, mainWindow)
mainWindow.on?.('resize', () => {
lib.symbols.setWindowCornerRadius(
mainWindow.ptr,
MAC_WINDOW_CORNER_RADIUS
)
lib.symbols.setWindowTrafficLightsPosition(
mainWindow.ptr,
MAC_TRAFFIC_LIGHT_OFFSET.x,
MAC_TRAFFIC_LIGHT_OFFSET.y
)
})
console.log(
`macOS vibrancy applied (vibrancy=${vibrancyEnabled}, shadow=${shadowEnabled}, cornerRadius=${MAC_WINDOW_CORNER_RADIUS})`
`macOS vibrancy applied (dylib=${dylibPath}, vibrancy=${vibrancyEnabled}, shadow=${shadowEnabled}, trafficLights=${trafficLightsScheduled}, cornerRadius=${MAC_WINDOW_CORNER_RADIUS})`
)
} catch (error) {
console.warn(

View File

@ -1,6 +1,9 @@
import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun'
import { applyApplicationMenu, setupApplicationMenuEvents } from './menu.js'
import { applyMacOSWindowEffects } from './macos-window-effects.js'
import {
applyMacOSWindowEffects,
MAC_TRAFFIC_LIGHT_OFFSET
} from './macos-window-effects.js'
import { sendToRenderer, setMessageSender } from './notify.js'
const isMacOS = process.platform === 'darwin'
@ -137,7 +140,9 @@ export async function createMainWindow(rpc) {
url,
rpc,
titleBarStyle: 'hiddenInset',
...(isMacOS ? { transparent: true } : {}),
...(isMacOS
? { transparent: true, trafficLightOffset: MAC_TRAFFIC_LIGHT_OFFSET }
: {}),
frame: {
width: 1200,
height: 800,