11require 'fileutils'
22require 'pathname'
3+ require 'json'
34
45# workaround for https://github.com/arduino/Arduino/issues/3535
56WORKAROUND_LIB = "USBHost" . freeze
@@ -24,17 +25,10 @@ def self.flag(name, text = nil)
2425 self . class_eval ( "def flag_#{ name } ;\" #{ text } \" ;end" , __FILE__ , __LINE__ )
2526 end
2627
27- # the array of command components to launch the Arduino executable
28- # @return [Array<String>]
29- attr_accessor :base_cmd
30-
3128 # the actual path to the executable on this platform
3229 # @return [Pathname]
3330 attr_accessor :binary_path
3431
35- # part of a workaround for https://github.com/arduino/Arduino/issues/3535
36- attr_reader :libraries_indexed
37-
3832 # @return [String] STDOUT of the most recently-run command
3933 attr_reader :last_out
4034
@@ -44,97 +38,29 @@ def self.flag(name, text = nil)
4438 # @return [String] the most recently-run command
4539 attr_reader :last_msg
4640
41+ # @return [Array<String>] Additional URLs for the boards manager
42+ attr_reader :additional_urls
43+
4744 # set the command line flags (undefined for now).
4845 # 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
5346 flag :install_boards # flag_install_boards
5447 flag :install_library # flag_install_library
5548 flag :verify # flag_verify
5649
57- def initialize
58- @prefs_cache = { }
59- @prefs_fetched = false
60- @libraries_indexed = false
50+ def initialize ( binary_path )
51+ @binary_path = binary_path
52+ @additional_urls = [ ]
6153 @last_out = ""
6254 @last_err = ""
6355 @last_msg = ""
6456 end
6557
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-
13258 def _wrap_run ( work_fn , *args , **kwargs )
13359 # do some work to extract & merge environment variables if they exist
13460 has_env = !args . empty? && args [ 0 ] . class == Hash
13561 env_vars = has_env ? args [ 0 ] : { }
13662 actual_args = has_env ? args [ 1 ..-1 ] : args # need to shift over if we extracted args
137- full_args = @base_cmd + actual_args
63+ full_args = [ binary_path . to_s , "--format" , "json" ] + actual_args
13864 full_cmd = env_vars . empty? ? full_args : [ env_vars ] + full_args
13965
14066 shell_vars = env_vars . map { |k , v | "#{ k } =#{ v } " } . join ( " " )
@@ -156,19 +82,34 @@ def run_and_capture(*args, **kwargs)
15682 ret
15783 end
15884
85+ def capture_json ( *args , **kwargs )
86+ ret = run_and_capture ( *args , **kwargs )
87+ ret [ :json ] = JSON . parse ( ret [ :out ] )
88+ end
89+
90+ # Get a dump of the entire config
91+ # @return [Hash] The configuration
92+ def config_dump
93+ capture_json ( "config" , "dump" )
94+ end
95+
96+ # @return [String] the path to the Arduino libraries directory
97+ def lib_dir
98+ Pathname . new ( config_dump [ "directories" ] [ "user" ] ) + "libraries"
99+ end
100+
159101 # Board manager URLs
160102 # @return [Array<String>] The additional URLs used by the board manager
161103 def board_manager_urls
162- url_list = get_pref ( "boardsmanager.additional.urls" )
163- return [ ] if url_list . nil?
164-
165- url_list . split ( "," )
104+ config_dump [ "board_manager" ] [ "additional_urls" ] + @additional_urls
166105 end
167106
168107 # Set board manager URLs
169108 # @return [Array<String>] The additional URLs used by the board manager
170109 def board_manager_urls = ( all_urls )
171- set_pref ( "boardsmanager.additional.urls" , all_urls . join ( "," ) )
110+ raise ArgumentError ( "all_urls should be an array, got #{ all_urls . class } " ) unless all_urls . is_a? Array
111+
112+ @additional_urls = all_urls
172113 end
173114
174115 # check whether a board is installed
@@ -177,48 +118,33 @@ def board_manager_urls=(all_urls)
177118 # @param boardname [String] The board to test
178119 # @return [bool] Whether the board is installed
179120 def board_installed? ( boardname )
180- run_and_capture ( flag_use_board , boardname ) [ :success ]
121+ # capture_json("core", "list")[:json].find { |b| b["ID"] == boardname } # nope, this is for the family
122+ run_and_capture ( "board" , "details" , "--fqbn" , boardname ) [ :success ]
181123 end
182124
183125 # install a board by name
184126 # @param name [String] the board name
185127 # @return [bool] whether the command succeeded
186128 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
129+ result = run_and_capture ( "core" , "install" , boardfamily )
130+ result [ :success ]
191131 end
192132
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
133+ # @return [Hash] information about installed libraries via the CLI
134+ def installed_libraries
135+ capture_json ( "lib" , "list" ) [ :json ]
214136 end
215137
216138 # install a library by name
217139 # @param name [String] the library name
140+ # @param version [String] the version to install
218141 # @return [bool] whether the command succeeded
219- def install_library ( library_name )
220- index_libraries
221- _install_library ( library_name )
142+ def install_library ( library_name , version = nil )
143+ return true if library_present? ( library_name )
144+
145+ fqln = version . nil? ? library_name : "#{ library_name } @#{ version } "
146+ result = run_and_capture ( "lib" , "install" , fqln )
147+ result [ :success ]
222148 end
223149
224150 # generate the (very likely) path of a library given its name
@@ -239,47 +165,20 @@ def library_present?(library_name)
239165 library_path ( library_name ) . exist?
240166 end
241167
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
168+ # @param path [String] The sketch to compile
251169 # @param boardname [String] The board to use
252170 # @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 )
171+ def compile_sketch ( path , boardname )
273172 ext = File . extname path
274173 unless ext . casecmp ( ".ino" ) . zero?
275- @last_msg = "Refusing to verify sketch with '#{ ext } ' extension -- rename it to '.ino'!"
174+ @last_msg = "Refusing to compile sketch with '#{ ext } ' extension -- rename it to '.ino'!"
276175 return false
277176 end
278177 unless File . exist? path
279- @last_msg = "Can't verify Sketch at nonexistent path '#{ path } '!"
178+ @last_msg = "Can't compile Sketch at nonexistent path '#{ path } '!"
280179 return false
281180 end
282- ret = run_and_capture ( flag_verify , path )
181+ ret = run_and_capture ( "compile" , "--fqbn" , boardname , "--warnings" , "all" , "--dry-run" , path )
283182 ret [ :success ]
284183 end
285184
0 commit comments