Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@ tests/visual_report.html
# Local scratch (screenshots, etc.)
tmp/
tmp-*/

# CEF binary distribution — provisioned by CEF/vendor/fetch_cef.sh
# (~270 MiB compressed, ~1.4 GiB extracted; never commit).
CEF/CEF/
CEF/Frameworks/
CEF/.build/
CEF/.swiftpm/
CEF/Package.resolved
1 change: 1 addition & 0 deletions CEF/CEFArtifacts
77 changes: 77 additions & 0 deletions CEF/INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# CEF integration notes

CEF is wired into `GhosttyTabs.xcodeproj` as a local Swift package named
`CMUXCEF`. New browser panes can opt into it through the Debug menu browser
engine selector; WKWebView remains the default.

## Source checkout setup

Run the repo setup script from the root:

```bash
./scripts/setup.sh
```

That script:

1. Initializes submodules.
2. Builds GhosttyKit.
3. Runs `CEF/vendor/fetch_cef.sh` to provision the CEF SDK used by SwiftPM and
the helper build.

The CEF SDK step downloads the pinned tarball from `vendor/cef.lock.json`,
verifies size and SHA1, extracts it under `CEF/CEF/`, builds
`libcef_dll_wrapper.a`, and populates `CEF/Frameworks/`.

## Xcode build behavior

The `Embed CEF` build phase runs `CEF/Scripts/embed_cef_into_cmux.sh`.

The build phase does not download the SDK. If `CEF/Frameworks/` is missing, it
fails with an explicit setup message. This keeps large network downloads in
`./scripts/setup.sh` instead of hiding them inside a regular Xcode build.

By default the build phase embeds only the small helper apps. It removes any
bundled `Chromium Embedded Framework.framework` so the app can start without a
large local runtime. CI or release experiments can opt into bundling the
framework with:

```bash
CMUX_EMBED_CEF_FRAMEWORK=1 ./scripts/reload.sh --tag cef-bundled
```

## App runtime behavior

When CEF is selected from `Debug > Browser Engine` and no runtime is installed,
cmux asks the user for confirmation, downloads the pinned runtime, verifies the
size and SHA1, installs it under Application Support, and starts CEF from that
installed framework. If install or startup fails, cmux leaves WKWebView
available.

CEF runtime startup and helper execution require macOS 15.0 or later. On older
macOS versions, the browser engine selector falls back to WKWebView.

The installed runtime is keyed by the app bundle ID, so tagged debug builds are
isolated from each other. A given app bundle ID reuses the runtime on subsequent
launches.

## Signing and entitlements

Debug builds use `Resources/cmux.debug.entitlements`. Helper apps get the JIT
and executable-memory entitlements needed by Chromium; development-only helper
entitlements such as `get-task-allow` are added only for Debug helper builds.

Release builds use `Resources/cmux.entitlements`. Do not add debug-only
entitlements to the release file.

## Verification

Use a tagged debug build:

```bash
./scripts/reload.sh --tag cef-dev
```

Then launch the printed `.app`, switch `Debug > Browser Engine > CEF`, and
confirm that the runtime progress window appears only on first use. Switching
back to WKWebView should not remove the installed runtime.
151 changes: 151 additions & 0 deletions CEF/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// swift-tools-version: 6.1
//
// CMUXCEF — cmux-owned Swift facade for embedding CEF Chrome runtime
// browsers inside cmux. See CEF/README.md and CEF/INTEGRATION.md for
// the local build and app-runtime installation model.
//
// This package is intentionally **isolated**:
// * It depends only on the system AppKit / Foundation modules and on
// the CEF framework + libcef_dll_wrapper.a placed by
// vendor/fetch_cef.sh.
// * It does **not** import anything from cmux.app.
// * It produces a public Swift library (CMUXCEF) plus two helper
// executables (CMUXCEFHelper, CMUXCEFHelperRenderer) that ship as
// embedded helper .app bundles inside cmux.app/Contents/Frameworks/.
//
// The Frameworks/ directory layout consumed here is the one prepared by
// `vendor/fetch_cef.sh`. `./scripts/setup.sh` runs it for cmux source
// checkouts; installed cmux apps download the CEF runtime separately on
// first CEF use.

