Class: ArduinoCI::ArduinoBackend

Inherits:
Object
  • Object
show all
Defined in:
lib/arduino_ci/arduino_backend.rb

Overview

Wrap the Arduino executable. This requires, in some cases, a faked display.

Constant Summary collapse

CONFIG_FILE_NAME =

We never even use this in code, it’s just here for reference because the backend is picky about it. Used for testing

Returns:

  • (String)

    the only allowable name for the arduino-cli config file.

"arduino-cli.yaml".freeze
CONFIG_FILE_APOLOGY =

Unfortunately we need error messaging around this quirk

Returns:

  • (String)

    The text to use for user apologies regarding the config file

"Sorry this is weird, see https://github.com/arduino/arduino-cli/issues/753".freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(binary_path) ⇒ ArduinoBackend

Returns a new instance of ArduinoBackend.



44
45
46
47
48
49
50
51
52
# File 'lib/arduino_ci/arduino_backend.rb', line 44

def initialize(binary_path)
  @binary_path        = binary_path
  @config_dir         = nil
  @additional_urls    = []
  @last_out           = ""
  @last_err           = ""
  @last_msg           = ""
  @config_dir_hack    = false
end

Instance Attribute Details

#additional_urlsArray<String> (readonly)

Returns Additional URLs for the boards manager.

Returns:

  • (Array<String>)

    Additional URLs for the boards manager



42
43
44
# File 'lib/arduino_ci/arduino_backend.rb', line 42

def additional_urls
  @additional_urls
end

#binary_pathPathname

the actual path to the executable on this platform

Returns:

  • (Pathname)


26
27
28
# File 'lib/arduino_ci/arduino_backend.rb', line 26

def binary_path
  @binary_path
end

#config_dirPathname (readonly)

The directory that contains the config file

Returns:

  • (Pathname)


30
31
32
# File 'lib/arduino_ci/arduino_backend.rb', line 30

def config_dir
  @config_dir
end

#last_errString (readonly)

Returns STDERR of the most recently-run command.

Returns:

  • (String)

    STDERR of the most recently-run command



36
37
38
# File 'lib/arduino_ci/arduino_backend.rb', line 36

def last_err
  @last_err
end

#last_msgString (readonly)

Returns the most recently-run command.

Returns:

  • (String)

    the most recently-run command



39
40
41
# File 'lib/arduino_ci/arduino_backend.rb', line 39

def last_msg
  @last_msg
end

#last_outString (readonly)

Returns STDOUT of the most recently-run command.

Returns:

  • (String)

    STDOUT of the most recently-run command



33
34
35
# File 'lib/arduino_ci/arduino_backend.rb', line 33

def last_out
  @last_out
end

Class Method Details

.config_file_path_from_dir(dir) ⇒ Pathname

Get an acceptable filename for use as a config file

Note github.com/arduino/arduino-cli/issues/753 : the –config-file option is really the directory that contains the file

Parameters:

  • dir (Pathname)

    the desired directory

Returns:

  • (Pathname)


110
111
112
# File 'lib/arduino_ci/arduino_backend.rb', line 110

def self.config_file_path_from_dir(dir)
  Pathname(dir) + CONFIG_FILE_NAME
end

Instance Method Details

#_wrap_run(work_fn, *args, **kwargs) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/arduino_ci/arduino_backend.rb', line 54

def _wrap_run(work_fn, *args, **kwargs)
  # do some work to extract & merge environment variables if they exist
  has_env = !args.empty? && args[0].instance_of?(Hash)
  env_vars = has_env ? args[0] : {}
  actual_args = has_env ? args[1..-1] : args  # need to shift over if we extracted args
  custom_config = []
  custom_config += ["--config-file", config_file_cli_param.to_s] unless @config_dir_hack || @config_dir.nil?
  full_args = [binary_path.to_s, "--format", "json"] + custom_config + actual_args
  full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args

  shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ")
  @last_msg = " $ #{shell_vars} #{full_args.join(' ')}"
  work_fn.call(*full_cmd, **kwargs)
end

#board_installed?(boardname) ⇒ bool

check whether a board is installed we do this by just selecting a board.

the arduino binary will error if unrecognized and do a successful no-op if it's installed

