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 162 commits into
cztomczak:cefpython147from
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 162 commits into
cztomczak:cefpython147from
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 18 commits April 29, 2026 22:38
- 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!

@linesight
Copy link
Copy Markdown
Contributor Author

#686 is fixed by this particular commit linesight@d8d32ea
i believe cefpython123 can backport this fix, but my focus is no longer cefpython123, not really tested at cefpython123

It is indeed continuation of prevoius work of cefpython123. After I opened this pull request, I seem cannot change pull request target branch (lack of permission?). Can you edit this pull request target branch?

@cztomczak cztomczak changed the base branch from master to cefpython147 May 9, 2026 09:22
@cztomczak
Copy link
Copy Markdown
Owner

Is there an upstream CEF issue for that GPU issue?

I've changed the base branch to cefpython147 - there are merge conflicts now.

…7-qt

# Conflicts:
#	.gitignore
#	README.md
#	api/Browser.md
#	docs/Build-instructions.md
#	examples/hello_world.py
#	examples/pywin32.py
#	examples/qt.py
#	examples/screenshot.py
#	examples/tkinter_.py
#	examples/tutorial.py
#	examples/wxpython.py
#	src/browser.pyx
#	src/cefpython.pyx
#	src/client_handler/cookie_access_filter.h
#	src/client_handler/dialog_handler.cpp
#	src/client_handler/dialog_handler.h
#	src/client_handler/request_handler.cpp
#	src/client_handler/request_handler.h
#	src/common/cefpython_public_api.h
#	src/cookie.pyx
#	src/extern/cef/cef_cookie.pxd
#	src/extern/cef/cef_types.pxd
#	src/frame.pyx
#	src/handlers/cookie_access_filter.pyx
#	src/handlers/render_handler.pyx
#	src/handlers/request_handler.pyx
#	src/handlers/v8context_handler.pyx
#	src/include/base/cef_bind.h
#	src/include/base/cef_build.h
#	src/include/base/cef_callback.h
#	src/include/base/cef_callback_helpers.h
#	src/include/base/cef_compiler_specific.h
#	src/include/base/cef_ref_counted.h
#	src/include/base/cef_scoped_refptr.h
#	src/include/base/cef_weak_ptr.h
#	src/include/base/internal/README-TRANSFER.txt
#	src/include/base/internal/cef_bind_internal.h
#	src/include/base/internal/cef_callback_internal.h
#	src/include/base/internal/cef_net_error_list.h
#	src/include/cef_api_hash.h
#	src/include/cef_api_version_test.h
#	src/include/cef_app.h
#	src/include/cef_application_mac.h
#	src/include/cef_base.h
#	src/include/cef_browser.h
#	src/include/cef_browser_process_handler.h
#	src/include/cef_command_handler.h
#	src/include/cef_command_ids.h
#	src/include/cef_command_line.h
#	src/include/cef_config.h
#	src/include/cef_dialog_handler.h
#	src/include/cef_display_handler.h
#	src/include/cef_download_handler.h
#	src/include/cef_download_item.h
#	src/include/cef_frame.h
#	src/include/cef_frame_handler.h
#	src/include/cef_life_span_handler.h
#	src/include/cef_media_router.h
#	src/include/cef_pack_resources.h
#	src/include/cef_pack_strings.h
#	src/include/cef_permission_handler.h
#	src/include/cef_preference.h
#	src/include/cef_render_handler.h
#	src/include/cef_request_context.h
#	src/include/cef_request_handler.h
#	src/include/cef_resource_bundle.h
#	src/include/cef_resource_bundle_handler.h
#	src/include/cef_sandbox_win.h
#	src/include/cef_test_server.h
#	src/include/cef_v8.h
#	src/include/cef_version.h
#	src/include/internal/cef_app_win.h
#	src/include/internal/cef_linux.h
#	src/include/internal/cef_string_wrappers.h
#	src/include/internal/cef_time.h
#	src/include/internal/cef_types.h
#	src/include/internal/cef_types_content_settings.h
#	src/include/internal/cef_types_linux.h
#	src/include/internal/cef_types_mac.h
#	src/include/internal/cef_types_win.h
#	src/include/internal/cef_types_wrappers.h
#	src/include/internal/cef_win.h
#	src/include/test/cef_translator_test.h
#	src/include/views/cef_browser_view.h
#	src/include/views/cef_browser_view_delegate.h
#	src/include/views/cef_display.h
#	src/include/views/cef_view.h
#	src/include/views/cef_window.h
#	src/include/views/cef_window_delegate.h
#	src/javascript_callback.pyx
#	src/process_message_utils.pyx
#	src/response.pyx
#	src/settings.pyx
#	src/subprocess/cefpython_app.cpp
#	src/version/cef_version_win.h
#	src/web_request.pyx
#	src/window_info.pyx
#	tools/automate.py
#	tools/build.py
#	tools/build_cpp_projects.py
#	tools/build_distrib.py
#	tools/common.py
#	tools/cython_setup.py
#	tools/installer/cefpython3.__init__.py
#	tools/installer/cefpython3.setup.py
#	tools/make_installer.py
#	tools/requirements.txt
@linesight
Copy link
Copy Markdown
Contributor Author

