arduino_ci 0.1.21 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -20
  3. data/REFERENCE.md +625 -0
  4. data/cpp/arduino/Arduino.h +1 -1
  5. data/cpp/arduino/AvrMath.h +117 -17
  6. data/cpp/arduino/Client.h +27 -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/OstreamHelpers.h +4 -0
  24. data/exe/arduino_ci.rb +400 -0
  25. data/exe/arduino_ci_remote.rb +2 -385
  26. data/exe/arduino_library_location.rb +2 -2
  27. data/lib/arduino_ci.rb +1 -0
  28. data/lib/arduino_ci/arduino_backend.rb +218 -0
  29. data/lib/arduino_ci/arduino_downloader.rb +42 -72
  30. data/lib/arduino_ci/arduino_downloader_linux.rb +17 -55
  31. data/lib/arduino_ci/arduino_downloader_osx.rb +21 -33
  32. data/lib/arduino_ci/arduino_downloader_windows.rb +11 -53
  33. data/lib/arduino_ci/arduino_installation.rb +18 -80
  34. data/lib/arduino_ci/ci_config.rb +12 -7
  35. data/lib/arduino_ci/cpp_library.rb +262 -48
  36. data/lib/arduino_ci/host.rb +59 -4
  37. data/lib/arduino_ci/library_properties.rb +96 -0
  38. data/lib/arduino_ci/version.rb +1 -1
  39. data/misc/default.yml +55 -4
  40. metadata +18 -83
  41. data/cpp/arduino/Arduino.h.orig +0 -143
  42. data/cpp/arduino/ci/Queue.h +0 -73
  43. data/exe/libasan.rb +0 -29
  44. data/lib/arduino_ci/arduino_cmd.rb +0 -328
  45. data/lib/arduino_ci/arduino_cmd_linux.rb +0 -17
  46. data/lib/arduino_ci/arduino_cmd_linux_builder.rb +0 -19
  47. data/lib/arduino_ci/arduino_cmd_osx.rb +0 -17
  48. data/lib/arduino_ci/arduino_cmd_windows.rb +0 -17
@@ -2,6 +2,6 @@
2
2
  require 'arduino_ci'
3
3
 
4
4
  # locate and/or forcibly install Arduino, keep stdout clean
5
- @arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate!($stderr)
5
+ @backend = ArduinoCI::ArduinoInstallation.autolocate!($stderr)
6
6
 
