arduino_ci 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,10 @@
1
1
  require 'pathname'
2
2
  require "arduino_ci/host"
3
- require "arduino_ci/arduino_cmd_osx"
4
- require "arduino_ci/arduino_cmd_linux"
5
- require "arduino_ci/arduino_cmd_windows"
6
- require "arduino_ci/arduino_cmd_linux_builder"
3
+ require "arduino_ci/arduino_backend"
7
4
  require "arduino_ci/arduino_downloader_osx"
8
5
  require "arduino_ci/arduino_downloader_linux"
9
-
10
6
  require "arduino_ci/arduino_downloader_windows" if ArduinoCI::Host.os == :windows
11
7
 
12
- DESIRED_ARDUINO_IDE_VERSION = "1.8.6".freeze
13
-
14
8
  module ArduinoCI
15
9
 
16
10
  class ArduinoInstallationError < StandardError; end
@@ -18,85 +12,29 @@ module ArduinoCI
18
12
  # Manage the OS-specific install location of Arduino
19
13
  class ArduinoInstallation
20
14
 
15
+ DESIRED_ARDUINO_CLI_VERSION = "0.13.0".freeze
16
+
21
17
  class << self
22
18
 
23
19
  # attempt to find a workable Arduino executable across platforms
24
20
  #
25
21
  # Autolocation assumed to be an expensive operation
26
- # @return [ArduinoCI::ArduinoCmd] an instance of the command or nil if it can't be found
22
+ # @return [ArduinoCI::ArduinoBackend] an instance of the command or nil if it can't be found
27
23
  def autolocate
28
- ret = nil
29
- case Host.os
30
- when :osx then
31
- ret = autolocate_osx
32
- when :linux then
33
- loc = ArduinoDownloaderLinux.autolocated_executable
34
- return nil if loc.nil?
35
-
36
- ret = ArduinoCmdLinux.new
37
- ret.base_cmd = [loc]
38
- ret.binary_path = Pathname.new(loc)
39
- when :windows then
40
- loc = ArduinoDownloaderWindows.autolocated_executable
41
- return nil if loc.nil?
42
-
43
- ret = ArduinoCmdWindows.new
44
- ret.base_cmd = [loc]
45
- ret.binary_path = Pathname.new(loc)
24
+ downloader_class = case Host.os
25
+ when :osx then ArduinoDownloaderOSX
26
+ when :linux then ArduinoDownloaderLinux
27
+ when :windows then ArduinoDownloaderWindows
46
28
  end
47
- ret
48
- end
49
-
50
- # @return [ArduinoCI::ArduinoCmdOSX] an instance of the command or nil if it can't be found
51
- def autolocate_osx
52
- osx_root = ArduinoDownloaderOSX.autolocated_installation
53
- return nil if osx_root.nil?
54
- return nil unless File.exist? osx_root
55
-
56
- launchers = [
57
- # try a hack that skips splash screen
58
- # from https://github.com/arduino/Arduino/issues/1970#issuecomment-321975809
59
- [
60
- "java",
61
- "-cp",
62
- "#{osx_root}/Contents/Java/*",
63
- "-DAPP_DIR=#{osx_root}/Contents/Java",
64
- "-Dfile.encoding=UTF-8",
65
- "-Dapple.awt.UIElement=true",
66
- "-Xms128M",
67
- "-Xmx512M",
68
- "processing.app.Base",
69
- ],
70
- # failsafe way
71
- [File.join(osx_root, "Contents", "MacOS", "Arduino")]
72
- ]
73
29
 
74
- # create return and find a command launcher that works
75
- ret = ArduinoCmdOSX.new
76
- launchers.each do |launcher|
77
- # test whether this method successfully launches the IDE
78
- # note that "successful launch" involves a command that will fail,
79
- # because that's faster than any command which succeeds. what we
80
- # don't want to see is a java error.
81
- args = launcher + ["--bogus-option"]
82
- result = Host.run_and_capture(*args)
30
+ loc = downloader_class.autolocated_executable
31
+ return nil if loc.nil?
83
32
 
