$appName = File.basename(Dir['../*.xcworkspace'].first, '.*') require_relative 'fastlane/touchlane/lib/touchlane' # ugly hack to add support for custom storage Match.module_eval do def self.storage_modes return %w(git google_cloud s3 local) end end private_lane :installDependencies do |options| podsReposPath = File.expand_path "~/.cocoapods/repos/master/" lockFilePath = "#{podsReposPath}/.git/index.lock" # check if .lock file exists in pod repos - then remove all master repo if File.exists? lockFilePath sh("rm -rf #{podsReposPath}") end cocoapods( repo_update: true ) if File.exists? "../Cartfile" use_rome = File.exists? "../Romefile" swift_version = sh("xcrun swift --version | head -1 | sed 's/.*\\(\(.*\)\\).*/\\1/' | tr -d \"()\" | tr \" \" \"-\"").chop rome_path = "Pods/Rome/rome" rome_options = "--platform iOS --cache-prefix #{swift_version} --romefile Romefile" carthage_install = lambda do if use_rome sh("cd .. && #{rome_path} download #{rome_options}") end carthage(command: "bootstrap", platform: "iOS", cache_builds: true) if use_rome sh("cd .. && #{rome_path} list --missing #{rome_options} | awk '{print $1}' | xargs -I framework_name #{rome_path} upload framework_name #{rome_options}") end end begin carthage_install.call rescue # workaround for https://github.com/Carthage/Carthage/issues/2298 sh("rm -rf ~/Library/Caches/org.carthage.CarthageKit") carthage_install.call end end end private_lane :uploadToFirebase do |options| releaseNotesFile = "release-notes.txt" sh("touch ../#{releaseNotesFile}") sh("yarn install") app_target_folder_name = options[:appName] || $appName configuration_type = Touchlane::ConfigurationType.from_type(options[:type]) gsp_plist_path = get_google_services_plist_path(app_target_folder_name, configuration_type) google_app_id = get_info_plist_value(path: gsp_plist_path, key: "GOOGLE_APP_ID") firebase_app_distribution( app: google_app_id, ipa_path: options[:ipa_path], groups: "touch-instinct", release_notes_file: releaseNotesFile, firebase_cli_path: File.expand_path("../node_modules/firebase-tools/lib/bin/firebase.js") ) upload_symbols_to_crashlytics( gsp_path: get_google_services_plist_path(app_target_folder_name, configuration_type) ) end private_lane :uploadToAppStore do |options| upload_to_app_store( username: options[:username] || options[:apple_id], ipa: options[:ipa_path], force: true, # skip metainfo prompt skip_metadata: true, team_id: options[:itc_team_id], dev_portal_team_id: options[:team_id] ) end private_lane :addShield do |options| buildNumber = options[:buildNumber] buildDescription = options[:lane_name] # EnterpriseCustomerDev1WithoutSSLPinningRelease .split(/(?=[A-Z])/) # -> ["Enterprise", "Customer", "Dev1", "Without", "S", "S", "L", "Pinning", "Release"] .map { |v| v.gsub(/[[:lower:]]+/, "") }[1..2] # -> ["E", "C", "D1", "W", "S", "S", "L", "P", "R"] -> ["C", "D1"] .join # -> "CD1" begin add_badge( shield: "#{buildDescription}-#{buildNumber}-green", no_badge: true ) rescue => error UI.error(error) end end private_lane :buildConfiguration do |options| appName = options[:appName] || $appName lane_name = options[:lane_name] || lane_context[SharedValues::LANE_NAME] options[:scheme] = options[:scheme] || appName options[:lane_name] = lane_name configuration_type = Touchlane::ConfigurationType.from_lane_name(lane_name) options = fill_up_options_using_configuration_type(options, configuration_type) openKeychain(options) if !options[:buildNumber].nil? increment_build_number( build_number: options[:buildNumber] ) end ipa_name = "#{appName}.ipa" options[:output_name] = ipa_name options[:ipa_path] = "./#{ipa_name}" options[:dsym_path] = "./#{appName}.app.dSYM.zip" options[:xcodeproj_path] = "../#{appName}.xcodeproj" options[:workspace] = "./#{appName}.xcworkspace" installDependencies(options) if !(options[:uploadToFabric] || options[:uploadToAppStore]) options[:skip_package_ipa] = true sync_code_signing_using_options(options) buildArchive(options) # check build failures and static analysis end if options[:uploadToFabric] sync_code_signing_using_options(options) addShield(options) buildArchive(options) uploadToFirebase(options) end if options[:uploadToAppStore] options[:compileBitcode] = options[:compileBitcode].nil? ? true : options[:compileBitcode] options[:include_symbols] = options[:include_symbols].nil? ? true : options[:include_symbols] sync_code_signing_using_options(options) buildArchive(options) uploadToAppStore(options) end end private_lane :buildArchive do |options| icloudEnvironment = options[:iCloudContainerEnvironment] || "" exportOptions = icloudEnvironment.to_s.empty? ? {} : {iCloudContainerEnvironment: icloudEnvironment} exportOptions[:compileBitcode] = options[:compileBitcode] || false lane_name = options[:lane_name] configuration = options[:configuration] xcodeproj_path = options[:xcodeproj_path] if configuration != "AppStore" # AppStore uses xcconfig choosen in Xcode set_xcconfig_for_configuration_of_project(lane_name, configuration, xcodeproj_path) end gym( clean: true, workspace: options[:workspace], scheme: options[:scheme], archive_path: "./", output_directory: "./", output_name: options[:output_name], configuration: configuration, export_method: options[:export_method], export_options: exportOptions, skip_package_ipa: options[:skip_package_ipa], include_symbols: options[:include_symbols] || false, include_bitcode: options[:compileBitcode] || false, ) end lane :CreatePushCertificate do |options| configuration = get_configuration_for_type(options[:type] || "development") options = configuration.to_options.merge(options) certificates_path = File.expand_path "../Certificates" Dir.mkdir(certificates_path) unless File.directory?(certificates_path) app_identifier = options[:app_identifier] get_push_certificate( development: options[:development].nil? ? true : options[:development], generate_p12: true, active_days_limit: 30, # create new certificate if old one will expire in 30 days save_private_key: false, app_identifier: (app_identifier.is_a? Array) ? app_identifier.first : app_identifier, username: options[:username] || options[:apple_id], team_id: options[:team_id], p12_password: "123", # empty password won't work with Pusher output_path: certificates_path ) end lane :SyncCodeSigning do |options| configuration_type = Touchlane::ConfigurationType.from_type(options[:type]) options = fill_up_options_using_configuration_type(options, configuration_type) sync_code_signing_using_options(options) end lane :SyncSymbols do |options| configuration = get_configuration_for_type(options[:type]) options = configuration.to_options.merge(options) appName = options[:appName] || $appName xcodeproj_path = File.expand_path "../#{appName}.xcodeproj" version_number = options[:version] || get_version_number(xcodeproj: xcodeproj_path, target: appName) build_number = options[:build_number] || get_build_number(xcodeproj: xcodeproj_path) if configuration.type.is_app_store download_dsyms( username: options[:username], app_identifier: options[:app_identifier].first, team_id: options[:itc_team_id], version: version_number, build_number: build_number ) end app_target_folder_name = appName upload_symbols_to_crashlytics( gsp_path: get_google_services_plist_path(app_target_folder_name, configuration.type) ) clean_build_artifacts end private_lane :openKeychain do |options| if is_ci? # workaround to avoid duplication problem # https://apple.stackexchange.com/questions/350633/multiple-duplicate-keychain-dbs-that-dont-get-cleaned-up keychain_path = File.expand_path("~/Library/Keychains/#{options[:keychain_name]}") keychain_exists = File.exist?("#{keychain_path}-db") || File.exist?(keychain_path) create_keychain( name: options[:keychain_name], password: options[:keychain_password], unlock: true, timeout: false, add_to_search_list: !keychain_exists ) else unlock_keychain( path: options[:keychain_name], password: options[:keychain_password] ) end end lane :ManuallyUpdateCodeSigning do |options| register_local_storage_for_match() require 'match' storage_factory = lambda do new_storage = Match::Storage.for_mode('local', { git_url: get_signing_identities_path() }) new_storage.download return new_storage end encryption_factory = lambda do |stor| new_encryption = Match::Encryption.for_storage_mode('local', { working_directory: stor.working_directory }) new_encryption.decrypt_files return new_encryption end get_all_files = lambda do |stor| Dir[File.join(stor.working_directory, "**", "*.{cer,p12,mobileprovision}")] end storage = storage_factory.call encryption = encryption_factory.call(storage) old_files = get_all_files.call(storage) sh("open #{storage.working_directory}") # we are not using prompt() since it requires non-empty input which is not a case for Enter (\n) puts "Enter any key when you're done" STDIN.gets encryption.encrypt_files files_to_commit = get_all_files.call(storage) old_directory = storage.working_directory storage.save_changes!(files_to_commit: files_to_commit) # need to check, because saving changes with delete is another function (update repo if needed) files_diff = old_files - files_to_commit # match can not work with both save/delete functionality `You can't provide both files_to_delete and files_to_commit right now` # to avoid this we use storage twice if needed if files_diff.length > 0 storage = storage_factory.call encryption = encryption_factory.call(storage) files_to_delete = files_diff.map do |file| old_file = file old_file.slice! old_directory new_file = File.join(storage.working_directory, old_file) File.delete(new_file) if File.exist?(new_file) file = new_file end encryption.encrypt_files storage.save_changes!(files_to_delete: files_to_delete) end end def sync_code_signing_using_options(options) register_local_storage_for_match() match( app_identifier: options[:app_identifier], username: options[:username] || options[:apple_id], team_id: options[:team_id], type: options[:type], readonly: options[:readonly].nil? ? true : options[:readonly], storage_mode: "local", # we can't pass signing_identities_path as parameter name since params is hardcoded in match/runner.rb git_url: get_signing_identities_path(), skip_docs: true, keychain_name: options[:keychain_name], keychain_password: options[:keychain_password] ) end def register_local_storage_for_match Match::Storage.register_backend(type: 'local', storage_class: Touchlane::LocalStorage) Match::Encryption.register_backend(type: 'local', encryption_class: Match::Encryption::OpenSSL) end def get_signing_identities_path File.expand_path "../EncryptedSigningIdentities" end def fill_up_options_using_configuration_type(options, configuration_type) configuration = get_configuration_for_type(configuration_type.type) configuration.to_options .merge(get_keychain_options(options)) .merge(options) end def get_keychain_options(options) keychain_name = options[:keychain_name] keychain_password = options[:keychain_password] if is_ci? keychain_name = keychain_name || "ci.keychain" keychain_password = keychain_password || "" else keychain_password = keychain_password || prompt( text: "Please enter your keychain password (account password): ", secure_text: true ) end return {:keychain_name => keychain_name, :keychain_password => keychain_password} end def get_configuration_for_type(type) config_path = File.expand_path "configurations.yaml" configuration = Touchlane::Configuration.from_file(config_path, type) end def get_google_services_plist_path(app_target_folder_name, configuration_type) File.expand_path "../#{app_target_folder_name}/Resources/#{configuration_type.prefix}-GoogleService-Info.plist" end def set_xcconfig_for_configuration_of_project(lane_name, configuration, xcodeproj_path) require 'xcodeproj' project = Xcodeproj::Project.open(xcodeproj_path) target_to_modify_selector = lambda do |t| supported_product_types = [ Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application], Xcodeproj::Constants::PRODUCT_TYPE_UTI[:app_extension] ] return !t.test_target_type? && supported_product_types.include?(t.product_type) end application_targets = project.native_targets.select(&target_to_modify_selector) application_targets.each do |target| build_configuration = target.build_configuration_list[configuration] config_name = target.name + lane_name build_configuration_reference = project.files.select { |f| f.path.start_with?(config_name) }.first build_configuration.base_configuration_reference = build_configuration_reference end project.save() end