7
- puts @arduino_cmd.lib_dir
7
+ puts @backend.lib_dir
@@ -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>
@@ -0,0 +1,218 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require 'json'
4
+
5
+ # workaround for https://github.com/arduino/Arduino/issues/3535
6
+ WORKAROUND_LIB = "USBHost".freeze
7
+
8
+ module ArduinoCI
9
+
10
+ # To report errors that we can't resolve or possibly even explain
11
+ class ArduinoExecutionError < StandardError; end
12
+
13
+ # Wrap the Arduino executable. This requires, in some cases, a faked display.
14
+ class ArduinoBackend
15
+
16
+ # We never even use this in code, it's just here for reference because the backend is picky about it. Used for testing
17
+ # @return [String] the only allowable name for the arduino-cli config file.
18
+ CONFIG_FILE_NAME = "arduino-cli.yaml".freeze
19
+
20
+ # the actual path to the executable on this platform
21
+ # @return [Pathname]
22
+ attr_accessor :binary_path
23
+
24
+ # If a custom config is deired (i.e. for testing), specify it here.
25
+ # Note https://github.com/arduino/arduino-cli/issues/753 : the --config-file option
26
+ # is really the director that contains the file
27
+ # @return [Pathname]
28
+ attr_accessor :config_dir
29
+
30
+ # @return [String] STDOUT of the most recently-run command
31
+ attr_reader :last_out
32
+
33
+ # @return [String] STDERR of the most recently-run command
34
+ attr_reader :last_err
35
+
36
+ # @return [String] the most recently-run command
37
+ attr_reader :last_msg
38
+
39
+ # @return [Array<String>] Additional URLs for the boards manager
40
+ attr_reader :additional_urls
41
+
42
+ def initialize(binary_path)
43
+ @binary_path = binary_path
44
+ @config_dir = nil
45
+ @additional_urls = []
46
+ @last_out = ""
47
+ @last_err = ""
48
+ @last_msg = ""
49
+ end
50
+
51
+ def _wrap_run(work_fn, *args, **kwargs)
52
+ # do some work to extract & merge environment variables if they exist
53
+ has_env = !args.empty? && args[0].class == Hash
54
+ env_vars = has_env ? args[0] : {}
55
+ actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args
56
+ custom_config = @config_dir.nil? ? [] : ["--config-file", @config_dir.to_s]
57
+ full_args = [binary_path.to_s, "--format", "json"] + custom_config + actual_args
58
+ full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args
59
+
60
+ shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ")
61
+ @last_msg = " $ #{shell_vars} #{full_args.join(' ')}"
62
+ work_fn.call(*full_cmd, **kwargs)
63
+ end
64
+
65
+ # build and run the arduino command
66
+ def run_and_output(*args, **kwargs)
67
+ _wrap_run((proc { |*a, **k| Host.run_and_output(*a, **k) }), *args, **kwargs)
68
+ end
69
+
70
+ # run a command and capture its output
71
+ # @return [Hash] {:out => String, :err => String, :success => bool}
72
+ def run_and_capture(*args, **kwargs)
73
+ ret = _wrap_run((proc { |*a, **k| Host.run_and_capture(*a, **k) }), *args, **kwargs)
74
+ @last_err = ret[:err]
75
+ @last_out = ret[:out]
76
+ ret
77
+ end
78
+
79
+ def capture_json(*args, **kwargs)
80
+ ret = run_and_capture(*args, **kwargs)
81
+ ret[:json] = JSON.parse(ret[:out])
82
+ ret
83
+ end
84
+
85
+ # Get a dump of the entire config
86
+ # @return [Hash] The configuration
87
+ def config_dump
88
+ capture_json("config", "dump")[:json]
89
+ end
90
+
91
+ # @return [String] the path to the Arduino libraries directory
92
+ def lib_dir
93
+ Pathname.new(config_dump["directories"]["user"]) + "libraries"
94
+ end
95
+
96
+ # Board manager URLs
97
+ # @return [Array<String>] The additional URLs used by the board manager
98
+ def board_manager_urls
99
+ config_dump["board_manager"]["additional_urls"] + @additional_urls
100
+ end
101
+
102
+ # Set board manager URLs
103
+ # @return [Array<String>] The additional URLs used by the board manager
104
+ def board_manager_urls=(all_urls)
105
+ raise ArgumentError("all_urls should be an array, got #{all_urls.class}") unless all_urls.is_a? Array
106
+
107
+ @additional_urls = all_urls
108
+ end
109
+
110
+ # check whether a board is installed
111
+ # we do this by just selecting a board.
112
+ # the arduino binary will error if unrecognized and do a successful no-op if it's installed
113
+ # @param boardname [String] The board to test
114
+ # @return [bool] Whether the board is installed
115
+ def board_installed?(boardname)
116
+ # capture_json("core", "list")[:json].find { |b| b["ID"] == boardname } # nope, this is for the family
117
+ run_and_capture("board", "details", "--fqbn", boardname)[:success]
118
+ end
119
+
120
+ # install a board by name
121
+ # @param name [String] the board name
122
+ # @return [bool] whether the command succeeded
123
+ def install_boards(boardfamily)
124
+ result = run_and_capture("core", "install", boardfamily)
125
+ result[:success]
126
+ end
127
+
128
+ # @return [Hash] information about installed libraries via the CLI
129
+ def installed_libraries
130
+ capture_json("lib", "list")[:json]
131
+ end
132
+
133
+ # @param path [String] The sketch to compile
134
+ # @param boardname [String] The board to use
135
+ # @return [bool] whether the command succeeded
136
+ def compile_sketch(path, boardname)
137
+ ext = File.extname path
138
+ unless ext.casecmp(".ino").zero?
139
+ @last_msg = "Refusing to compile sketch with '#{ext}' extension -- rename it to '.ino'!"
140
+ return false
141
+ end
142
+ unless File.exist? path
143
+ @last_msg = "Can't compile Sketch at nonexistent path '#{path}'!"
144
+ return false
145
+ end
146
+ ret = run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", "--dry-run", path.to_s)
147
+ ret[:success]
148
+ end
149
+
150
+ # Guess the name of a library
151
+ # @param path [Pathname] The path to the library (installed or not)
152
+ # @return [String] the probable library name
153
+ def name_of_library(path)
154
+ src_path = path.realpath
155
+ properties_file = src_path + CppLibrary::LIBRARY_PROPERTIES_FILE
156
+ return src_path.basename.to_s unless properties_file.exist?
157
+ return src_path.basename.to_s if LibraryProperties.new(properties_file).name.nil?
158
+
159
+ LibraryProperties.new(properties_file).name
160
+ end
161
+
162
+ # Create a handle to an Arduino library by name
163
+ # @param name [String] The library "real name"
164
+ # @return [CppLibrary] The library object
165
+ def library_of_name(name)
166
+ raise ArgumentError, "name is not a String (got #{name.class})" unless name.is_a? String
167
+
168
+ CppLibrary.new(name, self)
169
+ end
170
+
171
+ # Create a handle to an Arduino library by path
172
+ # @param path [Pathname] The path to the library
173
+ # @return [CppLibrary] The library object
174
+ def library_of_path(path)
175
+ # the path must exist... and if it does, brute-force search the installed libs for it
176
+ realpath = path.realpath # should produce error if the path doesn't exist to begin with
177
+ entry = installed_libraries.find { |l| Pathname.new(l["library"]["install_dir"]).realpath == realpath }
178
+ probable_name = entry["real_name"].nil? ? realpath.basename.to_s : entry["real_name"]
179
+ CppLibrary.new(probable_name, self)
180
+ end
181
+
182
+ # install a library from a path on the local machine (not via library manager), by symlink or no-op as appropriate
183
+ # @param path [Pathname] library to use
184
+ # @return [CppLibrary] the installed library, or nil
185
+ def install_local_library(path)
186
+ src_path = path.realpath
187
+ library_name = name_of_library(path)
188
+ cpp_library = library_of_name(library_name)
189
+ destination_path = cpp_library.path
190
+
191
+ # things get weird if the sketchbook contains the library.
192
+ # check that first
193
+ if cpp_library.installed?
194
+ # maybe the project has always lived in the libraries directory, no need to symlink
195
+ return cpp_library if destination_path == src_path
196
+
197
+ uhoh = "There is already a library '#{library_name}' in the library directory (#{destination_path})"
198
+ # maybe it's a symlink? that would be OK
199
+ if Host.symlink?(destination_path)
200
+ current_destination_target = Host.readlink(destination_path)
201
+ return cpp_library if current_destination_target == src_path
202
+
203
+ @last_msg = "#{uhoh} and it's symlinked to #{current_destination_target} (expected #{src_path})"
204
+ return nil
205
+ end
206
+
207
+ @last_msg = "#{uhoh}. It may need to be removed manually."
208
+ return nil
209
+ end
210
+
211
+ # install the library
212
+ libraries_dir = destination_path.parent
213
+ libraries_dir.mkpath unless libraries_dir.exist?
214
+ Host.symlink(src_path, destination_path)
215
+ cpp_library
216
+ end
217
+ end
218
+ end
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'pathname'
2
3
  require 'net/http'
