rpremote 0.6.0 → 0.7.0

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.
@@ -9,12 +9,13 @@ module Rpremote
9
9
  class Mrbgems
10
10
  DEFAULT_PATH = "Mrbgems"
11
11
  DEFAULT_LOCK_PATH = "Mrbgems.lock"
12
- LOCK_VERSION = 1
12
+ LOCK_VERSION = 2
13
13
  GITHUB_PATTERN = %r{\A[\w.-]+/[\w.-]+\z}
14
14
  COMMIT_PATTERN = /\A[0-9a-f]{40,64}\z/i
15
+ GROUP_PATTERN = /\A[a-zA-Z0-9][a-zA-Z0-9_-]*\z/
15
16
  VMS = %i[mruby mrubyc].freeze
16
17
 
17
- Dependency = Data.define(:type, :source, :branch, :commit, :path, :require_name, :auto_require)
18
+ Dependency = Data.define(:type, :source, :branch, :commit, :path, :require_name, :auto_require, :groups)
18
19
  Overlay = Data.define(:path, :fingerprint)
19
20
 
20
21
  class Error < Rpremote::Error; end
@@ -25,10 +26,30 @@ module Rpremote
25
26
 
26
27
  def initialize(path: DEFAULT_PATH, lock_path: nil, cwd: Dir.pwd, resolver: nil)
27
28
  @path = File.expand_path(path, cwd)
28
- @lock_path = File.expand_path(lock_path || DEFAULT_LOCK_PATH, File.dirname(@path))
29
+ @lock_path = if lock_path
30
+ File.expand_path(lock_path, cwd)
31
+ else
32
+ File.expand_path(DEFAULT_LOCK_PATH, File.dirname(@path))
33
+ end
29
34
  @resolver = resolver || method(:resolve_github)
30
35
  end
31
36
 
37
+ def self.parse_groups(value)
38
+ groups = value.to_s.split(",", -1).map(&:strip)
39
+ raise DefinitionError, "invalid mrbgem group: #{value.inspect}" if groups.empty?
40
+
41
+ normalize_groups(groups)
42
+ end
43
+
44
+ def self.normalize_groups(values)
45
+ Array(values).map do |name|
46
+ value = name.to_s
47
+ raise DefinitionError, "invalid mrbgem group: #{name.inspect}" unless GROUP_PATTERN.match?(value)
48
+
49
+ value.to_sym
50
+ end.uniq.freeze
51
+ end
52
+
32
53
  def exist?
33
54
  File.file?(path)
34
55
  end
@@ -37,6 +58,18 @@ module Rpremote
37
58
  @dependencies ||= load_definition
38
59
  end
39
60
 
61
+ def selected_dependencies(with: [], without: [])
62
+ selected_groups = self.class.normalize_groups(with)
63
+ excluded_groups = self.class.normalize_groups(without)
64
+ dependencies
65
+ available = @definition.groups
66
+ validate_selected_groups!(selected_groups + excluded_groups, available)
67
+ dependencies.select do |dependency|
68
+ dependency.groups.empty? ||
69
+ (dependency.groups.intersect?(selected_groups) && !dependency.groups.intersect?(excluded_groups))
70
+ end
71
+ end
72
+
40
73
  def vm
41
74
  dependencies
42
75
  @definition.vm_name
@@ -47,50 +80,36 @@ module Rpremote
47
80
  dependencies
48
81
  end
49
82
 
50
- def lock(update: false)
51
- check
52
- previous = update ? nil : read_lock(required: false)
53
- entries = dependencies.map { |dependency| lock_entry(dependency, previous) }
54
- contents = { "version" => LOCK_VERSION, "vm" => vm&.to_s, "gems" => entries }.compact
55
- write_json(lock_path, contents)
56
- contents
57
- end
58
-
59
- def read_lock(required: true)
60
- contents = JSON.parse(File.read(lock_path))
83
+ def lock(update: false, with: [], without: [])
84
+ selected_groups = self.class.normalize_groups(with)
85
+ excluded_groups = self.class.normalize_groups(without)
86
+ previous = update ? nil : read_previous_lock
87
+ selected = selected_dependencies(with: selected_groups, without: excluded_groups)
88
+ selected.each { |dependency| validate_local!(dependency) if dependency.type == :path }
89
+ entries = selected.map do |dependency|
90
+ lock_entry(dependency, previous)
91
+ end
92
+ contents = {
93
+ "version" => LOCK_VERSION,
94
+ "vm" => vm&.to_s,
95
+ "with" => selected_groups.map(&:to_s),
96
+ "without" => excluded_groups.map(&:to_s),
97
+ "gems" => entries
98
+ }.compact
61
99
  validate_lock!(contents)
