diff --git a/CHANGELOG.md b/CHANGELOG.md index acf3d0eb..153c7c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Changed +- Arduino backend is now `arduino-cli` version `0.13.0` ### Deprecated +- `arduino_ci_remote.rb` CLI switch `--skip-compilation` +- Deprecated `arduino_ci_remote.rb` in favor of `arduino_ci.rb` ### Removed +- `ARDUINO_CI_SKIP_SPLASH_SCREEN_RSPEC_TESTS` no longer affects any tests because there are no longer splash screens since switching to `arduino-cli` ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63e4de20..e0a691e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,6 @@ See `SampleProjects/TestSomething/test/*.cpp` for the existing tests (run by rsp To speed up testing by targeting only the files you're working on, you can set several environment variables that `bundle exec rspec` will respond to: -* `ARDUINO_CI_SKIP_SPLASH_SCREEN_RSPEC_TESTS`: if set, this will avoid any rspec test that calls the arduino executable (and as such, causes the splash screen to pop up). * `ARDUINO_CI_SKIP_RUBY_RSPEC_TESTS`: if set, this will skip all tests against ruby code (useful if you are not changing Ruby code). * `ARDUINO_CI_SKIP_CPP_RSPEC_TESTS`: if set, this will skip all tests against the `TestSomething` sample project (useful if you are not changing C++ code). * `ARDUINO_CI_SELECT_CPP_TESTS=`: if set, this will skip all C++ unit tests whose filenames don't match the provided glob (executed in the tests directory) diff --git a/exe/arduino_ci.rb b/exe/arduino_ci.rb old mode 100644 new mode 100755 index 4ea2d614..09000172 --- a/exe/arduino_ci.rb +++ b/exe/arduino_ci.rb @@ -262,9 +262,6 @@ def perform_compilation_tests(config) return end - # index the existing libraries - attempt("Indexing libraries") { @arduino_cmd.index_libraries } unless @arduino_cmd.libraries_indexed - # initialize library under test installed_library_path = attempt("Installing library under test") do @arduino_cmd.install_local_library(Pathname.new(".")) @@ -345,7 +342,6 @@ def perform_compilation_tests(config) install_arduino_library_dependencies(aux_libraries) - last_board = nil if config.platforms_to_build.empty? inform("Skipping builds") { "no platforms were requested" } return @@ -356,8 +352,6 @@ def perform_compilation_tests(config) return end - attempt("Setting compiler warning level") { @arduino_cmd.set_pref("compiler.warning_level", "all") } - # switching boards takes time, so iterate board first # _then_ whichever examples match it examples_by_platform = library_examples.each_with_object({}) do |example_path, acc| @@ -370,13 +364,10 @@ def perform_compilation_tests(config) examples_by_platform.each do |platform, example_paths| board = example_platform_info[platform][:board] - assure("Switching to board for #{platform} (#{board})") { @arduino_cmd.use_board(board) } unless last_board == board - last_board = board - example_paths.each do |example_path| example_name = File.basename(example_path) - attempt("Verifying #{example_name}") do - ret = @arduino_cmd.verify_sketch(example_path) + attempt("Compiling #{example_name} for #{board}") do + ret = @arduino_cmd.compile_sketch(example_path, board) unless ret puts puts "Last command: #{@arduino_cmd.last_msg}" @@ -393,7 +384,7 @@ def perform_compilation_tests(config) config = ArduinoCI::CIConfig.default.from_project_library @arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! -inform("Located Arduino binary") { @arduino_cmd.binary_path.to_s } +inform("Located arduino-cli binary") { @arduino_cmd.binary_path.to_s } perform_unit_tests(config) perform_compilation_tests(config) diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index 577617ab..e24d8036 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -1,5 +1,6 @@ require 'fileutils' require 'pathname' +require 'json' # workaround for https://github.com/arduino/Arduino/issues/3535 WORKAROUND_LIB = "USBHost".freeze @@ -24,17 +25,10 @@ def self.flag(name, text = nil) self.class_eval("def flag_#{name};\"#{text}\";end", __FILE__, __LINE__) end - # the array of command components to launch the Arduino executable - # @return [Array] - attr_accessor :base_cmd - # the actual path to the executable on this platform # @return [Pathname] attr_accessor :binary_path - # part of a workaround for https://github.com/arduino/Arduino/issues/3535 - attr_reader :libraries_indexed - # @return [String] STDOUT of the most recently-run command attr_reader :last_out @@ -44,97 +38,29 @@ def self.flag(name, text = nil) # @return [String] the most recently-run command attr_reader :last_msg + # @return [Array] Additional URLs for the boards manager + attr_reader :additional_urls + # set the command line flags (undefined for now). # These vary between gui/cli. Inline comments added for greppability - flag :get_pref # flag_get_pref - flag :set_pref # flag_set_pref - flag :save_prefs # flag_save_prefs - flag :use_board # flag_use_board flag :install_boards # flag_install_boards flag :install_library # flag_install_library flag :verify # flag_verify - def initialize - @prefs_cache = {} - @prefs_fetched = false - @libraries_indexed = false + def initialize(binary_path) + @binary_path = binary_path + @additional_urls = [] @last_out = "" @last_err = "" @last_msg = "" end - # Convert a preferences dump into a flat hash - # @param arduino_output [String] The raw Arduino executable output - # @return [Hash] preferences as a hash - def parse_pref_string(arduino_output) - lines = arduino_output.split("\n").select { |l| l.include? "=" } - ret = lines.each_with_object({}) do |e, acc| - parts = e.split("=", 2) - acc[parts[0]] = parts[1] - acc - end - ret - end - - # @return [String] the path to the Arduino libraries directory - def lib_dir - Pathname.new(get_pref("sketchbook.path")) + "libraries" - end - - # fetch preferences in their raw form - # @return [String] Preferences as a set of lines - def _prefs_raw - resp = run_and_capture(flag_get_pref) - fail_msg = "Arduino binary failed to operate as expected; you will have to troubleshoot it manually" - raise ArduinoExecutionError, "#{fail_msg}. The command was #{@last_msg}" unless resp[:success] - - @prefs_fetched = true - resp[:out] - end - - # Get the Arduino preferences, from cache if possible - # @return [Hash] The full set of preferences - def prefs - prefs_raw = _prefs_raw unless @prefs_fetched - return nil if prefs_raw.nil? - - @prefs_cache = parse_pref_string(prefs_raw) - @prefs_cache.clone - end - - # get a preference key - # @param key [String] The preferences key to look up - # @return [String] The preference value - def get_pref(key) - data = @prefs_fetched ? @prefs_cache : prefs - data[key] - end - - # underlying preference-setter. - # @param key [String] The preference name - # @param value [String] The value to set to - # @return [bool] whether the command succeeded - def _set_pref(key, value) - run_and_capture(flag_set_pref, "#{key}=#{value}", flag_save_prefs)[:success] - end - - # set a preference key/value pair, and update the cache. - # @param key [String] the preference key - # @param value [String] the preference value - # @return [bool] whether the command succeeded - def set_pref(key, value) - prefs unless @prefs_fetched # update cache first - success = _set_pref(key, value) - @prefs_cache[key] = value if success - success - end - def _wrap_run(work_fn, *args, **kwargs) # do some work to extract & merge environment variables if they exist has_env = !args.empty? && args[0].class == Hash env_vars = has_env ? args[0] : {} actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args - full_args = @base_cmd + actual_args + full_args = [binary_path.to_s, "--format", "json"] + actual_args full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ") @@ -156,19 +82,34 @@ def run_and_capture(*args, **kwargs) ret end + def capture_json(*args, **kwargs) + ret = run_and_capture(*args, **kwargs) + ret[:json] = JSON.parse(ret[:out]) + end + + # Get a dump of the entire config + # @return [Hash] The configuration + def config_dump + capture_json("config", "dump") + end + + # @return [String] the path to the Arduino libraries directory + def lib_dir + Pathname.new(config_dump["directories"]["user"]) + "libraries" + end + # Board manager URLs # @return [Array] The additional URLs used by the board manager def board_manager_urls - url_list = get_pref("boardsmanager.additional.urls") - return [] if url_list.nil? - - url_list.split(",") + config_dump["board_manager"]["additional_urls"] + @additional_urls end # Set board manager URLs # @return [Array] The additional URLs used by the board manager def board_manager_urls=(all_urls) - set_pref("boardsmanager.additional.urls", all_urls.join(",")) + raise ArgumentError("all_urls should be an array, got #{all_urls.class}") unless all_urls.is_a? Array + + @additional_urls = all_urls end # check whether a board is installed @@ -177,48 +118,33 @@ def board_manager_urls=(all_urls) # @param boardname [String] The board to test # @return [bool] Whether the board is installed def board_installed?(boardname) - run_and_capture(flag_use_board, boardname)[:success] + # capture_json("core", "list")[:json].find { |b| b["ID"] == boardname } # nope, this is for the family + run_and_capture("board", "details", "--fqbn", boardname)[:success] end # install a board by name # @param name [String] the board name # @return [bool] whether the command succeeded def install_boards(boardfamily) - # TODO: find out why IO.pipe fails but File::NULL succeeds :( - result = run_and_capture(flag_install_boards, boardfamily) - already_installed = result[:err].include?("Platform is already installed!") - result[:success] || already_installed + result = run_and_capture("core", "install", boardfamily) + result[:success] end - # install a library by name - # @param name [String] the library name - # @return [bool] whether the command succeeded - def _install_library(library_name) - result = run_and_capture(flag_install_library, library_name) - - already_installed = result[:err].include?("Library is already installed: #{library_name}") - success = result[:success] || already_installed - - @libraries_indexed = (@libraries_indexed || success) if library_name == WORKAROUND_LIB - success - end - - # index the set of libraries by installing a dummy library - # related to WORKAROUND_LIB and https://github.com/arduino/Arduino/issues/3535 - # TODO: unclear if this is still necessary - def index_libraries - return true if @libraries_indexed - - _install_library(WORKAROUND_LIB) - @libraries_indexed + # @return [Hash] information about installed libraries via the CLI + def installed_libraries + capture_json("lib", "list")[:json] end # install a library by name # @param name [String] the library name + # @param version [String] the version to install # @return [bool] whether the command succeeded - def install_library(library_name) - index_libraries - _install_library(library_name) + def install_library(library_name, version = nil) + return true if library_present?(library_name) + + fqln = version.nil? ? library_name : "#{library_name}@#{version}" + result = run_and_capture("lib", "install", fqln) + result[:success] end # generate the (very likely) path of a library given its name @@ -239,47 +165,20 @@ def library_present?(library_name) library_path(library_name).exist? end - # update the library index - # @return [bool] Whether the update succeeded - def update_library_index - # install random lib so the arduino IDE grabs a new library index - # see: https://github.com/arduino/Arduino/issues/3535 - install_library(WORKAROUND_LIB) - end - - # use a particular board for compilation + # @param path [String] The sketch to compile # @param boardname [String] The board to use # @return [bool] whether the command succeeded - def use_board(boardname) - run_and_capture(flag_use_board, boardname, flag_save_prefs)[:success] - end - - # use a particular board for compilation, installing it if necessary - # @param boardname [String] The board to use - # @return [bool] whether the command succeeded - def use_board!(boardname) - return true if use_board(boardname) - - boardfamily = boardname.split(":")[0..1].join(":") - puts "Board '#{boardname}' not found; attempting to install '#{boardfamily}'" - return false unless install_boards(boardfamily) # guess board family from first 2 :-separated fields - - use_board(boardname) - end - - # @param path [String] The sketch to verify - # @return [bool] whether the command succeeded - def verify_sketch(path) + def compile_sketch(path, boardname) ext = File.extname path unless ext.casecmp(".ino").zero? - @last_msg = "Refusing to verify sketch with '#{ext}' extension -- rename it to '.ino'!" + @last_msg = "Refusing to compile sketch with '#{ext}' extension -- rename it to '.ino'!" return false end unless File.exist? path - @last_msg = "Can't verify Sketch at nonexistent path '#{path}'!" + @last_msg = "Can't compile Sketch at nonexistent path '#{path}'!" return false end - ret = run_and_capture(flag_verify, path) + ret = run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", "--dry-run", path) ret[:success] end diff --git a/lib/arduino_ci/arduino_cmd_linux.rb b/lib/arduino_ci/arduino_cmd_linux.rb deleted file mode 100644 index ff6c137a..00000000 --- a/lib/arduino_ci/arduino_cmd_linux.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'arduino_ci/arduino_cmd' -require 'timeout' - -module ArduinoCI - - # Implementation of Arduino linux IDE commands - class ArduinoCmdLinux < ArduinoCmd - flag :get_pref, "--get-pref" - flag :set_pref, "--pref" - flag :save_prefs, "--save-prefs" - flag :use_board, "--board" - flag :install_boards, "--install-boards" - flag :install_library, "--install-library" - flag :verify, "--verify" - end - -end diff --git a/lib/arduino_ci/arduino_cmd_linux_builder.rb b/lib/arduino_ci/arduino_cmd_linux_builder.rb deleted file mode 100644 index 679c72b6..00000000 --- a/lib/arduino_ci/arduino_cmd_linux_builder.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "arduino_ci/host" -require 'arduino_ci/arduino_cmd' - -module ArduinoCI - - # Implementation of Arduino linux CLI commands - class ArduinoCmdLinuxBuilder < ArduinoCmd - - flag :get_pref, "--get-pref" # apparently doesn't exist - flag :set_pref, "--pref" # apparently doesn't exist - flag :save_prefs, "--save-prefs" # apparently doesn't exist - flag :use_board, "-fqbn" - flag :install_boards, "--install-boards" # apparently doesn't exist - flag :install_library, "--install-library" # apparently doesn't exist - flag :verify, "-compile" - - end - -end diff --git a/lib/arduino_ci/arduino_cmd_osx.rb b/lib/arduino_ci/arduino_cmd_osx.rb deleted file mode 100644 index 196e1453..00000000 --- a/lib/arduino_ci/arduino_cmd_osx.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "arduino_ci/host" -require 'arduino_ci/arduino_cmd' - -module ArduinoCI - - # Implementation of OSX commands - class ArduinoCmdOSX < ArduinoCmd - flag :get_pref, "--get-pref" - flag :set_pref, "--pref" - flag :save_prefs, "--save-prefs" - flag :use_board, "--board" - flag :install_boards, "--install-boards" - flag :install_library, "--install-library" - flag :verify, "--verify" - end - -end diff --git a/lib/arduino_ci/arduino_cmd_windows.rb b/lib/arduino_ci/arduino_cmd_windows.rb deleted file mode 100644 index 3282dfea..00000000 --- a/lib/arduino_ci/arduino_cmd_windows.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "arduino_ci/host" -require 'arduino_ci/arduino_cmd' - -module ArduinoCI - - # Implementation of OSX commands - class ArduinoCmdWindows < ArduinoCmd - flag :get_pref, "--get-pref" - flag :set_pref, "--pref" - flag :save_prefs, "--save-prefs" - flag :use_board, "--board" - flag :install_boards, "--install-boards" - flag :install_library, "--install-library" - flag :verify, "--verify" - end - -end diff --git a/lib/arduino_ci/arduino_downloader.rb b/lib/arduino_ci/arduino_downloader.rb index f619bcf2..77e9094a 100644 --- a/lib/arduino_ci/arduino_downloader.rb +++ b/lib/arduino_ci/arduino_downloader.rb @@ -1,4 +1,5 @@ require 'fileutils' +require 'pathname' require 'net/http' require 'open-uri' require 'zip' @@ -10,10 +11,10 @@ module ArduinoCI # Manage the OS-specific download & install of Arduino class ArduinoDownloader - # @param desired_ide_version [string] Version string e.g. 1.8.7 + # @param desired_version [string] Version string e.g. 1.8.7 # @param output [IO] $stdout, $stderr, File.new(/dev/null, 'w'), etc. where console output will be sent - def initialize(desired_ide_version, output = $stdout) - @desired_ide_version = desired_ide_version + def initialize(desired_version, output = $stdout) + @desired_version = desired_version @output = output end @@ -30,7 +31,7 @@ def prepare # The autolocated executable of the installation # - # @return [string] or nil + # @return [Pathname] or nil def self.autolocated_executable # Arbitrarily, I'm going to pick the force installed location first # if it exists. I'm not sure why we would have both, but if we did @@ -39,70 +40,54 @@ def self.autolocated_executable locations.find { |loc| !loc.nil? && File.exist?(loc) } end - # The autolocated directory of the installation - # - # @return [string] or nil - def self.autolocated_installation - # Arbitrarily, I'm going to pick the force installed location first - # if it exists. I'm not sure why we would have both, but if we did - # a force install then let's make sure we actually use it. - locations = [self.force_install_location, self.existing_installation] - locations.find { |loc| !loc.nil? && File.exist?(loc) } + # The executable Arduino file in an existing installation, or nil + # @return [Pathname] + def self.existing_executable + self.must_implement(__method__) end - # The path to the directory of an existing installation, or nil + # The local file (dir) name of the desired IDE package (zip/tar/etc) # @return [string] - def self.existing_installation - self.must_implement(__method__) + def package_file + self.class.must_implement(__method__) end - # The executable Arduino file in an existing installation, or nil + # The local filename of the extracted IDE package (zip/tar/etc) # @return [string] - def self.existing_executable + def self.extracted_file self.must_implement(__method__) end # The executable Arduino file in a forced installation, or nil - # @return [string] + # @return [Pathname] def self.force_installed_executable - self.must_implement(__method__) + Pathname.new(ENV['HOME']) + self.extracted_file end # The technology that will be used to complete the download # (for logging purposes) # @return [string] - def downloader + def self.downloader "open-uri" end # The technology that will be used to extract the download # (for logging purposes) # @return [string] - def extracter - "Zip" - end - - # The URL of the desired IDE package (zip/tar/etc) for this platform - # @return [string] - def package_url - "https://downloads.arduino.cc/#{package_file}" + def self.extracter + self.must_implement(__method__) end - # The local file (dir) name of the desired IDE package (zip/tar/etc) - # @return [string] - def package_file - self.class.must_implement(__method__) + # Extract the package_file to extracted_file + # @return [bool] whether successful + def self.extract + self.must_implement(__method__) end - # The local filename of the extracted IDE package (zip/tar/etc) + # The URL of the desired IDE package (zip/tar/etc) for this platform # @return [string] - def extracted_file - self.class.must_implement(__method__) - end - - # @return [String] The location where a forced install will go - def self.force_install_location - File.join(ENV['HOME'], 'arduino_ci_ide') + def package_url + "https://github.com/arduino/arduino-cli/releases/download/#{@desired_version}/#{package_file}" end # Download the package_url to package_file @@ -130,26 +115,10 @@ def download @output.puts "\nArduino force-install failed downloading #{package_url}: #{e}" end - # Extract the package_file to extracted_file - # @return [bool] whether successful - def extract - Zip::File.open(package_file) do |zip| - batch_size = [1, (zip.size / 100).to_i].max - dots = 0 - zip.each do |file| - @output.print "." if (dots % batch_size).zero? - file.restore_permissions = true - file.extract { accept_all } - dots += 1 - end - end - end - - # Move the extracted package file from extracted_file to the force_install_location + # Move the extracted package file from extracted_file to the force_installed_executable # @return [bool] whether successful def install - # Move only the content of the directory - FileUtils.mv extracted_file, self.class.force_install_location + FileUtils.mv self.class.extracted_file.to_s, self.class.force_installed_executable.to_s end # Forcibly install Arduino on linux from the web @@ -161,40 +130,40 @@ def execute return false end - arduino_package = "Arduino #{@desired_ide_version} package" + arduino_package = "Arduino #{@desired_version} package" attempts = 0 loop do if File.exist? package_file - @output.puts "#{arduino_package} seems to have been downloaded already" if attempts.zero? + @output.puts "#{arduino_package} seems to have been downloaded already at #{package_file}" if attempts.zero? break elsif attempts >= DOWNLOAD_ATTEMPTS break @output.puts "After #{DOWNLOAD_ATTEMPTS} attempts, failed to download #{package_url}" else - @output.print "Attempting to download #{arduino_package} with #{downloader}" + @output.print "Attempting to download #{arduino_package} with #{self.class.downloader}" download @output.puts end attempts += 1 end - if File.exist? extracted_file - @output.puts "#{arduino_package} seems to have been extracted already" + if File.exist? self.class.extracted_file + @output.puts "#{arduino_package} seems to have been extracted already at #{self.class.extracted_file}" elsif File.exist? package_file - @output.print "Extracting archive with #{extracter}" - extract + @output.print "Extracting archive with #{self.class.extracter}" + self.class.extract @output.puts end - if File.exist? self.class.force_install_location - @output.puts "#{arduino_package} seems to have been installed already" - elsif File.exist? extracted_file + if File.exist? self.class.force_installed_executable + @output.puts "#{arduino_package} seems to have been installed already at #{self.class.force_installed_executable}" + elsif File.exist? self.class.extracted_file install else - @output.puts "Could not find extracted archive (tried #{extracted_file})" + @output.puts "Could not find extracted archive (tried #{self.class.extracted_file})" end - File.exist? self.class.force_install_location + File.exist? self.class.force_installed_executable end end diff --git a/lib/arduino_ci/arduino_downloader_linux.rb b/lib/arduino_ci/arduino_downloader_linux.rb index efbc34e2..244cc4a3 100644 --- a/lib/arduino_ci/arduino_downloader_linux.rb +++ b/lib/arduino_ci/arduino_downloader_linux.rb @@ -1,7 +1,5 @@ require "arduino_ci/arduino_downloader" -USE_BUILDER = false - module ArduinoCI # Manage the linux download & install of Arduino @@ -10,13 +8,25 @@ class ArduinoDownloaderLinux < ArduinoDownloader # The local filename of the desired IDE package (zip/tar/etc) # @return [string] def package_file - "#{extracted_file}-linux64.tar.xz" + "arduino-cli_#{@desired_version}_Linux_64bit.tar.gz" + end + + # The local file (dir) name of the extracted IDE package (zip/tar/etc) + # @return [string] + def self.extracted_file + "arduino-cli" + end + + # The executable Arduino file in an existing installation, or nil + # @return [string] + def self.existing_executable + Host.which("arduino-cli") end # Make any preparations or run any checks prior to making changes # @return [string] Error message, or nil if success def prepare - reqs = [extracter] + reqs = [self.class.extracter] reqs.each do |req| return "#{req} does not appear to be installed!" unless Host.which(req) end @@ -26,62 +36,14 @@ def prepare # The technology that will be used to extract the download # (for logging purposes) # @return [string] - def extracter + def self.extracter "tar" end # Extract the package_file to extracted_file # @return [bool] whether successful - def extract - system(extracter, "xf", package_file) - end - - # The local file (dir) name of the extracted IDE package (zip/tar/etc) - # @return [string] - def extracted_file - "arduino-#{@desired_ide_version}" - end - - # The path to the directory of an existing installation, or nil - # @return [string] - def self.existing_installation - exe = self.existing_executable - return nil if exe.nil? - - File.dirname(exe) # it's not really this - # but for this platform it doesn't really matter - end - - # The executable Arduino file in an existing installation, or nil - # @return [string] - def self.existing_executable - if USE_BUILDER - # builder_name = "arduino-builder" - # cli_place = Host.which(builder_name) - # unless cli_place.nil? - # ret = ArduinoCmdLinuxBuilder.new - # ret.base_cmd = [cli_place] - # return ret - # end - end - Host.which("arduino") - end - - # The executable Arduino file in a forced installation, or nil - # @return [string] - def self.force_installed_executable - if USE_BUILDER - # forced_builder = File.join(ArduinoCmdLinuxBuilder.force_install_location, builder_name) - # if File.exist?(forced_builder) - # ret = ArduinoCmdLinuxBuilder.new - # ret.base_cmd = [forced_builder] - # return ret - # end - end - forced_arduino = File.join(self.force_install_location, "arduino") - return forced_arduino if File.exist? forced_arduino - - nil + def self.extract + system(extracter, "xf", package_file, extracted_file) end end diff --git a/lib/arduino_ci/arduino_downloader_osx.rb b/lib/arduino_ci/arduino_downloader_osx.rb index 02ea2347..e37a5e30 100644 --- a/lib/arduino_ci/arduino_downloader_osx.rb +++ b/lib/arduino_ci/arduino_downloader_osx.rb @@ -8,54 +8,42 @@ class ArduinoDownloaderOSX < ArduinoDownloader # The local filename of the desired IDE package (zip/tar/etc) # @return [string] def package_file - "arduino-#{@desired_ide_version}-macosx.zip" + "arduino-cli_#{@desired_version}_macOS_64bit.tar.gz" end # The local file (dir) name of the extracted IDE package (zip/tar/etc) # @return [string] - def extracted_file - "Arduino.app" + def self.extracted_file + "arduino-cli" end - # @return [String] The location where a forced install will go - def self.force_install_location - # include the .app extension - File.join(ENV['HOME'], 'Arduino.app') - end - - # An existing Arduino directory in one of the given directories, or nil - # @param Array a list of places to look + # The executable Arduino file in an existing installation, or nil # @return [string] - def self.find_existing_arduino_dir(paths) - paths.find(&File.method(:exist?)) + def self.existing_executable + Host.which("arduino-cli") end - # An existing Arduino file in one of the given directories, or nil - # @param Array a list of places to look for the executable - # @return [string] - def self.find_existing_arduino_exe(paths) - paths.find do |path| - exe = File.join(path, "MacOS", "Arduino") - File.exist? exe + # Make any preparations or run any checks prior to making changes + # @return [string] Error message, or nil if success + def prepare + reqs = [self.class.extracter] + reqs.each do |req| + return "#{req} does not appear to be installed!" unless Host.which(req) end + nil end - # The path to the directory of an existing installation, or nil + # The technology that will be used to extract the download + # (for logging purposes) # @return [string] - def self.existing_installation - self.find_existing_arduino_dir(["/Applications/Arduino.app"]) + def self.extracter + "tar" end - # The executable Arduino file in an existing installation, or nil - # @return [string] - def self.existing_executable - self.find_existing_arduino_exe(["/Applications/Arduino.app"]) - end - - # The executable Arduino file in a forced installation, or nil - # @return [string] - def self.force_installed_executable - self.find_existing_arduino_exe([self.force_install_location]) + # Extract the package_file to extracted_file + # @return [bool] whether successful + def self.extract + system(extracter, "xf", package_file, extracted_file) end end diff --git a/lib/arduino_ci/arduino_downloader_windows.rb b/lib/arduino_ci/arduino_downloader_windows.rb index f2c91669..c2cfbffb 100644 --- a/lib/arduino_ci/arduino_downloader_windows.rb +++ b/lib/arduino_ci/arduino_downloader_windows.rb @@ -10,19 +10,6 @@ module ArduinoCI # Manage the POSIX download & install of Arduino class ArduinoDownloaderWindows < ArduinoDownloader - # Make any preparations or run any checks prior to making changes - # @return [string] Error message, or nil if success - def prepare - nil - end - - # The technology that will be used to complete the download - # (for logging purposes) - # @return [string] - def downloader - "open-uri" - end - # Download the package_url to package_file # @return [bool] whether successful def download @@ -35,29 +22,28 @@ def download @output.puts "\nArduino force-install failed downloading #{package_url}: #{e}" end - # Move the extracted package file from extracted_file to the force_install_location - # @return [bool] whether successful - def install - # Move only the content of the directory - FileUtils.mv extracted_file, self.class.force_install_location - end - # The local filename of the desired IDE package (zip/tar/etc) # @return [string] def package_file - "#{extracted_file}-windows.zip" + "arduino-cli_#{@desired_version}_Windows_64bit.zip" + end + + # The executable Arduino file in an existing installation, or nil + # @return [string] + def self.existing_executable + Host.which("arduino-cli") end # The technology that will be used to extract the download # (for logging purposes) # @return [string] - def extracter + def self.extracter "Expand-Archive" end # Extract the package_file to extracted_file # @return [bool] whether successful - def extract + def self.extract Zip::File.open(package_file) do |zip| zip.each do |file| file.extract(file.name) @@ -67,36 +53,8 @@ def extract # The local file (dir) name of the extracted IDE package (zip/tar/etc) # @return [string] - def extracted_file - "arduino-#{@desired_ide_version}" - end - - # The path to the directory of an existing installation, or nil - # @return [string] - def self.existing_installation - exe = self.existing_executable - return nil if exe.nil? - - File.dirname(exe) - end - - # The executable Arduino file in an existing installation, or nil - # @return [string] - def self.existing_executable - arduino_reg = 'SOFTWARE\WOW6432Node\Arduino' - Win32::Registry::HKEY_LOCAL_MACHINE.open(arduino_reg).find do |reg| - path = reg.read_s('Install_Dir') - exe = File.join(path, "arduino_debug.exe") - File.exist? exe - end - rescue - nil - end - - # The executable Arduino file in a forced installation, or nil - # @return [string] - def self.force_installed_executable - File.join(self.force_install_location, "arduino_debug.exe") + def self.extracted_file + "arduino-cli.exe" end end diff --git a/lib/arduino_ci/arduino_installation.rb b/lib/arduino_ci/arduino_installation.rb index 9ca32ada..0f826446 100644 --- a/lib/arduino_ci/arduino_installation.rb +++ b/lib/arduino_ci/arduino_installation.rb @@ -1,15 +1,11 @@ require 'pathname' require "arduino_ci/host" -require "arduino_ci/arduino_cmd_osx" -require "arduino_ci/arduino_cmd_linux" -require "arduino_ci/arduino_cmd_windows" -require "arduino_ci/arduino_cmd_linux_builder" +require "arduino_ci/arduino_cmd" require "arduino_ci/arduino_downloader_osx" require "arduino_ci/arduino_downloader_linux" - require "arduino_ci/arduino_downloader_windows" if ArduinoCI::Host.os == :windows -DESIRED_ARDUINO_IDE_VERSION = "1.8.6".freeze +DESIRED_ARDUINO_CLI_VERSION = "0.13.0".freeze module ArduinoCI @@ -25,74 +21,16 @@ class << self # Autolocation assumed to be an expensive operation # @return [ArduinoCI::ArduinoCmd] an instance of the command or nil if it can't be found def autolocate - ret = nil - case Host.os - when :osx then - ret = autolocate_osx - when :linux then - loc = ArduinoDownloaderLinux.autolocated_executable - return nil if loc.nil? - - ret = ArduinoCmdLinux.new - ret.base_cmd = [loc] - ret.binary_path = Pathname.new(loc) - when :windows then - loc = ArduinoDownloaderWindows.autolocated_executable - return nil if loc.nil? - - ret = ArduinoCmdWindows.new - ret.base_cmd = [loc] - ret.binary_path = Pathname.new(loc) + downloader_class = case Host.os + when :osx then ArduinoDownloaderOSX + when :linux then ArduinoDownloaderLinux + when :windows then ArduinoDownloaderWindows end - ret - end - # @return [ArduinoCI::ArduinoCmdOSX] an instance of the command or nil if it can't be found - def autolocate_osx - osx_root = ArduinoDownloaderOSX.autolocated_installation - return nil if osx_root.nil? - return nil unless File.exist? osx_root + loc = downloader_class.autolocated_executable + return nil if loc.nil? - launchers = [ - # try a hack that skips splash screen - # from https://github.com/arduino/Arduino/issues/1970#issuecomment-321975809 - [ - "java", - "-cp", - "#{osx_root}/Contents/Java/*", - "-DAPP_DIR=#{osx_root}/Contents/Java", - "-Dfile.encoding=UTF-8", - "-Dapple.awt.UIElement=true", - "-Xms128M", - "-Xmx512M", - "processing.app.Base", - ], - # failsafe way - [File.join(osx_root, "Contents", "MacOS", "Arduino")] - ] - - # create return and find a command launcher that works - ret = ArduinoCmdOSX.new - launchers.each do |launcher| - # test whether this method successfully launches the IDE - # note that "successful launch" involves a command that will fail, - # because that's faster than any command which succeeds. what we - # don't want to see is a java error. - args = launcher + ["--bogus-option"] - result = Host.run_and_capture(*args) - - # NOTE: Was originally searching for "Error: unknown option: --bogus-option" - # but also need to find "Erreur: option inconnue : --bogus-option" - # and who knows how many other languages. - # For now, just search for the end of the error and hope that the java-style - # launch of this won't include a similar string in it - next unless result[:err].include? ": --bogus-option" - - ret.base_cmd = launcher - ret.binary_path = Pathname.new(osx_root) - return ret - end - nil + ArduinoCmd.new(loc) end # Attempt to find a workable Arduino executable across platforms, and install it if we don't @@ -109,7 +47,7 @@ def autolocate!(output = $stdout) # Forcibly install Arduino from the web # @return [bool] Whether the command succeeded - def force_install(output = $stdout, version = DESIRED_ARDUINO_IDE_VERSION) + def force_install(output = $stdout, version = DESIRED_ARDUINO_CLI_VERSION) worker_class = case Host.os when :osx then ArduinoDownloaderOSX when :windows then ArduinoDownloaderWindows diff --git a/spec/arduino_cmd_spec.rb b/spec/arduino_cmd_spec.rb index 1b098694..fe15f30c 100644 --- a/spec/arduino_cmd_spec.rb +++ b/spec/arduino_cmd_spec.rb @@ -8,7 +8,6 @@ def get_sketch(dir, file) RSpec.describe ArduinoCI::ArduinoCmd do next if skip_ruby_tests - next if skip_splash_screen_tests arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! @@ -24,8 +23,7 @@ def get_sketch(dir, file) context "initialize" do it "sets base vars" do - expect(arduino_cmd.base_cmd).not_to be nil - expect(arduino_cmd.prefs.class).to be Hash + expect(arduino_cmd.binary_path).not_to be nil end end @@ -46,7 +44,6 @@ def get_sketch(dir, file) context "installation of boards" do it "installs and sets boards" do expect(arduino_cmd.install_boards("arduino:sam")).to be true - expect(arduino_cmd.use_board("arduino:sam:arduino_due_x")).to be true end end @@ -59,16 +56,6 @@ def get_sketch(dir, file) end end - context "set_pref" do - - it "Sets key to what it was before" do - upload_verify = arduino_cmd.get_pref("upload.verify") - result = arduino_cmd.set_pref("upload.verify", upload_verify) - expect(result).to be true - end - end - - context "board_manager" do it "Reads and writes board_manager URLs" do fake_urls = ["http://foo.bar", "http://arduino.ci"] @@ -85,7 +72,7 @@ def get_sketch(dir, file) end - context "verify_sketch" do + context "compile_sketch" do sketch_path_ino = get_sketch("FakeSketch", "FakeSketch.ino") sketch_path_pde = get_sketch("FakeSketch", "FakeSketch.pde") @@ -93,19 +80,19 @@ def get_sketch(dir, file) sketch_path_bad = get_sketch("BadSketch", "BadSketch.ino") it "Rejects a PDE sketch at #{sketch_path_pde}" do - expect(arduino_cmd.verify_sketch(sketch_path_pde)).to be false + expect(arduino_cmd.compile_sketch(sketch_path_pde, "arduino:avr:uno")).to be false end it "Fails a missing sketch at #{sketch_path_mia}" do - expect(arduino_cmd.verify_sketch(sketch_path_mia)).to be false + expect(arduino_cmd.compile_sketch(sketch_path_mia, "arduino:avr:uno")).to be false end it "Fails a bad sketch at #{sketch_path_bad}" do - expect(arduino_cmd.verify_sketch(sketch_path_bad)).to be false + expect(arduino_cmd.compile_sketch(sketch_path_bad, "arduino:avr:uno")).to be false end it "Passes a simple INO sketch at #{sketch_path_ino}" do - expect(arduino_cmd.verify_sketch(sketch_path_ino)).to be true + expect(arduino_cmd.compile_sketch(sketch_path_ino, "arduino:avr:uno")).to be true end end end diff --git a/spec/arduino_downloader_spec.rb b/spec/arduino_downloader_spec.rb index 442b2bfc..70bd0c4a 100644 --- a/spec/arduino_downloader_spec.rb +++ b/spec/arduino_downloader_spec.rb @@ -7,18 +7,15 @@ it "has correct class properties" do ad = ArduinoCI::ArduinoDownloader - expect{ad.autolocated_executable}.to raise_error(NotImplementedError) - expect{ad.autolocated_installation}.to raise_error(NotImplementedError) - expect{ad.existing_installation}.to raise_error(NotImplementedError) expect{ad.existing_executable}.to raise_error(NotImplementedError) - expect{ad.force_installed_executable}.to raise_error(NotImplementedError) - expect(ad.force_install_location).to eq(File.join(ENV['HOME'], 'arduino_ci_ide')) + expect{ad.extracted_file}.to raise_error(NotImplementedError) + expect{ad.extracter}.to raise_error(NotImplementedError) + expect{ad.extract}.to raise_error(NotImplementedError) end it "has correct instance properties" do ad = ArduinoCI::ArduinoDownloader.new(DESIRED_VERSION) expect(ad.prepare).to be nil - expect{ad.package_url}.to raise_error(NotImplementedError) expect{ad.package_file}.to raise_error(NotImplementedError) end end @@ -29,23 +26,20 @@ context "Basics" do it "has correct class properties" do ad = ArduinoCI::ArduinoDownloaderLinux - # these will vary with CI. Don't test them. - # expect(ad.autolocated_executable).to be nil - # expect(ad.autolocated_installation).to be nil - # expect(ad.existing_installation).to be nil + # these can vary with CI. Don't test them. # expect(ad.existing_executable).to be nil + # expect(ad.autolocated_executable).to be nil # expect(ad.force_installed_executable).to be nil - expect(ad.force_install_location).to eq(File.join(ENV['HOME'], 'arduino_ci_ide')) + expect(ad.downloader).to eq("open-uri") + expect(ad.extracter).to eq("tar") end it "has correct instance properties" do ad = ArduinoCI::ArduinoDownloaderLinux.new(DESIRED_VERSION) expect(ad.prepare).to be nil - expect(ad.downloader).to eq("open-uri") - expect(ad.extracter).to eq("tar") - expect(ad.package_url).to eq("https://downloads.arduino.cc/arduino-rhubarb-linux64.tar.xz") - expect(ad.package_file).to eq("arduino-rhubarb-linux64.tar.xz") + expect(ad.package_url).to eq("https://github.com/arduino/arduino-cli/releases/download/rhubarb/arduino-cli_rhubarb_Linux_64bit.tar.gz") + expect(ad.package_file).to eq("arduino-cli_rhubarb_Linux_64bit.tar.gz") end end end @@ -55,23 +49,48 @@ context "Basics" do it "has correct class properties" do ad = ArduinoCI::ArduinoDownloaderOSX - # these will vary with CI. Don't test them. - # expect(ad.autolocated_executable).to be nil - # expect(ad.autolocated_installation).to be nil - # expect(ad.existing_installation).to be nil + # these can vary with CI. Don't test them. # expect(ad.existing_executable).to be nil + # expect(ad.autolocated_executable).to be nil # expect(ad.force_installed_executable).to be nil - expect(ad.force_install_location).to eq(File.join(ENV['HOME'], 'Arduino.app')) + expect(ad.downloader).to eq("open-uri") + expect(ad.extracter).to eq("tar") end it "has correct instance properties" do ad = ArduinoCI::ArduinoDownloaderOSX.new(DESIRED_VERSION) expect(ad.prepare).to be nil - expect(ad.downloader).to eq("open-uri") - expect(ad.extracter).to eq("Zip") - expect(ad.package_url).to eq("https://downloads.arduino.cc/arduino-rhubarb-macosx.zip") - expect(ad.package_file).to eq("arduino-rhubarb-macosx.zip") + expect(ad.package_url).to eq("https://github.com/arduino/arduino-cli/releases/download/rhubarb/arduino-cli_rhubarb_macOS_64bit.tar.gz") + expect(ad.package_file).to eq("arduino-cli_rhubarb_macOS_64bit.tar.gz") + end + end +end + + +if ArduinoCI::Host.os == :windows + RSpec.describe ArduinoCI::ArduinoDownloaderWindows do + next if skip_ruby_tests + context "Basics" do + it "has correct class properties" do + ad = ArduinoCI::ArduinoDownloaderWindows + # these will vary with CI. Don't test them. + # expect(ad.autolocated_executable).to be nil + # expect(ad.existing_executable).to be nil + # expect(ad.force_installed_executable).to be nil + + expect(ad.downloader).to eq("open-uri") + expect(ad.extracter).to eq("tar") + end + + it "has correct instance properties" do + ad = ArduinoCI::ArduinoDownloaderWindows.new(DESIRED_VERSION) + expect(ad.prepare).to be nil + expect(ad.package_url).to eq("https://github.com/arduino/arduino-cli/releases/download/rhubarb/arduino-cli_rhubarb_Windows_64bit.tar.gz") + expect(ad.package_file).to eq("arduino-cli_rhubarb_Windows_64bit.tar.gz") + end end end + + end diff --git a/spec/arduino_installation_spec.rb b/spec/arduino_installation_spec.rb index 551dc47f..459da559 100644 --- a/spec/arduino_installation_spec.rb +++ b/spec/arduino_installation_spec.rb @@ -2,7 +2,6 @@ RSpec.describe ArduinoCI::ArduinoInstallation do next if skip_ruby_tests - next if skip_splash_screen_tests context "autolocate" do it "doesn't fail" do @@ -13,7 +12,7 @@ context "autolocate!" do arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! it "doesn't fail" do - expect(arduino_cmd.base_cmd).not_to be nil + expect(arduino_cmd.binary_path).not_to be nil expect(arduino_cmd.lib_dir).not_to be nil end end @@ -31,4 +30,3 @@ end end - diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index eb6f5631..c698bfef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,10 +13,6 @@ end end -def skip_splash_screen_tests - !ENV["ARDUINO_CI_SKIP_SPLASH_SCREEN_RSPEC_TESTS"].nil? -end - def skip_ruby_tests !ENV["ARDUINO_CI_SKIP_RUBY_RSPEC_TESTS"].nil? end