diff --git a/iris/cargo-iris/src/package.rs b/iris/cargo-iris/src/package.rs index 979cbd7..17f8a79 100644 --- a/iris/cargo-iris/src/package.rs +++ b/iris/cargo-iris/src/package.rs @@ -173,7 +173,9 @@ fn build(options: &Options) -> Result { .join(variant) .join(&options.abi); 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 wrapper = metadata .target_directory @@ -213,9 +215,9 @@ fn build(options: &Options) -> Result { if !library.is_file() { 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))?; - let sources = materialize_host(&output, &library_name)?; + let sources = materialize_host(&staging, &library_name)?; let java_files = files_with_extension(&sources, "java")?; let mut javac = Command::new("javac"); javac @@ -230,7 +232,7 @@ fn build(options: &Options) -> Result { "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))?; let class_files = files_with_extension(&classes, "class")?; let mut d8 = Command::new(&sdk.d8); @@ -250,13 +252,13 @@ fn build(options: &Options) -> Result { "install Android SDK Build Tools", )?; - let manifest = output.join("AndroidManifest.xml"); + let manifest = staging.join("AndroidManifest.xml"); fs::write( &manifest, manifest_xml(&application_id, &label, &library_name, sdk.api), ) .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); aapt.arg("link") .arg("-o") @@ -278,14 +280,14 @@ fn build(options: &Options) -> Result { )?; append_payload( &unsigned, - &output, + &staging, &dex.join("classes.dex"), &library, &options.abi, &library_name, )?; - let aligned = output.join("aligned.apk"); + let aligned = staging.join("aligned.apk"); let mut zipalign = Command::new(&sdk.zipalign); zipalign .args(["-P", "16", "-f", "4"]) @@ -299,6 +301,8 @@ fn build(options: &Options) -> Result { let apk = output.join(format!("{artifact}-{variant}.apk")); sign(&sdk, options, &aligned, &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 { apk, application_id, @@ -405,23 +409,33 @@ fn materialize_example_wrapper( let manifest = format!( "[package]\nname = {name:?}\nversion = \"0.0.0\"\nedition = \"2024\"\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), iris_root = iris_root.as_str(), ); let manifest_path = wrapper.join("Cargo.toml"); - fs::write(&manifest_path, manifest) - .map_err(io_error("write Android example manifest", &manifest_path))?; + write_if_changed(&manifest_path, &manifest)?; let source = format!( "#[path = {:?}]\nmod example;\n", android_source.to_string_lossy() ); let source_path = source_dir.join("lib.rs"); - fs::write(&source_path, source) - .map_err(io_error("write Android example wrapper", &source_path))?; + write_if_changed(&source_path, &source)?; Ok((manifest_path, library_name)) } +fn write_if_changed(path: &Path, contents: &str) -> Result { + 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 { package .metadata @@ -481,6 +495,16 @@ struct Sdk { 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 { fn find() -> Result { 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); + // `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 .arg("sign") .args([ + "--v4-signing-enabled", + "false", "--ks-pass", "env:IRIS_APK_STORE_PASSWORD", "--key-pass", @@ -799,9 +827,9 @@ fn install_and_run(built: &Built, device: &str) -> Result<(), String> { fn recreate(path: &Path) -> Result<(), String> { 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> { @@ -842,4 +870,33 @@ mod tests { assert!(manifest.contains("A & <demo>")); 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()); + } } diff --git a/iris/cargo-iris/tests/fixture/.gitignore b/iris/cargo-iris/tests/fixture/.gitignore deleted file mode 100644 index 042776a..0000000 --- a/iris/cargo-iris/tests/fixture/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/Cargo.lock -/target/ diff --git a/iris/cargo-iris/tests/fixture/Cargo.toml b/iris/cargo-iris/tests/fixture/Cargo.toml deleted file mode 100644 index 79ca0da..0000000 --- a/iris/cargo-iris/tests/fixture/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "iris-apk-fixture" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -iris = { path = "../../.." } - -[workspace] diff --git a/iris/cargo-iris/tests/fixture/src/lib.rs b/iris/cargo-iris/tests/fixture/src/lib.rs deleted file mode 100644 index 386c756..0000000 --- a/iris/cargo-iris/tests/fixture/src/lib.rs +++ /dev/null @@ -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, - tasks: Tasks, - widget_state: WidgetState, - launches: u32, -} - -impl AndroidResources for Resources { - fn new(wake: Arc) -> (Self, TaskMsgReceiver) { - 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.events - } - - fn events_mut(&mut self) -> &mut EventManager { - &mut self.events - } -} - -impl HasTasks for Resources { - fn tasks_mut(&mut self) -> &mut Tasks { - &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 - } -} diff --git a/iris/readme.md b/iris/readme.md index ee955c8..dbf9b43 100644 --- a/iris/readme.md +++ b/iris/readme.md @@ -101,9 +101,9 @@ IRIS_KEYSTORE_PASSWORD=... IRIS_KEY_PASSWORD=... \ cargo iris apk --release --keystore /secure/upload.jks --key-alias upload ``` -APK staging and output live under -`target/iris-android/[-]///`; the -command's final line is the verified APK's absolute path. +The verified APK lives under +`target/iris-android/[-]///`; packaging +intermediates are removed before the command prints its absolute path. Goals, in general order: 1. does what I want it to (text, images, video, animations)