arduino_ci 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,16 +1,10 @@
1
1
  require 'pathname'
2
2
  require "arduino_ci/host"
3
- require "arduino_ci/arduino_cmd_osx"
4
- require "arduino_ci/arduino_cmd_linux"
5
- require "arduino_ci/arduino_cmd_windows"
6
- require "arduino_ci/arduino_cmd_linux_builder"
3
+ require "arduino_ci/arduino_backend"
7
4
  require "arduino_ci/arduino_downloader_osx"
8
5
  require "arduino_ci/arduino_downloader_linux"
9
-
10
6
  require "arduino_ci/arduino_downloader_windows" if ArduinoCI::Host.os == :windows
11
7
 
12
- DESIRED_ARDUINO_IDE_VERSION = "1.8.6".freeze
13
-
14
8
  module ArduinoCI
15
9
 
16
10
  class ArduinoInstallationError < StandardError; end
@@ -18,85 +12,29 @@ module ArduinoCI
18
12
  # Manage the OS-specific install location of Arduino
19
13
  class ArduinoInstallation
20
14
 
15
+ DESIRED_ARDUINO_CLI_VERSION = "0.13.0".freeze
16
+
21
17
  class << self
22
18
 
23
19
  # attempt to find a workable Arduino executable across platforms
24
20
  #
25
21
  # Autolocation assumed to be an expensive operation
26
- # @return [ArduinoCI::ArduinoCmd] an instance of the command or nil if it can't be found
22
+ # @return [ArduinoCI::ArduinoBackend] an instance of the command or nil if it can't be found
27
23
  def autolocate
28
- ret = nil
29
- case Host.os
30
- when :osx then
31
- ret = autolocate_osx
32
- when :linux then
33
- loc = ArduinoDownloaderLinux.autolocated_executable
34
- return nil if loc.nil?
35
-
36
- ret = ArduinoCmdLinux.new
37
- ret.base_cmd = [loc]
38
- ret.binary_path = Pathname.new(loc)
39
- when :windows then
40
- loc = ArduinoDownloaderWindows.autolocated_executable
41
- return nil if loc.nil?
42
-
43
- ret = ArduinoCmdWindows.new
44
- ret.base_cmd = [loc]
45
- ret.binary_path = Pathname.new(loc)
24
+ downloader_class = case Host.os
25
+ when :osx then ArduinoDownloaderOSX
26
+ when :linux then ArduinoDownloaderLinux
27
+ when :windows then ArduinoDownloaderWindows
46
28
  end
47
- ret
48
- end
49
-
50
- # @return [ArduinoCI::ArduinoCmdOSX] an instance of the command or nil if it can't be found
51
- def autolocate_osx
52
- osx_root = ArduinoDownloaderOSX.autolocated_installation
53
- return nil if osx_root.nil?
54
- return nil unless File.exist? osx_root
55
-
56
- launchers = [
57
- # try a hack that skips splash screen
58
- # from https://github.com/arduino/Arduino/issues/1970#issuecomment-321975809
59
- [
60
- "java",
61
- "-cp",
62
- "#{osx_root}/Contents/Java/*",
63
- "-DAPP_DIR=#{osx_root}/Contents/Java",
64
- "-Dfile.encoding=UTF-8",
65
- "-Dapple.awt.UIElement=true",
66
- "-Xms128M",
67
- "-Xmx512M",
68
- "processing.app.Base",
69
- ],
70
- # failsafe way
71
- [File.join(osx_root, "Contents", "MacOS", "Arduino")]
72
- ]
73
29
 
74
- # create return and find a command launcher that works
75
- ret = ArduinoCmdOSX.new
76
- launchers.each do |launcher|
77
- # test whether this method successfully launches the IDE
78
- # note that "successful launch" involves a command that will fail,
79
- # because that's faster than any command which succeeds. what we
80
- # don't want to see is a java error.
81
- args = launcher + ["--bogus-option"]
82
- result = Host.run_and_capture(*args)
30
+ loc = downloader_class.autolocated_executable
31
+ return nil if loc.nil?
83
32
 
