swift_bridge_build/
package.rs

1//! Generate a Swift Package from Rust code
2
3use std::collections::HashMap;
4use std::fs;
5use std::fs::OpenOptions;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::process::{Command, Stdio};
9use tempfile::tempdir;
10
11/// Config for generating Swift packages
12pub struct CreatePackageConfig {
13    /// The directory containing the generated bridges
14    pub bridge_dir: PathBuf,
15    /// Path per platform. e.g. `(ApplePlatform::iOS, "target/aarch64-apple-ios/debug/libmy_rust_lib.a")`
16    pub paths: HashMap<ApplePlatform, PathBuf>,
17    /// The directory where the package will be saved
18    pub out_dir: PathBuf,
19    /// The name for the Swift package
20    pub package_name: String,
21}
22
23impl CreatePackageConfig {
24    /// Creates a new `GeneratePackageConfig` for generating Swift Packages from Rust code.
25    pub fn new(
26        bridge_dir: PathBuf,
27        paths: HashMap<ApplePlatform, PathBuf>,
28        out_dir: PathBuf,
29        package_name: String,
30    ) -> Self {
31        Self {
32            bridge_dir,
33            paths,
34            out_dir,
35            package_name,
36        }
37    }
38}
39
40#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
41/// Currently supported platforms for genereting Swift Packages.
42pub enum ApplePlatform {
43    /// `aarch64-apple-ios`
44    IOS,
45    /// `x86_64-apple-ios`
46    /// `aarch64-apple-ios-sim`
47    ///
48    /// iOS simulator for debugging in XCode's simulator.
49    Simulator,
50    /// `x86_64-apple-darwin`
51    MacOS,
52    /// no official Rust target for this platform
53    MacCatalyst,
54    /// `aarch64-apple-tvos`
55    /// `x86_64-apple-tvos`
56    TvOS,
57    /// no official Rust target for this platform
58    WatchOS,
59    /// no official Rust target for this platform
60    WatchOSSimulator,
61    /// no official Rust target for this platform
62    CarPlayOS,
63    /// no official Rust target for this platform
64    CarPlayOSSimulator,
65}
66
67impl ApplePlatform {
68    /// The directory name inside of the xcframework for the specified platform.
69    pub fn dir_name(&self) -> &str {
70        match self {
71            ApplePlatform::IOS => "ios",
72            ApplePlatform::Simulator => "simulator",
73            ApplePlatform::MacOS => "macos",
74            ApplePlatform::MacCatalyst => "mac-catalyst",
75            ApplePlatform::TvOS => "tvos",
76            ApplePlatform::WatchOS => "watchos",
77            ApplePlatform::WatchOSSimulator => "watchos-simulator",
78            ApplePlatform::CarPlayOS => "carplay",
79            ApplePlatform::CarPlayOSSimulator => "carplay-simulator",
80        }
81    }
82
83    /// Array containing all `ApplePlatform` variants
84    pub const ALL: &'static [Self] = &[
85        ApplePlatform::IOS,
86        ApplePlatform::Simulator,
87        ApplePlatform::MacOS,
88        ApplePlatform::MacCatalyst,
89        ApplePlatform::TvOS,
90        ApplePlatform::WatchOS,
91        ApplePlatform::WatchOSSimulator,
92        ApplePlatform::CarPlayOS,
93        ApplePlatform::CarPlayOSSimulator,
94    ];
95}
96
97/// Generates an xcframework embedded in a Swift Package from the Rust project.
98///
99/// - Also see the [relevant book chapter](https://chinedufn.github.io/swift-bridge/building/swift-packages/index.html)
100pub fn create_package(config: CreatePackageConfig) {
101    // Create output directory //
102    let output_dir: &Path = config.out_dir.as_ref();
103    if !&output_dir.exists() {
104        fs::create_dir_all(&output_dir).expect("Couldn't create output directory");
105    }
106
107    // Generate RustXcframework //
108    gen_xcframework(&output_dir, &config);
109
110    // Generate Swift Package //
111    gen_package(&output_dir, &config);
112}
113
114/// Generates the RustXcframework
115fn gen_xcframework(output_dir: &Path, config: &CreatePackageConfig) {
116    // Create directories
117    let temp_dir = tempdir().expect("Couldn't create temporary directory");
118    let tmp_framework_path = &temp_dir.path().join("swiftbridge._tmp_framework");
119    fs::create_dir(&tmp_framework_path).expect("Couldn't create framework directory");
120
121    let include_dir = tmp_framework_path.join("include");
122    if !include_dir.exists() {
123        fs::create_dir(&include_dir).expect("Couldn't create inlcude directory for xcframework");
124    }
125
126    // Create modulemap
127    let modulemap_path = include_dir.join("module.modulemap");
128    fs::write(
129        &modulemap_path,
130        "module RustXcframework {\n    header \"SwiftBridgeCore.h\"\n",
131    )
132    .expect("Couldn't write modulemap file");
133    let mut modulemap_file = OpenOptions::new()
134        .write(true)
135        .append(true)
136        .open(&modulemap_path)
137        .expect("Couldn't open modulemap file for writing");
138
139    // copy headers
140    let bridge_dir: &Path = config.bridge_dir.as_ref();
141    fs::copy(
142        bridge_dir.join("SwiftBridgeCore.h"),
143        &include_dir.join("SwiftBridgeCore.h"),
144    )
145    .expect("Couldn't copy SwiftBirdgeCore header file");
146    let bridge_project_dir = fs::read_dir(&bridge_dir)
147        .expect("Couldn't read generated directory")
148        .find_map(|file| {
149            let file = file.unwrap().path();
150            if file.is_dir() {
151                Some(file)
152            } else {
153                None
154            }
155        })
156        .expect("Couldn't find project directory inside of generated directory");
157    let bridge_project_header_dir = fs::read_dir(&bridge_project_dir)
158        .expect("Couldn't read generated directory")
159        .find_map(|file| {
160            let file = file.unwrap().path();
161            if file.extension().unwrap() == "h" {
162                Some(file)
163            } else {
164                None
165            }
166        })
167        .expect("Couldn't find project's header file");
168    fs::copy(
169        &bridge_project_header_dir,
170        &include_dir.join(&bridge_project_header_dir.file_name().unwrap()),
171    )
172    .expect("Couldn't copy project's header file");
173    writeln!(
174        modulemap_file,
175        "    header \"{}\"",
176        bridge_project_header_dir
177            .file_name()
178            .unwrap()
179            .to_str()
180            .unwrap()
181    )
182    .expect("Couldn't write to modulemap");
183    writeln!(modulemap_file, "    export *\n}}").expect("Couldn't write to modulemap");
184
185    // Copy libraries
186    for platform in &config.paths {
187        let platform_path = &tmp_framework_path.join(platform.0.dir_name());
188        if !platform_path.exists() {
189            fs::create_dir(&platform_path).expect(&format!(
190                "Couldn't create directory for target {:?}",
191                platform.0
192            ));
193        }
194
195        let lib_path: &Path = platform.1.as_ref();
196        fs::copy(lib_path, platform_path.join(lib_path.file_name().unwrap())).expect(&format!(
197            "Couldn't copy library for platform {:?}",
198            platform.0
199        ));
200    }
201
202    // build xcframework
203    let xcframework_dir = output_dir.join("RustXcframework.xcframework");
204    if xcframework_dir.exists() {
205        fs::remove_dir_all(&xcframework_dir).expect("Couldn't delete previous xcframework file");
206    }
207    fs::create_dir(&xcframework_dir).expect("Couldn't create directory for xcframework");
208
209    let mut args: Vec<String> = Vec::new();
210    args.push("-create-xcframework".to_string());
211    for platform in &config.paths {
212        let file_path = Path::new(platform.0.dir_name())
213            .join((platform.1.as_ref() as &Path).file_name().unwrap());
214
215        args.push("-library".to_string());
216        args.push(file_path.to_str().unwrap().trim().to_string());
217        args.push("-headers".to_string());
218        args.push("include".to_string());
219    }
220    args.push("-output".to_string());
221    args.push(
222        fs::canonicalize(xcframework_dir)
223            .expect("Couldn't convert output directory to absolute path")
224            .as_path()
225            .to_str()
226            .unwrap()
227            .to_string(),
228    );
229
230    let output = Command::new("xcodebuild")
231        .current_dir(&tmp_framework_path)
232        .args(args)
233        .stdout(Stdio::piped())
234        .spawn()
235        .expect("Failed to spawn xcodebuild")
236        .wait_with_output()
237        .expect("Failed to execute xcodebuild");
238    if !output.status.success() {
239        let stderr = std::str::from_utf8(&output.stderr).unwrap();
240        panic!("{}", stderr);
241    }
242
243    // Remove temporary directory
244    let temp_dir_string = temp_dir.path().to_str().unwrap().to_string();
245    if let Err(err) = temp_dir.close() {
246        eprintln!(
247            "Couldn't close temporary directory {} - {}",
248            temp_dir_string, err
249        );
250    }
251}
252
253/// Generates the Swift Package.
254///
255/// We copy the Swift files from our generated bridge dir into the Swift Package's Sources
256/// directory. We prepend `import RustXcframework` at the top of all of the Swift files inside of
257/// the package, since without this they'll all error due to not being able to see the Rust code
258/// that they depend on.
259/// The alternative would be to use something like `@_exported import RustXcframework`, but this
260/// would make the Rust xcframework (i.e. methods like __swift_bridge__$some_method) available to
261/// the Swift Package's consumer, which we don't want.
262fn gen_package(output_dir: &Path, config: &CreatePackageConfig) {
263    let sources_dir = output_dir.join("Sources").join(&config.package_name);
264    if !sources_dir.exists() {
265        fs::create_dir_all(&sources_dir).expect("Couldn't create directory for source files");
266    }
267
268    // Copy bridge `.swift` files and append import statements
269    let bridge_dir: &Path = config.bridge_dir.as_ref();
270    fs::write(
271        sources_dir.join("SwiftBridgeCore.swift"),
272        format!(
273            "import RustXcframework\n{}",
274            fs::read_to_string(&bridge_dir.join("SwiftBridgeCore.swift"))
275                .expect("Couldn't read core bridging swift file")
276        ),
277    )
278    .expect("Couldn't write core bridging swift file");
279
280    let bridge_project_dir = fs::read_dir(&bridge_dir)
281        .expect("Couldn't read generated directory")
282        .find_map(|file| {
283            let file = file.unwrap().path();
284            if file.is_dir() {
285                Some(file)
286            } else {
287                None
288            }
289        })
290        .expect("Couldn't find project directory inside of generated directory");
291    let bridge_project_swift_dir = fs::read_dir(&bridge_project_dir)
292        .expect("Couldn't read generated directory")
293        .find_map(|file| {
294            let file = file.unwrap().path();
295            if file.extension().unwrap() == "swift" {
296                Some(file)
297            } else {
298                None
299            }
300        })
301        .expect("Couldn't find project's bridging swift file");
302    fs::write(
303        sources_dir.join(&bridge_project_swift_dir.file_name().unwrap()),
304        format!(
305            "import RustXcframework\n{}",
306            fs::read_to_string(&bridge_project_swift_dir)
307                .expect("Couldn't read project's bridging swift file")
308        ),
309    )
310    .expect("Couldn't copy project's bridging swift file to the package");
311
312    // Generate Package.swift
313    let package_name = &config.package_name;
314    let package_swift = format!(
315        r#"// swift-tools-version:5.5.0
316import PackageDescription
317let package = Package(
318	name: "{package_name}",
319	products: [
320		.library(
321			name: "{package_name}",
322			targets: ["{package_name}"]),
323	],
324	dependencies: [],
325	targets: [
326		.binaryTarget(
327			name: "RustXcframework",
328			path: "RustXcframework.xcframework"
329		),
330		.target(
331			name: "{package_name}",
332			dependencies: ["RustXcframework"])
333	]
334)
335	"#
336    );
337
338    fs::write(output_dir.join("Package.swift"), package_swift)
339        .expect("Couldn't write Package.swift file");
340}