100
+ write_json(lock_path, contents)
62
101
  contents
63
- rescue Errno::ENOENT
64
- raise LockError, "mrbgems lock file does not exist: #{lock_path}" if required
65
-
66
- nil
67
- rescue JSON::ParserError => e
68
- raise LockError, "invalid mrbgems lock file #{lock_path}: #{e.message}"
69
- end
70
-
71
- def require_names
72
- lock_data = read_lock(required: false)
73
- return [] unless lock_data
74
-
75
- lock_data.fetch("gems").filter_map do |gem|
76
- gem["require_name"] if gem.fetch("auto_require", true)
77
- end.uniq
78
- end
79
-
80
- def prepend_requires(source)
81
- names = require_names
82
- return source if names.empty?
83
-
84
- names.map { |name| "require #{name.inspect}\n" }.join.b + source.b
85
102
  end
86
103
 
87
- def generate_overlay(base_config:, target:, directory:, update: false)
88
- lock_data = lock(update: update)
104
+ def generate_overlay(base_config:, target:, directory:)
105
+ lock_data = read_lock
106
+ entries = lock_data.fetch("gems")
107
+ validate_locked_paths!(entries)
89
108
  fingerprint = build_fingerprint(base_config, lock_data)
90
109
  output_dir = File.join(File.expand_path(directory), fingerprint)
91
110
  output_path = File.join(output_dir, "build_config.rb")
92
111
  FileUtils.mkdir_p(output_dir)
93
- write_file(output_path, overlay_source(base_config, target, lock_data.fetch("gems")))
112
+ write_file(output_path, overlay_source(base_config, target, entries))
94
113
  Overlay.new(path: output_path, fingerprint: fingerprint)
95
114
  end
96
115
 
@@ -128,7 +147,7 @@ module Rpremote
128
147
  "branch" => dependency.branch, "commit" => validate_commit!(commit),
129
148
  "require_name" => dependency.require_name }.compact
130
149
  else
