Skip to content

Feature/aurora build variant - #387

Open
den3000 wants to merge 20 commits into
feature/expect-actual-isolationfrom
feature/aurora-build-variant
Open

Feature/aurora build variant#387
den3000 wants to merge 20 commits into
feature/expect-actual-isolationfrom
feature/aurora-build-variant

Conversation

@den3000

@den3000 den3000 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

den3000 and others added 19 commits July 31, 2026 13:47
The only thing in shared:core that needed Decompose was componentCoroutineScope,
and every one of its callers is a tv component. Moving it into :clients📺core:ui
lets shared:core stop declaring decompose and essenty at all — the tablet graph
was carrying a dependency that has no linux target and that it never used.

The tv modules that call it already depend on :clients📺core:ui and declare
decompose themselves, so only the import line changes there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Koin keeps the parameters of the graph currently being created on a stack and
matches them BY TYPE, ignoring qualifiers. The api clients build their HttpClient
in their constructor, i.e. lazily, inside whichever graph reaches them first — and
if that graph was created as parametersOf(route.event, route.roomName), then
get<String>(named("ApiKey")) hands back the graph's String parameter instead of the
qualified singleton. The request then goes out as

  GET http://localhost/api/v1/users   Authorization: Bearer <room name>

and the poisoned client is cached in the singleton for the rest of the process.

Wrap both values in ApiConfig: a dedicated type cannot collide with the String
parameters of any graph. The tv client shares the same HttpClientProvider, so it
moves over too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add expect painterResource/vectorResource to core:ui with signatures identical to
the compose-resources ones, so switching a call site is a change of import and
nothing else. Android and iOS delegate straight back to compose-resources, so
behaviour on both is unchanged.

The point is the seam: a platform whose resource loader does not handle every
format we ship can now supply its own implementation without touching the eleven
call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aurora packages compose resources flat: <qualifier>/<file> becomes
<qualifier>_<file>, with no package namespace. Several modules shipping an
identically named values/strings.xml therefore collapse into a single
values_strings.commonMain.cvr, and whichever module's Res reaches it first reads
someone else's bytes at its own offsets.

