I have a project with a slightly complex requirement, so let's start with the desired goal:
- The project is an SDK for interfacing with our back-end services (not an app)
- The project must output a
.framework
file that can be included in a Xamarin app (all of our internal apps are built with Xamarin) - The project must be a proper CocoaPod with a
.podspec
file so that third parties can include the SDK in their own apps (customer request) - The project depends on three third-party dependencies, one of which is delivered differently from the others:
- CocoaLumberjack and JSONModel, when listed as dependencies in the
Podfile
or.podspec
, will download their respective projects and include them in your own workspace as sub-projects, to be built - GoogleInterativeMediaAds, when listed as a dependency in the
Podfile
or.podspec
, will download a.framework
file and is completely closed-source
- CocoaLumberjack and JSONModel, when listed as dependencies in the
- The project must be able to build for both iOS and tvOS (we have apps and clients for both)
- Note that while JSONModel and CocoaLumberjack both advertise that they have iOS and tvOS support, there are separate CocoaPods for the GoogleInteractiveMediaAds (
GoogleAds-IMA-iOS-SDK ~> 3.9
andGoogleAds-IMA-tvOS-SDK ~> 4.0
)
- Note that while JSONModel and CocoaLumberjack both advertise that they have iOS and tvOS support, there are separate CocoaPods for the GoogleInteractiveMediaAds (
- Any
.framework
file output by the project must contain both embedded bitcode and be a fat binary - The project has several static assets (PNG files and XIB files) that need to be included as a
.bundle
within the.framework
Now, then: I have all of this working. It took a long time and a lot of experimenting but I have a project that meets all of the above requirements. My Podfile looks like so:
Podfile
use_frameworks!
def common()
pod 'CocoaLumberjack', '~> 3.5'
pod 'JSONModel', '~> 1.8'
end
target 'MyFramework-iOS' do
platform :ios, '9.0'
common()
pod 'GoogleAds-IMA-iOS-SDK', '~> 3.9'
end
target 'MyFramework-tvOS' do
platform :tvos, '9.1'
common()
pod 'GoogleAds-IMA-tvOS-SDK', '~> 4.0'
end
And my .podspec
looks like so:
MyFramework.podspec
Pod::Spec.new do |spec|
spec.name = "MyFramework"
spec.version = "1.0.0"
spec.summary = "A framework for interfacing with our back-end"
spec.description = <<-DESC
A longer description
DESC
spec.homepage = "https://example.com/"
spec.license = "MIT"
spec.authors = {
"Steven Barnett" => "xxx@example.com"
}
spec.ios.deployment_target = "9.0"
spec.tvos.deployment_target = "9.1"
spec.source = { :git => "https://bitbucket.org/xxx/MyFramework.git", :tag => "#{spec.version}" }
spec.source_files = "Sources/Common/**/*.{h,m}", "Sources/MyFramework.h"
spec.ios.source_files = "Sources/iOS/**/*.{h,m}"
spec.tvos.source_files = "Sources/tvOS/**/*.{h,m}"
spec.ios.resource_bundles = {
"MyFramework" => ["Assets.xcassets", "Sources/iOS/**/*.{xib}"]
}
spec.tvos.resource_bundles = {
"MyFramework" => ["Assets.xcassets", "Sources/tvOS/**/*.{xib}"]
}
spec.frameworks = "Foundation", "UIKit", "AVKit"
spec.requires_arc = true
spec.dependency "CocoaLumberjack", "~> 3.5"
spec.dependency "JSONModel", "~> 1.8"
spec.ios.dependency "GoogleAds-IMA-iOS-SDK", "~> 3.9"
spec.tvos.dependency "GoogleAds-IMA-tvOS-SDK", "~> 4.0"
end
My source code is divided into three folders:
./Sources/Common
contains 99% of our code and is common among iOS and tvOS./Sources/iOS
contains a handful of features that are only available on iOS. Specifically, iOS grants more flexibility in customizing the layout of a video player, so we have custom controls./Sources/tvOS
contains a handful of features that are only available on tvOS. Specifically, a paired-down collections of XIBs and.m
files for the player controls
Within my project's Build Phases, I added a Run Script
phase with the following script:
Run Script
UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal
if [ "true" == ${ALREADYINVOKED:-false} ]; then
echo "RECURSION: Detected, stopping"
else
export ALREADYINVOKED="true"
# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"
# Step "0". Build our project twice
echo "Building for AppleTV Device (redundantly... sigh)"
xcodebuild -workspace "${PROJECT_DIR}/${PROJECT}.xcworkspace" -scheme "${TARGET_NAME}" -configuration ${CONFIGURATION} -sdk appletvos ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" OBJROOT="${OBJROOT}/DependentBuilds" OTHER_CFLAGS="-fembed-bitcode"
echo "Building for AppleTV Simulator"
xcodebuild -workspace "${PROJECT_DIR}/${PROJECT}.xcworkspace" -scheme "${TARGET_NAME}" -configuration ${CONFIGURATION} -sdk appletvsimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" OBJROOT="${OBJROOT}/DependentBuilds" OTHER_CFLAGS="-fembed-bitcode"
# Step 1. Copy the framework structure (from iphoneos build) to the universal folder
echo "Copying to output folder"
cp -R "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" "${UNIVERSAL_OUTPUTFOLDER}/"
# Step 2. Create universal binary file using lipo and place the combined executable in the copied framework directory
echo "Combining executables"
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}" "${BUILD_DIR}/${CONFIGURATION}-appletvsimulator/${EXECUTABLE_PATH}" "${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}"
# Step 3. Create universal binaries for embedded frameworks
for FRAMEWORK in $( find "${TARGET_BUILD_DIR}" -type d -name "*.framework" ); do
FRAMEWORK_PATH="${FRAMEWORK##$TARGET_BUILD_DIR}"
BINARY_NAME="${FRAMEWORK##*/}"
BINARY_NAME="${BINARY_NAME%.*}"
# I don't know how to do "if not" in Bash. Sue me.
if [[ $BINARY_NAME == Pods_* ]] || [[ $BINARY_NAME == $TARGET_NAME ]]; then
echo "Skipping framework: $FRAMEWORK_PATH"
else
echo "Creating fat binary for framework: $FRAMEWORK_PATH"
cp -r "${FRAMEWORK}" "${UNIVERSAL_OUTPUTFOLDER}/"
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${BINARY_NAME}.framework/${BINARY_NAME}" "${BUILD_DIR}/${CONFIGURATION}-appletvsimulator/${FRAMEWORK_PATH}/${BINARY_NAME}" "${TARGET_BUILD_DIR}/${FRAMEWORK_PATH}/${BINARY_NAME}"
fi
done
# Step 4. Manually copy some files to the output dir
cp -r "${SOURCE_ROOT}/Pods/GoogleAds-IMA-tvOS-SDK/GoogleInteractiveMediaAds.framework" "${UNIVERSAL_OUTPUTFOLDER}"
cp "${SOURCE_ROOT}/generate-xamarin-bindings.py" "${UNIVERSAL_OUTPUTFOLDER}"
# Step 5. Convenience step
open "${UNIVERSAL_OUTPUTFOLDER}"
fi
This script is a modification of a similar script I found online. It basically does the following:
- Build my project for a device (ARM processor) with bitcode embedded
- Build my project for the simulator (x86 processor) with bitcode embedded
- Use lipo to create a fat binary for my project
- Iteratively use lipo to create fat binaries for all frameworks I depend on
- Copy my framework and all frameworks I depend on to a "universal" output directory
You may also notice the generate-xamarin-bindings.py
file. This can be pretty much ignored for my purposes. It just uses Objective Sharpie to automate the creation of Xamarin bindings, then fixes a handful of known bugs with JSONModel (variables defined as @property (retain, nonatomic, nullable) NSString <Optional> *test;
will get pulled into the bindings as Optional test { get; set; }
instead of string test { get; set; }
)
Beyond that, I can't recall what additional configurations and tweaks had to be made to all of the scripts, build settings, and config files - but if any questions arise I can look through the code and answer them.
I have a test project that uses CocoaPods to pull in my SDK like so:
Examples/tvOS/Podfile
use_frameworks!
platform :tvos, '9.1'
target 'MyFramework Example tvOS App' do
pod 'MyFramework', :path => '../../'
end
This test project builds perfectly, and I can run it on the simulator or on a real device. Several weeks ago I was also able to deploy my test project to the Apple Store (TestFlight) however since then I have made a few changes (including switching from standard build to a fat binary) and so I don't know for certain that it still works (I will test this)
Furthermore, when we build for profiling and create the Xamarin bindings, we can embed all four frameworks (MyFramework.framework
, CocoaLumberjack.framework
, JSONModel.framework
, and GoogleInteractiveMediaAds.framework
) into our Xamarin apps and they will build and run on both the simulator and on a real device.
The problem we ran into was when we tried to push one of these Xamarin-based apps to the Apple Store (TestFlight). We received 11 errors:
- ERROR ITMS-90166: "Missing Code Signing Entitlements. No entitlements found in bundle 'com.xxx.MyFramework' for executable 'Payload/InternalApp.app/Frameworks/MyFramework-tvOS.framework/MyFramework.bundle/MyFramework'."
- ERROR ITMS-90668: "Invalid Bundle Executable. The executable file 'InternalApp.app/Frameworks/MyFramework-tvOS.framework/MyFramework-tvOS' contains incomplete bitcode. To compile binaries with complete bitcode, open Xcode and choose Archive in the Product menu"
- ERROR ITMS-90668: "Invalid Bundle Executable. The executable file 'InternalApp.app/Frameworks/CocoaLumberjack.framework/CocoaLumberjack' contains incomplete bitcode. To compile binaries with complete bitcode, open Xcode and choose Archive in the Product menu"
- ERROR ITMS-90668: "Invalid Bundle Executable. The executable file 'InternalApp.app/Frameworks/JSONModel.framework/JSONModel' contains incomplete bitcode. To compile binaries with complete bitcode, open Xcode and choose Archive in the Product menu"
- ERROR ITMS-90635: "Invalid Mach-O Format. The Mach-O in bundle "InternalApp.app/Frameworks/CocoaLumberjack.framework" isn't consistent with the Mach-O in the main bundle. The main bundle Mach-O contains arm64(bitcode), while the nested bundle Mach-O contains arm64(machine code). Verify that all of the targets for a platform have a consistent value for the ENABLE_BITCODE build setting."
- ERROR ITMS-90635: "Invalid Mach-O Format. The Mach-O in bundle "InternalApp.app/Frameworks/MyFramework-tvOS.framework" isn't consistent with the Mach-O in the main bundle. The main bundle Mach-O contains arm64(bitcode), while the nested bundle Mach-O contains arm64(machine code). Verify that all of the targets for a platform have a consistent value for the ENABLE_BITCODE build setting."
- ERROR ITMS-90635: "Invalid Mach-O Format. The Mach-O in bundle "InternalApp.app/Frameworks/JSONModel.framework" isn't consistent with the Mach-O in the main bundle. The main bundle Mach-O contains arm64(bitcode), while the nested bundle Mach-O contains arm64(machine code). Verify that all of the targets for a platform have a consistent value for the ENABLE_BITCODE build setting."
- ERROR ITMS-90171: "Invalid Bundle Structure - The binary file 'InternalApp.app/Frameworks/MyFramework-tvOS.framework/MyFramework.bundle/MyFramework' is not permitted. Your app can't contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. Refer to the Bundle Programming Guide at https://developer.apple.com/go/?id=bundle-structure for information on the tvOS app bundle structure."
- ERROR ITMS-90209: "Invalid Segment Alignment. The app binary at 'InternalApp.app/Frameworks/MyFramework-tvOS.framework/MyFramework-tvOS' does not have proper segment alignment. Try rebuilding the app with the latest Xcode version."
- ERROR ITMS-90209: "Invalid Segment Alignment. The app binary at 'InternalApp.app/Frameworks/CocoaLumberjack.framework/CocoaLumberjack' does not have proper segment alignment. Try rebuilding the app with the latest Xcode version."
- ERROR ITMS-90209: "Invalid Segment Alignment. The app binary at 'InternalApp.app/Frameworks/JSONModel.framework/JSONModel' does not have proper segment alignment. Try rebuilding the app with the latest Xcode version."
So I started trying to trace the build process manually to figure out where or what went wrong. I'm already using the latest Xcode, and we've tried both Product > Archive
and Product > Build For > Profiling
, so I don't think that the suggested solutions for ITMS-90668 or ITMS-90209 are accurate.
While looking into the build process manually, I noticed a few things that were strange:
- When I build the tvOS version of my app, in
~/Library/Developer/Xcode/DerivedData/MyFramework-xxxxxx/Build/Products
(henceforth:${BUILD_ROOT}
) the iOS and tvOS versions of my dependencies were being built:${BUILD_ROOT}/Release-appletvos/JSONModel-iOS.framework
${BUILD_ROOT}/Release-appletvos/JSONModel-tvOS.framework
${BUILD_ROOT}/Release-appletvsimulator/JSONModel-iOS.framework
${BUILD_ROOT}/Release-appletvsimulator/JSONModel-tvOS.framework
- etc. (same for CocoaLumberjack)
- Sure enough, if I looked inside of
${BUILD_ROOT}/Release-universal/MyFramework-tvOS.framework/MyFramework.bundle
, there was an executable file in there titledMyFramework
. I have no idea who or what is creating this executable, where it's coming from, or why it's getting included in my bundle. There is no reference to it in any of the Build Phases, but this seems to be the cause for ITMS-90171 The GoogleInteractiveMediaAds framework was the only one that didn't have bitcode enabled, which leads me to suspect that bitcode isn't necessary to submit an app to the store since it's also the only framework that didn't throw any errors (despite what I was told before that you need to include bitcode)
What I'm trying to do doesn't sound like it should be that hard. I'm just creating a library (like CocoaLumberjack) which can be run on iOS or tvOS (like CocoaLumberjack) but which has dependencies (unlike CocoaLumberjack) and which can be delivered as a .framework
file instead of just as a CocoaPod (not sure if CocoaLumberjack has this?). But I can't find any answers to the problems I'm encountering or any explanation for some of the weird build behavior.
Specifically:
- Why is an executable file showing up in my
.bundle
? - Why is it building the iOS versions of dependent frameworks when I try to build a tvOS version of mine?
Do I need bitcode? Or should I disable it?
Update
- The Google Interactive Media Ads framework does have bitcode, it just doesn't have an
__LLVM
segment - Deleting the binary from my
.bundle
replaced ITMS-90171 with a new error (I forget the ITMS-X code) about the Info.plist referencing a CFBundleExecutable that did not exist. Deleting this binary also handled ITMS-90166 - By going to the resource bundle's target and selecting "Info" I could remove the "Executable file" key to remove this line in the output Info.plist, however it did not fix the executable being created in the first place. It DID prevent the new error when the binary was deleted manually
So by modifying the Info.plist and removing that executable (manually for now, since I don't know what's generating it), 2 of the 11 errors went away, leaving 3 instances of 3 errors (total of 9):
- CocoaLumberjack (ITMS-90668, ITMS-90635, and ITMS-90209)
- JSONModel (ITMS-90668, ITMS-90635, and ITMS-90209)
- MyFramework (ITMS-90668, ITMS-90635, and ITMS-90209)
Update 2
- Adding the
BITCODE_GENERATION_MODE=bitcode
flag to my xcodebuild command seemed to improve things. My output binary went from 1.2 MB to 3.4 MB (presumably containing "complete" bitcode). However something weird happened. Instead of the three instances of ITMS-90668 going away, all three errors (ITMS-90668, ITMS-90635, and ITMS-90209) went away for a single framework: JSONModel
I'm now seeing 6 errors (CocoaLumberjack and MyFramework each throwing 3 errosr). But I can't comprehend how or why JSONModel was fixed and the other issues weren't
One other addendum: When I added the bitcode generation mode flag, I started getting a linker error for the executable in my bundle (which shouldn't exist and I still don't know what's creating it). I noticed that the OTHER_CFLAGS
was being passed to EVERY invocation of clang and not just the relevant ones, so I removed OTHER_CFLAGS="-fembed-bitcode"
and this fixed the linker error
One final addendum: I noticed that when I don't use my Run Script to produce a fat binary, only the *-tvOS
versions of my dependencies are built. My Run Script is building the *-iOS
versions. This seems to have no impact on the errors I'm seeing from the Apple Store, but it slows down my builds, creates extra files, and emits several warnings about implicit dependency detection finding two candidates. I don't know how to fix this.
Update 3
I've solved one of my issues!
So the strange binary that was appearing inside of my bundle was due to the VERSIONING_SYSTEM
build setting being defaulted to apple-generic
. The Apple Generic Versioning system creates a source file calls <ProjectName>_vers.c
, compiles it, and then links this file against some libraries (I guess system libraries?)
It was this file which was causing my linker errors when I had OTHER_CFLAGS="-fembed-bitcode"
, since this binary was setting -bundle
which is incompatible. By setting the versioning system to None
I was able to bring back this flag without any linker errors!
I just compiled and tried to push to the Apple store once more and everything worked this time!
Summary
- To fix ITMS-90166 and ITMS-90171 I had to set
Versioning System
toNone
for any resource bundle targets - After this is fixed, you must ensure that the info.plist for the resource bundles does not contain an
Executable File
key (delete it if present) - To fix ITMS-90668, ITMS-90635, and ITMS-90209 I had to add the flags
BITCODE_GENERATION_MODE=bitcode
andENABLE_BITCODE=YES
to myxcodebuild
command (along with the existingOTHER_CFLAGS="-fembed-bitcode"
)
After all of this there's still a weird quirk of my build system, but it only throws warnings (not errors) and I can live with it for the short-term. That is the fact that it's building iOS versions of my dependencies when I run xcodebuild instead of just building the tvOS versions.