arduino_ci 0.1.20 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -19
  3. data/REFERENCE.md +625 -0
  4. data/cpp/arduino/Arduino.h +1 -2
  5. data/cpp/arduino/AvrMath.h +117 -17
  6. data/cpp/arduino/Client.h +26 -0
  7. data/cpp/arduino/EEPROM.h +64 -0
  8. data/cpp/arduino/Godmode.cpp +38 -19
  9. data/cpp/arduino/Godmode.h +88 -22
  10. data/cpp/arduino/HardwareSerial.h +9 -28
  11. data/cpp/arduino/IPAddress.h +59 -0
  12. data/cpp/arduino/MockEventQueue.h +86 -0
  13. data/cpp/arduino/PinHistory.h +64 -24
  14. data/cpp/arduino/Print.h +9 -12
  15. data/cpp/arduino/Printable.h +8 -0
  16. data/cpp/arduino/SPI.h +11 -3
  17. data/cpp/arduino/Server.h +5 -0
  18. data/cpp/arduino/Udp.h +27 -0
  19. data/cpp/arduino/Wire.h +234 -0
  20. data/cpp/arduino/avr/io.h +10 -1
  21. data/cpp/arduino/avr/pgmspace.h +76 -46
  22. data/cpp/arduino/ci/StreamTape.h +36 -0
  23. data/cpp/unittest/ArduinoUnitTests.h +1 -0
  24. data/cpp/unittest/Compare.h +91 -897
  25. data/cpp/unittest/OstreamHelpers.h +9 -0
  26. data/exe/arduino_ci.rb +401 -0
  27. data/exe/arduino_ci_remote.rb +2 -385
  28. data/lib/arduino_ci.rb +1 -0
  29. data/lib/arduino_ci/arduino_cmd.rb +13 -9
  30. data/lib/arduino_ci/arduino_downloader.rb +5 -4
  31. data/lib/arduino_ci/arduino_installation.rb +5 -5
  32. data/lib/arduino_ci/ci_config.rb +12 -0
  33. data/lib/arduino_ci/cpp_library.rb +152 -25
  34. data/lib/arduino_ci/installed_cpp_library.rb +0 -0
  35. data/lib/arduino_ci/library_properties.rb +86 -0
  36. data/lib/arduino_ci/version.rb +1 -1
  37. data/misc/default.yml +50 -3
  38. metadata +23 -13
  39. data/cpp/arduino/Arduino.h.orig +0 -143
  40. data/cpp/arduino/Nullptr.h +0 -7
  41. data/cpp/arduino/ci/Queue.h +0 -73
  42. data/exe/libasan.rb +0 -29
@@ -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>
@@ -6,6 +6,9 @@ WORKAROUND_LIB = "USBHost".freeze
6
6
 
7
7
  module ArduinoCI
8
8
 
9
+ # To report errors that we can't resolve or possibly even explain
10
+ class ArduinoExecutionError < StandardError; end
11
+
9
12
  # Wrap the Arduino executable. This requires, in some cases, a faked display.
10
13
  class ArduinoCmd
11
14
 
@@ -42,14 +45,14 @@ module ArduinoCI
42
45
  attr_reader :last_msg
43
46
 
44
47
  # set the command line flags (undefined for now).
45
- # These vary between gui/cli
46
- flag :get_pref
47
- flag :set_pref
48
- flag :save_prefs
49
- flag :use_board
50
- flag :install_boards
51
- flag :install_library
52
- flag :verify
48
+ # These vary between gui/cli. Inline comments added for greppability
49
+ flag :get_pref # flag_get_pref
50
+ flag :set_pref # flag_set_pref
51
+ flag :save_prefs # flag_save_prefs
52
+ flag :use_board # flag_use_board
53
+ flag :install_boards # flag_install_boards
54
+ flag :install_library # flag_install_library
55
+ flag :verify # flag_verify
53
56
 
54
57
  def initialize
55
58
  @prefs_cache = {}
@@ -82,7 +85,8 @@ module ArduinoCI
82
85
  # @return [String] Preferences as a set of lines
