DEV Community

Matt Catalfamo
Matt Catalfamo

Posted on • Updated on

How to Build and Manually Sign an iOS App with Fastlane

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)

Collapse
 
onlinemsr profile image
Raja MSR

🙌 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? 🤔