Give each component build modes and a settings sheet
A component now declares its ways of being built in one list -- `modes: ["release", "debug"]`, the first the default -- and every command it runs is handed the mode as its last argument, so a project whose script takes `release` or `debug` names that script once. A field may instead be written per mode, which is the escape hatch for the commands that cannot take the word: cargo takes `--release` or nothing, and its profile for the unoptimised build is called `dev` while the directory it writes is called `debug`, so no single word serves as both the flag and the path. A command written per mode is not handed the word as well; it already is the answer, and a stray argument to a service binary is a process that will not start. One list rather than gathering names from whichever fields happened to mention them is what makes a mode missing from one part unsayable: every per-mode map is checked against it, so a gap is named rather than resolved to some other mode's command. Which mode to build in and whether to strip are the build machine's -- there is one checkout and one set of outputs, so a per-device mode would have two phones rebuilding over each other silently. Which of the finished builds a phone installs stays that phone's. Neither choice is part of the acceptance gate, so both have to be carried across the components list being rewritten, by acceptance and by the self entry's startup reconciliation alike; without the second a mode chosen for this server's own component would not survive the restart that applies it. A mode switch moves no commit, so `builtMode` is recorded beside `builtFrom`. Without it a component built in debug and switched to release reads as current and serves the debug build for ever -- and for an APK nothing else notices, because the "never built at all" check finds any variant under the component's directory. Each component card gains a settings sheet behind a gear at the row's right-hand end, holding the mode, which build to install, strip, and an Enrol button. The variant picker moved into it: on the card the two read as one choice, both saying debug and release, and they are not. The row's text is now bounded and truncates, so a long status can no longer push the log and settings buttons off the edge. `enroll:` is a declared command whose one line of stdout is a URL for the phone to open after installing -- generic on purpose, run per press since such a link is one-shot and carries a credential, and in the acceptance gate because it runs on the build machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b7164bb30
commit
17d873ab7c
16 files changed
+2225
-220
No files matched your search
@@ -36,7 +36,9 @@ mutable at runtime from the phone.
|
||||
list). `discover.rs` is the scanner,
|
||||
`config.rs` the persisted schema and the RON both config files are in,
|
||||
`apkinfo.rs` the `aapt2` reads,
|
||||
`strip.rs` the slim-APK pipeline, `sdk.rs` the SDK/NDK tool lookups.
|
||||
`strip.rs` the slim-APK pipeline, `sdk.rs` the SDK/NDK tool lookups,
|
||||
`script.rs` the one runner for a project's short declared commands
|
||||
(`resources:`, `enroll:`) -- stdin closed, a deadline, stdout kept.
|
||||
- `app/` — Kotlin + Compose, a single `:androidApp` module. `UpdaterScreen.kt`
|
||||
is the list, `AddAppScreen.kt` the add/settings screen, `AppsApi.kt` the
|
||||
management calls, `UpdateManifest.kt` the read side, `ApkInstaller.kt` /
|
||||
@@ -618,6 +620,108 @@ mutable at runtime from the phone.
|
||||
the declaration build anyway. Pulling stays allowed while a request is
|
||||
unaccepted, since taking commits runs git rather than the project's
|
||||
command, and it is how the new request arrives to be read.
|
||||
- **A component declares its build modes in one list, and that list is the
|
||||
only place a mode is declared.** `modes: ["release", "debug"]` on the
|
||||
component; the first is the default, which is why a project puts the one
|
||||
it wants built by default first. Every command the component runs is
|
||||
then handed the mode **as its last argument** -- so a project whose
|
||||
script takes `release` or `debug` names that script once and nothing is
|
||||
written twice. The escape hatch is writing a field *per mode*
|
||||
(`build: {"release": ..., "debug": ...}`, `service: Managed({...})`),
|
||||
and it exists because some commands cannot take the word: cargo takes
|
||||
`--release` or nothing, and its profile for the unoptimised build is
|
||||
called `dev` while the directory it writes is called `debug`, so no
|
||||
single word serves as both the flag and the path. Gradle is the same
|
||||
shape (`assembleRelease` needs capitalising).
|
||||
**A command written per mode is not handed the mode as well** -- it
|
||||
already is the answer, and appending the word would pass a stray
|
||||
argument to a binary that never asked for one, which for a service is a
|
||||
process that will not start. `ByMode::mode_argument` is the one place
|
||||
that rule lives.
|
||||
Having *one* list is what makes "this part has a mode the other part
|
||||
never heard of" unsayable rather than something to detect: every
|
||||
per-mode map is checked against it (`config::mode_problem`), so a
|
||||
missing entry is named as missing instead of resolving to some other
|
||||
mode's command. A map on a component that declares no modes is the same
|
||||
mistake from the other side and is refused with a message saying what
|
||||
to write. A declaration that fails the check is discarded exactly as an
|
||||
unknown field is, and the card says so.
|
||||
**`ByMode` holds a `Command` and nothing else, and that is load-bearing
|
||||
rather than incidental.** Telling the single form from the map needs
|
||||
`deserialize_any`, and RON reports a struct `(path: "a")` as a *map* --
|
||||
so a struct-valued field behind it would read its own field names as
|
||||
mode names. Worse, measured: RON **discards the variant name** under
|
||||
`deserialize_any`, so `Cargo("server")` and the array command
|
||||
`["server"]` arrive as the same one-element sequence. That is why the
|
||||
by-mode map lives *inside* `Service` rather than around it, and why a
|
||||
`Cargo(...)`-style shorthand cannot be added to `build:` beside the
|
||||
bare-string form without a format migration.
|
||||
|
||||
- **Which mode to build in, and whether to strip, are the build
|
||||
machine's choices; which finished build to install is the phone's.**
|
||||
There is one checkout and one set of outputs, so a per-device mode
|
||||
would have two enrolled phones rebuilding over each other with nothing
|
||||
on either screen to say why -- the settings sheet says as much out
|
||||
loud. They are stored on the component in `config.ron` (`mode`,
|
||||
`stripHere`), set through `PUT /apps/{key}/components/{name}/settings`,
|
||||
and excluded from `Component::same_declaration`: choosing one of the
|
||||
declared modes is not the project asking for something new, and
|
||||
including it would make every settings change re-open the acceptance
|
||||
gate. Which means both halves of the carry-across have to be written --
|
||||
`registry::chosen_settings`/`restore_chosen_settings`, called by
|
||||
`approve_declaration` **and** `reconcile_self`. Without the second, a
|
||||
mode chosen for this server's own component would last until the next
|
||||
restart, and restarting is how this server is updated.
|
||||
`strip` stays declarable by the project and `stripHere` overrides it,
|
||||
rather than being copied in at acceptance: copied, a later change to
|
||||
the declaration would be ignored on every machine, silently.
|
||||
**`BuildState::matches` compares the effective mode as well as the
|
||||
declaration**, because that state holds a *snapshot* of the components
|
||||
it builds from -- reused across a mode change it would go on running
|
||||
the old mode's command from a card reporting the new one.
|
||||
|
||||
- **A mode switch moves no commit, so `builtMode` is recorded beside
|
||||
`builtFrom`.** Without it a component built in `debug` and switched to
|
||||
`release` reads as current, offers nothing to press, and serves the
|
||||
debug build for ever -- and for an `Apk` nothing else would notice,
|
||||
because the "never built at all" check finds *any* variant under the
|
||||
component's directory, so the debug APK sitting there is enough to
|
||||
satisfy it. `Component::built_in_another_mode` answers it and both
|
||||
`freshness` and `component_is_stale` ask, the first reporting `Behind`
|
||||
*before* consulting the checkout at all: a clean tree at the very
|
||||
commit the debug build was made from still does not make that build a
|
||||
release one. It stays quiet until this server has built the component
|
||||
once, so a project built by hand is not told it is out of date.
|
||||
|
||||
- **Each component card has a settings sheet, and the variant picker
|
||||
lives in it.** The gear sits at the row's right-hand end beside the log
|
||||
button, drawn unconditionally so its presence is never the signal. The
|
||||
sheet holds the build mode, which of the discovered builds this phone
|
||||
installs, whether to strip, and Enrol; a section with nothing to offer
|
||||
says so rather than vanishing, since "this project declares one way of
|
||||
building" and "we could not tell" must not look alike. The variant
|
||||
picker moved here from the foot of the card because on the card the two
|
||||
read as one choice -- both say `debug` and `release` -- and they are
|
||||
not: one decides what the build machine *builds*, the other which
|
||||
finished build this phone takes. Everything applies on Save except
|
||||
Enrol, which is an action and happens on the press.
|
||||
**The row's text is inside one weighted child so it can never push the
|
||||
controls off the edge**, and every reading in it truncates with an
|
||||
ellipsis. A control that leaves because the text grew is one the reader
|
||||
cannot get back to.
|
||||
|
||||
- **`enroll:` is a command whose one line of stdout is a URL for the
|
||||
phone to open after installing.** Deliberately "a URL to open" rather
|
||||
than anything named after enrolment -- a route that knew what enrolling
|
||||
was would be a special case of itself. Run **per press**
|
||||
(`POST /apps/{key}/components/{name}/enroll-link`), never cached: the
|
||||
link a project mints is ordinarily one-shot and carries a credential,
|
||||
so a stored one would be both stale and a secret sitting in a file. In
|
||||
the acceptance gate like `build`, because it runs on the build machine.
|
||||
`crate::script::capture` is the shared runner it and `resources` both
|
||||
use -- stdin closed, a deadline, stdout kept, and a failure naming the
|
||||
command and the first line of its stderr.
|
||||
|
||||
- **Which build variant to serve is the phone's choice, not the server's.**
|
||||
It arrives as `?variant=` on the download, beside the `?component=` that
|
||||
says whose build it is, and is validated against *that component's*
|
||||
|
||||
Reference in new issue
Block a user