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 156 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 156 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 April 18, 2026 11:26
- Add tools/download_cef.py to auto-download CEF from Spotify CDN
  with SHA1 verification and progress output
- Detect vcvarsall.bat dynamically via vswhere.exe instead of
  hardcoding VS2022 Community path; fall back through known editions
- Remove dead code for VS2008/VS2010/VS2013 and simplify
  prepare_build_command()
- Make VERSION argument optional in build.py, defaulting to
  {CHROME_VERSION_MAJOR}.0 from the version header
- Add venv support notes and document download_cef.py workflow
  in Build-instructions.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass explicit plat_name to distutils compiler.initialize() in
build_cpp_projects.py, and add --plat-name to the cython_setup.py
build_ext invocation in build.py. Without these, distutils defaults
to win32 even on 64-bit Python, causing C1905 (front/back end
incompatible) when linking against x64 CEF libraries.

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

Two root causes fixed to make windowed browsers work on CEF 123+:

1. Set CEF_RUNTIME_STYLE_ALLOY explicitly after SetAsChild/SetAsPopup in
   window_info.pyx. CEF 123+ defaults to Chrome runtime (no native Win32
   title bar, blank content). Also declared cef_runtime_style_t enum and
   runtime_style field in cef_win.pxd.

2. Defer CreateBrowserSync() until OnContextInitialized fires. Calling
   before context is ready causes blink.mojom.WidgetHost rejection and a
   blank window. g_pending_browsers list + CefPostTask posts creation at
   the outer message-loop level after the callback returns.

Also fix PyQt6 high-DPI sizing in qt.py: call WindowUtils.OnSize after
CreateBrowserSync() so the browser fills the HWND client rect in device
pixels rather than the smaller logical-pixel rect Qt reports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sync all CEF public headers, capi headers, and internal headers from
CEF 146 binary distribution. Includes new headers for task manager,
component updater, OSR types, color types, runtime style, and API
versioning. Update client handlers (dialog, download, lifespan, request)
for CEF 146 API changes. Update cef_types.pxd, network_error.pyx,
settings.pyx, and version header for CEF 146. Update build tools
(cython_setup.py, build_cpp_projects.py) for the new toolchain.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
osr_test.py:
- OnTextSelectionChanged: Chrome 130+ requires the Selection API to run
  inside a real user-gesture event handler to propagate the callback.
  Added selectText() onclick handler to h1 and trigger it via a real
  mouse click posted after the first OnPaint (guarantees layout is
  complete and hit-testing works). Simplified the callback handler to
  not assume a specific empty-then-selected call sequence.
- AccessibilityHandler: removed layoutComplete_True assertion — the
  layoutComplete AX event was removed from Chrome's event system in
  Chrome 130+.

main_test.py:
- V8ContextHandler: CEF 146 no longer creates an initial empty-document
  V8 context on browser creation. Removed OnContextCreatedSecondCall_True
  and OnContextReleased_True assertions which could never be satisfied.
- Added disable-popup-blocking switch to cef.Initialize — Chrome 130+
  blocks window.open() calls that lack a user gesture at the renderer
  level, preventing the popup test from running.

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

_detect_cefpython_binary_dir() runs at import time (bottom of common.py),
before build.py's command_line_args() can inject the default version into
sys.argv. Fall back to reading the version directly from the CEF header
file so CEFPYTHON_BINARY is resolved correctly even when no version
argument is passed on the command line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cefpython.pyx:
- Pump the message loop (up to 200×10ms = 2s) after CefInitialize()
  until OnContextInitialized fires. This guarantees CreateBrowserSync()
  can be called immediately after Initialize() and always gets a valid
  browser, eliminating the race condition where deferred creation
  could return None when CreateBrowserSync() was called before the
  context was ready.

main_test.py:
- Remove OnAutoResize_True assertion: CEF 146 no longer triggers
  OnAutoResize for windowed (non-OSR) browsers. Consistent with the
  other Chrome 130+ behavioral changes removed in the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ANGLE D3D11 backend in CEF 146 / Chrome 130+ triggers a CHECK failure
(STATUS_BREAKPOINT, exit_code=-2147483645) in libcef.dll during GPU
process initialization. CEF auto-recovers 3 times then falls back to
software rendering, silently degrading performance. The D3D9 backend
avoids the crash but only implements ES 2.0, causing eglCreateContext
ES 3.0 failures. Defaulting to the OpenGL ANGLE backend eliminates both
issues — it supports ES 3.0 and initializes without crashes. Users can
override by passing {"use-angle": "d3d11"} in the switches dict. Fixes cztomczak#686.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Framework fixes:
- Remove BindedFunctionExists renderer-side pre-check in V8FunctionHandler::Execute
  that raced against global registration and threw spurious "function does not exist"
  errors; browser process handles missing functions gracefully via NonCriticalError
