class Match::Runner

rubocop:disable Metrics/ClassLength

Attributes

files_to_commit[RW]
spaceship[RW]
storage[RW]

Public Instance Methods

api_token(params) click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File match/lib/match/runner.rb, line 142
def api_token(params)
  api_token = Spaceship::ConnectAPI::Token.from(hash: params[:api_key], filepath: params[:api_key_path])
  return api_token
end
certificate_count_different?(profile: nil, keychain_path: nil, platform: nil) click to toggle source
# File match/lib/match/runner.rb, line 411
def certificate_count_different?(profile: nil, keychain_path: nil, platform: nil)
  return false unless profile

  parsed = FastlaneCore::ProvisioningProfile.parse(profile, keychain_path)
  uuid = parsed["UUID"]

  all_profiles = Spaceship::ConnectAPI::Profile.all(includes: "certificates")
  portal_profile = all_profiles.detect { |i| i.uuid == uuid }

  return false unless portal_profile

  profile_certs_count = portal_profile.fetch_all_certificates.count

  certificate_types =
    case platform
    when :ios, :tvos
      [
        Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT,
        Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DEVELOPMENT
      ]
    when :macos, :catalyst
      [
        Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT,
        Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DEVELOPMENT
      ]
    else
      []
    end

  certificates = Spaceship::ConnectAPI::Certificate.all
  unless certificate_types.empty?
    certificates = certificates.select do |certificate|
      certificate_types.include?(certificate.certificateType) && certificate.valid?
    end
  end

  portal_certs_count = certificates.size

  return portal_certs_count != profile_certs_count
end
device_count_different?(profile: nil, keychain_path: nil, platform: nil) click to toggle source
# File match/lib/match/runner.rb, line 339
def device_count_different?(profile: nil, keychain_path: nil, platform: nil)
  return false unless profile

  parsed = FastlaneCore::ProvisioningProfile.parse(profile, keychain_path)
  uuid = parsed["UUID"]

  all_profiles = Spaceship::ConnectAPI::Profile.all(includes: "devices")
  portal_profile = all_profiles.detect { |i| i.uuid == uuid }

  if portal_profile
    profile_device_count = portal_profile.fetch_all_devices.count

    device_classes =
      case platform
      when :ios
        [
          Spaceship::ConnectAPI::Device::DeviceClass::IPAD,
          Spaceship::ConnectAPI::Device::DeviceClass::IPHONE,
          Spaceship::ConnectAPI::Device::DeviceClass::IPOD,
          Spaceship::ConnectAPI::Device::DeviceClass::APPLE_WATCH
        ]
      when :tvos
        [
          Spaceship::ConnectAPI::Device::DeviceClass::APPLE_TV
        ]
      when :macos, :catalyst
        [
          Spaceship::ConnectAPI::Device::DeviceClass::MAC
        ]
      else
        []
      end

    devices = Spaceship::ConnectAPI::Device.all
    unless device_classes.empty?
      devices = devices.select do |device|
        device_classes.include?(device.device_class) && device.enabled?
      end
    end

    portal_device_count = devices.size

    return portal_device_count != profile_device_count
  end
  return false
end
fetch_certificate(params: nil, working_directory: nil, specific_cert_type: nil) click to toggle source
# File match/lib/match/runner.rb, line 160
def fetch_certificate(params: nil, working_directory: nil, specific_cert_type: nil)
  cert_type = Match.cert_type_sym(specific_cert_type || params[:type])

  certs = Dir[File.join(prefixed_working_directory, "certs", cert_type.to_s, "*.cer")]
  keys = Dir[File.join(prefixed_working_directory, "certs", cert_type.to_s, "*.p12")]

  if certs.count == 0 || keys.count == 0
    UI.important("Couldn't find a valid code signing identity for #{cert_type}... creating one for you now")
    UI.crash!("No code signing identity found and can not create a new one because you enabled `readonly`") if params[:readonly]
    cert_path = Generator.generate_certificate(params, cert_type, prefixed_working_directory, specific_cert_type: specific_cert_type)
    private_key_path = cert_path.gsub(".cer", ".p12")

    self.files_to_commit << cert_path
    self.files_to_commit << private_key_path
  else
    cert_path = certs.last

    # Check validity of certificate
    if Utils.is_cert_valid?(cert_path)
      UI.verbose("Your certificate '#{File.basename(cert_path)}' is valid")
    else
      UI.user_error!("Your certificate '#{File.basename(cert_path)}' is not valid, please check end date and renew it if necessary")
    end

    if Helper.mac?
      UI.message("Installing certificate...")

      # Only looking for cert in "custom" (non login.keychain) keychain
      # Doing this for backwards compatibility
      keychain_name = params[:keychain_name] == "login.keychain" ? nil : params[:keychain_name]

      if FastlaneCore::CertChecker.installed?(cert_path, in_keychain: keychain_name)
        UI.verbose("Certificate '#{File.basename(cert_path)}' is already installed on this machine")
      else
        Utils.import(cert_path, params[:keychain_name], password: params[:keychain_password])

        # Import the private key
        # there seems to be no good way to check if it's already installed - so just install it
        # Key will only be added to the partition list if it isn't already installed
        Utils.import(keys.last, params[:keychain_name], password: params[:keychain_password])
      end
    else
      UI.message("Skipping installation of certificate as it would not work on this operating system.")
    end

    if params[:output_path]
      FileUtils.cp(cert_path, params[:output_path])
      FileUtils.cp(keys.last, params[:output_path])
    end

    # Get and print info of certificate
    info = Utils.get_cert_info(cert_path)
    TablePrinter.print_certificate_info(cert_info: info)
  end

  return File.basename(cert_path).gsub(".cer", "") # Certificate ID
