require 'pp' require 'fuzzy_match' require 'tmpdir' require 'tempfile' require 'fileutils' require 'set' require 'openssl' module Nixenvironment class Archiver ENTITLEMENTS_KEY = "Entitlements" APPLICATION_IDENTIFIER_KEY = "application-identifier" KEYCHAIN_ACCESS_GROUPS_KEY = "keychain-access-groups" PROFILE_TYPE_DEVELOPER = 'developer' PROFILE_TYPE_ADHOC = 'adhoc' PROFILE_TYPE_APPSTORE = 'appstore' AVAILABLE_PROFILE_TYPES = [PROFILE_TYPE_DEVELOPER, PROFILE_TYPE_ADHOC, PROFILE_TYPE_APPSTORE] class ProfileInfo attr_accessor :path, :name, :app_id, :device_count def initialize(path, name, app_id, device_count) @path = path @name = name @app_id = app_id @device_count = device_count end def to_s "<'path=#{@path}', name='#{@name}', app_id='#{@app_id}', device_count=#{@device_count}>" end end class << self def make_signed_ipa make_ipa(PROFILE_TYPE_DEVELOPER, nil, 'IPA_PRODUCT', 'IPA_BUNDLE_ID', 'NAME_FOR_DEPLOYMENT') end def make_resigned_ipa_for_device make_ipa(PROFILE_TYPE_DEVELOPER, '-Resigned', 'IPA_PRODUCT_RESIGNED_DEVICE', 'IPA_BUNDLE_ID_RESIGNED_DEVICE', 'NAME_FOR_DEPLOYMENT_RESIGNED_DEVICE') end def make_resigned_ipa_for_adhoc make_ipa(PROFILE_TYPE_ADHOC, '-Resigned-AdHoc', 'IPA_PRODUCT_RESIGNED_ADHOC', 'IPA_BUNDLE_ID_RESIGNED_ADHOC', 'NAME_FOR_DEPLOYMENT_RESIGNED_ADHOC') end def make_resigned_ipa_for_appstore make_ipa(PROFILE_TYPE_APPSTORE, '-Resigned-Appstore', 'IPA_PRODUCT_RESIGNED_APPSTORE', 'IPA_BUNDLE_ID_RESIGNED_APPSTORE', 'NAME_FOR_DEPLOYMENT_RESIGNED_APPSTORE') end def make_macos_zip build_env_vars = BuildEnvVarsLoader.load built_products_dir = build_env_vars[BUILT_PRODUCTS_DIR_KEY].presence executable_name = build_env_vars[EXECUTABLE_NAME_KEY].presence target_name = build_env_vars[TARGET_NAME_KEY].presence configuration = build_env_vars[CONFIGURATION_KEY].presence app_product = build_env_vars[APP_PRODUCT_KEY].presence new_ipa_name = "#{executable_name}-#{target_name}-#{configuration}" new_ipa_path = File.join(built_products_dir, new_ipa_name) + ZIP_EXT puts "IPA_PRODUCT = #{new_ipa_path}" Dir.mktmpdir do |tmp_dir| dest_app_dir = File.join(tmp_dir, new_ipa_name) dest_app_product = File.join(dest_app_dir, File.basename(app_product)) puts "--> Create '#{dest_app_dir}' ..." Dir.mkdir(dest_app_dir) puts "--> Copy '#{app_product}' into '#{dest_app_product}' ..." FileUtils.cp_r(app_product, dest_app_product) if Dir.exist?(new_ipa_path) puts "--> Remove old '#{new_ipa_path}' ..." FileUtils.rm_rf(new_ipa_path) end puts "--> Zip '#{tmp_dir}' into '#{new_ipa_path}' ..." Dir.chdir(tmp_dir) do zip_success = system("/usr/bin/zip --symlinks --verbose --recurse-paths \"#{new_ipa_path}\" .") raise unless zip_success end end ipa_bundle_id = get_bundle_id(File.join(app_product, 'Contents')) system("echo \"\n IPA_PRODUCT='#{new_ipa_path}' IPA_BUNDLE_ID='#{ipa_bundle_id}' NAME_FOR_DEPLOYMENT='#{configuration}' \" >> _last_build_vars.sh") end private def make_ipa(profile_type, ipa_product_suffix, ipa_product_key, ipa_bundle_id_key, name_for_deployment_key) is_resigned = ipa_product_suffix.present? && ipa_product_suffix.include?('Resigned') is_appstore = ipa_product_suffix.present? && ipa_product_suffix.include?('Appstore') build_env_vars = BuildEnvVarsLoader.load built_products_dir = build_env_vars[BUILT_PRODUCTS_DIR_KEY].presence executable_name = build_env_vars[EXECUTABLE_NAME_KEY].presence target_name = build_env_vars[TARGET_NAME_KEY].presence configuration = build_env_vars[CONFIGURATION_KEY].presence app_product = build_env_vars[APP_PRODUCT_KEY].presence watchkit_app_relative_product = build_env_vars['WATCHKIT_APP_RELATIVE_PRODUCT'].presence watchkit_extension_relative_product = build_env_vars['WATCHKIT_EXTENSION_RELATIVE_PRODUCT'].presence widget_relative_product = build_env_vars['WIDGET_RELATIVE_PRODUCT'].presence resigned_bundle_id = is_resigned ? build_env_vars[RESIGNED_BUNDLE_ID_KEY].presence : nil resigned_watchkit_app_bundle_id = is_resigned ? build_env_vars[RESIGNED_WATCHKIT_APP_BUNDLE_ID_KEY].presence : nil resigned_watchkit_extension_bundle_id = is_resigned ? build_env_vars[RESIGNED_WATCHKIT_EXTENSION_BUNDLE_ID_KEY].presence : nil resigned_widget_bundle_id = is_resigned ? build_env_vars[RESIGNED_WIDGET_BUNDLE_ID_KEY].presence : nil resigned_bundle_name = is_resigned ? build_env_vars[RESIGNED_BUNDLE_NAME_KEY].presence : nil resigned_entitlements_path = is_resigned ? build_env_vars[RESIGNED_ENTITLEMENTS_PATH_KEY].presence : nil resigned_watchkit_extension_entitlements_path = is_resigned ? build_env_vars[RESIGNED_WATCHKIT_EXTENSION_ENTITLEMENTS_PATH_KEY].presence : nil resigned_widget_entitlements_path = is_resigned ? build_env_vars[RESIGNED_WIDGET_ENTITLEMENTS_PATH_KEY].presence : nil ipa_product = "#{built_products_dir}/#{executable_name}-#{target_name}-#{configuration}#{ipa_product_suffix}.ipa" puts "#{ipa_product_key} = #{ipa_product}" if is_appstore app_name = File.basename(app_product) temp_app_product = File.join(Dir.tmpdir, app_name) FileUtils.cp_r(app_product, temp_app_product) set_plist_values_in_app_path(temp_app_product, 'Configuration' => 'Appstore') end @valid_keychain_identities_cache = get_valid_keychain_identities resign(app_product, ipa_product, watchkit_app_relative_product, watchkit_extension_relative_product, widget_relative_product, resigned_bundle_id, resigned_watchkit_app_bundle_id, resigned_watchkit_extension_bundle_id, resigned_widget_bundle_id, resigned_bundle_name, resigned_entitlements_path, resigned_watchkit_extension_entitlements_path, resigned_widget_entitlements_path, profile_type) ipa_bundle_id = is_resigned ? resigned_bundle_id : get_bundle_id(app_product) system("echo \"\n# generated by Nixenvironment: #{ipa_product_key}='#{ipa_product}' #{ipa_bundle_id_key}='#{ipa_bundle_id}' #{name_for_deployment_key}='#{configuration}' \" >> _last_build_vars.sh") FileUtils.rm_rf(temp_app_product) if is_appstore end # Resigns specified .app product and packages it into .ipa file. Finds best matching provisioning profile based on specified options. # app_product_path => full path to input .app folder # new_ipa_path => full path to output .ipa file # profile_type => type of profile to resign with AVAILABLE_PROFILE_TYPES def resign(app_product_path, new_ipa_path, watchkit_app_relative_product_path = nil, watchkit_extension_relative_product_path = nil, widget_relative_product_path = nil, new_bundle_id = nil, new_watchkit_app_bundle_id = nil, new_watchkit_extension_bundle_id = nil, new_widget_bundle_id = nil, new_bundle_name = nil, new_entitlements_path = nil, new_watchkit_extension_entitlements_path = nil, new_widget_entitlements_path = nil, profile_type = PROFILE_TYPE_DEVELOPER) raise "Unknown profile type '#{profile_type}'! Must be from #{AVAILABLE_PROFILE_TYPES}" unless AVAILABLE_PROFILE_TYPES.include?(profile_type) new_bundle_id ||= get_bundle_id(app_product_path) new_bundle_name ||= get_bundle_name(app_product_path) new_watchkit_app_bundle_id ||= get_bundle_id(File.join(app_product_path, watchkit_app_relative_product_path)) if watchkit_app_relative_product_path.present? new_watchkit_extension_bundle_id ||= get_bundle_id(File.join(app_product_path, watchkit_extension_relative_product_path)) if watchkit_extension_relative_product_path.present? new_widget_bundle_id ||= get_bundle_id(File.join(app_product_path, widget_relative_product_path)) if widget_relative_product_path.present? profile_info, identity_name = find_profile_info_and_identity_name(profile_type, new_bundle_id) puts "==> Sign with profile '".bold + profile_info.name.blink.underline + "', identity '" + identity_name.underline + "' ...".bold watchkit_app_profile_info = nil watchkit_app_identity_name = nil watchkit_extension_profile_info = nil watchkit_extension_identity_name = nil widget_profile_info = nil widget_identity_name = nil if watchkit_app_relative_product_path.present? watchkit_app_profile_info, watchkit_app_identity_name = find_profile_info_and_identity_name(profile_type, new_watchkit_app_bundle_id) puts "==> Sign watchkit_app with profile '".bold + watchkit_app_profile_info.name.blink.underline + "', identity '".bold + watchkit_app_identity_name.underline + "' ...".bold end if watchkit_extension_relative_product_path.present? watchkit_extension_profile_info, watchkit_extension_identity_name = find_profile_info_and_identity_name(profile_type, new_watchkit_extension_bundle_id) puts "==> Sign watchkit_extension with profile '".bold + watchkit_extension_profile_info.name.blink.underline + "', identity '".bold + watchkit_extension_identity_name.underline + "' ...".bold end if widget_relative_product_path.present? widget_profile_info, widget_identity_name = find_profile_info_and_identity_name(profile_type, new_widget_bundle_id) puts "==> Sign widget with profile '".bold + widget_profile_info.name.blink.underline + "', identity '".bold + widget_identity_name.underline + "' ...".bold end watchkit_app_profile_path = watchkit_app_profile_info ? watchkit_app_profile_info.path : nil watchkit_extension_profile_path = watchkit_extension_profile_info ? watchkit_extension_profile_info.path : nil widget_profile_path = widget_profile_info ? widget_profile_info.path : nil package_application(app_product_path, watchkit_app_relative_product_path, watchkit_extension_relative_product_path, widget_relative_product_path, new_ipa_path, new_bundle_id, new_watchkit_app_bundle_id, new_watchkit_extension_bundle_id, new_widget_bundle_id, new_bundle_name, new_entitlements_path, new_watchkit_extension_entitlements_path, new_widget_entitlements_path, profile_info.path, identity_name, watchkit_app_profile_path, watchkit_app_identity_name, watchkit_extension_profile_path, watchkit_extension_identity_name, widget_profile_path, widget_identity_name) end def find_profile_info_and_identity_name(profile_type, new_bundle_id) profiles_and_identities = case profile_type when PROFILE_TYPE_DEVELOPER then get_matching_developer_profiles_and_identities(new_bundle_id) when PROFILE_TYPE_ADHOC then get_matching_adhoc_profiles_and_identities(new_bundle_id) when PROFILE_TYPE_APPSTORE then get_matching_appstore_profiles_and_identities(new_bundle_id) else raise "Unknown profile_type '#{profile_type}'!" end raise 'No mathching profiles found, read logs for more info!' if profiles_and_identities.blank? puts_header '--> Matching profiles and identities:' profiles_and_identities.each { |item| pp item.first.to_s, item.last } puts "--> Looking for the best match among found profiles, based on similarity between profile's app id and desired bundle id ..." best_match = find_best_match_for_bundle_id(profiles_and_identities, new_bundle_id) puts_header "--> Best match: #{best_match.to_s}" profile_info = best_match.first identity_name = best_match.last return profile_info, identity_name end def find_best_match_for_bundle_id(profiles_and_identities, new_bundle_id) profiles_and_identities.max_by { |item| app_id_similarity_to_new_bundle_id(item, new_bundle_id) } end def app_id_similarity_to_new_bundle_id(match, new_bundle_id) app_id = match.first.app_id winner, dices_coefficient_similar, _levenshtein_similar = FuzzyMatch.new([new_bundle_id]).find_with_score(app_id) dices_coefficient_similar = 0 if winner.blank? puts "\tsimilarity ratio: '#{new_bundle_id}' <--> '#{app_id}' = #{dices_coefficient_similar}" dices_coefficient_similar end # Re-signs and packages specified app product. # Does NOT check that new bundle id corresponds to profile. # Automatically constructs new entitlements in case they are not specified. # new_entitlements_path, new_watchkit_extension_entitlements_path and new_widget_entitlements_path can be set to None or ''. # In this case the entitlements will be taken from the exisitng product or generated automatically. # If re-signing with a distribution profile and get-task-allow is set to true, the AppStore will reject the submission. # So the function automatically fixes the value of this entitlements field. def package_application(app_product_path, watchkit_app_relative_product_path, watchkit_extension_relative_product_path, widget_relative_product_path, new_ipa_path, new_bundle_id, new_watchkit_app_bundle_id, new_watchkit_extension_bundle_id, new_widget_bundle_id, new_bundle_name, new_entitlements_path, new_watchkit_extension_entitlements_path, new_widget_entitlements_path, profile_path, identity_name, watchkit_app_profile_path, watchkit_app_identity_name, watchkit_extension_profile_path, watchkit_extension_identity_name, widget_profile_path, widget_identity_name) Dir.mktmpdir do |tmp_dir| dest_app_dir = File.join(tmp_dir, "Payload") dest_app_product_path = File.join(dest_app_dir, File.basename(app_product_path)) puts "--> Create '#{dest_app_dir}' ..." Dir.mkdir(dest_app_dir) puts "--> Copy '#{app_product_path}' into '#{dest_app_product_path}' ..." FileUtils.cp_r(app_product_path, dest_app_product_path) # replace provision, rename bundle_id and bundle_name is_provision_replaced = replace_provision(dest_app_product_path, profile_path) is_bundle_id_or_name_changed = rename_bundle_id_and_name(dest_app_product_path, new_bundle_id, new_bundle_name) # replace provision, rename bundle_id for watchkit app if watchkit_app_relative_product_path.present? dest_watchkit_app_product_path = File.join(dest_app_product_path, watchkit_app_relative_product_path) _is_watchkit_app_provision_replaced = replace_provision(dest_watchkit_app_product_path, watchkit_app_profile_path) _is_watchkit_app_bundle_id = rename_bundle_id(dest_watchkit_app_product_path, new_watchkit_app_bundle_id) end is_watchkit_extension_provision_replaced = nil is_watchkit_extension_bundle_id_or_name_changed = nil dest_watchkit_extension_product_path = nil # replace provision, rename bundle_id and bundle_name for watchkit extension if watchkit_extension_relative_product_path.present? dest_watchkit_extension_product_path = File.join(dest_app_product_path, watchkit_extension_relative_product_path) is_watchkit_extension_provision_replaced = replace_provision(dest_watchkit_extension_product_path, watchkit_extension_profile_path) is_watchkit_extension_bundle_id_or_name_changed = rename_bundle_id_and_name(dest_watchkit_extension_product_path, new_watchkit_extension_bundle_id, nil) end is_widget_provision_replaced = nil is_widget_bundle_id_or_name_changed = nil dest_widget_product_path = nil # replace provision, rename bundle_id and bundle_name for widget if widget_relative_product_path.present? dest_widget_product_path = File.join(dest_app_product_path, widget_relative_product_path) is_widget_provision_replaced = replace_provision(dest_widget_product_path, widget_profile_path) is_widget_bundle_id_or_name_changed = rename_bundle_id_and_name(dest_widget_product_path, new_widget_bundle_id, nil) end # codesign watchkit extension if watchkit_extension_relative_product_path.present? codesign(watchkit_extension_identity_name, new_watchkit_extension_entitlements_path, is_watchkit_extension_provision_replaced, is_watchkit_extension_bundle_id_or_name_changed, dest_watchkit_extension_product_path, watchkit_extension_profile_path, new_watchkit_extension_bundle_id) end # codesign widget if widget_relative_product_path.present? codesign(widget_identity_name, new_widget_entitlements_path, is_widget_provision_replaced, is_widget_bundle_id_or_name_changed, dest_widget_product_path, widget_profile_path, new_widget_bundle_id) end codesign(identity_name, new_entitlements_path, is_provision_replaced, is_bundle_id_or_name_changed, dest_app_product_path, profile_path, new_bundle_id) if Dir.exist?(new_ipa_path) puts "--> Remove old '#{new_ipa_path}' ..." FileUtils.rm(new_ipa_path) end puts "--> Zip '#{tmp_dir}' into '#{new_ipa_path}' ..." Dir.chdir(tmp_dir) { system("/usr/bin/zip --symlinks --verbose --recurse-paths '#{new_ipa_path}' .") } end end def codesign(identity_name, new_entitlements_path, is_provision_replaced, is_bundle_id_or_name_changed, dest_app_product_path, profile_path, new_bundle_id) codesign_args = ['/usr/bin/codesign', '--force', '--sign', "'#{identity_name}'"] # now let's figure out the entitlements... if new_entitlements_path.present? # a) use explicitly set entitlements puts_header "--> Using explicitly set entitlements from '#{new_entitlements_path}'" # make a copy to not mess up the original file selected_entitlements_path = Tempfile.new('selected_entitlements').path FileUtils.cp(new_entitlements_path, selected_entitlements_path) else should_generate_entitlements_manually = is_provision_replaced || is_bundle_id_or_name_changed unless should_generate_entitlements_manually # b) existing entitlements are OK entitlements_file = get_entitlements_from_app(dest_app_product_path) selected_entitlements_path = entitlements_file.path # leave only plist data in file plist = get_plist_from_file(selected_entitlements_path) plist.save(selected_entitlements_path) puts_header '--> Using existing entitlements' else # c) no entitlements is bad, so we will construct them manually selected_entitlements_path = generate_temp_entitlements_file_from_profile(profile_path, new_bundle_id) puts_header "--> Using automatically generated entitlements from '#{selected_entitlements_path}'" end end # crucial for submission fix_get_task_allow(selected_entitlements_path, identity_name) puts_warning '--> Entitlements:' puts File.read(selected_entitlements_path) codesign_args.concat(['--entitlements', "'#{selected_entitlements_path}'", "'#{dest_app_product_path}'"]) begin puts_header "--> Codesign with params #{codesign_args} ..." raise unless system(codesign_args.join(' ')) ensure puts "--> Remove temp entitlements file '#{selected_entitlements_path}'" FileUtils.rm(selected_entitlements_path) end check_signature(dest_app_product_path) end def check_signature(app_product_path) puts '--> Check signature ...' raise unless system("/usr/bin/codesign --verify --no-strict -vvvv '#{app_product_path}'") end def replace_provision(app_product_path, provision_path) embedded_provision_path = File.join(app_product_path, 'embedded.mobileprovision') is_provision_the_same = FileUtils.cmp(provision_path, embedded_provision_path) unless is_provision_the_same puts "--> Embed profile '#{provision_path}' ..." FileUtils.cp(provision_path, embedded_provision_path) end !is_provision_the_same end def rename_bundle_id_and_name(app_product_path, new_bundle_id, new_bundle_name) rename_bundle_id(app_product_path, new_bundle_id) && rename_bundle_name(app_product_path, new_bundle_name) end def rename_bundle_id(app_product_path, new_bundle_id) original_bundle_id = get_bundle_id(app_product_path) is_bundle_id_the_same = false if new_bundle_id == original_bundle_id puts_header "--> Bundle id '#{new_bundle_id}' will not be modified" is_bundle_id_the_same = true else puts_header "--> Rename bundle id from '#{original_bundle_id}' into '#{new_bundle_id} in '#{app_product_path}' Info.plist ..." set_plist_values_in_app_path(app_product_path, 'CFBundleIdentifier' => new_bundle_id) end is_bundle_id_the_same end def rename_bundle_name(app_product_path, new_bundle_name) original_bundle_name = get_bundle_name(app_product_path) original_bundle_display_name = get_bundle_display_name(app_product_path) is_bundle_name_the_same = false if new_bundle_name == original_bundle_name && new_bundle_name == original_bundle_display_name puts_header "--> Bundle name '#{new_bundle_name}' will not be modified" is_bundle_name_the_same = true else puts_header "--> Rename bundle name/bundle display name from '#{original_bundle_name}'/'#{original_bundle_display_name}' into '#{new_bundle_name}' in '#{app_product_path}' Info.plist ..." set_plist_values_in_app_path(app_product_path, { 'CFBundleName' => new_bundle_name, 'CFBundleDisplayName' => new_bundle_name } ) end is_bundle_name_the_same end def get_bundle_id(app_product_path) plist_value_from_app_path(app_product_path, 'CFBundleIdentifier') end def get_bundle_name(app_product_path) plist_value_from_app_path(app_product_path, 'CFBundleName') end def get_bundle_display_name(app_product_path) plist_value_from_app_path(app_product_path, 'CFBundleDisplayName') end def plist_value_from_app_path(app_product_path, key) info_plist_path = File.join(app_product_path, 'Info.plist') info_plist = Plist.from_file(info_plist_path) info_plist[key] end def set_plist_values_in_app_path(app_product_path, hash) info_plist_path = File.join(app_product_path, 'Info.plist') info_plist = Plist.from_file(info_plist_path) hash.each { |key, value| info_plist[key] = value } info_plist.save end # Returns None if entitlements cannot be read (e.g. when signature is modified), otherwise - temp file with entitlements. # User is responsible for removing temp file. def get_entitlements_from_app(app_product_path) entitlements_file = Tempfile.new('entitlements_file') puts "--> Copy entitlements from '#{app_product_path}' into '#{entitlements_file.path}' ..." codesign_success = system("codesign -d '#{app_product_path}' --entitlements '#{entitlements_file.path}'") unless codesign_success entitlements_file.close entitlements_file.unlink end entitlements_file end def app_id_prefix_from_profile(profile_path) _profile_name, app_id, _certs, _device_count = parse_profile(profile_path) app_id.partition('.').first end def identity_is_for_development(identity_name) identity_name.start_with?('iPhone Developer') end def identity_is_for_distribution(identity_name) identity_name.start_with?('iPhone Distribution') end # User is responsible for removing temp file. Always sets the same get-task-allow. def generate_temp_entitlements_file_from_profile(profile_path, bundle_id) generated_entitlements_path = Tempfile.new('generated_entitlements').path puts_header "--> Automatically generate new entitlements at '#{generated_entitlements_path}' ..." app_id_prefix = app_id_prefix_from_profile(profile_path) application_identifier = app_id_prefix + "." + bundle_id plist = get_plist_from_file(profile_path) profile_entitlements = plist[ENTITLEMENTS_KEY] profile_entitlements[APPLICATION_IDENTIFIER_KEY] = application_identifier profile_entitlements[KEYCHAIN_ACCESS_GROUPS_KEY][0] = application_identifier entitlements = Plist.from_hash(profile_entitlements) entitlements.save(generated_entitlements_path) puts '--> Lint new entitlements ...' raise unless system("/usr/bin/plutil -lint '#{generated_entitlements_path}'") generated_entitlements_path end def fix_get_task_allow(entitlements_path, identity_name) puts '--> Fix get-task-allow if needed ...' plist = Plist.from_file(entitlements_path) # this entitlements field must be set to False for distribution new_value = !identity_is_for_distribution(identity_name) plist['get-task-allow'] = new_value plist.save puts "--> get-task-allow = #{new_value}" end # Use it to match only developer profiles. def get_matching_developer_profiles_and_identities(new_bundle_id) matches = [] get_matching_profiles_and_identities(new_bundle_id) do |match| identity_name = match[1] if identity_is_for_development(identity_name) matches << match else puts_warning "\t--> Skipping, because looking only for developer profiles" end end matches end # Use it to match only Appstore distribution profiles. def get_matching_appstore_profiles_and_identities(new_bundle_id) matches = [] get_matching_profiles_and_identities(new_bundle_id) do |match| device_count = match.first.device_count identity_name = match.last if !identity_is_for_distribution(identity_name) puts_warning "\t--> Skipping, because looking only for Appstore distribution profiles" elsif device_count > 0 puts_warning "\t--> Skipping, because profile contains devices" else matches << match end end matches end # Use it to match only AdHoc distribution profiles. def get_matching_adhoc_profiles_and_identities(new_bundle_id) matches = [] get_matching_profiles_and_identities(new_bundle_id) do |match| device_count = match.first.device_count identity_name = match.last if !identity_is_for_distribution(identity_name) puts_warning "\t--> Skipping, because looking only for AdHoc distribution profiles" elsif device_count == 0 puts_warning "\t--> Skipping, because profile does not contain devices" else matches << match end end matches end # Generator. Finds matching profile and identity based on specified bundle id. Matches only valid identities. def get_matching_profiles_and_identities(new_bundle_id) profiles_path = File.expand_path('~/Library/MobileDevice/Provisioning Profiles/') puts_bold "--> Scan profiles at '#{profiles_path}' ..." Dir.foreach(profiles_path) do |filename| next if file_is_not_profile(filename) print_underline "Profile '#{filename}', " profile_path = File.join(profiles_path, filename) profile_name, app_id, certs, device_count = parse_profile(profile_path) puts "name = '#{profile_name}', app id='#{app_id}', #{device_count} device(s)" unless bundle_id_corresponds_to_app_id(new_bundle_id, app_id) puts_warning "\t--> Can't use this profile, because app id '#{app_id}' doesn't allow new bundle id '#{new_bundle_id}'" next end puts_header "\t--> Profile's app id '#{app_id}' allows new bundle id '#{new_bundle_id}'" puts_bold "\t--> Scan certs ..." get_matching_identities_from_certs(certs) do |identity_name| profile_info = ProfileInfo.new(profile_path, profile_name, app_id, device_count) match = [profile_info, identity_name] puts_header "\t--> Match found: #{profile_info.to_s}, identity_name='#{identity_name}'" yield match end end end def file_is_not_profile(filename) File.extname(filename) != '.mobileprovision' end def parse_profile(profile_path) plist = get_plist_from_file(profile_path) profile_name = plist['Name'] app_id = plist[ENTITLEMENTS_KEY][APPLICATION_IDENTIFIER_KEY] certs = plist['DeveloperCertificates'] devices = plist['ProvisionedDevices'] device_count = devices.present? ? devices.size : 0 return profile_name, app_id, certs, device_count end def get_plist_from_file(path) content = File.read(path) plist_start_index = content.index('') + ''.length profile_plist_str = content[plist_start_index...plist_end_index] Plist.from_str(profile_plist_str) end def bundle_id_corresponds_to_app_id(bundle_id, app_id) app_id_without_seed_id = app_id[app_id.index('.') + 1..-1] app_id_without_seed_id_regexp = app_id_without_seed_id.gsub('.', '\.').gsub('*', '.*') bundle_id =~ /#{app_id_without_seed_id_regexp}/ end # Generator. Returns list of valid identities based on the list of embedded certs from the profile file. def get_matching_identities_from_certs(certs) certs.each do |cert| key = OpenSSL::X509::Certificate.new(cert) # key.subject.to_a => [["UID", ..., ...], ["CN", identity_name, ...], ...] identity_name = key.subject.to_a[1][1] print "\tIdentity '#{identity_name}'..." unless identity_is_valid(identity_name) puts_warning 'invalid' next end puts_header 'valid!' yield identity_name end end def identity_is_valid(identity_name) @valid_keychain_identities_cache.include?(identity_name) end def get_valid_keychain_identities() keychain_name = 'XCodeKeys' keychain_path = %x[ security list | grep '#{keychain_name}' | sed 's/\"//g' | xargs ].strip security_report = %x[ '#{IDENTITIESLIST_UTILITY_PATH}' -k '#{keychain_path}' ] entries = security_report.lines.map(&:strip).reject(&:empty?) entries.to_set end def puts_bold(msg) puts msg.bold end def print_underline(msg) print msg.underline end def puts_header(msg) puts_bold msg.blue end def puts_warning(msg) puts_bold msg.yellow end end end end