diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f1344f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# macOS + +.DS_Store \ No newline at end of file diff --git a/gradle/plugins/build.gradle.kts b/gradle/plugins/build.gradle.kts index 3611bcb..485bc2d 100644 --- a/gradle/plugins/build.gradle.kts +++ b/gradle/plugins/build.gradle.kts @@ -7,7 +7,7 @@ plugins { // The kotlin-dsl plugin requires a repository to be declared repositories { - jcenter() + mavenCentral() google() } diff --git a/gradle/plugins/src/main/java/static_analysis/linters/AndroidLinter.kt b/gradle/plugins/src/main/java/static_analysis/linters/AndroidLinter.kt index 8936fec..4ae4e94 100644 --- a/gradle/plugins/src/main/java/static_analysis/linters/AndroidLinter.kt +++ b/gradle/plugins/src/main/java/static_analysis/linters/AndroidLinter.kt @@ -33,8 +33,8 @@ class AndroidLinter : Linter { .flatten() override fun setupForProject(project: Project, extension: StaticAnalysisExtension) { - project.gradle.projectsEvaluated { - project.subprojects + project.beforeEvaluate { + subprojects .mapNotNull { it.extensions.findByType() } .first() .lintOptions.apply { @@ -45,8 +45,8 @@ class AndroidLinter : Linter { htmlReport = false isCheckDependencies = true disable("MissingConstraints", "VectorRaster") - xmlOutput = project.getLintReportFile() - lintConfig = project.file("${extension.buildScriptDir}/static_analysis_configs/lint.xml") + xmlOutput = getLintReportFile() + lintConfig = file("${extension.buildScriptDir}/static_analysis_configs/lint.xml") } } } diff --git a/gradle/plugins/src/main/java/static_analysis/linters/CpdLinter.kt b/gradle/plugins/src/main/java/static_analysis/linters/CpdLinter.kt index 1e7395a..68e4be7 100644 --- a/gradle/plugins/src/main/java/static_analysis/linters/CpdLinter.kt +++ b/gradle/plugins/src/main/java/static_analysis/linters/CpdLinter.kt @@ -34,15 +34,18 @@ class CpdLinter : Linter { } override fun setupForProject(project: Project, extension: StaticAnalysisExtension) { - project.extensions.findByType()!!.apply { - isSkipLexicalErrors = true - language = "kotlin" - minimumTokenCount = 60 - } - project.tasks.withType { - reports.xml.destination = project.getCpdReportFile() - ignoreFailures = true - source = project.getSources(extension.excludes) + project.afterEvaluate { + extensions.findByType()!!.apply { + isSkipLexicalErrors = true + language = "kotlin" + minimumTokenCount = 60 + } + tasks.withType { + reports.xml.required.set(true) + reports.xml.destination = getCpdReportFile() + ignoreFailures = true + source = getSources(extension.excludes) + } } } diff --git a/gradle/plugins/src/main/java/static_analysis/linters/DetektLinter.kt b/gradle/plugins/src/main/java/static_analysis/linters/DetektLinter.kt index 0aafb22..efb1ea8 100644 --- a/gradle/plugins/src/main/java/static_analysis/linters/DetektLinter.kt +++ b/gradle/plugins/src/main/java/static_analysis/linters/DetektLinter.kt @@ -32,27 +32,27 @@ class DetektLinter : Linter { .flatten() override fun setupForProject(project: Project, extension: StaticAnalysisExtension) { - project - .tasks - .withType(Detekt::class.java) { - exclude("**/test/**") - exclude("resources/") - exclude("build/") - exclude("tmp/") - jvmTarget = "1.8" + project.afterEvaluate { + tasks.withType(Detekt::class.java) { + exclude("**/test/**") + exclude("resources/") + exclude("build/") + exclude("tmp/") + jvmTarget = "1.8" - config.setFrom(project.files("${extension.buildScriptDir!!}/static_analysis_configs/detekt-config.yml")) - reports { - txt.enabled = false - html.enabled = false - xml { - enabled = true - destination = project.getDetektReportFile() - } + config.setFrom(files("${extension.buildScriptDir!!}/static_analysis_configs/detekt-config.yml")) + reports { + txt.enabled = false + html.enabled = false + xml { + enabled = true + destination = getDetektReportFile() } - - source = project.getSources(extension.excludes) } + + source = getSources(extension.excludes) + } + } } override fun getTaskNames(project: Project, buildType: String?): List = listOf(":detekt") diff --git a/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisAndroidPlugin.kt b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisAndroidPlugin.kt index 423d1ab..6116a8b 100644 --- a/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisAndroidPlugin.kt +++ b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisAndroidPlugin.kt @@ -28,8 +28,6 @@ class StaticAnalysisAndroidPlugin : StaticAnalysisPlugin() { ) } } - - } } } diff --git a/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisPlugin.kt b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisPlugin.kt index 38ded00..369fb90 100644 --- a/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisPlugin.kt +++ b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisPlugin.kt @@ -26,9 +26,7 @@ abstract class StaticAnalysisPlugin : Plugin { val linters = createLinters() - afterEvaluate { - linters.forEach { it.setupForProject(target, extensions.getByType()) } - } + linters.forEach { it.setupForProject(target, extensions.getByType()) } gradle.projectsEvaluated { createStaticAnalysisTasks(target, linters) diff --git a/scripts/export_src.sh b/scripts/export_src.sh index 3f48a1f..b44f4e6 100755 --- a/scripts/export_src.sh +++ b/scripts/export_src.sh @@ -1,31 +1,49 @@ #!/bin/sh +# Description: +# Creates archive with source code of multiple repositories. +# +# Parameters: +# $1 - github repository name without suffix (project name). +# $2, $3, ..., $n - repository suffixes (platforms). +# +# Optional environment variables: +# GIT_BRANCH - branch to use. Default - master. +# +# Example of usage: +# export_src.sh TestProject ios android backend +# GIT_BRANCH="develop"; ./export_src.sh TestProject ios web +# + +if [ -z "${GIT_BRANCH}" ]; then + GIT_BRANCH="master" +fi + PROJECT_NAME=$1 -SRC_FOLDER_NAME=${PROJECT_NAME}-src-$(date +%F) -SRC_DIR=./${SRC_FOLDER_NAME} +SRC_FOLDER_NAME="${PROJECT_NAME}-src-$(date +%F)" +SRC_DIR="./${SRC_FOLDER_NAME}" COMMAND_LINE_ARGUMENTS=$@ clone_platform() { - PROJECT_DIR=$1 + PROJECT_NAME=$1 PLATFORM=$2 - git clone --recurse-submodules -j8 git@github.com:TouchInstinct/${PROJECT_DIR}-${PLATFORM}.git --branch master + git clone --recurse-submodules -j8 "git@github.com:TouchInstinct/${PROJECT_NAME}-${PLATFORM}.git" --branch "${GIT_BRANCH}" } -mkdir -p ${SRC_DIR} -cd ${SRC_DIR} +mkdir -p "${SRC_DIR}" +cd "${SRC_DIR}" for argument in ${COMMAND_LINE_ARGUMENTS} do - if [ $argument != $PROJECT_NAME ] - then + if [ $argument != $PROJECT_NAME ]; then platform=${argument} # all arguments after project name treated as platforms clone_platform ${PROJECT_NAME} ${platform} fi done find . -name ".git*" -print0 | xargs -0 rm -rf -zip -r ${SRC_FOLDER_NAME}.zip . +zip -r -q ${SRC_FOLDER_NAME}.zip . open . diff --git a/static_analysis_configs/detekt-config.yml b/static_analysis_configs/detekt-config.yml index 5ea4cce..8a6b3e4 100644 --- a/static_analysis_configs/detekt-config.yml +++ b/static_analysis_configs/detekt-config.yml @@ -387,9 +387,6 @@ style: UnusedPrivateMember: active: true allowedNames: "(_|ignored|expected|serialVersionUID)" - UseDataClass: - active: true - excludeAnnotatedClasses: "" UtilityClassWithPublicConstructor: active: false VarCouldBeVal: diff --git a/xcode/.swiftlint.yml b/xcode/.swiftlint.yml index df3de2c..55c4b9e 100644 --- a/xcode/.swiftlint.yml +++ b/xcode/.swiftlint.yml @@ -115,6 +115,8 @@ identifier_name: warning_threshold: 1 +allow_zero_lintable_files: true + custom_rules: # General diff --git a/xcode/aux_scripts/download_file.sh b/xcode/aux_scripts/download_file.sh index 55d74ba..98aa2f2 100755 --- a/xcode/aux_scripts/download_file.sh +++ b/xcode/aux_scripts/download_file.sh @@ -3,8 +3,8 @@ file_link=$2 folder=$3 flag_of_delete=$4 -readonly key_of_delete="--remove-cached" -readonly default_folder="./Downloads" +key_of_delete="--remove-cached" +default_folder="./Downloads" if [[ ${folder} = ${key_of_delete} ]]; then folder="${default_folder}" diff --git a/xcode/aux_scripts/import_strings.php b/xcode/aux_scripts/import_strings.php index 7cfdc7d..1cd20ee 100644 --- a/xcode/aux_scripts/import_strings.php +++ b/xcode/aux_scripts/import_strings.php @@ -1,14 +1,13 @@ $value) { $value_without_linefeed = preg_replace("/\r|\n/", " ", $value); - $ios_swift_strings .= "\t/// ".$value_without_linefeed."\n\t".'static let '.preg_replace_callback('/_(.?)/', function ($m) { return strtoupper($m[1]); }, $key).' = NSLocalizedString("'.$key.'", comment: "")'."\n".PHP_EOL; + $ios_swift_strings .= "\t/// ".$value_without_linefeed."\n\t".'static let '.preg_replace_callback('/_(.?)/', function ($m) { return strtoupper($m[1]); }, $key).' = NSLocalizedString("'.$key.'", bundle: '.$BUNDLE.', comment: "'.addslashes($value_without_linefeed).'")'."\n".PHP_EOL; } $ios_swift_strings .= '}'.PHP_EOL; - file_put_contents($localization.'String+Localization.swift', $ios_swift_strings); + file_put_contents($LOCALIZATION_PATH.'String+Localization.swift', $ios_swift_strings); } } ?> diff --git a/xcode/aux_scripts/install_env.sh b/xcode/aux_scripts/install_env.sh new file mode 100644 index 0000000..59b0a2f --- /dev/null +++ b/xcode/aux_scripts/install_env.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# Description: +# Add user defined enviroment if programm not found +# +# Parameters: +# $1 - programm +# +# Examples of usage: +# . install_env.sh pmd +# + +# When you run Git from the command line, it runs in the environment as set up by your Shell. +# GUI OS X apps, however, have no knowledge about your shell - and the PATH environment can be changed in many different places. +# Export our profile with path by ourselves + +function source_home_file { + file="$HOME/$1" + + if [[ -f "${file}" ]]; then + if ! source "${file}"; then + export_commands="$(cat "${file}" | grep "^export PATH=")" + + while read export_command + do + eval "$export_command" + done <<< "$export_commands" + fi + fi + + return 1 +} + +# Use specific exec due to Xcode has custom value of $PATH +if [ -z "$(which $1)" ]; then + source_home_file ".bash_profile" || source_home_file ".zshrc" || source_home_file ".zprofile" || true + + echo "User defined enviroment has been set for ${1}" +fi \ No newline at end of file diff --git a/xcode/build_phases/api_generator.sh b/xcode/build_phases/api_generator.sh index bef1a04..add9f89 100755 --- a/xcode/build_phases/api_generator.sh +++ b/xcode/build_phases/api_generator.sh @@ -5,9 +5,16 @@ # # Parameters: # $1 - api generator version. +# $2 - path to generated code directory +# +# Required environment variables: +# SRCROOT - path to project folder. # # Optional environment variables: # OUTPUT_PATH - path to Generated folder. +# API_SPEC_DIR - path to api specification folder +# VERBOSE - print debug messages +# API_NAME - project name that will be used by generator (example: OUTPUT_PATH/API_NAME/Classes ) # # Examples of usage: # . api_generator.sh 1.4.0-beta1 @@ -20,6 +27,20 @@ readonly EXIT_FAILURE=1 readonly TRUE=0 readonly FALSE=1 +readonly LOG_TAG="API-GENERATOR" + +notice() +{ + echo "${LOG_TAG}:NOTICE: ${1}" +} + +debug() +{ + if [ ! -z "${VERBOSE}" ]; then + echo "${LOG_TAG}:DEBUG: ${1}" + fi +} + is_force_run() { if [ -z "${FORCE_RUN}" ]; then @@ -37,25 +58,21 @@ is_force_run() get_current_commit() { - if [ -z "${CURRENT_COMMIT}" ]; then - if [ -z "${REPO_PATH}" ]; then - if [ ! -z "${1}" ]; then - echo `git -C ${1} rev-parse --verify HEAD` - else - echo `git rev-parse --verify HEAD` - fi + if [ -z "${API_SPEC_DIR}" ]; then + if [ ! -z "${1}" ]; then + echo `git -C ${1} rev-parse --verify HEAD` else - echo `git -C ${REPO_PATH} rev-parse --verify HEAD` + echo `git rev-parse --verify HEAD` fi else - echo ${CURRENT_COMMIT} + echo `git -C ${API_SPEC_DIR} rev-parse --verify HEAD` fi } is_nothing_changed_since_last_check() { if is_force_run; then - echo "Force run detected. Skipping commits comparison." + notice "Force run detected. Skipping commits comparison." return ${EXIT_FAILURE} fi @@ -63,18 +80,14 @@ is_nothing_changed_since_last_check() if [ ! -z "${1}" ]; then local -r COMMIT_FILE_PATH=${1} else - echo "COMMIT_FILE_PATH should be defined or passed as first argument!" + debug "COMMIT_FILE_PATH should be defined or passed as first argument!" return ${EXIT_FAILURE} fi fi - if [ -z "${2}" ]; then - local -r CURRENT_COMMIT=`get_current_commit` - else - local -r CURRENT_COMMIT=${2} - fi + local -r CURRENT_COMMIT=`get_current_commit` - local -r LAST_CHECKED_COMMIT=`cat ${COMMIT_FILE_PATH}` || "" + local -r LAST_CHECKED_COMMIT=`cat ${COMMIT_FILE_PATH} 2> /dev/null || echo ""` if [ ${CURRENT_COMMIT} = "${LAST_CHECKED_COMMIT}" ]; then return ${EXIT_SUCCESS} @@ -86,43 +99,134 @@ is_nothing_changed_since_last_check() record_current_commit() { if is_force_run; then - echo "Force run detected. Commit won't be recorder." + notice "Force run detected. Commit won't be recorder." exit ${EXIT_SUCCESS} fi - if [ -z "${1}" ]; then - local -r CURRENT_COMMIT=`get_current_commit` - else - local -r CURRENT_COMMIT=${1} - fi - if [ -z "${COMMIT_FILE_PATH}" ]; then - if [ ! -v "${2}" ]; then - local -r COMMIT_FILE_PATH=${2} + if [ ! -v "${1}" ]; then + local -r COMMIT_FILE_PATH=${1} else - echo "COMMIT_FILE_PATH should be defined or passed as second argument!" + debug "COMMIT_FILE_PATH should be defined or passed as second argument!" return ${EXIT_FAILURE} fi fi + local -r CURRENT_COMMIT=`get_current_commit` + echo ${CURRENT_COMMIT} > ${COMMIT_FILE_PATH} } +openapi_codegen() +{ + if [ -z "${OPEN_API_SPEC_PATH}" ]; then + if [ ! -v "${1}" ]; then + local -r OPEN_API_SPEC_PATH=${1} + else + debug "OPEN_API_SPEC_PATH should be defined or passed as first argument!" + return ${EXIT_FAILURE} + fi + fi + + if [ -z "${OUTPUT_PATH}" ]; then + if [ ! -v "${2}" ]; then + local -r OUTPUT_PATH=${2} + else + debug "OUTPUT_PATH should be defined or passed as second argument!" + return ${EXIT_FAILURE} + fi + fi + + if [ -z "${VERSION}" ]; then + if [ ! -v "${3}" ]; then + local -r VERSION=${3} + else + debug "VERSION should be defined or passed as third argument!" + return ${EXIT_FAILURE} + fi + fi + + if [ -z "${API_NAME}" ]; then + local -r API_NAME="${PROJECT_NAME}API" + fi + + notice "OpenAPI spec generation for ${OPEN_API_SPEC_PATH}" + + local -r CODEGEN_VERSION="3.0.33" + + local -r CODEGEN_FILE_NAME="swagger-codegen-cli-${CODEGEN_VERSION}.jar" + local -r CODEGEN_DOWNLOAD_URL="https://repo1.maven.org/maven2/io/swagger/codegen/v3/swagger-codegen-cli/${CODEGEN_VERSION}/${CODEGEN_FILE_NAME}" + + . build-scripts/xcode/aux_scripts/download_file.sh ${CODEGEN_FILE_NAME} ${CODEGEN_DOWNLOAD_URL} + + local -r TINETWORKING_CODEGEN_FILE_NAME="codegen-${VERSION}.jar" + + local -r DOWNLOAD_URL="https://maven.dev.touchin.ru/ru/touchin/codegen/${VERSION}/${TINETWORKING_CODEGEN_FILE_NAME}" + + . build-scripts/xcode/aux_scripts/download_file.sh ${TINETWORKING_CODEGEN_FILE_NAME} ${DOWNLOAD_URL} + + 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} + + # flatten folders hierarchy + + mv ${OUTPUT_PATH}/${API_NAME}/Classes/Swaggers/* ${OUTPUT_PATH}/${API_NAME}/ + + rm -rf ${OUTPUT_PATH}/${API_NAME}/Classes +} + +api_generator_codegen() +{ + if [ -z "${API_SPEC_DIR}" ]; then + if [ ! -v "${1}" ]; then + local -r API_SPEC_DIR=${1} + else + debug "API_SPEC_DIR should be defined or passed as first argument!" + return ${EXIT_FAILURE} + fi + fi + + if [ -z "${OUTPUT_PATH}" ]; then + if [ ! -v "${2}" ]; then + local -r OUTPUT_PATH=${2} + else + debug "OUTPUT_PATH should be defined or passed as second argument!" + return ${EXIT_FAILURE} + fi + fi + + if [ -z "${VERSION}" ]; then + if [ ! -v "${3}" ]; then + local -r VERSION=${3} + else + debug "VERSION should be defined or passed as third argument!" + return ${EXIT_FAILURE} + fi + fi + + notice "api-generator spec generation for ${API_SPEC_DIR}/main.json" + + local -r FILE_NAME="api-generator-${VERSION}.jar" + local -r DOWNLOAD_URL="https://maven.dev.touchin.ru/ru/touchin/api-generator/${VERSION}/${FILE_NAME}" + + . build-scripts/xcode/aux_scripts/download_file.sh ${FILE_NAME} ${DOWNLOAD_URL} + + java -Xmx6g -jar "Downloads/${FILE_NAME}" generate-client-code --output-language SWIFT --specification-path ${API_SPEC_DIR} --output-path ${OUTPUT_PATH} --single-file true +} + readonly BUILD_PHASES_DIR=${SRCROOT}/build_phases mkdir -p ${BUILD_PHASES_DIR} readonly COMMIT_FILE_PATH=${BUILD_PHASES_DIR}/api-generator-commit -readonly REPO_PATH="common" - if is_nothing_changed_since_last_check; then - echo "Nothing was changed models generation skipped." + notice "Nothing was changed api generation skipped." exit ${EXIT_SUCCESS} fi -VERSION=$1 -FILE_NAME="api-generator-${VERSION}.jar" +readonly VERSION=$1 if [ -z "${OUTPUT_PATH}" ]; then if [ ! -z "${2}" ]; then @@ -132,13 +236,23 @@ if [ -z "${OUTPUT_PATH}" ]; then fi fi +if [ -z "${API_SPEC_DIR}" ]; then + readonly API_SPEC_DIR="common/api" +fi + mkdir -p ${OUTPUT_PATH} -# download api generator -readonly DOWNLOAD_URL="https://maven.dev.touchin.ru/ru/touchin/api-generator/${VERSION}/${FILE_NAME}" -. build-scripts/xcode/aux_scripts/download_file.sh ${FILE_NAME} ${DOWNLOAD_URL} +readonly OPEN_API_SPEC_PATH=`find ${API_SPEC_DIR} -maxdepth 1 -name '*.yaml' -o -name '*.yml' | head -n 1` -# execute api generator -java -Xmx6g -jar "Downloads/${FILE_NAME}" generate-client-code --output-language SWIFT --specification-path common/api --output-path ${OUTPUT_PATH} --single-file true +if [ -f "${OPEN_API_SPEC_PATH}" ]; then + openapi_codegen +elif [ -f "${API_SPEC_DIR}/main.json" ]; then + api_generator_codegen +else + notice "No api spec found!" + exit ${EXIT_FAILURE} +fi -record_current_commit +if [ $? -ne ${EXIT_FAILURE} ]; then + record_current_commit +fi diff --git a/xcode/build_phases/common/unused_resources b/xcode/build_phases/common/unused_resources new file mode 100755 index 0000000..1a5b4fc Binary files /dev/null and b/xcode/build_phases/common/unused_resources differ diff --git a/xcode/build_phases/copy_paste_detection.sh b/xcode/build_phases/copy_paste_detection.sh index 653b093..3fe30c1 100755 --- a/xcode/build_phases/copy_paste_detection.sh +++ b/xcode/build_phases/copy_paste_detection.sh @@ -24,6 +24,8 @@ readonly EXIT_SUCCESS=0 readonly EXIT_FAILURE=1 +. ${SCRIPT_DIR}/../aux_scripts/install_env.sh pmd + if which pmd >/dev/null; then readonly REPORTS_DIR="${PROJECT_DIR}/code-quality-reports" diff --git a/xcode/build_phases/localization.sh b/xcode/build_phases/localization.sh index 8c8bf61..1b3d6e8 100644 --- a/xcode/build_phases/localization.sh +++ b/xcode/build_phases/localization.sh @@ -1,16 +1,47 @@ -LOCALIZATION_PATH="${PRODUCT_NAME}/Resources/Localization" -#first argument set strings folder path +#!/bin/sh + +# Description: +# Generates Localizeable.strings and String+Localization.swift files. +# +# Parameters: +# $1 - path to strings folder containing json files. +# $2 - path to Localization folder (output). +# $3 - Bundle for localization. Default is `.main`. +# +# Required environment variables: +# SCRIPT_DIR - directory of current script. +# +# Optional environment variables: +# PRODUCT_NAME - product name to produce path to localization folder (output). +# +# Examples of usage: +# . localization.sh +# . localization.sh common/strings Resources/Localization/ .main +# + +readonly EXIT_SUCCESS=0 +readonly EXIT_FAILURE=1 + +. ${SCRIPT_DIR}/../aux_scripts/install_env.sh php + STRINGS_FOLDER=${1:-"common/strings"} +LOCALIZATION_PATH=${2:-"${PRODUCT_NAME}/Resources/Localization/"} +BUNDLE=${3:-".main"} if ! [ -e ${LOCALIZATION_PATH} ]; then - echo "${PROJECT_DIR}/${LOCALIZATION_PATH} path does not exist. Add these folders and try again." - exit 1 + echo "${LOCALIZATION_PATH} path does not exist. Add these folders and try again." + exit ${EXIT_FAILURE} fi -if ! [ -e "${PROJECT_DIR}/${STRINGS_FOLDER}" ]; then - echo "${PROJECT_DIR}/${STRINGS_FOLDER} path does not exist. Submodule with strings should be named common and contain strings folder." - exit 1 +if ! [ -e "${STRINGS_FOLDER}" ]; then + echo "${STRINGS_FOLDER} path does not exist. Submodule with strings should be named common and contain strings folder." + exit ${EXIT_FAILURE} fi -#second argument set strings script path -php ${2:-build-scripts/xcode/aux_scripts/import_strings.php} ${PRODUCT_NAME} ${STRINGS_FOLDER} +if which php >/dev/null; then + php ${SCRIPT_DIR}/../aux_scripts/import_strings.php ${LOCALIZATION_PATH} ${STRINGS_FOLDER} ${BUNDLE} +else + echo "warning: php not installed, install using 'brew install php'" + + exit ${EXIT_FAILURE} +fi \ No newline at end of file diff --git a/xcode/build_phases/swiftlint.sh b/xcode/build_phases/swiftlint.sh index 5052f54..887866a 100755 --- a/xcode/build_phases/swiftlint.sh +++ b/xcode/build_phases/swiftlint.sh @@ -17,9 +17,11 @@ # SWIFTLINT_CONFIG_PATH - path to swiftlint config. # SCRIPT_INPUT_FILE_COUNT - number of files listed in "Input files" of build phase. # SCRIPT_INPUT_FILE_{N} - file path to directory that should be checked. +# FORCE_LINT - lint all project. # # Example of usage: # swiftlint.sh +# FORCE_LINT=true; swiftlint.sh # swiftlint.sh Pods/Swiftlint/swiftlint build-scripts/xcode/.swiftlint.yml # @@ -41,7 +43,44 @@ if [ -z "${SWIFTLINT_CONFIG_PATH}" ]; then fi fi -for SOURCE_DIR in ${SOURCES_DIRS}; do - ${SWIFTLINT_EXECUTABLE} autocorrect --path ${SOURCE_DIR} --config ${SWIFTLINT_CONFIG_PATH} - ${SWIFTLINT_EXECUTABLE} --path ${SOURCE_DIR} --config ${SWIFTLINT_CONFIG_PATH} -done +if [ ! -z "${FORCE_LINT}" ]; then + # Если задана переменная FORCE_LINT, то проверяем все файлы проекта + for SOURCE_DIR in ${SOURCES_DIRS}; do + ${SWIFTLINT_EXECUTABLE} autocorrect --path ${SOURCE_DIR} --config ${SWIFTLINT_CONFIG_PATH} + ${SWIFTLINT_EXECUTABLE} --path ${SOURCE_DIR} --config ${SWIFTLINT_CONFIG_PATH} + done +else + # Xcode упадет, если будем использовать большое количество Script Input Files, + # так как просто переполнится стек - https://unix.stackexchange.com/questions/357843/setting-a-long-environment-variable-breaks-a-lot-of-commands + # Поэтому воспользуемся "скрытым" параметром Swiflint - https://github.com/realm/SwiftLint/pull/3313 + # Создадим временный файл swiftlint_files с префиксом @ и в нем уже определим список файлов + # необходимых для линтовки :) + + lint_files_path="${SRCROOT}/build_phases/swiftlint_files" + + # Если файл существует, то просто его очистим, если нет - создадим + > ${lint_files_path} + + # Проходимся по папкам, которые требуют линтовки + for SOURCE_DIR in ${SOURCES_DIRS}; do + # Путь к папке репозитория + path_prefix="`git rev-parse --show-toplevel`/" + + # Отбираем файлы, которые были изменены или созданы + source_unstaged_files=$(git diff --diff-filter=d --name-only --line-prefix=${path_prefix} ${SOURCE_DIR} | grep "\.swift$") + source_staged_files=$(git diff --diff-filter=d --name-only --line-prefix=${path_prefix} --cached ${SOURCE_DIR} | grep "\.swift$") + + if [ ! -z "${source_unstaged_files}" ]; then + echo "${source_unstaged_files}" >> ${lint_files_path} + fi + + if [ ! -z "${source_staged_files}" ]; then + echo "${source_staged_files}" >> ${lint_files_path} + fi + done + + swiftlint_files_path="@${lint_files_path}" + + ${SWIFTLINT_EXECUTABLE} autocorrect --path ${swiftlint_files_path} --config ${SWIFTLINT_CONFIG_PATH} --force-exclude --use-alternative-excluding + ${SWIFTLINT_EXECUTABLE} --path ${swiftlint_files_path} --config ${SWIFTLINT_CONFIG_PATH} --force-exclude --use-alternative-excluding +fi diff --git a/xcode/build_phases/unused_resources.sh b/xcode/build_phases/unused_resources.sh new file mode 100644 index 0000000..d4b91a9 --- /dev/null +++ b/xcode/build_phases/unused_resources.sh @@ -0,0 +1,8 @@ +readonly SOURCES_DIR=${1:-${PROJECT_DIR}/${PRODUCT_NAME}} # first argument set product dir +readonly UNUSED_RESOURCES_SCRIPT=${2:-${PROJECT_DIR}/build-scripts/xcode/build_phases/common/unused_resources} # second argument set script path +readonly REPORTS_DIR=${PROJECT_DIR}/code-quality-reports +readonly FILES_TO_EXCLUDE=`find ${SOURCES_DIR} -type d -name Localization -or -name Generated | paste -sd " " -` + +mkdir ${REPORTS_DIR} + +${UNUSED_RESOURCES_SCRIPT} --project ${SOURCES_DIR} --exclude ${FILES_TO_EXCLUDE} --action "l" > ${REPORTS_DIR}/Unused_resources_log.txt diff --git a/xcode/commonFastfile b/xcode/commonFastfile index a0f4641..0c00145 100644 --- a/xcode/commonFastfile +++ b/xcode/commonFastfile @@ -21,7 +21,7 @@ private_lane :installDependencies do |options| end cocoapods( - repo_update: true + try_repo_update_on_error: true ) end @@ -36,12 +36,24 @@ private_lane :uploadToFirebase do |options| google_app_id = get_info_plist_value(path: gsp_plist_path, key: "GOOGLE_APP_ID") - firebase_app_distribution( + firebase_app_distibution_groups_path = File.expand_path "../firebase_app_distribution_groups" + + # Select groups_file or groups parameter depending on groups file existence + if File.exists? firebase_app_distibution_groups_path + firebase_app_distribution( + app: google_app_id, + ipa_path: options[:ipa_path], + groups_file: firebase_app_distibution_groups_path, + release_notes_file: releaseNotesFile + ) + else + firebase_app_distribution( app: google_app_id, ipa_path: options[:ipa_path], groups: "touch-instinct", release_notes_file: releaseNotesFile - ) + ) + end upload_symbols_to_crashlytics( gsp_path: get_google_services_plist_path(app_target_folder_name, configuration_type) @@ -79,16 +91,27 @@ private_lane :addShield do |options| end private_lane :buildConfiguration do |options| - appName = options[:appName] || $appName + options[:appName] = options[:appName] || $appName lane_name = options[:lane_name] || lane_context[SharedValues::LANE_NAME] - options[:scheme] = options[:scheme] || appName + options[:scheme] = options[:scheme] || options[:appName] options[:lane_name] = lane_name + ipa_name = "#{options[:appName]}.ipa" + options[:output_name] = ipa_name + + options[:ipa_path] = "./#{ipa_name}" + options[:dsym_path] = "./#{options[:appName]}.app.dSYM.zip" + + options[:xcodeproj_path] = options[:xcodeproj_path] || "../#{options[:appName]}.xcodeproj" + 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) + generate_xcodeproj_if_needed(options) + openKeychain(options) if !options[:buildNumber].nil? @@ -97,15 +120,6 @@ private_lane :buildConfiguration do |options| ) 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) run_code_generation_phase_if_needed(options) @@ -139,6 +153,9 @@ private_lane :buildConfiguration do |options| end private_lane :buildArchive do |options| + + require 'json' + icloudEnvironment = options[:iCloudContainerEnvironment] || "" exportOptions = icloudEnvironment.to_s.empty? ? {} : {iCloudContainerEnvironment: icloudEnvironment} exportOptions[:compileBitcode] = options[:compileBitcode] || false @@ -146,17 +163,31 @@ 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 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], scheme: options[:scheme], - archive_path: "./", - output_directory: "./", + archive_path: "./#{$appName}.xcarchive", + buildlog_path: "./", output_name: options[:output_name], configuration: configuration, export_method: options[:export_method], @@ -237,7 +268,7 @@ private_lane :openKeychain do |options| name: options[:keychain_name], password: options[:keychain_password], unlock: true, - timeout: false, + timeout: 0, add_to_search_list: !keychain_exists ) else @@ -379,7 +410,7 @@ def get_configuration_for_type(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" + File.expand_path "../#{app_target_folder_name}/Resources/GoogleService-Info.plist" end def generate_enabled_features_extension_if_needed(options) @@ -418,7 +449,8 @@ def set_xcconfig_for_configuration_of_project(lane_name, configuration, xcodepro target_to_modify_selector = lambda do |t| supported_product_types = [ Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application], - Xcodeproj::Constants::PRODUCT_TYPE_UTI[:app_extension] + Xcodeproj::Constants::PRODUCT_TYPE_UTI[:app_extension], + Xcodeproj::Constants::PRODUCT_TYPE_UTI[:framework] ] return !t.test_target_type? && supported_product_types.include?(t.product_type) end @@ -426,23 +458,35 @@ def set_xcconfig_for_configuration_of_project(lane_name, configuration, xcodepro 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 + + if !build_configuration_reference.nil? # target has custom xcconfig + build_configuration = target.build_configuration_list[configuration] + build_configuration.base_configuration_reference = build_configuration_reference + end end project.save() end +def generate_xcodeproj_if_needed(options) + project_yml_path = File.expand_path "../project.yml" + + if !File.exists?(options[:xcodeproj_path]) && File.exists?(project_yml_path) + xcodegen( + spec: project_yml_path + ) + end +end + # Build phases def run_code_generation_phase_if_needed(options) code_generation_script_path = File.expand_path "../.githooks/scripts/CodeGen.sh" - xcodeproj_path = File.expand_path options[:xcodeproj_path] if File.exists? code_generation_script_path - sh(code_generation_script_path, xcodeproj_path) + sh(code_generation_script_path, options[:workspace]) end -end +end \ No newline at end of file diff --git a/xcode/config_generator/build_options_helper b/xcode/config_generator/build_options_helper index 3f81529..e591d65 160000 --- a/xcode/config_generator/build_options_helper +++ b/xcode/config_generator/build_options_helper @@ -1 +1 @@ -Subproject commit 3f81529c1425a74f43fe84fe8befffd81a442cc7 +Subproject commit e591d65722d78918c84c4eeda3df8c4012e75323 diff --git a/xcode/config_generator/config_renderer.rb b/xcode/config_generator/config_renderer.rb new file mode 100644 index 0000000..6d1a915 --- /dev/null +++ b/xcode/config_generator/config_renderer.rb @@ -0,0 +1,132 @@ +require 'json' +require 'mustache' +require 'yaml' + +require_relative '../fastlane/touchlane/lib/touchlane/configuration_type' + +class String + def in_current_dir + "#{__dir__}/#{self}" + end +end + +class ConfigRenderer + class XCConfigKeys + DEVELOPMENT_TEAM = "DEVELOPMENT_TEAM" + PRODUCT_BUNDLE_IDENTIFIER = "PRODUCT_BUNDLE_IDENTIFIER" + CODE_SIGN_STYLE = "CODE_SIGN_STYLE" + end + + INHERITED_PREFIX = "$(inherited)" + + private_constant :INHERITED_PREFIX + + def initialize(configurations_file_path, build_parameters_path, configs_folder_name) + @configurations_file_path = configurations_file_path + @build_parameters_path = build_parameters_path + @configs_folder_name = configs_folder_name + end + + def render_xconfigs + temp_configs_data_file_path = "configs_data.json".in_current_dir + generator_path = "build_options_helper/helper.py".in_current_dir + template_path = "target_xcconfig.mustache".in_current_dir + + # Create config directory if needed + Dir.mkdir(@configs_folder_name) unless Dir.exist?(@configs_folder_name) + + # Call python script and generate configs to config file + system("python #{generator_path} -bp #{@build_parameters_path} -o #{__dir__} -r ios_build_settings -p ios") + + # Open settings, configurations and template files + target_xcconfig_tempate = File.read(template_path) + $configurations = YAML.load(File.open(@configurations_file_path)) + $config_types = $configurations["types"] + + targets = $configurations["targets"] + + # Run through all target in project + targets.each do |target_name, target| + + # Need open everytime, because script make some changes only for this target + configs = JSON.load(File.open(temp_configs_data_file_path)) + + # Run through all configs + configs.each do |config| + + # Take default values + distribution_type = Touchlane::ConfigurationType.from_account_type(config["account_type"]).type + properties = target[distribution_type] + + # Add properties from settings file + properties.each do |key, value| + if config["xcconfig_options"].any? { |option| key == option["key"] } + config["xcconfig_options"].map! { |option| key == option["key"] ? merge_config_data(key, option["value"], value) : option } + else + config["xcconfig_options"].append(config_option(key, value)) + end + end + + # Add missing properties if needed + config["xcconfig_options"].concat(generate_missing_properties(target_name, properties, distribution_type)) + + # Create settings pack + config_data = { + "target_name": target_name, + "abstract_targets_prefix": target["abstract_targets_prefix"], + "configuration": config + } + + # Create file for every setting in loop + File.open(@configs_folder_name + "/" + target_name + config["name"] + ".xcconfig", 'w') { |file| + file.puts(Mustache.render(target_xcconfig_tempate, config_data)) + } + end + + end + + # Remove config file, it's trash + File.delete(temp_configs_data_file_path) if File.exist?(temp_configs_data_file_path) + end + + # Make tuple of key and value become mustache template element + def config_option(key, value) + return { "key" => key, "value" => value } + end + + def merge_config_data(key, config_value, settings_value) + if settings_value.start_with?(INHERITED_PREFIX) + new_value = settings_value.split(INHERITED_PREFIX).last + return config_option(key, config_value + new_value) + else + return config_option(key, settings_value) + end + end + + # Fetch development team from build configuration + def fetch_development_team(development_team_key, distribution_type) + current_config = $config_types[distribution_type] + team_value = current_config["team_id"] + return config_option(development_team_key, team_value) + end + + # Generate missing properties if needed + def generate_missing_properties(target_name, properties, distribution_type) + result = [] + + # Bundle_id_key should be among the properties (required by fastlane) + unless properties.key?(XCConfigKeys::PRODUCT_BUNDLE_IDENTIFIER) + raise "#{target_name}: Could not find #{XCConfigKeys::PRODUCT_BUNDLE_IDENTIFIER} for #{distribution_type}" + end + + unless properties.key?(XCConfigKeys::DEVELOPMENT_TEAM) + result.append(fetch_development_team(XCConfigKeys::DEVELOPMENT_TEAM, distribution_type)) + end + + unless properties.key?(XCConfigKeys::CODE_SIGN_STYLE) + result.append(config_option(XCConfigKeys::CODE_SIGN_STYLE, "Manual")) + end + + return result + end +end \ No newline at end of file diff --git a/xcode/config_generator/example_settings.yaml b/xcode/config_generator/example_settings.yaml index 608981a..624b218 100644 --- a/xcode/config_generator/example_settings.yaml +++ b/xcode/config_generator/example_settings.yaml @@ -1,13 +1,16 @@ targets: TestProject: + abstract_targets_prefix: "-TestProjectKit" development: PRODUCT_BUNDLE_IDENTIFIER: "ru.touchin.testproject" PROVISIONING_PROFILE_SPECIFIER: "TestProjectDev" CODE_SIGN_ENTITLEMENTS: "TestProject/Standard.entitlements" + SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) DEBUG_MENU" enterprise: PRODUCT_BUNDLE_IDENTIFIER: "com.touchin.testproject" PROVISIONING_PROFILE_SPECIFIER: "TestProjectEnterprise" CODE_SIGN_ENTITLEMENTS: "TestProject/Enterprise.entitlements" + SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) DEBUG_MENU" appstore: PRODUCT_BUNDLE_IDENTIFIER: "ru.customer.domain" PROVISIONING_PROFILE_SPECIFIER: "TestProjectAppStore" @@ -15,13 +18,13 @@ targets: types: development: - apple_id: "apple@touchin.ru" + apple_id: "iosdev@touchin.ru" team_id: "**********" itc_team_id: "**********" enterprise: apple_id: "enterpriseapple@touchin.ru" team_id: "**********" appstore: - apple_id: "apple@touchin.ru" + apple_id: "iosdev@touchin.ru" team_id: "**********" itc_team_id: "**********" \ No newline at end of file diff --git a/xcode/config_generator/render_xcconfigs.rb b/xcode/config_generator/render_xcconfigs.rb index 69aee0b..ba20335 100755 --- a/xcode/config_generator/render_xcconfigs.rb +++ b/xcode/config_generator/render_xcconfigs.rb @@ -1,7 +1,4 @@ -require 'json' -require 'mustache' -require 'yaml' - +require_relative "config_renderer" # # Usage: render_xcconfigs.rb [] # @@ -10,128 +7,9 @@ require 'yaml' # It is recommended to remove old .xcconfig files before running this script. # -class String - def in_current_dir - "#{__dir__}/#{self}" - end -end - # Input files paths configurations_file_path = ARGV[0] -temp_configs_data_file_path = "configs_data.json".in_current_dir -generator_path = "build_options_helper/helper.py".in_current_dir -template_path = "target_xcconfig.mustache".in_current_dir build_parameters_path = ARGV[1] configs_folder_name = ARGV[2] || "TargetConfigurations" -# Create config directory if needed -Dir.mkdir(configs_folder_name) unless Dir.exist?(configs_folder_name) - -# Call python script and generate configs to config file -system("python #{generator_path} -bp #{build_parameters_path} -o #{__dir__} -r ios_build_settings -p ios") - -# Open settings, configurations and template files -target_xcconfig_tempate = File.read(template_path) -$configurations = YAML.load(File.open(configurations_file_path)) -$config_types = $configurations["types"] - -# Set global property -targets = $configurations["targets"] - -# Make tuple of key and value become mustache template element -def config_option(key, value) - return { "key" => key, "value" => value } -end - -# Maps lane prefix to distribution type -def distribution_type_of(account_type) - case account_type - when "Standard" - "development" - when "Enterprise" - "enterprise" - when "AppStore" - "appstore" - else - raise "Error: Unsupported distribution type #{account_type}" - end -end - -# Fetch development team from build configuration -def fetch_development_team(development_team_key, distribution_type) - current_config = $config_types[distribution_type] - team_value = current_config["team_id"] - return config_option(development_team_key, team_value) -end - -# Return empty array or generated provisioning profile hash -def generate_provisioning_profile(provisioning_key, bundle_id, distribution_type) - case distribution_type - when "appstore" - app_store_profile = "match AppStore " + bundle_id - config_option(provisioning_key, app_store_profile) - else - config_option(provisioning_key, bundle_id) - end -end - -# Generate missing properties if needed -def generate_missing_properties(target_name, properties, distribution_type) - result = [] - development_team_key = "DEVELOPMENT_TEAM" - provisioning_key = "PROVISIONING_PROFILE_SPECIFIER" - bundle_id_key = "PRODUCT_BUNDLE_IDENTIFIER" - - # Bundle_id_key should be among the properties (required by fastlane) - unless properties.key?(bundle_id_key) - raise "#{target_name}: Could not find #{bundle_id_key} for #{distribution_type}" - end - - unless properties.key?(development_team_key) - result.append(fetch_development_team(development_team_key, distribution_type)) - end - - unless properties.key?(provisioning_key) - result.append(generate_provisioning_profile(provisioning_key, properties[bundle_id_key], distribution_type)) - end - - return result -end - -# Run through all target in project -targets.each do |target_name, target| - - # Need open everytime, because script make some changes only for this target - configs = JSON.load(File.open(temp_configs_data_file_path)) - - # Run through all configs - configs.each do |config| - - # Take default values - distribution_type = distribution_type_of(config["account_type"]) - properties = target[distribution_type] - - # Add properties from settings file - properties.each do |key, value| - config["xcconfig_options"].append(config_option(key, value)) - end - - # Add missing properties if needed - config["xcconfig_options"].concat(generate_missing_properties(target_name, properties, distribution_type)) - - # Create settings pack - config_data = { - "target_name": target_name, - "configuration": config - } - - # Create file for every setting in loop - File.open(configs_folder_name + "/" + target_name + config["name"] + ".xcconfig", 'w') { |file| - file.puts(Mustache.render(target_xcconfig_tempate, config_data)) - } - end - -end - -# Remove config file, it's trash -File.delete(temp_configs_data_file_path) if File.exist?(temp_configs_data_file_path) +ConfigRenderer.new(configurations_file_path, build_parameters_path, configs_folder_name).render_xconfigs() diff --git a/xcode/config_generator/target_xcconfig.mustache b/xcode/config_generator/target_xcconfig.mustache index 7e5477a..66d5387 100644 --- a/xcode/config_generator/target_xcconfig.mustache +++ b/xcode/config_generator/target_xcconfig.mustache @@ -1,7 +1,5 @@ -#include "Pods/Target Support Files/Pods-{{target_name}}/Pods-{{target_name}}.{{configuration.build_type}}.xcconfig" +#include "Pods/Target Support Files/Pods{{abstract_targets_prefix}}-{{target_name}}/Pods{{abstract_targets_prefix}}-{{target_name}}.{{configuration.build_type}}.xcconfig" {{#configuration.xcconfig_options}} {{key}} = {{value}} -{{/configuration.xcconfig_options}} - -CODE_SIGN_STYLE = Manual \ No newline at end of file +{{/configuration.xcconfig_options}} \ No newline at end of file diff --git a/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb b/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb index 96f5412..9c9e455 100644 --- a/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb +++ b/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb @@ -57,6 +57,20 @@ module Touchlane new(type) end + def self.from_account_type(account_type) + case account_type + when DEVELOPMENT_PREFIX + from_type(DEVELOPMENT) + when ENTERPRISE_PREFIX + from_type(ENTERPRISE) + when APP_STORE_PREFIX + from_type(APP_STORE) + else + raise "Unable to map #{account_type} to #{ConfigurationType.class}." + + "Available account types: #{DEVELOPMENT_PREFIX}, #{ENTERPRISE_PREFIX}, #{APP_STORE_PREFIX}" + end + end + def to_options { :type => @type,