diff --git a/README.md b/README.md index f2632cc..7626234 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ### Как подключить к проекту: -1. Скопировать директорию [`codeStyles`](/codeStyles) в директорию проекта `.idea` +1. Скопировать директорию [`codeStyles`](./codeStyles) в директорию проекта `.idea` 2. Добавить в файл `.gitignore` строку `!.idea/codeStyles` 3. Перезапустить Android Studio, чтобы настройки применились @@ -19,7 +19,7 @@ ### Как импортировать схему в Android Studio: -1. Скачать схему [`codeStyles/Project.xml`](/codeStyles/Project.xml) +1. Скачать схему [`codeStyles/Project.xml`](./codeStyles/Project.xml) 2. В Android Studio перейти в `File` > `Settings` > `Editor` > `Code Style` 3. Нажать на шестеренку справа от выпадающего списка схем и выбрать `Import Scheme` 4. В открывшемся окне указать путь до сохраненной схемы и нажать `ОК` diff --git a/scripts/export_src.sh b/scripts/export_src.sh index 57323dd..20c12bc 100755 --- a/scripts/export_src.sh +++ b/scripts/export_src.sh @@ -30,7 +30,7 @@ clone_platform() { PROJECT_NAME=$1 PLATFORM=$2 - if git clone --recurse-submodules -j8 "git@gitlab.ti:touchinstinct/${PROJECT_NAME}-${PLATFORM}.git" --branch "${GIT_BRANCH}"; then + if git clone --recurse-submodules -j8 "ssh://git@git.ti:7999/touchinstinct/${PROJECT_NAME}-${PLATFORM}.git" --branch "${GIT_BRANCH}"; then cd ${PROJECT_NAME}-${PLATFORM} COMMIT_DATE=`git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d'` diff --git a/xcode/bootstrap/Makefile b/xcode/bootstrap/Makefile index 09940d2..62e1282 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) @@ -44,7 +44,7 @@ init: bundle install - $(call gen) + xcodegen $(call install_pods) diff --git a/xcode/bootstrap/full_code_lint.sh b/xcode/bootstrap/full_code_lint.sh new file mode 100644 index 0000000..a036ef0 --- /dev/null +++ b/xcode/bootstrap/full_code_lint.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# Description: +# Runs full linting and copy-paste-detection for project +# +# Required environment variables: +# SRCROOT - project directory. +# SCRIPT_DIR - directory of current script. +# +# Optional environment variables: +# See swiftlint.sh and copy_paste_detection.sh for complete list of available variables +# +# Example of usage: +# ./full_code_lint.sh +# + +if [ -z "${SCRIPT_DIR}" ]; then + SCRIPT_DIR=${SRCROOT}/build-scripts/xcode/build_phases +fi + +. ${SRCROOT}/build-scripts/xcode/aux_scripts/install_env.sh swiftlint +FORCE_LINT=true; . ${SCRIPT_DIR}/swiftlint.sh + +. ${SRCROOT}/build-scripts/xcode/aux_scripts/install_env.sh cpd +. ${SCRIPT_DIR}/copy_paste_detection.sh Localization Generated Pods \ No newline at end of file diff --git a/xcode/bootstrap/incremental_code_lint.sh b/xcode/bootstrap/incremental_code_lint.sh new file mode 100644 index 0000000..427a465 --- /dev/null +++ b/xcode/bootstrap/incremental_code_lint.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# Description: +# Runs incremental linting for project +# +# Required environment variables: +# SRCROOT - project directory. +# SCRIPT_DIR - directory of current script. +# +# Optional environment variables: +# See swiftlint.sh for complete list of available variables +# +# Example of usage: +# ./incremetal_code_lint.sh +# + +if [ -z "${SCRIPT_DIR}" ]; then + SCRIPT_DIR=${SRCROOT}/build-scripts/xcode/build_phases +fi + +. ${SRCROOT}/build-scripts/xcode/aux_scripts/install_env.sh swiftlint +. ${SCRIPT_DIR}/swiftlint.sh \ No newline at end of file diff --git a/xcode/build_phases/api_generator.sh b/xcode/build_phases/api_generator.sh index ff31f91..0de96fd 100755 --- a/xcode/build_phases/api_generator.sh +++ b/xcode/build_phases/api_generator.sh @@ -24,36 +24,50 @@ readonly EXIT_SUCCESS=0 readonly EXIT_FAILURE=1 -readonly TRUE=0 -readonly FALSE=1 +readonly TRUE=1 +readonly FALSE=0 readonly LOG_TAG="API-GENERATOR" notice() { - echo "${LOG_TAG}:NOTICE: ${1}" + echo "${LOG_TAG}:NOTICE: ${1}" >&2 } debug() { if [ ! -z "${VERBOSE}" ]; then - echo "${LOG_TAG}:DEBUG: ${1}" + echo "${LOG_TAG}:DEBUG: ${1}" >&2 + fi +} + +exit_on_failure() +{ + eval ${1} + + local -r EXIT_CODE=$? + + if [ ${EXIT_CODE} -ne 0 ]; then + echo "Recent command: \`${1}\` failed with code ${EXIT_CODE}" + + exit ${EXIT_CODE} fi } is_force_run() { if [ -z "${FORCE_RUN}" ]; then - return ${FALSE} + echo ${FALSE} + return fi local -r STR_MODE=`tr "[:upper:]" "[:lower:]" <<< ${FORCE_RUN}` if [ ${STR_MODE} == "yes" ] || [ ${STR_MODE} == "true" ] || [ ${STR_MODE} == "1" ]; then - return ${TRUE} + echo ${TRUE} + else + echo ${FALSE} fi - - return ${FALSE} } is_single_file() @@ -85,6 +99,19 @@ get_api_spec_current_commit() fi } +get_api_spec_status() +{ + if [ -z "${API_SPEC_DIR}" ]; then + if [ ! -z "${1}" ]; then + echo `git -C ${1} status -s` + else + echo `git status -s` + fi + else + echo `git -C ${API_SPEC_DIR} status -s` + fi +} + is_api_spec_under_source_control() { local IS_UNDER_SOURCE_CONTROL_CHECK @@ -99,14 +126,33 @@ is_api_spec_under_source_control() IS_UNDER_SOURCE_CONTROL_CHECK=`git -C ${API_SPEC_DIR} rev-parse --is-inside-work-tree 2>/dev/null` fi - [ ${IS_UNDER_SOURCE_CONTROL_CHECK} = "true" ] + if [ "${IS_UNDER_SOURCE_CONTROL_CHECK}" = "true" ]; then + echo ${TRUE} + else + echo ${FALSE} + fi +} + +is_api_spec_has_uncommited_changes() +{ + if [ `is_api_spec_under_source_control` -eq ${TRUE} ]; then + local -r API_SPEC_STATUS=`get_api_spec_status` + + if [ -z "${API_SPEC_STATUS}" ]; then + echo ${FALSE} + else + echo ${TRUE} + fi + else + echo ${FALSE} + fi } is_nothing_changed_since_last_check() { - if is_force_run; then + if [ `is_force_run` -eq ${TRUE} ]; then notice "Force run detected. Skipping commits comparison." - return ${EXIT_FAILURE} + echo ${TRUE} fi if [ -z "${COMMIT_FILE_PATH}" ]; then @@ -114,28 +160,33 @@ is_nothing_changed_since_last_check() local -r COMMIT_FILE_PATH=${1} else debug "COMMIT_FILE_PATH should be defined or passed as first argument!" - return ${EXIT_FAILURE} + echo ${FALSE} fi fi - if is_api_spec_under_source_control; then + if [ `is_api_spec_under_source_control` -eq ${TRUE} ]; then local -r CURRENT_COMMIT=`get_api_spec_current_commit` local -r LAST_CHECKED_COMMIT=`cat ${COMMIT_FILE_PATH} 2> /dev/null || echo ""` if [ ${CURRENT_COMMIT} = "${LAST_CHECKED_COMMIT}" ]; then - return ${EXIT_SUCCESS} + if [ `is_api_spec_has_uncommited_changes` -eq ${TRUE} ]; then + notice "API spec has uncomitted changes." + echo ${FALSE} + else + echo ${TRUE} + fi else - return ${EXIT_FAILURE} + echo ${FALSE} fi else - return ${EXIT_SUCCESS} + echo ${FALSE} fi } record_current_commit() { - if is_force_run; then + if [ `is_force_run` -eq ${TRUE} ]; then notice "Force run detected. Commit won't be recorder." exit ${EXIT_SUCCESS} fi @@ -204,7 +255,9 @@ openapi_codegen() rm -rf ${OUTPUT_PATH}/${API_NAME} # remove previously generated API (if exists) - java -cp "Downloads/${CODEGEN_FILE_NAME}:Downloads/${TINETWORKING_CODEGEN_FILE_NAME}" io.swagger.codegen.v3.cli.SwaggerCodegen generate -l TINetworking -i ${OPEN_API_SPEC_PATH} -o ${OUTPUT_PATH} --additional-properties projectName=${API_NAME} + local -r OPENAPI_COMMAND="java -cp "Downloads/${CODEGEN_FILE_NAME}:Downloads/${TINETWORKING_CODEGEN_FILE_NAME}" io.swagger.codegen.v3.cli.SwaggerCodegen generate -l TINetworking -i ${OPEN_API_SPEC_PATH} -o ${OUTPUT_PATH} --additional-properties projectName=${API_NAME}" + + exit_on_failure "${OPENAPI_COMMAND}" # flatten folders hierarchy @@ -249,7 +302,9 @@ api_generator_codegen() . build-scripts/xcode/aux_scripts/download_file.sh ${FILE_NAME} ${DOWNLOAD_URL} - java -Xmx12g -jar "Downloads/${FILE_NAME}" generate-client-code --output-language SWIFT --specification-path ${API_SPEC_DIR} --output-path ${OUTPUT_PATH} --single-file $(is_single_file) + local -r API_GENERATOR_COMMAND="java -Xmx12g -jar Downloads/${FILE_NAME} generate-client-code --output-language SWIFT --specification-path ${API_SPEC_DIR} --output-path ${OUTPUT_PATH} --single-file $(is_single_file)" + + exit_on_failure "${API_GENERATOR_COMMAND}" } readonly BUILD_PHASES_DIR=${SRCROOT}/build_phases @@ -258,7 +313,7 @@ mkdir -p ${BUILD_PHASES_DIR} readonly COMMIT_FILE_PATH=${BUILD_PHASES_DIR}/api-generator-commit -if is_nothing_changed_since_last_check; then +if [ `is_nothing_changed_since_last_check` -eq ${TRUE} ]; then notice "Nothing was changed. API generation skipped." exit ${EXIT_SUCCESS} fi diff --git a/xcode/build_phases/common/read_input_file_names.sh b/xcode/build_phases/common/read_input_file_names.sh index 7de9439..436445f 100644 --- a/xcode/build_phases/common/read_input_file_names.sh +++ b/xcode/build_phases/common/read_input_file_names.sh @@ -61,13 +61,12 @@ if has_input_files && \ SCRIPT_INPUT_FILE_VARIABLE_NAME="SCRIPT_INPUT_FILE_${i}" SHELL_VARIABLE="\${${SCRIPT_INPUT_FILE_VARIABLE_NAME}}" RESOLVED_FILE_NAME=`envsubst <<< ${SHELL_VARIABLE}` - INPUT_FILE_NAMES=${INPUT_FILE_NAMES}${FILE_NAMES_SEPARATOR}${RESOLVED_FILE_NAME} if [ ! -z ${INPUT_FILE_NAMES} ]; then INPUT_FILE_NAMES=${INPUT_FILE_NAMES}${FILE_NAMES_SEPARATOR} + else + INPUT_FILE_NAMES=${INPUT_FILE_NAMES}${FILE_NAMES_SEPARATOR}${RESOLVED_FILE_NAME} fi - - INPUT_FILE_NAMES=${INPUT_FILE_NAMES}${RESOLVED_FILE_NAME} done elif has_input_file_lists; then for i in `seq 0 $((${SCRIPT_INPUT_FILE_LIST_COUNT}-1))` diff --git a/xcode/build_phases/copy_paste_detection.sh b/xcode/build_phases/copy_paste_detection.sh index 3fe30c1..4b5e71f 100755 --- a/xcode/build_phases/copy_paste_detection.sh +++ b/xcode/build_phases/copy_paste_detection.sh @@ -7,7 +7,7 @@ # $1 $2 $3 $n - folders to exclude from code checking. # # Required environment variables: -# PROJECT_DIR - project directory. +# SRCROOT - project directory. # SCRIPT_DIR - directory of current script. # # Optional environment variables: @@ -15,43 +15,47 @@ # SCRIPT_INPUT_FILE_{N} - file path to directory that should be checked. # # Modified files: -# ${PROJECT_DIR}/code-quality-reports/CPDLog.txt - check report. +# ${SRCROOT}/code-quality-reports/CPDLog.txt - check report. # # Example of usage: -# runner.sh copy_paste_detection.sh Generated Localization Pods +# copy_paste_detection.sh Generated Localization Pods # -readonly EXIT_SUCCESS=0 -readonly EXIT_FAILURE=1 - -. ${SCRIPT_DIR}/../aux_scripts/install_env.sh pmd +EXIT_SUCCESS=0 +EXIT_FAILURE=1 if which pmd >/dev/null; then - readonly REPORTS_DIR="${PROJECT_DIR}/code-quality-reports" + REPORTS_DIR="${SRCROOT}/code-quality-reports" - readonly SOURCES_DIRS=`. ${SCRIPT_DIR}/common/read_input_file_names.sh " " ${PROJECT_DIR}` + SOURCES_DIRS=`. ${SCRIPT_DIR}/common/read_input_file_names.sh " " ${SRCROOT}` - readonly COMMAND_LINE_ARGUMENTS=$@ + COMMAND_LINE_ARGUMENTS=$@ - FOLDERS_TO_EXLUDE="" + FOLDERS_TO_EXCLUDE="" for argument in ${COMMAND_LINE_ARGUMENTS} do - FOLDERS_TO_EXLUDE=${FOLDERS_TO_EXLUDE}"-or -name ${argument} " + FOLDERS_TO_EXCLUDE=${FOLDERS_TO_EXCLUDE}"-or -name ${argument} " done - FOLDERS_TO_EXLUDE=`echo ${FOLDERS_TO_EXLUDE} | cut -c5-` # remove first "-or" + FOLDERS_TO_EXCLUDE=`echo ${FOLDERS_TO_EXCLUDE} | cut -c5-` # remove first "-or" - readonly FILES_TO_EXCLUDE=`find ${PROJECT_DIR} -type d ${FOLDERS_TO_EXLUDE} | paste -sd " " -` + FILES_TO_EXCLUDE=`find ${SRCROOT} -type d ${FOLDERS_TO_EXCLUDE} | paste -sd " " -` mkdir -p ${REPORTS_DIR} - pmd cpd --files ${SOURCES_DIRS} --exclude ${FILES_TO_EXCLUDE} --minimum-tokens 50 --language swift --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer --failOnViolation true > ${REPORTS_DIR}/cpd-output.xml + DIRS_ARGUMENTS="" + + for SOURCE_DIR in ${SOURCES_DIRS}; do + DIRS_ARGUMENTS=${DIRS_ARGUMENTS}" --dir "${SOURCE_DIR} + done + + pmd cpd ${DIRS_ARGUMENTS} --exclude ${FILES_TO_EXCLUDE} --minimum-tokens 50 --language swift --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer --skip-lexical-errors true > ${REPORTS_DIR}/cpd-output.xml php ${SCRIPT_DIR}/../aux_scripts/cpd_script.php ${REPORTS_DIR}/cpd-output.xml | tee ${REPORTS_DIR}/CPDLog.txt - # Make paths relative to PROJECT_DIR, so different developers won't rewrite entire file - readonly SED_REPLACEMENT_STRING=$(echo ${PROJECT_DIR} | sed "s/\//\\\\\//g") + # Make paths relative to SRCROOT, so different developers won't rewrite entire file + SED_REPLACEMENT_STRING=$(echo ${SRCROOT} | sed "s/\//\\\\\//g") sed -i '' "s/${SED_REPLACEMENT_STRING}//g" "${REPORTS_DIR}/CPDLog.txt" else diff --git a/xcode/build_phases/swiftlint.sh b/xcode/build_phases/swiftlint.sh index 966dec9..ccb66f8 100755 --- a/xcode/build_phases/swiftlint.sh +++ b/xcode/build_phases/swiftlint.sh @@ -32,8 +32,10 @@ readonly SOURCES_DIRS=`. ${SCRIPT_DIR}/common/read_input_file_names.sh "\n" ${SR if [ -z "${SWIFTLINT_EXECUTABLE}" ]; then if [ ! -z "${1}" ]; then readonly SWIFTLINT_EXECUTABLE=${1} - else + elif [ ! -z "${PODS_ROOT}" ]; then readonly SWIFTLINT_EXECUTABLE=${PODS_ROOT}/SwiftLint/swiftlint + else + readonly SWIFTLINT_EXECUTABLE=${SRCROOT}/Pods/SwiftLint/swiftlint fi fi diff --git a/xcode/commonFastfile b/xcode/commonFastfile index e95320d..9ebb1b3 100644 --- a/xcode/commonFastfile +++ b/xcode/commonFastfile @@ -1,15 +1,7 @@ $appName = File.basename(Dir['../*.xcworkspace'].first, '.*') require_relative 'fastlane/touchlane/lib/touchlane' -require_relative 'managers/managers' -# 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/" @@ -56,13 +48,18 @@ private_lane :uploadToFirebase do |options| end end -def upload_to_app_store_using_options(options) +def upload_to_app_store_using_options(options, submit_for_review = false) upload_to_app_store( username: options[:username] || options[:apple_id], api_key_path: options[:api_key_path], api_key: options[:api_key], ipa: options[:ipa_path], + build_number: options[:ipa_path].nil? ? options[:buildNumber] : nil, + skip_binary_upload: options[:ipa_path].nil?, + skip_screenshots: true, force: true, # skip metainfo prompt + submit_for_review: submit_for_review, + submission_information: options[:submission_information], skip_metadata: true, team_id: options[:itc_team_id], dev_portal_team_id: options[:team_id], @@ -105,7 +102,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) @@ -120,18 +117,17 @@ private_lane :buildConfiguration do |options| installDependencies(options) run_code_generation_phase_if_needed(options) - generate_enabled_features_extension_if_needed(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) @@ -140,9 +136,9 @@ 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) + upload_to_app_store_using_options(options, false) end upload_symbols_to_crashlytics( @@ -160,25 +156,13 @@ private_lane :buildArchive do |options| lane_name = options[:lane_name] configuration = options[:configuration] xcodeproj_path = options[:xcodeproj_path] - xcode_version = options[:xcodeVersion] - cmd = 'system_profiler -json SPDeveloperToolsDataType' - cmd_result = `#{cmd}` - spdeveloperToolsDataType = JSON.parse(cmd_result)['SPDeveloperToolsDataType'] - sortedSPDeveloperToolsDataType = spdeveloperToolsDataType.sort_by { |hash| hash['spdevtools_version'].split(' ').first.to_i } # sort by increasing the version of xcode - default_xcode_version = sortedSPDeveloperToolsDataType.last['spdevtools_version'] # take the largest version in format: "13.0 (13A5212g)" - default_xcode_version_number = default_xcode_version.split(' ').first # take version number + xcodes(select_for_current_build_only: true) if configuration != "AppStore" # AppStore uses xcconfig choosen in Xcode set_xcconfig_for_configuration_of_project(lane_name, configuration, xcodeproj_path) end - if xcode_version.nil? - xcversion(version: default_xcode_version_number) - else - xcversion(version: xcode_version) - end - gym( clean: true, workspace: options[:workspace], @@ -194,33 +178,46 @@ private_lane :buildArchive do |options| ) end -lane :CreatePushCertificate do |options| - configuration = get_configuration_for_type(options[:type] || "development") - options = configuration.to_options.merge(options) +lane :SubmitForReview do |options| + configuration_type = Touchlane::ConfigurationType.from_type("appstore") + options = fill_up_options_using_configuration_type(options, configuration_type, false) - 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 - ) + upload_to_app_store_using_options(options, true) 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| @@ -245,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" @@ -344,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 @@ -357,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 @@ -387,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 ) @@ -414,34 +332,6 @@ def get_google_services_plist_path(app_target_folder_name, configuration_type) File.expand_path "../#{app_target_folder_name}/Resources/GoogleService-Info.plist" end -def generate_enabled_features_extension_if_needed(options) - app_target_folder_name = options[:appName] || $appName - - project_enabled_features_file_path = File.expand_path "../#{app_target_folder_name}/Resources/Features/Enabled.swift" - build_settings_file_path = File.expand_path "../common/build_settings.yaml" - - unless is_feature_extension_needed?(options, project_enabled_features_file_path) - return - end - - if options[:features].nil? - builder_features_list = [] # If Enabled.swift exists and features option is nil we need to create empty extension to avoid unexpected features - else - builder_features_list = options[:features] - .split(",").map { |feature_name| feature_name.strip } # [ "Feature1", "Feature2", "Feature3" ] - end - - build_settings_features_list = Managers::FileManager.load_from_file_YAML(build_settings_file_path)["features"] - - enabled_features_extension = Touchlane::Features.generate_enabled_features_extension(builder_features_list, build_settings_features_list) - - Managers::FileManager.save_data_to_file(project_enabled_features_file_path, enabled_features_extension) -end - -def is_feature_extension_needed?(options, project_enabled_features_file_path) - !options[:features].nil? || File.exists?(project_enabled_features_file_path) -end - def set_xcconfig_for_configuration_of_project(lane_name, configuration, xcodeproj_path) require 'xcodeproj' 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 } diff --git a/xcode/fastlane/touchlane/lib/touchlane/features.rb b/xcode/fastlane/touchlane/lib/touchlane/features.rb deleted file mode 100644 index cadfb0f..0000000 --- a/xcode/fastlane/touchlane/lib/touchlane/features.rb +++ /dev/null @@ -1,26 +0,0 @@ -require_relative '../../../../managers/managers' -require_relative '../../../../templates/templates' - -module Touchlane - class Features - - def self.generate_enabled_features_extension(builder_features_list, build_settings_features_list) - - # Check is entered features contains in configuration file - features_diff = builder_features_list - build_settings_features_list - - unless features_diff.empty? - raise "Unexpected features: " + features_diff.join(', ') - end - - # Generate enabled features extension from feature names - enabled_features_extension_template = Templates::FeatureTemplates.enabled_features_extension - utils = Managers::TemplateManager.new(builder_features_list) - - utils.render(enabled_features_extension_template).strip - end - - private_class_method :new - - end -end diff --git a/xcode/managers/lib/file_manager.rb b/xcode/managers/lib/file_manager.rb deleted file mode 100644 index 45bdda8..0000000 --- a/xcode/managers/lib/file_manager.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'yaml' -require 'json' - -module Managers - class FileManager - - def self.save_data_to_file(path, data) - unless File.exists? path - raise "Unable to save data to file at #{path}" - else - File.open(path, "w") do |f| - f.write(data) - end - end - end - - def self.load_from_file_YAML(path) - unless File.exists? path - raise "Unable to load data from file at #{path}" - else - YAML.load_file(path) - end - end - - def self.save_data_to_file_in_json(path, data) - json_data = JSON.pretty_generate(data) - save_data_to_file(path, json_data) - end - - private_class_method :new - - end -end diff --git a/xcode/managers/lib/template_manager.rb b/xcode/managers/lib/template_manager.rb deleted file mode 100644 index adb59c5..0000000 --- a/xcode/managers/lib/template_manager.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'erb' - -module Managers - class TemplateManager - - include ERB::Util - - attr_accessor :items - - def initialize(items) - @items = items - end - - def render(template) - ERB.new(template).result(binding) - end - - end -end diff --git a/xcode/managers/managers.rb b/xcode/managers/managers.rb deleted file mode 100644 index 9c3c53d..0000000 --- a/xcode/managers/managers.rb +++ /dev/null @@ -1,4 +0,0 @@ -module Managers - require_relative "lib/file_manager" - require_relative "lib/template_manager" -end diff --git a/xcode/templates/templates.rb b/xcode/templates/templates.rb deleted file mode 100644 index c26788d..0000000 --- a/xcode/templates/templates.rb +++ /dev/null @@ -1,3 +0,0 @@ -module Templates - require_relative "templates/features_templates" -end diff --git a/xcode/templates/templates/features_templates.rb b/xcode/templates/templates/features_templates.rb deleted file mode 100755 index 69eeeaf..0000000 --- a/xcode/templates/templates/features_templates.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Templates - module FeatureTemplates - - def self.features_enum -" -// MARK: - Generated feature toggles - -public enum Feature: String, Codable, RawRepresentable, CaseIterable { - <% for @item in @items %> - case <%= @item %> - <% end %> -} -" - end - - def self.enabled_features_extension -" -// MARK: - Generated enabled features - -public extension Feature { - - static var enabled: [Feature] { - [ - <% for @item in @items %> - \.<%= @item %>, - <% end %> - ] - } -} -" - end - - end -end \ No newline at end of file