Signing Embedded Frameworks in an Embedded Framework
I have been working on a feature for a mobile application that was developed by another company. My feature is written in Swift while the main app is in Objective-C. No biggie there – the bridging between the two works great. However, my feature is built as a dynamic framework and it has framework dependencies itself. When I add the framework by itself to the Objective-C application, I can run in the simulator but not on the device. The crash I sometimes get is often nothing more than below, with no messages in the console window:
dyld`__abort_with_payload:
0x4f53cc <+0>: mov r12, sp
0x4f53d0 <+4>: push {r4, r5, r6, r8}
0x4f53d4 <+8>: ldm r12, {r4, r5, r6}
0x4f53d8 <+12>: mov r12, #512
0x4f53dc <+16>: orr r12, r12, #9
0x4f53e0 <+20>: svc #0x80
-> 0x4f53e4 <+24>: pop {r4, r5, r6, r8}
0x4f53e8 <+28>: blo 0x4f5400 ; <+52>
0x4f53ec <+32>: ldr r12, [pc, #0x4] ; <+44>
0x4f53f0 <+36>: ldr r12, [pc, r12]
0x4f53f4 <+40>: b 0x4f53fc ; <+48>
0x4f53f8 <+44>: andeq r9, r0, r8, lsl #27
0x4f53fc <+48>: bx r12
0x4f5400 <+52>: bx lr
Other times, I get text in the console complaining about an unsigned framework, with the framework name being one of the frameworks embedded in my own:
dyld: Library not loaded: @rpath/Siesta.framework/Siesta
Referenced from: /private/var/containers/Bundle/Application/6370F0D4-4D48-4EBF-82DC-2E63EB421341/Nedbank.app/Frameworks/TaskMe.framework/TaskMe
Reason: no suitable image found. Did find:
/private/var/containers/Bundle/Application/6370F0D4-4D48-4EBF-82DC-2E63EB421341/Nedbank.app/Frameworks/TaskMe.framework/Frameworks/Siesta.framework/Siesta: required code signature missing for '/private/var/containers/Bundle/Application/6370F0D4-4D48-4EBF-82DC-2E63EB421341/Nedbank.app/Frameworks/TaskMe.framework/Frameworks/Siesta.framework/Siesta'
Note that I had previously updated the Runpath Search Paths build setting value with $executable_path/Frameworks/TaskMe.framework/Frameworks
so that the dynamic linker would see into my custom framework. However, clearly the code signing stage that takes place automatically in Xcode does not descend into such embedded frameworks. Fortunately a quick look at a build log shows what needs to take place:
/usr/bin/codesign --force --deep --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --entitlements "${TARGET_TEMP_DIR}/${PRODUCT_NAME}.app.xcent" --timestamp=none <FILE>
Here <FILE>
is the object to sign, such as a framework.
Deep into Nothing
The man page for codesign
documents a flag called --deep
which seems like the perfect match for this problem. Unfortunately, the codesign step is not configurable as far as I can tell. Natch.
A New Hope (in a Build Phase)
Fortunately, Xcode does allow for build customization in the Build Phases tab of a target, and one such customization option is the ability to run a shell script. I created a new one with the following content that signs each of the embedded frameworks in my own framework:
pushd ${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app/Frameworks/TaskMe.framework/Frameworks
for EACH in *.framework; do
echo "-- signing ${EACH}"
/usr/bin/codesign --force --deep --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --entitlements "${TARGET_TEMP_DIR}/${PRODUCT_NAME}.app.xcent" --timestamp=none $EACH
done
popd
Here we move into the Frameworks
directory of my own framework called TaskMe
and I iterate over all *.framework
entities found there. Each one gets the codesign
treatment using the same parameter settings that I found in the original build log. Note that the --deep
flag above is left-over from my other attempts – these embedded-embedded frameworks do not have any frameworks of their own to sign, so --deep
is useless here.
A quick clean and rebuild, and my app runs fine on my device.
Conclusion
I feel confident that this addition to the build process is kosher, but there is always a chance that future changes to Xcode iOS building will break it – or render it obsolete. There is an alternative approach which also works: embed all embedded frameworks in the top-level target. However, I am less enamored with this approach due to the fact that if I add or remove a framework from my own, I have to remember to do the same with the top-level app target. With my ‘Build Process’ script, this is handled for me auto-magically.