E5: package app/shellApp without Gradle (cargo xtask apk)
New xtask/ crate (no deps) runs cargo ndk -> javac -> d8 -> aapt2 -> zipalign -> apksigner directly, signed with the same key build-apk.sh uses. Both pass conditions proved on the ai-app-2 emulator: the xtask APK installs over the Gradle-built shellApp, and the notification service starts and posts a real notification while backgrounded. Adds one printRuntimeClasspathJars task to shellApp/build.gradle.kts (and a matching signingConfig) -- the one disclosed Gradle call the xtask still makes, to resolve the AndroidX/:link dependency graph. That call's Kotlin compilation of :link as a side effect also answers E3's open kotlinc question, so no Java port of ServerStore was needed. Wires a second Apk component into .dev-updater.ron beside the existing one. Full writeup in RUST.md's E5 box. Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
1 parent
32a5256a0d
commit
ceabd00805
11 files changed
+1289
-25
No files matched your search
@@ -0,0 +1,254 @@
|
||||
//! The signing key. Mirrors `app/build-apk.sh`'s exact logic for the
|
||||
//! release key -- same env vars, same path, same generation recipe -- so
|
||||
//! the two tools sign with the *same* key and their outputs can
|
||||
//! `adb install -r` over each other. That is the whole point of E5's pass
|
||||
//! condition: the key has to be identical, not merely present.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::Fail;
|
||||
|
||||
pub struct Signer {
|
||||
pub keystore: PathBuf,
|
||||
pub password: String,
|
||||
pub alias: String,
|
||||
}
|
||||
|
||||
/// The release key at `$AI_APP_KEYSTORE` or
|
||||
/// `$XDG_CONFIG_HOME/ai-app/release.jks` (`~/.config/ai-app/release.jks` by
|
||||
/// default) -- generated with `keytool` if it doesn't exist yet, exactly as
|
||||
/// `build-apk.sh` does, so either tool can run first on a fresh machine.
|
||||
pub fn release_signer() -> Result<Signer, Fail> {
|
||||
let keystore = std::env::var_os("AI_APP_KEYSTORE")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
let config_home = std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(std::env::var_os("HOME").unwrap()).join(".config")
|
||||
});
|
||||
config_home.join("ai-app").join("release.jks")
|
||||
});
|
||||
let alias = "ai-app".to_string();
|
||||
let password_file = keystore.with_extension("jks.password");
|
||||
|
||||
if keystore.is_file() {
|
||||
let password = std::fs::read_to_string(&password_file)
|
||||
.map_err(|e| {
|
||||
Fail::new(
|
||||
"release key exists but its password file is unreadable",
|
||||
&format!("{}: {e}", password_file.display()),
|
||||
"restore the password file, or delete both and let this regenerate them",
|
||||
)
|
||||
})?
|
||||
.trim()
|
||||
.to_string();
|
||||
return Ok(Signer {
|
||||
keystore,
|
||||
password,
|
||||
alias,
|
||||
});
|
||||
}
|
||||
|
||||
let keytool = which_keytool()?;
|
||||
if let Some(parent) = keystore.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| {
|
||||
Fail::new(
|
||||
"could not create the keystore's directory",
|
||||
&format!("{}: {e}", parent.display()),
|
||||
"check permissions on that path",
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let password = random_password();
|
||||
write_owner_only(&password_file, format!("{password}\n").as_bytes())?;
|
||||
|
||||
let status = Command::new(&keytool)
|
||||
.args(["-genkeypair", "-keystore"])
|
||||
.arg(&keystore)
|
||||
.args([
|
||||
"-alias",
|
||||
&alias,
|
||||
"-keyalg",
|
||||
"RSA",
|
||||
"-keysize",
|
||||
"2048",
|
||||
"-validity",
|
||||
"10000",
|
||||
])
|
||||
.args(["-storepass", &password, "-keypass", &password])
|
||||
.args(["-dname", "CN=ai-app"])
|
||||
.status()
|
||||
.map_err(|e| {
|
||||
Fail::new(
|
||||
"failed to run keytool",
|
||||
&format!("{}: {e}", keytool.display()),
|
||||
"set JAVA_HOME to the JDK Gradle uses",
|
||||
)
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(Fail::new(
|
||||
"keytool exited with an error while generating the release key",
|
||||
&format!("status: {status}"),
|
||||
"check the keytool output above",
|
||||
));
|
||||
}
|
||||
// Owner-only, matching build-apk.sh -- this key is what the phone
|
||||
// recognises the app by, so it never goes in the repo and it stays
|
||||
// unreadable to anything else on this machine.
|
||||
set_owner_only(&keystore)?;
|
||||
|
||||
Ok(Signer {
|
||||
keystore,
|
||||
password,
|
||||
alias,
|
||||
})
|
||||
}
|
||||
|
||||
/// The conventional Android debug key (`~/.android/debug.keystore`,
|
||||
/// well-known password `android`, alias `androiddebugkey`) -- generated on
|
||||
/// first use exactly the way Android Studio and Gradle's own debug signing
|
||||
/// config do, so a `--debug` build here needs no setup and never touches
|
||||
/// the real release key.
|
||||
pub fn debug_signer() -> Result<Signer, Fail> {
|
||||
let home = PathBuf::from(std::env::var_os("HOME").ok_or_else(|| {
|
||||
Fail::new(
|
||||
"no $HOME set",
|
||||
"the debug keystore lives under ~/.android",
|
||||
"set $HOME",
|
||||
)
|
||||
})?);
|
||||
let keystore = home.join(".android").join("debug.keystore");
|
||||
let alias = "androiddebugkey".to_string();
|
||||
let password = "android".to_string();
|
||||
|
||||
if !keystore.is_file() {
|
||||
let keytool = which_keytool()?;
|
||||
std::fs::create_dir_all(keystore.parent().unwrap()).map_err(|e| {
|
||||
Fail::new(
|
||||
"could not create ~/.android",
|
||||
&format!("{e}"),
|
||||
"check permissions on your home directory",
|
||||
)
|
||||
})?;
|
||||
let status = Command::new(&keytool)
|
||||
.args(["-genkeypair", "-keystore"])
|
||||
.arg(&keystore)
|
||||
.args([
|
||||
"-alias",
|
||||
&alias,
|
||||
"-keyalg",
|
||||
"RSA",
|
||||
"-keysize",
|
||||
"2048",
|
||||
"-validity",
|
||||
"10000",
|
||||
])
|
||||
.args(["-storepass", &password, "-keypass", &password])
|
||||
.args(["-dname", "CN=Android Debug,O=Android,C=US"])
|
||||
.status()
|
||||
.map_err(|e| {
|
||||
Fail::new(
|
||||
"failed to run keytool",
|
||||
&format!("{}: {e}", keytool.display()),
|
||||
"set JAVA_HOME to the JDK Gradle uses",
|
||||
)
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(Fail::new(
|
||||
"keytool exited with an error while generating the debug key",
|
||||
&format!("status: {status}"),
|
||||
"check the keytool output above",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Signer {
|
||||
keystore,
|
||||
password,
|
||||
alias,
|
||||
})
|
||||
}
|
||||
|
||||
fn which_keytool() -> Result<PathBuf, Fail> {
|
||||
if let Some(java_home) = std::env::var_os("JAVA_HOME") {
|
||||
let candidate = PathBuf::from(java_home).join("bin").join("keytool");
|
||||
if candidate.is_file() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
}
|
||||
if Command::new("keytool").arg("-help").output().is_ok() {
|
||||
return Ok(PathBuf::from("keytool"));
|
||||
}
|
||||
Err(Fail::new(
|
||||
"no keytool available to generate the release key",
|
||||
"checked $JAVA_HOME/bin/keytool and keytool on PATH",
|
||||
"set JAVA_HOME to the JDK Gradle uses, or set AI_APP_KEYSTORE to an existing key",
|
||||
))
|
||||
}
|
||||
|
||||
fn random_password() -> String {
|
||||
// No dependency on `rand`: /dev/urandom is what build-apk.sh's `head -c
|
||||
// 24 /dev/urandom | base64` reads too, so this reproduces exactly the
|
||||
// same recipe without shelling out to head/base64/tr for it.
|
||||
let mut bytes = [0u8; 24];
|
||||
std::fs::File::open("/dev/urandom")
|
||||
.and_then(|mut f| std::io::Read::read_exact(&mut f, &mut bytes))
|
||||
.expect("/dev/urandom must be readable to generate a signing key password");
|
||||
base64_no_padding(&bytes)
|
||||
}
|
||||
|
||||
fn base64_no_padding(bytes: &[u8]) -> String {
|
||||
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
let mut out = String::new();
|
||||
for chunk in bytes.chunks(3) {
|
||||
let b0 = chunk[0] as u32;
|
||||
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
|
||||
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
|
||||
let n = (b0 << 16) | (b1 << 8) | b2;
|
||||
out.push(ALPHABET[(n >> 18 & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[(n >> 12 & 0x3f) as usize] as char);
|
||||
if chunk.len() > 1 {
|
||||
out.push(ALPHABET[(n >> 6 & 0x3f) as usize] as char);
|
||||
}
|
||||
if chunk.len() > 2 {
|
||||
out.push(ALPHABET[(n & 0x3f) as usize] as char);
|
||||
}
|
||||
}
|
||||
// build-apk.sh strips '/', '+' and '=' from its password (tr -d
|
||||
// '/+='), so the value never needs quoting when it is passed as a
|
||||
// command-line argument later.
|
||||
out.retain(|c| c != '/' && c != '+' && c != '=');
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn write_owner_only(path: &std::path::Path, contents: &[u8]) -> Result<(), Fail> {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path)
|
||||
.and_then(|mut f| std::io::Write::write_all(&mut f, contents))
|
||||
.map_err(|e| {
|
||||
Fail::new(
|
||||
"could not write the keystore password file",
|
||||
&format!("{}: {e}", path.display()),
|
||||
"check permissions on that directory",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn set_owner_only(path: &std::path::Path) -> Result<(), Fail> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(|e| {
|
||||
Fail::new(
|
||||
"could not restrict the keystore's permissions",
|
||||
&format!("{}: {e}", path.display()),
|
||||
"chmod 600 it by hand",
|
||||
)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user