Packaging invoked `npx --yes electron-builder@26.15.5`, which pinned only
the top-level version: the ~275 transitive packages were re-resolved from
the registry on every build, with no lock state to check them against and
with install scripts enabled. That runs on the machine holding the
code-signing certificates and notarization credentials, so a compromise
anywhere in that tree executed with those.
The plugin now embeds a package.json / package-lock.json pair pinning 277
packages (all from registry.npmjs.org, each with a sha512 integrity hash),
stages it into a build-local directory, installs with
`npm ci --ignore-scripts`, and runs the CLI directly through
`node node_modules/electron-builder/cli.js`. A tarball whose bytes do not
match the recorded hash now fails the build (EINTEGRITY), and no
pre/postinstall hook from that tree runs at all.
node and npm still come from the build machine; only the dependency tree is
pinned. electron-builder's own run-time downloads (app-builder, 7-Zip, NSIS,
snap/AppImage templates) remain outside any npm lock file — they are
verified by its own checksums.
Also in this change:
- NodeJsDetector.detectNpm(), plus a shared detectSibling() helper for the
nodePath override; the task now requires both node and npm and says
what each is for when one is missing.
- MacDmgLzma.locateAppBuilder searched the npx cache, which no longer
exists, and never looked in ELECTRON_BUILDER_CACHE where
electron-builder 26 actually downloads app-builder. It now checks that
first, so the differential-update blockmap is no longer silently
dropped after the DMG is recompressed with LZMA.
- scripts/update-electron-builder-lock.sh regenerates the lock file,
resolving against the public registry explicitly and refusing to run
when certificate verification has been switched off — a lock file
resolved over an unverified connection would record hashes for bytes
nothing authenticated.
- ElectronBuilderToolchainLockTest guards the boundary offline: version
pinned in step with the Kotlin constant, lockfileVersion 3, every
package from the public registry, every one carrying a sha512 hash.
Verified end to end against a minimal consumer project (TargetFormat.Zip):
the nominal path provisions the toolchain and produces the signed .app zip
plus its blockmap, and tampering with a single integrity hash in the
embedded lock file fails the build as intended.
Apply kotlinx binary-compatibility-validator and Kotlin explicitApi() at the
root for every non-example module (same harness as decorated-window-tao).
Commit api/*.api baselines, wire apiCheck into preMerge, and document the
workflow. decorated-window-jewel keeps explicitApi but is skipped by BCV until
ASM can read JVM 25 class files.
Reproduces electron-updater's differential download on the JVM. The updater
matches the block map of the artifact it is about to fetch against the one it
already has, copies the unchanged blocks from disk, and fetches the rest with
HTTP range requests — using the `.blockmap` files the plugin already publishes,
so nothing changes at build time and no per-version delta files are produced.
Blocks are matched by content, so an update from 1.0 straight to 1.7 is
differential too. Every failure mode — no block map published, a host that
ignores Range, a stale cache, a digest mismatch on the assembled file — falls
back to a full download, and the result is validated against the manifest
SHA-512 before it reaches the installer.
AppImages update differentially from the first update, since the running
executable is itself the previous artifact. Other formats need the previous
artifact, which every download now leaves hard-linked in the update cache, so
they benefit from the second update on.
Measured on real NSIS installers of the sample apps, two builds apart:
JVM, no ProGuard 101.3 MB -> 1.4 MB (1.4 %)
JVM, ProGuard optimize + obfuscate 47.4 MB -> 13.8 MB (29.1 %)
GraalVM native image, -O3 46.2 MB -> 32.5 MB (70.3 %)
The bundled Java runtime deltas perfectly; obfuscation and native-image layout
are what invalidate blocks wholesale.
The Texture tab was added to the two UIA goldens (*.expected.json) but not
to their AT-SPI counterparts (*.atspi.json), whose `tabs` list also feeds
verify-atspi-tree-golden.py's --strict-extra allowlist. Windows stayed
green while both Linux golden fixtures failed with
"EXTRA unlisted interactive 'Texture' role=page tab".
TextureView in a Windows tray panel (TaoStandalonePopupHost) rendered a
permanently empty Box: that host was never given the plumbing its macOS and
Linux twins got, so LocalTaoPopupHostWindows was null inside its scene. It
cannot implement TaoPopupHostWindows either — it is not a popup host.
Introduce TaoWindowsTextureHost, the narrow Windows counterpart of
TaoGlTextureHost / TaoMetalTextureHost (host HWND, Skia context, requestRedraw),
provided by the window scene and by the tray panel; WindowsTextureView consumes
it instead of the popup host. Windows popup layers replay their parent's locals,
so they inherit it and keep sharing the host scene's context, as before.
The import registry now keys on the Skia context rather than the parent HWND.
Every Windows surface shares one ANGLE EGLContext but not one DirectContext:
the tray panel builds its own and has no HWND of its own, so an hwnd-keyed
registry would have handed it a window's image (hwnd 0 for both) and Skia would
have silently dropped the draw. Panels also release their imports before closing
their context, like the Linux surfaces.
Also:
- A skipped keyed-mutex copy now schedules a retry redraw. Nothing else would
invalidate — markFrameAvailable for that stamp already fired and the producer
may go idle — so one contended copy froze the view on the previous frame. The
import-time priming copy gets the same timeout as the per-frame path instead
of 0, for the same reason.
- The per-controller stamp maps (Windows 'copied', macOS 'consumed') hold weak
keys. An import outlives any single view, so strong keys pinned the controller
of every view that ever drew through it; a screen creating one controller per
item over a shared source grew them without bound.
- DmaBufTestTextureProducer.source is documented as invalid after close(): the
fd it carries is closed there and the OS may recycle it.
- nucleus_tao_texture.c releases its d3d11.dll module handle on every path.
- Drop a stale comment claiming there is no Linux TextureView producer.
The tab bar gained a Texture entry with this PR, so the AT-SPI/UIA golden
tree verifiers flagged it as an unlisted interactive node (pre-merge's
tao-a11y job, already red on the previous commit). Adds it to both golden
tab lists and to the per-tab tables of the all-tabs probes; the asserted
strings are the contentScale/filterQuality labels, the only ones identical
on every platform — the producer summary line differs, and Linux has no
producer at all.
Fixes the `tao-a11y` CI flake — `[FAIL] Increment carries the 'focused'
state (got [])` in `verify-atspi-keyboard.py`. It predates this branch and
also hits `main` (runs 30905167204, 30835144173, 30795312700 all failed on
this one assertion, ~1 in 3).
Root cause is in the log the failing job dumps, not in the probe:
Exception in thread "Thread-39" java.lang.IllegalArgumentException:
Detected multithreaded access to SnapshotStateObserver:
previousThreadId=1), currentThread={id=114, name=Thread-39}
at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads
at androidx.compose.foundation.FocusableNode.retrievePinnableContainer
at androidx.compose.foundation.FocusableNode.onFocusStateChange
at androidx.compose.ui.focus.FocusTransactionsKt.performRequestFocus
at androidx.compose.foundation.FocusableNode$applySemantics$1.invoke
`onActionInvoked` ran the handler lambdas on whatever thread called in, and
AccessKit calls in from its own worker — instrumenting the upcall shows a
different one every time (Thread-7 … Thread-11 locally, Thread-39 on the
runner) while the Tao main thread is `main`/id=1. Plain state writes survive
that, which is why click and increment always passed, but
`SemanticsActions.RequestFocus` walks Compose's focus machinery into
`observeReads`, whose SnapshotStateObserver belongs to the main thread. When
the main thread happens to be observing at that moment the check throws, the
focus transaction aborts half-applied, and no node ends up focused at all —
exactly what the assertion reports. On an idle desktop the collision window
is narrow (the assertion passes 3/3 locally on the pre-fix build); a loaded
CI runner hits it regularly.
- `withHandlersOnMainThread` marshals every a11y action onto
`TaoMainDispatcher` — inline when already on the Tao main thread (macOS AX,
where nothing changes), posted plus a redraw wake otherwise, since the
dispatcher only drains on `MAIN_EVENTS_CLEARED` and `pump()` sends the
apply notifications itself. All handlers, not just focus: Compose UI's
contract is single-threaded for every one of these paths.
- `verify-atspi-keyboard.py` polls for the 'focused' state instead of reading
it once after a fixed 400 ms sleep. Not the root cause, but that chain
(D-Bus round trip → Compose focus request → next frame → AccessKit push)
is asynchronous and the marshalling adds one loop tick to it; every other
timing-sensitive assertion in the probe already polls.
Verified on a GNOME/XWayland session: all four AT-SPI probes green
(`verify-atspi`, `-events`, `-all-tabs`, `-keyboard` — including the flaky
assertion, `focused accessibles: ['Increment']`) with zero multithreaded-access
exceptions in the app log.
Replace hand-rolled nucleus_tao_a11y COM projector with AccessKit on Windows,
share wire/decode path with Linux, and add golden tree + keyboard/event CI probes.
Surfaced by the advanced a11y probe: Compose CustomAccessibilityActions were
reachable from VoiceOver (macOS rotor) but invisible to Orca — the vendored
accesskit_atspi_common only exposed the synthetic 'click' action. n_actions/
get_action_name/do_action now append the node's custom actions after the
click slot; do_action routes them as Action::CustomAction with the per-node
index, which the embedder side (linux/a11y.rs) already dispatches to
Kotlin's customActions list. Linux natives rebuilt by CI.
Probes now assert the advanced A11yTab sections, not just the basics:
- disabled button: role survives AND the disabled state projects
(AX: AXButton + AXEnabled=false; UIA: IsEnabled=false; AT-SPI: no
STATE_ENABLED)
- custom actions: labels ride the a11y channel and are performable
(AX action names; AT-SPI doAction('Mark as read') round-trip -> counter)
- live region: pressing 'Update status' surfaces the new status text
- toggleable without an explicit role: STATE_CHECKABLE / TogglePattern
- heading role (AT-SPI)
- slider Value interface set/read (AT-SPI)
Also fixes a REAL a11y bug the new assertions caught: A11yButton's disabled
branch used semantics{} without mergeDescendants (unlike the enabled
clickable path), so the label stayed a separate enabled static-text node and
the disabled button projected unlabeled — VoiceOver read 'Cannot press' as
plain active text. Local AX probe: 7/7 PASS.
Mirrors the macOS TCC self-skip: on hosted runners the software display
adapter yields no WGL context, the scene never attaches and only the OS
nonclient elements exist — nothing assertable, loud SKIPPED, exit 0. Any
partial exposure with failing assertions still fails hard, and the job is
gating again (continue-on-error removed).
FindAll uses the filtered control view, which never descended into the
custom UIA provider on the runner (only the 7 nonclient elements showed).
scripts/dump-uia-tree.ps1 — validated during manual provider bring-up —
walks TreeWalker.RawViewWalker; the probe now does the same, and dumps the
raw tree on failure.
- test-graalvm/tao-native-tests: execute the binary from graalvm/output
(the packaging step extracts skiko/jawt/icudtl next to it; the bare
nativeCompile artifact lacks them — Windows failed unpacking skiko).
- verify-uia.ps1: dump the UIA descendant names when the A11y content is
missing, and try invoking the 'A11y' tab through UIA as a fallback.
The prebuild step starts the Gradle daemon without NUCLEUS_DEMO_TAB; the
launch step then reuses that daemon and JavaExec inherits ITS environment,
so the app could open on the Demo tab instead of A11y (observed on the
Windows runner). --no-daemon makes the app JVM inherit the step env
directly. The UIA probe also fails cleanly now instead of null-crashing
when an element is missing.
All three first-run failures were the same race: the probe's 120s window was
consumed by the cold gradle compile of tao-demo on a fresh runner. Build the
jar in a prior step and give the probes 300s.
GraalVM native-image testing (examples/tao-native-test):
- New runner app (nucleus plugin, graalvm enabled) that compiles the whole
Tao test pyramid into a native binary and RUNS it — the first place a
produced native image is actually executed in CI (test-graalvm.yaml only
compiled nucleus-demo before). Consumes decorated-window-tao's compiled
test classes via a taoTestArtifacts configuration.
- TaoSceneTestBattery: generated reflection-free registry of the stage-1
offscreen suite (60 cases) — JUnit discovery needs reflection metadata,
direct calls need none.
- Two process modes (battery / headful): the offscreen battery's global
Compose runtime state prevents the Tao loop from starting in the same
process, so CI compiles once and runs the binary once per mode.
- New tao-native-tests job in test-graalvm.yaml (3-OS matrix, Xvfb+openbox
on Linux). Local macOS: battery 60/60 and headful 6/6 with native=true.
Native accessibility protocol (scripts/ci/ + tao-a11y job in pre-merge.yaml):
- Verifies the OS-level a11y projection from the OUTSIDE, via the same
client APIs assistive tech uses, against tao-demo's A11y tab
(NUCLEUS_DEMO_TAB env added to tao-demo for direct navigation):
- Windows (hard gate): verify-uia.ps1 — UIA tree exposure, InvokePattern
round-trip (Increment -> 'click counter 1'), TogglePattern, RangeValue.
Derived from scripts/dump-uia-tree.ps1 / uia-action.ps1.
- Linux (hard gate): verify-atspi.py — pyatspi walk over the AT-SPI bus
(org.a11y.Status.IsEnabled flipped like scripts/a11y-linux-iter.sh),
element exposure + doAction round-trip.
- macOS: verify-ax.swift — AX client dump (axdump/axtool derived);
self-skips on runners without the TCC Accessibility permission.
Verified locally: 4/4 elements exposed, 53 named accessibles.
- REFACTOR_VERIFICATION.md: invariants + per-phase test procedure for the
JetBrains-like package reorganization (BCV ABI freeze, JNI/FQN coupling
checker, consumer-compile gate, runtime smoke, CI matrix gate)
- scripts/check-tao-refactor-invariants.sh: cross-checks compiled classes
against JNI symbols (.rs/.c/.m), macOS dylib exports, FindClass literals,
Class.forName, META-INF/services, ProGuard rules and GraalVM metadata
- binary-compatibility-validator applied to decorated-window-tao with the
baseline API dump committed under decorated-window-tao/api/
Move all top-level demo/sample modules into a single examples/ directory with
uniform <topic>-demo naming (example → nucleus-demo, sample-tao → tao-demo,
jewel-sample → jewel-demo, etc.), and update every CI workflow, script, and doc
that referenced the old Gradle paths.
Add examples/compose-demo: a port of bitsycore/compose-desktop-native's demo
running on the Tao backend with Material 3 decorated windows, FileKit file
dialogs, Navigation 3, and GraalVM native-image config. Screens relying on
Material 3 Expressive (Compose 1.12) are omitted to stay on the repo's 1.11.x.