mod spm;
mod xcframework;
use std::path::Path;
use crate::build::{BuildOptions, Builder, OutputCallback, all_successful, failed_targets};
use crate::cli::{CliError, Result};
use crate::commands::generate::{GenerateOptions, GenerateTarget, run_generate_with_output};
use crate::commands::pack::PackAppleOptions;
use crate::config::{Config, SpmDistribution, SpmLayout};
use crate::pack::PackError;
use crate::reporter::Reporter;
use super::{
discover_built_libraries_for_targets, missing_built_libraries, print_cargo_line,
resolve_build_cargo_args,
};
pub(crate) use self::spm::SpmPackageGenerator;
pub(crate) use self::xcframework::{XcframeworkBuilder, compute_checksum};
pub(crate) fn pack_apple(
config: &Config,
options: PackAppleOptions,
reporter: &Reporter,
) -> Result<()> {
if !config.is_apple_enabled() {
return Err(CliError::CommandFailed {
command: "targets.apple.enabled = false".to_string(),
status: None,
});
}
reporter.section("🍎", "Packing Apple");
if !config.apple_include_macos() {
reporter.warning("macOS excluded (targets.apple.include_macos = false)");
}
if options.spm_only && options.xcframework_only {
return Err(CliError::CommandFailed {
command: "cannot combine --spm-only and --xcframework-only".to_string(),
status: None,
});
}
let build_cargo_args = resolve_build_cargo_args(config, &options.execution.cargo_args);
let build_profile =
crate::build::resolve_build_profile(options.execution.release, &build_cargo_args);
let apple_targets = config.apple_targets();
if !options.execution.no_build {
let step = reporter.step("Building Apple targets");
build_apple_targets(
config,
&apple_targets,
options.execution.release,
&build_cargo_args,
&step,
)?;
step.finish_success();
}
let layout = options.layout.unwrap_or_else(|| config.apple_spm_layout());
let package_root = config.apple_spm_output();
if options.execution.regenerate {
let step = reporter.step("Generating Apple bindings");
generate_apple_bindings(config, layout, &package_root)?;
step.finish_success();
}
let libraries = discover_built_libraries_for_targets(
&config.crate_artifact_name(),
build_profile.output_directory_name(),
&apple_targets,
)?;
let apple_libraries: Vec<_> = libraries
.into_iter()
.filter(|library| library.target.platform().is_apple())
.collect();
let missing_targets = missing_built_libraries(&apple_targets, &apple_libraries);
if !missing_targets.is_empty() {
return Err(PackError::MissingBuiltLibraries {
platform: "Apple".to_string(),
targets: missing_targets,
}
.into());
}
let headers_dir = config.apple_header_output();
if !headers_dir.exists() {
return Err(CliError::FileNotFound(headers_dir));
}
let should_build_xcframework = !options.spm_only;
let should_generate_spm = !options.xcframework_only;
let xcframework_output = if should_build_xcframework {
let step = reporter.step("Creating xcframework");
let output = XcframeworkBuilder::new(config, apple_libraries.clone(), headers_dir.clone())
.build_with_zip()?;
step.finish_success();
Some(output)
} else {
None
};
if should_generate_spm {
let (checksum, version) = match config.apple_spm_distribution() {
SpmDistribution::Local => (None, None),
SpmDistribution::Remote => {
let checksum = xcframework_output
.as_ref()
.and_then(|output| output.checksum.clone())
.map(Ok)
.unwrap_or_else(|| {
let step = reporter.step("Computing checksum");
let result = existing_xcframework_checksum(config);
step.finish_success();
result
})?;
let version = options
.version
.or_else(detect_version)
.unwrap_or_else(|| "0.1.0".to_string());
(Some(checksum), Some(version))
}
};
if config.apple_spm_skip_package_swift() {
reporter.warning("Skipping Package.swift (skip_package_swift = true)");
} else {
let generator = match config.apple_spm_distribution() {
SpmDistribution::Local => SpmPackageGenerator::new_local(config, layout),
SpmDistribution::Remote => {
let checksum = checksum.ok_or_else(|| CliError::CommandFailed {
command: "remote SPM requires checksum".to_string(),
status: None,
})?;
let version = version.ok_or_else(|| CliError::CommandFailed {
command: "remote SPM requires version".to_string(),
status: None,
})?;
SpmPackageGenerator::new_remote(config, checksum, version, layout)
}
};
let step = reporter.step("Generating Package.swift");
let package_path = generator.generate()?;
step.finish_success_with(&format!("{}", package_path.display()));
}
}
Ok(())
}
fn build_apple_targets(
config: &Config,
targets: &[crate::target::RustTarget],
release: bool,
build_cargo_args: &[String],
step: &crate::reporter::Step,
) -> Result<()> {
let on_output: Option<OutputCallback> = if step.is_verbose() {
Some(Box::new(|line: &str| {
print_cargo_line(line);
}))
} else {
None
};
let build_options = BuildOptions {
release,
package: Some(config.library_name().to_string()),
cargo_args: build_cargo_args.to_vec(),
on_output,
};
let builder = Builder::new(config, build_options);
let results = builder.build_targets(targets)?;
if all_successful(&results) {
return Ok(());
}
let failed = failed_targets(&results);
Err(PackError::BuildFailed { targets: failed }.into())
}
fn generate_apple_bindings(config: &Config, layout: SpmLayout, package_root: &Path) -> Result<()> {
let swift_output_directory = match layout {
SpmLayout::Bundled => config
.apple_spm_wrapper_sources()
.map(|path| package_root.join(path).join("BoltFFI"))
.unwrap_or_else(|| package_root.join("Sources").join("BoltFFI")),
SpmLayout::FfiOnly => package_root.join("Sources").join("BoltFFI"),
SpmLayout::Split => config.apple_swift_output().join("BoltFFI"),
};
run_generate_with_output(
config,
GenerateOptions {
target: GenerateTarget::Swift,
output: Some(swift_output_directory),
experimental: false,
},
)?;
run_generate_with_output(
config,
GenerateOptions {
target: GenerateTarget::Header,
output: Some(config.apple_header_output()),
experimental: false,
},
)?;
Ok(())
}
fn existing_xcframework_checksum(config: &Config) -> Result<String> {
let xcframework_zip = config
.apple_xcframework_output()
.join(format!("{}.xcframework.zip", config.xcframework_name()));
if xcframework_zip.exists() {
return compute_checksum(&xcframework_zip);
}
Err(CliError::FileNotFound(xcframework_zip))
}
fn detect_version() -> Option<String> {
std::fs::read_to_string("Cargo.toml")
.ok()
.and_then(|content| {
content
.lines()
.find(|line| line.starts_with("version = "))
.and_then(|line| {
line.split('=')
.nth(1)
.map(|value| value.trim().trim_matches('"').to_string())
})
})
}