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/README.md b/README.md index 2c3832e..f2632cc 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ # BuildScripts + +## Настройки форматирования + +Позволяют настроить одинаковое форматирования кода в Android Studio у всех, кто работает на проекте. +Настройки соответствуют +[Правилам оформления Kotlin кода](https://styleguide.docs.touchin.ru/Coding/KotlinCodestyle.html) + +Есть два варианта использования: подключить к проекту или импортировать схему в Android Studio. + +### Как подключить к проекту: + +1. Скопировать директорию [`codeStyles`](/codeStyles) в директорию проекта `.idea` +2. Добавить в файл `.gitignore` строку `!.idea/codeStyles` +3. Перезапустить Android Studio, чтобы настройки применились + +При таком варианте настройки будут применены у всех, кто работает на проекте. +И только для одного конкретного проекта. + +### Как импортировать схему в Android Studio: + +1. Скачать схему [`codeStyles/Project.xml`](/codeStyles/Project.xml) +2. В Android Studio перейти в `File` > `Settings` > `Editor` > `Code Style` +3. Нажать на шестеренку справа от выпадающего списка схем и выбрать `Import Scheme` +4. В открывшемся окне указать путь до сохраненной схемы и нажать `ОК` +5. В открывшемся окне ввести название новой схемы и нажать `ОК` diff --git a/checkstyle/configuration/google_checks.xml b/checkstyle/configuration/google_checks.xml deleted file mode 100755 index 441f1af..0000000 --- a/checkstyle/configuration/google_checks.xml +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/checkstyle/configuration/touchin_checkstyle.xml b/checkstyle/configuration/touchin_checkstyle.xml deleted file mode 100755 index b36d47f..0000000 --- a/checkstyle/configuration/touchin_checkstyle.xml +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/code.style.schemes b/code.style.schemes deleted file mode 100644 index 825ffd3..0000000 --- a/code.style.schemes +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/code.style.schemes.xml b/code.style.schemes.xml deleted file mode 100644 index ef3f32c..0000000 --- a/code.style.schemes.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/codeStyles/Project.xml b/codeStyles/Project.xml new file mode 100644 index 0000000..f9c40c9 --- /dev/null +++ b/codeStyles/Project.xml @@ -0,0 +1,249 @@ + + + + diff --git a/codeStyles/codeStyleConfig.xml b/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..6a7bfad --- /dev/null +++ b/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + diff --git a/codestyles/TouchInstinct.xml b/codestyles/TouchInstinct.xml deleted file mode 100644 index 2beec96..0000000 --- a/codestyles/TouchInstinct.xml +++ /dev/null @@ -1,254 +0,0 @@ - - - - \ No newline at end of file diff --git a/export_src.sh b/export_src.sh deleted file mode 100755 index 3f48a1f..0000000 --- a/export_src.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh - -PROJECT_NAME=$1 -SRC_FOLDER_NAME=${PROJECT_NAME}-src-$(date +%F) -SRC_DIR=./${SRC_FOLDER_NAME} - -COMMAND_LINE_ARGUMENTS=$@ - -clone_platform() { - PROJECT_DIR=$1 - PLATFORM=$2 - - git clone --recurse-submodules -j8 git@github.com:TouchInstinct/${PROJECT_DIR}-${PLATFORM}.git --branch master -} - -mkdir -p ${SRC_DIR} -cd ${SRC_DIR} - -for argument in ${COMMAND_LINE_ARGUMENTS} -do - 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 . - -open . diff --git a/findbugs/filters/findbugs-filter.xml b/findbugs/filters/findbugs-filter.xml deleted file mode 100644 index bb462be..0000000 --- a/findbugs/filters/findbugs-filter.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gradle/apiGenerator.gradle b/gradle/apiGenerator.gradle deleted file mode 100644 index 62a7f67..0000000 --- a/gradle/apiGenerator.gradle +++ /dev/null @@ -1,42 +0,0 @@ -repositories { - maven { - url "https://maven.dev.touchin.ru" - metadataSources { - artifact() - } - } -} - -configurations { - apigenerator -} - -dependencies { - apigenerator 'ru.touchin:api-generator:1.4.0-beta1' -} - -android.applicationVariants.all { variant -> - final File generatedModelsDirectory = new File("${project.buildDir}/generated/source/models/${variant.dirName}") - - def generateJsonModelsTask = tasks.create("apiGenerator${variant.name}") doLast { - javaexec { - main = "-jar" - workingDir = file("${rootDir}") - args = [ - configurations.apigenerator.asPath, - "generate-client-code", - "--output-language", - "JAVA", - "--specification-path", - rootProject.extensions.findByName("pathToApiSchemes"), - "--output-path", - "${generatedModelsDirectory.path}", - "--package-name", - "${rootProject.extensions.findByName("applicationId") ?: applicationId}" - ] - } - } - - generateJsonModelsTask.description = 'Generates Java classes for JSON models' - variant.registerJavaGeneratingTask generateJsonModelsTask, generatedModelsDirectory -} diff --git a/gradle/apiGeneratorKotlinServer.gradle b/gradle/apiGeneratorKotlinServer.gradle deleted file mode 100644 index cffa86d..0000000 --- a/gradle/apiGeneratorKotlinServer.gradle +++ /dev/null @@ -1,38 +0,0 @@ -repositories { - maven { - url "https://maven.dev.touchin.ru" - metadataSources { - artifact() - } - } -} - -configurations { - apigeneratorKotlinServer -} - -dependencies { - apigeneratorKotlinServer 'ru.touchin:api-generator:1.4.0-beta1' -} - -task generateApiModelsKotlinServer doLast { - javaexec { - main = "-jar" - workingDir = file("${rootDir}") - args = [ - configurations.apigeneratorKotlinServer.asPath, - "generate-client-code", - "--output-language", - "KOTLIN_SERVER", - "--specification-path", - rootProject.extensions.findByName("pathToApiSchemes"), - "--output-path", - "${rootDir}/src/main/kotlin", - "--package-name", - rootProject.extensions.findByName("apiPackageName"), - "--recreate_output_dirs", - false - ] - } -} - diff --git a/gradle/commonStaticAnalysis.gradle b/gradle/commonStaticAnalysis.gradle deleted file mode 100644 index 7022d71..0000000 --- a/gradle/commonStaticAnalysis.gradle +++ /dev/null @@ -1,233 +0,0 @@ -apply plugin: 'cpd' -apply plugin: 'io.gitlab.arturbosch.detekt' - -def getCpdTask -def getLintTask -def getKotlinDetektTasks - -def appendError -def appendCpdErrors -def appendKotlinErrors -def appendLintErrors - -repositories { - maven { url "https://maven.dev.touchin.ru" } -} - -configurations { - pngtastic -} - -cpd { - skipLexicalErrors = true -} - -import org.apache.tools.ant.taskdefs.condition.Os - -ext.getIdeaFormatTask = { isAndroidProject, sources -> - def ideaPath = System.getenv("IDEA_HOME") - if (ideaPath == null) { - return tasks.create((isAndroidProject ? "android" : "server") + "donothing") - } - return tasks.create((isAndroidProject ? "android" : "server") + "IdeaFormat_$project.name", Exec) { - def inspectionPath - def params = ["-r", "-mask", "*.java,*.kt,*.xml"] - for (String source : sources) { - params.add(source) - } - - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - inspectionPath = ['cmd', '/c', "\"${ideaPath}\\bin\\format.bat\" ${params.join(" ")}"] - } else { - inspectionPath = ["$ideaPath/bin/format.sh"] - } - commandLine inspectionPath - if (!Os.isFamily(Os.FAMILY_WINDOWS)) { - args = params - } - } -} - -ext.getStaticAnalysisTaskNames = { isAndroidProject, sources, buildVariant -> - def tasksNames = new ArrayList() - try { - tasksNames.add(getCpdTask(isAndroidProject, sources)) - tasksNames.addAll(getKotlinDetektTasks()) - if (isAndroidProject) { - tasksNames.add(getLintTask(buildVariant)) - } - } catch (Exception exception) { - println(exception.toString()) - } - return tasksNames -} - -ext.generateReport = { isAndroidProject -> - StringBuilder consoleReport = new StringBuilder() - consoleReport.append("STATIC ANALYSIS RESULTS:") - def count = 0 - - def previousCount = count - count = appendCpdErrors(count, new File("${project.buildDir}/reports/cpd.xml")) - if (count - previousCount > 0) { - consoleReport.append("\nCPD: FAILED (" + (count - previousCount) + " errors)") - } else { - consoleReport.append("\nCPD: PASSED") - } - - previousCount = count - subprojects.forEach { subproject -> - def reportFile = new File("${rootProject.buildDir}/reports/kotlin-detekt-${subproject.name}.xml") - if (reportFile.exists()) { - count = appendKotlinErrors(count, reportFile).toInteger() - } - } - if (count - previousCount > 0) { - consoleReport.append("\nKotlin-detekt: FAILED (" + (count - previousCount) + " errors)") - } else { - consoleReport.append("\nKotlin-detekt: PASSED") - } - - if (isAndroidProject) { - previousCount = count - count = appendLintErrors(count, new File("${rootProject.buildDir}/reports/lint_report.xml")) - if (count - previousCount > 0) { - consoleReport.append("\nLint: FAILED (" + (count - previousCount) + " errors)") - } else { - consoleReport.append("\nLint: PASSED") - } - } - - if (count > 0) { - consoleReport.append("\nOverall: FAILED (" + count + " errors)") - throw new Exception(consoleReport.toString()) - } else { - consoleReport.append("\nOverall: PASSED") - println(consoleReport.toString()) - } -} - -appendError = { number, analyzer, file, line, errorId, errorLink, description -> - println("$number. $analyzer : $description ($errorId)\n\tat $file: $line") -} - -appendKotlinErrors = { count, checkstyleFile -> - def rootNode = new XmlParser().parse(checkstyleFile) - for (def fileNode : rootNode.children()) { - if (!fileNode.name().equals("file")) { - continue - } - - for (def errorNode : fileNode.children()) { - if (!errorNode.name().equals("error")) { - continue - } - count++ - - appendError(count, "Detekt", fileNode.attribute("name"), errorNode.attribute("line"), errorNode.attribute("source"), "", errorNode.attribute("message")) - } - } - return count -} - -appendCpdErrors = { count, cpdFile -> - def rootNode = new XmlParser().parse(cpdFile) - for (def duplicationNode : rootNode.children()) { - if (!duplicationNode.name().equals("duplication")) { - continue - } - count++ - - def duplicationIndex = 0 - - String duplicationPoints = "" - for (def filePointNode : duplicationNode.children()) { - if (filePointNode.name().equals("file")) { - def file = filePointNode.attribute("path") - def line = filePointNode.attribute("line") - duplicationPoints += "\n " + file + ":" + line - duplicationIndex++ - } - } - println("$count CPD: code duplication $duplicationPoints") - } - return count -} - -appendLintErrors = { count, lintFile -> - def rootNode = new XmlParser().parse(lintFile) - for (def issueNode : rootNode.children()) { - if (!issueNode.name().equals("issue") - || !issueNode.attribute("severity").equals("Error")) { - continue - } - for (def locationNode : issueNode.children()) { - if (!locationNode.name().equals("location")) { - continue - } - count++ - appendError(count, "Lint", locationNode.attribute("file"), locationNode.attribute("line"), - issueNode.attribute("id"), issueNode.attribute("explanation"), issueNode.attribute("message")) - } - } - return count -} - -getCpdTask = { isAndroidProject, sources -> - def taskName = (isAndroidProject ? "android" : "server") + "cpd_${rootProject.name}" - def task = tasks.findByName(taskName) - if (task == null) { - task = tasks.create(taskName, tasks.findByName('cpdCheck').getClass().getSuperclass()) { - minimumTokenCount = 60 - source = files(sources) - ignoreFailures = true - reports { - xml { - enabled = true - destination = file("${rootProject.buildDir}/reports/cpd.xml") - } - } - } - } - return task.path -} - -getLintTask = { buildVariant -> - def appProject = subprojects.find { it.plugins.hasPlugin("com.android.application") } - def lintTaskPath - if (buildVariant != null) { - lintTaskPath = ":${appProject.name}:lint${buildVariant.name.capitalize()}" - } else { - def lintDebugTasks = appProject.tasks.matching { it.getName().contains("lint") && it.getName().contains("Debug") } - lintTaskPath = lintDebugTasks.first().path - } - if (lintTaskPath == null) { - throw IllegalStateException("Unable to find lint debug task for build variant: ${buildVariant}") - } - return lintTaskPath -} - -getKotlinDetektTasks = { - subprojects - .findResults { it.tasks.findByName("detekt")?.path } - .findAll { !it.contains(":libs") } -} - -task optimizePng { - doFirst { - def jarArgs = new ArrayList() - jarArgs.add(configurations.pngtastic.asPath) - def relatedPathIndex = "${rootDir}".length() + 1 - for (def file : fileTree(dir: "${rootDir}", include: '**/src/**/res/drawable**/*.png')) { - jarArgs.add(file.absolutePath.substring(relatedPathIndex)) - } - for (def file : fileTree(dir: "${rootDir}", include: '**/src/**/res/mipmap**/*.png')) { - jarArgs.add(file.absolutePath.substring(relatedPathIndex)) - } - javaexec { main = "-jar"; args = jarArgs; workingDir = file("${rootDir}") } - } -} - -dependencies { - pngtastic 'com.github.depsypher:pngtastic:1.2' -} diff --git a/gradle/plugins/.gitignore b/gradle/plugins/.gitignore new file mode 100644 index 0000000..39d12d8 --- /dev/null +++ b/gradle/plugins/.gitignore @@ -0,0 +1,20 @@ +# Generated files +bin/ +gen/ + +# Gradle files +.gradle/ +build/ +/*/build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log Files +*.log + +.gradle +.idea +.DS_Store +/captures +*.iml diff --git a/gradle/plugins/build.gradle.kts b/gradle/plugins/build.gradle.kts new file mode 100644 index 0000000..254b329 --- /dev/null +++ b/gradle/plugins/build.gradle.kts @@ -0,0 +1,56 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-gradle-plugin` + `kotlin-dsl` +} + +// The kotlin-dsl plugin requires a repository to be declared +repositories { + mavenCentral() + google() +} + +dependencies { + // android gradle plugin, required by custom plugin + implementation("com.android.tools.build:gradle:4.0.1") + + implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.10.0") + implementation("de.aaschmid:gradle-cpd-plugin:3.1") + + // kotlin plugin, required by custom plugin + implementation(kotlin("gradle-plugin", embeddedKotlinVersion)) + + gradleKotlinDsl() + implementation(kotlin("stdlib-jdk8")) +} + +val compileKotlin: KotlinCompile by tasks +compileKotlin.kotlinOptions { + jvmTarget = "1.8" +} + +gradlePlugin { + plugins { + create("api-generator-android") { + id = "api-generator-android" + implementationClass = "apigen.ApiGeneratorAndroidPlugin" + } + create("swagger-generator-android") { + id = "swagger-generator-android" + implementationClass = "apigen.SwaggerApiGeneratorAndroidPlugin" + } + create("api-generator-backend") { + id = "api-generator-backend" + implementationClass = "apigen.ApiGeneratorBackendPlugin" + } + create("static-analysis-android") { + id = "static-analysis-android" + implementationClass = "static_analysis.plugins.StaticAnalysisAndroidPlugin" + } + create("static-analysis-backend") { + id = "static-analysis-backend" + implementationClass = "static_analysis.plugins.StaticAnalysisBackendPlugin" + } + } +} diff --git a/gradle/plugins/settings.gradle.kts b/gradle/plugins/settings.gradle.kts new file mode 100644 index 0000000..e69de29 diff --git a/gradle/plugins/src/main/java/apigen/ApiGeneratorAndroidPlugin.kt b/gradle/plugins/src/main/java/apigen/ApiGeneratorAndroidPlugin.kt new file mode 100644 index 0000000..ea06f40 --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/ApiGeneratorAndroidPlugin.kt @@ -0,0 +1,46 @@ +package apigen + +import com.android.build.gradle.AppExtension +import com.android.build.gradle.LibraryExtension +import org.gradle.api.Project +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.findByType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +class ApiGeneratorAndroidPlugin : ApiGeneratorPlugin() { + + override fun apply(target: Project) { + super.apply(target) + + with(target) { + val extension = getExtension() + val outputDir = getDirectoryForGeneration() + + extension.outputDirPath = outputDir.path + extension.recreateOutputDir = true + + afterEvaluate { + extensions.findByType()?.apply { + sourceSets.getByName("main") + .java + .srcDir(outputDir) + } + extensions.findByType()?.apply { + sourceSets.getByName("main") + .java + .srcDir(outputDir) + } + tasks + .filterIsInstance() + .forEach { it.source(outputDir) } + + tasks + .filterIsInstance() + .forEach { it.source(outputDir) } + } + } + } + + private fun Project.getDirectoryForGeneration() = file("$buildDir/generated/api") + +} diff --git a/gradle/plugins/src/main/java/apigen/ApiGeneratorBackendPlugin.kt b/gradle/plugins/src/main/java/apigen/ApiGeneratorBackendPlugin.kt new file mode 100644 index 0000000..b4a8723 --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/ApiGeneratorBackendPlugin.kt @@ -0,0 +1,17 @@ +package apigen + +import org.gradle.api.Project + +class ApiGeneratorBackendPlugin : ApiGeneratorPlugin() { + + override fun apply(target: Project) { + super.apply(target) + + val extension = target.getExtension() + + extension.outputDirPath = target.file("src/main/kotlin").path + extension.recreateOutputDir = false + extension.outputLanguage = OutputLanguage.KotlinServer + + } +} diff --git a/gradle/plugins/src/main/java/apigen/ApiGeneratorExtension.kt b/gradle/plugins/src/main/java/apigen/ApiGeneratorExtension.kt new file mode 100644 index 0000000..76e0ed9 --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/ApiGeneratorExtension.kt @@ -0,0 +1,22 @@ +package apigen + +open class ApiGeneratorExtension( + var pathToApiSchemes: String? = null, + var outputPackageName: String = "", + var outputDirPath: String = "", + var recreateOutputDir: Boolean = false, + var outputLanguage: OutputLanguage? = null +) + +sealed class OutputLanguage(val argName: String, val methodOutputType: MethodOutputType? = null) { + object KotlinServer : OutputLanguage("KOTLIN_SERVER") + class KotlinAndroid(methodOutputType: MethodOutputType = MethodOutputType.Rx) : OutputLanguage("KOTLIN", methodOutputType) + object Java : OutputLanguage("JAVA") + object Swift : OutputLanguage("SWIFT") +} + +sealed class MethodOutputType(val argName: String) { + object Rx : MethodOutputType("REACTIVE") + object RetrofitCall : MethodOutputType("CALL") + object Coroutine : MethodOutputType("COROUTINE") +} diff --git a/gradle/plugins/src/main/java/apigen/ApiGeneratorPlugin.kt b/gradle/plugins/src/main/java/apigen/ApiGeneratorPlugin.kt new file mode 100644 index 0000000..fcaf17a --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/ApiGeneratorPlugin.kt @@ -0,0 +1,76 @@ +package apigen + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.repositories + +abstract class ApiGeneratorPlugin : Plugin { + + companion object { + const val API_GENERATOR_CONFIG = "apiGenerator" + const val API_GENERATOR_EXT_NAME = "apiGenerator" + const val API_GENERATOR_DEFAULT_VERSION = "1.4.0-beta10" + } + + override fun apply(target: Project) { + with(target) { + repositories { + maven { + url = uri("https://maven.dev.touchin.ru") + metadataSources { + artifact() + } + } + } + + configurations.create(API_GENERATOR_CONFIG) + + dependencies { + add(API_GENERATOR_CONFIG, "ru.touchin:api-generator:$API_GENERATOR_DEFAULT_VERSION") + } + + extensions.create(API_GENERATOR_EXT_NAME) + + val apiGenTask = createApiGeneratorTask() + + gradle.projectsEvaluated { + tasks.getByName("preBuild").dependsOn(apiGenTask) + } + } + } + + protected fun Project.getExtension(): ApiGeneratorExtension = extensions.getByName(API_GENERATOR_EXT_NAME) as ApiGeneratorExtension + + private fun Project.createApiGeneratorTask(): Task = tasks.create(API_GENERATOR_CONFIG).doLast { + + val extension = getExtension() + + val pathToApiSchemes = extension.pathToApiSchemes ?: throw IllegalStateException("Configure path to api schemes for api generator plugin") + val outputLanguage = extension.outputLanguage ?: throw IllegalStateException("Configure output language code for api generator plugin") + + javaexec { + main = "-jar" + workingDir = rootDir + args = listOfNotNull( + configurations.getByName("apiGenerator").asPath, + "generate-client-code", + "--output-language", + outputLanguage.argName, + "--specification-path", + pathToApiSchemes, + "--kotlin-methods-generation-mode".takeIf { outputLanguage.methodOutputType != null }, + outputLanguage.methodOutputType?.argName, + "--output-path", + extension.outputDirPath, + "--package-name", + extension.outputPackageName, + "--recreate_output_dirs", + extension.recreateOutputDir.toString() + ) + } + } + +} diff --git a/gradle/plugins/src/main/java/apigen/SwaggerApiGeneratorAndroidPlugin.kt b/gradle/plugins/src/main/java/apigen/SwaggerApiGeneratorAndroidPlugin.kt new file mode 100644 index 0000000..82bc1ca --- /dev/null +++ b/gradle/plugins/src/main/java/apigen/SwaggerApiGeneratorAndroidPlugin.kt @@ -0,0 +1,91 @@ +package apigen + +import org.gradle.api.Action +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.repositories + +class SwaggerApiGeneratorAndroidPlugin : Plugin { + + private companion object { + const val GENERATOR_CONFIG = "swaggerCodegen" + const val GENERATOR_VERSION = "3.0.34" + const val TI_GENERATOR_CONFIG = "TIKotlin-swagger-codegen" + const val TI_GENERATOR_VERSION = "1.0.0" + const val GENERATOR_EXT_NAME = "swaggerApiGenerator" + const val MAVEN_URL = "https://maven.dev.touchin.ru" + } + + override fun apply(target: Project) { + with(target) { + repositories { + maven { + url = uri(MAVEN_URL) + metadataSources { + artifact() + } + } + } + + configurations.create(GENERATOR_CONFIG) + configurations.create(TI_GENERATOR_CONFIG) + + dependencies { + add(TI_GENERATOR_CONFIG, "ru.touchin:TIKotlin-swagger-codegen:$TI_GENERATOR_VERSION") + add(GENERATOR_CONFIG, "io.swagger.codegen.v3:swagger-codegen-cli:$GENERATOR_VERSION") + } + + extensions.create(GENERATOR_EXT_NAME) + + val apiGenTask = createSwaggerApiGeneratorTask() + + gradle.projectsEvaluated { + tasks.getByName("preBuild").dependsOn(apiGenTask) + } + } + } + + protected fun Project.getExtension(): SwaggerApiGeneratorExtension = extensions.getByName(GENERATOR_EXT_NAME) as SwaggerApiGeneratorExtension + + private fun Project.createSwaggerApiGeneratorTask(): Task = tasks.create(GENERATOR_CONFIG).doLast { + + val extension = getExtension() + + val taskWorkingDir = extension.taskWorkingDir ?: throw IllegalStateException("Configure taskWorkingDir for swagger generator plugin") + val apiSchemesFilePath = extension.apiSchemesFilePath ?: throw IllegalStateException("Configure sourceFilePath for swagger generator plugin") + val outputDir = extension.outputDir ?: throw IllegalStateException("Configure outputDir for swagger generator plugin") + val projectName = extension.projectName ?: throw IllegalStateException("Configure projectName for swagger generator plugin") + + javaexec { + workingDir = file(taskWorkingDir) + classpath = files(configurations.getByName(GENERATOR_CONFIG).asPath, + configurations.getByName(TI_GENERATOR_CONFIG).asPath) + main = "io.swagger.codegen.v3.cli.SwaggerCodegen" + args = listOfNotNull( + "generate", + "-i", + apiSchemesFilePath, + "-l", + "TIKotlinCodegen", + "-o", + outputDir, + "--additional-properties", + "projectName=$projectName" + ) + } + } + +} + +open class SwaggerApiGeneratorExtension( + var taskWorkingDir: String? = null, + var apiSchemesFilePath: String? = null, + var outputDir: String? = null, + var projectName: String? = null +) + +fun Project.swaggerApiGenerator(configure: Action): Unit = + (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("swaggerApiGenerator", configure) diff --git a/gradle/plugins/src/main/java/static_analysis/errors/AndroidLintError.kt b/gradle/plugins/src/main/java/static_analysis/errors/AndroidLintError.kt new file mode 100644 index 0000000..6b87d81 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/errors/AndroidLintError.kt @@ -0,0 +1,15 @@ +package static_analysis.errors + +class AndroidLintError( + private val filePath: String, + private val fileLine: String?, + private val errorId: String, + private val description: String +) : StaticAnalysisError { + + override fun print(count: Int): String = "\n$count. Android Lint. $description ($errorId)\n\tat [$filePath$fileLinePrefix]" + + private val fileLinePrefix: String + get() = fileLine?.let { ":$it" }.orEmpty() + +} diff --git a/gradle/plugins/src/main/java/static_analysis/errors/CpdError.kt b/gradle/plugins/src/main/java/static_analysis/errors/CpdError.kt new file mode 100644 index 0000000..cca1c8a --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/errors/CpdError.kt @@ -0,0 +1,12 @@ +package static_analysis.errors + +class CpdError( + private val duplications: List>, + private val codeFragment: String +) : StaticAnalysisError { + + override fun print(count: Int): String = "\n$count. CPD. Code duplication in files: " + + duplications.joinToString(separator = "") { (file, line) -> "\n\t[$file:$line]" } + + "\n\n Duplicated code:\n\n$codeFragment\n" + +} diff --git a/gradle/plugins/src/main/java/static_analysis/errors/DetektError.kt b/gradle/plugins/src/main/java/static_analysis/errors/DetektError.kt new file mode 100644 index 0000000..3aa9c09 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/errors/DetektError.kt @@ -0,0 +1,12 @@ +package static_analysis.errors + +class DetektError( + private val filePath: String, + private val fileLine: String, + private val errorId: String, + private val description: String +) : StaticAnalysisError { + + override fun print(count: Int): String = "\n$count. Detekt. $description ($errorId)\n\tat [$filePath:$fileLine]" + +} diff --git a/gradle/plugins/src/main/java/static_analysis/errors/StaticAnalysisError.kt b/gradle/plugins/src/main/java/static_analysis/errors/StaticAnalysisError.kt new file mode 100644 index 0000000..f932aca --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/errors/StaticAnalysisError.kt @@ -0,0 +1,5 @@ +package static_analysis.errors + +interface StaticAnalysisError { + fun print(count: Int): String +} diff --git a/gradle/plugins/src/main/java/static_analysis/linters/AndroidLinter.kt b/gradle/plugins/src/main/java/static_analysis/linters/AndroidLinter.kt new file mode 100644 index 0000000..fa6ff5b --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/linters/AndroidLinter.kt @@ -0,0 +1,72 @@ +package static_analysis.linters + +import com.android.build.gradle.AppExtension +import com.android.build.gradle.AppPlugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.findByType +import static_analysis.errors.AndroidLintError +import static_analysis.errors.StaticAnalysisError +import static_analysis.plugins.StaticAnalysisExtension +import static_analysis.utils.typedChildren +import static_analysis.utils.xmlParser + +class AndroidLinter : Linter { + + override val name: String = "Android lint" + + override fun getErrors(project: Project): List = xmlParser(project.getLintReportFile()) + .typedChildren() + .filter { it.name() == "issue" && (it.attribute("severity") as String) == "Error" } + .map { errorNode -> + errorNode + .typedChildren() + .filter { it.name() == "location" } + .map { locationNode -> + AndroidLintError( + filePath = locationNode.attribute("file") as String, + fileLine = locationNode.attribute("line") as String?, + errorId = errorNode.attribute("id") as String, + description = errorNode.attribute("message") as String + ) + } + } + .flatten() + + override fun setupForProject(project: Project, extension: StaticAnalysisExtension) { + project.beforeEvaluate { + subprojects + .mapNotNull { it.extensions.findByType() } + .first() + .lintOptions.apply { + isAbortOnError = false + isCheckAllWarnings = true + isWarningsAsErrors = false + xmlReport = true + htmlReport = false + isCheckDependencies = true + disable("MissingConstraints", "VectorRaster") + xmlOutput = getLintReportFile() + lintConfig = file("${extension.buildScriptDir}/static_analysis_configs/lint.xml") + } + } + } + + override fun getTaskNames(project: Project, buildType: String?): List { + if (buildType == null) { + throw IllegalStateException("Build type must not be null in android linter") + } + + return project + .subprojects + .filter { it.plugins.hasPlugin(AppPlugin::class.java) } + .mapNotNull { subproject: Project -> + subproject + .tasks + .find { task -> task.name.contains(buildType, ignoreCase = true) && task.name.contains("lint") } + ?.path + } + } + + private fun Project.getLintReportFile() = file("${rootProject.buildDir}/reports/lint-report.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 new file mode 100644 index 0000000..68e4be7 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/linters/CpdLinter.kt @@ -0,0 +1,60 @@ +package static_analysis.linters + +import de.aaschmid.gradle.plugins.cpd.Cpd +import de.aaschmid.gradle.plugins.cpd.CpdExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.withType +import static_analysis.errors.CpdError +import static_analysis.errors.StaticAnalysisError +import static_analysis.plugins.StaticAnalysisExtension +import static_analysis.utils.getSources +import static_analysis.utils.typedChildren +import static_analysis.utils.xmlParser + +class CpdLinter : Linter { + + override val name: String = "CPD" + + override fun getErrors(project: Project): List = xmlParser(project.getCpdReportFile()) + .typedChildren() + .filter { it.name() == "duplication" } + .map { duplicationNode -> + + val children = duplicationNode + .typedChildren() + + CpdError( + duplications = children + .filter { it.name() == "file" } + .map { fileNode -> fileNode.attribute("path") as String to fileNode.attribute("line") as String }, + codeFragment = children.findLast { it.name() == "codefragment" }!!.text() + ) + + } + + override fun setupForProject(project: Project, extension: StaticAnalysisExtension) { + 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) + } + } + } + + override fun getTaskNames(project: Project, buildType: String?): List = project + .rootProject + .tasks + .withType() + .map(Cpd::getPath) + + private fun Project.getCpdReportFile() = file("${rootProject.buildDir}/reports/cpd.xml") + +} diff --git a/gradle/plugins/src/main/java/static_analysis/linters/DetektLinter.kt b/gradle/plugins/src/main/java/static_analysis/linters/DetektLinter.kt new file mode 100644 index 0000000..efb1ea8 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/linters/DetektLinter.kt @@ -0,0 +1,62 @@ +package static_analysis.linters + +import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.api.Project +import static_analysis.errors.DetektError +import static_analysis.errors.StaticAnalysisError +import static_analysis.plugins.StaticAnalysisExtension +import static_analysis.utils.getSources +import static_analysis.utils.typedChildren +import static_analysis.utils.xmlParser + +class DetektLinter : Linter { + + override val name: String = "Detekt" + + override fun getErrors(project: Project): List = xmlParser(project.getDetektReportFile()) + .typedChildren() + .filter { fileNode -> fileNode.name() == "file" } + .map { fileNode -> + fileNode + .typedChildren() + .filter { it.name() == "error" } + .map { errorNode -> + DetektError( + filePath = fileNode.attribute("name") as String, + fileLine = errorNode.attribute("line") as String, + errorId = errorNode.attribute("source") as String, + description = errorNode.attribute("message") as String + ) + } + } + .flatten() + + override fun setupForProject(project: Project, extension: StaticAnalysisExtension) { + project.afterEvaluate { + tasks.withType(Detekt::class.java) { + exclude("**/test/**") + exclude("resources/") + exclude("build/") + exclude("tmp/") + jvmTarget = "1.8" + + config.setFrom(files("${extension.buildScriptDir!!}/static_analysis_configs/detekt-config.yml")) + reports { + txt.enabled = false + html.enabled = false + xml { + enabled = true + destination = getDetektReportFile() + } + } + + source = getSources(extension.excludes) + } + } + } + + override fun getTaskNames(project: Project, buildType: String?): List = listOf(":detekt") + + private fun Project.getDetektReportFile() = file("${rootProject.buildDir}/reports/detekt.xml") + +} diff --git a/gradle/plugins/src/main/java/static_analysis/linters/Linter.kt b/gradle/plugins/src/main/java/static_analysis/linters/Linter.kt new file mode 100644 index 0000000..65421a1 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/linters/Linter.kt @@ -0,0 +1,12 @@ +package static_analysis.linters + +import org.gradle.api.Project +import static_analysis.errors.StaticAnalysisError +import static_analysis.plugins.StaticAnalysisExtension + +interface Linter { + val name: String + fun getErrors(project: Project): List + fun setupForProject(project: Project, extension: StaticAnalysisExtension) + fun getTaskNames(project: Project, buildType: String? = null): List +} diff --git a/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisAndroidPlugin.kt b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisAndroidPlugin.kt new file mode 100644 index 0000000..6898784 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisAndroidPlugin.kt @@ -0,0 +1,41 @@ +package static_analysis.plugins + +import com.android.build.gradle.AppExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import static_analysis.linters.AndroidLinter +import static_analysis.linters.CpdLinter +import static_analysis.linters.DetektLinter +import static_analysis.linters.Linter + +class StaticAnalysisAndroidPlugin : StaticAnalysisPlugin() { + + override fun createStaticAnalysisTasks(project: Project, linters: List) { + project.subprojects { + if (plugins.hasPlugin("com.android.application")) { + + extensions.getByType().apply { + applicationVariants.forEach { variant -> + project.tasks.register("staticAnalysis${variant.name.capitalize()}") { + setupStaticAnalysisTask(linters, variant.name) + } + } + + project.tasks.register("staticAnalysis") { + setupStaticAnalysisTask( + linters = linters, + buildVariant = applicationVariants.first { it.name.contains("Debug") }.name + ) + } + } + } + } + } + + override fun createLinters(): List = listOf( + DetektLinter(), +// CpdLinter(), +// AndroidLinter() + ) + +} diff --git a/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisBackendPlugin.kt b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisBackendPlugin.kt new file mode 100644 index 0000000..fa35f1d --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisBackendPlugin.kt @@ -0,0 +1,21 @@ +package static_analysis.plugins + +import org.gradle.api.Project +import static_analysis.linters.CpdLinter +import static_analysis.linters.DetektLinter +import static_analysis.linters.Linter + +class StaticAnalysisBackendPlugin : StaticAnalysisPlugin() { + + override fun createStaticAnalysisTasks(project: Project, linters: List) { + project.tasks.register("staticAnalysis") { + setupStaticAnalysisTask(linters) + } + } + + override fun createLinters(): List = listOf( + CpdLinter(), + DetektLinter() + ) + +} diff --git a/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisExtension.kt b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisExtension.kt new file mode 100644 index 0000000..dfa698d --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisExtension.kt @@ -0,0 +1,6 @@ +package static_analysis.plugins + +open class StaticAnalysisExtension( + var excludes: String = "", + var buildScriptDir: String? = null +) diff --git a/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisPlugin.kt b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisPlugin.kt new file mode 100644 index 0000000..369fb90 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/plugins/StaticAnalysisPlugin.kt @@ -0,0 +1,45 @@ +package static_analysis.plugins + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.getByType +import static_analysis.linters.Linter +import static_analysis.utils.ReportGenerator + +abstract class StaticAnalysisPlugin : Plugin { + + companion object { + const val DETEKT_ID = "io.gitlab.arturbosch.detekt" + const val CPD_ID = "de.aaschmid.cpd" + const val STATIC_ANALYSIS_EXT_NAME = "staticAnalysis" + } + + override fun apply(target: Project) { + + with(target) { + pluginManager.apply(CPD_ID) + pluginManager.apply(DETEKT_ID) + + extensions.create(STATIC_ANALYSIS_EXT_NAME) + + val linters = createLinters() + + linters.forEach { it.setupForProject(target, extensions.getByType()) } + + gradle.projectsEvaluated { + createStaticAnalysisTasks(target, linters) + } + } + } + + fun Task.setupStaticAnalysisTask(linters: List, buildVariant: String? = null) { + doFirst { ReportGenerator.generate(linters, project) } + dependsOn(*(linters.map { it.getTaskNames(project, buildVariant) }.flatten().toTypedArray())) + } + + abstract fun createLinters(): List + abstract fun createStaticAnalysisTasks(project: Project, linters: List) + +} diff --git a/gradle/plugins/src/main/java/static_analysis/utils/Node.kt b/gradle/plugins/src/main/java/static_analysis/utils/Node.kt new file mode 100644 index 0000000..e8618e2 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/utils/Node.kt @@ -0,0 +1,5 @@ +package static_analysis.utils + +import groovy.util.Node + +fun Node.typedChildren() = children() as List diff --git a/gradle/plugins/src/main/java/static_analysis/utils/Project.kt b/gradle/plugins/src/main/java/static_analysis/utils/Project.kt new file mode 100644 index 0000000..f1c46cf --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/utils/Project.kt @@ -0,0 +1,27 @@ +package static_analysis.utils + +import org.gradle.api.Project +import org.gradle.api.file.FileTree +import java.io.File + +fun Project.getSources(excludes: String): FileTree = files( + project + .rootProject + .subprojects + .filter { subproject -> subproject.subprojects.isEmpty() && !excludes.contains(subproject.path) } + .map { subproject -> subproject.file("${subproject.projectDir.path}/src/main") } + .filter { it.exists() && it.isDirectory } + .flatMap { srcDir -> + srcDir + .listFiles() + .orEmpty() + .flatMap { + listOf( + File(srcDir.path, "java"), + File(srcDir.path, "kotlin") + ) + } + } + .filter { it.exists() && it.isDirectory } + .map { it.path } +).asFileTree diff --git a/gradle/plugins/src/main/java/static_analysis/utils/ReportGenerator.kt b/gradle/plugins/src/main/java/static_analysis/utils/ReportGenerator.kt new file mode 100644 index 0000000..b18f8f4 --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/utils/ReportGenerator.kt @@ -0,0 +1,52 @@ +package static_analysis.utils + +import org.gradle.api.Project +import static_analysis.errors.StaticAnalysisError +import static_analysis.linters.Linter + +object ReportGenerator { + + fun generate(linters: List, project: Project) { + + val groupedErrors = linters + .map { linter -> linter to linter.getErrors(project) } + + val lintersResults = groupedErrors + .map { (linter, linterErrors) -> linter.name to linterErrors.size } + + val allErrors = groupedErrors + .map(Pair>::second) + .flatten() + + val consoleReport = StringBuilder("\nSTATIC ANALYSIS ERRORS:").apply { + appendAllErrors(allErrors) + append("\nREPORT:\n") + appendReportsSummary(lintersResults) + appendOverallSummary(allErrors) + } + + if (allErrors.isEmpty()) { + println(consoleReport) + } else { + throw Exception(consoleReport.toString()) + } + + } + + private fun StringBuilder.appendAllErrors(errors: List) = errors + .mapIndexed { index, staticAnalysisError -> staticAnalysisError.print(index + 1) } + .forEach { error -> append(error) } + + private fun StringBuilder.appendReportsSummary(lintersResults: List>) = lintersResults + .forEach { this.appendSummary(it.first, it.second) } + + private fun StringBuilder.appendOverallSummary(errors: List) = appendSummary("Overall", errors.size) + + private fun StringBuilder.appendSummary(header: String, quantityOfErrors: Int) { + assert(quantityOfErrors < 0) + + append("\n$header: ") + append(if (quantityOfErrors == 0) "PASSED" else "FAILED ($quantityOfErrors errors)") + } + +} diff --git a/gradle/plugins/src/main/java/static_analysis/utils/XmlUtils.kt b/gradle/plugins/src/main/java/static_analysis/utils/XmlUtils.kt new file mode 100644 index 0000000..1754f4c --- /dev/null +++ b/gradle/plugins/src/main/java/static_analysis/utils/XmlUtils.kt @@ -0,0 +1,6 @@ +package static_analysis.utils + +import groovy.util.XmlParser +import java.io.File + +fun xmlParser(file: File) = XmlParser().parse(file) diff --git a/gradle/applicationFileNaming.gradle b/gradle/scripts/applicationFileNaming.gradle similarity index 100% rename from gradle/applicationFileNaming.gradle rename to gradle/scripts/applicationFileNaming.gradle diff --git a/gradle/stringGenerator.gradle b/gradle/scripts/stringGenerator.gradle similarity index 83% rename from gradle/stringGenerator.gradle rename to gradle/scripts/stringGenerator.gradle index a05b155..9c47e90 100644 --- a/gradle/stringGenerator.gradle +++ b/gradle/scripts/stringGenerator.gradle @@ -2,11 +2,11 @@ import groovy.json.JsonSlurper import groovy.xml.MarkupBuilder task stringGenerator { - generate(android.languageMap) + generate(android.languageMap, project) println("Strings generated!") } -private def generate(Map sources) { +private def generate(Map sources, Project project) { if (sources == null || sources.isEmpty()) { throw new IOException("languageMap can't be null or empty") } @@ -31,7 +31,7 @@ private def generate(Map sources) { } } - def stringsFile = getFile(key, key == defaultLang) + def stringsFile = getFile(key, key == defaultLang, project) stringsFile.write(sw.toString(), "UTF-8") } } @@ -69,15 +69,15 @@ private static Map getJsonsMap(Map sources) { } } -private static File getFile(String key, boolean defaultLang) { +private static File getFile(String key, boolean defaultLang, Project project) { if (defaultLang) { - return new File("app/src/main/res/values/strings.xml") + return project.file("src/main/res/values/strings.xml") } else { - def directory = new File("app/src/main/res/values-$key") + def directory = project.file("src/main/res/values-$key") if (!directory.exists()) { directory.mkdir() } - return new File("app/src/main/res/values-$key/strings.xml") + return project.file("src/main/res/values-$key/strings.xml") } } diff --git a/gradle/staticAnalysis.gradle b/gradle/staticAnalysis.gradle deleted file mode 100644 index b952ec9..0000000 --- a/gradle/staticAnalysis.gradle +++ /dev/null @@ -1,166 +0,0 @@ -buildscript { - repositories { - maven { url "https://plugins.gradle.org/m2" } - } - dependencies { - classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.5.1" - } -} - -def getServerProjectSources -def getAndroidProjectSources - -apply from: "$buildScriptsDir/gradle/commonStaticAnalysis.gradle" - -gradle.projectsEvaluated { - - tasks.withType(JavaCompile) { - options.compilerArgs << - "-Xlint:cast" << - "-Xlint:divzero" << - "-Xlint:empty" << - "-Xlint:deprecation" << - "-Xlint:finally" << - "-Xlint:overrides" << - "-Xlint:path" << - "-Werror" - } - - def excludes = rootProject.extensions.findByName("staticAnalysisExcludes") - - def androidSources = getAndroidProjectSources(excludes) - def androidStaticAnalysisTasks = getStaticAnalysisTaskNames(true, androidSources, null) - def androidIdeaFormatTask = getIdeaFormatTask(true, androidSources) - - task staticAnalysisWithFormatting { - androidStaticAnalysisTasks.each { task -> - tasks.findByName(task)?.mustRunAfter(androidIdeaFormatTask) - } - dependsOn androidIdeaFormatTask - dependsOn androidStaticAnalysisTasks - doFirst { - generateReport(true) - } - } - - task staticAnalysis { - dependsOn androidStaticAnalysisTasks - doFirst { - generateReport(true) - } - } - - def serverStaticAnalysisTasks = getStaticAnalysisTaskNames(false, getServerProjectSources(excludes), null) - def serverIdeaFormatTask = getIdeaFormatTask(false, getServerProjectSources(excludes)) - - task serverStaticAnalysisWithFormatting { - serverStaticAnalysisTasks.each { task -> - tasks.findByName(task)?.mustRunAfter(serverIdeaFormatTask) - } - dependsOn serverIdeaFormatTask - dependsOn serverStaticAnalysisTasks - doFirst { - generateReport(false) - } - } - - task serverStaticAnalysis { - dependsOn serverStaticAnalysisTasks - doFirst { - generateReport(false) - } - } - - subprojects { subproject -> - if (subproject.plugins.hasPlugin("com.android.application")) { - subproject.android { - lintOptions.abortOnError = false - lintOptions.checkAllWarnings = true - lintOptions.warningsAsErrors = false - lintOptions.xmlReport = true - lintOptions.xmlOutput = file "$rootProject.buildDir/reports/lint_report.xml" - lintOptions.htmlReport = false - lintOptions.lintConfig = file "$buildScriptsDir/lint/lint.xml" - lintOptions.checkDependencies true - lintOptions.disable 'MissingConstraints', 'VectorRaster' - - applicationVariants.all { variant -> - task("staticAnalysis${variant.name.capitalize()}") { - dependsOn getStaticAnalysisTaskNames(true, androidSources, variant) - doFirst { generateReport(true) } - } - } - } - } - - def regex = ~':detekt$' - tasks.forEach { task -> - if (!task.name.contains(":libs") && task.path =~ regex) { - task.exclude '**/test/**' - task.exclude 'resources/' - task.exclude 'build/' - task.exclude 'tmp/' - - task.jvmTarget = "1.8" - } - } - - detekt { - config = files("$buildScriptsDir/kotlin/detekt-config.yml") - - reports { - txt.enabled = false - html.enabled = false - xml { - enabled = true - destination = file("${rootProject.buildDir}/reports/kotlin-detekt-${subproject.name}.xml") - } - } - } - } -} - -getServerProjectSources = { excludes -> - def sources = new ArrayList() - def sourcesDirectory = new File(project.projectDir.path, 'src') - - for (def sourceFlavorDirectory : sourcesDirectory.listFiles()) { - def javaSourceDirectory = new File(sourceFlavorDirectory.path, 'java') - def kotlinSourceDirectory = new File(sourceFlavorDirectory.path, 'kotlin') - - if (javaSourceDirectory.exists() && javaSourceDirectory.isDirectory()) { - sources.add(javaSourceDirectory.absolutePath) - } - if (kotlinSourceDirectory.exists() && kotlinSourceDirectory.isDirectory()) { - sources.add(kotlinSourceDirectory.absolutePath) - } - } - return sources -} - -getAndroidProjectSources = { excludes -> - def sources = new ArrayList() - for (def project : rootProject.subprojects) { - if (!project.subprojects.isEmpty() || (excludes != null && excludes.contains(project.path))) { - continue - } - - def sourcesDirectory = new File(project.projectDir.path, 'src') - if (!sourcesDirectory.exists() || !sourcesDirectory.isDirectory()) { - continue - } - - for (def sourceFlavorDirectory : sourcesDirectory.listFiles()) { - def javaSourceDirectory = new File(sourceFlavorDirectory.path, 'java') - def kotlinSourceDirectory = new File(sourceFlavorDirectory.path, 'kotlin') - - if (javaSourceDirectory.exists() && javaSourceDirectory.isDirectory()) { - sources.add(javaSourceDirectory.absolutePath) - } - if (kotlinSourceDirectory.exists() && kotlinSourceDirectory.isDirectory()) { - sources.add(kotlinSourceDirectory.absolutePath) - } - } - } - return sources -} diff --git a/lint/lint.xml b/lint/lint.xml deleted file mode 100644 index 57a80e9..0000000 --- a/lint/lint.xml +++ /dev/null @@ -1,272 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pmd/rulesets/java/android.xml b/pmd/rulesets/java/android.xml deleted file mode 100644 index debe6f4..0000000 --- a/pmd/rulesets/java/android.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - Every Java Rule in PMD - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/proguard/common.pro b/proguard/common.pro index 2e69ab7..d7f2023 100644 --- a/proguard/common.pro +++ b/proguard/common.pro @@ -1,5 +1,4 @@ -include rules/components.pro - -include rules/okhttp.pro -include rules/retrofit.pro -include rules/logansquare.pro @@ -8,4 +7,3 @@ -include rules/kaspersky.pro -include rules/appsflyer.pro -include rules/moshi.pro --include rules/androidx_security.pro diff --git a/proguard/rules/moshi.pro b/proguard/rules/moshi.pro index 3bae28e..fe81601 100644 --- a/proguard/rules/moshi.pro +++ b/proguard/rules/moshi.pro @@ -6,6 +6,7 @@ } -keep @com.squareup.moshi.JsonQualifier interface * +-keep @com.squareup.moshi.JsonQualifier class * # Enum field names are used by the integrated EnumJsonAdapter. # values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly diff --git a/scripts/export_src.sh b/scripts/export_src.sh new file mode 100755 index 0000000..20c12bc --- /dev/null +++ b/scripts/export_src.sh @@ -0,0 +1,71 @@ +#!/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 + +LAST_COMMIT_DATE="" +PROJECT_NAME=$1 +SRC_FOLDER_NAME="${PROJECT_NAME}-src" +SRC_DIR="./${SRC_FOLDER_NAME}" + +COMMAND_LINE_ARGUMENTS=$@ + +clone_platform() { + PROJECT_NAME=$1 + PLATFORM=$2 + + 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'` + if [[ $LAST_COMMIT_DATE < $COMMIT_DATE ]]; then + LAST_COMMIT_DATE="${COMMIT_DATE}" + fi + + cd .. + else + exit 1 + fi +} + +mkdir -p "${SRC_DIR}" +cd "${SRC_DIR}" + +for argument in ${COMMAND_LINE_ARGUMENTS} +do + if [ $argument != $PROJECT_NAME ]; then + platform=${argument} # all arguments after project name treated as platforms + clone_platform ${PROJECT_NAME} ${platform} + fi +done + +ERR_PATHS=$(find . -name "*[<>:\\|?*]*" | xargs -I %s echo "- %s") +if [ "$ERR_PATHS" ]; then + echo "Export aborted! Invalid characters found in file or directories name(s):\n$ERR_PATHS" + exit 1 +fi + +if [ -z "${EXPORT_DATE}" ]; then + EXPORT_DATE="${LAST_COMMIT_DATE}" +fi + +find . -name ".git*" -print0 | xargs -0 rm -rf +zip -r -q "${SRC_FOLDER_NAME}-${EXPORT_DATE}".zip . + +open . diff --git a/kotlin/detekt-config.yml b/static_analysis_configs/detekt-config.yml similarity index 99% rename from kotlin/detekt-config.yml rename to static_analysis_configs/detekt-config.yml index 3afb6fa..8a6b3e4 100644 --- a/kotlin/detekt-config.yml +++ b/static_analysis_configs/detekt-config.yml @@ -23,7 +23,7 @@ formatting: active: true console-reports: - active: true + active: false exclude: # - 'ProjectStatisticsReport' # - 'ComplexityReport' @@ -387,9 +387,6 @@ style: UnusedPrivateMember: active: true allowedNames: "(_|ignored|expected|serialVersionUID)" - UseDataClass: - active: true - excludeAnnotatedClasses: "" UtilityClassWithPublicConstructor: active: false VarCouldBeVal: diff --git a/static_analysis_configs/lint.xml b/static_analysis_configs/lint.xml new file mode 100644 index 0000000..98227d4 --- /dev/null +++ b/static_analysis_configs/lint.xml @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xcode/.swiftlint.yml b/xcode/.swiftlint.yml index d502145..ceee568 100644 --- a/xcode/.swiftlint.yml +++ b/xcode/.swiftlint.yml @@ -15,7 +15,6 @@ opt_in_rules: # idiomatic - - legacy_random - legacy_multiple - pattern_matching_keywords - redundant_nil_coalescing @@ -32,6 +31,12 @@ opt_in_rules: - fatal_error_message - extension_access_modifier - explicit_init + - fallthrough + - unavailable_function + - prefer_zero_over_explicit_init + - discouraged_assert + - discouraged_none_name + - shorthand_optional_binding # style @@ -53,27 +58,46 @@ opt_in_rules: - closure_spacing - closure_end_indentation - prefer_self_type_over_type_of_self + - closure_parameter_position + - comma_inheritance + - self_binding + - prefer_self_in_static_references + - direct_return + - period_spacing # lint - private_action - private_outlet - prohibited_super_call - - unused_import - - unused_declaration - identical_operands - overridden_super_call - unowned_variable_capture + - strong_iboutlet + - lower_acl_than_parent + - comment_spacing + - ibinspectable_in_extension + - private_subject + - unhandled_throwing_task # metrics - enum_case_associated_values_count +analyzer_rules: + - capture_variable + - typesafe_array_init + - unused_declaration + - unused_import + excluded: - Carthage - Pods - Generated - - Localization + - "**/Generated" + - "**/Resources" + - ".gem" + - "**/*.app" line_length: warning: 128 @@ -105,40 +129,30 @@ identifier_name: - id - ok - URL + - qr - x - y - z warning_threshold: 1 +allow_zero_lintable_files: true + custom_rules: # General - uiwebview_disabled: - included: ".*.swift" - name: "UIWebView Usage Disabled" - regex: 'UIWebView' - message: "Do not use UIWebView. Use WKWebView Instead. https://developer.apple.com/reference/uikit/uiwebview" - severity: error - - native_print: - name: "print -> DDLog" - regex: '(print|NSLog)\(' - message: "Please use CocoaLumberjack instead `print` and `NSlog`" - severity: error - - uiedge_insets_zero: - name: "UIEdgeInsets .zero" - regex: '\(top: 0, left: 0, bottom: 0, right: 0\)' - message: "Please use short init `.zero`." - severity: error - - let_variable: - name: "Let Variable" - regex: 'var\s\w*(:|(\s=))\sVariable' - message: "Please make variable using `let`." + unsecure_logging: + name: "Unsecure logging" + regex: '\s(print|debugPrint|NSLog)\(' + message: "Please use os_log or remove this debug statement" severity: error + excluded_match_kinds: + - comment + - comment.mark + - comment.url + - doccomment + - doccomment.field marks_style: name: "Marks" @@ -159,19 +173,12 @@ custom_rules: message: "Type definition not needed" severity: error - unowned: - name: "Unowned" + unsafe_unowned: + name: "Unsafe unowned usage" regex: 'unowned' - message: "Please use `weak` instead. " + message: "Please use `weak` instead." severity: error - continue_keyword: - name: "Continue" - regex: 'continue' - message: "Don't use continue instruction" - severity: error - match_kinds: keyword - cyrillic_strings: name: "Cyrillic strings" regex: '[а-яА-Я]+' @@ -186,26 +193,12 @@ custom_rules: regex: '(?!\n)[^ \n]+ {2,}.+' message: "Remove excess empty spaces" severity: warning - match_kinds: - - argument - - attribute.builtin - - attribute.id - - buildconfig.id - - buildconfig.keyword - - identifier - - keyword - - number - - objectliteral - - parameter - - placeholder - # - string # all except string literals - # - comment # and comments - # - comment.mark - # - comment.url - # - doccomment - # - doccomment.field - - string_interpolation_anchor - - typeidentifier + excluded_match_kinds: + - comment + - comment.mark + - comment.url + - doccomment + - doccomment.field getter_setter_style: name: "Wrong getter/setter code style" @@ -213,43 +206,61 @@ custom_rules: match_kinds: - keyword message: "Make a new line break when use getter or setter" - severity: error + severity: warning redundant_boolean_condition: name: "Redundant Boolean Condition" regex: "(== true)|(== false)|(!= true)|(!= false)" message: "Comparing a boolean to true is redundant (use `?? false` for optionals), and `!`-syntax is preferred over comparing to false." - severity: error + severity: warning + excluded_match_kinds: + - comment + - comment.mark + - comment.url + - doccomment + - doccomment.field redundant_ternary_operator: name: "Redundant Ternary Operator" regex: "(\\? true \\: false)|(\\? false \\: true)" message: "Returning a boolean as true is redundant, and `!`-syntax is preferred over returning as false." - severity: error + severity: warning single_line_closure: name: "Single line closure" regex: '\{([^\n\/]*\[[^\]]+\][^\n\/]*)?([^\n\/]*[a-zA-Z]\w*(, \w+)*)? in [^\n\/]+' message: "Too complex expression for single line closure. Improve readability by making it multiline." - severity: error + severity: warning addSubview_in_cell: name: "Usage addSubview in cell" regex: '(extension|class)\s*\w+Cell(:| )(?s).*(self\.|\s{2,})add(Subv|V)iews?\(\w' message: "Use сontentView instead of self for addSubview or addSubviews methods in cell." - severity: error - - redundant_type_annotation_bool: - name: "Redundant type annotation for Bool" - regex: '((var|let)) *\w+ *((: *Bool *=)|((\w| |<|>|:)*= *BehaviorRelay\( *value *:)) *((true)|(false))' - message: "Using a type annotation for Bool is redundant." - severity: error + severity: warning parameter_repetition: name: "Parameter repetition" regex: 'func ((\w+([A-Z]\w+))|(\w+)) *(<[^>]+>)? *\( *(?i)(\3|\4):' message: "The parameter name is actually used in the function name. Use _ instead." - severity: error + severity: warning + + parameter_closure: + name: "Parameter closure" + regex: '\w*Closure<[^\r\n\t\f\v]*, Void[^\r\n\t\f\v]*>' + message: "Use `ParameterClosure` instead of declaring an explicit return value of `Void`." + severity: warning + + strong_self: + name: "Strong self" + regex: '(if|guard)\s+let\s+self\s+=\s+self' + message: "Use a local function instead of capture strong self" + severity: warning + + pattern_matching: + name: "Pattern matching" + regex: 'case[^\n\(]+\([^\)]*(let|var)\s' + message: "Use a let|var keyword behind parentheses" + severity: warning # Rx @@ -259,6 +270,12 @@ custom_rules: message: "Replace Rx.map operator with replace(with:) or asVoid(). For Sequence.map consider using forEach." severity: warning + disposable_nil: + name: "Disposable nil" + regex: ' *\S*(d|D)isposable\?? *= *nil' + message: "nil assigning doesn't dispose subscription. Call `dispose()` instead." + severity: error + # LeadKit multiple_add_subview: 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/bootstrap/Brewfile b/xcode/bootstrap/Brewfile new file mode 100644 index 0000000..396ea43 --- /dev/null +++ b/xcode/bootstrap/Brewfile @@ -0,0 +1,14 @@ +# Working environment +brew "rbenv" # ruby + bundler +brew "gettext" + +# Code, configs and project generation +brew "php" +brew "python" +brew "xcodegen" + +# code quality +brew "pmd" + +# CI badge +# brew "imagemagick" \ No newline at end of file diff --git a/xcode/bootstrap/Gemfile b/xcode/bootstrap/Gemfile new file mode 100644 index 0000000..8f79a29 --- /dev/null +++ b/xcode/bootstrap/Gemfile @@ -0,0 +1,9 @@ +source "https://rubygems.org" + +gem "cocoapods" +gem "fastlane" +gem 'mustache' # for config generator +gem 'xcode-install' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) \ No newline at end of file diff --git a/xcode/bootstrap/Makefile b/xcode/bootstrap/Makefile new file mode 100644 index 0000000..09940d2 --- /dev/null +++ b/xcode/bootstrap/Makefile @@ -0,0 +1,91 @@ +GREEN := $(shell tput -Txterm setaf 2) +YELLOW := $(shell tput -Txterm setaf 3) +WHITE := $(shell tput -Txterm setaf 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_pods=(bundle exec pod install || bundle exec pod install --repo-update) +init_rbenv=(if command -v rbenv &> /dev/null; then eval "$$(rbenv init -)"; fi) + +TARGET_MAX_CHAR_NUM=20 +## Show help +help: + @echo '' + @echo 'Использование:' + @echo ' ${YELLOW}make${RESET} ${GREEN}${RESET}' + @echo '' + @echo 'Команды:' + @awk '/^[a-zA-Z\-\_0-9]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = substr($$1, 0, index($$1, ":")-1); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + printf " ${YELLOW}%-$(TARGET_MAX_CHAR_NUM)s${RESET} ${GREEN}%s${RESET}\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) + +## Инициализирует проект и устанавливает системные утилиты +init: + brew bundle + + $(call init_rbenv) + + rbenv install -s ${RUBY_VERSION} + rbenv global ${RUBY_VERSION} + + if ! gem spec bundler > /dev/null 2>&1; then\ + echo "bundler gem is not installed!";\ + -sudo gem install bundler;\ + fi + + bundle install + + $(call gen) + + $(call install_pods) + + bundle exec fastlane install_plugins + + $(call install_dev_certs) + + $(call open_project) + + git config --local core.hooksPath .githooks + +## Устанавливает поды +pod: + $(call install_pods) + +## Запускает генерацию файла проекта +gen: + xcodegen + +## Устанавливает сертификат и профили для запуска на девайсе +dev_certs: + $(call install_dev_certs) + +## Открывает папку для ручного редактирования сертификатов и профайлов +update_certs: + bundle exec fastlane ManuallyUpdateCodeSigning + +## Поднимает версию приложения (параметр "X.Y.Z") +bumpAppVersion: + ifeq ($(version),undefined) + @echo "Version parameter is missing (ex: x.y.z)" $(target) + else + bundle exec fastlane run increment_version_number version_number:$(version) + endif + +## Позволяет быстро открыть workspace проекта +start: + $(call open_project) + +## Очищает содержимое папки DerivedData +clean: + rm -rf ~/Library/Developer/Xcode/DerivedData/* + + diff --git a/xcode/bootstrap/setup.command b/xcode/bootstrap/setup.command new file mode 100755 index 0000000..0ae8983 --- /dev/null +++ b/xcode/bootstrap/setup.command @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_DIR="${DIR}/../../../" + +make init -C ${PROJECT_DIR} \ No newline at end of file diff --git a/xcode/build_phases/api_generator.sh b/xcode/build_phases/api_generator.sh old mode 100644 new mode 100755 index dfbaf33..ff31f91 --- a/xcode/build_phases/api_generator.sh +++ b/xcode/build_phases/api_generator.sh @@ -1,9 +1,295 @@ -VERSION=$1 -FILE_NAME="api-generator-${VERSION}.jar" +#!/bin/sh -# download api generator -link="https://maven.dev.touchin.ru/ru/touchin/api-generator/${VERSION}/${FILE_NAME}" -. build-scripts/xcode/aux_scripts/download_file.sh ${FILE_NAME} ${link} +# Description: +# Generates API models & methods. +# +# 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 +# . api_generator.sh 1.4.0-beta1 ${TARGET_NAME}/Generated +# -# execute api generator -java -Xmx6g -jar "Downloads/${FILE_NAME}" generate-client-code --output-language SWIFT --specification-path common/api --output-path ${PRODUCT_NAME}/Generated --single-file true +readonly EXIT_SUCCESS=0 +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 + return ${FALSE} + fi + + local -r STR_MODE=`tr "[:upper:]" "[:lower:]" <<< ${FORCE_RUN}` + + if [ ${STR_MODE} == "yes" ] || [ ${STR_MODE} == "true" ] || [ ${STR_MODE} == "1" ]; then + return ${TRUE} + fi + + return ${FALSE} +} + +is_single_file() +{ + if [ -z "${SINGLE_FILE}" ]; then + echo "true" + return + fi + + local -r STR_MODE=`tr "[:upper:]" "[:lower:]" <<< ${SINGLE_FILE}` + + if [ ${STR_MODE} == "no" ] || [ ${STR_MODE} == "false" ] || [ ${STR_MODE} == "0" ]; then + echo "false" + else + echo "true" + fi +} + +get_api_spec_current_commit() +{ + if [ -z "${API_SPEC_DIR}" ]; then + if [ ! -z "${1}" ]; then + echo `git -C ${1} rev-parse --verify HEAD` + else + echo `git rev-parse --verify HEAD` + fi + else + echo `git -C ${API_SPEC_DIR} rev-parse --verify HEAD` + fi +} + +is_api_spec_under_source_control() +{ + local IS_UNDER_SOURCE_CONTROL_CHECK + + if [ -z "${API_SPEC_DIR}" ]; then + if [ ! -z "${1}" ]; then + IS_UNDER_SOURCE_CONTROL_CHECK=`git -C ${1} rev-parse --is-inside-work-tree 2>/dev/null` + else + IS_UNDER_SOURCE_CONTROL_CHECK=`git rev-parse --is-inside-work-tree 2>/dev/null` + fi + else + 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" ] +} + +is_nothing_changed_since_last_check() +{ + if is_force_run; then + notice "Force run detected. Skipping commits comparison." + return ${EXIT_FAILURE} + fi + + if [ -z "${COMMIT_FILE_PATH}" ]; then + if [ ! -z "${1}" ]; then + local -r COMMIT_FILE_PATH=${1} + else + debug "COMMIT_FILE_PATH should be defined or passed as first argument!" + return ${EXIT_FAILURE} + fi + fi + + if is_api_spec_under_source_control; 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} + else + return ${EXIT_FAILURE} + fi + else + return ${EXIT_SUCCESS} + fi +} + +record_current_commit() +{ + if is_force_run; then + notice "Force run detected. Commit won't be recorder." + exit ${EXIT_SUCCESS} + fi + + if [ -z "${COMMIT_FILE_PATH}" ]; then + if [ ! -v "${1}" ]; then + local -r COMMIT_FILE_PATH=${1} + else + debug "COMMIT_FILE_PATH should be defined or passed as second argument!" + return ${EXIT_FAILURE} + fi + fi + + local -r CURRENT_COMMIT=`get_api_spec_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.34" + + 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 -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) +} + +readonly BUILD_PHASES_DIR=${SRCROOT}/build_phases + +mkdir -p ${BUILD_PHASES_DIR} + +readonly COMMIT_FILE_PATH=${BUILD_PHASES_DIR}/api-generator-commit + +if is_nothing_changed_since_last_check; then + notice "Nothing was changed. API generation skipped." + exit ${EXIT_SUCCESS} +fi + +readonly VERSION=$1 + +if [ -z "${OUTPUT_PATH}" ]; then + if [ ! -z "${2}" ]; then + readonly OUTPUT_PATH=${2} + else + readonly OUTPUT_PATH="Generated" + fi +fi + +if [ -z "${API_SPEC_DIR}" ]; then + readonly API_SPEC_DIR="common/api" +fi + +mkdir -p ${OUTPUT_PATH} + +readonly OPEN_API_SPEC_PATH=`find ${API_SPEC_DIR} -maxdepth 1 -name '*.yaml' -o -name '*.yml' | head -n 1` + +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 + +if [ $? -ne ${EXIT_FAILURE} ]; then + record_current_commit +fi diff --git a/xcode/build_phases/common/localization_check b/xcode/build_phases/common/localization_check new file mode 100755 index 0000000..e1d1078 Binary files /dev/null and b/xcode/build_phases/common/localization_check differ diff --git a/xcode/build_phases/common/read_input_file_names.sh b/xcode/build_phases/common/read_input_file_names.sh new file mode 100644 index 0000000..7de9439 --- /dev/null +++ b/xcode/build_phases/common/read_input_file_names.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +# Description: +# Converts SCRIPT_INPUT_FILE_{N} or SCRIPT_INPUT_FILE_LIST_{N} variables to string that contains +# list of file names splitted by given separator. +# +# Parameters: +# $1 - separator to use. +# $2 - default value to return if SCRIPT_INPUT_FILE_COUNT or SCRIPT_INPUT_FILE_LIST_COUNT is zero. +# +# Optional environment variables: +# FILE_NAMES_SEPARATOR - separator to use. +# DEFAULT_FILE_NAMES - default value if was found in environment variables. +# SCRIPT_INPUT_FILE_COUNT - number of files listed in "Input files" section of build phase. +# SCRIPT_INPUT_FILE_{N} - file path of specific input file at index. +# SCRIPT_INPUT_FILE_LIST_COUNT - number of files listed in "Input File Lists" section of build phase. +# SCRIPT_INPUT_FILE_LIST_{N} - file path to specifis xcfilelist file at index. +# +# Examples of usage: +# read_input_file_names +# read_input_file_names.sh " " path/to/project +# + +has_input_files() +{ + [ ! -z "${SCRIPT_INPUT_FILE_COUNT}" ] && [ ${SCRIPT_INPUT_FILE_COUNT} -gt 0 ] +} + +has_input_file_lists() +{ + [ ! -z "${SCRIPT_INPUT_FILE_LIST_COUNT}" ] && [ ${SCRIPT_INPUT_FILE_LIST_COUNT} -gt 0 ] +} + +if [ -z "${FILE_NAMES_SEPARATOR}" ]; then + if [ ! -z "${1}" ]; then + FILE_NAMES_SEPARATOR=${1} + else + FILE_NAMES_SEPARATOR=" " + fi +fi + +if [ -z "${DEFAULT_FILE_NAMES}" ]; then + if [ ! -z "${2}" ]; then + DEFAULT_FILE_NAMES=${2} + else + DEFAULT_FILE_NAMES="" + fi +fi + +INPUT_FILE_NAMES="" + +if has_input_files && has_input_file_lists; then + >&2 echo "Passing Input Files and Input Files Lists is not supported!\nOnly Input Files will be used." +fi + +if has_input_files && \ + [ ${SCRIPT_INPUT_FILE_COUNT} -gt 0 ]; then + + for i in `seq 0 $((${SCRIPT_INPUT_FILE_COUNT}-1))` + do + 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} + 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))` + do + SCRIPT_INPUT_FILE_LIST_VARIABLE_NAME="SCRIPT_INPUT_FILE_LIST_${i}" + SHELL_VARIABLE="\${${SCRIPT_INPUT_FILE_LIST_VARIABLE_NAME}}" + FILE_NAME=`envsubst <<< ${SHELL_VARIABLE}` + RESOLVED_FILE_NAMES=`envsubst < ${FILE_NAME}` + + for INPUT_FILE_NAME in ${RESOLVED_FILE_NAMES}; do + INPUT_FILE_NAMES=${INPUT_FILE_NAMES}${INPUT_FILE_NAME}${FILE_NAMES_SEPARATOR} + done + done +fi + +if [ -z "${INPUT_FILE_NAMES}" ]; then + echo ${DEFAULT_FILE_NAMES} +else + echo ${INPUT_FILE_NAMES} +fi \ No newline at end of file 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 old mode 100644 new mode 100755 index c8b6e88..3fe30c1 --- a/xcode/build_phases/copy_paste_detection.sh +++ b/xcode/build_phases/copy_paste_detection.sh @@ -1,20 +1,61 @@ +#!/bin/sh + +# Description: +# Validates code for copy-paste, prints results to standard output and report file. +# +# Parameters: +# $1 $2 $3 $n - folders to exclude from code checking. +# +# Required environment variables: +# PROJECT_DIR - project directory. +# SCRIPT_DIR - directory of current script. +# +# Optional environment variables: +# 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. +# +# Modified files: +# ${PROJECT_DIR}/code-quality-reports/CPDLog.txt - check report. +# +# Example of usage: +# runner.sh copy_paste_detection.sh Generated Localization Pods +# + +readonly EXIT_SUCCESS=0 +readonly EXIT_FAILURE=1 + +. ${SCRIPT_DIR}/../aux_scripts/install_env.sh pmd + if which pmd >/dev/null; then - # running CPD - readonly SOURCES_DIR=${1:-${PROJECT_DIR}} # first argument or PROJECT_DIR - readonly REPORTS_DIR=${PROJECT_DIR}/code-quality-reports - readonly FILES_TO_EXCLUDE=`find ${SOURCES_DIR} -type d -name Localization -or -name Generated -or -name Carthage -or -name Pods | paste -sd " " -` + readonly REPORTS_DIR="${PROJECT_DIR}/code-quality-reports" - mkdir ${REPORTS_DIR} + readonly SOURCES_DIRS=`. ${SCRIPT_DIR}/common/read_input_file_names.sh " " ${PROJECT_DIR}` - pmd cpd --files ${SOURCES_DIR} --exclude ${FILES_TO_EXCLUDE} --minimum-tokens 50 --language swift --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer > ${REPORTS_DIR}/cpd-output.xml --failOnViolation true + readonly COMMAND_LINE_ARGUMENTS=$@ - php ./build-scripts/xcode/aux_scripts/cpd_script.php ${REPORTS_DIR}/cpd-output.xml | tee ${REPORTS_DIR}/CPDLog.txt + FOLDERS_TO_EXLUDE="" - # Make paths relative to SOURCES_DIR, so different developers won't rewrite entire file - readonly SED_REPLACEMENT_STRING=$(echo ${SOURCES_DIR} | sed "s/\//\\\\\//g") + for argument in ${COMMAND_LINE_ARGUMENTS} + do + FOLDERS_TO_EXLUDE=${FOLDERS_TO_EXLUDE}"-or -name ${argument} " + done + + FOLDERS_TO_EXLUDE=`echo ${FOLDERS_TO_EXLUDE} | cut -c5-` # remove first "-or" + + readonly FILES_TO_EXCLUDE=`find ${PROJECT_DIR} -type d ${FOLDERS_TO_EXLUDE} | 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 + + 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") sed -i '' "s/${SED_REPLACEMENT_STRING}//g" "${REPORTS_DIR}/CPDLog.txt" else echo "warning: pmd not installed, install using 'brew install pmd'" - exit 1 + + exit ${EXIT_FAILURE} fi diff --git a/xcode/build_phases/documentation_generator.sh b/xcode/build_phases/documentation_generator.sh index a5449b7..ffd4828 100644 --- a/xcode/build_phases/documentation_generator.sh +++ b/xcode/build_phases/documentation_generator.sh @@ -1 +1,2 @@ -. build-scripts/xcode/aux_scripts/certificates_readme_generator.sh > $PROJECT_DIR/Certificates/README.md \ No newline at end of file +readonly BUILD_SCRIPTS_DIR=${1:-${PROJECT_DIR}} # first argument or PROJECT_DIR +. $BUILD_SCRIPTS_DIR/build-scripts/xcode/aux_scripts/certificates_readme_generator.sh > $PROJECT_DIR/Certificates/README.md diff --git a/xcode/build_phases/features_generator/features_generator.rb b/xcode/build_phases/features_generator/features_generator.rb new file mode 100755 index 0000000..8a1bebd --- /dev/null +++ b/xcode/build_phases/features_generator/features_generator.rb @@ -0,0 +1,22 @@ +require 'yaml' + +require_relative '../../managers/managers' +require_relative '../../templates/templates' + +# Input files paths +build_settings_file_path = ARGV[0] +generated_features_enum_file_path = ARGV[1] + +build_settings_features_list = Managers::FileManager.load_from_file_YAML(build_settings_file_path)["features"] + +if build_settings_features_list.nil? or build_settings_features_list.empty? + raise "There are no features in " + build_settings_file_path +end + +# Generate enum Feature Toggles +features_enum_template = Templates::FeatureTemplates.features_enum +utils = Managers::TemplateManager.new(build_settings_features_list) + +rendered_enum = utils.render(features_enum_template).strip + +Managers::FileManager.save_data_to_file(generated_features_enum_file_path, rendered_enum) diff --git a/xcode/build_phases/features_generator/features_generator.sh b/xcode/build_phases/features_generator/features_generator.sh new file mode 100755 index 0000000..66da7cc --- /dev/null +++ b/xcode/build_phases/features_generator/features_generator.sh @@ -0,0 +1,19 @@ +# Input paths +readonly BUILD_SETTINGS_FILE_PATH=${1:-${PROJECT_DIR}/common/build_settings.yaml} +readonly FEATURES_ENUM_FILE_PATH=${2:-${PROJECT_DIR}/${PRODUCT_NAME}/Resources/Features/Feature.swift} + +# Features enunm generator script +readonly CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +readonly GENERATOR_SCRIPT=${CURRENT_DIR}/features_generator.rb + +if ! [ -e ${BUILD_SETTINGS_FILE_PATH} ]; then + echo "File ${BUILD_SETTINGS_FILE_PATH} does not exist. Add this file and try again." + exit 1 +fi + +if ! [ -e ${FEATURES_ENUM_FILE_PATH} ]; then + echo "File ${FEATURES_ENUM_FILE_PATH} does not exist. Add this file and try again." + exit 1 +fi + +ruby ${GENERATOR_SCRIPT} ${BUILD_SETTINGS_FILE_PATH} ${FEATURES_ENUM_FILE_PATH} \ No newline at end of file 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/localization_check.sh b/xcode/build_phases/localization_check.sh new file mode 100755 index 0000000..afb80eb --- /dev/null +++ b/xcode/build_phases/localization_check.sh @@ -0,0 +1,9 @@ +# source: https://github.com/iKenndac/verify-string-files + +# first argument set base localization strings path +readonly LOCALIZATION_PATH=${1:-${PRODUCT_NAME}/Resources/Localization/Base.lproj/Localizable.strings} + +# second argument set check script path +readonly CHECK_SCRIPT=${2:-${PROJECT_DIR}/build-scripts/xcode/build_phases/common/localization_check} + +${CHECK_SCRIPT} -master ${LOCALIZATION_PATH} diff --git a/xcode/build_phases/multiple_swiftlint/array_extension.rb b/xcode/build_phases/multiple_swiftlint/array_extension.rb new file mode 100644 index 0000000..e5bebf6 --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/array_extension.rb @@ -0,0 +1,5 @@ +class Array + def nilOrEmpty? + self.nil? or self.empty? + end +end diff --git a/xcode/build_phases/multiple_swiftlint/command_utils.rb b/xcode/build_phases/multiple_swiftlint/command_utils.rb new file mode 100644 index 0000000..cf12d07 --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/command_utils.rb @@ -0,0 +1,6 @@ +class CommandUtils + def self.make_command(command) + command = command.to_s + return `#{command}` + end +end \ No newline at end of file diff --git a/xcode/build_phases/multiple_swiftlint/git_caretaker.rb b/xcode/build_phases/multiple_swiftlint/git_caretaker.rb new file mode 100644 index 0000000..323464a --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/git_caretaker.rb @@ -0,0 +1,35 @@ +require_relative 'array_extension.rb' +require_relative 'command_utils.rb' +require_relative 'string_extension.rb' + +class GitСaretaker < CommandUtils + def self.get_modified_files + non_indexed_files = get_files_from('git diff --name-only | sed s/.*/"&,"/ ') + indexed_files = get_files_from('git diff --cached --name-only | sed s/.*/"&,"/ ') + + modified_files = non_indexed_files + indexed_files + unique_modified_files = modified_files.uniq + + unique_modified_swift_files = [] + if not unique_modified_files.nilOrEmpty? + unique_modified_swift_files = unique_modified_files.select { |file_path| + file_path.to_s.filter_allowed_symbol_into_path + file_path.to_s.include? '.swift' + } + end + + return unique_modified_swift_files + end + + def self.get_creation_date(file_path) + git_command = 'git log --follow --format=%cD --reverse -- ' + file_path + ' | head -1' + return make_command(git_command) + end + + private + + def self.get_files_from(command) + files_as_string = make_command(command) + return files_as_string.split(',') + end +end \ No newline at end of file diff --git a/xcode/build_phases/multiple_swiftlint/setting_option.rb b/xcode/build_phases/multiple_swiftlint/setting_option.rb new file mode 100644 index 0000000..4374b24 --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/setting_option.rb @@ -0,0 +1,78 @@ +require 'optparse' +require 'ostruct' + +require_relative 'array_extension.rb' + +class SettingOption + def initialize + @options = OpenStruct.new + OptionParser.new do |opt| + opt.on('-p', + '--project_root_path STRING', + 'The path of project directory and contains *.xcodeproj file always. ' + + 'Example: project_root_path=~/Projects/MyProject/Source/..') { |option| + @options.project_root_path = option + } + opt.on('-r', + '--source_root_path STRING', + 'The path of source directory and may not contains *.xcodeproj file in some cases. ' + + 'Example: source_root_path=~/Projects/MyProject/') { |option| + @options.source_root_path = option + } + opt.on('-s', + '--swiftlint_executable_path STRING', + 'The executable path of swiftlint') { |option| + @options.swiftlint_executable_path = option + } + opt.on('-c', + '--check_mode MODE', + 'The mode of check is "fully" or "simplified"') { |option| + @options.check_mode = option + } + opt.on('-u', + '--use_multiple BOOL', + 'The flag indicates the use of multiple yaml swiftlint configurations') { |option| + @options.use_multiple = option + } + opt.on('-d', + '--source_date DATE', + 'The date of grouping files according touchin and old swiftlint rules') { |option| + @options.source_date = option + } + opt.on('-tc', + '--touchin_swiftlint_yaml_path STRING', + 'The path to the touchin swiftlint yaml relative to the source directory') { |option| + @options.touchin_swiftlint_yaml_path = option + } + opt.on('-oc', + '--old_swiftlint_yaml_path STRING', + 'The path to the old swiftlint yaml relative to the source directory') { |option| + @options.old_swiftlint_yaml_path = option + } + end.parse! + + if @options.check_mode.to_s.nilOrEmpty? + @options.check_mode = 'fully' + end + + if @options.use_multiple.to_s.nilOrEmpty? + @options.use_multiple = 'false' + end + + if @options.source_root_path.to_s.nilOrEmpty? + @options.source_root_path = @options.project_root_path + end + + if @options.touchin_swiftlint_yaml_path.to_s.nilOrEmpty? + @options.touchin_swiftlint_yaml_path = File.join(project_root_path, 'build-scripts/xcode/.swiftlint.yml') + end + + if @options.old_swiftlint_yaml_path.to_s.nilOrEmpty? + @options.old_swiftlint_yaml_path = File.join(project_root_path, '.swiftlint.yml') + end + end + + def method_missing(method, *args, &block) + @options.send(method, *args, &block) + end +end diff --git a/xcode/build_phases/multiple_swiftlint/strategy_maker.rb b/xcode/build_phases/multiple_swiftlint/strategy_maker.rb new file mode 100644 index 0000000..d297dbf --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/strategy_maker.rb @@ -0,0 +1,161 @@ +require 'fileutils' +require 'tmpdir' + +require_relative 'array_extension.rb' +require_relative 'git_caretaker.rb' +require_relative 'string_extension.rb' +require_relative 'swift_file_manager.rb' +require_relative 'yaml_manager.rb' + +class StrategyMaker + def initialize(project_root_path, swiftlint_executable_path, touchin_swiftlint_yaml_path, old_swiftlint_yaml_path) + @project_root_path = project_root_path + @touchin_swiftlint_yaml_path = touchin_swiftlint_yaml_path + @old_swiftlint_yaml_path = old_swiftlint_yaml_path + + @temporary_swiftlint_folder_name = Dir.mktmpdir + @touchin_swiftlint_yaml_temporary_path = File.join(@temporary_swiftlint_folder_name, '.touchin_swiftlint.yml') + @old_swiftlint_yaml_temporary_path = File.join(@temporary_swiftlint_folder_name, '.old_swiftlint.yml') + + @swiftlint_autocorrect_command = swiftlint_executable_path + ' autocorrect --path ' + @project_root_path + ' --config ' + @swiftlint_lint_command = swiftlint_executable_path + ' --path ' + @project_root_path + ' --config ' + end + + def run_fully_multiple_strategy(source_date) + create_yaml_managers_and_copy_temporary_files + + exclude_files = unique_exclude_files(@touchin_swiftlint_yaml_manager, @old_swiftlint_yaml_manager) + + swift_files = SwiftFileManager.new(exclude_files, source_date) + swift_files.find_list_file_paths(@project_root_path) + + total_touchin_excluded_files = exclude_files + swift_files.old_files + total_old_excluded_files = exclude_files + swift_files.new_files + + @touchin_swiftlint_yaml_manager.update('excluded', total_touchin_excluded_files) + @old_swiftlint_yaml_manager.update('excluded', total_old_excluded_files) + + run_multiple_strategy(@touchin_swiftlint_yaml_temporary_path, @old_swiftlint_yaml_temporary_path) + end + + def run_simplified_multiple_strategy(source_date, source_root_path) + included_files = GitСaretaker.get_modified_files + + if included_files.nilOrEmpty? + puts 'Git did not found swift files to check' + return + end + + create_yaml_managers_and_copy_temporary_files + + exclude_files = unique_exclude_files(@touchin_swiftlint_yaml_manager, @old_swiftlint_yaml_manager) + included_files = included_files.map { |file_path| source_root_path + file_path } + + swift_file_manager = SwiftFileManager.new(exclude_files, source_date) + swift_file_manager.find_list_file_paths_from(included_files) + + total_touchin_included_files = swift_file_manager.new_files + total_old_included_files = swift_file_manager.old_files + + @touchin_swiftlint_yaml_manager.update('excluded', []) + @old_swiftlint_yaml_manager.update('excluded', []) + + @touchin_swiftlint_yaml_manager.update('included', total_touchin_included_files) + @old_swiftlint_yaml_manager.update('included', total_old_included_files) + + is_exist_total_touchin_included_files = (not total_touchin_included_files.nilOrEmpty?) + is_exist_total_old_included_files = (not total_old_included_files.nilOrEmpty?) + + if is_exist_total_touchin_included_files and is_exist_total_old_included_files + run_multiple_strategy(@touchin_swiftlint_yaml_temporary_path, @old_swiftlint_yaml_temporary_path) + elsif is_exist_total_touchin_included_files and not is_exist_total_old_included_files + run_single_strategy(@touchin_swiftlint_yaml_temporary_path) + elsif not is_exist_total_touchin_included_files and is_exist_total_old_included_files + run_single_strategy(@old_swiftlint_yaml_temporary_path) + else + puts 'Git did not found swift files to check' + end + end + + def run_fully_single_strategy + run_single_strategy(@touchin_swiftlint_yaml_path) + end + + def run_simplified_single_strategy(source_root_path) + included_files = GitСaretaker.get_modified_files + + if included_files.nilOrEmpty? + puts 'Git did not found swift files to check' + return + end + + create_copy_temporary_touchin_files + + touchin_swiftlint_yaml_manager = YamlManager.new(@touchin_swiftlint_yaml_temporary_path) + touchin_excluded_files = touchin_swiftlint_yaml_manager.get_configuration('excluded') + swift_files = SwiftFileManager.new(touchin_excluded_files, '') + + included_files = included_files.select { |file_name| not swift_files.is_excluded_file(file_name) } + included_files = included_files.map { |file_path| source_root_path + file_path } + + touchin_swiftlint_yaml_manager.update('excluded', []) + touchin_swiftlint_yaml_manager.update('included', included_files) + + if not included_files.nilOrEmpty? + run_single_strategy(@touchin_swiftlint_yaml_temporary_path) + else + puts 'Git found the swift files to check, but they are excluded in yaml' + end + end + + private + + def run_single_strategy(swiftlint_yaml_path) + result_swiftlint_command = get_swiftlint_command(swiftlint_yaml_path) + puts result_swiftlint_command + run_bash_command(result_swiftlint_command) + end + + def run_multiple_strategy(touchin_swiftlint_yaml_temporary_path, old_swiftlint_yaml_temporary_path) + touchin_swiftlint_command = get_swiftlint_command(touchin_swiftlint_yaml_temporary_path) + old_swiftlint_command = get_swiftlint_command(old_swiftlint_yaml_temporary_path) + result_swiftlint_command = touchin_swiftlint_command + ' && ' + old_swiftlint_command + puts result_swiftlint_command + run_bash_command(result_swiftlint_command) + end + + def get_swiftlint_command(swiftlint_yaml_path) + autocorrect_command = @swiftlint_autocorrect_command + swiftlint_yaml_path + lint_command = @swiftlint_lint_command + swiftlint_yaml_path + return autocorrect_command + ' && ' + lint_command + end + + def run_bash_command(bash_command) + exit (exec bash_command) + end + + def create_yaml_managers_and_copy_temporary_files + create_copy_temporary_files + + @touchin_swiftlint_yaml_manager = YamlManager.new(@touchin_swiftlint_yaml_temporary_path) + @old_swiftlint_yaml_manager = YamlManager.new(@old_swiftlint_yaml_temporary_path) + end + + def create_copy_temporary_files + create_copy_temporary_touchin_files + FileUtils.cp @old_swiftlint_yaml_path, @old_swiftlint_yaml_temporary_path + end + + def create_copy_temporary_touchin_files + Dir.mkdir(@temporary_swiftlint_folder_name) unless Dir.exist?(@temporary_swiftlint_folder_name) + FileUtils.cp @touchin_swiftlint_yaml_path, @touchin_swiftlint_yaml_temporary_path + end + + def unique_exclude_files(touchin_swiftlint_yaml_manager, old_swiftlint_yaml_manager) + touchin_excluded_files = touchin_swiftlint_yaml_manager.get_configuration('excluded') + old_excluded_files = old_swiftlint_yaml_manager.get_configuration('excluded') + common_exclude_files = touchin_excluded_files + old_excluded_files + unique_exclude_files = common_exclude_files.uniq + return unique_exclude_files + end +end \ No newline at end of file diff --git a/xcode/build_phases/multiple_swiftlint/string_extension.rb b/xcode/build_phases/multiple_swiftlint/string_extension.rb new file mode 100644 index 0000000..33a6357 --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/string_extension.rb @@ -0,0 +1,17 @@ +class String + def with_wrapped_whitespace + self.gsub(/\s+/, '\ ') + end + + def filter_allowed_symbol_into_path + self.gsub!(/[^0-9A-Za-z \-+.\/]/, '') + end + + def true? + self.to_s.downcase == "true" + end + + def nilOrEmpty? + self.nil? or self.empty? + end +end diff --git a/xcode/build_phases/multiple_swiftlint/swift_file_manager.rb b/xcode/build_phases/multiple_swiftlint/swift_file_manager.rb new file mode 100644 index 0000000..b8edf6c --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/swift_file_manager.rb @@ -0,0 +1,68 @@ +require 'fileutils' +require 'date' + +require_relative 'git_caretaker.rb' + +class SwiftFileManager + def initialize(excluded_files, source_date) + if not source_date.nilOrEmpty? + @source_date = Date.parse(source_date) + end + @excluded_files = excluded_files + @new_files = [] + @old_files = [] + end + + def old_files + @old_files + end + + def new_files + @new_files + end + + def find_list_file_paths(start_folder) + swift_files = File.join('**', '*.swift') + Dir.glob(swift_files, base: start_folder) { |file_path| + if not is_excluded_file(file_path) + compare_timestamp(file_path) + end + } + end + + def find_list_file_paths_from(files_path) + files_path.each { |file_path| + if not is_excluded_file(file_path) + compare_timestamp(file_path) + end + } + end + + def is_excluded_file(file_path) + @excluded_files.each do |exclude_file_path| + if file_path.include? exclude_file_path + return true + end + end + return false + end + + private + + def compare_timestamp(file_path) + wrapped_whitespace_file_path = file_path.with_wrapped_whitespace + creation_date_string = GitСaretaker.get_creation_date(wrapped_whitespace_file_path) + if creation_date_string.nilOrEmpty? + @old_files.push(file_path) + puts ('Creation date of ' + file_path + ' was not found') + else + creation_date = Date.parse(creation_date_string) + puts ('Creation date of ' + file_path + ' is ' + creation_date.to_s) + if @source_date < creation_date + @new_files.push(file_path) + else + @old_files.push(file_path) + end + end + end +end \ No newline at end of file diff --git a/xcode/build_phases/multiple_swiftlint/swiftlint.rb b/xcode/build_phases/multiple_swiftlint/swiftlint.rb new file mode 100644 index 0000000..9638347 --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/swiftlint.rb @@ -0,0 +1,19 @@ +#https://github.com/TouchInstinct/Styleguide/blob/multiple_swiftlint/IOS/Guides/BuildScripts/Multiple_Swiftlint_Guide.md +require_relative 'setting_option.rb' +require_relative 'strategy_maker.rb' + +setting = SettingOption.new +strategy_maker = StrategyMaker.new(setting.project_root_path, + setting.swiftlint_executable_path, + setting.touchin_swiftlint_yaml_path, + setting.old_swiftlint_yaml_path) + +if setting.check_mode.eql? 'fully' and setting.use_multiple.true? + strategy_maker.run_fully_multiple_strategy(setting.source_date) +elsif setting.check_mode.eql? 'fully' and not setting.use_multiple.true? + strategy_maker.run_fully_single_strategy +elsif setting.check_mode.eql? 'simplified' and setting.use_multiple.true? + strategy_maker.run_simplified_multiple_strategy(setting.source_date, setting.source_root_path) +elsif setting.check_mode.eql? 'simplified' and not setting.use_multiple.true? + strategy_maker.run_simplified_single_strategy(setting.source_root_path) +end diff --git a/xcode/build_phases/multiple_swiftlint/yaml_manager.rb b/xcode/build_phases/multiple_swiftlint/yaml_manager.rb new file mode 100644 index 0000000..cb3bbd8 --- /dev/null +++ b/xcode/build_phases/multiple_swiftlint/yaml_manager.rb @@ -0,0 +1,24 @@ +require 'yaml' +require 'fileutils' + +class YamlManager + def initialize(swiftlint_yaml_path) + @swiftlint_yaml_path = swiftlint_yaml_path + @configuration ||= YAML.load(File.read(@swiftlint_yaml_path)) + end + + def get_configuration(key) + @configuration[key] + end + + def update(key, new_configuration_values) + @configuration[key] = new_configuration_values + save_settings(@configuration) + end + + private + + def save_settings(settings) + File.write(@swiftlint_yaml_path, settings.to_yaml) + end +end diff --git a/xcode/build_phases/swiftlint.sh b/xcode/build_phases/swiftlint.sh old mode 100644 new mode 100755 index f1f8fb8..966dec9 --- a/xcode/build_phases/swiftlint.sh +++ b/xcode/build_phases/swiftlint.sh @@ -1,2 +1,96 @@ -SOURCES_DIR=${1:-${TARGET_NAME}} # first argument or TARGET_NAME -${PODS_ROOT}/SwiftLint/swiftlint autocorrect --path ${SOURCES_DIR} --config ${PROJECT_DIR}/build-scripts/xcode/.swiftlint.yml && ${PODS_ROOT}/SwiftLint/swiftlint --path ${SOURCES_DIR} --config ${PROJECT_DIR}/build-scripts/xcode/.swiftlint.yml +#!/bin/sh + +# Description: +# Runs swiftlint with selected or default config file. +# By default it runs only for modified files. +# +# Parameters: +# $1 - path to swiftlint executable. +# $2 - path to swiftlint config. +# +# Required environment variables: +# SCRIPT_DIR - directory of current script. +# SRCROOT - project directory. +# +# Optional environment variables: +# SWIFTLINT_EXECUTABLE - path to swiftlint executable. +# SWIFTLINT_CONFIG_PATH - path to swiftlint config. +# PODS_ROOT - cocoapods installation directory (eg. ${SRCROOT}/Pods) if SWIFTLINT_EXECUTABLE or ${1} is missing +# 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 - don't exclude not modified files. +# AUTOCORRECT - format and fix code before lint. +# +# Example of usage: +# swiftlint.sh +# FORCE_LINT=true; swiftlint.sh +# swiftlint.sh Pods/Swiftlint/swiftlint build-scripts/xcode/.swiftlint.yml +# + +readonly SOURCES_DIRS=`. ${SCRIPT_DIR}/common/read_input_file_names.sh "\n" ${SRCROOT}` + +if [ -z "${SWIFTLINT_EXECUTABLE}" ]; then + if [ ! -z "${1}" ]; then + readonly SWIFTLINT_EXECUTABLE=${1} + else + readonly SWIFTLINT_EXECUTABLE=${PODS_ROOT}/SwiftLint/swiftlint + fi +fi + +if [ -z "${SWIFTLINT_CONFIG_PATH}" ]; then + if [ ! -z "${2}" ]; then + readonly SWIFTLINT_CONFIG_PATH=${2} + else + readonly SWIFTLINT_CONFIG_PATH=${SCRIPT_DIR}/../.swiftlint.yml + fi +fi + +if [ ! -z "${FORCE_LINT}" ]; then + # Если задана переменная FORCE_LINT, то проверяем все файлы проекта + for SOURCE_DIR in ${SOURCES_DIRS}; do + if [ ! -z "${AUTOCORRECT}" ]; then + ${SWIFTLINT_EXECUTABLE} lint --config ${SWIFTLINT_CONFIG_PATH} --fix --format "${SRCROOT}/${SOURCE_DIR}" + fi + + ${SWIFTLINT_EXECUTABLE} lint --config ${SWIFTLINT_CONFIG_PATH} "${SRCROOT}/${SOURCE_DIR}" + 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=`mktemp` + + # Проходимся по папкам, которые требуют линтовки + for SOURCE_DIR in ${SOURCES_DIRS}; do + LINE_PREFIX="${SRCROOT}/" + + pushd . + + cd ${SRCROOT} # in case of runing script outside project folder (SPM) + + # Отбираем файлы, которые были изменены или созданы + source_unstaged_files=$(git diff --diff-filter=d --name-only --line-prefix=${LINE_PREFIX} ${SOURCE_DIR} | grep "\.swift$") + source_staged_files=$(git diff --diff-filter=d --name-only --line-prefix=${LINE_PREFIX} --cached ${SOURCE_DIR} | grep "\.swift$") + + popd + + 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}" + + if [ ! -z "${AUTOCORRECT}" ]; then + ${SWIFTLINT_EXECUTABLE} lint --config ${SWIFTLINT_CONFIG_PATH} --fix --format --force-exclude --use-alternative-excluding ${swiftlint_files_path} + fi + + ${SWIFTLINT_EXECUTABLE} lint --config ${SWIFTLINT_CONFIG_PATH} --force-exclude --use-alternative-excluding ${swiftlint_files_path} +fi diff --git a/xcode/build_phases/unused.sh b/xcode/build_phases/unused.sh index 5a0a906..f137d8c 100644 --- a/xcode/build_phases/unused.sh +++ b/xcode/build_phases/unused.sh @@ -1,4 +1,5 @@ -arguments=("$@") -ignored_files=$(IFS=, ; echo "${arguments[*]}") +readonly ARGUMENTS=("$@") +readonly IGNORED_FILES=$(IFS=, ; echo "${ARGUMENTS[*]}") +readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -ruby ${PROJECT_DIR}/build-scripts/xcode/build_phases/Unused.rb --config ${PROJECT_DIR}/build-scripts/xcode/UnusedConfig.yml --exclude ${ignored_files} +ruby ${SCRIPT_DIR}/Unused.rb --config ${SCRIPT_DIR}/../UnusedConfig.yml --exclude ${IGNORED_FILES} 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 d903469..e95320d 100644 --- a/xcode/commonFastfile +++ b/xcode/commonFastfile @@ -1,6 +1,15 @@ $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/" @@ -11,49 +20,15 @@ private_lane :installDependencies do |options| sh("rm -rf #{podsReposPath}") end - if File.exists? "../Gemfile" - bundle_install(path: "../.gem") - end - cocoapods( - repo_update: true + try_repo_update_on_error: true ) - - if File.exists? "../Cartfile" - use_rome = File.exists? "../Romefile" - - swift_version = sh("xcrun swift --version | head -1 | sed 's/.*\\(\(.*\)\\).*/\\1/' | tr -d \"()\" | tr \" \" \"-\"").chop - rome_path = "Pods/Rome/rome" - rome_options = "--platform iOS --cache-prefix #{swift_version} --romefile Romefile" - - carthage_install = lambda do - if use_rome - sh("cd .. && #{rome_path} download #{rome_options}") - end - - carthage(command: "bootstrap", platform: "iOS", cache_builds: true) - - if use_rome - sh("cd .. && #{rome_path} list --missing #{rome_options} | awk '{print $1}' | xargs -I framework_name #{rome_path} upload framework_name #{rome_options}") - end - end - - begin - carthage_install.call - rescue - # workaround for https://github.com/Carthage/Carthage/issues/2298 - sh("rm -rf ~/Library/Caches/org.carthage.CarthageKit") - carthage_install.call - end - end end private_lane :uploadToFirebase do |options| releaseNotesFile = "release-notes.txt" sh("touch ../#{releaseNotesFile}") - sh("yarn install") - app_target_folder_name = options[:appName] || $appName configuration_type = Touchlane::ConfigurationType.from_type(options[:type]) @@ -61,37 +36,47 @@ 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, - firebase_cli_path: File.expand_path("../node_modules/firebase-tools/lib/bin/firebase.js") - ) - - upload_symbols_to_crashlytics( - gsp_path: get_google_services_plist_path(app_target_folder_name, configuration_type) - ) + release_notes_file: releaseNotesFile + ) + end end -private_lane :uploadToAppStore do |options| +def upload_to_app_store_using_options(options) 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], force: true, # skip metainfo prompt skip_metadata: true, team_id: options[:itc_team_id], - dev_portal_team_id: options[:team_id] + dev_portal_team_id: options[:team_id], + precheck_include_in_app_purchases: false ) end private_lane :addShield do |options| buildNumber = options[:buildNumber] - buildDescription = options[:xcconfig_name] # EnterpriseCustomerDev1WithoutSSLPinningRelease + buildDescription = options[:lane_name] # EnterpriseCustomerDev1WithoutSSLPinningRelease .split(/(?=[A-Z])/) # -> ["Enterprise", "Customer", "Dev1", "Without", "S", "S", "L", "Pinning", "Release"] .map { |v| v.gsub(/[[:lower:]]+/, "") }[1..2] # -> ["E", "C", "D1", "W", "S", "S", "L", "P", "R"] -> ["C", "D1"] .join # -> "CD1" - + begin add_badge( shield: "#{buildDescription}-#{buildNumber}-green", @@ -103,35 +88,40 @@ private_lane :addShield do |options| end private_lane :buildConfiguration do |options| - appName = options[:appName] || $appName + options[:appName] = options[:appName] || $appName - lane_name = lane_context[SharedValues::LANE_NAME] + lane_name = options[:lane_name] || lane_context[SharedValues::LANE_NAME] - options[:scheme] = appName - options[:xcconfig_name] = lane_name + 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 is_ci + if !options[:buildNumber].nil? increment_build_number( build_number: options[:buildNumber] ) end - ipa_name = "#{appName}.ipa" - options[:output_name] = ipa_name - - options[:ipa_path] = "./#{ipa_name}" - options[:dsym_path] = "./#{appName}.app.dSYM.zip" - - options[:xcodeproj_path] = "../#{appName}.xcodeproj" - options[:workspace] = "./#{appName}.xcworkspace" - installDependencies(options) + run_code_generation_phase_if_needed(options) + generate_enabled_features_extension_if_needed(options) + if !(options[:uploadToFabric] || options[:uploadToAppStore]) options[:skip_package_ipa] = true @@ -144,49 +134,70 @@ private_lane :buildConfiguration do |options| sync_code_signing_using_options(options) addShield(options) buildArchive(options) - uploadToFirebase(options) end if options[:uploadToAppStore] - options[:compileBitcode] = options[:compileBitcode].nil? ? true : options[:compileBitcode] options[:include_symbols] = options[:include_symbols].nil? ? true : options[:include_symbols] sync_code_signing_using_options(options) - buildArchive(options) - uploadToAppStore(options) + upload_to_app_store_using_options(options) end + + upload_symbols_to_crashlytics( + gsp_path: get_google_services_plist_path(options[:appName], configuration_type) + ) end private_lane :buildArchive do |options| + + require 'json' + icloudEnvironment = options[:iCloudContainerEnvironment] || "" exportOptions = icloudEnvironment.to_s.empty? ? {} : {iCloudContainerEnvironment: icloudEnvironment} - exportOptions[:compileBitcode] = options[:compileBitcode] || false - xcconfig_name = options[:xcconfig_name] + lane_name = options[:lane_name] configuration = options[:configuration] xcodeproj_path = options[:xcodeproj_path] + xcode_version = options[:xcodeVersion] - set_xcconfig_for_configuration_of_project(xcconfig_name, configuration, xcodeproj_path) + 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], export_options: exportOptions, skip_package_ipa: options[:skip_package_ipa], - include_symbols: options[:include_symbols] || false, - include_bitcode: options[:compileBitcode] || false, + include_symbols: options[:include_symbols] || false ) end -lane :createPushCertificate do |options| +lane :CreatePushCertificate do |options| + configuration = get_configuration_for_type(options[:type] || "development") + options = configuration.to_options.merge(options) + certificates_path = File.expand_path "../Certificates" Dir.mkdir(certificates_path) unless File.directory?(certificates_path) @@ -212,36 +223,6 @@ lane :SyncCodeSigning do |options| sync_code_signing_using_options(options) end -lane :SyncSymbols do |options| - configuration = get_configuration_for_type(options[:type]) - options = configuration.to_options.merge(options) - - appName = options[:appName] || $appName - - xcodeproj_path = File.expand_path "../#{appName}.xcodeproj" - - version_number = options[:version] || get_version_number(xcodeproj: xcodeproj_path, target: appName) - build_number = options[:build_number] || get_build_number(xcodeproj: xcodeproj_path) - - if configuration.type.is_app_store - download_dsyms( - username: options[:username], - app_identifier: options[:app_identifier].first, - team_id: options[:itc_team_id], - version: version_number, - build_number: build_number - ) - end - - app_target_folder_name = appName - - upload_symbols_to_crashlytics( - gsp_path: get_google_services_plist_path(app_target_folder_name, configuration.type) - ) - - clean_build_artifacts -end - private_lane :openKeychain do |options| if is_ci? # workaround to avoid duplication problem @@ -253,7 +234,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 @@ -265,25 +246,18 @@ private_lane :openKeychain do |options| end lane :ManuallyUpdateCodeSigning do |options| - # based on this article https://medium.com/@jonathancardoso/using-fastlane-match-with-existing-certificates-without-revoking-them-a325be69dac6 - require 'fastlane_core' + register_local_storage_for_match() + require 'match' - conf = FastlaneCore::Configuration.create(Match::Options.available_options, {}) - conf.load_configuration_file("Matchfile") - - git_url = conf.config_file_options[:git_url] - shallow_clone = false - branch = 'fastlane_certificates' - - storage_conf = lambda do - new_storage = Match::Storage.for_mode('git', { git_url: git_url, shallow_clone: shallow_clone, git_branch: branch, clone_branch_directly: false}) + 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_conf_for_storage = lambda do |stor| - new_encryption = Match::Encryption.for_storage_mode('git', { git_url: git_url, working_directory: stor.working_directory}) + 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 @@ -292,8 +266,8 @@ lane :ManuallyUpdateCodeSigning do |options| Dir[File.join(stor.working_directory, "**", "*.{cer,p12,mobileprovision}")] end - storage = storage_conf.call - encryption = encryption_conf_for_storage.call(storage) + storage = storage_factory.call + encryption = encryption_factory.call(storage) old_files = get_all_files.call(storage) sh("open #{storage.working_directory}") @@ -316,8 +290,8 @@ lane :ManuallyUpdateCodeSigning do |options| # to avoid this we use storage twice if needed if files_diff.length > 0 - storage = storage_conf.call - encryption = encryption_conf_for_storage.call(storage) + storage = storage_factory.call + encryption = encryption_factory.call(storage) files_to_delete = files_diff.map do |file| old_file = file @@ -334,32 +308,85 @@ lane :ManuallyUpdateCodeSigning do |options| 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: "git", - git_url: options[:git_url], - git_branch: "fastlane_certificates", - shallow_clone: true, - clone_branch_directly: true, - keychain_name: options[:keychain_name], - keychain_password: options[:keychain_password], + 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, - platform: "ios" + keychain_name: options[:keychain_name], + keychain_password: options[:keychain_password] ) end +def register_local_storage_for_match + Match::Storage.register_backend(type: 'local', storage_class: Touchlane::LocalStorage) + Match::Encryption.register_backend(type: 'local', encryption_class: Match::Encryption::OpenSSL) +end + +def get_signing_identities_path + File.expand_path "../EncryptedSigningIdentities" +end + def fill_up_options_using_configuration_type(options, configuration_type) configuration = get_configuration_for_type(configuration_type.type) - configuration.to_options + api_key_path = File.expand_path "../fastlane/#{configuration_type.prefix}_api_key.json" + is_api_key_file_exists = File.exists?(api_key_path) + + # default_options required to be empty due to the possibility of skipping the configuration type check below + + default_options = {} + + # Check whether configuration type is required to configure one of api key parameters or not + + if configuration_type.is_app_store || configuration_type.is_development + + # Check whether API key JSON file exists or not + + if is_api_key_file_exists + + # 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} + 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()} + end + end + + default_options + .merge(configuration.to_options) .merge(get_keychain_options(options)) .merge(options) end +def get_app_store_connect_api_key() + require 'json' + + api_key_parameters = JSON.parse(ENV['API_KEY_JSON']) + + return app_store_connect_api_key( + key_id: api_key_parameters['key_id'], + issuer_id: api_key_parameters['issuer_id'], + key_content: api_key_parameters['key'], + duration: api_key_parameters['duration'], + in_house: api_key_parameters['in_house'] + ) +end + def get_keychain_options(options) keychain_name = options[:keychain_name] keychain_password = options[:keychain_password] @@ -384,10 +411,38 @@ 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 set_xcconfig_for_configuration_of_project(xcconfig_name, configuration, xcodeproj_path) +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' project = Xcodeproj::Project.open(xcodeproj_path) @@ -395,7 +450,8 @@ def set_xcconfig_for_configuration_of_project(xcconfig_name, configuration, xcod 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 @@ -403,12 +459,35 @@ def set_xcconfig_for_configuration_of_project(xcconfig_name, configuration, xcod 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 + xcconfig_name + 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" + + if File.exists? code_generation_script_path + sh(code_generation_script_path, options[:workspace]) + end +end \ No newline at end of file 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 64d6b8e..ba20335 100755 --- a/xcode/config_generator/render_xcconfigs.rb +++ b/xcode/config_generator/render_xcconfigs.rb @@ -1,158 +1,15 @@ -require 'json' -require 'mustache' -require 'yaml' - -# Usage: render_xcconfigs.rb +require_relative "config_renderer" # -# Result: Adds .xcconfig files to $configs_folder_name directory. +# Usage: render_xcconfigs.rb [] +# +# Result: Adds .xcconfig files to ouptut folder. # Files are only being added and changed, not removed! # It is recommended to remove old .xcconfig files before running this script. - - -# Constants -$configs_folder_name = "TargetConfigurations" - -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] || "build_parameters.yaml".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 - -def generate_google_service_info_plist_path(google_service_info_plist_key, target_name, distribution_type) - google_service_info_plist_path = target_name + "/Resources/" - - path_suffix = case distribution_type - when "development" - "Standard-GoogleService-Info.plist" - when "enterprise" - "Enterprise-GoogleService-Info.plist" - else - "AppStore-GoogleService-Info.plist" - end - - return config_option(google_service_info_plist_key, google_service_info_plist_path + path_suffix) -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" - google_service_info_plist_key = "GOOGLE_SERVICE_INFO_PLIST_PATH" - 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 - - unless properties.key?(google_service_info_plist_key) - result.append(generate_google_service_info_plist_path(google_service_info_plist_key, target_name, 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/match/storage/local_storage.rb b/xcode/fastlane/touchlane/lib/match/storage/local_storage.rb new file mode 100644 index 0000000..73759b8 --- /dev/null +++ b/xcode/fastlane/touchlane/lib/match/storage/local_storage.rb @@ -0,0 +1,84 @@ +require 'match' +require 'fileutils' +require 'fastlane_core/ui/ui' + +module Touchlane + class LocalStorage < Match::Storage::Interface + attr_accessor :signing_identities_path + + def self.configure(params) + return self.new( + # we can't pass signing_identities_path since params is hardcoded in match/runner.rb + signing_identities_path: params[:git_url] + ) + end + + def initialize(signing_identities_path: nil) + self.signing_identities_path = signing_identities_path + end + + def prefixed_working_directory + return working_directory + end + + def download + # Check if we already have a functional working_directory + return if @working_directory + + # No existing working directory, creating a new one now + self.working_directory = Dir.mktmpdir + + Dir.mkdir(self.signing_identities_path) unless File.exists?(self.signing_identities_path) + + FileUtils.cp_r("#{self.signing_identities_path}/.", self.working_directory) + end + + def human_readable_description + "Local folder [#{self.signing_identities_path}]" + end + + def upload_files(files_to_upload: [], custom_message: nil) + # `files_to_upload` is an array of files that need to be moved to signing identities dir + # Those doesn't mean they're new, it might just be they're changed + # Either way, we'll upload them using the same technique + + files_to_upload.each do |current_file| + # Go from + # "/var/folders/px/bz2kts9n69g8crgv4jpjh6b40000gn/T/d20181026-96528-1av4gge/profiles/development/Development_me.mobileprovision" + # to + # "profiles/development/Development_me.mobileprovision" + # + + # We also have to remove the trailing `/` as Google Cloud doesn't handle it nicely + target_path = current_file.gsub(self.working_directory + "/", "") + absolute_target_path = File.join(self.signing_identities_path, target_path) + + FileUtils.mkdir_p(File.dirname(absolute_target_path)) + + FileUtils.cp_r(current_file, absolute_target_path, remove_destination: true) + end + end + + def delete_files(files_to_delete: [], custom_message: nil) + files_to_delete.each do |file_name| + target_path = file_name.gsub(self.working_directory + "/", "") + File.delete(File.join(self.signing_identities_path, target_path)) + end + end + + def skip_docs + false + end + + def list_files(file_name: "", file_ext: "") + Dir[File.join(working_directory, "**", file_name, "*.#{file_ext}")] + end + + def generate_matchfile_content + path = Fastlane::UI.input("Path to the signing identities folder: ") + + return "git_url(\"#{path}\")" + end + + end +end diff --git a/xcode/fastlane/touchlane/lib/touchlane.rb b/xcode/fastlane/touchlane/lib/touchlane.rb index 8038507..7ece7cd 100644 --- a/xcode/fastlane/touchlane/lib/touchlane.rb +++ b/xcode/fastlane/touchlane/lib/touchlane.rb @@ -1,4 +1,6 @@ module Touchlane - require_relative "configuration_type" - require_relative "configuration" -end \ No newline at end of file + require_relative "touchlane/configuration_type" + require_relative "touchlane/configuration" + require_relative "touchlane/features" + require_relative "match/storage/local_storage" +end diff --git a/xcode/fastlane/touchlane/lib/configuration.rb b/xcode/fastlane/touchlane/lib/touchlane/configuration.rb similarity index 100% rename from xcode/fastlane/touchlane/lib/configuration.rb rename to xcode/fastlane/touchlane/lib/touchlane/configuration.rb diff --git a/xcode/fastlane/touchlane/lib/configuration_type.rb b/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb similarity index 65% rename from xcode/fastlane/touchlane/lib/configuration_type.rb rename to xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb index a47434e..9c9e455 100644 --- a/xcode/fastlane/touchlane/lib/configuration_type.rb +++ b/xcode/fastlane/touchlane/lib/touchlane/configuration_type.rb @@ -14,16 +14,21 @@ module Touchlane def initialize(type) @type = type + @is_app_store = type == APP_STORE + @is_development = type == DEVELOPMENT + case type - when DEVELOPMENT, ENTERPRISE + when DEVELOPMENT @export_method = type - @configuration = type == DEVELOPMENT ? "Debug" : "Release" - @is_app_store = false - @prefix = type == DEVELOPMENT ? DEVELOPMENT_PREFIX : ENTERPRISE_PREFIX + @configuration = "Debug" + @prefix = DEVELOPMENT_PREFIX + when ENTERPRISE + @export_method = type + @configuration = "Release" + @prefix = ENTERPRISE_PREFIX when APP_STORE @export_method = "app-store" @configuration = "AppStore" - @is_app_store = true @prefix = APP_STORE_PREFIX else raise "Unknown type passed #{type}" @@ -32,7 +37,7 @@ module Touchlane private_class_method :new - attr_reader :export_method, :type, :configuration, :is_app_store, :prefix + attr_reader :export_method, :type, :configuration, :is_app_store, :is_development, :prefix def self.from_lane_name(lane_name) case @@ -52,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, diff --git a/xcode/fastlane/touchlane/lib/touchlane/features.rb b/xcode/fastlane/touchlane/lib/touchlane/features.rb new file mode 100644 index 0000000..cadfb0f --- /dev/null +++ b/xcode/fastlane/touchlane/lib/touchlane/features.rb @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..45bdda8 --- /dev/null +++ b/xcode/managers/lib/file_manager.rb @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..adb59c5 --- /dev/null +++ b/xcode/managers/lib/template_manager.rb @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..9c3c53d --- /dev/null +++ b/xcode/managers/managers.rb @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..c26788d --- /dev/null +++ b/xcode/templates/templates.rb @@ -0,0 +1,3 @@ +module Templates + require_relative "templates/features_templates" +end diff --git a/xcode/templates/templates/features_templates.rb b/xcode/templates/templates/features_templates.rb new file mode 100755 index 0000000..69eeeaf --- /dev/null +++ b/xcode/templates/templates/features_templates.rb @@ -0,0 +1,34 @@ +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