testflight 0.1.2 → 0.1.4
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.
- 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
|