Created
September 30, 2026 19:46
-
-
Save dnicolson/d3542e7a69f9e8d1c4e1efbd3538dfe7 to your computer and use it in GitHub Desktop.
Build a minimal unsigned iOS IPA with CMake and Ninja that includes a raw arm64 Mach-O .so in Frameworks/, loads it at launch, and crashes if loading fails.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Build an unsigned arm64 iOS IPA containing an iOS shared library named .so. | |
| # Requirements: macOS, Xcode with the iOS SDK, CMake >= 3.24, and Ninja. | |
| set -euo pipefail | |
| script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" | |
| cd "$script_dir" | |
| for tool in cmake ninja xcrun zip; do | |
| command -v "$tool" >/dev/null 2>&1 || { | |
| echo "Missing required tool: $tool" >&2 | |
| exit 1 | |
| } | |
| done | |
| [ "$(uname -s)" = Darwin ] || { echo "Run this script on macOS with Xcode installed." >&2; exit 1; } | |
| source_dir="$script_dir/build/ios-source" | |
| build_dir="$script_dir/build/standalone-ios" | |
| mkdir -p "$source_dir/src" "$build_dir" | |
| # Keep the example project inside this script so it is the only source file needed. | |
| cat > "$source_dir/CMakeLists.txt" <<'CMAKE' | |
| cmake_minimum_required(VERSION 3.24) | |
| project(SoDemo LANGUAGES C OBJC) | |
| if(NOT CMAKE_SYSTEM_NAME STREQUAL "iOS") | |
| message(FATAL_ERROR "Configure this project for iOS.") | |
| endif() | |
| # Emit a raw Mach-O plugin, matching the .so entries in the reference IPA. | |
| add_library(demo_library MODULE src/demo_library.c) | |
| set_target_properties(demo_library PROPERTIES | |
| PREFIX "" | |
| OUTPUT_NAME "demo_library" | |
| SUFFIX ".so" | |
| ) | |
| add_executable(SoDemo MACOSX_BUNDLE src/main.m) | |
| set_target_properties(SoDemo PROPERTIES | |
| MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/src/Info.plist.in" | |
| MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.sodemo" | |
| ) | |
| target_compile_options(SoDemo PRIVATE -fobjc-arc) | |
| target_link_libraries(SoDemo PRIVATE "-framework UIKit" "-framework Foundation") | |
| # Produce an unsigned IPA for later signing/sideloading. | |
| target_link_options(demo_library PRIVATE "LINKER:-no_adhoc_codesign") | |
| target_link_options(SoDemo PRIVATE "LINKER:-no_adhoc_codesign") | |
| # Copy the unsigned plugin beside other app resources, as in the reference IPA. | |
| add_custom_target(embed_library ALL | |
| COMMAND ${CMAKE_COMMAND} -E make_directory | |
| "$<TARGET_BUNDLE_DIR:SoDemo>/Frameworks" | |
| COMMAND ${CMAKE_COMMAND} -E copy | |
| "$<TARGET_FILE:demo_library>" | |
| "$<TARGET_BUNDLE_DIR:SoDemo>/Frameworks/demo_library.so" | |
| DEPENDS SoDemo demo_library | |
| VERBATIM | |
| ) | |
| CMAKE | |
| cat > "$source_dir/src/demo_library.c" <<'C' | |
| const char *demo_message(void) | |
| { | |
| return "Hello from the .so library!"; | |
| } | |
| C | |
| cat > "$source_dir/src/Info.plist.in" <<'PLIST' | |
| <?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>SO Demo</string> | |
| <key>CFBundleExecutable</key><string>SoDemo</string> | |
| <key>CFBundleIdentifier</key><string>com.example.sodemo</string> | |
| <key>CFBundleInfoDictionaryVersion</key><string>6.0</string> | |
| <key>CFBundleName</key><string>SoDemo</string> | |
| <key>CFBundlePackageType</key><string>APPL</string> | |
| <key>CFBundleShortVersionString</key><string>1.0</string> | |
| <key>CFBundleVersion</key><string>1</string> | |
| <key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array> | |
| <key>MinimumOSVersion</key><string>15.0</string> | |
| <key>UIDeviceFamily</key><array><integer>1</integer><integer>2</integer></array> | |
| <key>LSRequiresIPhoneOS</key><true/> | |
| <key>UILaunchScreen</key><dict/> | |
| <key>UIApplicationSceneManifest</key> | |
| <dict> | |
| <key>UIApplicationSupportsMultipleScenes</key><false/> | |
| <key>UISceneConfigurations</key> | |
| <dict> | |
| <key>UIWindowSceneSessionRoleApplication</key> | |
| <array><dict> | |
| <key>UISceneConfigurationName</key><string>Default</string> | |
| <key>UISceneDelegateClassName</key><string>SceneDelegate</string> | |
| </dict></array> | |
| </dict> | |
| </dict> | |
| </dict> | |
| </plist> | |
| PLIST | |
| cat > "$source_dir/src/main.m" <<'OBJC' | |
| #import <UIKit/UIKit.h> | |
| #include <dlfcn.h> | |
| static NSString *loadMessage(void) | |
| { | |
| NSString *path = [NSBundle.mainBundle.privateFrameworksPath | |
| stringByAppendingPathComponent:@"demo_library.so"]; | |
| void *library = dlopen(path.fileSystemRepresentation, RTLD_NOW); | |
| if (!library) { | |
| NSLog(@"dlopen failed: %s", dlerror()); | |
| abort(); | |
| } | |
| dlerror(); | |
| const char *(*message)(void) = (const char *(*)(void))dlsym(library, "demo_message"); | |
| const char *error = dlerror(); | |
| if (error || !message) { | |
| NSLog(@"demo_message lookup failed: %s", error ? error : "symbol is null"); | |
| dlclose(library); | |
| abort(); | |
| } | |
| NSString *result = [NSString stringWithUTF8String:message()]; | |
| dlclose(library); | |
| return result; | |
| } | |
| @interface SceneDelegate : UIResponder <UIWindowSceneDelegate> | |
| @property(nonatomic, strong) UIWindow *window; | |
| @end | |
| @implementation SceneDelegate | |
| - (void)scene:(UIScene *)scene | |
| willConnectToSession:(UISceneSession *)session | |
| options:(UISceneConnectionOptions *)options | |
| { | |
| self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene]; | |
| UIViewController *controller = [[UIViewController alloc] init]; | |
| controller.view.backgroundColor = UIColor.systemBackgroundColor; | |
| UILabel *label = [[UILabel alloc] initWithFrame:controller.view.bounds]; | |
| label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | |
| label.textAlignment = NSTextAlignmentCenter; | |
| label.numberOfLines = 0; | |
| label.text = loadMessage(); | |
| [controller.view addSubview:label]; | |
| self.window.rootViewController = controller; | |
| [self.window makeKeyAndVisible]; | |
| } | |
| @end | |
| @interface AppDelegate : UIResponder <UIApplicationDelegate> | |
| @end | |
| @implementation AppDelegate | |
| @end | |
| int main(int argc, char *argv[]) | |
| { | |
| @autoreleasepool { | |
| return UIApplicationMain(argc, argv, nil, NSStringFromClass(AppDelegate.class)); | |
| } | |
| } | |
| OBJC | |
| cmake -S "$source_dir" -B "$build_dir" -G Ninja \ | |
| -DCMAKE_SYSTEM_NAME=iOS \ | |
| -DCMAKE_OSX_SYSROOT=iphoneos \ | |
| -DCMAKE_OSX_ARCHITECTURES=arm64 \ | |
| -DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \ | |
| -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \ | |
| -DCMAKE_BUILD_TYPE=Release | |
| cmake -E rm -rf "$build_dir/SoDemo.app/Frameworks" | |
| cmake --build "$build_dir" | |
| app="$build_dir/SoDemo.app" | |
| library="$app/Frameworks/demo_library.so" | |
| cp "$source_dir/src/Info.plist.in" "$app/Info.plist" | |
| [ -f "$app/SoDemo" ] && [ -f "$library" ] || { | |
| echo "Build did not produce the app and embedded .so library." >&2 | |
| exit 1 | |
| } | |
| package_root="$(mktemp -d "$build_dir/package.XXXXXX")" | |
| trap 'rm -rf -- "$package_root"' EXIT | |
| mkdir -p "$package_root/Payload" | |
| cp -R "$app" "$package_root/Payload/" | |
| (cd "$package_root" && zip -qry SoDemo-unsigned.ipa Payload) | |
| mv "$package_root/SoDemo-unsigned.ipa" "$build_dir/SoDemo-unsigned.ipa" | |
| # Keep just the deliverables after the IPA is safely written. The standalone | |
| # library stays beside it; app bundles, Ninja state, generated sources, and | |
| # outputs from the earlier framework build are disposable. | |
| find "$build_dir" -mindepth 1 -maxdepth 1 \ | |
| ! -name SoDemo-unsigned.ipa ! -name demo_library.so \ | |
| -exec /bin/rm -rf {} + | |
| find "$script_dir/build" -mindepth 1 -maxdepth 1 ! -name standalone-ios \ | |
| -exec /bin/rm -rf {} + | |
| /bin/rm -rf "$script_dir/dist" | |
| echo "Created unsigned IPA: $build_dir/SoDemo-unsigned.ipa" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment