Skip to content

Instantly share code, notes, and snippets.

@damian-rzeszot
Last active April 28, 2023 20:20
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save damian-rzeszot/0b23ad87e5ab5d52aa15c095cbf43c59 to your computer and use it in GitHub Desktop.
Save damian-rzeszot/0b23ad87e5ab5d52aa15c095cbf43c59 to your computer and use it in GitHub Desktop.
override_app_extensions.sh
alias plistbuddy=/usr/libexec/PlistBuddy
alias codesign=/usr/bin/codesign
#
# Bundle identifier
#
set_plist_bundle_identifier() {
local bundle_identifier="$1"
local plist_file="$2"
plistbuddy \
-c "set :CFBundleIdentifier $bundle_identifier" \
"$plist_file"
}
set_appex_bundle_identifier() {
local appex_target_name="$1"
local bundle_identifier_suffix="$2"
local bundle_identifier="$PRODUCT_BUNDLE_IDENTIFIER.$bundle_identifier_suffix"
local plist_file="$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/$BUNDLE_PLUGINS_FOLDER_PATH/$appex_target_name.appex/Info.plist"
set_plist_bundle_identifier "$bundle_identifier" "$plist_file"
}
#
# Bundle version
#
set_plist_bundle_version() {
local bundle_version="$1"
local plist_file="$2"
plistbuddy \
-c "set :CFBundleShortVersionString $bundle_version" \
"$plist_file"
}
get_plsit_bundle_version() {
local plist_file="$1"
plistbuddy \
-c "Print :CFBundleShortVersionString" \
"$plist_file"
}
get_app_bundle_version() {
local plist_file="$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH"
get_plsit_bundle_version "$plist_file"
}
set_appex_bundle_version() {
local appex_target_name="$1"
local bundle_version="$2"
local plist_file="$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/$BUNDLE_PLUGINS_FOLDER_PATH/$appex_target_name.appex/Info.plist"
set_plist_bundle_version "$bundle_version" "$plist_file"
}
#
# Bundle build
#
set_plist_bundle_build() {
local bundle_build="$1"
local plist_file="$2"
plistbuddy \
-c "set :CFBundleVersion $bundle_build" \
"$plist_file"
}
get_plist_bundle_build() {
local plist_file="$1"
plistbuddy \
-c "Print :CFBundleVersion" \
"$plist_file"
}
set_appex_bundle_build() {
local appex_target_name="$1"
local bundle_version="$2"
local plist_file="$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/$BUNDLE_PLUGINS_FOLDER_PATH/$appex_target_name.appex/Info.plist"
set_plist_bundle_build "$bundle_version" "$plist_file"
}
get_app_bundle_build() {
local plist_file="$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH"
get_plist_bundle_build "$plist_file"
}
#
# Code signing
#
prepare_entitlements_file() {
local appex_target_name="$1"
local bundle_identifier_suffix="$2"
local output_file="$3"
local original_entitlements="$CONFIGURATION_TEMP_DIR/$appex_target_name.build/$appex_target_name.appex.xcent"
local bundle_identifier="$DEVELOPMENT_TEAM.$PRODUCT_BUNDLE_IDENTIFIER.$bundle_identifier_suffix"
cp "$original_entitlements" "$output_file"
if [[ $CONFIGURATION == "Release" ]]
then
plistbuddy \
-c "set :application-identifier $bundle_identifier" \
"$output_file"
plistbuddy \
-c "set :keychain-access-groups:0 $bundle_identifier" \
"$output_file"
fi
}
copy_provisioning_profile() {
local appex_target_name="$1"
local provision_source="$2"
local provision_destination="$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/$BUNDLE_PLUGINS_FOLDER_PATH/$appex_target_name.appex/$EMBEDDED_PROFILE_NAME"
cp "$provision_source" "$provision_destination"
}
resign_appex() {
local appex_target_name="$1"
local entitlements_file="$2"
codesign \
--force \
--sign "$EXPANDED_CODE_SIGN_IDENTITY" \
--entitlements "$entitlements_file" \
--timestamp=none \
"$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/$BUNDLE_PLUGINS_FOLDER_PATH/$appex_target_name.appex"
}
#
#
#
#
#
set_appex_bundle_identifier \
"NotificationService" \
"notification-service"
set_appex_bundle_version \
"NotificationService" \
`get_app_bundle_version`
set_appex_bundle_build \
"NotificationService" \
`get_app_bundle_build`
# Be careful if using `keychain-access-groups` entitlement
prepare_entitlements_file \
"NotificationService" \
"notification-service" \
"$DERIVED_SOURCES_DIR/NotificationService-Entitlements.plist"
copy_provisioning_profile \
"NotificationService" \
"$SOURCE_ROOT/../.github/appstore/$TARGET_NAME/profiles/notification-service.mobileprovision"
resign_appex \
"NotificationService" \
"$DERIVED_SOURCES_DIR/NotificationService-Entitlements.plist"
@mfaani
Copy link

