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
+21
-2
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user