rpremote 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.ja.md +17 -3
- data/README.md +17 -3
- data/RELEASING.md +2 -2
- data/THIRD_PARTY_NOTICES.md +4 -0
- data/lib/rpremote/bootsel_command.rb +124 -0
- data/lib/rpremote/cli.rb +60 -9
- data/lib/rpremote/config.rb +2 -0
- data/lib/rpremote/deploy_command.rb +191 -0
- data/lib/rpremote/dfu_command.rb +65 -3
- data/lib/rpremote/flash_command.rb +4 -1
- data/lib/rpremote/flasher.rb +49 -11
- data/lib/rpremote/help.rb +64 -11
- data/lib/rpremote/mrbgems.rb +41 -12
- data/lib/rpremote/nuke_firmware.rb +71 -0
- data/lib/rpremote/picoruby_source_patch.rb +23 -6
- data/lib/rpremote/recursive_copy.rb +84 -0
- data/lib/rpremote/runner.rb +33 -6
- data/lib/rpremote/setup_command.rb +4 -1
- data/lib/rpremote/shell.rb +9 -5
- data/lib/rpremote/terminal.rb +13 -1
- data/lib/rpremote/version.rb +1 -1
- data/lib/rpremote.rb +4 -0
- data/patches/picoruby-3.4.5-bootsel.patch +71 -0
- data/patches/picoruby-4.0.3-bootsel.patch +71 -0
- data/sig/rpremote.rbs +50 -2
- metadata +7 -1
data/lib/rpremote/mrbgems.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Rpremote
|
|
|
14
14
|
COMMIT_PATTERN = /\A[0-9a-f]{40,64}\z/i
|
|
15
15
|
VMS = %i[mruby mrubyc].freeze
|
|
16
16
|
|
|
17
|
-
Dependency = Data.define(:type, :source, :branch, :commit, :path)
|
|
17
|
+
Dependency = Data.define(:type, :source, :branch, :commit, :path, :require_name)
|
|
18
18
|
Overlay = Data.define(:path, :fingerprint)
|
|
19
19
|
|
|
20
20
|
class Error < Rpremote::Error; end
|
|
@@ -68,6 +68,20 @@ module Rpremote
|
|
|
68
68
|
raise LockError, "invalid mrbgems lock file #{lock_path}: #{e.message}"
|
|
69
69
|
end
|
|
70
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 { |gem| gem["require_name"] }.uniq
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def prepend_requires(source)
|
|
79
|
+
names = require_names
|
|
80
|
+
return source if names.empty?
|
|
81
|
+
|
|
82
|
+
names.map { |name| "require #{name.inspect}\n" }.join.b + source.b
|
|
83
|
+
end
|
|
84
|
+
|
|
71
85
|
def generate_overlay(base_config:, target:, directory:, update: false)
|
|
72
86
|
lock_data = lock(update: update)
|
|
73
87
|
fingerprint = build_fingerprint(base_config, lock_data)
|
|
@@ -109,10 +123,12 @@ module Rpremote
|
|
|
109
123
|
dependency.source, dependency.branch
|
|
110
124
|
)
|
|
111
125
|
{ "type" => "github", "source" => dependency.source,
|
|
112
|
-
"branch" => dependency.branch, "commit" => validate_commit!(commit)
|
|
126
|
+
"branch" => dependency.branch, "commit" => validate_commit!(commit),
|
|
127
|
+
"require_name" => dependency.require_name }.compact
|
|
113
128
|
else
|
|
114
129
|
{ "type" => "path", "source" => dependency.source,
|
|
115
|
-
"sha256" => digest_directory(dependency.path)
|
|
130
|
+
"sha256" => digest_directory(dependency.path),
|
|
131
|
+
"require_name" => dependency.require_name || local_require_name(dependency.path) }.compact
|
|
116
132
|
end
|
|
117
133
|
end
|
|
118
134
|
|
|
@@ -184,12 +200,22 @@ module Rpremote
|
|
|
184
200
|
|
|
185
201
|
def valid_github_lock?(entry)
|
|
186
202
|
GITHUB_PATTERN.match?(entry["source"].to_s) && !entry["branch"].to_s.empty? &&
|
|
187
|
-
COMMIT_PATTERN.match?(entry["commit"].to_s)
|
|
203
|
+
COMMIT_PATTERN.match?(entry["commit"].to_s) && valid_require_name?(entry["require_name"])
|
|
188
204
|
end
|
|
189
205
|
|
|
190
206
|
def valid_path_lock?(entry)
|
|
191
207
|
entry["type"] == "path" && !entry["source"].to_s.empty? &&
|
|
192
|
-
/\A[0-9a-f]{64}\z/.match?(entry["sha256"].to_s)
|
|
208
|
+
/\A[0-9a-f]{64}\z/.match?(entry["sha256"].to_s) && valid_require_name?(entry["require_name"])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def valid_require_name?(name)
|
|
212
|
+
name.nil? || (name.is_a?(String) && !name.empty? && !name.match?(/[\x00-\x1f\x7f]/))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def local_require_name(directory)
|
|
216
|
+
source = File.read(File.join(directory, "mrbgem.rake"))
|
|
217
|
+
match = source.match(/^\s*spec\.require_name\s*=\s*["']([^"']+)["']\s*$/)
|
|
218
|
+
match && match[1]
|
|
193
219
|
end
|
|
194
220
|
|
|
195
221
|
def build_fingerprint(base_config, lock_data)
|
|
@@ -246,14 +272,17 @@ module Rpremote
|
|
|
246
272
|
@vm_name = value
|
|
247
273
|
end
|
|
248
274
|
|
|
249
|
-
def gem(github: nil, path: nil, branch: "main", commit: nil)
|
|
275
|
+
def gem(github: nil, path: nil, branch: "main", commit: nil, require: nil)
|
|
250
276
|
sources = [github, path].compact
|
|
251
277
|
raise DefinitionError, "gem requires exactly one of github or path" unless sources.length == 1
|
|
278
|
+
unless require.nil? || (require.is_a?(String) && !require.empty? && !require.match?(/[\x00-\x1f\x7f]/))
|
|
279
|
+
raise DefinitionError, "invalid mrbgem require name: #{require.inspect}"
|
|
280
|
+
end
|
|
252
281
|
|
|
253
282
|
dependency = if github
|
|
254
|
-
github_dependency(github, branch, commit)
|
|
283
|
+
github_dependency(github, branch, commit, require)
|
|
255
284
|
else
|
|
256
|
-
path_dependency(path, branch, commit)
|
|
285
|
+
path_dependency(path, branch, commit, require)
|
|
257
286
|
end
|
|
258
287
|
key = [dependency.type, dependency.source]
|
|
259
288
|
raise DefinitionError, "duplicate mrbgem: #{dependency.source}" if dependencies.any? do |item|
|
|
@@ -267,21 +296,21 @@ module Rpremote
|
|
|
267
296
|
|
|
268
297
|
attr_reader :directory
|
|
269
298
|
|
|
270
|
-
def github_dependency(source, branch, commit)
|
|
299
|
+
def github_dependency(source, branch, commit, require_name)
|
|
271
300
|
raise DefinitionError, "invalid GitHub mrbgem: #{source.inspect}" unless GITHUB_PATTERN.match?(source.to_s)
|
|
272
301
|
raise DefinitionError, "GitHub mrbgem branch must not be empty" if branch.to_s.empty?
|
|
273
302
|
raise DefinitionError, "invalid Git commit: #{commit.inspect}" if commit && !COMMIT_PATTERN.match?(commit.to_s)
|
|
274
303
|
|
|
275
304
|
Dependency.new(type: :github, source: source, branch: branch,
|
|
276
|
-
commit: commit&.downcase, path: nil)
|
|
305
|
+
commit: commit&.downcase, path: nil, require_name: require_name)
|
|
277
306
|
end
|
|
278
307
|
|
|
279
|
-
def path_dependency(source, branch, commit)
|
|
308
|
+
def path_dependency(source, branch, commit, require_name)
|
|
280
309
|
raise DefinitionError, "local mrbgem path must not be empty" if source.to_s.empty?
|
|
281
310
|
raise DefinitionError, "local mrbgem does not accept branch or commit" if branch != "main" || commit
|
|
282
311
|
|
|
283
312
|
Dependency.new(type: :path, source: source, branch: nil, commit: nil,
|
|
284
|
-
path: File.expand_path(source, directory))
|
|
313
|
+
path: File.expand_path(source, directory), require_name: require_name)
|
|
285
314
|
end
|
|
286
315
|
end
|
|
287
316
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Rpremote
|
|
9
|
+
class NukeFirmware
|
|
10
|
+
URL = "https://github.com/raspberrypi/pico-sdk-prebuilts/releases/latest/download/nuke_universal.uf2"
|
|
11
|
+
FILENAME = "nuke_universal.uf2"
|
|
12
|
+
MAX_REDIRECTS = 5
|
|
13
|
+
|
|
14
|
+
class DownloadError < Rpremote::Error; end
|
|
15
|
+
|
|
16
|
+
def initialize(directory: Target::DEFAULT_CACHE_DIR, fetcher: nil)
|
|
17
|
+
@directory = File.expand_path(directory)
|
|
18
|
+
@fetcher = fetcher || method(:download)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def setup(force: false)
|
|
22
|
+
FileUtils.mkdir_p(directory)
|
|
23
|
+
return path if File.size?(path) && !force
|
|
24
|
+
|
|
25
|
+
temporary = "#{path}.part"
|
|
26
|
+
File.binwrite(temporary, fetcher.call(URL))
|
|
27
|
+
raise DownloadError, "downloaded nuke_universal.uf2 is empty" unless File.size?(temporary)
|
|
28
|
+
|
|
29
|
+
File.rename(temporary, path)
|
|
30
|
+
path
|
|
31
|
+
rescue SystemCallError => e
|
|
32
|
+
raise DownloadError, "cannot store nuke_universal.uf2: #{e.message}"
|
|
33
|
+
ensure
|
|
34
|
+
FileUtils.rm_f(temporary) if temporary
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :directory, :fetcher
|
|
40
|
+
|
|
41
|
+
def path
|
|
42
|
+
File.join(directory, FILENAME)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def download(url, redirects = MAX_REDIRECTS)
|
|
46
|
+
raise DownloadError, "too many redirects while downloading nuke_universal.uf2" if redirects.negative?
|
|
47
|
+
|
|
48
|
+
uri = URI(url)
|
|
49
|
+
raise DownloadError, "nuke_universal.uf2 download requires HTTPS" unless uri.is_a?(URI::HTTPS)
|
|
50
|
+
|
|
51
|
+
request = Net::HTTP::Get.new(uri)
|
|
52
|
+
request["User-Agent"] = "rpremote/#{Rpremote::VERSION}"
|
|
53
|
+
response = Net::HTTP.start(
|
|
54
|
+
uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 60
|
|
55
|
+
) { |http| http.request(request) }
|
|
56
|
+
|
|
57
|
+
case response
|
|
58
|
+
when Net::HTTPSuccess then response.body
|
|
59
|
+
when Net::HTTPRedirection
|
|
60
|
+
location = response["location"]
|
|
61
|
+
raise DownloadError, "nuke_universal.uf2 download redirect has no location" unless location
|
|
62
|
+
|
|
63
|
+
download(URI.join(url, location).to_s, redirects - 1)
|
|
64
|
+
else
|
|
65
|
+
raise DownloadError, "nuke_universal.uf2 download failed: HTTP #{response.code} #{response.message}"
|
|
66
|
+
end
|
|
67
|
+
rescue SocketError, SystemCallError, Timeout::Error => e
|
|
68
|
+
raise DownloadError, "nuke_universal.uf2 download failed: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Rpremote
|
|
4
4
|
class PicoRubySourcePatch
|
|
5
5
|
JOB_PATH = "mrbgems/picoruby-shell/mrblib/job.rb"
|
|
6
|
+
BOOTSEL_PATH = "mrbgems/picoruby-machine/include/machine.h"
|
|
6
7
|
PATCH_ALIASES = { "3.4.2" => "3.4.5" }.freeze
|
|
7
8
|
|
|
8
9
|
class Error < Rpremote::Error; end
|
|
@@ -14,19 +15,35 @@ module Rpremote
|
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def apply(source)
|
|
18
|
+
return unless Dir.exist?(source)
|
|
19
|
+
|
|
17
20
|
patch_version = PATCH_ALIASES.fetch(version, version)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
patches(patch_version).each do |patch, prerequisite|
|
|
22
|
+
next unless File.file?(patch) && File.file?(File.join(source, prerequisite))
|
|
23
|
+
next if prerequisite == BOOTSEL_PATH && File.read(File.join(source, prerequisite)).include?("Machine_bootsel")
|
|
24
|
+
|
|
25
|
+
next if git_apply?(source, patch, "--reverse", "--check")
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
raise Error, "cannot apply rpremote patch to PicoRuby #{version}" unless git_apply?(source, patch, "--check")
|
|
28
|
+
raise Error, "cannot apply rpremote patch to PicoRuby #{version}" unless git_apply?(source, patch)
|
|
29
|
+
end
|
|
30
|
+
nil
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
private
|
|
27
34
|
|
|
35
|
+
def patches(patch_version)
|
|
36
|
+
definitions = {
|
|
37
|
+
"ruby-exception-status" => JOB_PATH,
|
|
38
|
+
"bootsel" => BOOTSEL_PATH
|
|
39
|
+
}
|
|
40
|
+
definitions.map do |name, prerequisite|
|
|
41
|
+
[File.expand_path("../../patches/picoruby-#{patch_version}-#{name}.patch", __dir__), prerequisite]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
28
45
|
def git_apply?(source, patch, *options)
|
|
29
|
-
system("git", "apply", *options, patch, chdir: source, out: File::NULL, err: File::NULL)
|
|
46
|
+
system("git", "apply", "--unidiff-zero", *options, patch, chdir: source, out: File::NULL, err: File::NULL)
|
|
30
47
|
end
|
|
31
48
|
end
|
|
32
49
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpremote
|
|
4
|
+
class RecursiveCopy
|
|
5
|
+
def initialize(output:, serial: Serial, device: Device)
|
|
6
|
+
@output = output
|
|
7
|
+
@serial = serial
|
|
8
|
+
@device = device
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(source, destination, options)
|
|
12
|
+
validate!(source, destination)
|
|
13
|
+
remote_root = RemotePath.validate(destination)
|
|
14
|
+
directories, files = collect(source, remote_root)
|
|
15
|
+
create_directories(directories, options)
|
|
16
|
+
upload_files(files, options)
|
|
17
|
+
output.puts("uploaded #{files.length} files: #{source} -> #{destination}")
|
|
18
|
+
rescue Errno::ENOENT => e
|
|
19
|
+
raise ArgumentError, e.message
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :output, :serial, :device
|
|
25
|
+
|
|
26
|
+
def validate!(source, destination)
|
|
27
|
+
raise ArgumentError, "recursive cp only supports a local source and remote destination" if RemotePath.remote?(source)
|
|
28
|
+
raise ArgumentError, "recursive cp source must be a directory: #{source}" unless File.directory?(source)
|
|
29
|
+
|
|
30
|
+
RemotePath.validate(destination)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def collect(source, remote_root)
|
|
34
|
+
directories = [remote_root]
|
|
35
|
+
files = []
|
|
36
|
+
local_entries(source).each do |path|
|
|
37
|
+
remote_path = File.join(remote_root, path.delete_prefix("#{source}/"))
|
|
38
|
+
File.directory?(path) ? directories << remote_path : files << [path, remote_path]
|
|
39
|
+
end
|
|
40
|
+
[directories.sort_by { |path| [path.count("/"), path] }.uniq, files.sort]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def local_entries(source)
|
|
44
|
+
entries = Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).reject do |path|
|
|
45
|
+
%w[. ..].include?(File.basename(path))
|
|
46
|
+
end
|
|
47
|
+
symbolic_link = entries.find { |path| File.symlink?(path) }
|
|
48
|
+
raise ArgumentError, "recursive cp does not support symbolic links: #{symbolic_link}" if symbolic_link
|
|
49
|
+
|
|
50
|
+
entries
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_directories(directories, options)
|
|
54
|
+
with_port(options) do |port|
|
|
55
|
+
shell = Shell.new(port, timeout: options[:timeout])
|
|
56
|
+
shell.synchronize!
|
|
57
|
+
directories.each do |path|
|
|
58
|
+
quoted_path = Shell.quote_argument(path)
|
|
59
|
+
next unless shell.execute("ls #{quoted_path}").start_with?("ls:")
|
|
60
|
+
|
|
61
|
+
result = shell.execute("mkdir #{quoted_path}")
|
|
62
|
+
raise Shell::CommandError, result.strip unless result.empty?
|
|
63
|
+
|
|
64
|
+
output.puts("created remote directory: :#{path}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def upload_files(files, options)
|
|
70
|
+
with_port(options) do |port|
|
|
71
|
+
modem = PicoModem.new(port, timeout: options[:timeout])
|
|
72
|
+
files.each do |local_path, remote_path|
|
|
73
|
+
size = modem.upload(remote_path, File.binread(local_path))
|
|
74
|
+
output.puts("uploaded #{size} bytes: #{local_path} -> :#{remote_path}")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def with_port(options, &)
|
|
80
|
+
port_path = device.main_port(options[:port])
|
|
81
|
+
serial.open(port_path, baud: options[:baud], &)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/rpremote/runner.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Rpremote
|
|
4
4
|
class Runner
|
|
5
5
|
REMOTE_DIRECTORY = "/home"
|
|
6
|
+
RUN_REMOTE_PATH = "#{REMOTE_DIRECTORY}/.rpremote-run.rb".freeze
|
|
7
|
+
DEPLOY_REMOTE_PATH = "#{REMOTE_DIRECTORY}/.rpremote-deploy.rb".freeze
|
|
6
8
|
|
|
7
9
|
attr_reader :io, :timeout
|
|
8
10
|
|
|
@@ -20,16 +22,33 @@ module Rpremote
|
|
|
20
22
|
@path_factory = path_factory || method(:temporary_path)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
def run(data, output: nil)
|
|
24
|
-
remote_path
|
|
25
|
+
def run(data, output: nil, cleanup: true, remote_path: nil, diagnostics: nil)
|
|
26
|
+
remote_path ||= @path_factory.call
|
|
25
27
|
shell = @shell_class.new(io, timeout: timeout)
|
|
28
|
+
trace(diagnostics, "SYNC_BEFORE_UPLOAD_START")
|
|
26
29
|
shell.synchronize!
|
|
30
|
+
trace(diagnostics, "SYNC_BEFORE_UPLOAD_DONE")
|
|
31
|
+
trace(diagnostics, "UPLOAD_START", "bytes=#{data.bytesize},path=#{remote_path}")
|
|
27
32
|
@modem_class.new(io, timeout: timeout).upload(remote_path, data)
|
|
33
|
+
trace(diagnostics, "UPLOAD_DONE")
|
|
34
|
+
trace(diagnostics, "SYNC_BEFORE_RUN_START")
|
|
28
35
|
shell.synchronize!
|
|
36
|
+
trace(diagnostics, "SYNC_BEFORE_RUN_DONE")
|
|
29
37
|
shell_ready = true
|
|
30
|
-
|
|
38
|
+
command = "./#{File.basename(remote_path)}"
|
|
39
|
+
trace(diagnostics, "EXECUTE_START", "command=#{command}")
|
|
40
|
+
result = output ? shell.execute(command, output: output, idle_timeout: true) : shell.execute(command, idle_timeout: true)
|
|
41
|
+
trace(diagnostics, "EXECUTE_DONE", "bytes=#{result.to_s.bytesize}")
|
|
42
|
+
result
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
trace(diagnostics, "ERROR", "class=#{e.class},message=#{e.message.to_s.gsub("\n", " ")}")
|
|
45
|
+
raise
|
|
31
46
|
ensure
|
|
32
|
-
cleanup
|
|
47
|
+
if cleanup && shell_ready && remote_path
|
|
48
|
+
trace(diagnostics, "CLEANUP_START", "path=#{remote_path}")
|
|
49
|
+
cleanup(shell, remote_path)
|
|
50
|
+
trace(diagnostics, "CLEANUP_DONE")
|
|
51
|
+
end
|
|
33
52
|
end
|
|
34
53
|
|
|
35
54
|
private
|
|
@@ -40,9 +59,17 @@ module Rpremote
|
|
|
40
59
|
nil
|
|
41
60
|
end
|
|
42
61
|
|
|
62
|
+
def trace(diagnostics, event, details = nil)
|
|
63
|
+
return unless diagnostics
|
|
64
|
+
|
|
65
|
+
line = "rpremote: run event=#{event}"
|
|
66
|
+
line += ",#{details}" if details
|
|
67
|
+
diagnostics.puts(line)
|
|
68
|
+
diagnostics.flush if diagnostics.respond_to?(:flush)
|
|
69
|
+
end
|
|
70
|
+
|
|
43
71
|
def temporary_path
|
|
44
|
-
|
|
45
|
-
"#{REMOTE_DIRECTORY}/.rpremote-run-#{Process.pid}-#{stamp}.rb"
|
|
72
|
+
RUN_REMOTE_PATH
|
|
46
73
|
end
|
|
47
74
|
end
|
|
48
75
|
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require_relative "nuke_firmware"
|
|
4
5
|
|
|
5
6
|
module Rpremote
|
|
6
7
|
class SetupCommand
|
|
7
|
-
def self.run(args, defaults:, config:, config_filename:, output: $stdout)
|
|
8
|
+
def self.run(args, defaults:, config:, config_filename:, output: $stdout, nuke_firmware: NukeFirmware)
|
|
8
9
|
options = {
|
|
9
10
|
force: false,
|
|
10
11
|
cache_dir: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
|
|
@@ -29,6 +30,8 @@ module Rpremote
|
|
|
29
30
|
cache_dir: target.cache_dir
|
|
30
31
|
).setup(force: options[:force])
|
|
31
32
|
output.puts("installed #{target.language} #{target.language_version}: #{path}")
|
|
33
|
+
nuke_path = nuke_firmware.new.setup(force: options[:force])
|
|
34
|
+
output.puts("installed Raspberry Pi nuke firmware: #{nuke_path}")
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
end
|
data/lib/rpremote/shell.rb
CHANGED
|
@@ -29,11 +29,12 @@ module Rpremote
|
|
|
29
29
|
|
|
30
30
|
attr_reader :io, :timeout
|
|
31
31
|
|
|
32
|
-
def initialize(io, timeout: DEFAULT_TIMEOUT)
|
|
32
|
+
def initialize(io, timeout: DEFAULT_TIMEOUT, clock: nil)
|
|
33
33
|
raise ArgumentError, "timeout must be positive" unless timeout.positive?
|
|
34
34
|
|
|
35
35
|
@io = io
|
|
36
36
|
@timeout = timeout
|
|
37
|
+
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def synchronize!
|
|
@@ -43,12 +44,14 @@ module Rpremote
|
|
|
43
44
|
nil
|
|
44
45
|
end
|
|
45
46
|
|
|
46
|
-
def execute(command, output: nil)
|
|
47
|
+
def execute(command, output: nil, idle_timeout: false)
|
|
47
48
|
command = validate_command(command)
|
|
48
49
|
send_command(command)
|
|
49
50
|
echo = ECHO_START + command.b + ERASE_LINE
|
|
50
51
|
stream_state = {}
|
|
51
|
-
response = read_until_prompt(after: echo)
|
|
52
|
+
response = read_until_prompt(after: echo, idle_timeout: idle_timeout) do |buffer|
|
|
53
|
+
stream_output(output, buffer, echo, stream_state)
|
|
54
|
+
end
|
|
52
55
|
command_output = extract_output(response, command)
|
|
53
56
|
ruby_exception = command_output.include?(RUBY_EXCEPTION_STATUS)
|
|
54
57
|
command_output = remove_ruby_exception_status(command_output)
|
|
@@ -76,7 +79,7 @@ module Rpremote
|
|
|
76
79
|
command
|
|
77
80
|
end
|
|
78
81
|
|
|
79
|
-
def read_until_prompt(wait: timeout, after: nil)
|
|
82
|
+
def read_until_prompt(wait: timeout, after: nil, idle_timeout: false)
|
|
80
83
|
deadline = monotonic_time + wait
|
|
81
84
|
buffer = +"".b
|
|
82
85
|
loop do
|
|
@@ -85,6 +88,7 @@ module Rpremote
|
|
|
85
88
|
return buffer if prompt_at && buffer.index(PROMPT_END, prompt_at + PROMPT_START.bytesize)
|
|
86
89
|
|
|
87
90
|
buffer << read_available(deadline)
|
|
91
|
+
deadline = monotonic_time + wait if idle_timeout
|
|
88
92
|
answer_terminal_queries(buffer)
|
|
89
93
|
yield buffer if block_given?
|
|
90
94
|
end
|
|
@@ -186,7 +190,7 @@ module Rpremote
|
|
|
186
190
|
end
|
|
187
191
|
|
|
188
192
|
def monotonic_time
|
|
189
|
-
|
|
193
|
+
@clock.call
|
|
190
194
|
end
|
|
191
195
|
end
|
|
192
196
|
end
|
data/lib/rpremote/terminal.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Rpremote
|
|
|
49
49
|
data = serial_io.read_nonblock(BUFFER_SIZE)
|
|
50
50
|
return false if data.nil? || data.empty?
|
|
51
51
|
|
|
52
|
-
output.write(data)
|
|
52
|
+
output.write(normalize_line_endings(data))
|
|
53
53
|
output.flush if output.respond_to?(:flush)
|
|
54
54
|
true
|
|
55
55
|
else
|
|
@@ -75,6 +75,18 @@ module Rpremote
|
|
|
75
75
|
true
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# input.raw disables the terminal's usual LF-to-CRLF output conversion.
|
|
79
|
+
# Preserve device CRLF while making a bare LF start at the left margin.
|
|
80
|
+
def normalize_line_endings(data)
|
|
81
|
+
normalized = +"".b
|
|
82
|
+
data.each_byte do |byte|
|
|
83
|
+
normalized << "\r" if byte == 0x0A && !@previous_output_was_cr
|
|
84
|
+
normalized << byte
|
|
85
|
+
@previous_output_was_cr = byte == 0x0D
|
|
86
|
+
end
|
|
87
|
+
normalized
|
|
88
|
+
end
|
|
89
|
+
|
|
78
90
|
def selectable_serial
|
|
79
91
|
@selectable_serial ||= serial_io.respond_to?(:to_io) ? serial_io.to_io : serial_io
|
|
80
92
|
end
|
data/lib/rpremote/version.rb
CHANGED
data/lib/rpremote.rb
CHANGED
|
@@ -19,8 +19,10 @@ require_relative "rpremote/terminal"
|
|
|
19
19
|
require_relative "rpremote/shell"
|
|
20
20
|
require_relative "rpremote/runner"
|
|
21
21
|
require_relative "rpremote/resetter"
|
|
22
|
+
require_relative "rpremote/recursive_copy"
|
|
22
23
|
require_relative "rpremote/picoruby_source_patch"
|
|
23
24
|
require_relative "rpremote/language_source"
|
|
25
|
+
require_relative "rpremote/nuke_firmware"
|
|
24
26
|
require_relative "rpremote/mrbgems"
|
|
25
27
|
require_relative "rpremote/mrbgems_command"
|
|
26
28
|
require_relative "rpremote/flasher"
|
|
@@ -29,3 +31,5 @@ require_relative "rpremote/dfu_command"
|
|
|
29
31
|
require_relative "rpremote/setup_command"
|
|
30
32
|
require_relative "rpremote/build_command"
|
|
31
33
|
require_relative "rpremote/flash_command"
|
|
34
|
+
require_relative "rpremote/bootsel_command"
|
|
35
|
+
require_relative "rpremote/deploy_command"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
diff --git a/mrbgems/picoruby-machine/include/machine.h b/mrbgems/picoruby-machine/include/machine.h
|
|
2
|
+
--- a/mrbgems/picoruby-machine/include/machine.h
|
|
3
|
+
+++ b/mrbgems/picoruby-machine/include/machine.h
|
|
4
|
+
@@ -48,1 +48,2 @@
|
|
5
|
+
void Machine_reboot(void);
|
|
6
|
+
+void Machine_bootsel(void);
|
|
7
|
+
diff --git a/mrbgems/picoruby-machine/mrblib/machine.rb b/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
8
|
+
--- a/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
9
|
+
+++ b/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
10
|
+
@@ -22,0 +23,4 @@
|
|
11
|
+
+ def self.bootsel
|
|
12
|
+
+ self._bootsel
|
|
13
|
+
+ end
|
|
14
|
+
+
|
|
15
|
+
diff --git a/mrbgems/picoruby-machine/ports/rp2040/machine.c b/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
16
|
+
--- a/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
17
|
+
+++ b/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
18
|
+
@@ -12,0 +13,1 @@
|
|
19
|
+
+#include "pico/bootrom.h"
|
|
20
|
+
@@ -32,0 +34,6 @@
|
|
21
|
+
+void
|
|
22
|
+
+Machine_bootsel(void)
|
|
23
|
+
+{
|
|
24
|
+
+ rom_reset_usb_boot(0, 0);
|
|
25
|
+
+}
|
|
26
|
+
+
|
|
27
|
+
diff --git a/mrbgems/picoruby-machine/src/mruby/machine.c b/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
28
|
+
--- a/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
29
|
+
+++ b/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
30
|
+
@@ -417,0 +418,11 @@
|
|
31
|
+
+static mrb_value
|
|
32
|
+
+mrb_s__bootsel(mrb_state *mrb, mrb_value self)
|
|
33
|
+
+{
|
|
34
|
+
+#if !defined(PICORB_PLATFORM_POSIX)
|
|
35
|
+
+ Machine_bootsel();
|
|
36
|
+
+#else
|
|
37
|
+
+ mrb_raise(mrb, E_NOTIMP_ERROR, "Machine._bootsel is not available on this platform");
|
|
38
|
+
+#endif
|
|
39
|
+
+ return mrb_nil_value();
|
|
40
|
+
+}
|
|
41
|
+
+
|
|
42
|
+
@@ -443,0 +455,1 @@
|
|
43
|
+
+ mrb_define_class_method_id(mrb, module_Machine, MRB_SYM(_bootsel), mrb_s__bootsel, MRB_ARGS_NONE());
|
|
44
|
+
diff --git a/mrbgems/picoruby-machine/src/mrubyc/machine.c b/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
45
|
+
--- a/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
46
|
+
+++ b/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
47
|
+
@@ -252,0 +253,11 @@
|
|
48
|
+
+static void
|
|
49
|
+
+c_Machine__bootsel(mrbc_vm *vm, mrbc_value *v, int argc)
|
|
50
|
+
+{
|
|
51
|
+
+#if !defined(PICORB_PLATFORM_POSIX)
|
|
52
|
+
+ Machine_bootsel();
|
|
53
|
+
+#else
|
|
54
|
+
+ mrbc_raise(vm, MRBC_CLASS(RuntimeError), "Machine.bootsel is not available on this platform.");
|
|
55
|
+
+#endif
|
|
56
|
+
+ SET_NIL_RETURN();
|
|
57
|
+
+}
|
|
58
|
+
+
|
|
59
|
+
@@ -439,0 +451,1 @@
|
|
60
|
+
+ mrbc_define_method(vm, module_Machine, "_bootsel", c_Machine__bootsel);
|
|
61
|
+
diff --git a/mrbgems/picoruby-shell/shell_executables/_path.txt b/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
62
|
+
--- a/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
63
|
+
+++ b/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
64
|
+
@@ -16,0 +17,1 @@
|
|
65
|
+
+/bin/bootsel
|
|
66
|
+
diff --git a/mrbgems/picoruby-shell/shell_executables/bootsel.rb b/mrbgems/picoruby-shell/shell_executables/bootsel.rb
|
|
67
|
+
new file mode 100644
|
|
68
|
+
--- /dev/null
|
|
69
|
+
+++ b/mrbgems/picoruby-shell/shell_executables/bootsel.rb
|
|
70
|
+
@@ -0,0 +1 @@
|
|
71
|
+
+Machine.bootsel
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
diff --git a/mrbgems/picoruby-machine/include/machine.h b/mrbgems/picoruby-machine/include/machine.h
|
|
2
|
+
--- a/mrbgems/picoruby-machine/include/machine.h
|
|
3
|
+
+++ b/mrbgems/picoruby-machine/include/machine.h
|
|
4
|
+
@@ -68,1 +68,2 @@
|
|
5
|
+
void Machine_reboot(void);
|
|
6
|
+
+void Machine_bootsel(void);
|
|
7
|
+
diff --git a/mrbgems/picoruby-machine/mrblib/machine.rb b/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
8
|
+
--- a/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
9
|
+
+++ b/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
10
|
+
@@ -22,0 +23,4 @@
|
|
11
|
+
+ def self.bootsel
|
|
12
|
+
+ self._bootsel
|
|
13
|
+
+ end
|
|
14
|
+
+
|
|
15
|
+
diff --git a/mrbgems/picoruby-machine/ports/rp2040/machine.c b/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
16
|
+
--- a/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
17
|
+
+++ b/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
18
|
+
@@ -10,0 +11,1 @@
|
|
19
|
+
+#include "pico/bootrom.h"
|
|
20
|
+
@@ -42,0 +44,6 @@
|
|
21
|
+
+void
|
|
22
|
+
+Machine_bootsel(void)
|
|
23
|
+
+{
|
|
24
|
+
+ rom_reset_usb_boot(0, 0);
|
|
25
|
+
+}
|
|
26
|
+
+
|
|
27
|
+
diff --git a/mrbgems/picoruby-machine/src/mruby/machine.c b/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
28
|
+
--- a/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
29
|
+
+++ b/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
30
|
+
@@ -496,0 +497,11 @@
|
|
31
|
+
+static mrb_value
|
|
32
|
+
+mrb_s__bootsel(mrb_state *mrb, mrb_value self)
|
|
33
|
+
+{
|
|
34
|
+
+#if !defined(PICORB_PLATFORM_POSIX)
|
|
35
|
+
+ Machine_bootsel();
|
|
36
|
+
+#else
|
|
37
|
+
+ mrb_raise(mrb, E_NOTIMP_ERROR, "Machine._bootsel is not available on this platform");
|
|
38
|
+
+#endif
|
|
39
|
+
+ return mrb_nil_value();
|
|
40
|
+
+}
|
|
41
|
+
+
|
|
42
|
+
@@ -529,0 +541,1 @@
|
|
43
|
+
+ mrb_define_class_method_id(mrb, module_Machine, MRB_SYM(_bootsel), mrb_s__bootsel, MRB_ARGS_NONE());
|
|
44
|
+
diff --git a/mrbgems/picoruby-machine/src/mrubyc/machine.c b/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
45
|
+
--- a/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
46
|
+
+++ b/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
47
|
+
@@ -316,0 +317,11 @@
|
|
48
|
+
+static void
|
|
49
|
+
+c_Machine__bootsel(mrbc_vm *vm, mrbc_value *v, int argc)
|
|
50
|
+
+{
|
|
51
|
+
+#if !defined(PICORB_PLATFORM_POSIX)
|
|
52
|
+
+ Machine_bootsel();
|
|
53
|
+
+#else
|
|
54
|
+
+ mrbc_raise(vm, MRBC_CLASS(RuntimeError), "Machine.bootsel is not available on this platform.");
|
|
55
|
+
+#endif
|
|
56
|
+
+ SET_NIL_RETURN();
|
|
57
|
+
+}
|
|
58
|
+
+
|
|
59
|
+
@@ -546,0 +558,1 @@
|
|
60
|
+
+ mrbc_define_method(vm, module_Machine, "_bootsel", c_Machine__bootsel);
|
|
61
|
+
diff --git a/mrbgems/picoruby-shell/shell_executables/_path.txt b/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
62
|
+
--- a/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
63
|
+
+++ b/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
64
|
+
@@ -17,0 +18,1 @@
|
|
65
|
+
+/bin/bootsel
|
|
66
|
+
diff --git a/mrbgems/picoruby-shell/shell_executables/bootsel.rb b/mrbgems/picoruby-shell/shell_executables/bootsel.rb
|
|
67
|
+
new file mode 100644
|
|
68
|
+
--- /dev/null
|
|
69
|
+
+++ b/mrbgems/picoruby-shell/shell_executables/bootsel.rb
|
|
70
|
+
@@ -0,0 +1 @@
|
|
71
|
+
+Machine.bootsel
|