Windows: desktop app build in CI, and GTK with AccessKit for screen readers #33

Manually merged
SkyfaR merged 16 commits from windows-ci into main 2026-10-06 07:43:09 +02:00
Owner

Two independent Windows items from W3: the desktop app's build and tests in CI, and GTK built with AccessKit, so that Windows screen readers can read the app. Missing AccessKit is a release blocker (plan §1.4, H22).

GTK with AccessKit (scripts/windows/build-gtk.sh, packaging/windows/gtk/)

  • Why we build it ourselves: MSYS2's CLANG64 gtk4 4.24.1 is built without AccessKit, because their accesskit-c is 0.17 and GTK 4.24 needs 0.18. So Narrator and NVDA got nothing from the app.
  • How it is built:
    • GTK 4.24.1 is built the way MSYS2 builds it: same tarball, the options of their PKGBUILD, their CLANG64 flags and their font-rendering patch. The only difference is -Daccesskit=enabled.
    • accesskit-c 0.18.0 is built as GTK's meson subproject (Rust, gnullvm).
    • It is cross-compiled with meson 1.12.1 and llvm-mingw against the pinned sysroot (UCRT).
  • Pinning: GTK, accesskit-c and meson are pinned by SHA-256 in packaging/windows/gtk/sources.lock; the crates are checked through accesskit-c's Cargo.lock. Only the gtk4 version that msys2.lock pins is built.
  • Self-checks (the build fails otherwise):
    • the AccessKit backend is present and not "Disabled during GTK build";
    • the DLL imports exactly what MSYS2's GTK imports, plus libaccesskit-c-0.18.dll;
    • it exports all 5315 symbols of MSYS2's;
    • no build-machine paths are in the DLL, and the PE time stamp is 0.
  • Reproducible: two builds with different paths gave byte-identical DLLs.
  • Bundle: it ships our libgtk-4-1.dll and libaccesskit-c-0.18.dll. check-bundle-windows.sh and dist.sh refuse a bundle whose GTK has no AccessKit. system-dlls.txt gains oleaut32.dll and uiautomationcore.dll, which AccessKit imports.
  • Translations: the bundle now also carries GTK's, libadwaita's and GLib's own .mo files from the sysroot, for the app's languages, so that GTK's own texts are not English (an entry's Copy and Paste, the about dialog). Before, it had none. This adds about 6 MiB, for 78 MiB in all.
  • Cost: the first build takes about 12-19 min; after that it is cached per recipe hash.

CI (.forgejo/workflows/ci.yml)

New jobs, all in rust:1-trixie with toolchain.sh --wine --gui, none using the Docker socket:

  • windows-sysroot: fetches the pinned MSYS2 packages and caches them by the lock's hash.
  • windows-gtk: builds GTK with AccessKit and caches it by the recipe hash.
  • windows-gui-check: check-gui.sh, including the script tests and a fetch of accesskit-c's crates into an empty cargo cache.
  • windows-gui-test: the app's Windows tests under Wine.
  • windows-installer-test: the installer tests under Wine.
  • windows-build: bundle, smoke test, and dist.sh with the app. It uploads the setup.exe, the portable zip and the test package as artifacts, plus the smoke test's log and screenshot.

On a v* tag the release job also attaches the setup.exe and the portable zip with their .sha256. It now waits for every Windows job, including the command line's.

toolchain.sh --gui holds the one package list that both CI and in-container.sh use; the Containerfile is just toolchain.sh --wine --gui.

Tests

  • In the container: check.sh, check-gui.sh (msys2 12, gtk 12 and bundle 9 script tests, plus clippy), build-gui.sh with a fresh GTK build, gui-smoke.sh, gui-test.sh (21), the installer's wine-test.sh, and dist.sh with the app.
  • Each new CI job ran step by step in a plain rust:1-trixie container, outside the runner. Times on a loaded machine: sysroot 2 min, gtk 20 min (cache miss), gui-test 14 min, gui-check 12 min, build 34 min.
  • Both pieces had a review with adversarial verification and a fix round. No Rust code changed.

Not verified yet

  • On the real Forgejo runner: actions/cache with an expression key, job outputs, artifacts and the release upload. This PR's own run is the first test.
  • Whether Narrator and NVDA actually read the app: this needs a real Windows PC.

Independent of the open W3 part 2 branch (windows-w3b). Both touch scripts/windows/system-dlls.txt and the docs; whichever merges second gets a small conflict.

