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 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
236const PLATFORM_COUNT: usize = 5;
238
239#[derive(ValueEnum, Copy, Clone, Debug)]
240#[value()]
241pub enum Platform {
242 Macos,
243 Ios,
244 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 .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
322fn 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
350fn 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 let mut rustup = command("rustup component list --toolchain nightly");
358 rustup.stdout(Stdio::piped());
359 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
379fn 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
399fn 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 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
432fn 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 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 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 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 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 let output_dir = PathBuf::from(package_name);
568 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}