diff --git a/android_hooks/pre-push b/android_hooks/pre-push new file mode 100755 index 0000000..cc5cdce --- /dev/null +++ b/android_hooks/pre-push @@ -0,0 +1,20 @@ +#!/bin/sh + +echo "Pre push static analysis" + +output=$(./gradlew staticAnalysis --daemon 2>&1) + +status=$? + +if [ "$status" = 0 ] ; then + exit 0 +else + build_date=$(date +'%Y-%m-%d_%T') + file_name="build_$build_date" + folder_name="./build_log/" + file_path="${folder_name}${file_name}" + mkdir -p "$folder_name" + echo "$output" >> "$file_path" + echo "Pre push static failed. Full log path: ${file_path}" + exit 1 +fi diff --git a/gradle/apiGenerator.gradle b/gradle/apiGenerator.gradle index c8680e4..e8f08af 100644 --- a/gradle/apiGenerator.gradle +++ b/gradle/apiGenerator.gradle @@ -7,7 +7,7 @@ configurations { } dependencies { - apigenerator 'ru.touchin:api-generator:1.2.9' + apigenerator 'ru.touchin:api-generator:1.4.0-beta1' } android.applicationVariants.all { variant -> diff --git a/gradle/apiGeneratorKotlinServer.gradle b/gradle/apiGeneratorKotlinServer.gradle index 2b980aa..2f98ab0 100644 --- a/gradle/apiGeneratorKotlinServer.gradle +++ b/gradle/apiGeneratorKotlinServer.gradle @@ -7,7 +7,7 @@ configurations { } dependencies { - apigeneratorKotlinServer 'ru.touchin:api-generator:1.3.2' + apigeneratorKotlinServer 'ru.touchin:api-generator:1.4.0-alpha1' } task generateApiModelsKotlinServer doLast { diff --git a/gradle/applicationFileNaming.gradle b/gradle/applicationFileNaming.gradle new file mode 100644 index 0000000..6c2e7e2 --- /dev/null +++ b/gradle/applicationFileNaming.gradle @@ -0,0 +1,12 @@ +android { + defaultConfig { + setProperty( + "archivesBaseName", + "${project.android.defaultConfig.applicationId}" + + "-" + + "${project.android.defaultConfig.versionName}" + + "-" + + "${project.android.defaultConfig.versionCode}" + ) + } +} diff --git a/gradle/installGitHooks.gradle b/gradle/installGitHooks.gradle new file mode 100644 index 0000000..8f1ebc4 --- /dev/null +++ b/gradle/installGitHooks.gradle @@ -0,0 +1,7 @@ +task installGitHooks(type: Copy) { + from new File(buildScriptsDir, 'android_hooks') + into { new File(rootProject.rootDir, '.git/hooks') } + fileMode 0777 +} + +tasks.getByPath(':app:preBuild').dependsOn installGitHooks diff --git a/lint/lint.xml b/lint/lint.xml index 4e33195..3709269 100644 --- a/lint/lint.xml +++ b/lint/lint.xml @@ -267,5 +267,6 @@ + diff --git a/proguard/common.pro b/proguard/common.pro index 937bc37..52715c2 100644 --- a/proguard/common.pro +++ b/proguard/common.pro @@ -6,3 +6,4 @@ -include rules/crashlytics.pro -include rules/glide.pro -include rules/kaspersky.pro +-include rules/appsflyer.pro diff --git a/proguard/rules/appsflyer.pro b/proguard/rules/appsflyer.pro new file mode 100644 index 0000000..6494fc5 --- /dev/null +++ b/proguard/rules/appsflyer.pro @@ -0,0 +1,5 @@ +-dontwarn com.google.android.gms.iid.** +-keep class com.appsflyer.** { *; } +-dontwarn android.app.job.JobParameters +-dontwarn com.android.installreferrer.** +-dontwarn com.appsflyer.** diff --git a/xcode/.swiftlint.yml b/xcode/.swiftlint.yml index 3e81483..b731824 100644 --- a/xcode/.swiftlint.yml +++ b/xcode/.swiftlint.yml @@ -60,6 +60,7 @@ excluded: - Carthage - Pods - Generated + - Localization line_length: 128 diff --git a/xcode/UnusedConfig.yml b/xcode/UnusedConfig.yml new file mode 100644 index 0000000..619dd71 --- /dev/null +++ b/xcode/UnusedConfig.yml @@ -0,0 +1,6 @@ +excluded-resources: + - Generated/ + - Resources/Localization + - Carthage/ + - Pods/ + - FormattingService.swift \ No newline at end of file diff --git a/xcode/aux_scripts/download_file.sh b/xcode/aux_scripts/download_file.sh index 9bfcbc7..55d74ba 100755 --- a/xcode/aux_scripts/download_file.sh +++ b/xcode/aux_scripts/download_file.sh @@ -1,12 +1,29 @@ file_name=$1 file_link=$2 +folder=$3 +flag_of_delete=$4 -folder="Downloads" -file_path="./${folder}/${file_name}" +readonly key_of_delete="--remove-cached" +readonly default_folder="./Downloads" + +if [[ ${folder} = ${key_of_delete} ]]; then + folder="${default_folder}" + flag_of_delete="${key_of_delete}" +fi + +if ! [ -n "$folder" ]; then + folder="${default_folder}" +fi + +file_path="${folder}/${file_name}" + +if [[ ${flag_of_delete} = ${key_of_delete} ]]; then + rm ${file_path} +fi # make folder if not exist if ! [ -e ${folder} ]; then - mkdir ${folder} + mkdir -p ${folder} fi # download file if not downloaded diff --git a/xcode/aux_scripts/import_strings.php b/xcode/aux_scripts/import_strings.php index 911e989..52f79d6 100644 --- a/xcode/aux_scripts/import_strings.php +++ b/xcode/aux_scripts/import_strings.php @@ -1,5 +1,5 @@ $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; diff --git a/xcode/build_phases/Unused.rb b/xcode/build_phases/Unused.rb new file mode 100644 index 0000000..478a8d2 --- /dev/null +++ b/xcode/build_phases/Unused.rb @@ -0,0 +1,189 @@ +# source: https://github.com/PaulTaykalo/swift-scripts/blob/master/unused.rb +#!/usr/bin/ruby +#encoding: utf-8 +require 'yaml' +require 'optparse' + +Encoding.default_external = Encoding::UTF_8 +Encoding.default_internal = Encoding::UTF_8 + +class Item + def initialize(file, line, at) + @file = file + @line = line + @at = at + 1 + if match = line.match(/(func|let|var|class|enum|struct|protocol)\s+(\w+)/) + @type = match.captures[0] + @name = match.captures[1] + end + end + + def modifiers + return @modifiers if @modifiers + @modifiers = [] + if match = @line.match(/(.*?)#{@type}/) + @modifiers = match.captures[0].split(" ") + end + return @modifiers + end + + def name + @name + end + + def file + @file + end + + def to_s + serialize + end + def to_str + serialize + end + + def full_file_path + Dir.pwd + '/' + @file + end + + def serialize + "#{@file} has unused \"#{@type.to_s} #{@name.to_s}\"" + end + def to_xcode + "#{full_file_path}:#{@at}:0: warning: #{@type.to_s} #{@name.to_s} is unused" + end + + +end +class Unused + def find + items = [] + unused_warnings = [] + + regexps = parse_arguments + + all_files = Dir.glob("**/*.swift").reject do |path| + File.directory?(path) + end + + all_files.each { |my_text_file| + file_items = grab_items(my_text_file) + file_items = filter_items(file_items) + + non_private_items, private_items = file_items.partition { |f| !f.modifiers.include?("private") && !f.modifiers.include?("fileprivate") } + items += non_private_items + + # Usage within the file + if private_items.length > 0 + unused_warnings += find_usages_in_files([my_text_file], [], private_items, regexps) + end + + } + + xibs = Dir.glob("**/*.xib") + storyboards = Dir.glob("**/*.storyboard") + + unused_warnings += find_usages_in_files(all_files, xibs + storyboards, items, regexps) + + if unused_warnings.length > 0 + # show warning + puts "Unused Code Warning!: warning: Total Count #{unused_warnings.length}" + puts "#{unused_warnings.map { |e| e.to_xcode}.join("\n")}" + # write log + File.open("UnusedLog.txt", "w") do |file| + file.write("Unused code warnings count: #{unused_warnings.length}\n\n") + file.write("#{unused_warnings.map { |e| e.serialize }.join("\n")}") + end + end + end + + def parse_arguments + resources = [] + + options = {} + OptionParser.new do |opts| + options[:exclude] = [] + + opts.on("-c", "--config=FileName") { |c| options[:config] = c } + opts.on("-i", "--exclude [a, b, c]", Array) { |i| options[:exclude] += i if !i.nil? } + + end.parse! + + # find --config file + if !options[:config].nil? + fileName = options[:config] + resources += YAML.load_file(fileName).fetch("excluded-resources") + elsif + puts "---------\n Warning: Config file is not provided \n---------" + end + + # find --exclude files + if !options[:exclude].nil? + resources += options[:exclude] + end + + # create and return Regexp + resources.map { |r| Regexp.new(r) } + end + + def grab_items(file) + lines = File.readlines(file).map {|line| line.gsub(/^\s*\/\/.*/, "") } + items = lines.each_with_index.select { |line, i| line[/(func|let|var|class|enum|struct|protocol)\s+\w+/] }.map { |line, i| Item.new(file, line, i)} + end + + def filter_items(items) + items.select { |f| + !f.name.start_with?("test") && !f.modifiers.include?("@IBAction") && !f.modifiers.include?("override") && !f.modifiers.include?("@objc") && !f.modifiers.include?("@IBInspectable") + } + end + + # remove files, that maches excluded Regexps array + def ignore_files_with_regexps(files, regexps) + files.select { |f| regexps.all? { |r| r.match(f.file).nil? } } + end + + def find_usages_in_files(files, xibs, items_in, regexps) + items = items_in + usages = items.map { |f| 0 } + files.each { |file| + lines = File.readlines(file).map {|line| line.gsub(/^[^\/]*\/\/.*/, "") } + words = lines.join("\n").split(/\W+/) + words_arrray = words.group_by { |w| w }.map { |w, ws| [w, ws.length] }.flatten + + wf = Hash[*words_arrray] + + items.each_with_index { |f, i| + usages[i] += (wf[f.name] || 0) + } + # Remove all items which has usage 2+ + indexes = usages.each_with_index.select { |u, i| u >= 2 }.map { |f, i| i } + + # reduce usage array if we found some functions already + indexes.reverse.each { |i| usages.delete_at(i) && items.delete_at(i) } + } + + xibs.each { |xib| + lines = File.readlines(xib).map {|line| line.gsub(/^\s*\/\/.*/, "") } + full_xml = lines.join(" ") + classes = full_xml.scan(/(class|customClass)="([^"]+)"/).map { |cd| cd[1] } + classes_array = classes.group_by { |w| w }.map { |w, ws| [w, ws.length] }.flatten + + wf = Hash[*classes_array] + + items.each_with_index { |f, i| + usages[i] += (wf[f.name] || 0) + } + # Remove all items which has usage 2+ + indexes = usages.each_with_index.select { |u, i| u >= 2 }.map { |f, i| i } + + # reduce usage array if we found some functions already + indexes.reverse.each { |i| usages.delete_at(i) && items.delete_at(i) } + + } + + items = ignore_files_with_regexps(items, regexps) + end +end + + +Unused.new.find diff --git a/xcode/build_phases/api_generator.sh b/xcode/build_phases/api_generator.sh index 552a538..a16d254 100644 --- a/xcode/build_phases/api_generator.sh +++ b/xcode/build_phases/api_generator.sh @@ -6,4 +6,4 @@ link="https://dl.bintray.com/touchin/touchin-tools/ru/touchin/api-generator/${VE . build-scripts/xcode/aux_scripts/download_file.sh ${FILE_NAME} ${link} # execute api generator -java -jar "Downloads/${FILE_NAME}" generate-client-code --output-language SWIFT --specification-path common/api --output-path ${PROJECT_NAME}/Generated --single-file true +java -jar "Downloads/${FILE_NAME}" generate-client-code --output-language SWIFT --specification-path common/api --output-path ${PRODUCT_NAME}/Generated --single-file true diff --git a/xcode/build_phases/localization.sh b/xcode/build_phases/localization.sh index e363844..8c8bf61 100644 --- a/xcode/build_phases/localization.sh +++ b/xcode/build_phases/localization.sh @@ -1,4 +1,4 @@ -LOCALIZATION_PATH="${PROJECT_NAME}/Resources/Localization" +LOCALIZATION_PATH="${PRODUCT_NAME}/Resources/Localization" #first argument set strings folder path STRINGS_FOLDER=${1:-"common/strings"} @@ -13,4 +13,4 @@ if ! [ -e "${PROJECT_DIR}/${STRINGS_FOLDER}" ]; then fi #second argument set strings script path -php ${2:-build-scripts/xcode/aux_scripts/import_strings.php} ${PROJECT_NAME} ${STRINGS_FOLDER} +php ${2:-build-scripts/xcode/aux_scripts/import_strings.php} ${PRODUCT_NAME} ${STRINGS_FOLDER} diff --git a/xcode/build_phases/unused.sh b/xcode/build_phases/unused.sh new file mode 100644 index 0000000..5a0a906 --- /dev/null +++ b/xcode/build_phases/unused.sh @@ -0,0 +1,4 @@ +arguments=("$@") +ignored_files=$(IFS=, ; echo "${arguments[*]}") + +ruby ${PROJECT_DIR}/build-scripts/xcode/build_phases/Unused.rb --config ${PROJECT_DIR}/build-scripts/xcode/UnusedConfig.yml --exclude ${ignored_files} diff --git a/xcode/commonFastfile b/xcode/commonFastfile index 040c3ab..6e8b230 100644 --- a/xcode/commonFastfile +++ b/xcode/commonFastfile @@ -9,6 +9,10 @@ private_lane :installDependencies do |options| sh("rm -rf #{podsReposPath}") end + if File.exists? "../Gemfile" + bundle_install(path: "../.gem") + end + if File.exists? "../Cartfile" begin carthage(command: "bootstrap", platform: "iOS") @@ -20,7 +24,7 @@ private_lane :installDependencies do |options| end cocoapods( - repo_update: false + repo_update: true ) end @@ -196,6 +200,76 @@ private_lane :openKeychain do |options| end 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' + 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}) + 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}) + new_encryption.decrypt_files + return new_encryption + end + + get_all_files = lambda do |stor| + Dir[File.join(stor.working_directory, "**", "*.{cer,p12,mobileprovision}")] + end + + storage = storage_conf.call + encryption = encryption_conf_for_storage.call(storage) + old_files = get_all_files.call(storage) + + sh("open #{storage.working_directory}") + + # we are not using prompt() since it requires non-empty input which is not a case for Enter (\n) + puts "Enter any key when you're done" + STDIN.gets + + encryption.encrypt_files + + files_to_commit = get_all_files.call(storage) + old_directory = storage.working_directory + storage.save_changes!(files_to_commit: files_to_commit) + + + # need to check, because saving changes with delete is another function (update repo if needed) + files_diff = old_files - files_to_commit + + # match can not work with both save/delete functionality `You can't provide both files_to_delete and files_to_commit right now` + # to avoid this we use storage twice if needed + + if files_diff.length > 0 + storage = storage_conf.call + encryption = encryption_conf_for_storage.call(storage) + + files_to_delete = files_diff.map do |file| + old_file = file + old_file.slice! old_directory + new_file = File.join(storage.working_directory, old_file) + File.delete(new_file) if File.exist?(new_file) + file = new_file + end + + encryption.encrypt_files + storage.save_changes!(files_to_delete: files_to_delete) + end + +end + + def get_keychain_options(options) keychain_name = options[:keychain_name] keychain_password = options[:keychain_password]