#![allow(dead_code)]
pub struct Bundle {
pub example: &'static str,
pub bundle_id: &'static str,
pub executable: &'static str,
pub display_name: &'static str,
pub launch_args: &'static [&'static str],
}
pub enum Mode {
Simulator,
Device,
Ipa,
}
pub fn mode() -> Mode {
if std::env::args().any(|a| a == "device") {
Mode::Device
} else if std::env::args().any(|a| a == "ipa") {
Mode::Ipa
} else {
Mode::Simulator
}
}
fn build_for(b: &Bundle, triple: &str) -> std::path::PathBuf {
use std::process::Command;
eprintln!("building for {triple}...");
let ok = Command::new("cargo")
.args(["build", "-p", "swiftui-native", "--example", b.example, "--target", triple])
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(ok, "cargo build for {triple} failed");
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../target")
.join(triple)
.join(format!("debug/examples/{}", b.example))
}
fn write_bundle(b: &Bundle, dir: &std::path::Path, binary: &std::path::Path, for_device: bool) {
let _ = std::fs::remove_dir_all(dir);
std::fs::create_dir_all(dir).expect("bundle directory");
std::fs::copy(binary, dir.join(b.executable)).expect("copying the executable");
let platform = if for_device { "iPhoneOS" } else { "iPhoneSimulator" };
std::fs::write(dir.join("Info.plist"), info_plist(b, platform)).expect("writing Info.plist");
}
fn info_plist(b: &Bundle, platform: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>{exe}</string>
<key>CFBundleIdentifier</key><string>{id}</string>
<key>CFBundleName</key><string>{name}</string>
<key>CFBundleDisplayName</key><string>{name}</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>MinimumOSVersion</key><string>17.0</string>
<key>UIDeviceFamily</key><array><integer>1</integer><integer>2</integer></array>
<key>CFBundleSupportedPlatforms</key><array><string>{platform}</string></array>
<key>UILaunchScreen</key><dict/>
<key>UIRequiredDeviceCapabilities</key><array><string>arm64</string></array>
</dict></plist>
"#,
exe = b.executable,
id = b.bundle_id,
name = b.display_name,
)
}
pub fn run_on_simulator(b: &Bundle, target_dir: &std::path::Path) {
use std::process::Command;
let binary = build_for(b, "aarch64-apple-ios-sim");
let bundle = target_dir.join(format!("ios-app/{}.app", b.executable));
write_bundle(b, &bundle, &binary, false);
let device = booted_device().unwrap_or_else(|| {
let name = first_available_iphone().expect("no iPhone simulator is available");
eprintln!("booting {name}...");
let _ = Command::new("xcrun").args(["simctl", "boot", &name]).status();
std::thread::sleep(std::time::Duration::from_secs(8));
booted_device().expect("simulator failed to boot")
});
let _ = Command::new("open").args(["-a", "Simulator"]).status();
eprintln!("simulator: {device}");
let simctl = |args: &[&str]| {
Command::new("xcrun").arg("simctl").args(args).status().map(|s| s.success())
};
let _ = simctl(&["terminate", &device, b.bundle_id]);
let _ = simctl(&["uninstall", &device, b.bundle_id]);
assert!(
simctl(&["install", &device, &bundle.to_string_lossy()]).unwrap_or(false),
"installing the bundle"
);
let mut launch = vec!["launch", &device, b.bundle_id];
launch.extend_from_slice(b.launch_args);
assert!(simctl(&launch).unwrap_or(false), "launching the application");
std::thread::sleep(std::time::Duration::from_secs(6));
let shot = target_dir.join(format!("ios-app/{}.png", b.executable));
let ok = simctl(&["io", &device, "screenshot", &shot.to_string_lossy()]).unwrap_or(false);
eprintln!("screenshot: {} ({})", shot.display(), if ok { "written" } else { "failed" });
}
pub fn run_on_device(b: &Bundle, target_dir: &std::path::Path, install: bool) {
use std::process::Command;
let binary = build_for(b, "aarch64-apple-ios");
let stage = target_dir.join("ios-app/device/Payload");
let bundle = stage.join(format!("{}.app", b.executable));
write_bundle(b, &bundle, &binary, true);
if let Ok(profile) = std::env::var("RSWIFT_PROVISIONING_PROFILE") {
std::fs::copy(&profile, bundle.join("embedded.mobileprovision"))
.expect("copying the provisioning profile");
eprintln!("profile: {profile}");
}
match codesign_identity() {
Some(id) => {
eprintln!("signing as {id}");
let mut cmd = Command::new("codesign");
cmd.args(["--force", "--sign", &id]);
if let Ok(ents) = std::env::var("RSWIFT_ENTITLEMENTS") {
cmd.args(["--entitlements", &ents]);
}
let ok = cmd.arg(&bundle).status().map(|s| s.success()).unwrap_or(false);
assert!(ok, "codesign failed");
}
None => eprintln!(
"no code-signing identity found — the .ipa will be unsigned and a device will \
refuse it. Set RSWIFT_CODESIGN_IDENTITY to choose one."
),
}
let ipa = target_dir.join(format!("ios-app/{}.ipa", b.executable));
let _ = std::fs::remove_file(&ipa);
let ok = Command::new("zip")
.args(["-qr", &ipa.to_string_lossy(), "Payload"])
.current_dir(stage.parent().expect("device dir"))
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(ok, "packaging the .ipa");
eprintln!("ipa: {}", ipa.display());
if !install {
return;
}
match connected_device() {
Some(id) => {
eprintln!("installing on {id}");
let ok = Command::new("xcrun")
.args(["devicectl", "device", "install", "app", "--device", &id])
.arg(&bundle)
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
let _ = Command::new("xcrun")
.args(["devicectl", "device", "process", "launch", "--device", &id, b.bundle_id])
.status();
} else {
eprintln!("install failed — check the profile covers {}", b.bundle_id);
}
}
None => eprintln!(
"no device connected. Pair one and re-run, or install the .ipa with \
`xcrun devicectl device install app --device <id> {}`",
bundle.display()
),
}
}
fn codesign_identity() -> Option<String> {
if let Ok(id) = std::env::var("RSWIFT_CODESIGN_IDENTITY") {
return Some(id);
}
let out = std::process::Command::new("security")
.args(["find-identity", "-v", "-p", "codesigning"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
text.lines()
.find(|l| l.contains('"') && !l.contains("CSSMERR"))
.and_then(|l| {
let start = l.find('"')? + 1;
let end = l[start..].find('"')? + start;
Some(l[start..end].to_string())
})
}
fn connected_device() -> Option<String> {
let out = std::process::Command::new("xcrun")
.args(["devicectl", "list", "devices"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
text.lines()
.find(|l| l.contains("connected") || l.contains("available"))
.and_then(|l| l.split_whitespace().find(|w| w.len() == 36 && w.contains('-')))
.map(str::to_string)
}
fn first_available_iphone() -> Option<String> {
let out = std::process::Command::new("xcrun")
.args(["simctl", "list", "devices", "available"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
let line = text.lines().find(|l| l.trim_start().starts_with("iPhone"))?;
Some(line.trim().split(" (").next()?.to_string())
}
fn booted_device() -> Option<String> {
let out = std::process::Command::new("xcrun")
.args(["simctl", "list", "devices", "booted"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
let line = text.lines().find(|l| l.contains("(Booted)"))?;
let start = line.find('(')? + 1;
let end = line[start..].find(')')? + start;
Some(line[start..end].to_string())
}
pub fn main(b: &Bundle) {
let target_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target");
match mode() {
Mode::Simulator => run_on_simulator(b, &target_dir),
Mode::Device => run_on_device(b, &target_dir, true),
Mode::Ipa => run_on_device(b, &target_dir, false),
}
}