From 0009ee83b0739a01c6fb36353b780a8af9b80bf8 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 28 Jul 2026 10:57:38 +1200 Subject: [PATCH] Patch macOS keychain handling for CEF builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a macOS post-build step to patch CEF’s keychain service string to a Nebula-specific name, then re-sign the modified framework (with configurable signing identity or ad-hoc). Also enables Chromium’s `use-mock-keychain` switch on Apple builds to prevent repeated local keychain prompts, fixes titlebar traffic-light hit testing in the mac window view, scopes a WM_CLOSE guard to Windows only, and updates `.clangd` to ignore non-mac platform sources during macOS development. --- .clangd | 19 ++++++++++ CMakeLists.txt | 27 ++++++++++++++ cmake/macos_patch_cef_keychain.py | 51 +++++++++++++++++++++++++++ src/app/nebula_controller.cpp | 2 ++ src/cef/nebula_app.cpp | 6 ++++ src/platform/mac/nebula_window_mac.mm | 17 +++++++++ 6 files changed, 122 insertions(+) create mode 100755 cmake/macos_patch_cef_keychain.py diff --git a/.clangd b/.clangd index 27ca799..9c482e7 100644 --- a/.clangd +++ b/.clangd @@ -15,3 +15,22 @@ CompileFlags: - -Ithirdparty/cef - -Ithirdparty/cef/include - -Wno-c++17-extensions + +# On macOS builds, Windows/Linux platform sources are not compiled. Ignore them +# so clangd does not try to parse windows.h / Linux headers with the Mac SDK. +--- +If: + PathMatch: src/platform/win/.* +CompileFlags: + Add: + - -Wno-everything + Remove: + - -xobjective-c++ +--- +If: + PathMatch: src/platform/linux/.* +CompileFlags: + Add: + - -Wno-everything + Remove: + - -xobjective-c++ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c52a31..e1f1433 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,6 +296,33 @@ function(add_nebula_app_target nebula_target entry_source) VERBATIM ) endforeach() + + # Give Nebula its own OSCrypt keychain service name, then resign so the + # patched framework still loads. Optional stable identity: + # cmake -DNEBULA_CODESIGN_IDENTITY="Apple Development: ..." + set(NEBULA_CODESIGN_IDENTITY "" CACHE STRING + "codesign identity for macOS app bundles (empty = ad-hoc)") + if(NEBULA_CODESIGN_IDENTITY) + set(_nebula_codesign_identity "${NEBULA_CODESIGN_IDENTITY}") + else() + set(_nebula_codesign_identity "-") + endif() + find_package(Python3 COMPONENTS Interpreter REQUIRED) + set(_nebula_cef_framework + "${NEBULA_APP}/Contents/Frameworks/Chromium Embedded Framework.framework") + set(_nebula_cef_binary + "${_nebula_cef_framework}/Versions/Current/Chromium Embedded Framework") + add_custom_command(TARGET ${nebula_target} POST_BUILD + COMMAND ${Python3_EXECUTABLE} + "${CMAKE_SOURCE_DIR}/cmake/macos_patch_cef_keychain.py" + "${_nebula_cef_binary}" + COMMAND codesign --force --sign "${_nebula_codesign_identity}" --timestamp=none + "${_nebula_cef_binary}" + COMMAND codesign --force --sign "${_nebula_codesign_identity}" --timestamp=none + "${_nebula_cef_framework}" + COMMENT "Patching CEF keychain service name and resigning framework" + VERBATIM + ) endif() # ------------------------------------------------------------ diff --git a/cmake/macos_patch_cef_keychain.py b/cmake/macos_patch_cef_keychain.py new file mode 100755 index 0000000..19cb84c --- /dev/null +++ b/cmake/macos_patch_cef_keychain.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Replace CEF's shared Chromium keychain service name with a Nebula-specific one. + +CEF/Chromium stores the OSCrypt cookie-encryption key in the login keychain under +"Chromium Safe Storage". That item is often owned by another Chromium-based app, +so unsigned/ad-hoc NebulaBrowser builds get a password prompt on every launch. + +The replacement must be the same length as the original (embedded C string). +""" + +from __future__ import annotations + +import argparse +import sys + +OLD = b"Chromium Safe Storage" +NEW = b"NebulaBrowser Storage" + +assert len(OLD) == len(NEW), "replacement must be the same length" + + +def patch(path: str) -> int: + with open(path, "rb") as handle: + data = bytearray(handle.read()) + + count = data.count(OLD) + if count == 0: + if NEW in data: + print(f"Already patched: {path}") + return 0 + print(f"ERROR: {OLD!r} not found in {path}", file=sys.stderr) + return 1 + + data = data.replace(OLD, NEW) + with open(path, "wb") as handle: + handle.write(data) + + print(f"Patched {count} occurrence(s) in {path}") + print(f" {OLD.decode()} -> {NEW.decode()}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("framework_binary") + args = parser.parse_args() + return patch(args.framework_binary) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/app/nebula_controller.cpp b/src/app/nebula_controller.cpp index e0762e0..4c0f649 100644 --- a/src/app/nebula_controller.cpp +++ b/src/app/nebula_controller.cpp @@ -241,11 +241,13 @@ void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layo } void NebulaController::OnWindowCloseRequested() { +#if defined(_WIN32) if (!closing_ && !closing_tab_browsers_.empty()) { // CEF Alloy can bubble a child browser close as WM_CLOSE on the host // window. Per-tab closes should not turn into full app shutdown. return; } +#endif if (launch_options_.mode == AppMode::BigPicture && !closing_) { if (!SwitchToSiblingMode(AppMode::Desktop)) { diff --git a/src/cef/nebula_app.cpp b/src/cef/nebula_app.cpp index 8f91404..ea24526 100644 --- a/src/cef/nebula_app.cpp +++ b/src/cef/nebula_app.cpp @@ -83,6 +83,12 @@ void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type, command_line->AppendSwitchWithValue("use-angle", "d3d11"); #elif defined(__APPLE__) command_line->AppendSwitchWithValue("use-angle", "metal"); + // Unsigned/ad-hoc local builds cannot get a stable Keychain ACL, which + // causes repeated "wants to use your confidential information" prompts. + // CEF's own samples use this switch for the same reason. Cookie encryption + // still works via Chromium's fixed mock password; real Keychain backing + // should only be re-enabled once the app is signed with a stable identity. + command_line->AppendSwitch("use-mock-keychain"); #else command_line->AppendSwitchWithValue("use-gl", "egl"); #endif diff --git a/src/platform/mac/nebula_window_mac.mm b/src/platform/mac/nebula_window_mac.mm index f817a35..166cf66 100644 --- a/src/platform/mac/nebula_window_mac.mm +++ b/src/platform/mac/nebula_window_mac.mm @@ -17,6 +17,23 @@ struct NebulaWindowImpl; - (BOOL)isFlipped { return YES; } + +// FullSizeContentView draws our CEF chrome under the titlebar. Without this, +// that view eats clicks meant for the system traffic lights. +- (NSView*)hitTest:(NSPoint)point { + NSWindow* window = [self window]; + NSButton* close_button = [window standardWindowButton:NSWindowCloseButton]; + NSView* button_container = close_button ? [close_button superview] : nil; + if (button_container && ![close_button isHidden]) { + const NSPoint window_point = [[self superview] convertPoint:point toView:nil]; + const NSRect container_rect = [button_container convertRect:[button_container bounds] + toView:nil]; + if (NSPointInRect(window_point, container_rect)) { + return nil; + } + } + return [super hitTest:point]; +} @end @interface NebulaWindowDelegate : NSObject {