84
- # NOTE: Was originally searching for "Error: unknown option: --bogus-option"
85
- # but also need to find "Erreur: option inconnue : --bogus-option"
86
- # and who knows how many other languages.
87
- # For now, just search for the end of the error and hope that the java-style
88
- # launch of this won't include a similar string in it
89
- next unless result[:err].include? ": --bogus-option"
90
-
91
- ret.base_cmd = launcher
92
- ret.binary_path = Pathname.new(osx_root)
93
- return ret
94
- end
95
- nil
33
+ ArduinoBackend.new(loc)
96
34
  end
97
35
 
98
36
  # Attempt to find a workable Arduino executable across platforms, and install it if we don't
99
- # @return [ArduinoCI::ArduinoCmd] an instance of a command
37
+ # @return [ArduinoCI::ArduinoBackend] an instance of a command
100
38
  def autolocate!(output = $stdout)
101
39
  candidate = autolocate
102
40
  return candidate unless candidate.nil?
@@ -109,7 +47,7 @@ module ArduinoCI
109
47
 
110
48
  # Forcibly install Arduino from the web
111
49
  # @return [bool] Whether the command succeeded
112
- def force_install(output = $stdout, version = DESIRED_ARDUINO_IDE_VERSION)
50
+ def force_install(output = $stdout, version = DESIRED_ARDUINO_CLI_VERSION)
113
51
  worker_class = case Host.os
114
52
  when :osx then ArduinoDownloaderOSX
115
53
  when :windows then ArduinoDownloaderWindows
@@ -232,13 +232,6 @@ module ArduinoCI
232
232
  deep_clone(defn)
233
233
  end
234
234
 
235
- # Whether a package is built-in to arduino
236
- # @param package [String] the package name (e.g. "arduino:avr")
237
- # @return [bool]
238
- def package_builtin?(package)
239
- package.start_with?("arduino:")
240
- end
241
-
242
235
  # the URL that gives the download info for a given package (a JSON file).
243
236
  # this is NOT where the package comes from.
244
237
  # @param package [String] the package name (e.g. "arduino:avr")
@@ -14,15 +14,21 @@ module ArduinoCI
14
14
  # Information about an Arduino CPP library, specifically for compilation purposes
15
15
  class CppLibrary
16
16
 
17
- # @return [Pathname] The path to the library being tested
18
- attr_reader :base_dir
17
+ # @return [String] The official library properties file name
18
+ LIBRARY_PROPERTIES_FILE = "library.properties".freeze
19
19
 
20
- # @return [Pathname] The path to the Arduino 3rd-party library directory
21
- attr_reader :arduino_lib_dir
20
+ # @return [String] The "official" name of the library, which can include spaces (in a way that the lib dir won't)
21
+ attr_reader :name
22
+
23
+ # @return [ArduinoBackend] The backend support for this library
24
+ attr_reader :backend
22
25
 
23
26
  # @return [Array<Pathname>] The set of artifacts created by this class (note: incomplete!)
24
27
  attr_reader :artifacts
25
28
 
29
+ # @return [Array<Pathname>] The set of directories that should be excluded from compilation
30
+ attr_reader :exclude_dirs
31
+
26
32
  # @return [String] STDERR from the last command
27
33
  attr_reader :last_err
28
34
 
@@ -35,30 +41,100 @@ module ArduinoCI
35
41
  # @return [Array<Pathname>] Directories suspected of being vendor-bundle
36
42
  attr_reader :vendor_bundle_cache
37
43
 