Two independent Windows items from W3: the desktop app's build and tests in CI, and GTK built with AccessKit, so that Windows screen readers can read the app. Missing AccessKit is a release blocker (plan §1.4, H22). ## GTK with AccessKit (`scripts/windows/build-gtk.sh`, `packaging/windows/gtk/`) - **Why we build it ourselves:** MSYS2's CLANG64 gtk4 4.24.1 is built without AccessKit, because their `accesskit-c` is 0.17 and GTK 4.24 needs 0.18. So Narrator and NVDA got nothing from the app. - **How it is built:** - GTK 4.24.1 is built the way MSYS2 builds it: same tarball, the options of their PKGBUILD, their CLANG64 flags and their font-rendering patch. The only difference is `-Daccesskit=enabled`. - accesskit-c 0.18.0 is built as GTK's meson subproject (Rust, gnullvm). - It is cross-compiled with meson 1.12.1 and llvm-mingw against the pinned sysroot (UCRT). - **Pinning:** GTK, accesskit-c and meson are pinned by SHA-256 in `packaging/windows/gtk/sources.lock`; the crates are checked through accesskit-c's `Cargo.lock`. Only the gtk4 version that `msys2.lock` pins is built. - **Self-checks** (the build fails otherwise): - the AccessKit backend is present and not "Disabled during GTK build"; - the DLL imports exactly what MSYS2's GTK imports, plus `libaccesskit-c-0.18.dll`; - it exports all 5315 symbols of MSYS2's; - no build-machine paths are in the DLL, and the PE time stamp is 0. - **Reproducible:** two builds with different paths gave byte-identical DLLs. - **Bundle:** it ships our `libgtk-4-1.dll` and `libaccesskit-c-0.18.dll`. `check-bundle-windows.sh` and `dist.sh` refuse a bundle whose GTK has no AccessKit. `system-dlls.txt` gains `oleaut32.dll` and `uiautomationcore.dll`, which AccessKit imports. - **Translations:** the bundle now also carries GTK's, libadwaita's and GLib's own `.mo` files from the sysroot, for the app's languages, so that GTK's own texts are not English (an entry's Copy and Paste, the about dialog). Before, it had none. This adds about 6 MiB, for 78 MiB in all. - **Cost:** the first build takes about 12-19 min; after that it is cached per recipe hash. ## CI (`.forgejo/workflows/ci.yml`) New jobs, all in `rust:1-trixie` with `toolchain.sh --wine --gui`, none using the Docker socket: - `windows-sysroot`: fetches the pinned MSYS2 packages and caches them by the lock's hash. - `windows-gtk`: builds GTK with AccessKit and caches it by the recipe hash. - `windows-gui-check`: `check-gui.sh`, including the script tests and a fetch of accesskit-c's crates into an empty cargo cache. - `windows-gui-test`: the app's Windows tests under Wine. - `windows-installer-test`: the installer tests under Wine. - `windows-build`: bundle, smoke test, and `dist.sh` with the app. It uploads the setup.exe, the portable zip and the test package as artifacts, plus the smoke test's log and screenshot. On a `v*` tag the release job also attaches the setup.exe and the portable zip with their `.sha256`. It now waits for every Windows job, including the command line's. `toolchain.sh --gui` holds the one package list that both CI and `in-container.sh` use; the Containerfile is just `toolchain.sh --wine --gui`. ## Tests - In the container: `check.sh`, `check-gui.sh` (msys2 12, gtk 12 and bundle 9 script tests, plus clippy), `build-gui.sh` with a fresh GTK build, `gui-smoke.sh`, `gui-test.sh` (21), the installer's `wine-test.sh`, and `dist.sh` with the app. - Each new CI job ran step by step in a plain `rust:1-trixie` container, outside the runner. Times on a loaded machine: sysroot 2 min, gtk 20 min (cache miss), gui-test 14 min, gui-check 12 min, build 34 min. - Both pieces had a review with adversarial verification and a fix round. No Rust code changed. ## Not verified yet - On the real Forgejo runner: `actions/cache` with an expression key, job outputs, artifacts and the release upload. This PR's own run is the first test. - Whether Narrator and NVDA actually read the app: this needs a real Windows PC. Independent of the open W3 part 2 branch (`windows-w3b`). Both touch `scripts/windows/system-dlls.txt` and the docs; whichever merges second gets a small conflict.
GTK's Vulkan renderer compiles against vulkan-headers, which no run-time
package of the lock brings. The lock is written anew from the same signed
database (2026-10-04); only vulkan-headers is new.
MSYS2's CLANG64 gtk4 4.24.1 is built without AccessKit, as its accesskit-c
(0.17) is older than the accesskit-c-0.18 GTK asks for, so Windows screen
readers get nothing of the app. build-gtk.sh builds the same GTK as MSYS2
does (PKGBUILD options, CLANG64 CFLAGS, its patch) with
-Daccesskit=enabled, cross-compiled with meson and llvm-mingw against the
pinned sysroot, with accesskit-c 0.18.0 as GTK's meson subproject. Sources
are pinned by SHA-256 in packaging/windows/gtk/sources.lock, the crates by
accesskit-c's Cargo.lock; the build is cached per recipe hash and checks
that GTK has the AccessKit backend, imports what MSYS2's GTK imports plus
accesskit-c's DLL, and exports everything MSYS2's exports.
crate-licenses.py collects the licences of the crates linked into
accesskit-c's DLL. The container gets ninja, GLib's code generators,
xmllint and glslc.
bundle-gui.py --gtk takes libgtk-4-1.dll and libaccesskit-c-0.18.dll from
build-gtk.sh's build before the sysroot's, with their licences;
build-gui.sh passes it and gui-test.sh puts it first on WINEPATH.
accesskit-c imports uiautomationcore.dll and oleaut32.dll, both in
System32 since Windows 7, which system-dlls.txt now allows.
The CI jobs for the Windows desktop app run in a plain rust:1-trixie
image and need the packages in-container.sh's image adds for the app's
build, its tests and the installer. toolchain.sh installs them with
--gui, and the Containerfile uses it, so both get one list.
Five jobs in rust:1-trixie run the scripts in-container.sh runs:
windows-sysroot fetches the MSYS2 packages the lock pins into the
runner's cache, keyed by the lock's hash; windows-gui-check runs
check-gui.sh, windows-gui-test gui-test.sh, windows-installer-test the
installer's wine-test.sh, and windows-build build-gui.sh, gui-smoke.sh
and dist.sh with the bundle, uploading the installer, the portable zip
and the test package. A v* tag's release waits for them and attaches
the installer and the portable zip with their .sha256.
# Conflicts:
#	docs/ARCHITECTURE.md
#	scripts/windows/Containerfile
cargo fetch with --target fetched only the crates the Windows target
needs, but cargo metadata reads every package of accesskit-c's lock,
even with --filter-platform. With --offline it then failed on an
empty cargo cache (accesskit_atspi_common and the other AT-SPI
crates), which is what every CI job and every new checkout has.

