Run Iris examples on desktop and Android
This commit is contained in:
1 parent
37f956707e
commit
b5666cdef9
25 files changed
+577
-205
No files matched your search
+168
-33
@@ -1,4 +1,4 @@
|
||||
use cargo_metadata::{CrateType, MetadataCommand, Package};
|
||||
use cargo_metadata::{CrateType, MetadataCommand, Package, Target, TargetKind};
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
@@ -36,9 +36,9 @@ pub fn run(mut args: Vec<String>) -> Result<(), String> {
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"Build an Iris library as an installable Android APK.\n\n\
|
||||
"Build an Iris application or example as an installable Android APK.\n\n\
|
||||
Usage:\n cargo iris apk [OPTIONS]\n cargo iris run --device SERIAL [OPTIONS]\n\n\
|
||||
Options:\n --manifest-path PATH\n --package NAME\n --abi arm64-v8a|x86_64\n --release\n\
|
||||
Options:\n --manifest-path PATH\n --package NAME\n --example NAME\n --abi arm64-v8a|x86_64\n --release\n\
|
||||
\x20 --application-id ID\n --label TEXT\n --keystore PATH --key-alias ALIAS\n\n\
|
||||
Release signing passwords come from IRIS_KEYSTORE_PASSWORD and, when different,\n\
|
||||
IRIS_KEY_PASSWORD. Iris uses the Android SDK and emulator/device supplied by you."
|
||||
@@ -49,6 +49,7 @@ fn print_help() {
|
||||
struct Options {
|
||||
manifest_path: Option<PathBuf>,
|
||||
package: Option<String>,
|
||||
example: Option<String>,
|
||||
abi: String,
|
||||
release: bool,
|
||||
application_id: Option<String>,
|
||||
@@ -77,6 +78,7 @@ impl Options {
|
||||
options.manifest_path = Some(value("--manifest-path", &mut i)?.into())
|
||||
}
|
||||
"--package" => options.package = Some(value("--package", &mut i)?),
|
||||
"--example" => options.example = Some(value("--example", &mut i)?),
|
||||
"--abi" => options.abi = value("--abi", &mut i)?,
|
||||
"--application-id" => {
|
||||
options.application_id = Some(value("--application-id", &mut i)?)
|
||||
@@ -128,43 +130,64 @@ fn build(options: &Options) -> Result<Built, String> {
|
||||
metadata.root_package(),
|
||||
options.package.as_deref(),
|
||||
)?;
|
||||
let target = package
|
||||
.targets
|
||||
.iter()
|
||||
.find(|target| {
|
||||
target
|
||||
.crate_types
|
||||
.iter()
|
||||
.any(|kind| kind == &CrateType::CDyLib)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"package {} has no cdylib target; add `[lib] crate-type = [\"cdylib\", \"rlib\"]` to {}",
|
||||
package.name, package.manifest_path
|
||||
)
|
||||
})?;
|
||||
let target = select_target(package, options.example.as_deref())?;
|
||||
let sdk = Sdk::find()?;
|
||||
let application_id = options
|
||||
.application_id
|
||||
.clone()
|
||||
.or_else(|| metadata_string(package, "application-id"))
|
||||
.unwrap_or_else(|| default_application_id(&package.name));
|
||||
.or_else(|| {
|
||||
options
|
||||
.example
|
||||
.is_none()
|
||||
.then(|| metadata_string(package, "application-id"))
|
||||
.flatten()
|
||||
})
|
||||
.unwrap_or_else(|| default_application_id(&package.name, options.example.as_deref()));
|
||||
validate_application_id(&application_id)?;
|
||||
let label = options
|
||||
.label
|
||||
.clone()
|
||||
.or_else(|| metadata_string(package, "label"))
|
||||
.unwrap_or_else(|| package.name.to_string());
|
||||
.or_else(|| {
|
||||
options
|
||||
.example
|
||||
.is_none()
|
||||
.then(|| metadata_string(package, "label"))
|
||||
.flatten()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
options
|
||||
.example
|
||||
.clone()
|
||||
.unwrap_or_else(|| package.name.to_string())
|
||||
});
|
||||
let variant = if options.release { "release" } else { "debug" };
|
||||
let artifact = options.example.as_ref().map_or_else(
|
||||
|| package.name.to_string(),
|
||||
|example| format!("{}-{example}", package.name),
|
||||
);
|
||||
let output = metadata
|
||||
.target_directory
|
||||
.as_std_path()
|
||||
.join("iris-android")
|
||||
.join(package.name.as_str())
|
||||
.join(&artifact)
|
||||
.join(variant)
|
||||
.join(&options.abi);
|
||||
recreate(&output)?;
|
||||
let native = output.join("native");
|
||||
let (build_manifest, library_name) = if options.example.is_some() {
|
||||
let wrapper = metadata
|
||||
.target_directory
|
||||
.as_std_path()
|
||||
.join("iris-android")
|
||||
.join("example-wrappers")
|
||||
.join(&artifact);
|
||||
materialize_example_wrapper(&metadata.packages, package, target, &wrapper)?
|
||||
} else {
|
||||
(
|
||||
package.manifest_path.as_std_path().to_path_buf(),
|
||||
target.name.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut cargo = Command::new("cargo");
|
||||
cargo
|
||||
@@ -173,7 +196,8 @@ fn build(options: &Options) -> Result<Built, String> {
|
||||
.arg("build")
|
||||
.arg("--lib")
|
||||
.arg("--manifest-path")
|
||||
.arg(package.manifest_path.as_std_path());
|
||||
.arg(&build_manifest)
|
||||
.env("CARGO_TARGET_DIR", metadata.target_directory.as_std_path());
|
||||
if options.release {
|
||||
cargo.arg("--release");
|
||||
}
|
||||
@@ -185,13 +209,13 @@ fn build(options: &Options) -> Result<Built, String> {
|
||||
|
||||
let library = native
|
||||
.join(&options.abi)
|
||||
.join(format!("lib{}.so", target.name));
|
||||
.join(format!("lib{library_name}.so"));
|
||||
if !library.is_file() {
|
||||
return Err(format!("cargo-ndk did not produce {}", library.display()));
|
||||
}
|
||||
let classes = output.join("classes");
|
||||
fs::create_dir_all(&classes).map_err(io_error("create Java output", &classes))?;
|
||||
let sources = materialize_host(&output, &target.name)?;
|
||||
let sources = materialize_host(&output, &library_name)?;
|
||||
let java_files = files_with_extension(&sources, "java")?;
|
||||
let mut javac = Command::new("javac");
|
||||
javac
|
||||
@@ -229,7 +253,7 @@ fn build(options: &Options) -> Result<Built, String> {
|
||||
let manifest = output.join("AndroidManifest.xml");
|
||||
fs::write(
|
||||
&manifest,
|
||||
manifest_xml(&application_id, &label, &target.name, sdk.api),
|
||||
manifest_xml(&application_id, &label, &library_name, sdk.api),
|
||||
)
|
||||
.map_err(io_error("write Android manifest", &manifest))?;
|
||||
let unsigned = output.join("unsigned.apk");
|
||||
@@ -258,7 +282,7 @@ fn build(options: &Options) -> Result<Built, String> {
|
||||
&dex.join("classes.dex"),
|
||||
&library,
|
||||
&options.abi,
|
||||
&target.name,
|
||||
&library_name,
|
||||
)?;
|
||||
|
||||
let aligned = output.join("aligned.apk");
|
||||
@@ -272,7 +296,7 @@ fn build(options: &Options) -> Result<Built, String> {
|
||||
"APK alignment",
|
||||
"install Android SDK Build Tools",
|
||||
)?;
|
||||
let apk = output.join(format!("{}-{variant}.apk", package.name));
|
||||
let apk = output.join(format!("{artifact}-{variant}.apk"));
|
||||
sign(&sdk, options, &aligned, &apk)?;
|
||||
verify(&sdk, &apk)?;
|
||||
Ok(Built {
|
||||
@@ -298,6 +322,106 @@ fn select_package<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
fn select_target<'a>(package: &'a Package, example: Option<&str>) -> Result<&'a Target, String> {
|
||||
if let Some(example) = example {
|
||||
return package
|
||||
.targets
|
||||
.iter()
|
||||
.find(|target| {
|
||||
target.name == example
|
||||
&& target.kind.iter().any(|kind| kind == &TargetKind::Example)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"package {} has no example named {example:?}; use one of: {}",
|
||||
package.name,
|
||||
package
|
||||
.targets
|
||||
.iter()
|
||||
.filter(|target| target
|
||||
.kind
|
||||
.iter()
|
||||
.any(|kind| kind == &TargetKind::Example))
|
||||
.map(|target| target.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
});
|
||||
}
|
||||
package
|
||||
.targets
|
||||
.iter()
|
||||
.find(|target| {
|
||||
target
|
||||
.crate_types
|
||||
.iter()
|
||||
.any(|kind| kind == &CrateType::CDyLib)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"package {} has no cdylib target; add `[lib] crate-type = [\"cdylib\", \"rlib\"]` to {}",
|
||||
package.name, package.manifest_path
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn materialize_example_wrapper(
|
||||
packages: &[Package],
|
||||
package: &Package,
|
||||
example: &Target,
|
||||
wrapper: &Path,
|
||||
) -> Result<(PathBuf, String), String> {
|
||||
let android_source = example
|
||||
.src_path
|
||||
.as_std_path()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("android.rs");
|
||||
if !android_source.is_file() {
|
||||
return Err(format!(
|
||||
"example {:?} has no Android entry point at {}; put shared code in lib.rs and add sibling desktop.rs and android.rs entries",
|
||||
example.name,
|
||||
android_source.display()
|
||||
));
|
||||
}
|
||||
let iris = packages
|
||||
.iter()
|
||||
.find(|dependency| dependency.name == "iris")
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"example {:?} does not depend on Iris; add `iris` to {}",
|
||||
example.name, package.manifest_path
|
||||
)
|
||||
})?;
|
||||
let source_dir = wrapper.join("src");
|
||||
fs::create_dir_all(&source_dir)
|
||||
.map_err(io_error("create Android example wrapper", &source_dir))?;
|
||||
let library_name = format!(
|
||||
"iris_android_{}_{}",
|
||||
identifier_segment(&package.name),
|
||||
identifier_segment(&example.name)
|
||||
);
|
||||
let iris_root = iris.manifest_path.parent().unwrap();
|
||||
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",
|
||||
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))?;
|
||||
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))?;
|
||||
Ok((manifest_path, library_name))
|
||||
}
|
||||
|
||||
fn metadata_string(package: &Package, key: &str) -> Option<String> {
|
||||
package
|
||||
.metadata
|
||||
@@ -308,8 +432,8 @@ fn metadata_string(package: &Package, key: &str) -> Option<String> {
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
fn default_application_id(package: &str) -> String {
|
||||
let segment: String = package
|
||||
fn identifier_segment(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
@@ -318,8 +442,15 @@ fn default_application_id(package: &str) -> String {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("dev.iris.app.{segment}")
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn default_application_id(package: &str, example: Option<&str>) -> String {
|
||||
let package = identifier_segment(package);
|
||||
match example {
|
||||
Some(example) => format!("dev.iris.example.{package}.{}", identifier_segment(example)),
|
||||
None => format!("dev.iris.app.{package}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_application_id(id: &str) -> Result<(), String> {
|
||||
@@ -699,6 +830,10 @@ mod tests {
|
||||
assert!(validate_application_id("one").is_err());
|
||||
assert!(validate_application_id("dev.2demo").is_err());
|
||||
assert!(validate_application_id("dev.iris.bad-name").is_err());
|
||||
assert_eq!(
|
||||
default_application_id("demo-app", Some("color-picker")),
|
||||
"dev.iris.example.demo_app.color_picker"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user