Renaming the files costs nothing anywhere else: the generated Res takes string
keys from <string name="...">, not from the file name, so no call site changes.
core:ui keeps its own name — it is already the only one — and drawable names were
unique across modules to begin with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`-PbuildVariant=aurora` swaps three things at once in settings.gradle.kts: the
plugin versions (the Aurora fork of Compose plus the fork's build/deploy plugins),
the repository list (a local maven fork whose path lives in local.properties), and
the set of included modules — only the tablet, because backend/tv/smsrouter pull
AGP and upstream Compose through build-logic and the two Compose plugins cannot
coexist in one invocation.

Each Aurora module builds from its own build.aurora.gradle.kts (linux targets, fork
dependencies) so the upstream build files stay untouched. The convention plugins
are deliberately not reused there — they are what drags AGP in.

Note the version binding has to sit in pluginManagement { plugins { } }: the
top-level plugins block of a settings script applies to the Settings object, and
declaring compose there actually applies it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither Napier nor compose-ui-tooling-preview publishes linux targets, and both are
referenced from common code. Rather than guard every call site, squat the package:
a source directory carrying the library's own package name, wired in with
kotlin.srcDir from the aurora build file only. Callers and their imports are
untouched, and the upstream builds never see these files.

The Napier stub has to live in exactly one module — two modules declaring the same
package would collide at klib link time. shared:core is that module, and core:domain
and core:ui re-export it with api(project(...)), so the tablet modules get it
transitively.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Linking a Kotlin/Native linux binary does not fit in the default Kotlin daemon heap.
Set it before the first link rather than after the first OOM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four of these are one-liners because the seams were put in earlier: the ktor engine
becomes Curl, getCurrentLanguageCode is hardcoded to ru, and SettingsStore gets an
in-memory map — multiplatform-settings has no linux target, so the selected room does
not survive a restart yet (ak-shared-preferences exists in the fork; TODO on the file).

Two are not:

toLocalisedString is written out by hand. kotlinx-datetime's byUnicodePattern refuses
locale-dependent directives on Kotlin/Native ("The directive 'MMMM' is locale-dependent"),
and delegating to the common toFormattedString throws — which on Aurora is worse than it
sounds, because the fork swallows an exception thrown from a composable and rolls the
frame back, so a crash presents as a screen that simply does not change.

painterResource/vectorResource resolve the drawable bytes themselves and dispatch on the
byte signature: raster magic bytes to a BitmapPainter, a <svg substring to Skia's SVGDOM,
and anything else as Android vector XML through a vendored AOSP parser (Apache 2.0 header
kept) with a hand-rolled DOM, since javax.xml is not available. That path keeps Icon(tint)
and intrinsic dp sizes; the fork's own loader renders SVG only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
isDebug is Platform.isDebugBinary, which the Aurora variant makes false — it links a
release executable. Worth remembering: that same flag also selects API_URL_RELEASE.

TimeReceiver has neither Android's time-change broadcast nor NSTimer to lean on, so it
just ticks a coroutine once a minute into CurrentTimeHolder.

The date and time pickers drop to Material3 directly, since calf publishes no linux
target — on Android and iOS calf draws the same Material3 widgets underneath. Colours
are mapped onto LocalCustomColorsPalette so they match the rest of the app.

With this the aurora variant compiles: -PbuildVariant=aurora
:clients:tablet:composeApp:compileKotlinLinuxX64 is green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main() initialises logging and koin and hands off to application { AppRoot() } — the
fork creates the window itself, so there is no Window or singleWindowApplication here.
The root LocalViewModelStoreOwner comes from AppRoot, since Aurora has no Activity or
ViewController to provide one, and there are no Firebase topics: no push updates.

Linking needs the fork's own flags (-Xoverride-konan-properties, or the link fails
against the sysroot) plus Qt5Network on top of what cmpLinkerOpts already adds, because
the http client is ktor-curl.

Resources need staging. aurora-build packages preparedResources of this module only —
it does not follow project dependencies — and Aurora flattens what it does package into
<qualifier>_<file> with no package namespace. stageAuroraResources copies the
composeResources of all six tablet modules into one directory for customDirectory to
hand over, with duplicatesStrategy = FAIL so a future name clash is a build error rather
than a wrong .cvr read at runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
auroraBuild describes the package itself: app id, the Internet permission, maliit-glib
for the on-screen keyboard, and the four icon sizes. auroraDevices describes where it
goes — host from -P, then local.properties, then a default — and which rpms to pick up.

runReleaseOnDevice and buildReleasePipeline both come from the fork's plugins and are
registered in afterEvaluate, so chaining them needs tasks.matching{}.configureEach;
tasks.named() at that point throws. That lives in aurora-tasks.gradle.kts to keep the
build file about the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things kept the device silent no matter how many log calls the code had.

LoggerInitializer only installs an Antilog when isDebug, and on linux that is
Platform.isDebugBinary — false, because the Aurora variant links a release executable.
Install it directly from the linux entry point instead. (The same flag also picks
API_URL_RELEASE over the debug one, which is worth remembering when a build talks to
the wrong backend.)

Even then the interesting lines were at risk: the Napier stub prints to stdout, and
under journald or a pipe stdout is fully buffered, so whatever was printed right before
a crash never made it out. Flush after every line — fflush(null) from a linux actual,
since the stub itself lives in common code.

With that working, log the two things worth watching from the start: how many organizers
came back from the backend (an empty list is the first symptom of an api client built
with the wrong config) and the raw key events reaching the organizer field, since maliit
input is still an open question. core:ui gains the napier dependency for the view side;
under Aurora it resolves to the stub from shared:core.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The overlay now also shows the window size the system handed over and the density and
font scale it set. On Aurora there is no other way to see what a device actually gave
the app, and the alternative is guessing at scale from screenshots — which is exactly
what cost several rounds on the device.

