xcodebuilder 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ module XcodeBuilder
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,151 @@
1
+ require 'pathname'
2
+ require File.dirname(__FILE__) + '/deployment_strategies'
3
+ require File.dirname(__FILE__) + '/release_strategies'
4
+
5
+ module XcodeBuilder
6
+ class Configuration < OpenStruct
7
+ def release_notes_text
8
+ return release_notes.call if release_notes.is_a? Proc
9
+ release_notes
10
+ end
11
+
12
+ def build_arguments
13
+ args = []
14
+ if workspace_file_path
15
+ raise "A scheme is required if building from a workspace" unless scheme
16
+ args << "-workspace '#{workspace_file_path}'"
17
+ args << "-scheme '#{scheme}'"
18
+ else
19
+ args << "-target '#{target}'"
20
+ args << "-project '#{project_file_path}'" if project_file_path
21
+ end
22
+
23
+ args << "-sdk #{sdk}"
24
+
25
+ args << "-configuration '#{configuration}'"
26
+
27
+ if xcodebuild_extra_args
28
+ args.concat xcodebuild_extra_args if xcodebuild_extra_args.is_a? Array
29
+ args << "#{xcodebuild_extra_args}" if xcodears.is_a? String
30
+ end
31
+
32
+ args
33
+ end
34
+
35
+ def app_file_name
36
+ raise ArgumentError, "app_name or target must be set in the BetaBuilder configuration block" if app_name.nil?
37
+ "#{app_name}.#{app_extension}"
38
+ end
39
+
40
+ def info_plist_path
41
+ if info_plist != nil then
42
+ File.expand_path info_plist
43
+ else
44
+ nil
45
+ end
46
+ end
47
+
48
+ def build_number
49
+ # no plist is found, return a nil version
50
+ if (info_plist_path == nil) || (!File.exists? info_plist_path) then
51
+ return nil
52
+ end
53
+
54
+ # read the plist and extract data
55
+ plist = CFPropertyList::List.new(:file => info_plist_path)
56
+ data = CFPropertyList.native_types(plist.value)
57
+ data["CFBundleVersion"]
58
+ end
59
+
60
+ def next_build_number
61
+ # if we don't have a current version, we don't have a next version :)
62
+ if build_number == nil then
63
+ return nil
64
+ end
65
+
66
+ # get a hold on the build number and increment it
67
+ version_components = build_number.split(".")
68
+ new_build_number = version_components.pop.to_i + 1
69
+ version_components.push new_build_number.to_s
70
+ version_components.join "."
71
+ end
72
+
73
+ def built_app_long_version_suffix
74
+ if build_number == nil then
75
+ ""
76
+ else
77
+ "-#{build_number}"
78
+ end
79
+ end
80
+
81
+ def ipa_name
82
+ prefix = app_name == nil ? target : app_name
83
+ "#{prefix}#{built_app_long_version_suffix}.ipa"
84
+ end
85
+
86
+ def built_app_path
87
+ sdk_extension = if sdk.eql? "macosx" then "" else "-#{sdk}" end
88
+ if build_dir == :derived
89
+ File.join("#{derived_build_dir}", "#{configuration}#{sdk_extension}", "#{app_file_name}")
90
+ else
91
+ File.join("#{build_dir}", "#{configuration}#{sdk_extension}", "#{app_file_name}")
92
+ end
93
+ end
94
+
95
+ def built_dsym_path
96
+ "#{built_app_path}.dSYM"
97
+ end
98
+
99
+ def derived_build_dir
100
+ workspace_name = Pathname.new(workspace_file_path).basename.to_s.split(".")[0]
101
+ for dir in Dir[File.join(File.expand_path("~/Library/Developer/Xcode/DerivedData"), "#{workspace_name}-*")]
102
+ return "#{dir}/Build/Products" if File.read( File.join(dir, "info.plist") ).match workspace_file_path
103
+ end
104
+ end
105
+
106
+
107
+ def derived_build_dir_from_build_output
108
+ output = BuildOutputParser.new(File.read("build.output"))
109
+ output.build_output_dir
110
+ end
111
+
112
+ def zipped_package_name
113
+ "#{app_name}#{built_app_long_version_suffix}.zip"
114
+ end
115
+
116
+ def ipa_path
117
+ File.join(File.expand_path(package_destination_path), ipa_name)
118
+ end
119
+
120
+ def dsym_name
121
+ "#{app_name}#{built_app_long_version_suffix}.dSYM.zip"
122
+ end
123
+
124
+ def dsym_path
125
+ File.join(File.expand_path(package_destination_path), dsym_name)
126
+ end
127
+
128
+ def app_bundle_path
129
+ "#{package_destination_path}/#{app_name}.#{app_extension}"
130
+ end
131
+
132
+ def deploy_using(strategy_name, &block)
133
+ if DeploymentStrategies.valid_strategy?(strategy_name.to_sym)
134
+ self.deployment_strategy = DeploymentStrategies.build(strategy_name, self)
135
+ self.deployment_strategy.configure(&block)
136
+ else
137
+ raise "Unknown deployment strategy '#{strategy_name}'."
138
+ end
139
+ end
140
+
141
+ def release_using(strategy_name, &block)
142
+ if ReleaseStrategies.valid_strategy?(strategy_name.to_sym)
143
+ self.release_strategy = ReleaseStrategies.build(strategy_name, self)
144
+ self.release_strategy.configure(&block)
145
+ self.release_strategy.prepare
146
+ else
147
+ raise "Unknown release strategy '#{strategy_name}'."
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,39 @@
1
+ module XcodeBuilder
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 XcodeBuilder
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 XcodeBuilder
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 XcodeBuilder
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
+