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