roku_builder 3.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +101 -0
  5. data/Guardfile +21 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +282 -0
  8. data/bin/roku +152 -0
  9. data/config.json.example +28 -0
  10. data/lib/roku_builder.rb +32 -0
  11. data/lib/roku_builder/config_manager.rb +157 -0
  12. data/lib/roku_builder/controller.rb +582 -0
  13. data/lib/roku_builder/inspector.rb +90 -0
  14. data/lib/roku_builder/keyer.rb +52 -0
  15. data/lib/roku_builder/linker.rb +46 -0
  16. data/lib/roku_builder/loader.rb +197 -0
  17. data/lib/roku_builder/manifest_manager.rb +63 -0
  18. data/lib/roku_builder/monitor.rb +62 -0
  19. data/lib/roku_builder/navigator.rb +107 -0
  20. data/lib/roku_builder/packager.rb +47 -0
  21. data/lib/roku_builder/tester.rb +32 -0
  22. data/lib/roku_builder/util.rb +31 -0
  23. data/lib/roku_builder/version.rb +4 -0
  24. data/rakefile +8 -0
  25. data/roku_builder.gemspec +36 -0
  26. data/tests/roku_builder/config_manager_test.rb +400 -0
  27. data/tests/roku_builder/controller_test.rb +250 -0
  28. data/tests/roku_builder/inspector_test.rb +153 -0
  29. data/tests/roku_builder/keyer_test.rb +88 -0
  30. data/tests/roku_builder/linker_test.rb +37 -0
  31. data/tests/roku_builder/loader_test.rb +153 -0
  32. data/tests/roku_builder/manifest_manager_test.rb +25 -0
  33. data/tests/roku_builder/monitor_test.rb +34 -0
  34. data/tests/roku_builder/navigator_test.rb +72 -0
  35. data/tests/roku_builder/packager_test.rb +125 -0
  36. data/tests/roku_builder/test_files/controller_test/load_config_test.json +28 -0
  37. data/tests/roku_builder/test_files/controller_test/valid_config.json +28 -0
  38. data/tests/roku_builder/test_files/loader_test/c +0 -0
  39. data/tests/roku_builder/test_files/loader_test/manifest +0 -0
  40. data/tests/roku_builder/test_files/loader_test/source/a +0 -0
  41. data/tests/roku_builder/test_files/loader_test/source/b +0 -0
  42. data/tests/roku_builder/test_files/manifest_manager_test/manifest_template +2 -0
  43. data/tests/roku_builder/test_helper.rb +6 -0
  44. data/tests/roku_builder/tester_test.rb +33 -0
  45. data/tests/roku_builder/util_test.rb +23 -0
  46. metadata +286 -0