mfaani commented May 29, 2020

If anyone is wondering what $1 is...

$1 is the parameter you you passed onto the shell function.
When we do:

override_bundle_identifier "NotificationService"

NotificationService is the first parameter aka $1. It get's passed to the override_bundle_identifier func and every $1 is replaced with NotificationService

On top of changing the bundleID and re-signing another benefit of this code is that it can be used for any app extension. All you have to do is give the app extensions target name

@mfaani
Copy link

mfaani commented Dec 9, 2020

Don't you also need to update the provisioning profile?

@damian-rzeszot
Copy link
Author

@prohoney not it my case. I use manual signing for release builds and proper provisioning profiles are set in build settings.

I added override_app_version to the script, so that it copies bundle version to appex - no warnings generated now.
Also functions take a bit different parameters which gives you more possibilities.

@mfaani
Copy link

mfaani commented Dec 9, 2020

🤔 How are you setting appropriate provisioning profiles in build settings? If you were able to switch over in build settings then wouldn't you have been able to switch the bundleId and code-singing too? ie no need for bundleId and code-signing related scripts...

I know how to switch over with different configurations but you and I both have different targets...

@damian-rzeszot
Copy link
Author

damian-rzeszot commented Dec 9, 2020

Let's say I have targets:

  • AppStaging (com.example.staging)
  • App1 (com.example.app1)
  • App2 (com.example.app2)
  • NotificationContent (com.example.staging.notification-content)

On daily basis I develop AppStaging. The differences between AppStaging and AppX are: Info.plist, Assets and configuration.json file.
AppStaging and AppX has dependency target to NotificationContent.

