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