#!/usr/bin/env ruby require 'rubygems' require 'nixenvironment' require 'commander/import' require 'yaml' require 'fileutils' 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 ('--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 ('--infoplist_path VALUE') { |value| $infoplist_path = 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 TYPE', String, 'Select sign (ipa, resigned_ipa_for_device, resigned_ipa_for_adhoc_distribution or resigned_ipa_for_appstore)' c.option '--ci_build VALUE', String, 'Define NIXENV_CI_BUILD environment variable (yes, true, 1 or on to enable)' c.action do |args, options| options.default :config => 'Debug', :ipa => 'ipa', :ci_build => 'yes' read_config(options) enable_ci_build(options.ci_build) build_settings = setup(options.config) prebuild(build_settings, options.config) build(build_settings, options.config, options.ipa) restore_info_plist end end command :deploy do |c| c.syntax = 'nixenvironment deploy' c.description = 'Deploy built artifacts to given server' c.action do |args, options| read_config(options) deploy 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(options) 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(options) code_coverage end end command :code_duplication_report do |c| c.syntax = 'nixenvironment code_duplication_report' c.description = 'Generate code duplication report for xctest' c.action do |args, options| read_config(options) 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.action do |args, options| clean_working_copy 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 = tag.strip } 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(options) begin @config = YAML.load(File.read(File.join(File.dirname(__FILE__), 'Config'))) rescue abort('Config file processing error!') end update_config('PROJECT_TO_BUILD', $project_to_build) update_config('PROJECT_TARGET_TO_BUILD', $project_target_to_build) update_config('PROJECT_TARGET_TO_TEST', $project_target_to_test) update_config('WORKSPACE_TO_BUILD', $workspace_to_build) update_config('WORKSPACE_SCHEME_TO_BUILD', $workspace_scheme_to_build) update_config('WORKSPACE_SCHEME_TO_TEST', $workspace_scheme_to_test) update_config('SDK', $sdk) update_config('SDK_FOR_TESTS', $sdk_for_tests) update_config('EXCLUDE_PATTERN_FOR_CODE_COVERAGE', $exclude_pattern_for_code_coverage) update_config('EXCLUDE_PATTERN_FOR_CODE_DUPLICATION', $exclude_pattern_for_code_duplication) update_config('DEPLOY_HOST', $deploy_host) update_config('DEPLOY_PATH', $deploy_path) update_config('DEPLOY_USERNAME', $deploy_username) update_config('DEPLOY_PASSWORD', $deploy_password) update_config('ICONS_PATH', $icons_path) update_config('XCTEST_DESTINATION_DEVICE', $xctest_destination_device) update_config('CONFIGURATION_FILES_PATH', $configuration_files_path) update_config('CODE_COVERAGE_CONFIGURATION', $code_coverage_configuration) update_config('CODE_COVERAGE_OUTPUT_DIRECTORY', $code_coverage_output_directory) update_config('ENV_VAR_PREFIX', $env_var_prefix) update_config('INFOPLIST_PATH', $infoplist_path) update_config('BUNDLE_ID', $bundle_id) update_config('RESIGNED_BUNDLE_ID', $resigned_bundle_id) update_config('RESIGNED_BUNDLE_NAME', $resigned_bundle_name) update_config('RESIGNED_ENTITLEMENTS_PATH', $resigned_entitlements_path) end def update_config(key, value) if value @config[key] = value p("#{key} |SPECIFIED| directly: #{value}") else p("#{key} |NOT specified| directly. Used from Config: #{@config[key]}") end end def enable_ci_build(ci_build) if ci_build == 'yes' or ci_build == 'true' or ci_build == '1' or ci_build == 'on' 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? 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") return is_clean end def setup(config) # Parse information about project cmd_output = %x[ xcodebuild -list ] cmd_output = cmd_output.lines.to_a[1..-1].join 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 abort("Build error! Configuration #{config} doesn't exist") unless info['Build Configurations'].include?(config) if @config['PROJECT_TO_BUILD'] and @config['PROJECT_TO_BUILD'].length > 0 if @config['PROJECT_TARGET_TO_BUILD'] and @config['PROJECT_TARGET_TO_BUILD'].length > 0 cmd_output = %x[ xcodebuild -project #{@config['PROJECT_TO_BUILD']}\ -target #{@config['PROJECT_TARGET_TO_BUILD']}\ -configuration #{config}\ -sdk #{@config['SDK']}\ -showBuildSettings ] else cmd_output = %x[ xcodebuild -project #{@config['PROJECT_TO_BUILD']}\ -scheme #{@config['WORKSPACE_SCHEME_TO_BUILD']}\ -configuration #{config}\ -sdk #{@config['SDK']}\ -showBuildSettings ] end elsif @config['WORKSPACE_TO_BUILD'] and @config['WORKSPACE_TO_BUILD'].length > 0 cmd_output = %x[ xcodebuild -workspace #{@config['WORKSPACE_TO_BUILD']}\ -scheme #{@config['WORKSPACE_SCHEME_TO_BUILD']}\ -configuration #{config}\ -sdk #{@config['SDK']}\ -showBuildSettings ] else abort('Build error! Either PROJECT_TO_BUILD or WORKSPACE_TO_BUILD must be specified!') end env_vars_list = cmd_output.split(/\n/).reject(&:empty?) build_settings = Hash.new 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 build_settings[stripped_key] = stripped_value end end end return build_settings end def save_build_env_vars(build_settings) app_product = "#{build_settings['BUILT_PRODUCTS_DIR']}/#{build_settings['EXECUTABLE_NAME']}.app" system(" echo \"#!/bin/sh\ ### AUTOGENERATED BY Nixenvironment; DO NOT EDIT ### PROJECT=\"#{build_settings['PROJECT']}\" BUILT_PRODUCTS_DIR=\"#{build_settings['BUILT_PRODUCTS_DIR']}\" OBJECTS_NORMAL_DIR=\"#{build_settings['OBJECT_FILE_DIR_normal']}\" EXECUTABLE_NAME=\"#{build_settings['EXECUTABLE_NAME']}\" APP_PRODUCT=\"#{app_product}\" APP_DSYM=\"#{app_product}.dSYM\" APP_INFOPLIST_FILE=\"#{@config['INFOPLIST_PATH']}\" EMBEDDED_PROFILE=\"##{app_product}/#{build_settings['EMBEDDED_PROFILE_NAME']}\" TARGET_NAME=\"#{build_settings['TARGET_NAME']}\" CONFIGURATION=\"#{build_settings['CONFIGURATION']}\" SDK_NAME=\"#{build_settings['SDK_NAME']}\" RESIGNED_BUNDLE_ID=\"#{build_settings['RESIGNED_BUNDLE_ID']}\" RESIGNED_BUNDLE_NAME=\"#{build_settings['RESIGNED_BUNDLE_NAME']}\" RESIGNED_ENTITLEMENTS_PATH=\"#{build_settings['RESIGNED_ENTITLEMENTS_PATH']}\"\" > _last_build_vars.sh ") end def prebuild(build_settings, config) save_revision = File.join(BUILD_SCRIPTS_PATH, 'SaveRevision.sh') tag_icons = File.join(BUILD_SCRIPTS_PATH, 'XcodeIconTagger/tagIcons.sh') system("#{save_revision}") abort unless working_copy_is_clean? backup_info_plist save_build_env_vars(build_settings) update_info_plist(build_settings, config) # TODO: rewrite tagIcons.sh # system("#{tag_icons} @config['ICONS_PATH']") end def build(build_settings, config, ipa) build = File.join(BUILD_SCRIPTS_PATH, 'Build.py') build_success = nil if @config['PROJECT_TO_BUILD'] and @config['PROJECT_TO_BUILD'].length > 0 if @config['PROJECT_TARGET_TO_BUILD'] and @config['PROJECT_TARGET_TO_BUILD'].length > 0 build_success = system("#{build} --project \"#{@config['PROJECT_TO_BUILD']}\"\ --target \"#{@config['PROJECT_TARGET_TO_BUILD']}\"\ --configuration \"#{config}\"\ --sdk \"#{@config['SDK']}\"\ --env-var-prefix \"#{@config['ENV_VAR_PREFIX']}\"\ DEBUG_INFORMATION_FORMAT=\"dwarf-with-dsym\"\ DWARF_DSYM_FOLDER_PATH=\"#{build_settings['CONFIGURATION_BUILD_DIR']}\"") else build_success = system("#{build} --project \"#{@config['PROJECT_TO_BUILD']}\"\ --scheme \"#{@config['WORKSPACE_SCHEME_TO_BUILD']}\"\ --configuration \"#{config}\"\ --sdk \"#{@config['SDK']}\"\ --env-var-prefix \"#{@config['ENV_VAR_PREFIX']}\"\ DEBUG_INFORMATION_FORMAT=\"dwarf-with-dsym\"\ DWARF_DSYM_FOLDER_PATH=\"#{build_settings['CONFIGURATION_BUILD_DIR']}\"") end elsif @config['WORKSPACE_TO_BUILD'] and @config['WORKSPACE_TO_BUILD'].length > 0 build_success = system("#{build} --workspace \"#{@config['WORKSPACE_TO_BUILD']}\"\ --scheme \"#{@config['WORKSPACE_SCHEME_TO_BUILD']}\"\ --configuration \"#{config}\"\ --sdk \"#{@config['SDK']}\"\ --env-var-prefix \"#{@config['ENV_VAR_PREFIX']}\"\ DEBUG_INFORMATION_FORMAT=\"dwarf-with-dsym\"\ DWARF_DSYM_FOLDER_PATH=\"#{build_settings['CONFIGURATION_BUILD_DIR']}\"") end unless build_success restore_info_plist abort('Build error!') end case ipa # create .ipa file from last built app product when 'ipa' 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_ipa_for_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_ipa_for_adhoc_distribution' 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_ipa_for_appstore' make = File.join(BUILD_SCRIPTS_PATH, 'MakeResignedIPAForAppstore.sh') else abort("Error: Unknown ipa '#{ipa}'!") end make_success = system("#{make}") if defined? make unless make_success restore_info_plist abort("#{make} error!") end end def backup_info_plist p('Backuping Info.plist ...') @info_plist_backup_name = @config['INFOPLIST_PATH'] + '.backup' FileUtils.cp(@config['INFOPLIST_PATH'], @info_plist_backup_name) p('Info.plist was backuped.') end def update_info_plist(build_settings, config) p('Updating Info.plist ...') update_build_number = File.join(BUILD_SCRIPTS_PATH, 'UpdateBuildNumber.sh') update_revision_number = File.join(BUILD_SCRIPTS_PATH, 'UpdateRevisionNumber.sh') update_success = system("/usr/libexec/PlistBuddy -c 'Set :CFBundleIdentifier \"#{@config['BUNDLE_ID']}\"' \"#{@config['INFOPLIST_PATH']}\"") update_success &&= system("/usr/libexec/PlistBuddy -c 'Add :Configuration string \"#{config}\"' \"#{@config['INFOPLIST_PATH']}\"") update_success &&= system("/usr/libexec/PlistBuddy -c 'Add :RevisionNumber string \"undefined\"' \"#{@config['INFOPLIST_PATH']}\"") unless update_success restore_info_plist abort('Update Info.plist error!') end system("#{update_build_number}") system("#{update_revision_number}") p('Info.plist was updated.') end def restore_info_plist p('Restoring Info.plist ...') File.delete(@config['INFOPLIST_PATH']) File.rename(@info_plist_backup_name, @config['INFOPLIST_PATH']) p('Info.plist was restored.') end def deploy deploy = File.join(BUILD_SCRIPTS_PATH, 'Deploy.sh') deploy_host = @config['DEPLOY_HOST'].nil? || @config['DEPLOY_HOST'].empty? ? ENV['DEPLOY_HOST'] : @config['DEPLOY_HOST'] deploy_path = @config['DEPLOY_PATH'].nil? || @config['DEPLOY_PATH'].empty? ? ENV['DEPLOY_PATH'] : @config['DEPLOY_PATH'] deploy_username = @config['DEPLOY_USERNAME'].nil? || @config['DEPLOY_USERNAME'].empty? ? ENV['DEPLOY_USERNAME'] : @config['DEPLOY_USERNAME'] deploy_password = @config['DEPLOY_PASSWORD'].nil? || @config['DEPLOY_PASSWORD'].empty? ? ENV['DEPLOY_PASSWORD'] : @config['DEPLOY_PASSWORD'] deploy_success = system("#{deploy} #{deploy_host} #{deploy_path} #{deploy_username} #{deploy_password}") abort('Deploy error!') unless deploy_success 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['CONFIGURATION_FILES_PATH'], @config['CODE_COVERAGE_CONFIGURATION']) timeout = 10 ocunit2junit = File.join(BUILD_SCRIPTS_PATH, 'Utils/ocunit2junit') run_tests_success = system("xcodebuild -workspace \"#{@config['WORKSPACE_TO_BUILD']}\"\ -scheme \"#{@config['WORKSPACE_SCHEME_TO_TEST']}\"\ -configuration \"Debug\"\ -sdk \"#{@config['SDK_FOR_TESTS']}\"\ -xcconfig \"#{code_coverage_configuration}\"\ -destination-timeout \"#{timeout}\"\ -destination \"#{@config['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['CONFIGURATION_FILES_PATH'], @config['CODE_COVERAGE_CONFIGURATION']) timeout = 10 report_success = system("#{generate_code_coverage_report} -workspace \"#{@config['WORKSPACE_TO_BUILD']}\"\ -scheme \"#{@config['WORKSPACE_SCHEME_TO_TEST']}\"\ -configuration \"Debug\"\ -sdk \"#{@config['SDK_FOR_TESTS']}\"\ -xcconfig \"#{code_coverage_configuration}\"\ -exclude \"#{@config['EXCLUDE_PATTERN_FOR_CODE_COVERAGE']}\"\ -output \"#{@config['CODE_COVERAGE_OUTPUT_DIRECTORY']}\"\ -destination-timeout \"#{timeout}\"\ -destination \"#{@config['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['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 clean_working_copy = File.join(BUILD_SCRIPTS_PATH, 'CleanWorkingCopy.sh') clean_success = system("#{clean_working_copy}") abort('Clean working copy error!') unless clean_success end