84
- # NOTE: Was originally searching for "Error: unknown option: --bogus-option"
85
- # but also need to find "Erreur: option inconnue : --bogus-option"
86
- # and who knows how many other languages.
87
- # For now, just search for the end of the error and hope that the java-style
88
- # launch of this won't include a similar string in it
89
- next unless result[:err].include? ": --bogus-option"
90
-
91
- ret.base_cmd = launcher
92
- ret.binary_path = Pathname.new(osx_root)
93
- return ret
94
- end
95
- nil
33
+ ArduinoBackend.new(loc)
96
34
  end
97
35
 
98
36
  # Attempt to find a workable Arduino executable across platforms, and install it if we don't
99
- # @return [ArduinoCI::ArduinoCmd] an instance of a command
37
+ # @return [ArduinoCI::ArduinoBackend] an instance of a command
100
38
  def autolocate!(output = $stdout)
101
39
  candidate = autolocate
102
40
  return candidate unless candidate.nil?
@@ -109,7 +47,7 @@ module ArduinoCI
109
47
 
110
48
  # Forcibly install Arduino from the web
111
49
  # @return [bool] Whether the command succeeded
112
- def force_install(output = $stdout, version = DESIRED_ARDUINO_IDE_VERSION)
50
+ def force_install(output = $stdout, version = DESIRED_ARDUINO_CLI_VERSION)
113
51
  worker_class = case Host.os
114
52
  when :osx then ArduinoDownloaderOSX
115
53
  when :windows then ArduinoDownloaderWindows
@@ -232,13 +232,6 @@ module ArduinoCI
232
232
  deep_clone(defn)
233
233
  end
234
234
 
235
- # Whether a package is built-in to arduino
236
- # @param package [String] the package name (e.g. "arduino:avr")
237
- # @return [bool]
238
- def package_builtin?(package)
239
- package.start_with?("arduino:")
240
- end
241
-
242
235
  # the URL that gives the download info for a given package (a JSON file).
243
236
  # this is NOT where the package comes from.
244
237
  # @param package [String] the package name (e.g. "arduino:avr")
@@ -14,15 +14,21 @@ module ArduinoCI
14
14
  # Information about an Arduino CPP library, specifically for compilation purposes
15
15
  class CppLibrary
16
16
 
17
- # @return [Pathname] The path to the library being tested
18
- attr_reader :base_dir
17
+ # @return [String] The official library properties file name
18
+ LIBRARY_PROPERTIES_FILE = "library.properties".freeze
19
19
 
20
- # @return [Pathname] The path to the Arduino 3rd-party library directory
21
- attr_reader :arduino_lib_dir
20
+ # @return [String] The "official" name of the library, which can include spaces (in a way that the lib dir won't)
21
+ attr_reader :name
22
+
23
+ # @return [ArduinoBackend] The backend support for this library
24
+ attr_reader :backend
22
25
 
23
26
  # @return [Array<Pathname>] The set of artifacts created by this class (note: incomplete!)
24
27
  attr_reader :artifacts
25
28
 
29
+ # @return [Array<Pathname>] The set of directories that should be excluded from compilation
30
+ attr_reader :exclude_dirs
31
+
26
32
  # @return [String] STDERR from the last command
27
33
  attr_reader :last_err
28
34
 
@@ -35,30 +41,100 @@ module ArduinoCI
35
41
  # @return [Array<Pathname>] Directories suspected of being vendor-bundle
36
42
  attr_reader :vendor_bundle_cache
37
43
 