83
86
  def _prefs_raw
84
87
  resp = run_and_capture(flag_get_pref)
85
- return nil unless resp[:success]
88
+ fail_msg = "Arduino binary failed to operate as expected; you will have to troubleshoot it manually"
89
+ raise ArduinoExecutionError, "#{fail_msg}. The command was #{@last_msg}" unless resp[:success]
86
90
 
87
91
  @prefs_fetched = true
88
92
  resp[:out]
@@ -161,16 +161,17 @@ module ArduinoCI
161
161
  return false
162
162
  end
163
163
 
164
+ arduino_package = "Arduino #{@desired_ide_version} package"
164
165
  attempts = 0
165
166
 
166
167
  loop do
167
168
  if File.exist? package_file
168
- @output.puts "Arduino package seems to have been downloaded already" if attempts.zero?
169
+ @output.puts "#{arduino_package} seems to have been downloaded already" if attempts.zero?
169
170
  break
170
171
  elsif attempts >= DOWNLOAD_ATTEMPTS
171
172
  break @output.puts "After #{DOWNLOAD_ATTEMPTS} attempts, failed to download #{package_url}"
172
173
  else
173
- @output.print "Attempting to download Arduino package with #{downloader}"
174
+ @output.print "Attempting to download #{arduino_package} with #{downloader}"
174
175
  download
175
176
  @output.puts
176
177
  end
@@ -178,7 +179,7 @@ module ArduinoCI
178
179
  end
179
180
 
180
181
  if File.exist? extracted_file
181
- @output.puts "Arduino package seems to have been extracted already"
182
+ @output.puts "#{arduino_package} seems to have been extracted already"
182
183
  elsif File.exist? package_file
183
184
  @output.print "Extracting archive with #{extracter}"
184
185
  extract
@@ -186,7 +187,7 @@ module ArduinoCI
186
187
  end
187
188
 
188
189
  if File.exist? self.class.force_install_location
189
- @output.puts "Arduino package seems to have been installed already"
190
+ @output.puts "#{arduino_package} seems to have been installed already"
190
191
  elsif File.exist? extracted_file
191
192
  install
192
193
  else
@@ -110,11 +110,11 @@ module ArduinoCI
110
110
  # Forcibly install Arduino from the web
111
111
  # @return [bool] Whether the command succeeded
112
112
  def force_install(output = $stdout, version = DESIRED_ARDUINO_IDE_VERSION)
113
- worker_class = case Host.os
114
- when :osx then ArduinoDownloaderOSX
115
- when :windows then ArduinoDownloaderWindows
116
- when :linux then ArduinoDownloaderLinux
117
- end
113
+ worker_class = case Host.os
114
+ when :osx then ArduinoDownloaderOSX
115
+ when :windows then ArduinoDownloaderWindows
116
+ when :linux then ArduinoDownloaderLinux
117
+ end
118
118
  worker = worker_class.new(version, output)
119
119
  worker.execute
120
120
  end