end
fetch_provisioning_profile(params: nil, certificate_id: nil, app_identifier: nil, working_directory: nil) click to toggle source

rubocop:disable Metrics/PerceivedComplexity @return [String] The UUID of the provisioning profile so we can verify it with the Apple Developer Portal

# File match/lib/match/runner.rb, line 220
def fetch_provisioning_profile(params: nil, certificate_id: nil, app_identifier: nil, working_directory: nil)
  prov_type = Match.profile_type_sym(params[:type])

  names = [Match::Generator.profile_type_name(prov_type), app_identifier]
  if params[:platform].to_s == :tvos.to_s || params[:platform].to_s == :catalyst.to_s
    names.push(params[:platform])
  end

  profile_name = names.join("_").gsub("*", '\*') # this is important, as it shouldn't be a wildcard
  base_dir = File.join(prefixed_working_directory, "profiles", prov_type.to_s)

  extension = ".mobileprovision"
  if [:macos.to_s, :catalyst.to_s].include?(params[:platform].to_s)
    extension = ".provisionprofile"
  end

  profile_file = "#{profile_name}#{extension}"
  profiles = Dir[File.join(base_dir, profile_file)]
  if Helper.mac?
    keychain_path = FastlaneCore::Helper.keychain_path(params[:keychain_name]) unless params[:keychain_name].nil?
  end

  # Install the provisioning profiles
  profile = profiles.last
  force = params[:force]

  if params[:force_for_new_devices]
    force = should_force_include_all_devices(params: params, prov_type: prov_type, profile: profile, keychain_path: keychain_path) unless force
  end

  if params[:include_all_certificates]
    # Clearing specified certificate id which will prevent a profile being created with only one certificate
    certificate_id = nil
    force = should_force_include_all_certificates(params: params, prov_type: prov_type, profile: profile, keychain_path: keychain_path) unless force
  end

  if profile.nil? || force
    if params[:readonly]
      UI.error("No matching provisioning profiles found for '#{profile_file}'")
      UI.error("A new one cannot be created because you enabled `readonly`")
      if Dir.exist?(base_dir) # folder for `prov_type` does not exist on first match use for that type
        all_profiles = Dir.entries(base_dir).reject { |f| f.start_with?(".") }
        UI.error("Provisioning profiles in your repo for type `#{prov_type}`:")
        all_profiles.each { |p| UI.error("- '#{p}'") }
      end
      UI.error("If you are certain that a profile should exist, double-check the recent changes to your match repository")
      UI.user_error!("No matching provisioning profiles found and can not create a new one because you enabled `readonly`. Check the output above for more information.")
    end

    profile = Generator.generate_provisioning_profile(params: params,
                                                   prov_type: prov_type,
                                              certificate_id: certificate_id,
                                              app_identifier: app_identifier,
                                                       force: force,
                                           working_directory: prefixed_working_directory)
    self.files_to_commit << profile
  end

  if Helper.mac?
    installed_profile = FastlaneCore::ProvisioningProfile.install(profile, keychain_path)
  end
  parsed = FastlaneCore::ProvisioningProfile.parse(profile, keychain_path)
  uuid = parsed["UUID"]

  if params[:output_path]
    FileUtils.cp(profile, params[:output_path])
  end

  if spaceship && !spaceship.profile_exists(username: params[:username], uuid: uuid, platform: params[:platform])
    # This profile is invalid, let's remove the local file and generate a new one
    File.delete(profile)
    # This method will be called again, no need to modify `files_to_commit`
    return nil
  end

  Utils.fill_environment(Utils.environment_variable_name(app_identifier: app_identifier,
                                                                   type: prov_type,
                                                               platform: params[:platform]),

                         uuid)

  # TeamIdentifier is returned as an array, but we're not sure why there could be more than one
  Utils.fill_environment(Utils.environment_variable_name_team_id(app_identifier: app_identifier,
                                                                           type: prov_type,
                                                                       platform: params[:platform]),
                         parsed["TeamIdentifier"].first)

  Utils.fill_environment(Utils.environment_variable_name_profile_name(app_identifier: app_identifier,
                                                                                type: prov_type,
                                                                            platform: params[:platform]),
                         parsed["Name"])

  Utils.fill_environment(Utils.environment_variable_name_profile_path(app_identifier: app_identifier,
                                                                                type: prov_type,
                                                                            platform: params[:platform]),
                         installed_profile)

  return uuid
