Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[match] add caching layer to significantly improve performance by up to 100x #21694

Merged
merged 16 commits into from Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions match/lib/match.rb
Expand Up @@ -12,3 +12,6 @@
require_relative 'match/storage'
require_relative 'match/encryption'
require_relative 'match/module'
require_relative 'match/portal_cache'
require_relative 'match/portal_fetcher'
require_relative 'match/profile_includes'
10 changes: 9 additions & 1 deletion match/lib/match/generator.rb
@@ -1,4 +1,5 @@
require_relative 'module'
require_relative 'profile_includes'

module Match
# Generate missing resources
Expand Down Expand Up @@ -57,7 +58,7 @@ def self.generate_certificate(params, cert_type, working_directory, specific_cer
end

# @return (String) The UUID of the newly generated profile
def self.generate_provisioning_profile(params: nil, prov_type: nil, certificate_id: nil, app_identifier: nil, force: true, working_directory: nil)
def self.generate_provisioning_profile(params: nil, prov_type: nil, certificate_id: nil, app_identifier: nil, force: true, cache: nil, working_directory: nil)
require 'sigh/manager'
require 'sigh/options'

Expand Down Expand Up @@ -104,6 +105,13 @@ def self.generate_provisioning_profile(params: nil, prov_type: nil, certificate_
values[:development] = true
end

if cache
values[:cached_certificates] = cache.certificates
values[:cached_devices] = cache.devices
values[:cached_bundle_ids] = cache.bundle_ids
values[:cached_profiles] = cache.profiles
end

arguments = FastlaneCore::Configuration.create(Sigh::Options.available_options, values)

Sigh.config = arguments
Expand Down
3 changes: 2 additions & 1 deletion match/lib/match/module.rb
Expand Up @@ -23,7 +23,8 @@ def self.profile_type_sym(type)
end

def self.cert_type_sym(type)
type = type.to_s
# To determine certificate types to fetch from the portal, we use `Sigh.certificate_types_for_profile_and_platform`, and it returns typed `Spaceship::ConnectAPI::Certificate::CertificateType` with the same values but uppercased, so we downcase them here
type = type.to_s.downcase
nekrich marked this conversation as resolved.
Show resolved Hide resolved
return :mac_installer_distribution if type == "mac_installer_distribution"
return :developer_id_installer if type == "developer_id_installer"
return :developer_id_application if type == "developer_id"
Expand Down
106 changes: 106 additions & 0 deletions match/lib/match/portal_cache.rb
@@ -0,0 +1,106 @@
require 'fastlane_core/provisioning_profile'
require 'spaceship/client'
require_relative 'portal_fetcher'
module Match
class Portal
class Cache
def self.build(params:, bundle_id_identifiers:)
require_relative 'profile_includes'
require 'sigh'

profile_type = Sigh.profile_type_for_distribution_type(
platform: params[:platform],
distribution_type: params[:type]
)

cache = Portal::Cache.new(
platform: params[:platform],
profile_type: profile_type,
additional_cert_types: params[:additional_cert_types],
bundle_id_identifiers: bundle_id_identifiers,
needs_profiles_devices: ProfileIncludes.can_force_include?(params: params, notify: true) && !params[:force] && !params[:readonly],
needs_profiles_certificate_content: !ProfileIncludes.can_force_include_all_certificates?(params: params),
include_mac_in_profiles: params[:include_mac_in_profiles]
)

return cache
end

attr_reader :platform, :profile_type, :bundle_id_identifiers, :additional_cert_types, :needs_profiles_devices, :needs_profiles_certificate_content, :include_mac_in_profiles

def initialize(platform:, profile_type:, additional_cert_types:, bundle_id_identifiers:, needs_profiles_devices:, needs_profiles_certificate_content:, include_mac_in_profiles:)
@platform = platform
@profile_type = profile_type

# Bundle Ids
@bundle_id_identifiers = bundle_id_identifiers

# Certs
@additional_cert_types = additional_cert_types

# Profiles
@needs_profiles_devices = needs_profiles_devices
@needs_profiles_certificate_content = needs_profiles_certificate_content

# Devices
@include_mac_in_profiles = include_mac_in_profiles
end

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

portal_profile = self.profiles.detect { |i| i.uuid == uuid }

portal_profile
end

def reset_certificates
@certificates = nil
end

def forget_portal_profile(portal_profile)
return unless @profiles && portal_profile

@profiles -= [portal_profile]
end

def bundle_ids
@bundle_ids ||= Match::Portal::Fetcher.bundle_ids(
bundle_id_identifiers: @bundle_id_identifiers
)

return @bundle_ids.dup
end

def certificates
@certificates ||= Match::Portal::Fetcher.certificates(
platform: @platform,
profile_type: @profile_type,
additional_cert_types: @additional_cert_types
)

return @certificates.dup
end

def profiles
@profiles ||= Match::Portal::Fetcher.profiles(
profile_type: @profile_type,
needs_profiles_devices: @needs_profiles_devices,
needs_profiles_certificate_content: @needs_profiles_certificate_content
)

return @profiles.dup
end

def devices
@devices ||= Match::Portal::Fetcher.devices(
platform: @platform,
include_mac_in_profiles: @include_mac_in_profiles
)

return @devices.dup
end
end
end
end
72 changes: 72 additions & 0 deletions match/lib/match/portal_fetcher.rb
@@ -0,0 +1,72 @@
require 'fastlane_core/provisioning_profile'
require 'spaceship/client'
require 'spaceship/connect_api/models/profile'

module Match
class Portal
module Fetcher
def self.profiles(profile_type:, needs_profiles_devices: false, needs_profiles_certificate_content: false, name: nil)
includes = ['bundleId']

if needs_profiles_devices
includes += ['devices', 'certificates']
end

if needs_profiles_certificate_content
includes += ['certificates']
end

profiles = Spaceship::ConnectAPI::Profile.all(
filter: { profileType: profile_type, name: name }.compact,
includes: includes.uniq.join(',')
)

profiles
end

def self.certificates(platform:, profile_type:, additional_cert_types:)
require 'sigh'
certificate_types = Sigh.certificate_types_for_profile_and_platform(platform: platform, profile_type: profile_type)

additional_cert_types ||= []
additional_cert_types.map! do |cert_type|
case Match.cert_type_sym(cert_type)
when :mac_installer_distribution
Spaceship::ConnectAPI::Certificate::CertificateType::MAC_INSTALLER_DISTRIBUTION
when :developer_id_installer
Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPER_ID_INSTALLER
end
end

certificate_types += additional_cert_types

filter = { certificateType: certificate_types.uniq.sort.join(',') } unless certificate_types.empty?

certificates = Spaceship::ConnectAPI::Certificate.all(
filter: filter
).select(&:valid?)

certificates
end

def self.devices(platform: nil, include_mac_in_profiles: false)
devices = Spaceship::ConnectAPI::Device.devices_for_platform(
platform: platform,
include_mac_in_profiles: include_mac_in_profiles
)

devices
end

def self.bundle_ids(bundle_id_identifiers: nil)
filter = { identifier: bundle_id_identifiers.join(',') } if bundle_id_identifiers

bundle_ids = Spaceship::ConnectAPI::BundleId.all(
filter: filter
)

bundle_ids
end
end
end
end
120 changes: 120 additions & 0 deletions match/lib/match/profile_includes.rb
@@ -0,0 +1,120 @@
require_relative 'portal_fetcher'
require_relative 'module'

module Match
class ProfileIncludes
PROV_TYPES_WITH_DEVICES = [:adhoc, :development]
PROV_TYPES_WITH_MULTIPLE_CERTIFICATES = [:development]

def self.can_force_include?(params:, notify:)
self.can_force_include_all_devices?(params: params, notify: notify) &&
self.can_force_include_all_certificates?(params: params, notify: notify)
end

###############
#
# DEVICES
#
###############

def self.should_force_include_all_devices?(params:, portal_profile:, cached_devices:)
return false unless self.can_force_include_all_devices?(params: params)

force = devices_differ?(portal_profile: portal_profile, platform: params[:platform], include_mac_in_profiles: params[:include_mac_in_profiles], cached_devices: cached_devices)

return force
end

def self.can_force_include_all_devices?(params:, notify: false)
return false if params[:readonly] || params[:force]
return false unless params[:force_for_new_devices]

provisioning_type = params[:type].to_sym

can_force = PROV_TYPES_WITH_DEVICES.include?(provisioning_type)

if !can_force && notify
# 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 #{provisioning_type}.")
UI.important("You can safely stop specifying `force_for_new_devices` when running Match for type '#{provisioning_type}'.")
end

can_force
end

def self.devices_differ?(portal_profile:, platform:, include_mac_in_profiles:, cached_devices:)
return false unless portal_profile

profile_devices = portal_profile.devices

portal_devices = cached_devices
portal_devices ||= Match::Portal::Fetcher.devices(platform: platform, include_mac_in_profiles: include_mac_in_profiles)

profile_device_ids = profile_devices.map(&:id).sort
portal_devices_ids = portal_devices.map(&:id).sort

devices_differs = profile_device_ids != portal_devices_ids

UI.important("Devices in the profile and available on the portal differ. Recreating a profile") if devices_differs

return devices_differs
end

###############
#
# CERTIFICATES
#
###############

def self.should_force_include_all_certificates?(params:, portal_profile:, cached_certificates:)
return false unless self.can_force_include_all_certificates?(params: params)

force = certificates_differ?(portal_profile: portal_profile, platform: params[:platform], cached_certificates: cached_certificates)

return force
end

def self.can_force_include_all_certificates?(params:, notify: false)
return false if params[:readonly] || params[:force]
return false unless params[:force_for_new_certificates]

unless params[:include_all_certificates]
UI.important("You specified 'force_for_new_certificates: true', but new certificates will not be added, cause 'include_all_certificates' is 'false'") if notify
return false
end

provisioning_type = params[:type].to_sym

can_force = PROV_TYPES_WITH_MULTIPLE_CERTIFICATES.include?(provisioning_type)

if !can_force && notify
# 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 '#{provisioning_type}' provisioning profiles.")
end

can_force
end

def self.certificates_differ?(portal_profile:, platform:, cached_certificates:)
return false unless portal_profile

profile_certs = portal_profile.certificates

portal_certs = cached_certificates
portal_certs ||= Match::Portal::Fetcher.certificates(platform: platform, profile_type: portal_profile.profile_type)

profile_certs_ids = profile_certs.map(&:id).sort
portal_certs_ids = portal_certs.map(&:id).sort

certificates_differ = profile_certs_ids != portal_certs_ids

UI.important("Certificates in the profile and available on the portal differ. Recreating a profile") if certificates_differ

return certificates_differ
end
end
end