The crates are now fetched and their licences collected before the
sysroot is unpacked, so gtk.tests.py can stop there
(OGC_GTK_FETCH_ONLY=crates) and check the step against an empty
cargo cache; check-gui.sh runs it with OGC_TEST_NETWORK=1.

The comment on accesskit-c's RUSTFLAGS said Rust's libunwind goes
into the DLL; the DLL imports libunwind.dll, as the docs say.
sources.lock pins GTK's tarball on its own, while msys2.lock's gtk4 is
rewritten by msys2-lock.py. After a relock to a patch release the two
could differ without any check failing: the import and export checks
pass a patch-level mismatch, and the bundle would ship a GTK other
than the one the sysroot's headers and libadwaita come from.

build-gtk.sh now reads gtk4's version from msys2.lock (without epoch
and pkgrel) and refuses a GTK tarball of another version before
downloading it, and checks the built gtk4.pc against it again.
Plan section 1.4 makes AccessKit a release condition, but only
build-gtk.sh checked it, on its own output. check-bundle-windows.sh
now requires the bundle's libgtk-4-1.dll to import accesskit-c's DLL,
which its import walk then requires in bin\, so a bundle made without
bundle-gui.py --gtk, with MSYS2's GTK, fails.

dist.sh copied any OGC_GUI_BUNDLE folder into the installer and the
portable zip without a check; it now runs check-bundle-windows.sh on
the bundle it packs.
The release attaches the setup.exe and the portable zip, which carry
ogc.exe and ogc-background.exe, but waited only for the app's and the
installer's jobs. A tag whose windows-check (clippy, the import check)
or windows-wine-test (the library's and ogc's tests under Wine) failed
still published them. The release now waits for both jobs too.
The self-built libgtk-4-1.dll carried the random work folder in its
__FILE__ strings, libaccesskit-c-0.18.dll the cargo cache in Rust's
panic locations, both below the builder's home, and both a link time
in the PE header. So a shipped DLL named the build machine's user
path, and two builds of one recipe could not be compared.

clang now maps the work folder to /src and the sysroot to /clang64
(-ffile-prefix-map), rustc the work folder and the cargo cache to
/src and /cargo (--remap-path-prefix), and lld writes no time stamp.
The script fails if either DLL still holds the checkout's, the
cache's, the sysroot's or cargo's path. A build in in-container.sh's
image and one in a plain rust:1-trixie container gave byte-identical
DLLs; of the 506 files only accesskit-c's .pc differs, in the order
of its private libraries.
windows-build and windows-gui-test each called build-gtk.sh with an
empty target/gtk, so every run built GTK and accesskit-c twice (about
twelve minutes each) and fetched their sources and crates twice.

