Skip to main content

waterui_cli/apple/
platform.rs

1//! Apple platform build and package utilities.
2//!
3//! This module provides utility functions for building and packaging Apple apps.
4//! These functions are used by `AppleBackend` to implement the `Backend` trait.
5
6use std::collections::BTreeSet;
7use std::ffi::OsString;
8use std::path::{Path, PathBuf};
9
10use askama::Template;
11use eyre::{Context, bail};
12use smol::fs;
13use target_lexicon::Architecture;
14use tracing::{debug, info};
15
16#[cfg(target_os = "macos")]
17use crate::browser_runtime;
18#[cfg(target_os = "macos")]
19use crate::macos_bundle::{package_cef_helper_app, remove_cef_helper_apps, sign_macos_app};
20use crate::{
21    apple::backend::AppleBackend,
22    apple::dynamic_runtime,
23    assets::{self, ResolvedFont},
24    build::{
25        BuildOptions, BuildProgress, BuiltTarget, RustBuild, RustDynamicLibraries, RustLinkage,
26    },
27    device::Artifact,
28    platform::{PackageOptions, TargetBackend, TargetPlatform},
29    project::{BrowserRuntimePlan, Project, ResolvedWebViewBackend},
30    templates::FontRegistrationTemplateEntry,
31    toolchain::Host,
32    utils::{copy_file, run_command_os},
33};
34
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
36struct AppleNativeLinkInputs {
37    archives: Vec<PathBuf>,
38    linker_flags: Vec<String>,
39}
40
41// ============================================================================
42// Build Utilities
43// ============================================================================
44
45/// The library shape an Apple build hands to Xcode.
46///
47/// A packaged app links the runtime into itself and needs a self-contained archive. A
48/// development build resolves the runtime from `libwaterui_dylib.dylib` at load time, so
49/// the archive's contents are redundant there: `ld` satisfies the symbols from the dylib
50/// and pulls almost nothing out of the archive, which is why the shipped executable comes
51/// out around 19 MB from a 428 MB input. Emitting a `cdylib` instead expresses the same
52/// final link without materializing the archive at all — 9.8 MB instead of 428 MB, and
53/// proportionally less I/O on machines whose storage is slower than the one this was
54/// measured on.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56enum AppleHostLibrary {
57    /// Self-contained archive linked into a packaged application.
58    Archive,
59    /// Shared library that resolves the `WaterUI` runtime at load time.
60    Dynamic,
61}
62
63impl AppleHostLibrary {
64    const fn for_linkage(linkage: RustLinkage) -> Self {
65        match linkage {
66            RustLinkage::Static => Self::Archive,
67            RustLinkage::SharedRuntime => Self::Dynamic,
68        }
69    }
70
71    const fn crate_type(self) -> &'static str {
72        match self {
73            Self::Archive => "staticlib",
74            Self::Dynamic => "cdylib",
75        }
76    }
77
78    /// Name Xcode links against, via `-lwaterui_app` in `OTHER_LDFLAGS`.
79    const fn linked_file_name(self) -> &'static str {
80        match self {
81            Self::Archive => "libwaterui_app.a",
82            Self::Dynamic => "libwaterui_app.dylib",
83        }
84    }
85
86    /// The shape this build must delete, so `-lwaterui_app` cannot resolve to a stale
87    /// artifact left by a build of the other kind.
88    const fn superseded(self) -> Self {
89        match self {
90            Self::Archive => Self::Dynamic,
91            Self::Dynamic => Self::Archive,
92        }
93    }
94}
95
96/// Remove the host library shape this build did not produce.
97///
98/// `-lwaterui_app` resolves against whatever sits in the products directory, and `ld`
99/// prefers a `.dylib` over a `.a` when both are present. Leaving the previous build's
100/// artifact behind would let a packaging build silently link the development shared
101/// library, or leave a stale archive shadowing nothing at all.
102async fn remove_superseded_host_library(
103    directory: &Path,
104    produced: AppleHostLibrary,
105) -> eyre::Result<()> {
106    let stale = directory.join(produced.superseded().linked_file_name());
107    match fs::remove_file(&stale).await {
108        Ok(()) => Ok(()),
109        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
110        Err(error) => Err(error).wrap_err_with(|| {
111            format!(
112                "Failed to remove superseded host library {}",
113                stale.display()
114            )
115        }),
116    }
117}
118
119/// Cargo features an Apple FFI build resolves its dependency graph with.
120/// The `waterui-ffi` features an Apple runtime is compiled with.
121///
122/// Anything loaded into that runtime has to be compiled with the same set. Cargo
123/// unifies features per build and folds the result into the `-C metadata` hash it
124/// mangles into every symbol, so a module that enables one feature more or fewer
125/// than its host links against a runtime whose symbols no longer match. Both
126/// callers derive the set here rather than each listing it, so the two cannot
127/// drift apart.
128///
129/// # Errors
130///
131/// Returns an error when the project's enabled capabilities cannot be resolved.
132pub(crate) async fn apple_ffi_dependency_features(
133    project: &Project,
134    browser_runtime: BrowserRuntimePlan,
135) -> eyre::Result<Vec<String>> {
136    let build_manifest = project.ffi_crate_path().join("Cargo.toml");
137    let mut features = vec!["waterui-ffi/c-api".to_string()];
138    features.extend(
139        crate::project_model::assets::capability_ffi_features(project, &build_manifest).await?,
140    );
141    if browser_runtime.chromium {
142        features.push("waterui-ffi/chromium".to_string());
143    }
144    if matches!(browser_runtime.webview, Some(ResolvedWebViewBackend::Cef)) {
145        features.push("waterui-ffi/webview-cef".to_string());
146    }
147    Ok(features)
148}
149
150async fn apple_ffi_build_features(
151    project: &Project,
152    browser_runtime: BrowserRuntimePlan,
153    linkage: RustLinkage,
154) -> eyre::Result<Vec<String>> {
155    let mut features = apple_ffi_dependency_features(project, browser_runtime).await?;
156    if linkage == RustLinkage::SharedRuntime {
157        features.push("dev".to_string());
158    }
159    Ok(features)
160}
161
162/// Build Rust library for an Apple platform.
163///
164/// # Errors
165/// Returns an error if the Rust build fails or the expected Apple archive cannot be copied.
166pub async fn build_rust_lib(
167    project: &Project,
168    platform: TargetPlatform,
169    options: BuildOptions,
170) -> eyre::Result<BuiltTarget> {
171    // Resolve fonts BEFORE cargo build - this ensures icons.json is present
172    // for crates like fontawesome7 that need it during build.rs
173    let font_declarations =
174        crate::assets::scan_fonts(project, &project.ffi_crate_path().join("Cargo.toml")).await?;
175    let _resolved_fonts = crate::assets::resolve_fonts(font_declarations).await?;
176    let browser_runtime_plan = project
177        .browser_runtime_plan(platform, TargetBackend::Apple)
178        .await?;
179
180    let triple = options
181        .target_triple()
182        .cloned()
183        .unwrap_or_else(|| platform.triple());
184    let target = triple.to_string();
185    let target_underscore = target.replace('-', "_");
186    let host_library = AppleHostLibrary::for_linkage(options.linkage());
187    let mut build = RustBuild::new(project.ffi_crate_path(), triple.clone())
188        .with_project(project)
189        .with_features(
190            apple_ffi_build_features(project, browser_runtime_plan, options.linkage()).await?,
191        )
192        .with_crate_type_override(host_library.crate_type())
193        .with_envs(options.cargo_envs().iter().cloned());
194    if let Some(sccache_path) = options.sccache_path() {
195        build = build.with_sccache(sccache_path.to_path_buf());
196    }
197    if let Some(progress) = options.progress() {
198        build = build.with_progress(progress.clone());
199    }
200    build = build
201        .with_env("PKG_CONFIG_ALLOW_CROSS", "1")
202        .with_env(format!("PKG_CONFIG_ALLOW_CROSS_{target_underscore}"), "1")
203        .with_env(format!("PKG_CONFIG_ALLOW_CROSS_{target}"), "1");
204    let (deployment_environment, deployment_target) =
205        apple_deployment_target(project, platform).await?;
206    build = build.with_env(deployment_environment, deployment_target);
207    if options.linkage() == RustLinkage::SharedRuntime {
208        build = build.with_preferred_dynamic_linking();
209    }
210    build = build.with_target_dir(project.water_target_dir(options.linkage()).await?);
211    let built_target = build.build_lib(options.is_release()).await?;
212    // The helper `[[bin]]` exists only when the manifest declared it — the
213    // application's linked engine, not chromium alone — so the build gates
214    // on the manifest's own predicate or Cargo reports `no bin target`.
215    if project.declares_cef_helper().await? {
216        build
217            .clone()
218            .with_final_rustc_arg("-Clink-arg=-Wl,-rpath,@executable_path/../Frameworks")
219            .build_binary(
220                &crate::project_model::project_types::cef_helper_binary_name(
221                    project.ffi_crate_name().as_str(),
222                ),
223                options.is_release(),
224            )
225            .await?;
226    }
227
228    // If output_dir is specified, copy the library there
229    if let Some(output_dir) = options.output_dir() {
230        fs::create_dir_all(output_dir).await?;
231        let dest_lib = output_dir.join(host_library.linked_file_name());
232        copy_file(&built_target.artifact, &dest_lib).await?;
233        remove_superseded_host_library(output_dir, host_library).await?;
234        if options.linkage() == RustLinkage::SharedRuntime {
235            let libraries = RustDynamicLibraries::resolve(&built_target, &triple, project).await?;
236            dynamic_runtime::prepare_host_runtime(libraries.waterui()).await?;
237            libraries.stage(output_dir).await?;
238        }
239    }
240
241    Ok(built_target)
242}
243
244/// Resolve the deployment-target environment variable an Apple build must carry.
245///
246/// # Errors
247///
248/// Returns an error when the Xcode project does not define exactly one value.
249pub async fn apple_deployment_target(
250    project: &Project,
251    platform: TargetPlatform,
252) -> eyre::Result<(&'static str, String)> {
253    let backend = project
254        .apple_backend()
255        .ok_or_else(|| eyre::eyre!("Apple backend must be configured"))?;
256    let (environment, build_setting) = match platform {
257        TargetPlatform::MacOS => ("MACOSX_DEPLOYMENT_TARGET", "MACOSX_DEPLOYMENT_TARGET"),
258        TargetPlatform::IOS | TargetPlatform::IOSSimulator => {
259            ("IPHONEOS_DEPLOYMENT_TARGET", "IPHONEOS_DEPLOYMENT_TARGET")
260        }
261        other => {
262            bail!("Platform {other:?} does not have an Apple deployment target");
263        }
264    };
265    let project_file = project
266        .backend_path::<AppleBackend>()
267        .join(format!("{}.xcodeproj", backend.scheme))
268        .join("project.pbxproj");
269    let contents = fs::read_to_string(&project_file)
270        .await
271        .wrap_err_with(|| format!("Failed to read {}", project_file.display()))?;
272    let target = unique_xcode_build_setting(&contents, build_setting)?;
273    Ok((environment, target))
274}
275
276fn unique_xcode_build_setting(contents: &str, key: &str) -> eyre::Result<String> {
277    let prefix = format!("{key} = ");
278    let values = contents
279        .lines()
280        .filter_map(|line| line.trim().strip_prefix(&prefix))
281        .filter_map(|value| value.strip_suffix(';'))
282        .map(|value| value.trim_matches('"').to_string())
283        .collect::<BTreeSet<_>>();
284    match values.len() {
285        1 => Ok(values.into_iter().next().expect("one build setting value")),
286        0 => {
287            bail!("Xcode project does not define {key}");
288        }
289        _ => {
290            bail!(
291                "Xcode project defines conflicting {key} values: {}",
292                values.into_iter().collect::<Vec<_>>().join(", ")
293            );
294        }
295    }
296}
297
298// ============================================================================
299// Validation
300// ============================================================================
301
302/// The local Apple backend `[backend.apple] backend_path` names is the
303/// checkout the generated project references — validate it is a real Swift
304/// package. `waterui_path` alone no longer supplies one: the framework
305/// checkout carries no `backends/apple` tree since the submodule was dropped.
306fn validate_local_apple_backend(project: &Project) -> eyre::Result<()> {
307    let Some(backend_path) = project
308        .manifest()
309        .backends
310        .apple()
311        .and_then(|backend| backend.backend_path.as_deref())
312    else {
313        return Ok(());
314    };
315
316    let backend_root = {
317        let candidate = PathBuf::from(backend_path);
318        if candidate.is_absolute() {
319            candidate
320        } else {
321            project.root().join(candidate)
322        }
323    };
324
325    let package_manifest = backend_root.join("Package.swift");
326    if package_manifest.exists() {
327        return Ok(());
328    }
329
330    bail!(
331        "`[backend.apple] backend_path` points at `{}`, which has no `Package.swift` — \
332         the Apple backend lives in its own repository now; point it at an \
333         `apple-backend` checkout, or remove `backend_path` to consume the pinned \
334         release from SwiftPM.",
335        backend_root.display()
336    );
337}
338
339async fn ensure_apple_linker_flags(
340    xcodeproj: &Path,
341    required_flags: &[String],
342) -> eyre::Result<()> {
343    let pbxproj_path = xcodeproj.join("project.pbxproj");
344    if !pbxproj_path.exists() {
345        return Ok(());
346    }
347
348    let content = fs::read_to_string(&pbxproj_path)
349        .await
350        .wrap_err_with(|| format!("Failed to read {}", pbxproj_path.display()))?;
351    let (updated, changed) = inject_other_ldflags(&content, required_flags);
352    if changed {
353        fs::write(&pbxproj_path, updated)
354            .await
355            .wrap_err_with(|| format!("Failed to write {}", pbxproj_path.display()))?;
356        info!(
357            "Updated {} with required Apple linker flags",
358            pbxproj_path.display()
359        );
360    }
361
362    Ok(())
363}
364
365fn inject_other_ldflags(content: &str, required_flags: &[String]) -> (String, bool) {
366    let mut changed = false;
367    let mut lines = Vec::new();
368    for line in content.lines() {
369        if line.contains("OTHER_LDFLAGS = \"")
370            && let Some((prefix, rest)) = line.split_once("OTHER_LDFLAGS = \"")
371            && let Some((flags, suffix)) = rest.split_once("\";")
372        {
373            let (mut merged, _) = sanitize_other_ldflags(flags);
374            for required in required_flags {
375                if !merged.contains(required) {
376                    if !merged.is_empty() {
377                        merged.push(' ');
378                    }
379                    merged.push_str(required);
380                }
381            }
382            let line_changed = merged != flags;
383            if line_changed {
384                changed = true;
385            }
386            lines.push(format!("{prefix}OTHER_LDFLAGS = \"{merged}\";{suffix}"));
387            continue;
388        }
389        lines.push(line.to_string());
390    }
391
392    let mut updated = lines.join("\n");
393    if content.ends_with('\n') {
394        updated.push('\n');
395    }
396    (updated, changed)
397}
398
399fn sanitize_other_ldflags(flags: &str) -> (String, bool) {
400    let normalized = flags
401        .split_whitespace()
402        .filter(|flag| !matches!(*flag, "-lwaterui_app" | "-lwaterui_dylib"))
403        .collect::<Vec<_>>()
404        .join(" ");
405    let changed = normalized != flags;
406    (normalized, changed)
407}
408
409async fn collect_apple_native_link_inputs(lib_dir: &Path) -> eyre::Result<AppleNativeLinkInputs> {
410    let lib_dir = lib_dir.to_path_buf();
411    smol::unblock(move || collect_apple_native_link_inputs_sync(&lib_dir)).await
412}
413
414fn collect_apple_native_link_inputs_sync(lib_dir: &Path) -> eyre::Result<AppleNativeLinkInputs> {
415    let build_root = lib_dir.join("build");
416    if !build_root.exists() {
417        return Ok(AppleNativeLinkInputs::default());
418    }
419
420    let mut archive_paths = BTreeSet::new();
421    let mut linker_flags = Vec::new();
422
423    for entry in std::fs::read_dir(&build_root)? {
424        let entry = entry?;
425        let crate_build_dir = entry.path();
426        if !crate_build_dir.is_dir() {
427            continue;
428        }
429
430        // Stable names each unit dir `<pkg>-<hash>`; current nightly nests one
431        // level deeper under `<pkg>/<hash>` (#901). A top-level dir that is a
432        // build-script unit is processed directly, otherwise its hash subdirs
433        // are.
434        if is_build_script_unit_dir(&crate_build_dir) {
435            collect_link_inputs_from_unit_dir(
436                &crate_build_dir,
437                &mut archive_paths,
438                &mut linker_flags,
439            )?;
440        } else {
441            for sub_entry in std::fs::read_dir(&crate_build_dir)?.flatten() {
442                let sub_dir = sub_entry.path();
443                if sub_dir.is_dir() && is_build_script_unit_dir(&sub_dir) {
444                    collect_link_inputs_from_unit_dir(
445                        &sub_dir,
446                        &mut archive_paths,
447                        &mut linker_flags,
448                    )?;
449                }
450            }
451        }
452    }
453
454    Ok(AppleNativeLinkInputs {
455        archives: archive_paths.into_iter().collect(),
456        linker_flags,
457    })
458}
459
460/// A build-script unit dir carries the script's captured stdout — `output` on
461/// stable, `run/stdout` on current nightly. Compile units get an `out/` dir
462/// for their own artifacts too, so `out/` alone is not proof of a
463/// build-script unit under nightly.
464fn is_build_script_unit_dir(dir: &Path) -> bool {
465    dir.join("output").is_file() || dir.join("run").join("stdout").is_file()
466}
467
468fn collect_link_inputs_from_unit_dir(
469    unit_dir: &Path,
470    archive_paths: &mut BTreeSet<PathBuf>,
471    linker_flags: &mut Vec<String>,
472) -> eyre::Result<()> {
473    // Every crate that ran a build script may have emitted
474    // `cargo:rustc-link-*` directives; `-sys` crates like
475    // `system-configuration-sys` emit only those, with no archive or Swift
476    // bridge artifact to show for it, so the parse cannot be gated on
477    // outputs.
478    for output_path in [unit_dir.join("output"), unit_dir.join("run").join("stdout")] {
479        if output_path.is_file() {
480            let output = std::fs::read_to_string(&output_path)?;
481            for flag in apple_linker_flags_from_build_output(&output) {
482                push_unique_flag(linker_flags, flag);
483            }
484        }
485    }
486
487    // For a build-script unit, `out/` is OUT_DIR (nightly records it in
488    // `run/root-output`), so a `lib*.a` inside is an artifact the crate ships
489    // for linking, with or without a Swift bridge alongside it.
490    let out_dir = unit_dir.join("out");
491    if !out_dir.is_dir() {
492        return Ok(());
493    }
494
495    for out_entry in std::fs::read_dir(&out_dir)? {
496        let out_entry = out_entry?;
497        let path = out_entry.path();
498        if path.extension().is_some_and(|ext| ext == "a")
499            && path
500                .file_name()
501                .and_then(|name| name.to_str())
502                .is_some_and(|name| name.starts_with("lib"))
503        {
504            archive_paths.insert(path.clone());
505            if let Some(flag) = static_archive_link_flag(&path) {
506                push_unique_flag(linker_flags, flag);
507            }
508        }
509    }
510    Ok(())
511}
512
513fn static_archive_link_flag(archive_path: &Path) -> Option<String> {
514    let file_name = archive_path.file_name()?.to_str()?;
515    let library_name = file_name
516        .strip_prefix("lib")?
517        .strip_suffix(".a")
518        .unwrap_or(file_name);
519    Some(format!("-l{library_name}"))
520}
521
522fn apple_linker_flags_from_build_output(output: &str) -> Vec<String> {
523    let mut flags = Vec::new();
524    for line in output.lines() {
525        if let Some(framework) = line.strip_prefix("cargo:rustc-link-lib=framework=") {
526            push_unique_flag(&mut flags, format!("-framework {framework}"));
527        } else if let Some(arg) = line.strip_prefix("cargo:rustc-link-arg=") {
528            push_unique_flag(&mut flags, arg.to_string());
529        } else if let Some(search) = line.strip_prefix("cargo:rustc-link-search=") {
530            // A `-l<lib>` link arg a build script emits (waterkit-build's
531            // `-lclang_rt.osx`, resolved from the toolchain's
532            // `lib/clang/<ver>/lib/darwin`) only resolves at Xcode's link
533            // alongside the search path the same script declared for it.
534            // `native=`/`all=`/bare paths are `-L`, `framework=` is `-F`.
535            let (kind, dir) = search
536                .split_once('=')
537                .map_or(("all", search), |(kind, dir)| (kind, dir));
538            match kind {
539                "framework" => push_unique_flag(&mut flags, format!("-F{dir}")),
540                "native" | "all" => push_unique_flag(&mut flags, format!("-L{dir}")),
541                // `crate=` and `dependency=` name rustc's own artifact lookups,
542                // which Xcode's clang never performs.
543                _ => {}
544            }
545        }
546    }
547    flags
548}
549
550fn push_unique_flag(flags: &mut Vec<String>, flag: String) {
551    if !flags.iter().any(|existing| existing == &flag) {
552        flags.push(flag);
553    }
554}
555
556// ============================================================================
557// Clean
558// ============================================================================
559
560/// Clean Xcode build artifacts for an Apple platform.
561///
562/// # Errors
563/// Returns an error if `xcodebuild clean` fails or generated build directories cannot be removed.
564pub async fn clean_apple(project: &Project) -> eyre::Result<()> {
565    let Some(backend) = project.apple_backend() else {
566        return Ok(()); // Nothing to clean if no backend configured
567    };
568
569    let project_path = project.backend_path::<AppleBackend>();
570    let xcodeproj = project_path.join(format!("{}.xcodeproj", backend.scheme));
571
572    if !xcodeproj.exists() {
573        return Ok(());
574    }
575
576    let args: Vec<OsString> = vec![
577        "-project".into(),
578        xcodeproj.as_os_str().to_owned(),
579        "-scheme".into(),
580        backend.scheme.as_str().into(),
581        "clean".into(),
582    ];
583    run_command_os("xcodebuild", args).await?;
584
585    let build_dir = project_path.join("build");
586    if build_dir.exists() {
587        fs::remove_dir_all(&build_dir).await?;
588    }
589
590    Ok(())
591}
592
593// ============================================================================
594// Package
595// ============================================================================
596
597/// Package an Apple app using xcodebuild.
598///
599/// # Errors
600/// Returns an error if the backend is missing, packaging prerequisites are invalid, or `xcodebuild` fails.
601#[allow(clippy::too_many_lines)]
602pub async fn package_apple(
603    project: &Project,
604    platform: TargetPlatform,
605    options: PackageOptions,
606    built: &BuiltTarget,
607) -> eyre::Result<Artifact> {
608    let backend = project
609        .apple_backend()
610        .ok_or_else(|| eyre::eyre!("Apple backend must be configured"))?;
611    let browser_runtime_plan = project
612        .browser_runtime_plan(platform, TargetBackend::Apple)
613        .await?;
614
615    let project_path = project.backend_path::<AppleBackend>();
616    let xcodeproj = project_path.join(format!("{}.xcodeproj", backend.scheme));
617
618    if !xcodeproj.exists() {
619        bail!(
620            "Xcode project not found at {}. Did you run 'water create'?",
621            xcodeproj.display()
622        );
623    }
624
625    validate_local_apple_backend(project)?;
626
627    // Copy project assets and fonts
628    let app_resources_dir = project_path.join(&backend.scheme);
629    copy_assets_and_fonts(
630        project,
631        &app_resources_dir,
632        None,
633        options.uses_dev_server(),
634        options.progress(),
635    )
636    .await?;
637
638    let configuration = if options.is_debug() {
639        "Debug"
640    } else {
641        "Release"
642    };
643
644    let derived_data = project_path.join("DerivedData");
645    let triple = platform.triple();
646
647    // Copy the built Rust library to where Xcode expects it
648    let linkage = if options.uses_shared_rust_runtime() {
649        RustLinkage::SharedRuntime
650    } else {
651        RustLinkage::Static
652    };
653    let lib_dir = &built.profile_dir;
654    let host_library = AppleHostLibrary::for_linkage(linkage);
655    let source_lib = &built.artifact;
656
657    // Get SDK name - must be an Apple platform
658    let sdk_name = platform
659        .sdk_name()
660        .ok_or_else(|| eyre::eyre!("Platform {:?} is not an Apple platform", platform))?;
661
662    // Xcode uses "Debug-iphonesimulator" for simulators, "Debug" for macOS
663    let products_config = if sdk_name == "macosx" {
664        configuration.to_string()
665    } else {
666        format!("{configuration}-{sdk_name}")
667    };
668    let products_dir = derived_data.join("Build/Products").join(&products_config);
669    fs::create_dir_all(&products_dir).await?;
670    // The bundle is named after the project, not after the scheme, so that
671    // macOS shows the application's own name rather than the scaffold's.
672    let product_name = crate::apple::backend::apple_product_name(project)?;
673    let app_path = products_dir.join(format!("{product_name}.app"));
674
675    #[cfg(target_os = "macos")]
676    if platform == TargetPlatform::MacOS {
677        browser_runtime::remove_macos_app(&app_path.join("Contents")).await?;
678        remove_cef_helper_apps(&app_path, product_name).await?;
679    }
680
681    let dest_lib = products_dir.join(host_library.linked_file_name());
682    copy_file(&source_lib, &dest_lib).await?;
683    remove_superseded_host_library(&products_dir, host_library).await?;
684    if host_library == AppleHostLibrary::Dynamic {
685        dynamic_runtime::set_rpath_install_name(&dest_lib, host_library.linked_file_name()).await?;
686    }
687
688    let shared_runtime = if options.uses_shared_rust_runtime() {
689        let libraries = RustDynamicLibraries::resolve(built, &triple, project).await?;
690        dynamic_runtime::prepare_host_runtime(libraries.waterui()).await?;
691        libraries.stage(&products_dir).await?;
692        Some(libraries)
693    } else {
694        RustDynamicLibraries::remove_staged(&products_dir, &triple).await?;
695        None
696    };
697
698    let native_link_inputs = collect_apple_native_link_inputs(lib_dir).await?;
699    for archive in &native_link_inputs.archives {
700        let file_name = archive.file_name().ok_or_else(|| {
701            eyre::eyre!(
702                "Bridge archive path had no file name: {}",
703                archive.display()
704            )
705        })?;
706        copy_file(archive, &products_dir.join(file_name)).await?;
707    }
708
709    let mut required_link_flags = vec![
710        "-framework VideoToolbox".to_string(),
711        // The Xcode project no longer names the Rust library, because its shape depends
712        // on the linkage this build selected. `-lwaterui_app` resolves to whichever of
713        // `libwaterui_app.a` / `libwaterui_app.dylib` this build left in
714        // `BUILT_PRODUCTS_DIR`; the other is removed so the choice is unambiguous.
715        "-lwaterui_app".to_string(),
716    ];
717    if shared_runtime.is_some() {
718        required_link_flags.push("-lwaterui_dylib".to_string());
719    }
720    for flag in native_link_inputs.linker_flags {
721        push_unique_flag(&mut required_link_flags, flag);
722    }
723    ensure_apple_linker_flags(&xcodeproj, &required_link_flags).await?;
724
725    // Build with xcodebuild
726    // Determine the Xcode arch name from the platform architecture
727    let arch_name = match platform.arch() {
728        Architecture::Aarch64(_) => "arm64",
729        Architecture::X86_64 => "x86_64",
730        other => {
731            bail!("Unsupported Apple architecture for xcodebuild ARCHS: {other:?}");
732        }
733    };
734    let archs_arg = format!("ARCHS={arch_name}");
735
736    let mut args = vec![
737        OsString::from("-project"),
738        xcodeproj.as_os_str().to_owned(),
739        OsString::from("-scheme"),
740        backend.scheme.as_str().into(),
741        OsString::from("-configuration"),
742        configuration.into(),
743        OsString::from("-sdk"),
744        sdk_name.into(),
745        OsString::from("-derivedDataPath"),
746        derived_data.as_os_str().to_owned(),
747        archs_arg.into(),
748        OsString::from("ONLY_ACTIVE_ARCH=YES"),
749        OsString::from("build"),
750    ];
751
752    // Physical Apple OSes refuse unsigned code in every profile, so signing
753    // is disabled only for simulators and for local macOS debug builds. For a
754    // device target the build sets automatic signing with the developer team
755    // resolved from the keychain — the generated Xcode project cannot know it.
756    let device_platform = matches!(
757        platform,
758        TargetPlatform::IOS
759            | TargetPlatform::TvOS
760            | TargetPlatform::WatchOS
761            | TargetPlatform::VisionOS
762    );
763    if device_platform {
764        let team = crate::apple::toolchain::development_team_id(&Host::current()).await?;
765        // `-allowProvisioningUpdates` lets automatic signing mint the
766        // development provisioning profile a fresh bundle id does not have
767        // yet; without it xcodebuild fails GatherProvisioningInputs.
768        args.extend([
769            OsString::from("-allowProvisioningUpdates"),
770            OsString::from("CODE_SIGN_STYLE=Automatic"),
771            OsString::from(format!("DEVELOPMENT_TEAM={team}")),
772        ]);
773    } else if platform.is_simulator() || options.is_debug() {
774        args.extend([
775            OsString::from("CODE_SIGNING_ALLOWED=NO"),
776            OsString::from("CODE_SIGNING_REQUIRED=NO"),
777            OsString::from("CODE_SIGN_IDENTITY=-"),
778        ]);
779    }
780
781    // Optional capabilities are compiled out of the backend unless the app
782    // enabled the matching FFI feature, which is what exports their symbols.
783    let swift_conditions = apple_swift_conditions(project).await?;
784    if !swift_conditions.is_empty() {
785        args.push(format!("OTHER_SWIFT_FLAGS={}", swift_conditions.join(" ")).into());
786    }
787
788    // Tell Xcode's run-script phases not to call `water build` again (the
789    // Rust library is already built). The flag is scoped to the xcodebuild
790    // child and propagates to its script phases; it must never be set on
791    // this process.
792    Host::current()
793        .with_env("WATERUI_SKIP_RUST_BUILD", "1")
794        .run("xcodebuild", args)
795        .await?;
796
797    if !app_path.exists() {
798        bail!(
799            "Built app not found at {}. Check xcodebuild output for errors.",
800            app_path.display()
801        );
802    }
803
804    let frameworks_dir = apple_frameworks_dir(&app_path, sdk_name);
805    if let Some(libraries) = shared_runtime {
806        fs::create_dir_all(&frameworks_dir).await?;
807        libraries.stage(&frameworks_dir).await?;
808        // The executable resolves `@rpath/libwaterui_app.dylib` through the bundle's
809        // Frameworks directory, the same way it resolves the shared runtime.
810        copy_file(
811            &dest_lib,
812            &frameworks_dir.join(host_library.linked_file_name()),
813        )
814        .await?;
815    } else {
816        RustDynamicLibraries::remove_staged(&frameworks_dir, &triple).await?;
817    }
818
819    // The dylibs staged above land after `xcodebuild` signed the bundle, so
820    // they carry no signature. A device refuses them at `dyld`; sign them
821    // with the identity Xcode resolved for the app.
822    #[cfg(target_os = "macos")]
823    if device_platform {
824        crate::macos_bundle::sign_staged_device_libraries(&app_path, &frameworks_dir).await?;
825    }
826
827    #[cfg(target_os = "macos")]
828    if platform == TargetPlatform::MacOS && browser_runtime_plan.requires_cef() {
829        browser_runtime::stage_macos_app(browser_runtime_plan, lib_dir, &app_path.join("Contents"))
830            .await?;
831        // Helper bundles wrap the helper `[[bin]]`, which the manifest
832        // declares only when the application links the CEF engine crate —
833        // chromium alone stages the runtime but builds no helper.
834        if project.declares_cef_helper().await? {
835            let main_binary = app_path.join("Contents/MacOS").join(product_name);
836            let helper_binary =
837                lib_dir.join(crate::project_model::project_types::cef_helper_binary_name(
838                    project.ffi_crate_name().as_str(),
839                ));
840            package_cef_helper_app(
841                &app_path,
842                &main_binary,
843                &helper_binary,
844                project.bundle_identifier(),
845            )
846            .await?;
847        }
848        let requires_stable_identity = project.manifest().permissions.iter().any(|(key, entry)| {
849            entry.is_enabled() && !key.macos_usage_description_keys().is_empty()
850        });
851        sign_macos_app(
852            &app_path,
853            project.bundle_identifier(),
854            requires_stable_identity,
855        )
856        .await?;
857    }
858
859    #[cfg(not(target_os = "macos"))]
860    let _ = browser_runtime_plan;
861
862    Ok(Artifact::new(project.bundle_identifier(), app_path))
863}
864
865fn apple_frameworks_dir(app_path: &Path, sdk_name: &str) -> PathBuf {
866    if sdk_name == "macosx" {
867        app_path.join("Contents/Frameworks")
868    } else {
869        app_path.join("Frameworks")
870    }
871}
872
873// ============================================================================
874// Asset and Font Handling
875// ============================================================================
876
877/// Copy project assets and dependency fonts to the app resources directory.
878async fn copy_assets_and_fonts(
879    project: &Project,
880    dest_dir: &Path,
881    sccache_path: Option<&Path>,
882    dev_server: bool,
883    progress: Option<&BuildProgress>,
884) -> eyre::Result<()> {
885    // Stage project assets using platform-native conventions.
886    let manifest = assets::stage_project_assets_for_apple(
887        project,
888        dest_dir,
889        sccache_path,
890        dev_server,
891        progress,
892    )
893    .await?;
894
895    // Scan and resolve dependency fonts
896    let font_declarations =
897        assets::scan_fonts(project, &project.ffi_crate_path().join("Cargo.toml")).await?;
898    let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
899    resolved_fonts.extend(assets::scan_project_font_assets(&manifest)?);
900
901    if !resolved_fonts.is_empty() {
902        // Copy fonts to app resources
903        let fonts_dest = dest_dir.join("fonts");
904        assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
905
906        // Generate WaterUIFonts.swift for font registration
907        generate_font_registration_swift(&resolved_fonts, dest_dir).await?;
908
909        info!("Copied {} fonts to Apple app", resolved_fonts.len());
910    }
911
912    Ok(())
913}
914
915#[derive(Template)]
916#[template(
917    path = "src/templates/apple/AppName/WaterUIFonts.swift.tpl",
918    escape = "none"
919)]
920struct WaterUiFontsSwiftTemplate<'a> {
921    font_entries: &'a [FontRegistrationTemplateEntry],
922}
923
924/// Generate WaterUIFonts.swift file for registering custom fonts.
925async fn generate_font_registration_swift(
926    fonts: &[ResolvedFont],
927    dest_dir: &Path,
928) -> eyre::Result<()> {
929    let font_entries = fonts
930        .iter()
931        .map(|font| FontRegistrationTemplateEntry {
932            family_name: font.name.clone(),
933            file_name: font
934                .path
935                .file_name()
936                .and_then(|n| n.to_str())
937                .unwrap_or_default()
938                .to_string(),
939        })
940        .collect::<Vec<_>>();
941
942    let content = WaterUiFontsSwiftTemplate {
943        font_entries: &font_entries,
944    }
945    .render()
946    .map_err(|error| eyre::eyre!("Failed to render WaterUIFonts.swift template: {error}"))?;
947
948    let swift_path = dest_dir.join("WaterUIFonts.swift");
949    fs::write(&swift_path, content).await?;
950
951    debug!("Generated {}", swift_path.display());
952
953    Ok(())
954}
955
956// ============================================================================
957// Platform Support Check
958// ============================================================================
959
960/// Check if a platform is supported by the Apple backend.
961#[must_use]
962pub const fn is_apple_platform(platform: TargetPlatform) -> bool {
963    matches!(
964        platform,
965        TargetPlatform::MacOS
966            | TargetPlatform::IOS
967            | TargetPlatform::IOSSimulator
968            | TargetPlatform::TvOS
969            | TargetPlatform::TvOSSimulator
970            | TargetPlatform::WatchOS
971            | TargetPlatform::WatchOSSimulator
972            | TargetPlatform::VisionOS
973            | TargetPlatform::VisionOSSimulator
974    )
975}
976
977/// Swift compilation conditions matching the optional capabilities this app's
978/// graph carries, so the backend compiles exactly the components whose symbols
979/// exist.
980///
981/// The decision must come from [`assets::capability_enabled`] — the same
982/// predicate that forwards each capability's feature to the FFI build. The FFI
983/// features travel on the build command line, never into the generated
984/// manifest, so re-resolving `waterui-ffi`'s features from the manifest graph
985/// reads every capability as off and prunes components whose symbols the
986/// dylib does export.
987///
988/// The two lists mirror each capability's default polarity, so a bare
989/// `swift build` of the backend package — no conditions at all — still
990/// compiles what a default-featured app links. A default-off capability gets a
991/// positive condition when carried; a default-on capability gets a negative
992/// condition when dropped.
993///
994/// [`assets::capability_enabled`]: crate::project_model::assets::capability_enabled
995async fn apple_swift_conditions(project: &Project) -> eyre::Result<Vec<String>> {
996    /// Default-off capabilities, named when the app's graph carries them.
997    const OPTIONAL_COMPONENTS: &[(&str, &str)] =
998        &[("map", "WATERUI_MAP"), ("webview", "WATERUI_WEBVIEW")];
999    /// Default-on capabilities, named when the app's graph drops them.
1000    const DEFAULT_COMPONENTS: &[(&str, &str)] =
1001        &[("gpu", "WATERUI_NO_GPU"), ("media", "WATERUI_NO_MEDIA")];
1002
1003    let build_manifest = project.ffi_crate_path().join("Cargo.toml");
1004    let mut conditions = Vec::new();
1005    for (capability, condition) in OPTIONAL_COMPONENTS {
1006        if crate::project_model::assets::capability_enabled(project, &build_manifest, capability)
1007            .await?
1008        {
1009            conditions.push(format!("-D{condition}"));
1010        }
1011    }
1012    for (capability, condition) in DEFAULT_COMPONENTS {
1013        if !crate::project_model::assets::capability_enabled(project, &build_manifest, capability)
1014            .await?
1015        {
1016            conditions.push(format!("-D{condition}"));
1017        }
1018    }
1019    Ok(conditions)
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024    use tempfile::tempdir;
1025
1026    use super::{
1027        apple_linker_flags_from_build_output, collect_apple_native_link_inputs_sync,
1028        inject_other_ldflags, unique_xcode_build_setting,
1029    };
1030
1031    #[test]
1032    fn derives_a_unique_xcode_deployment_target() {
1033        let settings = "MACOSX_DEPLOYMENT_TARGET = 15.0;\nMACOSX_DEPLOYMENT_TARGET = 15.0;\n";
1034        assert_eq!(
1035            unique_xcode_build_setting(settings, "MACOSX_DEPLOYMENT_TARGET")
1036                .expect("unique deployment target"),
1037            "15.0"
1038        );
1039    }
1040
1041    #[test]
1042    fn rejects_conflicting_xcode_deployment_targets() {
1043        let settings = "MACOSX_DEPLOYMENT_TARGET = 14.0;\nMACOSX_DEPLOYMENT_TARGET = 15.0;\n";
1044        let error = unique_xcode_build_setting(settings, "MACOSX_DEPLOYMENT_TARGET")
1045            .expect_err("conflicting targets must fail");
1046        assert!(error.to_string().contains("14.0, 15.0"));
1047    }
1048
1049    #[test]
1050    fn injects_required_apple_frameworks_into_other_ldflags() {
1051        let input =
1052            "OTHER_LDFLAGS = \"-lwaterui_app -lc++\";\nOTHER_LDFLAGS = \"-lwaterui_app -lc++\";\n";
1053        let required_flags = vec!["-framework VideoToolbox".to_string()];
1054        let (output, changed) = inject_other_ldflags(input, &required_flags);
1055        assert!(changed);
1056        assert_eq!(output.matches("-framework VideoToolbox").count(), 2);
1057        assert!(!output.contains("-lwaterui_app"));
1058    }
1059
1060    #[test]
1061    fn linker_flag_injection_is_idempotent() {
1062        let input = "OTHER_LDFLAGS = \"-lc++ -framework VideoToolbox\";\n";
1063        let required_flags = vec!["-framework VideoToolbox".to_string()];
1064        let (output, changed) = inject_other_ldflags(input, &required_flags);
1065        assert!(!changed);
1066        assert_eq!(output, input);
1067    }
1068
1069    #[test]
1070    fn removes_redundant_waterui_app_link_flag() {
1071        let input = "OTHER_LDFLAGS = \"-lwaterui_app -lc++ -framework VideoToolbox\";\n";
1072        let required_flags = vec!["-framework VideoToolbox".to_string()];
1073        let (output, changed) = inject_other_ldflags(input, &required_flags);
1074        assert!(changed);
1075        assert_eq!(
1076            output,
1077            "OTHER_LDFLAGS = \"-lc++ -framework VideoToolbox\";\n"
1078        );
1079    }
1080
1081    #[test]
1082    fn switches_between_shared_runtime_and_static_link_flags() {
1083        let input = "OTHER_LDFLAGS = \"-lc++ -framework VideoToolbox\";\n";
1084        let dynamic_flags = vec![
1085            "-framework VideoToolbox".to_string(),
1086            "-lwaterui_dylib".to_string(),
1087        ];
1088        let (dynamic, changed) = inject_other_ldflags(input, &dynamic_flags);
1089        assert!(changed);
1090        assert!(dynamic.contains("-lwaterui_dylib"));
1091
1092        let static_flags = vec!["-framework VideoToolbox".to_string()];
1093        let (static_linked, changed) = inject_other_ldflags(&dynamic, &static_flags);
1094        assert!(changed);
1095        assert_eq!(static_linked, input);
1096    }
1097
1098    #[test]
1099    fn parses_frameworks_and_link_args_from_build_output() {
1100        let output = "cargo:rustc-link-lib=framework=AppKit\ncargo:rustc-link-arg=-rpath\ncargo:rustc-link-arg=/usr/lib/swift\ncargo:rustc-link-lib=framework=Foundation\n";
1101        let flags = apple_linker_flags_from_build_output(output);
1102        assert_eq!(
1103            flags,
1104            vec![
1105                "-framework AppKit".to_string(),
1106                "-rpath".to_string(),
1107                "/usr/lib/swift".to_string(),
1108                "-framework Foundation".to_string()
1109            ]
1110        );
1111    }
1112
1113    #[test]
1114    fn forwards_link_search_dirs_that_link_args_depend_on() {
1115        // waterkit-build 0.1.3 declares the compiler-rt builtins this way; the
1116        // `-l` alone made Xcode's link fail with "library 'clang_rt.osx' not
1117        // found" on every macOS and iOS package in the Apple nightly.
1118        let output = "cargo:rustc-link-search=native=/Xcode/lib/clang/17/lib/darwin\ncargo:rustc-link-arg=-lclang_rt.osx\ncargo:rustc-link-search=framework=/Frameworks\ncargo:rustc-link-search=/plain\ncargo:rustc-link-search=crate=/target/deps\ncargo:rustc-link-search=native=/Xcode/lib/clang/17/lib/darwin\n";
1119        let flags = apple_linker_flags_from_build_output(output);
1120        assert_eq!(
1121            flags,
1122            vec![
1123                "-L/Xcode/lib/clang/17/lib/darwin".to_string(),
1124                "-lclang_rt.osx".to_string(),
1125                "-F/Frameworks".to_string(),
1126                "-L/plain".to_string(),
1127            ]
1128        );
1129    }
1130
1131    #[test]
1132    fn collects_swift_bridge_archives_and_flags_from_target_build_dir() {
1133        let dir = tempdir().expect("tempdir");
1134        let lib_dir = dir.path().join("aarch64-apple-darwin/debug");
1135        let build_dir = lib_dir.join("build/waterkit-haptic-1234");
1136        let out_dir = build_dir.join("out");
1137        std::fs::create_dir_all(&out_dir).expect("create out dir");
1138        std::fs::write(out_dir.join("CombinedHelper.swift"), "// bridge").expect("write swift");
1139        std::fs::write(out_dir.join("libHelper.a"), "").expect("write archive");
1140        std::fs::write(
1141            build_dir.join("output"),
1142            "cargo:rustc-link-lib=framework=AppKit\ncargo:rustc-link-arg=-rpath\ncargo:rustc-link-arg=/usr/lib/swift\n",
1143        )
1144        .expect("write build output");
1145
1146        let link_inputs =
1147            collect_apple_native_link_inputs_sync(&lib_dir).expect("collect native link inputs");
1148
1149        assert_eq!(link_inputs.archives, vec![out_dir.join("libHelper.a")]);
1150        assert_eq!(
1151            link_inputs.linker_flags,
1152            vec![
1153                "-framework AppKit".to_string(),
1154                "-rpath".to_string(),
1155                "/usr/lib/swift".to_string(),
1156                "-lHelper".to_string()
1157            ]
1158        );
1159    }
1160
1161    #[test]
1162    fn collects_link_flags_from_crates_without_swift_or_archives() {
1163        // A `-sys` crate that only emits `cargo:rustc-link-lib` directives has
1164        // nothing in `out/`; its flags must still reach the linker.
1165        let dir = tempdir().expect("tempdir");
1166        let lib_dir = dir.path().join("aarch64-apple-darwin/release");
1167        let sys_build_dir = lib_dir.join("build/system-configuration-sys-1234");
1168        std::fs::create_dir_all(sys_build_dir.join("out")).expect("create out dir");
1169        std::fs::write(
1170            sys_build_dir.join("output"),
1171            "cargo:rustc-link-lib=framework=SystemConfiguration\n",
1172        )
1173        .expect("write build output");
1174
1175        // A crate can also ship an archive with no Swift bridge at all; the
1176        // archive and its `-l` flag must still be collected.
1177        let plain_build_dir = lib_dir.join("build/some-native-5678");
1178        let plain_out_dir = plain_build_dir.join("out");
1179        std::fs::create_dir_all(&plain_out_dir).expect("create out dir");
1180        std::fs::write(plain_out_dir.join("libwrapper.a"), "").expect("write archive");
1181        std::fs::write(
1182            plain_build_dir.join("output"),
1183            "cargo:rustc-link-lib=static=wrapper\n",
1184        )
1185        .expect("write build output");
1186
1187        let link_inputs =
1188            collect_apple_native_link_inputs_sync(&lib_dir).expect("collect native link inputs");
1189
1190        assert_eq!(
1191            link_inputs.archives,
1192            vec![plain_out_dir.join("libwrapper.a")]
1193        );
1194        let mut flags = link_inputs.linker_flags;
1195        flags.sort_unstable();
1196        assert_eq!(
1197            flags,
1198            vec![
1199                "-framework SystemConfiguration".to_string(),
1200                "-lwrapper".to_string()
1201            ]
1202        );
1203    }
1204
1205    #[test]
1206    fn collects_framework_flags_from_crates_without_swift_archives() {
1207        let dir = tempdir().expect("tempdir");
1208        let lib_dir = dir.path().join("aarch64-apple-darwin/debug");
1209        let build_dir = lib_dir.join("build/system-configuration-sys-1234");
1210        std::fs::create_dir_all(build_dir.join("out")).expect("create out dir");
1211        std::fs::write(
1212            build_dir.join("output"),
1213            "cargo:rustc-link-lib=framework=SystemConfiguration\n",
1214        )
1215        .expect("write build output");
1216
1217        let link_inputs =
1218            collect_apple_native_link_inputs_sync(&lib_dir).expect("collect native link inputs");
1219
1220        assert!(link_inputs.archives.is_empty());
1221        assert_eq!(
1222            link_inputs.linker_flags,
1223            vec!["-framework SystemConfiguration".to_string()]
1224        );
1225    }
1226    #[test]
1227    fn collects_link_inputs_from_nightly_build_layout() {
1228        // Nightly cargo nests unit dirs as `build/<pkg>/<hash>` and records the
1229        // script's captured stdout at `run/stdout` instead of `output` (#901).
1230        let dir = tempdir().expect("tempdir");
1231        let lib_dir = dir.path().join("aarch64-apple-darwin/debug");
1232        let sys_unit = lib_dir.join("build/system-configuration-sys/1234abcd");
1233        std::fs::create_dir_all(sys_unit.join("run")).expect("create run dir");
1234        std::fs::write(
1235            sys_unit.join("run/stdout"),
1236            "cargo:rustc-link-lib=framework=SystemConfiguration\n",
1237        )
1238        .expect("write build stdout");
1239
1240        // A compile unit's `out/` holds its own artifacts, not OUT_DIR — it
1241        // must not be mined for archives.
1242        let compile_unit = lib_dir.join("build/plain-crate/5678efgh");
1243        std::fs::create_dir_all(compile_unit.join("out")).expect("create out dir");
1244        std::fs::write(compile_unit.join("out/libplain_crate.a"), "").expect("write archive");
1245
1246        let link_inputs =
1247            collect_apple_native_link_inputs_sync(&lib_dir).expect("collect native link inputs");
1248
1249        assert!(link_inputs.archives.is_empty());
1250        assert_eq!(
1251            link_inputs.linker_flags,
1252            vec!["-framework SystemConfiguration".to_string()]
1253        );
1254    }
1255}