app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 6c050db7688d49d378a4954b92d961d55ab1b750
parent 7ff92e96faf93269a294fe0963318e2a43223c5b
Author: triesap <tyson@radroots.org>
Date:   Fri, 20 Mar 2026 22:26:41 +0000

build: add initial ios launcher shell

- add the `radroots-app-ios` launcher crate and rust entrypoint
- add the `platforms/ios` host project, bridge headers, launch screen, and temporary icon resources
- add ios target checks, host build automation, and simulator launch scripts
- wire the ios shell to the shared rust app with the current unsupported onboarding state

Diffstat:
M.gitignore | 2++
MCargo.lock | 9+++++++++
MCargo.toml | 1+
Acrates/ios/Cargo.toml | 25+++++++++++++++++++++++++
Acrates/ios/src/lib.rs | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplatforms/ios/App/Bridge/RadRootsIOS-Bridging-Header.h | 1+
Aplatforms/ios/App/Bridge/RadRootsIOSBridge.h | 3+++
Aplatforms/ios/App/Resources/Info.plist | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplatforms/ios/App/Resources/LaunchScreen.storyboard | 37+++++++++++++++++++++++++++++++++++++
Aplatforms/ios/App/Resources/RadRootsIcon-20@2x.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-20@2x~ipad.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-20@3x.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-29@2x.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-29@2x~ipad.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-29@3x.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-40@2x.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-40@2x~ipad.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-40@3x.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-60@2x.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-60@3x.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-76@2x~ipad.png | 0
Aplatforms/ios/App/Resources/RadRootsIcon-83.5@2x~ipad.png | 0
Aplatforms/ios/App/main.swift | 3+++
Aplatforms/ios/Config/Base.xcconfig | 16++++++++++++++++
Aplatforms/ios/Config/Debug.xcconfig | 3+++
Aplatforms/ios/Config/Release.xcconfig | 3+++
Aplatforms/ios/Scripts/build_rust_ios.sh | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplatforms/ios/project.yml | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrust-toolchain.toml | 7++++++-
Ascripts/build-ios-host.sh | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/check-ios-target.sh | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/run-ios-simulator.sh | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
32 files changed, 611 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore @@ -2,6 +2,8 @@ /.trunk/ /crates/web/.trunk/ /crates/web/dist/ +/platforms/ios/.derived-data/ +/platforms/ios/*.xcodeproj/ # Local environment files .env diff --git a/Cargo.lock b/Cargo.lock @@ -2757,6 +2757,15 @@ dependencies = [ ] [[package]] +name = "radroots-app-ios" +version = "0.1.0" +dependencies = [ + "eframe", + "radroots-app-core", + "wgpu", +] + +[[package]] name = "radroots-app-web" version = "0.1.0" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/core", "crates/desktop", + "crates/ios", "crates/web", ] resolver = "2" diff --git a/crates/ios/Cargo.toml b/crates/ios/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "radroots-app-ios" +authors.workspace = true +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Rad Roots iOS launcher" +publish = false + +[lib] +path = "src/lib.rs" +crate-type = ["staticlib", "rlib"] + +[dependencies] +eframe.workspace = true +radroots-app-core = { path = "../core" } + +[target.'cfg(target_os = "ios")'.dependencies] +wgpu = { workspace = true, features = ["metal", "wgsl"] } + +[lints.rust] +unsafe_code = { level = "allow", priority = 1 } diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs @@ -0,0 +1,89 @@ +#![allow(unsafe_code)] + +#[cfg(target_os = "ios")] +use eframe::egui::ViewportBuilder; +#[cfg(target_os = "ios")] +use radroots_app_core::{ + APP_NAME, IdentityGateState, RadrootsApp, RadrootsAppBackend, SetupActionState, +}; + +#[cfg(target_os = "ios")] +struct IosBackend; + +#[cfg(target_os = "ios")] +impl RadrootsAppBackend for IosBackend { + fn load_identity_state(&self) -> Result<IdentityGateState, String> { + Ok(IdentityGateState::Unsupported { + reason: "Secure onboarding is not yet available on iOS.".to_owned(), + }) + } + + fn setup_action_state(&self) -> SetupActionState { + SetupActionState { + label: "Not Yet Available".to_owned(), + enabled: false, + pending: false, + } + } + + fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { + Ok(Some(IdentityGateState::Unsupported { + reason: "Secure onboarding is not yet available on iOS.".to_owned(), + })) + } +} + +#[cfg(target_os = "ios")] +fn native_options() -> eframe::NativeOptions { + eframe::NativeOptions { + renderer: eframe::Renderer::Wgpu, + viewport: ViewportBuilder::default() + .with_title(APP_NAME) + .with_fullscreen(true), + ..Default::default() + } +} + +#[cfg(target_os = "ios")] +pub fn run() -> Result<(), String> { + eframe::run_native( + APP_NAME, + native_options(), + Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(IosBackend))))), + ) + .map_err(|err| err.to_string()) +} + +#[cfg(not(target_os = "ios"))] +pub fn run() -> Result<(), String> { + Err("radroots-app-ios can only launch on an ios target".to_owned()) +} + +pub const ENTRYPOINT_SYMBOL: &str = "radroots_ios_run"; + +#[unsafe(no_mangle)] +pub extern "C" fn radroots_ios_run() -> i32 { + match run() { + Ok(()) => 0, + Err(_) => 1, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn non_ios_run_is_rejected() { + #[cfg(not(target_os = "ios"))] + assert_eq!( + run(), + Err("radroots-app-ios can only launch on an ios target".to_owned()) + ); + } + + #[test] + fn exported_entrypoint_symbol_is_stable() { + assert_eq!(ENTRYPOINT_SYMBOL, "radroots_ios_run"); + } +} diff --git a/platforms/ios/App/Bridge/RadRootsIOS-Bridging-Header.h b/platforms/ios/App/Bridge/RadRootsIOS-Bridging-Header.h @@ -0,0 +1 @@ +#include "RadRootsIOSBridge.h" diff --git a/platforms/ios/App/Bridge/RadRootsIOSBridge.h b/platforms/ios/App/Bridge/RadRootsIOSBridge.h @@ -0,0 +1,3 @@ +#include <stdint.h> + +int32_t radroots_ios_run(void); diff --git a/platforms/ios/App/Resources/Info.plist b/platforms/ios/App/Resources/Info.plist @@ -0,0 +1,74 @@ +<?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>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleDisplayName</key> + <string>Rad Roots</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleIcons</key> + <dict> + <key>CFBundlePrimaryIcon</key> + <dict> + <key>CFBundleIconFiles</key> + <array> + <string>RadRootsIcon-20@2x</string> + <string>RadRootsIcon-20@3x</string> + <string>RadRootsIcon-29@2x</string> + <string>RadRootsIcon-29@3x</string> + <string>RadRootsIcon-40@2x</string> + <string>RadRootsIcon-40@3x</string> + <string>RadRootsIcon-60@2x</string> + <string>RadRootsIcon-60@3x</string> + </array> + </dict> + </dict> + <key>CFBundleIcons~ipad</key> + <dict> + <key>CFBundlePrimaryIcon</key> + <dict> + <key>CFBundleIconFiles</key> + <array> + <string>RadRootsIcon-20@2x~ipad</string> + <string>RadRootsIcon-29@2x~ipad</string> + <string>RadRootsIcon-40@2x~ipad</string> + <string>RadRootsIcon-76@2x~ipad</string> + <string>RadRootsIcon-83.5@2x~ipad</string> + </array> + </dict> + </dict> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>Rad Roots</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>$(MARKETING_VERSION)</string> + <key>CFBundleVersion</key> + <string>$(CURRENT_PROJECT_VERSION)</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>UIApplicationSupportsIndirectInputEvents</key> + <true/> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>UISupportedInterfaceOrientations~ipad</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> +</dict> +</plist> diff --git a/platforms/ios/App/Resources/LaunchScreen.storyboard b/platforms/ios/App/Resources/LaunchScreen.storyboard @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM"> + <device id="retina6_12" orientation="portrait" appearance="light"/> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <scene sceneID="EHf-IW-A2E"> + <objects> + <viewController id="01J-lp-oVM" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> + <rect key="frame" x="0.0" y="0.0" width="393" height="852"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Rad Roots" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8c2-Jc-d8M"> + <rect key="frame" x="125.5" y="403" width="142" height="46"/> + <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="28"/> + <color key="textColor" red="0.96078431369999995" green="0.96862745100000002" blue="0.98039215690000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/> + <color key="backgroundColor" red="0.062745098039215685" green="0.074509803921568626" blue="0.10196078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="8c2-Jc-d8M" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="1Vb-H9-Hoy"/> + <constraint firstItem="8c2-Jc-d8M" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="rOw-uE-8Jh"/> + </constraints> + </view> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="80" y="84"/> + </scene> + </scenes> +</document> diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@2x.png b/platforms/ios/App/Resources/RadRootsIcon-20@2x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-20@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@3x.png b/platforms/ios/App/Resources/RadRootsIcon-20@3x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@2x.png b/platforms/ios/App/Resources/RadRootsIcon-29@2x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-29@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@3x.png b/platforms/ios/App/Resources/RadRootsIcon-29@3x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@2x.png b/platforms/ios/App/Resources/RadRootsIcon-40@2x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-40@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@3x.png b/platforms/ios/App/Resources/RadRootsIcon-40@3x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-60@2x.png b/platforms/ios/App/Resources/RadRootsIcon-60@2x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-60@3x.png b/platforms/ios/App/Resources/RadRootsIcon-60@3x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-76@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-76@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-83.5@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-83.5@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/main.swift b/platforms/ios/App/main.swift @@ -0,0 +1,3 @@ +import Foundation + +_ = radroots_ios_run() diff --git a/platforms/ios/Config/Base.xcconfig b/platforms/ios/Config/Base.xcconfig @@ -0,0 +1,16 @@ +PRODUCT_NAME = $(TARGET_NAME) +PRODUCT_MODULE_NAME = RadRootsIOS +GENERATE_INFOPLIST_FILE = NO +IPHONEOS_DEPLOYMENT_TARGET = 17.0 +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/App/Bridge/RadRootsIOS-Bridging-Header.h +LIBRARY_SEARCH_PATHS = $(inherited) +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks +OTHER_LDFLAGS = $(inherited) "$(RUST_LIBRARY_PATH)" -framework Foundation -framework UIKit -framework CoreFoundation -framework Metal -lobjc -liconv +RUST_TARGET_TRIPLE = aarch64-apple-ios-sim +RUST_TARGET_TRIPLE[sdk=iphoneos*] = aarch64-apple-ios +RUST_TARGET_TRIPLE[sdk=iphonesimulator*] = aarch64-apple-ios-sim +RUST_CARGO_PROFILE = debug +RUST_LIBRARY_PATH = $(SRCROOT)/../../target/aarch64-apple-ios-sim/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a +RUST_LIBRARY_PATH[sdk=iphoneos*] = $(SRCROOT)/../../target/aarch64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a +RUST_LIBRARY_PATH[sdk=iphonesimulator*][arch=x86_64] = $(SRCROOT)/../../target/x86_64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a +SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO diff --git a/platforms/ios/Config/Debug.xcconfig b/platforms/ios/Config/Debug.xcconfig @@ -0,0 +1,3 @@ +#include "Base.xcconfig" + +RUST_CARGO_PROFILE = debug diff --git a/platforms/ios/Config/Release.xcconfig b/platforms/ios/Config/Release.xcconfig @@ -0,0 +1,3 @@ +#include "Base.xcconfig" + +RUST_CARGO_PROFILE = release diff --git a/platforms/ios/Scripts/build_rust_ios.sh b/platforms/ios/Scripts/build_rust_ios.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +ios_root="$(cd "${script_dir}/.." && pwd -P)" +app_root="$(cd "${ios_root}/../.." && pwd -P)" +ios_target_dir="${app_root}/target" + +require_command() { + if command -v "$1" >/dev/null 2>&1; then + return + fi + echo "missing required command: $1" >&2 + exit 1 +} + +require_rust_target() { + local target="$1" + if rustup target list --installed | grep -Fx "${target}" >/dev/null 2>&1; then + return + fi + echo "missing required rust target: ${target}" >&2 + exit 1 +} + +profile_for_configuration() { + case "${1}" in + Release) + echo "release" + ;; + *) + echo "debug" + ;; + esac +} + +build_target() { + local target="$1" + local profile="$2" + local cargo_args=( + build + --manifest-path "${app_root}/Cargo.toml" + -p radroots-app-ios + --target "${target}" + ) + if [[ "${profile}" == "release" ]]; then + cargo_args+=(--release) + fi + CARGO_TARGET_DIR="${ios_target_dir}" cargo "${cargo_args[@]}" +} + +build_targets() { + local profile="$1" + shift + for target in "$@"; do + require_rust_target "${target}" + build_target "${target}" "${profile}" + done +} + +require_command cargo +require_command rustup + +configuration="${CONFIGURATION:-Debug}" +profile="$(profile_for_configuration "${configuration}")" +sdk_name="${SDK_NAME:-}" +archs="${ARCHS:-}" + +if [[ -n "${sdk_name}" ]]; then + case "${sdk_name}" in + iphoneos*) + build_targets "${profile}" aarch64-apple-ios + ;; + iphonesimulator*) + if [[ " ${archs} " == *" x86_64 "* ]]; then + build_targets "${profile}" aarch64-apple-ios-sim x86_64-apple-ios + else + build_targets "${profile}" aarch64-apple-ios-sim + fi + ;; + *) + echo "unsupported iOS SDK_NAME: ${sdk_name}" >&2 + exit 1 + ;; + esac + exit 0 +fi + +build_targets "${profile}" aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios diff --git a/platforms/ios/project.yml b/platforms/ios/project.yml @@ -0,0 +1,68 @@ +name: RadRootsIOS + +options: + createIntermediateGroups: true + deploymentTarget: + iOS: "17.0" + +settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: org.radroots.app.ios + MARKETING_VERSION: 0.1.0 + CURRENT_PROJECT_VERSION: 1 + SWIFT_VERSION: 6.0 + CODE_SIGN_STYLE: Automatic + +configs: + Debug: debug + Release: release + +targetTemplates: + app_base: + type: application + platform: iOS + deploymentTarget: "17.0" + supportedDestinations: + - iOS + - iOS Simulator + configFiles: + Debug: Config/Debug.xcconfig + Release: Config/Release.xcconfig + sources: + - path: App/main.swift + - path: App/Bridge + buildPhase: headers + - path: App/Resources + excludes: + - Info.plist + settings: + base: + PRODUCT_NAME: $(TARGET_NAME) + INFOPLIST_FILE: App/Resources/Info.plist + TARGETED_DEVICE_FAMILY: 1,2 + preBuildScripts: + - name: Build Rust iOS Library + script: | + "$SRCROOT/Scripts/build_rust_ios.sh" + basedOnDependencyAnalysis: false + inputFiles: + - $(SRCROOT)/../../Cargo.toml + - $(SRCROOT)/../../Cargo.lock + - $(SRCROOT)/../../crates/core/Cargo.toml + - $(SRCROOT)/../../crates/core/src/lib.rs + - $(SRCROOT)/../../crates/ios/Cargo.toml + - $(SRCROOT)/../../crates/ios/src/lib.rs + - $(SRCROOT)/Scripts/build_rust_ios.sh + outputFiles: + - $(SRCROOT)/../../target/$(RUST_TARGET_TRIPLE)/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a + - $(SRCROOT)/../../target/x86_64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a + dependencies: + - sdk: UIKit.framework + - sdk: Foundation.framework + - sdk: CoreFoundation.framework + - sdk: Metal.framework + +targets: + RadRootsIOS: + templates: + - app_base diff --git a/rust-toolchain.toml b/rust-toolchain.toml @@ -1,3 +1,8 @@ [toolchain] channel = "1.92.0" -targets = ["wasm32-unknown-unknown"] +targets = [ + "aarch64-apple-ios", + "aarch64-apple-ios-sim", + "wasm32-unknown-unknown", + "x86_64-apple-ios", +] diff --git a/scripts/build-ios-host.sh b/scripts/build-ios-host.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +app_root="$(cd "${script_dir}/.." && pwd -P)" +ios_root="${app_root}/platforms/ios" +project_name="RadRootsIOS" +configuration="${CONFIGURATION:-Debug}" +derived_data_dir="${ios_root}/.derived-data" +expected_app="${derived_data_dir}/Build/Products/${configuration}-iphonesimulator/${project_name}.app" + +require_command() { + if command -v "$1" >/dev/null 2>&1; then + return + fi + echo "missing required command: $1" >&2 + exit 1 +} + +ios_sim_host_arch() { + case "$(uname -m)" in + arm64|aarch64) + echo "arm64" + ;; + x86_64) + echo "x86_64" + ;; + *) + echo "unsupported host architecture for ios simulator: $(uname -m)" >&2 + exit 1 + ;; + esac +} + +require_command xcodegen +require_command xcodebuild + +"${script_dir}/check-ios-target.sh" + +host_arch="$(ios_sim_host_arch)" + +( + cd "${ios_root}" + xcodegen generate + xcodebuild \ + -project "${project_name}.xcodeproj" \ + -scheme "${project_name}" \ + -configuration "${configuration}" \ + -sdk iphonesimulator \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "${derived_data_dir}" \ + ARCHS="${host_arch}" \ + CODE_SIGNING_ALLOWED=NO \ + ONLY_ACTIVE_ARCH=YES \ + build +) + +if [[ ! -d "${expected_app}" ]]; then + echo "missing expected ios app bundle: ${expected_app}" >&2 + exit 1 +fi + +printf '%s\n' "${expected_app}" diff --git a/scripts/check-ios-target.sh b/scripts/check-ios-target.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +app_root="$(cd "${script_dir}/.." && pwd -P)" +ios_target_dir="${app_root}/target" + +require_command() { + if command -v "$1" >/dev/null 2>&1; then + return + fi + echo "missing required command: $1" >&2 + exit 1 +} + +ios_sim_rust_targets_for_host() { + case "$(uname -m)" in + arm64|aarch64) + printf '%s\n' "aarch64-apple-ios-sim" + ;; + x86_64) + printf '%s\n' "aarch64-apple-ios-sim" "x86_64-apple-ios" + ;; + *) + echo "unsupported host architecture for ios simulator: $(uname -m)" >&2 + exit 1 + ;; + esac +} + +require_rust_target() { + local target="$1" + if rustup target list --installed | grep -Fx "${target}" >/dev/null 2>&1; then + return + fi + echo "missing required rust target: ${target}" >&2 + exit 1 +} + +require_command cargo +require_command rustup + +cd "${app_root}" + +declare -a targets=() +if [[ -n "${IOS_SIM_RUST_TARGET:-}" ]]; then + targets=("${IOS_SIM_RUST_TARGET}") +else + while IFS= read -r target; do + targets+=("${target}") + done < <(ios_sim_rust_targets_for_host) +fi + +for target in "${targets[@]}"; do + require_rust_target "${target}" + CARGO_TARGET_DIR="${ios_target_dir}" \ + cargo check --manifest-path "${app_root}/Cargo.toml" -p radroots-app-ios --target "${target}" +done diff --git a/scripts/run-ios-simulator.sh b/scripts/run-ios-simulator.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +app_root="$(cd "${script_dir}/.." && pwd -P)" +bundle_id="org.radroots.app.ios" +device_selector="${1:-${IOS_SIMULATOR_DEVICE:-iPhone 16}}" + +require_command() { + if command -v "$1" >/dev/null 2>&1; then + return + fi + echo "missing required command: $1" >&2 + exit 1 +} + +resolve_simulator_udid() { + local selector="$1" + if [[ "${selector}" =~ ^[0-9A-F-]{36}$ ]]; then + printf '%s\n' "${selector}" + return + fi + + local line + line="$( + xcrun simctl list devices available | + awk -v name="${selector}" '$0 ~ ("^[[:space:]]+" name " \\(") { print; exit }' + )" + + if [[ -z "${line}" ]]; then + echo "unable to find available iOS simulator: ${selector}" >&2 + exit 1 + fi + + printf '%s\n' "${line}" | awk -F '[()]' '{ print $2 }' +} + +require_command open +require_command xcrun +require_command mktemp + +build_log="$(mktemp)" +trap 'rm -f "${build_log}"' EXIT + +if ! "${script_dir}/build-ios-host.sh" | tee "${build_log}"; then + exit 1 +fi + +app_path="$(tail -n 1 "${build_log}")" +if [[ ! -d "${app_path}" ]]; then + echo "missing built iOS app bundle: ${app_path}" >&2 + exit 1 +fi + +device_udid="$(resolve_simulator_udid "${device_selector}")" + +xcrun simctl boot "${device_udid}" >/dev/null 2>&1 || true +xcrun simctl bootstatus "${device_udid}" -b +open -a Simulator --args -CurrentDeviceUDID "${device_udid}" >/dev/null 2>&1 || open -a Simulator >/dev/null 2>&1 +xcrun simctl install "${device_udid}" "${app_path}" +xcrun simctl launch "${device_udid}" "${bundle_id}"