Is there an upstream CEF issue for that GPU issue?

I've changed the base branch to cefpython147 - there are merge conflicts now.

Merge conflict is now fixed. Regarding the GPU issue: I wasn't super knowledgeable about the root cause, I found some reference issue at chromiumembedded/cef#3765, cefsharp/CefSharp#4707 . I simply used an acceptable workaround to silence the error without letting CEF fall back to software rendering. Using gl seem to be more robust than d3d11 or d3d12 per my testing. If you have other preference of workaround implementation, I am happy to iterate ideas.

Squash-merge of 10 commits from cefpython147 to refresh PR cztomczak#691 with
the latest fixes and enhancements developed since the last sync.

Features:
* Add native Wayland support — explicit opt-in via WindowInfo;
  X11/XWayland remains the default (9c2ae77)
* Implement RenderHandler.GetScreenInfo with HiDPI support so
  high-DPI displays report accurate device scale factors (9969797)
* examples/pysdl2: support multiple popup windows with clean
  shutdown (bb24151)

Fixes:
* Sync render_handler with current CEF API (c6f2a56)
* Guard X11 error-handler install at compile time in
  cefpython.pyx (7a65e6b)
* examples/pysdl2: window resize, HiDPI rendering, mouse drag,
  Wayland DPI handling (2aff2c9)
* examples/pysdl2: scale mouse wheel by deviceScaleFactor for
  HiDPI (3e45abd)

Refactor / docs:
* Audit cef.Initialize defaults on Linux; drop dead switches and
  stale workarounds (db13d7d)
* Correct version-tagged comments to match actual upstream
  history (bd924c4)
* Drop defensive parenthetical from sandbox History comment in
  window_utils_linux.pyx (7ae0553)
@cztomczak
Copy link
Copy Markdown
Owner

Looks like adding compatibility.manifest fixes the GPU issue: chromiumembedded/cef#3765 (comment)

When running through python.exe you can create python.exe.manifest.

The issue with GL backend on Windows is that it is less tested, slower and has other bugs. From what I read it is a survival legacy path.

I don't know if there is an easy way to automate the fix.

It should be well documented and when someone creates final executeable using pyinstaller he should have an option to embed compatibility.manifest and not use a buggy/slower GL workaround.

If we want to make the GL backend the default fix, then it should be explicit and well documented, because it can have issues. Then we should add the use-angle=gl flag explicitilly in all .py examples with a comment that either this flag is required (but it is less tested, possibly buggy on some configurations and slower) or you need to add compatibility.manifest to python.exe or your app exe.

Do you have other ideas?

The ANGLE D3D11 GPU process crashed with STATUS_BREAKPOINT
(exit_code=-2147483645) on Windows because subprocess.exe was linked
with /MANIFEST:NO and had no supportedOS=Win10 GUID. Verified end-to-end:
with the manifest absent, --use-angle=d3d11 produces 3 GPU CHECK crashes
and falls back to software rendering; with CEF's compatibility.manifest
embedded via /MANIFEST:EMBED /MANIFESTINPUT, D3D11 init succeeds and
NVIDIA GPU is detected. The previous workaround (silently defaulting
use-angle=gl on Windows) is removed because it is no longer needed; ANGLE
GL is documented as a slower, less-tested fallback by upstream CEF.

The build references CEF's own tests/cefsimple/win/compatibility.manifest
rather than vendoring a copy, so any future change CEF makes to the
supportedOS chain is picked up automatically on the next CEF bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linesight
Copy link
Copy Markdown
Contributor Author

linesight commented May 11, 2026

You were right that compatibility.manifest is the proper fix — but the binary that needs it is cefpython's own subprocess.exe, not python.exe.
python.exe already ships with the supportedOS=Win10 GUID embedded. I dumped its RT_MANIFEST resource and it contains all five supportedOS GUIDs (Vista → Win10), same as cefsimple's compatibility.manifest. Adding python.exe.manifest next to it is a no-op because Windows uses the embedded manifest when both exist. https://learn.microsoft.com/en-us/archive/blogs/junfeng/internal-manifest-vs-external-manifest Tested — still 3 GPU CHECK crashes.
subprocess.exe is the one running the GPU process. It was being built with /MANIFEST:NO and shipped with no manifest at all. That's the actual gap.

What this PR now does:

  • src/subprocess/CMakeLists.txt links subprocess.exe with /MANIFEST:EMBED /MANIFESTINPUT:/tests/cefsimple/win/compatibility.manifest. We reference CEF's own file directly — no copy of the manifest in our depot, so we automatically pick up any future change CEF makes to the supportedOS chain.
  • The Windows use-angle=gl auto-default in cefpython.pyx is removed. D3D11 is the default now, exactly as upstream Chromium intends. The "GL is a less-tested survival path" concern goes away with it.

@cztomczak
Copy link
Copy Markdown
Owner

Nice work - good fix 🙂

Will using manifest from cefclient work when building cefpython with CEF prebuilt binaries and libraries?

@linesight
Copy link
Copy Markdown
Contributor Author

Nice work - good fix 🙂

Will using manifest from cefclient work when building cefpython with CEF prebuilt binaries and libraries?

yes, it works. I have confirmed on my local development machine and github action CI

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