1
0

builder.js 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094
  1. /* vim: set expandtab ts=4 sw=4: */
  2. /*
  3. * You may redistribute this program and/or modify it under the terms of
  4. * the GNU General Public License as published by the Free Software Foundation,
  5. * either version 3 of the License, or (at your option) any later version.
  6. *
  7. * This program is distributed in the hope that it will be useful,
  8. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. * GNU General Public License for more details.
  11. *
  12. * You should have received a copy of the GNU General Public License
  13. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. var Os = require('os');
  16. var Fs = require('fs');
  17. var Spawn = require('child_process').spawn;
  18. var nThen = require('nthen');
  19. var Crypto = require('crypto');
  20. var Semaphore = require('./Semaphore');
  21. /*
  22. * Why hello dear packager,
  23. *
  24. * I suppose you have found this place as you are trying to figure out how to work this into your
  25. * build system. You're probably faced with a decision between getting node.js into your build and
  26. * "fixing" this build process so it doesn't need such a silly thing. A 500 line script is certainly
  27. * not unapproachable, right?
  28. * The reason why I am speaking to you now is because I care about you. I want you to be happy
  29. * and live a carefree life, and because you are standing on the precipice of a cavern so dark and
  30. * deep that while you may well make it out alive, your personal pride and innocence almost
  31. * certainly will not. Imagine yourself after months of sleepless nights wallowing in the quicksand,
  32. * forever trying to slay the dragon which is always so close yet and so far away. Imagine the deep
  33. * hatred you will have for humanity, code, and the creator of this doomsday machine. I beg you to
  34. * turn back now while there is still hope. You need not die here, your life is important, and
  35. * whether you close this file now or not, in the end you will still end up including node.js in
  36. * your build.
  37. *
  38. * The Creator
  39. */
  40. // Since many of the compile operations are short, the best
  41. // performance seems to be when running 1.25x the number of jobs as
  42. // cpu cores. On BSD and iphone systems, os.cpus() is not reliable so
  43. // if it returns undefined let's just assume 1
  44. var cpus = Os.cpus(); // workaround, nodejs seems to be broken on openbsd (undefined result after second call)
  45. var PROCESSORS = Math.floor((typeof cpus === 'undefined' ? 1 : cpus.length) * 1.25);
  46. var error = function (message)
  47. {
  48. try {
  49. throw new Error(message);
  50. } catch (e) {
  51. return e;
  52. }
  53. };
  54. var expandArgs = function (args) {
  55. var out = [];
  56. for (var i = 0; i < args.length; i++) {
  57. if (typeof(args[i]) === 'object') {
  58. if (Array.isArray(args[i])) {
  59. out.push.apply(out, expandArgs(args[i]));
  60. } else {
  61. throw new Error("object in arguments [" + args + "]");
  62. }
  63. } else {
  64. out.push(args[i]);
  65. }
  66. }
  67. return out;
  68. };
  69. var sema = Semaphore.create(PROCESSORS);
  70. var compiler = function (compilerPath, args, callback, content) {
  71. args = expandArgs(args);
  72. sema.take(function (returnAfter) {
  73. var gcc = Spawn(compilerPath, args);
  74. var err = '';
  75. var out = '';
  76. gcc.stdout.on('data', function (dat) { out += dat.toString(); });
  77. gcc.stderr.on('data', function (dat) { err += dat.toString(); });
  78. gcc.on('close', returnAfter(function (ret) {
  79. callback(ret, out, err);
  80. }));
  81. gcc.on('error', function (err) {
  82. if (err.code === 'ENOENT') {
  83. console.error('\033[1;31mError: ' + compilerPath + ' is required!\033[0m');
  84. } else {
  85. console.error(
  86. '\033[1;31mFail run ' + process.cwd() + ': ' + compilerPath + ' '
  87. + args.join(' ') + '\033[0m'
  88. );
  89. console.error('Message:' + err);
  90. }
  91. // handle the error safely
  92. console.log(args);
  93. });
  94. if (content) {
  95. gcc.stdin.write(content, function (err) {
  96. if (err) { throw err; }
  97. gcc.stdin.end();
  98. });
  99. }
  100. });
  101. };
  102. var cc = function (gcc, args, callback, content) {
  103. compiler(gcc, args, function (ret, out, err) {
  104. if (ret) {
  105. callback(error("gcc " + args.join(' ') + "\n\n" + err));
  106. }
  107. if (err !== '') {
  108. debug(err);
  109. }
  110. callback(undefined, out);
  111. }, content);
  112. };
  113. var tmpFile = function (state, name) {
  114. name = name || '';
  115. return state.tempDir + '/jsmake-' + name + Crypto.pseudoRandomBytes(10).toString('hex');
  116. };
  117. var mkBuilder = function (state) {
  118. var builder = {
  119. cc: function (args, callback) {
  120. compiler(builder.config.gcc, args, callback);
  121. },
  122. buildExecutable: function (cFile, outputFile) {
  123. if (!outputFile) {
  124. outputFile = cFile.replace(/^.*\/([^\/\.]*).*$/, function (a, b) { return b; });
  125. }
  126. if (state.systemName === 'win32' && !(/\.exe$/.test(outputFile))) {
  127. outputFile += '.exe';
  128. }
  129. var temp = state.buildDir + '/' + getExecutableFile(cFile);
  130. compile(cFile, temp, builder, builder.waitFor());
  131. builder.executables.push([temp, outputFile]);
  132. return temp;
  133. },
  134. buildTest: function (cFile) {
  135. return builder.buildExecutable(cFile, state.buildDir + '/' + getExecutableFile(cFile));
  136. },
  137. runTest: function (outFile, testRunner) {
  138. builder.tests.push(function (cb) { testRunner(outFile, cb); });
  139. },
  140. lintFiles: function (linter) {
  141. builder.linters.push(linter);
  142. },
  143. config: state,
  144. tmpFile: function (name) {
  145. return tmpFile(state, name);
  146. },
  147. rebuiltFiles: [],
  148. // Test executables (arrays containing name in build dir and test runner)
  149. tests: [],
  150. // Executables (arrays containing name in build dir and final name)
  151. executables: [],
  152. linters: [],
  153. // Concurrency...
  154. processors: PROCESSORS
  155. };
  156. return builder;
  157. };
  158. // You Were Warned
  159. var execJs = function (js, builder, file, fileName, callback) {
  160. var res;
  161. var x;
  162. var err;
  163. // # 74 "./wire/Message.h"
  164. js = js.replace(/\n#.*\n/g, '');
  165. var to = setTimeout(function () {
  166. throw new Error("Inline JS did not return after 120 seconds [" + js + "]");
  167. }, 120000);
  168. var REQUIRE = function (str) {
  169. if (typeof(str) !== 'string') {
  170. throw new Error("must be a string");
  171. }
  172. try { return require(str); } catch (e) { }
  173. return require(process.cwd() + '/' + str);
  174. };
  175. nThen(function (waitFor) {
  176. try {
  177. /* jshint -W054 */ // Suppress jshint warning on Function being a form of eval
  178. var func = new Function('file', 'require', 'fileName', 'console', 'builder', js);
  179. func.async = function () {
  180. return waitFor(function (result) {
  181. res = result;
  182. });
  183. };
  184. x = func.call(func,
  185. file,
  186. REQUIRE,
  187. fileName,
  188. console,
  189. builder);
  190. } catch (e) {
  191. err = e;
  192. err.message += "\nContent: [" + js + "]";
  193. clearTimeout(to);
  194. throw err;
  195. }
  196. }).nThen(function (waitFor) {
  197. if (err) { return; }
  198. res = res || x || '';
  199. clearTimeout(to);
  200. process.nextTick(function () { callback(undefined, res); });
  201. });
  202. };
  203. var debug = console.log;
  204. var preprocessBlock = function (block, builder, fileObj, fileName, callback) {
  205. // a block is an array of strings and arrays, any inside arrays must be
  206. // preprocessed first. deep first top to bottom.
  207. var error = false;
  208. var nt = nThen;
  209. block.forEach(function (elem, i) {
  210. if (typeof(elem) === 'string') { return; }
  211. nt = nt(function (waitFor) {
  212. preprocessBlock(elem, builder, fileObj, fileName, waitFor(function (err, ret) {
  213. if (err) { throw err; }
  214. block[i] = ret;
  215. }));
  216. }).nThen;
  217. });
  218. nt(function (waitFor) {
  219. if (error) { return; }
  220. var capture = block.join('');
  221. execJs(capture, builder, fileObj, fileName, waitFor(function (err, ret) {
  222. if (err) { throw err; }
  223. callback(undefined, ret);
  224. }));
  225. });
  226. };
  227. var preprocess = function (content, builder, fileObj, fileName, callback) {
  228. // <?js file.Test_mainFunc = "<?js return 'RootTest_'+file.RootTest_mainFunc; ?>" ?>
  229. // worse:
  230. // <?js file.Test_mainFunc = "<?js var done = this.async(); process.nextTick(done); ?>" ?>
  231. var flatArray = content.split(/(<\?js|\?>)/);
  232. var elems = [];
  233. var unflatten = function (array, startAt, out) {
  234. for (var i = startAt; i < array.length; i++) {
  235. /* jshint -W018 */ // Suppress jshint warning on ! being confusing
  236. if (!((i - startAt) % 2)) {
  237. out.push(array[i]);
  238. } else if (array[i] === '<?js') {
  239. var next = [];
  240. out.push(next);
  241. i = unflatten(array, i+1, next);
  242. } else if (array[i] === '?>') {
  243. return i;
  244. }
  245. }
  246. return i;
  247. };
  248. if (unflatten(flatArray, 0, elems) !== flatArray.length) {
  249. throw new Error();
  250. }
  251. var nt = nThen;
  252. elems.forEach(function (elem, i) {
  253. if (typeof(elem) === 'string') { return; }
  254. nt = nt(function (waitFor) {
  255. preprocessBlock(elem, builder, fileObj, fileName, waitFor(function (err, ret) {
  256. if (err) { throw err; }
  257. elems[i] = ret;
  258. }));
  259. }).nThen;
  260. });
  261. nt(function (waitFor) {
  262. callback(undefined, elems.join(''));
  263. });
  264. };
  265. var getFile = function ()
  266. {
  267. return {
  268. includes: [],
  269. links: [],
  270. cflags: [],
  271. oldmtime: 0
  272. };
  273. };
  274. var getObjectFile = function (cFile) {
  275. return cFile.replace(/[^a-zA-Z0-9_-]/g, '_') + '.o';
  276. };
  277. var getExecutableFile = function (cFile) {
  278. return cFile.replace(/[^a-zA-Z0-9_-]/g, '_');
  279. };
  280. var getFlags = function (state, fileName, includeDirs) {
  281. var flags = [];
  282. flags.push.apply(flags, state.cflags);
  283. flags.push.apply(flags, state['cflags'+fileName]);
  284. if (includeDirs) {
  285. for (var i = 0; i < state.includeDirs.length; i++) {
  286. if (flags[flags.indexOf(state.includeDirs[i])-1] === '-I') {
  287. continue;
  288. }
  289. flags.push('-I');
  290. flags.push(state.includeDirs[i]);
  291. }
  292. }
  293. for (var ii = flags.length-1; ii >= 0; ii--) {
  294. // might be undefined because splicing causes us to be off the end of the array
  295. if (typeof(flags[ii]) === 'string' && flags[ii][0] === '!') {
  296. var f = flags[ii].substring(1);
  297. flags.splice(ii, 1);
  298. var index;
  299. while ((index = flags.indexOf(f)) > -1) {
  300. flags.splice(index, 1);
  301. }
  302. }
  303. }
  304. return flags;
  305. };
  306. var currentlyCompiling = {};
  307. var compileFile = function (fileName, builder, tempDir, callback)
  308. {
  309. var state = builder.config;
  310. if (typeof(state.files[fileName]) !== 'undefined') {
  311. callback();
  312. return;
  313. }
  314. if (typeof(currentlyCompiling[fileName]) !== 'undefined') {
  315. currentlyCompiling[fileName].push(callback);
  316. return;
  317. } else {
  318. currentlyCompiling[fileName] = [];
  319. }
  320. currentlyCompiling[fileName].push(callback);
  321. //debug('\033[2;32mCompiling ' + fileName + '\033[0m');
  322. var preprocessed = state.buildDir + '/' + getObjectFile(fileName) + '.i';
  323. var outFile = state.buildDir + '/' + getObjectFile(fileName);
  324. var fileContent;
  325. var fileObj = getFile();
  326. nThen(function (waitFor) {
  327. (function () {
  328. //debug("CPP -MM");
  329. var flags = ['-E', '-MM'];
  330. flags.push.apply(flags, getFlags(state, fileName, true));
  331. flags.push(fileName);
  332. cc(state.gcc, flags, waitFor(function (err, output) {
  333. if (err) { throw err; }
  334. // replace the escapes and newlines
  335. output = output.replace(/ \\|\n/g, '').split(' ');
  336. // first 2 entries are crap
  337. output.splice(0, 2);
  338. for (var i = output.length-1; i >= 0; i--) {
  339. //console.log('Removing empty dependency [' +
  340. // state.gcc + ' ' + flags.join(' ') + ']');
  341. if (output[i] === '') {
  342. output.splice(i, 1);
  343. }
  344. }
  345. fileObj.includes = output;
  346. }));
  347. })();
  348. (function () {
  349. //debug("CPP");
  350. var flags = ['-E'];
  351. flags.push.apply(flags, getFlags(state, fileName, true));
  352. flags.push(fileName);
  353. cc(state.gcc, flags, waitFor(function (err, output) {
  354. if (err) { throw err; }
  355. fileContent = output;
  356. }));
  357. })();
  358. }).nThen(function (waitFor) {
  359. Fs.exists(preprocessed, waitFor(function (exists) {
  360. if (!exists) { return; }
  361. Fs.unlink(preprocessed, waitFor(function (err) {
  362. if (err) { throw err; }
  363. }));
  364. }));
  365. }).nThen(function (waitFor) {
  366. //debug("Preprocess");
  367. preprocess(fileContent, builder, fileObj, fileName, waitFor(function (err, output) {
  368. if (err) { throw err; }
  369. Fs.writeFile(preprocessed, output, waitFor(function (err) {
  370. if (err) { throw err; }
  371. }));
  372. // important, this will prevent the file from also being piped to gcc.
  373. fileContent = undefined;
  374. }));
  375. Fs.exists(outFile, waitFor(function (exists) {
  376. if (!exists) { return; }
  377. Fs.unlink(outFile, waitFor(function (err) {
  378. if (err) { throw err; }
  379. }));
  380. }));
  381. }).nThen(function (waitFor) {
  382. //debug("CC");
  383. var flags = ['-c', '-x', 'cpp-output', '-o', outFile];
  384. flags.push.apply(flags, getFlags(state, fileName, false));
  385. flags.push(preprocessed);
  386. cc(state.gcc, flags, waitFor(function (err) {
  387. if (err) { throw err; }
  388. fileObj.obj = outFile;
  389. }), fileContent);
  390. }).nThen(function (waitFor) {
  391. debug('\033[2;32mBuilding C object ' + fileName + ' complete\033[0m');
  392. state.files[fileName] = fileObj;
  393. var callbacks = currentlyCompiling[fileName];
  394. delete currentlyCompiling[fileName];
  395. callbacks.forEach(function (cb) { cb(); });
  396. });
  397. };
  398. /**
  399. * @param files state.files
  400. * @param mtimes a mapping of files to times for files for which the times are known
  401. * @param callback when done.
  402. */
  403. var getMTimes = function (files, mtimes, callback)
  404. {
  405. nThen(function (waitFor) {
  406. Object.keys(files).forEach(function (fileName) {
  407. mtimes[fileName] = mtimes[fileName] || 0;
  408. files[fileName].includes.forEach(function (incl) {
  409. mtimes[incl] = mtimes[incl] || 0;
  410. });
  411. });
  412. Object.keys(mtimes).forEach(function (fileName) {
  413. if (mtimes[fileName] !== 0) { return; }
  414. Fs.stat(fileName, waitFor(function (err, stat) {
  415. if (err) {
  416. waitFor.abort();
  417. callback(err);
  418. return;
  419. }
  420. mtimes[fileName] = stat.mtime.getTime();
  421. }));
  422. });
  423. }).nThen(function (waitFor) {
  424. callback(undefined, mtimes);
  425. });
  426. };
  427. var removeFile = function (state, fileName, callback)
  428. {
  429. //debug("remove " + fileName);
  430. nThen(function (waitFor) {
  431. // And every file which includes it
  432. Object.keys(state.files).forEach(function (file) {
  433. // recursion could remove it
  434. if (typeof(state.files[file]) === 'undefined') {
  435. return;
  436. }
  437. if (state.files[file].includes.indexOf(fileName) !== -1) {
  438. removeFile(state, file, waitFor());
  439. }
  440. });
  441. // we'll set the oldmtime on the file to 0 since it's getting rebuilt.
  442. state.oldmtimes[fileName] = 0;
  443. var f = state.files[fileName];
  444. if (typeof(f) === 'undefined') {
  445. return;
  446. }
  447. delete state.files[fileName];
  448. if (typeof(f.obj) === 'string') {
  449. Fs.unlink(f.obj, waitFor(function (err) {
  450. if (err && err.code !== 'ENOENT') {
  451. throw err;
  452. }
  453. }));
  454. }
  455. }).nThen(function (waitFor) {
  456. callback();
  457. });
  458. };
  459. var recursiveCompile = function (fileName, builder, tempDir, callback)
  460. {
  461. // Recursive compilation
  462. var state = builder.config;
  463. var doCycle = function (toCompile, parentStack, callback) {
  464. if (toCompile.length === 0) {
  465. callback();
  466. return;
  467. }
  468. nThen(function (waitFor) {
  469. var filefunc = function (file) {
  470. var stack = [];
  471. stack.push.apply(stack, parentStack);
  472. //debug("compiling " + file);
  473. stack.push(file);
  474. if (stack.indexOf(file) !== stack.length - 1) {
  475. throw new Error("Dependency loops are bad and you should feel bad\n" +
  476. "Dependency stack:\n" + stack.reverse().join('\n'));
  477. }
  478. compileFile(file, builder, tempDir, waitFor(function () {
  479. var toCompile = [];
  480. state.files[file].links.forEach(function (link) {
  481. if (link === file) {
  482. return;
  483. }
  484. toCompile.push(link);
  485. });
  486. doCycle(toCompile, stack, waitFor(function () {
  487. if (stack[stack.length - 1] !== file) {
  488. throw new Error();
  489. }
  490. stack.pop();
  491. }));
  492. }));
  493. };
  494. for (var file = toCompile.pop(); file; file = toCompile.pop()) {
  495. filefunc(file);
  496. }
  497. }).nThen(function (waitFor) {
  498. callback();
  499. });
  500. };
  501. doCycle([fileName], [], callback);
  502. };
  503. var getLinkOrder = function (fileName, files) {
  504. var completeFiles = [];
  505. var getFile = function (name) {
  506. var f = files[name];
  507. //debug('Resolving links for ' + name + ' ' + f);
  508. for (var i = 0; i < f.links.length; i++) {
  509. if (f.links[i] === name) {
  510. continue;
  511. }
  512. if (completeFiles.indexOf(f.links[i]) > -1) {
  513. continue;
  514. }
  515. getFile(f.links[i]);
  516. }
  517. completeFiles.push(name);
  518. };
  519. getFile(fileName);
  520. return completeFiles;
  521. };
  522. var needsToLink = function (fileName, state) {
  523. if (typeof(state.oldmtimes[fileName]) !== 'number') {
  524. return true;
  525. }
  526. if (state.oldmtimes[fileName] !== state.mtimes[fileName]) {
  527. return true;
  528. }
  529. var links = state.files[fileName].links;
  530. for (var i = 0; i < links.length; i++) {
  531. if (links[i] !== fileName && needsToLink(links[i], state)) {
  532. return true;
  533. }
  534. }
  535. return false;
  536. };
  537. var makeTime = function () {
  538. return function () {
  539. var oldTime = this.time || 0;
  540. var newTime = this.time = new Date().getTime();
  541. return newTime - oldTime;
  542. };
  543. };
  544. var compile = function (file, outputFile, builder, callback) {
  545. var state = builder.config;
  546. var tempDir;
  547. if (!needsToLink(file, state)) {
  548. process.nextTick(callback);
  549. return;
  550. }
  551. nThen(function (waitFor) {
  552. tempDir = tmpFile(state);
  553. Fs.mkdir(tempDir, waitFor(function (err) {
  554. if (err) { throw err; }
  555. }));
  556. }).nThen(function (waitFor) {
  557. recursiveCompile(file, builder, tempDir, waitFor());
  558. }).nThen(function (waitFor) {
  559. var linkOrder = getLinkOrder(file, state.files);
  560. for (var i = 0; i < linkOrder.length; i++) {
  561. linkOrder[i] = state.buildDir + '/' + getObjectFile(linkOrder[i]);
  562. }
  563. var ldArgs = [state.ldflags, '-o', outputFile, linkOrder, state.libs];
  564. debug('\033[1;31mLinking C executable ' + file + '\033[0m');
  565. cc(state.gcc, ldArgs, waitFor(function (err, ret) {
  566. if (err) { throw err; }
  567. }));
  568. }).nThen(function (waitFor) {
  569. Fs.readdir(tempDir, waitFor(function (err, files) {
  570. if (err) { throw err; }
  571. files.forEach(function (file) {
  572. Fs.unlink(tempDir + '/' + file, waitFor(function (err) {
  573. if (err) { throw err; }
  574. }));
  575. });
  576. }));
  577. }).nThen(function (waitFor) {
  578. Fs.rmdir(tempDir, waitFor(function (err) {
  579. if (err) { throw err; }
  580. }));
  581. }).nThen(function (waitFor) {
  582. if (callback) {
  583. callback();
  584. }
  585. });
  586. };
  587. var getStatePrototype = function (params) {
  588. var base = {
  589. includeDirs: ['.'],
  590. files: {},
  591. mtimes: {},
  592. cflags: [],
  593. ldflags: [],
  594. libs: [],
  595. rebuildIfChanges: [],
  596. rebuildIfChangesHash: undefined,
  597. tempDir: '/tmp',
  598. systemName: 'linux'
  599. };
  600. for (var key in params) {
  601. if (params.hasOwnProperty(key)) {
  602. if (typeof params[key] !== 'object') {
  603. base[key] = params[key];
  604. }
  605. }
  606. }
  607. return base;
  608. };
  609. /**
  610. * Get a copy of process.env with a few entries which are constantly changing removed.
  611. * This prevents isStaleState from returning true every time one builds in a different
  612. * window.
  613. */
  614. var normalizedProcessEnv = function () {
  615. var out = process.env;
  616. delete out.WINDOWID;
  617. delete out.OLDPWD;
  618. return out;
  619. };
  620. var getRebuildIfChangesHash = function (rebuildIfChanges, callback) {
  621. var ret = false;
  622. var hash = Crypto.createHash('sha256');
  623. var rebIfChg = [];
  624. nThen(function (waitFor) {
  625. rebuildIfChanges.forEach(function (fileName, i) {
  626. Fs.readFile(fileName, waitFor(function (err, ret) {
  627. if (err) { throw err; }
  628. rebIfChg[i] = ret;
  629. }));
  630. });
  631. hash.update(JSON.stringify(normalizedProcessEnv()));
  632. }).nThen(function (waitFor) {
  633. rebIfChg.forEach(function (data) {
  634. hash.update(data);
  635. });
  636. callback(hash.digest('hex'));
  637. });
  638. };
  639. process.on('exit', function () {
  640. console.log("Total build time: " + Math.floor(process.uptime() * 1000) + "ms.");
  641. });
  642. var stage = function (st, builder, waitFor) {
  643. builder.waitFor = waitFor;
  644. st(builder, waitFor);
  645. };
  646. var throwIfErr = function (err) { if (err) { throw err; } };
  647. var configure = module.exports.configure = function (params, configFunc) {
  648. // Track time taken for various steps
  649. var time = makeTime();
  650. time();
  651. if (typeof(params.systemName) !== 'string') {
  652. throw new Error("system not specified");
  653. }
  654. params.buildDir = params.buildDir || 'build_' + params.systemName;
  655. var state;
  656. var builder;
  657. var buildStage = function () {};
  658. var testStage = function () {};
  659. var packStage = function () {};
  660. var successStage = function () {};
  661. var failureStage = function () {};
  662. var completeStage = function () {};
  663. nThen(function (waitFor) {
  664. // make the build directory
  665. Fs.exists(params.buildDir, waitFor(function (exists) {
  666. if (exists) { return; }
  667. Fs.mkdir(params.buildDir, waitFor(function (err) {
  668. if (err) { throw err; }
  669. }));
  670. }));
  671. }).nThen(function (waitFor) {
  672. // read out the state if it exists
  673. Fs.exists(params.buildDir + '/state.json', waitFor(function (exists) {
  674. if (!exists) { return; }
  675. Fs.readFile(params.buildDir + '/state.json', waitFor(function (err, ret) {
  676. if (err) { throw err; }
  677. state = JSON.parse(ret);
  678. }));
  679. }));
  680. }).nThen(function (waitFor) {
  681. if (!state || !state.rebuildIfChanges) {
  682. // no state
  683. state = undefined;
  684. } else {
  685. getRebuildIfChangesHash(state.rebuildIfChanges, waitFor(function (rich) {
  686. if (rich !== state.rebuildIfChangesHash) {
  687. debug("rebuildIfChanges changed, rebuilding");
  688. state = undefined;
  689. }
  690. }));
  691. }
  692. }).nThen(function (waitFor) {
  693. debug("Initialize " + time() + "ms");
  694. // Do the configuration step
  695. if (state) {
  696. builder = mkBuilder(state);
  697. return;
  698. }
  699. state = getStatePrototype(params);
  700. builder = mkBuilder(state);
  701. configFunc(builder, waitFor);
  702. }).nThen(function (waitFor) {
  703. debug("Configure " + time() + "ms");
  704. if (state.rebuildIfChangesHash) {
  705. return;
  706. }
  707. if (state.rebuildIfChanges.indexOf(module.parent.filename) === -1) {
  708. // Always always rebuild if the makefile was changed.
  709. state.rebuildIfChanges.push(module.parent.filename);
  710. }
  711. getRebuildIfChangesHash(state.rebuildIfChanges, waitFor(function (rich) {
  712. state.rebuildIfChangesHash = rich;
  713. }));
  714. }).nThen(function (waitFor) {
  715. state.oldmtimes = state.mtimes;
  716. state.mtimes = {};
  717. Object.keys(state.oldmtimes).forEach(function (fileName) {
  718. Fs.stat(fileName, waitFor(function (err, stat) {
  719. if (err) {
  720. if (err.code === 'ENOENT') {
  721. // Doesn't matter as long as it's not referenced...
  722. debug("File [" + fileName + "] was removed");
  723. delete state.files[fileName];
  724. return;
  725. } else {
  726. throw err;
  727. }
  728. }
  729. state.mtimes[fileName] = stat.mtime.getTime();
  730. if (state.oldmtimes[fileName] !== stat.mtime.getTime()) {
  731. debug(fileName + ' is out of date, rebuilding');
  732. removeFile(state, fileName, waitFor());
  733. }
  734. }));
  735. });
  736. }).nThen(function (waitFor) {
  737. debug("Scan for out of date files " + time() + "ms");
  738. }).nThen(function (waitFor) {
  739. stage(buildStage, builder, waitFor);
  740. }).nThen(function (waitFor) {
  741. debug("Compile " + time() + "ms");
  742. var allFiles = {};
  743. Object.keys(state.files).forEach(function (fileName) {
  744. allFiles[fileName] = 1;
  745. state.files[fileName].includes.forEach(function (fileName) {
  746. allFiles[fileName] = 1;
  747. });
  748. });
  749. Object.keys(allFiles).forEach(function (fileName) {
  750. var omt = state.oldmtimes[fileName];
  751. if (omt > 0 && omt === state.mtimes[fileName]) {
  752. return;
  753. }
  754. builder.rebuiltFiles.push(fileName);
  755. });
  756. }).nThen(function (waitFor) {
  757. builder.tests.forEach(function (test) {
  758. test(waitFor(function (output, failure) {
  759. debug(output);
  760. if (failure) {
  761. builder.failure = true;
  762. }
  763. }));
  764. });
  765. }).nThen(function (waitFor) {
  766. if (builder.linters.length === 0) {
  767. return;
  768. }
  769. debug("Checking codestyle");
  770. var sema = Semaphore.create(64);
  771. builder.rebuiltFiles.forEach(function (fileName) {
  772. sema.take(waitFor(function (returnAfter) {
  773. Fs.readFile(fileName, waitFor(function (err, ret) {
  774. if (err) { throw err; }
  775. ret = ret.toString('utf8');
  776. nThen(function (waitFor) {
  777. builder.linters.forEach(function (linter) {
  778. linter(fileName, ret, waitFor(function (out, isErr) {
  779. if (isErr) {
  780. debug(out);
  781. builder.failure = true;
  782. }
  783. }));
  784. });
  785. }).nThen(returnAfter(waitFor()));
  786. }));
  787. }));
  788. });
  789. }).nThen(function (waitFor) {
  790. stage(testStage, builder, waitFor);
  791. }).nThen(function (waitFor) {
  792. if (builder.failure) { return; }
  793. debug("Test " + time() + "ms");
  794. builder.executables.forEach(function (array) {
  795. if (array[1] === array[0]) { return; }
  796. Fs.rename(array[0], array[1], waitFor(function (err) {
  797. // TODO(cjd): It would be better to know in advance whether to expect the file.
  798. if (err && err.code !== 'ENOENT') {
  799. throw err;
  800. }
  801. }));
  802. });
  803. }).nThen(function (waitFor) {
  804. if (builder.failure) { return; }
  805. stage(packStage, builder, waitFor);
  806. }).nThen(function (waitFor) {
  807. if (builder.failure) { return; }
  808. debug("Pack " + time() + "ms");
  809. getMTimes(state.files, state.mtimes, waitFor(function (err, mtimes) {
  810. if (err) { throw err; }
  811. state.mtimes = mtimes;
  812. debug("Get mtimes " + time() + "ms");
  813. }));
  814. }).nThen(function (waitFor) {
  815. if (builder.failure) { return; }
  816. // save state
  817. var stateJson = JSON.stringify(state, null, ' ');
  818. Fs.writeFile(state.buildDir + '/state.json', stateJson, waitFor(function (err) {
  819. if (err) { throw err; }
  820. debug("Save State " + time() + "ms");
  821. }));
  822. }).nThen(function (waitFor) {
  823. if (builder.failure) { return; }
  824. stage(successStage, builder, waitFor);
  825. }).nThen(function (waitFor) {
  826. if (!builder.failure) { return; }
  827. stage(failureStage, builder, waitFor);
  828. }).nThen(function (waitFor) {
  829. stage(completeStage, builder, waitFor);
  830. });
  831. var out = {
  832. build: function (x) { buildStage = x; return out; },
  833. test: function (x) { testStage = x; return out; },
  834. pack: function (x) { packStage = x; return out; },
  835. failure: function (x) { failureStage = x; return out; },
  836. success: function (x) { successStage = x; return out; },
  837. complete: function (x) { completeStage = x; return out; },
  838. };
  839. return out;
  840. };