Patch macOS keychain handling for CEF builds

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.
This commit is contained in:
2026-07-28 10:57:38 +12:00
parent 67eedf2fca
commit 0009ee83b0
6 changed files with 122 additions and 0 deletions
+19
View File
@@ -15,3 +15,22 @@ CompileFlags:
- -Ithirdparty/cef - -Ithirdparty/cef
- -Ithirdparty/cef/include - -Ithirdparty/cef/include
- -Wno-c++17-extensions - -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++
+27
View File
@@ -296,6 +296,33 @@ function(add_nebula_app_target nebula_target entry_source)
VERBATIM VERBATIM
) )
endforeach() 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() endif()
# ------------------------------------------------------------ # ------------------------------------------------------------
+51
View File
@@ -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())
+2
View File
@@ -241,11 +241,13 @@ void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layo
} }
void NebulaController::OnWindowCloseRequested() { void NebulaController::OnWindowCloseRequested() {
#if defined(_WIN32)
if (!closing_ && !closing_tab_browsers_.empty()) { if (!closing_ && !closing_tab_browsers_.empty()) {
// CEF Alloy can bubble a child browser close as WM_CLOSE on the host // 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. // window. Per-tab closes should not turn into full app shutdown.
return; return;
} }
#endif
if (launch_options_.mode == AppMode::BigPicture && !closing_) { if (launch_options_.mode == AppMode::BigPicture && !closing_) {
if (!SwitchToSiblingMode(AppMode::Desktop)) { if (!SwitchToSiblingMode(AppMode::Desktop)) {
+6
View File
@@ -83,6 +83,12 @@ void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type,
command_line->AppendSwitchWithValue("use-angle", "d3d11"); command_line->AppendSwitchWithValue("use-angle", "d3d11");
#elif defined(__APPLE__) #elif defined(__APPLE__)
command_line->AppendSwitchWithValue("use-angle", "metal"); 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 #else
command_line->AppendSwitchWithValue("use-gl", "egl"); command_line->AppendSwitchWithValue("use-gl", "egl");
#endif #endif
+17
View File
@@ -17,6 +17,23 @@ struct NebulaWindowImpl;
- (BOOL)isFlipped { - (BOOL)isFlipped {
return YES; 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 @end
@interface NebulaWindowDelegate : NSObject <NSWindowDelegate> { @interface NebulaWindowDelegate : NSObject <NSWindowDelegate> {