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
110fn default_apple_project_path() -> PathBuf {
111 PathBuf::from("apple")
112}
113
114fn is_default_apple_project_path(s: &Path) -> bool {
115 s == Path::new("apple")
116}
117
118impl Backend for AppleBackend {
119 const DEFAULT_PATH: &'static str = "apple";
120
121 const CACHE_PATHS: &'static [&'static str] = &["DerivedData"];
123
124 fn path(&self) -> &Path {
125 &self.project_path
126 }
127
128 async fn init(project: &Project) -> Result<Self, crate::backend::FailToInitBackend> {
129 let manifest = project.manifest();
130 let existing = manifest.backends.apple();
133
134 let is_playground =
137 manifest.package.package_type == crate::project::PackageType::Playground;
138
139 let (scheme, app_name, crate_name_for_template) = if is_playground {
142 (
143 "WaterUIApp".to_string(),
144 "WaterUIApp".to_string(),
145 CrateName::try_from("WaterUIApp").expect("playground crate name must be valid"),
146 )
147 } else {
148 let crate_name = project.crate_name().clone();
149 let app_name = templates::apple_app_name(&crate_name);
152 (crate_name.to_string(), app_name, crate_name)
153 };
154
155 let project_path = default_apple_project_path();
156
157 let ios_permissions = manifest
158 .permissions
159 .iter()
160 .filter(|(_, entry)| entry.is_enabled())
161 .filter_map(|(key, entry)| {
162 key.ios_plist_key()
163 .map(|plist_key| templates::IosPermissionTemplateEntry {
164 plist_key,
165 description: entry.description().to_string(),
166 })
167 })
168 .collect();
169 let webview_enabled = project
170 .uses_standard_webview()
171 .await
172 .map_err(crate::backend::FailToInitBackend::Config)?;
173 let chromium_enabled = project
174 .links_runtime_package("waterui-chromium")
175 .await
176 .map_err(crate::backend::FailToInitBackend::Config)?;
177 let browser_engine = project
178 .linked_browser_engine()
179 .await
180 .map_err(crate::backend::FailToInitBackend::Config)?;
181 let launch = crate::assets::project_launch_assets(project)
184 .map_err(crate::backend::FailToInitBackend::Config)?;
185 let launch_entry = templates::LaunchTemplateEntry {
186 has_background: launch.plan().background(ColorScheme::Light).is_some(),
187 has_image: launch.has_artwork(),
188 };
189 let ctx = TemplateContext::for_project_manifest(
190 manifest,
191 crate_name_for_template,
192 app_name,
193 &project
194 .resolved_framework()
195 .await
196 .map_err(crate::backend::FailToInitBackend::Config)?,
197 )
198 .with_backend_project_path(project.backend_path::<Self>())
199 .with_project_root_path(project.root().to_path_buf())
200 .with_ios_permissions(ios_permissions)
201 .with_webview_enabled(webview_enabled)
202 .with_chromium_enabled(chromium_enabled)
203 .with_browser_engine(browser_engine)
204 .with_launch(launch_entry);
205
206 templates::apple::scaffold(&project.backend_path::<Self>(), &ctx)
207 .await
208 .map_err(crate::backend::FailToInitBackend::Io)?;
209
210 Ok(Self {
211 project_path,
212 scheme,
213 branch: existing.and_then(|backend| backend.branch.clone()),
214 revision: existing.and_then(|backend| backend.revision.clone()),
215 backend_path: existing.and_then(|backend| backend.backend_path.clone()),
216 })
217 }
218
219 fn supports(&self, platform: TargetPlatform) -> bool {
220 is_apple_platform(platform)
221 }
222
223 async fn build(
224 &self,
225 project: &Project,
226 platform: TargetPlatform,
227 options: BuildOptions,
228 ) -> eyre::Result<PathBuf> {
229 project
230 .browser_runtime_plan(platform, TargetBackend::Apple)
231 .await?;
232 build_rust_lib(project, platform, options).await
233 }
234
235 async fn package(
236 &self,
237 project: &Project,
238 platform: TargetPlatform,
239 options: PackageOptions,
240 ) -> eyre::Result<Artifact> {
241 package_apple(project, platform, options).await
242 }
243
244 async fn clean(&self, project: &Project, _platform: TargetPlatform) -> eyre::Result<()> {
245 clean_apple(project).await
246 }
247}