131
- entry = { "type" => "path", "source" => dependency.source,
150
+ entry = { "type" => "path", "source" => locked_path(dependency.path),
132
151
  "sha256" => digest_directory(dependency.path),
133
152
  "require_name" => dependency.require_name || local_require_name(dependency.path) }.compact
134
153
  end
@@ -136,6 +155,7 @@ module Rpremote
136
155
  entry.delete("require_name")
137
156
  entry["auto_require"] = false
138
157
  end
158
+ entry["groups"] = dependency.groups.map(&:to_s) unless dependency.groups.empty?
139
159
  entry
140
160
  end
141
161
 
@@ -149,6 +169,26 @@ module Rpremote
149
169
  entry&.fetch("commit", nil)
150
170
  end
151
171
 
172
+ def read_previous_lock
173
+ contents = JSON.parse(File.read(lock_path))
174
+ raise LockError, "invalid mrbgems lock file: #{lock_path}" unless contents.is_a?(Hash)
175
+
176
+ version = contents["version"]
177
+ supported = [1, LOCK_VERSION].include?(version) && contents["gems"].is_a?(Array)
178
+ raise LockError, "unsupported mrbgems lock format: #{lock_path}" unless supported
179
+
180
+ if version == LOCK_VERSION
181
+ validate_lock!(contents)
182
+ elsif contents["gems"].any? { |entry| !entry.is_a?(Hash) }
183
+ raise LockError, "invalid mrbgems lock file: #{lock_path}"
184
+ end
185
+ contents
186
+ rescue Errno::ENOENT
187
+ nil
188
+ rescue JSON::ParserError => e
189
+ raise LockError, "invalid mrbgems lock file #{lock_path}: #{e.message}"
190
+ end
191
+
152
192
  def validate_commit!(commit)
153
193
  value = commit.to_s.downcase
154
194
  raise LockError, "invalid Git commit: #{commit.inspect}" unless COMMIT_PATTERN.match?(value)
@@ -190,41 +230,6 @@ module Rpremote
190
230
  relative.split(File::SEPARATOR).intersect?(%w[.git build tmp])
191
231
  end
192
232
 
193
- def validate_lock!(contents)
194
- unless contents.is_a?(Hash) && contents["version"] == LOCK_VERSION && contents["gems"].is_a?(Array)
195
- raise LockError, "unsupported mrbgems lock format: #{lock_path}"
196
- end
197
- unless contents["vm"].nil? || VMS.map(&:to_s).include?(contents["vm"])
198
- raise LockError, "invalid mrbgems VM in lock file: #{contents["vm"].inspect}"
199
- end
200
-
201
- contents["gems"].each do |entry|
202
- type = entry["type"]
203
- valid = type == "github" ? valid_github_lock?(entry) : valid_path_lock?(entry)
204
- raise LockError, "invalid mrbgems lock entry: #{entry.inspect}" unless valid
205
- end
206
- end
207
-
208
- def valid_github_lock?(entry)
209
- GITHUB_PATTERN.match?(entry["source"].to_s) && !entry["branch"].to_s.empty? &&
210
- COMMIT_PATTERN.match?(entry["commit"].to_s) && valid_require_name?(entry["require_name"]) &&
211
- valid_auto_require?(entry)
212
- end
213
-
214
- def valid_path_lock?(entry)
215
- entry["type"] == "path" && !entry["source"].to_s.empty? &&
216
- /\A[0-9a-f]{64}\z/.match?(entry["sha256"].to_s) && valid_require_name?(entry["require_name"]) &&
217
- valid_auto_require?(entry)
218
- end
219
-
220
- def valid_require_name?(name)
221
- name.nil? || (name.is_a?(String) && !name.empty? && !name.match?(/[\x00-\x1f\x7f]/))
222
- end
223
-
224
- def valid_auto_require?(entry)
225
- !entry.key?("auto_require") || entry["auto_require"] == true || entry["auto_require"] == false
226
- end
227
-
228
233
  def local_require_name(directory)
229
234
  source = File.read(File.join(directory, "mrbgem.rake"))
230
235
  match = source.match(/^\s*spec\.require_name\s*=\s*["']([^"']+)["']\s*$/)
@@ -233,7 +238,7 @@ module Rpremote
233
238
 
234
239
  def build_fingerprint(base_config, lock_data)
235
240
  Digest::SHA256.hexdigest(
236
- [File.binread(path), File.binread(base_config), JSON.generate(lock_data)].join("\0")
241
+ [File.binread(base_config), JSON.generate(lock_data)].join("\0")
237
242
  ).slice(0, 12)
238
243
  end
239
244
 
@@ -245,93 +250,28 @@ module Rpremote
245
250
  "load #{File.expand_path(base_config).inspect}",
246
251
  "conf = MRuby.targets.fetch(#{target.inspect})"
247
252
  ]
248
- dependencies.zip(locked_entries).each do |dependency, locked|
249
- lines << if dependency.type == :github
250
- "conf.gem github: #{dependency.source.inspect}, " \
251
- "branch: #{dependency.branch.inspect}, checksum_hash: #{locked.fetch("commit").inspect}"
253
+ locked_entries.each do |locked|
254
+ lines << if locked.fetch("type") == "github"
255
+ "conf.gem github: #{locked.fetch("source").inspect}, " \
256
+ "branch: #{locked.fetch("branch").inspect}, checksum_hash: #{locked.fetch("commit").inspect}"
252
257
  else
253
- "conf.gem #{dependency.path.inspect}"
258
+ "conf.gem #{resolved_locked_path(locked).inspect}"
254
259
  end
255
260
  end
256
261
  "#{lines.join("\n")}\n"
257
262
  end
258
263
 