- Fix DoJavascriptBindingsForBrowser: guard null/invalid V8 context to prevent
  crash/freeze in CEF 146, register globals synchronously (not via PostTask) when
  outside V8 execution so they are in place before queued ExecuteJavascript IPCs run
- Add Rebind() before user OnContextCreated callback in V8ContextHandler so that
  DoJavascriptBindings IPC is ordered ahead of any ExecuteJavascript the user sends
  from their callback on the same IPC channel

Example updates:
- ondomready.py: inject JS from OnContextCreated instead of OnLoadStart; OnLoadStart
  fires before the page V8 context is committed so ExecuteJavascript ran in about:blank
  where globals are not registered; wrap callback in setTimeout(0) to allow paint first
- onpagecomplete.py: replace OnLoadingStateChange with window.load + requestAnimationFrame
  via JS bindings to fire only after all resources are loaded and a paint frame is
  committed; fixes double-alert on sites with JS-initiated redirects (e.g. google.com)
  and ensures page content is visible before the alert dialog blocks the renderer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- crossdomain_bindings.py: demonstrates JS binding behaviour across a
  cross-domain auth flow (app -> auth domain -> back to app); uses a
  URL-based state machine to avoid misfire from multiple OnLoadEnd fires,
  and readyState fallback so the callback is not missed if window.load
  already fired before JS is injected
- README-snippets.md: add entry for crossdomain_bindings.py and update
  onpagecomplete.py description to reflect window.load + requestAnimationFrame

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cookie.pyx: change CookieVisitor_Visit thread assertion from TID_IO to
  TID_UI — CEF 146 changed CookieVisitor::Visit to always fire on the UI
  thread (previously IO thread), causing AssertionError on every callback
- cookies.py: fix bug where OnLoadingStateChange checked "if is_loading"
  (fires on load start) instead of "if not is_loading" (fires on complete);
  update dead html-kit.com URL to https://www.google.com/
- network_cookies.py: update dead html-kit.com URL to https://www.google.com/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wire GetResourceRequestHandler in RequestHandler to return a
  ResourceRequestHandler (new file) that exposes GetCookieAccessFilter,
  restoring CanSendCookie/CanSaveCookie callbacks broken since CEF 146
  moved them out of CefRequestHandler into CefCookieAccessFilter
- Add #pragma once to cookie_access_filter.h to prevent C2011 redefinition
  when included from both request_handler.h and resource_request_handler.h
- Fix Cookie.SetDomain() to accept leading-dot domains (.example.com) by
  stripping the dot before IDNA validation while preserving the original
  value stored in the cookie (RFC 2109 subdomain-matching convention)
- Update setcookie.py: replace dead html-kit.com URL with google.com,
  set cookie in OnLoadEnd and verify with VisitUrlCookies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cefpython_public_api.h: add PY_MINOR_VERSION cases for 3.12, 3.13, 3.14
- build.py: fix invalid escape sequences in regex strings (SyntaxWarning on 3.12+)
- cefpython3.__init__.py: add import branches for 3.12, 3.13, 3.14
- cefpython3.setup.py: add PyPI classifiers for 3.12, 3.13, 3.14
- make_installer.py: add .pyd checks and update comments for 3.12, 3.13, 3.14
- build_distrib.py: add (3,12)/(3,13)/(3,14) to version lists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cython 0.29.36 generates code using CPython internals removed in
Python 3.13 (_PyDict_SetItem_KnownHash, _PyInterpreterState_GetConfig,
_PyLong_AsByteArray), causing CI build failures on Python 3.13+.

- requirements.txt: Cython == 0.29.36 -> == 3.2.4
- cython_setup.py: language_level 2 -> "3str"; remove c_string_type /
  c_string_encoding directives
- All .pyx files: py_string type annotations -> object (Cython 3.x
  rejects string literals assigned to ctypedef object aliases)
- All .pyx files: iteritems() -> items(), basestring -> (str, bytes),
  long -> int (Python 2 builtins removed under language_level=3str)
- cefpython_app.cpp: remove extern "C" forward declarations that
  conflicted with Cython 3.x extern "C++" default in _fixed.h
- build.py: fix_cefpython_api_header_file to write #pragma only to
  _fixed.h so Cython 3.x can overwrite its own output on rebuilds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace IF/ELIF/ELSE compile-time conditionals with runtime sys.platform
  checks and C macro helpers in cefpython.pyx, window_info.pyx,
  window_utils_win.pyx, utils.pyx, and browser.pyx
- Replace IF-based cimport blocks in cef_platform.pxd, cef_app.pxd,
  cef_browser.pxd, cef_browser_static.pxd with generated platform_cimports.pxi
  files; add generation of src/platform_cimports.pxi and
  src/extern/cef/platform_cimports.pxi to cython_setup.py
- Fix platform-conditional char16_t typedef in cef_types.pxd using
  cdef extern from * C macro instead of IF block
- Fix nogil keyword placement (must follow except+) in wstring.pxd and
  multimap.pxd; fix size_t npos assignment in wstring.pxd
