arduino_ci 0.4.0 → 1.0.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.
@@ -6,6 +6,13 @@ module ArduinoCI
6
6
 
7
7
  # Tools for interacting with the host machine
8
8
  class Host
9
+ # TODO: this came from https://stackoverflow.com/a/22716582/2063546
10
+ # and I'm not sure if it can be replaced by self.os == :windows
11
+ WINDOWS_VARIANT_REGEX = /mswin32|cygwin|mingw|bccwin/
12
+
13
+ # e.g. 11/27/2020 01:02 AM <SYMLINKD> ExcludeSomething [C:\projects\arduino-ci\SampleProjects\ExcludeSomething]
14
+ DIR_SYMLINK_REGEX = %r{\d+/\d+/\d+\s+[^<]+<SYMLINKD?>\s+(.*) \[([^\]]+)\]}
15
+
9
16
  # Cross-platform way of finding an executable in the $PATH.
10
17
  # via https://stackoverflow.com/a/5471032/2063546
11
18
  # which('ruby') #=> /usr/bin/ruby
@@ -38,21 +45,69 @@ module ArduinoCI
38
45
  return :windows if OS.windows?
39
46
  end
40
47
 
48
+ # Cross-platform symlinking
41
49
  # if on windows, call mklink, else self.symlink
42
50
  # @param [Pathname] old_path
43
51
  # @param [Pathname] new_path
44
52
  def self.symlink(old_path, new_path)
45
- return FileUtils.ln_s(old_path.to_s, new_path.to_s) unless RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/
53
+ # we would prefer `new_path.make_symlink(old_path)` but "symlink function is unimplemented on this machine" with windows
54
+ return new_path.make_symlink(old_path) unless needs_symlink_hack?
46
55
 
47
- # https://stackoverflow.com/a/22716582/2063546
56
+ # via https://stackoverflow.com/a/22716582/2063546
48
57
  # windows mklink syntax is reverse of unix ln -s
49
58
  # windows mklink is built into cmd.exe
50
59
  # vulnerable to command injection, but okay because this is a hack to make a cli tool work.
51
- orp = old_path.realpath.to_s.tr("/", "\\") # HACK DUE TO REALPATH BUG where it
52
- np = new_path.to_s.tr("/", "\\") # still joins windows paths with '/'
60
+ orp = pathname_to_windows(old_path.realpath)
61
+ np = pathname_to_windows(new_path)
53
62
 
54
63
  _stdout, _stderr, exitstatus = Open3.capture3('cmd.exe', "/C mklink /D #{np} #{orp}")
55
64
  exitstatus.success?
56
65
  end
66
+
67
+ # Hack for "realpath" which on windows joins paths with slashes instead of backslashes
68
+ # @param path [Pathname] the path to render
69
+ # @return [String] A path that will work on windows
70
+ def self.pathname_to_windows(path)
71
+ path.to_s.tr("/", "\\")
72
+ end
73
+
74
+ # Hack for "realpath" which on windows joins paths with slashes instead of backslashes
75
+ # @param str [String] the windows path
76
+ # @return [Pathname] A path that will be recognized by pathname
77
+ def self.windows_to_pathname(str)
78
+ Pathname.new(str.tr("\\", "/"))
79
+ end
80
+
81
+ # Whether this OS requires a hack for symlinks
82
+ # @return [bool]
83
+ def self.needs_symlink_hack?
84
+ RUBY_PLATFORM =~ WINDOWS_VARIANT_REGEX
85
+ end
86
+
87
+ # Cross-platform is-this-a-symlink function
88
+ # @param [Pathname] path
89
+ # @return [bool] Whether the file is a symlink
90
+ def self.symlink?(path)
91
+ return path.symlink? unless needs_symlink_hack?
92
+
93
+ !readlink(path).nil?
94
+ end
95
+
96
+ # Cross-platform "read link" function
97
+ # @param [Pathname] path
98
+ # @return [Pathname] the link target
99
+ def self.readlink(path)
100
+ return path.readlink unless needs_symlink_hack?
101
+
102
+ the_dir = pathname_to_windows(path.parent)
103
+ the_file = path.basename.to_s
104
+
105
+ stdout, _stderr, _exitstatus = Open3.capture3('cmd.exe', "/c dir /al #{the_dir}")
106
+ symlinks = stdout.lines.map { |l| DIR_SYMLINK_REGEX.match(l) }.compact
107
+ our_link = symlinks.find { |m| m[1] == the_file }
108
+ return nil if our_link.nil?
109
+
110
+ windows_to_pathname(our_link[2])
111
+ end
57
112
  end
58
113
  end
@@ -11,12 +11,22 @@ module ArduinoCI
11
11
  # @param path [Pathname] The path to the library.properties file
12
12
  def initialize(path)
13
13
  @fields = {}
14
- File.foreach(path) do |line|
14
+ File.foreach(path) do |line_with_delim|
15
+ line = line_with_delim.chomp
15
16
  parts = line.split("=", 2)
16
- @fields[parts[0]] = parts[1].chomp unless parts.empty?
17
+ next if parts[0].nil?
18
+ next if parts[0].empty?
19
+ next if parts[1].nil?
20
+
21
+ @fields[parts[0]] = parts[1] unless parts[1].empty?
17
22
  end
18
23
  end
19
24
 
25
+ # @return [Hash] the properties as a hash, all strings
26
+ def to_h
27
+ @fields.clone
28
+ end
29
+
20
30
  # Enable a shortcut syntax for library property accessors, in the style of `attr_accessor` metaprogramming.
21
31
  # This is used to create a named field pointing to a specific property in the file, optionally applying
22
32
  # a specific formatting function.
@@ -1,3 +1,3 @@
1
1
  module ArduinoCI
2
- VERSION = "0.4.0".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
@@ -3,8 +3,12 @@
3
3
  # https://en.wikipedia.org/wiki/List_of_Arduino_boards_and_compatible_systems
4
4
 
5
5
  packages:
6
- # arduino:xxx are builtin, we don't need to include them here
7
- # but if we did, it would be url: https://downloads.arduino.cc/packages/package_index.json
6
+ arduino:avr:
7
+ url: https://downloads.arduino.cc/packages/package_index.json
8
+ arduino:sam:
9
+ url: https://downloads.arduino.cc/packages/package_index.json
10
+ arduino:samd:
11
+ url: https://downloads.arduino.cc/packages/package_index.json
8
12
  esp8266:esp8266:
9
13
  url: http://arduino.esp8266.com/stable/package_esp8266com_index.json
10
14
  adafruit:avr:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arduino_ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Katz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-22 00:00:00.000000000 Z
11
+ date: 2020-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: os
@@ -38,76 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.2'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">"
46
- - !ruby/object:Gem::Version
47
- version: '1.15'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">"
53
- - !ruby/object:Gem::Version
54
- version: '1.15'
55
- - !ruby/object:Gem::Dependency
56
- name: keepachangelog_manager
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: 0.0.2
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: 0.0.2
69
- - !ruby/object:Gem::Dependency
70
- name: rspec
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '3.0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '3.0'
83
- - !ruby/object:Gem::Dependency
84
- name: rubocop
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: 0.59.0
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: 0.59.0
97
- - !ruby/object:Gem::Dependency
98
- name: yard
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: 0.9.11
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: 0.9.11
111
41
  description: ''
112
42
  email:
113
43
  - arduino.continuous.integration@gmail.com
@@ -437,11 +367,7 @@ files:
437
367
  - exe/arduino_library_location.rb
438
368
  - exe/ensure_arduino_installation.rb
439
369
  - lib/arduino_ci.rb
440
- - lib/arduino_ci/arduino_cmd.rb
441
- - lib/arduino_ci/arduino_cmd_linux.rb
442
- - lib/arduino_ci/arduino_cmd_linux_builder.rb
443
- - lib/arduino_ci/arduino_cmd_osx.rb
444
- - lib/arduino_ci/arduino_cmd_windows.rb
370
+ - lib/arduino_ci/arduino_backend.rb
445
371
  - lib/arduino_ci/arduino_downloader.rb
446
372
  - lib/arduino_ci/arduino_downloader_linux.rb
447
373
  - lib/arduino_ci/arduino_downloader_osx.rb
@@ -450,7 +376,6 @@ files:
450
376
  - lib/arduino_ci/ci_config.rb
451
377
  - lib/arduino_ci/cpp_library.rb
452
378
  - lib/arduino_ci/host.rb
453
- - lib/arduino_ci/installed_cpp_library.rb
454
379
  - lib/arduino_ci/library_properties.rb
455
380
  - lib/arduino_ci/version.rb
456
381
  - misc/default.yml
@@ -1,332 +0,0 @@
1
- require 'fileutils'
2
- require 'pathname'
3
-
4
- # workaround for https://github.com/arduino/Arduino/issues/3535
5
- WORKAROUND_LIB = "USBHost".freeze
6
-
7
- module ArduinoCI
8
-
9
- # To report errors that we can't resolve or possibly even explain
10
- class ArduinoExecutionError < StandardError; end
11
-
12
- # Wrap the Arduino executable. This requires, in some cases, a faked display.
13
- class ArduinoCmd
14
-
15
- # Enable a shortcut syntax for command line flags
16
- # @param name [String] What the flag will be called (prefixed with 'flag_')
17
- # @return [void]
18
- # @macro [attach] flag
19
- # The text of the command line flag for $1
20
- # @!attribute [r] flag_$1
21
- # @return [String] the text of the command line flag (`$2` in this case)
22
- def self.flag(name, text = nil)
23
- text = "(flag #{name} not defined)" if text.nil?
24
- self.class_eval("def flag_#{name};\"#{text}\";end", __FILE__, __LINE__)
25
- end
26
-
27
- # the array of command components to launch the Arduino executable
28
- # @return [Array<String>]
29
- attr_accessor :base_cmd
30
-
31
- # the actual path to the executable on this platform
32
- # @return [Pathname]
33
- attr_accessor :binary_path
34
-
35
- # part of a workaround for https://github.com/arduino/Arduino/issues/3535
36
- attr_reader :libraries_indexed
37
-
38
- # @return [String] STDOUT of the most recently-run command
39
- attr_reader :last_out
40
-
41
- # @return [String] STDERR of the most recently-run command
42
- attr_reader :last_err
43
-
44
- # @return [String] the most recently-run command
45
- attr_reader :last_msg
46
-
47
- # set the command line flags (undefined for now).
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
56
-
57
- def initialize
58
- @prefs_cache = {}
59
- @prefs_fetched = false
60
- @libraries_indexed = false
61
- @last_out = ""
62
- @last_err = ""
63
- @last_msg = ""
64
- end
65
-
66
- # Convert a preferences dump into a flat hash
67
- # @param arduino_output [String] The raw Arduino executable output
68
- # @return [Hash] preferences as a hash
69
- def parse_pref_string(arduino_output)
70
- lines = arduino_output.split("\n").select { |l| l.include? "=" }
71
- ret = lines.each_with_object({}) do |e, acc|
72
- parts = e.split("=", 2)
73
- acc[parts[0]] = parts[1]
74
- acc
75
- end
76
- ret
77
- end
78
-
79
- # @return [String] the path to the Arduino libraries directory
80
- def lib_dir
81
- Pathname.new(get_pref("sketchbook.path")) + "libraries"
82
- end
83
-
84
- # fetch preferences in their raw form
85
- # @return [String] Preferences as a set of lines
86
- def _prefs_raw
87
- resp = run_and_capture(flag_get_pref)
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]
90
-
91
- @prefs_fetched = true
92
- resp[:out]
93
- end
94
-
95
- # Get the Arduino preferences, from cache if possible
96
- # @return [Hash] The full set of preferences
97
- def prefs
98
- prefs_raw = _prefs_raw unless @prefs_fetched
99
- return nil if prefs_raw.nil?
100
-
101
- @prefs_cache = parse_pref_string(prefs_raw)
102
- @prefs_cache.clone
103
- end
104
-
105
- # get a preference key
106
- # @param key [String] The preferences key to look up
107
- # @return [String] The preference value
108
- def get_pref(key)
109
- data = @prefs_fetched ? @prefs_cache : prefs
110
- data[key]
111
- end
112
-
113
- # underlying preference-setter.
114
- # @param key [String] The preference name
115
- # @param value [String] The value to set to
116
- # @return [bool] whether the command succeeded
117
- def _set_pref(key, value)
118
- run_and_capture(flag_set_pref, "#{key}=#{value}", flag_save_prefs)[:success]
119
- end
120
-
121
- # set a preference key/value pair, and update the cache.
122
- # @param key [String] the preference key
123
- # @param value [String] the preference value
124
- # @return [bool] whether the command succeeded
125
- def set_pref(key, value)
126
- prefs unless @prefs_fetched # update cache first
127
- success = _set_pref(key, value)
128
- @prefs_cache[key] = value if success
129
- success
130
- end
131
-
132
- def _wrap_run(work_fn, *args, **kwargs)
133
- # do some work to extract & merge environment variables if they exist
134
- has_env = !args.empty? && args[0].class == Hash
135
- env_vars = has_env ? args[0] : {}
136
- actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args
137
- full_args = @base_cmd + actual_args
138
- full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args
139
-
140
- shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ")
141
- @last_msg = " $ #{shell_vars} #{full_args.join(' ')}"
142
- work_fn.call(*full_cmd, **kwargs)
143
- end
144
-
145
- # build and run the arduino command
146
- def run_and_output(*args, **kwargs)
147
- _wrap_run((proc { |*a, **k| Host.run_and_output(*a, **k) }), *args, **kwargs)
148
- end
149
-
150
- # run a command and capture its output
151
- # @return [Hash] {:out => String, :err => String, :success => bool}
152
- def run_and_capture(*args, **kwargs)
153
- ret = _wrap_run((proc { |*a, **k| Host.run_and_capture(*a, **k) }), *args, **kwargs)
154
- @last_err = ret[:err]
155
- @last_out = ret[:out]
156
- ret
157
- end
158
-
159
- # Board manager URLs
160
- # @return [Array<String>] The additional URLs used by the board manager
161
- def board_manager_urls
162
- url_list = get_pref("boardsmanager.additional.urls")
163
- return [] if url_list.nil?
164
-
165
- url_list.split(",")
166
- end
167
-
168
- # Set board manager URLs
169
- # @return [Array<String>] The additional URLs used by the board manager
170
- def board_manager_urls=(all_urls)
171
- set_pref("boardsmanager.additional.urls", all_urls.join(","))
172
- end
173
-
174
- # check whether a board is installed
175
- # we do this by just selecting a board.
176
- # the arduino binary will error if unrecognized and do a successful no-op if it's installed
177
- # @param boardname [String] The board to test
178
- # @return [bool] Whether the board is installed
179
- def board_installed?(boardname)
180
- run_and_capture(flag_use_board, boardname)[:success]
181
- end
182
-
183
- # install a board by name
184
- # @param name [String] the board name
185
- # @return [bool] whether the command succeeded
186
- def install_boards(boardfamily)
187
- # TODO: find out why IO.pipe fails but File::NULL succeeds :(
188
- result = run_and_capture(flag_install_boards, boardfamily)
189
- already_installed = result[:err].include?("Platform is already installed!")
190
- result[:success] || already_installed
191
- end
192
-
193
- # install a library by name
194
- # @param name [String] the library name
195
- # @return [bool] whether the command succeeded
196
- def _install_library(library_name)
197
- result = run_and_capture(flag_install_library, library_name)
198
-
199
- already_installed = result[:err].include?("Library is already installed: #{library_name}")
200
- success = result[:success] || already_installed
201
-
202
- @libraries_indexed = (@libraries_indexed || success) if library_name == WORKAROUND_LIB
203
- success
204
- end
205
-
206
- # index the set of libraries by installing a dummy library
207
- # related to WORKAROUND_LIB and https://github.com/arduino/Arduino/issues/3535
208
- # TODO: unclear if this is still necessary
209
- def index_libraries
210
- return true if @libraries_indexed
211
-
212
- _install_library(WORKAROUND_LIB)
213
- @libraries_indexed
214
- end
215
-
216
- # install a library by name
217
- # @param name [String] the library name
218
- # @return [bool] whether the command succeeded
219
- def install_library(library_name)
220
- index_libraries
221
- _install_library(library_name)
222
- end
223
-
224
- # generate the (very likely) path of a library given its name
225
- # @param library_name [String] The name of the library
226
- # @return [Pathname] The fully qualified library name
227
- def library_path(library_name)
228
- Pathname.new(lib_dir) + library_name
229
- end
230
-
231
- # Determine whether a library is present in the lib dir
232
- #
233
- # Note that `true` doesn't guarantee that the library is valid/installed
234
- # and `false` doesn't guarantee that the library isn't built-in
235
- #
236
- # @param library_name [String] The name of the library
237
- # @return [bool]
238
- def library_present?(library_name)
239
- library_path(library_name).exist?
240
- end
241
-
242
- # update the library index
243
- # @return [bool] Whether the update succeeded
244
- def update_library_index
245
- # install random lib so the arduino IDE grabs a new library index
246
- # see: https://github.com/arduino/Arduino/issues/3535
247
- install_library(WORKAROUND_LIB)
248
- end
249
-
250
- # use a particular board for compilation
251
- # @param boardname [String] The board to use
252
- # @return [bool] whether the command succeeded
253
- def use_board(boardname)
254
- run_and_capture(flag_use_board, boardname, flag_save_prefs)[:success]
255
- end
256
-
257
- # use a particular board for compilation, installing it if necessary
258
- # @param boardname [String] The board to use
259
- # @return [bool] whether the command succeeded
260
- def use_board!(boardname)
261
- return true if use_board(boardname)
262
-
263
- boardfamily = boardname.split(":")[0..1].join(":")
264
- puts "Board '#{boardname}' not found; attempting to install '#{boardfamily}'"
265
- return false unless install_boards(boardfamily) # guess board family from first 2 :-separated fields
266
-
267
- use_board(boardname)
268
- end
269
-
270
- # @param path [String] The sketch to verify
271
- # @return [bool] whether the command succeeded
272
- def verify_sketch(path)
273
- ext = File.extname path
274
- unless ext.casecmp(".ino").zero?
275
- @last_msg = "Refusing to verify sketch with '#{ext}' extension -- rename it to '.ino'!"
276
- return false
277
- end
278
- unless File.exist? path
279
- @last_msg = "Can't verify Sketch at nonexistent path '#{path}'!"
280
- return false
281
- end
282
- ret = run_and_capture(flag_verify, path)
283
- ret[:success]
284
- end
285
-
286
- # ensure that the given library is installed, or symlinked as appropriate
287
- # return the path of the prepared library, or nil
288
- # @param path [Pathname] library to use
289
- # @return [String] the path of the installed library
290
- def install_local_library(path)
291
- src_path = path.realpath
292
- library_name = src_path.basename
293
- destination_path = library_path(library_name)
294
-
295
- # things get weird if the sketchbook contains the library.
296
- # check that first
297
- if destination_path.exist?
298
- uhoh = "There is already a library '#{library_name}' in the library directory"
299
- return destination_path if destination_path == src_path
300
-
301
- # maybe it's a symlink? that would be OK
302
- if destination_path.symlink?
303
- return destination_path if destination_path.readlink == src_path
304
-
305
- @last_msg = "#{uhoh} and it's not symlinked to #{src_path}"
306
- return nil
307
- end
308
-
309
- @last_msg = "#{uhoh}. It may need to be removed manually."
310
- return nil
311
- end
312
-
313
- # install the library
314
- Host.symlink(src_path, destination_path)
315
- destination_path
316
- end
317
-
318
- # @param installed_library_path [String] The library to query
319
- # @return [Array<String>] Example sketch files
320
- def library_examples(installed_library_path)
321
- example_path = Pathname.new(installed_library_path) + "examples"
322
- return [] unless File.exist?(example_path)
323
-
324
- examples = example_path.children.select(&:directory?).map(&:to_path).map(&File.method(:basename))
325
- files = examples.map do |e|
326
- proj_file = example_path + e + "#{e}.ino"
327
- proj_file.exist? ? proj_file.to_s : nil
328
- end
329
- files.reject(&:nil?).sort_by(&:to_s)
330
- end
331
- end
332
- end