roku_builder 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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