38
- # @param base_dir [Pathname] The path to the library being tested
39
- # @param arduino_lib_dir [Pathname] The path to the libraries directory
40
- # @param exclude_dirs [Array<Pathname>] Directories that should be excluded from compilation
41
- def initialize(base_dir, arduino_lib_dir, exclude_dirs)
42
- raise ArgumentError, 'base_dir is not a Pathname' unless base_dir.is_a? Pathname
43
- raise ArgumentError, 'arduino_lib_dir is not a Pathname' unless arduino_lib_dir.is_a? Pathname
44
- raise ArgumentError, 'exclude_dir is not an array of Pathnames' unless exclude_dirs.is_a?(Array)
45
- raise ArgumentError, 'exclude_dir array contains non-Pathname elements' unless exclude_dirs.all? { |p| p.is_a? Pathname }
46
-
47
- @base_dir = base_dir
48
- @exclude_dirs = exclude_dirs
49
- @arduino_lib_dir = arduino_lib_dir.expand_path
44
+ # @param friendly_name [String] The "official" name of the library, which can contain spaces
45
+ # @param backend [ArduinoBackend] The support backend
46
+ def initialize(friendly_name, backend)
47
+ raise ArgumentError, "friendly_name is not a String (got #{friendly_name.class})" unless friendly_name.is_a? String
48
+ raise ArgumentError, 'backend is not a ArduinoBackend' unless backend.is_a? ArduinoBackend
49
+
50
+ @name = friendly_name
51
+ @backend = backend
52
+ @info_cache = nil
50
53
  @artifacts = []
51
54
  @last_err = ""
52
55
  @last_out = ""
53
56
  @last_msg = ""
54
57
  @has_libasan_cache = {}
55
58
  @vendor_bundle_cache = nil
59
+ @exclude_dirs = []
60
+ end
61
+
62
+ # Generate a guess as to the on-disk (coerced character) name of this library
63
+ #
64
+ # @TODO: delegate this to the backend in some way? It uses "official" names for install, but dir names in lists :(
65
+ # @param friendly_name [String] The library name as it might appear in library manager
66
+ # @return [String] How the path will be stored on disk -- spaces are coerced to underscores
67
+ def self.library_directory_name(friendly_name)
68
+ friendly_name.tr(" ", "_")
69
+ end
70
+
71
+ # Generate a guess as to the on-disk (coerced character) name of this library
72
+ #
73
+ # @TODO: delegate this to the backend in some way? It uses "official" names for install, but dir names in lists :(
74
+ # @return [String] How the path will be stored on disk -- spaces are coerced to underscores
75
+ def name_on_disk
76
+ self.class.library_directory_name(@name)
77
+ end
78
+
79
+ # Get the path to this library, whether or not it exists
80
+ # @return [Pathname] The fully qualified library path
81
+ def path
82
+ @backend.lib_dir + name_on_disk
83
+ end
84
+
85
+ # Determine whether a library is present in the lib dir
86
+ #
87
+ # Note that `true` doesn't guarantee that the library is valid/installed
88
+ # and `false` doesn't guarantee that the library isn't built-in
89
+ #
90
+ # @return [bool]
91
+ def installed?
92
+ path.exist?
93
+ end
94
+
95
+ # install a library by name
96
+ # @param version [String] the version to install
97
+ # @param recursive [bool] whether to also install its dependencies
98
+ # @return [bool] whether the command succeeded
99
+ def install(version = nil, recursive = false)
100
+ return true if installed? && !recursive
101
+
102
+ fqln = version.nil? ? @name : "#{@name}@#{version}"
103
+ result = if recursive
104
+ @backend.run_and_capture("lib", "install", fqln)
105
+ else
106
+ @backend.run_and_capture("lib", "install", "--no-deps", fqln)
107
+ end
108
+ result[:success]
109
+ end
110
+
111
+ # information about the library as reported by the backend
112
+ # @return [Hash] the metadata object
113
+ def info
114
+ return nil unless installed?
115
+
116
+ # note that if the library isn't found, we're going to do a lot of cache attempts...
117
+ if @info_cache.nil?
118
+ @info_cache = @backend.installed_libraries.find do |l|
119
+ lib_info = l["library"]
120
+ Pathname.new(lib_info["install_dir"]).realpath == path.realpath
121
+ end
122
+ end
123
+
124
+ @info_cache
125
+ end
126
+
127
+ # @param installed_library_path [String] The library to query
128
+ # @return [Array<String>] Example sketch files
129
+ def example_sketches
130
+ reported_dirs = info["library"]["examples"].map(&Pathname::method(:new))
131
+ reported_dirs.map { |e| e + e.basename.sub_ext(".ino") }.select(&:exist?).sort_by(&:to_s)
56
132
  end
57
133
 
58
134
  # The expected path to the library.properties file (i.e. even if it does not exist)
59
135
  # @return [Pathname]
60
136
  def library_properties_path
61
- @base_dir + "library.properties"
137
+ path + LIBRARY_PROPERTIES_FILE
62
138
  end
63
139
 
64
140
  # Whether library.properties definitions for this library exist
@@ -68,16 +144,29 @@ module ArduinoCI
68
144
  lib_props.exist? && lib_props.file?
69
145
  end
70
146
 
147
+ # Library properties
148
+ # @return [LibraryProperties] The library.properties metadata wrapper for this library
149
+ def library_properties
150
+ return nil unless library_properties?
151
+
152
+ LibraryProperties.new(library_properties_path)
153
+ end
154
+
155
+ # Set directories that should be excluded from compilation
156
+ # @param rval [Array] Array of strings or pathnames that will be coerced to pathnames
157
+ def exclude_dirs=(rval)
158
+ @exclude_dirs = rval.map { |d| d.is_a?(Pathname) ? d : Pathname.new(d) }
159
+ end
160
+
71
161
  # Decide whether this is a 1.5-compatible library
72
162
  #
73
- # according to https://arduino.github.io/arduino-cli/latest/library-specification
74
- #
75
- # Should match logic from https://github.com/arduino/arduino-cli/blob/master/arduino/libraries/loader.go
163
+ # This should be according to https://arduino.github.io/arduino-cli/latest/library-specification
164
+ # but we rely on the cli to decide for us
76
165
  # @return [bool]
77
166
  def one_point_five?
78
167
  return false unless library_properties?
79
168
 
80
- src_dir = (@base_dir + "src")
169
+ src_dir = path + "src"
81
170
  src_dir.exist? && src_dir.directory?
82
171
  end
83
172
 
@@ -88,9 +177,9 @@ module ArduinoCI
88
177
  # That gets us the vendor directory (or multiple directories). We can check
89
178
  # if the given path is contained by any of those.
90
179
  #
91
- # @param path [Pathname] The path to check
180
+ # @param some_path [Pathname] The path to check
92
181
  # @return [bool]
93
- def vendor_bundle?(path)
182
+ def vendor_bundle?(some_path)
94
183
  # Cache bundle information, as it is (1) time consuming to fetch and (2) not going to change while we run
95
184
  if @vendor_bundle_cache.nil?
96
185
  bundle_info = Host.run_and_capture("bundle show --paths")
@@ -125,7 +214,7 @@ module ArduinoCI
125
214
 
126
215
  # With vendor bundles located, check this file against those
127
216
  @vendor_bundle_cache.any? do |gem_path|
128
- path.ascend do |part|
217
+ some_path.ascend do |part|
129
218
  break true if gem_path == part
130
219
  end
131
220
  end
@@ -135,13 +224,13 @@ module ArduinoCI
135
224
  #
136
225
  # @param path [Pathname] The path to check
137
226
  # @return [bool]
138
- def in_tests_dir?(path)
227
+ def in_tests_dir?(sourcefile_path)
139
228
  return false unless tests_dir.exist?
140
229
 
141
230
  tests_dir_aliases = [tests_dir, tests_dir.realpath]
142
231
  # we could do this but some rubies don't return an enumerator for ascend
143
232
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
144
- path.ascend do |part|
233
+ sourcefile_path.ascend do |part|
145
234
  return true if tests_dir_aliases.include?(part)
146
235
  end
147
236
  false
@@ -151,11 +240,11 @@ module ArduinoCI
151
240
  #
152
241
  # @param path [Pathname] The path to check
153
242
  # @return [bool]
154
- def in_exclude_dir?(path)
243
+ def in_exclude_dir?(sourcefile_path)
155
244
  # we could do this but some rubies don't return an enumerator for ascend
156
245
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
157
- path.ascend do |part|
158
- return true if exclude_dir.any? { |p| p.realpath == part }
246
+ sourcefile_path.ascend do |part|
247
+ return true if exclude_dir.any? { |p| p.realpath == part.realpath }
159
248
  end
160
249
  false
