#!/usr/bin/env ruby require 'rubygems' require 'nixenvironment' require 'commander/import' require 'yaml' require 'fileutils' require 'tmpdir' require 'active_support/core_ext/object/blank' require 'terminal-table' require 'colorize' include Nixenvironment CONFIG_SETTINGS_FILE_PATH = File.join(File.dirname(__FILE__), CONFIG_SETTINGS_FILE_NAME) program :name, 'nixenvironment' program :version, VERSION program :description, 'NIX projects build and deploy utility' default_command :help global_option ('--project_to_build VALUE') { |value| $project_to_build = value } global_option ('--project_target_to_build VALUE') { |value| $project_target_to_build = value } global_option ('--project_target_to_test VALUE') { |value| $project_target_to_test = value } global_option ('--workspace_to_build VALUE') { |value| $workspace_to_build = value } global_option ('--workspace_scheme_to_build VALUE') { |value| $workspace_scheme_to_build = value } global_option ('--workspace_scheme_to_test VALUE') { |value| $workspace_scheme_to_test = value } global_option ('--sdk VALUE') { |value| $sdk = value } global_option ('--sdk_for_tests VALUE') { |value| $sdk_for_tests = value } global_option ('--exclude_pattern_for_code_coverage VALUE') { |value| $exclude_pattern_for_code_coverage = value } global_option ('--exclude_pattern_for_code_duplication VALUE') { |value| $exclude_pattern_for_code_duplication = value } global_option ('--deploy_host VALUE') { |value| $deploy_host = value } global_option ('--deploy_path VALUE') { |value| $deploy_path = value } global_option ('--deploy_username VALUE') { |value| $deploy_username = value } global_option ('--deploy_password VALUE') { |value| $deploy_password = value } global_option ('--deploy_itunesconnect_username VALUE') { |value| $deploy_itunesconnect_username = value } global_option ('--icons_path VALUE') { |value| $icons_path = value } global_option ('--xctest_destination_device VALUE') { |value| $xctest_destination_device = value } global_option ('--configuration_files_path VALUE') { |value| $configuration_files_path = value } global_option ('--code_coverage_configuration VALUE') { |value| $code_coverage_configuration = value } global_option ('--code_coverage_output_directory VALUE') { |value| $code_coverage_output_directory = value } global_option ('--env_var_prefix VALUE') { |value| $env_var_prefix = value } global_option ('--bundle_id VALUE') { |value| $bundle_id = value } global_option ('--resigned_bundle_id VALUE') { |value| $resigned_bundle_id = value } global_option ('--resigned_watchkit_app_bundle_id VALUE') { |value| $resigned_watchkit_app_bundle_id = value } global_option ('--resigned_watchkit_extension_bundle_id VALUE') { |value| $resigned_watchkit_extension_bundle_id = value } global_option ('--resigned_widget_bundle_id VALUE') { |value| $resigned_widget_bundle_id = value } global_option ('--resigned_bundle_name VALUE') { |value| $resigned_bundle_name = value } global_option ('--resigned_entitlements_path VALUE') { |value| $resigned_entitlements_path = value } global_option ('--resigned_watchkit_extension_entitlements_path VALUE') { |value| $resigned_watchkit_extension_entitlements_path = value } global_option ('--resigned_widget_entitlements_path VALUE') { |value| $resigned_widget_entitlements_path = value } command :init do |c| c.syntax = 'nixenvironment init' c.description = 'Initialize template project of selected type in the destination repository and clone it to current folder' c.action { init } end command :build do |c| c.syntax = 'nixenvironment build [options]' c.description = 'Build project for selected configuration and make signed/resigned ipa' c.option '--config NAME', String, 'Select configuration' c.option '--ipa TYPES', String, 'Select sign (device, resigned_device, resigned_adhoc, resigned_appstore)' c.option '--unity_path PATH', String, 'Select unity executable path (UNITY, UNITY_4 or custom path)' c.option '--development_build', 'Enable Developmen flag in Unity project' c.option '--keystore_path PATH', String, 'Specify the path to .keystore file' c.option '--keystore_password PASSWORD', String, 'Specify the password for accessing a .keystore file' c.option '--key_alias_name NAME', String, 'Specify the alias name which should be used from .keystore file to sign a release version of an APK' c.option '--key_alias_password PASSWORD', String, 'Specify the password for accessing an alias name' c.option '--unity_platform TARGET PLATFORM', String, 'Select target platform for unity build (ios or android)' c.option '--ndsym', 'Disable .dsym generation for ios project' c.option '--icon_tagger MODE', String, 'Set XcodeIconTagger mode (full, short, off)' c.action do |_args, options| options.default :config => 'Debug', :ipa => 'device', :icon_tagger => 'full', :unity_path => 'UNITY' need_to_build_ios = true is_unity_platform = options.unity_platform.present? if is_unity_platform unity_path = ENV[options.unity_path] || options.unity_path need_to_build_ios = unity_build(options.config, options.unity_platform, unity_path, options.development_build, options.keystore_path, options.keystore_password, options.key_alias_name, options.key_alias_password) end if need_to_build_ios Dir.chdir(UNITY_IOS_PROJECT_PATH) if is_unity_platform read_config_settings @config_settings[CONFIGURATION_KEY] = options.config if is_unity_platform @config_settings[WORKSPACE_TO_BUILD_KEY] = nil @config_settings[PROJECT_TO_BUILD_KEY] = UNITY_BUILDS_IOS_PROJECT @config_settings[WORKSPACE_SCHEME_TO_BUILD_KEY] = UNITY_BUILDS_IOS_SCHEME @config_settings[PROJECT_TARGET_TO_BUILD_KEY] = nil @config_settings[ICONS_PATH_KEY] = UNITY_BUILDS_ICONS_PATH end begin Xcodebuild.read_config_settings(@config_settings[WORKSPACE_TO_BUILD_KEY], @config_settings[PROJECT_TO_BUILD_KEY], @config_settings[WORKSPACE_SCHEME_TO_BUILD_KEY], @config_settings[PROJECT_TARGET_TO_BUILD_KEY], @config_settings[SDK_KEY], @config_settings[CONFIGURATION_KEY]) raise 'Failed read_config_settings!' unless Xcodebuild.last_cmd_success? rescue => e error('Build error!', e) end @config_settings.merge!(Xcodebuild.config_settings) begin prebuild(options.config) build(options.config, options.ipa, options.ndsym, options.icon_tagger) ensure restore_info_plists end end end end command :deploy do |c| c.syntax = 'nixenvironment deploy' c.description = 'Deploy built artifacts to given server' c.option '--unity_platform TARGET PLATFORM', String, 'Select target platform for unity deploy (ios or android)' c.option '--deliver_deploy', 'Not only verify but also submit the build on iTunes Connect. (resigned_appstore builds only)' c.action do |_args, options| need_to_deploy_ios = true is_unity_platform = options.unity_platform.present? need_to_deploy_ios = unity_deploy(options.unity_platform) if is_unity_platform if need_to_deploy_ios Dir.chdir(UNITY_IOS_PROJECT_PATH) if is_unity_platform read_config_settings deploy(options.deliver_deploy) end end end command :clean do |c| c.syntax = 'nixenvironment clean' c.description = 'Remove temp files and clean all targets for xcode project' c.action { clean } end command :test do |c| c.syntax = 'nixenvironment test' c.description = 'Build xctest unit tests and run them in simulator' c.action { read_config_settings and test } end command :code_coverage do |c| c.syntax = 'nixenvironment code_coverage' c.description = 'Generate xctest unit tests code coverage report' c.action { read_config_settings and code_coverage } end command :code_duplication_report do |c| c.syntax = 'nixenvironment code_duplication_report' c.description = 'Generate code duplication report' c.action { read_config_settings and code_duplication_report } end command :tag do |c| c.syntax = 'nixenvironment tag' c.description = 'Make SVN/git/mercurial tag, SCM_USERNAME and SCM_PASSWORD must be defined on calling this target' c.action { tag } end command :svn_tag_from_jenkins do |c| c.syntax = 'nixenvironment svn_tag_from_jenkins' c.description = 'Make tag by finding the first credential in local credential storage' c.action { svn_tag_from_jenkins } end command :clean_working_copy do |c| c.syntax = 'nixenvironment clean_working_copy' c.description = 'Make working copy clean' c.option '--all', 'Remove ignored files too' c.action { |_args, options| clean_working_copy(options.all) } end def init template_project_url = nil template_project_types = [] puts 'Select project type (put index or name):' TEMPLATES_REPO_LIST.each_with_index do |(key, _value), index| selection_index = index + 1 selection = "#{selection_index}. #{key}" template_project_types.push(selection) puts selection end loop do type = ask('> ', template_project_types) type_index = type.to_i - 1 if TEMPLATES_REPO_LIST.key?(type) template_project_url = TEMPLATES_REPO_LIST[type] break elsif (0...TEMPLATES_REPO_LIST.size).include?(type_index) template_project_url = TEMPLATES_REPO_LIST.values[type_index] break end end puts dest_url = ask('Destination repository url: ') local_directory_to_clone = ask('Directory to clone into: ') repo_name = File.basename(dest_url, GIT_EXT) template_project_name = File.basename(template_project_url, GIT_EXT) begin FileUtils.rm_rf(ADJUSTER_WORKING_COPY_PATH) Dir.mkdir(ADJUSTER_WORKING_COPY_PATH) Dir.chdir(ADJUSTER_WORKING_COPY_PATH) do puts puts 'Cloning template project ...' Git.clone(template_project_url, nil, :r => true) raise "Authentication failed for #{template_project_url}!" unless Git.last_cmd_success? tags = nil Dir.chdir(template_project_name) do puts 'Fetch available tags ...' Git.fetch(:t => true) tags = Git.tag end raise 'Failed to get last tag!' if tags.blank? FileUtils.rm_rf(ADJUSTER_TEMP_PROJECT_NAME) if Dir.exist?(ADJUSTER_TEMP_PROJECT_NAME) FileUtils.cp_r(template_project_name, ADJUSTER_TEMP_PROJECT_NAME) Dir.chdir(ADJUSTER_TEMP_PROJECT_NAME) do puts 'Checkout newest template project tag ...' Git.checkout(tags.last, :orphan => true) puts "Push ... #{dest_url}" Git.remote_add(repo_name, dest_url) Git.commit(:m => 'Initial commit') Git.push(repo_name, Git::DEFAULT_REFSPEC, :f => true) raise "Push failed for #{dest_url} repository!" unless Git.last_cmd_success? end end puts "Cloning new created project from #{dest_url} to #{local_directory_to_clone} ..." Git.clone(dest_url, local_directory_to_clone, :r => true) raise "Error cloning #{dest_url}!" unless Git.last_cmd_success? rescue => e error('Project initialization failed!', e) end success('Project initialization complete!') end def read_config_settings begin @config_settings = YAML.load(File.read(CONFIG_SETTINGS_FILE_PATH)) rescue => e error("'#{CONFIG_SETTINGS_FILE_PATH}' file processing error!", e) end table = Terminal::Table.new table.title = 'Configuration Settings' table.headings = [{:value => 'Key', :alignment => :center}, {:value => 'Specification', :alignment => :center}, {:value => 'Value', :alignment => :center}] update_config_settings(PROJECT_TO_BUILD_KEY, $project_to_build, table, true) update_config_settings(PROJECT_TARGET_TO_BUILD_KEY, $project_target_to_build, table, true) update_config_settings(PROJECT_TARGET_TO_TEST_KEY, $project_target_to_test, table, true) update_config_settings(WORKSPACE_TO_BUILD_KEY, $workspace_to_build, table, true) update_config_settings(WORKSPACE_SCHEME_TO_BUILD_KEY, $workspace_scheme_to_build, table, true) update_config_settings(WORKSPACE_SCHEME_TO_TEST_KEY, $workspace_scheme_to_test, table, true) update_config_settings(SDK_KEY, $sdk, table, true) update_config_settings(SDK_FOR_TESTS_KEY, $sdk_for_tests, table, true) update_config_settings(EXCLUDE_PATTERN_FOR_CODE_COVERAGE_KEY, $exclude_pattern_for_code_coverage, table, true) update_config_settings(EXCLUDE_PATTERN_FOR_CODE_DUPLICATION_KEY, $exclude_pattern_for_code_duplication, table, true) update_config_settings(DEPLOY_HOST_KEY, $deploy_host, table, true) update_config_settings(DEPLOY_PATH_KEY, $deploy_path, table, true) update_config_settings(DEPLOY_USERNAME_KEY, $deploy_username, table, true) update_config_settings(DEPLOY_PASSWORD_KEY, $deploy_password, table, true) update_config_settings(DEPLOY_ITUNESCONNECT_USERNAME_KEY, $deploy_itunesconnect_username, table, true) update_config_settings(ICONS_PATH_KEY, $icons_path, table, true) update_config_settings(XCTEST_DESTINATION_DEVICE_KEY, $xctest_destination_device, table, true) update_config_settings(CONFIGURATION_FILES_PATH_KEY, $configuration_files_path, table, true) update_config_settings(CODE_COVERAGE_CONFIGURATION_KEY, $code_coverage_configuration, table, true) update_config_settings(CODE_COVERAGE_OUTPUT_DIRECTORY_KEY, $code_coverage_output_directory, table, true) update_config_settings(ENV_VAR_PREFIX_KEY, $env_var_prefix, table, true) update_config_settings(BUNDLE_ID_KEY, $bundle_id, table, true) update_config_settings(RESIGNED_BUNDLE_ID_KEY, $resigned_bundle_id, table, true) update_config_settings(RESIGNED_WATCHKIT_APP_BUNDLE_ID_KEY, $resigned_watchkit_app_bundle_id, table, true) update_config_settings(RESIGNED_WATCHKIT_EXTENSION_BUNDLE_ID_KEY, $resigned_watchkit_extension_bundle_id, table, true) update_config_settings(RESIGNED_WIDGET_BUNDLE_ID_KEY, $resigned_widget_bundle_id, table, true) update_config_settings(RESIGNED_BUNDLE_NAME_KEY, $resigned_bundle_name, table, true) update_config_settings(RESIGNED_ENTITLEMENTS_PATH_KEY, $resigned_entitlements_path, table, true) update_config_settings(RESIGNED_WATCHKIT_EXTENSION_ENTITLEMENTS_PATH_KEY, $resigned_watchkit_extension_entitlements_path, table, true) update_config_settings(RESIGNED_WIDGET_ENTITLEMENTS_PATH_KEY, $resigned_widget_entitlements_path, table, false) puts table end def update_config_settings(key, value, table, need_separator) if value @config_settings[key] = value spec_column = "|SPECIFIED| directly" else spec_column = "|NOT specified| directly. Used from Config" end table.add_row [key, spec_column, @config_settings[key]] table.add_separator if need_separator end def working_copy_is_clean? system(" LAST_REVISION_FILE=\"_last_revision.sh\" source ${LAST_REVISION_FILE} if [ ${WORKING_COPY_IS_CLEAN} -eq 1 ]; then echo \"Working copy is clean. Continuing ...\" else echo \"error: working copy must not have local modifications.\" 1>&2 echo \"You must add following files and folders to .gitignore:\" echo \"#{Git.status(:porcelain => true)}\" exit 1 fi") end def save_build_env_vars executable_name = @config_settings[EXECUTABLE_NAME_KEY] watchkit_app_executable = executable_name + WATCHKIT_APP_SUFFIX_WITH_EXT watchkit_extension_executable = executable_name + WATCHKIT_EXTENSION_SUFFIX_WITH_EXT widget_executable = executable_name + WIDGET_SUFFIX_WITH_EXT app_product = File.join(@config_settings[BUILT_PRODUCTS_DIR_KEY], executable_name) + APP_EXT watchkit_extension_relative_product = @config_settings[RESIGNED_WATCHKIT_EXTENSION_BUNDLE_ID_KEY].nil? ? nil : File.join(IOS_PLUGINS_FOLDER_NAME, watchkit_extension_executable) watchkit_app_relative_product = @config_settings[RESIGNED_WATCHKIT_APP_BUNDLE_ID_KEY].nil? ? nil : File.join(watchkit_extension_relative_product, watchkit_app_executable) widget_relative_product = @config_settings[RESIGNED_WIDGET_BUNDLE_ID_KEY].nil? ? nil : File.join(IOS_PLUGINS_FOLDER_NAME, widget_executable) resigned_watchkit_extension_entitlements_path = @config_settings[RESIGNED_WATCHKIT_EXTENSION_ENTITLEMENTS_PATH_KEY].nil? ? nil : @config_settings[RESIGNED_WATCHKIT_EXTENSION_ENTITLEMENTS_PATH_KEY] resigned_widget_entitlements_path = @config_settings[RESIGNED_WIDGET_ENTITLEMENTS_PATH_KEY].nil? ? nil : @config_settings[RESIGNED_WIDGET_ENTITLEMENTS_PATH_KEY] system(" echo \"#!/bin/sh\ ### AUTOGENERATED BY Nixenvironment; DO NOT EDIT ### PROJECT='#{@config_settings[PROJECT_KEY]}' BUILT_PRODUCTS_DIR='#{@config_settings[BUILT_PRODUCTS_DIR_KEY]}' OBJECTS_NORMAL_DIR='#{@config_settings[OBJECT_FILE_DIR_NORMAL_KEY]}' EXECUTABLE_NAME='#{@config_settings[EXECUTABLE_NAME_KEY]}' APP_PRODUCT='#{app_product}' WATCHKIT_APP_RELATIVE_PRODUCT='#{watchkit_app_relative_product}' WATCHKIT_EXTENSION_RELATIVE_PRODUCT='#{watchkit_extension_relative_product}' WIDGET_RELATIVE_PRODUCT='#{widget_relative_product}' APP_DSYM='#{app_product + DSYM_EXT}' APP_INFOPLIST_FILE='#{@config_settings[PRODUCT_SETTINGS_PATH_KEY]}' EMBEDDED_PROFILE='#{app_product}/#{@config_settings[EMBEDDED_PROFILE_NAME_KEY]}' TARGET_NAME='#{@config_settings[TARGET_NAME_KEY]}' CONFIGURATION='#{@config_settings[CONFIGURATION_KEY]}' SDK_NAME='#{@config_settings[SDK_NAME_KEY]}' RESIGNED_BUNDLE_ID='#{@config_settings[RESIGNED_BUNDLE_ID_KEY]}' RESIGNED_WATCHKIT_APP_BUNDLE_ID='#{@config_settings[RESIGNED_WATCHKIT_APP_BUNDLE_ID_KEY]}' RESIGNED_WATCHKIT_EXTENSION_BUNDLE_ID='#{@config_settings[RESIGNED_WATCHKIT_EXTENSION_BUNDLE_ID_KEY]}' RESIGNED_WIDGET_BUNDLE_ID='#{@config_settings[RESIGNED_WIDGET_BUNDLE_ID_KEY]}' RESIGNED_BUNDLE_NAME='#{@config_settings[RESIGNED_BUNDLE_NAME_KEY]}' RESIGNED_ENTITLEMENTS_PATH='#{@config_settings[RESIGNED_ENTITLEMENTS_PATH_KEY]}' RESIGNED_WATCHKIT_EXTENSION_ENTITLEMENTS_PATH='#{resigned_watchkit_extension_entitlements_path}' RESIGNED_WIDGET_ENTITLEMENTS_PATH='#{resigned_widget_entitlements_path}'\" > _last_build_vars.sh ") end def prebuild(config) system("#{SAVE_REVISION_SCRIPT_PATH}") error('Error! Working copy is not clean!') unless working_copy_is_clean? backup_info_plists save_build_env_vars update_info_plists(config) end def build(config, ipa, ndsym, icon_tagger) project_to_build = @config_settings[PROJECT_TO_BUILD_KEY] project_target_to_build = @config_settings[PROJECT_TARGET_TO_BUILD_KEY] workspace_to_build = @config_settings[WORKSPACE_TO_BUILD_KEY] workspace_scheme_to_build = @config_settings[WORKSPACE_SCHEME_TO_BUILD_KEY] sdk = @config_settings[SDK_KEY] configuration_build_dir = @config_settings[CONFIGURATION_BUILD_DIR_KEY] dwarf_dsym_folder_path = @config_settings[DWARF_DSYM_FOLDER_PATH_KEY] built_products_dir = @config_settings[BUILT_PRODUCTS_DIR_KEY] env_var_prefix = @config_settings[ENV_VAR_PREFIX_KEY] debug_information_format = ndsym ? 'dwarf' : 'dwarf-with-dsym' other_args = { 'DEBUG_INFORMATION_FORMAT' => debug_information_format, 'DWARF_DSYM_FOLDER_PATH' => dwarf_dsym_folder_path, 'CONFIGURATION_BUILD_DIR' => configuration_build_dir, 'BUILT_PRODUCTS_DIR' => built_products_dir } Xcodebuild.build(sdk, config, project_to_build, project_target_to_build, workspace_to_build, workspace_scheme_to_build, env_var_prefix, other_args) error('Build error!') unless Xcodebuild.last_cmd_success? if sdk.include?('macos') begin Archiver.make_macos_zip rescue => e error("Make 'macos_zip' error!", e) end success("Make 'macos_zip' complete!") elsif sdk.include?('iphoneos') if config == 'Release' puts 'IconTagger: configuration is Release. Skipping ...' else case icon_tagger when 'full' then tag_icon(false) when 'short' then tag_icon(true) when 'off' then puts 'IconTagger is disabled. Skipping ...' else puts "Unknown IconTagger mode: '#{icon_tagger}'. Skipping ..." end end ipa.split.each do |current_ipa| begin case current_ipa when 'device' then Archiver.make_signed_ipa when 'resigned_device' then Archiver.make_resigned_ipa_for_device when 'resigned_adhoc' then Archiver.make_resigned_ipa_for_adhoc when 'resigned_appstore' then Archiver.make_resigned_ipa_for_appstore else raise "Unknown ipa '#{current_ipa}'!" end rescue => e error("Make '#{current_ipa}' error!", e) end success("Make '#{current_ipa}' complete!") end else error("Build error! Unknown sdk: #{sdk}!") end success('Build complete!') end def unity_build(configuration, unity_platform, unity_path, development_build, keystore_path, keystore_password, key_alias_name, key_alias_password) need_to_build_ios = false system("#{SAVE_REVISION_SCRIPT_PATH}") error('Error! Working copy is not clean!') unless working_copy_is_clean? if File.directory?(UNITY_EDITOR_DIR) FileUtils.cp_r(UNITY_BUILD_SCRIPTS_DIR, UNITY_EDITOR_DIR) else error("Copy #{UNITY_BUILD_SCRIPTS_PATH} error! #{UNITY_EDITOR_DIR} doesn't exist!") end case unity_platform when 'ios' need_to_build_ios = true development_build_arg = development_build ? ';developmentBuild=' : '' puts 'Generating IOS project from UNITY project ...' unity_success = system("#{unity_path} -executeMethod NIXBuilder.MakeiOSBuild -projectPath '#{Dir.pwd}'\ -batchmode -logFile -quit -customArgs:buildPath='#{UNITY_IOS_PROJECT_PATH}#{development_build_arg}'") error('iOS build unity error!') unless unity_success success('IOS project was generated.') clean_working_copy(false) $project_to_build = 'Unity-iPhone.xcodeproj' $project_target_to_build = 'Unity-iPhone' when 'android' development_build_arg = development_build ? '--development-build' : '' keystore_path_arg = keystore_path ? "--keystore-path #{keystore_path}" : '' keystore_password_arg = keystore_password ? "--keystore-password #{keystore_password}" : '' key_alias_name_arg = key_alias_name ? "--key-alias-name #{key_alias_name}" : '' key_alias_password_arg = key_alias_password ? "--key-alias-password #{key_alias_password}" : '' build_success = system("#{UNITY_BUILD_ANDROID_SCRIPT_PATH} --configuration #{configuration} --unity-path #{unity_path}\ #{development_build_arg} #{keystore_path_arg} #{keystore_password_arg} #{key_alias_name_arg} #{key_alias_password_arg}") error('Android build unity error!') unless build_success clean_working_copy(false) else error("Error: Unknown unity target platform '#{unity_platform}'!") end success('Unity build complete!') need_to_build_ios end def tag_icon(short_version) plist_path = @config_settings[PRODUCT_SETTINGS_PATH_KEY] info_plist = Plist.from_file(plist_path) version = info_plist['CFBundleShortVersionString'] monotonic_revision = %x[ source _last_revision.sh && echo ${MONOTONIC_REVISION} ].strip! style = short_version ? 'OneLine' : 'TwoLine' mask_path = File.join(TAGGER_UTILITY_DIRECTORY, "masks/#{style}Mask.png") icons_dir = File.join(Dir.pwd, @config_settings[ICONS_PATH_KEY]) app_product = File.join(@config_settings[BUILT_PRODUCTS_DIR_KEY], @config_settings[EXECUTABLE_NAME_KEY]) + APP_EXT system("#{TAGGER_UTILITY_PATH} --shortVersion=#{version}\ --buildNumber=#{monotonic_revision}\ --style=#{style}\ --maskPath=\"#{mask_path}\"\ --plist=\"#{plist_path}\"\ --sourceIconsPath=\"#{icons_dir}\"\ --destinationIconsPath=#{app_product}") end def backup_info_plists @info_plist_backup_name = backup_info_plist(@config_settings[PRODUCT_SETTINGS_PATH_KEY], nil) @watchkit_app_info_plist_backup_name = backup_info_plist(@config_settings[WATCHKIT_APP_PRODUCT_SETTINGS_PATH_KEY], WATCHKIT_APP_PREFIX) @watchkit_extension_info_plist_backup_name = backup_info_plist(@config_settings[WATCHKIT_EXTENSION_PRODUCT_SETTINGS_PATH_KEY], WATCHKIT_EXTENSION_PREFIX) @widget_info_plist_backup_name = backup_info_plist(@config_settings[WIDGET_PRODUCT_SETTINGS_PATH_KEY], WIDGET_PREFIX) end def backup_info_plist(product_settings_path, description) return if product_settings_path.blank? puts "Backuping #{description}Info.plist ..." info_plist_backup_name = product_settings_path + '.backup' FileUtils.cp(product_settings_path, info_plist_backup_name) puts "#{description}Info.plist was backuped." info_plist_backup_name end def update_info_plists(config) revision = %x[ source _last_revision.sh && echo ${REVISION} ].strip! monotonic_revision = %x[ source _last_revision.sh && echo ${MONOTONIC_REVISION} ].strip! bundle_id = @config_settings[BUNDLE_ID_KEY] update_info_plist(@config_settings[PRODUCT_SETTINGS_PATH_KEY], monotonic_revision, revision, bundle_id, config, nil) update_info_plist(@config_settings[WATCHKIT_APP_PRODUCT_SETTINGS_PATH_KEY], monotonic_revision, nil, nil, nil, WATCHKIT_APP_PREFIX) update_info_plist(@config_settings[WATCHKIT_EXTENSION_PRODUCT_SETTINGS_PATH_KEY], monotonic_revision, nil, nil, nil, WATCHKIT_EXTENSION_PREFIX) update_info_plist(@config_settings[WIDGET_PRODUCT_SETTINGS_PATH_KEY], monotonic_revision, nil, nil, nil, WIDGET_PREFIX) end def update_info_plist(product_settings_path, monotonic_revision, revision, bundle_id, config, description) return if product_settings_path.blank? puts "Updating #{description}Info.plist ..." begin info_plist = Plist.from_file(product_settings_path) info_plist['CFBundleVersion'] = monotonic_revision if monotonic_revision info_plist['RevisionNumber'] = revision if revision info_plist['CFBundleIdentifier'] = bundle_id if bundle_id info_plist['Configuration'] = config if config info_plist.save(product_settings_path, Plist::FORMAT_XML) rescue => e error("Update #{description}Info.plist error!", e) end success("#{description}Info.plist was updated.") end def restore_info_plists restore_info_plist(@config_settings[PRODUCT_SETTINGS_PATH_KEY], @info_plist_backup_name, nil) restore_info_plist(@config_settings[WATCHKIT_APP_PRODUCT_SETTINGS_PATH_KEY], @watchkit_app_info_plist_backup_name, WATCHKIT_APP_PREFIX) restore_info_plist(@config_settings[WATCHKIT_EXTENSION_PRODUCT_SETTINGS_PATH_KEY], @watchkit_extension_info_plist_backup_name, WATCHKIT_EXTENSION_PREFIX) restore_info_plist(@config_settings[WIDGET_PRODUCT_SETTINGS_PATH_KEY], @widget_info_plist_backup_name, WIDGET_PREFIX) end def restore_info_plist(product_settings_path, info_plist_backup_name, description) return if product_settings_path.blank? || info_plist_backup_name.blank? puts "Restoring #{description}Info.plist ..." File.delete(product_settings_path) File.rename(info_plist_backup_name, product_settings_path) puts "#{description}Info.plist was restored." end def deploy(deliver_deploy) deploy_host = @config_settings[DEPLOY_HOST_KEY].blank? ? ENV[DEPLOY_HOST_KEY] : @config_settings[DEPLOY_HOST_KEY] deploy_username = @config_settings[DEPLOY_USERNAME_KEY].blank? ? ENV[DEPLOY_USERNAME_KEY] : @config_settings[DEPLOY_USERNAME_KEY] deploy_password = @config_settings[DEPLOY_PASSWORD_KEY].blank? ? ENV[DEPLOY_PASSWORD_KEY] : @config_settings[DEPLOY_PASSWORD_KEY] deploy_itunesconnect_username = @config_settings[DEPLOY_ITUNESCONNECT_USERNAME_KEY].blank? ? ENV[DEPLOY_ITUNESCONNECT_USERNAME_KEY] : @config_settings[DEPLOY_ITUNESCONNECT_USERNAME_KEY] deploy_itunesconnect_username ||= 'unknown' deliver_deploy = deliver_deploy ? 1 : 0 sdk_name = %x[ source _last_build_vars.sh && echo ${SDK_NAME} ].strip! if sdk_name.include?('macos') build_env_vars = BuildEnvVarsLoader.load ipa_bundle_id = build_env_vars[IPA_BUNDLE_ID_KEY] current_app_version = build_env_vars[CURRENT_APP_VERSION_KEY] current_build_version = build_env_vars[CURRENT_BUILD_VERSION_KEY] name_for_deployment = build_env_vars[NAME_FOR_DEPLOYMENT_KEY] executable_name = build_env_vars[EXECUTABLE_NAME_KEY] ipa_product = build_env_vars[IPA_PRODUCT_KEY] app_dsym = build_env_vars[APP_DSYM_KEY] built_products_dir = build_env_vars[BUILT_PRODUCTS_DIR_KEY] local_path_to_app = File.join(Dir.tmpdir, ipa_bundle_id) local_path_to_build = File.join(local_path_to_app, "v.#{current_app_version}_#{current_build_version}") FileUtils.rm_rf(local_path_to_build) FileUtils.mkdir_p(local_path_to_build) configuration_full_path = File.join(local_path_to_build, name_for_deployment) FileUtils.mkdir(configuration_full_path) FileUtils.cp(ipa_product, File.join(configuration_full_path, executable_name) + ZIP_EXT) if File.exist?(app_dsym) Dir.chdir(built_products_dir) destination = File.join(configuration_full_path, "#{executable_name + APP_EXT + DSYM_EXT + ZIP_EXT}") system("zip -r \"#{destination}\" \"#{executable_name + APP_EXT + DSYM_EXT}\"") end deploy_path = @config_settings[DEPLOY_PATH_KEY].blank? ? MACOS_PROJECTS_DEPLOY_PATH : @config_settings[DEPLOY_PATH_KEY] deploy_success = system("#{DEPLOY_SCRIPT_PATH} #{deploy_host} #{deploy_path} #{deploy_username} #{deploy_password} #{local_path_to_app}") else deploy_path = @config_settings[DEPLOY_PATH_KEY].blank? ? ENV[DEPLOY_PATH_KEY] : @config_settings[DEPLOY_PATH_KEY] deploy_success = system("#{DEPLOY_IPA_SCRIPT_PATH} #{deploy_host} #{deploy_path} #{deploy_username} #{deploy_password} #{deploy_itunesconnect_username} #{deliver_deploy}") end deploy_success ? success('Deploy complete!') : error('Deploy error!') end def unity_deploy(unity_platform) need_to_deploy_ios = false case unity_platform when 'ios' then need_to_deploy_ios = true when 'android' then system(DEPLOY_APK_SCRIPT_PATH) ? success('Unity android deploy complete!') : error('Unity android deploy error!') else error("Error: Unknown unity target platform '#{unity_platform}'!") end need_to_deploy_ios end def clean system("#{REMOVE_TEMPORARY_FILES_SCRIPT_PATH}") system('rm -rf test-results/') system("find . -name \"*.pyc\" -exec rm -rf {} \;") Xcodebuild.clean_all_targets end def test code_coverage_config = File.join(@config_settings[CONFIGURATION_FILES_PATH_KEY], @config_settings[CODE_COVERAGE_CONFIGURATION_KEY]) Xcodebuild.test(@config_settings[SDK_FOR_TESTS_KEY], 'Debug', @config_settings[WORKSPACE_TO_BUILD_KEY], @config_settings[WORKSPACE_SCHEME_TO_TEST_KEY], code_coverage_config) error('Run test error!') unless Xcodebuild.last_cmd_success? end def code_coverage code_coverage_configuration = File.join(@config_settings[CONFIGURATION_FILES_PATH_KEY], @config_settings[CODE_COVERAGE_CONFIGURATION_KEY]) # TODO: move to Xcodebuild class report_success = system("#{CODE_COVERAGE_REPORT_SCRIPT_PATH} -workspace \"#{@config_settings[WORKSPACE_TO_BUILD_KEY]}\"\ -scheme \"#{@config_settings[WORKSPACE_SCHEME_TO_TEST_KEY]}\"\ -configuration \"Debug\"\ -sdk \"#{@config_settings[SDK_FOR_TESTS_KEY]}\"\ -xcconfig \"#{code_coverage_configuration}\"\ -exclude \"#{@config_settings[EXCLUDE_PATTERN_FOR_CODE_COVERAGE_KEY]}\"\ -output \"#{@config_settings[CODE_COVERAGE_OUTPUT_DIRECTORY_KEY]}\"\ -destination-timeout \"#{TESTS_AND_COVERAGE_TIMEOUT}\"\ -destination \"#{@config_settings[XCTEST_DESTINATION_DEVICE_KEY]}\"") error('Code coverage error!') unless report_success end def code_duplication_report duplication_success = system("#{CODE_DUPLICATION_REPORT_SCRIPT_PATH} \"#{@config_settings[EXCLUDE_PATTERN_FOR_CODE_DUPLICATION_KEY]}\" duplication.xml") error('Generate code duplication error!') unless duplication_success end def tag tag_success = system("#{MAKE_TAG_SCRIPT_PATH} \"#{ENV[SCM_USERNAME_KEY]}\" \"#{ENV[SCM_PASSWORD_KEY]}\"") error('Make tag error!') unless tag_success end # Jenkins stores SVN credentials locally in XML, so this command gets and uses them on making tag by finding the first credential in local credential storage # ATTENTION: if this command picks up wrong credentials, then you should manually edit subversion.credentials file on Jenkins in order to remove the wrong credential # Additional |echo| is needed in order to add newline, otherwise base64 encoding doesn't work def svn_tag_from_jenkins ENV[SCM_USERNAME_KEY]=%x[ $(shell xpath ../subversion.credentials \(//userName\)[1]/text\(\)) ] ENV[SCM_PASSWORD_KEY]=%x[ $(shell echo $$(xpath ../subversion.credentials \(//password\)[1]/text\(\) 2>/dev/null && echo) | openssl base64 -d) ] tag end def clean_working_copy(all) clean_all = all ? 'all' : nil clean_success = system("#{CLEAN_WORKING_COPY_SCRIPT_PATH} #{clean_all}") error('Clean working copy error!') unless clean_success end def error(msg, e = nil) if e.present? puts e.inspect puts e.backtrace end abort "#{msg} #{e.to_s.strip}".red.on_yellow.bold end def success(msg) puts msg.green.on_blue.bold end