1use std::path::{Path, PathBuf};
7
8use askama::Template;
9use eyre::{self, bail};
10use smol::{fs, unblock};
11use target_lexicon::{Aarch64Architecture, Architecture, Triple};
12
13use tracing::{debug, info};
14
15use std::str::FromStr;
16
17use crate::{
18 android::{
19 backend::AndroidBackend,
20 output_metadata::{OutputKind, packaged_artifact},
21 toolchain::{AndroidNdk, AndroidSdk, Java, Kotlin, java_proxy_properties_from_env},
22 },
23 assets::{self, ResolvedFont},
24 build::{BuildOptions, BuildProgress, RustBuild, RustDynamicLibraries, RustLinkage},
25 device::Artifact,
26 platform::{PackageOptions, TargetPlatform},
27 project::Project,
28 templates::FontRegistrationTemplateEntry,
29 toolchain::{Host, ToolchainError, windows_arm64_llvm::WindowsArm64LlvmToolchain},
30 utils::copy_file,
31};
32
33fn gradle_cmd(gradlew: &Path, backend_path: &Path, task: &str) -> smol::process::Command {
34 let mut cmd = smol::process::Command::new(gradlew);
35 cmd.arg(task).arg("--project-dir").arg(backend_path);
36 cmd
37}
38
39fn apply_gradle_proxy_env(host: &Host, cmd: &mut smol::process::Command) -> eyre::Result<()> {
40 let proxy_properties = java_proxy_properties_from_env(host)?;
41 if proxy_properties.is_empty() {
42 return Ok(());
43 }
44
45 cmd.args(&proxy_properties);
46
47 let mut gradle_opts = proxy_properties.join(" ");
48 if let Some(existing) = host.env_string("GRADLE_OPTS")
49 && !existing.trim().is_empty()
50 {
51 gradle_opts.push(' ');
52 gradle_opts.push_str(&existing);
53 }
54 cmd.env("GRADLE_OPTS", gradle_opts);
55 Ok(())
56}
57
58fn ndk_host_tag(ndk_path: &Path) -> &'static str {
63 use target_lexicon::{Architecture, OperatingSystem, Triple};
64
65 let host = Triple::host();
66
67 match (&host.operating_system, &host.architecture) {
68 (OperatingSystem::Darwin(_), Architecture::Aarch64(_)) => {
69 let native = ndk_path
70 .join("toolchains/llvm/prebuilt")
71 .join("darwin-arm64");
72 if native.exists() {
73 "darwin-arm64"
74 } else {
75 "darwin-x86_64"
76 }
77 }
78 (OperatingSystem::Darwin(_), _) => "darwin-x86_64",
79 (OperatingSystem::Windows, _) => "windows-x86_64",
80 (OperatingSystem::Linux, _) => "linux-x86_64",
82 _ => panic!("Unsupported host triple for Android NDK: {host}"),
83 }
84}
85
86fn ndk_bin_dir(ndk_path: &Path) -> PathBuf {
87 ndk_path
88 .join("toolchains/llvm/prebuilt")
89 .join(ndk_host_tag(ndk_path))
90 .join("bin")
91}
92
93fn ndk_ar_path(ndk_path: &Path) -> PathBuf {
95 ndk_bin_dir(ndk_path).join("llvm-ar")
96}
97
98fn ndk_clang_path(ndk_path: &Path, abi: AndroidAbi, cxx: bool, api_level: u32) -> PathBuf {
99 let suffix = if cxx { "clang++" } else { "clang" };
100 ndk_bin_dir(ndk_path).join(format!("{}{api_level}-{suffix}", abi.ndk_target()))
101}
102
103fn ndk_linker_path(ndk_path: &Path, abi: AndroidAbi, api_level: u32) -> PathBuf {
105 ndk_clang_path(ndk_path, abi, false, api_level)
106}
107
108fn ndk_builtins_lib(ndk_path: &Path, abi: AndroidAbi) -> eyre::Result<PathBuf> {
118 let arch = match abi {
119 AndroidAbi::Arm64V8a => "aarch64",
120 AndroidAbi::X86_64 => "x86_64",
121 AndroidAbi::ArmeabiV7a => "arm",
122 AndroidAbi::X86 => "i686",
123 };
124 let clang_libs = ndk_path
125 .join("toolchains/llvm/prebuilt")
126 .join(ndk_host_tag(ndk_path))
127 .join("lib/clang");
128 let mut candidates: Vec<PathBuf> = std::fs::read_dir(&clang_libs)
129 .map_err(|error| {
130 eyre::eyre!(
131 "Failed to read NDK clang libraries at {}: {error}",
132 clang_libs.display()
133 )
134 })?
135 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
136 .map(|version_dir| {
137 version_dir.join(format!("lib/linux/libclang_rt.builtins-{arch}-android.a"))
138 })
139 .filter(|path| path.is_file())
140 .collect();
141 candidates.sort_unstable();
142 match candidates.as_slice() {
143 [path] => Ok(path.clone()),
144 [] => Err(eyre::eyre!(
145 "The NDK at {} ships no libclang_rt.builtins-{arch}-android.a; \
146 a `-Zbuild-std` build needs it for the compiler-rt builtins",
147 ndk_path.display()
148 )),
149 _ => Err(eyre::eyre!(
150 "The NDK at {} ships multiple libclang_rt.builtins-{arch}-android.a \
151 copies: {candidates:?}",
152 ndk_path.display()
153 )),
154 }
155}
156
157async fn create_android_toolchain_wrapper(
163 ndk_path: &Path,
164 abi: AndroidAbi,
165 api_level: u32,
166) -> eyre::Result<PathBuf> {
167 let wrapper_dir = std::env::temp_dir().join("waterui-cmake-toolchains");
169 fs::create_dir_all(&wrapper_dir).await?;
170
171 let wrapper_path = wrapper_dir.join(format!("android-{}.cmake", abi.as_str()));
172 let ndk_toolchain = ndk_path.join("build/cmake/android.toolchain.cmake");
173
174 let content = format!(
175 include_str!("android_toolchain_wrapper.cmake.tpl"),
176 abi = abi.as_str(),
177 api_level = api_level,
178 ndk_toolchain = ndk_toolchain.display(),
179 asm_compiler = ndk_clang_path(ndk_path, abi, false, api_level).display(),
180 );
181 fs::write(&wrapper_path, content).await?;
182
183 Ok(wrapper_path)
184}
185
186fn ndk_cxx_path(ndk_path: &Path, abi: AndroidAbi, api_level: u32) -> PathBuf {
188 ndk_clang_path(ndk_path, abi, true, api_level)
189}
190
191fn ndk_libcxx_path(ndk_path: &Path, abi: AndroidAbi) -> PathBuf {
196 let new_path = ndk_path
197 .join("toolchains/llvm/prebuilt")
198 .join(ndk_host_tag(ndk_path))
199 .join("sysroot/usr/lib")
200 .join(abi.ndk_libcxx_triple())
201 .join("libc++_shared.so");
202
203 if new_path.exists() {
204 return new_path;
205 }
206
207 ndk_path
208 .join("sources/cxx-stl/llvm-libc++/libs")
209 .join(abi.as_str())
210 .join("libc++_shared.so")
211}
212
213pub(crate) const ANDROID_MAX_PAGE_SIZE_LINK_ARG: &str = "-Clink-arg=-Wl,-z,max-page-size=16384";
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
221pub enum AndroidAbi {
222 Arm64V8a,
224 X86_64,
226 ArmeabiV7a,
228 X86,
230}
231
232#[derive(Debug, thiserror::Error)]
234#[error("Unsupported Android ABI: {abi}")]
235pub struct UnsupportedAndroidAbi {
236 abi: String,
237}
238
239impl FromStr for AndroidAbi {
240 type Err = UnsupportedAndroidAbi;
241
242 fn from_str(s: &str) -> Result<Self, Self::Err> {
243 match s {
244 "arm64-v8a" => Ok(Self::Arm64V8a),
245 "x86_64" => Ok(Self::X86_64),
246 "armeabi-v7a" => Ok(Self::ArmeabiV7a),
247 "x86" => Ok(Self::X86),
248 other => Err(UnsupportedAndroidAbi {
249 abi: other.to_string(),
250 }),
251 }
252 }
253}
254
255impl AndroidAbi {
256 #[must_use]
257 pub const fn as_str(self) -> &'static str {
259 match self {
260 Self::Arm64V8a => "arm64-v8a",
261 Self::X86_64 => "x86_64",
262 Self::ArmeabiV7a => "armeabi-v7a",
263 Self::X86 => "x86",
264 }
265 }
266
267 #[must_use]
268 pub const fn ndk_target(self) -> &'static str {
270 match self {
271 Self::Arm64V8a => "aarch64-linux-android",
272 Self::X86_64 => "x86_64-linux-android",
273 Self::ArmeabiV7a => "armv7a-linux-androideabi",
274 Self::X86 => "i686-linux-android",
275 }
276 }
277
278 #[must_use]
279 pub const fn ndk_libcxx_triple(self) -> &'static str {
281 match self {
282 Self::Arm64V8a => "aarch64-linux-android",
283 Self::X86_64 => "x86_64-linux-android",
284 Self::ArmeabiV7a => "arm-linux-androideabi",
285 Self::X86 => "i686-linux-android",
286 }
287 }
288
289 #[must_use]
293 pub const fn from_triple(triple: &Triple) -> Option<Self> {
294 match triple.architecture {
295 Architecture::Aarch64(_) => Some(Self::Arm64V8a),
296 Architecture::X86_64 => Some(Self::X86_64),
297 Architecture::Arm(target_lexicon::ArmArchitecture::Armv7) => Some(Self::ArmeabiV7a),
298 Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => Some(Self::X86),
299 _ => None,
300 }
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub struct AndroidPlatform {
307 abi: AndroidAbi,
308}
309
310struct AndroidBuildContext {
311 abi: AndroidAbi,
312 ndk_path: PathBuf,
313 linker: PathBuf,
314 ar: PathBuf,
315 cxx: PathBuf,
316 target_underscore: String,
317 target_upper: String,
318 llvm_envs: Vec<(String, std::ffi::OsString)>,
319 java_home: PathBuf,
320 java_bin_dir: PathBuf,
321 kotlin_compiler: PathBuf,
322 kotlin_bin_dir: PathBuf,
323 kotlin_home: PathBuf,
324 sdk_path: PathBuf,
325 android_jar: PathBuf,
326 wrapper_toolchain: PathBuf,
327 android_platform: String,
328}
329
330impl AndroidPlatform {
331 #[must_use]
333 pub const fn new(abi: AndroidAbi) -> Self {
334 Self { abi }
335 }
336
337 #[must_use]
339 pub const fn arm64() -> Self {
340 Self {
341 abi: AndroidAbi::Arm64V8a,
342 }
343 }
344
345 #[must_use]
347 pub const fn x86_64() -> Self {
348 Self {
349 abi: AndroidAbi::X86_64,
350 }
351 }
352
353 #[must_use]
354 pub const fn abi(&self) -> AndroidAbi {
356 self.abi
357 }
358
359 #[must_use]
360 pub const fn abi_str(&self) -> &'static str {
362 self.abi.as_str()
363 }
364
365 pub fn try_from_abi(abi: &str) -> eyre::Result<Self> {
370 let abi = AndroidAbi::from_str(abi).map_err(|e| eyre::eyre!(e))?;
371 Ok(Self { abi })
372 }
373}
374
375pub const ALL_ABIS: &[AndroidAbi] = &[
377 AndroidAbi::Arm64V8a,
378 AndroidAbi::X86_64,
379 AndroidAbi::ArmeabiV7a,
380 AndroidAbi::X86,
381];
382
383impl AndroidPlatform {
384 #[must_use]
386 pub fn all() -> Vec<Self> {
387 ALL_ABIS.iter().copied().map(Self::new).collect()
388 }
389
390 #[must_use]
392 pub const fn triple(&self) -> Triple {
393 let architecture = match self.abi {
394 AndroidAbi::Arm64V8a => Architecture::Aarch64(Aarch64Architecture::Aarch64),
395 AndroidAbi::X86_64 => Architecture::X86_64,
396 AndroidAbi::ArmeabiV7a => Architecture::Arm(target_lexicon::ArmArchitecture::Armv7),
397 AndroidAbi::X86 => Architecture::X86_32(target_lexicon::X86_32Architecture::I686),
398 };
399 Triple {
400 architecture,
401 vendor: target_lexicon::Vendor::Unknown,
402 operating_system: target_lexicon::OperatingSystem::Linux,
403 environment: target_lexicon::Environment::Android,
404 binary_format: target_lexicon::BinaryFormat::Elf,
405 }
406 }
407
408 pub async fn build(&self, project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
413 let options = if options.loads_dynamic_modules() {
421 options
422 } else {
423 options.with_static_runtime()
424 };
425 let font_declarations = crate::assets::scan_fonts(project).await?;
428 let _resolved_fonts = crate::assets::resolve_fonts(font_declarations).await?;
429
430 let abi = self.abi();
431 let triple = self.triple();
432 let min_api_level = project
433 .resolved_framework()
434 .await?
435 .android_min_api_level()?;
436 let host = Host::current();
437 let build_context =
438 resolve_android_build_context(&host, abi, &triple, min_api_level).await?;
439 let build = configure_android_rust_build(&host, project, &triple, &build_context, &options)
440 .await?
441 .with_envs(options.cargo_envs().iter().cloned())
442 .with_target_dir(project.water_target_dir(options.linkage()).await?);
443
444 let built_target = build.build_lib(options.is_release()).await?;
445 copy_android_build_outputs(
446 project,
447 &options,
448 abi,
449 &build_context.ndk_path,
450 &built_target.profile_dir,
451 &built_target.artifact,
452 )
453 .await?;
454 Ok(built_target.profile_dir)
455 }
456
457 pub async fn clean_jni_libs(project: &Project) -> eyre::Result<()> {
462 let jni_libs_dir = project
463 .backend_path::<AndroidBackend>()
464 .join("app/src/main/jniLibs");
465
466 if jni_libs_dir.exists() {
467 fs::remove_dir_all(&jni_libs_dir).await?;
468 }
469 Ok(())
470 }
471
472 pub async fn package_with_abis(
480 project: &Project,
481 options: PackageOptions,
482 abis: &[AndroidAbi],
483 ) -> eyre::Result<Artifact> {
484 let backend_path = project.backend_path::<AndroidBackend>();
485
486 copy_assets_and_fonts(
488 project,
489 &backend_path,
490 None,
491 options.uses_dev_server(),
492 options.progress(),
493 )
494 .await?;
495
496 let gradlew = backend_path.join(if cfg!(windows) {
497 "gradlew.bat"
498 } else {
499 "gradlew"
500 });
501
502 let (command_name, output_kind, variant) =
503 match (options.is_distribution(), options.is_debug()) {
504 (true, false) => ("bundleRelease", OutputKind::Bundle, "release"),
505 (false, false) => ("assembleRelease", OutputKind::Apk, "release"),
506 (false, true) => ("assembleDebug", OutputKind::Apk, "debug"),
507 (true, true) => ("bundleDebug", OutputKind::Bundle, "debug"),
508 };
509
510 let abis_str = abis
512 .iter()
513 .map(|a| a.as_str())
514 .collect::<Vec<_>>()
515 .join(",");
516
517 let mut cmd = gradle_cmd(&gradlew, &backend_path, command_name);
520 cmd.env("WATERUI_SKIP_RUST_BUILD", "1")
521 .env("WATERUI_ANDROID_ABIS", &abis_str);
522
523 let host = Host::current();
524 if let Some(java_home) = Java::detect_home(&host).await {
525 cmd.env("JAVA_HOME", java_home);
526 }
527 if let Some(sdk_path) = AndroidSdk::detect_path(&host) {
528 cmd.env("ANDROID_HOME", &sdk_path)
529 .env("ANDROID_SDK_ROOT", &sdk_path);
530 }
531 apply_gradle_proxy_env(&host, &mut cmd)?;
532
533 let output = cmd.output().await?;
534
535 if !output.status.success() {
536 let stderr = String::from_utf8_lossy(&output.stderr);
537 let stdout = String::from_utf8_lossy(&output.stdout);
538 bail!("Gradle build failed:\n{}\n{}", stdout.trim(), stderr.trim());
539 }
540
541 let path = packaged_artifact(&backend_path, output_kind, variant).await?;
542 Ok(Artifact::new(project.bundle_identifier(), path))
543 }
544
545 pub async fn list_avds(host: &Host) -> eyre::Result<Vec<String>> {
550 let emulator_path = AndroidSdk::emulator_path(host)
551 .ok_or_else(|| eyre::eyre!("Android emulator not found"))?;
552
553 let output = host.output(&emulator_path, ["-list-avds"]).await?;
554
555 let stdout = String::from_utf8_lossy(&output.stdout);
556 let avds: Vec<String> = stdout
557 .lines()
558 .filter(|line| !line.is_empty())
559 .map(String::from)
560 .collect();
561
562 Ok(avds)
563 }
564}
565
566async fn resolve_android_build_context(
567 host: &Host,
568 abi: AndroidAbi,
569 triple: &Triple,
570 api_level: u32,
571) -> eyre::Result<AndroidBuildContext> {
572 let ndk_path = AndroidNdk::detect_path(host).ok_or_else(|| {
573 eyre::eyre!("Android NDK not found. Please install it via Android Studio.")
574 })?;
575 let linker = ndk_linker_path(&ndk_path, abi, api_level);
576 let ar = ndk_ar_path(&ndk_path);
577 let cxx = ndk_cxx_path(&ndk_path, abi, api_level);
578 for wrapper in [&linker, &cxx] {
582 if !wrapper.is_file() {
583 eyre::bail!(
584 "the Android NDK at {} ships no compiler wrapper for API {api_level} \
585 ({}); the framework's android-min-api-level needs an NDK that targets it",
586 ndk_path.display(),
587 wrapper.display()
588 );
589 }
590 }
591 let target_underscore = triple.to_string().replace('-', "_");
592 let target_upper = target_underscore.to_uppercase();
593 let llvm_envs = resolve_windows_arm64_llvm_envs(host).await?;
594 let (java_home, java_bin_dir) = resolve_java_home(host).await?;
595 let (kotlin_compiler, kotlin_bin_dir, kotlin_home) = resolve_kotlin_home(host).await?;
596 let (sdk_path, android_jar) = resolve_android_sdk_paths(host).await?;
597 let wrapper_toolchain = create_android_toolchain_wrapper(&ndk_path, abi, api_level).await?;
598
599 Ok(AndroidBuildContext {
600 abi,
601 ndk_path,
602 linker,
603 ar,
604 cxx,
605 target_underscore,
606 target_upper,
607 llvm_envs,
608 java_home,
609 java_bin_dir,
610 kotlin_compiler,
611 kotlin_bin_dir,
612 kotlin_home,
613 sdk_path,
614 android_jar,
615 wrapper_toolchain,
616 android_platform: format!("android-{api_level}"),
617 })
618}
619
620async fn resolve_windows_arm64_llvm_envs(
621 host: &Host,
622) -> eyre::Result<Vec<(String, std::ffi::OsString)>> {
623 WindowsArm64LlvmToolchain
624 .cargo_envs(host)
625 .await
626 .map_err(|error| match error {
627 ToolchainError::Fixable(_) => eyre::eyre!(
628 "Windows ARM64 LLVM toolchain is missing. Run `water doctor --fix` to install it automatically."
629 ),
630 ToolchainError::Unfixable(unfixable) => {
631 eyre::eyre!("Windows ARM64 LLVM toolchain check failed: {unfixable}")
632 }
633 })
634}
635
636async fn resolve_java_home(host: &Host) -> eyre::Result<(PathBuf, PathBuf)> {
637 let java_home = Java::detect_home(host).await.ok_or_else(|| {
638 eyre::eyre!(
639 "Java runtime not found. Install a JDK (or Android Studio JBR), then re-run `water doctor --fix`."
640 )
641 })?;
642 let java_bin_dir = java_home.join("bin");
643 Ok((java_home, java_bin_dir))
644}
645
646async fn resolve_kotlin_home(host: &Host) -> eyre::Result<(PathBuf, PathBuf, PathBuf)> {
647 let kotlin_compiler = Kotlin::detect_path(host).await.ok_or_else(|| {
648 eyre::eyre!(
649 "Kotlin compiler (kotlinc) not found. Install Android Studio or set `KOTLIN_HOME`, then re-run `water doctor`."
650 )
651 })?;
652 let kotlin_bin_dir = kotlin_compiler.parent().map(PathBuf::from).ok_or_else(|| {
653 eyre::eyre!(
654 "Failed to determine Kotlin bin directory from `{}`.",
655 kotlin_compiler.display()
656 )
657 })?;
658 let kotlin_home = kotlin_bin_dir.parent().map(PathBuf::from).ok_or_else(|| {
659 eyre::eyre!(
660 "Failed to determine KOTLIN_HOME from `{}`.",
661 kotlin_bin_dir.display()
662 )
663 })?;
664 Ok((kotlin_compiler, kotlin_bin_dir, kotlin_home))
665}
666
667async fn resolve_android_sdk_paths(host: &Host) -> eyre::Result<(PathBuf, PathBuf)> {
672 let host = host.clone();
673 smol::unblock(move || {
674 let sdk_path = AndroidSdk::detect_path(&host).ok_or_else(|| {
675 eyre::eyre!("Android SDK not found. Please install it via Android Studio.")
676 })?;
677 let android_jar = AndroidSdk::android_jar_path(&host).ok_or_else(|| {
678 eyre::eyre!(
679 "Android platforms not found in SDK at {}. Install an Android platform (SDK) in Android Studio.",
680 sdk_path.display()
681 )
682 })?;
683 Ok((sdk_path, android_jar))
684 })
685 .await
686}
687
688pub(crate) async fn android_ffi_dependency_features(
697 project: &Project,
698) -> eyre::Result<Vec<String>> {
699 let mut features = vec!["waterui-ffi/android-jni".to_string()];
700 features.extend(crate::project_model::assets::capability_ffi_features(project).await?);
701 features.extend(crate::project_model::assets::self_drawn_realization_features(project).await?);
703 Ok(features)
704}
705
706async fn configure_android_rust_build(
707 host: &Host,
708 project: &Project,
709 triple: &Triple,
710 context: &AndroidBuildContext,
711 options: &BuildOptions,
712) -> eyre::Result<RustBuild> {
713 let mut build = RustBuild::new(project.ffi_crate_path(), triple.clone())
716 .with_project(project)
717 .with_features(android_ffi_dependency_features(project).await?)
718 .with_crate_type_override("cdylib")
719 .with_rustc_flag(ANDROID_MAX_PAGE_SIZE_LINK_ARG);
720 if options.linkage() == RustLinkage::SharedRuntime {
721 let nightly = crate::toolchain::rust::nightly_toolchain_with_rust_src(host).await?;
728 build = build
729 .with_feature("dev")
730 .with_preferred_dynamic_linking()
731 .with_build_std(nightly)
732 .with_env(
733 "LLVM_COMPILER_RT_LIB",
734 ndk_builtins_lib(&context.ndk_path, context.abi)?,
735 );
736 }
737 if let Some(sccache_path) = options.sccache_path() {
738 build = build.with_sccache(sccache_path.to_path_buf());
739 }
740 if let Some(progress) = options.progress() {
741 build = build.with_progress(progress.clone());
742 }
743 for (key, value) in &context.llvm_envs {
744 build = build.with_env(key.clone(), value.clone());
745 }
746
747 build = build.with_envs(android_cargo_envs(context, triple));
748
749 let current_path = host
750 .env("PATH")
751 .ok_or_else(|| eyre::eyre!("PATH environment variable is not set"))?;
752 let mut paths: Vec<PathBuf> = std::env::split_paths(¤t_path).collect();
753 paths.insert(0, context.java_bin_dir.clone());
754 paths.insert(0, context.kotlin_bin_dir.clone());
755 let new_path = std::env::join_paths(paths).map_err(|error| {
756 eyre::eyre!("Failed to construct PATH for Java/Kotlin compiler resolution: {error}")
757 })?;
758
759 Ok(build.with_env("PATH", new_path))
760}
761
762fn android_cargo_envs(
771 context: &AndroidBuildContext,
772 triple: &Triple,
773) -> Vec<(String, std::ffi::OsString)> {
774 [
775 (
776 format!("CARGO_TARGET_{}_LINKER", context.target_upper),
777 context.linker.as_os_str().to_os_string(),
778 ),
779 (
780 format!("CARGO_TARGET_{}_AR", context.target_upper),
781 context.ar.as_os_str().to_os_string(),
782 ),
783 (
784 format!("CC_{}", context.target_underscore),
785 context.linker.as_os_str().to_os_string(),
786 ),
787 (
788 format!("CXX_{}", context.target_underscore),
789 context.cxx.as_os_str().to_os_string(),
790 ),
791 (
792 format!("AR_{}", context.target_underscore),
793 context.ar.as_os_str().to_os_string(),
794 ),
795 (
796 "ANDROID_NDK".to_string(),
797 context.ndk_path.as_os_str().to_os_string(),
798 ),
799 (
800 "ANDROID_NDK_HOME".to_string(),
801 context.ndk_path.as_os_str().to_os_string(),
802 ),
803 (
804 "ANDROID_NDK_ROOT".to_string(),
805 context.ndk_path.as_os_str().to_os_string(),
806 ),
807 (
808 "ANDROID_HOME".to_string(),
809 context.sdk_path.as_os_str().to_os_string(),
810 ),
811 (
812 "ANDROID_SDK_ROOT".to_string(),
813 context.sdk_path.as_os_str().to_os_string(),
814 ),
815 (
816 "ANDROID_JAR".to_string(),
817 context.android_jar.as_os_str().to_os_string(),
818 ),
819 (
820 "JAVA_HOME".to_string(),
821 context.java_home.as_os_str().to_os_string(),
822 ),
823 (
824 "KOTLIN_HOME".to_string(),
825 context.kotlin_home.as_os_str().to_os_string(),
826 ),
827 (
828 "KOTLINC".to_string(),
829 context.kotlin_compiler.as_os_str().to_os_string(),
830 ),
831 (
832 "CMAKE_TOOLCHAIN_FILE".to_string(),
833 context.wrapper_toolchain.as_os_str().to_os_string(),
834 ),
835 (
836 format!("CMAKE_TOOLCHAIN_FILE_{}", context.target_underscore),
837 context.wrapper_toolchain.as_os_str().to_os_string(),
838 ),
839 (
840 "CMAKE_ASM_COMPILER".to_string(),
841 context.linker.as_os_str().to_os_string(),
842 ),
843 ("ANDROID_ABI".to_string(), context.abi.as_str().into()),
844 (
845 "ANDROID_PLATFORM".to_string(),
846 context.android_platform.clone().into(),
847 ),
848 ("PKG_CONFIG_ALLOW_CROSS".to_string(), "1".into()),
849 (
850 format!("PKG_CONFIG_ALLOW_CROSS_{}", context.target_underscore),
851 "1".into(),
852 ),
853 (format!("PKG_CONFIG_ALLOW_CROSS_{triple}"), "1".into()),
854 ]
855 .into_iter()
856 .collect()
857}
858
859pub(crate) async fn android_rust_build_envs(
865 host: &Host,
866 project: &Project,
867 abi: AndroidAbi,
868 triple: &Triple,
869 build_std: bool,
870) -> eyre::Result<Vec<(String, std::ffi::OsString)>> {
871 let min_api_level = project
872 .resolved_framework()
873 .await?
874 .android_min_api_level()?;
875 let context = resolve_android_build_context(host, abi, triple, min_api_level).await?;
876 let mut envs = android_cargo_envs(&context, triple);
877 if build_std {
878 envs.push((
879 "LLVM_COMPILER_RT_LIB".to_string(),
880 ndk_builtins_lib(&context.ndk_path, abi)?.into_os_string(),
881 ));
882 }
883 Ok(envs)
884}
885
886async fn copy_android_build_outputs(
887 project: &Project,
888 options: &BuildOptions,
889 abi: AndroidAbi,
890 ndk_path: &Path,
891 lib_dir: &Path,
892 source_lib: &Path,
893) -> eyre::Result<()> {
894 let output_dir = options.output_dir().map_or_else(
895 || {
896 project
897 .backend_path::<AndroidBackend>()
898 .join("app/src/main/jniLibs")
899 .join(abi.as_str())
900 },
901 std::path::Path::to_path_buf,
902 );
903 fs::create_dir_all(&output_dir).await?;
904 copy_file(source_lib, &output_dir.join("libwaterui_app.so")).await?;
905
906 if options.linkage() == RustLinkage::SharedRuntime {
907 let triple = AndroidPlatform::new(abi).triple();
908 let libraries = RustDynamicLibraries::resolve(lib_dir, &triple, project).await?;
909 libraries.stage(&output_dir).await?;
910 } else {
911 RustDynamicLibraries::remove_staged(&output_dir, &AndroidPlatform::new(abi).triple())
912 .await?;
913 }
914
915 let libcxx_target = output_dir.join("libc++_shared.so");
919 if staged_libs_need_libcxx(&output_dir).await? {
920 let libcxx_path = ndk_libcxx_path(ndk_path, abi);
921 if libcxx_path.exists() {
922 copy_file(&libcxx_path, &libcxx_target).await?;
923 }
924 } else if libcxx_target.exists() {
925 fs::remove_file(&libcxx_target).await?;
927 }
928
929 crate::elf::require_aligned_shared_libraries(&output_dir).await?;
933
934 Ok(())
935}
936
937async fn staged_libs_need_libcxx(output_dir: &Path) -> eyre::Result<bool> {
944 let output_dir = output_dir.to_path_buf();
945 unblock(move || {
946 let mut needs = false;
947 for entry in std::fs::read_dir(&output_dir)? {
948 let path = entry?.path();
949 if path.extension() != Some(std::ffi::OsStr::new("so")) {
950 continue;
951 }
952 let needed = std::fs::read(&path)
953 .ok()
954 .and_then(|data| elf_needs_libcxx(&data))
955 .unwrap_or_else(|| {
956 tracing::warn!(
957 library = %path.display(),
958 "could not parse staged library; assuming it needs libc++_shared.so"
959 );
960 true
961 });
962 needs |= needed;
963 }
964 Ok(needs)
965 })
966 .await
967}
968
969fn elf_needs_libcxx(data: &[u8]) -> Option<bool> {
972 use object::read::elf::{Dyn as _, ElfFile, FileHeader};
973
974 fn scan<Elf>(data: &[u8]) -> Option<bool>
975 where
976 Elf: FileHeader<Endian = object::Endianness>,
977 {
978 let file = ElfFile::<Elf>::parse(data).ok()?;
979 let endian = file.endian();
980 let sections = file.elf_section_table();
981 let (dyns, strings_index) = sections.dynamic(endian, data).ok()??;
982 let strings = sections.strings(endian, data, strings_index).ok()?;
983 Some(dyns.iter().any(|d| {
984 d.tag32(endian) == Some(object::elf::DT_NEEDED)
985 && d.string(endian, strings).ok() == Some(&b"libc++_shared.so"[..])
986 }))
987 }
988
989 scan::<object::elf::FileHeader64<object::Endianness>>(data)
990 .or_else(|| scan::<object::elf::FileHeader32<object::Endianness>>(data))
991}
992
993pub async fn clean_android(project: &Project) -> eyre::Result<()> {
1002 let backend_path = project.backend_path::<AndroidBackend>();
1003 let gradlew = backend_path.join(if cfg!(windows) {
1004 "gradlew.bat"
1005 } else {
1006 "gradlew"
1007 });
1008
1009 if !gradlew.exists() {
1010 return Ok(());
1012 }
1013
1014 let host = Host::current();
1016 let mut cmd = gradle_cmd(&gradlew, &backend_path, "clean");
1017
1018 if let Some(java_home) = Java::detect_home(&host).await {
1019 cmd.env("JAVA_HOME", java_home);
1020 }
1021 if let Some(sdk_path) = AndroidSdk::detect_path(&host) {
1022 cmd.env("ANDROID_HOME", &sdk_path)
1023 .env("ANDROID_SDK_ROOT", &sdk_path);
1024 }
1025 apply_gradle_proxy_env(&host, &mut cmd)?;
1026
1027 let output = cmd.output().await?;
1028
1029 if !output.status.success() {
1030 let stderr = String::from_utf8_lossy(&output.stderr);
1031 bail!("Gradle clean failed: {}", stderr.trim());
1032 }
1033
1034 Ok(())
1035}
1036
1037#[must_use]
1043pub const fn is_android_platform(platform: TargetPlatform) -> bool {
1044 matches!(platform, TargetPlatform::Android)
1045}
1046
1047async fn copy_assets_and_fonts(
1053 project: &Project,
1054 backend_path: &Path,
1055 sccache_path: Option<&Path>,
1056 dev_server: bool,
1057 progress: Option<&BuildProgress>,
1058) -> eyre::Result<()> {
1059 let assets_dir = backend_path.join("app/src/main/assets");
1060
1061 let manifest = assets::stage_project_assets_for_android(
1063 project,
1064 backend_path,
1065 sccache_path,
1066 dev_server,
1067 progress,
1068 )
1069 .await?;
1070
1071 let font_declarations = assets::scan_fonts(project).await?;
1073 let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
1074 resolved_fonts.extend(assets::scan_project_font_assets(&manifest)?);
1075
1076 if !resolved_fonts.is_empty() {
1077 let fonts_dest = assets_dir.join("fonts");
1079 assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
1080
1081 info!("Copied {} fonts to Android app", resolved_fonts.len());
1082 }
1083
1084 let java_dir = backend_path.join("app/src/main/java");
1086 generate_font_registration_kotlin(project, &resolved_fonts, &java_dir).await?;
1087
1088 Ok(())
1089}
1090
1091#[derive(Template)]
1092#[template(
1093 path = "src/templates/android_dynamic/WaterUIFonts.kt.tpl",
1094 escape = "none"
1095)]
1096struct WaterUiFontsKotlinTemplate<'a> {
1097 namespace: &'a str,
1098 font_entries: &'a [FontRegistrationTemplateEntry],
1099}
1100
1101async fn generate_font_registration_kotlin(
1103 project: &Project,
1104 fonts: &[ResolvedFont],
1105 java_dir: &Path,
1106) -> eyre::Result<()> {
1107 let namespace = project.bundle_identifier().android_package_name();
1109
1110 let legacy_path = java_dir.join("WaterUIFonts.kt");
1114 let _ = fs::remove_file(&legacy_path).await;
1115
1116 let font_entries = fonts
1118 .iter()
1119 .map(|font| FontRegistrationTemplateEntry {
1120 family_name: font.name.clone(),
1121 file_name: font
1122 .path
1123 .file_name()
1124 .and_then(|name| name.to_str())
1125 .unwrap_or_default()
1126 .to_string(),
1127 })
1128 .collect::<Vec<_>>();
1129
1130 let content = WaterUiFontsKotlinTemplate {
1131 namespace: namespace.as_str(),
1132 font_entries: &font_entries,
1133 }
1134 .render()
1135 .map_err(|error| eyre::eyre!("Failed to render WaterUIFonts.kt template: {error}"))?;
1136
1137 let package_dir = java_dir.join(namespace.as_str().replace('.', "/"));
1139 fs::create_dir_all(&package_dir).await?;
1140
1141 let kotlin_path = package_dir.join("WaterUIFonts.kt");
1142 fs::write(&kotlin_path, content).await?;
1143
1144 debug!("Generated {}", kotlin_path.display());
1145
1146 Ok(())
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151 use super::elf_needs_libcxx;
1152
1153 #[test]
1154 fn elf_needs_libcxx_rejects_non_elf_data() {
1155 assert_eq!(elf_needs_libcxx(b"not an elf"), None);
1160 assert_eq!(elf_needs_libcxx(&[]), None);
1161 assert_eq!(elf_needs_libcxx(&[0x7f, b'E', b'L', b'F']), None);
1162 }
1163}