161
250
  end
@@ -166,42 +255,26 @@ module ArduinoCI
166
255
  # @param gcc_binary [String]
167
256
  def libasan?(gcc_binary)
168
257
  unless @has_libasan_cache.key?(gcc_binary)
169
- file = Tempfile.new(["arduino_ci_libasan_check", ".cpp"])
170
- begin
258
+ Tempfile.create(["arduino_ci_libasan_check", ".cpp"]) do |file|
171
259
  file.write "int main(){}"
172
260
  file.close
173
261
  @has_libasan_cache[gcc_binary] = run_gcc(gcc_binary, "-o", "/dev/null", "-fsanitize=address", file.path)
174
- ensure
175
- file.delete
176
262
  end
177
263
  end
178
264
  @has_libasan_cache[gcc_binary]
179
265
  end
180
266
 
181
- # Library properties
182
- def library_properties
183
- return nil unless library_properties?
184
-
185
- LibraryProperties.new(library_properties_path)
186
- end
187
-
188
- # Get a list of all dependencies as defined in library.properties
189
- # @return [Array<String>] The library names of the dependencies (not the paths)
190
- def arduino_library_dependencies
191
- return nil unless library_properties?
192
-
193
- library_properties.depends
194
- end
195
-
196
267
  # Get a list of all CPP source files in a directory and its subdirectories
197
268
  # @param some_dir [Pathname] The directory in which to begin the search
198
269
  # @param extensions [Array<Sring>] The set of allowable file extensions
199
270
  # @return [Array<Pathname>] The paths of the found files
200
271
  def code_files_in(some_dir, extensions)
201
272
  raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
202
- return [] unless some_dir.exist? && some_dir.directory?
203
273
 
204
- files = some_dir.realpath.children.reject(&:directory?)
274
+ full_dir = path + some_dir
275
+ return [] unless full_dir.exist? && full_dir.directory?
276
+
277
+ files = full_dir.children.reject(&:directory?)
205
278
  cpp = files.select { |path| extensions.include?(path.extname.downcase) }
206
279
  not_hidden = cpp.reject { |path| path.basename.to_s.start_with?(".") }
207
280
  not_hidden.sort_by(&:to_s)
@@ -215,17 +288,18 @@ module ArduinoCI
215
288
  raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
216
289
  return [] unless some_dir.exist? && some_dir.directory?
217
290
 
218
- real = some_dir.realpath
219
- Find.find(real).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten
291
+ Find.find(some_dir).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten
220
292
  end
221
293
 
222
- # Header files that are part of the project library under test
294
+ # Source files that are part of the library under test
295
+ # @param extensions [Array<String>] the allowed extensions (or, the ones we're looking for)
223
296
  # @return [Array<Pathname>]
224
- def header_files
297
+ def source_files(extensions)
298
+ source_dir = Pathname.new(info["library"]["source_dir"])
225
299
  ret = if one_point_five?
226
- code_files_in_recursive(@base_dir + "src", HPP_EXTENSIONS)
300
+ code_files_in_recursive(source_dir, extensions)
227
301
  else
228
- [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, HPP_EXTENSIONS) }.flatten
302
+ [source_dir, source_dir + "utility"].map { |d| code_files_in(d, extensions) }.flatten
229
303
  end
230
304
 
231
305
  # note to future troubleshooter: some of these tests may not be relevant, but at the moment at
@@ -233,18 +307,16 @@ module ArduinoCI
233
307
  ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) }
234
308
  end
235
309
 
310
+ # Header files that are part of the project library under test
311
+ # @return [Array<Pathname>]
312
+ def header_files
313
+ source_files(HPP_EXTENSIONS)
314
+ end
315
+
236
316
  # CPP files that are part of the project library under test
237
317
  # @return [Array<Pathname>]
238
318
  def cpp_files
239
- ret = if one_point_five?
240
- code_files_in_recursive(@base_dir + "src", CPP_EXTENSIONS)
241
- else
242
- [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten
243
- end
244
-
245
- # note to future troubleshooter: some of these tests may not be relevant, but at the moment at
246
- # least some of them are tied to existing features
247
- ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) }
319
+ source_files(CPP_EXTENSIONS)
248
320
  end
