waterui_cli/android/
backend.rs1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 android::platform::{AndroidAbi, AndroidPlatform, clean_android, is_android_platform},
7 backend::Backend,
8 build::BuildOptions,
9 device::Artifact,
10 platform::{PackageOptions, TargetBackend, TargetPlatform},
11 project::Project,
12 templates::{self, TemplateContext},
13};
14
15#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct AndroidBackend {
20 #[serde(
21 default = "default_android_project_path",
22 skip_serializing_if = "is_default_android_project_path"
23 )]
24 project_path: PathBuf,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 version: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 backend_path: Option<String>,
30}
31
32impl AndroidBackend {
33 #[must_use]
35 pub fn new() -> Self {
36 Self {
37 project_path: default_android_project_path(),
38 version: None,
39 backend_path: None,
40 }
41 }
42
43 #[must_use]
45 pub fn with_project_path(mut self, path: impl Into<PathBuf>) -> Self {
46 self.project_path = path.into();
47 self
48 }
49
50 #[must_use]
52 pub fn with_backend_path(mut self, path: impl Into<String>) -> Self {
53 self.backend_path = Some(path.into());
54 self
55 }
56
57 #[must_use]
59 pub const fn project_path(&self) -> &PathBuf {
60 &self.project_path
61 }
62
63 #[must_use]
65 pub fn backend_path(&self) -> Option<&str> {
66 self.backend_path.as_deref()
67 }
68
69 #[must_use]
72 pub fn configures_project(&self) -> bool {
73 self.project_path != default_android_project_path() || self.version.is_some()
74 }
75
76 #[must_use]
78 pub fn gradlew_path(&self) -> PathBuf {
79 let base = &self.project_path;
80 if cfg!(windows) {
81 base.join("gradlew.bat")
82 } else {
83 base.join("gradlew")
84 }
85 }
86}
87
88impl Default for AndroidBackend {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl Backend for AndroidBackend {
95 const DEFAULT_PATH: &'static str = "android";
96
97 const CACHE_PATHS: &'static [&'static str] = &[".gradle", "build", "app"];
99
100 fn path(&self) -> &Path {
101 &self.project_path
102 }
103
104 async fn init(project: &Project) -> Result<Self, crate::backend::FailToInitBackend> {
105 let manifest = project.manifest();
106
107 let app_name = manifest
109 .package
110 .name
111 .chars()
112 .filter(|c| c.is_alphanumeric())
113 .collect::<String>();
114
115 match crate::assets::scan_required_permissions(&project.ffi_crate_path().join("Cargo.toml"))
121 .await
122 {
123 Ok(required) => crate::assets::warn_missing_permissions(project, &required, |key| {
124 key.android_permission_name().is_some()
125 }),
126 Err(error) => tracing::debug!("skipped permission audit: {error}"),
127 }
128
129 let android_permissions = manifest
131 .permissions
132 .iter()
133 .filter(|(_, entry)| entry.is_enabled())
134 .filter_map(|(key, _)| {
135 key.android_permission_name()
136 .map(|name| templates::AndroidPermissionTemplateEntry { name })
137 })
138 .collect();
139
140 let ctx = TemplateContext::for_project_manifest(
141 manifest,
142 project.crate_name().clone(),
143 app_name,
144 &project
145 .resolved_framework()
146 .await
147 .map_err(crate::backend::FailToInitBackend::Config)?,
148 )
149 .with_backend_project_path(project.backend_path::<Self>())
150 .with_project_root_path(project.root().to_path_buf())
151 .with_android_permissions(android_permissions);
152
153 templates::android::scaffold(&project.backend_path::<Self>(), &ctx)
154 .await
155 .map_err(crate::backend::FailToInitBackend::Io)?;
156
157 let existing = manifest.backends.android();
158 Ok(Self {
159 project_path: existing.map_or_else(default_android_project_path, |backend| {
160 backend.project_path.clone()
161 }),
162 version: existing.and_then(|backend| backend.version.clone()),
163 backend_path: existing.and_then(|backend| backend.backend_path.clone()),
164 })
165 }
166
167 fn supports(&self, platform: TargetPlatform) -> bool {
168 is_android_platform(platform)
169 }
170
171 async fn build(
172 &self,
173 project: &Project,
174 platform: TargetPlatform,
175 options: BuildOptions,
176 ) -> eyre::Result<crate::build::BuiltTarget> {
177 debug_assert_eq!(platform, TargetPlatform::Android);
178 project
179 .browser_runtime_plan(platform, TargetBackend::Android)
180 .await?;
181 AndroidPlatform::arm64().build(project, options).await
182 }
183
184 async fn package(
185 &self,
186 project: &Project,
187 platform: TargetPlatform,
188 options: PackageOptions,
189 _built: &crate::build::BuiltTarget,
190 ) -> eyre::Result<Artifact> {
191 debug_assert_eq!(platform, TargetPlatform::Android);
192 AndroidPlatform::package_with_abis(project, options, &[AndroidAbi::Arm64V8a]).await
193 }
194
195 async fn clean(&self, project: &Project, _platform: TargetPlatform) -> eyre::Result<()> {
196 clean_android(project).await
197 }
198}
199
200fn default_android_project_path() -> PathBuf {
201 PathBuf::from("android")
202}
203
204fn is_default_android_project_path(s: &Path) -> bool {
205 s == Path::new("android")
206}