waterui_cli/apple/
backend.rs1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use waterui_assets_planner::ColorScheme;
5
6use crate::{
7 apple::platform::{build_rust_lib, clean_apple, is_apple_platform, package_apple},
8 backend::Backend,
9 build::BuildOptions,
10 device::Artifact,
11 platform::{PackageOptions, TargetBackend, TargetPlatform},
12 project::Project,
13 project_types::CrateName,
14 templates::{self, TemplateContext},
15};
16
17#[derive(Debug, Serialize, Deserialize, Clone)]
18pub struct AppleBackend {
23 #[serde(
24 default = "default_apple_project_path",
25 skip_serializing_if = "is_default_apple_project_path"
26 )]
27 pub project_path: PathBuf,
29 pub scheme: String,
31 #[serde(skip_serializing_if = "Option::is_none")]
35 pub branch: Option<String>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
41 pub revision: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub backend_path: Option<String>,
45}
46
47pub fn apple_product_name(project: &Project) -> Result<&str, eyre::Report> {
64 let name = project.manifest().package.name.as_str();
65 if name.is_empty() {
66 eyre::bail!("This project has no name; `package.name` in Water.toml names the application");
67 }
68 if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
69 eyre::bail!(
70 "The project name {name:?} contains a path separator, so it cannot name an application bundle"
71 );
72 }
73 Ok(name)
74}
75
76impl AppleBackend {
77 #[must_use]
79 pub fn new(scheme: impl Into<String>) -> Self {
80 Self {
81 project_path: default_apple_project_path(),
82 scheme: scheme.into(),
83 branch: None,
84 revision: None,
85 backend_path: None,
86 }
87 }
88
89 #[must_use]
91 pub fn with_project_path(mut self, path: impl Into<PathBuf>) -> Self {
92 self.project_path = path.into();
93 self
94 }
95
96 #[must_use]
98 pub fn with_backend_path(mut self, path: impl Into<String>) -> Self {
99 self.backend_path = Some(path.into());
100 self
101 }
102
103 #[must_use]
105 pub fn project_path(&self) -> &Path {
106 &self.project_path
107 }
108
109 #[must_use]
112 pub fn configures_project(&self) -> bool {
113 self.project_path != default_apple_project_path()
114 || !self.scheme.is_empty()
115 || self.branch.is_some()
116 || self.revision.is_some()
117 }
118}
119
120fn default_apple_project_path() -> PathBuf {
121 PathBuf::from("apple")
122}
123
124fn is_default_apple_project_path(s: &Path) -> bool {
125 s == Path::new("apple")
126}
127
128impl Backend for AppleBackend {
129 const DEFAULT_PATH: &'static str = "apple";
130
131 const CACHE_PATHS: &'static [&'static str] = &["DerivedData"];
133
134 fn path(&self) -> &Path {
135 &self.project_path
136 }
137
138 async fn init(project: &Project) -> Result<Self, crate::backend::FailToInitBackend> {
139 let manifest = project.manifest();
140 let existing = manifest.backends.apple();
143
144 let is_playground =
147 manifest.package.package_type == crate::project::PackageType::Playground;
148
149 let (scheme, app_name, crate_name_for_template) = if is_playground {
152 (
153 "WaterUIApp".to_string(),
154 "WaterUIApp".to_string(),
155 CrateName::try_from("WaterUIApp").expect("playground crate name must be valid"),
156 )
157 } else {
158 let crate_name = project.crate_name().clone();
159 let app_name = templates::apple_app_name(&crate_name);
162 (crate_name.to_string(), app_name, crate_name)
163 };
164
165 let project_path = default_apple_project_path();
166
167 let ios_permissions = manifest
168 .permissions
169 .iter()
170 .filter(|(_, entry)| entry.is_enabled())
171 .filter_map(|(key, entry)| {
172 key.ios_plist_key()
173 .map(|plist_key| templates::IosPermissionTemplateEntry {
174 plist_key,
175 description: entry.description().to_string(),
176 })
177 })
178 .collect();
179 let webview_enabled = project
180 .uses_standard_webview()
181 .await
182 .map_err(crate::backend::FailToInitBackend::Config)?;
183 let chromium_enabled = project
184 .links_runtime_package("waterui-chromium")
185 .await
186 .map_err(crate::backend::FailToInitBackend::Config)?;
187 let browser_engine = project
188 .linked_browser_engine()
189 .await
190 .map_err(crate::backend::FailToInitBackend::Config)?;
191 let launch = crate::assets::project_launch_assets(project)
194 .map_err(crate::backend::FailToInitBackend::Config)?;
195 let launch_entry = templates::LaunchTemplateEntry {
196 has_background: launch.plan().background(ColorScheme::Light).is_some(),
197 has_image: launch.has_artwork(),
198 };
199 let ctx = TemplateContext::for_project_manifest(
200 manifest,
201 crate_name_for_template,
202 app_name,
203 &project
204 .resolved_framework()
205 .await
206 .map_err(crate::backend::FailToInitBackend::Config)?,
207 )
208 .with_backend_project_path(project.backend_path::<Self>())
209 .with_project_root_path(project.root().to_path_buf())
210 .with_ios_permissions(ios_permissions)
211 .with_webview_enabled(webview_enabled)
212 .with_chromium_enabled(chromium_enabled)
213 .with_browser_engine(browser_engine)
214 .with_launch(launch_entry);
215
216 templates::apple::scaffold(&project.backend_path::<Self>(), &ctx)
217 .await
218 .map_err(crate::backend::FailToInitBackend::Io)?;
219
220 Ok(Self {
221 project_path,
222 scheme,
223 branch: existing.and_then(|backend| backend.branch.clone()),
224 revision: existing.and_then(|backend| backend.revision.clone()),
225 backend_path: existing.and_then(|backend| backend.backend_path.clone()),
226 })
227 }
228
229 fn supports(&self, platform: TargetPlatform) -> bool {
230 is_apple_platform(platform)
231 }
232
233 async fn build(
234 &self,
235 project: &Project,
236 platform: TargetPlatform,
237 options: BuildOptions,
238 ) -> eyre::Result<PathBuf> {
239 project
240 .browser_runtime_plan(platform, TargetBackend::Apple)
241 .await?;
242 build_rust_lib(project, platform, options).await
243 }
244
245 async fn package(
246 &self,
247 project: &Project,
248 platform: TargetPlatform,
249 options: PackageOptions,
250 ) -> eyre::Result<Artifact> {
251 package_apple(project, platform, options).await
252 }
253
254 async fn clean(&self, project: &Project, _platform: TargetPlatform) -> eyre::Result<()> {
255 clean_apple(project).await
256 }
257}