259
- def write_json(filename, contents)
260
- write_file(filename, "#{JSON.pretty_generate(contents)}\n")
261
- end
262
-
263
- def write_file(filename, contents)
264
- return if File.file?(filename) && File.binread(filename) == contents
265
-
266
- FileUtils.mkdir_p(File.dirname(filename))
267
- temporary = "#{filename}.tmp-#{Process.pid}"
268
- File.binwrite(temporary, contents)
269
- File.rename(temporary, filename)
270
- ensure
271
- FileUtils.rm_f(temporary) if temporary
264
+ def locked_path(directory)
265
+ Pathname.new(directory).relative_path_from(Pathname.new(File.dirname(lock_path))).to_s
272
266
  end
273
267
 
274
- class Definition
275
- attr_reader :dependencies, :vm_name
276
-
277
- def initialize(filename)
278
- @directory = File.dirname(filename)
279
- @dependencies = []
280
- end
281
-
282
- def vm(name)
283
- value = name.to_sym
284
- raise DefinitionError, "unsupported mrbgems VM: #{name.inspect}" unless VMS.include?(value)
285
- raise DefinitionError, "mrbgems VM specified more than once" if vm_name
286
-
287
- @vm_name = value
288
- end
289
-
290
- def gem(github: nil, path: nil, branch: "main", commit: nil, require: nil, auto_require: true)
291
- sources = [github, path].compact
292
- raise DefinitionError, "gem requires exactly one of github or path" unless sources.length == 1
293
- unless require.nil? || (require.is_a?(String) && !require.empty? && !require.match?(/[\x00-\x1f\x7f]/))
294
- raise DefinitionError, "invalid mrbgem require name: #{require.inspect}"
295
- end
296
-
297
- valid_auto_require = [true, false].include?(auto_require)
298
- raise DefinitionError, "mrbgem auto_require must be true or false: #{auto_require.inspect}" unless valid_auto_require
299
-
300
- dependency = if github
301
- github_dependency(github, branch, commit, require, auto_require)
302
- else
303
- path_dependency(path, branch, commit, require, auto_require)
304
- end
305
- key = [dependency.type, dependency.source]
306
- raise DefinitionError, "duplicate mrbgem: #{dependency.source}" if dependencies.any? do |item|
307
- [item.type, item.source] == key
308
- end
309
-
310
- dependencies << dependency
311
- end
312
-
313
- private
314
-
315
- attr_reader :directory
316
-
317
- def github_dependency(source, branch, commit, require_name, auto_require)
318
- raise DefinitionError, "invalid GitHub mrbgem: #{source.inspect}" unless GITHUB_PATTERN.match?(source.to_s)
319
- raise DefinitionError, "GitHub mrbgem branch must not be empty" if branch.to_s.empty?
320
- raise DefinitionError, "invalid Git commit: #{commit.inspect}" if commit && !COMMIT_PATTERN.match?(commit.to_s)
321
-
322
- Dependency.new(type: :github, source: source, branch: branch,
323
- commit: commit&.downcase, path: nil, require_name: require_name,
324
- auto_require: auto_require)
325
- end
326
-
327
- def path_dependency(source, branch, commit, require_name, auto_require)
328
- raise DefinitionError, "local mrbgem path must not be empty" if source.to_s.empty?
329
- raise DefinitionError, "local mrbgem does not accept branch or commit" if branch != "main" || commit
330
-
331
- Dependency.new(type: :path, source: source, branch: nil, commit: nil,
332
- path: File.expand_path(source, directory), require_name: require_name,
333
- auto_require: auto_require)
334
- end
268
+ def validate_selected_groups!(selected, available)
269
+ unknown = selected - available
270
+ raise DefinitionError, "unknown mrbgem group: #{unknown.join(", ")}" unless unknown.empty?
335
271
  end
336
272
  end
337
273
  end
274
+
275
+ require_relative "mrbgems/file_writer"
276
+ require_relative "mrbgems/lockfile"
277
+ require_relative "mrbgems/definition"
@@ -10,12 +10,21 @@ module Rpremote
10
10
  def self.run(args, output: $stdout, mrbgems: Mrbgems)
11
11
  command = args.shift
12
12
  options = { path: DEFAULT_PATH, lock_path: nil }
13
+ with = nil
14
+ without = nil
13
15
  OptionParser.new do |opts|
14
16
  opts.on("--file FILE") { |value| options[:path] = value }
