1use std::path::PathBuf;
4use std::{env, fmt::Write};
5
6use color_eyre::eyre::{self, bail};
7use smol::fs;
8use target_lexicon::{
9 Aarch64Architecture, Architecture, DefaultToHost, Environment, OperatingSystem, Triple, Vendor,
10};
11
12use crate::{
13 apple::{
14 backend::AppleBackend,
15 device::{AppleDevice, AppleSimulator},
16 toolchain::{AppleSdk, AppleToolchain, Xcode},
17 },
18 build::{BuildOptions, RustBuild},
19 device::Artifact,
20 platform::{PackageOptions, Platform},
21 project::Project,
22 utils::{copy_file, run_command},
23};
24
25pub trait ApplePlatformExt: Platform {
34 fn sdk_name(&self) -> &'static str;
36
37 fn is_simulator(&self) -> bool;
39
40 fn arch(&self) -> Architecture;
42}
43
44async fn build_rust_lib(
50 project: &Project,
51 triple: Triple,
52 options: BuildOptions,
53) -> eyre::Result<PathBuf> {
54 let build = RustBuild::new(project.root(), triple, options.is_hot_reload());
55 let lib_dir = build.build_lib(options.is_release()).await?;
56
57 if let Some(output_dir) = options.output_dir() {
59 let lib_name = project.crate_name().replace('-', "_");
60 let source_lib = lib_dir.join(format!("lib{lib_name}.a"));
61
62 if source_lib.exists() {
63 fs::create_dir_all(output_dir).await?;
64 let dest_lib = output_dir.join("libwaterui_app.a");
65 copy_file(&source_lib, &dest_lib).await?;
66 }
67 }
68
69 Ok(lib_dir)
70}
71
72async fn validate_local_apple_backend(project: &Project) -> eyre::Result<()> {
73 let Some(waterui_path) = project.manifest().waterui_path.as_deref() else {
74 return Ok(());
75 };
76
77 let waterui_root = {
78 let candidate = PathBuf::from(waterui_path);
79 if candidate.is_absolute() {
80 candidate
81 } else {
82 project.root().join(candidate)
83 }
84 };
85
86 let package_manifest = waterui_root.join("backends/apple/Package.swift");
87 if package_manifest.exists() {
88 return Ok(());
89 }
90
91 let gitmodules_path = waterui_root.join(".gitmodules");
92 let submodule_hint = if gitmodules_path.exists() {
93 fs::read_to_string(&gitmodules_path)
94 .await
95 .ok()
96 .filter(|c| c.contains("backends/apple"))
97 .map(|_| {
98 format!(
99 "It looks like `backends/apple` is a git submodule; run `git submodule update --init --recursive` in `{}`.",
100 waterui_root.display()
101 )
102 })
103 } else {
104 None
105 };
106
107 let mut message = format!(
108 "Local Apple backend Swift package manifest not found at `{}`.\n\
109 This is typically caused by an incomplete local WaterUI checkout (e.g. missing submodules) or an incorrect `waterui_path` in `Water.toml`.\n",
110 package_manifest.display()
111 );
112
113 if let Some(hint) = submodule_hint {
114 writeln!(&mut message, "{hint}\n").unwrap();
115 } else {
116 writeln!(
117 &mut message,
118 "If you're using a local WaterUI checkout, ensure `backends/apple/` exists and contains `Package.swift`."
119 ).unwrap();
120 }
121
122 bail!("{message}");
123}
124
125async fn clean_apple(project: &Project) -> eyre::Result<()> {
127 let Some(backend) = project.apple_backend() else {
128 return Ok(()); };
130
131 let project_path = project.backend_path::<AppleBackend>();
132 let xcodeproj = project_path.join(format!("{}.xcodeproj", backend.scheme));
133
134 if !xcodeproj.exists() {
135 return Ok(());
136 }
137
138 run_command(
139 "xcodebuild",
140 [
141 "-project",
142 xcodeproj.to_str().unwrap_or_default(),
143 "-scheme",
144 &backend.scheme,
145 "clean",
146 ],
147 )
148 .await?;
149
150 let build_dir = project_path.join("build");
151 if build_dir.exists() {
152 fs::remove_dir_all(&build_dir).await?;
153 }
154
155 Ok(())
156}
157
158async fn package_apple<P: ApplePlatformExt>(
160 platform: &P,
161 project: &Project,
162 options: PackageOptions,
163) -> eyre::Result<Artifact> {
164 let backend = project
165 .apple_backend()
166 .ok_or_else(|| eyre::eyre!("Apple backend must be configured"))?;
167
168 let project_path = project.backend_path::<AppleBackend>();
169 let xcodeproj = project_path.join(format!("{}.xcodeproj", backend.scheme));
170
171 if !xcodeproj.exists() {
172 bail!(
173 "Xcode project not found at {}. Did you run 'water create'?",
174 xcodeproj.display()
175 );
176 }
177
178 validate_local_apple_backend(project).await?;
179
180 unsafe {
183 env::set_var("WATERUI_SKIP_RUST_BUILD", "1");
184 }
185
186 let configuration = if options.is_debug() {
187 "Debug"
188 } else {
189 "Release"
190 };
191
192 let derived_data = project_path.join(".water/DerivedData");
193
194 let target_dir = project.target_dir();
195
196 let profile = if options.is_debug() {
198 "debug"
199 } else {
200 "release"
201 };
202 let lib_dir = target_dir.join(platform.triple().to_string()).join(profile);
203 let lib_name = project.crate_name().replace('-', "_");
204 let source_lib = lib_dir.join(format!("lib{lib_name}.a"));
205
206 let products_config = if platform.sdk_name() == "macosx" {
208 configuration.to_string()
209 } else {
210 format!("{configuration}-{}", platform.sdk_name())
211 };
212 let products_dir = derived_data.join("Build/Products").join(&products_config);
213 fs::create_dir_all(&products_dir).await?;
214 let dest_lib = products_dir.join("libwaterui_app.a");
215 copy_file(&source_lib, &dest_lib).await?;
216
217 let arch_name = match platform.arch() {
220 Architecture::Aarch64(_) => "arm64",
221 Architecture::X86_64 => "x86_64",
222 _ => unimplemented!(),
223 };
224 let archs_arg = format!("ARCHS={arch_name}");
225
226 let mut args = vec![
227 "-project",
228 xcodeproj.to_str().unwrap_or_default(),
229 "-scheme",
230 &backend.scheme,
231 "-configuration",
232 configuration,
233 "-sdk",
234 platform.sdk_name(),
235 "-derivedDataPath",
236 derived_data.to_str().unwrap_or_default(),
237 &archs_arg,
238 "ONLY_ACTIVE_ARCHITECTURE=YES",
239 "build",
240 ];
241
242 if platform.is_simulator() || options.is_debug() {
243 args.extend([
244 "CODE_SIGNING_ALLOWED=NO",
245 "CODE_SIGNING_REQUIRED=NO",
246 "CODE_SIGN_IDENTITY=-",
247 ]);
248 }
249
250 run_command("xcodebuild", args.iter().copied()).await?;
251
252 unsafe {
254 env::set_var("WATERUI_SKIP_RUST_BUILD", "0");
255 }
256
257 let app_path = products_dir.join(format!("{}.app", backend.scheme));
258
259 if !app_path.exists() {
260 bail!(
261 "Built app not found at {}. Check xcodebuild output for errors.",
262 app_path.display()
263 );
264 }
265
266 Ok(Artifact::new(project.bundle_identifier(), app_path))
267}
268
269#[derive(Debug, Clone, Copy, Default)]
275pub struct MacOS;
276
277impl MacOS {
278 #[must_use]
280 pub const fn new() -> Self {
281 Self
282 }
283}
284
285impl Platform for MacOS {
286 type Device = AppleDevice;
287 type Toolchain = AppleToolchain;
288
289 async fn scan(&self) -> eyre::Result<Vec<Self::Device>> {
290 Ok(vec![])
292 }
293
294 async fn build(&self, project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
295 build_rust_lib(project, self.triple(), options).await
296 }
297
298 fn toolchain(&self) -> Self::Toolchain {
299 (Xcode, AppleSdk::Macos)
300 }
301
302 fn triple(&self) -> Triple {
303 Triple {
304 architecture: self.arch(),
305 vendor: Vendor::Apple,
306 operating_system: OperatingSystem::Darwin(None),
307 environment: Environment::Unknown,
308 binary_format: target_lexicon::BinaryFormat::Macho,
309 }
310 }
311
312 async fn clean(&self, project: &Project) -> eyre::Result<()> {
313 clean_apple(project).await
314 }
315
316 async fn package(&self, project: &Project, options: PackageOptions) -> eyre::Result<Artifact> {
317 package_apple(self, project, options).await
318 }
319}
320
321impl ApplePlatformExt for MacOS {
322 fn sdk_name(&self) -> &'static str {
323 "macosx"
324 }
325
326 fn is_simulator(&self) -> bool {
327 false
328 }
329
330 fn arch(&self) -> Architecture {
331 DefaultToHost::default().0.architecture
332 }
333}
334
335#[derive(Debug, Clone, Copy, Default)]
341pub struct Ios;
342
343impl Ios {
344 #[must_use]
346 pub const fn new() -> Self {
347 Self
348 }
349}
350
351impl Platform for Ios {
352 type Device = AppleDevice;
353 type Toolchain = AppleToolchain;
354
355 async fn scan(&self) -> eyre::Result<Vec<Self::Device>> {
356 Ok(vec![])
358 }
359
360 async fn build(&self, project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
361 build_rust_lib(project, self.triple(), options).await
362 }
363
364 fn toolchain(&self) -> Self::Toolchain {
365 (Xcode, AppleSdk::Ios)
366 }
367
368 fn triple(&self) -> Triple {
369 Triple {
370 architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64),
371 vendor: Vendor::Apple,
372 operating_system: OperatingSystem::IOS(None),
373 environment: Environment::Unknown,
374 binary_format: target_lexicon::BinaryFormat::Macho,
375 }
376 }
377
378 async fn clean(&self, project: &Project) -> eyre::Result<()> {
379 clean_apple(project).await
380 }
381
382 async fn package(&self, project: &Project, options: PackageOptions) -> eyre::Result<Artifact> {
383 package_apple(self, project, options).await
384 }
385}
386
387impl ApplePlatformExt for Ios {
388 fn sdk_name(&self) -> &'static str {
389 "iphoneos"
390 }
391
392 fn is_simulator(&self) -> bool {
393 false
394 }
395
396 fn arch(&self) -> Architecture {
397 Architecture::Aarch64(Aarch64Architecture::Aarch64)
398 }
399}
400
401#[derive(Debug, Clone, Copy, Default)]
407pub struct IosSimulator;
408
409impl IosSimulator {
410 #[must_use]
412 pub const fn new() -> Self {
413 Self
414 }
415}
416
417impl Platform for IosSimulator {
418 type Device = AppleDevice;
419 type Toolchain = AppleToolchain;
420
421 async fn scan(&self) -> eyre::Result<Vec<Self::Device>> {
422 let simulators = AppleSimulator::scan().await?;
423
424 let filtered: Vec<AppleDevice> = simulators
425 .into_iter()
426 .filter(|sim| {
427 !sim.device_type_identifier.contains("Apple-TV")
429 && !sim.device_type_identifier.contains("Apple-Watch")
430 && !sim.device_type_identifier.contains("Apple-Vision")
431 })
432 .map(AppleDevice::Simulator)
433 .collect();
434
435 Ok(filtered)
436 }
437
438 async fn build(&self, project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
439 build_rust_lib(project, self.triple(), options).await
440 }
441
442 fn toolchain(&self) -> Self::Toolchain {
443 (Xcode, AppleSdk::Ios)
444 }
445
446 fn triple(&self) -> Triple {
447 let arch = self.arch();
448 let env = match arch {
449 Architecture::X86_64 => Environment::Unknown,
450 _ => Environment::Sim,
451 };
452
453 Triple {
454 architecture: arch,
455 vendor: Vendor::Apple,
456 operating_system: OperatingSystem::IOS(None),
457 environment: env,
458 binary_format: target_lexicon::BinaryFormat::Macho,
459 }
460 }
461
462 async fn clean(&self, project: &Project) -> eyre::Result<()> {
463 clean_apple(project).await
464 }
465
466 async fn package(&self, project: &Project, options: PackageOptions) -> eyre::Result<Artifact> {
467 package_apple(self, project, options).await
468 }
469}
470
471impl ApplePlatformExt for IosSimulator {
472 fn sdk_name(&self) -> &'static str {
473 "iphonesimulator"
474 }
475
476 fn is_simulator(&self) -> bool {
477 true
478 }
479
480 fn arch(&self) -> Architecture {
481 DefaultToHost::default().0.architecture
482 }
483}
484
485#[derive(Debug, Clone)]
494pub struct ApplePlatform {
495 arch: Architecture,
496 kind: ApplePlatformKind,
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum ApplePlatformKind {
502 MacOS,
504 Ios,
506 IosSimulator,
508 TvOs,
510 TvOsSimulator,
512 WatchOs,
514 WatchOsSimulator,
516 VisionOs,
518 VisionOsSimulator,
520}
521
522impl ApplePlatform {
523 #[must_use]
525 pub const fn new(arch: Architecture, kind: ApplePlatformKind) -> Self {
526 Self { arch, kind }
527 }
528
529 #[must_use]
531 pub fn macos() -> Self {
532 Self {
533 arch: DefaultToHost::default().0.architecture,
534 kind: ApplePlatformKind::MacOS,
535 }
536 }
537
538 #[must_use]
540 pub const fn ios() -> Self {
541 Self {
542 arch: Architecture::Aarch64(Aarch64Architecture::Aarch64),
543 kind: ApplePlatformKind::Ios,
544 }
545 }
546
547 #[must_use]
549 pub fn ios_simulator() -> Self {
550 Self {
551 arch: DefaultToHost::default().0.architecture,
552 kind: ApplePlatformKind::IosSimulator,
553 }
554 }
555
556 #[must_use]
558 pub const fn ios_simulator_arm64() -> Self {
559 Self {
560 arch: Architecture::Aarch64(Aarch64Architecture::Aarch64),
561 kind: ApplePlatformKind::IosSimulator,
562 }
563 }
564
565 #[must_use]
567 pub const fn ios_simulator_x86_64() -> Self {
568 Self {
569 arch: Architecture::X86_64,
570 kind: ApplePlatformKind::IosSimulator,
571 }
572 }
573
574 #[must_use]
576 pub const fn macos_arm64() -> Self {
577 Self {
578 arch: Architecture::Aarch64(Aarch64Architecture::Aarch64),
579 kind: ApplePlatformKind::MacOS,
580 }
581 }
582
583 #[must_use]
585 pub const fn macos_x86_64() -> Self {
586 Self {
587 arch: Architecture::X86_64,
588 kind: ApplePlatformKind::MacOS,
589 }
590 }
591
592 #[must_use]
594 pub fn from_device_type_identifier(id: &str) -> Self {
595 let is_simulator = id.contains("CoreSimulator");
596 let arch = if is_simulator {
597 DefaultToHost::default().0.architecture
598 } else {
599 Architecture::Aarch64(Aarch64Architecture::Aarch64)
600 };
601
602 let kind = if id.contains("Apple-TV") {
603 if is_simulator {
604 ApplePlatformKind::TvOsSimulator
605 } else {
606 ApplePlatformKind::TvOs
607 }
608 } else if id.contains("Apple-Watch") {
609 if is_simulator {
610 ApplePlatformKind::WatchOsSimulator
611 } else {
612 ApplePlatformKind::WatchOs
613 }
614 } else if id.contains("Apple-Vision") {
615 if is_simulator {
616 ApplePlatformKind::VisionOsSimulator
617 } else {
618 ApplePlatformKind::VisionOs
619 }
620 } else if id.contains("Mac") {
621 ApplePlatformKind::MacOS
622 } else if is_simulator {
623 ApplePlatformKind::IosSimulator
624 } else {
625 ApplePlatformKind::Ios
626 };
627
628 Self { arch, kind }
629 }
630
631 #[must_use]
633 pub const fn kind(&self) -> &ApplePlatformKind {
634 &self.kind
635 }
636
637 #[must_use]
639 pub const fn arch(&self) -> &Architecture {
640 &self.arch
641 }
642
643 #[must_use]
645 pub const fn sdk_name(&self) -> &'static str {
646 match self.kind {
647 ApplePlatformKind::MacOS => "macosx",
648 ApplePlatformKind::Ios => "iphoneos",
649 ApplePlatformKind::IosSimulator => "iphonesimulator",
650 ApplePlatformKind::TvOs => "appletvos",
651 ApplePlatformKind::TvOsSimulator => "appletvsimulator",
652 ApplePlatformKind::WatchOs => "watchos",
653 ApplePlatformKind::WatchOsSimulator => "watchsimulator",
654 ApplePlatformKind::VisionOs => "xros",
655 ApplePlatformKind::VisionOsSimulator => "xrsimulator",
656 }
657 }
658
659 #[must_use]
661 pub const fn is_simulator(&self) -> bool {
662 matches!(
663 self.kind,
664 ApplePlatformKind::IosSimulator
665 | ApplePlatformKind::TvOsSimulator
666 | ApplePlatformKind::WatchOsSimulator
667 | ApplePlatformKind::VisionOsSimulator
668 )
669 }
670}
671
672impl Platform for ApplePlatform {
673 type Device = AppleDevice;
674 type Toolchain = AppleToolchain;
675
676 async fn scan(&self) -> eyre::Result<Vec<Self::Device>> {
677 let simulators = AppleSimulator::scan().await?;
678
679 let filtered: Vec<AppleDevice> = simulators
680 .into_iter()
681 .filter(|sim| {
682 let sim_platform = Self::from_device_type_identifier(&sim.device_type_identifier);
683 matches!(
684 (&self.kind, &sim_platform.kind),
685 (
686 ApplePlatformKind::IosSimulator,
687 ApplePlatformKind::IosSimulator
688 ) | (
689 ApplePlatformKind::TvOsSimulator,
690 ApplePlatformKind::TvOsSimulator
691 ) | (
692 ApplePlatformKind::WatchOsSimulator,
693 ApplePlatformKind::WatchOsSimulator
694 ) | (
695 ApplePlatformKind::VisionOsSimulator,
696 ApplePlatformKind::VisionOsSimulator,
697 )
698 )
699 })
700 .map(AppleDevice::Simulator)
701 .collect();
702
703 Ok(filtered)
704 }
705
706 async fn build(&self, project: &Project, options: BuildOptions) -> eyre::Result<PathBuf> {
707 build_rust_lib(project, self.triple(), options).await
708 }
709
710 fn toolchain(&self) -> Self::Toolchain {
711 let sdk = match self.kind {
712 ApplePlatformKind::MacOS => AppleSdk::Macos,
713 ApplePlatformKind::Ios | ApplePlatformKind::IosSimulator => AppleSdk::Ios,
714 ApplePlatformKind::TvOs | ApplePlatformKind::TvOsSimulator => AppleSdk::TvOs,
715 ApplePlatformKind::WatchOs | ApplePlatformKind::WatchOsSimulator => AppleSdk::WatchOs,
716 ApplePlatformKind::VisionOs | ApplePlatformKind::VisionOsSimulator => {
717 AppleSdk::VisionOs
718 }
719 };
720 (Xcode, sdk)
721 }
722
723 fn triple(&self) -> Triple {
724 let (os, env) = match self.kind {
725 ApplePlatformKind::MacOS => (OperatingSystem::Darwin(None), Environment::Unknown),
726 ApplePlatformKind::Ios => (OperatingSystem::IOS(None), Environment::Unknown),
727 ApplePlatformKind::IosSimulator => match self.arch {
728 Architecture::X86_64 => (OperatingSystem::IOS(None), Environment::Unknown),
729 _ => (OperatingSystem::IOS(None), Environment::Sim),
730 },
731 ApplePlatformKind::TvOs => (OperatingSystem::TvOS(None), Environment::Unknown),
732 ApplePlatformKind::TvOsSimulator => (OperatingSystem::TvOS(None), Environment::Sim),
733 ApplePlatformKind::WatchOs => (OperatingSystem::WatchOS(None), Environment::Unknown),
734 ApplePlatformKind::WatchOsSimulator => {
735 (OperatingSystem::WatchOS(None), Environment::Sim)
736 }
737 ApplePlatformKind::VisionOs => (OperatingSystem::VisionOS(None), Environment::Unknown),
738 ApplePlatformKind::VisionOsSimulator => {
739 (OperatingSystem::VisionOS(None), Environment::Sim)
740 }
741 };
742
743 Triple {
744 architecture: self.arch,
745 vendor: Vendor::Apple,
746 operating_system: os,
747 environment: env,
748 binary_format: target_lexicon::BinaryFormat::Macho,
749 }
750 }
751
752 async fn clean(&self, project: &Project) -> eyre::Result<()> {
753 clean_apple(project).await
754 }
755
756 async fn package(&self, project: &Project, options: PackageOptions) -> eyre::Result<Artifact> {
757 package_apple(self, project, options).await
758 }
759}
760
761impl ApplePlatformExt for ApplePlatform {
762 fn sdk_name(&self) -> &'static str {
763 self.sdk_name()
764 }
765
766 fn is_simulator(&self) -> bool {
767 self.is_simulator()
768 }
769
770 fn arch(&self) -> Architecture {
771 self.arch
772 }
773}