Enhance macOS window effects validation and build process
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
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:
parent
8d9455eaa9
commit
a8339aa9f8
@ -5,7 +5,27 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const srcFile = path.join(rootDir, "native/macos/window-effects.mm");
|
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() {
|
function createPlaceholder() {
|
||||||
mkdirSync(path.dirname(outFile), { recursive: true });
|
mkdirSync(path.dirname(outFile), { recursive: true });
|
||||||
@ -13,6 +33,49 @@ function createPlaceholder() {
|
|||||||
console.log(`build-macos-effects: created placeholder dylib at ${outFile}`);
|
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") {
|
if (process.platform !== "darwin") {
|
||||||
createPlaceholder();
|
createPlaceholder();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
@ -25,12 +88,16 @@ if (!existsSync(srcFile)) {
|
|||||||
|
|
||||||
mkdirSync(path.dirname(outFile), { recursive: true });
|
mkdirSync(path.dirname(outFile), { recursive: true });
|
||||||
|
|
||||||
|
const targetArch = resolveTargetArch();
|
||||||
const result = spawnSync(
|
const result = spawnSync(
|
||||||
"xcrun",
|
"xcrun",
|
||||||
[
|
[
|
||||||
"clang++",
|
"clang++",
|
||||||
"-dynamiclib",
|
"-dynamiclib",
|
||||||
"-fobjc-arc",
|
"-fobjc-arc",
|
||||||
|
"-arch",
|
||||||
|
targetArch,
|
||||||
|
"-mmacosx-version-min=11.0",
|
||||||
"-framework",
|
"-framework",
|
||||||
"Cocoa",
|
"Cocoa",
|
||||||
srcFile,
|
srcFile,
|
||||||
@ -44,5 +111,11 @@ if (result.status !== 0) {
|
|||||||
process.exit(result.status ?? 1);
|
process.exit(result.status ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateMacosEffectsDylib({ dylibPath: outFile, expectedArch: targetArch })) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const { size } = statSync(outFile);
|
const { size } = statSync(outFile);
|
||||||
console.log(`build-macos-effects: built ${outFile} (${size} bytes)`);
|
console.log(
|
||||||
|
`build-macos-effects: built ${outFile} (${size} bytes, arch=${targetArch})`,
|
||||||
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { spawnSync } from "node:child_process";
|
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 rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev";
|
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || "dev";
|
||||||
@ -46,6 +46,21 @@ if (buildMacosEffects.status !== 0) {
|
|||||||
process.exit(buildMacosEffects.status ?? 1);
|
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(
|
const prepareIcons = spawnSync(
|
||||||
"bun",
|
"bun",
|
||||||
[
|
[
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import { getReleaseArch } from "./release-artifact-utils.mjs";
|
import { getReleaseArch } from "./release-artifact-utils.mjs";
|
||||||
|
|
||||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
@ -36,6 +36,21 @@ if (macosEffectsResult.status !== 0) {
|
|||||||
process.exit(macosEffectsResult.status ?? 1);
|
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 = {
|
const ensureCoreEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
ELECTROBUN_TARGET_ARCH: targetArch,
|
ELECTROBUN_TARGET_ARCH: targetArch,
|
||||||
|
|||||||
@ -4,22 +4,38 @@ import path from 'node:path'
|
|||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
export const MAC_WINDOW_CORNER_RADIUS = 15
|
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() {
|
function resolveDylibPath() {
|
||||||
|
const candidates = new Set()
|
||||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url))
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
const candidates = [
|
|
||||||
path.join(moduleDir, '../bun/libMacWindowEffects.dylib'),
|
candidates.add(path.join(moduleDir, DYLIB_NAME))
|
||||||
path.join(moduleDir, 'libMacWindowEffects.dylib')
|
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) {
|
for (const candidate of candidates) {
|
||||||
if (!existsSync(candidate)) {
|
const resolved = path.resolve(candidate)
|
||||||
|
if (!existsSync(resolved)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (statSync(candidate).size > 0) {
|
if (statSync(resolved).size >= MIN_DYLIB_BYTES) {
|
||||||
return candidate
|
return resolved
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore unreadable paths and keep searching.
|
// Ignore unreadable paths and keep searching.
|
||||||
@ -29,50 +45,99 @@ function resolveDylibPath() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyMacOSWindowEffects(mainWindow) {
|
function loadMacWindowEffectsLibrary() {
|
||||||
if (process.platform !== 'darwin' || !mainWindow?.ptr) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const dylibPath = resolveDylibPath()
|
const dylibPath = resolveDylibPath()
|
||||||
if (!dylibPath) {
|
if (!dylibPath) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'macOS vibrancy: native effects library not found; using transparent window only.'
|
'macOS vibrancy: native effects library not found; using transparent window only.'
|
||||||
)
|
)
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const lib = dlopen(dylibPath, {
|
return {
|
||||||
enableWindowVibrancy: {
|
path: dylibPath,
|
||||||
args: [FFIType.ptr, FFIType.f64],
|
lib: dlopen(dylibPath, {
|
||||||
returns: FFIType.bool
|
enableWindowVibrancy: {
|
||||||
},
|
args: [FFIType.ptr, FFIType.f64],
|
||||||
ensureWindowShadow: {
|
returns: FFIType.bool
|
||||||
args: [FFIType.ptr],
|
},
|
||||||
returns: FFIType.bool
|
ensureWindowShadow: {
|
||||||
},
|
args: [FFIType.ptr],
|
||||||
setWindowCornerRadius: {
|
returns: FFIType.bool
|
||||||
args: [FFIType.ptr, FFIType.f64],
|
},
|
||||||
returns: FFIType.bool
|
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(
|
const vibrancyEnabled = lib.symbols.enableWindowVibrancy(
|
||||||
mainWindow.ptr,
|
mainWindow.ptr,
|
||||||
MAC_WINDOW_CORNER_RADIUS
|
MAC_WINDOW_CORNER_RADIUS
|
||||||
)
|
)
|
||||||
const shadowEnabled = lib.symbols.ensureWindowShadow(mainWindow.ptr)
|
const shadowEnabled = lib.symbols.ensureWindowShadow(mainWindow.ptr)
|
||||||
|
const trafficLightsScheduled = applyTrafficLightsPosition(lib, mainWindow)
|
||||||
|
|
||||||
mainWindow.on?.('resize', () => {
|
mainWindow.on?.('resize', () => {
|
||||||
lib.symbols.setWindowCornerRadius(
|
lib.symbols.setWindowCornerRadius(
|
||||||
mainWindow.ptr,
|
mainWindow.ptr,
|
||||||
MAC_WINDOW_CORNER_RADIUS
|
MAC_WINDOW_CORNER_RADIUS
|
||||||
)
|
)
|
||||||
|
lib.symbols.setWindowTrafficLightsPosition(
|
||||||
|
mainWindow.ptr,
|
||||||
|
MAC_TRAFFIC_LIGHT_OFFSET.x,
|
||||||
|
MAC_TRAFFIC_LIGHT_OFFSET.y
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(
|
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) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun'
|
import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun'
|
||||||
import { applyApplicationMenu, setupApplicationMenuEvents } from './menu.js'
|
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'
|
import { sendToRenderer, setMessageSender } from './notify.js'
|
||||||
|
|
||||||
const isMacOS = process.platform === 'darwin'
|
const isMacOS = process.platform === 'darwin'
|
||||||
@ -137,7 +140,9 @@ export async function createMainWindow(rpc) {
|
|||||||
url,
|
url,
|
||||||
rpc,
|
rpc,
|
||||||
titleBarStyle: 'hiddenInset',
|
titleBarStyle: 'hiddenInset',
|
||||||
...(isMacOS ? { transparent: true } : {}),
|
...(isMacOS
|
||||||
|
? { transparent: true, trafficLightOffset: MAC_TRAFFIC_LIGHT_OFFSET }
|
||||||
|
: {}),
|
||||||
frame: {
|
frame: {
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user