15
17
  opts.on("--lockfile FILE") { |value| options[:lock_path] = value }
18
+ opts.on("--with GROUPS") { |value| with = Mrbgems.parse_groups(value) }
19
+ opts.on("--without GROUPS") { |value| without = Mrbgems.parse_groups(value) }
16
20
  end.parse!(args)
17
21
  raise ArgumentError, "mrbgems does not accept arguments" unless args.empty?
18
22
 
23
+ unless %w[lock update].include?(command)
24
+ raise ArgumentError, "--with is only available for mrbgems lock and update" if with
25
+ raise ArgumentError, "--without is only available for mrbgems lock and update" if without
26
+ end
27
+
19
28
  manager = mrbgems.new(**options)
20
29
  case command
21
30
  when "check"
@@ -24,7 +33,7 @@ module Rpremote
24
33
  when "list"
25
34
  list(manager, output)
26
35
  when "lock", "update"
27
- result = manager.lock(update: command == "update")
36
+ result = manager.lock(update: command == "update", with: with || [], without: without || [])
28
37
  output.puts("locked #{result.fetch("gems").length} mrbgems: #{manager.lock_path}")
29
38
  else
30
39
  raise ArgumentError, "unknown mrbgems command: #{command || "(none)"}"
@@ -36,13 +45,19 @@ module Rpremote
36
45
  lock = manager.read_lock(required: false)
37
46
  entries = lock&.fetch("gems", []) || []
38
47
  dependencies.each do |dependency|
39
- entry = entries.find do |item|
40
- item["type"] == dependency.type.to_s && item["source"] == dependency.source
41
- end
48
+ entry = entries.find { |item| matching_entry?(manager, dependency, item) }
42
49
  suffix = entry && (entry["commit"] || entry["sha256"])
43
50
  output.puts([dependency.type, dependency.source, suffix&.slice(0, 12)].compact.join(" "))
44
51
  end
45
52
  end
46
53
  private_class_method :list
54
+
55
+ def self.matching_entry?(manager, dependency, entry)
56
+ return false unless entry["type"] == dependency.type.to_s
57
+ return entry["source"] == dependency.source if dependency.type == :github
58
+
59
+ File.expand_path(entry["source"], File.dirname(manager.lock_path)) == dependency.path
60
+ end
61
+ private_class_method :matching_entry?
47
62
  end
48
63
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rpremote
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -0,0 +1,27 @@
1
+ diff --git a/mrbgems/picoruby-pwm/ports/rp2040/pwm.c b/mrbgems/picoruby-pwm/ports/rp2040/pwm.c
2
+ --- a/mrbgems/picoruby-pwm/ports/rp2040/pwm.c
3
+ +++ b/mrbgems/picoruby-pwm/ports/rp2040/pwm.c
4
+ @@ -1,3 +1,4 @@
5
+ #include "pico/stdlib.h"
6
+ #include "hardware/clocks.h"
7
+ #include "hardware/pwm.h"
8
+ +#include "hardware/structs/clocks.h"
9
+ @@ -61,4 +62,18 @@
10
+ {
11
+ uint slice_num = pwm_gpio_to_slice_num(pin);
12
+ + /* The scheduler uses WFI when no task is runnable, and the HAL normally
13
+ + * gates clk_sys_pwm while a core sleeps. Ungate it before starting a slice
14
+ + * so autonomous PWM output continues between scheduler ticks. */
15
+ + if (enabled) {
16
+ + hw_set_bits(&clocks_hw->sleep_en0,
17
+ + CLOCKS_SLEEP_EN0_CLK_SYS_PWM_BITS);
18
+ + }
19
+ pwm_set_enabled(slice_num, enabled);
20
+ + /* Preserve the clock for every active slice. Once the final slice stops,
21
+ + * restore the HAL's low-power policy instead of keeping PWM clocked merely
22
+ + * because a PWM object was initialized earlier. */
23
+ + if (!enabled && pwm_hw->en == 0) {
24
+ + hw_clear_bits(&clocks_hw->sleep_en0,
25
+ + CLOCKS_SLEEP_EN0_CLK_SYS_PWM_BITS);
26
+ + }
27
+ }
data/sig/rpremote.rbs CHANGED
@@ -208,7 +208,7 @@ module Rpremote
208
208
  ?board: String,
