0

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
  • 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 and GoogleAds-IMA-tvOS-SDK ~> 4.0)
  • 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:

  1. Build my project for a device (ARM processor) with bitcode embedded
  2. Build my project for the simulator (x86 processor) with bitcode embedded
  3. Use lipo to create a fat binary for my project
  4. Iteratively use lipo to create fat binaries for all frameworks I depend on
  5. 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:

  1. 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)
  2. Sure enough, if I looked inside of ${BUILD_ROOT}/Release-universal/MyFramework-tvOS.framework/MyFramework.bundle, there was an executable file in there titled MyFramework. 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
  3. 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 to None 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 and ENABLE_BITCODE=YES to my xcodebuild command (along with the existing OTHER_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.

stevendesu
  • 15,753
  • 22
  • 105
  • 182
  • Bitcode is required for watchOS and tvOS apps (this is for ALL the targets; apps and frameworks). For iOS it is optional, but if it is included in one (app or framework) it must be included in all: https://help.apple.com/xcode/mac/current/#/devbbdc5ce4f – SushiHangover Dec 03 '19 at 20:01
  • @SushiHangover When I inspect the GoogleInteractiveMediaAds framework using `otool -l | grep __LLVM`, it doesn't seem to have bitcode. Could you explain how `GoogleAds-IMA-tvOS-SDK` works if it doesn't have bitcode? – stevendesu Dec 03 '19 at 20:02
  • 1
    There is bitcode in that framework (v4.2.0) binary (`GoogleInteractiveMediaAds.framework/GoogleInteractiveMediaAds`, use `otool -arch arm64 -l GoogleInteractiveMediaAds.framework/GoogleInteractiveMediaAds` and you will find it in the `__bundle` section. Note: You have to specify an arch for your fat binaries – SushiHangover Dec 03 '19 at 20:27
  • @SushiHangover I found [this SO post](https://stackoverflow.com/a/32869670/436976) which suggested the bitcode would be found in an `__LLVM` section, not a `__bundle` section. Did something change? – stevendesu Dec 03 '19 at 21:01
  • 1
    The __LVMM "segment", if it exists, will be within the __bundle "section" of the binary, additional info for that segment will be found within MachO format `LC_SEGMENT_64`(i think...?, not at my desk)... Either way, specify an `arch`, grep the output if desired, GoogleInteractiveMediaAds has bitcode for tvOS. – SushiHangover Dec 03 '19 at 21:12

1 Answers1

1

I don't know how useful this question / answer will be to others who stumble across it, since I had a huge number of issues and a very specific setup. But I wanted to go ahead and post that I did find a solution to my issues.

First and foremost, I needed to set the ENABLE_BITCODE=YES and BITCODE_GENERATION_MODE=bitcode flags. However settings these two flags prevented my code from building with a linker error. This error was caused because my Resource Bundle target was using Apple Generic Versioning -- which is not supported for Resource Bundles

By setting the Versioning System build setting to "None", it fixed everything

stevendesu
  • 15,753
  • 22
  • 105
  • 182