Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Branch: master
executable file 372 lines (267 sloc) 12.125 kB
#!/usr/bin/env ruby
# xcodearchive.rb
#
# Created by Guillaume Cerquant on 2011-11-16.
# Copyright 2011 MacMation. All rights reserved.
#
# What is this?
# xcodebuild builds an Xcode project
# xcodearchive archive an Xcode project... wait! Apple did not ship an xcodearchive command
# This script intends to substitute to it.
# It allows you to generate an ipa via the command line
#
# CHANGELOG
# 0.2 - Now reads the iPhone developper identity from the Xcode project
# 0.3 - Option to set the developper identity - Read the application version number and use it in the filename of zip dSYM symbols
# 0.4 - Build the project in a temporary directory
# 1.0 - When in verbose mode, displays the logs output by Xcode
# 1.0.1 - Can now use the --project option using a relative or absolute path
# 1.0.2 - Status code return real errors
# CREDITS
# Thank you to Vincent Daubry for his discovery of the xcrun command, which greatly simplified this script
# http://blog.octo.com/automatiser-le-deploiement-over-the-air/
#
# Thank you to Yannick Cadin. Some of his code to detect the SDK version of an Xcode project has been used and adapted to
# detect the iPhone developper identity
# http://diablotin.info/
# TODO
# Know bugs
# - Running the shell commands with the backticks, we loose the stderr output
# - handle the case where the product name is different from target name
#
# New Features
# - generate a manifest plist file (equivalent of the checkbox "Save for Enterprise" in Xcode)
# - be able to force a sdk version
# - print the information about the project (base sdk, deployement target, size of ipa)
require 'optparse'
require 'open3'
require 'tmpdir'
require 'pathname'
@version_number="1.0.2"
# Use xcode-select -switch <xcode_folder_path> to set your Xcode folder path
XCODEBUILD="/usr/bin/xcodebuild"
BZR="/usr/local/bin/bzr"
SVN="/usr/bin/svn"
PLISTBUDDY = "/usr/libexec/PlistBuddy"
ERROR_NO_XCODE_PROJECT_FOUND=2
ERROR_MULTIPLE_XCODE_PROJECTS_FOUND=3
ERROR_DID_NOT_FOUND_RELEASE_CONFIGURATION=4
ERROR_CLEAN=5
ERROR_BUILD=6
ERROR_CODESIGN=7
def parse_options
@options = {}
optparse = OptionParser.new do |opts|
opts.banner = "Usage: xcodearchive [OPTIONS]"
opts.on( '', '--version', 'Show version number' ) do
puts "Version #{@version_number}"
exit
end
@options[:verbose] = false
opts.on( '-v', '--verbose', 'Output more information' ) do
@options[:verbose] = true
end
@options[:growl] = false
opts.on( '-g', '--growl', 'Show growl alerts to inform about progress of the build' ) do
@options[:growl] = true
end
@options[:no_symbol] = false
opts.on( '-n', '--do_not_keep_dsym_symbols', 'Do not keep the dSYM symbols' ) do
@options[:no_symbol] = true
end
@options[:show] = false
opts.on( '-s', '--show', 'Show archive in Finder once created' ) do
@options[:show] = true
end
@options[:clean_before_building] = false
opts.on( '-c', '--clean', 'Do a clean before building the Xcode project' ) do
@options[:clean_before_building] = true
end
@options[:ipa_export_path] = nil
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|
@options[:ipa_export_path] = ipa_export_folder_path
end
@options[:developper_identity] = nil
opts.on( '-i', '--developper_identity DEVELOPPER_IDENTITY', 'Force the developper identity value' ) do |developper_identity|
@options[:developper_identity] = developper_identity
end
@options[:mobile_provision] = nil
opts.on( '-m', '--mobile_provision MOBILE_PROVISION_NAME', 'Force the mobile provision file to use' ) do |mobile_provision|
@options[:mobile_provision] = mobile_provision
end
@options[:project] = nil
opts.on( '-p', '--project PROJECT', 'Specifiy xcode project') do |xcodeproject_file|
@options[:project] = xcodeproject_file
#todo : WILL not work with a full file path
end
# todo : generate a manifest plist file (will be useful when we will be parsing the version number)
opts.on( '-h', '--help', 'Display this screen' ) do
puts opts
puts "\n\n\nExamples:\n
xcodearchive => Build the Xcode project of the current folder, generate an archive (ipa), and create a zip with the dSYM symbols
xcodearchive -n => Same as above, but do not keep the symbols
xcodearchive -o ~/Documents/my_archives -s => Save the ipa in the given folder, and reveal it in the Finder"
exit
end
end
optparse.parse!
end
def xcode_project_file_path
return Pathname.new(@options[:project]).realpath if (@options[:project])
# TODO: Does not work with spaces in the path
all_xcode_projs = Dir.glob("*.xcodeproj")
if (all_xcode_projs.count == 0)
puts "Error: 0 xcodeprojects found"
exit ERROR_NO_XCODE_PROJECT_FOUND
end
if (all_xcode_projs.count != 1)
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."
exit ERROR_MULTIPLE_XCODE_PROJECTS_FOUND
end
Dir.pwd + "/" + all_xcode_projs[0]
end
# def sdk_version
# "iphoneos5.0" #TODO - Be able to force a sdk version
# Will be useful to compile with an older sdk, to make sure no api is used in a version where they do not exists
# end
def project_name
File.basename( xcode_project_file_path(), ".xcodeproj")
end
def target_name
end
def archive_name
end
@temp_build_directory = nil
def path_of_temp_directory_where_to_build
return @temp_build_directory if @temp_build_directory
@temp_build_directory = Dir.mktmpdir
return @temp_build_directory
end
def path_of_directory_where_to_export
if @options[:ipa_export_path]
return @options[:ipa_export_path]
else
return "#{ENV['HOME']}/Desktop/"
end
end
def path_of_created_ipa
"#{path_of_directory_where_to_export}/#{project_name}.ipa"
end
def developper_identity
if @options[:developper_identity]
return @options[:developper_identity]
end
root_id = `#{PLISTBUDDY} -c Print\\ :rootObject #{xcode_project_file_path}/project.pbxproj`.chop
build_configurations_ID = `#{PLISTBUDDY} -c Print\\ :objects:#{root_id}:buildConfigurationList #{xcode_project_file_path}/project.pbxproj`.chop
# TODO: Here we are using an hard coded index
release_id = `#{PLISTBUDDY} -c Print\\ :objects:#{build_configurations_ID}:buildConfigurations:1 #{xcode_project_file_path}/project.pbxproj`.chop
name_of_configuration = `#{PLISTBUDDY} -c Print\\ :objects:#{release_id}:name #{xcode_project_file_path}/project.pbxproj`.chop
if (name_of_configuration != "Release")
puts "Did not found expected configuration - got '#{name_of_configuration}' ; expected 'Release'"
exit ERROR_DID_NOT_FOUND_RELEASE_CONFIGURATION
end
# all = `#{PLISTBUDDY} -c Print\\ :objects:#{release_id}:buildSettings #{xcode_project_file_path}/project.pbxproj`
# puts "all #{all}"
identity = `#{PLISTBUDDY} -c Print\\ :objects:#{release_id}:buildSettings:CODE_SIGN_IDENTITY[sdk=iphoneos*] #{xcode_project_file_path}/project.pbxproj`.chop
identity
end
def mobile_provisionning_profile_path
"#{path_of_builded_application}/embedded.mobileprovision"
end
def show_all_parameters
puts "Working with #{xcode_project_file_path}"
# puts "SDK Version: #{sdk_version()}"
# TODO print everything useful here
end
def verbose
@options[:verbose]
end
def mobileprovision_command_installed
return system("mobileprovision --version")
end
def path_of_builded_application
"#{path_of_temp_directory_where_to_build}/Release-iphoneos/#{project_name}.app"
end
def archive_xcode_project
puts "Using temporary path for build: #{path_of_temp_directory_where_to_build}" if verbose
build_command="#{XCODEBUILD} -project #{xcode_project_file_path()} SYMROOT=\"#{path_of_temp_directory_where_to_build}\""
build_command += " PROVISIONING_PROFILE=#{@options[:mobile_provision]}" if @options[:mobile_provision]
puts "Building:\n#{build_command}" if verbose
growl_alert("Building", "Building xCode project #{xcode_project_file_path}")
if @options[:clean_before_building]
puts "Cleaning Xcode project" if verbose
`#{XCODEBUILD} -project #{xcode_project_file_path()} clean`
if (0 != $?.to_i)
puts "Error in xcodebuild (clean): #{$?.to_s}"
exit ERROR_CLEAN
end
end
output = `#{build_command}`
if (0 != $?.to_i)
puts "Error in xcodebuild: #{$?.to_s}"
puts "#{output}"
exit ERROR_BUILD
end
if (verbose)
if (mobileprovision_command_installed)
puts "\nmobileprovision file info:"
puts `mobileprovision #{mobile_provisionning_profile_path}`
puts "\n\n"
else
puts "mobileprovision command not found. Unable to give details about the provisionningprofile."
end
puts "Developper identity: #{developper_identity}"
puts "\nApplication version number: #{application_version_number(path_of_builded_application)}"
end
growl_alert("Archiving", "Identity: #{developper_identity}\nmobileprovision: `mobileprovision #{mobile_provisionning_profile_path}`")
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}\""
puts "Archiving:\n #{xcrun_command}\n\n\n" if verbose
output = `#{xcrun_command}`
if (0 != $?.to_i)
puts "Error in xcrun: #{$?.to_s}"
puts "#{output}"
exit ERROR_CODESIGN
end
puts "Archiving succeedeed: IPA created"
puts "IPA file saved to: '#{path_of_created_ipa}'" if verbose
reveal_file_in_finder(path_of_created_ipa) if @options[:show]
end
def application_version_number(application_path)
# product_version_number=`#{PLISTBUDDY} "#{path_of_builded_application}/Regions-Info.plist" -c Print\\ :CFBundleVersion`.chop
product_version_number=`#{PLISTBUDDY} "#{path_of_builded_application}/Info.plist" -c Print\\ :CFBundleVersion`.chop
product_version_number=`#{PLISTBUDDY} "#{path_of_builded_application}/#{project_name}-Info.plist" -c Print\\ :CFBundleVersion`.chop if (nil == product_version_number)
product_version_number
end
def create_zip_archive_of_the_symbols
return if (@options[:no_symbol])
puts "Archiving the dSYM symbols"
date=`date '+%Y%m%d_%H'h'%M'`.chop
filename_for_dsym_symbols_archive="#{project_name}_version_#{application_version_number(path_of_builded_application)}_#{date}_dSYM_symbols.zip"
filepath_for_dsym_symbols_archive="#{path_of_directory_where_to_export}/#{filename_for_dsym_symbols_archive}"
growl_alert("dSYM symbols", "Archiving the dSYM symbols into #{filepath_for_dsym_symbols_archive}")
filename_of_generated_symbols="#{project_name}.app.dSYM"
# If we don't want to have the archive contain hierarchy, we need to cd first
Dir.chdir "#{path_of_temp_directory_where_to_build}/Release-iphoneos" do
`zip -r -T -y "#{filepath_for_dsym_symbols_archive}" "#{filename_of_generated_symbols}"`
# TODO: Check for error here when zipping
end
puts "dSYM symbols archived into #{filepath_for_dsym_symbols_archive}"
end
def growl_alert(title, message)
if (@options[:growl])
growlnotify="/usr/local/bin/growlnotify" # Edit this if you installed growlnotify in a different place
if (File.executable?(growlnotify))
`#{growlnotify} "#{title}" -m "#{message}" -d archivingBubble`
else
puts "Did not found growlnotify command"
end
end
end
def reveal_file_in_finder(file_path)
applescript_command = "tell application \"Finder\"\nreveal POSIX file \"#{file_path}\"\n activate\nend tell"
`osascript -e '#{applescript_command}'`
end
parse_options
show_all_parameters if verbose
archive_xcode_project()
create_zip_archive_of_the_symbols()
Jump to Line
Something went wrong with that request. Please try again.