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