use anyhow::{Result, bail};
use std::path::{Path, PathBuf};
const BINARY_EXTS: &[&str] = &["jar", "webp", "png", "ico"];
const BINARY_NAMES: &[&str] = &["gradle-wrapper.jar", "gradlew.bat"];
pub(crate) struct Subs {
pub name: String,
pub package: String,
pub package_path: String,
pub package_shared: String,
pub package_shared_types: String,
pub ndk_version: String,
}
impl Subs {
pub(crate) fn from_package(package: String, name: String, ndk_version: String) -> Subs {
Subs {
package_path: package.replace('.', "/"),
package_shared: format!("{package}.shared"),
package_shared_types: format!("{package}.shared.types"),
name,
package,
ndk_version,
}
}
pub(crate) fn from_app_root(root: &Path) -> Result<Subs> {
let java = root.join("Android/app/src/main/java");
let main_activity = find_file(&java, "MainActivity.kt").ok_or_else(|| {
anyhow::anyhow!(
"couldn't find MainActivity.kt under {} — run this from a Mobiler app root",
java.display()
)
})?;
let pkg_dir = main_activity
.parent()
.and_then(|p| p.strip_prefix(&java).ok())
.ok_or_else(|| anyhow::anyhow!("could not derive package from MainActivity.kt"))?;
let package_path = pkg_dir.to_string_lossy().replace('\\', "/");
if package_path.is_empty() {
bail!("MainActivity.kt is not inside a package directory");
}
let package = package_path.replace('/', ".");
let name = read_app_name(root)
.unwrap_or_else(|| package.rsplit('.').next().unwrap_or("App").to_string());
Ok(Subs::from_package(package, name, String::new()))
}
}
pub(crate) fn templated_path(rel: &Path, subs: &Subs) -> PathBuf {
let as_str = rel.to_string_lossy();
let replaced = as_str.replace("__PACKAGE_PATH__", &subs.package_path);
let replaced = replaced.strip_suffix(".tmpl").unwrap_or(&replaced);
PathBuf::from(replaced)
}
pub(crate) fn substitute(raw: &str, subs: &Subs) -> String {
raw.replace("{{PACKAGE_SHARED_TYPES}}", &subs.package_shared_types)
.replace("{{PACKAGE_SHARED}}", &subs.package_shared)
.replace("{{PACKAGE_PATH}}", &subs.package_path)
.replace("{{PACKAGE}}", &subs.package)
.replace("{{NDK_VERSION}}", &subs.ndk_version)
.replace("{{NAME}}", &subs.name)
}
pub(crate) fn is_binary(p: &Path) -> bool {
if let Some(name) = p.file_name().and_then(|n| n.to_str()) {
if BINARY_NAMES.contains(&name) {
return true;
}
}
p.extension()
.and_then(|e| e.to_str())
.map(|e| BINARY_EXTS.contains(&e))
.unwrap_or(false)
}
fn find_file(dir: &Path, name: &str) -> Option<PathBuf> {
let mut subdirs = Vec::new();
for entry in std::fs::read_dir(dir).ok()?.flatten() {
let path = entry.path();
if path.is_dir() {
subdirs.push(path);
} else if path.file_name().is_some_and(|n| n == name) {
return Some(path);
}
}
subdirs.iter().find_map(|d| find_file(d, name))
}
fn read_app_name(root: &Path) -> Option<String> {
let settings = std::fs::read_to_string(root.join("Android/settings.gradle.kts")).ok()?;
let line = settings.lines().find(|l| l.contains("rootProject.name"))?;
let start = line.find('"')? + 1;
let end = line[start..].find('"')? + start;
Some(line[start..end].to_string())
}