@@ -28,6 +28,7 @@ UNITTEST_SCHEMA = {
28
28
  compilers: Array,
29
29
  platforms: Array,
30
30
  libraries: Array,
31
+ exclude_dirs: Array,
31
32
  testfiles: {
32
33
  select: Array,
33
34
  reject: Array,
@@ -256,6 +257,14 @@ module ArduinoCI
256
257
  @unittest_info[:compilers]
257
258
  end
258
259
 
260
+ # paths to exclude all files in for building and unitttests
261
+ # @return [Array<String>] The directories (relative to base dir) to exclude
262
+ def exclude_dirs
263
+ return [] if @unittest_info[:exclude_dirs].nil?
264
+
265
+ @unittest_info[:exclude_dirs]
266
+ end
267
+
259
268
  # platforms to build [the examples on]
260
269
  # @return [Array<String>] The platforms to build
261
270
  def platforms_to_build
@@ -293,9 +302,12 @@ module ArduinoCI
293
302
  return paths if @unittest_info[:testfiles].nil?
294
303
 
295
304
  ret = paths
305
+ # Check for array emptiness, otherwise nothing will be selected!
296
306
  unless @unittest_info[:testfiles][:select].nil? || @unittest_info[:testfiles][:select].empty?
297
307
  ret.select! { |p| unittest_info[:testfiles][:select].any? { |glob| p.basename.fnmatch(glob) } }
298
308
  end
309
+
310
+ # It's OK for the :reject array to be empty, that means nothing will be rejected by default
299
311
  unless @unittest_info[:testfiles][:reject].nil?
300
312
  ret.reject! { |p| unittest_info[:testfiles][:reject].any? { |glob| p.basename.fnmatch(glob) } }
301
313
  end
@@ -37,11 +37,15 @@ module ArduinoCI
37
37
 
38
38
  # @param base_dir [Pathname] The path to the library being tested
39
39
  # @param arduino_lib_dir [Pathname] The path to the libraries directory
40
- def initialize(base_dir, arduino_lib_dir)
40
+ # @param exclude_dirs [Array<Pathname>] Directories that should be excluded from compilation
41
+ def initialize(base_dir, arduino_lib_dir, exclude_dirs)
41
42
  raise ArgumentError, 'base_dir is not a Pathname' unless base_dir.is_a? Pathname
42
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 }
43
46
 
44
47
  @base_dir = base_dir
48
+ @exclude_dirs = exclude_dirs
45
49
  @arduino_lib_dir = arduino_lib_dir.expand_path
46
50
  @artifacts = []
47
51
  @last_err = ""
@@ -51,6 +55,32 @@ module ArduinoCI
51
55
  @vendor_bundle_cache = nil
52
56
  end
53
57
 
58
+ # The expected path to the library.properties file (i.e. even if it does not exist)
59
+ # @return [Pathname]
60
+ def library_properties_path
61
+ @base_dir + "library.properties"
62
+ end
63
+
64
+ # Whether library.properties definitions for this library exist
65
+ # @return [bool]
66
+ def library_properties?
67
+ lib_props = library_properties_path
68
+ lib_props.exist? && lib_props.file?
69
+ end
70
+
71
+ # Decide whether this is a 1.5-compatible library
72
+ #
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
76
+ # @return [bool]
77
+ def one_point_five?
78
+ return false unless library_properties?
79
+
80
+ src_dir = (@base_dir + "src")
81
+ src_dir.exist? && src_dir.directory?
82
+ end
83
+
54
84
  # Guess whether a file is part of the vendor bundle (indicating we should ignore it).
55
85
  #
56
86
  # A safe way to do this seems to be to check whether any of the installed gems
@@ -106,6 +136,8 @@ module ArduinoCI
106
136
  # @param path [Pathname] The path to check
107
137
  # @return [bool]
108
138
  def in_tests_dir?(path)
139
+ return false unless tests_dir.exist?
140
+
109
141
  tests_dir_aliases = [tests_dir, tests_dir.realpath]
110
142
  # we could do this but some rubies don't return an enumerator for ascend
111
143
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
@@ -115,6 +147,19 @@ module ArduinoCI
115
147
  false
116
148
  end
117
149
 
150
+ # Guess whether a file is part of any @excludes_dir dir (indicating library compilation should ignore it).
151
+ #
152
+ # @param path [Pathname] The path to check
153
+ # @return [bool]
154
+ def in_exclude_dir?(path)
155
+ # we could do this but some rubies don't return an enumerator for ascend
156
+ # 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 }
159
+ end
160
+ false
161
+ end
162
+
118
163
  # Check whether libasan (and by extension -fsanitizer=address) is supported
119
164
  #
120
165
  # This requires compilation of a sample program, and will be cached
@@ -133,43 +178,98 @@ module ArduinoCI
133
178
  @has_libasan_cache[gcc_binary]
134
179
  end
135
180
 
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
+
136
196
  # Get a list of all CPP source files in a directory and its subdirectories
137
197
  # @param some_dir [Pathname] The directory in which to begin the search
198
+ # @param extensions [Array<Sring>] The set of allowable file extensions
138
199
  # @return [Array<Pathname>] The paths of the found files
139
- def cpp_files_in(some_dir)
200
+ def code_files_in(some_dir, extensions)
140
201
  raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
141
202
  return [] unless some_dir.exist? && some_dir.directory?
142
203
 
143
- real = some_dir.realpath
144
- files = Find.find(real).map { |p| Pathname.new(p) }.reject(&:directory?)
145
- cpp = files.select { |path| CPP_EXTENSIONS.include?(path.extname.downcase) }
204
+ files = some_dir.realpath.children.reject(&:directory?)
205
+ cpp = files.select { |path| extensions.include?(path.extname.downcase) }
146
206
  not_hidden = cpp.reject { |path| path.basename.to_s.start_with?(".") }
147
207
  not_hidden.sort_by(&:to_s)
148
208
  end
149
209
 
210
+ # Get a list of all CPP source files in a directory and its subdirectories
211
+ # @param some_dir [Pathname] The directory in which to begin the search
212
+ # @param extensions [Array<Sring>] The set of allowable file extensions
213
+ # @return [Array<Pathname>] The paths of the found files
214
+ def code_files_in_recursive(some_dir, extensions)
215
+ raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
216
+ return [] unless some_dir.exist? && some_dir.directory?
217
+
218
+ real = some_dir.realpath
219
+ Find.find(real).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten
220
+ end
221
+
222
+ # Header files that are part of the project library under test
223
+ # @return [Array<Pathname>]
224
+ def header_files
225
+ ret = if one_point_five?
226
+ code_files_in_recursive(@base_dir + "src", HPP_EXTENSIONS)
227
+ else
228
+ [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, HPP_EXTENSIONS) }.flatten
229
+ end
230
+
231
+ # note to future troubleshooter: some of these tests may not be relevant, but at the moment at
232
+ # least some of them are tied to existing features
233
+ ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) }
234
+ end
235
+
150
236
  # CPP files that are part of the project library under test