data/bin/roku ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
4
+
5
+ #require "byebug"
6
+ require "roku_builder"
7
+ require "optparse"
8
+ require "pathname"
9
+
10
+ options = {}
11
+ options[:config] = '~/.roku_config.json'
12
+ options[:stage] = 'production'
13
+ options[:update_manifest] = false
14
+ options[:fetch] = false
15
+
16
+ OptionParser.new do |opts|
17
+ opts.banner = "Usage: roku <command> [options]"
18
+
19
+ opts.on("-l", "--sideload", "Command: Sideload an app") do |s|
20
+ options[:sideload] = s
21
+ end
22
+
23
+ opts.on("-p", "--package", "Command: Package an app") do |p|
24
+ options[:package] = p
25
+ end
26
+
27
+ opts.on("-t", "--test", "Command: Test an app") do |t|
28
+ options[:test] = t
29
+ end
30
+
31
+ opts.on("-L", "--deeplink", "Command: Deeplink into app. Requires mgid and type options.") do |d|
32
+ options[:deeplink] = d
33
+ end
34
+
35
+ opts.on("--configure", "Command: Copy base configuration file to the --config location. Default: '~/.roku_config.json'") do |c|
36
+ options[:configure] = c
37
+ end
38
+
39
+ opts.on("--validate", "Command: Validate configuration'") do |v|
40
+ options[:validate] = v
41
+ end
42
+
43
+ opts.on("-d", "--delete", "Command: Delete the currently sideloaded app") do |d|
44
+ options[:delete] = d
45
+ end
46
+
47
+ opts.on("-N", "--navigate CMD", "Command: send the given command to the roku") do |n|
48
+ options[:navigate] = n
49
+ end
50
+
51
+ opts.on("-S", "--screencapture", "Command: save a screencapture to the output file/folder") do |s|
52
+ options[:screencapture] = s
53
+ end
54
+
55
+ opts.on("-y", "--type TEXT", "Command: type the given text on the roku device") do |t|
56
+ options[:text] = t
57
+ end
58
+
59
+ opts.on("-b", "--build", "Command: build a zip to be sideloaded") do |b|
60
+ options[:build] = b
61
+ end
62
+
63
+ opts.on("--screen SCREEN", "Command: show a screen") do |s|
64
+ options[:screen] = s
65
+ end
66
+
67
+ opts.on("--screens", "Command: show possible screens") do |s|
68
+ options[:screens] = s
69
+ end
70
+
71
+ opts.on("-m", "--monitor TYPE", "Command: run telnet to monitor roku log") do |m|
72
+ options[:monitor] = m
73
+ end
74
+
75
+ opts.on("-u", "--update-manifest", "Command: update the manifest file") do |u|
76
+ options[:update] = u
77
+ end
78
+
79
+ opts.on("-r", "--ref REF", "Git referance to use for sideloading") do |r|
80
+ options[:ref] = r
81
+ end
82
+
83
+ opts.on("-w", "--working", "Use working directory to sideload or test") do |w|
84
+ options[:working] = w
85
+ end
86
+
87
+ opts.on("-c", "--current", "Use current directory to sideload or test. Overides any project config") do |w|
88
+ options[:current] = true
89
+ end
90
+
91
+ opts.on("-s", "--stage STAGE", "Set the stage to use. Default: 'production'") do |b|
92
+ options[:stage] = b
93
+ options[:set_stage] = true
94
+ end
95
+
96
+ opts.on("-M", "--manifest-update", "Update the manifest file while packaging") do |n|
97
+ options[:update_manifest] = true
98
+ end
99
+
100
+ opts.on("-i", "--inspect", "Print inspection information while packaging") do |n|
101
+ options[:inspect] = true
102
+ end
103
+
104
+ opts.on("-f", "--fetch", "Preform a `git fetch --all` on the repository before building or sideloading.") do
105
+ options[:fetch] = true
106
+ end
107
+
108
+ opts.on("-o", "--deeplink-options TYPE", "Additional deeplink options. (eg. a:b, c:d,e:f)") do |o|
109
+ options[:deeplink_options] = o
110
+ end
111
+
112
+ opts.on("-e", "--edit PARAMS", "Edit config params when configuring. (eg. a:b, c:d,e:f)") do |p|
113
+ options[:edit_params] = p
114
+ end
115
+
116
+ opts.on("--config CONFIG", "Set a custom config file. Default: '~/.roku_config.rb'") do |c|
117
+ options[:config] = c
118
+ end
119
+
120
+ opts.on("-D", "--device ID", "Use a different device corresponding to the given ID") do |d|
121
+ options[:device] = d
122
+ options[:device_given] = true
123
+ end
124
+
125
+ opts.on("-P", "--project ID", "Use a different project") do |p|
126
+ options[:project] = p
127
+ end
128
+
129
+ opts.on("-O", "--out PATH", "Output file/folder. If PATH ends in .pkg/.zip/.jpg, file is assumed, otherwise folder is assumed") do |o|
130
+ options[:out] = o
131
+ end
132
+
133
+ opts.on("-V", "--verbose", "Print Info message") do |v|
134
+ options[:verbose] = v
135
+ end
136
+
137
+ opts.on("--debug", "Print Debug messages") do |d|
138
+ options[:debug] = d
139
+ end
140
+
141
+ opts.on("-h", "--help", "Show this message") do |h|
142
+ puts opts
143
+ exit
144
+ end
145
+
146
+ opts.on("-v", "--version", "Show version") do
147
+ puts RokuBuilder::VERSION
148
+ exit
149
+ end
150
+ end.parse!
151
+
152
+ RokuBuilder::Controller.run(options: options)
@@ -0,0 +1,28 @@
1
+ {
2
+ "devices": {
3
+ "default": "roku",
4
+ "roku": {
5
+ "ip": "xxx.xxx.xxx.xxx",
6
+ "user": "<username>",
7
+ "password": "<password>"
8
+ }
9
+ },
10
+ "projects": {
11
+ "default": "<project id>",
12
+ "<project id>": {
13
+ "directory": "<path/to/repo>",
14
+ "folders": ["resources","source"],
15
+ "files": ["manifest"],
16
+ "app_name": "<app name>",
17
+ "stages":{
18
+ "production": {
19
+ "branch": "production",
20
+ "key": {
21
+ "keyed_pkg": "<path/to/signed/pkg>",
22
+ "password": "<password for pkg>"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,32 @@
1
+ require "logger"
2
+ require "faraday"
3
+ require "faraday/digestauth"
4
+ #controller
5
+ require "net/ping"
6
+ #loader
7
+ require "net/telnet"
8
+ require "fileutils"
9
+ require "tempfile"
10
+ require "zip"
11
+ require "git"
12
+ #config_manager
13
+ require 'json'
14
+
15
+ require "roku_builder/controller"
16
+ require "roku_builder/util"
17
+ require "roku_builder/keyer"
18
+ require "roku_builder/inspector"
19
+ require "roku_builder/loader"
20
+ require "roku_builder/packager"
21
+ require "roku_builder/linker"
22
+ require "roku_builder/tester"
23
+ require "roku_builder/manifest_manager"
24
+ require "roku_builder/config_manager"
25
+ require "roku_builder/navigator"
26
+ require "roku_builder/monitor"
27
+ require "roku_builder/version"
28
+
29
+ # Wrapping module for the Roku Builder Gem
30
+ module RokuBuilder
31
+ # For documentation
32
+ end
@@ -0,0 +1,157 @@
1
+ module RokuBuilder
2
+
3
+ MISSING_DEVICES = 1
4
+ MISSING_DEVICES_DEFAULT = 2
5
+ DEVICE_DEFAULT_BAD = 3
6
+ MISSING_PROJECTS = 4
7
+ MISSING_PROJECTS_DEFAULT = 5
8
+ PROJECTS_DEFAULT_BAD = 6
9
+ DEVICE_MISSING_IP = 7
10
+ DEVICE_MISSING_USER = 8
11
+ DEVICE_MISSING_PASSWORD = 9
12
+ PROJECT_MISSING_APP_NAME = 10
13
+ PROJECT_MISSING_DIRECTORY = 11
14
+ PROJECT_MISSING_FOLDERS = 12
15
+ PROJECT_FOLDERS_BAD = 13
16
+ PROJECT_MISSING_FILES = 14
17
+ PROJECT_FILES_BAD = 15
18
+ STAGE_MISSING_BRANCH = 16
19
+
20
+ # Load and validate config files.
21
+ class ConfigManager
22
+
23
+ # Loads the roku config from file
24
+ # @param config [String] path for the roku config
25
+ # @return [Hash] roku config object
26
+ def self.get_config(config:, logger:)
27
+ begin
28
+ config = JSON.parse(File.open(config).read, {symbolize_names: true})
29
+ config[:devices][:default] = config[:devices][:default].to_sym
30
+ config[:projects][:default] = config[:projects][:default].to_sym
31
+ config
32
+ rescue JSON::ParserError
33
+ logger.fatal "Config file is not valid JSON"
34
+ nil
35
+ end
36
+ end
37
+
38
+ # Validates the roku config
39
+ # @param config [Hash] roku config object
40
+ # @return [Array] error codes for valid config (see self.error_codes)
41
+ def self.validate_config(config:, logger:)
42
+ codes = []
43
+ codes.push(MISSING_DEVICES) if not config[:devices]
44
+ codes.push(MISSING_DEVICES_DEFAULT) if config[:devices] and not config[:devices][:default]
45
+ codes.push(DEVICE_DEFAULT_BAD) if config[:devices] and config[:devices][:default] and not config[:devices][:default].is_a?(Symbol)
46
+ codes.push(MISSING_PROJECTS) if not config[:projects]
47
+ codes.push(MISSING_PROJECTS_DEFAULT) if config[:projects] and not config[:projects][:default]
48
+ codes.push(MISSING_PROJECTS_DEFAULT) if config[:projects] and config[:projects][:default] == "<project id>".to_sym
49
+ codes.push(PROJECTS_DEFAULT_BAD) if config[:projects] and config[:projects][:default] and not config[:projects][:default].is_a?(Symbol)
50
+ if config[:devices]
51
+ config[:devices].each {|k,v|
52
+ next if k == :default
53
+ codes.push(DEVICE_MISSING_IP) if not v[:ip]
54
+ codes.push(DEVICE_MISSING_IP) if v[:ip] == "xxx.xxx.xxx.xxx"
55
+ codes.push(DEVICE_MISSING_IP) if v[:ip] == ""
56
+ codes.push(DEVICE_MISSING_USER) if not v[:user]
57
+ codes.push(DEVICE_MISSING_USER) if v[:user] == "<username>"
58
+ codes.push(DEVICE_MISSING_USER) if v[:user] == ""
59
+ codes.push(DEVICE_MISSING_PASSWORD) if not v[:password]
60
+ codes.push(DEVICE_MISSING_PASSWORD) if v[:password] == "<password>"
61
+ codes.push(DEVICE_MISSING_PASSWORD) if v[:password] == ""
62
+ }
63
+ end
64
+ if config[:projects]
65
+ config[:projects].each {|k,v|
66
+ next if k == :default
67
+ codes.push(PROJECT_MISSING_APP_NAME) if not v[:app_name]
68
+ codes.push(PROJECT_MISSING_DIRECTORY) if not v[:directory]
69
+ codes.push(PROJECT_MISSING_FOLDERS) if not v[:folders]
70
+ codes.push(PROJECT_FOLDERS_BAD) if v[:folders] and not v[:folders].is_a?(Array)
71
+ codes.push(PROJECT_MISSING_FILES) if not v[:files]
72
+ codes.push(PROJECT_FILES_BAD) if v[:files] and not v[:files].is_a?(Array)
73
+ v[:stages].each {|k,v|
74
+ codes.push(STAGE_MISSING_BRANCH) if not v[:branch]
75
+ }
76
+ }
77
+ end
78
+ codes.push(0) if codes.empty?
79
+ codes
80
+ end
81
+
82
+ # Error code messages for config validation
83
+ # @return [Array] error code messages
84
+ def self.error_codes()
85
+ [
86
+ #===============FATAL ERRORS===============#
87
+ "Valid Config.",
88
+ "Devices config is missing.",
89
+ "Devices default is missing.",
90
+ "Devices default is not a hash.",
91
+ "Projects config is missing.",
92
+ "Projects default is missing.", #5
93
+ "Projects default is not a hash.",
94
+ "A device config is missing its IP address.",
95
+ "A device config is missing its username.",
96
+ "A device config is missing its password.",
97
+ "A project config is missing its app_name.", #10
98
+ "A project config is missing its directorty.",
99
+ "A project config is missing its folders.",
100
+ "A project config's folders is not an array.",
101
+ "A project config is missing its files.",
102
+ "A project config's files is not an array.", #15
103
+ "A project stage is missing its branch."
104
+ #===============WARNINGS===============#
105
+ ]
106
+ end
107
+
108
+ # Edit the roku config
109
+ # @param config [String] path for the roku config
110
+ # @param options [String] options to set in the config
111
+ # @param device [String] which device to use
112
+ # @param project [String] which project to use
113
+ # @param stage[String] which stage to use
114
+ # @return [Boolean] success
115
+ def self.edit_config(config:, options:, device:, project:, stage:, logger:)
116
+ config_object = get_config(config: config, logger: logger)
117
+ return false unless config_object
118
+ unless project
119
+ project = config_object[:projects][:default]
120
+ else
121
+ project = project.to_sym
122
+ end
123
+ unless device
124
+ device = config_object[:devices][:default]
125
+ else
126
+ device = device.to_sym
127
+ end
128
+ unless stage
129
+ stage = :production
130
+ else
131
+ stage = stage.to_sym
132
+ end
133
+ changes = {}
134
+ opts = options.split(/,\s*/)
135
+ opts.each do |opt|
136
+ opt = opt.split(":")
137
+ key = opt.shift.to_sym
138
+ value = opt.join(":")
139
+ changes[key] = value
140
+ end
141
+ changes.each {|key,value|
142
+ if [:ip, :user, :password].include?(key)
143
+ config_object[:devices][device][key] = value
144
+ elsif [:directory, :app_name].include?(key) #:folders, :files
145
+ config_object[:projects][project][key] = value
146
+ elsif [:branch]
147
+ config_object[:projects][project][:stages][stage][key] = value
148
+ end
149
+ }
150
+ config_string = JSON.pretty_generate(config_object)
151
+ file = File.open(config, "w")
152
+ file.write(config_string)
153
+ file.close
154
+ return true
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,582 @@
1
+ module RokuBuilder
2
+
3
+ # Controls all interaction with other classes
4
+ class Controller
5
+
6
+
7
+
8
+ ### Validation Codes ###
9
+
10
+ # Valid Options
11
+ VALID = 0
12
+
13
+ # Too many commands given
14
+ EXTRA_COMMANDS = 1
15
+
16
+ # No commands given
17
+ NO_COMMANDS = 2
18
+
19
+ # Too many source options given
20
+ EXTRA_SOURCES = 3
21
+
22
+ # No source options given
23
+ NO_SOURCE = 4
24
+
25
+ # Incorrect use of current option
26
+ BAD_CURRENT = 5
27
+
28
+ # No deeplink options supplied for deeplink
29
+ BAD_DEEPLINK = 6
30
+
31
+
32
+
33
+ ### Device Codes ###
34
+
35
+ # The default device is offline switched to a secondary device
36
+ CHANGED_DEVICE = -1
37
+
38
+ # Device is online
39
+ GOOD_DEVCICE = 0
40
+
41
+ # User defined device was not online
42
+ BAD_DEVICE = 1
43
+
44
+ # No configured devices were online
45
+ NO_DEVICES = 2
46
+
47
+
48
+
49
+ ### Run Codes ###
50
+
51
+ # Config has deplicated options
52
+ DEPRICATED_CONFIG = -1
53
+
54
+ # Valid config
55
+ SUCCESS = 0
56
+
57
+ # Tring to overwrite existing config file
58
+ CONFIG_OVERWRITE = 1
59
+
60
+ # Missing config file
61
+ MISSING_CONFIG = 2
62
+
63
+ # Invalid config file
64
+ INVALID_CONFIG = 3
65
+
66
+ # Missing manifest file
67
+ MISSING_MANIFEST = 4
68
+
69
+ # Unknow device given
70
+ UNKNOWN_DEVICE = 5
71
+
72
+ # Unknown project given
73
+ UNKNOWN_PROJECT = 6
74
+
75
+ # Unknown stage given
76
+ UNKNOWN_STAGE = 7
77
+
78
+ # Failed to sideload app
79
+ FAILED_SIDELOAD = 8
80
+
81
+ # Failed to sign app
82
+ FAILED_SIGNING = 9
83
+
84
+ # Failed to deeplink to app
85
+ FAILED_DEEPLINKING = 10
86
+
87
+ # Failed to send navigation command
88
+ FAILED_NAVIGATING = 11
89
+
90
+ # Failed to capture screen
91
+ FAILED_SCREENCAPTURE = 12
92
+
93
+ # Run the builder
94
+ # @param options [Hash] The options hash
95
+ def self.run(options:)
96
+ logger = Logger.new(STDOUT)
97
+ logger.formatter = proc {|severity, datetime, progname, msg|
98
+ "[%s #%s] %5s: %s\n\r" % [datetime.strftime("%Y-%m-%d %H:%M:%S.%4N"), $$, severity, msg]
99
+ }
100
+ if options[:debug]
101
+ logger.level = Logger::DEBUG
102
+ elsif options[:verbose]
103
+ logger.level = Logger::INFO
104
+ else
105
+ logger.level = Logger::WARN
106
+ end
107
+
108
+
109
+ options_code = self.validate_options(options: options, logger: logger)
110
+
111
+ self.handle_error_codes(options_code: options_code, logger: logger)
112
+
113
+ handle_code = self.handle_options(options: options, logger: logger)
114
+
115
+ self.handle_error_codes(handle_code: handle_code, logger: logger)
116
+ end
117
+
118
+ protected
119
+
120
+ # Validates the commands
121
+ # @param options [Hash] The options hash
122
+ # @return [Integer] Status code for command validation
123
+ # @param logger [Logger] system logger
124
+ def self.validate_options(options:, logger:)
125
+ commands = options.keys & self.commands
126
+ return EXTRA_COMMANDS if commands.count > 1
127
+ return NO_COMMANDS if commands.count < 1
128
+ sources = options.keys & self.sources
129
+ return EXTRA_SOURCES if sources.count > 1
130
+ if (options.keys & self.source_commands).count == 1
131
+ return NO_SOURCE unless sources.count == 1
132
+ end
133
+ if sources.include?(:current)
134
+ return BAD_CURRENT unless options[:build] or options[:sideload]
135
+ end
136
+ if options[:deeplink]
137
+ return BAD_DEEPLINK if !options[:deeplink_options] or options[:deeplink_options].chomp == ""
138
+ end
139
+ return VALID
140
+ end
141
+
142
+ # Run commands
143
+ # @param options [Hash] The options hash
144
+ # @return [Integer] Return code for options handeling
145
+ # @param logger [Logger] system logger
146
+ def self.handle_options(options:, logger:)
147
+ if options[:configure]
148
+ return configure(options: options, logger: logger)
149
+ end
150
+ code, config, configs = self.load_config(options: options, logger: logger)
151
+ return code if code != SUCCESS
152
+
153
+ # Check devices
154
+ device_code, configs = self.check_devices(options: options, config: config, configs: configs, logger: logger)
155
+ self.handle_error_codes(device_code: device_code, logger: logger)
156
+
157
+ command = (self.commands & options.keys).first
158
+ case command
159
+ when :validate
160
+ # Do Nothing #
161
+ when :sideload
162
+ ### Sideload App ###
163
+ loader = Loader.new(**configs[:device_config])
164
+ success = loader.sideload(**configs[:sideload_config])
165
+ return FAILED_SIDELOAD unless success
166
+ when :package
167
+ ### Package App ###
168
+ keyer = Keyer.new(**configs[:device_config])
169
+ loader = Loader.new(**configs[:device_config])
170
+ packager = Packager.new(**configs[:device_config])
171
+ inspector = Inspector.new(**configs[:device_config])
172
+ logger.warn "Packaging working directory" if options[:working]
173
+ # Sideload #
174
+ build_version = loader.sideload(**configs[:sideload_config])
175
+ return FAILED_SIGNING unless build_version
176
+ # Key #
177
+ success = keyer.rekey(**configs[:key])
178
+ logger.info "Key did not change" unless success
179
+ # Package #
180
+ options[:build_version] = build_version
181
+ configs = self.update_configs(configs: configs, options: options)
182
+ success = packager.package(**configs[:package_config])
183
+ logger.info "Signing Successful: #{configs[:package_config][:out_file]}" if success
184
+ return FAILED_SIGNING unless success
185
+ # Inspect #
186
+ if options[:inspect]
187
+ info = inspector.inspect(configs[:inspect_config])
188
+ logger.unknown "App Name: #{info[:app_name]}"
189
+ logger.unknown "Dev ID: #{info[:dev_id]}"
190
+ logger.unknown "Creation Date: #{info[:creation_date]}"
191
+ logger.unknown "dev.zip: #{info[:dev_zip]}"
192
+ end
193
+ when :build
194
+ ### Build ###
195
+ loader = Loader.new(**configs[:device_config])
196
+ build_version = ManifestManager.build_version(**configs[:manifest_config], logger: logger)
197
+ options[:build_version] = build_version
198
+ configs = self.update_configs(configs: configs, options: options)
199
+ outfile = loader.build(**configs[:build_config])
200
+ logger.info "Build: #{outfile}"
201
+ when :update
202
+ ### Update ###
203
+ old_version = ManifestManager.build_version(**configs[:manifest_config])
204
+ new_version = ManifestManager.update_build(**configs[:manifest_config])
205
+ logger.info "Update build version from:\n#{old_version}\nto:\n#{new_version}"
206
+ when :deeplink
207
+ ### Deeplink ###
208
+ linker = Linker.new(**configs[:device_config])
209
+ success = linker.link(**configs[:deeplink_config])
210
+ return FAILED_DEEPLINKING unless success
211
+ when :delete
212
+ loader = Loader.new(**configs[:device_config])
213
+ loader.unload()
214
+ when :monitor
215
+ monitor = Monitor.new(**configs[:device_config])
216
+ monitor.monitor(**configs[:monitor_config])
217
+ when :navigate
218
+ navigator = Navigator.new(**configs[:device_config])
219
+ success = navigator.nav(**configs[:navigate_config])
220
+ return FAILED_NAVIGATING unless success
221
+ when :screen
222
+ navigator = Navigator.new(**configs[:device_config])
223
+ success = navigator.screen(**configs[:screen_config])
224
+ return FAILED_NAVIGATING unless success
225
+ when :screens
226
+ navigator = Navigator.new(**configs[:device_config])
227
+ navigator.screens
228
+ when :text
229
+ navigator = Navigator.new(**configs[:device_config])
230
+ navigator.type(**configs[:text_config])
231
+ when :test
232
+ tester = Tester.new(**configs[:device_config])
233
+ tester.run_tests(**configs[:test_config])
234
+ when :screencapture
235
+ inspector = Inspector.new(**configs[:device_config])
236
+ success = inspector.screencapture(**configs[:screencapture_config])
237
+ return FAILED_SCREENCAPTURE unless success
238
+ end
239
+ return SUCCESS
240
+ end
241
+
242
+ # Ensure that the selected device is accessable
243
+ # @param options [Hash] The options hash
244
+ # @param logger [Logger] system logger
245
+ def self.check_devices(options:, config:, configs:, logger:)
246
+ ping = Net::Ping::External.new
247
+ host = configs[:device_config][:ip]
248
+ return [GOOD_DEVCICE, configs] if ping.ping? host, 1, 0.2, 1
249
+ return [BAD_DEVICE, nil] if options[:device_given]
250
+ config[:devices].each_pair {|key, value|
251
+ unless key == :default
252
+ host = value[:ip]
253
+ if ping.ping? host, 1, 0.2, 1
254
+ configs[:device_config] = value
255
+ configs[:device_config][:logger] = logger
256
+ return [CHANGED_DEVICE, configs]
257
+ end
258
+ end
259
+ }
260
+ return [NO_DEVICES, nil]
261
+ end
262
+
263
+ # List of command options
264
+ # @return [Array<Symbol>] List of command symbols that can be used in the options hash
265
+ def self.commands
266
+ [:sideload, :package, :test, :deeplink,:configure, :validate, :delete,
267
+ :navigate, :text, :build, :monitor, :update, :screencapture, :screen,
268
+ :screens]
269
+ end
270
+
271
+ # List of source options
272
+ # @return [Array<Symbol>] List of source symbols that can be used in the options hash
273
+ def self.sources
274
+ [:ref, :set_stage, :working, :current]
275
+ end
276
+
277
+ # List of commands requiring a source option
278
+ # @return [Array<Symbol>] List of command symbols that require a source in the options hash
279
+ def self.source_commands
280
+ [:sideload, :package, :test, :build]
281
+ end
282
+
283
+ # Handle error codes
284
+ # @param options_code [Integer] the error code returned by validate_options
285
+ # @param handle_code [Integer] the error code returned by handle_options
286
+ # @param logger [Logger] system logger
287
+ def self.handle_error_codes(options_code: nil, device_code: nil, handle_code: nil, logger:)
288
+ if options_code
289
+ case options_code
290
+ when EXTRA_COMMANDS
291
+ logger.fatal "Only one command is allowed"
292
+ abort
293
+ when NO_COMMANDS
294
+ logger.fatal "At least one command is required"
295
+ abort
296
+ when EXTRA_SOURCES
297
+ logger.fatal "Only use one of --ref, --working, --current or --stage"
298
+ abort
299
+ when NO_SOURCE
300
+ logger.fatal "Must use at least one of --ref, --working, --current or --stage"
301
+ abort
302
+ when BAD_CURRENT
303
+ logger.fatal "Can only sideload or build 'current' directory"
304
+ abort
305
+ when BAD_DEEPLINK
306
+ logger.fatal "Must supply deeplinking options when deeplinking"
307
+ abort
308
+ end
309
+ elsif device_code
310
+ case device_code
311
+ when CHANGED_DEVICE
312
+ logger.info "The default device was not online so a secondary device is being used"
313
+ when BAD_DEVICE
314
+ logger.fatal "The selected device was not online"
315
+ abort
316
+ when NO_DEVICES
317
+ logger.fatal "No configured devices were found"
318
+ abort
319
+ end
320
+ elsif handle_code
321
+ case handle_code
322
+ when DEPRICATED_CONFIG
323
+ logger.warn 'Depricated config. See Above'
324
+ when CONFIG_OVERWRITE
325
+ logger.fatal 'Config already exists. To create default please remove config first.'
326
+ abort
327
+ when MISSING_CONFIG
328
+ logger.fatal "Missing config file: #{options[:config]}"
329
+ abort
330
+ when INVALID_CONFIG
331
+ logger.fatal 'Invalid config. See Above'
332
+ abort
333
+ when MISSING_MANIFEST
334
+ logger.fatal 'Manifest file missing'
335
+ abort
336
+ when UNKNOWN_DEVICE
337
+ logger.fatal "Unkown device id"
338
+ abort
339
+ when UNKNOWN_PROJECT
340
+ logger.fatal "Unknown project id"
341
+ abort
342
+ when UNKNOWN_STAGE
343
+ logger.fatal "Unknown stage"
344
+ abort
345
+ when FAILED_SIDELOAD
346
+ logger.fatal "Failed Sideloading App"
347
+ abort
348
+ when FAILED_SIGNING
349
+ logger.fatal "Failed Signing App"
350
+ abort
351
+ when FAILED_DEEPLINKING
352
+ logger.fatal "Failed Deeplinking To App"
353
+ abort
354
+ when FAILED_NAVIGATING
355
+ logger.fatal "Command not sent"
356
+ abort
357
+ when FAILED_SCREENCAPTURE
358
+ logger.fatal "Failed to Capture Screen"
359
+ abort
360
+ end
361
+ end
362
+ end
363
+
364
+ # Configure the gem
365
+ # @param options [Hash] The options hash
366
+ # @return [Integer] Success or failure code
367
+ # @param logger [Logger] system logger
368
+ def self.configure(options:, logger:)
369
+ source_config = File.expand_path(File.join(File.dirname(__FILE__), "..", '..', 'config.json.example'))
370
+ target_config = File.expand_path(options[:config])
371
+ if File.exist?(target_config)
372
+ unless options[:edit_params]
373
+ return CONFIG_OVERWRITE
374
+ end
375
+ else
376
+ ### Copy Config File ###
377
+ FileUtils.copy(source_config, target_config)
378
+ end
379
+ if options[:edit_params]
380
+ ConfigManager.edit_config(config: target_config, options: options[:edit_params], device: options[:device], project: options[:project], stage: options[:stage], logger: logger)
381
+ end
382
+ return SUCCESS
383
+ end
384
+
385
+ # Load config file and generate intermeidate configs
386
+ # @param options [Hash] The options hash
387
+ # @param logger [Logger] system logger
388
+ # @return [Integer] Return code
389
+ # @return [Hash] Loaded config
390
+ # @return [Hash] Intermeidate configs
391
+ def self.load_config(options:, logger:)
392
+ config_file = File.expand_path(options[:config])
393
+ return MISSING_CONFIG unless File.exists?(config_file)
394
+ code = SUCCESS
395
+ config = ConfigManager.get_config(config: config_file, logger: logger)
396
+ return INVALID_CONFIG unless config
397
+ configs = {}
398
+ codes = ConfigManager.validate_config(config: config, logger: logger)
399
+ fatal = false
400
+ warning = false
401
+ codes.each {|code|
402
+ if code > 0
403
+ logger.fatal "Invalid Config: "+ ConfigManager.error_codes()[code]
404
+ fatal = true
405
+ elsif code < 0
406
+ logger.warn "Depricated Config: "+ ConfigManager.error_codes()[code]
407
+ warning = true
408
+ elsif code == 0 and options[:validate]
409
+ logger.info "Config Valid"
410
+ end
411
+ }
412
+ return [INVALID_CONFIG, nil, nil] if fatal
413
+ code = DEPRICATED_CONFIG if warning
414
+
415
+ #set device
416
+ unless options[:device]
417
+ options[:device] = config[:devices][:default]
418
+ end
419
+ #set project
420
+ if options[:current] or not options[:project]
421
+ path = self.system(command: "pwd")
422
+ project = nil
423
+ config[:projects].each_pair {|key,value|
424
+ if value.is_a?(Hash)
425
+ repo_path = Pathname.new(value[:directory]).realdirpath.to_s
426
+ if path.start_with?(repo_path)
427
+ project = key
428
+ break
429
+ end
430
+ end
431
+ }
432
+ if project
433
+ options[:project] = project
434
+ else
435
+ options[:project] = config[:projects][:default]
436
+ end
437
+ end
438
+ #set outfile
439
+ options[:out_folder] = nil
440
+ options[:out_file] = nil
441
+ if options[:out]
442
+ if options[:out].end_with?(".zip") or options[:out].end_with?(".pkg") or options[:out].end_with?(".jpg")
443
+ options[:out_folder], options[:out_file] = Pathname.new(options[:out]).split.map{|p| p.to_s}
444
+ else
445
+ options[:out_folder] = options[:out]
446
+ end
447
+ end
448
+ unless options[:out_folder]
449
+ options[:out_folder] = "/tmp"
450
+ end
451
+
452
+ # Create Device Config
453
+ configs[:device_config] = config[:devices][options[:device].to_sym]
454
+ return [UNKNOWN_DEVICE, nil, nil] unless configs[:device_config]
455
+ configs[:device_config][:logger] = logger
456
+
457
+ #Create Project Config
458
+ project_config = {}
459
+ if options[:current]
460
+ pwd = self.system(command: "pwd")
461
+ return [MISSING_MANIFEST, nil, nil] unless File.exist?(File.join(pwd, "manifest"))
462
+ project_config = {
463
+ directory: pwd,
464
+ folders: nil,
465
+ files: nil,
466
+ stages: { production: { branch: nil } }
467
+ }
468
+ else
469
+ project_config = config[:projects][options[:project].to_sym]
470
+ end
471
+ return [UNKNOWN_PROJECT, nil, nil] unless project_config
472
+ configs[:project_config] = project_config
473
+ stage = options[:stage].to_sym
474
+ return [UNKNOWN_STAGE, nil, nil] unless project_config[:stages][stage]
475
+ configs[:stage] = stage
476
+
477
+ root_dir = project_config[:directory]
478
+ branch = project_config[:stages][stage][:branch]
479
+ branch = options[:ref] if options[:ref]
480
+ branch = nil if options[:current]
481
+ branch = nil if options[:working]
482
+
483
+ # Create Sideload Config
484
+ configs[:sideload_config] = {
485
+ root_dir: root_dir,
486
+ branch: branch,
487
+ update_manifest: options[:update_manifest],
488
+ fetch: options[:fetch],
489
+ folders: project_config[:folders],
490
+ files: project_config[:files]
491
+ }
492
+ if options[:package]
493
+ # Create Key Config
494
+ configs[:key] = project_config[:stages][stage][:key]
495
+ # Create Package Config
496
+ configs[:package_config] = {
497
+ password: configs[:key][:password],
498
+ app_name_version: "#{project_config[:app_name]} - #{stage}"
499
+ }
500
+ if options[:outfile]
501
+ configs[:package_config][:out_file] = File.join(options[:out_folder], options[:out_file])
502
+ end
503
+ # Create Inspector Config
504
+ configs[:inspect_config] = {
505
+ pkg: configs[:package_config][:out_file],
506
+ password: configs[:key][:password]
507
+ }
508
+ end if
509
+ # Create Build Config
510
+ configs[:build_config] = {
511
+ root_dir: root_dir,
512
+ branch: branch,
513
+ fetch: options[:fetch],
514
+ folders: project_config[:folders],
515
+ files: project_config[:files]
516
+ }
517
+ # Create Manifest Config
518
+ configs[:manifest_config] = {
519
+ root_dir: project_config[:directory]
520
+ }
521
+ # Create Deeplink Config
522
+ configs[:deeplink_config] ={
523
+ options: options[:deeplink_options]
524
+ }
525
+ # Create Monitor Config
526
+ if options[:monitor]
527
+ configs[:monitor_config] = {
528
+ type: options[:monitor].to_sym
529
+ }
530
+ end
531
+ # Create Navigate Config
532
+ if options[:navigate]
533
+ configs[:navigate_config] = {
534
+ command: options[:navigate].to_sym
535
+ }
536
+ end
537
+ # Create Text Config
538
+ configs[:text_config] = {
539
+ text: options[:text]
540
+ }
541
+ # Create Test Config
542
+ configs[:test_config] = {
543
+ sideload_config: configs[:sideload_config]
544
+ }
545
+ #Create screencapture config
546
+ configs[:screencapture_config] = {
547
+ out_folder: options[:out_folder],
548
+ out_file: options[:out_file]
549
+ }
550
+
551
+ if options[:screen]
552
+ configs[:screen_config] = {
553
+ type: options[:screen].to_sym
554
+ }
555
+ end
556
+ return [code, config, configs]
557
+ end
558
+
559
+ # Update the intermeidate configs
560
+ # @param configs [Hash] Intermeidate configs hash
561
+ # @param options [Hash] Options hash
562
+ # @return [Hash] New intermeidate configs hash
563
+ def self.update_configs(configs:, options:)
564
+ if options[:build_version]
565
+ configs[:package_config][:app_name_version] = "#{configs[:project_config][:app_name]} - #{configs[:stage]} - #{options[:build_version]}" if configs[:package_config]
566
+ unless options[:outfile]
567
+ configs[:package_config][:out_file] = File.join(options[:out_folder], "#{configs[:project_config][:app_name]}_#{configs[:stage]}_#{options[:build_version]}.pkg") if configs[:package_config]
568
+ configs[:build_config][:outfile] = File.join(options[:out_folder], "#{configs[:project_config][:app_name]}_#{configs[:stage]}_#{options[:build_version]}.zip") if configs[:build_config]
569
+ configs[:inspect_config][:pkg] = configs[:package_config][:out_file] if configs[:inspect_config] and configs[:package_config]
570
+ end
571
+ end
572
+ return configs
573
+ end
574
+
575
+ # Run a system command
576
+ # @param command [String] The command to be run
577
+ # @return [String] The output of the command
578
+ def self.system(command:)
579
+ `#{command}`.chomp
580
+ end
581
+ end
582
+ end