testflight 0.1.2 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +21 -11
- data/bin/testflight +34 -0
- data/lib/templates/testflight.yml.erb +10 -0
- data/lib/testflight.rb +28 -0
- data/lib/testflight/builder.rb +212 -0
- data/lib/testflight/cli.rb +195 -0
- data/lib/testflight/config.rb +36 -75
- data/lib/testflight/version.rb +1 -1
- metadata +25 -6
- data/bin/takeoff +0 -182
- data/lib/testflight/prompter.rb +0 -81
data/README.rdoc
CHANGED
@@ -2,46 +2,56 @@
|
|
2
2
|
|
3
3
|
If you are an iOS developer, like me, you probably have dealt with the tedious
|
4
4
|
tasks of manually building, packaging, tagging, commiting and deploying your
|
5
|
-
iOS applications to testflightapp.com.
|
5
|
+
iOS applications to testflightapp.com. You are probably wondering if there is a better way to do so.
|
6
6
|
|
7
7
|
Well, you are absolutely right! This gem is going to make your life much easier. Here is what it does:
|
8
8
|
|
9
|
-
1. Reads your XCode project
|
9
|
+
1. Reads your XCode project info file to get your app version and build number
|
10
10
|
2. Commits all your changes to the git repository
|
11
11
|
3. Builds your project (or workspace)
|
12
12
|
4. Packages your application into an IPA file
|
13
13
|
5. Uploads the IPA file to testflightapp.com
|
14
14
|
6. Tags your project with the version and build
|
15
15
|
7. Increments your app build number and commits it to the repo
|
16
|
+
8. Keeps a log of all your deployments to testflightapp.com in a FLIGHTLOG file
|
16
17
|
|
17
18
|
All that with a simple command:
|
18
19
|
|
19
|
-
$ takeoff
|
20
|
+
$ testflight takeoff
|
20
21
|
|
21
22
|
|
22
23
|
= Configuration
|
23
24
|
|
24
|
-
|
25
|
+
To setup your project with the deployment script, run the following command:
|
26
|
+
|
27
|
+
$ testflight checkin
|
28
|
+
|
29
|
+
This command will ask you a few questions that will guide you through a configuration process.
|
30
|
+
If you don't like being asked questions and prefer to do it manually, just create a .tesflight file
|
31
|
+
in your iOS project folder and provide the following information:
|
25
32
|
|
26
33
|
build:
|
27
|
-
developer_name: # This must be your company name or your name as it appears in your
|
28
|
-
increment_bundle: true
|
29
|
-
|
34
|
+
developer_name: # This must be your company name or your name as it appears in your .cer from Apple
|
35
|
+
increment_bundle: true # If you want each deployment to increment your build number
|
36
|
+
git:
|
37
|
+
commit_changes: true # if you are using git and would like to push changes before each build
|
38
|
+
tag_build: true # will tag each build in git
|
30
39
|
testflight:
|
31
40
|
api_token: # Get it from https://testflightapp.com/account/#api
|
32
41
|
team_token: # Get it from https://testflightapp.com/dashboard/team/edit/
|
33
|
-
distribution_lists: ["Internal", "Everyone"] # Configure your distribution lists
|
42
|
+
distribution_lists: ["Internal", "Everyone"] # Configure your distribution lists at https://testflightapp.com
|
34
43
|
|
35
44
|
|
36
|
-
|
45
|
+
= Execution
|
37
46
|
|
38
|
-
|
47
|
+
Once you have checked in (you only need to do it once per project), you are ready for takeoff. Go to your project folder and simple type:
|
39
48
|
|
49
|
+
$ testflight takeoff
|
40
50
|
|
41
51
|
Have a safe flight!
|
42
52
|
|
43
53
|
|
44
|
-
=
|
54
|
+
= Contribution
|
45
55
|
|
46
56
|
Clone the repository, make any changes you like and send me a pull request.
|
47
57
|
|
data/bin/testflight
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Copyright (c) 2012 Michael Berkovich
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
# a copy of this software and associated documentation files (the
|
8
|
+
# "Software"), to deal in the Software without restriction, including
|
9
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
# the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be
|
15
|
+
# included in all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
24
|
+
#++
|
25
|
+
|
26
|
+
# Abort beautifully with ctrl+c.
|
27
|
+
Signal.trap(:INT) { abort "\nAborting testflight task." }
|
28
|
+
|
29
|
+
['cli.rb', 'builder.rb', 'config.rb'].each do |f|
|
30
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "../lib/testflight/#{f}"))
|
31
|
+
end
|
32
|
+
|
33
|
+
# require 'testflight'
|
34
|
+
Testflight::Cli.start
|
@@ -0,0 +1,10 @@
|
|
1
|
+
build:
|
2
|
+
developer_name: "<%= @company_name %>"
|
3
|
+
increment_bundle: <%= @should_increment_bundle == true %>
|
4
|
+
git:
|
5
|
+
commit_changes: <%= @should_commit_changes == true %>
|
6
|
+
tag_build: <%= @should_tag_build == true %>
|
7
|
+
testflight:
|
8
|
+
api_token: <%= @api_token %>
|
9
|
+
team_token: <%= @team_token %>
|
10
|
+
distribution_lists: [<%=@teams.collect{|t| "\"#{t}\""}.join(", ")%>]
|
data/lib/testflight.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2012 Michael Berkovich
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module Testflight
|
25
|
+
autoload :Cli, 'testflight/cli'
|
26
|
+
autoload :Builder, 'testflight/builder'
|
27
|
+
autoload :Config, 'testflight/config'
|
28
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2012 Michael Berkovich
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'appscript'
|
25
|
+
require 'yaml'
|
26
|
+
|
27
|
+
module Testflight
|
28
|
+
class Builder
|
29
|
+
|
30
|
+
XCODE_BUILDER = "/usr/bin/xcodebuild"
|
31
|
+
XCODE_PACKAGER = "/usr/bin/xcrun"
|
32
|
+
TESTFLIGHT_ENDPOINT = "http://testflightapp.com/api/builds.json"
|
33
|
+
|
34
|
+
include Appscript
|
35
|
+
|
36
|
+
def initialize(opts = {})
|
37
|
+
t0 = Time.now
|
38
|
+
|
39
|
+
if Testflight::Config.commit_changes?
|
40
|
+
commit_changes(opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
if Testflight::Config.tag_build?
|
44
|
+
tag_build(opts)
|
45
|
+
end
|
46
|
+
|
47
|
+
if Testflight::Config.workspace?
|
48
|
+
build_workspace(opts)
|
49
|
+
package_workspace(opts)
|
50
|
+
else
|
51
|
+
build_project(opts)
|
52
|
+
package_project(opts)
|
53
|
+
end
|
54
|
+
|
55
|
+
upload_to_testflightapp(opts)
|
56
|
+
|
57
|
+
append_log_entry(opts)
|
58
|
+
|
59
|
+
if Testflight::Config.increment_bundle?
|
60
|
+
increment_bundle_version(opts)
|
61
|
+
if Testflight::Config.commit_changes?
|
62
|
+
commit_changes(opts.merge(:message => "Incrementing build number to #{Testflight::Config.project_version}"))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
t1 = Time.now
|
67
|
+
|
68
|
+
puts("Build took #{t1-t0} seconds.")
|
69
|
+
end
|
70
|
+
|
71
|
+
####################################################################################
|
72
|
+
## Building Project
|
73
|
+
####################################################################################
|
74
|
+
|
75
|
+
def build_workspace(opts = {})
|
76
|
+
cmd = "#{XCODE_BUILDER} -workspace '#{Testflight::Config.workspace_name}' "
|
77
|
+
cmd << "-scheme '#{Testflight::Config.application_name}' "
|
78
|
+
cmd << "-sdk 'iphoneos6.0' "
|
79
|
+
cmd << "-configuration 'AdHoc' "
|
80
|
+
cmd << "-arch 'armv6 armv7' "
|
81
|
+
cmd << "CONFIGURATION_BUILD_DIR='#{Testflight::Config.build_dir}' "
|
82
|
+
execute(cmd, opts)
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_project(opts = {})
|
86
|
+
cmd = "#{XCODE_BUILDER} -target '#{Testflight::Config.application_name}' "
|
87
|
+
cmd << "-sdk 'iphoneos6.0' "
|
88
|
+
cmd << "-configuration 'AdHoc' "
|
89
|
+
execute(cmd, opts)
|
90
|
+
end
|
91
|
+
|
92
|
+
####################################################################################
|
93
|
+
## Packaging Project
|
94
|
+
####################################################################################
|
95
|
+
|
96
|
+
def package_workspace(opts = {})
|
97
|
+
cmd = "#{XCODE_PACKAGER} -sdk iphoneos PackageApplication "
|
98
|
+
cmd << "-v '#{Testflight::Config.build_dir}/#{Testflight::Config.application_name}.app' "
|
99
|
+
cmd << "-o '#{Testflight::Config.distribution_file}' "
|
100
|
+
cmd << "--sign '#{Testflight::Config.developer_name}' "
|
101
|
+
cmd << "--embed '#{Testflight::Config.provisioning_dir}/#{Testflight::Config.ad_hoc_provisioning_name}'"
|
102
|
+
execute(cmd, opts)
|
103
|
+
end
|
104
|
+
|
105
|
+
def package_project(opts = {})
|
106
|
+
cmd = "#{XCODE_PACKAGER} -sdk iphoneos PackageApplication "
|
107
|
+
cmd << "-v '#{Testflight::Config.build_dir}/AdHoc-iphoneos/#{Testflight::Config.application_name}.app' "
|
108
|
+
cmd << "-o '#{Testflight::Config.distribution_file}' "
|
109
|
+
cmd << "--sign '#{Testflight::Config.developer_name}' "
|
110
|
+
cmd << "--embed '#{Testflight::Config.provisioning_dir}/#{Testflight::Config.ad_hoc_provisioning_name}'"
|
111
|
+
execute(cmd, opts)
|
112
|
+
end
|
113
|
+
|
114
|
+
####################################################################################
|
115
|
+
## Uploading Project
|
116
|
+
####################################################################################
|
117
|
+
|
118
|
+
def upload_to_testflightapp(opts = {})
|
119
|
+
cmd = "curl #{TESTFLIGHT_ENDPOINT} "
|
120
|
+
cmd << "-F file=@#{Testflight::Config.distribution_file} "
|
121
|
+
cmd << "-F api_token=#{Testflight::Config.api_token} "
|
122
|
+
cmd << "-F team_token=#{Testflight::Config.team_token} "
|
123
|
+
cmd << "-F notify=#{opts[:notify]} "
|
124
|
+
cmd << "-F distribution_lists=#{opts[:teams].join(",")} "
|
125
|
+
cmd << "-F notes='#{opts[:message]}'"
|
126
|
+
execute(cmd, opts)
|
127
|
+
end
|
128
|
+
|
129
|
+
####################################################################################
|
130
|
+
## Project Versioning
|
131
|
+
####################################################################################
|
132
|
+
|
133
|
+
def increment_bundle_version(opts = {})
|
134
|
+
# Check if build numbers are numeric
|
135
|
+
build_number = Testflight::Config.build_number.to_i
|
136
|
+
build_number += 1
|
137
|
+
|
138
|
+
puts("\r\nIncrementing build number to #{build_number}...")
|
139
|
+
|
140
|
+
return if opts[:cold]
|
141
|
+
Testflight::Config.project_info["CFBundleVersion"] = build_number.to_s
|
142
|
+
|
143
|
+
File.open(Testflight::Config.project_info_path, "w") do |f|
|
144
|
+
f.write(Testflight::Config.project_info.to_plist)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
####################################################################################
|
149
|
+
## Git Support
|
150
|
+
####################################################################################
|
151
|
+
def commit_changes(opts = {})
|
152
|
+
execute("git add .", opts.merge(:ignore_result => true))
|
153
|
+
execute("git add . --update", opts.merge(:ignore_result => true))
|
154
|
+
execute("git commit -m '#{opts[:message]}'", opts.merge(:ignore_result => true))
|
155
|
+
execute("git push", opts.merge(:ignore_result => true))
|
156
|
+
end
|
157
|
+
|
158
|
+
def tag_build(opts = {})
|
159
|
+
execute("git tag -a #{Testflight::Config.project_version_short} -m 'Release #{Testflight::Config.project_version}'", opts.merge(:ignore_result => true))
|
160
|
+
end
|
161
|
+
|
162
|
+
####################################################################################
|
163
|
+
## FlightLog Support
|
164
|
+
####################################################################################
|
165
|
+
def append_log_entry(opts)
|
166
|
+
lines = []
|
167
|
+
lines << "#{Testflight::Config.application_name} #{Testflight::Config.project_version}"
|
168
|
+
lines << "Deplyed On: #{Time.now}"
|
169
|
+
lines << "Distributed To: #{opts[:teams].join(", ")}"
|
170
|
+
lines << "Notified By Email: #{opts[:notify]}"
|
171
|
+
lines << "Notes: #{opts[:message]}"
|
172
|
+
lines << "---------------------------------------------------------------------------"
|
173
|
+
|
174
|
+
log = "FLIGHTLOG"
|
175
|
+
tmp = log + "~"
|
176
|
+
|
177
|
+
File.open(tmp, "w") do |newfile|
|
178
|
+
lines.each do |line|
|
179
|
+
newfile.puts(line)
|
180
|
+
end
|
181
|
+
|
182
|
+
if File.exist?(log)
|
183
|
+
File.open(log, "r+") do |oldfile|
|
184
|
+
oldfile.each_line do |line|
|
185
|
+
newfile.puts(line)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
File.delete(log) if File.exist?(log)
|
192
|
+
File.rename(tmp, log)
|
193
|
+
end
|
194
|
+
|
195
|
+
####################################################################################
|
196
|
+
## Command Support
|
197
|
+
####################################################################################
|
198
|
+
def execute(cmd, opts = {})
|
199
|
+
puts("\r\n$ " + cmd)
|
200
|
+
return if opts[:cold]
|
201
|
+
|
202
|
+
result = system(cmd)
|
203
|
+
return if opts[:ignore_result]
|
204
|
+
|
205
|
+
unless result
|
206
|
+
puts("Build failed.")
|
207
|
+
exit 1
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2012 Michael Berkovich
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'thor'
|
25
|
+
|
26
|
+
module Testflight
|
27
|
+
class Cli < Thor
|
28
|
+
include Thor::Actions
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def source_root
|
32
|
+
File.expand_path('../../',__FILE__)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
map 'c' => :checkin
|
37
|
+
map 't' => :takeoff
|
38
|
+
|
39
|
+
desc 'checkin', 'Initializes project configuration file and prepares you for takeoff'
|
40
|
+
def checkin
|
41
|
+
return unless is_project_folder?
|
42
|
+
|
43
|
+
say("\r\nConfiguring #{Testflight::Config.application_name} #{Testflight::Config.type} for deployment to testflightapp.com...")
|
44
|
+
say("Please answer the following questions to prepare you for takeoff.")
|
45
|
+
|
46
|
+
["Provisioning", "Distributions", "build"].each do |name|
|
47
|
+
create_folder(name)
|
48
|
+
end
|
49
|
+
|
50
|
+
@company_name = ask("\r\nWhat is the name of your company, as it appears in your .cer file from Apple?")
|
51
|
+
@should_increment_bundle = yes?("\r\nWould you like to automatically increment build numbers (bundle value) for every deployment? (y/n)")
|
52
|
+
|
53
|
+
if yes?("\r\nDo you use git as your SCM? (y/n)")
|
54
|
+
@should_commit_changes = yes?("\r\nWould you like to commit and push your changes to git as the first step of your deployment? (y/n)")
|
55
|
+
if @should_increment_bundle
|
56
|
+
@should_tag_build = yes?("\r\nWould you like to tag every build in git with the version/build number? (y/n)")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
say("\r\nFor the next question, please get your API TOKEN from the following URL: https://testflightapp.com/account/#api")
|
61
|
+
@api_token = ask("Paste your API TOKEN here:")
|
62
|
+
|
63
|
+
say("\r\nFor the next question, please get your TEAM TOKEN from the following URL: https://testflightapp.com/dashboard/team/edit")
|
64
|
+
@team_token = ask("Paste your TEAM TOKEN here:")
|
65
|
+
|
66
|
+
@teams = ask("\r\nPlease enter your distribution lists as you have set them up on testflight (separate team names with a comma):")
|
67
|
+
@teams = @teams.split(",").collect{|t| t.strip}
|
68
|
+
|
69
|
+
template 'templates/testflight.yml.erb', "./#{Testflight::Config.path}"
|
70
|
+
|
71
|
+
update_git_ignore
|
72
|
+
|
73
|
+
say("\r\nConfiguration file '.testflight' has been created. You can edit it manually or run the init command again.")
|
74
|
+
say("\r\nOnly a few steps left, but make sure you do them all.")
|
75
|
+
say("\r\n1). Copy your Ad Hoc Provisioning Profile (.mobileprovision) into the 'Provisioning' folder.")
|
76
|
+
say("2). Create a new build profile called AdHoc and make sure you set it as the default profile for command line builds.")
|
77
|
+
say("\r\nOnce you are done, you can run: testflight takeoff")
|
78
|
+
end
|
79
|
+
|
80
|
+
desc 'takeoff', 'Builds and deploys your project based on your configuration.'
|
81
|
+
method_option :cold, :type => :boolean, :aliases => "c", :required => false
|
82
|
+
def takeoff
|
83
|
+
return unless ready_for_takeoff?
|
84
|
+
|
85
|
+
say("Preparing #{Testflight::Config.application_name} #{Testflight::Config.type} #{Testflight::Config.project_version} for deployment to testflightapp.com...")
|
86
|
+
|
87
|
+
@message = ask("\r\nWhat changes did you make in this build?")
|
88
|
+
@teams = ask_with_multiple_options("\r\nWhich team(s) would you like to distirbute the build to? Provide team number(s, separated by a comma)",
|
89
|
+
Testflight::Config.distribution_lists)
|
90
|
+
|
91
|
+
@notify = yes?("\r\nWould you like to notify the team members by email about this build? (y/n)")
|
92
|
+
|
93
|
+
Testflight::Builder.new(:message => @message, :teams => @teams, :notify => @notify, :cold => options['cold'])
|
94
|
+
|
95
|
+
say("\r\nCongratulations, the build has been deployed! Have a safe flight!")
|
96
|
+
say("")
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def is_project_folder?
|
102
|
+
unless Testflight::Config.application_name
|
103
|
+
say("This folder does not contain an xCode project or a workspace.")
|
104
|
+
say("Please run this command in the folder where you have the project or workspace you want to deploy.")
|
105
|
+
return false
|
106
|
+
end
|
107
|
+
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
def ready_for_takeoff?
|
112
|
+
return false unless is_project_folder?
|
113
|
+
|
114
|
+
unless File.exists?(Testflight::Config.path)
|
115
|
+
say("Project configuration file does not exist. Please run 'testflight checkin' first.")
|
116
|
+
return false
|
117
|
+
end
|
118
|
+
|
119
|
+
unless Testflight::Config.ad_hoc_provisioning_name
|
120
|
+
say("Please copy your Ad Hoc Provisioning Profile into the Provisioning folder.")
|
121
|
+
return false
|
122
|
+
end
|
123
|
+
|
124
|
+
unless Testflight::Config.project_info_path
|
125
|
+
say("Cannot locate #{Testflight::Config.project_info_file_name} file. Please make sure such file exists in your project.")
|
126
|
+
return false
|
127
|
+
end
|
128
|
+
|
129
|
+
true
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_folder(name)
|
133
|
+
return if File.exist?("#{Dir.pwd}/#{name}")
|
134
|
+
say("Creating folder: #{name}")
|
135
|
+
FileUtils.mkdir(name)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Remove the new files from git commits
|
139
|
+
def update_git_ignore
|
140
|
+
lines = File.exist?('.gitignore') ? File.open('.gitignore').readlines : []
|
141
|
+
|
142
|
+
changed = false
|
143
|
+
['build', 'Distributions', 'Provisioning', '.testflight'].each do |name|
|
144
|
+
next if lines.include?(name)
|
145
|
+
lines << name
|
146
|
+
changed = true
|
147
|
+
end
|
148
|
+
return unless changed
|
149
|
+
|
150
|
+
File.open('.gitignore', "w") do |f|
|
151
|
+
lines.each do |line|
|
152
|
+
f.write(line + "\n")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def ask_for_info(question=nil, prompt="> ", allow_blank=false)
|
158
|
+
say(question) if question
|
159
|
+
|
160
|
+
$stdout.print(prompt)
|
161
|
+
|
162
|
+
$stdin.each_line do |line|
|
163
|
+
value = line.strip.downcase
|
164
|
+
return value if allow_blank
|
165
|
+
return value unless value.empty?
|
166
|
+
$stdout.print(prompt)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def collect_option_selections(opts)
|
171
|
+
lists = []
|
172
|
+
vals = ask_for_info(nil, prompt="? ")
|
173
|
+
vals.split(",").each do |index|
|
174
|
+
index = index.to_i - 1
|
175
|
+
return nil if index<0 or index>=opts.size
|
176
|
+
lists << opts[index]
|
177
|
+
end
|
178
|
+
lists
|
179
|
+
end
|
180
|
+
|
181
|
+
def ask_with_multiple_options(question, opts = [])
|
182
|
+
say(question)
|
183
|
+
opts.each_with_index{ |opt, index| say("\t#{index+1}) #{opt}") }
|
184
|
+
|
185
|
+
vals = collect_option_selections(opts)
|
186
|
+
while vals.nil?
|
187
|
+
say("Invalid selection, please try again.")
|
188
|
+
vals = collect_option_selections(opts)
|
189
|
+
end
|
190
|
+
|
191
|
+
vals
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
end
|
data/lib/testflight/config.rb
CHANGED
@@ -23,7 +23,6 @@
|
|
23
23
|
|
24
24
|
require 'fileutils'
|
25
25
|
require 'yaml'
|
26
|
-
require 'pp'
|
27
26
|
require 'plist'
|
28
27
|
|
29
28
|
module Testflight
|
@@ -33,69 +32,57 @@ module Testflight
|
|
33
32
|
'.testflight'
|
34
33
|
end
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
"developer_name" => "As it appears in your Apple certificate",
|
40
|
-
"increment_bundle" => true,
|
41
|
-
"commit_changes" => true
|
42
|
-
},
|
43
|
-
"testflight" => {
|
44
|
-
"api_token" => "Get it from https://testflightapp.com/account/#api",
|
45
|
-
"team_token" => "Get it from https://testflightapp.com/dashboard/team/edit/",
|
46
|
-
"distribution_lists" => [""]
|
47
|
-
}
|
48
|
-
}
|
49
|
-
end
|
35
|
+
##################################################
|
36
|
+
## Configuration Attributes
|
37
|
+
##################################################
|
50
38
|
|
51
39
|
def self.config
|
52
|
-
@config ||=
|
53
|
-
if File.exist?(path)
|
54
|
-
YAML::load(File.open(path))
|
55
|
-
else
|
56
|
-
File.open(path, "w") do |f|
|
57
|
-
f.write(defaults.to_yaml)
|
58
|
-
end
|
59
|
-
raise "Please update #{path} with all necessary properties."
|
60
|
-
end
|
61
|
-
end
|
40
|
+
@config ||= YAML::load(File.open(path))
|
62
41
|
end
|
63
42
|
|
64
|
-
def self.
|
65
|
-
config["build"]
|
43
|
+
def self.build
|
44
|
+
config["build"]
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.developer_name
|
48
|
+
build["developer_name"]
|
66
49
|
end
|
67
50
|
|
68
51
|
def self.increment_bundle?
|
69
|
-
|
52
|
+
build["increment_bundle"]
|
70
53
|
end
|
71
54
|
|
72
|
-
def self.
|
73
|
-
config["
|
55
|
+
def self.git
|
56
|
+
config["git"]
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.commit_changes?
|
60
|
+
git["commit_changes"]
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.tag_build?
|
64
|
+
git["tag_build"]
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.testflight
|
68
|
+
config["testflight"]
|
74
69
|
end
|
75
70
|
|
76
71
|
def self.distribution_lists
|
77
|
-
|
72
|
+
testflight["distribution_lists"]
|
78
73
|
end
|
79
74
|
|
80
75
|
def self.api_token
|
81
|
-
|
76
|
+
testflight["api_token"]
|
82
77
|
end
|
83
78
|
|
84
79
|
def self.team_token
|
85
|
-
|
80
|
+
testflight["team_token"]
|
86
81
|
end
|
87
82
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
return false if config["build"].nil?
|
92
|
-
return false if config["build"]["developer_name"].nil?
|
93
|
-
return false if config["testflight"].nil?
|
94
|
-
return false if config["testflight"]["api_token"].nil?
|
95
|
-
return false if config["testflight"]["team_token"].nil?
|
96
|
-
return false if config["testflight"]["distribution_lists"].nil?
|
97
|
-
true
|
98
|
-
end
|
83
|
+
##################################################
|
84
|
+
## Helper Methods
|
85
|
+
##################################################
|
99
86
|
|
100
87
|
def self.project_dir
|
101
88
|
Dir.pwd
|
@@ -158,42 +145,16 @@ module Testflight
|
|
158
145
|
end
|
159
146
|
|
160
147
|
def self.distribution_file
|
161
|
-
"#{distributions_dir}/#{application_name}.ipa"
|
148
|
+
"#{distributions_dir}/#{application_name}_#{project_version_short}.ipa"
|
162
149
|
end
|
163
150
|
|
164
|
-
def self.
|
165
|
-
|
166
|
-
pp "This folder does not contain an xCode project or a workspace."
|
167
|
-
exit 1
|
168
|
-
end
|
169
|
-
|
170
|
-
unless valid?
|
171
|
-
pp "Ensure that you have provided all of the information in the #{config_file} config file"
|
172
|
-
exit 1
|
173
|
-
end
|
174
|
-
|
175
|
-
unless project_files.include?("Distributions")
|
176
|
-
FileUtils.mkdir("Distributions")
|
177
|
-
@project_files = nil
|
178
|
-
end
|
179
|
-
|
180
|
-
unless project_files.include?("Provisioning")
|
181
|
-
FileUtils.mkdir("Provisioning")
|
182
|
-
@project_files = nil
|
183
|
-
end
|
184
|
-
|
185
|
-
unless ad_hoc_provisioning_name
|
186
|
-
pp "Please copy your Ad Hoc Provisioning Profile into the provisioning folder."
|
187
|
-
exit 1
|
188
|
-
end
|
151
|
+
def self.project_info_file_name
|
152
|
+
"#{application_name}-Info.plist"
|
189
153
|
end
|
190
154
|
|
191
155
|
def self.project_info_path
|
192
|
-
files = Dir["**/#{
|
193
|
-
if files.empty?
|
194
|
-
pp "Cannot locate #{application_name}-Info.plist file. Please make sure such file exists in your project."
|
195
|
-
exit 1
|
196
|
-
end
|
156
|
+
files = Dir["**/#{project_info_file_name}"]
|
157
|
+
return nil if files.empty?
|
197
158
|
files.first
|
198
159
|
end
|
199
160
|
|
data/lib/testflight/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: testflight
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-12-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.16.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.16.0
|
14
30
|
- !ruby/object:Gem::Dependency
|
15
31
|
name: plist
|
16
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -27,19 +43,22 @@ dependencies:
|
|
27
43
|
- - ~>
|
28
44
|
- !ruby/object:Gem::Version
|
29
45
|
version: 3.1.0
|
30
|
-
description: Mechanism for building, packaging, tagging and deploying
|
46
|
+
description: Mechanism for building, packaging, tagging and deploying xCode projects
|
31
47
|
to testflightapp.com
|
32
48
|
email:
|
33
49
|
- theiceberk@gmail.com
|
34
50
|
executables:
|
35
|
-
-
|
51
|
+
- testflight
|
36
52
|
extensions: []
|
37
53
|
extra_rdoc_files: []
|
38
54
|
files:
|
39
|
-
- bin/
|
55
|
+
- bin/testflight
|
56
|
+
- lib/templates/testflight.yml.erb
|
57
|
+
- lib/testflight/builder.rb
|
58
|
+
- lib/testflight/cli.rb
|
40
59
|
- lib/testflight/config.rb
|
41
|
-
- lib/testflight/prompter.rb
|
42
60
|
- lib/testflight/version.rb
|
61
|
+
- lib/testflight.rb
|
43
62
|
- LICENSE
|
44
63
|
- README.rdoc
|
45
64
|
homepage: https://github.com/berk/testflight
|
data/bin/takeoff
DELETED
@@ -1,182 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'pp'
|
4
|
-
|
5
|
-
require File.expand_path(File.join(File.dirname(__FILE__), "../lib/testflight/config.rb"))
|
6
|
-
require File.expand_path(File.join(File.dirname(__FILE__), "../lib/testflight/prompter.rb"))
|
7
|
-
|
8
|
-
XCODE_BUILDER = "/usr/bin/xcodebuild"
|
9
|
-
XCODE_PACKAGER = "/usr/bin/xcrun"
|
10
|
-
TESTFLIGHT_ENDPOINT = "http://testflightapp.com/api/builds.json"
|
11
|
-
|
12
|
-
# read project definition
|
13
|
-
# read app version and build number
|
14
|
-
# add all files to git and commit files
|
15
|
-
# tag git with the version and build
|
16
|
-
# build files
|
17
|
-
# package files
|
18
|
-
# deploy to testflight app
|
19
|
-
|
20
|
-
####################################################################################
|
21
|
-
## Building Project
|
22
|
-
####################################################################################
|
23
|
-
|
24
|
-
def build_workspace
|
25
|
-
cmd = "#{XCODE_BUILDER} -workspace '#{Testflight::Config.workspace_name}' "
|
26
|
-
cmd << "-scheme '#{Testflight::Config.application_name}' "
|
27
|
-
cmd << "-sdk 'iphoneos6.0' "
|
28
|
-
cmd << "-configuration 'AdHoc' "
|
29
|
-
cmd << "-arch 'armv6 armv7' "
|
30
|
-
cmd << "CONFIGURATION_BUILD_DIR='#{Testflight::Config.build_dir}' "
|
31
|
-
pp "Building Workspace..."
|
32
|
-
pp cmd
|
33
|
-
unless system(cmd)
|
34
|
-
pp "Failed to build workspace"
|
35
|
-
exit 1
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def build_project
|
40
|
-
cmd = "#{XCODE_BUILDER} -target '#{Testflight::Config.application_name}' "
|
41
|
-
cmd << "-sdk 'iphoneos6.0' "
|
42
|
-
cmd << "-configuration 'AdHoc' "
|
43
|
-
|
44
|
-
pp "Building Project..."
|
45
|
-
pp cmd
|
46
|
-
unless system(cmd)
|
47
|
-
pp "Failed to build project"
|
48
|
-
exit 1
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
####################################################################################
|
53
|
-
## Packaging Project
|
54
|
-
####################################################################################
|
55
|
-
|
56
|
-
def package_workspace
|
57
|
-
cmd = "#{XCODE_PACKAGER} -sdk iphoneos PackageApplication "
|
58
|
-
cmd << "-v '#{Testflight::Config.build_dir}/#{Testflight::Config.application_name}.app' "
|
59
|
-
cmd << "-o '#{Testflight::Config.distribution_file}' "
|
60
|
-
cmd << "--sign '#{Testflight::Config.developer_name}' "
|
61
|
-
cmd << "--embed '#{Testflight::Config.provisioning_dir}/#{Testflight::Config.ad_hoc_provisioning_name}'"
|
62
|
-
|
63
|
-
pp "Packaging Workspace..."
|
64
|
-
pp cmd
|
65
|
-
unless system(cmd)
|
66
|
-
pp "Failed to package workspace"
|
67
|
-
exit 1
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def package_project
|
72
|
-
cmd = "#{XCODE_PACKAGER} -sdk iphoneos PackageApplication "
|
73
|
-
cmd << "-v '#{Testflight::Config.build_dir}/AdHoc-iphoneos/#{Testflight::Config.application_name}.app' "
|
74
|
-
cmd << "-o '#{Testflight::Config.distribution_file}' "
|
75
|
-
cmd << "--sign '#{Testflight::Config.developer_name}' "
|
76
|
-
cmd << "--embed '#{Testflight::Config.provisioning_dir}/#{Testflight::Config.ad_hoc_provisioning_name}'"
|
77
|
-
|
78
|
-
pp "Packaging Project..."
|
79
|
-
pp cmd
|
80
|
-
unless system(cmd)
|
81
|
-
pp "Failed to package project"
|
82
|
-
exit 1
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
####################################################################################
|
87
|
-
## Uploading Project
|
88
|
-
####################################################################################
|
89
|
-
|
90
|
-
def upload_to_testflightapp
|
91
|
-
pp "Uploading to TestFlightApp..."
|
92
|
-
|
93
|
-
notes = Testflight::Prompter.read_info("What has changed in this build?")
|
94
|
-
|
95
|
-
lists = Testflight::Prompter.select_set("Which distribution lists would you like to send this build to? Select one or more lists, separated with comma.", Testflight::Config.distribution_lists)
|
96
|
-
|
97
|
-
notify = Testflight::Prompter.read_options("Would you like to notify your team members by email about this build?")
|
98
|
-
["y", "yes"].include?(notify) ? notify = "true" : notify = "false"
|
99
|
-
|
100
|
-
cmd = "curl #{TESTFLIGHT_ENDPOINT} "
|
101
|
-
cmd << "-F file=@#{Testflight::Config.distribution_file} "
|
102
|
-
cmd << "-F api_token=#{Testflight::Config.api_token} "
|
103
|
-
cmd << "-F team_token=#{Testflight::Config.team_token} "
|
104
|
-
cmd << "-F notify=#{notify} "
|
105
|
-
cmd << "-F distribution_lists=#{lists.join(",")} "
|
106
|
-
cmd << "-F notes='#{notes}'"
|
107
|
-
|
108
|
-
pp cmd
|
109
|
-
unless system(cmd)
|
110
|
-
pp "Failed to upload to testflight"
|
111
|
-
exit 1
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
####################################################################################
|
116
|
-
## Project Versioning
|
117
|
-
####################################################################################
|
118
|
-
|
119
|
-
def increment_bundle_version
|
120
|
-
Testflight::Config.project_info["CFBundleVersion"] = (Testflight::Config.build_number.to_i + 1).to_s
|
121
|
-
pp "Incrementing bundle version to #{Testflight::Config.build_number}..."
|
122
|
-
|
123
|
-
File.open(Testflight::Config.project_info_path, "w") do |f|
|
124
|
-
f.write(Testflight::Config.project_info.to_plist)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
####################################################################################
|
129
|
-
## Git Support
|
130
|
-
####################################################################################
|
131
|
-
def update_git_ignore
|
132
|
-
# build
|
133
|
-
# Distributions
|
134
|
-
# Provisioning
|
135
|
-
# .testflight
|
136
|
-
end
|
137
|
-
|
138
|
-
def commit_changes(msg)
|
139
|
-
system("git add .")
|
140
|
-
system("git add . --update")
|
141
|
-
system("git commit -m '#{msg}'")
|
142
|
-
system("git push")
|
143
|
-
|
144
|
-
system("git tag -a #{Testflight::Config.project_version_short} -m 'Release #{Testflight::Config.project_version}'")
|
145
|
-
end
|
146
|
-
|
147
|
-
####################################################################################
|
148
|
-
## Main
|
149
|
-
####################################################################################
|
150
|
-
|
151
|
-
def deploy
|
152
|
-
Testflight::Config.setup
|
153
|
-
|
154
|
-
if Testflight::Config.commit_changes?
|
155
|
-
commit_changes("Preparing build #{Testflight::Config.project_version}")
|
156
|
-
end
|
157
|
-
|
158
|
-
if Testflight::Config.workspace?
|
159
|
-
build_workspace
|
160
|
-
package_workspace
|
161
|
-
else
|
162
|
-
build_project
|
163
|
-
package_project
|
164
|
-
end
|
165
|
-
|
166
|
-
upload_to_testflightapp
|
167
|
-
|
168
|
-
pp ""
|
169
|
-
|
170
|
-
if Testflight::Config.increment_bundle?
|
171
|
-
increment_bundle_version
|
172
|
-
if Testflight::Config.commit_changes?
|
173
|
-
commit_changes("Incrementing build number to #{Testflight::Config.project_version}")
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
pp "Congratulations! The app has been deployed!"
|
178
|
-
end
|
179
|
-
|
180
|
-
deploy
|
181
|
-
|
182
|
-
|
data/lib/testflight/prompter.rb
DELETED
@@ -1,81 +0,0 @@
|
|
1
|
-
#--
|
2
|
-
# Copyright (c) 2012 Michael Berkovich
|
3
|
-
#
|
4
|
-
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
-
# a copy of this software and associated documentation files (the
|
6
|
-
# "Software"), to deal in the Software without restriction, including
|
7
|
-
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
-
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
-
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
-
# the following conditions:
|
11
|
-
#
|
12
|
-
# The above copyright notice and this permission notice shall be
|
13
|
-
# included in all copies or substantial portions of the Software.
|
14
|
-
#
|
15
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
-
#++
|
23
|
-
|
24
|
-
module Testflight
|
25
|
-
class Prompter
|
26
|
-
|
27
|
-
def self.read_options(question=nil, opts=["Yes", "No"], vals=["y", "yes", "n", "no"], joiner = "/")
|
28
|
-
prompt = "(#{opts.join(joiner)})? "
|
29
|
-
pp question if question
|
30
|
-
|
31
|
-
$stdout.print(prompt)
|
32
|
-
|
33
|
-
$stdin.each_line do |line|
|
34
|
-
value = line.strip.downcase
|
35
|
-
return value if vals.include?(value)
|
36
|
-
$stdout.print(prompt)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.read_info(question=nil, prompt="> ", allow_blank=false)
|
41
|
-
pp question if question
|
42
|
-
|
43
|
-
$stdout.print(prompt)
|
44
|
-
|
45
|
-
$stdin.each_line do |line|
|
46
|
-
value = line.strip.downcase
|
47
|
-
return value if allow_blank
|
48
|
-
return value unless value.empty?
|
49
|
-
$stdout.print(prompt)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def self.read_set_values(opts)
|
54
|
-
lists = []
|
55
|
-
vals = read_info(nil, prompt="? ")
|
56
|
-
vals.split(",").each do |index|
|
57
|
-
index = index.to_i - 1
|
58
|
-
return nil if index<0 or index>=opts.size
|
59
|
-
lists << opts[index]
|
60
|
-
end
|
61
|
-
lists
|
62
|
-
end
|
63
|
-
|
64
|
-
def self.select_set(question, opts = [])
|
65
|
-
pp question
|
66
|
-
|
67
|
-
opts.each_with_index do |opt, index|
|
68
|
-
pp " #{index+1}) #{opt}"
|
69
|
-
end
|
70
|
-
|
71
|
-
vals = read_set_values(opts)
|
72
|
-
while vals.nil?
|
73
|
-
pp "Invalid selection, please try again."
|
74
|
-
vals = read_set_values(opts)
|
75
|
-
end
|
76
|
-
|
77
|
-
vals
|
78
|
-
end
|
79
|
-
|
80
|
-
end
|
81
|
-
end
|