151
237
  # @return [Array<Pathname>]
152
238
  def cpp_files
153
- cpp_files_in(@base_dir).reject { |p| vendor_bundle?(p) || in_tests_dir?(p) }
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) }
154
248
  end
155
249
 
156
250
  # CPP files that are part of the arduino mock library we're providing
157
251
  # @return [Array<Pathname>]
158
252
  def cpp_files_arduino
159
- cpp_files_in(ARDUINO_HEADER_DIR)
253
+ code_files_in(ARDUINO_HEADER_DIR, CPP_EXTENSIONS)
160
254
  end
161
255
 
162
256
  # CPP files that are part of the unit test library we're providing
163
257
  # @return [Array<Pathname>]
164
258
  def cpp_files_unittest
165
- cpp_files_in(UNITTEST_HEADER_DIR)
259
+ code_files_in(UNITTEST_HEADER_DIR, CPP_EXTENSIONS)
166
260
  end
167
261
 
168
262
  # CPP files that are part of the 3rd-party libraries we're including
169
263
  # @param [Array<String>] aux_libraries
170
264
  # @return [Array<Pathname>]
171
265
  def cpp_files_libraries(aux_libraries)
172
- arduino_library_src_dirs(aux_libraries).map { |d| cpp_files_in(d) }.flatten.uniq
266
+ arduino_library_src_dirs(aux_libraries).map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten.uniq
267
+ end
268
+
269
+ # Returns the Pathnames for all paths to exclude from testing and compilation
270
+ # @return [Array<Pathname>]
271
+ def exclude_dir
272
+ @exclude_dirs.map { |p| Pathname.new(@base_dir) + p }.select(&:exist?)
173
273
  end
174
274
 
175
275
  # The directory where we expect to find unit test defintions provided by the user
@@ -181,16 +281,15 @@ module ArduinoCI
181
281
  # The files provided by the user that contain unit tests
182
282
  # @return [Array<Pathname>]
183
283
  def test_files
184
- cpp_files_in(tests_dir)
284
+ code_files_in(tests_dir, CPP_EXTENSIONS)
185
285
  end