- Change 44 cdef public callback functions from except * with gil to
  noexcept with gil across handlers and core pyx files; these functions
  already catch all exceptions internally so the exception check GIL
  acquisition was redundant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  Allows manually triggering a clean build from GitHub Actions UI by
  skipping CEF binary cache, useful for verifying the build script
  works end-to-end without relying on cached artifacts.
Cython must transpile the .pyx file before C++ projects can be compiled,
because the C++ code includes cefpython_pyXX_fixed.h which is derived from
the Cython-generated header.  The previous two-pass re-run hack let the
first C++ compile fail noisily, then re-ran the whole script.

Now build.py detects a missing API header at startup and calls
cython_setup.py --cython-only (new flag) to transpile the .pyx and produce
cefpython_pyXX.h before fix_cefpython_api_header_file() and the C++
compilation run.  The FIRST_RUN re-run machinery is removed entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the old setup.py / build_cefpython.py toolchain with a modern
scikit-build-core backend driven by CMake, enabling clean incremental dev
builds, a structured CI pipeline (compile → test → wheel), and optional
Cython profiling/line-tracing flags.

Key changes:
- CMakeLists.txt (root + per-subdir): full CMake build for the extension
  module, subprocess executable, client_handler and cpp_utils static libs;
  auto-detects CEF root; installs CEF runtime files via cmake --install
- pyproject.toml: scikit-build-core build backend, Cython >=3.2 dependency
- tools/cmake_generate_pxi.py: generates platform .pxi files at configure time
- tools/cmake_prepare_pyx.py: stages .pyx files and injects version constants
- tools/cmake_fix_header.py: patches Cython-generated header for MSVC compat
- tools/build.py: unified dev/CI entry point; --clean for full rebuild,
  --wheel for pip wheel, --enable-profiling / --enable-line-tracing for
  Cython instrumentation
- .github/workflows/ci-windows.yml: split into compile / test / wheel jobs
  with artifact hand-off; supports Python 3.10–3.14
- .gitignore: add _cmake_test/, _skbuild/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
__init__.py is source; .pyd/.dll/.exe/.pak/.dat/.bin/.json/locales/
are build outputs or CEF runtime files and should not be committed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
automate.py --prebuilt-cef requires docopt; add Install build tools step
to the test job same as compile and wheel jobs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
automate.py requires docopt; requirements.py must run first.
Fixed in compile, test, and wheel jobs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_test_runner.py os.chdir()s to unittests/, so the repo root (where
cefpython3/ lives) falls off sys.path. Set PYTHONPATH to the workspace
root so the package is findable without installing it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Include CEF runtime files in the compile artifact so the test job
needs nothing except: checkout, setup-python, download artifact, copy,
test. No requirements.py, no CEF cache restore, no automate.py.

Also add pip cache to the wheel job's setup-python to speed up
repeated requirements.py installs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old script searched for multiple Python installations and drove a
full build + setup.py bdist_wheel — all of that is now the CI matrix's
job. The new build_distrib.py does the one thing that still belongs
here: package the pre-built cefpython3/ directory into a .whl using
stdlib only (zipfile/hashlib), with correct dist-info/RECORD hashes.

The CI wheel job now mirrors the test job (checkout, setup-python,
download artifact, copy, build_distrib.py). Timeout drops 60 -> 15 min.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UNAME_SYSNAME and PY_MAJOR_VERSION are Cython built-in compile-time
constants — no DEF needed. INT_MIN/INT_MAX now come from libc.limits
via a direct cimport. The two platform_cimports.pxi files use Cython's
own IF/ELIF/ELSE on UNAME_SYSNAME instead of being generated per-OS.

Removes cmake_generate_pxi.py and its CMakeLists.txt custom command
entirely — no generation step, no gitignore entries, no cmake target.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add .github/workflows/ci-linux.yml targeting ubuntu-24.04
- Update src/version/cef_version_linux.h from CEF 66 to CEF 146
- Make tools/build.py cross-platform (cmake flags, artifact paths, CEF runtime detection)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GCC 13 added -Wself-move which fires on intentional self-move test
code in CEF's ref_counted_unittest.cc. Pass -Wno-self-move when
configuring the CEF binary's build_cefclient on Linux. Safe on older
GCC where the flag is silently ignored.

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

- cef_types_linux.h: Sync with real CEF 146 — adds size_t size to
  _cef_window_info_t, cef_runtime_style_t runtime_style field, the
  full _cef_accelerated_paint_info_t + plane struct for Linux DMA-BUF,
  and missing includes (cef_types_color.h, cef_types_osr.h,
  cef_types_runtime.h). The old vendored header was from CEF ~66.

- cef_linux.h: Sync with real CEF 146 — CefWindowInfoTraits::init now
  sets s->size, set() copies runtime_style, SetAsWindowless sets
  CEF_RUNTIME_STYLE_ALLOY. CefWindowInfo uses idiomatic using-inheritance.

