Skip to content

Add CEF 147, Python 3.10–3.14, Linux/macOS ARM64/Windows support with modern build system#691

Open
linesight wants to merge 159 commits intocztomczak:masterfrom
linesight:cefpython147-qt
Open

Add CEF 147, Python 3.10–3.14, Linux/macOS ARM64/Windows support with modern build system#691
linesight wants to merge 159 commits intocztomczak:masterfrom
linesight:cefpython147-qt

Conversation

@linesight
Copy link
Copy Markdown
Contributor

Overview

This PR modernizes cefpython from CEF 66 (2018) to CEF 147, adds full support
for Linux and macOS Apple Silicon, and replaces the legacy build toolchain with
a pip-installable wheel workflow. It also incorporates all work from the
cefpython123 branch (PR #679) which was never merged to master.

What's new

CEF & Python versions

  • CEF updated to 147.0.10 (Chromium 147.0.7727.118)
  • Supports Python 3.10–3.14 (64-bit); Python 2 and pre-3.10 dropped
  • Cython updated to 3.2+

Platform support

Platform Architecture Status
Windows 10+ x64 Full support
Linux (Ubuntu 20.04+ / Debian 11+) x64 Full support (GTK3/X11)
macOS 10.15+ ARM64 (Apple Silicon) Full support

Build system

  • Replaced legacy setup.py with scikit-build-core + CMake
  • build_distrib.py produces installable wheels per platform/Python version
  • CI publishes wheel artifacts for all 15 combinations (3 platforms × 5 Python versions)

CI (GitHub Actions)

  • Separate workflows for Windows, Linux, and macOS
  • Each workflow builds and runs the unit test suite across all 5 Python versions
  • CEF is downloaded once in a dedicated job and cached; all matrix jobs restore
    from cache rather than downloading independently

Linux

  • Native-windowed embedding via GTK3/X11
  • Wayland sessions: automatic XCB backend forcing with HiDPI scaling support
  • Compatible with GCC 13 / Ubuntu 24.04

macOS

  • Apple Silicon (ARM64) only — Intel Mac dropped
  • Handles Mach port rendezvous requirements for CEF 130+ subprocess isolation
  • Ad-hoc code signing for local use; wheel binaries are codesigned

Qt

  • PyQt6 and PySide6 embedding and context menu support on Linux

API changes

  • Removed APIs dropped in CEF 123+: OnPluginCrashed; SendFocusEvent kept
    as no-op stub for compatibility
  • Cookie API updated: CanSendCookie / CanSaveCookie handler signatures revised
  • All examples updated and verified on current Chromium behavior

Open issues addressed

Definitely fixed

Issue Title
#393 [gtk3.py] Blank window / browser embedding fails due to invalid X11 handle
#467 GTK 2 dependency will be removed in CEF v70+
#528 Linux: Discontinue x86 32-bit build support
#585 Use python_requires in setup.py
#609 New v66.1 release only for Windows?
#641 Problem with cefpython on Python 3.8/3.9/3.10 on Linux
#646 No support for Python 3.10
#650 Doesn't support the latest Python version
#652 Chromium 100+ support
#673 Support for Python 3.12
#676 CanSendCookie and CanSaveCookie not called by handler
#683 Error in CookieVisitor_Visit (IO thread assertion)
#685 "Please customize CefSettings.root_cache_path" warning
#686 GPU process crashes 3 times when running unit tests
#530 cef.DpiAware.EnableHighDpiSupport() doesn't work well

Likely fixed

Issue Title
#452 Linux: Crash in Qt and wxPython examples
#520 API changes due to implementing support for NetworkService
#523 Support for ozone builds / Wayland and X11 backends
#538 Mac: CEF 76 requires multiple helper app bundles
#645 Error when running qt.py example on Windows

linesight and others added 30 commits January 27, 2024 20:59
Fixes: cztomczak#546

Had to include harfbuzz manually as newer Pango change this.
See:
    eiskaltdcpp/eiskaltdcpp#413
    https://gitlab.gnome.org/GNOME/pango/-/issues/387

Also had to add `-Wno-deprecated-declarations` to get this to compile because
of the following errors that didn't seem to be coming from this code directly:

    warning: ‘GTimeVal’ is deprecated: Use 'GDateTime' instead
    warning: ‘GTypeDebugFlags’ is deprecated
…k#484).

These callbacks were never called previously.

Rename --no-run-examples flag to --unittests in build scripts.
…nd CanSaveCookie; restore network_cookies.py and make slight tweak to use CanSendCookie and CanSaveCookie
linesight and others added 29 commits April 29, 2026 15:16
…ailures

Chrome 130+ MachPortRendezvousServerMac validates the signature of each
connecting subprocess via process_requirement.cc. Without a valid code
signature the server rejects the connection, bootstrap_look_up returns
BOOTSTRAP_UNKNOWN_SERVICE (1102), and shared_memory_switch.cc kills the
subprocess ("No rendezvous client, terminating process").

The browser process also fails its own process_requirement check (-67030)
when it has loaded the unsigned cefpython_py*.so extension, because macOS
marks the process code signature as invalid when an unsigned library is
dlopen'd into a signed process.

Ad-hoc signing both the subprocess binary and the Python extension gives
them a valid (self-signed) code identity that process_requirement.cc
accepts, restoring normal MachPortRendezvousClient operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ous lookup

CEF 130+ builds the MachPortRendezvousClient service name as:
  CFBundleIdentifier + ".MachPortRendezvousServer." + parent_pid

The browser process injects CFBundleIdentifier = "org.cefpython" via
MacInitialize() in util_mac.mm before CefInitialize(), so it registers:
  "org.cefpython.MachPortRendezvousServer.<pid>"

The subprocess binary is a flat binary (not in an app bundle), so
CFBundleGetMainBundle() returns no CFBundleIdentifier and the client
looks up ".MachPortRendezvousServer.<parent_pid>" — a name starting with
"." that bootstrap_look_up can never find (BOOTSTRAP_UNKNOWN_SERVICE 1102).
This caused every spawned subprocess to terminate immediately with
"No rendezvous client, terminating process (parent died?)".

Fix: add main_mac.mm (compiled only on Apple) which injects the same
"org.cefpython" bundle identifier into the subprocess's main bundle dict
before CefExecuteProcess runs, matching what the parent registered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
extern "C" linkage specification must be at namespace (file) scope in
C++, not inside a function body.  Move the SubprocessMacInit() forward
declaration out of main() into file scope, guarded by OS_MAC.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main_mac.mm uses CFBundleGetMainBundle, CFBundleGetIdentifier, etc.
Add -framework CoreFoundation so the linker resolves those symbols.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the broken SubprocessMacInit() runtime injection approach with
a reliable compile-time solution: embed Info.plist in the subprocess
Mach-O binary using the -sectcreate __TEXT __info_plist linker flag.

CoreFoundation reads the __TEXT,__info_plist section automatically, so
CFBundleGetMainBundle() returns CFBundleIdentifier = "org.cefpython"
without any runtime code. This matches the service name the browser
process registers with MachPortRendezvousServer, fixing bootstrap_look_up.

The previous approach (CFDictionarySetValue on the info dict) was broken
because CFBundleGetInfoDictionary() returns an immutable CFDictionaryRef
for flat binaries; the set operation silently did nothing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

Without an ad-hoc signature on the Python binary (the browser process),
process_requirement.cc fails with errSecCSUnsigned (-67030). Chrome
responds by launching subprocesses with a restricted bootstrap namespace
that cannot see Mach services registered by the parent. This causes
bootstrap_look_up for MachPortRendezvousServer to return 1102 regardless
of whether the service name is correct.

Signing the Python binary (in addition to subprocess and .so files)
prevents the namespace restriction and allows subprocesses to find the
registered service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--in-process-renderer was removed from Chrome 130+.  The flag no
longer appears in content_switches.cc so passing it was a no-op,
leaving the renderer as a full subprocess.  That subprocess calls
MachPortRendezvousClientMac::AcquirePorts() -> bootstrap_look_up,
which returns 1102 (BOOTSTRAP_UNKNOWN_SERVICE) on unsigned CI runners
because Chrome gives them a restricted bootstrap namespace.  Without
a working renderer, g_js_code_completed is never set and the tests
fail.

Switch to --single-process, which runs the renderer in the browser
process and eliminates the renderer's bootstrap_look_up call entirely.

Also drop the Python-interpreter codesigning step: ad-hoc signing
never fixed the -67030 (errSecCSUnsigned) error from
process_requirement.cc, and may have broken any valid Developer ID
signature that python.org Python already carried.  Our own compiled
binaries (subprocess, cefpython_py*.so) still need ad-hoc signing
since they ship unsigned from the build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--single-process runs the renderer's V8 in the browser process, which
requires a large contiguous virtual memory reservation for JIT code
(CodeRange). On older macOS-14 CI runner images this reservation fails:

  V8 process OOM (Failed to reserve virtual memory for CodeRange)

--jitless disables all V8 JIT compilers (Sparkplug, Maglev, Turbofan),
eliminating the CodeRange requirement entirely. Unit test JS is simple
enough that interpreted execution is sufficient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restore LF line endings (linux branch base) and keep only the
functional macOS additions:
- build.py: MAC/LINUX flags, CEF framework detection, _codesign_macos(),
  symlinks=True in copytree
- main_test.py / osr_test.py: if MAC: switch block (no-sandbox,
  single-process, jitless, NetworkServiceInProcess2); also update
  Linux block: no-zygote, NetworkServiceInProcess2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete cef_version_mac.h (CEF 66 x86_64 header, never updated for 146).
CMakeLists.txt, common.py, and build_distrib.py all now unconditionally
use cef_version_macarm64.h and the cef*_macarm64 CEF directory on macOS.
The -mmacosx-version-min is hardcoded to 11.0 (ARM64 minimum).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- screenshot.py: add macOS switches matching osr_test.py; remove dead
  switches (enable-begin-frame-scheduling, disable-surfaces not present
  anywhere in CEF 146 source) from the macOS path
- osr_test.py: drop --no-sandbox (cefpython.pyx already sets no_sandbox=1
  at the C level) and --in-process-gpu (macos-14-arm64 CI has real Apple
  Silicon GPU; subprocess is ad-hoc signed in the test setup step);
  update comments to cite actual mechanism (restricted bootstrap namespace
  for ad-hoc-only signed processes) verified against CEF source
- ci-macos.yml wheel job: ad-hoc codesign subprocess and .so before
  packaging so installed wheels don't ship unsigned binaries; unsigned
  subprocess binary is the root cause of GPU_DEAD_ON_ARRIVAL (exit
  code 1003) on macOS 14+ Gatekeeper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ZipFile.write() does not preserve Unix file mode bits, so the subprocess
binary inside the wheel lost its +x permission. pip extracts it as a
non-executable file, and CEF cannot launch the renderer process, leaving
browser windows blank. Use ZipInfo.from_file() which stores the real
file mode in the zip entry's external_attr field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…LTO on Linux

GCC's alias analysis loses track of object sizes when small ref-counted
classes are inlined across translation units during LTO, triggering
spurious -Wstringop-overflow warnings on the atomic ref_count_ write.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cp -r does not preserve permissions, so subprocess lost its +x bit
before build_distrib zipped it into the wheel. The ZipInfo.from_file()
fix in a68d6b1 preserves whatever mode is on disk — but by the time
the wheel job copies files into cefpython3/, the permission was already
gone. Add chmod +x to match what the test job already does.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Merge compile/test/wheel into a single matrix job per Python version so that
each version's pipeline is fully independent — a 3.11 failure no longer delays
the 3.10 wheel. Extract CEF download into a dedicated job to avoid 5 concurrent
downloads of the same 600 MB archive from Spotify's CDN.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GCC's lto-wrapper re-compiles combined IR during linking and doesn't
propagate -Wno-* flags from the compile phase, so the false-positive
-Wstringop-overflow warnings resurfaced at link time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
qt.py: override QT_QPA_PLATFORM=xcb unconditionally on Linux for all Qt
bindings (PyQt5/PyQt6/PySide6).  Wayland desktops such as KDE Plasma on
Kubuntu often pre-set QT_QPA_PLATFORM=wayland in the session environment;
setdefault was insufficient to override it, causing CEF to open a detached
top-level window instead of embedding into the Qt widget.

window_info.pyx: add a warnings.warn() in SetAsChild when
parentWindowHandle is 0 and WAYLAND_DISPLAY is set.  This catches the
same misconfiguration for any toolkit (Qt, GTK, SDL2) and points the
developer to the correct per-toolkit env var fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Linux

qt.py: force QT_QPA_PLATFORM=xcb on Linux for all Qt bindings before
QApplication is created.  On Wayland desktops (e.g. KDE Plasma on
Kubuntu) the session environment often pre-sets QT_QPA_PLATFORM=wayland;
setdefault was insufficient to override it, causing CEF to receive a
non-X11 window handle and open a detached top-level window instead of
embedding into the Qt widget.

qt.py: add CefWidget._phys() and apply it to the initial embed rect and
all SetBounds calls on Linux for PyQt6/PySide6.  Qt6 enables
AA_EnableHighDpiScaling by default so width()/height() return logical
pixels; CEF expects physical pixels.  Without this fix, a display at
125-200% DPI causes the browser to fill only the top-left fraction of
the window and resizing does not correct it.  PyQt5 is unaffected as it
uses the hidden_window/XReparentWindow path where X11 geometry drives
browser sizing independently of SetBounds.

window_info.pyx: warn when SetAsChild receives parentWindowHandle=0 on
Linux in a Wayland session (WAYLAND_DISPLAY set).  Fires at the caller
site via stacklevel=2 and lists the per-toolkit env var fix for Qt,
GTK, and SDL2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches the pattern already used in ci-linux.yml and ci-windows.yml:
a dedicated download-cef job saves the cache, and each compile matrix
job restores from it rather than downloading independently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches the pattern already used in ci-linux.yml and ci-windows.yml:
a dedicated download-cef job saves the cache, and each compile matrix
job restores from it rather than downloading independently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pi.h

The #else branch with hardcoded version-specific include paths was only
reachable by the old root-level build.py which no longer exists; all
builds now go through CMake which always defines CEFPYTHON_API_H_FILE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the third-party `packaging` dependency from all example scripts.
The upstream master branch has no such requirement, and the version
checks only need simple numeric comparisons that a split+tuple handles cleanly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…itions

CefFrame::GetBrowser() can return a null CefRefPtr while a frame is being
created or destroyed during rapid navigation (e.g. heavy pages with many
iframes).  The naked .get().GetIdentifier() chain in GetPyFrame() was a
latent SIGSEGV; all handler entry points that call GetPyFrame() or dereference
the frame's browser pointer now return their safe defaults when the browser
pointer is NULL.

Affected files:
- src/frame.pyx: store GetBrowser() result, check .get() before chaining
- src/handlers/load_handler.pyx: OnLoadStart / OnLoadEnd / OnLoadError
- src/handlers/display_handler.pyx: OnAddressChange
- src/handlers/v8context_handler.pyx: OnContextCreated
- src/handlers/request_handler.pyx: OnBeforeBrowse, OnBeforeResourceLoad,
  GetResourceHandler, OnResourceRedirect, GetAuthCredentials
- src/handlers/cookie_access_filter.pyx: CanSendCookie, CanSaveCookie
- src/handlers/lifespan_handler.pyx: OnBeforePopup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t variance

Move onclick from <h1> to <body> and click at viewport center (400,300)
instead of hardcoded h1 coordinates (200,43). selectText() now selects
the h1 via querySelector rather than ev.target, so any click on the page
reliably triggers OnTextSelectionChanged regardless of font metrics or
rendering differences in CI environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cztomczak
Copy link
Copy Markdown
Owner

Hi, thank you for the PR.

If this is continuation of changes started in cepfython123 branch - then I have created branch cefpython147, please send the PR against that branch. It will make reviewing easier. Changes in cefpython123 were already reviewed and were good.

Regarding #686 GPU process crashes 3 times when running unit tests - was this an issue with upstream CEF 123 or was this a problem with cefpython?

"CI publishes wheel artifacts for all 15 combinations (3 platforms × 5 Python versions)" - sounds awesome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants