arduino_ci 0.1.20 → 0.4.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 (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