diff --git a/xcode/bootstrap/Makefile b/xcode/bootstrap/Makefile index 09940d2..21ca414 100644 --- a/xcode/bootstrap/Makefile +++ b/xcode/bootstrap/Makefile @@ -6,7 +6,7 @@ RESET := $(shell tput -Txterm sgr0) RUBY_VERSION="2.7.6" open_project=(open *.xcworkspace) -install_dev_certs=(bundle exec fastlane SyncCodeSigning type:development readonly:true) +install_dev_certs=(bundle exec fastlane InstallDevelopmentSigningIdentities) install_pods=(bundle exec pod install || bundle exec pod install --repo-update) init_rbenv=(if command -v rbenv &> /dev/null; then eval "$$(rbenv init -)"; fi) diff --git a/xcode/commonFastfile b/xcode/commonFastfile index 09e8757..cb37987 100644 --- a/xcode/commonFastfile +++ b/xcode/commonFastfile @@ -2,13 +2,6 @@ $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/" @@ -104,7 +97,7 @@ private_lane :buildConfiguration do |options| options[:workspace] = options[:workspace] || File.expand_path("../#{options[:appName]}.xcworkspace") configuration_type = Touchlane::ConfigurationType.from_lane_name(lane_name) - options = fill_up_options_using_configuration_type(options, configuration_type) + options = fill_up_options_using_configuration_type(options, configuration_type, true) generate_xcodeproj_if_needed(options) @@ -123,13 +116,13 @@ private_lane :buildConfiguration do |options| if !(options[:uploadToFabric] || options[:uploadToAppStore]) options[:skip_package_ipa] = true - sync_code_signing_using_options(options) + install_signing_identities(options) buildArchive(options) # check build failures and static analysis end if options[:uploadToFabric] - sync_code_signing_using_options(options) + install_signing_identities(options) addShield(options) buildArchive(options) uploadToFirebase(options) @@ -138,7 +131,7 @@ private_lane :buildConfiguration do |options| if options[:uploadToAppStore] options[:include_symbols] = options[:include_symbols].nil? ? true : options[:include_symbols] - sync_code_signing_using_options(options) + install_signing_identities(options) buildArchive(options) upload_to_app_store_using_options(options) end @@ -192,11 +185,39 @@ private_lane :buildArchive do |options| ) end -lane :SyncCodeSigning do |options| - configuration_type = Touchlane::ConfigurationType.from_type(options[:type]) +lane :InstallDevelopmentSigningIdentities do |options| + configuration_type = Touchlane::ConfigurationType.from_type("development") options = fill_up_options_using_configuration_type(options, configuration_type) - sync_code_signing_using_options(options) + install_signing_identities(options) +end + +lane :RefreshProfiles do |options| + type = options[:type] || "development" + + configuration_type = Touchlane::ConfigurationType.from_type(type) + options = fill_up_options_using_configuration_type(options, configuration_type) + + refresh_profiles(options) +end + +lane :ReplaceDevelopmentCertificate do |options| + configuration_type = Touchlane::ConfigurationType.from_type("development") + options = fill_up_options_using_configuration_type(options, configuration_type, true) + + replace_development_certificate(options) +end + +lane :SyncAppStoreIdentities do |options| + configuration_type = Touchlane::ConfigurationType.from_type("appstore") + options = fill_up_options_using_configuration_type(options, configuration_type, true) + + options[:readonly] = false + sync_signing_identities(options) +end + +lane :ManuallyUpdateCodeSigning do |options| + manually_update_code_signing(get_default_options.merge(options)) end private_lane :openKeychain do |options| @@ -221,98 +242,19 @@ private_lane :openKeychain do |options| 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], - api_key_path: options[:api_key_path], - api_key: options[:api_key], - 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) +def get_default_options + { + :git_url => get_signing_identities_path(), + :signing_identities_path => get_signing_identities_path(), + :storage_mode => Touchlane::LocalStorage::STORAGE_TYPE + } end def get_signing_identities_path File.expand_path "../EncryptedSigningIdentities" end -def fill_up_options_using_configuration_type(options, configuration_type) +def fill_up_options_using_configuration_type(options, configuration_type, keychain_password_required = false) configuration = get_configuration_for_type(configuration_type.type) api_key_path = File.expand_path "../fastlane/#{configuration_type.prefix}_api_key.json" @@ -320,7 +262,7 @@ def fill_up_options_using_configuration_type(options, configuration_type) # default_options required to be empty due to the possibility of skipping the configuration type check below - default_options = {} + default_options = get_default_options # Check whether configuration type is required to configure one of api key parameters or not @@ -333,19 +275,19 @@ def fill_up_options_using_configuration_type(options, configuration_type) # If exists then fill in all required information through api_key_path parameter # and set a value to an options` parameter respectively - default_options = {:api_key_path => api_key_path} + default_options[:api_key_path] = api_key_path else # If doesn't exist then build api_key parameter through app_store_connect_api_key action # and set a value to an options` parameter respectively also - default_options = {:api_key => get_app_store_connect_api_key()} + default_options[:api_key] = get_app_store_connect_api_key() end end default_options .merge(configuration.to_options) - .merge(get_keychain_options(options)) + .merge(get_keychain_options(options, keychain_password_required)) .merge(options) end @@ -363,15 +305,15 @@ def get_app_store_connect_api_key() ) end -def get_keychain_options(options) +def get_keychain_options(options, keychain_password_required = false) 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( + elsif keychain_password_required && keychain_password.nil? + keychain_password = prompt( text: "Please enter your keychain password (account password): ", secure_text: true ) diff --git a/xcode/fastlane/touchlane/lib/match/storage/local_storage.rb b/xcode/fastlane/touchlane/lib/match/storage/local_storage.rb index 73759b8..e2b6bc0 100644 --- a/xcode/fastlane/touchlane/lib/match/storage/local_storage.rb +++ b/xcode/fastlane/touchlane/lib/match/storage/local_storage.rb @@ -6,6 +6,8 @@ module Touchlane class LocalStorage < Match::Storage::Interface attr_accessor :signing_identities_path + STORAGE_TYPE = "local" + def self.configure(params) return self.new( # we can't pass signing_identities_path since params is hardcoded in match/runner.rb diff --git a/xcode/fastlane/touchlane/lib/match/storage/local_storage_register.rb b/xcode/fastlane/touchlane/lib/match/storage/local_storage_register.rb new file mode 100644 index 0000000..e6dd662 --- /dev/null +++ b/xcode/fastlane/touchlane/lib/match/storage/local_storage_register.rb @@ -0,0 +1,16 @@ +require 'match' + +# ugly hack to add support for custom storage + +Match.module_eval do + def self.storage_modes + return ['git', 'google_cloud' 's3', Touchlane::LocalStorage::STORAGE_TYPE] + end +end + +def register_local_storage_for_match + storage_type = Touchlane::LocalStorage::STORAGE_TYPE + + Match::Storage.register_backend(type: storage_type, storage_class: Touchlane::LocalStorage) + Match::Encryption.register_backend(type: storage_type, encryption_class: Match::Encryption::OpenSSL) +end diff --git a/xcode/fastlane/touchlane/lib/touchlane.rb b/xcode/fastlane/touchlane/lib/touchlane.rb index 7ece7cd..049b040 100644 --- a/xcode/fastlane/touchlane/lib/touchlane.rb +++ b/xcode/fastlane/touchlane/lib/touchlane.rb @@ -1,6 +1,9 @@ module Touchlane require_relative "touchlane/configuration_type" require_relative "touchlane/configuration" - require_relative "touchlane/features" - require_relative "match/storage/local_storage" + require_relative "match/storage/local_storage_register" + require_relative "touchlane/actions/sync_signing_identities" + require_relative "touchlane/actions/refresh_profiles" + require_relative "touchlane/actions/replace_development_certificate" + require_relative "touchlane/actions/manually_update_code_signing" end diff --git a/xcode/fastlane/touchlane/lib/touchlane/actions/manually_update_code_signing.rb b/xcode/fastlane/touchlane/lib/touchlane/actions/manually_update_code_signing.rb new file mode 100644 index 0000000..c943ee2 --- /dev/null +++ b/xcode/fastlane/touchlane/lib/touchlane/actions/manually_update_code_signing.rb @@ -0,0 +1,62 @@ +require 'match' +require_relative '../../match/storage/local_storage' + +def manually_update_code_signing(options) + register_local_storage_for_match() + + storage_factory = lambda do + new_storage = Match::Storage.from_params(options) + new_storage.download + return new_storage + end + + encryption_factory = lambda do |stor| + new_encryption = Match::Encryption.for_storage_mode(options[:storage_mode], { 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 \ No newline at end of file diff --git a/xcode/fastlane/touchlane/lib/touchlane/actions/refresh_profiles.rb b/xcode/fastlane/touchlane/lib/touchlane/actions/refresh_profiles.rb new file mode 100644 index 0000000..e069bc9 --- /dev/null +++ b/xcode/fastlane/touchlane/lib/touchlane/actions/refresh_profiles.rb @@ -0,0 +1,96 @@ +require 'match' +require 'fastlane_core' +require 'Spaceship' + +def refresh_profiles(options) + register_local_storage_for_match() + + profiles_tmp_dir = Dir.mktmpdir + + unless options[:cert_id] + cert_type = Match.cert_type_sym(options[:type]) + + storage = Match::Storage.from_params(options) + storage.download + + output_dir_certs = File.join(storage.prefixed_working_directory, "certs", cert_type.to_s) + + matched_certs = Dir.glob("*.cer", base: output_dir_certs) + + if matched_certs.empty? + FastlaneCore::UI.error("Unable to locate certificate to upate profiles") + raise "No certificates found at #{output_dir_certs}" + end + + if matched_certs.length > 1 + options[:cert_id] = File.basename(FastlaneCore::UI.select("Please select the certificate", matched_certs), ".cer") + else + options[:cert_id] = File.basename(matched_certs.first, ".cer") + end + + if options[:cert_path].nil? && options[:p12_path].nil? + encryption = Match::Encryption.for_storage_mode(options[:storage_mode], { working_directory: storage.working_directory }) + encryption.decrypt_files + + tmp_certs_dir = Dir.mktmpdir + + cer_file_name = "#{options[:cert_id]}.cer" + cert_path = File.join(output_dir_certs, cer_file_name) + options[:cert_path] = File.join(tmp_certs_dir, cer_file_name) + + p12_file_name = "#{options[:cert_id]}.p12" + p12_path = File.join(output_dir_certs, p12_file_name) + options[:p12_path] = File.join(tmp_certs_dir, p12_file_name) + + IO.copy_stream(cert_path, options[:cert_path]) + IO.copy_stream(p12_path, options[:p12_path]) + end + end + + app_identifier = options[:app_identifier] + + Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.from_json_file(options[:api_key_path]) + + if app_identifier.is_a? Array + app_identifier.each { |app_id| refresh_profile_for_app(options, app_id, profiles_tmp_dir) } + else + refresh_profile_for_app(options, app_identifier, profiles_tmp_dir) + end +end + +def refresh_profile_for_app(options, app_id, profiles_tmp_dir) + provisioning_name = Fastlane::Actions.lane_context[Touchlane::SharedValues::TOUCH_BUNDLE_ID_PROFILE_NAME_MAPPING][app_id] + + profiles = Spaceship::ConnectAPI::Profile.all(filter: { name: provisioning_name }) + + if profiles.empty? + sigh_for_app(options, app_id, provisioning_name, profiles_tmp_dir) + else + FastlaneCore::UI.important("Did find existing profile #{provisioning_name}. Removing it from dev portal.") + + profiles.each { |profile| profile.delete! if profile.name == provisioning_name } + sigh_for_app(options, app_id, provisioning_name, profiles_tmp_dir) + end + + Match::Importer.new.import_cert( + options, + cert_path: options[:cert_path], + p12_path: options[:p12_path], + profile_path: lane_context[Fastlane::Actions::SharedValues::SIGH_PROFILE_PATH] + ) +end + +def sigh_for_app(options, app_id, provisioning_name, profiles_tmp_dir) + sigh( + app_identifier: app_id, + development: options[:development], + username: options[:username] || options[:apple_id], + api_key_path: options[:api_key_path], + api_key: options[:api_key], + team_id: options[:team_id], + provisioning_name: provisioning_name, + output_path: profiles_tmp_dir, + cert_id: options[:cert_id], + force: true # will also add all available devices to this profile + ) +end \ No newline at end of file diff --git a/xcode/fastlane/touchlane/lib/touchlane/actions/replace_development_certificate.rb b/xcode/fastlane/touchlane/lib/touchlane/actions/replace_development_certificate.rb new file mode 100644 index 0000000..76e736b --- /dev/null +++ b/xcode/fastlane/touchlane/lib/touchlane/actions/replace_development_certificate.rb @@ -0,0 +1,24 @@ +def replace_development_certificate(options) + register_local_storage_for_match() + + certs_path_tmp_dir = Dir.mktmpdir + + cert( + development: true, + username: options[:username] || options[:apple_id], + api_key_path: options[:api_key_path], + api_key: options[:api_key], + team_id: options[:team_id], + output_path: certs_path_tmp_dir, + keychain_password: options[:keychain_password] + ) + + options[:cert_id] = lane_context[Fastlane::Actions::SharedValues::CERT_CERTIFICATE_ID] + options[:cert_path] = lane_context[Fastlane::Actions::SharedValues::CERT_FILE_PATH] + options[:p12_path] = File.join(File.dirname(options[:cert_path]), "#{options[:cert_id]}.p12") + options[:readonly] = false + + refresh_profiles(options) + + sh("open #{certs_path_tmp_dir}") +end \ No newline at end of file diff --git a/xcode/fastlane/touchlane/lib/touchlane/actions/sync_signing_identities.rb b/xcode/fastlane/touchlane/lib/touchlane/actions/sync_signing_identities.rb new file mode 100644 index 0000000..aaaca2e --- /dev/null +++ b/xcode/fastlane/touchlane/lib/touchlane/actions/sync_signing_identities.rb @@ -0,0 +1,26 @@ +def install_signing_identities(options) + readonly_options = options + readonly_options[:readonly] = true + + sync_signing_identities(readonly_options) +end + +def sync_signing_identities(options) + register_local_storage_for_match() + + match( + app_identifier: options[:app_identifier], + username: options[:username] || options[:apple_id], + api_key_path: options[:api_key_path], + api_key: options[:api_key], + team_id: options[:team_id], + type: options[:type], + readonly: options[:readonly].nil? ? true : options[:readonly], + storage_mode: options[:storage_mode], + # we can't pass signing_identities_path as parameter name since params is hardcoded in match/runner.rb + git_url: options[:signing_identities_path] || options[:git_url], + skip_docs: true, + keychain_name: options[:keychain_name], + keychain_password: options[:keychain_password] + ) +end \ No newline at end of file diff --git a/xcode/fastlane/touchlane/lib/touchlane/configuration.rb b/xcode/fastlane/touchlane/lib/touchlane/configuration.rb index 02cc4d3..0a24e0d 100644 --- a/xcode/fastlane/touchlane/lib/touchlane/configuration.rb +++ b/xcode/fastlane/touchlane/lib/touchlane/configuration.rb @@ -1,6 +1,10 @@ require "yaml" module Touchlane + module SharedValues + TOUCH_BUNDLE_ID_PROFILE_NAME_MAPPING = :TOUCH_BUNDLE_ID_PROFILE_NAME_MAPPING + end + class Configuration def initialize(type, app_identifier, apple_id, team_id, itc_team_id) @type = type @@ -13,6 +17,8 @@ module Touchlane attr_reader :type, :app_identifier, :apple_id, :team_id, :itc_team_id def self.from_file(path, type) + Fastlane::Actions.lane_context[SharedValues::TOUCH_BUNDLE_ID_PROFILE_NAME_MAPPING] = {} + configuration_hash = load_configuration_from_file(path) attrs_hash = configuration_hash["types"][type] identifiers = get_app_identifiers_from_configuration_hash(configuration_hash, type) @@ -35,8 +41,15 @@ module Touchlane def self.get_app_identifiers_from_configuration_hash(configuration_hash, type) identifier_key = "PRODUCT_BUNDLE_IDENTIFIER" + profile_name_key = "PROVISIONING_PROFILE_SPECIFIER" + configuration_hash["targets"].collect do |target, types| - types[type][identifier_key] or raise "#{target}: There is no #{identifier_key} field in #{type}" + bundle_id = types[type][identifier_key] + profile_name = types[type][profile_name_key] + + Fastlane::Actions.lane_context[SharedValues::TOUCH_BUNDLE_ID_PROFILE_NAME_MAPPING][bundle_id] = profile_name + + bundle_id or raise "#{target}: There is no #{identifier_key} field in #{type}" end end diff --git a/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb b/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb index 9c9e455..d5e5a55 100644 --- a/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb +++ b/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb @@ -74,6 +74,7 @@ module Touchlane def to_options { :type => @type, + :development => @is_development, :export_method => @export_method, :configuration => @configuration }