end
prefixed_working_directory() click to toggle source

Used when creating a new certificate or profile

# File match/lib/match/runner.rb, line 148
def prefixed_working_directory
  return self.storage.prefixed_working_directory
end
run(params) click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File match/lib/match/runner.rb, line 23
def run(params)
  self.files_to_commit = []

  FileUtils.mkdir_p(params[:output_path]) if params[:output_path]

  FastlaneCore::PrintTable.print_values(config: params,
                                         title: "Summary for match #{Fastlane::VERSION}")

  update_optional_values_depending_on_storage_type(params)

  # Choose the right storage and encryption implementations
  self.storage = Storage.for_mode(params[:storage_mode], {
    git_url: params[:git_url],
    shallow_clone: params[:shallow_clone],
    skip_docs: params[:skip_docs],
    git_branch: params[:git_branch],
    git_full_name: params[:git_full_name],
    git_user_email: params[:git_user_email],
    clone_branch_directly: params[:clone_branch_directly],
    git_basic_authorization: params[:git_basic_authorization],
    git_bearer_authorization: params[:git_bearer_authorization],
    git_private_key: params[:git_private_key],
    type: params[:type].to_s,
    generate_apple_certs: params[:generate_apple_certs],
    platform: params[:platform].to_s,
    google_cloud_bucket_name: params[:google_cloud_bucket_name].to_s,
    google_cloud_keys_file: params[:google_cloud_keys_file].to_s,
    google_cloud_project_id: params[:google_cloud_project_id].to_s,
    s3_region: params[:s3_region],
    s3_access_key: params[:s3_access_key],
    s3_secret_access_key: params[:s3_secret_access_key],
    s3_bucket: params[:s3_bucket],
    s3_object_prefix: params[:s3_object_prefix],
    readonly: params[:readonly],
    username: params[:readonly] ? nil : params[:username], # only pass username if not readonly
    team_id: params[:team_id],
    team_name: params[:team_name],
    api_key_path: params[:api_key_path],
    api_key: params[:api_key]
  })
  storage.download

  # Init the encryption only after the `storage.download` was called to have the right working directory
  encryption = Encryption.for_storage_mode(params[:storage_mode], {
    git_url: params[:git_url],
    working_directory: storage.working_directory
  })
  encryption.decrypt_files if encryption

  unless params[:readonly]
    self.spaceship = SpaceshipEnsure.new(params[:username], params[:team_id], params[:team_name], api_token(params))
    if params[:type] == "enterprise" && !Spaceship.client.in_house?
      UI.user_error!("You defined the profile type 'enterprise', but your Apple account doesn't support In-House profiles")
    end
  end

  if params[:app_identifier].kind_of?(Array)
    app_identifiers = params[:app_identifier]
  else
    app_identifiers = params[:app_identifier].to_s.split(/\s*,\s*/).uniq
  end

  # sometimes we get an array with arrays, this is a bug. To unblock people using match, I suggest we flatten!
  # then in the future address the root cause of https://github.com/fastlane/fastlane/issues/11324
  app_identifiers.flatten!

  # Verify the App ID (as we don't want 'match' to fail at a later point)
  if spaceship
    app_identifiers.each do |app_identifier|
      spaceship.bundle_identifier_exists(username: params[:username], app_identifier: app_identifier, platform: params[:platform])
    end
  end

  # Certificate
  cert_id = fetch_certificate(params: params, working_directory: storage.working_directory)

  # Mac Installer Distribution Certificate
  additional_cert_types = params[:additional_cert_types] || []
  cert_ids = additional_cert_types.map do |additional_cert_type|
    fetch_certificate(params: params, working_directory: storage.working_directory, specific_cert_type: additional_cert_type)
  end

  cert_ids << cert_id
  spaceship.certificates_exists(username: params[:username], certificate_ids: cert_ids) if spaceship

  # Provisioning Profiles
  unless params[:skip_provisioning_profiles]
    app_identifiers.each do |app_identifier|
      loop do
        break if fetch_provisioning_profile(params: params,
                                    certificate_id: cert_id,
                                    app_identifier: app_identifier,
                                working_directory: storage.working_directory)
      end
    end
  end

  if self.files_to_commit.count > 0 && !params[:readonly]
    encryption.encrypt_files if encryption
    storage.save_changes!(files_to_commit: self.files_to_commit)
  end

  # Print a summary table for each app_identifier
  app_identifiers.each do |app_identifier|
    TablePrinter.print_summary(app_identifier: app_identifier, type: params[:type], platform: params[:platform])
  end

  UI.success("All required keys, certificates and provisioning profiles are installed 🙌".green)
