├── .gitignore ├── README.md └── xcodearchive.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xcodearchive is a command line tool to build and archive your Xcode projects. 2 | 3 | 4 | xcodearchive will generate an IPA of your project. 5 | 6 | 7 | By default, it saves the dSYM symbols of your project in a ZIP archive (useful for later, when you will want to symbolicate your crash report). 8 | It automatically reads the settings from your Xcode project. You can override some of them, if you need to. 9 | 10 | 11 | I have only tested it on iPhone projects. If you want to add support for Mac projects, I would be happy to receive a pull request. -------------------------------------------------------------------------------- /xcodearchive.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # xcodearchive.rb 3 | # 4 | # Created by Guillaume Cerquant on 2011-11-16. 5 | # Copyright 2011 MacMation. All rights reserved. 6 | # 7 | 8 | # What is this? 9 | # xcodebuild builds an Xcode project 10 | # xcodearchive archive an Xcode project... wait! Apple did not ship an xcodearchive command 11 | # This script intends to substitute to it. 12 | # It allows you to generate an ipa via the command line 13 | # 14 | 15 | # CHANGELOG 16 | # 0.2 - Now reads the iPhone developper identity from the Xcode project 17 | # 0.3 - Option to set the developper identity - Read the application version number and use it in the filename of zip dSYM symbols 18 | # 0.4 - Build the project in a temporary directory 19 | # 1.0 - When in verbose mode, displays the logs output by Xcode 20 | # 1.0.1 - Can now use the --project option using a relative or absolute path 21 | # 1.0.2 - Status code return real errors 22 | 23 | # CREDITS 24 | # Thank you to Vincent Daubry for his discovery of the xcrun command, which greatly simplified this script 25 | # http://blog.octo.com/automatiser-le-deploiement-over-the-air/ 26 | # 27 | # Thank you to Yannick Cadin. Some of his code to detect the SDK version of an Xcode project has been used and adapted to 28 | # detect the iPhone developper identity 29 | # http://diablotin.info/ 30 | 31 | # TODO 32 | # Know bugs 33 | # - Running the shell commands with the backticks, we loose the stderr output 34 | # - handle the case where the product name is different from target name 35 | 36 | # 37 | # New Features 38 | # - generate a manifest plist file (equivalent of the checkbox "Save for Enterprise" in Xcode) 39 | # - be able to force a sdk version 40 | # - print the information about the project (base sdk, deployement target, size of ipa) 41 | 42 | 43 | require 'optparse' 44 | require 'open3' 45 | require 'tmpdir' 46 | require 'pathname' 47 | 48 | @version_number="1.0.2" 49 | 50 | # Use xcode-select -switch to set your Xcode folder path 51 | XCODEBUILD="/usr/bin/xcodebuild" 52 | BZR="/usr/local/bin/bzr" 53 | SVN="/usr/bin/svn" 54 | PLISTBUDDY = "/usr/libexec/PlistBuddy" 55 | 56 | ERROR_NO_XCODE_PROJECT_FOUND=2 57 | ERROR_MULTIPLE_XCODE_PROJECTS_FOUND=3 58 | ERROR_DID_NOT_FOUND_RELEASE_CONFIGURATION=4 59 | ERROR_CLEAN=5 60 | ERROR_BUILD=6 61 | ERROR_CODESIGN=7 62 | 63 | 64 | def parse_options 65 | @options = {} 66 | 67 | optparse = OptionParser.new do |opts| 68 | opts.banner = "Usage: xcodearchive [OPTIONS]" 69 | 70 | opts.on( '', '--version', 'Show version number' ) do 71 | puts "Version #{@version_number}" 72 | exit 73 | end 74 | 75 | 76 | @options[:verbose] = false 77 | opts.on( '-v', '--verbose', 'Output more information' ) do 78 | @options[:verbose] = true 79 | end 80 | 81 | @options[:growl] = false 82 | opts.on( '-g', '--growl', 'Show growl alerts to inform about progress of the build' ) do 83 | @options[:growl] = true 84 | end 85 | 86 | @options[:no_symbol] = false 87 | opts.on( '-n', '--do_not_keep_dsym_symbols', 'Do not keep the dSYM symbols' ) do 88 | @options[:no_symbol] = true 89 | end 90 | 91 | @options[:show] = false 92 | opts.on( '-s', '--show', 'Show archive in Finder once created' ) do 93 | @options[:show] = true 94 | end 95 | 96 | @options[:clean_before_building] = false 97 | opts.on( '-c', '--clean', 'Do a clean before building the Xcode project' ) do 98 | @options[:clean_before_building] = true 99 | end 100 | 101 | 102 | @options[:ipa_export_path] = nil 103 | opts.on( '-o', '--ipa_export_path FOLDER', 'Set the path of the folder where the ipa will be saved. Default is \'~/Desktop\'' ) do |ipa_export_folder_path| 104 | @options[:ipa_export_path] = ipa_export_folder_path 105 | end 106 | 107 | @options[:developper_identity] = nil 108 | opts.on( '-i', '--developper_identity DEVELOPPER_IDENTITY', 'Force the developper identity value' ) do |developper_identity| 109 | @options[:developper_identity] = developper_identity 110 | end 111 | 112 | @options[:mobile_provision] = nil 113 | opts.on( '-m', '--mobile_provision MOBILE_PROVISION_NAME', 'Force the mobile provision file to use' ) do |mobile_provision| 114 | @options[:mobile_provision] = mobile_provision 115 | end 116 | 117 | 118 | @options[:project] = nil 119 | opts.on( '-p', '--project PROJECT', 'Specifiy xcode project') do |xcodeproject_file| 120 | @options[:project] = xcodeproject_file 121 | #todo : WILL not work with a full file path 122 | end 123 | 124 | # todo : generate a manifest plist file (will be useful when we will be parsing the version number) 125 | 126 | opts.on( '-h', '--help', 'Display this screen' ) do 127 | puts opts 128 | 129 | puts "\n\n\nExamples:\n 130 | xcodearchive => Build the Xcode project of the current folder, generate an archive (ipa), and create a zip with the dSYM symbols 131 | xcodearchive -n => Same as above, but do not keep the symbols 132 | xcodearchive -o ~/Documents/my_archives -s => Save the ipa in the given folder, and reveal it in the Finder" 133 | 134 | exit 135 | end 136 | end 137 | 138 | optparse.parse! 139 | 140 | end 141 | 142 | 143 | def xcode_project_file_path 144 | return Pathname.new(@options[:project]).realpath if (@options[:project]) 145 | # TODO: Does not work with spaces in the path 146 | 147 | all_xcode_projs = Dir.glob("*.xcodeproj") 148 | if (all_xcode_projs.count == 0) 149 | puts "Error: 0 xcodeprojects found" 150 | exit ERROR_NO_XCODE_PROJECT_FOUND 151 | end 152 | 153 | if (all_xcode_projs.count != 1) 154 | puts "Error: The directory #{Dir.pwd} contains #{all_xcode_projs.count} projects (file with the extension .xcodeproj). Specify the project to use with the --project option." 155 | exit ERROR_MULTIPLE_XCODE_PROJECTS_FOUND 156 | end 157 | 158 | Dir.pwd + "/" + all_xcode_projs[0] 159 | end 160 | 161 | # def sdk_version 162 | # "iphoneos5.0" #TODO - Be able to force a sdk version 163 | # Will be useful to compile with an older sdk, to make sure no api is used in a version where they do not exists 164 | # end 165 | 166 | 167 | def project_name 168 | File.basename( xcode_project_file_path(), ".xcodeproj") 169 | 170 | end 171 | 172 | def target_name 173 | 174 | end 175 | 176 | def archive_name 177 | 178 | end 179 | 180 | @temp_build_directory = nil 181 | def path_of_temp_directory_where_to_build 182 | return @temp_build_directory if @temp_build_directory 183 | @temp_build_directory = Dir.mktmpdir 184 | return @temp_build_directory 185 | end 186 | 187 | 188 | def path_of_directory_where_to_export 189 | if @options[:ipa_export_path] 190 | return @options[:ipa_export_path] 191 | else 192 | return "#{ENV['HOME']}/Desktop/" 193 | end 194 | end 195 | 196 | def path_of_created_ipa 197 | "#{path_of_directory_where_to_export}/#{project_name}.ipa" 198 | end 199 | 200 | def developper_identity 201 | if @options[:developper_identity] 202 | return @options[:developper_identity] 203 | end 204 | 205 | root_id = `#{PLISTBUDDY} -c Print\\ :rootObject #{xcode_project_file_path}/project.pbxproj`.chop 206 | build_configurations_ID = `#{PLISTBUDDY} -c Print\\ :objects:#{root_id}:buildConfigurationList #{xcode_project_file_path}/project.pbxproj`.chop 207 | 208 | # TODO: Here we are using an hard coded index 209 | release_id = `#{PLISTBUDDY} -c Print\\ :objects:#{build_configurations_ID}:buildConfigurations:1 #{xcode_project_file_path}/project.pbxproj`.chop 210 | 211 | name_of_configuration = `#{PLISTBUDDY} -c Print\\ :objects:#{release_id}:name #{xcode_project_file_path}/project.pbxproj`.chop 212 | if (name_of_configuration != "Release") 213 | puts "Did not found expected configuration - got '#{name_of_configuration}' ; expected 'Release'" 214 | exit ERROR_DID_NOT_FOUND_RELEASE_CONFIGURATION 215 | end 216 | 217 | # all = `#{PLISTBUDDY} -c Print\\ :objects:#{release_id}:buildSettings #{xcode_project_file_path}/project.pbxproj` 218 | # puts "all #{all}" 219 | 220 | identity = `#{PLISTBUDDY} -c Print\\ :objects:#{release_id}:buildSettings:CODE_SIGN_IDENTITY[sdk=iphoneos*] #{xcode_project_file_path}/project.pbxproj`.chop 221 | 222 | identity 223 | end 224 | 225 | def mobile_provisionning_profile_path 226 | "#{path_of_builded_application}/embedded.mobileprovision" 227 | end 228 | 229 | 230 | def show_all_parameters 231 | puts "Working with #{xcode_project_file_path}" 232 | # puts "SDK Version: #{sdk_version()}" 233 | # TODO print everything useful here 234 | end 235 | 236 | def verbose 237 | @options[:verbose] 238 | end 239 | 240 | 241 | def mobileprovision_command_installed 242 | return system("mobileprovision --version") 243 | end 244 | 245 | 246 | def path_of_builded_application 247 | 248 | "#{path_of_temp_directory_where_to_build}/Release-iphoneos/#{project_name}.app" 249 | end 250 | 251 | 252 | def archive_xcode_project 253 | 254 | puts "Using temporary path for build: #{path_of_temp_directory_where_to_build}" if verbose 255 | 256 | build_command="#{XCODEBUILD} -project #{xcode_project_file_path()} SYMROOT=\"#{path_of_temp_directory_where_to_build}\"" 257 | build_command += " PROVISIONING_PROFILE=#{@options[:mobile_provision]}" if @options[:mobile_provision] 258 | puts "Building:\n#{build_command}" if verbose 259 | growl_alert("Building", "Building xCode project #{xcode_project_file_path}") 260 | 261 | if @options[:clean_before_building] 262 | puts "Cleaning Xcode project" if verbose 263 | `#{XCODEBUILD} -project #{xcode_project_file_path()} clean` 264 | if (0 != $?.to_i) 265 | puts "Error in xcodebuild (clean): #{$?.to_s}" 266 | exit ERROR_CLEAN 267 | end 268 | end 269 | 270 | output = `#{build_command}` 271 | 272 | if (0 != $?.to_i) 273 | puts "Error in xcodebuild: #{$?.to_s}" 274 | puts "#{output}" 275 | exit ERROR_BUILD 276 | end 277 | 278 | 279 | if (verbose) 280 | if (mobileprovision_command_installed) 281 | puts "\nmobileprovision file info:" 282 | puts `mobileprovision #{mobile_provisionning_profile_path}` 283 | puts "\n\n" 284 | else 285 | puts "mobileprovision command not found. Unable to give details about the provisionningprofile." 286 | end 287 | 288 | puts "Developper identity: #{developper_identity}" 289 | puts "\nApplication version number: #{application_version_number(path_of_builded_application)}" 290 | end 291 | 292 | growl_alert("Archiving", "Identity: #{developper_identity}\nmobileprovision: `mobileprovision #{mobile_provisionning_profile_path}`") 293 | 294 | xcrun_command = "/usr/bin/xcrun -sdk iphoneos PackageApplication -v \"#{path_of_builded_application}\" -o \"#{path_of_created_ipa}\" --sign \"#{developper_identity}\" --embed \"#{mobile_provisionning_profile_path}\"" 295 | puts "Archiving:\n #{xcrun_command}\n\n\n" if verbose 296 | output = `#{xcrun_command}` 297 | 298 | if (0 != $?.to_i) 299 | puts "Error in xcrun: #{$?.to_s}" 300 | puts "#{output}" 301 | exit ERROR_CODESIGN 302 | end 303 | 304 | puts "Archiving succeedeed: IPA created" 305 | puts "IPA file saved to: '#{path_of_created_ipa}'" if verbose 306 | 307 | reveal_file_in_finder(path_of_created_ipa) if @options[:show] 308 | 309 | end 310 | 311 | 312 | def application_version_number(application_path) 313 | # product_version_number=`#{PLISTBUDDY} "#{path_of_builded_application}/Regions-Info.plist" -c Print\\ :CFBundleVersion`.chop 314 | product_version_number=`#{PLISTBUDDY} "#{path_of_builded_application}/Info.plist" -c Print\\ :CFBundleVersion`.chop 315 | product_version_number=`#{PLISTBUDDY} "#{path_of_builded_application}/#{project_name}-Info.plist" -c Print\\ :CFBundleVersion`.chop if (nil == product_version_number) 316 | 317 | product_version_number 318 | end 319 | 320 | 321 | def create_zip_archive_of_the_symbols 322 | return if (@options[:no_symbol]) 323 | 324 | puts "Archiving the dSYM symbols" 325 | 326 | date=`date '+%Y%m%d_%H'h'%M'`.chop 327 | filename_for_dsym_symbols_archive="#{project_name}_version_#{application_version_number(path_of_builded_application)}_#{date}_dSYM_symbols.zip" 328 | filepath_for_dsym_symbols_archive="#{path_of_directory_where_to_export}/#{filename_for_dsym_symbols_archive}" 329 | 330 | growl_alert("dSYM symbols", "Archiving the dSYM symbols into #{filepath_for_dsym_symbols_archive}") 331 | 332 | 333 | filename_of_generated_symbols="#{project_name}.app.dSYM" 334 | 335 | # If we don't want to have the archive contain hierarchy, we need to cd first 336 | Dir.chdir "#{path_of_temp_directory_where_to_build}/Release-iphoneos" do 337 | `zip -r -T -y "#{filepath_for_dsym_symbols_archive}" "#{filename_of_generated_symbols}"` 338 | 339 | # TODO: Check for error here when zipping 340 | end 341 | 342 | puts "dSYM symbols archived into #{filepath_for_dsym_symbols_archive}" 343 | 344 | end 345 | 346 | def growl_alert(title, message) 347 | if (@options[:growl]) 348 | growlnotify="/usr/local/bin/growlnotify" # Edit this if you installed growlnotify in a different place 349 | 350 | if (File.executable?(growlnotify)) 351 | `#{growlnotify} "#{title}" -m "#{message}" -d archivingBubble` 352 | else 353 | puts "Did not found growlnotify command" 354 | end 355 | end 356 | end 357 | 358 | 359 | def reveal_file_in_finder(file_path) 360 | applescript_command = "tell application \"Finder\"\nreveal POSIX file \"#{file_path}\"\n activate\nend tell" 361 | `osascript -e '#{applescript_command}'` 362 | end 363 | 364 | 365 | parse_options 366 | 367 | show_all_parameters if verbose 368 | 369 | archive_xcode_project() 370 | create_zip_archive_of_the_symbols() 371 | 372 | --------------------------------------------------------------------------------