Parameters:

  • boardname (String)

    The board to test

Returns:

  • (bool)

    Whether the board is installed



166
167
168
# File 'lib/arduino_ci/arduino_backend.rb', line 166

def board_installed?(boardname)
  run_and_capture("board", "details", "--fqbn", boardname)[:success]
end

#board_manager_urlsArray<String>

Board manager URLs

Returns:

  • (Array<String>)

    The additional URLs used by the board manager



149
150
151
# File 'lib/arduino_ci/arduino_backend.rb', line 149

def board_manager_urls
  config_dump["board_manager"]["additional_urls"] + @additional_urls
end

#board_manager_urls=(all_urls) ⇒ Array<String>

Set board manager URLs

Returns:

  • (Array<String>)

    The additional URLs used by the board manager



155
156
157
158
159
# File 'lib/arduino_ci/arduino_backend.rb', line 155

def board_manager_urls=(all_urls)
  raise ArgumentError("all_urls should be an array, got #{all_urls.class}") unless all_urls.is_a? Array

  @additional_urls = all_urls
end

#boards_installed?(boardfamily_name) ⇒ bool

check whether a board family is installed (e.g. arduino:avr)

Parameters:

  • boardfamily_name (String)

    The board family to test

Returns:

  • (bool)

    Whether the board is installed



174
175
176
# File 'lib/arduino_ci/arduino_backend.rb', line 174

def boards_installed?(boardfamily_name)
  capture_json("core", "list")[:json].any? { |b| b["ID"] == boardfamily_name }
end

#capture_json(*args, **kwargs) ⇒ Object



128
129
130
131
132
# File 'lib/arduino_ci/arduino_backend.rb', line 128

def capture_json(*args, **kwargs)
  ret = run_and_capture(*args, **kwargs)
  ret[:json] = JSON.parse(ret[:out])
  ret
end

#compile_sketch(path, boardname) ⇒ bool

Returns whether the command succeeded.

Parameters:

  • path (String)

    The sketch to compile

  • boardname (String)

    The board to use

Returns:

  • (bool)

    whether the command succeeded



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/arduino_ci/arduino_backend.rb', line 210

def compile_sketch(path, boardname)
  ext = File.extname path
  unless ext.casecmp(".ino").zero?
    @last_msg = "Refusing to compile sketch with '#{ext}' extension -- rename it to '.ino'!"
    return false
  end
  unless File.exist? path
    @last_msg = "Can't compile Sketch at nonexistent path '#{path}'!"
    return false
  end

  ret = if should_use_dry_run?
    run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", "--dry-run", path.to_s)
  else
    run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", path.to_s)
  end
  @last_msg = ret[:out]
  ret[:success]
end

#config_dumpHash

Get a dump of the entire config

Returns:

  • (Hash)

    The configuration



136
137
138
# File 'lib/arduino_ci/arduino_backend.rb', line 136

def config_dump
  capture_json("config", "dump")[:json]
end

#config_file_cli_paramPathname

The config file to be used as a CLI param

This format changes based on version, which is very annoying. See unit tests.

Returns:

  • (Pathname)

    the path to use for a given OS



99
100
101
# File 'lib/arduino_ci/arduino_backend.rb', line 99

def config_file_cli_param
  should_use_config_dir? ? @config_dir : config_file_path
end

#config_file_pathPathname

The config file name to be passed on the command line

Note github.com/arduino/arduino-cli/issues/753 : the –config-file option is really the directory that contains the file

Returns:

  • (Pathname)


75
76
77
# File 'lib/arduino_ci/arduino_backend.rb', line 75

def config_file_path
  @config_dir + CONFIG_FILE_NAME
end

#config_file_path=(rhs) ⇒ Pathname

The config file name to be passed on the command line

Note github.com/arduino/arduino-cli/issues/753 : the –config-file option is really the directory that contains the file

Parameters:

  • val (Pathname)

    The config file that will be used

Returns:

  • (Pathname)

Raises:

  • (ArgumentError)


86
87
88
89
90
91
92
# File 'lib/arduino_ci/arduino_backend.rb', line 86

