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(&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<Built, String> {
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<Built, String> {
"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<Built, String> {
"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<Built, String> {
)?;
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<Built, String> {
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<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> {
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<Self, String> {
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 &amp; &lt;demo&gt;"));
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
```
APK staging and output live under
`target/iris-android/<package>[-<example>]/<debug|release>/<abi>/`; the
command's final line is the verified APK's absolute path.
The verified APK lives under
`target/iris-android/<package>[-<example>]/<debug|release>/<abi>/`; 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)