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, 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
108async fn create_android_toolchain_wrapper(
114 ndk_path: &Path,
115 abi: AndroidAbi,
116 api_level: u32,
117) -> eyre::Result<PathBuf> {
118 let wrapper_dir = std::env::temp_dir().join("waterui-cmake-toolchains");
120 fs::create_dir_all(&wrapper_dir).await?;
121
122 let wrapper_path = wrapper_dir.join(format!("android-{}.cmake", abi.as_str()));
123 let ndk_toolchain = ndk_path.join("build/cmake/android.toolchain.cmake");
124
125 let content = format!(
126 include_str!("android_toolchain_wrapper.cmake.tpl"),
127 abi = abi.as_str(),
128 api_level = api_level,
129 ndk_toolchain = ndk_toolchain.display(),
130 asm_compiler = ndk_clang_path(ndk_path, abi, false, api_level).display(),
131 );
132 fs::write(&wrapper_path, content).await?;
133
134 Ok(wrapper_path)
135}
136
137fn ndk_cxx_path(ndk_path: &Path, abi: AndroidAbi, api_level: u32) -> PathBuf {
139 ndk_clang_path(ndk_path, abi, true, api_level)
140}
141
142fn ndk_libcxx_path(ndk_path: &Path, abi: AndroidAbi) -> PathBuf {
147 let new_path = ndk_path
148 .join("toolchains/llvm/prebuilt")
149 .join(ndk_host_tag(ndk_path))
150 .join("sysroot/usr/lib")
151 .join(abi.ndk_libcxx_triple())
152 .join("libc++_shared.so");
153
154 if new_path.exists() {
155 return new_path;
156 }
157
158 ndk_path
159 .join("sources/cxx-stl/llvm-libc++/libs")
160 .join(abi.as_str())
161 .join("libc++_shared.so")
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166pub enum AndroidAbi {
167 Arm64V8a,
169 X86_64,
171 ArmeabiV7a,
173 X86,
175}
176
177#[derive(Debug, thiserror::Error)]
179#[error("Unsupported Android ABI: {abi}")]
180pub struct UnsupportedAndroidAbi {
181 abi: String,
182}
183
184impl FromStr for AndroidAbi {
185 type Err = UnsupportedAndroidAbi;
186
187 fn from_str(s: &str) -> Result<Self, Self::Err> {
188 match s {
189 "arm64-v8a" => Ok(Self::Arm64V8a),
190 "x86_64" => Ok(Self::X86_64),
191 "armeabi-v7a" => Ok(Self::ArmeabiV7a),
192 "x86" => Ok(Self::X86),
193 other => Err(UnsupportedAndroidAbi {
194 abi: other.to_string(),
195 }),
196 }
197 }
198}
199
200impl AndroidAbi {
201 #[must_use]
202 pub const fn as_str(self) -> &'static str {
204 match self {
205 Self::Arm64V8a => "arm64-v8a",
206 Self::X86_64 => "x86_64",
207 Self::ArmeabiV7a => "armeabi-v7a",
208 Self::X86 => "x86",
209 }
210 }
211
212 #[must_use]
213 pub const fn ndk_target(self) -> &'static str {
215 match self {
216 Self::Arm64V8a => "aarch64-linux-android",
217 Self::X86_64 => "x86_64-linux-android",
218 Self::ArmeabiV7a => "armv7a-linux-androideabi",
219 Self::X86 => "i686-linux-android",
220 }
221 }
222
223 #[must_use]
224 pub const fn ndk_libcxx_triple(self) -> &'static str {
226 match self {
227 Self::Arm64V8a => "aarch64-linux-android",
228 Self::X86_64 => "x86_64-linux-android",
229 Self::ArmeabiV7a => "arm-linux-androideabi",
230 Self::X86 => "i686-linux-android",
231 }
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub struct AndroidPlatform {
238 abi: AndroidAbi,
239}
240
241struct AndroidBuildContext {
242 abi: AndroidAbi,
243 ndk_path: PathBuf,
244 linker: PathBuf,
245 ar: PathBuf,
246 cxx: PathBuf,
247 target_underscore: String,
248 target_upper: String,
249 llvm_envs: Vec<(String, std::ffi::OsString)>,
250 java_home: PathBuf,
251 java_bin_dir: PathBuf,
252 kotlin_compiler: PathBuf,
253 kotlin_bin_dir: PathBuf,
254 kotlin_home: PathBuf,
255 sdk_path: PathBuf,
256 android_jar: PathBuf,
257 wrapper_toolchain: PathBuf,
258 android_platform: String,
259}
260
261impl AndroidPlatform {
262 #[must_use]
264 pub const fn new(abi: AndroidAbi) -> Self {
265 Self { abi }
266 }
267
268 #[must_use]
270 pub const fn arm64() -> Self {
271 Self {
272 abi: AndroidAbi::Arm64V8a,
273 }
274 }
275
276 #[must_use]
278 pub const fn x86_64() -> Self {
279 Self {
280 abi: AndroidAbi::X86_64,
281 }
282 }
283
284 #[must_use]
285 pub const fn abi(&self) -> AndroidAbi {
287 self.abi
288 }
289
290 #[must_use]
291 pub const fn abi_str(&self) -> &'static str {
293 self.abi.as_str()
294 }
295
296 pub fn try_from_abi(abi: &str) -> eyre::Result<Self> {
301 let abi = AndroidAbi::from_str(abi).map_err(|e| eyre::eyre!(e))?;
302 Ok(Self { abi })
303 }
304}
305
306pub const ALL_ABIS: &[AndroidAbi] = &[
308 AndroidAbi::Arm64V8a,
309 AndroidAbi::X86_64,
310 AndroidAbi::ArmeabiV7a,
311 AndroidAbi::X86,
312];
313
314impl AndroidPlatform {
315 #[must_use]
317 pub fn all() -> Vec<Self> {
318 ALL_ABIS.iter().copied().map(Self::new).collect()
319 }
320
321 #[must_use]
323 pub const fn triple(&self) -> Triple {
324 let architecture = match self.abi {
325 AndroidAbi::Arm64V8a => Architecture::Aarch64(Aarch64Architecture::Aarch64),
326 AndroidAbi::X86_64 => Architecture::X86_64,
327 AndroidAbi::ArmeabiV7a => Architecture::Arm(target_lexicon::ArmArchitecture::Armv7),
328 AndroidAbi::X86 => Architecture::X86_32(target_lexicon::X86_32Architecture::I686),
329 };
330 Triple {
331 architecture,
332 vendor: target_lexicon::Vendor::Unknown,
333 operating_system: target_lexicon::OperatingSystem::Linux,
334 environment: target_lexicon::Environment::Android,
335 binary_format: target_lexicon::BinaryFormat::Elf,
336 }
337 }
338
339 pub async fn build(&self, project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
344 let options = options.with_static_runtime();
355 let font_declarations = crate::assets::scan_fonts(project).await?;
358 let _resolved_fonts = crate::assets::resolve_fonts(font_declarations).await?;
359
360 let abi = self.abi();
361 let triple = self.triple();
362 let min_api_level = project
363 .resolved_framework()
364 .await?
365 .android_min_api_level()?;
366 let host = Host::current();
367 let build_context =
368 resolve_android_build_context(&host, abi, &triple, min_api_level).await?;
369 let build = configure_android_rust_build(&host, project, &triple, &build_context, &options)
370 .await?
371 .with_envs(options.cargo_envs().iter().cloned())
372 .with_target_dir(project.water_target_dir(options.linkage()).await?);
373
374 let lib_dir = build.build_lib(options.is_release()).await?;
375 copy_android_build_outputs(project, &options, abi, &build_context.ndk_path, &lib_dir)
376 .await?;
377 Ok(lib_dir)
378 }
379
380 pub async fn clean_jni_libs(project: &Project) -> eyre::Result<()> {
385 let jni_libs_dir = project
386 .backend_path::<AndroidBackend>()
387 .join("app/src/main/jniLibs");
388
389 if jni_libs_dir.exists() {
390 fs::remove_dir_all(&jni_libs_dir).await?;
391 }
392 Ok(())
393 }
394
395 pub async fn package_with_abis(
403 project: &Project,
404 options: PackageOptions,
405 abis: &[AndroidAbi],
406 ) -> eyre::Result<Artifact> {
407 let backend_path = project.backend_path::<AndroidBackend>();
408
409 copy_assets_and_fonts(project, &backend_path, None, options.uses_dev_server()).await?;
411
412 let gradlew = backend_path.join(if cfg!(windows) {
413 "gradlew.bat"
414 } else {
415 "gradlew"
416 });
417
418 let (command_name, output_kind, variant) =
419 match (options.is_distribution(), options.is_debug()) {
420 (true, false) => ("bundleRelease", OutputKind::Bundle, "release"),
421 (false, false) => ("assembleRelease", OutputKind::Apk, "release"),
422 (false, true) => ("assembleDebug", OutputKind::Apk, "debug"),
423 (true, true) => ("bundleDebug", OutputKind::Bundle, "debug"),
424 };
425
426 let abis_str = abis
428 .iter()
429 .map(|a| a.as_str())
430 .collect::<Vec<_>>()
431 .join(",");
432
433 let mut cmd = gradle_cmd(&gradlew, &backend_path, command_name);
436 cmd.env("WATERUI_SKIP_RUST_BUILD", "1")
437 .env("WATERUI_ANDROID_ABIS", &abis_str);
438
439 let host = Host::current();
440 if let Some(java_home) = Java::detect_home(&host).await {
441 cmd.env("JAVA_HOME", java_home);
442 }
443 if let Some(sdk_path) = AndroidSdk::detect_path(&host) {
444 cmd.env("ANDROID_HOME", &sdk_path)
445 .env("ANDROID_SDK_ROOT", &sdk_path);
446 }
447 apply_gradle_proxy_env(&host, &mut cmd)?;
448
449 let output = cmd.output().await?;
450
451 if !output.status.success() {
452 let stderr = String::from_utf8_lossy(&output.stderr);
453 let stdout = String::from_utf8_lossy(&output.stdout);
454 bail!("Gradle build failed:\n{}\n{}", stdout.trim(), stderr.trim());
455 }
456
457 let path = packaged_artifact(&backend_path, output_kind, variant).await?;
458 Ok(Artifact::new(project.bundle_identifier(), path))
459 }
460
461 pub async fn list_avds(host: &Host) -> eyre::Result<Vec<String>> {
466 let emulator_path = AndroidSdk::emulator_path(host)
467 .ok_or_else(|| eyre::eyre!("Android emulator not found"))?;
468
469 let output = host.output(&emulator_path, ["-list-avds"]).await?;
470
471 let stdout = String::from_utf8_lossy(&output.stdout);
472 let avds: Vec<String> = stdout
473 .lines()
474 .filter(|line| !line.is_empty())
475 .map(String::from)
476 .collect();
477
478 Ok(avds)
479 }
480}
481
482async fn resolve_android_build_context(
483 host: &Host,
484 abi: AndroidAbi,
485 triple: &Triple,
486 api_level: u32,
487) -> eyre::Result<AndroidBuildContext> {
488 let ndk_path = AndroidNdk::detect_path(host).ok_or_else(|| {
489 eyre::eyre!("Android NDK not found. Please install it via Android Studio.")
490 })?;
491 let linker = ndk_linker_path(&ndk_path, abi, api_level);
492 let ar = ndk_ar_path(&ndk_path);
493 let cxx = ndk_cxx_path(&ndk_path, abi, api_level);
494 for wrapper in [&linker, &cxx] {
498 if !wrapper.is_file() {
499 eyre::bail!(
500 "the Android NDK at {} ships no compiler wrapper for API {api_level} \
501 ({}); the framework's android-min-api-level needs an NDK that targets it",
502 ndk_path.display(),
503 wrapper.display()
504 );
505 }
506 }
507 let target_underscore = triple.to_string().replace('-', "_");
508 let target_upper = target_underscore.to_uppercase();
509 let llvm_envs = resolve_windows_arm64_llvm_envs(host).await?;
510 let (java_home, java_bin_dir) = resolve_java_home(host).await?;
511 let (kotlin_compiler, kotlin_bin_dir, kotlin_home) = resolve_kotlin_home(host).await?;
512 let (sdk_path, android_jar) = resolve_android_sdk_paths(host).await?;
513 let wrapper_toolchain = create_android_toolchain_wrapper(&ndk_path, abi, api_level).await?;
514
515 Ok(AndroidBuildContext {
516 abi,
517 ndk_path,
518 linker,
519 ar,
520 cxx,
521 target_underscore,
522 target_upper,
523 llvm_envs,
524 java_home,
525 java_bin_dir,
526 kotlin_compiler,
527 kotlin_bin_dir,
528 kotlin_home,
529 sdk_path,
530 android_jar,
531 wrapper_toolchain,
532 android_platform: format!("android-{api_level}"),
533 })
534}
535
536async fn resolve_windows_arm64_llvm_envs(
537 host: &Host,
538) -> eyre::Result<Vec<(String, std::ffi::OsString)>> {
539 WindowsArm64LlvmToolchain
540 .cargo_envs(host)
541 .await
542 .map_err(|error| match error {
543 ToolchainError::Fixable(_) => eyre::eyre!(
544 "Windows ARM64 LLVM toolchain is missing. Run `water doctor --fix` to install it automatically."
545 ),
546 ToolchainError::Unfixable(unfixable) => {
547 eyre::eyre!("Windows ARM64 LLVM toolchain check failed: {unfixable}")
548 }
549 })
550}
551
552async fn resolve_java_home(host: &Host) -> eyre::Result<(PathBuf, PathBuf)> {
553 let java_home = Java::detect_home(host).await.ok_or_else(|| {
554 eyre::eyre!(
555 "Java runtime not found. Install a JDK (or Android Studio JBR), then re-run `water doctor --fix`."
556 )
557 })?;
558 let java_bin_dir = java_home.join("bin");
559 Ok((java_home, java_bin_dir))
560}
561
562async fn resolve_kotlin_home(host: &Host) -> eyre::Result<(PathBuf, PathBuf, PathBuf)> {
563 let kotlin_compiler = Kotlin::detect_path(host).await.ok_or_else(|| {
564 eyre::eyre!(
565 "Kotlin compiler (kotlinc) not found. Install Android Studio or set `KOTLIN_HOME`, then re-run `water doctor`."
566 )
567 })?;
568 let kotlin_bin_dir = kotlin_compiler.parent().map(PathBuf::from).ok_or_else(|| {
569 eyre::eyre!(
570 "Failed to determine Kotlin bin directory from `{}`.",
571 kotlin_compiler.display()
572 )
573 })?;
574 let kotlin_home = kotlin_bin_dir.parent().map(PathBuf::from).ok_or_else(|| {
575 eyre::eyre!(
576 "Failed to determine KOTLIN_HOME from `{}`.",
577 kotlin_bin_dir.display()
578 )
579 })?;
580 Ok((kotlin_compiler, kotlin_bin_dir, kotlin_home))
581}
582
583async fn resolve_android_sdk_paths(host: &Host) -> eyre::Result<(PathBuf, PathBuf)> {
588 let host = host.clone();
589 smol::unblock(move || {
590 let sdk_path = AndroidSdk::detect_path(&host).ok_or_else(|| {
591 eyre::eyre!("Android SDK not found. Please install it via Android Studio.")
592 })?;
593 let android_jar = AndroidSdk::android_jar_path(&host).ok_or_else(|| {
594 eyre::eyre!(
595 "Android platforms not found in SDK at {}. Install an Android platform (SDK) in Android Studio.",
596 sdk_path.display()
597 )
598 })?;
599 Ok((sdk_path, android_jar))
600 })
601 .await
602}
603
604pub(crate) async fn android_ffi_dependency_features(
613 project: &Project,
614) -> eyre::Result<Vec<String>> {
615 let mut features = vec!["waterui-ffi/android-jni".to_string()];
616 features.extend(crate::project_model::assets::capability_ffi_features(project).await?);
617 features.extend(crate::project_model::assets::self_drawn_realization_features(project).await?);
619 Ok(features)
620}
621
622async fn configure_android_rust_build(
623 host: &Host,
624 project: &Project,
625 triple: &Triple,
626 context: &AndroidBuildContext,
627 options: &BuildOptions,
628) -> eyre::Result<RustBuild> {
629 let mut build = RustBuild::new(project.ffi_crate_path(), triple.clone())
632 .with_project(project)
633 .with_features(android_ffi_dependency_features(project).await?)
634 .with_crate_type_override("cdylib")
635 .with_rustc_flag("-Clink-arg=-Wl,-z,max-page-size=16384");
638 if let Some(sccache_path) = options.sccache_path() {
639 build = build.with_sccache(sccache_path.to_path_buf());
640 }
641 for (key, value) in &context.llvm_envs {
642 build = build.with_env(key.clone(), value.clone());
643 }
644
645 build = build
646 .with_env(
647 format!("CARGO_TARGET_{}_LINKER", context.target_upper),
648 context.linker.as_os_str(),
649 )
650 .with_env(
651 format!("CARGO_TARGET_{}_AR", context.target_upper),
652 context.ar.as_os_str(),
653 )
654 .with_env(
655 format!("CC_{}", context.target_underscore),
656 context.linker.as_os_str(),
657 )
658 .with_env(
659 format!("CXX_{}", context.target_underscore),
660 context.cxx.as_os_str(),
661 )
662 .with_env(
663 format!("AR_{}", context.target_underscore),
664 context.ar.as_os_str(),
665 )
666 .with_env("ANDROID_NDK", context.ndk_path.as_os_str())
667 .with_env("ANDROID_NDK_HOME", context.ndk_path.as_os_str())
668 .with_env("ANDROID_NDK_ROOT", context.ndk_path.as_os_str())
669 .with_env("ANDROID_HOME", context.sdk_path.as_os_str())
670 .with_env("ANDROID_SDK_ROOT", context.sdk_path.as_os_str())
671 .with_env("ANDROID_JAR", context.android_jar.as_os_str())
672 .with_env("JAVA_HOME", context.java_home.as_os_str())
673 .with_env("KOTLIN_HOME", context.kotlin_home.as_os_str())
674 .with_env("KOTLINC", context.kotlin_compiler.as_os_str())
675 .with_env(
676 "CMAKE_TOOLCHAIN_FILE",
677 context.wrapper_toolchain.as_os_str(),
678 )
679 .with_env(
680 format!("CMAKE_TOOLCHAIN_FILE_{}", context.target_underscore),
681 context.wrapper_toolchain.as_os_str(),
682 )
683 .with_env("CMAKE_ASM_COMPILER", context.linker.as_os_str())
684 .with_env("ANDROID_ABI", context.abi.as_str())
685 .with_env("ANDROID_PLATFORM", &context.android_platform)
686 .with_env("PKG_CONFIG_ALLOW_CROSS", "1")
687 .with_env(
688 format!("PKG_CONFIG_ALLOW_CROSS_{}", context.target_underscore),
689 "1",
690 )
691 .with_env(format!("PKG_CONFIG_ALLOW_CROSS_{triple}"), "1");
692
693 let current_path = host
694 .env("PATH")
695 .ok_or_else(|| eyre::eyre!("PATH environment variable is not set"))?;
696 let mut paths: Vec<PathBuf> = std::env::split_paths(¤t_path).collect();
697 paths.insert(0, context.java_bin_dir.clone());
698 paths.insert(0, context.kotlin_bin_dir.clone());
699 let new_path = std::env::join_paths(paths).map_err(|error| {
700 eyre::eyre!("Failed to construct PATH for Java/Kotlin compiler resolution: {error}")
701 })?;
702
703 Ok(build.with_env("PATH", new_path))
704}
705
706async fn copy_android_build_outputs(
707 project: &Project,
708 options: &BuildOptions,
709 abi: AndroidAbi,
710 ndk_path: &Path,
711 lib_dir: &Path,
712) -> eyre::Result<()> {
713 let lib_name = project.ffi_crate_name().replace('-', "_");
714 let source_lib = lib_dir.join(format!("lib{lib_name}.so"));
715
716 if !source_lib.exists() {
717 bail!(
718 "Rust shared library not found at {}. Did the build succeed?",
719 source_lib.display()
720 );
721 }
722
723 let output_dir = options.output_dir().map_or_else(
724 || {
725 project
726 .backend_path::<AndroidBackend>()
727 .join("app/src/main/jniLibs")
728 .join(abi.as_str())
729 },
730 std::path::Path::to_path_buf,
731 );
732 fs::create_dir_all(&output_dir).await?;
733 copy_file(&source_lib, &output_dir.join("libwaterui_app.so")).await?;
734
735 if options.linkage() == RustLinkage::SharedRuntime {
736 let triple = AndroidPlatform::new(abi).triple();
737 let libraries = RustDynamicLibraries::resolve(lib_dir, &triple).await?;
738 libraries.stage(&output_dir).await?;
739 } else {
740 RustDynamicLibraries::remove_staged(&output_dir, &AndroidPlatform::new(abi).triple())
741 .await?;
742 }
743
744 let libcxx_target = output_dir.join("libc++_shared.so");
748 if staged_libs_need_libcxx(&output_dir).await? {
749 let libcxx_path = ndk_libcxx_path(ndk_path, abi);
750 if libcxx_path.exists() {
751 copy_file(&libcxx_path, &libcxx_target).await?;
752 }
753 } else if libcxx_target.exists() {
754 fs::remove_file(&libcxx_target).await?;
756 }
757
758 Ok(())
759}
760
761async fn staged_libs_need_libcxx(output_dir: &Path) -> eyre::Result<bool> {
768 let output_dir = output_dir.to_path_buf();
769 unblock(move || {
770 let mut needs = false;
771 for entry in std::fs::read_dir(&output_dir)? {
772 let path = entry?.path();
773 if path.extension() != Some(std::ffi::OsStr::new("so")) {
774 continue;
775 }
776 let needed = std::fs::read(&path)
777 .ok()
778 .and_then(|data| elf_needs_libcxx(&data))
779 .unwrap_or_else(|| {
780 tracing::warn!(
781 library = %path.display(),
782 "could not parse staged library; assuming it needs libc++_shared.so"
783 );
784 true
785 });
786 needs |= needed;
787 }
788 Ok(needs)
789 })
790 .await
791}
792
793fn elf_needs_libcxx(data: &[u8]) -> Option<bool> {
796 use object::read::elf::{Dyn as _, ElfFile, FileHeader};
797
798 fn scan<Elf>(data: &[u8]) -> Option<bool>
799 where
800 Elf: FileHeader<Endian = object::Endianness>,
801 {
802 let file = ElfFile::<Elf>::parse(data).ok()?;
803 let endian = file.endian();
804 let sections = file.elf_section_table();
805 let (dyns, strings_index) = sections.dynamic(endian, data).ok()??;
806 let strings = sections.strings(endian, data, strings_index).ok()?;
807 Some(dyns.iter().any(|d| {
808 d.tag32(endian) == Some(object::elf::DT_NEEDED)
809 && d.string(endian, strings).ok() == Some(&b"libc++_shared.so"[..])
810 }))
811 }
812
813 scan::<object::elf::FileHeader64<object::Endianness>>(data)
814 .or_else(|| scan::<object::elf::FileHeader32<object::Endianness>>(data))
815}
816
817pub async fn clean_android(project: &Project) -> eyre::Result<()> {
826 let backend_path = project.backend_path::<AndroidBackend>();
827 let gradlew = backend_path.join(if cfg!(windows) {
828 "gradlew.bat"
829 } else {
830 "gradlew"
831 });
832
833 if !gradlew.exists() {
834 return Ok(());
836 }
837
838 let host = Host::current();
840 let mut cmd = gradle_cmd(&gradlew, &backend_path, "clean");
841
842 if let Some(java_home) = Java::detect_home(&host).await {
843 cmd.env("JAVA_HOME", java_home);
844 }
845 if let Some(sdk_path) = AndroidSdk::detect_path(&host) {
846 cmd.env("ANDROID_HOME", &sdk_path)
847 .env("ANDROID_SDK_ROOT", &sdk_path);
848 }
849 apply_gradle_proxy_env(&host, &mut cmd)?;
850
851 let output = cmd.output().await?;
852
853 if !output.status.success() {
854 let stderr = String::from_utf8_lossy(&output.stderr);
855 bail!("Gradle clean failed: {}", stderr.trim());
856 }
857
858 Ok(())
859}
860
861#[must_use]
867pub const fn is_android_platform(platform: TargetPlatform) -> bool {
868 matches!(platform, TargetPlatform::Android)
869}
870
871async fn copy_assets_and_fonts(
877 project: &Project,
878 backend_path: &Path,
879 sccache_path: Option<&Path>,
880 dev_server: bool,
881) -> eyre::Result<()> {
882 let assets_dir = backend_path.join("app/src/main/assets");
883
884 let manifest =
886 assets::stage_project_assets_for_android(project, backend_path, sccache_path, dev_server)
887 .await?;
888
889 let font_declarations = assets::scan_fonts(project).await?;
891 let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
892 resolved_fonts.extend(assets::scan_project_font_assets(&manifest)?);
893
894 if !resolved_fonts.is_empty() {
895 let fonts_dest = assets_dir.join("fonts");
897 assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
898
899 info!("Copied {} fonts to Android app", resolved_fonts.len());
900 }
901
902 let java_dir = backend_path.join("app/src/main/java");
904 generate_font_registration_kotlin(project, &resolved_fonts, &java_dir).await?;
905
906 Ok(())
907}
908
909#[derive(Template)]
910#[template(
911 path = "src/templates/android_dynamic/WaterUIFonts.kt.tpl",
912 escape = "none"
913)]
914struct WaterUiFontsKotlinTemplate<'a> {
915 namespace: &'a str,
916 font_entries: &'a [FontRegistrationTemplateEntry],
917}
918
919async fn generate_font_registration_kotlin(
921 project: &Project,
922 fonts: &[ResolvedFont],
923 java_dir: &Path,
924) -> eyre::Result<()> {
925 let namespace = project.bundle_identifier().android_package_name();
927
928 let legacy_path = java_dir.join("WaterUIFonts.kt");
932 let _ = fs::remove_file(&legacy_path).await;
933
934 let font_entries = fonts
936 .iter()
937 .map(|font| FontRegistrationTemplateEntry {
938 family_name: font.name.clone(),
939 file_name: font
940 .path
941 .file_name()
942 .and_then(|name| name.to_str())
943 .unwrap_or_default()
944 .to_string(),
945 })
946 .collect::<Vec<_>>();
947
948 let content = WaterUiFontsKotlinTemplate {
949 namespace: namespace.as_str(),
950 font_entries: &font_entries,
951 }
952 .render()
953 .map_err(|error| eyre::eyre!("Failed to render WaterUIFonts.kt template: {error}"))?;
954
955 let package_dir = java_dir.join(namespace.as_str().replace('.', "/"));
957 fs::create_dir_all(&package_dir).await?;
958
959 let kotlin_path = package_dir.join("WaterUIFonts.kt");
960 fs::write(&kotlin_path, content).await?;
961
962 debug!("Generated {}", kotlin_path.display());
963
964 Ok(())
965}
966
967#[cfg(test)]
968mod tests {
969 use super::elf_needs_libcxx;
970
971 #[test]
972 fn elf_needs_libcxx_rejects_non_elf_data() {
973 assert_eq!(elf_needs_libcxx(b"not an elf"), None);
978 assert_eq!(elf_needs_libcxx(&[]), None);
979 assert_eq!(elf_needs_libcxx(&[0x7f, b'E', b'L', b'F']), None);
980 }
981}