arduino_ci 0.3.0 → 1.3.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.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +125 -69
  3. data/REFERENCE.md +711 -0
  4. data/cpp/arduino/Arduino.h +1 -7
  5. data/cpp/arduino/ArduinoDefines.h +3 -0
  6. data/cpp/arduino/AvrMath.h +117 -17
  7. data/cpp/arduino/Client.h +27 -0
  8. data/cpp/arduino/EEPROM.h +64 -0
  9. data/cpp/arduino/Godmode.cpp +7 -0
  10. data/cpp/arduino/Godmode.h +121 -15
  11. data/cpp/arduino/HardwareSerial.h +4 -4
  12. data/cpp/arduino/IPAddress.h +59 -0
  13. data/cpp/arduino/Print.h +9 -12
  14. data/cpp/arduino/Printable.h +8 -0
  15. data/cpp/arduino/SPI.h +11 -3
  16. data/cpp/arduino/Server.h +5 -0
  17. data/cpp/arduino/Udp.h +27 -0
  18. data/cpp/arduino/Wire.h +197 -77
  19. data/cpp/arduino/avr/io.h +10 -1
  20. data/cpp/arduino/avr/pgmspace.h +76 -46
  21. data/cpp/unittest/ArduinoUnitTests.h +32 -0
  22. data/cpp/unittest/Assertion.h +54 -26
  23. data/cpp/unittest/Compare.h +58 -51
  24. data/cpp/unittest/OstreamHelpers.h +4 -0
  25. data/exe/arduino_ci.rb +538 -0
  26. data/exe/arduino_ci_remote.rb +2 -393
  27. data/exe/arduino_library_location.rb +2 -2
  28. data/exe/ensure_arduino_installation.rb +7 -1
  29. data/lib/arduino_ci.rb +1 -0
  30. data/lib/arduino_ci/arduino_backend.rb +238 -0
  31. data/lib/arduino_ci/arduino_downloader.rb +43 -73
  32. data/lib/arduino_ci/arduino_downloader_linux.rb +17 -55
  33. data/lib/arduino_ci/arduino_downloader_osx.rb +21 -33
  34. data/lib/arduino_ci/arduino_downloader_windows.rb +11 -53
  35. data/lib/arduino_ci/arduino_installation.rb +18 -80
  36. data/lib/arduino_ci/ci_config.rb +8 -11
  37. data/lib/arduino_ci/cpp_library.rb +250 -59
  38. data/lib/arduino_ci/host.rb +59 -4
  39. data/lib/arduino_ci/library_properties.rb +101 -0
  40. data/lib/arduino_ci/version.rb +1 -1
  41. data/misc/default.yml +57 -6
  42. metadata +19 -87
  43. data/cpp/arduino/Arduino.h.orig +0 -143
  44. data/exe/libasan.rb +0 -29
  45. data/lib/arduino_ci/arduino_cmd.rb +0 -332
  46. data/lib/arduino_ci/arduino_cmd_linux.rb +0 -17
  47. data/lib/arduino_ci/arduino_cmd_linux_builder.rb +0 -19
  48. data/lib/arduino_ci/arduino_cmd_osx.rb +0 -17
  49. data/lib/arduino_ci/arduino_cmd_windows.rb +0 -17
