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:
irisandClaude Opus 5 committed 2026-09-01 11:31:59 -04:00
1 parent b67c70fa2f
commit c7b3af4861
6 files changed
+358 -72

No files matched your search

+21 -2
View File
@@ -283,6 +283,15 @@ struct ManifestComponent {
/// succeeded, which is what makes the runtime log the ordinary
/// default.
build_failed: bool,
/// This component has a command to run and is allowed to run it, so
/// the card can offer to build this one alone.
///
/// Per component rather than read off the project's `needsBuild`,
/// which is the OR of every component's: a project where only one of
/// two has a build step would otherwise draw the button on both, and
/// the one that cannot build would answer a press by doing nothing
/// at all.
has_build: bool,
/// Whether what is built is current with the checkout.
///
/// Sent for every component, including the unknown case, because
@@ -338,6 +347,10 @@ impl ManifestComponent {
key: &str,
entry: &AppEntry,
component: &crate::config::Component,
// Whether this project's build step may run at all -- false while
// a declaration is waiting to be accepted, which is the project's
// answer rather than this component's.
may_build: bool,
) -> Result<Self, ApiError> {
let name = component.name().to_string();
let is_server = matches!(component, crate::config::Component::Server { .. });
@@ -376,6 +389,7 @@ impl ManifestComponent {
.build
.as_ref()
.is_some_and(|build| build.build_failed(&name)),
has_build: may_build && !component.build().is_empty(),
freshness: entry
.build
.as_ref()
@@ -626,9 +640,15 @@ async fn describe(state: &Arc<AppState>, entry: &AppEntry) -> Result<ManifestApp
// A loop rather than a map because each component's APK is a `stat`,
// and they are described in declaration order -- which is build order,
// and the order the card draws them in.
// Read before the components, because each of them reports whether it
// can be built and none of them can while this is set: an unaccepted
// declaration is exactly the command that must not run.
let pending = entry.pending_declaration();
let mut components = Vec::with_capacity(entry.components.len());
for component in &entry.components {
components.push(ManifestComponent::read(state, &entry.key, entry, component).await?);
components.push(
ManifestComponent::read(state, &entry.key, entry, component, pending.is_none()).await?,
);
}
let git = crate::git::status(&entry.project_path);
@@ -638,7 +658,6 @@ async fn describe(state: &Arc<AppState>, entry: &AppEntry) -> Result<ManifestApp
// twice -- "no upstream" on the branch line, and the check's own
// complaint about the same thing underneath it.
let can_pull = entry.git_pull && git.as_ref().is_some_and(|git| git.upstream.is_some());
let pending = entry.pending_declaration();
Ok(ManifestApp {
// Read from the cache a refresh populated, rather than asking here.
new_commits: can_pull && state.remote_checks.new_commits(&entry.project_path),
+84 -8
View File
@@ -184,18 +184,94 @@ pub fn llvm_strip_path() -> Result<PathBuf> {
}
}
/// Where the Android tools keep their preferences, resolved the way the
/// Android Gradle plugin resolves it -- because the only thing that makes
/// the keystore below the right one is that it is the same file Gradle
/// signed with.
///
/// `~/.android` is what this used to assume, and it is only the last of
/// five answers. Measured against AGP 32.3.2's `AbstractAndroidLocations`
/// rather than read off a document, by running real builds here:
///
/// * `$ANDROID_USER_HOME` is the preferences directory itself, with no
/// `.android` under it (`ANDROID_USER_HOME=/tmp/auh` put the keystore at
/// `/tmp/auh/debug.keystore`);
/// * `$ANDROID_PREFS_ROOT` and the older `$ANDROID_SDK_HOME` get
/// `.android` appended (`/tmp/ash` -> `/tmp/ash/.android/debug.keystore`);
/// * `$XDG_CONFIG_HOME/.android` comes next **but only when that directory
/// already exists** -- the bytecode tests it and falls through
/// otherwise, and a build here with `XDG_CONFIG_HOME` pointed at a fresh
/// directory did indeed keep using `~/.android`;
/// * `$HOME/.android` otherwise.
///
/// That fourth step is the one that cost a day. A desktop Linux machine
/// with `XDG_CONFIG_HOME` set and a `.android` under it -- which is
/// ordinary -- 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. This VM does not set `XDG_CONFIG_HOME` at all, which is
/// why the missing case never showed up here.
fn android_prefs_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(home) = std::env::var_os("ANDROID_USER_HOME") {
dirs.push(PathBuf::from(home));
}
for root in ["ANDROID_PREFS_ROOT", "ANDROID_SDK_HOME"] {
if let Some(root) = std::env::var_os(root) {
dirs.push(PathBuf::from(root).join(".android"));
}
}
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
let dir = PathBuf::from(xdg).join(".android");
// Existence is the condition AGP applies, so applying it here is
// what keeps the two agreeing about which file is in use.
if dir.is_dir() {
dirs.push(dir);
}
}
dirs.push(home_dir().join(".android"));
dirs
}
/// The debug keystore every locally-built debug APK is already signed with,
/// reused to re-sign a stripped copy so it still installs over the original.
///
/// The first candidate is the one Gradle would use, so it is the one taken
/// even if a keystore exists somewhere further down the list: a file in a
/// place AGP would not look is not the key anything here was signed with.
/// The failure names those anyway, because an inert keystore and an absent
/// one look identical from a phone, and mistaking one for the other is
/// what turned "this machine has no keystore" into a signature mismatch
/// nobody could explain.
pub fn debug_keystore_path() -> Result<PathBuf> {
let path = home_dir().join(".android/debug.keystore");
if !path.is_file() {
bail!(
"debug keystore not found at {} -- build any Android app once to have \
Gradle create it",
path.display(),
);
let candidates: Vec<PathBuf> = android_prefs_dirs()
.iter()
.map(|dir| dir.join("debug.keystore"))
.collect();
let (wanted, elsewhere) = candidates
.split_first()
.expect("home is always a candidate");
if wanted.is_file() {
return Ok(wanted.clone());
}
Ok(path)
let found: Vec<String> = elsewhere
.iter()
.filter(|path| path.is_file())
.map(|path| path.display().to_string())
.collect();
bail!(
"debug keystore not found at {} -- build any Android app once to have Gradle create \
it{}",
wanted.display(),
if found.is_empty() {
String::new()
} else {
format!(
". There is one at {}, but that is not where the Android tools look on this \
machine, so nothing here was signed with it",
found.join(", "),
)
},
)
}
/// Runs `cmd`, turning a nonzero exit into an error naming the program and