This post will cover how to build and manually code sign an iOS app with Fastlane. Why would you need to do this? If you want/need to distribute an app to different Apple App Store accounts. For example if you have an Enterprise App Store account and want/need to sign the QA build of your app with the enterprise account to distribute internally and then sign the Release build with a normal/publicly available account to distribute to the public.
TL;DR
- How to build an iOS app using Fastlane
- How to manually code sign your iOS app using Fastlane
Prerequisites:
- Knowledge of iOS build process
- Knowledge of iOS code signing process
- Basic understanding of Fastlane
- Install Fastlane following their recommended setup
Assumptions:
- You are building your app on a macOS image that doesn't have your signing certificates or provisioning profiles installed.
- You have password encrypted signing certificate downloaded to directory on your build machine
- You have a mobile provisioning profile downloaded to a directory on your build machine
- Automatic code signing is disabled in your Xcode project
For the record, I would recommend if you have a simple iOS project that you use Fastlane match and Xcode automatic code signing. But sometimes in the real world there are restrictions and external reasons to have to do manual code signing, so we will go through the process of building and signing manually. Here is the process.
Lets get started
Create a new Fastfile, this can be done by using Fastlane's cli tool
fastlane init
This creates a Fastlane directory with a Fastfile.
Your fastfile will look something like this:
default_platform(:ios)
platform :ios do
desc "Description of what the lane does"
lane :custom_lane do
# add actions here: https://docs.fastlane.tools/actions
end
end
I am going to rename the default lane that was created to "build" and I will add the command line options array |options|
so we can have access to that later on.
platform :ios do
desc "Build app"
lane :build do |options|
end
end
Now that the setup is done we can continue on. First we need to create a new keychain, second install our provisioning profiles, and lastly import the certificates.
Since we may have different profiles or certificates for different builds of our app, let's create a function called setupCodeSigning
. The function will take 4 parameters- keychainPassword, certificatePassword, profilePath, and certificatePath.
keychainPassword: Made up password to set for your keychain you are creating on the Mac you are building your iOS app.
certificatePassword: The certificate password you used to encrypt your signing certificate
profilePath: Path to the provisioning profile (ie: ./dependencies/profile-release.mobileprovision) on your build machine
certificatePath: Path to the certificate (ie: ./dependencies/certificate.p12)
def setupCodeSigning(keychainPassword, certificatePassword, profilePath, certificatePath)
end
Now let's create the keychain. We can use the create_keychain plugin.
Make sure you set a name for the keychain because we will reference it when importing the certificates. If your use case needs other parameters, refer to the create_keychain plugin documentation for more details.
def setupCodeSigning(keychainPassword, certificatePassword, profilePath, certificatePath)
create_keychain(
name: "CI",
password: keychainPassword,
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
end
Now that we have the keychain created we need to install the provisioning profile(s). We can use the install_provisioning_profile plugin. Be sure to pass the profilePath parameter as the path variable to the plugin.
def setupCodeSigning(keychainPassword, certificatePassword, profilePath, certificatePath)
create_keychain(
name: "CI",
password: keychainPassword,
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
install_provisioning_profile(path: profilePath)
end
Last, import the certificate(s) to the keychain by passing the certificate path, keychain name that we called our keychain, and the password we made up for the keychain.
The setupCodeSigning function should look like this:
def setupCodeSigning(keychainPassword, certificatePassword, profilePath, certificatePath)
create_keychain(
name: "CI",
password: keychainPassword,
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
install_provisioning_profile(path: profilePath)
import_certificate(
certificate_path: certificatePath,
certificate_password: certificatePassword,
keychain_name: "CI",
keychain_password: keychainPassword
)
end
Now that the hard part is over we can call the setupCodeSigning function in the build lane before we build the app. The build lane will now look like this:
lane :build do |options|
begin
setupCodeSigning(ENV["MATCH_PASSWORD"], ENV["CERTIFICATE_PASSWORD"], './path-to-your-profile/your-profile.mobileprovision', './path-to-your-certificate/certificate.p12')
cocoapods(clean_install: true, use_bundle_exec: false, error_callback: true)
build_app(
scheme: "your-scheme",
configuration: 'Release'
)
upload_to_testflight(
username: options[:appStoreEmail],
skip_waiting_for_build_processing: true,
skip_submission: true)
rescue => exception
on_error(options[:slackUrl], "Build Failed", "#slack-channel", exception)
end
end
Optional: I like to add a "begin, rescue" block around the build to catch any errors and send them to a slack channel if anything fails for convenience. slack plugin
Our complete fast file should look like this:
platform :ios do
desc "Build app"
lane :build do |options|
begin
setupCodeSigning(ENV["KEYCHAIN_PASSWORD"], ENV["CERTIFICATE_PASSWORD"], './path-to-your-profile/your-profile.mobileprovision', './path-to-your-certificate/certificate.p12')
cocoapods(clean_install: true, use_bundle_exec: false, error_callback: true)
build_app(
scheme: "your-scheme",
configuration: 'Release'
)
upload_to_testflight(
username: options[:appStoreEmail],
skip_waiting_for_build_processing: true,
skip_submission: true)
rescue => exception
on_error(options[:slackUrl], "Build Failed", "#slack-channel", exception)
end
end
end
def setupCodeSigning(keychainPassword, certificatePassword, profilePath, certificatePath)
create_keychain(
name: "CI",
password: keychainPassword,
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
install_provisioning_profile(path: profilePath)
import_certificate(
certificate_path: certificatePath,
certificate_password: certificatePassword,
keychain_name: "CI",
keychain_password: keychainPassword
)
end
def on_error(slackUrl, message, channel, exception)
slack(
slack_url: slackUrl,
channel: channel,
message: "iOS App :appleinc: " + message,
success: false,
payload: {},
default_payloads: [],
attachment_properties: { # Optional, lets you specify any other properties available for attachments in the slack API (see https://api.slack.com/docs/attachments).
color: "#FC100D",
fields: [
{
title: "Error",
value: exception.to_s,
},
]
}
)
raise exception
end
Top comments (1)
🙌 Your clarity and practical insights make this a must-read for iOS developers. Thanks for sharing your expertise!
What challenges did you encounter while implementing manual code signing, and how did you overcome them? 🤔