@@ -1,394 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
- require 'arduino_ci'
3
- require 'set'
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 install_arduino_library_dependencies(aux_libraries)
171
- aux_libraries.each do |l|
172
- if @arduino_cmd.library_present?(l)
173
- inform("Using pre-existing library") { l.to_s }
174
- else
175
- assure("Installing aux library '#{l}'") { @arduino_cmd.install_library(l) }
176
- end
177
- end
178
- end
179
-
180
- def perform_unit_tests(file_config)
181
- if @cli_options[:skip_unittests]
182
- inform("Skipping unit tests") { "as requested via command line" }
183
- return
184
- end
185
- config = file_config.with_override_config(@cli_options[:ci_config])
186
- cpp_library = ArduinoCI::CppLibrary.new(Pathname.new("."),
187
- @arduino_cmd.lib_dir,
188
- config.exclude_dirs.map(&Pathname.method(:new)))
189
-
190
- # check GCC
191
- compilers = config.compilers_to_use
192
- assure("The set of compilers (#{compilers.length}) isn't empty") { !compilers.empty? }
193
- compilers.each do |gcc_binary|
194
- attempt_multiline("Checking #{gcc_binary} version") do
195
- version = cpp_library.gcc_version(gcc_binary)
196
- next nil unless version
197
-
198
- puts version.split("\n").map { |l| " #{l}" }.join("\n")
199
- version
200
- end
201
- inform("libasan availability for #{gcc_binary}") { cpp_library.libasan?(gcc_binary) }
202
- end
203
-
204
- # Ensure platforms exist for unit test, and save their info in all_platform_info keyed by name
205
- all_platform_info = {}
206
- config.platforms_to_unittest.each { |p| all_platform_info[p] = assured_platform("unittest", p, config) }
207
-
208
- # iterate boards / tests
209
- if !cpp_library.tests_dir.exist?
210
- inform_multiline("Skipping unit tests; no tests dir at #{cpp_library.tests_dir}") do
211
- puts " In case that's an error, this is what was found in the library:"
212
- display_files(cpp_library.tests_dir.parent)
213
- true
214
- end
215
- elsif cpp_library.test_files.empty?
216
- inform_multiline("Skipping unit tests; no test files were found in #{cpp_library.tests_dir}") do
217
- puts " In case that's an error, this is what was found in the tests directory:"
218
- display_files(cpp_library.tests_dir)
219
- true
220
- end
221
- elsif config.platforms_to_unittest.empty?
222
- inform("Skipping unit tests") { "no platforms were requested" }
223
- else
224
- install_arduino_library_dependencies(config.aux_libraries_for_unittest)
225
-
226
- config.platforms_to_unittest.each do |p|
227
- config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path|
228
- unittest_name = unittest_path.basename.to_s
229
- compilers.each do |gcc_binary|
230
- attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary}") do
231
- exe = cpp_library.build_for_test_with_configuration(
232
- unittest_path,
233
- config.aux_libraries_for_unittest,
234
- gcc_binary,
235
- config.gcc_config(p)
236
- )
237
- puts
238
- unless exe
239
- puts "Last command: #{cpp_library.last_cmd}"
240
- puts cpp_library.last_out
241
- puts cpp_library.last_err
242
- next false
243
- end
244
- cpp_library.run_test_file(exe)
245
- end
246
- end
247
- end
248
- end
249
- end
250
- end
251
-
252
- def perform_compilation_tests(config)
253
- if @cli_options[:skip_compilation]
254
- inform("Skipping compilation of examples") { "as requested via command line" }
255
- return
256
- end
257
-
258
- # index the existing libraries
259
- attempt("Indexing libraries") { @arduino_cmd.index_libraries } unless @arduino_cmd.libraries_indexed
260
-
261
- # initialize library under test
262
- installed_library_path = attempt("Installing library under test") do
263
- @arduino_cmd.install_local_library(Pathname.new("."))
264
- end
265
-
266
- if !installed_library_path.nil? && installed_library_path.exist?
267
- inform("Library installed at") { installed_library_path.to_s }
268
- else
269
- assure_multiline("Library installed successfully") do
270
- if installed_library_path.nil?
271
- puts @arduino_cmd.last_msg
272
- else
273
- # print out the contents of the deepest directory we actually find
274
- @arduino_cmd.lib_dir.ascend do |path_part|
275
- next unless path_part.exist?
276
-
277
- break display_files(path_part)
278
- end
279
- false
280
- end
281
- end
282
- end
283
- library_examples = @arduino_cmd.library_examples(installed_library_path)
284
-
285
- # gather up all required boards for compilation so we can install them up front.
286
- # start with the "platforms to unittest" and add the examples
287
- # while we're doing that, get the aux libraries as well
288
- example_platform_info = {}
289
- board_package_url = {}
290
- aux_libraries = Set.new(config.aux_libraries_for_build)
291
- # while collecting the platforms, ensure they're defined
292
-
293
- library_examples.each do |path|
294
- ovr_config = config.from_example(path)
295
- ovr_config.platforms_to_build.each do |platform|
296
- # assure the platform if we haven't already
297
- next if example_platform_info.key?(platform)
298
-
299
- platform_info = assured_platform("library example", platform, config)
300
- next if platform_info.nil?
301
-
302
- example_platform_info[platform] = platform_info
303
- package = platform_info[:package]
304
- board_package_url[package] = ovr_config.package_url(package)
305
- end
306
- aux_libraries.merge(ovr_config.aux_libraries_for_build)
307
- end
308
-
309
- # with all platform info, we can extract unique packages and their urls
310
- # do that, set the URLs, and download the packages
311
- all_packages = example_platform_info.values.map { |v| v[:package] }.uniq.reject(&:nil?)
312
-
313
- # inform about builtin packages
314
- all_packages.select { |p| config.package_builtin?(p) }.each do |p|
315
- inform("Using built-in board package") { p }
316
- end
317
-
318
- # make sure any non-builtin package has a URL defined
319
- all_packages.reject { |p| config.package_builtin?(p) }.each do |p|
320
- assure("Board package #{p} has a defined URL") { board_package_url[p] }
321
- end
322
-
323
- # set up all the board manager URLs.
324
- # we can safely reject nils now, they would be for the builtins
325
- all_urls = all_packages.map { |p| board_package_url[p] }.uniq.reject(&:nil?)
326
-
327
- unless all_urls.empty?
328
- assure("Setting board manager URLs") do
329
- @arduino_cmd.board_manager_urls = all_urls
330
- end
331
- end
332
-
333
- all_packages.each do |p|
334
- assure("Installing board package #{p}") do
335
- @arduino_cmd.install_boards(p)
336
- end
337
- end
338
-
339
- install_arduino_library_dependencies(aux_libraries)
340
-
341
- last_board = nil
342
- if config.platforms_to_build.empty?
343
- inform("Skipping builds") { "no platforms were requested" }
344
- return
345
- elsif library_examples.empty?
346
- inform_multiline("Skipping builds; no examples found in #{installed_library_path}") do
347
- display_files(installed_library_path)
348
- end
349
- return
350
- end
351
-
352
- attempt("Setting compiler warning level") { @arduino_cmd.set_pref("compiler.warning_level", "all") }
353
-
354
- # switching boards takes time, so iterate board first
355
- # _then_ whichever examples match it
356
- examples_by_platform = library_examples.each_with_object({}) do |example_path, acc|
357
- ovr_config = config.from_example(example_path)
358
- ovr_config.platforms_to_build.each do |p|
359
- acc[p] = [] unless acc.key?(p)
360
- acc[p] << example_path
361
- end
362
- end
363
-
364
- examples_by_platform.each do |platform, example_paths|
365
- board = example_platform_info[platform][:board]
366
- assure("Switching to board for #{platform} (#{board})") { @arduino_cmd.use_board(board) } unless last_board == board
367
- last_board = board
368
-
369
- example_paths.each do |example_path|
370
- example_name = File.basename(example_path)
371
- attempt("Verifying #{example_name}") do
372
- ret = @arduino_cmd.verify_sketch(example_path)
373
- unless ret
374
- puts
375
- puts "Last command: #{@arduino_cmd.last_msg}"
376
- puts @arduino_cmd.last_err
377
- end
378
- ret
379
- end
380
- end
381
- end
382
-
383
- end
384
-
385
- # initialize command and config
386
- config = ArduinoCI::CIConfig.default.from_project_library
387
-
388
- @arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate!
389
- inform("Located Arduino binary") { @arduino_cmd.binary_path.to_s }
390
-
391
- perform_unit_tests(config)
392
- perform_compilation_tests(config)
393
-
394
- 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
- @arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate!($stderr)
5
+ @backend = ArduinoCI::ArduinoInstallation.autolocate!($stderr)
6
6
 
