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
@@ -0,0 +1,90 @@
1
+ module RokuBuilder
2
+
3
+ # Collects information on a package for submission
4
+ class Inspector < Util
5
+
6
+ # Inspects the given pkg
7
+ # @param pkg [String] Path to the pkg to be inspected
8
+ # @param password [String] Password for the given pkg
9
+ # @return [Hash] Package information. Contains the following keys:
10
+ # * app_name
11
+ # * dev_id
12
+ # * creation_date
13
+ # * dev_zip
14
+ def inspect(pkg:, password:)
15
+
16
+ # upload new key with password
17
+ path = "/plugin_inspect"
18
+ conn = Faraday.new(url: @url) do |f|
19
+ f.request :digest, @dev_username, @dev_password
20
+ f.request :multipart
21
+ f.request :url_encoded
22
+ f.adapter Faraday.default_adapter
23
+ end
24
+ payload = {
25
+ mysubmit: "Inspect",
26
+ passwd: password,
27
+ archive: Faraday::UploadIO.new(pkg, 'application/octet-stream')
28
+ }
29
+ response = conn.post path, payload
30
+
31
+ app_name = /App Name:\s*<\/td>\s*<td>\s*<font[^>]*>([^<]*)<\/font>\s*<\/td>/.match(response.body)
32
+ dev_id = nil
33
+ creation_date = nil
34
+ dev_zip = nil
35
+ if app_name
36
+ app_name = app_name[1]
37
+ dev_id = /Dev ID:\s*<\/td>\s*<td>\s*<font[^>]*>([^<]*)<\/font>\s*<\/td>/.match(response.body)[1]
38
+ creation_date = /Creation Date:\s*<\/td>\s*<td>\s*<font[^>]*>\s*<script[^>]*>\s*var d = new Date\(([^\)]*)\)[^<]*<\/script><\/font>\s*<\/td>/.match(response.body.gsub("\n", ''))[1]
39
+ dev_zip = /dev.zip:\s*<\/td>\s*<td>\s*<font[^>]*>([^<]*)<\/font>\s*<\/td>/.match(response.body)[1]
40
+ else
41
+ app_name = /App Name:[^<]*<div[^>]*>([^<]*)<\/div>/.match(response.body)[1]
42
+ dev_id = /Dev ID:[^<]*<div[^>]*><font[^>]*>([^<]*)<\/font><\/div>/.match(response.body)[1]
43
+ creation_date = /new Date\(([^\/]*)\)/.match(response.body.gsub("\n", ''))[1]
44
+ dev_zip = /dev.zip:[^<]*<div[^>]*><font[^>]*>([^<]*)<\/font><\/div>/.match(response.body)[1]
45
+ end
46
+
47
+ return {app_name: app_name, dev_id: dev_id, creation_date: Time.at(creation_date.to_i).to_s, dev_zip: dev_zip}
48
+
49
+ end
50
+
51
+ # Capture a screencapture for the currently sideloaded app
52
+ # @return [Boolean] Success
53
+ def screencapture(out_folder:, out_file: nil)
54
+ path = "/plugin_inspect"
55
+ conn = Faraday.new(url: @url) do |f|
56
+ f.request :digest, @dev_username, @dev_password
57
+ f.request :multipart
58
+ f.request :url_encoded
59
+ f.adapter Faraday.default_adapter
60
+ end
61
+ payload = {
62
+ mysubmit: "Screenshot",
63
+ passwd: @dev_password,
64
+ archive: Faraday::UploadIO.new("/dev/null", 'application/octet-stream')
65
+ }
66
+ response = conn.post path, payload
67
+
68
+ path = /<img src="([^"]*)">/.match(response.body)
69
+ return false unless path
70
+ path = path[1]
71
+ unless out_file
72
+ out_file = /time=([^"]*)">/.match(response.body)
73
+ out_file = "dev_#{out_file[1]}.jpg" if out_file
74
+ end
75
+
76
+ conn = Faraday.new(url: @url) do |f|
77
+ f.request :digest, @dev_username, @dev_password
78
+ f.adapter Faraday.default_adapter
79
+ end
80
+
81
+ response = conn.get path
82
+
83
+ File.open(File.join(out_folder, out_file), "w") do |io|
84
+ io.write(response.body)
85
+ end
86
+ @logger.info "Screen captured to #{File.join(out_folder, out_file)}"
87
+ return response.success?
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,52 @@
1
+ module RokuBuilder
2
+
3
+ # Change or get dev key
4
+ class Keyer < Util
5
+
6
+ # Sets the key on the roku device
7
+ # @param keyed_pkg [String] Path for a package signed with the desired key
8
+ # @param password [String] Password for the package
9
+ # @return [Boolean] True if key changed, false otherwise
10
+ def rekey(keyed_pkg:, password:)
11
+ oldId = dev_id
12
+
13
+ # upload new key with password
14
+ path = "/plugin_inspect"
15
+ conn = Faraday.new(url: @url) do |f|
16
+ f.request :digest, @dev_username, @dev_password
17
+ f.request :multipart
18
+ f.request :url_encoded
19
+ f.adapter Faraday.default_adapter
20
+ end
21
+ payload = {
22
+ mysubmit: "Rekey",
23
+ passwd: password,
24
+ archive: Faraday::UploadIO.new(keyed_pkg, 'application/octet-stream')
25
+ }
26
+ response = conn.post path, payload
27
+
28
+ # check key
29
+ newId = dev_id
30
+ newId != oldId
31
+ end
32
+
33
+ # Get the current dev id
34
+ # @return [String] The current dev id
35
+ def dev_id
36
+ path = "/plugin_package"
37
+ conn = Faraday.new(url: @url) do |f|
38
+ f.request :digest, @dev_username, @dev_password
39
+ f.adapter Faraday.default_adapter
40
+ end
41
+ response = conn.get path
42
+
43
+ dev_id = /Your Dev ID:\s*<font[^>]*>([^<]*)<\/font>/.match(response.body)
44
+ if dev_id
45
+ dev_id = dev_id[1]
46
+ else
47
+ dev_id = /Your Dev ID:[^>]*<\/label> ([^<]*)/.match(response.body)[1]
48
+ end
49
+ dev_id
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ module RokuBuilder
2
+
3
+ # Launch application, sending parameters
4
+ class Linker < Util
5
+ # Deeplink to the currently sideloaded app
6
+ # @param options [String] Options string
7
+ # @note Options string should be formated like the following: "<key>:<value>[, <key>:<value>]*"
8
+ # @note Any options will be accepted and sent to the app
9
+ def link(options:)
10
+ path = "/launch/dev"
11
+ payload = {}
12
+ return false unless options
13
+ opts = options.split(/,\s*/)
14
+ opts.each do |opt|
15
+ opt = opt.split(":")
16
+ key = opt.shift.to_sym
17
+ value = opt.join(":")
18
+ payload[key] = value
19
+ end
20
+
21
+ unless payload.keys.count > 0
22
+ return false
23
+ end
24
+
25
+ path = "#{path}?#{parameterize(payload)}"
26
+ conn = Faraday.new(url: "#{@url}:8060") do |f|
27
+ f.request :digest, @dev_username, @dev_password
28
+ f.request :multipart
29
+ f.request :url_encoded
30
+ f.adapter Faraday.default_adapter
31
+ end
32
+
33
+ response = conn.post path
34
+ return response.success?
35
+ end
36
+
37
+ private
38
+
39
+ # Parameterize options to be sent to the app
40
+ # @param params [Hash] Parameters to be sent
41
+ # @return [String] Parameters as a string, URI escaped
42
+ def parameterize(params)
43
+ URI.escape(params.collect{|k,v| "#{k}=#{URI.escape(v, "?&")}"}.join('&'))
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,197 @@
1
+ module RokuBuilder
2
+
3
+ # Load/Unload/Build roku applications
4
+ class Loader < Util
5
+
6
+ # Sideload an app onto a roku device
7
+ # @param root_dir [String] Path to the root directory of the roku app
8
+ # @param branch [String] Branch of the git repository to sideload. Pass nil to use working directory. Default: nil
9
+ # @param update_manifest [Boolean] Flag to update the manifest file before sideloading. Default: false
10
+ # @param fetch [Boolean] Flag to fetch all remotes before sideloading. Default: false
11
+ # @param folders [Array<String>] Array of folders to be sideloaded. Pass nil to send all folders. Default: nil
12
+ # @param files [Array<String>] Array of files to be sideloaded. Pass nil to send all files. Default: nil
13
+ # @return [String] Build version on success, nil otherwise
14
+ def sideload(root_dir:, branch: nil, update_manifest: false, fetch: false, folders: nil, files: nil)
15
+ @root_dir = root_dir
16
+ result = nil
17
+ stash = nil
18
+ if branch
19
+ git = Git.open(@root_dir)
20
+ if fetch
21
+ for remote in git.remotes
22
+ git.fetch(remote)
23
+ end
24
+ end
25
+ end
26
+ current_dir = Dir.pwd
27
+ begin
28
+ if git and branch and branch != git.current_branch
29
+ Dir.chdir(@root_dir)
30
+ current_branch = git.current_branch
31
+ stash = git.branch.stashes.save("roku-builder-temp-stash")
32
+ git.checkout(branch)
33
+ end
34
+
35
+ # Update manifest
36
+ build_version = ""
37
+ if update_manifest
38
+ build_version = ManifestManager.update_build(root_dir: root_dir, logger: @logger)
39
+ else
40
+ build_version = ManifestManager.build_version(root_dir: root_dir, logger: @logger)
41
+ end
42
+
43
+ outfile = build(root_dir: root_dir, branch: branch, build_version: build_version, folders: folders, files: files)
44
+
45
+ path = "/plugin_install"
46
+
47
+ # Connect to roku and upload file
48
+ conn = Faraday.new(url: @url) do |f|
49
+ f.request :digest, @dev_username, @dev_password
50
+ f.request :multipart
51
+ f.request :url_encoded
52
+ f.adapter Faraday.default_adapter
53
+ end
54
+ payload = {
55
+ mysubmit: "Replace",
56
+ archive: Faraday::UploadIO.new(outfile, 'application/zip')
57
+ }
58
+ response = conn.post path, payload
59
+
60
+ # Cleanup
61
+ File.delete(outfile)
62
+
63
+ if git and current_branch
64
+ git.checkout(current_branch)
65
+ git.branch.stashes.apply if stash
66
+ end
67
+
68
+ if response.status == 200 and response.body =~ /Install Success/
69
+ result = build_version
70
+ end
71
+
72
+ rescue Git::GitExecuteError => e
73
+ @logger.error "Branch or ref does not exist"
74
+ @logger.error e.message
75
+ @logger.error e.backtrace
76
+ ensure
77
+ Dir.chdir(current_dir) unless current_dir == Dir.pwd
78
+ end
79
+ result
80
+ end
81
+
82
+
83
+ # Build an app to sideload later
84
+ # @param root_dir [String] Path to the root directory of the roku app
85
+ # @param branch [String] Branch of the git repository to sideload. Pass nil to use working directory. Default: nil
86
+ # @param build_version [String] Version to assigne to the build. If nil will pull the build version form the manifest. Default: nil
87
+ # @param outfile [String] Path for the output file. If nil will create a file in /tmp. Default: nil
88
+ # @param fetch [Boolean] Flag to fetch all remotes before sideloading. Default: false
89
+ # @param folders [Array<String>] Array of folders to be sideloaded. Pass nil to send all folders. Default: nil
90
+ # @param files [Array<String>] Array of files to be sideloaded. Pass nil to send all files. Default: nil
91
+ # @return [String] Path of the build
92
+ def build(root_dir:, branch: nil, build_version: nil, outfile: nil, fetch: false, folders: nil, files: nil)
93
+ @root_dir = root_dir
94
+ result = nil
95
+ stash = nil
96
+ if branch
97
+ git = Git.open(@root_dir)
98
+ if fetch
99
+ for remote in git.remotes
100
+ git.fetch(remote)
101
+ end
102
+ end
103
+ end
104
+ current_dir = Dir.pwd
105
+ begin
106
+ if git and branch and branch != git.current_branch
107
+ Dir.chdir(@root_dir)
108
+ current_branch = git.current_branch
109
+ stash = git.branch.stashes.save("roku-builder-temp-stash")
110
+ git.checkout(branch)
111
+ end
112
+
113
+ build_version = ManifestManager.build_version(root_dir: root_dir, logger: @logger) unless build_version
114
+ unless folders
115
+ folders = Dir.entries(root_dir).select {|entry| File.directory? File.join(root_dir, entry) and !(entry =='.' || entry == '..') }
116
+ end
117
+ unless files
118
+ files = Dir.entries(root_dir).select {|entry| File.file? File.join(root_dir, entry)}
119
+ end
120
+ outfile = "/tmp/build_#{build_version}.zip" unless outfile
121
+
122
+ File.delete(outfile) if File.exists?(outfile)
123
+ io = Zip::File.open(outfile, Zip::File::CREATE)
124
+
125
+ # Add folders to zip
126
+ folders.each do |folder|
127
+ base_folder = File.join(@root_dir, folder)
128
+ entries = Dir.entries(base_folder)
129
+ entries.delete(".")
130
+ entries.delete("..")
131
+ writeEntries(@root_dir, entries, folder, io)
132
+ end
133
+
134
+ # Add file to zip
135
+ writeEntries(@root_dir, files, "", io)
136
+
137
+ io.close()
138
+
139
+ if git and current_branch
140
+ git.checkout(current_branch)
141
+ git.branch.stashes.apply if stash
142
+ end
143
+ rescue Git::GitExecuteError => e
144
+ @logger.error "Branch or ref does not exist"
145
+ @logger.error e.message
146
+ @logger.error e.backtrace
147
+ ensure
148
+ Dir.chdir(current_dir) unless current_dir == Dir.pwd
149
+ end
150
+ outfile
151
+ end
152
+
153
+ # Remove the currently sideloaded app
154
+ def unload()
155
+ path = "/plugin_install"
156
+
157
+ # Connect to roku and upload file
158
+ conn = Faraday.new(url: @url) do |f|
159
+ f.headers['Content-Type'] = Faraday::Request::Multipart.mime_type
160
+ f.request :digest, @dev_username, @dev_password
161
+ f.request :multipart
162
+ f.request :url_encoded
163
+ f.adapter Faraday.default_adapter
164
+ end
165
+ payload = {
166
+ mysubmit: "Delete",
167
+ archive: ""
168
+ }
169
+ response = conn.post path, payload
170
+ if response.status == 200 and response.body =~ /Install Success/
171
+ return true
172
+ end
173
+ return false
174
+ end
175
+
176
+ private
177
+
178
+ # Recursively write directory contents to a zip archive
179
+ # @param root_dir [String] Path of the root directory
180
+ # @param entries [Array<String>] Array of file paths of files/directories to store in the zip archive
181
+ # @param path [String] The path of the current directory starting at the root directory
182
+ # @param io [IO] zip IO object
183
+ def writeEntries(root_dir, entries, path, io)
184
+ entries.each { |e|
185
+ zipFilePath = path == "" ? e : File.join(path, e)
186
+ diskFilePath = File.join(root_dir, zipFilePath)
187
+ if File.directory?(diskFilePath)
188
+ io.mkdir(zipFilePath)
189
+ subdir =Dir.entries(diskFilePath); subdir.delete("."); subdir.delete("..")
190
+ writeEntries(root_dir, subdir, zipFilePath, io)
191
+ else
192
+ io.get_output_stream(zipFilePath) { |f| f.puts(File.open(diskFilePath, "rb").read()) }
193
+ end
194
+ }
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,63 @@
1
+ module RokuBuilder
2
+
3
+ # Updates or retrives build version
4
+ class ManifestManager
5
+
6
+ # Updates the build version in the manifest file
7
+ # @param root_dir [String] Path to the root directory for the app
8
+ # @return [String] Build version on success, empty string otherwise
9
+ def self.update_build(root_dir:, logger:)
10
+
11
+ build_version = ""
12
+
13
+ temp_file = Tempfile.new('manifest')
14
+ path = File.join(root_dir, 'manifest')
15
+ begin
16
+ File.open(path, 'r') do |file|
17
+ file.each_line do |line|
18
+ if line.include?("build_version")
19
+
20
+ #Update build version.
21
+ build_version = line.split(".")
22
+ iteration = 0
23
+ if 2 == build_version.length
24
+ iteration = build_version[1].to_i + 1
25
+ build_version[0] = Time.now.strftime("%m%d%y")
26
+ build_version[1] = iteration
27
+ build_version = build_version.join(".")
28
+ else
29
+ #Use current date.
30
+ build_version = Time.now.strftime("%m%d%y")+".1"
31
+ end
32
+ temp_file.puts "build_version=#{build_version}"
33
+ else
34
+ temp_file.puts line
35
+ end
36
+ end
37
+ end
38
+ temp_file.rewind
39
+ FileUtils.cp(temp_file.path, path)
40
+ ensure
41
+ temp_file.close
42
+ temp_file.unlink
43
+ end
44
+ build_version
45
+ end
46
+
47
+ # Retrive the build version from the manifest file
48
+ # @param root_dir [String] Path to the root directory for the app
49
+ # @return [String] Build version on success, empty string otherwise
50
+ def self.build_version(root_dir:, logger:)
51
+ path = File.join(root_dir, 'manifest')
52
+ build_version = ""
53
+ File.open(path, 'r') do |file|
54
+ file.each_line do |line|
55
+ if line.include?("build_version")
56
+ build_version = line.split("=")[1].chomp
57
+ end
58
+ end
59
+ end
60
+ build_version
61
+ end
62
+ end
63
+ end