arduino_ci 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c01ddca13fd4bd941dcff9e9faeeae700f184bbf647876d520c7ccfce0c87b3
4
- data.tar.gz: 974ce43ef7eba01113b9470750c78ad4cc5f81f0296f72f18ce43def9ad1c597
3
+ metadata.gz: cad9df4f8ade3fbe801da837c8b6a73d129c9a5497e1d43b4f655de8d9b5b640
4
+ data.tar.gz: b42f9624e6af593c467d0f67a94b0695fb36b584d740f4fc38e256b8f703c075
5
5
  SHA512:
6
- metadata.gz: a044d9ab06badda8a648f189786c14209d7bee59d4ba299dc8790bd29e54adfd107f15a53fcb28d7ad7f286c6daf3e559f6a09b15b3f625c5344c400b0a4c5e8
7
- data.tar.gz: f134b211ce71483ee88794a8a9d9f742deed82ee3d93ccecbbdd745ff53af12d6b44f71e2480c7ea98ce60d62bb28f0553c22b773bd6376ea7854bae29f19c9c
6
+ metadata.gz: 25d40817ac30d531979d197768aad671e8efa9b3e2bc023fde41f3dbcbbb1b829a4f6a9915714a6eec2e1e234f2f70749390802431af81a7b23cf8b847a39750
7
+ data.tar.gz: 9ef54c3d3fc6704ad70d347b85f2fc039d697a81edfafaa3e9fea329cc20c0a0919f42267452e4f62be5d508460f38a3264f04b0c51ea98a3b98062cc4bd18b1
data/README.md CHANGED
@@ -1,32 +1,44 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/arduino_ci.svg)](https://rubygems.org/gems/arduino_ci)
2
2
  [![Build Status](https://travis-ci.org/ifreecarve/arduino_ci.svg)](https://travis-ci.org/ifreecarve/arduino_ci)
3
- [![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/arduino_ci/)
3
+ [![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/arduino_ci/0.1.0)
4
4
 
5
5
  # ArduinoCI Ruby gem (`arduino_ci`)
6
6
 
7
7
  [Arduino CI](https://github.com/ifreecarve/arduino_ci) is a Ruby gem for executing Continuous Integration (CI) tests on an Arduino library -- both locally and as part of a service like Travis CI.
8
8
 
9
9
 
10
- ## Installation
10
+ ## Installation In Your GitHub Project And Using Travis CI
11
11
 
12
- Add this line to your application's Gemfile:
12
+ Add a file called `Gemfile` (no extension) to your Arduino project:
13
13
 
14
14
  ```ruby
15
+ source 'https://rubygems.org'
15
16
  gem 'arduino_ci'
16
17
  ```
17
18
 
18
- And then execute:
19
+ Next, you need this in `.travis.yml`
19
20
 
20
- $ bundle
21
+ ```yaml
22
+ sudo: false
23
+ language: ruby
24
+ script:
25
+ - bundle install
26
+ - bundle exec arduino_ci_remote.rb
27
+ ```
28
+
29
+ That's literally all there is to it on the repository side. You'll need to go to https://travis-ci.org/profile/ and enable testing for your Arduino project. Once that happens, you should be all set.
21
30
 
22
- Or install it yourself as:
23
31
 
24
- $ gem install arduino_ci
32
+ ## More Documentation
25
33
 
34
+ This software is in alpha. But [SampleProjects/DoSomething](SampleProjects/DoSomething) has a decent writeup and is a good bare-bones example of all the features.
26
35
 
27
- ## Usage
36
+ ## Known Problems
28
37
 
29
- TODO: Write usage instructions here, based on other TODO of writing the actual gem.
38
+ * The Arduino library is not fully mocked.
39
+ * I don't have preprocessor defines for all the Arduino board flavors
40
+ * Arduino Zero boards don't work in CI. I'm confused.
41
+ * https://github.com/ifreecarve/arduino_ci/issues
30
42
 
31
43
 
32
44
  ## Author
@@ -37,3 +49,5 @@ This gem was written by Ian Katz (ifreecarve@gmail.com) in 2018. It's released
37
49
  ## See Also
38
50
 
39
51
  * [Contributing](CONTRIBUTING.md)
52
+ * [Adafruit/travis-ci-arduino](https://github.com/adafruit/travis-ci-arduino) which inspired this project
53
+ * [mmurdoch/arduinounit](https://github.com/mmurdoch/arduinounit) from which the unit test macros were adopted
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env ruby
2
+ require 'arduino_ci'
3
+ require 'set'
4
+
5
+ WIDTH = 80
6
+
7
+ @failure_count = 0
8
+
9
+ # terminate after printing any debug info. TODO: capture debug info
10
+ def terminate(final = nil)
11
+ puts "Failures: #{@failure_count}"
12
+ unless @failure_count.zero? || final
13
+ puts "Last message: #{@arduino_cmd.last_msg}"
14
+ puts "========== Stdout:"
15
+ puts @arduino_cmd.last_out
16
+ puts "========== Stderr:"
17
+ puts @arduino_cmd.last_err
18
+ end
19
+ retcode = @failure_count.zero? ? 0 : 1
20
+ exit(retcode)
21
+ end
22
+
23
+ # make a nice status line for an action and react to the action
24
+ def perform_action(message, on_fail_msg, abort_on_fail)
25
+ line = "#{message}..."
26
+ print line
27
+ result = yield
28
+ mark = result ? "✓" : "X"
29
+ puts mark.rjust(WIDTH - line.length, " ")
30
+ unless result
31
+ puts on_fail_msg unless on_fail_msg.nil?
32
+ @failure_count += 1
33
+ # print out error messaging here if we've captured it
34
+ terminate if abort_on_fail
35
+ end
36
+ result
37
+ end
38
+
39
+ # Make a nice status for something that defers any failure code until script exit
40
+ def attempt(message, &block)
41
+ perform_action(message, nil, false, &block)
42
+ end
43
+
44
+ # Make a nice status for something that kills the script immediately on failure
45
+ def assure(message, &block)
46
+ perform_action(message, "This may indicate a problem with ArduinoCI!", true, &block)
47
+ end
48
+
49
+ # initialize command and config
50
+ config = ArduinoCI::CIConfig.default.from_project_library
51
+ @arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate!
52
+
53
+ # initialize library under test
54
+ installed_library_path = assure("Installing library under test") { @arduino_cmd.install_local_library(".") }
55
+ library_examples = @arduino_cmd.library_examples(installed_library_path)
56
+ cpp_library = ArduinoCI::CppLibrary.new(installed_library_path)
57
+ attempt("Library installed at #{installed_library_path}") { true }
58
+
59
+ # gather up all required boards so we can install them up front.
60
+ # start with the "platforms to unittest" and add the examples
61
+ # while we're doing that, get the aux libraries as well
62
+ all_platforms = {}
63
+ aux_libraries = Set.new(config.aux_libraries_for_unittest + config.aux_libraries_for_build)
64
+ config.platforms_to_unittest.each { |p| all_platforms[p] = config.platform_definition(p) }
65
+ library_examples.each do |path|
66
+ ovr_config = config.from_example(path)
67
+ ovr_config.platforms_to_build.each { |p| all_platforms[p] = config.platform_definition(p) }
68
+ aux_libraries.merge(ovr_config.aux_libraries_for_build)
69
+ end
70
+
71
+ # with all platform info, we can extract unique packages and their urls
72
+ # do that, set the URLs, and download the packages
73
+ all_packages = all_platforms.values.map { |v| v[:package] }.uniq.reject(&:nil?)
74
+ all_urls = all_packages.map { |p| config.package_url(p) }.uniq.reject(&:nil?)
75
+ assure("Setting board manager URLs") do
76
+ @arduino_cmd.set_pref("boardsmanager.additional.urls", all_urls.join(","))
77
+ end
78
+
79
+ all_packages.each do |p|
80
+ assure("Installing board package #{p}") do
81
+ @arduino_cmd.install_boards(p)
82
+ end
83
+ end
84
+
85
+ aux_libraries.each do |l|
86
+ assure("Installing aux library '#{l}'") { @arduino_cmd.install_library(l) }
87
+ end
88
+
89
+ attempt("Setting compiler warning level") { @arduino_cmd.set_pref("compiler.warning_level", "all") }
90
+
91
+ library_examples.each do |example_path|
92
+ ovr_config = config.from_example(example_path)
93
+ ovr_config.platforms_to_build.each do |p|
94
+ board = all_platforms[p][:board]
95
+ assure("Switching to board for #{p} (#{board})") { @arduino_cmd.use_board(board) }
96
+ example_name = File.basename(example_path)
97
+ attempt("Verifying #{example_name}") do
98
+ ret = @arduino_cmd.verify_sketch(example_path)
99
+ unless ret
100
+ puts
101
+ puts "Last command: #{@arduino_cmd.last_msg}"
102
+ puts @arduino_cmd.last_err
103
+ end
104
+ ret
105
+ end
106
+ end
107
+ end
108
+
109
+ config.platforms_to_unittest.each do |p|
110
+ board = all_platforms[p][:board]
111
+ assure("Switching to board for #{p} (#{board})") { @arduino_cmd.use_board(board) }
112
+ cpp_library.test_files.each do |unittest_path|
113
+ unittest_name = File.basename(unittest_path)
114
+ attempt("Unit testing #{unittest_name}") do
115
+ exe = cpp_library.build_for_test_with_configuration(
116
+ unittest_path,
117
+ config.aux_libraries_for_unittest,
118
+ config.gcc_config(p)
119
+ )
120
+ puts
121
+ unless exe
122
+ puts "Last command: #{cpp_library.last_cmd}"
123
+ puts cpp_library.last_out
124
+ puts cpp_library.last_err
125
+ next false
126
+ end
127
+ cpp_library.run_test_file(exe)
128
+ end
129
+ end
130
+ end
131
+
132
+ terminate(true)
data/lib/arduino_ci.rb CHANGED
@@ -1,124 +1,10 @@
1
1
  require "arduino_ci/version"
2
-
3
- require 'singleton'
4
-
5
- # Cross-platform way of finding an executable in the $PATH.
6
- # via https://stackoverflow.com/a/5471032/2063546
7
- # which('ruby') #=> /usr/bin/ruby
8
- def which(cmd)
9
- exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
10
- ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
11
- exts.each do |ext|
12
- exe = File.join(path, "#{cmd}#{ext}")
13
- return exe if File.executable?(exe) && !File.directory?(exe)
14
- end
15
- end
16
- nil
17
- end
2
+ require "arduino_ci/arduino_installation"
3
+ require "arduino_ci/cpp_library"
4
+ require "arduino_ci/ci_config"
18
5
 
19
6
  # ArduinoCI contains classes for automated testing of Arduino code on the command line
20
7
  # @author Ian Katz <ifreecarve@gmail.com>
21
8
  module ArduinoCI
22
9
 
23
- # Wrap the Arduino executable. This requires, in some cases, a faked display.
24
- class ArduinoCmd
25
-
26
- # create as many ArduinoCmds as you like, but we need one and only one display manager
27
- class DisplayMgr
28
- include Singleton
29
- attr_reader :enabled
30
-
31
- def initialize
32
- @existing = existing_display?
33
- @enabled = false
34
- @pid = nil
35
- end
36
-
37
- # attempt to determine if the machine is running a graphical display (i.e. not Travis)
38
- def existing_display?
39
- return true if RUBY_PLATFORM.include? "darwin"
40
- return true if ENV["DISPLAY"].nil?
41
- return true if ENV["DISPLAY"].include? ":"
42
- false
43
- end
44
-
45
- # enable a virtual display
46
- def enable
47
- return @enabled = true if @existing # silent no-op if built in display
48
- return unless @pid.nil?
49
-
50
- @enabled = true
51
- @pid = fork do
52
- puts "Forking Xvfb"
53
- system("Xvfb", ":1", "-ac", "-screen", "0", "1280x1024x16")
54
- puts "Xvfb unexpectedly quit!"
55
- end
56
- sleep(3) # TODO: test a connection to the X server?
57
- end
58
-
59
- # disable the virtual display
60
- def disable
61
- return @enabled = false if @existing # silent no-op if built in display
62
- return if @pid.nil?
63
-
64
- begin
65
- Process.kill 9, @pid
66
- ensure
67
- Process.wait @pid
68
- @pid = nil
69
- end
70
- puts "Xvfb killed"
71
- end
72
-
73
- # Enable a virtual display for the duration of the given block
74
- def with_display
75
- enable
76
- begin
77
- yield environment
78
- ensure
79
- disable
80
- end
81
- end
82
-
83
- def environment
84
- return nil unless @existing || @enabled
85
- return {} if @existing
86
- { DISPLAY => ":1.0" }
87
- end
88
-
89
- # On finalize, ensure child process is ended
90
- def self.finalize
91
- disable
92
- end
93
- end
94
-
95
- class << self
96
- protected :new
97
-
98
- # attempt to find a workable Arduino executable across platforms
99
- def guess_executable_location
100
- osx_place = "/Applications/Arduino.app/Contents/MacOS/Arduino"
101
- places = {
102
- "arduino" => !which("arduino").nil?,
103
- osx_place => (File.exist? osx_place),
104
- }
105
- places.each { |k, v| return k if v }
106
- nil
107
- end
108
-
109
- def autolocate
110
- ret = new
111
- ret.path = guess_executable_location
112
- ret
113
- end
114
- end
115
-
116
- attr_accessor :path
117
-
118
- def initialize
119
- @display_mgr = DisplayMgr::instance
120
- end
121
-
122
- end
123
-
124
10
  end
@@ -0,0 +1,296 @@
1
+ require 'fileutils'
2
+
3
+ module ArduinoCI
4
+
5
+ # Wrap the Arduino executable. This requires, in some cases, a faked display.
6
+ class ArduinoCmd
7
+
8
+ # Enable a shortcut syntax for command line flags
9
+ # @param name [String] What the flag will be called (prefixed with 'flag_')
10
+ # @return [void]
11
+ # @macro [attach] flag
12
+ # The text of the command line flag for $1
13
+ # @!attribute [r] flag_$1
14
+ # @return [String] the text of the command line flag (`$2` in this case)
15
+ def self.flag(name, text = nil)
16
+ text = "(flag #{name} not defined)" if text.nil?
17
+ self.class_eval("def flag_#{name};\"#{text}\";end")
18
+ end
19
+
20
+ # the path to the Arduino executable
21
+ # @return [String]
22
+ attr_accessor :base_cmd
23
+
24
+ # part of a workaround for https://github.com/arduino/Arduino/issues/3535
25
+ attr_reader :library_is_indexed
26
+
27
+ # @return [String] STDOUT of the most recently-run command
28
+ attr_reader :last_out
29
+
30
+ # @return [String] STDERR of the most recently-run command
31
+ attr_reader :last_err
32
+
33
+ # @return [String] the most recently-run command
34
+ attr_reader :last_msg
35
+
36
+ # set the command line flags (undefined for now).
37
+ # These vary between gui/cli
38
+ flag :get_pref
39
+ flag :set_pref
40
+ flag :save_prefs
41
+ flag :use_board
42
+ flag :install_boards
43
+ flag :install_library
44
+ flag :verify
45
+
46
+ def initialize
47
+ @prefs_cache = {}
48
+ @prefs_fetched = false
49
+ @library_is_indexed = false
50
+ @last_out = ""
51
+ @last_err = ""
52
+ @last_msg = ""
53
+ end
54
+
55
+ # Convert a preferences dump into a flat hash
56
+ # @param arduino_output [String] The raw Arduino executable output
57
+ # @return [Hash] preferences as a hash
58
+ def parse_pref_string(arduino_output)
59
+ lines = arduino_output.split("\n").select { |l| l.include? "=" }
60
+ ret = lines.each_with_object({}) do |e, acc|
61
+ parts = e.split("=", 2)
62
+ acc[parts[0]] = parts[1]
63
+ acc
64
+ end
65
+ ret
66
+ end
67
+
68
+ # @return [String] the path to the Arduino libraries directory
69
+ def _lib_dir
70
+ "<lib dir not defined>"
71
+ end
72
+
73
+ # fetch preferences in their raw form
74
+ # @return [String] Preferences as a set of lines
75
+ def _prefs_raw
76
+ resp = run_and_capture(flag_get_pref)
77
+ return nil unless resp[:success]
78
+ resp[:out]
79
+ end
80
+
81
+ # Get the Arduino preferences, from cache if possible
82
+ # @return [Hash] The full set of preferences
83
+ def prefs
84
+ prefs_raw = _prefs_raw unless @prefs_fetched
85
+ return nil if prefs_raw.nil?
86
+ @prefs_cache = parse_pref_string(prefs_raw)
87
+ @prefs_cache.clone
88
+ end
89
+
90
+ # get a preference key
91
+ # @param key [String] The preferences key to look up
92
+ # @return [String] The preference value
93
+ def get_pref(key)
94
+ data = @prefs_fetched ? @prefs_cache : prefs
95
+ data[key]
96
+ end
97
+
98
+ # underlying preference-setter.
99
+ # @param key [String] The preference name
100
+ # @param value [String] The value to set to
101
+ # @return [bool] whether the command succeeded
102
+ def _set_pref(key, value)
103
+ run_and_capture(flag_set_pref, "#{key}=#{value}", flag_save_prefs)[:success]
104
+ end
105
+
106
+ # set a preference key/value pair, and update the cache.
107
+ # @param key [String] the preference key
108
+ # @param value [String] the preference value
109
+ # @return [bool] whether the command succeeded
110
+ def set_pref(key, value)
111
+ success = _set_pref(key, value)
112
+ @prefs_cache[key] = value if success
113
+ success
114
+ end
115
+
116
+ # run the arduino command
117
+ def _run(*args, **kwargs)
118
+ raise "Ian needs to implement this in a subclass #{args} #{kwargs}"
119
+ end
120
+
121
+ # build and run the arduino command
122
+ def run(*args, **kwargs)
123
+ # do some work to extract & merge environment variables if they exist
124
+ has_env = !args.empty? && args[0].class == Hash
125
+ env_vars = has_env ? args[0] : {}
126
+ actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args
127
+ full_args = @base_cmd + actual_args
128
+ full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args
129
+
130
+ shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ")
131
+ @last_msg = " $ #{shell_vars} #{full_args.join(' ')}"
132
+ _run(*full_cmd, **kwargs)
133
+ end
134
+
135
+ # run a command and capture its output
136
+ # @return [Hash] {:out => String, :err => String, :success => bool}
137
+ def run_and_capture(*args, **kwargs)
138
+ pipe_out, pipe_out_wr = IO.pipe
139
+ pipe_err, pipe_err_wr = IO.pipe
140
+ our_kwargs = { out: pipe_out_wr, err: pipe_err_wr }
141
+ eventual_kwargs = our_kwargs.merge(kwargs)
142
+ success = run(*args, **eventual_kwargs)
143
+ pipe_out_wr.close
144
+ pipe_err_wr.close
145
+ str_out = pipe_out.read
146
+ str_err = pipe_err.read
147
+ pipe_out.close
148
+ pipe_err.close
149
+ @last_err = str_err
150
+ @last_out = str_out
151
+ { out: str_out, err: str_err, success: success }
152
+ end
153
+
154
+ # run a command and don't capture its output, but use the same signature
155
+ # @return [Hash] {:out => String, :err => String, :success => bool}
156
+ def run_wrap(*args, **kwargs)
157
+ success = run(*args, **kwargs)
158
+ { out: "NOPE, use run_and_capture", err: "NOPE, use run_and_capture", success: success }
159
+ end
160
+
161
+ # check whether a board is installed
162
+ # we do this by just selecting a board.
163
+ # the arduino binary will error if unrecognized and do a successful no-op if it's installed
164
+ # @param boardname [String] The board to test
165
+ # @return [bool] Whether the board is installed
166
+ def board_installed?(boardname)
167
+ run_and_capture(flag_use_board, boardname)[:success]
168
+ end
169
+
170
+ # install a board by name
171
+ # @param name [String] the board name
172
+ # @return [bool] whether the command succeeded
173
+ def install_boards(boardfamily)
174
+ # TODO: find out why IO.pipe fails but File::NULL succeeds :(
175
+ result = run_and_capture(flag_install_boards, boardfamily)
176
+ already_installed = result[:err].include?("Platform is already installed!")
177
+ result[:success] || already_installed
178
+ end
179
+
180
+ # install a library by name
181
+ # @param name [String] the library name
182
+ # @return [bool] whether the command succeeded
183
+ def install_library(library_name)
184
+ # workaround for https://github.com/arduino/Arduino/issues/3535
185
+ # use a dummy library name but keep open the possiblity that said library
186
+ # might be selected by choice for installation
187
+ workaround_lib = "USBHost"
188
+ unless @library_is_indexed || workaround_lib == library_name
189
+ @library_is_indexed = run_and_capture(flag_install_library, workaround_lib)
190
+ end
191
+
192
+ # actual installation
193
+ result = run_and_capture(flag_install_library, library_name)
194
+
195
+ # update flag if necessary
196
+ @library_is_indexed = (@library_is_indexed || result[:success]) if library_name == workaround_lib
197
+ result[:success]
198
+ end
199
+
200
+ # generate the (very likely) path of a library given its name
201
+ # @param library_name [String] The name of the library
202
+ # @return [String] The fully qualified library name
203
+ def library_path(library_name)
204
+ File.join(_lib_dir, library_name)
205
+ end
206
+
207
+ # update the library index
208
+ # @return [bool] Whether the update succeeded
209
+ def update_library_index
210
+ # install random lib so the arduino IDE grabs a new library index
211
+ # see: https://github.com/arduino/Arduino/issues/3535
212
+ install_library("USBHost")
213
+ end
214
+
215
+ # use a particular board for compilation
216
+ # @param boardname [String] The board to use
217
+ # @return [bool] whether the command succeeded
218
+ def use_board(boardname)
219
+ run_and_capture(flag_use_board, boardname, flag_save_prefs)[:success]
220
+ end
221
+
222
+ # use a particular board for compilation, installing it if necessary
223
+ # @param boardname [String] The board to use
224
+ # @return [bool] whether the command succeeded
225
+ def use_board!(boardname)
226
+ return true if use_board(boardname)
227
+ boardfamily = boardname.split(":")[0..1].join(":")
228
+ puts "Board '#{boardname}' not found; attempting to install '#{boardfamily}'"
229
+ return false unless install_boards(boardfamily) # guess board family from first 2 :-separated fields
230
+ use_board(boardname)
231
+ end
232
+
233
+ # @param path [String] The sketch to verify
234
+ # @return [bool] whether the command succeeded
235
+ def verify_sketch(path)
236
+ ext = File.extname path
237
+ unless ext.casecmp(".ino").zero?
238
+ @last_msg = "Refusing to verify sketch with '#{ext}' extension -- rename it to '.ino'!"
239
+ return false
240
+ end
241
+ unless File.exist? path
242
+ @last_msg = "Can't verify Sketch at nonexistent path '#{path}'!"
243
+ return false
244
+ end
245
+ ret = run_and_capture(flag_verify, path)
246
+ puts "=============="
247
+ puts ret[:out]
248
+ puts "--------------"
249
+ puts ret[:err]
250
+ ret[:success]
251
+ end
252
+
253
+ # ensure that the given library is installed, or symlinked as appropriate
254
+ # return the path of the prepared library, or nil
255
+ # @param path [String] library to use
256
+ # @return [String] the path of the installed library
257
+ def install_local_library(path)
258
+ realpath = File.expand_path(path)
259
+ library_name = File.basename(realpath)
260
+ destination_path = library_path(library_name)
261
+
262
+ # things get weird if the sketchbook contains the library.
263
+ # check that first
264
+ if File.exist? destination_path
265
+ uhoh = "There is already a library '#{library_name}' in the library directory"
266
+ return destination_path if destination_path == realpath
267
+
268
+ # maybe it's a symlink? that would be OK
269
+ if File.symlink?(destination_path)
270
+ return destination_path if File.readlink(destination_path) == realpath
271
+ @last_msg = "#{uhoh} and it's not symlinked to #{realpath}"
272
+ return nil
273
+ end
274
+
275
+ @last_msg = "#{uhoh}. It may need to be removed manually."
276
+ return nil
277
+ end
278
+
279
+ # install the library
280
+ FileUtils.ln_s(realpath, destination_path)
281
+ destination_path
282
+ end
283
+
284
+ # @param installed_library_path [String] The library to query
285
+ # @return [Array<String>] Example sketch files
286
+ def library_examples(installed_library_path)
287
+ example_path = File.join(installed_library_path, "examples")
288
+ examples = Pathname.new(example_path).children.select(&:directory?).map(&:to_path).map(&File.method(:basename))
289
+ files = examples.map do |e|
290
+ proj_file = File.join(example_path, e, "#{e}.ino")
291
+ File.exist?(proj_file) ? proj_file : nil
292
+ end
293
+ files.reject(&:nil?)
294
+ end
295
+ end
296
+ end