ot-ios-builder 0.7.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,101 @@
1
+ require 'uuid'
2
+ require 'fileutils'
3
+ require 'cfpropertylist'
4
+
5
+ module BetaBuilder
6
+ def self.archive(configuration)
7
+ if configuration.xcode4_archive_mode
8
+ Xcode4ArchivedBuild.new(configuration)
9
+ else
10
+ ArchivedBuild.new(configuration)
11
+ end
12
+ end
13
+
14
+ class ArchivedBuild
15
+ def initialize(configuration)
16
+ @configuration = configuration
17
+ @uuid = UUID.generate.upcase
18
+ end
19
+
20
+ def save_to(path)
21
+ archive_path = File.join(path, "#{@uuid}.apparchive")
22
+ FileUtils.mkdir(archive_path)
23
+ FileUtils.cp_r(@configuration.built_app_path, archive_path)
24
+ FileUtils.cp_r(@configuration.built_app_dsym_path, archive_path)
25
+ archive_path
26
+ end
27
+ end
28
+
29
+ class Xcode4ArchivedBuild
30
+ def initialize(configuration)
31
+ @configuration = configuration
32
+ end
33
+
34
+ def archive_file_name
35
+ "#{@configuration.archive_name} #{Time.now.strftime('%Y-%m-%d %H.%M')}.xcarchive"
36
+ end
37
+
38
+ def archive_path_within(path)
39
+ File.join(path, "#{Time.now.strftime('%Y-%m-%d')}", archive_file_name)
40
+ end
41
+
42
+ def applications_path
43
+ File.join("Products", "Applications")
44
+ end
45
+
46
+ def dsyms_path
47
+ "dSYMs"
48
+ end
49
+
50
+ def plist_info_path
51
+ File.join(@configuration.built_app_path, "Info.plist")
52
+ end
53
+
54
+ def save_to(path)
55
+ archive_path = archive_path_within(path)
56
+ FileUtils.mkdir_p(archive_path)
57
+
58
+ application_path = File.join(archive_path, applications_path)
59
+ FileUtils.mkdir_p(application_path)
60
+ FileUtils.cp_r(@configuration.built_app_path, application_path)
61
+
62
+ dsym_path = File.join(archive_path, dsyms_path)
63
+ FileUtils.mkdir_p(dsym_path)
64
+ FileUtils.cp_r(@configuration.built_app_dsym_path, dsym_path)
65
+
66
+ write_plist_to(archive_path)
67
+ archive_path
68
+ end
69
+
70
+ private
71
+
72
+ def write_plist_to(path)
73
+ version = metadata["CFBundleShortVersionString"] || metadata["CFBundleVersion"]
74
+ plist = {
75
+ "ApplicationProperties" => {
76
+ "ApplicationPath" => File.join("Applications", @configuration.app_file_name),
77
+ "CFBundleIdentifier" => metadata["CFBundleIdentifier"],
78
+ "CFBundleShortVersionString" => version,
79
+ "IconPaths" => metadata["CFBundleIcons"]["CFBundlePrimaryIcon"]["CFBundleIconFiles"].map { |file| File.join("Applications", @configuration.app_file_name, file) }
80
+ },
81
+ "ArchiveVersion" => 1.0,
82
+ "Comment" => @configuration.release_notes_text,
83
+ "CreationDate" => Time.now,
84
+ "Name" => @configuration.archive_name,
85
+ "SchemeName" => @configuration.scheme
86
+ }
87
+ File.open(File.join(path, "Info.plist"), "w") do |io|
88
+ io.write plist.to_plist(:convert_unknown_to_string => true)
89
+ end
90
+ end
91
+
92
+ def metadata
93
+ @metadata ||= load_property_list(plist_info_path)
94
+ end
95
+
96
+ def load_property_list(path)
97
+ plist = CFPropertyList::List.new(:file => path)
98
+ CFPropertyList.native_types(plist.value)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,56 @@
1
+ module BetaBuilder
2
+ class BuildOutputParser
3
+
4
+ def initialize(output)
5
+ @output = output
6
+ end
7
+
8
+ def build_output_dir
9
+ # yes, this is truly horrible, but unless somebody else can find a better way...
10
+ found = @output.split("\n").grep(/^Validate(.*)\/Xcode\/DerivedData\/(.*)-(.*)/).first
11
+ if found && found =~ /Validate [\"]?([^\"|$]*)/
12
+ reference = $1
13
+ else
14
+ raise "Cannot parse build_dir from build output."
15
+ end
16
+ derived_data_directory = reference.split("/Build/Products/").first
17
+ "#{derived_data_directory}/Build/Products/"
18
+ end
19
+
20
+ def failed?
21
+ @output.split("\n").any? {|line| line.include? "** BUILD FAILED **"}
22
+ end
23
+ end
24
+ end
25
+
26
+ # quick testing
27
+ if __FILE__ == $0
28
+
29
+ require 'test/unit'
30
+ class BuildOutputTest < Test::Unit::TestCase
31
+
32
+ def test_parses_output_with_unquoted_build_path
33
+ bop = BetaBuilder::BuildOutputParser.new(<<eos)
34
+ Validate /Users/johnsmith/Library/Developer/Xcode/DerivedData/Application-hegpgdbpjylesafhkxnsymrzjavl/Build/Products/Distribution-iphoneos/Application.app
35
+ cd /Users/user/app/ios
36
+ setenv PATH \"/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin:/Developer/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11/bin\"
37
+ setenv PRODUCT_TYPE com.apple.product-type.application
38
+ /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/Validation /Users/user/Library/Developer/Xcode/DerivedData/Application-hegpgdbpjylesafhkxnsymrzjavl/Build/Products/Distribution-iphoneos/Application.app
39
+ eos
40
+ assert_equal "/Users/johnsmith/Library/Developer/Xcode/DerivedData/Application-hegpgdbpjylesafhkxnsymrzjavl/Build/Products/", bop.build_output_dir
41
+ end
42
+
43
+ def test_parses_output_with_quoted_build_path
44
+ bop = BetaBuilder::BuildOutputParser.new(<<eos)
45
+ Validate \"/Users/john smith/Library/Developer/Xcode/DerivedData/Application-hegpgdbpjylesafhkxnsymrzjavl/Build/Products/Distribution-iphoneos/Application.app\"
46
+ cd /Users/user/app/ios
47
+ setenv PATH \"/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin:/Developer/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11/bin\"
48
+ setenv PRODUCT_TYPE com.apple.product-type.application
49
+ /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/Validation /Users/user/Library/Developer/Xcode/DerivedData/Application-hegpgdbpjylesafhkxnsymrzjavl/Build/Products/Distribution-iphoneos/Application.app
50
+ eos
51
+ assert_equal "/Users/john smith/Library/Developer/Xcode/DerivedData/Application-hegpgdbpjylesafhkxnsymrzjavl/Build/Products/", bop.build_output_dir
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,39 @@
1
+ module BetaBuilder
2
+ module DeploymentStrategies
3
+ def self.valid_strategy?(strategy_name)
4
+ strategies.keys.include?(strategy_name.to_sym)
5
+ end
6
+
7
+ def self.build(strategy_name, configuration)
8
+ strategies[strategy_name.to_sym].new(configuration)
9
+ end
10
+
11
+ class Strategy
12
+ def initialize(configuration)
13
+ @configuration = configuration
14
+
15
+ if respond_to?(:extended_configuration_for_strategy)
16
+ @configuration.instance_eval(&extended_configuration_for_strategy)
17
+ end
18
+ end
19
+
20
+ def configure(&block)
21
+ yield @configuration
22
+ end
23
+
24
+ def prepare
25
+ puts "Nothing to prepare!" if @configuration.verbose
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def self.strategies
32
+ {:web => Web, :testflight => TestFlight}
33
+ end
34
+ end
35
+ end
36
+
37
+ require File.dirname(__FILE__) + '/deployment_strategies/web'
38
+ require File.dirname(__FILE__) + '/deployment_strategies/testflight'
39
+
@@ -0,0 +1,92 @@
1
+ require 'rest_client'
2
+ require 'json'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+
6
+ module BetaBuilder
7
+ module DeploymentStrategies
8
+ class TestFlight < Strategy
9
+ include Rake::DSL
10
+ include FileUtils
11
+ ENDPOINT = "https://testflightapp.com/api/builds.json"
12
+
13
+ def extended_configuration_for_strategy
14
+ proc do
15
+ def generate_release_notes(&block)
16
+ self.release_notes = block if block
17
+ end
18
+ end
19
+ end
20
+
21
+ def deploy
22
+ release_notes = get_notes
23
+ payload = {
24
+ :api_token => @configuration.api_token,
25
+ :team_token => @configuration.team_token,
26
+ :file => File.new(@configuration.ipa_path, 'rb'),
27
+ :notes => release_notes,
28
+ :distribution_lists => (@configuration.distribution_lists || []).join(","),
29
+ :notify => @configuration.notify || false,
30
+ :replace => @configuration.replace || false
31
+ }
32
+ if @configuration.verbose
33
+ puts "ipa path: #{@configuration.ipa_path}"
34
+ puts "release notes: #{release_notes}"
35
+ end
36
+
37
+ if @configuration.dry_run
38
+ puts '** Dry Run - No action here! **'
39
+ return
40
+ end
41
+
42
+ print "Uploading build to TestFlight..."
43
+
44
+ begin
45
+ response = RestClient.post(ENDPOINT, payload, :accept => :json)
46
+ rescue => e
47
+ response = e.response
48
+ end
49
+
50
+ if (response.code == 201) || (response.code == 200)
51
+ puts "Done."
52
+ else
53
+ puts "Failed."
54
+ puts "#{response}"
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def get_notes
61
+ notes = @configuration.release_notes_text
62
+ notes || get_notes_using_editor || get_notes_using_prompt
63
+ end
64
+
65
+ def get_notes_using_editor
66
+ return unless (editor = ENV["EDITOR"])
67
+
68
+ dir = Dir.mktmpdir
69
+ begin
70
+ filepath = "#{dir}/release_notes"
71
+ system("#{editor} #{filepath}")
72
+ @configuration.release_notes = File.read(filepath)
73
+ ensure
74
+ rm_rf(dir)
75
+ end
76
+ end
77
+
78
+ def get_notes_using_prompt
79
+ puts "Enter the release notes for this build (hit enter twice when done):\n"
80
+ @configuration.release_notes = gets_until_match(/\n{2}$/).strip
81
+ end
82
+
83
+ def gets_until_match(pattern, string = "")
84
+ if (string += STDIN.gets) =~ pattern
85
+ string
86
+ else
87
+ gets_until_match(pattern, string)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,104 @@
1
+ module BetaBuilder
2
+ module DeploymentStrategies
3
+ class Web < Strategy
4
+ def extended_configuration_for_strategy
5
+ proc do
6
+ def deployment_url
7
+ File.join(deploy_to, ipa_name)
8
+ end
9
+
10
+ def manifest_url
11
+ File.join(deploy_to, "manifest.plist")
12
+ end
13
+
14
+ def remote_installation_path
15
+ File.join(remote_directory)
16
+ end
17
+ end
18
+ end
19
+
20
+ def prepare
21
+ plist = CFPropertyList::List.new(:file => "#{@configuration.built_app_path}/Info.plist")
22
+ plist_data = CFPropertyList.native_types(plist.value)
23
+ File.open("pkg/dist/manifest.plist", "w") do |io|
24
+ io << %{<?xml version="1.0" encoding="UTF-8"?>
25
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
26
+ <plist version="1.0">
27
+ <dict>
28
+ <key>items</key>
29
+ <array>
30
+ <dict>
31
+ <key>assets</key>
32
+ <array>
33
+ <dict>
34
+ <key>kind</key>
35
+ <string>software-package</string>
36
+ <key>url</key>
37
+ <string>#{@configuration.deployment_url}</string>
38
+ </dict>
39
+ </array>
40
+ <key>metadata</key>
41
+ <dict>
42
+ <key>bundle-identifier</key>
43
+ <string>#{plist_data['CFBundleIdentifier']}</string>
44
+ <key>bundle-version</key>
45
+ <string>#{plist_data['CFBundleVersion']}</string>
46
+ <key>kind</key>
47
+ <string>software</string>
48
+ <key>title</key>
49
+ <string>#{plist_data['CFBundleDisplayName']}</string>
50
+ </dict>
51
+ </dict>
52
+ </array>
53
+ </dict>
54
+ </plist>
55
+ }
56
+ end
57
+ File.open("pkg/dist/index.html", "w") do |io|
58
+ io << %{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
59
+ <html xmlns="http://www.w3.org/1999/xhtml">
60
+ <head>
61
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
62
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
63
+ <title>Beta Download</title>
64
+ <style type="text/css">
65
+ body {background:#fff;margin:0;padding:0;font-family:arial,helvetica,sans-serif;text-align:center;padding:10px;color:#333;font-size:16px;}
66
+ #container {width:300px;margin:0 auto;}
67
+ h1 {margin:0;padding:0;font-size:14px;}
68
+ p {font-size:13px;}
69
+ .link {background:#ecf5ff;border-top:1px solid #fff;border:1px solid #dfebf8;margin-top:.5em;padding:.3em;}
70
+ .link a {text-decoration:none;font-size:15px;display:block;color:#069;}
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <div id="container">
75
+ <div class="link"><a href="itms-services://?action=download-manifest&url=#{@configuration.manifest_url}">Tap Here to Install<br />#{@configuration.target} #{plist_data['CFBundleVersion']}<br />On Your Device</a></div>
76
+ <p><strong>Link didn't work?</strong><br />
77
+ Make sure you're visiting this page on your device, not your computer.</p>
78
+ </div>
79
+ </body>
80
+ </html>
81
+ }
82
+ end
83
+ end
84
+
85
+ def deploy
86
+ cmd = []
87
+
88
+ cmd.push "scp"
89
+
90
+ if @configuration.remote_port
91
+ cmd.push "-P #{@configuration.remote_port}"
92
+ end
93
+
94
+ cmd.push "pkg/dist/*"
95
+ cmd.push "#{@configuration.remote_host}:#{@configuration.remote_installation_path}"
96
+
97
+ cmd = cmd.join(" ")
98
+
99
+ puts "* Running `#{cmd}`"
100
+ system(cmd)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,34 @@
1
+ module BetaBuilder
2
+ module ReleaseStrategies
3
+ def self.valid_strategy?(strategy_name)
4
+ strategies.keys.include?(strategy_name.to_sym)
5
+ end
6
+
7
+ def self.build(strategy_name, configuration)
8
+ strategies[strategy_name.to_sym].new(configuration)
9
+ end
10
+
11
+ class ReleaseStrategy
12
+ def initialize(configuration)
13
+ @configuration = configuration
14
+ end
15
+
16
+ def configure(&block)
17
+ yield self
18
+ end
19
+ end
20
+
21
+ def prepare
22
+ puts "Nothing to prepare!" if @configuration.verbose
23
+ end
24
+
25
+ private
26
+
27
+ def self.strategies
28
+ {:git => Git}
29
+ end
30
+ end
31
+ end
32
+
33
+ require File.dirname(__FILE__) + '/release_strategies/git'
34
+
@@ -0,0 +1,116 @@
1
+ module BetaBuilder
2
+ module ReleaseStrategies
3
+ class Git < ReleaseStrategy
4
+ attr_accessor :branch, :origin, :tag_name
5
+
6
+ def prepare
7
+
8
+ if @origin == nil then
9
+ @origin = "origin"
10
+ end
11
+
12
+ if @branch == nil then
13
+ @branch = "master"
14
+ end
15
+
16
+ if @tag_name == nil then
17
+ @tag_name = "v#{@configuration.build_number}"
18
+ end
19
+ end
20
+
21
+ def tag_current_version
22
+ puts "Relasing with Git"
23
+ print "Tagging version #{@tag_name}"
24
+ cmd = []
25
+
26
+ #first, tag
27
+ cmd << "git"
28
+ cmd << "tag"
29
+ # -f sounds brutal to start with, so let's give it a try without
30
+ # cmd << "-f"
31
+ cmd << @tag_name
32
+
33
+ cmd << "2>&1 %s git.output" % (@configuration.verbose ? '| tee' : '>')
34
+ system(cmd.join " ")
35
+ puts
36
+ puts "Done"
37
+
38
+ # then push tags to the remote server
39
+ print "Pushing tag to #{@origin} on branch #{@branch}"
40
+ cmd = []
41
+
42
+ cmd << "git"
43
+ cmd << "push"
44
+ cmd << "--tags"
45
+ cmd << @origin
46
+ cmd << @branch
47
+ cmd << "2>&1 %s git.output" % (@configuration.verbose ? '| tee' : '>')
48
+ system(cmd.join " ")
49
+
50
+ puts
51
+ puts "Done"
52
+
53
+ end
54
+
55
+ def prepare_for_next_pod_release
56
+ build_number = @configuration.build_number
57
+ raise "build number cannot be empty on release" unless (build_number != nil) && (!build_number.empty?)
58
+
59
+ print "Committing #{@configuration.app_info_plist} and #{@configuration.spec_file} with version #{build_number}"
60
+
61
+ stage_files [@configuration.app_info_plist, @configuration.spec_file]
62
+ commit_and_push_with_message "Preparing for next pod release..."
63
+
64
+ puts "Done"
65
+ end
66
+
67
+ def prepare_for_next_release
68
+ build_number = @configuration.build_number
69
+ raise "build number cannot be empty on release" unless (build_number != nil) && (!build_number.empty?)
70
+
71
+ print "Committing #{@configuration.app_info_plist} with version #{build_number}"
72
+
73
+ stage_files [@configuration.app_info_plist]
74
+ commit_and_push_with_message "Preparing for next release..."
75
+
76
+ puts "Done"
77
+ end
78
+
79
+ def stage_files files
80
+ cmd = []
81
+
82
+ cmd << "git"
83
+ cmd << "add"
84
+ files.each do |value|
85
+ cmd << value
86
+ end
87
+ system(cmd.join " ")
88
+ end
89
+
90
+ def commit_and_push_with_message message
91
+ # then commit it
92
+ cmd = []
93
+ cmd << "git"
94
+ cmd << "commit"
95
+ cmd << "-m"
96
+ cmd << "'#{message}'"
97
+ cmd << "2>&1 %s git.output" % (@configuration.verbose ? '| tee' : '>')
98
+ system(cmd.join " ")
99
+ puts
100
+ puts "Done"
101
+
102
+ # now, push the updated plist
103
+ print "Pushing update to #{@origin}/#{@branch}"
104
+ cmd = []
105
+ cmd << "git"
106
+ cmd << "push"
107
+ cmd << @origin
108
+ cmd << @branch
109
+ cmd << "2>&1 %s git.output" % (@configuration.verbose ? '| tee' : '>')
110
+
111
+ system(cmd.join " ")
112
+ puts
113
+ end
114
+ end
115
+ end
116
+ end