UNPKG

17.6 kBJavaScriptView Raw
1/**
2 * @overview
3 * The base class for platform specific build commands. This ensures some
4 * commonality between build commands so that hooks can consistently
5 * access build properties.
6 *
7 * @copyright
8 * Copyright (c) 2009-2015 by Appcelerator, Inc. All Rights Reserved.
9 *
10 * @license
11 * Licensed under the terms of the Apache Public License
12 * Please see the LICENSE included with this distribution for details.
13 */
14'use strict';
15
16const
17 appc = require('node-appc'),
18 crypto = require('crypto'),
19 fs = require('fs-extra'),
20 path = require('path'),
21 ti = require('./titanium'),
22 i18n = appc.i18n(__dirname),
23 __ = i18n.__,
24 __n = i18n.__n;
25
26// shim String.prototype.normalize()
27require('unorm');
28
29/**
30 * The base class for platform specific build commands. This ensures some
31 * commonality between build commands so that hooks can consistently
32 * access build properties.
33 *
34 * General usage is to extend the Builder class and override the config(),
35 * validate(), and run() methods:
36 *
37 * var Builder = require('node-titanium-sdk/lib/builder');
38 * var util = require('util');
39 *
40 * function SomePlatformBuilder() {
41 * Builder.apply(this, arguments);
42 * }
43 *
44 * util.inherits(SomePlatformBuilder, Builder);
45 *
46 * SomePlatformBuilder.prototype.config = function config(logger, config, cli) {
47 * Builder.prototype.config.apply(this, arguments);
48 * // TODO
49 * };
50 *
51 * SomePlatformBuilder.prototype.validate = function validate(logger, config, cli) {
52 * // TODO
53 * };
54 *
55 * SomePlatformBuilder.prototype.run = function run(logger, config, cli, finished) {
56 * Builder.prototype.run.apply(this, arguments);
57 * // TODO
58 * finished();
59 * };
60 *
61 * @module lib/builder
62 */
63
64module.exports = Builder;
65
66/**
67 * Constructs the build state. This needs to be explicitly called from the
68 * derived builder's constructor.
69 *
70 * @class
71 * @classdesc Base class for all build states.
72 * @constructor
73 *
74 * @param {Module} buildModule The "module" variable from the build command file
75 */
76function Builder(buildModule) {
77 this.titaniumSdkPath = (function scan(dir) {
78 const file = path.join(dir, 'manifest.json');
79 if (fs.existsSync(file)) {
80 return dir;
81 }
82 dir = path.dirname(dir);
83 return dir !== '/' && scan(dir);
84 }(__dirname));
85
86 this.titaniumSdkName = path.basename(this.titaniumSdkPath);
87
88 this.titaniumSdkVersion = ti.manifest.version;
89
90 this.platformPath = (function scan(dir) {
91 const file = path.join(dir, 'package.json');
92 if (fs.existsSync(file)) {
93 return dir;
94 }
95 dir = path.dirname(dir);
96 return dir !== '/' && scan(dir);
97 }(path.dirname(buildModule.filename)));
98
99 this.platformName = path.basename(this.platformPath);
100
101 this.globalModulesPath = path.join(this.titaniumSdkPath, '..', '..', '..', 'modules');
102
103 this.packageJson = require(path.join(this.platformPath, 'package.json'));
104
105 this.conf = {};
106
107 this.buildDirFiles = {};
108}
109
110/**
111 * Defines common variables prior to running the build's config(). This super
112 * function should be called prior to the platform-specific build command's config().
113 *
114 * @param {Object} logger - The logger instance
115 * @param {Object} config - The CLI config
116 * @param {Object} cli - The CLI instance
117 */
118Builder.prototype.config = function config(logger, config, cli) {
119 // note: this function must be sync!
120 this.logger = logger;
121 this.config = config;
122 this.cli = cli;
123 this.symlinkFilesOnCopy = false;
124 this.ignoreDirs = new RegExp(config.get('cli.ignoreDirs'));
125 this.ignoreFiles = new RegExp(config.get('cli.ignoreFiles'));
126};
127
128/**
129 * Validation stub function. Meant to be overwritten.
130 *
131 * @param {Object} logger - The logger instance
132 * @param {Object} config - The CLI config
133 * @param {Object} cli - The CLI instance
134 */
135Builder.prototype.validate = function validate(logger, config, cli) {
136 // note: this function must be sync!
137
138 this.tiapp = cli.tiapp;
139 this.timodule = cli.timodule;
140 this.projectDir = cli.argv['project-dir'];
141 this.buildDir = path.join(this.projectDir, 'build', this.platformName);
142
143 this.defaultIcons = [
144 path.join(this.projectDir, 'DefaultIcon-' + this.platformName + '.png'),
145 path.join(this.projectDir, 'DefaultIcon.png')
146 ];
147};
148
149/**
150 * Defines common variables prior to running the build. This super function
151 * should be called prior to the platform-specific build command's run().
152 *
153 * @param {Object} _logger - The logger instance
154 * @param {Object} _config - The CLI config
155 * @param {Object} _cli - The CLI instance
156 * @param {Function} _finished - A function to call after the function finishes
157 */
158Builder.prototype.run = function run(_logger, _config, _cli, _finished) {
159 // note: this function must be sync!
160
161 var buildDirFiles = this.buildDirFiles = {};
162
163 // walk the entire build dir and build a map of all files
164 if (fs.existsSync(this.buildDir)) {
165 this.logger.trace(__('Snapshotting build directory'));
166 (function walk(dir) {
167 fs.readdirSync(dir).forEach(function (name) {
168 var file = path.join(dir, name).normalize();
169 try {
170 var stat = fs.lstatSync(file);
171 if (stat.isDirectory()) {
172 walk(file);
173 } else {
174 buildDirFiles[file] = stat;
175 }
176 } catch (ex) {
177 buildDirFiles[file] = true;
178 }
179 });
180 }(this.buildDir));
181 }
182};
183
184/**
185 * Removes a file from the buildDirFiles map.
186 *
187 * @param {String} file - The file to unmark.
188 */
189Builder.prototype.unmarkBuildDirFile = function unmarkBuildDirFile(file) {
190 delete this.buildDirFiles[file.normalize()];
191};
192
193/**
194 * Removes all paths from the buildDirFiles map that start with the specified path.
195 *
196 * @param {String} dir - The path prefix to unmark files.
197 */
198Builder.prototype.unmarkBuildDirFiles = function unmarkBuildDirFiles(dir) {
199 if (/\*$/.test(dir)) {
200 dir = dir.substring(0, dir.length - 1);
201 } else if (!/\/$/.test(dir)) {
202 dir += '/';
203 }
204 dir = dir.normalize();
205 Object.keys(this.buildDirFiles).forEach(function (file) {
206 if (file.indexOf(dir) === 0) {
207 delete this.buildDirFiles[file];
208 }
209 }, this);
210};
211
212/**
213 * Copies or symlinks a file to the specified destination.
214 *
215 * @param {String} src - The file to copy.
216 * @param {String} dest - The destination of the file.
217 * @param {Object} [opts] - An object containing various options.
218 * @param {Boolean} [opts.forceCopy] - When true, forces the file to be copied and not symlinked.
219 * @param {Boolean} [opts.forceSymlink] - When true, ignores `opts.contents` and `opts.forceCopy` and symlinks the `src` to the `dest`.
220 * @param {Buffer|String} [opts.contents] - The contents to write to the file instead of reading the specified source file.
221 */
222Builder.prototype.copyFileSync = function copyFileSync(src, dest, opts) {
223 var parent = path.dirname(dest),
224 exists = fs.existsSync(dest);
225
226 opts && typeof opts === 'object' || (opts = {});
227
228 fs.ensureDirSync(parent);
229
230 if (!opts.forceSymlink && (opts.forceCopy || !this.symlinkFilesOnCopy || opts.contents)) {
231 if (exists) {
232 this.logger.debug(__('Overwriting %s => %s', src.cyan, dest.cyan));
233 fs.unlinkSync(dest);
234 } else {
235 this.logger.debug(__('Copying %s => %s', src.cyan, dest.cyan));
236 }
237 fs.writeFileSync(dest, opts.contents || fs.readFileSync(src));
238 return true;
239
240 } else if (!exists || (fs.lstatSync(dest).isSymbolicLink() && fs.realpathSync(dest) !== src)) {
241 exists && fs.unlinkSync(dest);
242 this.logger.debug(__('Symlinking %s => %s', src.cyan, dest.cyan));
243 fs.symlinkSync(src, dest);
244 return true;
245 }
246};
247
248/**
249 * Copies or symlinks a file to the specified destination.
250 *
251 * @param {String} src - The directory to copy.
252 * @param {String} dest - The destination of the files.
253 * @param {Object} [opts] - An object containing various options.
254 * @param {RegExp} [opts.rootIgnoreDirs] - A regular expression of directories to ignore only in the root directory.
255 * @param {RegExp} [opts.ignoreDirs] - A regular expression of directories to ignore.
256 * @param {RegExp} [opts.ignoreFiles] - A regular expression of files to ignore.
257 * @param {Function} [opts.beforeCopy] - A function called before copying the file. This function can abort the copy or modify the contents being copied.
258 * @param {Boolean} [opts.forceCopy] - When true, forces the file to be copied and not symlinked.
259 * @param {Function} [opts.afterCopy] - A function called with the result of the file being copied.
260 */
261Builder.prototype.copyDirSync = function copyDirSync(src, dest, opts) {
262 if (!fs.existsSync(src)) {
263 return;
264 }
265
266 opts && typeof opts === 'object' || (opts = {});
267
268 (function copy(src, dest, isRootDir) {
269 fs.ensureDirSync(dest);
270
271 fs.readdirSync(src).forEach(function (name) {
272 const srcFile = path.join(src, name);
273 const destFile = path.join(dest, name);
274
275 // skip broken symlinks
276 if (!fs.existsSync(srcFile)) {
277 return;
278 }
279
280 const srcStat = fs.statSync(srcFile);
281 if (srcStat.isDirectory()) {
282 // we are copying a subdirectory
283 if ((isRootDir && opts.rootIgnoreDirs && opts.rootIgnoreDirs.test(name)) || (opts.ignoreDirs && opts.ignoreDirs.test(name))) {
284 // ignoring directory
285 } else {
286 copy.call(this, srcFile, destFile);
287 }
288 return;
289 }
290
291 // we're copying a file, check if we should ignore it
292 if (opts.ignoreFiles && opts.ignoreFiles.test(name)) {
293 return;
294 }
295
296 if (typeof opts.beforeCopy === 'function') {
297 const result = opts.beforeCopy(srcFile, destFile, srcStat);
298 if (result === null) {
299 return; // skip
300 } else if (result !== undefined) {
301 this.logger.debug(__('Writing %s => %s', srcFile.cyan, destFile.cyan));
302 fs.writeFileSync(destFile, result);
303 return;
304 }
305 // fall through and copy the file normally
306 }
307
308 const result = this.copyFileSync(srcFile, destFile, opts);
309 if (typeof opts.afterCopy === 'function') {
310 opts.afterCopy(srcFile, destFile, srcStat, result);
311 }
312 }, this);
313 }).call(this, src, dest, true);
314};
315
316/**
317 * Validates that all required Titanium Modules defined in the tiapp.xml are
318 * installed.
319 *
320 * This function is intended to be called asynchronously from the validate()
321 * implementation. In other words, validate() should return a function that
322 * calls this function.
323 *
324 * Note: This function will forcefully exit the application on error!
325 *
326 * @example
327 * SomePlatformBuilder.prototype.validate = function validate(logger, config, cli) {
328 * Builder.prototype.validate.apply(this, arguments);
329 *
330 * // TODO: synchronous platform specific validation code goes here
331 *
332 * return function (callback) {
333 * // TODO: asynchronous platform specific validation code goes here
334 *
335 * this.validateTiModules(callback);
336 * }.bind(this);
337 * };
338 *
339 * @param {String|Array} platformName - One or more platform names to use when finding Titanium modules
340 * @param {String} deployType - The deployment type (development, test, production)
341 * @param {Function} callback(err) - A function to call after the function finishes
342 */
343Builder.prototype.validateTiModules = function validateTiModules(platformName, deployType, callback) {
344 var moduleSearchPaths = [ this.projectDir ],
345 customSDKPaths = this.config.get('paths.sdks'),
346 customModulePaths = this.config.get('paths.modules');
347
348 function addSearchPath(p) {
349 p = appc.fs.resolvePath(p);
350 if (fs.existsSync(p) && moduleSearchPaths.indexOf(p) === -1) {
351 moduleSearchPaths.push(p);
352 }
353 }
354
355 this.cli.env.os.sdkPaths.forEach(addSearchPath);
356 Array.isArray(customSDKPaths) && customSDKPaths.forEach(addSearchPath);
357 Array.isArray(customModulePaths) && customModulePaths.forEach(addSearchPath);
358
359 appc.timodule.find(this.cli.tiapp.modules, platformName, deployType, ti.manifest, moduleSearchPaths, this.logger, function (modules) {
360 if (modules.missing.length) {
361 this.logger.error(__('Could not find all required Titanium Modules:'));
362 modules.missing.forEach(function (m) {
363 this.logger.error(' id: ' + m.id + '\t version: ' + (m.version || 'latest') + '\t platform: ' + m.platform + '\t deploy-type: ' + m.deployType);
364 }, this);
365 this.logger.log();
366 process.exit(1);
367 }
368
369 if (modules.incompatible.length) {
370 this.logger.error(__('Found incompatible Titanium Modules:'));
371 modules.incompatible.forEach(function (m) {
372 this.logger.error(' id: ' + m.id + '\t version: ' + (m.version || 'latest') + '\t platform: ' + m.platform + '\t min sdk: ' + (m.manifest && m.manifest.minsdk || '?'));
373 }, this);
374 this.logger.log();
375 process.exit(1);
376 }
377
378 if (modules.conflict.length) {
379 this.logger.error(__('Found conflicting Titanium modules:'));
380 modules.conflict.forEach(function (m) {
381 this.logger.error(' ' + __('Titanium module "%s" requested for both Android and CommonJS platforms, but only one may be used at a time.', m.id));
382 }, this);
383 this.logger.log();
384 process.exit(1);
385 }
386
387 callback(null, modules);
388 }.bind(this)); // end timodule.find()
389};
390
391/**
392 * Returns the hexadecimal md5 hash of a string.
393 *
394 * @param {String} str - The string to hash
395 *
396 * @returns {String}
397 */
398Builder.prototype.hash = function hash(str) {
399 return crypto.createHash('md5').update(str || '').digest('hex');
400};
401
402/**
403 * Generates missing app icons based on the DefaultIcon.png.
404 *
405 * @param {Array<Object>} icons - An array of objects describing the icon size to generate and the destination
406 * @param {Function} callback - A function to call after the icons have been generated
407 */
408Builder.prototype.generateAppIcons = function generateAppIcons(icons, callback) {
409 const requiredMissing = icons.filter(icon => icon.required).length;
410 let size = null;
411 var fail = function () {
412 this.logger.error(__('Unable to create missing icons:'));
413 printMissing(this.logger.error);
414 callback(true);
415 }.bind(this);
416
417 function printMissing(logger, all) {
418 icons.forEach(function (icon) {
419 if (all || size === null || icon.width > size.width) {
420 logger(' '
421 + __('%s - size: %sx%s',
422 icon.description,
423 icon.width,
424 icon.height
425 )
426 );
427 }
428 });
429 }
430
431 let iconLabels;
432 if (this.defaultIcons.length > 2) {
433 const labels = this.defaultIcons.map(icon => '"' + path.basename(icon) + '"');
434 const last = labels.pop();
435 iconLabels = labels.join(', ') + ', or ' + last;
436 } else {
437 iconLabels = this.defaultIcons.map(icon => '"' + path.basename(icon) + '"').join(' or ');
438 }
439
440 const defaultIcon = this.defaultIcons.find(icon => fs.existsSync(icon));
441
442 if (!defaultIcon) {
443 if (requiredMissing === 0) {
444 this.logger.warn(__n('There is a missing app icon, but it is not required', 'There are missing app icons, but they are not required', icons.length));
445 this.logger.warn(__('You can either create the missing icons below or create an image named %s in the root of your project', iconLabels));
446 this.logger.warn(__('If the DefaultIcon.png image is present, the build will use it to generate all missing icons'));
447 this.logger.warn(__('It is highly recommended that the DefaultIcon.png be 1024x1024'));
448 printMissing(this.logger.warn);
449 return callback();
450 }
451
452 this.logger.error(__n('There is a missing required app icon', 'There are missing required app icons', icons.length));
453 this.logger.error(__('You must either create the missing icons below or create an image named %s in the root of your project', iconLabels));
454 this.logger.error(__('If the DefaultIcon.png image is present, the build will use it to generate all missing icons'));
455 this.logger.error(__('It is highly recommended that the DefaultIcon.png be 1024x1024'));
456 return fail();
457 }
458
459 const contents = fs.readFileSync(defaultIcon);
460 size = appc.image.pngInfo(contents);
461
462 if (size.width !== size.height) {
463 this.logger.error(__('The %s is %sx%s, however the width and height must be equal', defaultIcon, size.width, size.height));
464 this.logger.error(__('It is highly recommended that the %s be 1024x1024', defaultIcon));
465 return fail();
466 }
467
468 this.logger.debug(__('Found %s (%sx%s)', defaultIcon.cyan, size.width, size.height));
469 this.logger.info(__n(
470 'Missing %s app icon, generating missing icon',
471 'Missing %s app icons, generating missing icons',
472 icons.length
473 ));
474 printMissing(this.logger.info, true);
475
476 const rename = [];
477 let minRequiredSize = null;
478 let minSize = null;
479 for (let i = 0; i < icons.length; i++) {
480 const icon = icons[i];
481 if (icon.required) {
482 if (minRequiredSize === null || icon.width > minRequiredSize) {
483 minRequiredSize = icon.width;
484 }
485 } else if (icon.width > size.width) {
486 // default icon isn't big enough, so we just skip this image
487 this.logger.warn(__('%s (%sx%s) is not large enough to generate missing icon "%s" (%sx%s), skipping', defaultIcon, size.width, size.height, path.basename(icon.file), icon.width, icon.height));
488 icons.splice(i--, 1);
489 continue;
490 }
491 if (minSize === null || icon.width > minSize) {
492 minSize = icon.width;
493 }
494 if (!path.extname(icon.file)) {
495 // the file doesn't have an extension, so we need to temporarily set
496 // one so that the image resizer doesn't blow up
497 rename.push({
498 from: icon.file + '.png',
499 to: icon.file
500 });
501 icon.file += '.png';
502 }
503 }
504
505 if (minRequiredSize !== null && size.width < minRequiredSize) {
506 this.logger.error(__('The %s must be at least %sx%s', defaultIcon, minRequiredSize, minRequiredSize));
507 this.logger.error(__('It is highly recommended that the %s be 1024x1024', defaultIcon));
508 return fail();
509 }
510
511 appc.image.resize(defaultIcon, icons, function (error, _stdout, _stderr) {
512 if (error) {
513 this.logger.error(error);
514 this.logger.log();
515 process.exit(1);
516 }
517
518 rename.forEach(function (file) {
519 fs.renameSync(file.from, file.to);
520 });
521
522 callback();
523 }.bind(this), this.logger);
524};