use std::path::{Path, PathBuf};
use askama::Template;
use eyre::{self, bail};
use smol::{fs, unblock};
use target_lexicon::{Aarch64Architecture, Architecture, Triple};
use tracing::{debug, info};
use std::str::FromStr;
use crate::{
android::{
backend::AndroidBackend,
output_metadata::{OutputKind, packaged_artifact},
toolchain::{AndroidNdk, AndroidSdk, Java, Kotlin, java_proxy_properties_from_env},
},
assets::{self, ResolvedFont},
build::{BuildOptions, BuildProgress, RustBuild, RustDynamicLibraries, RustLinkage},
device::Artifact,
platform::{PackageOptions, TargetPlatform},
project::Project,
templates::FontRegistrationTemplateEntry,
toolchain::{Host, ToolchainError, windows_arm64_llvm::WindowsArm64LlvmToolchain},
utils::copy_file,
};
fn gradle_cmd(gradlew: &Path, backend_path: &Path, task: &str) -> smol::process::Command {
let mut cmd = smol::process::Command::new(gradlew);
cmd.arg(task).arg("--project-dir").arg(backend_path);
cmd
}
fn apply_gradle_proxy_env(host: &Host, cmd: &mut smol::process::Command) -> eyre::Result<()> {
let proxy_properties = java_proxy_properties_from_env(host)?;
if proxy_properties.is_empty() {
return Ok(());
}
cmd.args(&proxy_properties);
let mut gradle_opts = proxy_properties.join(" ");
if let Some(existing) = host.env_string("GRADLE_OPTS")
&& !existing.trim().is_empty()
{
gradle_opts.push(' ');
gradle_opts.push_str(&existing);
}
cmd.env("GRADLE_OPTS", gradle_opts);
Ok(())
}
fn ndk_host_tag(ndk_path: &Path) -> &'static str {
use target_lexicon::{Architecture, OperatingSystem, Triple};
let host = Triple::host();
match (&host.operating_system, &host.architecture) {
(OperatingSystem::Darwin(_), Architecture::Aarch64(_)) => {
let native = ndk_path
.join("toolchains/llvm/prebuilt")
.join("darwin-arm64");
if native.exists() {
"darwin-arm64"
} else {
"darwin-x86_64"
}
}
(OperatingSystem::Darwin(_), _) => "darwin-x86_64",
(OperatingSystem::Windows, _) => "windows-x86_64",
(OperatingSystem::Linux, _) => "linux-x86_64",
_ => panic!("Unsupported host triple for Android NDK: {host}"),
}
}
fn ndk_bin_dir(ndk_path: &Path) -> PathBuf {
ndk_path
.join("toolchains/llvm/prebuilt")
.join(ndk_host_tag(ndk_path))
.join("bin")
}
fn ndk_ar_path(ndk_path: &Path) -> PathBuf {
ndk_bin_dir(ndk_path).join("llvm-ar")
}
fn ndk_clang_path(ndk_path: &Path, abi: AndroidAbi, cxx: bool, api_level: u32) -> PathBuf {
let suffix = if cxx { "clang++" } else { "clang" };
ndk_bin_dir(ndk_path).join(format!("{}{api_level}-{suffix}", abi.ndk_target()))
}
fn ndk_linker_path(ndk_path: &Path, abi: AndroidAbi, api_level: u32) -> PathBuf {
ndk_clang_path(ndk_path, abi, false, api_level)
}
fn ndk_builtins_lib(ndk_path: &Path, abi: AndroidAbi) -> eyre::Result<PathBuf> {
let arch = match abi {
AndroidAbi::Arm64V8a => "aarch64",
AndroidAbi::X86_64 => "x86_64",
AndroidAbi::ArmeabiV7a => "arm",
AndroidAbi::X86 => "i686",
};
let clang_libs = ndk_path
.join("toolchains/llvm/prebuilt")
.join(ndk_host_tag(ndk_path))
.join("lib/clang");
let mut candidates: Vec<PathBuf> = std::fs::read_dir(&clang_libs)
.map_err(|error| {
eyre::eyre!(
"Failed to read NDK clang libraries at {}: {error}",
clang_libs.display()
)
})?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.map(|version_dir| {
version_dir.join(format!("lib/linux/libclang_rt.builtins-{arch}-android.a"))
})
.filter(|path| path.is_file())
.collect();
candidates.sort_unstable();
match candidates.as_slice() {
[path] => Ok(path.clone()),
[] => Err(eyre::eyre!(
"The NDK at {} ships no libclang_rt.builtins-{arch}-android.a; \
a `-Zbuild-std` build needs it for the compiler-rt builtins",
ndk_path.display()
)),
_ => Err(eyre::eyre!(
"The NDK at {} ships multiple libclang_rt.builtins-{arch}-android.a \
copies: {candidates:?}",
ndk_path.display()
)),
}
}
async fn create_android_toolchain_wrapper(
ndk_path: &Path,
abi: AndroidAbi,
api_level: u32,
) -> eyre::Result<PathBuf> {
let wrapper_dir = std::env::temp_dir().join("waterui-cmake-toolchains");
fs::create_dir_all(&wrapper_dir).await?;
let wrapper_path = wrapper_dir.join(format!("android-{}.cmake", abi.as_str()));
let ndk_toolchain = ndk_path.join("build/cmake/android.toolchain.cmake");
let content = format!(
include_str!("android_toolchain_wrapper.cmake.tpl"),
abi = abi.as_str(),
api_level = api_level,
ndk_toolchain = ndk_toolchain.display(),
asm_compiler = ndk_clang_path(ndk_path, abi, false, api_level).display(),
);
fs::write(&wrapper_path, content).await?;
Ok(wrapper_path)
}
fn ndk_cxx_path(ndk_path: &Path, abi: AndroidAbi, api_level: u32) -> PathBuf {
ndk_clang_path(ndk_path, abi, true, api_level)
}
fn ndk_libcxx_path(ndk_path: &Path, abi: AndroidAbi) -> PathBuf {
let new_path = ndk_path
.join("toolchains/llvm/prebuilt")
.join(ndk_host_tag(ndk_path))
.join("sysroot/usr/lib")
.join(abi.ndk_libcxx_triple())
.join("libc++_shared.so");
if new_path.exists() {
return new_path;
}
ndk_path
.join("sources/cxx-stl/llvm-libc++/libs")
.join(abi.as_str())
.join("libc++_shared.so")
}
pub(crate) const ANDROID_MAX_PAGE_SIZE_LINK_ARG: &str = "-Clink-arg=-Wl,-z,max-page-size=16384";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AndroidAbi {
Arm64V8a,
X86_64,
ArmeabiV7a,
X86,
}
#[derive(Debug, thiserror::Error)]
#[error("Unsupported Android ABI: {abi}")]
pub struct UnsupportedAndroidAbi {
abi: String,
}
impl FromStr for AndroidAbi {
type Err = UnsupportedAndroidAbi;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"arm64-v8a" => Ok(Self::Arm64V8a),
"x86_64" => Ok(Self::X86_64),
"armeabi-v7a" => Ok(Self::ArmeabiV7a),
"x86" => Ok(Self::X86),
other => Err(UnsupportedAndroidAbi {
abi: other.to_string(),
}),
}
}
}
impl AndroidAbi {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Arm64V8a => "arm64-v8a",
Self::X86_64 => "x86_64",
Self::ArmeabiV7a => "armeabi-v7a",
Self::X86 => "x86",
}
}
#[must_use]
pub const fn ndk_target(self) -> &'static str {
match self {
Self::Arm64V8a => "aarch64-linux-android",
Self::X86_64 => "x86_64-linux-android",
Self::ArmeabiV7a => "armv7a-linux-androideabi",
Self::X86 => "i686-linux-android",
}
}
#[must_use]
pub const fn ndk_libcxx_triple(self) -> &'static str {
match self {
Self::Arm64V8a => "aarch64-linux-android",
Self::X86_64 => "x86_64-linux-android",
Self::ArmeabiV7a => "arm-linux-androideabi",
Self::X86 => "i686-linux-android",
}
}
#[must_use]
pub const fn from_triple(triple: &Triple) -> Option<Self> {
match triple.architecture {
Architecture::Aarch64(_) => Some(Self::Arm64V8a),
Architecture::X86_64 => Some(Self::X86_64),
Architecture::Arm(target_lexicon::ArmArchitecture::Armv7) => Some(Self::ArmeabiV7a),
Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => Some(Self::X86),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AndroidPlatform {
abi: AndroidAbi,
}
struct AndroidBuildContext {
abi: AndroidAbi,
ndk_path: PathBuf,
linker: PathBuf,
ar: PathBuf,
cxx: PathBuf,
target_underscore: String,
target_upper: String,
llvm_envs: Vec<(String, std::ffi::OsString)>,
java_home: PathBuf,
java_bin_dir: PathBuf,
kotlin_compiler: PathBuf,
kotlin_bin_dir: PathBuf,
kotlin_home: PathBuf,
sdk_path: PathBuf,
android_jar: PathBuf,
wrapper_toolchain: PathBuf,
android_platform: String,
}
impl AndroidPlatform {
#[must_use]
pub const fn new(abi: AndroidAbi) -> Self {
Self { abi }
}
#[must_use]
pub const fn arm64() -> Self {
Self {
abi: AndroidAbi::Arm64V8a,
}
}
#[must_use]
pub const fn x86_64() -> Self {
Self {
abi: AndroidAbi::X86_64,
}
}
#[must_use]
pub const fn abi(&self) -> AndroidAbi {
self.abi
}
#[must_use]
pub const fn abi_str(&self) -> &'static str {
self.abi.as_str()
}
pub fn try_from_abi(abi: &str) -> eyre::Result<Self> {
let abi = AndroidAbi::from_str(abi).map_err(|e| eyre::eyre!(e))?;
Ok(Self { abi })
}
}
pub const ALL_ABIS: &[AndroidAbi] = &[
AndroidAbi::Arm64V8a,
AndroidAbi::X86_64,
AndroidAbi::ArmeabiV7a,
AndroidAbi::X86,
];
impl AndroidPlatform {
#[must_use]
pub fn all() -> Vec<Self> {
ALL_ABIS.iter().copied().map(Self::new).collect()
}
#[must_use]
pub const fn triple(&self) -> Triple {
let architecture = match self.abi {
AndroidAbi::Arm64V8a => Architecture::Aarch64(Aarch64Architecture::Aarch64),
AndroidAbi::X86_64 => Architecture::X86_64,
AndroidAbi::ArmeabiV7a => Architecture::Arm(target_lexicon::ArmArchitecture::Armv7),
AndroidAbi::X86 => Architecture::X86_32(target_lexicon::X86_32Architecture::I686),
};
Triple {
architecture,
vendor: target_lexicon::Vendor::Unknown,
operating_system: target_lexicon::OperatingSystem::Linux,
environment: target_lexicon::Environment::Android,
binary_format: target_lexicon::BinaryFormat::Elf,
}
}
pub async fn build(&self, project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
let options = if options.loads_dynamic_modules() {
options
} else {
options.with_static_runtime()
};
let font_declarations = crate::assets::scan_fonts(project).await?;
let _resolved_fonts = crate::assets::resolve_fonts(font_declarations).await?;
let abi = self.abi();
let triple = self.triple();
let min_api_level = project
.resolved_framework()
.await?
.android_min_api_level()?;
let host = Host::current();
let build_context =
resolve_android_build_context(&host, abi, &triple, min_api_level).await?;
let build = configure_android_rust_build(&host, project, &triple, &build_context, &options)
.await?
.with_envs(options.cargo_envs().iter().cloned())
.with_target_dir(project.water_target_dir(options.linkage()).await?);
let built_target = build.build_lib(options.is_release()).await?;
copy_android_build_outputs(
project,
&options,
abi,
&build_context.ndk_path,
&built_target.profile_dir,
&built_target.artifact,
)
.await?;
Ok(built_target.profile_dir)
}
pub async fn clean_jni_libs(project: &Project) -> eyre::Result<()> {
let jni_libs_dir = project
.backend_path::<AndroidBackend>()
.join("app/src/main/jniLibs");
if jni_libs_dir.exists() {
fs::remove_dir_all(&jni_libs_dir).await?;
}
Ok(())
}
pub async fn package_with_abis(
project: &Project,
options: PackageOptions,
abis: &[AndroidAbi],
) -> eyre::Result<Artifact> {
let backend_path = project.backend_path::<AndroidBackend>();
copy_assets_and_fonts(
project,
&backend_path,
None,
options.uses_dev_server(),
options.progress(),
)
.await?;
let gradlew = backend_path.join(if cfg!(windows) {
"gradlew.bat"
} else {
"gradlew"
});
let (command_name, output_kind, variant) =
match (options.is_distribution(), options.is_debug()) {
(true, false) => ("bundleRelease", OutputKind::Bundle, "release"),
(false, false) => ("assembleRelease", OutputKind::Apk, "release"),
(false, true) => ("assembleDebug", OutputKind::Apk, "debug"),
(true, true) => ("bundleDebug", OutputKind::Bundle, "debug"),
};
let abis_str = abis
.iter()
.map(|a| a.as_str())
.collect::<Vec<_>>()
.join(",");
let mut cmd = gradle_cmd(&gradlew, &backend_path, command_name);
cmd.env("WATERUI_SKIP_RUST_BUILD", "1")
.env("WATERUI_ANDROID_ABIS", &abis_str);
let host = Host::current();
if let Some(java_home) = Java::detect_home(&host).await {
cmd.env("JAVA_HOME", java_home);
}
if let Some(sdk_path) = AndroidSdk::detect_path(&host) {
cmd.env("ANDROID_HOME", &sdk_path)
.env("ANDROID_SDK_ROOT", &sdk_path);
}
apply_gradle_proxy_env(&host, &mut cmd)?;
let output = cmd.output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
bail!("Gradle build failed:\n{}\n{}", stdout.trim(), stderr.trim());
}
let path = packaged_artifact(&backend_path, output_kind, variant).await?;
Ok(Artifact::new(project.bundle_identifier(), path))
}
pub async fn list_avds(host: &Host) -> eyre::Result<Vec<String>> {
let emulator_path = AndroidSdk::emulator_path(host)
.ok_or_else(|| eyre::eyre!("Android emulator not found"))?;
let output = host.output(&emulator_path, ["-list-avds"]).await?;
let stdout = String::from_utf8_lossy(&output.stdout);
let avds: Vec<String> = stdout
.lines()
.filter(|line| !line.is_empty())
.map(String::from)
.collect();
Ok(avds)
}
}
async fn resolve_android_build_context(
host: &Host,
abi: AndroidAbi,
triple: &Triple,
api_level: u32,
) -> eyre::Result<AndroidBuildContext> {
let ndk_path = AndroidNdk::detect_path(host).ok_or_else(|| {
eyre::eyre!("Android NDK not found. Please install it via Android Studio.")
})?;
let linker = ndk_linker_path(&ndk_path, abi, api_level);
let ar = ndk_ar_path(&ndk_path);
let cxx = ndk_cxx_path(&ndk_path, abi, api_level);
for wrapper in [&linker, &cxx] {
if !wrapper.is_file() {
eyre::bail!(
"the Android NDK at {} ships no compiler wrapper for API {api_level} \
({}); the framework's android-min-api-level needs an NDK that targets it",
ndk_path.display(),
wrapper.display()
);
}
}
let target_underscore = triple.to_string().replace('-', "_");
let target_upper = target_underscore.to_uppercase();
let llvm_envs = resolve_windows_arm64_llvm_envs(host).await?;
let (java_home, java_bin_dir) = resolve_java_home(host).await?;
let (kotlin_compiler, kotlin_bin_dir, kotlin_home) = resolve_kotlin_home(host).await?;
let (sdk_path, android_jar) = resolve_android_sdk_paths(host).await?;
let wrapper_toolchain = create_android_toolchain_wrapper(&ndk_path, abi, api_level).await?;
Ok(AndroidBuildContext {
abi,
ndk_path,
linker,
ar,
cxx,
target_underscore,
target_upper,
llvm_envs,
java_home,
java_bin_dir,
kotlin_compiler,
kotlin_bin_dir,
kotlin_home,
sdk_path,
android_jar,
wrapper_toolchain,
android_platform: format!("android-{api_level}"),
})
}
async fn resolve_windows_arm64_llvm_envs(
host: &Host,
) -> eyre::Result<Vec<(String, std::ffi::OsString)>> {
WindowsArm64LlvmToolchain
.cargo_envs(host)
.await
.map_err(|error| match error {
ToolchainError::Fixable(_) => eyre::eyre!(
"Windows ARM64 LLVM toolchain is missing. Run `water doctor --fix` to install it automatically."
),
ToolchainError::Unfixable(unfixable) => {
eyre::eyre!("Windows ARM64 LLVM toolchain check failed: {unfixable}")
}
})
}
async fn resolve_java_home(host: &Host) -> eyre::Result<(PathBuf, PathBuf)> {
let java_home = Java::detect_home(host).await.ok_or_else(|| {
eyre::eyre!(
"Java runtime not found. Install a JDK (or Android Studio JBR), then re-run `water doctor --fix`."
)
})?;
let java_bin_dir = java_home.join("bin");
Ok((java_home, java_bin_dir))
}
async fn resolve_kotlin_home(host: &Host) -> eyre::Result<(PathBuf, PathBuf, PathBuf)> {
let kotlin_compiler = Kotlin::detect_path(host).await.ok_or_else(|| {
eyre::eyre!(
"Kotlin compiler (kotlinc) not found. Install Android Studio or set `KOTLIN_HOME`, then re-run `water doctor`."
)
})?;
let kotlin_bin_dir = kotlin_compiler.parent().map(PathBuf::from).ok_or_else(|| {
eyre::eyre!(
"Failed to determine Kotlin bin directory from `{}`.",
kotlin_compiler.display()
)
})?;
let kotlin_home = kotlin_bin_dir.parent().map(PathBuf::from).ok_or_else(|| {
eyre::eyre!(
"Failed to determine KOTLIN_HOME from `{}`.",
kotlin_bin_dir.display()
)
})?;
Ok((kotlin_compiler, kotlin_bin_dir, kotlin_home))
}
async fn resolve_android_sdk_paths(host: &Host) -> eyre::Result<(PathBuf, PathBuf)> {
let host = host.clone();
smol::unblock(move || {
let sdk_path = AndroidSdk::detect_path(&host).ok_or_else(|| {
eyre::eyre!("Android SDK not found. Please install it via Android Studio.")
})?;
let android_jar = AndroidSdk::android_jar_path(&host).ok_or_else(|| {
eyre::eyre!(
"Android platforms not found in SDK at {}. Install an Android platform (SDK) in Android Studio.",
sdk_path.display()
)
})?;
Ok((sdk_path, android_jar))
})
.await
}
pub(crate) async fn android_ffi_dependency_features(
project: &Project,
) -> eyre::Result<Vec<String>> {
let mut features = vec!["waterui-ffi/android-jni".to_string()];
features.extend(crate::project_model::assets::capability_ffi_features(project).await?);
features.extend(crate::project_model::assets::self_drawn_realization_features(project).await?);
Ok(features)
}
async fn configure_android_rust_build(
host: &Host,
project: &Project,
triple: &Triple,
context: &AndroidBuildContext,
options: &BuildOptions,
) -> eyre::Result<RustBuild> {
let mut build = RustBuild::new(project.ffi_crate_path(), triple.clone())
.with_project(project)
.with_features(android_ffi_dependency_features(project).await?)
.with_crate_type_override("cdylib")
.with_rustc_flag(ANDROID_MAX_PAGE_SIZE_LINK_ARG);
if options.linkage() == RustLinkage::SharedRuntime {
let nightly = crate::toolchain::rust::nightly_toolchain_with_rust_src(host).await?;
build = build
.with_feature("dev")
.with_preferred_dynamic_linking()
.with_build_std(nightly)
.with_env(
"LLVM_COMPILER_RT_LIB",
ndk_builtins_lib(&context.ndk_path, context.abi)?,
);
}
if let Some(sccache_path) = options.sccache_path() {
build = build.with_sccache(sccache_path.to_path_buf());
}
if let Some(progress) = options.progress() {
build = build.with_progress(progress.clone());
}
for (key, value) in &context.llvm_envs {
build = build.with_env(key.clone(), value.clone());
}
build = build.with_envs(android_cargo_envs(context, triple));
let current_path = host
.env("PATH")
.ok_or_else(|| eyre::eyre!("PATH environment variable is not set"))?;
let mut paths: Vec<PathBuf> = std::env::split_paths(¤t_path).collect();
paths.insert(0, context.java_bin_dir.clone());
paths.insert(0, context.kotlin_bin_dir.clone());
let new_path = std::env::join_paths(paths).map_err(|error| {
eyre::eyre!("Failed to construct PATH for Java/Kotlin compiler resolution: {error}")
})?;
Ok(build.with_env("PATH", new_path))
}
fn android_cargo_envs(
context: &AndroidBuildContext,
triple: &Triple,
) -> Vec<(String, std::ffi::OsString)> {
[
(
format!("CARGO_TARGET_{}_LINKER", context.target_upper),
context.linker.as_os_str().to_os_string(),
),
(
format!("CARGO_TARGET_{}_AR", context.target_upper),
context.ar.as_os_str().to_os_string(),
),
(
format!("CC_{}", context.target_underscore),
context.linker.as_os_str().to_os_string(),
),
(
format!("CXX_{}", context.target_underscore),
context.cxx.as_os_str().to_os_string(),
),
(
format!("AR_{}", context.target_underscore),
context.ar.as_os_str().to_os_string(),
),
(
"ANDROID_NDK".to_string(),
context.ndk_path.as_os_str().to_os_string(),
),
(
"ANDROID_NDK_HOME".to_string(),
context.ndk_path.as_os_str().to_os_string(),
),
(
"ANDROID_NDK_ROOT".to_string(),
context.ndk_path.as_os_str().to_os_string(),
),
(
"ANDROID_HOME".to_string(),
context.sdk_path.as_os_str().to_os_string(),
),
(
"ANDROID_SDK_ROOT".to_string(),
context.sdk_path.as_os_str().to_os_string(),
),
(
"ANDROID_JAR".to_string(),
context.android_jar.as_os_str().to_os_string(),
),
(
"JAVA_HOME".to_string(),
context.java_home.as_os_str().to_os_string(),
),
(
"KOTLIN_HOME".to_string(),
context.kotlin_home.as_os_str().to_os_string(),
),
(
"KOTLINC".to_string(),
context.kotlin_compiler.as_os_str().to_os_string(),
),
(
"CMAKE_TOOLCHAIN_FILE".to_string(),
context.wrapper_toolchain.as_os_str().to_os_string(),
),
(
format!("CMAKE_TOOLCHAIN_FILE_{}", context.target_underscore),
context.wrapper_toolchain.as_os_str().to_os_string(),
),
(
"CMAKE_ASM_COMPILER".to_string(),
context.linker.as_os_str().to_os_string(),
),
("ANDROID_ABI".to_string(), context.abi.as_str().into()),
(
"ANDROID_PLATFORM".to_string(),
context.android_platform.clone().into(),
),
("PKG_CONFIG_ALLOW_CROSS".to_string(), "1".into()),
(
format!("PKG_CONFIG_ALLOW_CROSS_{}", context.target_underscore),
"1".into(),
),
(format!("PKG_CONFIG_ALLOW_CROSS_{triple}"), "1".into()),
]
.into_iter()
.collect()
}
pub(crate) async fn android_rust_build_envs(
host: &Host,
project: &Project,
abi: AndroidAbi,
triple: &Triple,
build_std: bool,
) -> eyre::Result<Vec<(String, std::ffi::OsString)>> {
let min_api_level = project
.resolved_framework()
.await?
.android_min_api_level()?;
let context = resolve_android_build_context(host, abi, triple, min_api_level).await?;
let mut envs = android_cargo_envs(&context, triple);
if build_std {
envs.push((
"LLVM_COMPILER_RT_LIB".to_string(),
ndk_builtins_lib(&context.ndk_path, abi)?.into_os_string(),
));
}
Ok(envs)
}
async fn copy_android_build_outputs(
project: &Project,
options: &BuildOptions,
abi: AndroidAbi,
ndk_path: &Path,
lib_dir: &Path,
source_lib: &Path,
) -> eyre::Result<()> {
let output_dir = options.output_dir().map_or_else(
|| {
project
.backend_path::<AndroidBackend>()
.join("app/src/main/jniLibs")
.join(abi.as_str())
},
std::path::Path::to_path_buf,
);
fs::create_dir_all(&output_dir).await?;
copy_file(source_lib, &output_dir.join("libwaterui_app.so")).await?;
if options.linkage() == RustLinkage::SharedRuntime {
let triple = AndroidPlatform::new(abi).triple();
let libraries = RustDynamicLibraries::resolve(lib_dir, &triple, project).await?;
libraries.stage(&output_dir).await?;
} else {
RustDynamicLibraries::remove_staged(&output_dir, &AndroidPlatform::new(abi).triple())
.await?;
}
let libcxx_target = output_dir.join("libc++_shared.so");
if staged_libs_need_libcxx(&output_dir).await? {
let libcxx_path = ndk_libcxx_path(ndk_path, abi);
if libcxx_path.exists() {
copy_file(&libcxx_path, &libcxx_target).await?;
}
} else if libcxx_target.exists() {
fs::remove_file(&libcxx_target).await?;
}
crate::elf::require_aligned_shared_libraries(&output_dir).await?;
Ok(())
}
async fn staged_libs_need_libcxx(output_dir: &Path) -> eyre::Result<bool> {
let output_dir = output_dir.to_path_buf();
unblock(move || {
let mut needs = false;
for entry in std::fs::read_dir(&output_dir)? {
let path = entry?.path();
if path.extension() != Some(std::ffi::OsStr::new("so")) {
continue;
}
let needed = std::fs::read(&path)
.ok()
.and_then(|data| elf_needs_libcxx(&data))
.unwrap_or_else(|| {
tracing::warn!(
library = %path.display(),
"could not parse staged library; assuming it needs libc++_shared.so"
);
true
});
needs |= needed;
}
Ok(needs)
})
.await
}
fn elf_needs_libcxx(data: &[u8]) -> Option<bool> {
use object::read::elf::{Dyn as _, ElfFile, FileHeader};
fn scan<Elf>(data: &[u8]) -> Option<bool>
where
Elf: FileHeader<Endian = object::Endianness>,
{
let file = ElfFile::<Elf>::parse(data).ok()?;
let endian = file.endian();
let sections = file.elf_section_table();
let (dyns, strings_index) = sections.dynamic(endian, data).ok()??;
let strings = sections.strings(endian, data, strings_index).ok()?;
Some(dyns.iter().any(|d| {
d.tag32(endian) == Some(object::elf::DT_NEEDED)
&& d.string(endian, strings).ok() == Some(&b"libc++_shared.so"[..])
}))
}
scan::<object::elf::FileHeader64<object::Endianness>>(data)
.or_else(|| scan::<object::elf::FileHeader32<object::Endianness>>(data))
}
pub async fn clean_android(project: &Project) -> eyre::Result<()> {
let backend_path = project.backend_path::<AndroidBackend>();
let gradlew = backend_path.join(if cfg!(windows) {
"gradlew.bat"
} else {
"gradlew"
});
if !gradlew.exists() {
return Ok(());
}
let host = Host::current();
let mut cmd = gradle_cmd(&gradlew, &backend_path, "clean");
if let Some(java_home) = Java::detect_home(&host).await {
cmd.env("JAVA_HOME", java_home);
}
if let Some(sdk_path) = AndroidSdk::detect_path(&host) {
cmd.env("ANDROID_HOME", &sdk_path)
.env("ANDROID_SDK_ROOT", &sdk_path);
}
apply_gradle_proxy_env(&host, &mut cmd)?;
let output = cmd.output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Gradle clean failed: {}", stderr.trim());
}
Ok(())
}
#[must_use]
pub const fn is_android_platform(platform: TargetPlatform) -> bool {
matches!(platform, TargetPlatform::Android)
}
async fn copy_assets_and_fonts(
project: &Project,
backend_path: &Path,
sccache_path: Option<&Path>,
dev_server: bool,
progress: Option<&BuildProgress>,
) -> eyre::Result<()> {
let assets_dir = backend_path.join("app/src/main/assets");
let manifest = assets::stage_project_assets_for_android(
project,
backend_path,
sccache_path,
dev_server,
progress,
)
.await?;
let font_declarations = assets::scan_fonts(project).await?;
let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
resolved_fonts.extend(assets::scan_project_font_assets(&manifest)?);
if !resolved_fonts.is_empty() {
let fonts_dest = assets_dir.join("fonts");
assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
info!("Copied {} fonts to Android app", resolved_fonts.len());
}
let java_dir = backend_path.join("app/src/main/java");
generate_font_registration_kotlin(project, &resolved_fonts, &java_dir).await?;
Ok(())
}
#[derive(Template)]
#[template(
path = "src/templates/android_dynamic/WaterUIFonts.kt.tpl",
escape = "none"
)]
struct WaterUiFontsKotlinTemplate<'a> {
namespace: &'a str,
font_entries: &'a [FontRegistrationTemplateEntry],
}
async fn generate_font_registration_kotlin(
project: &Project,
fonts: &[ResolvedFont],
java_dir: &Path,
) -> eyre::Result<()> {
let namespace = project.bundle_identifier().android_package_name();
let legacy_path = java_dir.join("WaterUIFonts.kt");
let _ = fs::remove_file(&legacy_path).await;
let font_entries = fonts
.iter()
.map(|font| FontRegistrationTemplateEntry {
family_name: font.name.clone(),
file_name: font
.path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
.to_string(),
})
.collect::<Vec<_>>();
let content = WaterUiFontsKotlinTemplate {
namespace: namespace.as_str(),
font_entries: &font_entries,
}
.render()
.map_err(|error| eyre::eyre!("Failed to render WaterUIFonts.kt template: {error}"))?;
let package_dir = java_dir.join(namespace.as_str().replace('.', "/"));
fs::create_dir_all(&package_dir).await?;
let kotlin_path = package_dir.join("WaterUIFonts.kt");
fs::write(&kotlin_path, content).await?;
debug!("Generated {}", kotlin_path.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::elf_needs_libcxx;
#[test]
fn elf_needs_libcxx_rejects_non_elf_data() {
assert_eq!(elf_needs_libcxx(b"not an elf"), None);
assert_eq!(elf_needs_libcxx(&[]), None);
assert_eq!(elf_needs_libcxx(&[0x7f, b'E', b'L', b'F']), None);
}
}