arduino_ci 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -9
- data/exe/arduino_ci_remote.rb +132 -0
- data/lib/arduino_ci.rb +3 -117
- data/lib/arduino_ci/arduino_cmd.rb +296 -0
- data/lib/arduino_ci/arduino_cmd_linux.rb +86 -0
- data/lib/arduino_ci/arduino_cmd_linux_builder.rb +32 -0
- data/lib/arduino_ci/arduino_cmd_osx.rb +27 -0
- data/lib/arduino_ci/arduino_installation.rb +135 -0
- data/lib/arduino_ci/ci_config.rb +218 -0
- data/lib/arduino_ci/cpp_library.rb +186 -0
- data/lib/arduino_ci/display_manager.rb +187 -0
- data/lib/arduino_ci/host.rb +36 -0
- data/lib/arduino_ci/version.rb +1 -1
- metadata +32 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cad9df4f8ade3fbe801da837c8b6a73d129c9a5497e1d43b4f655de8d9b5b640
|
4
|
+
data.tar.gz: b42f9624e6af593c467d0f67a94b0695fb36b584d740f4fc38e256b8f703c075
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25d40817ac30d531979d197768aad671e8efa9b3e2bc023fde41f3dbcbbb1b829a4f6a9915714a6eec2e1e234f2f70749390802431af81a7b23cf8b847a39750
|
7
|
+
data.tar.gz: 9ef54c3d3fc6704ad70d347b85f2fc039d697a81edfafaa3e9fea329cc20c0a0919f42267452e4f62be5d508460f38a3264f04b0c51ea98a3b98062cc4bd18b1
|
data/README.md
CHANGED
@@ -1,32 +1,44 @@
|
|
1
1
|
[![Gem Version](https://badge.fury.io/rb/arduino_ci.svg)](https://rubygems.org/gems/arduino_ci)
|
2
2
|
[![Build Status](https://travis-ci.org/ifreecarve/arduino_ci.svg)](https://travis-ci.org/ifreecarve/arduino_ci)
|
3
|
-
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/arduino_ci/)
|
3
|
+
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/arduino_ci/0.1.0)
|
4
4
|
|
5
5
|
# ArduinoCI Ruby gem (`arduino_ci`)
|
6
6
|
|
7
7
|
[Arduino CI](https://github.com/ifreecarve/arduino_ci) is a Ruby gem for executing Continuous Integration (CI) tests on an Arduino library -- both locally and as part of a service like Travis CI.
|
8
8
|
|
9
9
|
|
10
|
-
## Installation
|
10
|
+
## Installation In Your GitHub Project And Using Travis CI
|
11
11
|
|
12
|
-
Add
|
12
|
+
Add a file called `Gemfile` (no extension) to your Arduino project:
|
13
13
|
|
14
14
|
```ruby
|
15
|
+
source 'https://rubygems.org'
|
15
16
|
gem 'arduino_ci'
|
16
17
|
```
|
17
18
|
|
18
|
-
|
19
|
+
Next, you need this in `.travis.yml`
|
19
20
|
|
20
|
-
|
21
|
+
```yaml
|
22
|
+
sudo: false
|
23
|
+
language: ruby
|
24
|
+
script:
|
25
|
+
- bundle install
|
26
|
+
- bundle exec arduino_ci_remote.rb
|
27
|
+
```
|
28
|
+
|
29
|
+
That's literally all there is to it on the repository side. You'll need to go to https://travis-ci.org/profile/ and enable testing for your Arduino project. Once that happens, you should be all set.
|
21
30
|
|
22
|
-
Or install it yourself as:
|
23
31
|
|
24
|
-
|
32
|
+
## More Documentation
|
25
33
|
|
34
|
+
This software is in alpha. But [SampleProjects/DoSomething](SampleProjects/DoSomething) has a decent writeup and is a good bare-bones example of all the features.
|
26
35
|
|
27
|
-
##
|
36
|
+
## Known Problems
|
28
37
|
|
29
|
-
|
38
|
+
* The Arduino library is not fully mocked.
|
39
|
+
* I don't have preprocessor defines for all the Arduino board flavors
|
40
|
+
* Arduino Zero boards don't work in CI. I'm confused.
|
41
|
+
* https://github.com/ifreecarve/arduino_ci/issues
|
30
42
|
|
31
43
|
|
32
44
|
## Author
|
@@ -37,3 +49,5 @@ This gem was written by Ian Katz (ifreecarve@gmail.com) in 2018. It's released
|
|
37
49
|
## See Also
|
38
50
|
|
39
51
|
* [Contributing](CONTRIBUTING.md)
|
52
|
+
* [Adafruit/travis-ci-arduino](https://github.com/adafruit/travis-ci-arduino) which inspired this project
|
53
|
+
* [mmurdoch/arduinounit](https://github.com/mmurdoch/arduinounit) from which the unit test macros were adopted
|
@@ -0,0 +1,132 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'arduino_ci'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
WIDTH = 80
|
6
|
+
|
7
|
+
@failure_count = 0
|
8
|
+
|
9
|
+
# terminate after printing any debug info. TODO: capture debug info
|
10
|
+
def terminate(final = nil)
|
11
|
+
puts "Failures: #{@failure_count}"
|
12
|
+
unless @failure_count.zero? || final
|
13
|
+
puts "Last message: #{@arduino_cmd.last_msg}"
|
14
|
+
puts "========== Stdout:"
|
15
|
+
puts @arduino_cmd.last_out
|
16
|
+
puts "========== Stderr:"
|
17
|
+
puts @arduino_cmd.last_err
|
18
|
+
end
|
19
|
+
retcode = @failure_count.zero? ? 0 : 1
|
20
|
+
exit(retcode)
|
21
|
+
end
|
22
|
+
|
23
|
+
# make a nice status line for an action and react to the action
|
24
|
+
def perform_action(message, on_fail_msg, abort_on_fail)
|
25
|
+
line = "#{message}..."
|
26
|
+
print line
|
27
|
+
result = yield
|
28
|
+
mark = result ? "✓" : "X"
|
29
|
+
puts mark.rjust(WIDTH - line.length, " ")
|
30
|
+
unless result
|
31
|
+
puts on_fail_msg unless on_fail_msg.nil?
|
32
|
+
@failure_count += 1
|
33
|
+
# print out error messaging here if we've captured it
|
34
|
+
terminate if abort_on_fail
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
# Make a nice status for something that defers any failure code until script exit
|
40
|
+
def attempt(message, &block)
|
41
|
+
perform_action(message, nil, false, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Make a nice status for something that kills the script immediately on failure
|
45
|
+
def assure(message, &block)
|
46
|
+
perform_action(message, "This may indicate a problem with ArduinoCI!", true, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# initialize command and config
|
50
|
+
config = ArduinoCI::CIConfig.default.from_project_library
|
51
|
+
@arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate!
|
52
|
+
|
53
|
+
# initialize library under test
|
54
|
+
installed_library_path = assure("Installing library under test") { @arduino_cmd.install_local_library(".") }
|
55
|
+
library_examples = @arduino_cmd.library_examples(installed_library_path)
|
56
|
+
cpp_library = ArduinoCI::CppLibrary.new(installed_library_path)
|
57
|
+
attempt("Library installed at #{installed_library_path}") { true }
|
58
|
+
|
59
|
+
# gather up all required boards so we can install them up front.
|
60
|
+
# start with the "platforms to unittest" and add the examples
|
61
|
+
# while we're doing that, get the aux libraries as well
|
62
|
+
all_platforms = {}
|
63
|
+
aux_libraries = Set.new(config.aux_libraries_for_unittest + config.aux_libraries_for_build)
|
64
|
+
config.platforms_to_unittest.each { |p| all_platforms[p] = config.platform_definition(p) }
|
65
|
+
library_examples.each do |path|
|
66
|
+
ovr_config = config.from_example(path)
|
67
|
+
ovr_config.platforms_to_build.each { |p| all_platforms[p] = config.platform_definition(p) }
|
68
|
+
aux_libraries.merge(ovr_config.aux_libraries_for_build)
|
69
|
+
end
|
70
|
+
|
71
|
+
# with all platform info, we can extract unique packages and their urls
|
72
|
+
# do that, set the URLs, and download the packages
|
73
|
+
all_packages = all_platforms.values.map { |v| v[:package] }.uniq.reject(&:nil?)
|
74
|
+
all_urls = all_packages.map { |p| config.package_url(p) }.uniq.reject(&:nil?)
|
75
|
+
assure("Setting board manager URLs") do
|
76
|
+
@arduino_cmd.set_pref("boardsmanager.additional.urls", all_urls.join(","))
|
77
|
+
end
|
78
|
+
|
79
|
+
all_packages.each do |p|
|
80
|
+
assure("Installing board package #{p}") do
|
81
|
+
@arduino_cmd.install_boards(p)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
aux_libraries.each do |l|
|
86
|
+
assure("Installing aux library '#{l}'") { @arduino_cmd.install_library(l) }
|
87
|
+
end
|
88
|
+
|
89
|
+
attempt("Setting compiler warning level") { @arduino_cmd.set_pref("compiler.warning_level", "all") }
|
90
|
+
|
91
|
+
library_examples.each do |example_path|
|
92
|
+
ovr_config = config.from_example(example_path)
|
93
|
+
ovr_config.platforms_to_build.each do |p|
|
94
|
+
board = all_platforms[p][:board]
|
95
|
+
assure("Switching to board for #{p} (#{board})") { @arduino_cmd.use_board(board) }
|
96
|
+
example_name = File.basename(example_path)
|
97
|
+
attempt("Verifying #{example_name}") do
|
98
|
+
ret = @arduino_cmd.verify_sketch(example_path)
|
99
|
+
unless ret
|
100
|
+
puts
|
101
|
+
puts "Last command: #{@arduino_cmd.last_msg}"
|
102
|
+
puts @arduino_cmd.last_err
|
103
|
+
end
|
104
|
+
ret
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
config.platforms_to_unittest.each do |p|
|
110
|
+
board = all_platforms[p][:board]
|
111
|
+
assure("Switching to board for #{p} (#{board})") { @arduino_cmd.use_board(board) }
|
112
|
+
cpp_library.test_files.each do |unittest_path|
|
113
|
+
unittest_name = File.basename(unittest_path)
|
114
|
+
attempt("Unit testing #{unittest_name}") do
|
115
|
+
exe = cpp_library.build_for_test_with_configuration(
|
116
|
+
unittest_path,
|
117
|
+
config.aux_libraries_for_unittest,
|
118
|
+
config.gcc_config(p)
|
119
|
+
)
|
120
|
+
puts
|
121
|
+
unless exe
|
122
|
+
puts "Last command: #{cpp_library.last_cmd}"
|
123
|
+
puts cpp_library.last_out
|
124
|
+
puts cpp_library.last_err
|
125
|
+
next false
|
126
|
+
end
|
127
|
+
cpp_library.run_test_file(exe)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
terminate(true)
|
data/lib/arduino_ci.rb
CHANGED
@@ -1,124 +1,10 @@
|
|
1
1
|
require "arduino_ci/version"
|
2
|
-
|
3
|
-
require
|
4
|
-
|
5
|
-
# Cross-platform way of finding an executable in the $PATH.
|
6
|
-
# via https://stackoverflow.com/a/5471032/2063546
|
7
|
-
# which('ruby') #=> /usr/bin/ruby
|
8
|
-
def which(cmd)
|
9
|
-
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
10
|
-
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
11
|
-
exts.each do |ext|
|
12
|
-
exe = File.join(path, "#{cmd}#{ext}")
|
13
|
-
return exe if File.executable?(exe) && !File.directory?(exe)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
nil
|
17
|
-
end
|
2
|
+
require "arduino_ci/arduino_installation"
|
3
|
+
require "arduino_ci/cpp_library"
|
4
|
+
require "arduino_ci/ci_config"
|
18
5
|
|
19
6
|
# ArduinoCI contains classes for automated testing of Arduino code on the command line
|
20
7
|
# @author Ian Katz <ifreecarve@gmail.com>
|
21
8
|
module ArduinoCI
|
22
9
|
|
23
|
-
# Wrap the Arduino executable. This requires, in some cases, a faked display.
|
24
|
-
class ArduinoCmd
|
25
|
-
|
26
|
-
# create as many ArduinoCmds as you like, but we need one and only one display manager
|
27
|
-
class DisplayMgr
|
28
|
-
include Singleton
|
29
|
-
attr_reader :enabled
|
30
|
-
|
31
|
-
def initialize
|
32
|
-
@existing = existing_display?
|
33
|
-
@enabled = false
|
34
|
-
@pid = nil
|
35
|
-
end
|
36
|
-
|
37
|
-
# attempt to determine if the machine is running a graphical display (i.e. not Travis)
|
38
|
-
def existing_display?
|
39
|
-
return true if RUBY_PLATFORM.include? "darwin"
|
40
|
-
return true if ENV["DISPLAY"].nil?
|
41
|
-
return true if ENV["DISPLAY"].include? ":"
|
42
|
-
false
|
43
|
-
end
|
44
|
-
|
45
|
-
# enable a virtual display
|
46
|
-
def enable
|
47
|
-
return @enabled = true if @existing # silent no-op if built in display
|
48
|
-
return unless @pid.nil?
|
49
|
-
|
50
|
-
@enabled = true
|
51
|
-
@pid = fork do
|
52
|
-
puts "Forking Xvfb"
|
53
|
-
system("Xvfb", ":1", "-ac", "-screen", "0", "1280x1024x16")
|
54
|
-
puts "Xvfb unexpectedly quit!"
|
55
|
-
end
|
56
|
-
sleep(3) # TODO: test a connection to the X server?
|
57
|
-
end
|
58
|
-
|
59
|
-
# disable the virtual display
|
60
|
-
def disable
|
61
|
-
return @enabled = false if @existing # silent no-op if built in display
|
62
|
-
return if @pid.nil?
|
63
|
-
|
64
|
-
begin
|
65
|
-
Process.kill 9, @pid
|
66
|
-
ensure
|
67
|
-
Process.wait @pid
|
68
|
-
@pid = nil
|
69
|
-
end
|
70
|
-
puts "Xvfb killed"
|
71
|
-
end
|
72
|
-
|
73
|
-
# Enable a virtual display for the duration of the given block
|
74
|
-
def with_display
|
75
|
-
enable
|
76
|
-
begin
|
77
|
-
yield environment
|
78
|
-
ensure
|
79
|
-
disable
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def environment
|
84
|
-
return nil unless @existing || @enabled
|
85
|
-
return {} if @existing
|
86
|
-
{ DISPLAY => ":1.0" }
|
87
|
-
end
|
88
|
-
|
89
|
-
# On finalize, ensure child process is ended
|
90
|
-
def self.finalize
|
91
|
-
disable
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
class << self
|
96
|
-
protected :new
|
97
|
-
|
98
|
-
# attempt to find a workable Arduino executable across platforms
|
99
|
-
def guess_executable_location
|
100
|
-
osx_place = "/Applications/Arduino.app/Contents/MacOS/Arduino"
|
101
|
-
places = {
|
102
|
-
"arduino" => !which("arduino").nil?,
|
103
|
-
osx_place => (File.exist? osx_place),
|
104
|
-
}
|
105
|
-
places.each { |k, v| return k if v }
|
106
|
-
nil
|
107
|
-
end
|
108
|
-
|
109
|
-
def autolocate
|
110
|
-
ret = new
|
111
|
-
ret.path = guess_executable_location
|
112
|
-
ret
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
attr_accessor :path
|
117
|
-
|
118
|
-
def initialize
|
119
|
-
@display_mgr = DisplayMgr::instance
|
120
|
-
end
|
121
|
-
|
122
|
-
end
|
123
|
-
|
124
10
|
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module ArduinoCI
|
4
|
+
|
5
|
+
# Wrap the Arduino executable. This requires, in some cases, a faked display.
|
6
|
+
class ArduinoCmd
|
7
|
+
|
8
|
+
# Enable a shortcut syntax for command line flags
|
9
|
+
# @param name [String] What the flag will be called (prefixed with 'flag_')
|
10
|
+
# @return [void]
|
11
|
+
# @macro [attach] flag
|
12
|
+
# The text of the command line flag for $1
|
13
|
+
# @!attribute [r] flag_$1
|
14
|
+
# @return [String] the text of the command line flag (`$2` in this case)
|
15
|
+
def self.flag(name, text = nil)
|
16
|
+
text = "(flag #{name} not defined)" if text.nil?
|
17
|
+
self.class_eval("def flag_#{name};\"#{text}\";end")
|
18
|
+
end
|
19
|
+
|
20
|
+
# the path to the Arduino executable
|
21
|
+
# @return [String]
|
22
|
+
attr_accessor :base_cmd
|
23
|
+
|
24
|
+
# part of a workaround for https://github.com/arduino/Arduino/issues/3535
|
25
|
+
attr_reader :library_is_indexed
|
26
|
+
|
27
|
+
# @return [String] STDOUT of the most recently-run command
|
28
|
+
attr_reader :last_out
|
29
|
+
|
30
|
+
# @return [String] STDERR of the most recently-run command
|
31
|
+
attr_reader :last_err
|
32
|
+
|
33
|
+
# @return [String] the most recently-run command
|
34
|
+
attr_reader :last_msg
|
35
|
+
|
36
|
+
# set the command line flags (undefined for now).
|
37
|
+
# These vary between gui/cli
|
38
|
+
flag :get_pref
|
39
|
+
flag :set_pref
|
40
|
+
flag :save_prefs
|
41
|
+
flag :use_board
|
42
|
+
flag :install_boards
|
43
|
+
flag :install_library
|
44
|
+
flag :verify
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@prefs_cache = {}
|
48
|
+
@prefs_fetched = false
|
49
|
+
@library_is_indexed = false
|
50
|
+
@last_out = ""
|
51
|
+
@last_err = ""
|
52
|
+
@last_msg = ""
|
53
|
+
end
|
54
|
+
|
55
|
+
# Convert a preferences dump into a flat hash
|
56
|
+
# @param arduino_output [String] The raw Arduino executable output
|
57
|
+
# @return [Hash] preferences as a hash
|
58
|
+
def parse_pref_string(arduino_output)
|
59
|
+
lines = arduino_output.split("\n").select { |l| l.include? "=" }
|
60
|
+
ret = lines.each_with_object({}) do |e, acc|
|
61
|
+
parts = e.split("=", 2)
|
62
|
+
acc[parts[0]] = parts[1]
|
63
|
+
acc
|
64
|
+
end
|
65
|
+
ret
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [String] the path to the Arduino libraries directory
|
69
|
+
def _lib_dir
|
70
|
+
"<lib dir not defined>"
|
71
|
+
end
|
72
|
+
|
73
|
+
# fetch preferences in their raw form
|
74
|
+
# @return [String] Preferences as a set of lines
|
75
|
+
def _prefs_raw
|
76
|
+
resp = run_and_capture(flag_get_pref)
|
77
|
+
return nil unless resp[:success]
|
78
|
+
resp[:out]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the Arduino preferences, from cache if possible
|
82
|
+
# @return [Hash] The full set of preferences
|
83
|
+
def prefs
|
84
|
+
prefs_raw = _prefs_raw unless @prefs_fetched
|
85
|
+
return nil if prefs_raw.nil?
|
86
|
+
@prefs_cache = parse_pref_string(prefs_raw)
|
87
|
+
@prefs_cache.clone
|
88
|
+
end
|
89
|
+
|
90
|
+
# get a preference key
|
91
|
+
# @param key [String] The preferences key to look up
|
92
|
+
# @return [String] The preference value
|
93
|
+
def get_pref(key)
|
94
|
+
data = @prefs_fetched ? @prefs_cache : prefs
|
95
|
+
data[key]
|
96
|
+
end
|
97
|
+
|
98
|
+
# underlying preference-setter.
|
99
|
+
# @param key [String] The preference name
|
100
|
+
# @param value [String] The value to set to
|
101
|
+
# @return [bool] whether the command succeeded
|
102
|
+
def _set_pref(key, value)
|
103
|
+
run_and_capture(flag_set_pref, "#{key}=#{value}", flag_save_prefs)[:success]
|
104
|
+
end
|
105
|
+
|
106
|
+
# set a preference key/value pair, and update the cache.
|
107
|
+
# @param key [String] the preference key
|
108
|
+
# @param value [String] the preference value
|
109
|
+
# @return [bool] whether the command succeeded
|
110
|
+
def set_pref(key, value)
|
111
|
+
success = _set_pref(key, value)
|
112
|
+
@prefs_cache[key] = value if success
|
113
|
+
success
|
114
|
+
end
|
115
|
+
|
116
|
+
# run the arduino command
|
117
|
+
def _run(*args, **kwargs)
|
118
|
+
raise "Ian needs to implement this in a subclass #{args} #{kwargs}"
|
119
|
+
end
|
120
|
+
|
121
|
+
# build and run the arduino command
|
122
|
+
def run(*args, **kwargs)
|
123
|
+
# do some work to extract & merge environment variables if they exist
|
124
|
+
has_env = !args.empty? && args[0].class == Hash
|
125
|
+
env_vars = has_env ? args[0] : {}
|
126
|
+
actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args
|
127
|
+
full_args = @base_cmd + actual_args
|
128
|
+
full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args
|
129
|
+
|
130
|
+
shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ")
|
131
|
+
@last_msg = " $ #{shell_vars} #{full_args.join(' ')}"
|
132
|
+
_run(*full_cmd, **kwargs)
|
133
|
+
end
|
134
|
+
|
135
|
+
# run a command and capture its output
|
136
|
+
# @return [Hash] {:out => String, :err => String, :success => bool}
|
137
|
+
def run_and_capture(*args, **kwargs)
|
138
|
+
pipe_out, pipe_out_wr = IO.pipe
|
139
|
+
pipe_err, pipe_err_wr = IO.pipe
|
140
|
+
our_kwargs = { out: pipe_out_wr, err: pipe_err_wr }
|
141
|
+
eventual_kwargs = our_kwargs.merge(kwargs)
|
142
|
+
success = run(*args, **eventual_kwargs)
|
143
|
+
pipe_out_wr.close
|
144
|
+
pipe_err_wr.close
|
145
|
+
str_out = pipe_out.read
|
146
|
+
str_err = pipe_err.read
|
147
|
+
pipe_out.close
|
148
|
+
pipe_err.close
|
149
|
+
@last_err = str_err
|
150
|
+
@last_out = str_out
|
151
|
+
{ out: str_out, err: str_err, success: success }
|
152
|
+
end
|
153
|
+
|
154
|
+
# run a command and don't capture its output, but use the same signature
|
155
|
+
# @return [Hash] {:out => String, :err => String, :success => bool}
|
156
|
+
def run_wrap(*args, **kwargs)
|
157
|
+
success = run(*args, **kwargs)
|
158
|
+
{ out: "NOPE, use run_and_capture", err: "NOPE, use run_and_capture", success: success }
|
159
|
+
end
|
160
|
+
|
161
|
+
# check whether a board is installed
|
162
|
+
# we do this by just selecting a board.
|
163
|
+
# the arduino binary will error if unrecognized and do a successful no-op if it's installed
|
164
|
+
# @param boardname [String] The board to test
|
165
|
+
# @return [bool] Whether the board is installed
|
166
|
+
def board_installed?(boardname)
|
167
|
+
run_and_capture(flag_use_board, boardname)[:success]
|
168
|
+
end
|
169
|
+
|
170
|
+
# install a board by name
|
171
|
+
# @param name [String] the board name
|
172
|
+
# @return [bool] whether the command succeeded
|
173
|
+
def install_boards(boardfamily)
|
174
|
+
# TODO: find out why IO.pipe fails but File::NULL succeeds :(
|
175
|
+
result = run_and_capture(flag_install_boards, boardfamily)
|
176
|
+
already_installed = result[:err].include?("Platform is already installed!")
|
177
|
+
result[:success] || already_installed
|
178
|
+
end
|
179
|
+
|
180
|
+
# install a library by name
|
181
|
+
# @param name [String] the library name
|
182
|
+
# @return [bool] whether the command succeeded
|
183
|
+
def install_library(library_name)
|
184
|
+
# workaround for https://github.com/arduino/Arduino/issues/3535
|
185
|
+
# use a dummy library name but keep open the possiblity that said library
|
186
|
+
# might be selected by choice for installation
|
187
|
+
workaround_lib = "USBHost"
|
188
|
+
unless @library_is_indexed || workaround_lib == library_name
|
189
|
+
@library_is_indexed = run_and_capture(flag_install_library, workaround_lib)
|
190
|
+
end
|
191
|
+
|
192
|
+
# actual installation
|
193
|
+
result = run_and_capture(flag_install_library, library_name)
|
194
|
+
|
195
|
+
# update flag if necessary
|
196
|
+
@library_is_indexed = (@library_is_indexed || result[:success]) if library_name == workaround_lib
|
197
|
+
result[:success]
|
198
|
+
end
|
199
|
+
|
200
|
+
# generate the (very likely) path of a library given its name
|
201
|
+
# @param library_name [String] The name of the library
|
202
|
+
# @return [String] The fully qualified library name
|
203
|
+
def library_path(library_name)
|
204
|
+
File.join(_lib_dir, library_name)
|
205
|
+
end
|
206
|
+
|
207
|
+
# update the library index
|
208
|
+
# @return [bool] Whether the update succeeded
|
209
|
+
def update_library_index
|
210
|
+
# install random lib so the arduino IDE grabs a new library index
|
211
|
+
# see: https://github.com/arduino/Arduino/issues/3535
|
212
|
+
install_library("USBHost")
|
213
|
+
end
|
214
|
+
|
215
|
+
# use a particular board for compilation
|
216
|
+
# @param boardname [String] The board to use
|
217
|
+
# @return [bool] whether the command succeeded
|
218
|
+
def use_board(boardname)
|
219
|
+
run_and_capture(flag_use_board, boardname, flag_save_prefs)[:success]
|
220
|
+
end
|
221
|
+
|
222
|
+
# use a particular board for compilation, installing it if necessary
|
223
|
+
# @param boardname [String] The board to use
|
224
|
+
# @return [bool] whether the command succeeded
|
225
|
+
def use_board!(boardname)
|
226
|
+
return true if use_board(boardname)
|
227
|
+
boardfamily = boardname.split(":")[0..1].join(":")
|
228
|
+
puts "Board '#{boardname}' not found; attempting to install '#{boardfamily}'"
|
229
|
+
return false unless install_boards(boardfamily) # guess board family from first 2 :-separated fields
|
230
|
+
use_board(boardname)
|
231
|
+
end
|
232
|
+
|
233
|
+
# @param path [String] The sketch to verify
|
234
|
+
# @return [bool] whether the command succeeded
|
235
|
+
def verify_sketch(path)
|
236
|
+
ext = File.extname path
|
237
|
+
unless ext.casecmp(".ino").zero?
|
238
|
+
@last_msg = "Refusing to verify sketch with '#{ext}' extension -- rename it to '.ino'!"
|
239
|
+
return false
|
240
|
+
end
|
241
|
+
unless File.exist? path
|
242
|
+
@last_msg = "Can't verify Sketch at nonexistent path '#{path}'!"
|
243
|
+
return false
|
244
|
+
end
|
245
|
+
ret = run_and_capture(flag_verify, path)
|
246
|
+
puts "=============="
|
247
|
+
puts ret[:out]
|
248
|
+
puts "--------------"
|
249
|
+
puts ret[:err]
|
250
|
+
ret[:success]
|
251
|
+
end
|
252
|
+
|
253
|
+
# ensure that the given library is installed, or symlinked as appropriate
|
254
|
+
# return the path of the prepared library, or nil
|
255
|
+
# @param path [String] library to use
|
256
|
+
# @return [String] the path of the installed library
|
257
|
+
def install_local_library(path)
|
258
|
+
realpath = File.expand_path(path)
|
259
|
+
library_name = File.basename(realpath)
|
260
|
+
destination_path = library_path(library_name)
|
261
|
+
|
262
|
+
# things get weird if the sketchbook contains the library.
|
263
|
+
# check that first
|
264
|
+
if File.exist? destination_path
|
265
|
+
uhoh = "There is already a library '#{library_name}' in the library directory"
|
266
|
+
return destination_path if destination_path == realpath
|
267
|
+
|
268
|
+
# maybe it's a symlink? that would be OK
|
269
|
+
if File.symlink?(destination_path)
|
270
|
+
return destination_path if File.readlink(destination_path) == realpath
|
271
|
+
@last_msg = "#{uhoh} and it's not symlinked to #{realpath}"
|
272
|
+
return nil
|
273
|
+
end
|
274
|
+
|
275
|
+
@last_msg = "#{uhoh}. It may need to be removed manually."
|
276
|
+
return nil
|
277
|
+
end
|
278
|
+
|
279
|
+
# install the library
|
280
|
+
FileUtils.ln_s(realpath, destination_path)
|
281
|
+
destination_path
|
282
|
+
end
|
283
|
+
|
284
|
+
# @param installed_library_path [String] The library to query
|
285
|
+
# @return [Array<String>] Example sketch files
|
286
|
+
def library_examples(installed_library_path)
|
287
|
+
example_path = File.join(installed_library_path, "examples")
|
288
|
+
examples = Pathname.new(example_path).children.select(&:directory?).map(&:to_path).map(&File.method(:basename))
|
289
|
+
files = examples.map do |e|
|
290
|
+
proj_file = File.join(example_path, e, "#{e}.ino")
|
291
|
+
File.exist?(proj_file) ? proj_file : nil
|
292
|
+
end
|
293
|
+
files.reject(&:nil?)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|