A project can produce more than one APK

tdep-survey is one checkout with a backend and two independent Android
clients, so a project has to be able to declare two Apk components. It
could not: APK_PATTERNS was anchored at the project root, which reached
the first client and stopped, and package, strip, the variant list and
the download all came from AppEntry::apk_component()'s first match --
correct only while a project produced one APK.

Each component's builds are now found under its own cwd, which is what
cwd already meant everywhere else: the directory the build command runs
in, the subtree staleness is scoped to, a server's WorkingDirectory. A
component that declares none sits at the project root, so nothing about
the single-APK case changes. The alternative -- naming the file on the
component -- would have made a component a file path, and an app being a
project path rather than a file path is this project's central invariant.

Everything derived from an APK follows it onto the component: package,
previousPackage, strip, size, mtime, variants and the rename note, in a
nested `apk` block that a Server simply doesn't have. Two clients install
over different packages, so first-wins would have checked the installed
state of one and reported it as the other's -- which looks exactly like a
correct answer. For the same reason a download naming no component is
refused rather than guessed; naming none still answers for a project with
one, which is what lets the frozen /self/apk keep working.

On the phone the per-device state is keyed by project and component, so
the variant picker moved inside the component's own card, beside the
build it picks, and the installed state, size and icon are each their own
component's. A project building two clients shows no single icon of its
own rather than borrowing the first one's.

Measured against the real thing: pointed at tdep-survey, the two clients
resolve to their own builds (15 MB and 153 MB), strip applies only to the
one that asked for it (153 MB served as 52 MB), pressing Install on one
row installed that row's package and left the other row offering Install,
and a download with no component named answers 400 saying which flag to
pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-31 23:00:32 -04:00
1 parent db47972a25
commit c4e19c2e2b
9 files changed
+760 -309

No files matched your search

+38 -2
View File
@@ -427,10 +427,46 @@ mutable at runtime from the phone.
unaccepted, since taking commits runs git rather than the project's
command, and it is how the new request arrives to be read.
- **Which build variant to serve is the phone's choice, not the server's.**
It arrives as `?variant=` on the download and is validated against the
It arrives as `?variant=` on the download, beside the `?component=` that
says whose build it is, and is validated against *that component's*
discovered builds -- an unvalidated path would let a phone name any file
on disk to be served. Storing it server-side meant one enrolled device
silently changing what another was offered.
silently changing what another was offered. The stored choice is keyed
by project *and* component on the device, so pinning one client to a
release build says nothing about the other.
- **A project can produce more than one APK, and each component's builds
are found under its own `cwd`.** `APK_PATTERNS` is anchored at
`project.join(cwd)` rather than at the project root, which is what
`cwd` already meant everywhere else -- the directory the build command
runs in, the subtree `subtree_head` scopes staleness to, a server's
`WorkingDirectory`. A component that declares none sits at the root,
which is what every single-APK project has always meant, so nothing
about that case changed. tdep-survey is the project that needed it: one
checkout, a backend and *two* independent Android clients, where the
root-anchored patterns reached the first and stopped.
The alternative -- naming the file on the component, `apk: "path"` --
was rejected because it makes a component a file path, and **an app
being a project path rather than a file path** is the invariant at the
top of this document.
Everything downstream is per component in consequence: `package`,
`strip`, the variant list, the size, the mtime and the rename note.
Those were all `apk_component()`'s *first* match before, which was
correct only while a project had one. Two clients install over
different packages and differ in whether their symbols are worth
carrying to a phone, so first-wins would have checked the installed
state of one app and reported it as the other's -- the expensive kind
of wrong, because it looks exactly like an answer.
**A download that names no component is refused, not guessed**
(`ApiError::AmbiguousApk`), for that reason. Naming none still answers
for a project with one, which is nearly all of them and is what lets
the frozen `/self/apk` keep working -- it cannot carry a component, and
the project it describes has a single APK.
The measurement carried across an acceptance is keyed by name *and*
`cwd` (`registry::component_id`): once the directory decides which
builds a component has, a reused name is not the same APK, and handing
it the old one's package would be wrong until that component happened
to be downloaded.
- **The self entry's project is the working directory itself**, so this
server has to be started from the root of its own checkout -- which is
where everything else here is driven from, and what the service unit