This repository has been archived by the owner on Nov 3, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
webapp-zip.js
620 lines (546 loc) · 20.9 KB
/
webapp-zip.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
/*global require, FileUtils, exports*/
var utils = require('./utils');
var config;
const { Cc, Ci, Cr, Cu } = require('chrome');
Cu.import('resource://gre/modules/FileUtils.jsm');
function debug(msg) {
//dump('-*- webapps-zip.js ' + msg + '\n');
}
// Header values usefull for zip xpcom component
const PR_RDONLY = 0x01;
const PR_WRONLY = 0x02;
const PR_RDWR = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_APPEND = 0x10;
const PR_TRUNCATE = 0x20;
const PR_SYNC = 0x40;
const PR_EXCL = 0x80;
// Make all timestamps the same so we always generate the same
// output zip file for the same inputs
const DEFAULT_TIME = 0;
const MANIFEST_FILENAME = 'manifest.webapp';
/**
* Add a file to a zip file with the specified time
*/
function addEntryFileWithTime(zip, pathInZip, file, time, compression) {
if (compression === undefined) {
compression = Ci.nsIZipWriter.COMPRESSION_BEST;
}
let fis = Cc['@mozilla.org/network/file-input-stream;1'].
createInstance(Ci.nsIFileInputStream);
fis.init(file, -1, -1, 0);
zip.addEntryStream(
pathInZip, time, compression, fis, false);
fis.close();
}
/**
* Add a string to a zip file with the specified time
*/
function addEntryStringWithTime(zip, pathInZip, data, time, compression) {
if (compression === undefined) {
compression = Ci.nsIZipWriter.COMPRESSION_BEST;
}
let converter = Cc['@mozilla.org/intl/scriptableunicodeconverter']
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = 'UTF-8';
let sis = converter.convertToInputStream(data);
zip.addEntryStream(
pathInZip, time, compression, sis, false);
sis.close();
}
function getCompression(pathInZip, webapp) {
if (webapp.metaData && webapp.metaData.external === false &&
webapp.metaData.zip && webapp.metaData.zip.mmap_files &&
webapp.metaData.zip.mmap_files.indexOf(pathInZip) !== -1) {
return Ci.nsIZipWriter.COMPRESSION_NONE;
} else {
// Don't store some files compressed since that's not giving us any
// benefit but costs cpu when reading from the zip.
var ext = pathInZip.split('.').reverse()[0].toLowerCase();
return (['gif', 'jpg', 'jpeg', 'png',
'ogg', 'opus'].indexOf(ext) !== -1) ?
Ci.nsIZipWriter.COMPRESSION_NONE :
Ci.nsIZipWriter.COMPRESSION_BEST;
}
}
function exclude(path, options, appPath) {
var firstDir = path.substr(appPath.length+1).split(/[\\/]/)[0];
var isShared = firstDir === 'shared';
var isTest = firstDir === 'test';
var file = utils.getFile(path);
// Ignore l10n files if they have been inlined or concatenated
if ( options.GAIA_CONCAT_LOCALES === '1' &&
(file.leafName === 'locales' || file.leafName === 'locales.ini' ||
file.parent.leafName === 'locales')) {
return true;
}
// Ignore concatenated l10n files if options.GAIA_CONCAT_LOCALES
// is not set
if ((file.leafName === 'locales-obj' ||
file.parent.leafName === 'locales-obj') &&
options.GAIA_CONCAT_LOCALES !== '1') {
return true;
}
// Ignore files from /shared directory (these files were created by
// Makefile code). Also ignore files in the /test directory.
if (isShared || isTest) {
return true;
}
return false;
}
/**
* Add a file or a directory, recursively, to a zip file
*
* @param {nsIZipWriter} zip zip xpcom instance.
* @param {String} pathInZip relative path to use in zip.
* @param {nsIFile} file file xpcom to add.
*/
function addToZip(zip, pathInZip, file, compression) {
let suffix = '@' + config.GAIA_DEV_PIXELS_PER_PX + 'x';
if (file.isHidden()) {
return;
}
// If config.GAIA_DEV_PIXELS_PER_PX is not 1 and the file is a bitmap let's
// check if there is a bigger version in the directory. If so let's ignore the
// file in order to use the bigger version later.
let isBitmap = /\.(png|gif|jpg)$/.test(file.path);
if (isBitmap) {
let matchResult = /@([0-9]+\.?[0-9]*)x/.exec(file.path);
if ((config.GAIA_DEV_PIXELS_PER_PX === '1' && matchResult) ||
(matchResult && matchResult[1] !== config.GAIA_DEV_PIXELS_PER_PX)) {
return;
}
if (config.GAIA_DEV_PIXELS_PER_PX !== '1') {
if (matchResult && matchResult[1] === config.GAIA_DEV_PIXELS_PER_PX) {
// Save the hidpi file to the zip, strip the name to be more generic.
pathInZip = pathInZip.replace(suffix, '');
} else {
// Check if there a hidpi file. If yes, let's ignore this bitmap since
// it will be loaded later (or it has already been loaded, depending on
// how the OS organize files.
let hqfile = new FileUtils.File(
file.path.replace(/(\.[a-z]+$)/, suffix + '$1'));
if (hqfile.exists()) {
return;
}
}
}
}
if (utils.isSubjectToBranding(file.path)) {
file.append((config.OFFICIAL == 1) ? 'official' : 'unofficial');
}
if (!file.exists()) {
throw new Error('Can\'t add inexistent file to zip : ' + file.path);
}
// nsIZipWriter should not receive any path starting with `/`,
// it would put files in a folder with empty name...
pathInZip = pathInZip.replace(/^\/+/, '');
// Case 1/ Regular file
if (file.isFile()) {
try {
debug(' +file to zip ' + pathInZip);
if (/\.html$/.test(file.leafName)) {
// this file might have been pre-translated for the default locale
let l10nFile = file.parent.clone();
l10nFile.append(file.leafName + '.' + config.GAIA_DEFAULT_LOCALE);
if (l10nFile.exists()) {
addEntryFileWithTime(zip, pathInZip, l10nFile, DEFAULT_TIME,
compression);
return;
}
}
let re = new RegExp('\\.html\\.' + config.GAIA_DEFAULT_LOCALE);
if (!zip.hasEntry(pathInZip) && !re.test(file.leafName)) {
addEntryFileWithTime(zip, pathInZip, file, DEFAULT_TIME, compression);
}
} catch (e) {
throw new Error('Unable to add following file in zip: ' +
file.path + '\n' + e);
}
}
// Case 2/ Directory
else if (file.isDirectory()) {
debug(' +directory to zip ' + pathInZip);
if (!zip.hasEntry(pathInZip)) {
zip.addEntryDirectory(pathInZip, DEFAULT_TIME, false);
}
}
}
/**
* Copy a "Building Block" (i.e. shared style resource)
*
* @param {nsIZipWriter} zip zip xpcom instance.
* @param {String} blockName name of the building block to copy.
* @param {String} dirName name of the shared directory to use.
*/
function copyBuildingBlock(zip, blockName, dirName, webapp) {
let dirPath = '/shared/' + dirName + '/';
// Compute the nsIFile for this shared style
let styleFolder = utils.getGaia(config).sharedFolder.clone();
styleFolder.append(dirName);
let cssFile = styleFolder.clone();
if (!styleFolder.exists()) {
throw new Error('Using inexistent shared style: ' + blockName);
}
cssFile.append(blockName + '.css');
var pathInZip = dirPath + blockName + '.css';
var compression = getCompression(pathInZip, webapp);
addToZip(zip, pathInZip, cssFile, compression);
// Copy everything but index.html and any other HTML page into the
// style/<block> folder.
let subFolder = styleFolder.clone();
subFolder.append(blockName);
utils.ls(subFolder, true).forEach(function(file) {
let relativePath = file.getRelativeDescriptor(styleFolder);
// Ignore HTML files at style root folder
if (relativePath.match(/^[^\/]+\.html$/)) {
return;
}
// Do not process directory as `addToZip` will add files recursively
if (file.isDirectory()) {
return;
}
addToZip(zip, dirPath + relativePath, file);
});
}
function customizeFiles(zip, src, dest, webapp) {
// Add customize file to the zip
var distDir = utils.getGaia(config).distributionDir;
let files = utils.ls(utils.getFile(distDir, src));
files.forEach(function(file) {
let filename = dest + file.leafName;
if (zip.hasEntry(filename)) {
zip.removeEntry(filename, false);
}
addEntryFileWithTime(zip, filename, file, DEFAULT_TIME,
getCompression(filename, webapp));
});
}
function getResource(distDir, path, resources, json, key) {
if (path) {
var file = utils.getFile(distDir, path);
if (!file.exists()) {
throw new Error('Invalid single variant configuration: ' +
file.path + ' not found');
}
resources.push(file);
json[key] = '/resources/' + file.leafName;
}
}
function getSingleVariantResources(conf) {
var distDir = utils.getGaia(config).distributionDir;
conf = utils.getJSON(conf);
let output = {};
let resources = [];
conf['operators'].forEach(function(operator) {
let object = {};
getResource(distDir, operator['wallpaper'], resources, object, 'wallpaper');
getResource(distDir, operator['default_contacts'],
resources, object, 'default_contacts');
getResource(distDir, operator['support_contacts'],
resources, object, 'support_contacts');
let ringtone = operator['ringtone'];
if (ringtone) {
let ringtoneName = ringtone['name'];
if (!ringtoneName) {
throw new Error('Missing name for ringtone in single variant conf.');
}
getResource(distDir, ringtone['path'], resources, object, 'ringtone');
if (!object.ringtone) {
throw new Error('Missing path for ringtone in single variant conf.');
}
// Generate ringtone JSON
let uuidGenerator = Cc['@mozilla.org/uuid-generator;1'].
createInstance(Ci.nsIUUIDGenerator);
let ringtoneObj = { filename: uuidGenerator.generateUUID().toString() +
'.json',
content: { uri: object['ringtone'],
name: ringtoneName }};
resources.push(ringtoneObj);
object['ringtone'] = '/resources/' + ringtoneObj.filename;
}
operator['mcc-mnc'].forEach(function(mcc) {
if (Object.keys(object).length !== 0) {
output[mcc] = object;
}
});
});
return {'conf': output, 'files': resources};
}
function execute(options) {
config = options;
var gaia = utils.getGaia(config);
var localesFile = utils.resolve(config.LOCALES_FILE,
config.GAIA_DIR);
if (!localesFile.exists()) {
throw new Error('LOCALES_FILE doesn\'t exists: ' + localesFile.path);
}
let webappsTargetDir = Cc['@mozilla.org/file/local;1']
.createInstance(Ci.nsILocalFile);
webappsTargetDir.initWithPath(config.PROFILE_DIR);
// Create profile folder if doesn't exists
utils.ensureFolderExists(webappsTargetDir);
// Create webapps folder if doesn't exists
webappsTargetDir.append('webapps');
utils.ensureFolderExists(webappsTargetDir);
gaia.webapps.forEach(function(webapp) {
// If config.BUILD_APP_NAME isn't `*`, we only accept one webapp
if (config.BUILD_APP_NAME != '*' &&
webapp.sourceDirectoryName != config.BUILD_APP_NAME) {
return;
}
// Zip generation is not needed for external apps, aaplication data
// is copied to profile webapps folder in webapp-manifests.js
if (utils.isExternalApp(webapp)) {
return;
}
// Compute webapp folder name in profile
let webappTargetDir = webappsTargetDir.clone();
webappTargetDir.append(webapp.domain);
utils.ensureFolderExists(webappTargetDir);
let zip = Cc['@mozilla.org/zipwriter;1'].createInstance(Ci.nsIZipWriter);
let mode = PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE;
let zipFile = webappTargetDir.clone();
zipFile.append('application.zip');
zip.open(zipFile, mode);
// Add webapp folder to the zip
debug('# Create zip for: ' + webapp.domain);
let files = utils.ls(webapp.buildDirectoryFile, true);
files.forEach(function(file) {
if (!exclude(file.path, options, webapp.buildDirectoryFile.path)) {
var pathInZip = file.path.substr(
webapp.buildDirectoryFile.path.length + 1);
var compression = getCompression(pathInZip, webapp);
pathInZip = pathInZip.replace(/\\/g, '/');
addToZip(zip, pathInZip, file, compression);
}
});
if (webapp.sourceDirectoryName === 'homescreen' && gaia.distributionDir) {
let customization = utils.getFile(gaia.distributionDir,
'temp', 'apps', 'conf', 'singlevariantconf.json');
if (customization.exists()) {
addToZip(zip, 'js/singlevariantconf.json', customization);
}
}
if (webapp.sourceDirectoryName === 'communications' &&
gaia.distributionDir &&
utils.getFile(gaia.distributionDir).exists()) {
let conf = utils.getFile(gaia.distributionDir, 'variant.json');
if (conf.exists()) {
let resources = getSingleVariantResources(conf);
addEntryStringWithTime(zip, 'resources/customization.json',
JSON.stringify(resources.conf), DEFAULT_TIME);
resources.files.forEach(function(file) {
if (file instanceof Ci.nsILocalFile) {
let filename = 'resources/' + file.leafName;
if (zip.hasEntry(filename)) {
zip.removeEntry(filename, false);
}
var compression = getCompression(filename, webapp);
addEntryFileWithTime(zip, filename, file, DEFAULT_TIME,
compression);
} else {
let filename = 'resources/' + file.filename;
if (zip.hasEntry(filename)) {
zip.removeEntry(filename, false);
}
addEntryStringWithTime(zip, filename, JSON.stringify(file.content),
DEFAULT_TIME);
}
});
} else {
utils.log(conf.path + ' not found. Single variant resources will not' +
' be added.\n');
}
}
// Put shared files, but copy only files actually used by the webapp.
// We search for shared file usage by parsing webapp source code.
let EXTENSIONS_WHITELIST = ['html'];
let SHARED_USAGE =
/<(?:script|link).+=['"]\.?\.?\/?shared\/([^\/]+)\/([^''\s]+)("|')/g;
let used = {
js: [], // List of JS file paths to copy
locales: [], // List of locale names to copy
resources: [], // List of resources to copy
styles: [], // List of stable style names to copy
unstable_styles: [] // List of unstable style names to copy
};
function sortResource(kind, path) {
switch (kind) {
case 'js':
if (used.js.indexOf(path) == -1) {
used.js.push(path);
}
break;
case 'locales':
if (config.GAIA_INLINE_LOCALES !== '1') {
let localeName = path.substr(0, path.lastIndexOf('.'));
if (used.locales.indexOf(localeName) == -1) {
used.locales.push(localeName);
}
}
break;
case 'resources':
if (used.resources.indexOf(path) == -1) {
used.resources.push(path);
}
break;
case 'style':
let styleName = path.substr(0, path.lastIndexOf('.'));
if (used.styles.indexOf(styleName) == -1) {
used.styles.push(styleName);
}
break;
case 'style_unstable':
let unstableStyleName = path.substr(0, path.lastIndexOf('.'));
if (used.unstable_styles.indexOf(unstableStyleName) == -1) {
used.unstable_styles.push(unstableStyleName);
}
break;
}
}
// Scan the files
let files = utils.ls(webapp.buildDirectoryFile, true);
files.filter(function(file) {
// Process only files that may require a shared file
let extension = utils.getExtension(file.leafName);
return file.isFile() && EXTENSIONS_WHITELIST.indexOf(extension) != -1;
}).
forEach(function(file) {
// Grep files to find shared/* usages
let content = utils.getFileContent(file);
while ((matches = SHARED_USAGE.exec(content)) !== null) {
let kind = matches[1]; // js | locales | resources | style
let path = matches[2];
sortResource(kind, path);
}
});
if (gaia.l10nManager) {
// Only localize app manifest file if we inlined properties files.
var inlineOrConcat = (config.GAIA_INLINE_LOCALES === '1' ||
config.GAIA_CONCAT_LOCALES === '1');
gaia.l10nManager.localize(files, zip, webapp, inlineOrConcat);
}
// Look for gaia_shared.json in case app uses resources not specified
// in HTML
let sharedDataFile = webapp.buildDirectoryFile.clone();
sharedDataFile.append('gaia_shared.json');
if (sharedDataFile.exists()) {
let sharedData = JSON.parse(utils.getFileContent(sharedDataFile));
Object.keys(sharedData).forEach(function(kind) {
sharedData[kind].forEach(function(path) {
sortResource(kind, path);
});
});
}
used.js.forEach(function(path) {
// Compute the nsIFile for this shared JS file
let file = gaia.sharedFolder.clone();
file.append('js');
path.split('/').forEach(function(segment) {
file.append(segment);
});
if (!file.exists()) {
throw new Error('Using inexistent shared JS file: ' + path + ' from: ' +
webapp.domain);
}
var pathInZip = '/shared/js/' + path;
var compression = getCompression(pathInZip, webapp);
addToZip(zip, pathInZip, file, compression);
});
used.locales.forEach(function(name) {
// Compute the nsIFile for this shared locale
let localeFolder = gaia.sharedFolder.clone();
localeFolder.append('locales');
let ini = localeFolder.clone();
localeFolder.append(name);
if (!localeFolder.exists()) {
throw new Error('Using inexistent shared locale: ' + name + ' from: ' +
webapp.domain);
}
ini.append(name + '.ini');
if (!ini.exists()) {
throw new Error(name + ' locale doesn`t have `.ini` file.');
}
// And the locale folder itself
addToZip(zip, 'shared/locales/' + name, localeFolder);
// Add the .ini file
var pathInZip = 'shared/locales/' + name + '.ini';
var compression = getCompression(pathInZip, webapp);
if (!gaia.l10nManager) {
addToZip(zip, pathInZip, ini, compression);
} else {
gaia.l10nManager.localizeIni(zip, ini, webapp, pathInZip, compression);
}
utils.ls(localeFolder, true).forEach(function(fileInSharedLocales) {
var relativePath =
fileInSharedLocales.path.substr(config.GAIA_DIR.length);
var compression = getCompression(relativePath, webapp);
addToZip(zip, relativePath, fileInSharedLocales, compression);
});
});
used.resources.forEach(function(path) {
// Compute the nsIFile for this shared resource file
let file = gaia.sharedFolder.clone();
file.append('resources');
path.split('/').forEach(function(segment) {
file.append(segment);
if (utils.isSubjectToBranding(file.path)) {
file.append((config.OFFICIAL == 1) ? 'official' : 'unofficial');
}
});
if (!file.exists()) {
throw new Error('Using inexistent shared resource: ' + path +
' from: ' + webapp.domain + '\n');
}
if (path === 'languages.json') {
var pathInZip = 'shared/resources/languages.json';
return addToZip(zip, pathInZip, localesFile,
getCompression(pathInZip, webapp));
}
// Add not only file itself but all its hidpi-suffixed versions.
let fileNameRegexp = new RegExp(
'^' + file.leafName.replace(/(\.[a-z]+$)/, '(@.*x)?\\$1') + '$');
utils.ls(file.parent, false).forEach(function(listFile) {
if (fileNameRegexp.test(listFile.leafName)) {
var pathInZip = '/shared/resources/' + path;
addToZip(zip, pathInZip, listFile, getCompression(pathInZip, webapp));
}
});
if (file.isDirectory()) {
utils.ls(file, true).forEach(function(fileInResources) {
var pathInZip = 'shared' +
fileInResources.path.substr(gaia.sharedFolder.path.length);
addToZip(zip, pathInZip, fileInResources,
getCompression(pathInZip, webapp));
})
}
if (path === 'media/ringtones/' && gaia.distributionDir &&
utils.getFile(gaia.distributionDir, 'ringtones').exists()) {
customizeFiles(zip, 'ringtones', 'shared/resources/media/ringtones/',
webapp);
}
});
used.styles.forEach(function(name) {
try {
copyBuildingBlock(zip, name, 'style', webapp);
} catch (e) {
throw new Error(e + ' from: ' + webapp.domain);
}
});
used.unstable_styles.forEach(function(name) {
try {
copyBuildingBlock(zip, name, 'style_unstable', webapp);
} catch (e) {
throw new Error(e + ' from: ' + webapp.domain);
}
});
if (zip.alignStoredFiles) {
zip.alignStoredFiles(4096);
}
zip.close();
});
}
exports.execute = execute;
exports.addEntryFileWithTime = addEntryFileWithTime;
exports.addEntryStringWithTime = addEntryStringWithTime;