diff --git a/CHANGES.md b/CHANGES.md index c6c9b42..18a20b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,18 @@ +## 0.7.5 +* Allows automation of version/build number setting +* Adds full paths to xcodebuild and xcrun; they are override-able in the config +* Adds ability to add arguments to package command +* Refactors commands and argument code +* Makes build and signing output optional (use config.verbose to trigger) +* Adds simplified output +* Allows custom SCP ports (@smtlaissezfaire) +* Fixes Archive task failure (@rennarda) +* Uses Apple's Packager to produce valid IPAs (@dts) +* Fixes cases where CFPropertyList could fail to load (@svelix) +* Uses relative paths for requires (@smtlaissezfaire) +* Raises an exception if the build fails (@epall) + + ## 0.7.4.1 * Allow auto-archiving from other Rake namespaces (@victor) * Fixed bug with Xcode archive sharing (@victor) diff --git a/README.md b/README.md index 1f55b05..2a439ae 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,82 @@ To use a namespace other than "beta" for the generated tasks, simply pass in you This lets you set up different sets of BetaBuilder tasks for different configurations in the same Rakefile (e.g. a production and staging build). +## Configuration +A full list of configuration options and their details + +`configuration` - (String) The Xcode Configuration to use (Defined on the Info tab of the Project) + +`build_dir` - (File Path) The directory the build output will be. (`:derived` for Xcode 4 for versions < 0.8) + +`auto_archive` - (true/**false**) Automatically archive when packaging + +`archive_path` - (File Path) Path to the Archives + +`xcodebuild_path` - (File Path) Path to xcodebuild, if its non-standard + +`xcodeargs` - (Arguments) Arguments passed to `xcodebuild` when it runs + +`project_file_path` - (File Path) Path to the `.xcodeproj` file + +`workspace_path` - (File Path) Path to the `.xcworkspace` file + +`ipa_destination_path` - (File Path) Path to output Packaged IPA to + +`scheme` - (String) What Scheme to use when building + +`app_name` - (String) The name of the app being built (should match the file name, less the `.app` extension) + +`arch` - (String) The architecture to build for, if different from project settings + +`xcode4_archive_mode` - (true/**false**) Use Xcode4's Archive mode + +`skip_clean` - (true/**false**) Skip the clean step when building + +`verbose` - (true/**false**) Increased output for debugging + +`dry_run` - (true/**false**) Don't upload to Distribution Strategy, just act like it + +`set_version_number` - (true/**false**) Attempts to set a version number, see below + +### Configuration (Testflight) +Testflight presents its own set of options that can be configured + +`api_token` - (String) Can be your API key, but its recommended to use an `ENV[""]` variable + +`team_token` - (String) Your Team's Testflight API token + +`distribution_lists` - (Array) A Ruby array (`[1,2]` or `%w{1 2}`) of distribution list names for Testflight + +`notify` - (true/**false**) Notify the distribution list of this build + +`replace` - (true/**false**) Replace if an existing build exists with the same ID and version + +### Configuration (Web) +Pushing to a web server has the following options. + +SSH keys will simplify authentication and make this process seamless + +`remote_host` - (String) Hostname for the server the build will be pushed to + +`remote_port` - (String) Port Number to use for SCP/SFTP + +`remote_installation_path` - (String) Remote Path + +### Configuration (RunTime) +Certain configuration options are availabe at the command line, so that you can temporarily set them for a single run without modifying your configuration. + +Pass any of these in as environment variables: + +`DRY` - (true/**false**) Enable Dry Run +`VERBOSE` - (true/**false**) Turn on all output; lets you see the clean, build, and signing output +`SKIPCLEAN` - (true/**false**) Skips the clean step and goes right to Build. + +####Examples + +`rake staging:deploy DRY=true` + +`rake staging:redeploy VERBOSE=true SKIPCLEAN=true` + ## Xcode 4 support Betabuilder works with Xcode 4, but you may need to tweak your task configuration slightly. The most important change you will need to make is the build directory location, unless you have configured Xcode 4 to use the "build" directory relative to your project, as in Xcode 3. @@ -81,13 +157,29 @@ If you are working with an Xcode 4 workspace instead of a project file, you will config.workspace_path = "MyWorkspace.xcworkspace" config.scheme = "My App Scheme" config.app_name = "MyApp" - + set_version_number If you are using a workspace, then you must specify the scheme. You can still specify the build configuration (e.g. Release). ## Automatic deployment with deployment strategies BetaBuilder allows you to deploy your built package using it's extensible deployment strategy system; the gem currently comes with support for simple web-based deployment or uploading to [TestFlightApp.com](http://www.testflightapp.com). Eventually, you will be able to write your own custom deployment strategies if neither of these are suitable for your needs. +## Setting version numbers automatically + +You can use betabuilder to set a build number using Git's `describe` system. In order for that to work, at least one `tag` must exist somewhere in the git hierarchy for the branch being built from. + +Also, you are required to set your `CFBundleVersion` to `${VERSION_LONG}` inside your `Info.plist`. To accomodate manual builds, add a `VERSION_LONG` Build Setting to your app's Project, and treat it as you normally would your `Info.plist` version number. + +Once a tag is created and your App is configured, simply add this to your BetaBuilder config and it will use Git to generate the + + config.set_version_number = true + +It will generate a version number like: `1.0-15-g6b3c1bb`. + +* *1.0* is the most recent tag +* *15* is the number of commits since the tag was generated +* *g6b3c1bb* is the beginning of the hash of the last commit. + ### Deploying your app with TestFlight By far the easiest way to get your beta release into the hands of your testers is using the excellent [TestFlight service](http://testflightapp.com/), although at the time of writing it is still in private beta. You can use TestFlight to manage your beta testers and notify them of new releases instantly. diff --git a/Rakefile b/Rakefile index 16ec69d..85f6c29 100644 --- a/Rakefile +++ b/Rakefile @@ -15,11 +15,11 @@ spec = Gem::Specification.new do |s| # Change these as appropriate s.name = "betabuilder" - s.version = "0.7.4.1" + s.version = "0.7.5" s.summary = "A set of Rake tasks and utilities for managing iOS ad-hoc builds" - s.author = "Luke Redpath" - s.email = "luke@lukeredpath.co.uk" - s.homepage = "http://github.com/lukeredpath/betabuilder" + s.authors = ["Luke Redpath", "Nick Peelman"] + s.email = ["luke@lukeredpath.co.uk", "nick@peelman.us"] + s.homepage = "http://github.com/peelman/betabuilder" s.has_rdoc = false s.extra_rdoc_files = %w(README.md LICENSE CHANGES.md) diff --git a/betabuilder.gemspec b/betabuilder.gemspec index 3a37189..c0e5585 100644 --- a/betabuilder.gemspec +++ b/betabuilder.gemspec @@ -1,9 +1,13 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |s| - s.name = "betabuilder" - s.version = "0.7.4.1" - + s.name = "betabuilder" + s.version = "0.7.5" + s.summary = "A set of Rake tasks and utilities for managing iOS ad-hoc builds" + s.authors = ["Luke Redpath", "Nick Peelman"] + s.email = ["luke@lukeredpath.co.uk", "nick@peelman.us"] + s.homepage = "http://github.com/peelman/betabuilder" + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Luke Redpath"] s.date = "2012-05-16" @@ -14,7 +18,6 @@ Gem::Specification.new do |s| s.rdoc_options = ["--main", "README.md"] s.require_paths = ["lib"] s.rubygems_version = "1.8.11" - s.summary = "A set of Rake tasks and utilities for managing iOS ad-hoc builds" if s.respond_to? :specification_version then s.specification_version = 3 diff --git a/lib/beta_builder.rb b/lib/beta_builder.rb index 878a6a7..20a26fb 100644 --- a/lib/beta_builder.rb +++ b/lib/beta_builder.rb @@ -2,9 +2,9 @@ require 'ostruct' require 'fileutils' require 'cfpropertylist' -require 'beta_builder/archived_build' -require 'beta_builder/deployment_strategies' -require 'beta_builder/build_output_parser' +require File.dirname(__FILE__) + '/beta_builder/archived_build' +require File.dirname(__FILE__) + '/beta_builder/deployment_strategies' +require File.dirname(__FILE__) + '/beta_builder/build_output_parser' module BetaBuilder class Tasks < ::Rake::TaskLib @@ -14,16 +14,21 @@ def initialize(namespace = :beta, &block) :build_dir => "build", :auto_archive => false, :archive_path => File.expand_path("~/Library/Developer/Xcode/Archives"), - :xcodebuild_path => "xcodebuild", + :xcodebuild_path => "/usr/bin/xcodebuild", + :xcrun_path => "/usr/bin/xcrun", + :xcodeargs => nil, + :packageargs => nil, :project_file_path => nil, :workspace_path => nil, + :ipa_destination_path => "./", :scheme => nil, :app_name => nil, :arch => nil, :xcode4_archive_mode => false, - :skip_clean => false, - :verbose => false, - :dry_run => false + :skip_clean => ENV.fetch('SKIPCLEAN', false), + :verbose => ENV.fetch('VERBOSE', false), + :dry_run => ENV.fetch('DRY', false), + :set_version_number => false ) @namespace = namespace yield @configuration if block_given? @@ -32,7 +37,13 @@ def initialize(namespace = :beta, &block) def xcodebuild(*args) # we're using tee as we still want to see our build output on screen - system("#{@configuration.xcodebuild_path} #{args.join(" ")} | tee build.output") + cmd = [] + cmd << @configuration.xcodebuild_path + cmd.concat args + puts "Running: #{cmd.join(" ")}" if @configuration.verbose + cmd << "2>&1 %s build.output" % (@configuration.verbose ? '| tee' : '>') + cmd = cmd.join(" ") + system(cmd) end class Configuration < OpenStruct @@ -41,17 +52,26 @@ def release_notes_text release_notes end def build_arguments - args = "" + args = [] if workspace_path raise "A scheme is required if building from a workspace" unless scheme - args << "-workspace '#{workspace_path}' -scheme '#{scheme}' -configuration '#{configuration}'" + args << "-workspace '#{workspace_path}'" + args << "-scheme '#{scheme}'" else - args = "-target '#{target}' -configuration '#{configuration}' -sdk iphoneos" - args << " -project #{project_file_path}" if project_file_path + args << "-target '#{target}'" + args << "-sdk iphoneos" + args << "-project '#{project_file_path}'" if project_file_path end - - args << " -arch \"#{arch}\"" unless arch.nil? - + + args << "-configuration '#{configuration}'" + args << "-arch '#{arch}'" unless arch.nil? + args << "VERSION_LONG='#{build_number_git}'" if set_version_number + + if xcodeargs + args.concat xcodeargs if xcodeargs.is_a? Array + args << "#{xcodeargs}" if xcodears.is_a? String + end + args end @@ -74,13 +94,13 @@ def ipa_name else "#{target}.ipa" end - end + end def built_app_path if build_dir == :derived - "#{derived_build_dir_from_build_output}/#{configuration}-iphoneos/#{app_file_name}" + File.join("#{derived_build_dir_from_build_output}", "#{configuration}-iphoneos", "#{app_file_name}") else - "#{build_dir}/#{configuration}-iphoneos/#{app_file_name}" + File.join("#{build_dir}", "#{configuration}-iphoneos", "#{app_file_name}") end end @@ -93,12 +113,12 @@ def built_app_dsym_path "#{built_app_path}.dSYM" end - def dist_path - File.join("pkg/dist") + def ipa_path + File.join(File.expand_path(ipa_destination_path), ipa_name) end - def ipa_path - File.join(dist_path, ipa_name) + def build_number_git + `git describe --tags --abbrev=1`.chop end def deploy_using(strategy_name, &block) @@ -114,34 +134,64 @@ def deploy_using(strategy_name, &block) private def define - namespace(@namespace) do - desc "Build the beta release of the app" - task :build => :clean do - xcodebuild @configuration.build_arguments, "build" - end - + namespace(@namespace) do + desc "Clean the Build" task :clean do unless @configuration.skip_clean + print "Cleaning Project..." xcodebuild @configuration.build_arguments, "clean" + puts "Done" end end + desc "Build the beta release of the app" + task :build => :clean do + print "Building Project..." + xcodebuild @configuration.build_arguments, "build" + raise "** BUILD FAILED **" if BuildOutputParser.new(File.read("build.output")).failed? + puts "Done" + end + desc "Package the beta release as an IPA file" task :package => :build do if @configuration.auto_archive Rake::Task["#{@namespace}:archive"].invoke end - - FileUtils.rm_rf('pkg') && FileUtils.mkdir_p('pkg') - FileUtils.mkdir_p("pkg/Payload") - FileUtils.mv(@configuration.built_app_path, "pkg/Payload/#{@configuration.app_file_name}") - Dir.chdir("pkg") do - system("zip -r '#{@configuration.ipa_name}' Payload") + print "Packaging and Signing..." + raise "** PACKAGE FAILED ** No Signing Identity Found" unless @configuration.signing_identity + raise "** PACKAGE FAILED ** No Provisioning Profile Found" unless @configuration.provisioning_profile + + # Construct the IPA and Sign it + cmd = [] + cmd << @configuration.xcrun_path + cmd << "-sdk iphoneos" + cmd << "PackageApplication" + cmd << "-v '#{@configuration.built_app_path}'" + cmd << "-o '#{@configuration.ipa_path}'" + cmd << "--sign '#{@configuration.signing_identity}'" + cmd << "--embed '#{@configuration.provisioning_profile}'" + if @configuration.packageargs + cmd.concat @configuration.packageargs if @configuration.packageargs.is_a? Array + cmd << @configuration.packageargs if @configuration.packageargs.is_a? String end - FileUtils.mkdir('pkg/dist') - FileUtils.mv("pkg/#{@configuration.ipa_name}", "pkg/dist") + puts "Running #{cmd.join(" ")}" if @configuration.verbose + cmd << "2>&1 %s build.output" % (@configuration.verbose ? '| tee' : '>') + cmd = cmd.join(" ") + system(cmd) + + puts "Done" + + puts "IPA File: #{@configuration.ipa_path}" if @configuration.verbose end - + + desc "Build and archive the app" + task :archive => :build do + puts "Archiving build..." + archive = BetaBuilder.archive(@configuration) + output_path = archive.save_to(@configuration.archive_path) + puts "Archive saved to #{output_path}." + end + if @configuration.deployment_strategy desc "Prepare your app for deployment" task :prepare => :package do @@ -159,14 +209,6 @@ def define @configuration.deployment_strategy.deploy end end - - desc "Build and archive the app" - task :archive => :build do - puts "Archiving build..." - archive = BetaBuilder.archive(@configuration) - output_path = archive.save_to(@configuration.archive_path) - puts "Archive saved to #{output_path}." - end end end end diff --git a/lib/beta_builder/archived_build.rb b/lib/beta_builder/archived_build.rb index d9639e0..a79ac1c 100644 --- a/lib/beta_builder/archived_build.rb +++ b/lib/beta_builder/archived_build.rb @@ -1,6 +1,6 @@ require 'uuid' require 'fileutils' -require 'CFPropertyList' +require 'cfpropertylist' module BetaBuilder def self.archive(configuration) @@ -76,7 +76,7 @@ def write_plist_to(path) "ApplicationPath" => File.join("Applications", @configuration.app_file_name), "CFBundleIdentifier" => metadata["CFBundleIdentifier"], "CFBundleShortVersionString" => version, - "IconPaths" => metadata["CFBundleIconFiles"].map { |file| File.join("Applications", @configuration.app_file_name, file) } + "IconPaths" => metadata["CFBundleIcons"]["CFBundlePrimaryIcon"]["CFBundleIconFiles"].map { |file| File.join("Applications", @configuration.app_file_name, file) } }, "ArchiveVersion" => 1.0, "Comment" => @configuration.release_notes_text, diff --git a/lib/beta_builder/build_output_parser.rb b/lib/beta_builder/build_output_parser.rb index ec8ff97..f0fe1bc 100644 --- a/lib/beta_builder/build_output_parser.rb +++ b/lib/beta_builder/build_output_parser.rb @@ -16,6 +16,10 @@ def build_output_dir derived_data_directory = reference.split("/Build/Products/").first "#{derived_data_directory}/Build/Products/" end + + def failed? + @output.split("\n").any? {|line| line.include? "** BUILD FAILED **"} + end end end diff --git a/lib/beta_builder/deployment_strategies.rb b/lib/beta_builder/deployment_strategies.rb index 24a89f7..4520ebd 100644 --- a/lib/beta_builder/deployment_strategies.rb +++ b/lib/beta_builder/deployment_strategies.rb @@ -22,7 +22,7 @@ def configure(&block) end def prepare - puts "Nothing to prepare!" + puts "Nothing to prepare!" if @configuration.verbose end end @@ -34,6 +34,6 @@ def self.strategies end end -require 'beta_builder/deployment_strategies/web' -require 'beta_builder/deployment_strategies/testflight' +require File.dirname(__FILE__) + '/deployment_strategies/web' +require File.dirname(__FILE__) + '/deployment_strategies/testflight' diff --git a/lib/beta_builder/deployment_strategies/testflight.rb b/lib/beta_builder/deployment_strategies/testflight.rb index 41250aa..3afbaa1 100644 --- a/lib/beta_builder/deployment_strategies/testflight.rb +++ b/lib/beta_builder/deployment_strategies/testflight.rb @@ -6,6 +6,8 @@ module BetaBuilder module DeploymentStrategies class TestFlight < Strategy + include Rake::DSL + include FileUtils ENDPOINT = "https://testflightapp.com/api/builds.json" def extended_configuration_for_strategy @@ -27,7 +29,6 @@ def deploy :notify => @configuration.notify || false, :replace => @configuration.replace || false } - puts "Uploading build to TestFlight..." if @configuration.verbose puts "ipa path: #{@configuration.ipa_path}" puts "release notes: #{release_notes}" @@ -38,6 +39,8 @@ def deploy return end + print "Uploading build to TestFlight..." + begin response = RestClient.post(ENDPOINT, payload, :accept => :json) rescue => e @@ -45,9 +48,10 @@ def deploy end if (response.code == 201) || (response.code == 200) - puts "Upload complete." + puts "Done." else - puts "Upload failed. (#{response})" + puts "Failed." + puts "#{response}" end end diff --git a/lib/beta_builder/deployment_strategies/web.rb b/lib/beta_builder/deployment_strategies/web.rb index 7510d2b..62293e1 100644 --- a/lib/beta_builder/deployment_strategies/web.rb +++ b/lib/beta_builder/deployment_strategies/web.rb @@ -4,87 +4,100 @@ class Web < Strategy def extended_configuration_for_strategy proc do def deployment_url - File.join(deploy_to, target.downcase, ipa_name) + File.join(deploy_to, ipa_name) end def manifest_url - File.join(deploy_to, target.downcase, "manifest.plist") + File.join(deploy_to, "manifest.plist") end def remote_installation_path - File.join(remote_directory, target.downcase) + File.join(remote_directory) end end end - + def prepare - plist = CFPropertyList::List.new(:file => "pkg/Payload/#{@configuration.app_name}/Info.plist") + plist = CFPropertyList::List.new(:file => "#{@configuration.built_app_path}/Info.plist") plist_data = CFPropertyList.native_types(plist.value) File.open("pkg/dist/manifest.plist", "w") do |io| - io << %{ - - - - - items - - - assets - - - kind - software-package - url - #{@configuration.deployment_url} - - - metadata - - bundle-identifier - #{plist_data['CFBundleIdentifier']} - bundle-version - #{plist_data['CFBundleVersion']} - kind - software - title - #{plist_data['CFBundleDisplayName']} - - - - - - } + io << %{ + + + + items + + + assets + + + kind + software-package + url + #{@configuration.deployment_url} + + + metadata + + bundle-identifier + #{plist_data['CFBundleIdentifier']} + bundle-version + #{plist_data['CFBundleVersion']} + kind + software + title + #{plist_data['CFBundleDisplayName']} + + + + + +} end File.open("pkg/dist/index.html", "w") do |io| - io << %{ - - - - - - Beta Download - - - -
- -

Link didn't work?
- Make sure you're visiting this page on your device, not your computer.

- - - } + io << %{ + + + + + Beta Download + + + +
+ +

Link didn't work?
+ Make sure you're visiting this page on your device, not your computer.

+
+ + +} end end - + def deploy - system("scp pkg/dist/* #{@configuration.remote_host}:#{@configuration.remote_installation_path}") + cmd = [] + + cmd.push "scp" + + if @configuration.remote_port + cmd.push "-P #{@configuration.remote_port}" + end + + cmd.push "pkg/dist/*" + cmd.push "#{@configuration.remote_host}:#{@configuration.remote_installation_path}" + + cmd = cmd.join(" ") + + puts "* Running `#{cmd}`" + system(cmd) end end end diff --git a/lib/betabuilder.rb b/lib/betabuilder.rb index 98cdd18..0974f93 100644 --- a/lib/betabuilder.rb +++ b/lib/betabuilder.rb @@ -1 +1 @@ -require 'beta_builder' +require File.dirname(__FILE__) + '/beta_builder'