Build one component instead of every one with a command

Pressing Update on one client of a multi-client project ran every
component's build to get the one that was actually asked for -- cheap
while a project had one APK, expensive the moment it had two and one of
them was slow (an ARM cross-compile, say). /prepare and /build now take
?component= to restrict a run to one named component; absent still means
the whole project, which is what Pull & Build, the project-row Rebuild,
and every single-component project keep doing. There is deliberately no
component-scoped Rebuild -- forcing one component's build without
touching the rest happens by pressing Update on it, which now runs
/prepare scoped to that component.

Verified against a live server driving a scratch two-Apk project:
building one component leaves the other's marker untouched, an unknown
?component= answers 404 naming the project and the component, and naming
none still builds both.

That verification surfaced a second, sharper bug the scoping change had
not caused but did make newly visible: component_is_stale's "never built
at all" check still asked find_apks of the whole project root, left
behind when per-component discovery (c4e19c2) moved everywhere else to
each component's own directory. A project with two Apk components has
one's output sitting under the root-anchored patterns too, so the moment
either component had ever been built, the whole project read as
"something is built here" -- and the other, never built, silently stopped
being offered its own first build. /prepare saw a component with a
command and no output and declared it current. Fixed by scoping the same
check to the component's own directory, guarded to Apk components only:
a Server never has an APK to find under its directory by definition, and
asking would have reported every server "never built" forever, which
broke two existing tests before the guard was added. Component::dir is
now the one definition of what a component's directory is, used by the
build command, the staleness check, and discovery alike.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Sonnet 5 committed 2026-09-01 00:28:36 -04:00
1 parent c4e19c2e2b
commit 7eaf79370e
7 files changed
+326 -49

No files matched your search

@@ -1,5 +1,6 @@
package com.example.devupdater
import java.net.URLEncoder
import org.json.JSONObject
// The three routes that act on the *build machine* rather than this
@@ -112,14 +113,32 @@ private fun requestBuildStatus(path: String, method: String): BuildStatus =
fun pullAndBuild(key: String, force: Boolean = false): BuildStatus =
requestBuildStatus("/apps/$key/pull" + if (force) "?force=true" else "", "POST")
fun prepareBuild(key: String): BuildStatus = requestBuildStatus("/apps/$key/prepare", "POST")
/**
* Builds one component if its staleness rule says so, before it is downloaded.
*
* Scoped to that one component rather than the whole project: a project with more than one -- two
* independent Android clients sharing a checkout, say -- would otherwise pay for every component's
* build to get the one that was actually pressed, which for a slow one is the whole complaint. The
* server still only runs a component's command when *that* component is behind, same as before;
* naming it just keeps the others out of the run entirely.
*/
fun prepareBuild(key: String, component: String): BuildStatus =
requestBuildStatus(
"/apps/$key/prepare?component=${URLEncoder.encode(component, "UTF-8")}",
"POST",
)
/**
* Builds because the person asked, not because anything looked stale.
* Builds every component, because the person asked, not because anything looked stale.
*
* The staleness rules keep a download from rebuilding the world; they have no business overruling a
* button. This is also the only way a project already current with its checkout ever records what
* it was built from, which is what the "out of date" signal is compared against.
*
* Whole-project on purpose, unlike [prepareBuild]: this is the project row's Rebuild, which sits
* beside Pull & Build and means the same "the whole checkout" that one does. Forcing just one
* component's build without touching the rest happens by pressing Update on that component, which
* runs [prepareBuild] scoped to it -- there is no second, component-scoped Rebuild.
*/
fun buildNow(key: String): BuildStatus = requestBuildStatus("/apps/$key/build", "POST")
@@ -720,7 +720,7 @@ private fun AppListScreen(
if (entry.needsBuild) {
cardStates = cardStates + (entry.key to CardState.Preparing(null))
try {
var status = withContext(Dispatchers.IO) { prepareBuild(entry.key) }
var status = withContext(Dispatchers.IO) { prepareBuild(entry.key, component) }
while (status.building) {
cardStates = cardStates + (entry.key to CardState.Preparing(status))
delay(BUILD_POLL_INTERVAL_MS)