import Foundation
import PackageDescription

// `vendor/fetch_cef.sh` produces:
// <package>/Frameworks/Chromium Embedded Framework.framework
// <package>/Frameworks/libcef_dll_wrapper.a
// <package>/Frameworks/include/
// We point cxxSettings + linkerSettings at that Frameworks/ directory.
// (Inside the prototype we used a `CEFArtifacts` symlink; in the cmux
// vendored copy we drop straight into `Frameworks/`.)
private let cefFrameworksDir: String = {
let pkgDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
return pkgDir.appendingPathComponent("Frameworks").path
}()

let package = Package(
name: "CMUXCEF",
platforms: [
// Keep the package buildable at cmux's deployment target so the app can
// launch and fall back to WKWebView on older hosts. CEFEngine.start()
// and the helper bundles enforce the actual CEF runtime floor:
// macOS 15.0 or later.
.macOS(.v14),
],
products: [
.library(name: "CMUXCEF", targets: ["CMUXCEF"]),
.executable(name: "CMUXCEFHelper", targets: ["CMUXCEFHelper"]),
.executable(name: "CMUXCEFHelperRenderer", targets: ["CMUXCEFHelperRenderer"]),
.executable(name: "CMUXCEFDemoApp", targets: ["CMUXCEFDemoApp"]),
],
targets: [
// MARK: - Bridge — ObjC++ bridge that owns all CEF C++ interop.
.target(
name: "CMUXCEFBridge",
publicHeadersPath: "include",
cxxSettings: [
.headerSearchPath("../../Frameworks/include"),
.headerSearchPath("../../Frameworks"),
.unsafeFlags(["-std=c++20"]),
],
linkerSettings: [
// Everything in one unsafeFlags block so the linker sees
// -L/-F before -l/-weak_framework. `.linkedLibrary` /
// `.linkedFramework` are not used because SwiftPM/Xcode
// do not preserve their position relative to unsafeFlags.
.unsafeFlags([
"-L", cefFrameworksDir,
"-F", cefFrameworksDir,
"-lcef_dll_wrapper",
// Keep the CEF framework weak-linked so cmux can launch
// before the optional runtime has been installed. The
// bridge calls cef_load_library() with the resolved
// runtime path before touching CEF APIs.
"-Xlinker", "-weak_framework",
"-Xlinker", "Chromium Embedded Framework",
]),
]
),

// MARK: - CMUXCEF — Swift facade. The only API cmux app code
// imports.
.target(
name: "CMUXCEF",
dependencies: ["CMUXCEFBridge"]
),

// MARK: - Helper executables. Both are tiny — they exist solely
// so CEF's multi-process launcher has a binary to spawn for each
// helper role. Real per-process logic lives in CEF itself.
.executableTarget(
name: "CMUXCEFHelper",
dependencies: ["CMUXCEFBridge"],
sources: ["main.mm"],
cxxSettings: [
.headerSearchPath("../../Frameworks/include"),
.headerSearchPath("../../Frameworks"),
.unsafeFlags(["-std=c++20"]),
],
linkerSettings: [
.linkedFramework("Foundation"),
.linkedFramework("AppKit"),
.unsafeFlags([
"-L", cefFrameworksDir,
"-F", cefFrameworksDir,
"-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../..",
"-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../../Frameworks",
]),
]
),
.executableTarget(
name: "CMUXCEFHelperRenderer",
dependencies: ["CMUXCEFBridge"],
sources: ["main.mm"],
cxxSettings: [
.headerSearchPath("../../Frameworks/include"),
.headerSearchPath("../../Frameworks"),
.unsafeFlags(["-std=c++20"]),
],
linkerSettings: [
.linkedFramework("Foundation"),
.linkedFramework("AppKit"),
.unsafeFlags([
"-L", cefFrameworksDir,
"-F", cefFrameworksDir,
"-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../..",
"-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../../Frameworks",
]),
]
),

// MARK: - Demo / smoke executable. Runs an end-to-end check that
// CEFEngine.start + makeBrowser produce a working Chrome runtime
// window. Not shipped; for local development only.
.executableTarget(
name: "CMUXCEFDemoApp",
dependencies: ["CMUXCEF"],
linkerSettings: [
.unsafeFlags([
"-Xlinker", "-rpath", "-Xlinker", cefFrameworksDir,
]),
]
),

// MARK: - Tests. Behavioral; no source-grep tests.
.testTarget(
name: "CMUXCEFTests",
dependencies: ["CMUXCEF"]
),
],
swiftLanguageModes: [.v6]
)
78 changes: 78 additions & 0 deletions CEF/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# CMUXCEF

