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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +101 -0
- data/Guardfile +21 -0
- data/LICENSE.txt +22 -0
- data/README.md +282 -0
- data/bin/roku +152 -0
- data/config.json.example +28 -0
- data/lib/roku_builder.rb +32 -0
- data/lib/roku_builder/config_manager.rb +157 -0
- data/lib/roku_builder/controller.rb +582 -0
- data/lib/roku_builder/inspector.rb +90 -0
- data/lib/roku_builder/keyer.rb +52 -0
- data/lib/roku_builder/linker.rb +46 -0
- data/lib/roku_builder/loader.rb +197 -0
- data/lib/roku_builder/manifest_manager.rb +63 -0
- data/lib/roku_builder/monitor.rb +62 -0
- data/lib/roku_builder/navigator.rb +107 -0
- data/lib/roku_builder/packager.rb +47 -0
- data/lib/roku_builder/tester.rb +32 -0
- data/lib/roku_builder/util.rb +31 -0
- data/lib/roku_builder/version.rb +4 -0
- data/rakefile +8 -0
- data/roku_builder.gemspec +36 -0
- data/tests/roku_builder/config_manager_test.rb +400 -0
- data/tests/roku_builder/controller_test.rb +250 -0
- data/tests/roku_builder/inspector_test.rb +153 -0
- data/tests/roku_builder/keyer_test.rb +88 -0
- data/tests/roku_builder/linker_test.rb +37 -0
- data/tests/roku_builder/loader_test.rb +153 -0
- data/tests/roku_builder/manifest_manager_test.rb +25 -0
- data/tests/roku_builder/monitor_test.rb +34 -0
- data/tests/roku_builder/navigator_test.rb +72 -0
- data/tests/roku_builder/packager_test.rb +125 -0
- data/tests/roku_builder/test_files/controller_test/load_config_test.json +28 -0
- data/tests/roku_builder/test_files/controller_test/valid_config.json +28 -0
- data/tests/roku_builder/test_files/loader_test/c +0 -0
- data/tests/roku_builder/test_files/loader_test/manifest +0 -0
- data/tests/roku_builder/test_files/loader_test/source/a +0 -0
- data/tests/roku_builder/test_files/loader_test/source/b +0 -0
- data/tests/roku_builder/test_files/manifest_manager_test/manifest_template +2 -0
- data/tests/roku_builder/test_helper.rb +6 -0
- data/tests/roku_builder/tester_test.rb +33 -0
- data/tests/roku_builder/util_test.rb +23 -0
- 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)
|
data/config.json.example
ADDED
@@ -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
|
+
}
|
data/lib/roku_builder.rb
ADDED
@@ -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
|