#!/usr/bin/env ruby require 'rubygems' require 'nixenvironment' require 'commander/import' require 'yaml' require 'fileutils' require 'shellwords' include Nixenvironment # :name is optional, otherwise uses the basename of this executable program :name, 'nixenvironment' program :version, VERSION program :description, 'NIX projects build and deploy utility' 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_bundle_name VALUE') { |value| $resigned_bundle_name = value } global_option ('--resigned_entitlements_path VALUE') { |value| $resigned_entitlements_path = value } command :update do |c| c.syntax = 'nixenvironment update' c.description = 'Install or update ninbas and other environment stuff' c.option '--ninbas NAME', String, 'Select ninbas branch, tag or revision to clone' c.action do |args, options| update(options.ninbas) end 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 '--ci_build', 'Define NIXENV_CI_BUILD environment variable' 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 and options.unity_platform.length > 0 if is_unity_platform unity_path = ENV[options.unity_path].nil? ? options.unity_path : ENV[options.unity_path] need_to_build_ios, root_working_dir, need_chdir_to_root_working_dir = 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 begin read_config_settings if is_unity_platform @config_settings['ICONS_PATH'] = 'Unity-iPhone/Images.xcassets' unless $icons_path elsif @config_settings['WORKSPACE_TO_BUILD'] and @config_settings['WORKSPACE_TO_BUILD'].length > 0 scheme = @config_settings['WORKSPACE_SCHEME_TO_BUILD'] abort("Build error! Scheme #{scheme} doesn't exist") unless xcode_project_contains_scheme?(scheme) end enable_ci_build(options.ci_build) supplement_config_settings(options.config) prebuild(options.config) build(options.config, options.ipa, options.ndsym, options.icon_tagger) restore_info_plist rescue raise # re-rise exception but chdir to root_working_dir in ensure block first if needed ensure if need_chdir_to_root_working_dir and root_working_dir Dir.chdir(root_working_dir) end 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 if options.unity_platform and options.unity_platform.length > 0 need_to_deploy_ios, root_working_dir, need_chdir_to_root_working_dir = unity_deploy(options.unity_platform) end if need_to_deploy_ios begin read_config_settings deploy(options.deliver_deploy) rescue raise # re-rise exception but chdir to root_working_dir in ensure block first if needed ensure if need_chdir_to_root_working_dir and root_working_dir Dir.chdir(root_working_dir) end end 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 do |args, options| clean end end command :test do |c| c.syntax = 'nixenvironment test' c.description = 'Build xctest unit tests and run them in simulator' c.action do |args, options| read_config_settings test end end command :code_coverage do |c| c.syntax = 'nixenvironment code_coverage' c.description = 'Generate xctest unit tests code coverage report' c.action do |args, options| read_config_settings code_coverage end end command :code_duplication_report do |c| c.syntax = 'nixenvironment code_duplication_report' c.description = 'Generate code duplication report' c.action do |args, options| read_config_settings code_duplication_report end 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 do |args, options| tag end 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 do |args, options| svn_tag_from_jenkins end 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 do |args, options| clean_working_copy(options.all) end end def update(ninbas) root_working_directory = Dir.pwd target_directory = File.join(Dir.home, NIXENV_ROOT) begin Dir.mkdir(target_directory) unless Dir.exist?(target_directory) Dir.chdir(target_directory) REPO_LIST.each do |repo_name, repo_url| unless Dir.exist?(repo_name) clone_success = system("git clone #{repo_url} --recursive") unless clone_success p("Authentication failed for #{repo_name} project!") next end end Dir.chdir(repo_name) if repo_name == BUILD_SCRIPTS if ninbas system("git checkout #{ninbas}") Dir.chdir('..') next end end system("git fetch -t") tags = IO.popen('git tag').readlines tags.map! { |tag| tag.strip! } tags.sort_by!(&:to_i) if tags.size > 0 p("Checkout newest #{repo_name} tag...") system("git checkout #{tags.last}") else abort("Error checkout #{repo_name}! There is no tags!") end p("Checkout #{repo_name} #{tags.last} tag success!") Dir.chdir('..') end rescue @error_message = "#{$!}" ensure p(@error_message) if @error_message Dir.chdir(root_working_directory) end end def read_config_settings begin @config_settings = YAML.load(File.read(File.join(File.dirname(__FILE__), 'Config'))) rescue abort('Config file processing error!') end update_config_settings('PROJECT_TO_BUILD', $project_to_build) update_config_settings('PROJECT_TARGET_TO_BUILD', $project_target_to_build) update_config_settings('PROJECT_TARGET_TO_TEST', $project_target_to_test) update_config_settings('WORKSPACE_TO_BUILD', $workspace_to_build) update_config_settings('WORKSPACE_SCHEME_TO_BUILD', $workspace_scheme_to_build) update_config_settings('WORKSPACE_SCHEME_TO_TEST', $workspace_scheme_to_test) update_config_settings('SDK', $sdk) update_config_settings('SDK_FOR_TESTS', $sdk_for_tests) update_config_settings('EXCLUDE_PATTERN_FOR_CODE_COVERAGE', $exclude_pattern_for_code_coverage) update_config_settings('EXCLUDE_PATTERN_FOR_CODE_DUPLICATION', $exclude_pattern_for_code_duplication) update_config_settings('DEPLOY_HOST', $deploy_host) update_config_settings('DEPLOY_PATH', $deploy_path) update_config_settings('DEPLOY_USERNAME', $deploy_username) update_config_settings('DEPLOY_PASSWORD', $deploy_password) update_config_settings('DEPLOY_ITUNESCONNECT_USERNAME', $deploy_itunesconnect_username) update_config_settings('ICONS_PATH', $icons_path) update_config_settings('XCTEST_DESTINATION_DEVICE', $xctest_destination_device) update_config_settings('CONFIGURATION_FILES_PATH', $configuration_files_path) update_config_settings('CODE_COVERAGE_CONFIGURATION', $code_coverage_configuration) update_config_settings('CODE_COVERAGE_OUTPUT_DIRECTORY', $code_coverage_output_directory) update_config_settings('ENV_VAR_PREFIX', $env_var_prefix) update_config_settings('BUNDLE_ID', $bundle_id) update_config_settings('RESIGNED_BUNDLE_ID', $resigned_bundle_id) update_config_settings('RESIGNED_BUNDLE_NAME', $resigned_bundle_name) update_config_settings('RESIGNED_ENTITLEMENTS_PATH', $resigned_entitlements_path) end def update_config_settings(key, value) if value @config_settings[key] = value p("#{key} |SPECIFIED| directly: #{value}") else p("#{key} |NOT specified| directly. Used from Config: #{@config_settings[key]}") end end def enable_ci_build(ci_build) if ci_build ENV['NIXENV_CI_BUILD'] = '1' p('CI_BUILD enabled.') else ENV['NIXENV_CI_BUILD'] = nil p('CI_BUILD disabled.') end 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)\" exit 1 fi") end def supplement_config_settings(config) abort("Build error! Configuration #{config} doesn't exist") unless xcode_project_contains_config?(config) if @config_settings['PROJECT_TO_BUILD'] and @config_settings['PROJECT_TO_BUILD'].length > 0 if @config_settings['PROJECT_TARGET_TO_BUILD'] and @config_settings['PROJECT_TARGET_TO_BUILD'].length > 0 cmd_output = %x[ xcodebuild -project \"#{@config_settings['PROJECT_TO_BUILD']}\"\ -target \"#{@config_settings['PROJECT_TARGET_TO_BUILD']}\"\ -configuration \"#{config}\"\ -sdk \"#{@config_settings['SDK']}\"\ -showBuildSettings ] else cmd_output = %x[ xcodebuild -project \"#{@config_settings['PROJECT_TO_BUILD']}\"\ -scheme \"#{@config_settings['WORKSPACE_SCHEME_TO_BUILD']}\"\ -configuration \"#{config}\"\ -sdk \"#{@config_settings['SDK']}\"\ -showBuildSettings ] end elsif @config_settings['WORKSPACE_TO_BUILD'] and @config_settings['WORKSPACE_TO_BUILD'].length > 0 cmd_output = %x[ xcodebuild -workspace \"#{@config_settings['WORKSPACE_TO_BUILD']}\"\ -scheme \"#{@config_settings['WORKSPACE_SCHEME_TO_BUILD']}\"\ -configuration \"#{config}\"\ -sdk \"#{@config_settings['SDK']}\"\ -showBuildSettings ] @unescaped_watchkit_extension_product_settings_path = unescaped_product_settings_path(' WatchKit Extension', config) @unescaped_watchkit_app_product_settings_path = unescaped_product_settings_path(' WatchKit App', config) else abort('Build error! Either PROJECT_TO_BUILD or WORKSPACE_TO_BUILD must be specified!') end # $? - last cmd exit code unless $? == 0 abort('Build error!') end # Parse build settings env_vars_list = cmd_output.split(/\n/).reject(&:empty?) if env_vars_list and env_vars_list.length > 0 build_settings_to_strip = Hash[env_vars_list.map { |it| it.split('=', 2) }] build_settings_to_strip.each do |key, value| if key and value stripped_key = key.strip stripped_value = value.strip # TODO: investigate and fix me! Brutal hack with PRODUCT_SETTINGS_PATH because ruby File & FileUtils can't work with escaped paths if stripped_key == 'PRODUCT_SETTINGS_PATH' @unescaped_product_settings_path = stripped_value end @config_settings[stripped_key] ||= stripped_value.shellescape # Assign new value only if assignee is nil end end end build_directory = File.join(Dir.pwd, 'build').shellescape @config_settings['CONFIGURATION_BUILD_DIR'] = build_directory @config_settings['BUILT_PRODUCTS_DIR'] = build_directory @config_settings['DWARF_DSYM_FOLDER_PATH'] = build_directory plist_path = @unescaped_product_settings_path bundle_display_name = %x[ /usr/libexec/PlistBuddy -c 'Print CFBundleDisplayName' '#{plist_path}' ].strip! @config_settings['RESIGNED_BUNDLE_NAME'] ||= bundle_display_name.shellescape end def unescaped_product_settings_path(scheme_suffix, config) scheme = @config_settings['WORKSPACE_SCHEME_TO_BUILD'] + scheme_suffix if xcode_project_contains_scheme?(scheme) cmd_output = %x[ xcodebuild -workspace \"#{@config_settings['WORKSPACE_TO_BUILD']}\"\ -scheme \"#{scheme}\"\ -showBuildSettings ] # Parse build settings env_vars_list = cmd_output.split(/\n/).reject(&:empty?) if env_vars_list and env_vars_list.length > 0 build_settings_to_strip = Hash.new.compare_by_identity env_vars_list.each do |it| key_value = it.split('=', 2) build_settings_to_strip[key_value[0]] = key_value[1] end build_settings_to_strip.each do |key, value| if key and value stripped_key = key.strip stripped_value = value.strip if stripped_key == 'PRODUCT_SETTINGS_PATH' if stripped_value.include?(scheme_suffix) return stripped_value end end end end end end nil end def xcode_project_contains_config?(config) xcode_project_info['Build Configurations'].include?(config) end def xcode_project_contains_scheme?(scheme) xcode_project_info['Schemes'].include?(scheme) end def xcode_project_info # Parse information about project cmd_output = %x[ xcodebuild -list 2>&1 ] abort("Getting xcode_project_info error! #{cmd_output}") if cmd_output.include?('error') cmd_output = cmd_output.lines[1..-1].join # Get all lines except first in order to ignore description message info = {} cmd_output.split(/\n\n/).each do |pair| key,value = pair.split(/:/) next unless key and value lines = value.lines.map { |line| line.strip } lines.reject! { |line| line.empty? } info[key.strip] = lines end info end def save_build_env_vars app_product = File.join(@config_settings['BUILT_PRODUCTS_DIR'], @config_settings['EXECUTABLE_NAME']) + '.app' system(" echo \"#!/bin/sh\ ### AUTOGENERATED BY Nixenvironment; DO NOT EDIT ### PROJECT=#{@config_settings['PROJECT']} BUILT_PRODUCTS_DIR=#{@config_settings['BUILT_PRODUCTS_DIR']} OBJECTS_NORMAL_DIR=#{@config_settings['OBJECT_FILE_DIR_normal']} EXECUTABLE_NAME=#{@config_settings['EXECUTABLE_NAME']} APP_PRODUCT=#{app_product} APP_DSYM=#{app_product}.dSYM APP_INFOPLIST_FILE=#{@config_settings['PRODUCT_SETTINGS_PATH']} EMBEDDED_PROFILE=#{app_product}/#{@config_settings['EMBEDDED_PROFILE_NAME']} TARGET_NAME=#{@config_settings['TARGET_NAME']} CONFIGURATION=#{@config_settings['CONFIGURATION']} SDK_NAME=#{@config_settings['SDK_NAME']} RESIGNED_BUNDLE_ID=#{@config_settings['RESIGNED_BUNDLE_ID']} RESIGNED_BUNDLE_NAME=#{@config_settings['RESIGNED_BUNDLE_NAME']} RESIGNED_ENTITLEMENTS_PATH=#{@config_settings['RESIGNED_ENTITLEMENTS_PATH']}\" > _last_build_vars.sh ") end def prebuild(config) save_revision = File.join(BUILD_SCRIPTS_PATH, 'SaveRevision.sh') system("#{save_revision}") abort unless working_copy_is_clean? backup_info_plist save_build_env_vars update_info_plist(config) end def build(config, ipa, ndsym, icon_tagger) build = File.join(BUILD_SCRIPTS_PATH, 'Build.py') build_success = nil configuration_build_dir = @config_settings['CONFIGURATION_BUILD_DIR'] dwarf_dsym_folder_path = @config_settings['DWARF_DSYM_FOLDER_PATH'] built_products_dir = @config_settings['BUILT_PRODUCTS_DIR'] debug_information_format = ndsym ? 'dwarf' : 'dwarf-with-dsym' if @config_settings['PROJECT_TO_BUILD'] and @config_settings['PROJECT_TO_BUILD'].length > 0 if @config_settings['PROJECT_TARGET_TO_BUILD'] and @config_settings['PROJECT_TARGET_TO_BUILD'].length > 0 build_success = system("#{build} --project \"#{@config_settings['PROJECT_TO_BUILD']}\"\ --target \"#{@config_settings['PROJECT_TARGET_TO_BUILD']}\"\ --configuration \"#{config}\"\ --sdk \"#{@config_settings['SDK']}\"\ --env-var-prefix \"#{@config_settings['ENV_VAR_PREFIX']}\"\ 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}\"") else build_success = system("#{build} --project \"#{@config_settings['PROJECT_TO_BUILD']}\"\ --scheme \"#{@config_settings['WORKSPACE_SCHEME_TO_BUILD']}\"\ --configuration \"#{config}\"\ --sdk \"#{@config_settings['SDK']}\"\ --env-var-prefix \"#{@config_settings['ENV_VAR_PREFIX']}\"\ 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}\"") end elsif @config_settings['WORKSPACE_TO_BUILD'] and @config_settings['WORKSPACE_TO_BUILD'].length > 0 build_success = system("#{build} --workspace \"#{@config_settings['WORKSPACE_TO_BUILD']}\"\ --scheme \"#{@config_settings['WORKSPACE_SCHEME_TO_BUILD']}\"\ --configuration \"#{config}\"\ --sdk \"#{@config_settings['SDK']}\"\ --env-var-prefix \"#{@config_settings['ENV_VAR_PREFIX']}\"\ 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}\"") end unless build_success restore_info_plist abort('Build error!') end if config == 'Release' p('IconTagger: configuration is Release. Skipping...') else case icon_tagger when 'full' tag_icon(false) when 'short' tag_icon(true) when 'off' p('IconTagger is disabled. Skipping...') else p("Unknown IconTagger mode: '#{icon_tagger}'. Skipping...") end end ipa.split.each do |current_ipa| case current_ipa # create .ipa file from last built app product when 'device' make = File.join(BUILD_SCRIPTS_PATH, 'MakeIPA.sh') # resign last built app product with iPhone Developer profile and package it into .ipa file when 'resigned_device' make = File.join(BUILD_SCRIPTS_PATH, 'MakeResignedIPAForDevice.sh') # resign last built app product with iPhone Distribution AdHoc profile and package it into .ipa file when 'resigned_adhoc' make = File.join(BUILD_SCRIPTS_PATH, 'MakeResignedIPAForAdHocDistribution.sh') # resign last built app product with Appstore distribution profile and package it into .ipa file when 'resigned_appstore' make = File.join(BUILD_SCRIPTS_PATH, 'MakeResignedIPAForAppstore.sh') else restore_info_plist abort("Error: Unknown ipa '#{current_ipa}'!") end make_success = system("#{make}") if defined? make unless make_success restore_info_plist abort("#{make} error!") end end end def unity_build(configuration, unity_platform, unity_path, development_build, keystore_path, keystore_password, key_alias_name, key_alias_password) root_working_dir = nil need_chdir_to_root_working_dir = false need_to_build_ios = false save_revision = File.join(BUILD_SCRIPTS_PATH, 'SaveRevision.sh') system("#{save_revision}") abort unless working_copy_is_clean? unity_build_scripts_dir = File.join(BUILD_SCRIPTS_PATH, 'UnityBuildAutomationScripts') unity_editor_dir = File.join(Dir.pwd, 'Assets/Editor') if File.directory?(unity_editor_dir) FileUtils.cp_r(unity_build_scripts_dir, unity_editor_dir) else abort("Copy UnityBuildAutomationScripts error! #{unity_editor_dir} doesn't exist!") end case unity_platform when 'ios' need_to_build_ios = true root_working_dir = Dir.pwd ios_project_path = File.join(root_working_dir, UNITY_BUILDS_IOS_PATH) development_build_arg = development_build ? ';developmentBuild=' : '' p('Generating IOS project from UNITY project ...') unity_success = system("#{unity_path} -projectPath '#{root_working_dir}' -executeMethod NIXBuilder.MakeiOSBuild -projectPath #{root_working_dir.shellescape} -batchmode -logFile -quit -customArgs:buildPath='#{ios_project_path}#{development_build_arg}'") abort('iOS build unity error!') unless unity_success p('IOS project was generated.') need_chdir_to_root_working_dir = true clean_working_copy(false) Dir.chdir(ios_project_path) $project_to_build = 'Unity-iPhone.xcodeproj' $project_target_to_build = 'Unity-iPhone' when 'android' unity_build_android = File.join(BUILD_SCRIPTS_PATH, 'UnityBuildAndroid.py') 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} --configuration #{configuration} --unity-path #{unity_path} #{development_build_arg} #{keystore_path_arg} #{keystore_password_arg} #{key_alias_name_arg} #{key_alias_password_arg}") abort('Android build unity error!') unless build_success clean_working_copy(false) else abort("Error: Unknown unity target platform '#{unity_platform}'!") end return need_to_build_ios, root_working_dir, need_chdir_to_root_working_dir end def tag_icon(short_version) tagger_directory = File.join(BUILD_SCRIPTS_PATH, 'XcodeIconTagger') utility = File.join(tagger_directory, 'IconTagger') plist_path = @unescaped_product_settings_path version = %x[ /usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' '#{plist_path}' ].strip! monotonic_revision = %x[ source _last_revision.sh && echo ${MONOTONIC_REVISION} ].strip! style = short_version ? 'OneLine' : 'TwoLine' mask_path = File.join(tagger_directory, "masks/#{style}Mask.png") icons_dir = File.join(Dir.pwd, @config_settings['ICONS_PATH']) app_product = File.join(@config_settings['BUILT_PRODUCTS_DIR'], @config_settings['EXECUTABLE_NAME']) + '.app' system("#{utility} --shortVersion=#{version}\ --buildNumber=#{monotonic_revision}\ --style=#{style}\ --maskPath=\"#{mask_path}\"\ --plist=\"#{plist_path}\"\ --sourceIconsPath=\"#{icons_dir}\"\ --destinationIconsPath=#{app_product}") end def backup_info_plist p('Backuping Info.plist ...') @info_plist_backup_name = @unescaped_product_settings_path + '.backup' FileUtils.cp(@unescaped_product_settings_path, @info_plist_backup_name) p('Info.plist was backuped.') unless @unescaped_watchkit_app_product_settings_path.nil? p('Backuping WatchKit App Info.plist ...') @watchkit_app_info_plist_backup_name = @unescaped_watchkit_app_product_settings_path + '.backup' FileUtils.cp(@unescaped_watchkit_app_product_settings_path, @watchkit_app_info_plist_backup_name) p('WatchKit App Info.plist was backuped.') end unless @unescaped_watchkit_extension_product_settings_path.nil? p('Backuping WatchKit Extension Info.plist ...') @watchkit_extension_info_plist_backup_name = @unescaped_watchkit_extension_product_settings_path + '.backup' FileUtils.cp(@unescaped_watchkit_extension_product_settings_path, @watchkit_extension_info_plist_backup_name) p('WatchKit Extension Info.plist was backuped.') end end def update_info_plist(config) p('Updating Info.plist ...') 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'] update_success = system("/usr/libexec/PlistBuddy -c 'Set :CFBundleVersion \"#{monotonic_revision}\"' \"#{@unescaped_product_settings_path}\"") if bundle_id update_success &&= system("/usr/libexec/PlistBuddy -c 'Set :CFBundleIdentifier \"#{bundle_id}\"' \"#{@unescaped_product_settings_path}\"") end update_success &&= system("/usr/libexec/PlistBuddy -c 'Add :Configuration string \"#{config}\"' \"#{@unescaped_product_settings_path}\"") update_success &&= system("/usr/libexec/PlistBuddy -c 'Add :RevisionNumber string \"#{revision}\"' \"#{@unescaped_product_settings_path}\"") unless update_success restore_info_plist abort('Update Info.plist error!') end p('Info.plist was updated.') unless @unescaped_watchkit_app_product_settings_path.nil? p('Updating WatchKit App Info.plist ...') update_success = system("/usr/libexec/PlistBuddy -c 'Set :CFBundleVersion \"#{monotonic_revision}\"' \"#{@unescaped_watchkit_app_product_settings_path}\"") unless update_success restore_info_plist abort('Update WatchKit App Info.plist error!') end p('WatchKit App Info.plist was updated.') end unless @unescaped_watchkit_extension_product_settings_path.nil? p('Updating Extension App Info.plist ...') update_success = system("/usr/libexec/PlistBuddy -c 'Set :CFBundleVersion \"#{monotonic_revision}\"' \"#{@unescaped_watchkit_extension_product_settings_path}\"") unless update_success restore_info_plist abort('Update WatchKit Extension Info.plist error!') end p('WatchKit Extension Info.plist was updated.') end end def restore_info_plist p('Restoring Info.plist ...') File.delete(@unescaped_product_settings_path) File.rename(@info_plist_backup_name, @unescaped_product_settings_path) p('Info.plist was restored.') unless @unescaped_watchkit_app_product_settings_path.nil? p('Restoring WatchKit App Info.plist ...') File.delete(@unescaped_watchkit_app_product_settings_path) File.rename(@watchkit_app_info_plist_backup_name, @unescaped_watchkit_app_product_settings_path) p('WatchKit App Info.plist was restored.') end unless @unescaped_watchkit_extension_product_settings_path.nil? p('Restoring WatchKit Extension Info.plist ...') File.delete(@unescaped_watchkit_extension_product_settings_path) File.rename(@watchkit_extension_info_plist_backup_name, @unescaped_watchkit_extension_product_settings_path) p('WatchKit Extension Info.plist was restored.') end end def deploy(deliver_deploy) deploy = File.join(BUILD_SCRIPTS_PATH, 'DeployIPA.sh') deploy_host = @config_settings['DEPLOY_HOST'].nil? || @config_settings['DEPLOY_HOST'].empty? ? ENV['DEPLOY_HOST'] : @config_settings['DEPLOY_HOST'] deploy_path = @config_settings['DEPLOY_PATH'].nil? || @config_settings['DEPLOY_PATH'].empty? ? ENV['DEPLOY_PATH'] : @config_settings['DEPLOY_PATH'] deploy_username = @config_settings['DEPLOY_USERNAME'].nil? || @config_settings['DEPLOY_USERNAME'].empty? ? ENV['DEPLOY_USERNAME'] : @config_settings['DEPLOY_USERNAME'] deploy_password = @config_settings['DEPLOY_PASSWORD'].nil? || @config_settings['DEPLOY_PASSWORD'].empty? ? ENV['DEPLOY_PASSWORD'] : @config_settings['DEPLOY_PASSWORD'] deploy_itunesconnect_username = @config_settings['DEPLOY_ITUNESCONNECT_USERNAME'].nil? || @config_settings['DEPLOY_ITUNESCONNECT_USERNAME'].empty? ? ENV['DEPLOY_ITUNESCONNECT_USERNAME'] : @config_settings['DEPLOY_ITUNESCONNECT_USERNAME'] deploy_itunesconnect_username ||= 'unknown' deliver_deploy = deliver_deploy ? 1 : 0 deploy_success = system("#{deploy} #{deploy_host} #{deploy_path} #{deploy_username} #{deploy_password} #{deploy_itunesconnect_username} #{deliver_deploy}") abort('Deploy error!') unless deploy_success end def unity_deploy(unity_platform) root_working_dir = nil need_chdir_to_root_working_dir = false need_to_deploy_ios = false case unity_platform when 'ios' root_working_dir = Dir.pwd need_chdir_to_root_working_dir = true need_to_deploy_ios = true ios_project_path = File.join(root_working_dir, UNITY_BUILDS_IOS_PATH) Dir.chdir(ios_project_path) when 'android' deploy_success = system(File.join(BUILD_SCRIPTS_PATH, 'DeployAPK.py')) abort('Android deploy unity error!') unless deploy_success else abort("Error: Unknown unity target platform '#{unity_platform}'!") end return need_to_deploy_ios, root_working_dir, need_chdir_to_root_working_dir end def clean remove_temporary_files = File.join(BUILD_SCRIPTS_PATH, 'RemoveTemporaryFiles.sh') system("#{remove_temporary_files}") system('rm -rf test-results/') system("find . -name \"*.pyc\" -exec rm -rf {} \;") system('xcodebuild -alltargets clean') end def test code_coverage_configuration = File.join(@config_settings['CONFIGURATION_FILES_PATH'], @config_settings['CODE_COVERAGE_CONFIGURATION']) timeout = 10 ocunit2junit = File.join(BUILD_SCRIPTS_PATH, 'Utils/ocunit2junit') run_tests_success = system("xcodebuild -workspace \"#{@config_settings['WORKSPACE_TO_BUILD']}\"\ -scheme \"#{@config_settings['WORKSPACE_SCHEME_TO_TEST']}\"\ -configuration \"Debug\"\ -sdk \"#{@config_settings['SDK_FOR_TESTS']}\"\ -xcconfig \"#{code_coverage_configuration}\"\ -destination-timeout \"#{timeout}\"\ -destination \"#{@config_settings['XCTEST_DESTINATION_DEVICE']}\"\ test 2>&1 | \"#{ocunit2junit}\"") abort('Run test error!') unless run_tests_success end def code_coverage generate_code_coverage_report = File.join(BUILD_SCRIPTS_PATH, 'GenerateCodeCoverageForXCTests.sh') code_coverage_configuration = File.join(@config_settings['CONFIGURATION_FILES_PATH'], @config_settings['CODE_COVERAGE_CONFIGURATION']) timeout = 10 report_success = system("#{generate_code_coverage_report} -workspace \"#{@config_settings['WORKSPACE_TO_BUILD']}\"\ -scheme \"#{@config_settings['WORKSPACE_SCHEME_TO_TEST']}\"\ -configuration \"Debug\"\ -sdk \"#{@config_settings['SDK_FOR_TESTS']}\"\ -xcconfig \"#{code_coverage_configuration}\"\ -exclude \"#{@config_settings['EXCLUDE_PATTERN_FOR_CODE_COVERAGE']}\"\ -output \"#{@config_settings['CODE_COVERAGE_OUTPUT_DIRECTORY']}\"\ -destination-timeout \"#{timeout}\"\ -destination \"#{@config_settings['XCTEST_DESTINATION_DEVICE']}\"") abort('Code coverage error!') unless report_success end def code_duplication_report generate_code_duplication_report = File.join(BUILD_SCRIPTS_PATH, 'GenerateCodeDuplicationReport.sh') duplication_success = system("#{generate_code_duplication_report} \"#{@config_settings['EXCLUDE_PATTERN_FOR_CODE_DUPLICATION']}\" duplication.xml") abort('Generate code duplication error!') unless duplication_success end def tag make_tag = File.join(BUILD_SCRIPTS_PATH, 'MakeTag.sh') tag_success = system("#{make_tag} \"#{ENV['SCM_USERNAME']}\" \"#{ENV['SCM_PASSWORD']}\"") abort('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']=%x[ $(shell xpath ../subversion.credentials \(//userName\)[1]/text\(\)) ] ENV['SCM_PASSWORD']=%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_working_copy = File.join(BUILD_SCRIPTS_PATH, 'CleanWorkingCopy.sh') clean_success = system("#{clean_working_copy} #{clean_all}") abort('Clean working copy error!') unless clean_success end