$appName = File.basename(Dir['../*.xcworkspace'].first, '.*') 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 if File.exists? "../Gemfile" bundle_install(path: "../.gem") end if File.exists? "../Cartfile" begin carthage(command: "bootstrap", platform: "iOS") rescue # workaround for https://github.com/Carthage/Carthage/issues/2298 sh("rm -rf ~/Library/Caches/org.carthage.CarthageKit") carthage(command: "update", platform: "iOS") end end cocoapods( repo_update: true ) end private_lane :uploadToFabric do |options| token, secret = fabric_keys_from_xcodeproj(options[:xcodeproj_path]) releaseNotesFile = "release-notes.txt" sh("touch ../#{releaseNotesFile}") crashlytics( ipa_path: options[:ipa_path], crashlytics_path: "./Pods/Crashlytics/submit", api_token: token, build_secret: secret, notes_path: releaseNotesFile, groups: "touch-instinct" ) upload_symbols_to_crashlytics( dsym_path: options[:dsym_path], api_token: token ) 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 :buildConfiguration do |options| appName = options[:appName] || $appName options[:scheme] = appName configuration = options[:configuration] || lane_context[SharedValues::LANE_NAME] options = options.merge(make_options_for_lane_name(configuration)) options = merge_options_with_config_file(options) options = options.merge(get_keychain_options(options)) openKeychain(options) if is_ci 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 = merge_options_with_config_file(options) options[:skip_package_ipa] = true syncCodeSigning(options) buildArchive(options) # check build failures and static analysis end if options[:uploadToFabric] options = merge_options_with_config_file(options) syncCodeSigning(options) buildArchive(options) uploadToFabric(options) end if options[:uploadToAppStore] options[:compileBitcode] = options[:compileBitcode].nil? ? true : options[:compileBitcode] options[:include_symbols] = options[:include_symbols].nil? ? true : options[:include_symbols] options = options.merge(make_options_for_lane_name("AppStore")) options = merge_options_with_config_file(options) syncCodeSigning(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 gym( clean: true, workspace: options[:workspace], scheme: options[:scheme], archive_path: "./", output_directory: "./", output_name: options[:output_name], configuration: options[:configuration], export_method: options[: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| 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| 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: "git", git_url: options[:git_url], git_branch: "fastlane_certificates", keychain_name: options[:keychain_name], keychain_password: options[:keychain_password], skip_docs: true, platform: "ios" ) 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| # based on this article https://medium.com/@jonathancardoso/using-fastlane-match-with-existing-certificates-without-revoking-them-a325be69dac6 require 'fastlane_core' require 'match' conf = FastlaneCore::Configuration.create(Match::Options.available_options, {}) conf.load_configuration_file("Matchfile") git_url = conf.config_file_options[:git_url] shallow_clone = false branch = 'fastlane_certificates' storage_conf = lambda do new_storage = Match::Storage.for_mode('git', { git_url: git_url, shallow_clone: shallow_clone, git_branch: branch, clone_branch_directly: false}) new_storage.download return new_storage end encryption_conf_for_storage = lambda do |stor| new_encryption = Match::Encryption.for_storage_mode('git', { git_url: git_url, 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_conf.call encryption = encryption_conf_for_storage.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_conf.call encryption = encryption_conf_for_storage.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 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 configuration_type_from_lane_name(lane_name) case when lane_name.start_with?("Enterprise") "enterprise" when lane_name.start_with?("AppStore") "app-store" else "development" end end def profile_type_from_configuration_type(configuration_type) configuration_type.gsub("-", "") # "app-store" => "appstore" end def make_options_for_lane_name(lane_name) method = configuration_type_from_lane_name(lane_name) return { :configuration => lane_name, :method => method, :type => profile_type_from_configuration_type(method) } end def merge_options_with_config_file(options) type = options[:type] || "development" options_override = load_options_from("configurations.yaml") return options.merge(options_override[type.to_sym]) end def load_options_from(file_path) if File.exists? file_path require "yaml" options = YAML.load_file(file_path) return symbolize_keys(options) end return nil end # http://www.virtuouscode.com/2009/07/14/recursively-symbolize-keys/ def symbolize_keys(hash) hash.inject({}){|result, (key, value)| new_key = case key when String then key.to_sym else key end new_value = case value when Hash then symbolize_keys(value) else value end result[new_key] = new_value result } end def fabric_keys_from_xcodeproj(xcodeproj_path) fabric_keys_from_shell_script(sh("cat #{xcodeproj_path}/project.pbxproj | grep 'Fabric/run'")) end def fabric_keys_from_shell_script(shell_script_contents) shell_script_contents.gsub("\\n", "").partition('Fabric/run\" ').last.partition('";').first.split(" ") end