A Rebuild on every component, and the keystore AGP actually uses
Two things, both from the same stuck download. Each component row now carries its own Rebuild, which is /build scoped to it -- the route already took ?component=, only the button was missing. Update builds only what the staleness rules call behind, and those rules cannot see a command reading undeclared files, an output changed underneath this server, or a signing key replaced since the APK was made; in all of those Update does nothing and the only force was a project-wide Rebuild that rebuilds every sibling. Same action at two scales, so it takes the project Rebuild's word and colour. hasBuild is new on the manifest component so a component with no command of its own draws none, which the project-level needsBuild could not answer. And debug_keystore_path() now resolves the preferences directory the way AGP does instead of assuming ~/.android: ANDROID_USER_HOME as-is, then ANDROID_PREFS_ROOT and ANDROID_SDK_HOME with .android appended, then $XDG_CONFIG_HOME/.android when that directory exists, then $HOME/.android. Measured by running real builds against AGP 32.3.2, including the existence test on the XDG step, which the documentation does not mention. An ordinary desktop machine with XDG_CONFIG_HOME set signs its APKs with a keystore this server never looked at, so it reported "no debug keystore" about a machine that had one and was using it -- and this VM sets no XDG_CONFIG_HOME, which is why it could not happen here. The failure now names a keystore found further down the list, since an inert one and an absent one are otherwise identical from a phone. Verified on the emulator (component Rebuild: press, that component's own progress, back to resting with no sibling moving) and against a throwaway server for both keystore branches. Reported by the tdep-survey session, which found the two keystores on the serving host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b67c70fa2f
commit
c7b3af4861
6 files changed
+358
-72
No files matched your search
@@ -141,11 +141,19 @@ fun prepareBuild(key: String, component: String): BuildStatus =
|
||||
* 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.
|
||||
* [component] is one component's own Rebuild; null is the project row's, which means the whole
|
||||
* checkout the way Pull & Build does. Both exist because a component can need building with nothing
|
||||
* to press: staleness is decided by commits under the paths a component declares, so a build that
|
||||
* reads something nobody declared, an output changed underneath this server, or a signing key
|
||||
* replaced since the APK was made leaves every rule saying "current" -- and then Update, which runs
|
||||
* [prepareBuild], does nothing, and the project-wide Rebuild is a heavy way to get one component
|
||||
* built.
|
||||
*/
|
||||
fun buildNow(key: String): BuildStatus = requestBuildStatus("/apps/$key/build", "POST")
|
||||
fun buildNow(key: String, component: String? = null): BuildStatus =
|
||||
requestBuildStatus(
|
||||
"/apps/$key/build" +
|
||||
(component?.let { "?component=${URLEncoder.encode(it, "UTF-8")}" } ?: ""),
|
||||
"POST",
|
||||
)
|
||||
|
||||
fun buildStatus(key: String): BuildStatus = requestBuildStatus("/apps/$key/status", "GET")
|
||||
@@ -110,6 +110,12 @@ data class ProjectComponent(
|
||||
// The last build stopped at this component, which is the one case
|
||||
// where its build log matters more than what it is doing now.
|
||||
val buildFailed: Boolean,
|
||||
// This component has a command and is allowed to run it, so its row
|
||||
// offers to build this one alone. Per component rather than the
|
||||
// project's own needsBuild, which is the OR of every component's: on
|
||||
// a project where only one of two builds, that one would draw the
|
||||
// button on both rows and the other would do nothing when pressed.
|
||||
val hasBuild: Boolean,
|
||||
// Where Uninstall's "remove data" and "remove config" would delete,
|
||||
// and whether anything is there. Null for an APK, which keeps nothing
|
||||
// on the build machine.
|
||||
@@ -348,6 +354,7 @@ private fun readEntry(entry: JSONObject): ManifestEntry {
|
||||
hasBuildLogs = component.optBoolean("hasBuildLogs", false),
|
||||
hasRuntimeLogs = component.optBoolean("hasRuntimeLogs", false),
|
||||
buildFailed = component.optBoolean("buildFailed", false),
|
||||
hasBuild = component.optBoolean("hasBuild", false),
|
||||
dataPath = component.optString("dataPath").ifEmpty { null },
|
||||
configPath = component.optString("configPath").ifEmpty { null },
|
||||
dataPresent = component.optBoolean("dataPresent", false),
|
||||
|
||||
@@ -757,6 +757,45 @@ private fun AppListScreen(
|
||||
context.startActivity(installApkIntent(context, file))
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-reads one entry once a build has finished, whatever asked for the build.
|
||||
*
|
||||
* The APK's mtime is what decides "update available", so the list has to come from the server
|
||||
* again rather than be guessed at here.
|
||||
*
|
||||
* Retried, because building this server's own project rebuilds its binary and restarts it: the
|
||||
* socket goes away for a moment, and reporting that as a failure would be telling somebody
|
||||
* their update broke at the moment it worked.
|
||||
*
|
||||
* The card stays marked as being worked on until that entry lands, which is a second or several
|
||||
* while this server restarts — [settle] is what clears it, and runs only once there is a fresh
|
||||
* entry to clear it against. Clearing at the end of the build instead left the card reading as
|
||||
* idle against a list still describing the state before it.
|
||||
*/
|
||||
suspend fun readBackAfterBuilding(entry: ManifestEntry, settle: () -> Unit) {
|
||||
repeat(REFRESH_ATTEMPTS_AFTER_PULL) { attempt ->
|
||||
try {
|
||||
applyOne(entry.key)
|
||||
settle()
|
||||
// Building this app's own project is what produces the
|
||||
// newer copy of it, and restarts the server it has to
|
||||
// keep talking to. So this is the moment to offer it,
|
||||
// rather than at some later launch.
|
||||
if (entry.builtIn) {
|
||||
onOwnProjectBuilt()
|
||||
}
|
||||
return
|
||||
} catch (e: DownloadServerException) {
|
||||
if (attempt == REFRESH_ATTEMPTS_AFTER_PULL - 1) {
|
||||
settle()
|
||||
failure(e)?.let { manifestState = ManifestState.Error(it) }
|
||||
} else {
|
||||
delay(RESTART_WAIT_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Pull acts on the build machine: fetch, fast-forward, rebuild. */
|
||||
/**
|
||||
* Runs a build on the build machine and follows it to the end, whichever button asked for it.
|
||||
@@ -802,45 +841,12 @@ private fun AppListScreen(
|
||||
setComponent(entry.key, component.name, ComponentState.Error(it))
|
||||
}
|
||||
}
|
||||
// The APK's mtime is what decides "update available", so the
|
||||
// list has to come from the server again rather than be
|
||||
// guessed at here.
|
||||
//
|
||||
// Retried, because building this server's own project rebuilds
|
||||
// its binary and restarts it: the socket goes away for a
|
||||
// moment, and reporting that as a failure would be telling
|
||||
// somebody their update broke at the moment it worked.
|
||||
//
|
||||
// The card stays marked as being worked on until that entry
|
||||
// lands, which is a second or several while this server
|
||||
// restarts. Clearing it at the end of the build instead left
|
||||
// the card reading as idle against a list still describing the
|
||||
// state before it -- the same wrong group, arrived at from the
|
||||
// other side.
|
||||
repeat(REFRESH_ATTEMPTS_AFTER_PULL) { attempt ->
|
||||
try {
|
||||
applyOne(entry.key)
|
||||
// The project's own state only. A component that
|
||||
// failed inside this run has just been given its own,
|
||||
// and clearing that here would take away the only
|
||||
// thing saying so.
|
||||
setProject(entry.key, null)
|
||||
// Building this app's own project is what produces the
|
||||
// newer copy of it, and restarts the server it has to
|
||||
// keep talking to. So this is the moment to offer it,
|
||||
// rather than at some later launch.
|
||||
if (entry.builtIn) {
|
||||
onOwnProjectBuilt()
|
||||
}
|
||||
return
|
||||
} catch (e: DownloadServerException) {
|
||||
if (attempt == REFRESH_ATTEMPTS_AFTER_PULL - 1) {
|
||||
setProject(entry.key, null)
|
||||
failure(e)?.let { manifestState = ManifestState.Error(it) }
|
||||
} else {
|
||||
delay(RESTART_WAIT_MS)
|
||||
}
|
||||
}
|
||||
readBackAfterBuilding(entry) {
|
||||
// The project's own state only. A component that failed
|
||||
// inside this run has just been given its own, and
|
||||
// clearing that here would take away the only thing
|
||||
// saying so.
|
||||
setProject(entry.key, null)
|
||||
}
|
||||
} catch (e: DownloadServerException) {
|
||||
setProject(entry.key, failure(e)?.let(ProjectState::Error))
|
||||
@@ -877,6 +883,53 @@ private fun AppListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds one component because somebody pressed its Rebuild, and installs nothing.
|
||||
*
|
||||
* The component-scoped twin of [startRebuild], and the counterpart to [startUpdate] that does
|
||||
* not end at the phone: Update is "get me this build", which builds only when the rules say the
|
||||
* output is behind, while this is "build it again" for the cases those rules cannot see — a
|
||||
* command that reads files nobody declared, an output changed underneath this server, a signing
|
||||
* key replaced since the APK was made. None of them move a commit, so nothing reads as stale
|
||||
* and Update does nothing at all.
|
||||
*
|
||||
* Recorded against the component like every other action on one, so a sibling stays pressable
|
||||
* and silent throughout.
|
||||
*/
|
||||
fun startComponentBuild(entry: ManifestEntry, component: String) {
|
||||
scope.launch {
|
||||
setComponent(entry.key, component, ComponentState.Preparing(null))
|
||||
try {
|
||||
var status = withContext(Dispatchers.IO) { buildNow(entry.key, component) }
|
||||
// This component's own step, not the project's `building`,
|
||||
// for the same reason the download path reads it: a
|
||||
// sibling being built at the same time says yes to that
|
||||
// one.
|
||||
var started = false
|
||||
while (status.component(component)?.running == true) {
|
||||
started = true
|
||||
setComponent(entry.key, component, ComponentState.Preparing(status))
|
||||
delay(BUILD_POLL_INTERVAL_MS)
|
||||
status = withContext(Dispatchers.IO) { buildStatus(entry.key) }
|
||||
}
|
||||
val buildError = status.component(component)?.error?.takeIf { started }
|
||||
if (buildError != null) {
|
||||
setComponent(entry.key, component, ComponentState.Error(buildError))
|
||||
return@launch
|
||||
}
|
||||
// What changed is this component's build: its size, its
|
||||
// freshness, and whether the phone's copy is now the old
|
||||
// one. Only the card can say, so it is read back rather
|
||||
// than guessed at -- and the row stays busy until it
|
||||
// lands, which for this server's own component is the
|
||||
// second or two it spends restarting.
|
||||
readBackAfterBuilding(entry) { setComponent(entry.key, component, null) }
|
||||
} catch (e: DownloadServerException) {
|
||||
setComponent(entry.key, component, failure(e)?.let(ComponentState::Error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds one component if it needs it, downloads it, and hands it to the installer.
|
||||
*
|
||||
@@ -1264,6 +1317,9 @@ private fun AppListScreen(
|
||||
onUpdate = { updated, component ->
|
||||
startUpdate(updated, component)
|
||||
},
|
||||
onComponentBuild = { built, component ->
|
||||
startComponentBuild(built, component)
|
||||
},
|
||||
onPull = { startPull(entry) },
|
||||
onRebuild = { startRebuild(entry) },
|
||||
onRefresh = { refreshOne(entry) },
|
||||
@@ -1406,6 +1462,8 @@ private fun AppCard(
|
||||
/** What each of its components is doing, by component name. */
|
||||
componentStates: Map<String, ComponentState>,
|
||||
onUpdate: (ManifestEntry, component: String) -> Unit,
|
||||
/** Build this one component again, whatever the staleness rules make of it. */
|
||||
onComponentBuild: (ManifestEntry, component: String) -> Unit,
|
||||
onPull: () -> Unit,
|
||||
onRebuild: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
@@ -1653,9 +1711,11 @@ private fun AppCard(
|
||||
?.running == true,
|
||||
busy = serviceBusy == component.name,
|
||||
state = componentState,
|
||||
projectState = projectState,
|
||||
onAction = { action, purge ->
|
||||
onServiceAction(component.name, action, purge)
|
||||
},
|
||||
onBuild = { onComponentBuild(entry, component.name) },
|
||||
controls = {
|
||||
if (!component.isServer) {
|
||||
// Said before the button, because it
|
||||
@@ -1664,18 +1724,36 @@ private fun AppCard(
|
||||
self = component,
|
||||
others = components,
|
||||
)
|
||||
// The button that asked for it, then
|
||||
// The buttons that asked for it, then
|
||||
// how far along it is: a bar reports on
|
||||
// the control above it.
|
||||
UpdateButton(
|
||||
built = component.apk?.built == true,
|
||||
needsBuild = entry.needsBuild,
|
||||
installed = installed != null,
|
||||
upToDate = upToDate,
|
||||
state = componentState,
|
||||
projectState = projectState,
|
||||
onUpdate = { onUpdate(entry, component.name) },
|
||||
)
|
||||
// the controls above it.
|
||||
//
|
||||
// Laid out as the project's own row is
|
||||
// -- what you would press first on the
|
||||
// left, Rebuild held to the right edge
|
||||
// -- so the two scales of the same
|
||||
// action are in the same place at both.
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
UpdateButton(
|
||||
built = component.apk?.built == true,
|
||||
needsBuild = entry.needsBuild,
|
||||
installed = installed != null,
|
||||
upToDate = upToDate,
|
||||
state = componentState,
|
||||
projectState = projectState,
|
||||
onUpdate = { onUpdate(entry, component.name) },
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
ComponentBuildButton(
|
||||
component = component,
|
||||
state = componentState,
|
||||
projectState = projectState,
|
||||
onBuild = { onComponentBuild(entry, component.name) },
|
||||
)
|
||||
}
|
||||
ApkProgress(componentState)
|
||||
}
|
||||
},
|
||||
@@ -2016,6 +2094,42 @@ private fun UpdateButton(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds this one component again, whatever the staleness rules make of it.
|
||||
*
|
||||
* The project row's Rebuild at one component's scale, so it takes that button's word and its
|
||||
* colour: the same consequence should look the same wherever it is, and what this leaves behind is
|
||||
* no more obvious than what that one does. It sits at the right-hand end of the component's own
|
||||
* action row for the same reason Rebuild sits at the right-hand end of the project's.
|
||||
*
|
||||
* Why it exists beside Update, which also builds: Update asks the server whether the output is
|
||||
* behind and builds only then, which is right for "get me this build" and blind to everything a
|
||||
* commit does not describe — a command that reads files nobody declared, an output changed
|
||||
* underneath this server, a signing key replaced since the APK was made. In all of those nothing is
|
||||
* stale, so Update does nothing, and before this the only force was the project-wide Rebuild.
|
||||
*
|
||||
* Absent rather than disabled for a component with no command, which is the one thing here that is
|
||||
* not a state it is in: the project row hides its Rebuild on the same grounds, and a permanently
|
||||
* dead button on every row of every hand-built project teaches nothing. Anything that *is* a state
|
||||
* — busy, or the whole checkout busy — disables it instead, the same pair that disables Update.
|
||||
*/
|
||||
@Composable
|
||||
private fun ComponentBuildButton(
|
||||
component: ProjectComponent,
|
||||
state: ComponentState?,
|
||||
projectState: ProjectState?,
|
||||
onBuild: () -> Unit,
|
||||
) {
|
||||
if (!component.hasBuild) return
|
||||
TextButton(
|
||||
onClick = onBuild,
|
||||
enabled = !state.busy && !projectState.busy,
|
||||
colors = ActionTone.Caution.colors(),
|
||||
) {
|
||||
Text("Rebuild")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The dot between a component's name and whatever is said about it.
|
||||
*
|
||||
@@ -2232,7 +2346,14 @@ private fun ComponentCard(
|
||||
* so on the row it was pressed in.
|
||||
*/
|
||||
state: ComponentState?,
|
||||
/**
|
||||
* What the whole project is doing, which disables this component's Rebuild as well: a pull
|
||||
* rewrites the files it would build from, and the server refuses a build while one is running.
|
||||
*/
|
||||
projectState: ProjectState?,
|
||||
onAction: (String, Purge) -> Unit,
|
||||
/** Build this component again. A server's is drawn here, beside its service buttons. */
|
||||
onBuild: () -> Unit,
|
||||
// Controls belonging to this component that only the caller can build
|
||||
// -- an APK's Update button, which needs the project's build state.
|
||||
// The service's own buttons are decided here, from the component.
|
||||
@@ -2394,9 +2515,22 @@ private fun ComponentCard(
|
||||
// script takes rather than leaving: a row that vanishes on
|
||||
// every press makes its own presence the signal, and takes the
|
||||
// card's height with it on the way out and back.
|
||||
if (component.isServer && component.state != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (!component.isInstalled) {
|
||||
//
|
||||
// Rebuild is in this row but outside that condition, because
|
||||
// it is about the build rather than the service: a server
|
||||
// whose script cannot be reached is still one this machine can
|
||||
// compile, and hiding the way to do that until an unrelated
|
||||
// question is answered would be the same absence-as-signal.
|
||||
if (component.isServer && (component.state != null || component.hasBuild)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (component.state == null) {
|
||||
// Nothing on the left yet, so the one button there
|
||||
// is holds the right edge it will keep once the
|
||||
// service answers.
|
||||
} else if (!component.isInstalled) {
|
||||
TextButton(
|
||||
onClick = { onAction("install", Purge()) },
|
||||
enabled = !busy,
|
||||
@@ -2443,6 +2577,13 @@ private fun ComponentCard(
|
||||
Text("Uninstall")
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
ComponentBuildButton(
|
||||
component = component,
|
||||
state = state,
|
||||
projectState = projectState,
|
||||
onBuild = onBuild,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user