3
4
  require 'open-uri'
4
5
  require 'zip'
@@ -10,10 +11,10 @@ module ArduinoCI
10
11
  # Manage the OS-specific download & install of Arduino
11
12
  class ArduinoDownloader
12
13
 
13
- # @param desired_ide_version [string] Version string e.g. 1.8.7
14
+ # @param desired_version [string] Version string e.g. 1.8.7
14
15
  # @param output [IO] $stdout, $stderr, File.new(/dev/null, 'w'), etc. where console output will be sent
15
- def initialize(desired_ide_version, output = $stdout)
16
- @desired_ide_version = desired_ide_version
16
+ def initialize(desired_version, output = $stdout)
17
+ @desired_version = desired_version
17
18
  @output = output
18
19
  end
19
20
 
@@ -30,7 +31,7 @@ module ArduinoCI
30
31
 
31
32
  # The autolocated executable of the installation
32
33
  #
33
- # @return [string] or nil
34
+ # @return [Pathname] or nil
34
35
  def self.autolocated_executable
35
36
  # Arbitrarily, I'm going to pick the force installed location first
36
37
  # if it exists. I'm not sure why we would have both, but if we did
@@ -39,70 +40,54 @@ module ArduinoCI
39
40
  locations.find { |loc| !loc.nil? && File.exist?(loc) }