249
321
 
250
322
  # CPP files that are part of the arduino mock library we're providing
@@ -269,13 +341,13 @@ module ArduinoCI
269
341
  # Returns the Pathnames for all paths to exclude from testing and compilation
270
342
  # @return [Array<Pathname>]
271
343
  def exclude_dir
272
- @exclude_dirs.map { |p| Pathname.new(@base_dir) + p }.select(&:exist?)
344
+ @exclude_dirs.map { |p| Pathname.new(path) + p }.select(&:exist?)
273
345
  end
274
346
 
275
347
  # The directory where we expect to find unit test defintions provided by the user
276
348
  # @return [Pathname]
277
349
  def tests_dir
278
- Pathname.new(@base_dir) + "test"
350
+ Pathname.new(path) + "test"
279
351
  end
280
352
 
281
353
  # The files provided by the user that contain unit tests
@@ -311,18 +383,33 @@ module ArduinoCI
311
383
  @last_err
312
384
  end
313
385
 
314
- # Arduino library directories containing sources -- only those of the dependencies
315
- # @return [Array<Pathname>]
316
- def arduino_library_src_dirs(aux_libraries)
386
+ # Get a list of all dependencies as defined in library.properties
387
+ # @return [Array<String>] The library names of the dependencies (not the paths)
388
+ def arduino_library_dependencies
389
+ return [] unless library_properties?
390
+ return [] if library_properties.depends.nil?
391
+
392
+ library_properties.depends
393
+ end
394
+
395
+ # Arduino library dependencies all the way down, installing if they are not present
396
+ # @return [Array<String>] The library names of the dependencies (not the paths)
397
+ def all_arduino_library_dependencies!(additional_libraries = [])
317
398
  # Pull in all possible places that headers could live, according to the spec:
318
399
  # https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification
400
+ recursive = (additional_libraries + arduino_library_dependencies).map do |n|
401
+ other_lib = self.class.new(n, @backend)
402
+ other_lib.install unless other_lib.installed?
403
+ other_lib.all_arduino_library_dependencies!
404
+ end.flatten
405
+ ret = (additional_libraries + recursive).uniq
406
+ ret
407
+ end
319
408
 
320
- aux_libraries.map do |d|
321
- # library manager coerces spaces in package names to underscores
322
- # see https://github.com/ianfixes/arduino_ci/issues/132#issuecomment-518857059
323
- legal_dir = d.tr(" ", "_")
324
- self.class.new(@arduino_lib_dir + legal_dir, @arduino_lib_dir, @exclude_dirs).header_dirs
325
- end.flatten.uniq
409
+ # Arduino library directories containing sources -- only those of the dependencies
410
+ # @return [Array<Pathname>]
411
+ def arduino_library_src_dirs(aux_libraries)
412
+ all_arduino_library_dependencies!(aux_libraries).map { |l| self.class.new(l, @backend).header_dirs }.flatten.uniq
326
413
  end
327
414
 
328
415
  # GCC command line arguments for including aux libraries
@@ -415,9 +502,9 @@ module ArduinoCI
415
502
 
416
503
  # combine library.properties defs (if existing) with config file.
417
504
  # TODO: as much as I'd like to rely only on the properties file(s), I think that would prevent testing 1.0-spec libs
418
- full_aux_libraries = arduino_library_dependencies.nil? ? aux_libraries : aux_libraries + arduino_library_dependencies
419
- arg_sets << test_args(full_aux_libraries, ci_gcc_config)
420
- arg_sets << cpp_files_libraries(full_aux_libraries).map(&:to_s)
505
+ full_dependencies = all_arduino_library_dependencies!(aux_libraries)
506
+ arg_sets << test_args(full_dependencies, ci_gcc_config)
507
+ arg_sets << cpp_files_libraries(full_dependencies).map(&:to_s)
421
508
  arg_sets << [test_file.to_s]
422
509
  args = arg_sets.flatten(1)
423
510
  return nil unless run_gcc(gcc_binary, *args)