rescue Spaceship::Client::UnexpectedResponse, Spaceship::Client::InvalidUserCredentialsError, Spaceship::Client::NoUserCredentialsError => ex
  UI.error("An error occurred while verifying your certificates and profiles with the Apple Developer Portal.")
  UI.error("If you already have your certificates stored in git, you can run `fastlane match` in readonly mode")
  UI.error("to just install the certificates and profiles without accessing the Dev Portal.")
  UI.error("To do so, just pass `readonly: true` to your match call.")
  raise ex
ensure
  storage.clear_changes if storage
end
should_force_include_all_certificates(params: nil, prov_type: nil, profile: nil, keychain_path: nil) click to toggle source
# File match/lib/match/runner.rb, line 386
def should_force_include_all_certificates(params: nil, prov_type: nil, profile: nil, keychain_path: nil)
  unless params[:include_all_certificates]
    if params[:force_for_new_certificates]
      UI.important("You specified 'force_for_new_certificates: true', but new certificates will not be added, cause 'include_all_certificates' is 'false'")
    end
    return false
  end

  force = false

  if params[:force_for_new_certificates] && !params[:readonly]
    if prov_type == :development && !params[:force]
      force = certificate_count_different?(profile: profile, keychain_path: keychain_path, platform: params[:platform].to_sym)
    else
      # All other (not development) provisioning profiles don't contain
      # multiple certificates, thus shouldn't be renewed
      # if the certificates  count has changed.
      UI.important("Warning: `force_for_new_certificates` is set but is ignored for non-'development' provisioning profiles.")
      UI.important("You can safely stop specifying `force_for_new_certificates` when running Match for '#{prov_type}' provisioning profiles.")
    end
  end

  return force
end
should_force_include_all_devices(params: nil, prov_type: nil, profile: nil, keychain_path: nil) click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File match/lib/match/runner.rb, line 321
def should_force_include_all_devices(params: nil, prov_type: nil, profile: nil, keychain_path: nil)
  return false unless params[:force_for_new_devices] && !params[:readonly]

  force = false

  prov_types_without_devices = [:appstore, :developer_id]
  if !prov_types_without_devices.include?(prov_type) && !params[:force]
    force = device_count_different?(profile: profile, keychain_path: keychain_path, platform: params[:platform].to_sym)
  else
    # App Store provisioning profiles don't contain device identifiers and
    # thus shouldn't be renewed if the device count has changed.
    UI.important("Warning: `force_for_new_devices` is set but is ignored for App Store & Developer ID provisioning profiles.")
    UI.important("You can safely stop specifying `force_for_new_devices` when running Match for type 'appstore' or 'developer_id'.")
  end

  return force
end
update_optional_values_depending_on_storage_type(params) click to toggle source

Be smart about optional values here Depending on the storage mode, different values are required

# File match/lib/match/runner.rb, line 154
def update_optional_values_depending_on_storage_type(params)
  if params[:storage_mode] != "git"
    params.option_for_key(:git_url).optional = true
  end
end