38
- # @param base_dir [Pathname] The path to the library being tested
39
- # @param arduino_lib_dir [Pathname] The path to the libraries directory
40
- # @param exclude_dirs [Array<Pathname>] Directories that should be excluded from compilation
41
- def initialize(base_dir, arduino_lib_dir, exclude_dirs)
42
- raise ArgumentError, 'base_dir is not a Pathname' unless base_dir.is_a? Pathname
43
- raise ArgumentError, 'arduino_lib_dir is not a Pathname' unless arduino_lib_dir.is_a? Pathname
44
- raise ArgumentError, 'exclude_dir is not an array of Pathnames' unless exclude_dirs.is_a?(Array)
45
- raise ArgumentError, 'exclude_dir array contains non-Pathname elements' unless exclude_dirs.all? { |p| p.is_a? Pathname }
46
-
47
- @base_dir = base_dir
48
- @exclude_dirs = exclude_dirs
49
- @arduino_lib_dir = arduino_lib_dir.expand_path
44
+ # @param friendly_name [String] The "official" name of the library, which can contain spaces
45
+ # @param backend [ArduinoBackend] The support backend
46
+ def initialize(friendly_name, backend)
47
+ raise ArgumentError, "friendly_name is not a String (got #{friendly_name.class})" unless friendly_name.is_a? String
48
+ raise ArgumentError, 'backend is not a ArduinoBackend' unless backend.is_a? ArduinoBackend
49
+
50
+ @name = friendly_name
51
+ @backend = backend
52
+ @info_cache = nil
50
53
  @artifacts = []
51
54
  @last_err = ""
52
55
  @last_out = ""
53
56
  @last_msg = ""
54
57
  @has_libasan_cache = {}
55
58
  @vendor_bundle_cache = nil
59
+ @exclude_dirs = []
60
+ end
61
+
62
+ # Generate a guess as to the on-disk (coerced character) name of this library
63
+ #
64
+ # @TODO: delegate this to the backend in some way? It uses "official" names for install, but dir names in lists :(
65
+ # @param friendly_name [String] The library name as it might appear in library manager
66
+ # @return [String] How the path will be stored on disk -- spaces are coerced to underscores
67
+ def self.library_directory_name(friendly_name)
68
+ friendly_name.tr(" ", "_")
69
+ end
70
+
71
+ # Generate a guess as to the on-disk (coerced character) name of this library
72
+ #
73
+ # @TODO: delegate this to the backend in some way? It uses "official" names for install, but dir names in lists :(
74
+ # @return [String] How the path will be stored on disk -- spaces are coerced to underscores
75
+ def name_on_disk
76
+ self.class.library_directory_name(@name)
77
+ end
78
+
79
+ # Get the path to this library, whether or not it exists
80
+ # @return [Pathname] The fully qualified library path
81
+ def path
82
+ @backend.lib_dir + name_on_disk
83
+ end
84
+
85
+ # Determine whether a library is present in the lib dir
86
+ #
87
+ # Note that `true` doesn't guarantee that the library is valid/installed
88
+ # and `false` doesn't guarantee that the library isn't built-in
89
+ #
90
+ # @return [bool]
91
+ def installed?
92
+ path.exist?
93
+ end
94
+
95
+ # install a library by name
96
+ # @param version [String] the version to install
97
+ # @param recursive [bool] whether to also install its dependencies
98
+ # @return [bool] whether the command succeeded
99
+ def install(version = nil, recursive = false)
100
+ return true if installed? && !recursive
101
+
102
+ fqln = version.nil? ? @name : "#{@name}@#{version}"
103
+ result = if recursive
104
+ @backend.run_and_capture("lib", "install", fqln)
105
+ else
106
+ @backend.run_and_capture("lib", "install", "--no-deps", fqln)
107
+ end
108
+ result[:success]
109
+ end
110
+
111
+ # information about the library as reported by the backend
112
+ # @return [Hash] the metadata object
113
+ def info
114
+ return nil unless installed?
115
+
116
+ # note that if the library isn't found, we're going to do a lot of cache attempts...
117
+ if @info_cache.nil?
118
+ @info_cache = @backend.installed_libraries.find do |l|
119
+ lib_info = l["library"]
120
+ Pathname.new(lib_info["install_dir"]).realpath == path.realpath
121
+ end
122
+ end
123
+
124
+ @info_cache
125
+ end
126
+
127
+ # @param installed_library_path [String] The library to query
128
+ # @return [Array<String>] Example sketch files
129
+ def example_sketches
130
+ reported_dirs = info["library"]["examples"].map(&Pathname::method(:new))
131
+ reported_dirs.map { |e| e + e.basename.sub_ext(".ino") }.select(&:exist?).sort_by(&:to_s)
56
132
  end