7
- puts @arduino_cmd.lib_dir
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
@@ -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,238 @@
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..-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
+ run_and_capture("board", "details", "--fqbn", boardname)[:success]
117
+ end
118
+
119
+ # check whether a board family is installed (e.g. arduino:avr)
120
+ #
121
+ # @param boardfamily_name [String] The board family to test
122
+ # @return [bool] Whether the board is installed
123
+ def boards_installed?(boardfamily_name)
124
+ capture_json("core", "list")[:json].any? { |b| b["ID"] == boardfamily_name }
125
+ end
126
+
127
+ # install a board by name
128
+ # @param name [String] the board name
129
+ # @return [bool] whether the command succeeded
130
+ def install_boards(boardfamily)
131
+ result = if @additional_urls.empty?
132
+ run_and_capture("core", "install", boardfamily)
133
+ else
134
+ run_and_capture("core", "install", boardfamily, "--additional-urls", @additional_urls.join(","))
135
+ end
136
+ result[:success]
137
+ end
138
+
139
+ # Find out if a library is available
140
+ #
141
+ # @param name [String] the library name
142
+ # @return [bool] whether the library can be installed via the library manager
143
+ def library_available?(name)
144
+ # the --names flag limits the size of the response to just the name field
145
+ capture_json("lib", "search", "--names", name)[:json]["libraries"].any? { |l| l["name"] == name }
146
+ end
147
+
148
+ # @return [Hash] information about installed libraries via the CLI
149
+ def installed_libraries
150
+ capture_json("lib", "list")[:json]
151
+ end
152
+
153
+ # @param path [String] The sketch to compile
154
+ # @param boardname [String] The board to use
155
+ # @return [bool] whether the command succeeded
156
+ def compile_sketch(path, boardname)
157
+ ext = File.extname path
158
+ unless ext.casecmp(".ino").zero?
159
+ @last_msg = "Refusing to compile sketch with '#{ext}' extension -- rename it to '.ino'!"
160
+ return false
161
+ end
162
+ unless File.exist? path
163
+ @last_msg = "Can't compile Sketch at nonexistent path '#{path}'!"
164
+ return false
165
+ end
166
+ ret = run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", "--dry-run", path.to_s)
167
+ ret[:success]
168
+ end
169
+
170
+ # Guess the name of a library
171
+ # @param path [Pathname] The path to the library (installed or not)
172
+ # @return [String] the probable library name
173
+ def name_of_library(path)
174
+ src_path = path.realpath
175
+ properties_file = src_path + CppLibrary::LIBRARY_PROPERTIES_FILE
176
+ return src_path.basename.to_s unless properties_file.exist?
177
+ return src_path.basename.to_s if LibraryProperties.new(properties_file).name.nil?
178
+
179
+ LibraryProperties.new(properties_file).name
180
+ end
181
+
182
+ # Create a handle to an Arduino library by name
183
+ # @param name [String] The library "real name"
184
+ # @return [CppLibrary] The library object
185
+ def library_of_name(name)
186
+ raise ArgumentError, "name is not a String (got #{name.class})" unless name.is_a? String
187
+
188
+ CppLibrary.new(name, self)
189
+ end
190
+
191
+ # Create a handle to an Arduino library by path
192
+ # @param path [Pathname] The path to the library
193
+ # @return [CppLibrary] The library object
194
+ def library_of_path(path)
195
+ # the path must exist... and if it does, brute-force search the installed libs for it
196
+ realpath = path.realpath # should produce error if the path doesn't exist to begin with
197
+ entry = installed_libraries.find { |l| Pathname.new(l["library"]["install_dir"]).realpath == realpath }
198
+ probable_name = entry["real_name"].nil? ? realpath.basename.to_s : entry["real_name"]
199
+ CppLibrary.new(probable_name, self)
200
+ end
201
+
202
+ # install a library from a path on the local machine (not via library manager), by symlink or no-op as appropriate
203
+ # @param path [Pathname] library to use
204
+ # @return [CppLibrary] the installed library, or nil
205
+ def install_local_library(path)
206
+ src_path = path.realpath
207
+ library_name = name_of_library(path)
208
+ cpp_library = library_of_name(library_name)
209
+ destination_path = cpp_library.path
210
+
211
+ # things get weird if the sketchbook contains the library.
212
+ # check that first
213
+ if cpp_library.installed?
214
+ # maybe the project has always lived in the libraries directory, no need to symlink
215
+ return cpp_library if destination_path == src_path
216
+
217
+ uhoh = "There is already a library '#{library_name}' in the library directory (#{destination_path})"
218
+ # maybe it's a symlink? that would be OK
219
+ if Host.symlink?(destination_path)
220
+ current_destination_target = Host.readlink(destination_path)
221
+ return cpp_library if current_destination_target == src_path
222
+
223
+ @last_msg = "#{uhoh} and it's symlinked to #{current_destination_target} (expected #{src_path})"
224
+ return nil
225
+ end
226
+
227
+ @last_msg = "#{uhoh}. It may need to be removed manually."
228
+ return nil
229
+ end
230
+
231
+ # install the library
232
+ libraries_dir = destination_path.parent
233
+ libraries_dir.mkpath unless libraries_dir.exist?
234
+ Host.symlink(src_path, destination_path)
235
+ cpp_library
236
+ end
237
+ end
238
+ end