40
41
  end
41
42
 
42
- # The autolocated directory of the installation
43
- #
44
- # @return [string] or nil
45
- def self.autolocated_installation
46
- # Arbitrarily, I'm going to pick the force installed location first
47
- # if it exists. I'm not sure why we would have both, but if we did
48
- # a force install then let's make sure we actually use it.
49
- locations = [self.force_install_location, self.existing_installation]
50
- locations.find { |loc| !loc.nil? && File.exist?(loc) }
43
+ # The executable Arduino file in an existing installation, or nil
44
+ # @return [Pathname]
45
+ def self.existing_executable
46
+ self.must_implement(__method__)
51
47
  end
52
48
 
53
- # The path to the directory of an existing installation, or nil
49
+ # The local file (dir) name of the desired IDE package (zip/tar/etc)
54
50
  # @return [string]
55
- def self.existing_installation
56
- self.must_implement(__method__)
51
+ def package_file
52
+ self.class.must_implement(__method__)
57
53
  end
58
54
 
59
- # The executable Arduino file in an existing installation, or nil
55
+ # The local filename of the extracted IDE package (zip/tar/etc)
60
56
  # @return [string]
61
- def self.existing_executable
57
+ def self.extracted_file
62
58
  self.must_implement(__method__)
63
59
  end
64
60
 
65
61
  # The executable Arduino file in a forced installation, or nil
66
- # @return [string]
62
+ # @return [Pathname]
67
63
  def self.force_installed_executable
68
- self.must_implement(__method__)
64
+ Pathname.new(ENV['HOME']) + self.extracted_file
69
65
  end
70
66
 
71
67
  # The technology that will be used to complete the download
72
68
  # (for logging purposes)
73
69
  # @return [string]
74
- def downloader
70
+ def self.downloader
75
71
  "open-uri"
76
72
  end
77
73
 
78
74
  # The technology that will be used to extract the download
79
75
  # (for logging purposes)
80
76
  # @return [string]
81
- def extracter
82
- "Zip"
83
- end
84
-
85
- # The URL of the desired IDE package (zip/tar/etc) for this platform
86
- # @return [string]
87
- def package_url
88
- "https://downloads.arduino.cc/#{package_file}"
77
+ def self.extracter
78
+ self.must_implement(__method__)
89
79
  end
90
80
 
91
- # The local file (dir) name of the desired IDE package (zip/tar/etc)
92
- # @return [string]
93
- def package_file
94
- self.class.must_implement(__method__)
81
+ # Extract the package_file to extracted_file
82
+ # @return [bool] whether successful
83
+ def self.extract(_package_file)
84
+ self.must_implement(__method__)
95
85
  end