57
133
 
58
134
  # The expected path to the library.properties file (i.e. even if it does not exist)
59
135
  # @return [Pathname]
60
136
  def library_properties_path
61
- @base_dir + "library.properties"
137
+ path + LIBRARY_PROPERTIES_FILE
62
138
  end
63
139
 
64
140
  # Whether library.properties definitions for this library exist
@@ -68,16 +144,29 @@ module ArduinoCI
68
144
  lib_props.exist? && lib_props.file?
69
145
  end
70
146
 
147
+ # Library properties
148
+ # @return [LibraryProperties] The library.properties metadata wrapper for this library
149
+ def library_properties
150
+ return nil unless library_properties?
151
+
152
+ LibraryProperties.new(library_properties_path)
153
+ end
154
+
155
+ # Set directories that should be excluded from compilation
156
+ # @param rval [Array] Array of strings or pathnames that will be coerced to pathnames
157
+ def exclude_dirs=(rval)
158
+ @exclude_dirs = rval.map { |d| d.is_a?(Pathname) ? d : Pathname.new(d) }
159
+ end
160
+
71
161
  # Decide whether this is a 1.5-compatible library
72
162
  #
73
- # according to https://arduino.github.io/arduino-cli/latest/library-specification
74
- #
75
- # Should match logic from https://github.com/arduino/arduino-cli/blob/master/arduino/libraries/loader.go
163
+ # This should be according to https://arduino.github.io/arduino-cli/latest/library-specification
164
+ # but we rely on the cli to decide for us
76
165
  # @return [bool]
77
166
  def one_point_five?
78
167
  return false unless library_properties?
79
168
 
80
- src_dir = (@base_dir + "src")
169
+ src_dir = path + "src"
81
170
  src_dir.exist? && src_dir.directory?
82
171
  end
83
172
 
@@ -88,9 +177,9 @@ module ArduinoCI
88
177
  # That gets us the vendor directory (or multiple directories). We can check
89
178
  # if the given path is contained by any of those.
90
179
  #
91
- # @param path [Pathname] The path to check
180
+ # @param some_path [Pathname] The path to check
92
181
  # @return [bool]
93
- def vendor_bundle?(path)
182
+ def vendor_bundle?(some_path)
94
183
  # Cache bundle information, as it is (1) time consuming to fetch and (2) not going to change while we run
95
184
  if @vendor_bundle_cache.nil?
96
185
  bundle_info = Host.run_and_capture("bundle show --paths")
@@ -125,7 +214,7 @@ module ArduinoCI
125
214
 
126
215
  # With vendor bundles located, check this file against those
127
216
  @vendor_bundle_cache.any? do |gem_path|
128
- path.ascend do |part|
217
+ some_path.ascend do |part|
129
218
  break true if gem_path == part
130
219
  end
131
220
  end
@@ -135,13 +224,13 @@ module ArduinoCI
135
224
  #
136
225
  # @param path [Pathname] The path to check
137
226
  # @return [bool]
138
- def in_tests_dir?(path)
227
+ def in_tests_dir?(sourcefile_path)
139
228
  return false unless tests_dir.exist?
140
229
 
141
230
  tests_dir_aliases = [tests_dir, tests_dir.realpath]
142
231
  # we could do this but some rubies don't return an enumerator for ascend
143
232
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
144
- path.ascend do |part|
233
+ sourcefile_path.ascend do |part|
145
234
  return true if tests_dir_aliases.include?(part)
146
235
  end
147
236
  false
@@ -151,11 +240,11 @@ module ArduinoCI
151
240
  #
152
241
  # @param path [Pathname] The path to check
153
242
  # @return [bool]
154
- def in_exclude_dir?(path)
243
+ def in_exclude_dir?(sourcefile_path)
155
244
  # we could do this but some rubies don't return an enumerator for ascend
156
245
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
157
- path.ascend do |part|
158
- return true if exclude_dir.any? { |p| p.realpath == part }
246
+ sourcefile_path.ascend do |part|
247
+ return true if exclude_dir.any? { |p| p.realpath == part.realpath }
159
248
  end