A new job, windows-gtk, builds it once after windows-sysroot and keeps
target/gtk/gtk-<hash> in the runner's cache. The cache key is
build-gtk.sh's own recipe hash, which build-gtk.sh --key prints: it
covers the script, the locks, the patches and the tools' versions, so
a new image with another rustc gets a new entry rather than a stale
one. windows-build and windows-gui-test run after it and restore that
build; where they find no cache, build-gtk.sh builds GTK itself.
Note how long the Windows CI jobs take with GTK's own build
All checks were successful
CI / Windows, MSYS2 sysroot (pull_request) Successful in 34s
CI / Windows, command line (clippy with tests) (pull_request) Successful in 39s
CI / Windows, command line (tests under Wine) (pull_request) Successful in 1m29s
CI / Windows, desktop app (clippy, script tests) (pull_request) Successful in 1m51s
CI / Windows, installer (tests under Wine) (pull_request) Successful in 2m30s
CI / Windows, GTK with AccessKit (pull_request) Successful in 2m20s
CI / Format, lint and test (pull_request) Successful in 5m45s
CI / Windows, desktop app (tests under Wine) (pull_request) Successful in 3m17s
CI / Bundle programs, command line and helper (pull_request) Successful in 2m13s
CI / Arch package (pull_request) Successful in 3m16s
CI / Windows, installer and portable zip (pull_request) Successful in 5m31s
CI / Bundle programs, desktop app (pull_request) Successful in 3m3s
CI / Release (pull_request) Has been skipped
CI / Bundle for any distribution (pull_request) Successful in 46s
1228a000d8
Bundle GTK's, libadwaita's and GLib's own translations on Windows
Some checks failed
CI / Format, lint and test (pull_request) Has been cancelled
CI / Windows, desktop app (clippy, script tests) (pull_request) Has been cancelled
CI / Windows, GTK with AccessKit (pull_request) Has been cancelled
CI / Windows, desktop app (tests under Wine) (pull_request) Has been cancelled
CI / Windows, installer (tests under Wine) (pull_request) Has been cancelled
CI / Windows, installer and portable zip (pull_request) Has been cancelled
CI / Arch package (pull_request) Has been cancelled
CI / Bundle programs, command line and helper (pull_request) Has been cancelled
CI / Bundle programs, desktop app (pull_request) Has been cancelled
CI / Bundle for any distribution (pull_request) Has been cancelled
CI / Release (pull_request) Has been cancelled
CI / Windows, MSYS2 sysroot (pull_request) Has been cancelled
CI / Windows, command line (tests under Wine) (pull_request) Has been cancelled
CI / Windows, command line (clippy with tests) (pull_request) Has been cancelled
c75eee6129
The Windows bundle carried no .mo files, so GTK's and libadwaita's own
texts (an entry's Copy and Paste, the about dialog, size units) stayed
English where a Linux system shows them in the user's language. The
bundle now takes the sysroot's gtk40, libadwaita and glib20 catalogs
for the app's languages, and the bundle check requires the German ones.
Join the bundle size's two remarks into one
All checks were successful
CI / Windows, command line (clippy with tests) (pull_request) Successful in 40s
CI / Windows, MSYS2 sysroot (pull_request) Successful in 20s
CI / Windows, command line (tests under Wine) (pull_request) Successful in 1m29s
CI / Windows, desktop app (clippy, script tests) (pull_request) Successful in 1m31s
CI / Windows, GTK with AccessKit (pull_request) Successful in 50s
CI / Windows, installer (tests under Wine) (pull_request) Successful in 2m26s
CI / Windows, desktop app (tests under Wine) (pull_request) Successful in 2m31s
CI / Format, lint and test (pull_request) Successful in 5m34s
CI / Bundle programs, command line and helper (pull_request) Successful in 2m13s
CI / Windows, installer and portable zip (pull_request) Successful in 5m59s
CI / Arch package (pull_request) Successful in 3m8s
CI / Bundle programs, desktop app (pull_request) Successful in 3m16s
CI / Bundle for any distribution (pull_request) Successful in 40s
CI / Release (pull_request) Has been skipped
341f8e2cfa
SkyfaR manually merged commit a467413cd8 into main 2026-10-06 07:43:09 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
LevelXStudios/OpenGameCompressor!33
No description provided.