In this article we will create a new Xcode project and implement the crust-build
pipeline for building the iOS target platform. This will let us run crust
on an iPhone - such that it could also be published to the Apple App Store - though you would need an active Apple Developer subscription to do that. We will:
crust-build
as part of its build pipelinecrust-build
projectIn the previous article we implemented the MacOS Desktop target - for iOS we will take a very similar approach in that the Xcode project will invoke crust-build
through a shell script build step to compile and prepare the Rust dependencies.
To start off, create a new ios
directory in your workspace:
+ root
+ android
+ crust-build
+ crust-main
+ ios
+ macos-desktop
Then open Xcode and create a new iOS app project:
Fill in the details as below - again noting that we don’t need Swift support as we will write barely any Apple code:
Select your ios
directory to save the project into. After completion it should look like this:
In the Deployment Info
section, change the following settings:
Portrait
in Device Orientation
Hide status bar
in Status bar style
Requires full screen
in Status bar style
main
from the Main Interface
fieldNext we will delete redundant source files that our app won’t need. Delete the following files (choose Move to Trash
):
AppDelegate.h
AppDelegate.m
SceneDelegate.h
SceneDelegate.m
ViewController.h
ViewController.m
Main.storyboard
Your project should now look like this:
The directory structure should be like:
+ root
...
+ ios
+ crust
+ crust
+ crust.xcodeproj
We need to tidy a few things in the Info.plist
file as well, open it and delete the Scene configuration
node as it contains references to some of the source files we just removed.
Also delete the two portrait related properties for iPad support - we only want crust
to run in landscape regardless of phone or tablet:
Next we will add the shell script build step which will invoke crust-build
for the iOS target. Navigate to the Build Phases
section and add a new script step:
Move the script step as far up the list as possible - just under the Dependencies
step, disable the Based on dependency analysis
checkbox and enter the following script:
set -e
export PATH="$PATH:/usr/local/bin:/opt/homebrew/bin:$HOME/.cargo/bin"
cd "$SRCROOT/../../crust-build"
cargo run -- --target ios --variant $CONFIGURATION
This is exactly the same as what we did for the MacOS Desktop article, except we are specifying ios
for crust-build
instead of macos-desktop
.
You can run the Xcode project now and observe in the Report navigator that our crust-build
application ran. You will have build errors but we’ll fix that soon enough.
The Rust code to build iOS is a bit more complicated than for the MacOS targets because we can’t use SDL as frameworks - rather we need them to be static libraries.
We also have an additional complication that with the introduction of Apple Silicon Mac computers, we have to differentiate between arm64
simulators versus arm64
devices - historically there was an assumption that building for arm64
means a phsyical phone which is no longer the case. We will need to build our Rust code for 3 targets:
We also need to combine the two simulator architectures together as they are not regarded as discrete platforms in the Xcode tooling.
The build flow for iOS is:
aarch64-apple-ios
, aarch64-apple-ios-sim
and x86_64-apple-ios
Frameworks
directory and symlink it into the Xcode project structurexcframework
containing its build outputsxcframework
containing its build outputsCargo.toml
manifest to specify a crate-type
of staticlib
- we did something similar in the Android target to change the Cargo crate typexcframework
containing our compiled code as a static libraryI’ll step through this flow a bit more methodically than we did for MacOS Desktop as some of the code needs a bit of explaining as we go.
Edit crust-build/src/ios.rs
and add the following constants:
const ARCHITECTURE_ARM64_IPHONE: &str = "aarch64-apple-ios";
const ARCHITECTURE_ARM64_SIMULATOR: &str = "aarch64-apple-ios-sim";
const ARCHITECTURE_X86_64_SIMULATOR: &str = "x86_64-apple-ios";
Then add a new method to install the required Rust toolchains - there isn’t anything here you haven’t seen before:
fn install_rust_dependencies() -> FailableUnit {
scripts::run(&Script::new(&format!(
"rustup target add {} {} {}",
ARCHITECTURE_ARM64_IPHONE, ARCHITECTURE_ARM64_SIMULATOR, ARCHITECTURE_X86_64_SIMULATOR
)))
}
Update the build
method:
pub fn build(context: &Context) -> FailableUnit {
context.print_summary();
install_rust_dependencies()?;
Ok(())
}
Add the following method to create a new Frameworks
working directory where our custom xcframeworks
will be published to, along with a symlink inside the Xcode project directory structure:
fn setup_frameworks_dir(context: &Context) -> Failable<PathBuf> {
let frameworks_dir = context.working_dir.join("Frameworks");
io::create_dir(&frameworks_dir)?;
io::create_symlink(&frameworks_dir, &context.target_home_dir.join("crust").join("Frameworks"), &context.working_dir)?;
Ok(frameworks_dir)
}
Update build
to call this method and hold onto the resulting directory path:
pub fn build(context: &Context) -> FailableUnit {
context.print_summary();
install_rust_dependencies()?;
let frameworks_dir = setup_frameworks_dir(context)?;
Ok(())
}
Ok this is where things start to get a bit more involved. We will need to download the SDL2 source code then build the static library Xcode project within it for all three of our target architectures. The SDL2 source code doesn’t currently (at the time of writing this) have an xcframework
output target of its own, so we need to manufacture our own xcframework
once all the architectures are built.
Add the following constants:
const SDL2_URL: &str = "https://www.libsdl.org/release/SDL2-2.0.14.zip";
const SDL2_DIR: &str = "SDL";
const SDL2_FRAMEWORK_NAME: &str = "SDL2.xcframework";
The SDL2_FRAMEWORK_NAME
constant is the name of the xcframework
we will manufacture with the outputs of the compiled source code.
Note: The directory where for SDL2 source code must be
SDL
so libraries such asSDL2_image
can automatically locate it.
Add the following method:
fn setup_sdl2(context: &Context, frameworks_dir: &PathBuf) -> FailableUnit {
let xcframework_path = frameworks_dir.join(SDL2_FRAMEWORK_NAME);
if xcframework_path.exists() {
return Ok(());
}
// The source code for SDL2 needs to be available for us to build the framework.
remote_zips::fetch(SDL2_URL, SDL2_DIR, &context.working_dir)?;
// This is the directory where the SDL2 Xcode project can be found which when compiled produces the static libraries we need.
let xcode_project_dir = context.working_dir.join(SDL2_DIR).join("Xcode").join("SDL");
// We need to build the static library for both simulators (ARM64 + x86_64). Note also that we are deliberately building the Release variant only.
logs::out(log_tag!(), "Compiling SDL2 simulator release static library ...");
scripts::run(&Script::new(
r#"xcodebuild -project "SDL.xcodeproj" -scheme "Static Library-iOS" -configuration Release -sdk iphonesimulator -derivedDataPath "build_iphonesimulator" -arch arm64 -arch x86_64 only_active_arch=no"#
).working_dir(&xcode_project_dir))?;
// We now need to build the static library for the phone target (ARM64).
logs::out(log_tag!(), "Compiling SDL2 phone release static library ...");
scripts::run(&Script::new(
r#"xcodebuild -project "SDL.xcodeproj" -scheme "Static Library-iOS" -configuration Release -sdk iphoneos -derivedDataPath "build_iphoneos" -arch arm64 only_active_arch=no"#
).working_dir(&xcode_project_dir))?;
// Once both the simulators + phone static libraries have been built, we need to merge them together into an XCFramework which will be embedded in the iOS project.
let headers_path = context.working_dir.join(SDL2_DIR).join("include");
logs::out(log_tag!(), "Creating SDL2 XCFramework ...");
scripts::run(&Script::new(&format!(
r#"xcodebuild -create-xcframework -library build_iphonesimulator/Build/Products/Release-iphonesimulator/libSDL2.a -headers {:?} -library build_iphoneos/Build/Products/Release-iphoneos/libSDL2.a -headers {:?} -output {:?}"#,
&headers_path,
&headers_path,
&xcframework_path
)).working_dir(&xcode_project_dir))?;
Ok(())
}
I have added code comments to describe what each part of the method does, but in essence we are compiling the SDL2 source code for both the iphonesimulator
and iphoneos
platforms, specifying explicitly the -derivedDataPath
so we know where the build outputs land.
Then we use the Xcode -create-xcframework
command line tool to generate our custom SDL2.xcframework
, pulling into it the libSDL2.a
outputs from the build_iphonesimulator
and build_iphoneos
derived data paths.
The result is an SDL2 xcframework
that contains the compiled static library artifacts to support iOS simulators and phones. We want to use the xcframework
approach to let Xcode build against the appropriate platform architecture automatically.
Update the build
method to run this:
pub fn build(context: &Context) -> FailableUnit {
context.print_summary();
install_rust_dependencies()?;
let frameworks_dir = setup_frameworks_dir(context)?;
setup_sdl2(context, &frameworks_dir)?;
Ok(())
}
The setup for SDL2 Image is very similar to SDL2. First add some constants:
const SDL2_IMAGE_URL: &str = "https://www.libsdl.org/projects/SDL_image/release/SDL2_image-2.0.5.zip";
const SDL2_IMAGE_DIR: &str = "SDL2_image";
const SDL2_IMAGE_FRAMEWORK_NAME: &str = "SDL2_image.xcframework";
Then add a new method to prepare the SDL2 Image libraries:
fn setup_sdl2_image(context: &Context, frameworks_dir: &PathBuf) -> FailableUnit {
let xcframework_path = frameworks_dir.join(SDL2_IMAGE_FRAMEWORK_NAME);
// No need to build if its output already exist.
if xcframework_path.exists() {
return Ok(());
}
// The source code for SDL2 image needs to be available for us to build the framework.
remote_zips::fetch(SDL2_IMAGE_URL, SDL2_IMAGE_DIR, &context.working_dir)?;
// // This is the directory where the SDL2 Image Xcode project can be found which when compiled produces the static libraries we need.
let xcode_project_dir = context.working_dir.join(SDL2_IMAGE_DIR).join("Xcode-iOS");
// We need to build the static library for both simulators (ARM64 + x86_64). Note also that we are deliberately building the Release variant only.
logs::out(log_tag!(), "Compiling SDL2 Image simulator release static library ...");
scripts::run(&Script::new(
r#"xcodebuild -project "SDL_image.xcodeproj" -scheme "libSDL_image-iOS" -configuration Release -sdk iphonesimulator -derivedDataPath "build_iphonesimulator" -arch arm64 -arch x86_64 only_active_arch=no"#
).working_dir(&xcode_project_dir))?;
// We now need to build the static library for the phone target (ARM64).
logs::out(log_tag!(), "Compiling SDL2 Image phone release static library ...");
scripts::run(&Script::new(
r#"xcodebuild -project "SDL_image.xcodeproj" -scheme "libSDL_image-iOS" -configuration Release -sdk iphoneos -derivedDataPath "build_iphoneos" -arch arm64 only_active_arch=no"#
).working_dir(&xcode_project_dir))?;
// Once both the simulators + phone static libraries have been built, we need to merge them together into an XCFramework which will be embedded in the iOS project.
// Note: We do not need to include the headers in the framework as we won't be accessing them in our iOS project directly.
logs::out(log_tag!(), "Creating SDL2 Image XCFramework ...");
scripts::run(&Script::new(&format!(
r#"xcodebuild -create-xcframework -library build_iphonesimulator/Build/Products/Release-iphonesimulator/libSDL2_image.a -library build_iphoneos/Build/Products/Release-iphoneos/libSDL2_image.a -output {:?}"#,
&xcframework_path)).working_dir(&xcode_project_dir))?;
Ok(())
}
Although the code is very similar to the SDL2 library, I haven’t tried to be too clever and extract out the common parts, preferring a bit of duplication to keep the code easier to read - fundamentally though we are doing the same thing.
Update the build
method to run this:
pub fn build(context: &Context) -> FailableUnit {
context.print_summary();
install_rust_dependencies()?;
let frameworks_dir = setup_frameworks_dir(context)?;
setup_sdl2(context, &frameworks_dir)?;
setup_sdl2_image(context, &frameworks_dir)?;
Ok(())
}
Next up we need to use our manifests
service that we wrote in the Android article to create a copy of the main Cargo.toml
but replace the crate-type
property to staticlib
. If we don’t do this, our own Rust code will not be compiled in a way that we can link as a static library in Xcode.
Update the build
method to create a manifest with staticlib
as the crate-type
via the manifests::create
invocation:
pub fn build(context: &Context) -> FailableUnit {
context.print_summary();
install_rust_dependencies()?;
let frameworks_dir = setup_frameworks_dir(context)?;
setup_sdl2(context, &frameworks_dir)?;
setup_sdl2_image(context, &frameworks_dir)?;
manifests::create(context, "staticlib")?;
Ok(())
}
We can now compile our Rust code, keeping in mind that we need to build the library Rust target rather than the binary target. We will loop through each of the three architectures and perform the appropriate Rust compilation command:
Note: debug builds will produce WAY bigger output files compared to release builds which are much more compact.
fn compile(context: &Context) -> FailableUnit {
for architecture in &vec![
ARCHITECTURE_ARM64_IPHONE,
ARCHITECTURE_ARM64_SIMULATOR,
ARCHITECTURE_X86_64_SIMULATOR,
] {
logs::out(log_tag!(), &format!("Compiling crust for architecture: {}", &architecture));
scripts::run(
&Script::new(&format!(
"cargo rustc {} --target-dir {:?} --lib --target {}",
context.variant.rust_compiler_flag(),
context.rust_build_dir,
architecture,
))
.working_dir(&context.working_dir),
)?;
}
Ok(())
}
The Rust compilation command is fairly straight forward - we just tell it to target each of our iOS architectures one at a time. Update build
to run the compilation:
pub fn build(context: &Context) -> FailableUnit {
context.print_summary();
install_rust_dependencies()?;
let frameworks_dir = setup_frameworks_dir(context)?;
setup_sdl2(context, &frameworks_dir)?;
setup_sdl2_image(context, &frameworks_dir)?;
manifests::create(context, "staticlib")?;
compile(context)?;
Ok(())
}
The final part of the Rust implementation is to collect the compiled Rust code artifacts and merge them into a single static library xcframework
that our Xcode project can consume. I have added comments in the code to describe what we are doing:
fn create_output(context: &Context, frameworks_dir: &PathBuf) -> FailableUnit {
let variant_dir = context.variant.id();
// Now that each of the architectures has been compiled, we need to generate an XCFramework which merges them all together for the iOS project.
// When generating an XCFramework we must actually join any compiled architectures that belong to the same family together first - in our case
// we have compiled 2 iOS simulator architectures (ARM64 + x86_64). If we were to try and make an XCFramework where each of the simulator
// architectures were bundled separately, the XCFramework command would fail so we must first join them into 1 static library. To join compiled
// architectures together we use the 'lipo' tool.
logs::out(log_tag!(), "Joining iOS simulator architectures together ...");
let simulator_static_library_dir = context
.rust_build_dir
.join(&format!("{}-{}", ARCHITECTURE_ARM64_SIMULATOR, ARCHITECTURE_X86_64_SIMULATOR))
.join(variant_dir);
// Make sure the path to our merged static library exists.
io::create_dir(&simulator_static_library_dir)?;
let simulator_static_library_path = simulator_static_library_dir.join("libcrustlib.a");
scripts::run(
&Script::new(&format!(
"lipo -create -output {:?} {:?} {:?}",
&simulator_static_library_path,
&context.rust_build_dir.join(ARCHITECTURE_ARM64_SIMULATOR).join(variant_dir).join("libcrustlib.a"),
&context.rust_build_dir.join(ARCHITECTURE_X86_64_SIMULATOR).join(variant_dir).join("libcrustlib.a"),
))
.working_dir(&context.working_dir),
)?;
// Now that we have a static library for the iPhone architecture and a static library representing both iOS simulator architectures (via the lipo tool),
// we can build the XCFramework which wraps it all up and can then be used as a framework dependency in the iOS project.
let xcframework_path = frameworks_dir.join("crust.xcframework");
let iphone_static_library_path =
context.rust_build_dir.join(ARCHITECTURE_ARM64_IPHONE).join(variant_dir).join("libcrustlib.a");
// Start off by deleting the existing framework if it exists - we should do this so building a debug/release variant always produces the appropriate XCFramework.
io::delete(&xcframework_path)?;
// Now run the appropriate Xcode command to create the framework.
logs::out(log_tag!(), "Creating XCFramework for 'crust' ...");
scripts::run(
&Script::new(&format!(
"xcodebuild -create-xcframework -library {:?} -library {:?} -output {:?}",
&iphone_static_library_path, &simulator_static_library_path, &xcframework_path,
))
.working_dir(&context.working_dir),
)?;
// The Frameworks directory now contains 'crust.xcframework' ready to be used in the iOS Xcode project.
Ok(())
}
Update the build
method to call this too - the full source code for ios.rs
will end up like this:
use crate::{
core::{
context::Context, failable::Failable, failable_unit::FailableUnit, io, logs, manifests, remote_zips, script::Script,
scripts,
},
log_tag,
};
use std::path::PathBuf;
const ARCHITECTURE_ARM64_IPHONE: &str = "aarch64-apple-ios";
const ARCHITECTURE_ARM64_SIMULATOR: &str = "aarch64-apple-ios-sim";
const ARCHITECTURE_X86_64_SIMULATOR: &str = "x86_64-apple-ios";
const SDL2_URL: &str = "https://www.libsdl.org/release/SDL2-2.0.14.zip";
const SDL2_DIR: &str = "SDL";
const SDL2_FRAMEWORK_NAME: &str = "SDL2.xcframework";
const SDL2_IMAGE_URL: &str = "https://www.libsdl.org/projects/SDL_image/release/SDL2_image-2.0.5.zip";
const SDL2_IMAGE_DIR: &str = "SDL2_image";
const SDL2_IMAGE_FRAMEWORK_NAME: &str = "SDL2_image.xcframework";
pub fn build(context: &Context) -> FailableUnit {
context.print_summary();
install_rust_dependencies()?;
let frameworks_dir = setup_frameworks_dir(context)?;
setup_sdl2(context, &frameworks_dir)?;
setup_sdl2_image(context, &frameworks_dir)?;
manifests::create(context, "staticlib")?;
compile(context)?;
create_output(context, &frameworks_dir)?;
Ok(())
}
fn install_rust_dependencies() -> FailableUnit {
scripts::run(&Script::new(&format!(
"rustup target add {} {} {}",
ARCHITECTURE_ARM64_IPHONE, ARCHITECTURE_ARM64_SIMULATOR, ARCHITECTURE_X86_64_SIMULATOR
)))
}
fn setup_frameworks_dir(context: &Context) -> Failable<PathBuf> {
let frameworks_dir = context.working_dir.join("Frameworks");
io::create_dir(&frameworks_dir)?;
io::create_symlink(&frameworks_dir, &context.target_home_dir.join("crust").join("Frameworks"), &context.working_dir)?;
Ok(frameworks_dir)
}
fn setup_sdl2(context: &Context, frameworks_dir: &PathBuf) -> FailableUnit {
let xcframework_path = frameworks_dir.join(SDL2_FRAMEWORK_NAME);
if xcframework_path.exists() {
return Ok(());
}
// The source code for SDL2 needs to be available for us to build the framework.
remote_zips::fetch(SDL2_URL, SDL2_DIR, &context.working_dir)?;
// This is the directory where the SDL2 Xcode project can be found which when compiled produces the static libraries we need.
let xcode_project_dir = context.working_dir.join(SDL2_DIR).join("Xcode").join("SDL");
// We need to build the static library for both simulators (ARM64 + x86_64). Note also that we are deliberately building the Release variant only.
logs::out(log_tag!(), "Compiling SDL2 simulator release static library ...");
scripts::run(&Script::new(
r#"xcodebuild -project "SDL.xcodeproj" -scheme "Static Library-iOS" -configuration Release -sdk iphonesimulator -derivedDataPath "build_iphonesimulator" -arch arm64 -arch x86_64 only_active_arch=no"#
).working_dir(&xcode_project_dir))?;
// We now need to build the static library for the phone target (ARM64).
logs::out(log_tag!(), "Compiling SDL2 phone release static library ...");
scripts::run(&Script::new(
r#"xcodebuild -project "SDL.xcodeproj" -scheme "Static Library-iOS" -configuration Release -sdk iphoneos -derivedDataPath "build_iphoneos" -arch arm64 only_active_arch=no"#
).working_dir(&xcode_project_dir))?;
// Once both the simulators + phone static libraries have been built, we need to merge them together into an XCFramework which will be embedded in the iOS project.
let headers_path = context.working_dir.join(SDL2_DIR).join("include");
logs::out(log_tag!(), "Creating SDL2 XCFramework ...");
scripts::run(&Script::new(&format!(
r#"xcodebuild -create-xcframework -library build_iphonesimulator/Build/Products/Release-iphonesimulator/libSDL2.a -headers {:?} -library build_iphoneos/Build/Products/Release-iphoneos/libSDL2.a -headers {:?} -output {:?}"#,
&headers_path,
&headers_path,
&xcframework_path
)).working_dir(&xcode_project_dir))?;
Ok(())
}
fn setup_sdl2_image(context: &Context, frameworks_dir: &PathBuf) -> FailableUnit {
let xcframework_path = frameworks_dir.join(SDL2_IMAGE_FRAMEWORK_NAME);
// No need to build if its output already exist.
if xcframework_path.exists() {
return Ok(());
}
// The source code for SDL2 image needs to be available for us to build the framework.
remote_zips::fetch(SDL2_IMAGE_URL, SDL2_IMAGE_DIR, &context.working_dir)?;
// // This is the directory where the SDL2 Image Xcode project can be found which when compiled produces the static libraries we need.
let xcode_project_dir = context.working_dir.join(SDL2_IMAGE_DIR).join("Xcode-iOS");
// We need to build the static library for both simulators (ARM64 + x86_64). Note also that we are deliberately building the Release variant only.
logs::out(log_tag!(), "Compiling SDL2 Image simulator release static library ...");
scripts::run(&Script::new(
r#"xcodebuild -project "SDL_image.xcodeproj" -scheme "libSDL_image-iOS" -configuration Release -sdk iphonesimulator -derivedDataPath "build_iphonesimulator" -arch arm64 -arch x86_64 only_active_arch=no"#
).working_dir(&xcode_project_dir))?;
// We now need to build the static library for the phone target (ARM64).
logs::out(log_tag!(), "Compiling SDL2 Image phone release static library ...");
scripts::run(&Script::new(
r#"xcodebuild -project "SDL_image.xcodeproj" -scheme "libSDL_image-iOS" -configuration Release -sdk iphoneos -derivedDataPath "build_iphoneos" -arch arm64 only_active_arch=no"#
).working_dir(&xcode_project_dir))?;
// Once both the simulators + phone static libraries have been built, we need to merge them together into an XCFramework which will be embedded in the iOS project.
// Note: We do not need to include the headers in the framework as we won't be accessing them in our iOS project directly.
logs::out(log_tag!(), "Creating SDL2 Image XCFramework ...");
scripts::run(&Script::new(&format!(
r#"xcodebuild -create-xcframework -library build_iphonesimulator/Build/Products/Release-iphonesimulator/libSDL2_image.a -library build_iphoneos/Build/Products/Release-iphoneos/libSDL2_image.a -output {:?}"#,
&xcframework_path)).working_dir(&xcode_project_dir))?;
Ok(())
}
fn compile(context: &Context) -> FailableUnit {
for architecture in &vec![
ARCHITECTURE_ARM64_IPHONE,
ARCHITECTURE_ARM64_SIMULATOR,
ARCHITECTURE_X86_64_SIMULATOR,
] {
logs::out(log_tag!(), &format!("Compiling crust for architecture: {}", &architecture));
scripts::run(
&Script::new(&format!(
"cargo rustc {} --target-dir {:?} --lib --target {}",
context.variant.rust_compiler_flag(),
context.rust_build_dir,
architecture,
))
.working_dir(&context.working_dir),
)?;
}
Ok(())
}
fn create_output(context: &Context, frameworks_dir: &PathBuf) -> FailableUnit {
let variant_dir = context.variant.id();
// Now that each of the architectures has been compiled, we need to generate an XCFramework which merges them all together for the iOS project.
// When generating an XCFramework we must actually join any compiled architectures that belong to the same family together first - in our case
// we have compiled 2 iOS simulator architectures (ARM64 + x86_64). If we were to try and make an XCFramework where each of the simulator
// architectures were bundled separately, the XCFramework command would fail so we must first join them into 1 static library. To join compiled
// architectures together we use the 'lipo' tool.
logs::out(log_tag!(), "Joining iOS simulator architectures together ...");
let simulator_static_library_dir = context
.rust_build_dir
.join(&format!("{}-{}", ARCHITECTURE_ARM64_SIMULATOR, ARCHITECTURE_X86_64_SIMULATOR))
.join(variant_dir);
// Make sure the path to our merged static library exists.
io::create_dir(&simulator_static_library_dir)?;
let simulator_static_library_path = simulator_static_library_dir.join("libcrustlib.a");
scripts::run(
&Script::new(&format!(
"lipo -create -output {:?} {:?} {:?}",
&simulator_static_library_path,
&context.rust_build_dir.join(ARCHITECTURE_ARM64_SIMULATOR).join(variant_dir).join("libcrustlib.a"),
&context.rust_build_dir.join(ARCHITECTURE_X86_64_SIMULATOR).join(variant_dir).join("libcrustlib.a"),
))
.working_dir(&context.working_dir),
)?;
// Now that we have a static library for the iPhone architecture and a static library representing both iOS simulator architectures (via the lipo tool),
// we can build the XCFramework which wraps it all up and can then be used as a framework dependency in the iOS project.
let xcframework_path = frameworks_dir.join("crust.xcframework");
let iphone_static_library_path =
context.rust_build_dir.join(ARCHITECTURE_ARM64_IPHONE).join(variant_dir).join("libcrustlib.a");
// Start off by deleting the existing framework if it exists - we should do this so building a debug/release variant always produces the appropriate XCFramework.
io::delete(&xcframework_path)?;
// Now run the appropriate Xcode command to create the framework.
logs::out(log_tag!(), "Creating XCFramework for 'crust' ...");
scripts::run(
&Script::new(&format!(
"xcodebuild -create-xcframework -library {:?} -library {:?} -output {:?}",
&iphone_static_library_path, &simulator_static_library_path, &xcframework_path,
))
.working_dir(&context.working_dir),
)?;
// The Frameworks directory now contains 'crust.xcframework' ready to be used in the iOS Xcode project.
Ok(())
}
Save ios.rs
, go back to Xcode and run the app again, triggering our new Rust build code. You should see a whole heap of crust-build
activity when the shell script step is run and it may take some time to finish while it is downloading and compiling the SDL2 libraries and our Rust code:
Compiling crust-build v1.0.0 (<snip>/crust-build)
Finished dev [unoptimized + debuginfo] target(s) in 6.45s
Running `target/debug/crust-build --target ios --variant Debug`
[ print_summary ] ---------------------------------------------
[ print_summary ] Assets dir: "<snip>/crust-main/assets"
[ print_summary ] Working dir: "<snip>/ios/.rust-build"
[ print_summary ] Rust build dir: "<snip>/ios/.rust-build/rust"
[ print_summary ] Variant: Debug
[ print_summary ] Target home dir: "<snip>/ios"
[ print_summary ] Main source dir: "<snip>/crust-main"
[ print_summary ] ---------------------------------------------
info: component 'rust-std' for target 'aarch64-apple-ios' is up to date
info: component 'rust-std' for target 'aarch64-apple-ios-sim' is up to date
info: component 'rust-std' for target 'x86_64-apple-ios' is up to date
[ delete ] Deleting "<snip>/ios/crust/Frameworks"
[ create_symlink ] Creating symlink: "<snip>/ios/.rust-build/Frameworks" <=> "<snip>/ios/crust/Frameworks"
[ download ] Download: "https://www.libsdl.org/release/SDL2-2.0.14.zip"
[ download ] Into: "/var/folders/ln/d7j2d7pn5jb8z3vdvvmttp_r0000gn/T/crustfThdBT/download.zip"
[ download ] Download complete: "/var/folders/ln/d7j2d7pn5jb8z3vdvvmttp_r0000gn/T/crustfThdBT/download.zip"
[ unzip ] Unzipping: "/var/folders/ln/d7j2d7pn5jb8z3vdvvmttp_r0000gn/T/crustfThdBT/download.zip" => "/var/folders/ln/d7j2d7pn5jb8z3vdvvmttp_r0000gn/T/crustfThdBT/unzipped"
[ rename ] Renaming: "/var/folders/ln/d7j2d7pn5jb8z3vdvvmttp_r0000gn/T/crustfThdBT/unzipped/SDL2-2.0.14" => "/var/folders/ln/d7j2d7pn5jb8z3vdvvmttp_r0000gn/T/crustfThdBT/unzipped/SDL"
[ setup_sdl2 ] Compiling SDL2 simulator release static library ...
...
When the build is complete (you will still get a build error which we’ll fix soon), you can observe the following new files and directories:
+ root
+ ios
+ .rust-build
+ crust
+ Frameworks (symlink)
+ crust.xcframework
+ SDL2.xcframework
+ SDL2_image.xcframework
If you were to look inside one of the frameworks you will see that is a static library for two platforms:
ios-arm64
: Physical iPhone devicesios-arm64_x86_64_simulator
: The combined architectures for iOS simulators, which we had merged together via the lipo
toolWe can now focus on the Xcode project again, first by adding our 3D models, textures and shader files. Right click on the crust
folder in Xcode and add files to the project:
Select crust-main/assets
and leave the options as shown below:
Your project should then look like this:
Similar to the MacOS Desktop project, these assets will be bundled into our resulting iOS app during the build.
Next up we will add all the required frameworks - there are actually quite a few to add but most of them are Apple ones.
Navigate to the Frameworks, Libraries and Embedded Content
section in the Build
tab of the crust
target:
One by one, add the following frameworks, leaving them as Do Not Embed
:
AudioToolbox.framework
AVFoundation.framework
CoreAudio.framework
CoreGraphics.framework
CoreHaptics.framework
CoreMotion.framework
CoreServices.framework
Foundation.framework
GameController.framework
ImageIO.framework
Metal.framework
OpenGLES.framework
QuartzCore.framework
Security.framework
UIKit.framework
Next we will add our SDL and Rust frameworks that we compiled ourselves. Add a new framework and select the Add Other
-> Add Files
option. Select the three xcframework
directories in our Frameworks
symlink location - leave them as Embed & Sign
:
The frameworks section should look like this when you are done:
Now go to the Build Settings
tab and add an entry to the Framework Search Paths
field of $(SRCROOT)/Frameworks
- this is very similar to what we did in the MacOS Desktop target and allows our custom frameworks to be found at build time.
One of the main differences between the iOS and MacOS targets is that on iOS we don’t actually compile a binary version of crust
- instead we made the crust.xcframework
which is really more of a library. In order to launch our crust
application we actually need for this method to be run which we introduced during the Android article:
#[cfg(any(target_os = "android", target_os = "ios"))]
#[no_mangle]
pub extern "C" fn SDL_main(_argc: libc::c_int, _argv: *const *const libc::c_char) -> libc::c_int {
main();
return 0;
}
The SDL_main
method is a bit magical - the SDL framework itself will automatically run this method instead of a main
method, so long as include the file SDL.h
in our code. Edit main.m
and replace it with:
// Including the SDL header will cause the default main function
// to be redirected to the SDL main method instead. This means
// we don't even have to bootstrap our application - it will
// happen automatically.
#include "SDL.h"
It seems a bit crazy but that is literally all we need to do for our crust
code to be invoked via the SDL_main
we included.
Now run the app again and bask in the brilliance of crust
on iOS!
Woohoo! That was the last of our target platforms integrated with crust
!
The code for this article can be found here.
End of part 12