For Debug configuration I use automatic signing (I don't care what's going on there if it's working).
For Release proper provisioning profiles and other signing settings are set (for all targets):

CODE_SIGN_IDENTITY="iOS Distribution"
CODE_SIGN_STYLE="Manual"
PROVISIONING_PROFILE_SPECIFIER="<uuid>"

There is also a build step Override appex after Embed App Extensions where I run the script.
And magic happens here. If I build AppStaging basically everything works as expected (overriding will set exact the same value as were set before). But if I build AppX target:

  1. NotificationContent (com.example.staging.notification-content) is built and signed
  2. AppX (e.g. com.example.app1) target is built
  3. NotificationContent appex is embedded into app bundle
    // It's clear there is a conflict here - but there is a Override appex step
  4. Appex in the app bundle is update (bundle id, version) and signed
  5. The app is validated ✅

@mfaani
Copy link

mfaani commented Dec 9, 2020

Thank you for answering my questions. I really Appreciate it.

  1. AppX means App1 & App2? Or appex (app extension)

  2. You said:

For Release proper provisioning profiles and other signing settings are set (for all targets)

Does 'all targets' mean all non-app extension targets or it includes the app extension targets as well?

I mean an app extension needs its own distinct provisioning profile. If you are to have an appex with a bundleId of: com.example.app2. notification-content then where are you giving its provisioning profile? ...Before the scripts are ran, that bundleId doesn't exist. Hence you can't tell Xcode of it. RIght?

I mean how do set the PROVISIONING_PROFILE_SPECIFIER for com.example.app2. notification-content?

  1. What's the Override appex step? You already mentioned the following three:
override_bundle_identifier "NotificationService" "$PRODUCT_BUNDLE_IDENTIFIER.notification-service"
override_app_version "NotificationService"
override_code_signing "NotificationService"

Is that something else? Or you're just naming all 3 steps override appex?

@damian-rzeszot
Copy link
Author

damian-rzeszot commented Dec 10, 2020

@prohoney you're right, there is an issue with provisioning profile while uploading to App Store. I updated the script. Within this I was able to upload 2 apps to TestFlight.

@mfaani
Copy link

mfaani commented Dec 10, 2020

For one who originally did it wrong you figured it all out super quick. How do you do it?!

This whole build settings stuff is new to me. Took me a few rounds of reading to understand it. Also you should post more frequently on Stack Overflow or blog about stuff.

  1. But if I understand correct, the "$SOURCE_ROOT/../.github/appstore/$TARGET_NAME/profiles/notification-service.mobileprovision is a predetermined path to the target's the provisioning profile. You just need to to figure out where to put it. You do that that in your copy_provisioning_profile func. Right?

  2. I like how you pulled in $DEVELOPMENT_TEAM into local bundle_identifier="$DEVELOPMENT_TEAM.$PRODUCT_BUNDLE_IDENTIFIER.$bundle_identifier_suffix". That helps you to sign with different teams. Right?

  3. My 'Notification Service Extension' for which I use to download images, doesn't have any entitlements, but I understand the need to modify the bundleId inside the entitlement's plist. Can I ask why yours needed certain entitlements? Is it because of keychain sharing?

@damian-rzeszot
Copy link
Author

  1. That's right. $SOURCE_ROOT/.../notification-service.mobileprovision is there, because I use GitHub Actions for deploying the build to App Store. I didn't want to dig in target's build settings via e.g. xcodeproj. That's fair enough solution.

  2. Yeap. I don't like hardcoding things in scripts. $DEVELOPMENT_TEAM is provided by Xcode.

  3. Xcode generates entitlements. The generated one for the notification service extension contains original [bundle] ids, so it has to be overridden.

@mfaani
Copy link

mfaani commented Dec 14, 2020

Thank you. I also wanted to suggest that you can use plutil instead of PlistBuddy. Based on what an Apple Engineer told me plutil is actively updated, PlistBuddy seems to not be updated

Also was there a reason you did alias codesign=/usr/bin/codesign? Couldn't you just used codesign without doing that? It's already in the bin directory`

@mfaani
Copy link

mfaani commented Dec 14, 2020

Sorry. One more thing what's the difference between .entitlements file and .xcent file?

@damian-rzeszot
Copy link
Author

damian-rzeszot commented Dec 14, 2020

  • plutil - good point 👍
  • alias codesign - true, it's not needed 👍
  • .xcent - I see no difference. Probably somebody decided to use xc prefixes there, as in xcconfig, xcproject, etc. Looks like legacy naming.

@mfaani
Copy link

mfaani commented Dec 15, 2020

Can see my answer here and the steps I've written. The steps it has are not complete as yours. But I just don't get how you get yours is working given that you're updating the bundleId after 'embed App Extension' step. Based on my understanding that step should fail.

FYI this fails when I'm using a physical device. I haven't tried archiving yet...

Specifically the point I'm referencing from my answer:

Similarly the appex can't be embedded if the bundleId isn't prefixed with the parent app's bundleId.

i.e. I think bundleId should change before the 'embed App Extension' step.

Or is it that you're also updating the bundleId of the parent app in some other script before changing stuff for the appex ie you're able to embed the Appex's into the main app, but then later change the bundle Id for both the main app and appex?
Basically are you doing this?

  1. embed apppex
  2. update parent app bundleId and stuff
  3. update appex bundleId and stuff

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment