mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(mac): opening General Settings crashes in packaged app (#111300)
* fix(macos): package KeyboardShortcuts resources * fix(macos): keep dependency patch whitespace-safe * fix(macos): preserve locked Swift resolution * fix(macos): guard Swift lock across builds * fix(macos): simplify KeyboardShortcuts packaging
This commit is contained in:
@@ -155,6 +155,14 @@ struct SettingsViewSmokeTests {
|
||||
_ = view.body
|
||||
}
|
||||
|
||||
@Test func `general settings renders the keyboard shortcut recorder`() {
|
||||
let state = AppState(preview: true)
|
||||
let hosting = NSHostingView(rootView: GeneralSettings(state: state))
|
||||
hosting.frame = NSRect(x: 0, y: 0, width: 760, height: 640)
|
||||
hosting.layoutSubtreeIfNeeded()
|
||||
_ = hosting.fittingSize
|
||||
}
|
||||
|
||||
@Test func `general settings exercises branches`() {
|
||||
GeneralSettings.exerciseForTesting()
|
||||
}
|
||||
|
||||
@@ -89,6 +89,28 @@ sparkle_framework_for_arch() {
|
||||
echo "$(build_path_for_arch "$1")/$BUILD_CONFIG/Sparkle.framework"
|
||||
}
|
||||
|
||||
run_with_locked_swift_packages() {
|
||||
local resolved_file="$ROOT_DIR/apps/macos/Package.resolved"
|
||||
local resolved_snapshot
|
||||
local command_status=0
|
||||
|
||||
if [[ ! -f "$resolved_file" ]]; then
|
||||
echo "ERROR: Swift package lockfile not found at $resolved_file" >&2
|
||||
return 1
|
||||
fi
|
||||
resolved_snapshot="$(mktemp)"
|
||||
cp "$resolved_file" "$resolved_snapshot"
|
||||
"$@" || command_status=$?
|
||||
if ! cmp -s "$resolved_snapshot" "$resolved_file"; then
|
||||
cp "$resolved_snapshot" "$resolved_file"
|
||||
rm "$resolved_snapshot"
|
||||
echo "ERROR: Swift package resolution changed Package.resolved; update it in a separate reviewed change" >&2
|
||||
return 1
|
||||
fi
|
||||
rm "$resolved_snapshot"
|
||||
return "$command_status"
|
||||
}
|
||||
|
||||
PNPM_CMD=()
|
||||
|
||||
resolve_pnpm_cmd() {
|
||||
@@ -220,8 +242,10 @@ cd "$ROOT_DIR/apps/macos"
|
||||
echo "🔨 Building $PRODUCT ($BUILD_CONFIG) [${BUILD_ARCHS[*]}]"
|
||||
for arch in "${BUILD_ARCHS[@]}"; do
|
||||
BUILD_PATH="$(build_path_for_arch "$arch")"
|
||||
echo "📦 Resolving Swift packages [$arch]"
|
||||
run_with_locked_swift_packages swift package --scratch-path "$BUILD_PATH" resolve
|
||||
echo "🔨 Building $PRODUCT ($BUILD_CONFIG) [$arch]"
|
||||
swift build -c "$BUILD_CONFIG" --product "$PRODUCT" --build-path "$BUILD_PATH" --arch "$arch" -Xlinker -rpath -Xlinker @executable_path/../Frameworks
|
||||
run_with_locked_swift_packages swift build -c "$BUILD_CONFIG" --product "$PRODUCT" --build-path "$BUILD_PATH" --arch "$arch" -Xlinker -rpath -Xlinker @executable_path/../Frameworks
|
||||
echo "🔨 Building $MLX_TTS_HELPER_PRODUCT ($BUILD_CONFIG) [$arch]"
|
||||
swift build --package-path "$MLX_TTS_HELPER_ROOT" -c "$BUILD_CONFIG" --product "$MLX_TTS_HELPER_PRODUCT" --build-path "$(helper_build_path_for_arch "$arch")" --arch "$arch"
|
||||
done
|
||||
@@ -366,6 +390,18 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "⌨️ Copying KeyboardShortcuts resources"
|
||||
KEYBOARD_SHORTCUTS_BUNDLE="$(build_path_for_arch "$PRIMARY_ARCH")/$BUILD_CONFIG/KeyboardShortcuts_KeyboardShortcuts.bundle"
|
||||
if [ -d "$KEYBOARD_SHORTCUTS_BUNDLE" ]; then
|
||||
# SwiftPM's generated Bundle.module accessor searches Bundle.main.resourceURL for app resources.
|
||||
# Keep this under Contents/Resources or Recorder localization traps before Settings renders.
|
||||
rm -rf "$APP_ROOT/Contents/Resources/KeyboardShortcuts_KeyboardShortcuts.bundle"
|
||||
cp -R "$KEYBOARD_SHORTCUTS_BUNDLE" "$APP_ROOT/Contents/Resources/KeyboardShortcuts_KeyboardShortcuts.bundle"
|
||||
else
|
||||
echo "ERROR: KeyboardShortcuts resource bundle not found at $KEYBOARD_SHORTCUTS_BUNDLE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
running_packaged_app_pids() {
|
||||
command -v pgrep >/dev/null 2>&1 || return 0
|
||||
local app_binary="$APP_ROOT/Contents/MacOS/OpenClaw"
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// Package Mac App tests cover package mac app script behavior.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
const scriptPath = "scripts/package-mac-app.sh";
|
||||
|
||||
function makePlist(): string {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-plistbuddy-"));
|
||||
tempDirs.push(dir);
|
||||
const dir = tempDirs.make("openclaw-plistbuddy-");
|
||||
const plist = path.join(dir, "Info.plist");
|
||||
writeFileSync(
|
||||
plist,
|
||||
@@ -70,6 +69,17 @@ function getSparkleBuildHelperBlock(): string {
|
||||
return script.slice(start, end);
|
||||
}
|
||||
|
||||
function getSwiftPackageResolutionBlock(): string {
|
||||
const script = readFileSync(scriptPath, "utf8");
|
||||
const start = script.indexOf("run_with_locked_swift_packages()");
|
||||
const end = script.indexOf("PNPM_CMD=()");
|
||||
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
|
||||
return script.slice(start, end);
|
||||
}
|
||||
|
||||
function getStopPackagedAppBlock(): string {
|
||||
const script = readFileSync(scriptPath, "utf8");
|
||||
const start = script.indexOf("running_packaged_app_pids()");
|
||||
@@ -93,9 +103,8 @@ function getSwiftCompatibilityBlock(): string {
|
||||
}
|
||||
|
||||
function runStopPackagedAppHarness(killZeroStatus: 0 | 1) {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "openclaw-package-stop-root-"));
|
||||
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-package-stop-tools-"));
|
||||
tempDirs.push(root, toolsDir);
|
||||
const root = tempDirs.make("openclaw-package-stop-root-");
|
||||
const toolsDir = tempDirs.make("openclaw-package-stop-tools-");
|
||||
|
||||
const appRoot = path.join(root, "dist", "OpenClaw.app");
|
||||
const appBinary = path.join(appRoot, "Contents", "MacOS", "OpenClaw");
|
||||
@@ -131,12 +140,11 @@ function runStopPackagedAppHarness(killZeroStatus: 0 | 1) {
|
||||
}
|
||||
|
||||
function runSwiftCompatibilityHarness(buildConfig: "debug" | "release") {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "openclaw-package-swift-root-"));
|
||||
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-package-swift-tools-"));
|
||||
const root = tempDirs.make("openclaw-package-swift-root-");
|
||||
const toolsDir = tempDirs.make("openclaw-package-swift-tools-");
|
||||
const developerDir = path.join(root, "Xcode.app", "Contents", "Developer");
|
||||
const appRoot = path.join(root, "OpenClaw.app");
|
||||
const xcodeSelectPath = path.join(toolsDir, "xcode-select");
|
||||
tempDirs.push(root, toolsDir);
|
||||
|
||||
writeFileSync(
|
||||
xcodeSelectPath,
|
||||
@@ -155,11 +163,34 @@ function runSwiftCompatibilityHarness(buildConfig: "debug" | "release") {
|
||||
`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
function runSwiftPackageResolutionHarness(mutateLockfile: boolean) {
|
||||
const root = tempDirs.make("openclaw-swift-resolve-root-");
|
||||
const toolsDir = tempDirs.make("openclaw-swift-resolve-tools-");
|
||||
const resolvedFile = path.join(root, "apps", "macos", "Package.resolved");
|
||||
const swiftPath = path.join(toolsDir, "swift");
|
||||
|
||||
mkdirSync(path.dirname(resolvedFile), { recursive: true });
|
||||
writeFileSync(resolvedFile, "locked\n", { encoding: "utf8", flag: "wx" });
|
||||
writeFileSync(
|
||||
swiftPath,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
mutateLockfile ? `printf 'changed\\n' > ${JSON.stringify(resolvedFile)}` : ":",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
chmodSync(swiftPath, 0o755);
|
||||
|
||||
const result = runHelper(`
|
||||
set -euo pipefail
|
||||
ROOT_DIR=${JSON.stringify(root)}
|
||||
PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)}
|
||||
${getSwiftPackageResolutionBlock()}
|
||||
run_with_locked_swift_packages swift package --scratch-path "$ROOT_DIR/apps/macos/.build/arm64" resolve
|
||||
`);
|
||||
|
||||
return { result, resolvedFile };
|
||||
}
|
||||
|
||||
describe("package-mac-app plist stamping", () => {
|
||||
it("resolves canonical build provenance and rejects explicit invalid overrides", () => {
|
||||
@@ -354,10 +385,9 @@ describe("package-mac-app plist stamping", () => {
|
||||
|
||||
it("falls back to corepack pnpm when the pnpm shim is absent", () => {
|
||||
const helperBlock = getPackageManagerHelperBlock();
|
||||
const tempRoot = mkdtempSync(path.join(tmpdir(), "openclaw-package-pnpm-root-"));
|
||||
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-package-pnpm-tools-"));
|
||||
const tempRoot = tempDirs.make("openclaw-package-pnpm-root-");
|
||||
const toolsDir = tempDirs.make("openclaw-package-pnpm-tools-");
|
||||
const logPath = path.join(tempRoot, "corepack.log");
|
||||
tempDirs.push(tempRoot, toolsDir);
|
||||
|
||||
const corepackPath = path.join(toolsDir, "corepack");
|
||||
writeFileSync(
|
||||
@@ -396,11 +426,10 @@ describe("package-mac-app plist stamping", () => {
|
||||
|
||||
it("prefers repo Corepack pnpm over a global pnpm shim", () => {
|
||||
const helperBlock = getPackageManagerHelperBlock();
|
||||
const tempRoot = mkdtempSync(path.join(tmpdir(), "openclaw-package-pnpm-root-"));
|
||||
const outerRoot = mkdtempSync(path.join(tmpdir(), "openclaw-package-pnpm-outer-"));
|
||||
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-package-pnpm-tools-"));
|
||||
const tempRoot = tempDirs.make("openclaw-package-pnpm-root-");
|
||||
const outerRoot = tempDirs.make("openclaw-package-pnpm-outer-");
|
||||
const toolsDir = tempDirs.make("openclaw-package-pnpm-tools-");
|
||||
const logPath = path.join(tempRoot, "pnpm.log");
|
||||
tempDirs.push(tempRoot, outerRoot, toolsDir);
|
||||
|
||||
writeFileSync(
|
||||
path.join(tempRoot, "package.json"),
|
||||
@@ -458,9 +487,8 @@ describe("package-mac-app plist stamping", () => {
|
||||
|
||||
it("fails with an actionable error when neither pnpm nor corepack pnpm is available", () => {
|
||||
const helperBlock = getPackageManagerHelperBlock();
|
||||
const tempRoot = mkdtempSync(path.join(tmpdir(), "openclaw-package-pnpm-root-"));
|
||||
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-package-pnpm-tools-"));
|
||||
tempDirs.push(tempRoot, toolsDir);
|
||||
const tempRoot = tempDirs.make("openclaw-package-pnpm-root-");
|
||||
const toolsDir = tempDirs.make("openclaw-package-pnpm-tools-");
|
||||
|
||||
const result = runHelper(`
|
||||
set -euo pipefail
|
||||
@@ -485,8 +513,7 @@ describe("package-mac-app plist stamping", () => {
|
||||
|
||||
it("fails with an actionable error when Swift tools are too old", () => {
|
||||
const helperBlock = getSwiftToolchainBlock();
|
||||
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-package-swift-tools-"));
|
||||
tempDirs.push(toolsDir);
|
||||
const toolsDir = tempDirs.make("openclaw-package-swift-tools-");
|
||||
|
||||
const swiftPath = path.join(toolsDir, "swift");
|
||||
writeFileSync(
|
||||
@@ -514,8 +541,7 @@ describe("package-mac-app plist stamping", () => {
|
||||
|
||||
it("accepts Swift tools 6.2 or newer", () => {
|
||||
const helperBlock = getSwiftToolchainBlock();
|
||||
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-package-swift-tools-"));
|
||||
tempDirs.push(toolsDir);
|
||||
const toolsDir = tempDirs.make("openclaw-package-swift-tools-");
|
||||
|
||||
const swiftPath = path.join(toolsDir, "swift");
|
||||
writeFileSync(
|
||||
@@ -542,9 +568,8 @@ describe("package-mac-app plist stamping", () => {
|
||||
|
||||
it("runs Sparkle build metadata derivation from the repository root", () => {
|
||||
const helperBlock = getSparkleBuildHelperBlock();
|
||||
const tempRoot = mkdtempSync(path.join(tmpdir(), "openclaw-package-sparkle-root-"));
|
||||
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-package-sparkle-tools-"));
|
||||
tempDirs.push(tempRoot, toolsDir);
|
||||
const tempRoot = tempDirs.make("openclaw-package-sparkle-root-");
|
||||
const toolsDir = tempDirs.make("openclaw-package-sparkle-tools-");
|
||||
|
||||
const nodePath = path.join(toolsDir, "node");
|
||||
writeFileSync(
|
||||
@@ -642,6 +667,10 @@ describe("package-mac-app plist stamping", () => {
|
||||
script.indexOf(
|
||||
'OPENCLAWKIT_BUNDLE="$(build_path_for_arch "$PRIMARY_ARCH")/$BUILD_CONFIG/OpenClawKit_OpenClawKit.bundle"',
|
||||
),
|
||||
script.indexOf('echo "⌨️ Copying KeyboardShortcuts resources"'),
|
||||
);
|
||||
const keyboardShortcutsBlock = script.slice(
|
||||
script.indexOf('echo "⌨️ Copying KeyboardShortcuts resources"'),
|
||||
script.indexOf("running_packaged_app_pids()"),
|
||||
);
|
||||
|
||||
@@ -653,10 +682,50 @@ describe("package-mac-app plist stamping", () => {
|
||||
expect(openClawKitBlock).toContain("exit 1");
|
||||
expect(openClawKitBlock).not.toContain("WARN:");
|
||||
expect(openClawKitBlock).not.toContain("continuing");
|
||||
expect(keyboardShortcutsBlock).toContain("KeyboardShortcuts_KeyboardShortcuts.bundle");
|
||||
expect(keyboardShortcutsBlock).toContain(
|
||||
'cp -R "$KEYBOARD_SHORTCUTS_BUNDLE" "$APP_ROOT/Contents/Resources/KeyboardShortcuts_KeyboardShortcuts.bundle"',
|
||||
);
|
||||
expect(keyboardShortcutsBlock).toContain("ERROR: KeyboardShortcuts resource bundle not found");
|
||||
expect(keyboardShortcutsBlock).toContain("exit 1");
|
||||
expect(keyboardShortcutsBlock).not.toContain("WARN:");
|
||||
expect(keyboardShortcutsBlock).not.toContain("continuing");
|
||||
expect(script).not.toContain("Textual resource bundle");
|
||||
expect(script).not.toContain("ALLOW_MISSING_TEXTUAL_BUNDLE");
|
||||
});
|
||||
|
||||
it("preserves locked Swift package resolution before building", () => {
|
||||
const script = readFileSync(scriptPath, "utf8");
|
||||
const resolveCall =
|
||||
'run_with_locked_swift_packages swift package --scratch-path "$BUILD_PATH" resolve';
|
||||
const buildCall =
|
||||
'run_with_locked_swift_packages swift build -c "$BUILD_CONFIG" --product "$PRODUCT"';
|
||||
|
||||
expect(script).toContain('resolved_file="$ROOT_DIR/apps/macos/Package.resolved"');
|
||||
expect(script).toContain('cmp -s "$resolved_snapshot" "$resolved_file"');
|
||||
expect(script).toContain('cp "$resolved_snapshot" "$resolved_file"');
|
||||
expect(script).toContain("ERROR: Swift package resolution changed Package.resolved");
|
||||
expect(script).toContain(resolveCall);
|
||||
expect(script).toContain(buildCall);
|
||||
expect(script.indexOf(resolveCall)).toBeLessThan(script.indexOf(buildCall));
|
||||
});
|
||||
|
||||
it("restores and rejects a Swift package resolution that changes the lockfile", () => {
|
||||
const { result, resolvedFile } = runSwiftPackageResolutionHarness(true);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("ERROR: Swift package resolution changed Package.resolved");
|
||||
expect(readFileSync(resolvedFile, "utf8")).toBe("locked\n");
|
||||
});
|
||||
|
||||
it("accepts a Swift package resolution that preserves the lockfile", () => {
|
||||
const { result, resolvedFile } = runSwiftPackageResolutionHarness(false);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
expect(readFileSync(resolvedFile, "utf8")).toBe("locked\n");
|
||||
});
|
||||
|
||||
it("embeds the canonical CLI installer as a signed app resource", () => {
|
||||
const script = readFileSync(scriptPath, "utf8");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user