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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +49 -20
  3. data/REFERENCE.md +636 -0
  4. data/cpp/arduino/Arduino.h +1 -1
  5. data/cpp/arduino/AvrMath.h +117 -17
  6. data/cpp/arduino/Client.h +27 -0
  7. data/cpp/arduino/EEPROM.h +64 -0
  8. data/cpp/arduino/Godmode.cpp +11 -0
  9. data/cpp/arduino/Godmode.h +58 -9
  10. data/cpp/arduino/HardwareSerial.h +9 -28
  11. data/cpp/arduino/IPAddress.h +59 -0
  12. data/cpp/arduino/Print.h +9 -12
  13. data/cpp/arduino/Printable.h +8 -0
  14. data/cpp/arduino/SPI.h +11 -3
  15. data/cpp/arduino/Server.h +5 -0
  16. data/cpp/arduino/Udp.h +27 -0
  17. data/cpp/arduino/Wire.h +234 -0
  18. data/cpp/arduino/avr/io.h +10 -1
  19. data/cpp/arduino/avr/pgmspace.h +76 -46
  20. data/cpp/arduino/ci/StreamTape.h +36 -0
  21. data/cpp/unittest/OstreamHelpers.h +4 -0
  22. data/exe/arduino_ci.rb +427 -0
  23. data/exe/arduino_ci_remote.rb +2 -385
  24. data/exe/arduino_library_location.rb +2 -2
  25. data/exe/ensure_arduino_installation.rb +7 -1
  26. data/lib/arduino_ci.rb +1 -0
  27. data/lib/arduino_ci/arduino_backend.rb +222 -0
  28. data/lib/arduino_ci/arduino_downloader.rb +43 -73
  29. data/lib/arduino_ci/arduino_downloader_linux.rb +17 -55
  30. data/lib/arduino_ci/arduino_downloader_osx.rb +21 -33
  31. data/lib/arduino_ci/arduino_downloader_windows.rb +11 -53
  32. data/lib/arduino_ci/arduino_installation.rb +18 -80
  33. data/lib/arduino_ci/ci_config.rb +15 -9
  34. data/lib/arduino_ci/cpp_library.rb +266 -48
  35. data/lib/arduino_ci/host.rb +59 -4
  36. data/lib/arduino_ci/library_properties.rb +96 -0
  37. data/lib/arduino_ci/version.rb +1 -1
  38. data/misc/default.yml +55 -4
  39. metadata +21 -87
  40. data/cpp/arduino/Arduino.h.orig +0 -143
  41. data/exe/libasan.rb +0 -29
  42. data/lib/arduino_ci/arduino_cmd.rb +0 -328
  43. data/lib/arduino_ci/arduino_cmd_linux.rb +0 -17
  44. data/lib/arduino_ci/arduino_cmd_linux_builder.rb +0 -19
  45. data/lib/arduino_ci/arduino_cmd_osx.rb +0 -17
  46. data/lib/arduino_ci/arduino_cmd_windows.rb +0 -17
@@ -1,386 +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 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
- @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,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