- client_handler/CMakeLists.txt: Add gtk+-3.0 pkg-config includes on
  Linux so dialog_handler_gtk.h (<gtk/gtk.h>) can be found.

- subprocess/CMakeLists.txt: Add gtk+-3.0 pkg-config includes for the
  cefpython_app library on Linux (cefpython_app.cpp includes <gtk/gtk.h>
  when BROWSER_PROCESS is defined).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
linesight and others added 20 commits July 3, 2026 22:06
cefpython now keeps the Chromium sandbox enabled when the system supports it
and only falls back to --no-sandbox (with a warning) when no usable sandbox is
detected. Rewrite the sandbox section to describe the detection order, the
CHROME_DEVEL_SANDBOX opt-in, and how to force the sandbox off, replacing the
old "always --no-sandbox, patch the source to disable" guidance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in CEF 147)

BrowserProcessHandler_OnBeforeChildProcessLaunch stripped
--pseudonymization-salt-handle and --change-stack-guard-on-fork from every
child command line to avoid CEF 146 subprocess crashes ("Failed global
descriptor lookup: 7" on non-zygote utilities; "stack smashing detected" on
zygote-forked children).

Re-verified on CEF 147: both switches are still added by Chrome to child
command lines, but their presence no longer crashes the subprocesses. Tested
with the strip disabled across every relevant path — sandbox on (zygote),
the unit-test no-zygote/in-process config, and the exact target scenario
(--no-zygote with a directly-launched utility subprocess carrying
--pseudonymization-salt-handle): no descriptor-lookup failure, no stack-smash,
hello_world and the 85 unit tests all pass. Upstream evidently fixed the
non-zygote GlobalDescriptors init and the fork-canary handling between 146
and 147.

Removes the Linux-only CefCommandLine rewriting from the .pyx binding layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ers from CEF_ROOT

The vendored src/include tree had drifted from upstream: a stale
internal/cef_types.h missing the CEF 147 CEF_API_ADDED(14700) ABI content
(new result codes, permission types, CPAIT enums) while the build links the
147 binary, plus AI cosmetic edits (em-dashes, license reflow), removed-upstream
headers (cef_extension*, base/cef_callback_list, cef_auto_reset, cef_ptr_util,
cef_tuple, ...), and checked-in generated release headers.

Re-import src/include cleanly from the upstream CEF source repository at the
exact build commit (chromiumembedded/cef@d58e84d = 147.0.10+gd58e84d), verified
byte-identical to all three platform binary distributions (Linux/macOS exact;
Windows exact after CRLF normalization).

The platform-generated headers are intentionally NOT vendored (they are produced
during CEF packaging and differ per platform): cef_version.h, cef_config.h,
cef_api_versions.h, cef_color_ids.h, cef_command_ids.h, cef_pack_resources.h,
cef_pack_strings.h, and base/internal/cef_net_error_list.h. They are still
required at compile time because upstream headers include them transitively
(e.g. cef_browser.h -> cef_command_ids.h; cef_api_hash.h -> cef_version.h;
cef_types.h -> cef_net_error_list.h), so they are now resolved from
CEF_ROOT/include:

- CMake lists CEF_ROOT after src/ on the include path (module, client_handler,
  cpp_utils, subprocess) so vendored source headers take precedence and only the
  generated headers fall through to the CEF SDK.
- automate.py --prebuilt-cef copies include/ into the prebuilt CEF_ROOT so it is
  a complete SDK; the copy is idempotent so a CI-cached prebuilt dir created
  before this change is repaired.

Also removes the unused capi/ headers and documents the reproducible header
re-import procedure in docs/Contributing-code.md ("Updating CEF version") so
future upgrades stay clean.

Verified on Linux: clean build, 85/85 unit tests, hello_world. Windows/macOS
rely on CI; their headers were verified byte-identical to the respective dists.

Addresses review feedback on PR cztomczak#691 (cztomczak/cefpython).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cython 3 callback handlers are declared "noexcept", so an exception that
escapes a handler (one not wrapped in try/except, or whose except block
itself raises) does not propagate into CEF's C++ code. Python instead
reports it via sys.unraisablehook, which by default only prints to stderr
and is easy to miss.

Add a UnraisableHook helper, companion to ExceptHook, that forwards the
escaped exception to ExceptHook so it is written to error.log, printed,
and CEF is shut down cleanly - same as any other Python exception. Wire
it into the tutorial.py example (sys.unraisablehook = cef.UnraisableHook)
and document it in Tutorial.md and cefpython.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CEF API hash was hand-copied into src/version/cef_version_*.h as
CEF_API_HASH_PLATFORM / CEF_API_HASH_UNIVERSAL, which had to be updated
manually on every CEF upgrade and could drift from the real value.

