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 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 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 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
262const PLATFORM_COUNT: usize = 5;
264
265#[derive(ValueEnum, Copy, Clone, Debug)]
266#[value()]
267pub enum Platform {
268 Macos,
269 Ios,
270 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 .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
422fn 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
450fn 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 let mut rustup = command("rustup component list --toolchain nightly");
458 rustup.stdout(Stdio::piped());
459 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
479fn 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
499fn 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 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
532fn 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 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 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 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 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 let output_dir = PathBuf::from(package_name);
670 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}