209
209
  ?cache_dir: String,
210
210
  ?firmware: String?,
211
- ?mrbgems: String | false | nil,
211
+ ?mrbgems_lock: String | false | nil,
212
212
  ?output: IO,
213
213
  ?error: IO
214
214
  ) -> void
@@ -237,6 +237,7 @@ module Rpremote
237
237
  attr_reader path: String?
238
238
  attr_reader require_name: String?
239
239
  attr_reader auto_require: bool
240
+ attr_reader groups: Array[Symbol]
240
241
  end
241
242
 
242
243
  class Overlay < Data
@@ -247,6 +248,9 @@ module Rpremote
247
248
  attr_reader path: String
248
249
  attr_reader lock_path: String
249
250
 
251
+ def self.parse_groups: (String value) -> Array[Symbol]
252
+ def self.normalize_groups: (untyped values) -> Array[Symbol]
253
+
250
254
  def initialize: (
251
255
  ?path: String,
252
256
  ?lock_path: String?,
@@ -255,15 +259,18 @@ module Rpremote
255
259
  ) -> void
256
260
  def exist?: () -> bool
257
261
  def dependencies: () -> Array[Dependency]
262
+ def selected_dependencies: (?with: Array[String | Symbol], ?without: Array[String | Symbol]) -> Array[Dependency]
258
263
  def vm: () -> Symbol?
259
264
  def check: () -> Array[Dependency]
260
- def lock: (?update: bool) -> Hash[String, untyped]
265
+ def lock: (?update: bool, ?with: Array[String | Symbol], ?without: Array[String | Symbol]) -> Hash[String, untyped]
261
266
  def read_lock: (?required: bool) -> Hash[String, untyped]?
267
+ def require_names: () -> Array[String]
268
+ def prepend_requires: (String source) -> String
269
+ def locked_vm: () -> Symbol?
262
270
  def generate_overlay: (
263
271
  base_config: String,
264
272
  target: String,
265
- directory: String,
266
- ?update: bool
273
+ directory: String
267
274
  ) -> Overlay
268
275
  end
269
276
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rpremote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ogom
@@ -31,6 +31,7 @@ files:
31
31
  - lib/rpremote/builder.rb
32
32
  - lib/rpremote/checksum.rb
33
33
  - lib/rpremote/cli.rb
34
+ - lib/rpremote/cli/filesystem_commands.rb
34
35
  - lib/rpremote/config.rb
35
36
  - lib/rpremote/config_show.rb
36
37
  - lib/rpremote/deploy_command.rb
@@ -40,9 +41,17 @@ files:
40
41
  - lib/rpremote/flash_command.rb
41
42
  - lib/rpremote/flasher.rb
42
43
  - lib/rpremote/help.rb
44
+ - lib/rpremote/help/dfu_commands.rb
45
+ - lib/rpremote/help/filesystem_commands.rb
46
+ - lib/rpremote/help/project_commands.rb
47
+ - lib/rpremote/help/remote_commands.rb
48
+ - lib/rpremote/help/root_text.rb
43
49
  - lib/rpremote/language.rb
44
50
  - lib/rpremote/language_source.rb
45
51
  - lib/rpremote/mrbgems.rb
52
+ - lib/rpremote/mrbgems/definition.rb
53
+ - lib/rpremote/mrbgems/file_writer.rb
54
+ - lib/rpremote/mrbgems/lockfile.rb
46
55
  - lib/rpremote/mrbgems_command.rb
47
56
  - lib/rpremote/nuke_firmware.rb
48
57
  - lib/rpremote/picomodem.rb
@@ -61,6 +70,7 @@ files:
61
70
  - patches/picoruby-3.4.5-ruby-exception-status.patch
62
71
  - patches/picoruby-4.0.3-pwm-sleep-clock.patch
63
72
  - patches/picoruby-4.0.3-ruby-exception-status.patch
73
+ - patches/picoruby-4.0.4-pwm-sleep-clock.patch
64
74
  - sig/rpremote.rbs
65
75
  - tasks/firmware_build.rb
66
76
  - tasks/release.rake