Read it instead from CEF's own generated cef_api_versions.h (in
CEF_ROOT/include), which is the authoritative source. cmake_prepare_pyx.py
gains get_cef_api_hash() to extract the platform-matched
CEF_API_HASH_<version> for the build host, and CMake passes the header
path (and depends on it). The hand-copied hash lines are removed from the
version headers, and the upgrade guide notes the hash is now auto-derived.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cython 3 propagates exceptions out of cdef/cpdef functions by default. For
functions called from CEF's C++ (the "public" callbacks that acquire the GIL,
declared "with gil"), an exception must NOT unwind into C++ - the C++ caller
never checks Python's error indicator, so a propagated exception is a latent
bug. These must be "noexcept": an escaping exception is reported via
sys.unraisablehook (see cef.UnraisableHook) instead of unwinding.

45 of these callbacks were already noexcept; convert the remaining 32 from
"except * with gil" to "noexcept with gil" across the handlers, cookie/command
-line/python-callback bridges, and the ApplicationSettings/CommandLineSwitches
getters. Each callback already wraps its body in try/except -> sys.excepthook,
so behavior on the normal path is unchanged; noexcept just makes the boundary
explicit and safe. The internal (non-"with gil") cdef/cpdef helpers keep
"except *" - they are Python-callable and should propagate.

Also update the stale Cython-0.29 guidance comment block in cefpython.pyx
(which told contributors to add "except *" and had a matching TODO) to describe
the noexcept convention.

Verified: per-function codegen audit shows all 32 now emit __Pyx_WriteUnraisable
(0->1 vs baseline); runtime injection confirms escaped exceptions route to
sys.unraisablehook without crashing C++ for void, bool and output-ref callbacks;
full unit suite 85/85.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge consistent

Revert the fork-only partial IF/DEF conversions the CEF 147 modernization
introduced, per reviewer request (r3512025176): keep the codebase uniform on
IF; a complete IF/DEF migration is out of scope for this PR.

- Delete the fork-only src/platform_cimports.pxi and
  src/extern/cef/platform_cimports.pxi (both absent from upstream cefpython147).
  Restore upstream's inline per-file `IF UNAME_SYSNAME` platform cimports in
  cefpython.pyx and cef_app/cef_browser/cef_browser_static/cef_platform.pxd,
  plus the CEF 147 `cimport sandbox_linux`.
- utils.pyx: revert GetSystemError() to its original `IF UNAME_SYSNAME` form,
  dropping the inline `cdef extern from *` C block.
- browser.pyx: restore `IF UNAME_SYSNAME == "Linux": cimport x11`.
- window_utils_win.pyx: restore the IsWindowHandle IF/ELSE guard.

Verified on Windows: clean build + 85/85 unit tests. Cython front-end also
transpiles clean for Linux and Darwin (forced via -E UNAME_SYSNAME); C++
compile/link on those platforms is left to CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in cookie filter

The IO-thread CanSendCookie/CanSaveCookie callbacks can receive a CefFrame
whose GetBrowser() returns NULL; the early-return fallbacks were previously
silent and unexplained.

- frame.pyx GetPyFrame(): comment now explains when GetBrowser() is NULL off
  the UI thread (IO-thread cookie callbacks, cross-origin subresource requests,
  service workers / CefURLRequest).
- cookie_access_filter.pyx CanSendCookie/CanSaveCookie: log the
  no-resolvable-browser fallback and cite the CEF header doc
  (cef_resource_request_handler.h > CefCookieAccessFilter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix Linux comments that asserted platform behavior contradicted by CEF's
source and empirical testing on CEF 147:

- x11.cpp GetGtkWindow: drop the unreproducible "gtk_plug reparents the browser
  window" claim; explain the cefclient divergence (cefpython embeds into a
  foreign toolkit window and owns no GtkWindow, so wraps the browser X11 handle).
- x11.cpp null guards: cefpython forces ozone-platform=x11, so cef_get_xdisplay()
  is NULL only if the app overrides to Wayland (segfault without the guard);
  xwindow is 0 for OSR.
- window_info.pyx: Alloy is required not because Chrome style "can't be parented"
  (it can), but because OSR is Alloy-only, JS bindings don't work under Chrome,
  and macOS embedded can't use Chrome (CEF #3294).
- window_utils_linux.pyx: the ozone-platform=x11 default is needed because CEF's
  native windowed embedding is X11-only (browser_platform_delegate_native_linux.cc,
  #if SUPPORTS_OZONE_X11, no Wayland impl); Wayland has no foreign-window embedding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The RunContextMenu callback (a Qt-menu workaround from the PyQt6 embedding
exercise) let the app render its own context menu instead of CEF's native
Alloy/X11 one. With embedded-Wayland support dropped, the X11/XWayland native
context menu is used, so the custom path is no longer needed.

- Delete src/handlers/context_menu_handler.pyx (ContextMenuHandler_RunContextMenu
  dispatch + Menu/Callback wrappers).
- Revert the RunContextMenu override in context_menu_handler.cpp/.h back to
  master; OnBeforeContextMenu (native menu customization) is unchanged.