Float.toString would print "1.4666667" and there is no String.format in common code,
hence the local two-decimal helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
indexSelectRoom is -1 until the room saved in settings turns up in the list that came
back from the backend, and roomList[-1] takes the composable down with it. On Aurora
that reads as "navigation did not work": the fork swallows an exception thrown from a
composable and rolls the frame back, so the screen simply does not change and nothing
lands in the log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The window arrives portrait on every Aurora device seen so far, and the tablet UI is
laid out horizontally, so it gets squashed. ForcedLandscape rotates the content 90° when
the window really is portrait: a landscape-sized box (H×W) centred and rotated about its
own centre ends up exactly W×H. On Android and iOS forceLandscape is false and the whole
thing is a no-op.

It has to wrap all three layers, not just the root: the fork renders Popup and dialog<>
as separate scenes in the untouched window, so nothing applied at the root reaches them.

That makes the organizer dropdown position itself. The popup provider now returns
IntOffset.Zero and the list is placed by hand, opening upward from the field with a gap
measured in px so a substituted density cannot shift it. The popup layer is explicitly
stretched to fill the window — a popup window is sized to its content by default, and the
offset list would fall outside its own window and be clipped, which is exactly what it did
on Android before this line existed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aurora's status bar covers the top of the window. Padding the whole window from the
entry point would put the gap in the portrait coordinate space, and after the rotation
it shows up as a stripe down the side. Applying it inside the rotated content puts it
where it belongs, and painting the background before the padding keeps the strip dark.

Zero on Android and iOS, where systemBarsPadding already handles it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fork builds its Compose scene as ComposeScene(density = Density(window.contentScale)),
and contentScale comes from the system with the window — the app cannot set it. So fix the
dp space instead: ScaledUiDensity substitutes LocalDensity with
short_side_px / uiScaleBaseline, and the layout occupies the same fraction of the screen
whatever scale the system handed over. fontScale is pinned to 1 so the system font scale
does not multiply on top of ours.

Baseline is 800 dp — a 10" tablet, and exactly what the reference Android tablet reports
(2560x1600 at density 2.0). Android and iOS set it to 0, which makes the wrapper a no-op.

Applied in the same three layers as the rotation, for the same reason. The version overlay
stays outside it and gains the computed density, so what the system gave and what we made
of it are both visible at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three small things added up to a visible frame. AppTheme's bare Surface defaulted to
colorScheme.surface, which is lighter than the page background. The sticky-header room
card carried an opaque background about 50dp wider than its own rounded shape, clipping
the slots underneath — the background belongs on the panel, not on the card. And the
release button's Material elevation drew a shadow on top of its own border.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What the variant switch does, what local.properties and Docker it needs, the three
commands worth knowing, and — the part that matters — each Aurora-specific piece with
the reason it exists, since none of them are obvious from the code alone.

The scale section records all three baseline candidates with what each costs, because
the measurements say the devices differ by ~3%, not the 20-30% the first screenshots
suggested: the baseline is buying room for text the fork draws wider, not correcting a
density mismatch. That choice may well be revisited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@den3000
den3000 force-pushed the feature/aurora-build-variant branch from b23540b to b340fc3 Compare July 31, 2026 12:04
The explanations of why the Aurora variant is built the way it is do not belong next to
every line they touch — a per-module build file does not need to re-explain why the
convention plugins are avoided, and the version overlay is scaffolding that comes out once
the scale baseline is settled.

So the code keeps only what cannot be inferred from it (the flat resource packing, which
is the one comment left in a build file), and everything else — the variant switch, the
stubs, linking, packaging, resources, the platform APIs and the scale measurements — moves
into clients/AURORA.md. clients/README.md keeps a short pointer.

The doc also gains an explicit list of what is not finished: keyboard input, in-memory
settings, the hardcoded locale, the naive time ticker, no FCM, no kiosk mode, the wrong
icons, and the unexplained dropdown position.

Co-Authored-By: Claude Opus 5 <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.

1 participant