186
286
 
187
287
  # Find all directories in the project library that include C++ header files
188
288
  # @return [Array<Pathname>]
189
289
  def header_dirs
190
- real = @base_dir.realpath
191
- all_files = Find.find(real).map { |f| Pathname.new(f) }.reject(&:directory?)
192
- unbundled = all_files.reject { |path| vendor_bundle?(path) }
193
- files = unbundled.select { |path| HPP_EXTENSIONS.include?(path.extname.downcase) }
290
+ unbundled = header_files.reject { |path| vendor_bundle?(path) }
291
+ unexcluded = unbundled.reject { |path| in_exclude_dir?(path) }
292
+ files = unexcluded.select { |path| HPP_EXTENSIONS.include?(path.extname.downcase) }
194
293
  files.map(&:dirname).uniq
195
294
  end
196
295
 
@@ -212,20 +311,24 @@ module ArduinoCI
212
311
  @last_err
213
312
  end
214
313
 
215
- # Arduino library directories containing sources
314
+ # Arduino library directories containing sources -- only those of the dependencies
216
315
  # @return [Array<Pathname>]
217
316
  def arduino_library_src_dirs(aux_libraries)
218
317
  # Pull in all possible places that headers could live, according to the spec:
219
318
  # https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification
220
- # TODO: be smart and implement library spec (library.properties, etc)?
221
- subdirs = ["", "src", "utility"]
222
- all_aux_include_dirs_nested = aux_libraries.map do |libdir|
223
- subdirs.map { |subdir| Pathname.new(@arduino_lib_dir) + libdir + subdir }
224
- end
225
- all_aux_include_dirs_nested.flatten.select(&:exist?).select(&:directory?)
319
+
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
226
326
  end
227
327
 
228
328
  # GCC command line arguments for including aux libraries
329
+ #
330
+ # This function recursively collects the library directores of the dependencies
331
+ #
229
332
  # @param aux_libraries [Array<Pathname>] The external Arduino libraries required by this project
230
333
  # @return [Array<String>] The GCC command-line flags necessary to include those libraries
231
334
  def include_args(aux_libraries)
@@ -288,6 +391,9 @@ module ArduinoCI
288
391
  end
289
392
 
290
393
  # build a file for running a test of the given unit test file
394
+ #
395
+ # The dependent libraries configuration is appended with data from library.properties internal to the library under test
396
+ #
291
397
  # @param test_file [Pathname] The path to the file containing the unit tests
292
398
  # @param aux_libraries [Array<Pathname>] The external Arduino libraries required by this project
293
399
  # @param ci_gcc_config [Hash] The GCC config object
@@ -306,8 +412,12 @@ module ArduinoCI
306
412
  "-fsanitize=address"
307
413
  ]
308
414
  end
309
- arg_sets << test_args(aux_libraries, ci_gcc_config)
310
- arg_sets << cpp_files_libraries(aux_libraries).map(&:to_s)
415
+
416
+ # combine library.properties defs (if existing) with config file.
417
+ # 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)
311
421
  arg_sets << [test_file.to_s]
312
422
  args = arg_sets.flatten(1)
313
423
  return nil unless run_gcc(gcc_binary, *args)
@@ -316,14 +426,31 @@ module ArduinoCI
316
426
  executable
317
427
  end
318
428
 
429
+ # print any found stack dumps
430
+ # @param executable [Pathname] the path to the test file
431
+ def print_stack_dump(executable)
432
+ possible_dumpfiles = [
433
+ executable.sub_ext(executable.extname + ".stackdump")
434
+ ]
435
+ possible_dumpfiles.select(&:exist?).each do |dump|
436
+ puts "========== Stack dump from #{dump}:"
437
+ File.foreach(dump) { |line| print " #{line}" }
438
+ end
439
+ end
440
+
319
441
  # run a test file
320
- # @param [Pathname] the path to the test file
442
+ # @param executable [Pathname] the path to the test file
321
443
  # @return [bool] whether all tests were successful
