arduino_ci 0.2.0 → 1.1.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 +5 -5
- data/README.md +49 -20
- data/REFERENCE.md +636 -0
- data/cpp/arduino/Arduino.h +1 -1
- data/cpp/arduino/AvrMath.h +117 -17
- data/cpp/arduino/Client.h +27 -0
- data/cpp/arduino/EEPROM.h +64 -0
- data/cpp/arduino/Godmode.cpp +11 -0
- data/cpp/arduino/Godmode.h +58 -9
- data/cpp/arduino/HardwareSerial.h +9 -28
- data/cpp/arduino/IPAddress.h +59 -0
- data/cpp/arduino/Print.h +9 -12
- data/cpp/arduino/Printable.h +8 -0
- data/cpp/arduino/SPI.h +11 -3
- data/cpp/arduino/Server.h +5 -0
- data/cpp/arduino/Udp.h +27 -0
- data/cpp/arduino/Wire.h +234 -0
- data/cpp/arduino/avr/io.h +10 -1
- data/cpp/arduino/avr/pgmspace.h +76 -46
- data/cpp/arduino/ci/StreamTape.h +36 -0
- data/cpp/unittest/OstreamHelpers.h +4 -0
- data/exe/arduino_ci.rb +427 -0
- data/exe/arduino_ci_remote.rb +2 -385
- data/exe/arduino_library_location.rb +2 -2
- data/exe/ensure_arduino_installation.rb +7 -1
- data/lib/arduino_ci.rb +1 -0
- data/lib/arduino_ci/arduino_backend.rb +222 -0
- data/lib/arduino_ci/arduino_downloader.rb +43 -73
- data/lib/arduino_ci/arduino_downloader_linux.rb +17 -55
- data/lib/arduino_ci/arduino_downloader_osx.rb +21 -33
- data/lib/arduino_ci/arduino_downloader_windows.rb +11 -53
- data/lib/arduino_ci/arduino_installation.rb +18 -80
- data/lib/arduino_ci/ci_config.rb +15 -9
- data/lib/arduino_ci/cpp_library.rb +266 -48
- data/lib/arduino_ci/host.rb +59 -4
- data/lib/arduino_ci/library_properties.rb +96 -0
- data/lib/arduino_ci/version.rb +1 -1
- data/misc/default.yml +55 -4
- metadata +21 -87
- data/cpp/arduino/Arduino.h.orig +0 -143
- data/exe/libasan.rb +0 -29
- data/lib/arduino_ci/arduino_cmd.rb +0 -328
- data/lib/arduino_ci/arduino_cmd_linux.rb +0 -17
- data/lib/arduino_ci/arduino_cmd_linux_builder.rb +0 -19
- data/lib/arduino_ci/arduino_cmd_osx.rb +0 -17
- data/lib/arduino_ci/arduino_cmd_windows.rb +0 -17
data/exe/arduino_ci_remote.rb
CHANGED
@@ -1,386 +1,3 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
|
4
|
-
require 'pathname'
|
5
|
-
require 'optparse'
|
6
|
-
|
7
|
-
WIDTH = 80
|
8
|
-
FIND_FILES_INDENT = 4
|
9
|
-
|
10
|
-
@failure_count = 0
|
11
|
-
@passfail = proc { |result| result ? "✓" : "✗" }
|
12
|
-
|
13
|
-
# Use some basic parsing to allow command-line overrides of config
|
14
|
-
class Parser
|
15
|
-
def self.parse(options)
|
16
|
-
unit_config = {}
|
17
|
-
output_options = {
|
18
|
-
skip_unittests: false,
|
19
|
-
skip_compilation: false,
|
20
|
-
ci_config: {
|
21
|
-
"unittest" => unit_config
|
22
|
-
},
|
23
|
-
}
|
24
|
-
|
25
|
-
opt_parser = OptionParser.new do |opts|
|
26
|
-
opts.banner = "Usage: #{File.basename(__FILE__)} [options]"
|
27
|
-
|
28
|
-
opts.on("--skip-unittests", "Don't run unit tests") do |p|
|
29
|
-
output_options[:skip_unittests] = p
|
30
|
-
end
|
31
|
-
|
32
|
-
opts.on("--skip-compilation", "Don't compile example sketches") do |p|
|
33
|
-
output_options[:skip_compilation] = p
|
34
|
-
end
|
35
|
-
|
36
|
-
opts.on("--testfile-select=GLOB", "Unit test file (or glob) to select") do |p|
|
37
|
-
unit_config["testfiles"] ||= {}
|
38
|
-
unit_config["testfiles"]["select"] ||= []
|
39
|
-
unit_config["testfiles"]["select"] << p
|
40
|
-
end
|
41
|
-
|
42
|
-
opts.on("--testfile-reject=GLOB", "Unit test file (or glob) to reject") do |p|
|
43
|
-
unit_config["testfiles"] ||= {}
|
44
|
-
unit_config["testfiles"]["reject"] ||= []
|
45
|
-
unit_config["testfiles"]["reject"] << p
|
46
|
-
end
|
47
|
-
|
48
|
-
opts.on("-h", "--help", "Prints this help") do
|
49
|
-
puts opts
|
50
|
-
exit
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
opt_parser.parse!(options)
|
55
|
-
output_options
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Read in command line options and make them read-only
|
60
|
-
@cli_options = (Parser.parse ARGV).freeze
|
61
|
-
|
62
|
-
# terminate after printing any debug info. TODO: capture debug info
|
63
|
-
def terminate(final = nil)
|
64
|
-
puts "Failures: #{@failure_count}"
|
65
|
-
unless @failure_count.zero? || final
|
66
|
-
puts "Last message: #{@arduino_cmd.last_msg}"
|
67
|
-
puts "========== Stdout:"
|
68
|
-
puts @arduino_cmd.last_out
|
69
|
-
puts "========== Stderr:"
|
70
|
-
puts @arduino_cmd.last_err
|
71
|
-
end
|
72
|
-
retcode = @failure_count.zero? ? 0 : 1
|
73
|
-
exit(retcode)
|
74
|
-
end
|
75
|
-
|
76
|
-
# make a nice status line for an action and react to the action
|
77
|
-
# TODO / note to self: inform_multline is tougher to write
|
78
|
-
# without altering the signature because it only leaves space
|
79
|
-
# for the checkmark _after_ the multiline, it doesn't know how
|
80
|
-
# to make that conditionally the body
|
81
|
-
# @param message String the text of the progress indicator
|
82
|
-
# @param multiline boolean whether multiline output is expected
|
83
|
-
# @param mark_fn block (string) -> string that says how to describe the result
|
84
|
-
# @param on_fail_msg String custom message for failure
|
85
|
-
# @param tally_on_fail boolean whether to increment @failure_count
|
86
|
-
# @param abort_on_fail boolean whether to abort immediately on failure (i.e. if this is a fatal error)
|
87
|
-
def perform_action(message, multiline, mark_fn, on_fail_msg, tally_on_fail, abort_on_fail)
|
88
|
-
line = "#{message}... "
|
89
|
-
endline = "...#{message} "
|
90
|
-
if multiline
|
91
|
-
puts line
|
92
|
-
else
|
93
|
-
print line
|
94
|
-
end
|
95
|
-
STDOUT.flush
|
96
|
-
result = yield
|
97
|
-
mark = mark_fn.nil? ? "" : mark_fn.call(result)
|
98
|
-
# if multline, put checkmark at full width
|
99
|
-
print endline if multiline
|
100
|
-
puts mark.to_s.rjust(WIDTH - line.length, " ")
|
101
|
-
unless result
|
102
|
-
puts on_fail_msg unless on_fail_msg.nil?
|
103
|
-
@failure_count += 1 if tally_on_fail
|
104
|
-
# print out error messaging here if we've captured it
|
105
|
-
terminate if abort_on_fail
|
106
|
-
end
|
107
|
-
result
|
108
|
-
end
|
109
|
-
|
110
|
-
# Make a nice status for something that defers any failure code until script exit
|
111
|
-
def attempt(message, &block)
|
112
|
-
perform_action(message, false, @passfail, nil, true, false, &block)
|
113
|
-
end
|
114
|
-
|
115
|
-
# Make a nice status for something that defers any failure code until script exit
|
116
|
-
def attempt_multiline(message, &block)
|
117
|
-
perform_action(message, true, @passfail, nil, true, false, &block)
|
118
|
-
end
|
119
|
-
|
120
|
-
# Make a nice status for something that kills the script immediately on failure
|
121
|
-
FAILED_ASSURANCE_MESSAGE = "This may indicate a problem with ArduinoCI, or your configuration".freeze
|
122
|
-
def assure(message, &block)
|
123
|
-
perform_action(message, false, @passfail, FAILED_ASSURANCE_MESSAGE, true, true, &block)
|
124
|
-
end
|
125
|
-
|
126
|
-
def assure_multiline(message, &block)
|
127
|
-
perform_action(message, true, @passfail, FAILED_ASSURANCE_MESSAGE, true, true, &block)
|
128
|
-
end
|
129
|
-
|
130
|
-
def inform(message, &block)
|
131
|
-
perform_action(message, false, proc { |x| x }, nil, false, false, &block)
|
132
|
-
end
|
133
|
-
|
134
|
-
def inform_multiline(message, &block)
|
135
|
-
perform_action(message, true, nil, nil, false, false, &block)
|
136
|
-
end
|
137
|
-
|
138
|
-
# Assure that a platform exists and return its definition
|
139
|
-
def assured_platform(purpose, name, config)
|
140
|
-
platform_definition = config.platform_definition(name)
|
141
|
-
assure("Requested #{purpose} platform '#{name}' is defined in 'platforms' YML") do
|
142
|
-
!platform_definition.nil?
|
143
|
-
end
|
144
|
-
platform_definition
|
145
|
-
end
|
146
|
-
|
147
|
-
# Return true if the file (or one of the dirs containing it) is hidden
|
148
|
-
def file_is_hidden_somewhere?(path)
|
149
|
-
# this is clunkly but pre-2.2-ish ruby doesn't return ascend as an enumerator
|
150
|
-
path.ascend do |part|
|
151
|
-
return true if part.basename.to_s.start_with? "."
|
152
|
-
end
|
153
|
-
false
|
154
|
-
end
|
155
|
-
|
156
|
-
# print out some files
|
157
|
-
def display_files(pathname)
|
158
|
-
# `find` doesn't follow symlinks, so we should instead
|
159
|
-
realpath = pathname.symlink? ? pathname.readlink : pathname
|
160
|
-
|
161
|
-
# suppress directories and dotfile-based things
|
162
|
-
all_files = realpath.find.select(&:file?)
|
163
|
-
non_hidden = all_files.reject { |path| file_is_hidden_somewhere?(path) }
|
164
|
-
|
165
|
-
# print files with an indent
|
166
|
-
margin = " " * FIND_FILES_INDENT
|
167
|
-
non_hidden.each { |p| puts "#{margin}#{p}" }
|
168
|
-
end
|
169
|
-
|
170
|
-
def perform_unit_tests(file_config)
|
171
|
-
if @cli_options[:skip_unittests]
|
172
|
-
inform("Skipping unit tests") { "as requested via command line" }
|
173
|
-
return
|
174
|
-
end
|
175
|
-
config = file_config.with_override_config(@cli_options[:ci_config])
|
176
|
-
cpp_library = ArduinoCI::CppLibrary.new(Pathname.new("."), @arduino_cmd.lib_dir)
|
177
|
-
|
178
|
-
# check GCC
|
179
|
-
compilers = config.compilers_to_use
|
180
|
-
assure("The set of compilers (#{compilers.length}) isn't empty") { !compilers.empty? }
|
181
|
-
compilers.each do |gcc_binary|
|
182
|
-
attempt_multiline("Checking #{gcc_binary} version") do
|
183
|
-
version = cpp_library.gcc_version(gcc_binary)
|
184
|
-
next nil unless version
|
185
|
-
|
186
|
-
puts version.split("\n").map { |l| " #{l}" }.join("\n")
|
187
|
-
version
|
188
|
-
end
|
189
|
-
inform("libasan availability for #{gcc_binary}") { cpp_library.libasan?(gcc_binary) }
|
190
|
-
end
|
191
|
-
|
192
|
-
# Ensure platforms exist for unit test, and save their info in all_platform_info keyed by name
|
193
|
-
all_platform_info = {}
|
194
|
-
config.platforms_to_unittest.each { |p| all_platform_info[p] = assured_platform("unittest", p, config) }
|
195
|
-
|
196
|
-
# iterate boards / tests
|
197
|
-
if !cpp_library.tests_dir.exist?
|
198
|
-
inform_multiline("Skipping unit tests; no tests dir at #{cpp_library.tests_dir}") do
|
199
|
-
puts " In case that's an error, this is what was found in the library:"
|
200
|
-
display_files(cpp_library.tests_dir.parent)
|
201
|
-
true
|
202
|
-
end
|
203
|
-
elsif cpp_library.test_files.empty?
|
204
|
-
inform_multiline("Skipping unit tests; no test files were found in #{cpp_library.tests_dir}") do
|
205
|
-
puts " In case that's an error, this is what was found in the tests directory:"
|
206
|
-
display_files(cpp_library.tests_dir)
|
207
|
-
true
|
208
|
-
end
|
209
|
-
elsif config.platforms_to_unittest.empty?
|
210
|
-
inform("Skipping unit tests") { "no platforms were requested" }
|
211
|
-
else
|
212
|
-
config.platforms_to_unittest.each do |p|
|
213
|
-
config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path|
|
214
|
-
unittest_name = unittest_path.basename.to_s
|
215
|
-
compilers.each do |gcc_binary|
|
216
|
-
attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary}") do
|
217
|
-
exe = cpp_library.build_for_test_with_configuration(
|
218
|
-
unittest_path,
|
219
|
-
config.aux_libraries_for_unittest,
|
220
|
-
gcc_binary,
|
221
|
-
config.gcc_config(p)
|
222
|
-
)
|
223
|
-
puts
|
224
|
-
unless exe
|
225
|
-
puts "Last command: #{cpp_library.last_cmd}"
|
226
|
-
puts cpp_library.last_out
|
227
|
-
puts cpp_library.last_err
|
228
|
-
next false
|
229
|
-
end
|
230
|
-
cpp_library.run_test_file(exe)
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
def perform_compilation_tests(config)
|
239
|
-
if @cli_options[:skip_compilation]
|
240
|
-
inform("Skipping compilation of examples") { "as requested via command line" }
|
241
|
-
return
|
242
|
-
end
|
243
|
-
|
244
|
-
# index the existing libraries
|
245
|
-
attempt("Indexing libraries") { @arduino_cmd.index_libraries } unless @arduino_cmd.libraries_indexed
|
246
|
-
|
247
|
-
# initialize library under test
|
248
|
-
installed_library_path = attempt("Installing library under test") do
|
249
|
-
@arduino_cmd.install_local_library(Pathname.new("."))
|
250
|
-
end
|
251
|
-
|
252
|
-
if !installed_library_path.nil? && installed_library_path.exist?
|
253
|
-
inform("Library installed at") { installed_library_path.to_s }
|
254
|
-
else
|
255
|
-
assure_multiline("Library installed successfully") do
|
256
|
-
if installed_library_path.nil?
|
257
|
-
puts @arduino_cmd.last_msg
|
258
|
-
else
|
259
|
-
# print out the contents of the deepest directory we actually find
|
260
|
-
@arduino_cmd.lib_dir.ascend do |path_part|
|
261
|
-
next unless path_part.exist?
|
262
|
-
|
263
|
-
break display_files(path_part)
|
264
|
-
end
|
265
|
-
false
|
266
|
-
end
|
267
|
-
end
|
268
|
-
end
|
269
|
-
library_examples = @arduino_cmd.library_examples(installed_library_path)
|
270
|
-
|
271
|
-
# gather up all required boards for compilation so we can install them up front.
|
272
|
-
# start with the "platforms to unittest" and add the examples
|
273
|
-
# while we're doing that, get the aux libraries as well
|
274
|
-
example_platform_info = {}
|
275
|
-
board_package_url = {}
|
276
|
-
aux_libraries = Set.new(config.aux_libraries_for_unittest + config.aux_libraries_for_build)
|
277
|
-
# while collecting the platforms, ensure they're defined
|
278
|
-
|
279
|
-
library_examples.each do |path|
|
280
|
-
ovr_config = config.from_example(path)
|
281
|
-
ovr_config.platforms_to_build.each do |platform|
|
282
|
-
# assure the platform if we haven't already
|
283
|
-
next if example_platform_info.key?(platform)
|
284
|
-
|
285
|
-
platform_info = assured_platform("library example", platform, config)
|
286
|
-
next if platform_info.nil?
|
287
|
-
|
288
|
-
example_platform_info[platform] = platform_info
|
289
|
-
package = platform_info[:package]
|
290
|
-
board_package_url[package] = ovr_config.package_url(package)
|
291
|
-
end
|
292
|
-
aux_libraries.merge(ovr_config.aux_libraries_for_build)
|
293
|
-
end
|
294
|
-
|
295
|
-
# with all platform info, we can extract unique packages and their urls
|
296
|
-
# do that, set the URLs, and download the packages
|
297
|
-
all_packages = example_platform_info.values.map { |v| v[:package] }.uniq.reject(&:nil?)
|
298
|
-
|
299
|
-
# inform about builtin packages
|
300
|
-
all_packages.select { |p| config.package_builtin?(p) }.each do |p|
|
301
|
-
inform("Using built-in board package") { p }
|
302
|
-
end
|
303
|
-
|
304
|
-
# make sure any non-builtin package has a URL defined
|
305
|
-
all_packages.reject { |p| config.package_builtin?(p) }.each do |p|
|
306
|
-
assure("Board package #{p} has a defined URL") { board_package_url[p] }
|
307
|
-
end
|
308
|
-
|
309
|
-
# set up all the board manager URLs.
|
310
|
-
# we can safely reject nils now, they would be for the builtins
|
311
|
-
all_urls = all_packages.map { |p| board_package_url[p] }.uniq.reject(&:nil?)
|
312
|
-
|
313
|
-
unless all_urls.empty?
|
314
|
-
assure("Setting board manager URLs") do
|
315
|
-
@arduino_cmd.board_manager_urls = all_urls
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
all_packages.each do |p|
|
320
|
-
assure("Installing board package #{p}") do
|
321
|
-
@arduino_cmd.install_boards(p)
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
aux_libraries.each do |l|
|
326
|
-
if @arduino_cmd.library_present?(l)
|
327
|
-
inform("Using pre-existing library") { l.to_s }
|
328
|
-
else
|
329
|
-
assure("Installing aux library '#{l}'") { @arduino_cmd.install_library(l) }
|
330
|
-
end
|
331
|
-
end
|
332
|
-
|
333
|
-
last_board = nil
|
334
|
-
if config.platforms_to_build.empty?
|
335
|
-
inform("Skipping builds") { "no platforms were requested" }
|
336
|
-
return
|
337
|
-
elsif library_examples.empty?
|
338
|
-
inform_multiline("Skipping builds; no examples found in #{installed_library_path}") do
|
339
|
-
display_files(installed_library_path)
|
340
|
-
end
|
341
|
-
return
|
342
|
-
end
|
343
|
-
|
344
|
-
attempt("Setting compiler warning level") { @arduino_cmd.set_pref("compiler.warning_level", "all") }
|
345
|
-
|
346
|
-
# switching boards takes time, so iterate board first
|
347
|
-
# _then_ whichever examples match it
|
348
|
-
examples_by_platform = library_examples.each_with_object({}) do |example_path, acc|
|
349
|
-
ovr_config = config.from_example(example_path)
|
350
|
-
ovr_config.platforms_to_build.each do |p|
|
351
|
-
acc[p] = [] unless acc.key?(p)
|
352
|
-
acc[p] << example_path
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
examples_by_platform.each do |platform, example_paths|
|
357
|
-
board = example_platform_info[platform][:board]
|
358
|
-
assure("Switching to board for #{platform} (#{board})") { @arduino_cmd.use_board(board) } unless last_board == board
|
359
|
-
last_board = board
|
360
|
-
|
361
|
-
example_paths.each do |example_path|
|
362
|
-
example_name = File.basename(example_path)
|
363
|
-
attempt("Verifying #{example_name}") do
|
364
|
-
ret = @arduino_cmd.verify_sketch(example_path)
|
365
|
-
unless ret
|
366
|
-
puts
|
367
|
-
puts "Last command: #{@arduino_cmd.last_msg}"
|
368
|
-
puts @arduino_cmd.last_err
|
369
|
-
end
|
370
|
-
ret
|
371
|
-
end
|
372
|
-
end
|
373
|
-
end
|
374
|
-
|
375
|
-
end
|
376
|
-
|
377
|
-
# initialize command and config
|
378
|
-
config = ArduinoCI::CIConfig.default.from_project_library
|
379
|
-
|
380
|
-
@arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate!
|
381
|
-
inform("Located Arduino binary") { @arduino_cmd.binary_path.to_s }
|
382
|
-
|
383
|
-
perform_unit_tests(config)
|
384
|
-
perform_compilation_tests(config)
|
385
|
-
|
386
|
-
terminate(true)
|
2
|
+
puts "arduino_ci_remote.rb is deprecated in favor of arduino_ci.rb."
|
3
|
+
require_relative "arduino_ci"
|
@@ -2,6 +2,6 @@
|
|
2
2
|
require 'arduino_ci'
|
3
3
|
|
4
4
|
# locate and/or forcibly install Arduino, keep stdout clean
|
5
|
-
@
|
5
|
+
@backend = ArduinoCI::ArduinoInstallation.autolocate!($stderr)
|
6
6
|
|
7
|
-
puts @
|
7
|
+
puts @backend.lib_dir
|
@@ -2,4 +2,10 @@
|
|
2
2
|
require 'arduino_ci'
|
3
3
|
|
4
4
|
# this will exit after Arduino is located and/or forcibly installed
|
5
|
-
ArduinoCI::ArduinoInstallation.autolocate!
|
5
|
+
backend = ArduinoCI::ArduinoInstallation.autolocate!
|
6
|
+
lib_dir = backend.lib_dir
|
7
|
+
|
8
|
+
unless lib_dir.exist?
|
9
|
+
puts "Creating libraries directory #{lib_dir}"
|
10
|
+
lib_dir.mkpath
|
11
|
+
end
|
data/lib/arduino_ci.rb
CHANGED
@@ -2,6 +2,7 @@ require "arduino_ci/version"
|
|
2
2
|
require "arduino_ci/arduino_installation"
|
3
3
|
require "arduino_ci/cpp_library"
|
4
4
|
require "arduino_ci/ci_config"
|
5
|
+
require "arduino_ci/library_properties"
|
5
6
|
|
6
7
|
# ArduinoCI contains classes for automated testing of Arduino code on the command line
|
7
8
|
# @author Ian Katz <ianfixes@gmail.com>
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# workaround for https://github.com/arduino/Arduino/issues/3535
|
6
|
+
WORKAROUND_LIB = "USBHost".freeze
|
7
|
+
|
8
|
+
module ArduinoCI
|
9
|
+
|
10
|
+
# To report errors that we can't resolve or possibly even explain
|
11
|
+
class ArduinoExecutionError < StandardError; end
|
12
|
+
|
13
|
+
# Wrap the Arduino executable. This requires, in some cases, a faked display.
|
14
|
+
class ArduinoBackend
|
15
|
+
|
16
|
+
# We never even use this in code, it's just here for reference because the backend is picky about it. Used for testing
|
17
|
+
# @return [String] the only allowable name for the arduino-cli config file.
|
18
|
+
CONFIG_FILE_NAME = "arduino-cli.yaml".freeze
|
19
|
+
|
20
|
+
# the actual path to the executable on this platform
|
21
|
+
# @return [Pathname]
|
22
|
+
attr_accessor :binary_path
|
23
|
+
|
24
|
+
# If a custom config is deired (i.e. for testing), specify it here.
|
25
|
+
# Note https://github.com/arduino/arduino-cli/issues/753 : the --config-file option
|
26
|
+
# is really the director that contains the file
|
27
|
+
# @return [Pathname]
|
28
|
+
attr_accessor :config_dir
|
29
|
+
|
30
|
+
# @return [String] STDOUT of the most recently-run command
|
31
|
+
attr_reader :last_out
|
32
|
+
|
33
|
+
# @return [String] STDERR of the most recently-run command
|
34
|
+
attr_reader :last_err
|
35
|
+
|
36
|
+
# @return [String] the most recently-run command
|
37
|
+
attr_reader :last_msg
|
38
|
+
|
39
|
+
# @return [Array<String>] Additional URLs for the boards manager
|
40
|
+
attr_reader :additional_urls
|
41
|
+
|
42
|
+
def initialize(binary_path)
|
43
|
+
@binary_path = binary_path
|
44
|
+
@config_dir = nil
|
45
|
+
@additional_urls = []
|
46
|
+
@last_out = ""
|
47
|
+
@last_err = ""
|
48
|
+
@last_msg = ""
|
49
|
+
end
|
50
|
+
|
51
|
+
def _wrap_run(work_fn, *args, **kwargs)
|
52
|
+
# do some work to extract & merge environment variables if they exist
|
53
|
+
has_env = !args.empty? && args[0].instance_of?(Hash)
|
54
|
+
env_vars = has_env ? args[0] : {}
|
55
|
+
actual_args = has_env ? args[1..] : args # need to shift over if we extracted args
|
56
|
+
custom_config = @config_dir.nil? ? [] : ["--config-file", @config_dir.to_s]
|
57
|
+
full_args = [binary_path.to_s, "--format", "json"] + custom_config + actual_args
|
58
|
+
full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args
|
59
|
+
|
60
|
+
shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ")
|
61
|
+
@last_msg = " $ #{shell_vars} #{full_args.join(' ')}"
|
62
|
+
work_fn.call(*full_cmd, **kwargs)
|
63
|
+
end
|
64
|
+
|
65
|
+
# build and run the arduino command
|
66
|
+
def run_and_output(*args, **kwargs)
|
67
|
+
_wrap_run((proc { |*a, **k| Host.run_and_output(*a, **k) }), *args, **kwargs)
|
68
|
+
end
|
69
|
+
|
70
|
+
# run a command and capture its output
|
71
|
+
# @return [Hash] {:out => String, :err => String, :success => bool}
|
72
|
+
def run_and_capture(*args, **kwargs)
|
73
|
+
ret = _wrap_run((proc { |*a, **k| Host.run_and_capture(*a, **k) }), *args, **kwargs)
|
74
|
+
@last_err = ret[:err]
|
75
|
+
@last_out = ret[:out]
|
76
|
+
ret
|
77
|
+
end
|
78
|
+
|
79
|
+
def capture_json(*args, **kwargs)
|
80
|
+
ret = run_and_capture(*args, **kwargs)
|
81
|
+
ret[:json] = JSON.parse(ret[:out])
|
82
|
+
ret
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get a dump of the entire config
|
86
|
+
# @return [Hash] The configuration
|
87
|
+
def config_dump
|
88
|
+
capture_json("config", "dump")[:json]
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [String] the path to the Arduino libraries directory
|
92
|
+
def lib_dir
|
93
|
+
Pathname.new(config_dump["directories"]["user"]) + "libraries"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Board manager URLs
|
97
|
+
# @return [Array<String>] The additional URLs used by the board manager
|
98
|
+
def board_manager_urls
|
99
|
+
config_dump["board_manager"]["additional_urls"] + @additional_urls
|
100
|
+
end
|
101
|
+
|
102
|
+
# Set board manager URLs
|
103
|
+
# @return [Array<String>] The additional URLs used by the board manager
|
104
|
+
def board_manager_urls=(all_urls)
|
105
|
+
raise ArgumentError("all_urls should be an array, got #{all_urls.class}") unless all_urls.is_a? Array
|
106
|
+
|
107
|
+
@additional_urls = all_urls
|
108
|
+
end
|
109
|
+
|
110
|
+
# check whether a board is installed
|
111
|
+
# we do this by just selecting a board.
|
112
|
+
# the arduino binary will error if unrecognized and do a successful no-op if it's installed
|
113
|
+
# @param boardname [String] The board to test
|
114
|
+
# @return [bool] Whether the board is installed
|
115
|
+
def board_installed?(boardname)
|
116
|
+
# capture_json("core", "list")[:json].find { |b| b["ID"] == boardname } # nope, this is for the family
|
117
|
+
run_and_capture("board", "details", "--fqbn", boardname)[:success]
|
118
|
+
end
|
119
|
+
|
120
|
+
# install a board by name
|
121
|
+
# @param name [String] the board name
|
122
|
+
# @return [bool] whether the command succeeded
|
123
|
+
def install_boards(boardfamily)
|
124
|
+
result = if @additional_urls.empty?
|
125
|
+
run_and_capture("core", "install", boardfamily)
|
126
|
+
else
|
127
|
+
run_and_capture("core", "install", boardfamily, "--additional-urls", @additional_urls.join(","))
|
128
|
+
end
|
129
|
+
result[:success]
|
130
|
+
end
|
131
|
+
|
132
|
+
# @return [Hash] information about installed libraries via the CLI
|
133
|
+
def installed_libraries
|
134
|
+
capture_json("lib", "list")[:json]
|
135
|
+
end
|
136
|
+
|
137
|
+
# @param path [String] The sketch to compile
|
138
|
+
# @param boardname [String] The board to use
|
139
|
+
# @return [bool] whether the command succeeded
|
140
|
+
def compile_sketch(path, boardname)
|
141
|
+
ext = File.extname path
|
142
|
+
unless ext.casecmp(".ino").zero?
|
143
|
+
@last_msg = "Refusing to compile sketch with '#{ext}' extension -- rename it to '.ino'!"
|
144
|
+
return false
|
145
|
+
end
|
146
|
+
unless File.exist? path
|
147
|
+
@last_msg = "Can't compile Sketch at nonexistent path '#{path}'!"
|
148
|
+
return false
|
149
|
+
end
|
150
|
+
ret = run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", "--dry-run", path.to_s)
|
151
|
+
ret[:success]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Guess the name of a library
|
155
|
+
# @param path [Pathname] The path to the library (installed or not)
|
156
|
+
# @return [String] the probable library name
|
157
|
+
def name_of_library(path)
|
158
|
+
src_path = path.realpath
|
159
|
+
properties_file = src_path + CppLibrary::LIBRARY_PROPERTIES_FILE
|
160
|
+
return src_path.basename.to_s unless properties_file.exist?
|
161
|
+
return src_path.basename.to_s if LibraryProperties.new(properties_file).name.nil?
|
162
|
+
|
163
|
+
LibraryProperties.new(properties_file).name
|
164
|
+
end
|
165
|
+
|
166
|
+
# Create a handle to an Arduino library by name
|
167
|
+
# @param name [String] The library "real name"
|
168
|
+
# @return [CppLibrary] The library object
|
169
|
+
def library_of_name(name)
|
170
|
+
raise ArgumentError, "name is not a String (got #{name.class})" unless name.is_a? String
|
171
|
+
|
172
|
+
CppLibrary.new(name, self)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Create a handle to an Arduino library by path
|
176
|
+
# @param path [Pathname] The path to the library
|
177
|
+
# @return [CppLibrary] The library object
|
178
|
+
def library_of_path(path)
|
179
|
+
# the path must exist... and if it does, brute-force search the installed libs for it
|
180
|
+
realpath = path.realpath # should produce error if the path doesn't exist to begin with
|
181
|
+
entry = installed_libraries.find { |l| Pathname.new(l["library"]["install_dir"]).realpath == realpath }
|
182
|
+
probable_name = entry["real_name"].nil? ? realpath.basename.to_s : entry["real_name"]
|
183
|
+
CppLibrary.new(probable_name, self)
|
184
|
+
end
|
185
|
+
|
186
|
+
# install a library from a path on the local machine (not via library manager), by symlink or no-op as appropriate
|
187
|
+
# @param path [Pathname] library to use
|
188
|
+
# @return [CppLibrary] the installed library, or nil
|
189
|
+
def install_local_library(path)
|
190
|
+
src_path = path.realpath
|
191
|
+
library_name = name_of_library(path)
|
192
|
+
cpp_library = library_of_name(library_name)
|
193
|
+
destination_path = cpp_library.path
|
194
|
+
|
195
|
+
# things get weird if the sketchbook contains the library.
|
196
|
+
# check that first
|
197
|
+
if cpp_library.installed?
|
198
|
+
# maybe the project has always lived in the libraries directory, no need to symlink
|
199
|
+
return cpp_library if destination_path == src_path
|
200
|
+
|
201
|
+
uhoh = "There is already a library '#{library_name}' in the library directory (#{destination_path})"
|
202
|
+
# maybe it's a symlink? that would be OK
|
203
|
+
if Host.symlink?(destination_path)
|
204
|
+
current_destination_target = Host.readlink(destination_path)
|
205
|
+
return cpp_library if current_destination_target == src_path
|
206
|
+
|
207
|
+
@last_msg = "#{uhoh} and it's symlinked to #{current_destination_target} (expected #{src_path})"
|
208
|
+
return nil
|
209
|
+
end
|
210
|
+
|
211
|
+
@last_msg = "#{uhoh}. It may need to be removed manually."
|
212
|
+
return nil
|
213
|
+
end
|
214
|
+
|
215
|
+
# install the library
|
216
|
+
libraries_dir = destination_path.parent
|
217
|
+
libraries_dir.mkpath unless libraries_dir.exist?
|
218
|
+
Host.symlink(src_path, destination_path)
|
219
|
+
cpp_library
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|