160
249
  false
161
250
  end
@@ -166,42 +255,26 @@ module ArduinoCI
166
255
  # @param gcc_binary [String]
167
256
  def libasan?(gcc_binary)
168
257
  unless @has_libasan_cache.key?(gcc_binary)
169
- file = Tempfile.new(["arduino_ci_libasan_check", ".cpp"])
170
- begin
258
+ Tempfile.create(["arduino_ci_libasan_check", ".cpp"]) do |file|
171
259
  file.write "int main(){}"
172
260
  file.close
173
261
  @has_libasan_cache[gcc_binary] = run_gcc(gcc_binary, "-o", "/dev/null", "-fsanitize=address", file.path)
174
- ensure
175
- file.delete
176
262
  end
177
263
  end
178
264
  @has_libasan_cache[gcc_binary]
179
265
  end
180
266
 
181
- # Library properties
182
- def library_properties
183
- return nil unless library_properties?
184
-
185
- LibraryProperties.new(library_properties_path)
186
- end
187
-
188
- # Get a list of all dependencies as defined in library.properties
189
- # @return [Array<String>] The library names of the dependencies (not the paths)
190
- def arduino_library_dependencies
191
- return nil unless library_properties?
192
-
193
- library_properties.depends
194
- end
195
-
196
267
  # Get a list of all CPP source files in a directory and its subdirectories
197
268
  # @param some_dir [Pathname] The directory in which to begin the search
198
269
  # @param extensions [Array<Sring>] The set of allowable file extensions
199
270
  # @return [Array<Pathname>] The paths of the found files
200
271
  def code_files_in(some_dir, extensions)
201
272
  raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
202
- return [] unless some_dir.exist? && some_dir.directory?
203
273
 
204
- files = some_dir.realpath.children.reject(&:directory?)
274
+ full_dir = path + some_dir
275
+ return [] unless full_dir.exist? && full_dir.directory?
276
+
277
+ files = full_dir.children.reject(&:directory?)
205
278
  cpp = files.select { |path| extensions.include?(path.extname.downcase) }
206
279
  not_hidden = cpp.reject { |path| path.basename.to_s.start_with?(".") }
207
280
  not_hidden.sort_by(&:to_s)
@@ -215,17 +288,18 @@ module ArduinoCI
215
288
  raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
216
289
  return [] unless some_dir.exist? && some_dir.directory?
217
290
 
218
- real = some_dir.realpath
219
- Find.find(real).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten
291
+ Find.find(some_dir).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten
220
292
  end
221
293
 
222
- # Header files that are part of the project library under test
294
+ # Source files that are part of the library under test
295
+ # @param extensions [Array<String>] the allowed extensions (or, the ones we're looking for)
223
296
  # @return [Array<Pathname>]
224
- def header_files
297
+ def source_files(extensions)
298
+ source_dir = Pathname.new(info["library"]["source_dir"])
225
299
  ret = if one_point_five?
226
- code_files_in_recursive(@base_dir + "src", HPP_EXTENSIONS)
300
+ code_files_in_recursive(source_dir, extensions)
227
301
  else
228
- [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, HPP_EXTENSIONS) }.flatten
302
+ [source_dir, source_dir + "utility"].map { |d| code_files_in(d, extensions) }.flatten
229
303
  end
230
304
 
231
305
  # note to future troubleshooter: some of these tests may not be relevant, but at the moment at
@@ -233,18 +307,16 @@ module ArduinoCI
233
307
  ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) }
234
308
  end
235
309
 
310
+ # Header files that are part of the project library under test
311
+ # @return [Array<Pathname>]
312
+ def header_files
313
+ source_files(HPP_EXTENSIONS)
314
+ end
315
+
236
316
  # CPP files that are part of the project library under test
237
317
  # @return [Array<Pathname>]
238
318
  def cpp_files
239
- ret = if one_point_five?
240
- code_files_in_recursive(@base_dir + "src", CPP_EXTENSIONS)
241
- else
242
- [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten
243
- end
244
-
245
- # note to future troubleshooter: some of these tests may not be relevant, but at the moment at
246
- # least some of them are tied to existing features
247
- ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) }
319
+ source_files(CPP_EXTENSIONS)
248
320
  end