322
444
  def run_test_file(executable)
323
445
  @last_cmd = executable
324
446
  @last_out = ""
325
447
  @last_err = ""
326
- Host.run_and_output(executable.to_s.shellescape)
448
+ ret = Host.run_and_output(executable.to_s.shellescape)
449
+
450
+ # print any stack traces found during a failure
451
+ print_stack_dump(executable) unless ret
452
+
453
+ ret
327
454
  end
328
455
 
329
456
  end
@@ -0,0 +1,86 @@
1
+ module ArduinoCI
2
+
3
+ # Information about an Arduino library package, as specified by the library.properties file
4
+ #
5
+ # See https://arduino.github.io/arduino-cli/library-specification/#libraryproperties-file-format
6
+ class LibraryProperties
7
+
8
+ # @return [Hash] The properties file parsed as a hash
9
+ attr_reader :fields
10
+
11
+ # @param path [Pathname] The path to the library.properties file
12
+ def initialize(path)
13
+ @fields = {}
14
+ File.foreach(path) do |line|
15
+ parts = line.split("=", 2)
16
+ @fields[parts[0]] = parts[1].chomp unless parts.empty?
17
+ end
18
+ end
19
+
20
+ # Enable a shortcut syntax for library property accessors, in the style of `attr_accessor` metaprogramming.
21
+ # This is used to create a named field pointing to a specific property in the file, optionally applying
22
+ # a specific formatting function.
23
+ #
24
+ # The formatting function MUST be a static method on this class. This is a limitation caused by the desire
25
+ # to both (1) expose the formatters outside this class, and (2) use them for metaprogramming without the
26
+ # having to name the entire function. field_reader is a static method, so if not for the fact that
27
+ # `self.class.methods.include? formatter` fails to work for class methods in this context (unlike
28
+ # `self.methods.include?`, which properly finds instance methods), I would allow either one and just
29
+ # conditionally `define_method` the proper definition
30
+ #
31
+ # @param name [String] What the accessor will be called
32
+ # @param field_num [Integer] The name of the key of the property
33
+ # @param formatter [Symbol] The symbol for the formatting function to apply to the field (optional)
34
+ # @return [void]
35
+ # @macro [attach] field_reader
36
+ # @!attribute [r] $1
37
+ # @return property $2 of the library.properties file, formatted with the function {$3}
38
+ def self.field_reader(name, formatter = nil)
39
+ key = name.to_s
40
+ if formatter.nil?
41
+ define_method(name) { @fields[key] }
42
+ else
43
+ define_method(name) { @fields.key?(key) ? self.class.send(formatter.to_sym, @fields[key]) : nil }
44
+ end
45
+ end
46
+
47
+ # Parse a value as a comma-separated array
48
+ # @param input [String]
49
+ # @return [Array<String>] The individual values
50
+ def self._csv(input)
51
+ input.split(",").map(&:strip)
52
+ end
53
+
54
+ # Parse a value as a boolean
55
+ # @param input [String]
56
+ # @return [Array<String>] The individual values
57
+ def self._bool(input)
58
+ input == "true" # no indication given in the docs that anything but lowercase "true" indicates boolean true.
59
+ end
60
+
61
+ field_reader :name
62
+ field_reader :version
63
+ field_reader :author, :_csv
64
+ field_reader :maintainer
65
+ field_reader :sentence
66
+ field_reader :paragraph
67
+ field_reader :category
68
+ field_reader :url
69
+ field_reader :architectures, :_csv
70
+ field_reader :depends, :_csv
71
+ field_reader :dot_a_linkage, :_bool
72
+ field_reader :includes, :_csv
73
+ field_reader :precompiled, :_bool
74
+ field_reader :ldflags, :_csv
75
+
76
+ # The value of sentence always will be prepended, so you should start by writing the second sentence here
77
+ #
78
+ # (according to the docs)
79
+ # @return [String] the sentence and paragraph together
80
+ def full_paragraph
81
+ [sentence, paragraph].join(" ")
82
+ end
83
+
84
+ end
85
+
86
+ end