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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- xmlns:android
- ^$
-
-
-
-
-
-
-
-
- xmlns:.*
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
- ^$
-
-
-
-
-
-
-
-
- style
- ^$
-
-
-
-
-
-
-
-
- .*
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:layout_width
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:layout_height
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:layout_.*
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*:width
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*:height
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*
- http://schemas.android.com/apk/res/android
-
-
- BY_NAME
-
-
-
-
-
-
- .*
- .*
-
-
- BY_NAME
-
-
-
-
-
-
\ 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