96
86
 
97
- # The local filename of the extracted IDE package (zip/tar/etc)
87
+ # The URL of the desired IDE package (zip/tar/etc) for this platform
98
88
  # @return [string]
99
- def extracted_file
100
- self.class.must_implement(__method__)
101
- end
102
-
103
- # @return [String] The location where a forced install will go
104
- def self.force_install_location
105
- File.join(ENV['HOME'], 'arduino_ci_ide')
89
+ def package_url
90
+ "https://github.com/arduino/arduino-cli/releases/download/#{@desired_version}/#{package_file}"
106
91
  end
107
92
 
108
93
  # Download the package_url to package_file
@@ -130,26 +115,10 @@ module ArduinoCI
130
115
  @output.puts "\nArduino force-install failed downloading #{package_url}: #{e}"
131
116
  end
132
117
 
133
- # Extract the package_file to extracted_file
134
- # @return [bool] whether successful
135
- def extract
136
- Zip::File.open(package_file) do |zip|
137
- batch_size = [1, (zip.size / 100).to_i].max
138
- dots = 0
139
- zip.each do |file|
140
- @output.print "." if (dots % batch_size).zero?
141
- file.restore_permissions = true
142
- file.extract { accept_all }
143
- dots += 1
144
- end
145
- end
146
- end
147
-
148
- # Move the extracted package file from extracted_file to the force_install_location
118
+ # Move the extracted package file from extracted_file to the force_installed_executable
149
119
  # @return [bool] whether successful
150
120
  def install
151
- # Move only the content of the directory
152
- FileUtils.mv extracted_file, self.class.force_install_location
121
+ FileUtils.mv self.class.extracted_file.to_s, self.class.force_installed_executable.to_s
153
122
  end
154
123
 
155
124
  # Forcibly install Arduino on linux from the web
@@ -161,39 +130,40 @@ module ArduinoCI
161
130
  return false
162
131
  end
163
132
 
133
+ arduino_package = "Arduino #{@desired_version} package"
164
134
  attempts = 0
165
135
 
166
136
  loop do
167
- if File.exist? package_file
168
- @output.puts "Arduino package seems to have been downloaded already" if attempts.zero?
137
+ if File.exist?(package_file)
138
+ @output.puts "#{arduino_package} seems to have been downloaded already at #{package_file}" if attempts.zero?
169
139
  break
170
140
  elsif attempts >= DOWNLOAD_ATTEMPTS
171
141
  break @output.puts "After #{DOWNLOAD_ATTEMPTS} attempts, failed to download #{package_url}"
172
142
  else
173
- @output.print "Attempting to download Arduino package with #{downloader}"
143
+ @output.print "Attempting to download #{arduino_package} with #{self.class.downloader}"
174
144
  download
175
145
  @output.puts
176
146
  end
177
147
  attempts += 1
178
148
  end
179
149
 
180
- if File.exist? extracted_file
181
- @output.puts "Arduino package seems to have been extracted already"
182
- elsif File.exist? package_file
183
- @output.print "Extracting archive with #{extracter}"
184
- extract
150
+ if File.exist?(self.class.extracted_file)
151
+ @output.puts "#{arduino_package} seems to have been extracted already at #{self.class.extracted_file}"
152
+ elsif File.exist?(package_file)
153
+ @output.print "Extracting archive with #{self.class.extracter}"
154
+ self.class.extract(package_file)
185
155
  @output.puts
186
156
  end
187
157
 
188
- if File.exist? self.class.force_install_location
189
- @output.puts "Arduino package seems to have been installed already"
190
- elsif File.exist? extracted_file
158
+ if File.exist?(self.class.force_installed_executable)
159
+ @output.puts "#{arduino_package} seems to have been installed already at #{self.class.force_installed_executable}"
160
+ elsif File.exist?(self.class.extracted_file)
191
161
  install
192
162
  else