def config_file_path=(rhs)
  path_rhs = Pathname(rhs)
  err_text = "Config file basename must be '#{CONFIG_FILE_NAME}'. #{CONFIG_FILE_APOLOGY}"
  raise ArgumentError, err_text unless path_rhs.basename.to_s == CONFIG_FILE_NAME

  @config_dir = path_rhs.dirname
end

#install_boards(boardfamily) ⇒ bool

install a board by name

Parameters:

  • name (String)

    the board name

Returns:

  • (bool)

    whether the command succeeded



181
182
183
184
185
186
187
188
189
190
191
# File 'lib/arduino_ci/arduino_backend.rb', line 181

def install_boards(boardfamily)
  result = if @additional_urls.empty?
    run_and_capture("core", "install", boardfamily)
  else
    urls = @additional_urls.join(",")
    # update the index, then install. if the update step fails, return that result
    updater = run_and_capture("core", "update-index", "--additional-urls", urls)
    updater[:success] ? run_and_capture("core", "install", boardfamily, "--additional-urls", urls) : updater
  end
  result[:success]
end

#install_local_library(path) ⇒ CppLibrary

install a library from a path on the local machine (not via library manager), by symlink or no-op as appropriate

Parameters:

  • path (Pathname)

    library to use

Returns:



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/arduino_ci/arduino_backend.rb', line 265

def install_local_library(path)
  src_path         = path.realpath
  library_name     = name_of_library(path)
  cpp_library      = library_of_name(library_name)
  destination_path = cpp_library.path

  # things get weird if the sketchbook contains the library.
  # check that first
  if cpp_library.installed?
    # maybe the project has always lived in the libraries directory, no need to symlink
    return cpp_library if destination_path == src_path

    uhoh = "There is already a library '#{library_name}' in the library directory (#{destination_path})"
    # maybe it's a symlink? that would be OK
    if Host.symlink?(destination_path)
      current_destination_target = Host.readlink(destination_path)
      return cpp_library if current_destination_target == src_path

      @last_msg = "#{uhoh} and it's symlinked to #{current_destination_target} (expected #{src_path})"
      return nil
    end

    @last_msg = "#{uhoh}.  It may need to be removed manually."
    return nil
  end

  # install the library
  libraries_dir = destination_path.parent
  libraries_dir.mkpath unless libraries_dir.exist?
  Host.symlink(src_path, destination_path)
  cpp_library
end

#installed_librariesHash

Returns information about installed libraries via the CLI.

Returns:

  • (Hash)

    information about installed libraries via the CLI



203
204
205
# File 'lib/arduino_ci/arduino_backend.rb', line 203

def installed_libraries
  capture_json("lib", "list")[:json]
end

#last_bytes_usageHash

extract the “Free space remaining” amount from the last run

Returns:

  • (Hash)

    the usage, as a hash with keys :free, :max, and :globals



300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/arduino_ci/arduino_backend.rb', line 300

def last_bytes_usage
  # Free-spacing syntax for regexes is not working today, not sure why. Make a string and convert to regex.
  re_str = [
    'Global variables use (?<globals>\d+) bytes',
    '\(\d+%\) of dynamic memory,',
    'leaving (?<free>\d+) bytes for local variables.',
    'Maximum is (?<max>\d+) bytes.'
  ].join(" ")
  mem_info = Regexp.new(re_str).match(@last_msg)
  return {} if mem_info.nil?

  Hash[mem_info.names.map(&:to_sym).zip(mem_info.captures.map(&:to_i))]
end

#lib_dirString

Returns the path to the Arduino libraries directory.

Returns:

  • (String)

    the path to the Arduino libraries directory



141
142
143
144
145
# File 'lib/arduino_ci/arduino_backend.rb', line 141

def lib_dir
  user_dir_raw = config_dump["directories"]["user"]
  user_dir = OS.windows? ? Host.windows_to_pathname(user_dir_raw) : user_dir_raw
  Pathname.new(user_dir) + "libraries"
end

#library_available?(name) ⇒ bool

Find out if a library is available

Parameters:

  • name (String)

    the library name

Returns:

  • (bool)

    whether the library can be installed via the library manager



197
198
199
200
# File 'lib/arduino_ci/arduino_backend.rb', line 197

