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).await {
118 Ok(required) => crate::assets::warn_missing_permissions(project, &required, |key| {
119 key.android_permission_name().is_some()
120 }),
121 Err(error) => tracing::debug!("skipped permission audit: {error}"),
122 }
123
124 let android_permissions = manifest
126 .permissions
127 .iter()
128 .filter(|(_, entry)| entry.is_enabled())
129 .filter_map(|(key, _)| {
130 key.android_permission_name()
131 .map(|name| templates::AndroidPermissionTemplateEntry { name })
132 })
133 .collect();
134
135 let ctx = TemplateContext::for_project_manifest(
136 manifest,
137 project.crate_name().clone(),
138 app_name,
139 &project
140 .resolved_framework()
141 .await
142 .map_err(crate::backend::FailToInitBackend::Config)?,
143 )
144 .with_backend_project_path(project.backend_path::<Self>())
145 .with_project_root_path(project.root().to_path_buf())
146 .with_android_permissions(android_permissions);
147
148 templates::android::scaffold(&project.backend_path::<Self>(), &ctx)
149 .await
150 .map_err(crate::backend::FailToInitBackend::Io)?;
151
152 let existing = manifest.backends.android();
153 Ok(Self {
154 project_path: existing.map_or_else(default_android_project_path, |backend| {
155 backend.project_path.clone()
156 }),
157 version: existing.and_then(|backend| backend.version.clone()),
158 backend_path: existing.and_then(|backend| backend.backend_path.clone()),
159 })
160 }
161
162 fn supports(&self, platform: TargetPlatform) -> bool {
163 is_android_platform(platform)
164 }
165
166 async fn build(
167 &self,
168 project: &Project,
169 platform: TargetPlatform,
170 options: BuildOptions,
171 ) -> eyre::Result<PathBuf> {
172 debug_assert_eq!(platform, TargetPlatform::Android);
173 project
174 .browser_runtime_plan(platform, TargetBackend::Android)
175 .await?;
176 AndroidPlatform::arm64().build(project, options).await
177 }
178
179 async fn package(
180 &self,
181 project: &Project,
182 platform: TargetPlatform,
183 options: PackageOptions,
184 ) -> eyre::Result<Artifact> {
185 debug_assert_eq!(platform, TargetPlatform::Android);
186 AndroidPlatform::package_with_abis(project, options, &[AndroidAbi::Arm64V8a]).await
187 }
188
189 async fn clean(&self, project: &Project, _platform: TargetPlatform) -> eyre::Result<()> {
190 clean_android(project).await
191 }
192}
193
194fn default_android_project_path() -> PathBuf {
195 PathBuf::from("android")
196}
197
198fn is_default_android_project_path(s: &Path) -> bool {
199 s == Path::new("android")
200}