Skip to main content

cargo_swift/commands/
package.rs

1use std::fmt::Display;
2use std::ops::Not;
3use std::path::PathBuf;
4use std::process::{Command, Stdio};
5
6use camino::Utf8PathBuf;
7use cargo_metadata::{Package, TargetKind};
8use clap::builder::TypedValueParser;
9use clap::{Args, ValueEnum};
10use convert_case::{Case, Casing};
11use dialoguer::{Input, MultiSelect};
12use execute::{command, Execute};
13use indicatif::MultiProgress;
14
15use crate::bindings::generate_bindings;
16use crate::console::*;
17use crate::console::{run_step, run_step_with_commands};
18use crate::lib_type::LibType;
19use crate::metadata::{metadata, MetadataExt};
20use crate::swiftpackage::{create_swiftpackage, recreate_output_dir};
21use crate::targets::*;
22use crate::xcframework::create_xcframework;
23
24#[derive(ValueEnum, Debug, Clone)]
25#[value()]
26pub enum LibTypeArg {
27    Automatic,
28    Dynamic,
29    Static,
30}
31
32impl Display for LibTypeArg {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::Automatic => write!(f, "automatic"),
36            Self::Static => write!(f, "static"),
37            Self::Dynamic => write!(f, "dynamic"),
38        }
39    }
40}
41
42impl From<LibTypeArg> for Option<LibType> {
43    fn from(value: LibTypeArg) -> Self {
44        match value {
45            LibTypeArg::Automatic => None,
46            LibTypeArg::Dynamic => Some(LibType::Dynamic),
47            LibTypeArg::Static => Some(LibType::Static),
48        }
49    }
50}
51
52#[derive(Debug, Clone)]
53pub struct FeatureOptions {
54    pub features: Option<Vec<String>>,
55    pub all_features: bool,
56    pub no_default_features: bool,
57}
58
59#[allow(clippy::too_many_arguments)]
60pub fn run(
61    platforms: Option<Vec<PlatformSpec>>,
62    build_target: Option<&str>,
63    package_name: Option<String>,
64    xcframework_name: Option<String>,
65    disable_warnings: bool,
66    config: Config,
67    mode: Mode,
68    lib_type_arg: LibTypeArg,
69    features: FeatureOptions,
70    skip_toolchains_check: bool,
71    swift_tools_version: &str,
72) -> Result<()> {
73    // Show deprecation warning if --xcframework-name is used
74    if xcframework_name.is_some() {
75        warning!(
76            &config,
77            "The --xcframework-name flag is deprecated and will be removed in a future release. \
78             The xcframework name is now derived from the FFI module name in uniffi.toml."
79        );
80    }
81
82    // TODO: Allow path as optional argument to take other directories than current directory
83    // let crates = metadata().uniffi_crates();
84    let crates = [metadata()
85        .current_crate()
86        .ok_or("Current directory is not part of a crate!")?];
87
88    if crates.len() == 1 {
89        return run_for_crate(
90            crates[0],
91            platforms.clone(),
92            build_target,
93            package_name,
94            xcframework_name,
95            disable_warnings,
96            &config,
97            mode,
98            lib_type_arg,
99            features,
100            skip_toolchains_check,
101            swift_tools_version,
102        );
103    } else if package_name.is_some() {
104        Err("Package name can only be specified when building a single crate!")?;
105    }
106
107    crates
108        .iter()
109        .map(|current_crate| {
110            info!(&config, "Packaging crate {}", current_crate.name);
111            run_for_crate(
112                current_crate,
113                platforms.clone(),
114                build_target,
115                None,
116                xcframework_name.clone(),
117                disable_warnings,
118                &config,
119                mode,
120                lib_type_arg.clone(),
121                features.clone(),
122                skip_toolchains_check,
123                swift_tools_version,
124            )
125        })
126        .filter_map(|result| result.err())
127        .collect::<Errors>()
128        .into()
129}
130
131#[allow(clippy::too_many_arguments)]
132fn run_for_crate(
133    current_crate: &Package,
134    platforms: Option<Vec<PlatformSpec>>,
135    build_target: Option<&str>,
136    package_name: Option<String>,
137    xcframework_name: Option<String>,
138    disable_warnings: bool,
139    config: &Config,
140    mode: Mode,
141    lib_type_arg: LibTypeArg,
142    features: FeatureOptions,
143    skip_toolchains_check: bool,
144    swift_tools_version: &str,
145) -> Result<()> {
146    let lib = current_crate
147        .targets
148        .iter()
149        .find(|t| t.kind.contains(&TargetKind::Lib))
150        .ok_or("No library tag defined in Cargo.toml!")?;
151    let lib_types = lib
152        .crate_types
153        .iter()
154        .filter_map(|t| t.clone().try_into().ok())
155        .collect::<Vec<_>>();
156    let lib_type = pick_lib_type(&lib_types, lib_type_arg.clone().into(), config)?;
157
158    if lib_type == LibType::Dynamic {
159        warning!(
160            &config,
161            "Building as dynamic library is discouraged. It might prevent apps that use this library from publishing to the App Store."
162        );
163    }
164
165    let crate_name = current_crate.name.to_lowercase();
166    let package_name =
167        package_name.unwrap_or_else(|| prompt_package_name(&crate_name, config.accept_all));
168
169    let platforms = platforms.unwrap_or_else(|| prompt_platforms(config.accept_all));
170
171    if platforms.is_empty() {
172        Err("At least 1 platform needs to be selected!")?;
173    }
174
175    let mut targets: Vec<_> = platforms
176        .iter()
177        .flat_map(|p| p.platform.into_apple_platforms())
178        .map(|p| p.target())
179        .collect();
180
181    if let Some(build_target) = build_target {
182        targets.retain_mut(|platform_target| match platform_target {
183            Target::Single { architecture, .. } => *architecture == build_target,
184            Target::Universal {
185                architectures,
186                display_name,
187                platform,
188                ..
189            } => {
190                let Some(architecture) = architectures.iter().find(|t| **t == build_target) else {
191                    return false;
192                };
193                *platform_target = Target::Single {
194                    architecture,
195                    display_name,
196                    platform: *platform,
197                };
198                true
199            }
200        });
201        if targets.is_empty() {
202            return Err(Error::from(format!(
203                "No matching build target for {}",
204                build_target
205            )));
206        }
207    }
208
209    if !skip_toolchains_check {
210        let missing_toolchains = check_installed_toolchains(&targets);
211        let nightly_toolchains = check_nightly_installed(&targets);
212
213        let installation_required =
214            &[missing_toolchains.as_slice(), nightly_toolchains.as_slice()].concat();
215
216        if !installation_required.is_empty() {
217            if config.accept_all || prompt_toolchain_installation(installation_required) {
218                install_toolchains(&missing_toolchains, config.silent)?;
219                if !nightly_toolchains.is_empty() {
220                    install_nightly_src(config.silent)?;
221                }
222            } else {
223                Err("Toolchains for some target platforms were missing!")?;
224            }
225        }
226    }
227
228    let crate_name = lib.name.replace('-', "_");
229    for target in &targets {
230        build_with_output(target, &crate_name, mode, lib_type, config, &features)?;
231    }
232
233    let ffi_module_name =
234        generate_bindings_with_output(&targets, &crate_name, mode, lib_type, config)?;
235
236    // Use the FFI module name as the xcframework name by default
237    let xcframework_name = xcframework_name.unwrap_or_else(|| ffi_module_name.clone());
238
239    recreate_output_dir(&package_name).expect("Could not create package output directory!");
240    create_xcframework_with_output(
241        &targets,
242        &crate_name,
243        &package_name,
244        &xcframework_name,
245        &ffi_module_name,
246        mode,
247        lib_type,
248        config,
249    )?;
250    create_package_with_output(
251        &package_name,
252        &xcframework_name,
253        disable_warnings,
254        &platforms,
255        swift_tools_version,
256        config,
257    )?;
258
259    Ok(())
260}
261
262// FIXME: This can be removed once variant_count is stabilized: https://doc.rust-lang.org/std/mem/fn.variant_count.html#:~:text=Function%20std%3A%3Amem%3A%3Avariant_count&text=Returns%20the%20number%20of%20variants,the%20return%20value%20is%20unspecified.
263const PLATFORM_COUNT: usize = 5;
264
265#[derive(ValueEnum, Copy, Clone, Debug)]
266#[value()]
267pub enum Platform {
268    Macos,
269    Ios,
270    // Platforms below are experimental
271    Tvos,
272    Watchos,
273    Visionos,
274    Maccatalyst,
275}
276
277impl Platform {
278    fn into_apple_platforms(self) -> Vec<ApplePlatform> {
279        match self {
280            Platform::Macos => vec![ApplePlatform::MacOS],
281            Platform::Ios => vec![ApplePlatform::IOS, ApplePlatform::IOSSimulator],
282            Platform::Tvos => vec![ApplePlatform::TvOS, ApplePlatform::TvOSSimulator],
283            Platform::Watchos => vec![ApplePlatform::WatchOS, ApplePlatform::WatchOSSimulator],
284            Platform::Visionos => vec![ApplePlatform::VisionOS, ApplePlatform::VisionOSSimulator],
285            Platform::Maccatalyst => vec![ApplePlatform::MacCatalyst],
286        }
287    }
288
289    fn display_name(&self) -> String {
290        let name = match self {
291            Platform::Macos => "macOS",
292            Platform::Ios => "iOS",
293            Platform::Tvos => "tvOS",
294            Platform::Watchos => "watchOS",
295            Platform::Visionos => "visionOS",
296            Platform::Maccatalyst => "Mac Catalyst",
297        };
298
299        format!(
300            "{name}{}",
301            if self.is_experimental() {
302                " (Experimental)"
303            } else {
304                ""
305            }
306        )
307    }
308
309    fn is_experimental(&self) -> bool {
310        match self {
311            Platform::Macos | Platform::Ios => false,
312            Platform::Tvos | Platform::Watchos | Platform::Visionos | Platform::Maccatalyst => true,
313        }
314    }
315
316    fn all() -> [Self; PLATFORM_COUNT] {
317        [
318            Self::Macos,
319            Self::Ios,
320            Self::Tvos,
321            Self::Watchos,
322            Self::Visionos,
323        ]
324    }
325}
326
327#[derive(Debug, Clone, Args)]
328pub struct PlatformSpec {
329    pub platform: Platform,
330    pub min_version: Option<String>,
331}
332
333impl PlatformSpec {
334    pub(crate) fn package_swift(&self) -> String {
335        let v = self.min_version.as_deref();
336        match self.platform {
337            Platform::Macos => format!(".macOS(.v{})", v.unwrap_or("10_15")),
338            Platform::Ios => format!(".iOS(.v{})", v.unwrap_or("13")),
339            Platform::Tvos => format!(".tvOS(.v{})", v.unwrap_or("13")),
340            Platform::Watchos => format!(".watchOS(.v{})", v.unwrap_or("6")),
341            Platform::Visionos => format!(".visionOS(.v{})", v.unwrap_or("1")),
342            Platform::Maccatalyst => format!(".macCatalyst(.v{})", v.unwrap_or("13")),
343        }
344    }
345}
346
347#[derive(Clone)]
348pub struct PlatformSpecParser;
349
350impl TypedValueParser for PlatformSpecParser {
351    type Value = PlatformSpec;
352
353    fn parse_ref(
354        &self,
355        _cmd: &clap::Command,
356        _arg: Option<&clap::Arg>,
357        value: &std::ffi::OsStr,
358    ) -> clap::error::Result<Self::Value> {
359        let s = value.to_string_lossy();
360
361        let (platform_str, min_version) = match s.split_once('@') {
362            Some((p, v)) => (p, Some(v.to_string())),
363            None => (s.as_ref(), None),
364        };
365
366        let platform = Platform::from_str(platform_str, true).map_err(|_| {
367            clap::error::Error::raw(
368                clap::error::ErrorKind::InvalidValue,
369                format!("invalid platform `{}`", platform_str),
370            )
371        })?;
372
373        Ok(PlatformSpec {
374            platform,
375            min_version,
376        })
377    }
378
379    fn possible_values(&self) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue>>> {
380        Some(Box::new(
381            Platform::value_variants()
382                .iter()
383                .filter_map(|v| v.to_possible_value()),
384        ))
385    }
386}
387
388fn prompt_platforms(accept_all: bool) -> Vec<PlatformSpec> {
389    let platforms = Platform::all();
390    let items = platforms.map(|p| p.display_name());
391
392    if accept_all {
393        return platforms
394            .into_iter()
395            .filter(|p| !p.is_experimental())
396            .map(|platform| PlatformSpec {
397                platform,
398                min_version: None,
399            })
400            .collect();
401    }
402
403    let theme = prompt_theme();
404    let selector = MultiSelect::with_theme(&theme)
405        .items(&items)
406        .with_prompt("Select Target Platforms")
407        // TODO: Move this to separate class and disable reporting to change style on success
408        // .report(false)
409        .defaults(&platforms.map(|p| !p.is_experimental()));
410
411    let chosen: Vec<usize> = selector.interact().unwrap();
412
413    chosen
414        .into_iter()
415        .map(|i| PlatformSpec {
416            platform: platforms[i],
417            min_version: None,
418        })
419        .collect()
420}
421
422/// Checks if toolchains for all target architectures are installed and returns a
423/// list containing the names of all missing toolchains
424fn check_installed_toolchains(targets: &[Target]) -> Vec<&'static str> {
425    let mut rustup = command!("rustup target list");
426    rustup.stdout(Stdio::piped());
427    let output = rustup
428        .execute_output()
429        .expect("Failed to check installed toolchains. Is rustup installed on your system?");
430    let output = String::from_utf8_lossy(&output.stdout);
431
432    let installed: Vec<_> = output
433        .split('\n')
434        .filter(|s| s.contains("installed"))
435        .map(|s| s.replace("(installed)", "").trim().to_owned())
436        .collect();
437
438    targets
439        .iter()
440        .filter(|t| !t.platform().is_tier_3())
441        .flat_map(|t| t.architectures())
442        .filter(|arch| {
443            !installed
444                .iter()
445                .any(|toolchain| toolchain.eq_ignore_ascii_case(arch))
446        })
447        .collect()
448}
449
450/// Checks if rust-src component for tier 3 targets are installed
451fn check_nightly_installed(targets: &[Target]) -> Vec<&'static str> {
452    if !targets.iter().any(|t| t.platform().is_tier_3()) {
453        return vec![];
454    }
455
456    // TODO: Check if the correct nightly toolchain itself is installed
457    let mut rustup = command("rustup component list --toolchain nightly");
458    rustup.stdout(Stdio::piped());
459    // HACK: Silence error that toolchain is not installed
460    rustup.stderr(Stdio::null());
461
462    let output = rustup
463        .execute_output()
464        .expect("Failed to check installed components. Is rustup installed on your system?");
465    let output = String::from_utf8_lossy(&output.stdout);
466
467    if output
468        .split('\n')
469        .filter(|s| s.contains("installed"))
470        .map(|s| s.replace("(installed)", "").trim().to_owned())
471        .any(|s| s.eq_ignore_ascii_case("rust-src"))
472    {
473        vec![]
474    } else {
475        vec!["rust-src (nightly)"]
476    }
477}
478
479/// Prompts the user to install the given **toolchains** by name
480fn prompt_toolchain_installation(toolchains: &[&str]) -> bool {
481    println!("The following toolchains are not installed:");
482
483    for toolchain in toolchains {
484        println!("\t{toolchain}")
485    }
486
487    let theme = prompt_theme();
488    let answer = Input::with_theme(&theme)
489        .with_prompt("Do you want to install them? [Y/n]")
490        .default("yes".to_owned())
491        .interact_text()
492        .unwrap()
493        .trim()
494        .to_lowercase();
495
496    answer.eq_ignore_ascii_case("yes") || answer.eq_ignore_ascii_case("y")
497}
498
499/// Attempts to install the given **toolchains**
500fn install_toolchains(toolchains: &[&str], silent: bool) -> Result<()> {
501    if toolchains.is_empty() {
502        return Ok(());
503    };
504
505    let multi = silent.not().then(MultiProgress::new);
506    let spinner = silent
507        .not()
508        .then(|| MainSpinner::with_message("Installing Toolchains...".to_owned()));
509    multi.add(&spinner);
510    spinner.start();
511    for toolchain in toolchains {
512        let mut install = Command::new("rustup");
513        install.args(["target", "install", toolchain]);
514        install.stdin(Stdio::null());
515
516        let step = silent.not().then(|| CommandSpinner::with_command(&install));
517        multi.add(&step);
518        step.start();
519
520        // TODO: make this a separate function and show error spinner on fail
521        install
522            .execute()
523            .map_err(|e| format!("Error while downloading toolchain {toolchain}: \n\t{e}"))?;
524
525        step.finish();
526    }
527    spinner.finish();
528
529    Ok(())
530}
531
532/// Attempts to install the "rust-src" component on nightly
533fn install_nightly_src(silent: bool) -> Result<()> {
534    let multi = silent.not().then(MultiProgress::new);
535    let spinner = silent
536        .not()
537        .then(|| MainSpinner::with_message("Installing Toolchains...".to_owned()));
538    multi.add(&spinner);
539    spinner.start();
540
541    let mut install = command("rustup toolchain install nightly");
542    install.stdin(Stdio::null());
543
544    let step = silent.not().then(|| CommandSpinner::with_command(&install));
545    multi.add(&step);
546    step.start();
547
548    // TODO: make this a separate function and show error spinner on fail
549    install
550        .execute()
551        .map_err(|e| format!("Error while installing rust-src on nightly: \n\t{e}"))?;
552
553    step.finish();
554
555    let mut install = command("rustup component add rust-src --toolchain nightly");
556    install.stdin(Stdio::null());
557
558    let step = silent.not().then(|| CommandSpinner::with_command(&install));
559    multi.add(&step);
560    step.start();
561
562    // TODO: make this a separate function and show error spinner on fail
563    install
564        .execute()
565        .map_err(|e| format!("Error while installing rust-src on nightly: \n\t{e}"))?;
566
567    step.finish();
568    spinner.finish();
569
570    Ok(())
571}
572
573fn prompt_package_name(crate_name: &str, accept_all: bool) -> String {
574    let default = crate_name.to_case(Case::UpperCamel);
575
576    if accept_all {
577        return default;
578    }
579
580    let theme = prompt_theme();
581    Input::with_theme(&theme)
582        .with_prompt("Swift Package Name")
583        .default(default)
584        .interact_text()
585        .unwrap()
586}
587
588fn pick_lib_type(
589    options: &[LibType],
590    suggested: Option<LibType>,
591    config: &Config,
592) -> Result<LibType> {
593    if let Some(result) = suggested.and_then(|t| options.iter().find(|&&i| t == i)) {
594        return Ok(*result);
595    }
596
597    // TODO: Prompt user if multiple library types are present
598    let choosen = if options.iter().any(|i| matches!(&i, LibType::Static)) {
599        LibType::Static
600    } else {
601        *options.first().ok_or("No supported library type was specified in Cargo.toml! \n\n Supported types are: \n\t- staticlib \n\t- cdylib")?
602    };
603
604    if let Some(suggested) = suggested {
605        // TODO: Show part of Cargo.toml here to help user fix this
606        warning!(config,
607            "No matching library type for --lib-type {suggested} found in Cargo.toml.\n  Building as {choosen} instead...")
608    }
609    Ok(choosen)
610}
611
612fn generate_bindings_with_output(
613    targets: &[Target],
614    lib_name: &str,
615    mode: Mode,
616    lib_type: LibType,
617    config: &Config,
618) -> Result<String> {
619    run_step(config, "Generating Swift bindings...", || {
620        let lib_file = library_file_name(lib_name, lib_type);
621        let target = metadata().target_dir();
622        let archs = targets
623            .first()
624            .ok_or("Could not generate UniFFI bindings: No target platform selected!")?
625            .architectures();
626        let arch = archs.first();
627        let lib_path: Utf8PathBuf = format!("{target}/{arch}/{mode}/{lib_file}").into();
628
629        generate_bindings(&lib_path)
630            .map_err(|e| format!("Could not generate UniFFI bindings for udl files due to the following error: \n {e}").into())
631    })
632}
633
634fn build_with_output(
635    target: &Target,
636    lib_name: &str,
637    mode: Mode,
638    lib_type: LibType,
639    config: &Config,
640    features: &FeatureOptions,
641) -> Result<()> {
642    let mut commands = target.commands(lib_name, mode, lib_type, features);
643    for command in &mut commands {
644        command.env("CARGO_TERM_COLOR", "always");
645    }
646
647    run_step_with_commands(
648        config,
649        format!("Building target {}", target.display_name()),
650        &mut commands,
651    )?;
652
653    Ok(())
654}
655
656#[allow(clippy::too_many_arguments)]
657fn create_xcframework_with_output(
658    targets: &[Target],
659    lib_name: &str,
660    package_name: &str,
661    xcframework_name: &str,
662    ffi_module_name: &str,
663    mode: Mode,
664    lib_type: LibType,
665    config: &Config,
666) -> Result<()> {
667    run_step(config, "Creating XCFramework...", || {
668        // TODO: show command spinner here with xcbuild command
669        let output_dir = PathBuf::from(package_name);
670        // TODO: make this configurable
671        let generated_dir = PathBuf::from("./generated");
672
673        create_xcframework(
674            targets,
675            lib_name,
676            xcframework_name,
677            ffi_module_name,
678            &generated_dir,
679            &output_dir,
680            mode,
681            lib_type,
682        )
683    })
684    .map_err(|e| format!("Failed to create XCFramework due to the following error: \n {e}").into())
685}
686
687fn create_package_with_output(
688    package_name: &str,
689    xcframework_name: &str,
690    disable_warnings: bool,
691    platforms: &[PlatformSpec],
692    swift_tools_version: &str,
693    config: &Config,
694) -> Result<()> {
695    run_step(
696        config,
697        format!("Creating Swift Package '{package_name}'..."),
698        || {
699            create_swiftpackage(
700                package_name,
701                xcframework_name,
702                disable_warnings,
703                platforms,
704                swift_tools_version,
705            )
706        },
707    )?;
708
709    let spinner = config.silent.not().then(|| {
710        MainSpinner::with_message(format!(
711            "Successfully created Swift Package in '{package_name}/'!"
712        ))
713    });
714    spinner.finish();
715
716    Ok(())
717}