- Drop the pyx include (cefpython.pyx) and "RunContextMenu" from
  allowedClientCallbacks (browser.pyx).
- examples/qt.py: remove the custom Qt ContextMenuHandler class, its _active_menu
  focus-poll machinery, the _x11_button_state helper, and the now-unused
  subprocess import; the example now uses CEF's native context menu.

Build green; 85/85 unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The three CI workflows were added separately and diverged (macOS used a 4-job
compile/test/wheel pipeline; linux/windows a 2-job one), each drawing reviewer
comments. Unify them to a common 2-job shape (download-cef -> build) and resolve
the CI review threads:

- download-cef exposes the CEF version as a job output; the build job's cache
  key references it, so the "Read CEF version" step is no longer duplicated
  across jobs (ci-macos).
- Harmonized cache key on all three: cef-<plat>64-<hashFiles(download_cef.py,
  automate.py)>-<version>. Keeps the version (own cache per version, clean
  miss on bump), invalidates when the download/prepare scripts change
  (ci-linux), and drops the unexplained macOS "-v3-".
- Verify CEF architecture on all three (was macOS-only): lipo arm64 on macOS,
  `file` x86-64 on linux, PE machine 0x8664 on windows.
- Test the built wheel on all three: pip install build/dist/*.whl and run the
  unit tests against the installed package (staging dir moved aside so it does
  not shadow it), mirroring the old build_distrib behaviour.
- Collapse macOS to the single build job (codesign + app-bundle kept as steps;
  app-bundle created once and CFProcessPath exported via GITHUB_ENV, reused by
  both test steps).
- Remove the linux /dev/shm remount: redundant now that the test runner sets
  disable-dev-shm-usage in CI (unittests/main_test.py, osr_test.py).

YAML validated locally; actual CI runs on push (the wheel-test step may need a
CI round-trip to settle paths).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer asked that every GitHub Actions build produce a uniquely identifiable
wheel version (for dev-release testing / bug reports), e.g.
cefpython3-147.0.dev5+g98cd08e.whl.

- build_distrib.py: add --dev, deriving a PEP 440 dev version from git -
  <major>.0.dev<commit-count>+g<short-hash> (e.g. 147.0.dev1196+g6329d14);
  falls back to <major>.0.dev0 without git. Also add --version to override
  verbatim. Default (no flag) stays <major>.0 for releases.
- CI (all three platforms): build the wheel with --dev, and checkout the build
  job with fetch-depth: 0 so git rev-list --count sees full history (a shallow
  clone would always report 1).

Verified locally: --dev yields 147.0.dev<count>+g<hash>, a valid PEP 440 dev
release, and the produced wheel filename / METADATA / dist-info all carry it and
parse via packaging.utils.parse_wheel_filename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cstring

The old build_distrib.py had a high-level overview of the packaging workflow;
the rewrite documented only usage/options. Add a short overview describing the
current pipeline (download CEF -> automate --prebuilt-cef -> CMake build ->
stage into cefpython3/ -> build_distrib.py wheel -> CI wheel test) and clarify
that this script performs only the final packaging step (no compilation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer noted the generated wheel metadata was missing fields present in the
old setuptools-based build (which baked them from cefpython3.setup.py). Restore
them without changing the build architecture (build_distrib.py stays the wheel
builder, per reviewer preference to minimize build-system changes):

- pyproject.toml [project]: add authors, keywords, [project.urls]
  (Homepage/Repository/Download) and the missing classifiers (Development
  Status, Intended Audience, Natural Language, OS macOS/Windows/Linux, Topic).
  Fix the invalid "BSD Software License" classifier -> canonical "BSD License"
  and drop the incorrect "OS Independent" (platform-specific wheels).
- build_distrib.py: read [project] from pyproject.toml (tomllib, tomli on 3.10)
  and emit full core METADATA - Summary, Author/Author-email, License,
  Requires-Python, Keywords, Project-URL, Classifier. Version stays dynamic
  (CEF header / --dev git). Single source of truth: wheel and pyproject no
  longer duplicate metadata.
- requirements.txt: tomli for Python < 3.11 (stdlib tomllib on 3.11+).

Verified: the built wheel's METADATA carries these fields and parses cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The logic that parses src/version/cef_version_*.h was copied in four places:
common.py (get_version_from_file), cmake_prepare_pyx.py (get_cefpython_version),
build_distrib.py (_read_version) and build.py (_read_cef_version). The three
build scripts each re-implemented it because importing common.py runs
module-level path detection at import.

Add tools/cef_version.py - a small, side-effect-free reader (parse_header /
header_path / read) with no import of common.py - and route all four callers
through it:
- common.py get_version_from_file() -> cef_version.parse_header()
- cmake_prepare_pyx.py -> cef_version.parse_header() (local copy removed)
- build_distrib.py _read_version() -> cef_version.read()["CHROME_VERSION_MAJOR"]
- build.py _read_cef_version() -> cef_version.read()["CEF_VERSION"]

common.py's import-time detection is left untouched (separate concern). The
module lives in tools/ next to its callers, so script-run and imported contexts
both resolve it.

Verified: all four readers return the correct version; build_distrib --dev and
cmake_prepare_pyx (CMake-style invocation) both work end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The distutils/setuptools build driver is dead code since the move to
CMake. The Cython invocation now passes --cplus --3str directly on the
command line (CMakeLists.txt). Nothing imports or calls it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#262]

The rewritten build_distrib.py dropped two things that kept the Linux
wheel reasonable:

1. libcef.so symbol stripping. CEF ships it with ~1.1 GB of debug
   symbols. Restore the pre-CMake reduce_package_size_issue262() step:
   strip libcef.so on Linux (keeps .dynsym for linking). 1342 -> 252 MB
   unpacked.

2. Wheel compression. zipfile.ZipInfo.from_file() defaults to
   ZIP_STORED, so package files were written uncompressed despite the
   ZIP_DEFLATED archive. Set compress_type = ZIP_DEFLATED so files are
   actually compressed (libcef.so 252 -> 102 MB in the wheel).

Together the Linux wheel drops from ~356 MB to ~139 MB, comparable to
Windows/macOS. Verified: fresh-venv install + 85/85 unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rewritten wheel build dropped the msvcp140.dll handling the old
make_installer.py had, so the wheel shipped without it. The Cython /MD
extension depends on msvcp140.dll, and Python ships vcruntime140* but
not msvcp140, so import fails on a clean Windows. CI stayed green only
because the runners already have the VC++ redist.

Restore it in the packaging step: build_distrib.py copies msvcp140.dll
from System32 next to the extension on Windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The harmonized workflows ran the suite twice: once pre-wheel against the
staged build (PYTHONPATH) and again against the pip-installed wheel.
Drop the pre-wheel run and keep only 'Test the built wheel' — it's the
representative one (it's what users install, and it exercises the same
compiled artifacts), matching the reviewer's preference and the old
build_distrib.py behavior of testing the installed package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/client_handler/sandbox_linux.cpp Outdated
Comment thread examples/tutorial.py
def main():
check_versions()
sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error
sys.unraisablehook = cef.UnraisableHook # Same, for exceptions that

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need that? Just type: sys.unraisablehook = cef.ExceptHook and remove all that code. Only mention in docs the issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to consolidate onto ExceptHook — one heads-up first, since it affects how.

sys.excepthook(type, value, traceback) takes three args, but sys.unraisablehook(unraisable, /) passes a single object with .exc_type / .exc_value / .exc_traceback (https://docs.python.org/3/library/sys.html#sys.unraisablehook). So sys.unraisablehook = cef.ExceptHook as-is fails with:

TypeError: ExceptHook() missing 2 required positional arguments: 'exc_value' and 'exc_trace'

CPython catches that TypeError, prints "Exception ignored in sys.unraisablehook" to stderr, and continues — so ExceptHook's body never runs (no Shutdown/exit) and the original escaped exception goes unhandled. cef.UnraisableHook was added only to bridge that signature gap.

Which do you prefer?

  1. Keep the tiny cef.UnraisableHook adapter (2 lines, forwards to ExceptHook); ExceptHook unchanged.
  2. Make ExceptHook accept both forms (three args or the single unraisable object), then sys.unraisablehook = cef.ExceptHook works and the wrapper is removed — but this changes ExceptHook's signature.
  3. Drop it from the example entirely and only document the issue.

I'll go with whichever you'd like.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a different signature, then let's keep cef.UnraisableHook. I would prefer not to change the ExceptHook signature.

There is still the question of whether sys.unraisablehook only catches exceptions from handlers (as stated in the documentation), because I noticed other functions using noexcept as well.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so, we should update the documentation and in code comments.

#elif defined(OS_MAC)
#define CEF_API_HASH_999999 "..."
#elif defined(OS_LINUX)
#define CEF_API_HASH_999999 "..."

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean? "CEF_API_HASH_999999" ? Looks like a hack.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

999999 is CEF's own default, not a made-up value. From the Spotify distro headers:

cef_api_hash.h

#define CEF_API_VERSION_EXPERIMENTAL 999999

#if !defined(CEF_API_VERSION)
#define CEF_API_VERSION CEF_API_VERSION_EXPERIMENTAL
#endif

So when CEF_API_VERSION isn't set, CEF selects CEF_API_VERSION_EXPERIMENTAL (999999) by default.

cef_api_versions.h

#if defined(OS_WIN)
#define CEF_API_HASH_999999 "65db327c10558a625d0650a0e454cc24ea6f23b4"
#elif defined(OS_MAC)
#define CEF_API_HASH_999999 "48dac8d7565e012841b9d3c3935776c1a7bff589"
#elif defined(OS_LINUX)
#define CEF_API_HASH_999999 "e1f04162d75f4f36056af4e4a02a4da0457451dd"
#endif

Old cefpython predates CEF's API versioning, so there's no prior version to carry over — leaving CEF_API_VERSION unset is a deliberate choice here. I'd prefer to keep it simple and stick with the default: 999999 is stable and consistent across CEF releases, so upgrading cefpython to a newer CEF needs no version-pin to bump or re-validate --- one less moving part. I don't see an immediate downside for us either, since cefpython always builds its headers and libcef from the same CEF release (so experimental's "not compatible across CEF versions" caveat doesn't apply in practice). Open to pinning an explicit CEF_API_VERSION if you'd rather, but simplicity of upgrades is why I lean toward the default.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that 999999 is CEF's default experimental API version. What I don't understand is why we need to derive the API hash dynamically from the CEF headers. We already maintain platform-specific cef_version_*.h files, so I would expect the API version/hash to be part of those files as well.

We should define CEF_API_VERSION explicitly for each platform and there should be no 999999 in the cefpython codebase.

Comment thread src/cefpython.pyx
linesight and others added 6 commits July 4, 2026 11:24
…xt init

Under CEF's Chrome runtime the browser context initializes asynchronously:
CefBrowserProcessHandler::OnContextInitialized() fires after the message loop
starts, not when CefInitialize() returns (upstream CEF issue #2969). The
Chrome runtime became the only runtime after the Alloy runtime was removed in
CEF M128, so a browser can no longer be created immediately after
cef.Initialize() -- doing so brings the renderer up before its host bindings
are wired and the page renders blank.

Replace the polling / deferred-creation workaround in CefInitialize() and
CreateBrowserSync() with the pattern CEF's own cefsimple sample uses: create
the browser from an "OnContextInitialized" global client callback, registered
before cef.Initialize(). CreateBrowserSync() stays synchronous and now raises
a clear, actionable error when called before the context is initialized
instead of silently rendering a blank page.

- Expose "OnContextInitialized" as a global client callback
  (SetGlobalClientCallback), invoked from the browser process handler.
- Migrate the unit-test harness (main_test, osr_test, issue517) and all
  examples. Windowed GUI examples embed the browser only once both the window
  is realized and the context is initialized (event-driven, no polling).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wheel was missing the long description (the review thread's other
fields were already restored). Add it via pyproject [project].readme
(inline text), emitted by build_distrib.py — reusing the concise blurb
the old setuptools build shipped, not the full README.
…itionally

Revert Linux sandbox handling to the minimal change needed for CEF 147.
The old code set no_sandbox=1 in CefSettings, which in 147 makes
BasicStartupComplete() append --no-sandbox before the Mojo bootstrap fd
(GlobalDescriptors key 7) is registered, crashing every subprocess with
"Failed global descriptor lookup: 7". Linux now passes the --no-sandbox
command-line switch unconditionally instead -- the same "sandbox off"
behavior cefpython has always had, via the mechanism 147 requires.

Removes the runtime sandbox-availability probe (sandbox_linux.cpp/.h/
.pxd, the cimport, the window_utils_linux.pyx conditional and its
Knowledge-Base section). Keeping the sandbox enabled where the system
supports it belongs in a separate change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ontract

CEF documents browser and frame as optional (optional_param=browser,frame in
cef_resource_request_handler.h) for the CefResourceRequestHandler callbacks --
they may be NULL for requests originating from service workers or CefURLRequest,
and CefFrame::GetBrowser() may be NULL off the UI thread.

- OnBeforeResourceLoad / GetResourceHandler / OnResourceRedirect: these are the
  callbacks where CEF documents frame as optional. Guard the documented NULL by
  checking both the frame and its browser, log the fallback via Debug(), and
  return CEF's default. The previous guard only checked GetBrowser() (which
  would dereference a NULL frame) and returned silently.

- OnBeforeBrowse / GetAuthCredentials: remove the NULL-browser guard. CEF does
  not mark frame optional for these CefRequestHandler callbacks, so frame is
  present; and GetPyFrame() now raises on a NULL browser (surfaced via the
  handler's exception hook) instead of the old SIGSEGV, so the guard is no
  longer needed to avoid a crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The comment attributed CefFrame::GetBrowser() returning NULL to issue cztomczak#676,
but cztomczak#676 is specifically the "CanSendCookie/CanSaveCookie not called by handler"
cookie bug. The NULL is CEF's documented optional-param behavior: browser/frame
may be NULL for the IO-thread request callbacks (service workers / CefURLRequest,
per cef_resource_request_handler.h). Reword the comment to cite that instead;
the cookie access filter is mentioned only as an example caller that guards it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump actions/upload-artifact v4 -> v7 and set archive: false so the built wheel
downloads directly from a run's Artifacts page instead of inside a
GitHub-generated .zip wrapper. With archive: false the artifact takes the
wheel's own filename (e.g. cefpython3-147.0.devN+g<hash>-cpXX-...-<platform>.whl),
so the exact version is visible without extracting. Non-zipped uploads require
upload-artifact v7+.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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