193
- @output.puts "Could not find extracted archive (tried #{extracted_file})"
163
+ @output.puts "Could not find extracted archive (tried #{self.class.extracted_file})"
194
164
  end
195
165
 
196
- File.exist? self.class.force_install_location
166
+ File.exist?(self.class.force_installed_executable)
197
167
  end
198
168
 
199
169
  end
@@ -1,7 +1,5 @@
1
1
  require "arduino_ci/arduino_downloader"
2
2
 
3
- USE_BUILDER = false
4
-
5
3
  module ArduinoCI
6
4
 
7
5
  # Manage the linux download & install of Arduino
@@ -10,13 +8,25 @@ module ArduinoCI
10
8
  # The local filename of the desired IDE package (zip/tar/etc)
11
9
  # @return [string]
12
10
  def package_file
13
- "#{extracted_file}-linux64.tar.xz"
11
+ "arduino-cli_#{@desired_version}_Linux_64bit.tar.gz"
12
+ end
13
+
14
+ # The local file (dir) name of the extracted IDE package (zip/tar/etc)
15
+ # @return [string]
16
+ def self.extracted_file
17
+ "arduino-cli"
18
+ end
19
+
20
+ # The executable Arduino file in an existing installation, or nil
21
+ # @return [string]
22
+ def self.existing_executable
23
+ Host.which("arduino-cli")
14
24
  end
15
25
 
16
26
  # Make any preparations or run any checks prior to making changes
17
27
  # @return [string] Error message, or nil if success
18
28
  def prepare
19
- reqs = [extracter]
29
+ reqs = [self.class.extracter]
20
30
  reqs.each do |req|
21
31
  return "#{req} does not appear to be installed!" unless Host.which(req)
22
32
  end
@@ -26,62 +36,14 @@ module ArduinoCI
26
36
  # The technology that will be used to extract the download
27
37
  # (for logging purposes)
28
38
  # @return [string]
29
- def extracter
39
+ def self.extracter
30
40
  "tar"
31
41
  end
32
42
 
33
43
  # Extract the package_file to extracted_file
34
44
  # @return [bool] whether successful
35
- def extract
36
- system(extracter, "xf", package_file)
37
- end
38
-
39
- # The local file (dir) name of the extracted IDE package (zip/tar/etc)
40
- # @return [string]
41
- def extracted_file
42
- "arduino-#{@desired_ide_version}"
43
- end
44
-
45
- # The path to the directory of an existing installation, or nil
46
- # @return [string]
47
- def self.existing_installation
48
- exe = self.existing_executable
49
- return nil if exe.nil?
50
-
51
- File.dirname(exe) # it's not really this
52
- # but for this platform it doesn't really matter
53
- end
54
-
55
- # The executable Arduino file in an existing installation, or nil
56
- # @return [string]
57
- def self.existing_executable
58
- if USE_BUILDER
59
- # builder_name = "arduino-builder"
60
- # cli_place = Host.which(builder_name)
61
- # unless cli_place.nil?
62
- # ret = ArduinoCmdLinuxBuilder.new
63
- # ret.base_cmd = [cli_place]
64
- # return ret
65
- # end
66
- end
67
- Host.which("arduino")
68
- end
69
-
70
- # The executable Arduino file in a forced installation, or nil
71
- # @return [string]
72
- def self.force_installed_executable
73
- if USE_BUILDER
74
- # forced_builder = File.join(ArduinoCmdLinuxBuilder.force_install_location, builder_name)
75
- # if File.exist?(forced_builder)
76
- # ret = ArduinoCmdLinuxBuilder.new
77
- # ret.base_cmd = [forced_builder]
78
- # return ret
79
- # end
80
- end
81
- forced_arduino = File.join(self.force_install_location, "arduino")
82
- return forced_arduino if File.exist? forced_arduino
83
-
84
- nil
45
+ def self.extract(package_file)
46
+ system(extracter, "xf", package_file, extracted_file)
85
47
  end
86
48
 
87
49
  end