Port to Avalonia 12 on a locally built CefGlue.Avalonia (OutSystems/CefGlue#249) - #75
danielmeza wants to merge 4 commits into
Conversation
The embedded browser is the one thing holding the app on Avalonia 11 (#67): no CefGlue.Avalonia built against Avalonia 12 is published, and the port is an open upstream pull request. scripts/pack-cefglue-avalonia12.sh builds CefGlue.Avalonia from that pull request at one pinned commit (e204172) into the git-ignored local-packages/ feed, the same feed use-local-libs.sh uses. The version, 120.6099.211-pr249.e204172, lives in Directory.Build.props as $(CefGlueAvaloniaVersion). It names the pull request and the commit, so no published package can ever satisfy it and restore cannot fall back to one, and moving to another commit is a new version rather than a stale cache hit. Switching back when upstream publishes is a change to that one property. Only CefGlue.Avalonia is packed. The pull request does not touch CefGlue.Common (the CEF bindings, browser subprocess and native redistributables; its source is identical to the commit the published 120.6099.211 was built from), so that keeps coming from nuget.org. local-packages/ is git-ignored, so CI runs the script before restoring. The commit is pinned by SHA, so a push to the pull request's branch cannot change what CI builds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHQsqcKVEiAKvCS98Hn4Yh
Avalonia 11.3.21 -> 12.1.2 (#67), with CefGlue.Avalonia built from OutSystems/CefGlue#249 (previous commit). WebViewControl-Avalonia is dropped, not upgraded. It has no Avalonia 12 build, and it is not binary compatible: with Avalonia 12.1.2 loaded, its WebView.ExtraInitialize fails to JIT (MissingMethodException for AvaloniaObject.get_Item(IndexerDescriptor), the indexer-binding API 12 removed), and ExtraInitialize runs in the WebView constructor. The app only used it as a host for CefGlue anyway, so MainWindow now uses CefGlue's AvaloniaCefBrowser directly, through a one-line EmbeddedBrowser subclass: AvaloniaCefBrowser's only constructor takes an optional argument, which the XAML compiler does not accept as parameterless (AVLN3000). What WebViewControl did and now happens here: - Address was a bindable property; on AvaloniaCefBrowser it is a CLR property, so MainWindow sets it from WebviewUrl. - CEF initialisation: App already called CefRuntimeLoader.Initialize, but that only stores the settings for the first browser, and WebViewControl's WebView called it again with its own before that, so App's never took effect. They do now (PersistSessionCookies, PersistUserPreferences). - Links that open a new window went to the system browser; a download started there never reaches InternalDownloadHandler. They now open in the embedded browser (SameBrowserPopupHandler). - The six request/resource handler classes at the end of MainWindow.axaml.cs were unreferenced copies of WebViewControl internals that no longer compile without it; removed. InternalDownloadHandler no longer reads the window's DataContext. CEF calls it on its own UI thread, and reading an Avalonia property off Avalonia's UI thread throws; unhandled on a CEF thread, that aborted the process the moment a download started (seen at runtime on this branch). The window now hands the handler its view model on the UI thread. Master has the same read and Avalonia 11.3 checks the thread the same way, so master probably crashes too, but that is untested: there the datasheet link I used went to the system browser, so no download reached the handler. Avalonia 12 API changes: Avalonia.Diagnostics has no 12.x, so AvaloniaUI.DiagnosticsSupport replaces it (still Debug-only, attached once in App.Initialize); binding plugins are gone and the DataAnnotations validator is off by default, so DisableAvaloniaDataAnnotationValidation goes; IClipboard.SetTextAsync is now an extension method; TextBox.Watermark is obsolete for PlaceholderText. Avalonia.Xaml.Interactions has no 12.x and nothing used it, so it is removed rather than replaced. Lemon.Hosting.AvaloniauiDesktop 1.1.1 stays: every method in it JIT-compiles against Avalonia 12.1.2, and the app starts on it. ReactiveUI is not bumped here: CefGlue.Avalonia from the pull request brings ReactiveUI.Avalonia 12.0.3 (ReactiveUI 23.2.28), the same ReactiveUI #49 adds; the move to ReactiveUI 24 waits for #49 to land. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHQsqcKVEiAKvCS98Hn4Yh
Brings the branch up to master 8e4ed2e, KiCadSharp 0.4.0 and SExpressions 0.2.0 included. One textual conflict, and four places where master's code needed the Avalonia 12 port applied to it. Views/MainWindow.axaml.cs, the conflict. Master's download handler is kept whole: - refusing never calls Continue, and a refused id is cancelled on its next update (#97, #106); - only a download the handler accepted becomes an import, and only at the path it accepted; - the folder comes from IConfigService, and nothing reads Avalonia state on CEF's thread (#108, #111). This branch had its own answer to the last one, handing the handler the view model; master's replaces it. On top of master's handler go this branch's browser changes: CefGlue's AvaloniaCefBrowser used directly, the handlers set in the constructor instead of through WebViewControl's GlobalWebViewInitialized, WebviewUrl followed through NavigateTo, and SameBrowserPopupHandler. NavigateTo is also how Find on Ultra Librarian (#100) reaches the browser. CefGlue.Common is still 120.6099.211, where OnBeforeDownload returns void, so the override is unchanged. ReactiveUI.Avalonia 11.4.13 -> 12.0.3. The previous master merge kept 11.4.13, but the CefGlue.Avalonia built from OutSystems/CefGlue#249 depends on ReactiveUI.Avalonia >= 12.0.3, and restore failed with NU1605. That was the red CI build on the last push. 12.0.3 is still ReactiveUI 23.2.28, the ReactiveUI master has, so #116's search pipeline builds unchanged here. ReactiveUI 24 is the next commit. Settings (#96, #103, #115): the text boxes master added use Watermark, which Avalonia 12 makes obsolete; they use PlaceholderText, like the others. The #if DEBUG `using Avalonia;` that #90 added in AboutWindow and SettingsWindow goes with the AttachDevTools calls this branch had already removed. Avalonia 12's developer tools attach once, in App. Program.cs (#95, #104) is merged as master has it: HarfBuzzPreload, SetLogDirectory and the MCP logging allowlist are unchanged. Only the UseReactiveUI comment changes, because the CefGlue build no longer brings in the old Avalonia.ReactiveUI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHQsqcKVEiAKvCS98Hn4Yh
|
Decisions from the maintainer, so the branch has them on record:
Unresolved upstream: CefGlue published 120.6099.214 today and #249 now conflicts with their master, so moving off 120.6099.211 needs #249 rebased. This stays a draft until #249 merges. |
Summary
The app now runs on Avalonia 12.1.2. The embedded browser uses the Avalonia 12 port of
CefGlue.Avaloniafrom the open upstream pull requestOutSystems/CefGlue#249, which this repo builds
locally until upstream publishes it.
WebViewControl-Avaloniais removed.MainWindowusesCefGlue's own browser control.
This is a draft for one reason: CI and every fresh clone depend on code from an unmerged
third-party pull request, built at a pinned commit (see CI and the open questions).
Part of #67. The issue's "Done when" also requires that "a part downloaded in the embedded browser
still imports end to end". I could not show that: it needs an UltraLibrarian account. What I did show
is the download being intercepted into the configured folder, and refused when it would escape it
(see Verification).
What changed since the last push (2026-09-21 17:49)
1. Master is merged in, up to
8e4ed2e(#88 to #118), KiCadSharp 0.4.0 and SExpressions 0.2.0included. One textual conflict,
Views/MainWindow.axaml.cs, plus four places where master's codeneeded the Avalonia 12 port applied to it.
Continue, and a refused id iscancelled on its next update (Refused browser downloads are not refused: Continue("") downloads to CEF's temp directory #97, Refuse browser downloads for real, and import only accepted ones (#97) #106); only a download the handler accepted becomes an import,
and only at the path it accepted; the folder comes from
IConfigService, and nothing reads Avaloniastate on CEF's thread (Browser downloads ignore the download folder set in Settings #108, Use the configured download folder for browser downloads (#108) #111). This branch had its own answer to that last bug — handing the
handler the view model — and master's replaces it.
AvaloniaCefBrowserused directly, thedownload and life-span handlers set in the constructor instead of through WebViewControl's
GlobalWebViewInitialized,WebviewUrlfollowed throughNavigateTo, andSameBrowserPopupHandler.NavigateTois also how Find on Ultra Librarian (Give every Part Explorer result an import route or a reason (#47) #100) reaches the browser.CefGlue.Commonis still 120.6099.211, whereOnBeforeDownloadreturnsvoid, so the override isunchanged.
Watermark, which Avalonia 12 makesobsolete; they now use
PlaceholderText, like the others. The#if DEBUGusing Avalonia;that Compile Debug builds again, and build Debug in CI #90added in
AboutWindowandSettingsWindowgoes with theAttachDevToolscalls this branch hadalready removed — Avalonia 12's developer tools attach once, in
App.Program.cs(Log MCP mode to nlog.config's files and stderr, never stdout (#80) #95, Set NLog's log folder from code, and make nlog.config's rules take effect (#98) #104) is merged as master has it:HarfBuzzPreload,SetLogDirectoryand theMCP logging allowlist are untouched. Only the
UseReactiveUIcomment changes, because the CefGluebuild no longer brings in the old
Avalonia.ReactiveUI.2.
ReactiveUI.Avalonia11.4.13 → 12.0.3. This is what made CI red on the last push: the mastermerge done through GitHub kept 11.4.13, while the CefGlue.Avalonia built from #249 depends on
ReactiveUI.Avalonia >= 12.0.3, and restore failed with NU1605 (package downgrade). 12.0.3 isstill ReactiveUI 23.2.28 — the ReactiveUI master has — so the #116 search pipeline compiles unchanged.
3. ReactiveUI 24 is not here, and that is a change of plan. See the next section.
ReactiveUI 24: tried, and left out for now
The plan in this PR (and in #67, scope item 4) was
ReactiveUI.Avalonia12.1.x, i.e. ReactiveUI 24.I ported it and then took it out, because the CefGlue.Avalonia built from #249 cannot run on it:
ReactiveUI.Primitives.ReactiveUI.Avalonia12.0.3 (ReactiveUI 23) and usesSystem.Reactive in three places:
SubscribeinExtendedAvaloniaNativeControlHostand inAvaloniaOffScreenControlHost(every platform), andObservable.Interval(...).ObserveOn(AvaloniaScheduler.Instance)in
AvaloniaBrowserProcessHandler(the external message pump, created on macOS only).Xilium.CefGlue.Avalonia's 206 type references failto bind. Run headless on Linux, the app aborted the moment the browser was created — exit 134,
System.IO.FileNotFoundException: Could not load file or assembly 'System.Reactive, Version=6.1.0.0'from
ExtendedAvaloniaNativeControlHost..ctor. This is the whole browser tab, not a corner case.System.Reactive6.1.0 runtime-only (IncludeAssets="runtime"; at compile time itsextension methods clash with ReactiveUI.Primitives') gets Linux and Windows working again — I ran
that variant and the browser loaded — but
ReactiveUI.Avalonia.AvaloniaScheduleris still gone, soCefGlue's macOS message pump would still fail. Nothing here was run on macOS.
So the branch stays on ReactiveUI 23.2.28 through
ReactiveUI.Avalonia12.0.3, which is the pairing#249 itself was built against, and where every reference of that build binds.
The port itself is done and checked, and sits on the local branch
avalonia12-rui24in theworktree this was prepared in (one commit on top of the merge). It moves the Part Explorer pipeline
from System.Reactive to ReactiveUI.Primitives (
RxVoidforUnit,ISequencerforIScheduler,Signal.Return/Signal.Empty, the sameWhere/Select/Switch/SelectMany/Append/Catch/ObserveOn/StartWithin the same order), and rewritesAsyncEnumerableObservableExtensions.ToObservableas a plain
IObservable<T>that keeps #86's cancellation rules. A throwaway harness runs 106 checks —14 for those cancellation rules and 92 view-model checks adapted from #49's harness, under two
stand-ins for Avalonia's dispatcher — and they pass against both that build and a build of master.
It is one
git cherry-pickaway whenever the question below is answered.The upstream pull request
selway.Open, last updated 2026-07-31, still no review.
e204172cdf6ad8d048fe0a3a3c0436db8a7c767c, still the PR head. It is fetched fromOutSystems/CefGlueby SHA, so a later push to the PR branch cannot change what is built here.(2026-09-22). #249 is now CONFLICTING with upstream master, and one of those commits touches
CefGlue.Avalonia/AvaloniaBrowserProcessHandler.cs(a macOS idle-CPU fix). The pin here isdeliberately unchanged: 120.6099.211 for
CefGlue.Common, and #249 ate204172forCefGlue.Avalonia. Moving to 214 means waiting for #249 to be rebased, or rebasing it ourselves.CefGlue.Avaloniamoves to Avalonia 12.1.0; focus event args,TopLevel.GetTopLevelinstead ofGetVisualRoot,DataTransferinstead ofDataObject,WindowDecorations,ContextMenu.Placement. It dropsAvalonia.ReactiveUI11.0.9 and theSystem.Reactive.Linqpreview forReactiveUI.Avalonia12.0.3 andAvaloniaUI.DiagnosticsSupport2.2.3.
CefGlue.Commonis untouched by it.How the local build is consumed
Unchanged from the last push:
scripts/pack-cefglue-avalonia12.shshallow-fetches the pinned commit,builds
CefGlue.Avaloniaand copies only that.nupkginto the git-ignoredlocal-packages/feed, atversion
120.6099.211-pr249.e204172($(CefGlueAvaloniaVersion)inDirectory.Build.props). Theversion names the pull request and the commit, so no published package can satisfy it and restore
cannot silently fall back.
CefGlue.Commonkeeps coming from nuget.org at 120.6099.211. Nothing binaryis committed.
Switching back when upstream publishes: point
CefGlueAvaloniaVersionat the published version,bump
CefGlue.Commonto match, and delete the script, its CI step and the README paragraph.CI
Two jobs are red, for two different reasons, and only one of them is about the local package.
build(CI workflow). This job does build CefGlue#249 from source:ci.ymlhas had a "PackCefGlue.Avalonia" step before Restore since the first commit here, and it works — it was green on
run 35625857982 (pack about
33 s, whole job 1 min 16 s). What failed on the last push was NU1605, the ReactiveUI downgrade
the master merge introduced. With that pinned correctly, this push is
green again: pack, restore,
Release (0 warnings, 0 errors), Debug (0/0) and Format all pass. The pack step logs 71 warnings of
its own; they come from compiling CefGlue, which does not treat warnings as errors.
submit-nuget(GitHub's "Automatic Dependency Submission", a dynamic workflow, not a file in thisrepo). This one is the "CI cannot restore a locally built package" case: it runs its own
dotnet restorewithout running any repo script, solocal-packages/is empty and restore reportsNU1603 — "CefGlue.Avalonia 120.6099.211-pr249.e204172 was not found; 120.6099.211 was resolved
instead" — which
TreatWarningsAsErrorsturns into an error(this push's run). It has
failed on every push since the local package was introduced, and it cannot be put right from
ci.yml.Copilot job; it failed in its own setup, unrelated to this branch.)
Suggestions, not implemented here — pick one and I'll do it:
submit-nugetred while this is a draft. The dependency graph cannot be submitted for apackage that does not exist publicly anyway.
Dependency graph) and add a workflow step that runs the pack script first and then submits the
graph, so the check stays meaningful.
CefGlue.Avaloniaonce to this repository's GitHub Packages feed and addthat feed to
NuGet.config. Every job then restores it normally, and the pack step disappears. Itstill ships an unreviewed third-party build, and it needs a token for restore.
.nupkgto the repo. Simplest for every consumer; a binary in git, and I havenot done it because the instructions here are to ask first.
Design
WebViewControl-Avaloniais dropped, not upgraded. Its latest release, 3.120.11, pins Avalonia 11,its Avalonia 12 issue (OutSystems/WebView#403) is open with no fix, and it is not binary compatible:
with Avalonia 12.1.2 loaded,
WebView.ExtraInitializefails to JIT (AvaloniaObject.get_Item(IndexerDescriptor),removed in 12) and it runs in the
WebViewconstructor. The app used it only as a host for CefGlue, soMainWindownow declaresviews:EmbeddedBrowser, a one-line subclass of CefGlue'sAvaloniaCefBrowser(whose only constructor takes an optional argument, which the XAML compiler rejects as not
parameterless, AVLN3000).
What WebViewControl did, and what happens now:
Addressbinding.AvaloniaCefBrowser.Addressis a CLR property, soMainWindowsets it fromWebviewUrl: once when the DataContext arrives, then on every change.Appalready calledCefRuntimeLoader.Initialize(...), butWebViewControl'sWebViewconstructor called it again with its own settings, so App's never took effect. They donow:
PersistSessionCookies,PersistUserPreferences,CookieableSchemesList. This is abehaviour change; see the open questions.
target=_blank,window.open). WebViewControl sent them to the systembrowser, where a download never reaches
InternalDownloadHandler. They now open in the embeddedbrowser (
SameBrowserPopupHandler), and their downloads are intercepted (measured, seeVerification). Non-http(s) targets keep CEF's default. Also a behaviour change; see the open
questions.
MainWindow.axaml.cswere referenced nowhere and depended on WebViewControl types.
file-dialog, drag and keyboard handlers, and
AllowDeveloperTools.Avalonia 12 API changes:
Avalonia.Diagnosticshas no 12.x, soAvaloniaUI.DiagnosticsSupportreplaces it, Debug-only, attached once in
App.Initialize; binding plugins are gone and theDataAnnotations validator is off by default, so
DisableAvaloniaDataAnnotationValidationgoes;IClipboard.SetTextAsyncis an extension method now;TextBox.Watermark→PlaceholderText;Avalonia.Xaml.Interactionshas no 12.x and nothing used it.Lemon.Hosting.AvaloniauiDesktop1.1.1 stays. Every one of its type and member references bindsagainst this build (72 types, 119 members, 0 failures), and the app starts on it.
Verification
All of it on
e7a4764, the merge commit this push adds.Build:
dotnet build UltraLibrarianImporter.sln -c Releaseand-c Debug, both with--no-incremental: 0 warnings, 0 errors.Format:
dotnet format UltraLibrarianImporter.sln --severity warn --verify-no-changes: clean,exit 0.
Vulnerable packages:
dotnet list ... --vulnerable --include-transitive: none, in either project.Parser:
dotnet run --project src/importer/SampleConsole -c Release -- --test-parser: exit 0,reaches
=== Tests Complete ===.Binding check. A throwaway probe binds every type and member reference of the built assemblies
against the app's own output folder, the way the runtime does.
Xilium.CefGlue.Avaloniafrom #249:0 of 206 type references and 0 of 363 member references fail.
Lemon.Hosting.AvaloniauiDesktopandReactiveUI.Avalonia: 0 failures. (On ReactiveUI 24 that same probe is what showed the 4 failuresdescribed above.)
Runtime, headless. Xvfb, a temp
HOMEwith the XDG directories deliberately not created(Resolve special folders to absolute paths even when they do not exist yet (#70) #93),
DBUS_SESSION_BUS_ADDRESSpointing at nothing (so the real keyring is never touched), a shortTMPDIR, noLD_PRELOAD, andenv -iotherwise.Pre-bound .../libHarfBuzzSharp.so with RTLD_NOW(Linux: segfault in HarfBuzz right after the main window opens (CEF's GTK vs HarfBuzzSharp) #78, no
LD_PRELOADneeded) andMain window created and configured, and the app runs untiltimeoutkills it (exit 124) with no crash markers and no core dump.NE555, clicked Search All Providers, and the grid filled with 25 live rowsfrom JLCPCB's endpoint (
Aggregated search completed for query: ne555, found: 25).started, and
app.ultralibrarian.comrendered (its login page, since the search needs an account).The status line read "Searching Ultra Librarian for NE555DR…". No crash.
Downloads, end to end, through the real handler. An instrumented scratch copy of this commit
(an environment variable picks the first URL and the browser tab; stderr traces every handler
callback) against a local server that sends
Content-Disposition: attachment, withconfig.jsonseeding
DownloadDirectoryto<temp home>/custom-downloads:filename="part.zip"OnBeforeDownloadon CEF's thread (uiThread=False),Continue(<custom-downloads>/part.zip), completed at that path, 260,118 bytes, andDetected download for UltraLibrarian: part.zip. One copy of the file, in the configured folder.filename=".."Continue,Cancelon each following update until CEF reported it canceled, and "Browser download refused: its file name would place it outside the download folder". No file of the served size anywhere — not in the temp home, not in the browser cache, not inTMPDIR.window.open('/good')(a new-window link)OnBeforePopup target=[…/good] disposition=NewForegroundTab gesture=False, the same browser took it, and the download landed in the configured folder as above.No run logged "Call from invalid thread", and none crashed (each ended at
timeout, exit 124).MCP:
./UltraLibrarianImporter.UI --mcpwithinitialize,notifications/initialized,tools/listand twotools/calls piped in. stdout had 4 lines, all valid JSON-RPC 2.0; the toolsare
search_components,list_providers,get_component_details;list_providersandsearch_componentsboth answered (isError=false, 20 components for NE555); every log line went tostderr. Exit 0.
Not verified: the end-to-end import of a downloaded UltraLibrarian archive (needs an account);
Windows and macOS; Flatpak KiCad; JS dialogs, context menus, drag and drop, and CEF's off-screen
rendering mode.
Open questions
ReactiveUI.Avalonia12.0.3, as this push does, until upstream CefGlue builds against ReactiveUI 24;(b) take the ported commit plus a runtime-only
System.Reactivereference, which works on Linux andWindows and leaves CefGlue's macOS message pump broken; (c) patch
AvaloniaBrowserProcessHandlerin the local CefGlue build (three lines) and offer that upstream on #249 — more unreviewed code
here, but nothing platform-specific left behind.
submit-nuget: which of the five options above? (4) needs your go-ahead to commit abinary.
That is what lets their downloads be intercepted — measured above with
window.open. Is that thebehaviour you want, or should some of them still go out to the system browser?
PersistSessionCookies,PersistUserPreferences), because App'sCEF settings finally take effect: an UltraLibrarian login would survive a restart. Keep it?
Lemon.Hosting.AvaloniauiDesktopis unmaintained and built against Avalonia 11. It works today(binding check above, and the app starts on it). Replace it now or later?
AvaloniaUI.DiagnosticsSupport, which #249 makes a runtime dependency ofCefGlue.Avalonia,declares no license in its nuspec. It stays out of Release builds here (the direct reference sets
IncludeAssets=Noneoutside Debug, and it is indeed absent frombin/Release), so it would not bedistributed, but you may want to keep an eye on it.
What
CLAUDE.mdshould say after this landsI did not edit
CLAUDE.md(it is kept up to date in its own PRs). Suggested changes:#if DEBUGexample now points at files that no longer have it: Avalonia 12'sdeveloper tools are attached once in
App.Initialize, soAboutWindow/SettingsWindowno longercarry a Debug-only
using. The rule stands; the example needs another file.🤖 Generated with Claude Code
https://claude.ai/code/session_01DHQsqcKVEiAKvCS98Hn4Yh