def library_available?(name)
  # the --names flag limits the size of the response to just the name field
  capture_json("lib", "search", "--names", name)[:json]["libraries"].any? { |l| l["name"] == name }
end

#library_of_name(name) ⇒ CppLibrary

Create a handle to an Arduino library by name

Parameters:

  • name (String)

    The library “real name”

Returns:

Raises:

  • (ArgumentError)


245
246
247
248
249
# File 'lib/arduino_ci/arduino_backend.rb', line 245

def library_of_name(name)
  raise ArgumentError, "name is not a String (got #{name.class})" unless name.is_a? String

  CppLibrary.new(name, self)
end

#library_of_path(path) ⇒ CppLibrary

Create a handle to an Arduino library by path

Parameters:

  • path (Pathname)

    The path to the library

Returns:



254
255
256
257
258
259
260
# File 'lib/arduino_ci/arduino_backend.rb', line 254

def library_of_path(path)
  # the path must exist... and if it does, brute-force search the installed libs for it
  realpath = path.realpath  # should produce error if the path doesn't exist to begin with
  entry = installed_libraries.find { |l| Pathname.new(l["library"]["install_dir"]).realpath == realpath }
  probable_name = entry["real_name"].nil? ? realpath.basename.to_s : entry["real_name"]
  CppLibrary.new(probable_name, self)
end

#name_of_library(path) ⇒ String

Guess the name of a library

Parameters:

  • path (Pathname)

    The path to the library (installed or not)

Returns:

  • (String)

    the probable library name



233
234
235
236
237
238
239
240
# File 'lib/arduino_ci/arduino_backend.rb', line 233

def name_of_library(path)
  src_path = path.realpath
  properties_file = src_path + CppLibrary::LIBRARY_PROPERTIES_FILE
  return src_path.basename.to_s unless properties_file.exist?
  return src_path.basename.to_s if LibraryProperties.new(properties_file).name.nil?

  LibraryProperties.new(properties_file).name
end

#run_and_capture(*args, **kwargs) ⇒ Hash

run a command and capture its output

Returns:

  • (Hash)

    => String, :err => String, :success => bool



121
122
123
124
125
126
# File 'lib/arduino_ci/arduino_backend.rb', line 121

def run_and_capture(*args, **kwargs)
  ret = _wrap_run((proc { |*a, **k| Host.run_and_capture(*a, **k) }), *args, **kwargs)
  @last_err = ret[:err]
  @last_out = ret[:out]
  ret
end

#run_and_output(*args, **kwargs) ⇒ Object

build and run the arduino command



115
116
117
# File 'lib/arduino_ci/arduino_backend.rb', line 115

def run_and_output(*args, **kwargs)
  _wrap_run((proc { |*a, **k| Host.run_and_output(*a, **k) }), *args, **kwargs)
end

#should_use_config_dir?Bool

Since the config dir behavior has changed from a directory to a file (At some point??)

Returns:

  • (Bool)

    whether to specify configuration by directory or filename



332
333
334
335
336
337
# File 'lib/arduino_ci/arduino_backend.rb', line 332

def should_use_config_dir?
  @config_dir_hack = true   # prevent an infinite loop when trying to run the command
  version < Gem::Version.new('0.14')
ensure
  @config_dir_hack = false
end

#should_use_dry_run?Bool

Since the dry-run behavior became default in arduino-cli 0.14, the command line flag was removed

Returns:

  • (Bool)

    whether the –dry-run flag is available for this arduino-cli version



326
327
328
# File 'lib/arduino_ci/arduino_backend.rb', line 326

def should_use_dry_run?
  version < Gem::Version.new('0.14')
end

#versionGem::Version

Returns the arduino-cli version that the backend is using, as a semver object.

Returns:

  • (Gem::Version)

    the arduino-cli version that the backend is using, as a semver object



320
321
322
# File 'lib/arduino_ci/arduino_backend.rb', line 320

def version
  Gem::Version.new(version_str)
end

#version_strString

Returns the arduino-cli version that the backend is using, as String.

Returns:

  • (String)

    the arduino-cli version that the backend is using, as String



315
316
317
# File 'lib/arduino_ci/arduino_backend.rb', line 315

def version_str
  capture_json("version")[:json]["VersionString"]
end