`CEF/` contains cmux's Chromium Embedded Framework integration. The app code
imports only the Swift `CMUXCEF` facade; CEF's C++ API stays behind the ObjC++
bridge in `Sources/CMUXCEFBridge`.

## Runtime Model

cmux keeps the CEF SDK needed for source builds separate from the Chromium
runtime shipped to end users:

- Source builds use `CEF/vendor/fetch_cef.sh` to download the pinned SDK,
verify it against `vendor/cef.lock.json`, build `libcef_dll_wrapper.a`,
and populate `CEF/Frameworks/`.
- Installed apps do not bundle the large Chromium framework by default. When a
user selects CEF from the Debug menu for the first time, cmux downloads the
same pinned runtime, verifies the size and SHA1, and installs it in
Application Support.
- Subsequent launches reuse the installed runtime for the same app bundle ID.

This keeps the repository and app bundle small while still making the CEF
runtime opt-in and repeatable.

CEF runtime startup and helper execution require macOS 15.0 or later. On older
macOS versions, cmux falls back to WKWebView.

## Local Build

From the repo root:

```bash
./scripts/setup.sh
./scripts/reload.sh --tag cef-dev
```

`setup.sh` initializes submodules, builds GhosttyKit, and provisions the CEF
SDK for local builds. The CEF tarball is cached under
`~/Library/Caches/cmux-cef-vendor/`, so repeated setup runs are fast.

To refresh only the CEF SDK:

```bash
cd CEF
vendor/fetch_cef.sh
```

`CEF/CEF/`, `CEF/Frameworks/`, `.build/`, and `Package.resolved` are generated
artifacts and must not be committed.

## Package Layout

```text
CEF/
├── Package.swift
├── Sources/
│ ├── CMUXCEF/ Swift facade used by cmux.app
│ ├── CMUXCEFBridge/ ObjC++ bridge and public C bridge header
│ ├── CMUXCEFHelper/ Browser helper entrypoint
│ ├── CMUXCEFHelperRenderer/ Renderer helper entrypoint
│ └── CMUXCEFDemoApp/ Local demo executable
├── Tests/
└── vendor/
├── cef.lock.json Authoritative pinned CEF version
├── cef.lock.schema.json
└── fetch_cef.sh
```

## Rules

- Do not commit CEF binaries or extracted SDK artifacts.
- Treat `vendor/cef.lock.json` as the source of truth for the CEF version,
tarball, SHA1, size, and extracted directory name.
- Keep direct CEF C++ usage inside `CMUXCEFBridge`; Swift code should use the
`CMUXCEF` API.
- Helper apps are embedded into `cmux.app/Contents/Frameworks/` by
`Scripts/embed_cef_into_cmux.sh`.
- Runtime download UI and verification live in the main app so end users do
not need to run provisioning scripts.
Loading