Reuse Android builds and discard staging

This commit is contained in:
iris committed 2026-09-11 13:56:32 -04:00
1 parent 779f5c63d3
commit f7e7950908
5 files changed
+75 -129

No files matched your search

+72 -15
View File
@@ -173,7 +173,9 @@ fn build(options: &Options) -> Result<Built, String> {
.join(variant) .join(variant)
.join(&options.abi); .join(&options.abi);
recreate(&output)?; recreate(&output)?;
let native = output.join("native"); let staging = output.join("staging");
let staging_cleanup = RemoveDirOnDrop(&staging);
let native = staging.join("native");
let (build_manifest, library_name) = if options.example.is_some() { let (build_manifest, library_name) = if options.example.is_some() {
let wrapper = metadata let wrapper = metadata
.target_directory .target_directory
@@ -213,9 +215,9 @@ fn build(options: &Options) -> Result<Built, String> {
if !library.is_file() { if !library.is_file() {
return Err(format!("cargo-ndk did not produce {}", library.display())); return Err(format!("cargo-ndk did not produce {}", library.display()));
} }
let classes = output.join("classes"); let classes = staging.join("classes");
fs::create_dir_all(&classes).map_err(io_error("create Java output", &classes))?; fs::create_dir_all(&classes).map_err(io_error("create Java output", &classes))?;
let sources = materialize_host(&output, &library_name)?; let sources = materialize_host(&staging, &library_name)?;
let java_files = files_with_extension(&sources, "java")?; let java_files = files_with_extension(&sources, "java")?;
let mut javac = Command::new("javac"); let mut javac = Command::new("javac");
javac javac
@@ -230,7 +232,7 @@ fn build(options: &Options) -> Result<Built, String> {
"install a JDK containing javac", "install a JDK containing javac",
)?; )?;
let dex = output.join("dex"); let dex = staging.join("dex");
fs::create_dir_all(&dex).map_err(io_error("create DEX output", &dex))?; fs::create_dir_all(&dex).map_err(io_error("create DEX output", &dex))?;
let class_files = files_with_extension(&classes, "class")?; let class_files = files_with_extension(&classes, "class")?;
let mut d8 = Command::new(&sdk.d8); let mut d8 = Command::new(&sdk.d8);
@@ -250,13 +252,13 @@ fn build(options: &Options) -> Result<Built, String> {
"install Android SDK Build Tools", "install Android SDK Build Tools",
)?; )?;
let manifest = output.join("AndroidManifest.xml"); let manifest = staging.join("AndroidManifest.xml");
fs::write( fs::write(
&manifest, &manifest,
manifest_xml(&application_id, &label, &library_name, sdk.api), manifest_xml(&application_id, &label, &library_name, sdk.api),
) )
.map_err(io_error("write Android manifest", &manifest))?; .map_err(io_error("write Android manifest", &manifest))?;
let unsigned = output.join("unsigned.apk"); let unsigned = staging.join("unsigned.apk");
let mut aapt = Command::new(&sdk.aapt2); let mut aapt = Command::new(&sdk.aapt2);
aapt.arg("link") aapt.arg("link")
.arg("-o") .arg("-o")
@@ -278,14 +280,14 @@ fn build(options: &Options) -> Result<Built, String> {
)?; )?;
append_payload( append_payload(
&unsigned, &unsigned,
&output, &staging,
&dex.join("classes.dex"), &dex.join("classes.dex"),
&library, &library,
&options.abi, &options.abi,
&library_name, &library_name,
)?; )?;
let aligned = output.join("aligned.apk"); let aligned = staging.join("aligned.apk");
let mut zipalign = Command::new(&sdk.zipalign); let mut zipalign = Command::new(&sdk.zipalign);
zipalign zipalign
.args(["-P", "16", "-f", "4"]) .args(["-P", "16", "-f", "4"])
@@ -299,6 +301,8 @@ fn build(options: &Options) -> Result<Built, String> {
let apk = output.join(format!("{artifact}-{variant}.apk")); let apk = output.join(format!("{artifact}-{variant}.apk"));
sign(&sdk, options, &aligned, &apk)?; sign(&sdk, options, &aligned, &apk)?;
verify(&sdk, &apk)?; verify(&sdk, &apk)?;
fs::remove_dir_all(&staging).map_err(io_error("remove APK staging directory", &staging))?;
std::mem::forget(staging_cleanup);
Ok(Built { Ok(Built {
apk, apk,
application_id, application_id,
@@ -405,23 +409,33 @@ fn materialize_example_wrapper(
let manifest = format!( let manifest = format!(
"[package]\nname = {name:?}\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\ "[package]\nname = {name:?}\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[lib]\nname = {library_name:?}\ncrate-type = [\"cdylib\", \"rlib\"]\n\n\ [lib]\nname = {library_name:?}\ncrate-type = [\"cdylib\", \"rlib\"]\n\n\
[dependencies]\niris = {{ path = {iris_root:?} }}\n\n[workspace]\n", [dependencies]\niris = {{ path = {iris_root:?} }}\n\n[workspace]\n\n\
[profile.dev]\ndebug = \"line-tables-only\"\n",
name = format!("iris-android-{}-{}", package.name, example.name), name = format!("iris-android-{}-{}", package.name, example.name),
iris_root = iris_root.as_str(), iris_root = iris_root.as_str(),
); );
let manifest_path = wrapper.join("Cargo.toml"); let manifest_path = wrapper.join("Cargo.toml");
fs::write(&manifest_path, manifest) write_if_changed(&manifest_path, &manifest)?;
.map_err(io_error("write Android example manifest", &manifest_path))?;
let source = format!( let source = format!(
"#[path = {:?}]\nmod example;\n", "#[path = {:?}]\nmod example;\n",
android_source.to_string_lossy() android_source.to_string_lossy()
); );
let source_path = source_dir.join("lib.rs"); let source_path = source_dir.join("lib.rs");
fs::write(&source_path, source) write_if_changed(&source_path, &source)?;
.map_err(io_error("write Android example wrapper", &source_path))?;
Ok((manifest_path, library_name)) Ok((manifest_path, library_name))
} }
fn write_if_changed(path: &Path, contents: &str) -> Result<bool, String> {
match fs::read(path) {
Ok(existing) if existing == contents.as_bytes() => return Ok(false),
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(format!("cannot read {}: {error}", path.display())),
}
fs::write(path, contents).map_err(io_error("write generated file", path))?;
Ok(true)
}
fn metadata_string(package: &Package, key: &str) -> Option<String> { fn metadata_string(package: &Package, key: &str) -> Option<String> {
package package
.metadata .metadata
@@ -481,6 +495,16 @@ struct Sdk {
adb: PathBuf, adb: PathBuf,
} }
/// Failed builds have no reusable staging output either; the next invocation
/// starts from scratch, so do not make a failure consume disk indefinitely.
struct RemoveDirOnDrop<'a>(&'a Path);
impl Drop for RemoveDirOnDrop<'_> {
fn drop(&mut self) {
let _ = fs::remove_dir_all(self.0);
}
}
impl Sdk { impl Sdk {
fn find() -> Result<Self, String> { fn find() -> Result<Self, String> {
let root = env::var_os("ANDROID_HOME") let root = env::var_os("ANDROID_HOME")
@@ -700,9 +724,13 @@ fn sign(sdk: &Sdk, options: &Options, input: &Path, output: &Path) -> Result<(),
) )
}; };
let mut command = Command::new(&sdk.apksigner); let mut command = Command::new(&sdk.apksigner);
// `install_and_run` uses `--no-streaming`, so it cannot consume the separate
// v4 `.idsig` file and retaining that sidecar beside the APK serves no caller.
command command
.arg("sign") .arg("sign")
.args([ .args([
"--v4-signing-enabled",
"false",
"--ks-pass", "--ks-pass",
"env:IRIS_APK_STORE_PASSWORD", "env:IRIS_APK_STORE_PASSWORD",
"--key-pass", "--key-pass",
@@ -799,9 +827,9 @@ fn install_and_run(built: &Built, device: &str) -> Result<(), String> {
fn recreate(path: &Path) -> Result<(), String> { fn recreate(path: &Path) -> Result<(), String> {
if path.exists() { if path.exists() {
fs::remove_dir_all(path).map_err(io_error("clear prior APK staging directory", path))?; fs::remove_dir_all(path).map_err(io_error("clear prior APK output directory", path))?;
} }
fs::create_dir_all(path).map_err(io_error("create APK staging directory", path)) fs::create_dir_all(path).map_err(io_error("create APK output directory", path))
} }
fn run_command(command: &mut Command, thing: &str, fix: &str) -> Result<(), String> { fn run_command(command: &mut Command, thing: &str, fix: &str) -> Result<(), String> {
@@ -842,4 +870,33 @@ mod tests {
assert!(manifest.contains("A &amp; &lt;demo&gt;")); assert!(manifest.contains("A &amp; &lt;demo&gt;"));
assert!(manifest.contains("android:minSdkVersion=\"29\"")); assert!(manifest.contains("android:minSdkVersion=\"29\""));
} }
#[test]
fn an_unchanged_generated_file_is_not_rewritten() {
let root = env::temp_dir().join(format!("cargo-iris-write-test-{}", std::process::id()));
let path = root.join("generated.rs");
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
assert!(write_if_changed(&path, "first").unwrap());
assert!(!write_if_changed(&path, "first").unwrap());
assert!(write_if_changed(&path, "second").unwrap());
assert_eq!(fs::read_to_string(&path).unwrap(), "second");
fs::remove_dir_all(root).unwrap();
}
#[test]
fn failed_build_staging_is_removed_when_its_scope_ends() {
let root = env::temp_dir().join(format!("cargo-iris-staging-test-{}", std::process::id()));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
fs::write(root.join("intermediate"), "not reusable").unwrap();
{
let _cleanup = RemoveDirOnDrop(&root);
}
assert!(!root.exists());
}
} }
-2
View File
@@ -1,2 +0,0 @@
/Cargo.lock
/target/
-12
View File
@@ -1,12 +0,0 @@
[package]
name = "iris-apk-fixture"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
iris = { path = "../../.." }
[workspace]
-97
View File
@@ -1,97 +0,0 @@
use iris::prelude::*;
use std::sync::Arc;
#[derive(AndroidUiState)]
pub struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = Resources;
}
#[iris::android_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut Resources) -> State {
rsc.launches += 1;
rect(PaintId::RED)
.label("Iris APK fixture")
.set_root(rsc, &mut ui_state);
State { ui_state }
}
pub struct Resources {
ui: Ui,
events: EventManager<Self>,
tasks: Tasks<Self>,
widget_state: WidgetState,
launches: u32,
}
impl AndroidResources<State> for Resources {
fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(wake);
(
Self {
ui: Ui::default(),
events: EventManager::default(),
tasks,
widget_state: WidgetState::default(),
launches: 0,
},
receiver,
)
}
}
impl UiRsc for Resources {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
self.events.draw(active);
}
fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active);
}
fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id);
self.widget_state.remove(id);
}
}
impl HasState for Resources {
type State = State;
}
impl HasEvents for Resources {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl HasTasks for Resources {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl HasWidgetState for Resources {
fn widget_state(&self) -> &WidgetState {
&self.widget_state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.widget_state
}
}
+3 -3
View File
@@ -101,9 +101,9 @@ IRIS_KEYSTORE_PASSWORD=... IRIS_KEY_PASSWORD=... \
cargo iris apk --release --keystore /secure/upload.jks --key-alias upload cargo iris apk --release --keystore /secure/upload.jks --key-alias upload
``` ```
APK staging and output live under The verified APK lives under
`target/iris-android/<package>[-<example>]/<debug|release>/<abi>/`; the `target/iris-android/<package>[-<example>]/<debug|release>/<abi>/`; packaging
command's final line is the verified APK's absolute path. intermediates are removed before the command prints its absolute path.
Goals, in general order: Goals, in general order:
1. does what I want it to (text, images, video, animations) 1. does what I want it to (text, images, video, animations)