249
321
 
250
322
  # CPP files that are part of the arduino mock library we're providing
@@ -269,13 +341,13 @@ module ArduinoCI
269
341
  # Returns the Pathnames for all paths to exclude from testing and compilation
270
342
  # @return [Array<Pathname>]
271
343
  def exclude_dir
272
- @exclude_dirs.map { |p| Pathname.new(@base_dir) + p }.select(&:exist?)
344
+ @exclude_dirs.map { |p| Pathname.new(path) + p }.select(&:exist?)
273
345
  end
274
346
 
275
347
  # The directory where we expect to find unit test defintions provided by the user
276
348
  # @return [Pathname]
277
349
  def tests_dir
278
- Pathname.new(@base_dir) + "test"
350
+ Pathname.new(path) + "test"
279
351
  end
280
352
 
281
353
  # The files provided by the user that contain unit tests
@@ -311,18 +383,33 @@ module ArduinoCI
311
383
  @last_err
312
384
  end
313
385
 
314
- # Arduino library directories containing sources -- only those of the dependencies
315
- # @return [Array<Pathname>]
316
- def arduino_library_src_dirs(aux_libraries)
386
+ # Get a list of all dependencies as defined in library.properties
387
+ # @return [Array<String>] The library names of the dependencies (not the paths)
388
+ def arduino_library_dependencies
389
+ return [] unless library_properties?
390
+ return [] if library_properties.depends.nil?
391
+
392
+ library_properties.depends
393
+ end
394
+
395
+ # Arduino library dependencies all the way down, installing if they are not present
396
+ # @return [Array<String>] The library names of the dependencies (not the paths)
397
+ def all_arduino_library_dependencies!(additional_libraries = [])
317
398
  # Pull in all possible places that headers could live, according to the spec:
318
399
  # https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification
400
+ recursive = (additional_libraries + arduino_library_dependencies).map do |n|
401
+ other_lib = self.class.new(n, @backend)
402
+ other_lib.install unless other_lib.installed?
403
+ other_lib.all_arduino_library_dependencies!
404
+ end.flatten
405
+ ret = (additional_libraries + recursive).uniq
406
+ ret
407
+ end
319
408
 
320
- aux_libraries.map do |d|
321
- # library manager coerces spaces in package names to underscores
322
- # see https://github.com/ianfixes/arduino_ci/issues/132#issuecomment-518857059
323
- legal_dir = d.tr(" ", "_")
324
- self.class.new(@arduino_lib_dir + legal_dir, @arduino_lib_dir, @exclude_dirs).header_dirs
325
- end.flatten.uniq
409
+ # Arduino library directories containing sources -- only those of the dependencies
410
+ # @return [Array<Pathname>]
411
+ def arduino_library_src_dirs(aux_libraries)
412
+ all_arduino_library_dependencies!(aux_libraries).map { |l| self.class.new(l, @backend).header_dirs }.flatten.uniq
326
413
  end
327
414
 
328
415
  # GCC command line arguments for including aux libraries
@@ -415,9 +502,9 @@ module ArduinoCI
415
502
 
416
503
  # combine library.properties defs (if existing) with config file.
417
504
  # TODO: as much as I'd like to rely only on the properties file(s), I think that would prevent testing 1.0-spec libs
418
- full_aux_libraries = arduino_library_dependencies.nil? ? aux_libraries : aux_libraries + arduino_library_dependencies
419
- arg_sets << test_args(full_aux_libraries, ci_gcc_config)
420
- arg_sets << cpp_files_libraries(full_aux_libraries).map(&:to_s)
505
+ full_dependencies = all_arduino_library_dependencies!(aux_libraries)
506
+ arg_sets << test_args(full_dependencies, ci_gcc_config)
507
+ arg_sets << cpp_files_libraries(full_dependencies).map(&:to_s)
421
508
  arg_sets << [test_file.to_s]
422
509
  args = arg_sets.flatten(1)
423
510
  return nil unless run_gcc(gcc_binary, *args)