1 | var path = require('path'); |
||
2 | var Promise = require('bluebird'); |
||
3 | var fs = Promise.promisifyAll(require('fs-extra')); |
||
4 | var spawnAsync = require('child-process-promise').spawn; |
||
5 | var execAsync = require('child-process-promise').exec; |
||
6 | |||
7 | |||
8 | var ArduinoBuilder = function() {}; |
||
9 | |||
10 | ArduinoBuilder.prototype.BuildSketch = function(options, onStdout, onStderr) { |
||
11 | var buildOpts = []; |
||
12 | var sketchOpt = ''; |
||
13 | var sketchName = ''; |
||
14 | var stagedSketchDir = ''; |
||
15 | var installDir = ''; |
||
16 | var hash = ''; |
||
17 | |||
18 | return Promise.try(function() |
||
19 | { |
||
20 | // Process the options passed in |
||
21 | buildOpts = ProcessOptions(options); |
||
22 | sketchName = path.basename(options.sketchDir); |
||
23 | |||
24 | onStdout('Processed options'); |
||
25 | |||
26 | }) |
||
27 | .then(function() |
||
28 | { |
||
29 | // Create temp build directory if it doesn't already exist |
||
30 | return fs.mkdirsAsync(options.buildDir); |
||
31 | }) |
||
32 | .then(function() |
||
33 | { |
||
34 | installDir = path.join(options.installBaseDir, options.productID); |
||
35 | return fs.mkdirsAsync(installDir); |
||
36 | }) |
||
37 | .then(function() |
||
38 | { |
||
39 | onStdout('Made staging dir'); |
||
40 | |||
41 | stagedSketchDir = path.join('/opt/openrov/firmware/staged/', sketchName); |
||
42 | |||
43 | // Remove existing sketch folder if it exists |
||
44 | return fs.removeAsync(stagedSketchDir) |
||
45 | .then(function() |
||
46 | { |
||
47 | // Copy the sketch folder to the staging sketch dir |
||
48 | return fs.copyAsync(options.sketchDir, stagedSketchDir); |
||
49 | }); |
||
50 | }) |
||
51 | .then(function() |
||
52 | { |
||
53 | onStdout('Removed old sketch'); |
||
54 | |||
55 | // Handle code generation, if requested |
||
56 | if( options.generateCode ) |
||
57 | { |
||
58 | // Generate preproc header file |
||
59 | var output = '#pragma once\n\n'; |
||
60 | |||
61 | // Create defines |
||
62 | for (i = 0; i < options.preproc.length; ++i) |
||
63 | { |
||
64 | output += '#define ' + |
||
65 | options.preproc[i].split('=') |
||
66 | .toString() |
||
67 | .replace(',', ' ') + '\n'; |
||
68 | } |
||
69 | |||
70 | // Write CompileOptions.h file |
||
71 | return fs.writeFileAsync(path.join(stagedSketchDir, 'CompileOptions.h'), output) |
||
72 | .then( function() |
||
73 | { |
||
74 | onStdout( 'Generated CompileOptions.h' ); |
||
75 | |||
76 | var pluginDirs = GetDirectories('/opt/openrov/cockpit/src/plugins'); |
||
77 | var pluginString = '#pragma once\n\n'; |
||
78 | |||
79 | // Find all plugin folders and generate a Plugins.h file that pulls them in |
||
80 | return Promise.map(pluginDirs, function(pluginName) |
||
81 | { |
||
82 | var pluginDir = path.join('/opt/openrov/cockpit/src/plugins', pluginName); |
||
83 | |||
84 | // Check for a firmware folder |
||
85 | return fs.statAsync(path.join(pluginDir, 'firmware')) |
||
86 | .then(function() |
||
87 | { |
||
88 | // Copy all files to the sketch directory |
||
89 | return fs.copyAsync(path.join(pluginDir, 'firmware'), stagedSketchDir); |
||
90 | }) |
||
91 | .then(function() |
||
92 | { |
||
93 | // Add include for plugin to Plugins.h |
||
94 | pluginString += '#include "' + pluginName + '.h"\n'; |
||
95 | }) |
||
96 | .catch(function(err) |
||
97 | { |
||
98 | // Ignore errors. Most plugins don't have firmware folders and failing to copy a plugin isn't fatal |
||
99 | }); |
||
100 | }) |
||
101 | .then(function() |
||
102 | { |
||
103 | // Write the generated include string to Plugins.h |
||
104 | return fs.writeFileAsync(path.join(stagedSketchDir, 'Plugins.h'), pluginString); |
||
105 | }); |
||
106 | }) |
||
107 | .then( function() |
||
108 | { |
||
109 | // Generate a hash for everything in stagedSketchDir |
||
110 | // TODO: Make this cross platform and not a linux command call |
||
111 | return execAsync("find " + stagedSketchDir + " -name \\*.h -print0 -o -name \\*.hpp -print0 -o -name \\*.c -print0 -o -name \\*.cpp -print0 | xargs -0 sha1sum | sha1sum | awk '{print $1}'") |
||
112 | .then( function (result) |
||
113 | { |
||
114 | hash = result.stdout.trim(); |
||
115 | onStdout( "Arduino builder generated hash: " + hash ); |
||
116 | |||
117 | // Should look something like: "ver:<<{{10024121ae3fa7fc60a5945be1e155520fb929dd}}>>;" |
||
118 | var hashDef = "#define VERSION_HASH F(\"ver:<<{{" + hash + "}}>>;\")\n"; |
||
119 | |||
120 | // Append a define to the compile options |
||
121 | return fs.appendFileAsync( path.join(stagedSketchDir, 'CompileOptions.h'), hashDef ); |
||
122 | }) |
||
0 ignored issues
–
show
|
|||
123 | |||
124 | // TODO: |
||
125 | // Generate hash for the board core |
||
126 | // Generate hash for all libraries used |
||
127 | // Generate final hash for the combined generated hashes |
||
128 | }); |
||
129 | } |
||
130 | }) |
||
131 | .then(function() |
||
132 | { |
||
133 | buildOpts.push('-fqbn', options.fqbn); |
||
134 | buildOpts.push(stagedSketchDir); |
||
135 | |||
136 | // Create promise |
||
137 | var promise = spawnAsync('arduino-builder', buildOpts); |
||
138 | var childProcess = promise.childProcess; |
||
139 | |||
140 | // Attach listeners |
||
141 | childProcess.stdout.on('data', onStdout); |
||
142 | childProcess.stderr.on('data', onStderr); |
||
143 | |||
144 | return promise; |
||
145 | }) |
||
146 | .then(function() |
||
147 | { |
||
148 | // Copy final binaries into install dir, whatever type they may be |
||
149 | return Promise.any( |
||
150 | [ |
||
151 | fs.copyAsync(path.join(options.buildDir, sketchName + '.ino.bin'), path.join(installDir, sketchName + '.bin')) |
||
152 | .then(function() |
||
153 | { |
||
154 | return path.join(installDir, sketchName + '.bin'); |
||
155 | }), |
||
156 | fs.copyAsync(path.join(options.buildDir, sketchName + '.ino.elf'), path.join(installDir, sketchName + '.elf')) |
||
157 | .then(function() |
||
158 | { |
||
159 | return path.join(installDir, sketchName + '.elf'); |
||
160 | }), |
||
161 | fs.copyAsync(path.join(options.buildDir, sketchName + '.ino.hex'), path.join(installDir, sketchName + '.hex')) |
||
162 | .then(function() |
||
163 | { |
||
164 | return path.join(installDir, sketchName + '.hex'); |
||
165 | }) |
||
166 | ]); |
||
167 | }) |
||
168 | .then( function() |
||
169 | { |
||
170 | // Only write hash if building generated code |
||
171 | if( options.generateCode ) |
||
172 | { |
||
173 | onStdout( "Writing hash: " + hash ); |
||
174 | |||
175 | // Write the hash to file |
||
176 | return fs.writeFileAsync( "/opt/openrov/system/config/lastBuildHash", hash ); |
||
177 | } |
||
178 | }) |
||
179 | .then(function(firmwareFile) |
||
180 | { |
||
181 | onStdout('SUCCESS'); |
||
182 | |||
183 | return Promise.try(function() |
||
184 | { |
||
185 | if (options.cleanAfterBuild) |
||
186 | { |
||
187 | // Clean up temp dir |
||
188 | return fs.removeAsync(options.buildDir) |
||
189 | .then(function() |
||
190 | { |
||
191 | return fs.removeAsync('/opt/openrov/firmware/staged'); |
||
192 | }); |
||
193 | } |
||
194 | }) |
||
195 | .catch(function(error) |
||
196 | { |
||
197 | onStderr("Strange error while trying to cleanup firmware staged files: " + error.message); |
||
198 | }) |
||
199 | .then(function() |
||
200 | { |
||
201 | return firmwareFile; |
||
202 | }); |
||
203 | |||
204 | }) |
||
205 | .catch(function(err) |
||
206 | { |
||
207 | if (options.cleanAfterBuild) |
||
208 | { |
||
209 | // Clean up temp dir first |
||
210 | return fs.removeAsync(options.buildDir) |
||
211 | .then(function() |
||
212 | { |
||
213 | // Rethrow error |
||
214 | throw err; |
||
215 | }); |
||
216 | } |
||
217 | else |
||
218 | { |
||
219 | throw err; |
||
220 | } |
||
221 | }); |
||
222 | }; |
||
223 | |||
224 | function ProcessOptions(options) |
||
225 | { |
||
226 | // First, validate required options |
||
227 | ValidateOptions(options); |
||
228 | |||
229 | var defaults = |
||
230 | { |
||
231 | sketchDir: '', |
||
232 | productID: '', |
||
233 | buildDir: '/opt/openrov/firmware/build', |
||
234 | installBaseDir: '/opt/openrov/firmware/bin', |
||
235 | cleanAfterBuild: true, |
||
236 | fqbn: '', |
||
237 | hardware: '/opt/openrov/arduino/hardware', |
||
238 | tools: '/opt/openrov/arduino/hardware/tools', |
||
239 | warnings: 'all', |
||
240 | verbose: true, |
||
241 | quiet: false, |
||
242 | debug: 5, |
||
243 | libs: [], |
||
244 | preproc: [], |
||
245 | generateCode: true |
||
246 | }; |
||
247 | |||
248 | // Override defaults with options |
||
249 | options = options || {}; |
||
250 | |||
251 | for (var opt in defaults) |
||
252 | { |
||
253 | if (defaults.hasOwnProperty(opt) && !options.hasOwnProperty(opt)) |
||
254 | { |
||
255 | options[opt] = defaults[opt]; |
||
256 | } |
||
257 | } |
||
258 | // Create optArray |
||
259 | var optArray = []; |
||
260 | |||
261 | if (options.verbose) |
||
262 | { |
||
263 | optArray.push('-verbose'); |
||
264 | } |
||
265 | |||
266 | if (options.quiet) |
||
267 | { |
||
268 | optArray.push('-quiet'); |
||
269 | } |
||
270 | |||
271 | optArray.push('-compile'); |
||
272 | optArray.push('-warnings', options.warnings); |
||
273 | optArray.push('-build-path', options.buildDir); |
||
274 | optArray.push('-hardware', options.hardware); |
||
275 | optArray.push('-tools', options.tools); |
||
276 | |||
277 | for (i = 0; i < options.libs.length; ++i) |
||
278 | { |
||
279 | optArray.push('-libraries', options.libs[i]); |
||
280 | } |
||
281 | |||
282 | return optArray; |
||
283 | } |
||
284 | |||
285 | function ValidateOptions(options) |
||
286 | { |
||
287 | // Required parameters |
||
288 | if (IsBlank(options.sketchDir)) |
||
289 | { |
||
290 | throw new Error('Missing required option: sketchDir'); |
||
291 | } |
||
292 | |||
293 | if (IsBlank(options.productID)) |
||
294 | { |
||
295 | throw new Error('Missing required option: productID'); |
||
296 | } |
||
297 | |||
298 | if (IsBlank(options.installBaseDir)) |
||
299 | { |
||
300 | throw new Error('Missing required option: installBaseDir'); |
||
301 | } |
||
302 | |||
303 | if (IsBlank(options.fqbn)) |
||
304 | { |
||
305 | throw new Error('Missing required option: fqbn'); |
||
306 | } |
||
307 | } |
||
308 | |||
309 | function IsBlank(str) { |
||
310 | return !str || /^\s*$/.test(str); |
||
311 | } |
||
312 | |||
313 | function GetDirectories(dir) |
||
314 | { |
||
315 | return fs.readdirSync(dir) |
||
316 | .filter(function(file) |
||
317 | { |
||
318 | return fs.statSync(path.join(dir, file)) |
||
319 | .isDirectory(); |
||
320 | }); |
||
321 | } |
||
322 | |||
323 | function HashDirectory( pathIn ) |
||
324 | { |
||
325 | |||
326 | } |
||
327 | |||
328 | module.exports = new ArduinoBuilder(); |
Requirement of semicolons purely is a coding style issue since JavaScript has specific rules about semicolons which are followed by all browsers.
Further Readings: