Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/build
diff options
context:
space:
mode:
Diffstat (limited to 'build')
-rw-r--r--build/BUSYBOX_LICENSE16
-rw-r--r--build/applications-data.js243
-rw-r--r--build/busybox-armv6lbin0 -> 1085140 bytes
-rw-r--r--build/install-gaia.py176
-rw-r--r--build/multilocale.py248
-rw-r--r--build/offline-cache.js159
-rw-r--r--build/optimize-clean.js28
-rwxr-xr-xbuild/otoro-install-busybox.sh17
-rw-r--r--build/payment-prefs.js5
-rw-r--r--build/preferences.js100
-rw-r--r--build/settings.py236
-rw-r--r--build/ua-override-prefs.js102
-rw-r--r--build/utils.js222
-rw-r--r--build/wallpaper.jpgbin0 -> 33533 bytes
-rw-r--r--build/webapp-manifests.js179
-rw-r--r--build/webapp-optimize.js386
-rw-r--r--build/webapp-zip.js293
17 files changed, 2410 insertions, 0 deletions
diff --git a/build/BUSYBOX_LICENSE b/build/BUSYBOX_LICENSE
new file mode 100644
index 0000000..e5b619c
--- /dev/null
+++ b/build/BUSYBOX_LICENSE
@@ -0,0 +1,16 @@
+About BusyBox
+
+BusyBox is licensed under the GNU General Public License version 2
+
+Check busybox' site for more information:
+ http://busybox.net
+ http://busybox.net/license.html
+
+Distributed busybox-armv6l binary was obtained from:
+ http://www.busybox.net/downloads/binaries/1.19.0/busybox-armv6l
+
+You can check this by comparing the sha1 signature of the binary:
+ b7d3edcd61956f4dfcccad7c5ee4d92bcbbcb172
+
+The sources are available at:
+ http://www.busybox.net/downloads/busybox-1.19.0.tar.bz2
diff --git a/build/applications-data.js b/build/applications-data.js
new file mode 100644
index 0000000..69ef24b
--- /dev/null
+++ b/build/applications-data.js
@@ -0,0 +1,243 @@
+'use strict';
+
+const PREFERRED_ICON_SIZE = 60;
+const GAIA_CORE_APP_SRCDIR = 'apps';
+const GAIA_EXTERNAL_APP_SRCDIR = 'external-apps';
+const INSTALL_TIME = 132333986000; // Match this to value in webapp-manifests.js
+
+// Initial Homescreen icon descriptors.
+
+// c.f. the corresponding implementation in the Homescreen app.
+function bestMatchingIcon(preferred_size, manifest, origin) {
+ var icons = manifest.icons;
+ if (!icons) {
+ return undefined;
+ }
+
+ var preferredSize = Number.MAX_VALUE;
+ var max = 0;
+
+ for (var size in icons) {
+ size = parseInt(size, 10);
+ if (size > max)
+ max = size;
+
+ if (size >= PREFERRED_ICON_SIZE && size < preferredSize)
+ preferredSize = size;
+ }
+ // If there is an icon matching the preferred size, we return the result,
+ // if there isn't, we will return the maximum available size.
+ if (preferredSize === Number.MAX_VALUE)
+ preferredSize = max;
+
+ var url = icons[preferredSize];
+ if (!url) {
+ return undefined;
+ }
+
+ // If the icon path is not an absolute URL, prepend the app's origin.
+ if (url.indexOf('data:') == 0 ||
+ url.indexOf('app://') == 0 ||
+ url.indexOf('http://') == 0 ||
+ url.indexOf('https://') == 0)
+ return url;
+
+ return origin + url;
+}
+
+function iconDescriptor(directory, app_name, entry_point) {
+ let origin = gaiaOriginURL(app_name);
+ let manifestURL = gaiaManifestURL(app_name);
+
+ // For external/3rd party apps that don't use the Gaia domain, we have an
+ // 'origin' file that specifies the URL.
+ let dir = getFile(GAIA_DIR, directory, app_name);
+ let originFile = dir.clone();
+ originFile.append("origin");
+ if (originFile.exists()) {
+ origin = getFileContent(originFile).replace(/^\s+|\s+$/, '');
+ if (origin.slice(-1) == "/") {
+ manifestURL = origin + "manifest.webapp";
+ } else {
+ manifestURL = origin + "/manifest.webapp";
+ }
+ }
+
+ let manifestFile = dir.clone();
+ manifestFile.append("manifest.webapp");
+ let manifest = getJSON(manifestFile);
+
+ if (entry_point &&
+ manifest.entry_points &&
+ manifest.entry_points[entry_point]) {
+ manifest = manifest.entry_points[entry_point];
+ }
+ let icon = bestMatchingIcon(PREFERRED_ICON_SIZE, manifest, origin);
+
+ //TODO set localizedName once we know the default locale
+ return {
+ manifestURL: manifestURL,
+ entry_point: entry_point,
+ updateTime: INSTALL_TIME,
+ name: manifest.name,
+ icon: icon
+ };
+}
+
+// zeroth grid page is the dock
+let customize = {"homescreens": [
+ [
+ ["apps", "communications", "dialer"],
+ ["apps", "sms"],
+ ["apps", "communications", "contacts"],
+ ["apps", "browser"]
+ ], [
+ ["apps", "camera"],
+ ["apps", "gallery"],
+ ["apps", "fm"],
+ ["apps", "settings"],
+ ["external-apps", "marketplace"]
+ ], [
+ ["apps", "calendar"],
+ ["apps", "clock"],
+ ["apps", "costcontrol"],
+ ["apps", "email"],
+ ["apps", "music"],
+ ["apps", "video"]
+ ]
+]};
+
+if (DOGFOOD == 1) {
+ customize.homescreens[0].push(["dogfood_apps", "feedback"]);
+}
+
+let init = getFile(GAIA_DIR, 'customize.json');
+if (init.exists()) {
+ customize = getJSON(init);
+}
+
+let content = {
+ search_page: {
+ provider: 'EverythingME',
+ enabled: true
+ },
+
+ grid: customize.homescreens.map(
+ function map_homescreens(applist) {
+ var output = [];
+ for (var i = 0; i < applist.length; i++) {
+ if (applist[i] !== null) {
+ output.push(iconDescriptor.apply(null, applist[i]));
+ }
+ }
+ return output;
+ }
+ )
+};
+
+init = getFile(GAIA_DIR, GAIA_CORE_APP_SRCDIR, 'homescreen', 'js', 'init.json');
+writeContent(init, JSON.stringify(content));
+
+// Apps that should never appear in settings > app permissions
+// bug 830659: We want homescreen to appear in order to remove e.me geolocation permission
+let hidden_apps = [
+ gaiaManifestURL('keyboard'),
+ gaiaManifestURL('wallpaper'),
+ gaiaManifestURL('bluetooth'),
+ gaiaManifestURL('pdfjs')
+];
+
+init = getFile(GAIA_DIR, GAIA_CORE_APP_SRCDIR, 'settings', 'js', 'hiddenapps.js');
+writeContent(init, "var HIDDEN_APPS = " + JSON.stringify(hidden_apps));
+
+// Apps that should never appear as icons in the homescreen grid or dock
+hidden_apps = hidden_apps.concat([
+ gaiaManifestURL('homescreen'),
+ gaiaManifestURL('system')
+]);
+
+init = getFile(GAIA_DIR, GAIA_CORE_APP_SRCDIR, 'homescreen', 'js', 'hiddenapps.js');
+writeContent(init, "var HIDDEN_APPS = " + JSON.stringify(hidden_apps));
+
+// Cost Control
+init = getFile(GAIA_DIR, 'apps', 'costcontrol', 'js', 'config.json');
+
+content = {
+ provider: 'Vivo',
+ enable_on: { 724: [6, 10, 11, 23] }, // { MCC: [ MNC1, MNC2, ...] }
+ is_free: true,
+ is_roaming_free: true,
+ credit: { currency : 'R$' },
+ balance: {
+ destination: '8000',
+ text: 'SALDO',
+ senders: ['1515'],
+ regexp: 'Saldo Recarga: R\\$\\s*([0-9]+)(?:[,\\.]([0-9]+))?'
+ },
+ topup: {
+ destination: '7000',
+ ussd_destination: '*321#',
+ text: '&code',
+ senders: ['1515', '7000'],
+ confirmation_regexp: 'Voce recarregou R\\$\\s*([0-9]+)(?:[,\\.]([0-9]+))?',
+ incorrect_code_regexp: '(Favor enviar|envie novamente|Verifique) o codigo de recarga'
+ },
+ default_low_limit_threshold: 3
+};
+
+writeContent(init, JSON.stringify(content));
+
+// SMS
+init = getFile(GAIA_DIR, 'apps', 'sms', 'js', 'blacklist.json');
+content = ["1515", "7000"];
+writeContent(init, JSON.stringify(content));
+
+// Browser
+init = getFile(GAIA_DIR, 'apps', 'browser', 'js', 'init.json');
+
+content = {
+ "bookmarks": [
+ { "title": "Vivo Busca",
+ "uri": "http://www.google.com.br/m/search?client=ms-hms-tef-br",
+ "iconUri": "data:image/png;base64,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"
+ },
+ { "title": "Serviços e Downloads",
+ "uri": "http://vds.vivo.com.br",
+ "iconUri": "data:image/png;base64,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"
+ },
+ {
+ "title": "Site Vivo",
+ "uri": "http://www.vivo.com.br/conteudosmartphone",
+ "iconUri": "data:image/jpg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAgACADASIAAhEBAxEB/8QAGgAAAgIDAAAAAAAAAAAAAAAABQYBAwQHCP/EAC4QAAEDAwIEBQIHAAAAAAAAAAECAwQFBhEAIQcSMUETFFFhcSKBMkKCkqGxwf/EABkBAAIDAQAAAAAAAAAAAAAAAAMGAAECBP/EACQRAAIBAwQCAgMAAAAAAAAAAAECEQMEEgAFITEiQVFxMmGh/9oADAMBAAIRAxEAPwDrG7rkg25AEiVlx1eQywk/U4f8A7nSXFrXEW4k+ZpMJiFEV+BxYSAfgqyT8gasmQBcPEQpmDnjNuFPIenI2OnwVf3ozOnSKnJ8rDeUhnPIyyyoJKwO59Btn2Gky53F7lncswQMVVVMFiOzPxpko0KVsiqEDORJLcgA9CPnQGbVOJdvtGXUIrE+IjdxTaUr5R6kJwoD3wQNN9lXVCuaEXGR4UlsAuMk52PRST3ToLBRNgywtp59pxDwbcaWvmC9wMehznY++sOi01qlcRZSYIDbHmikIT0AW0lah8cxOpZX9ak6sC2JYKysZImYIPB4jkau4pUbimwKgMBIKiAY7BHXvgjQldRXa/F+WmouLEOWQ6ypXQJUCDj9RP7dTSLHrsC6mpER1C6c49zpmMPAEtE53Gc5x6ZGn29LUpd1QEx56VNvNEqYkN7LaJ649QcDIOxwO4B0jxba4kW2S1RqjGqEXP0guBKvulY5QfcHJ0W82rB/KmXTLIY/kCYkR7BgaNbbgtWl4OEfEKQ3RA6IPo/erJ9PumJeTtRapbstIfUtsBWW1D8pOD22ODo9Zdt1RmsP1+vPDzTpJbjpVlKCeqj2zjYY6DQZUHilUh4cmQ1BQepEhKMfdsc3862HSWpLFMjMzHQ7IbbSlxYJIUoDc7763tW3UzcM5RwAchnAGR98cn9Trm3C7dKIQMhJGJxkmB/PvX//2Q=="
+ }
+ ]
+}
+
+writeContent(init, JSON.stringify(content));
+
+// Support
+init = getFile(GAIA_DIR, 'apps', 'settings', 'resources', 'support.json');
+content = {
+ "onlinesupport": {
+ "href": "http://www.vivo.com.br/portalweb/appmanager/env/web?_nfls=false&_nfpb=true&_pageLabel=vcAtendMovelBook&WT.ac=portal.atendimento.movel",
+ "title": "Vivo"
+ },
+ "callsupport": [
+ {
+ "href": "tel:*8486",
+ "title": "*8486"
+ },
+ {
+ "href": "tel:1058",
+ "title": "1058"
+ }
+ ]
+}
+writeContent(init, JSON.stringify(content));
+
+// ICC / STK
+init = getFile(GAIA_DIR, 'apps', 'settings', 'resources', 'icc.json');
+content = {
+ "defaultURL": "http://www.mozilla.org/en-US/firefoxos/"
+}
+writeContent(init, JSON.stringify(content));
diff --git a/build/busybox-armv6l b/build/busybox-armv6l
new file mode 100644
index 0000000..5f34bbc
--- /dev/null
+++ b/build/busybox-armv6l
Binary files differ
diff --git a/build/install-gaia.py b/build/install-gaia.py
new file mode 100644
index 0000000..9ff2ae0
--- /dev/null
+++ b/build/install-gaia.py
@@ -0,0 +1,176 @@
+"""Usage: python %prog [ADB_PATH] [REMOTE_PATH]
+
+ADB_PATH is the path to the |adb| executable we should run.
+REMOTE_PATH is the path to push the gaia webapps directory to.
+
+Used by |make install-gaia| to push files to a device. You shouldn't run
+this file directly.
+
+"""
+
+import sys
+import os
+import hashlib
+import subprocess
+from tempfile import mkstemp
+
+def compute_local_hash(filename, hashes):
+ h = hashlib.sha1()
+ with open(filename,'rb') as f:
+ for chunk in iter(lambda: f.read(256 * h.block_size), b''):
+ h.update(chunk)
+ hashes[filename] = h.hexdigest()
+
+def compute_local_hashes_in_dir(dir, hashes):
+ def visit(arg, dirname, names):
+ for filename in [os.path.join(dirname, name) for name in names]:
+ if not os.path.isfile(filename):
+ continue
+ compute_local_hash(filename, hashes)
+
+ os.path.walk(dir, visit, None)
+
+def compute_local_hashes():
+ hashes = {}
+ compute_local_hashes_in_dir('webapps', hashes)
+ compute_local_hash('user.js', hashes)
+ return hashes
+
+def adb_push(local, remote):
+ global adb_cmd
+ subprocess.check_call([adb_cmd, 'push', local, remote])
+
+def adb_shell(cmd, ignore_error=False):
+ global adb_cmd
+
+ # Output the return code so we can check whether the command executed
+ # successfully.
+ new_cmd = cmd + '; echo "RETURN CODE: $?"'
+
+ # universal_newlines=True because adb shell returns CRLF separators.
+ proc = subprocess.Popen([adb_cmd, 'shell', new_cmd],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True)
+ (stdout, stderr) = proc.communicate()
+ if stderr.strip():
+ raise Exception('adb shell "%s" returned the following unexpected error: "%s"' %
+ (cmd, stderr.strip()))
+ if proc.returncode != 0:
+ raise Exception('adb shell "%s" exited with error %d' % (cmd, proc.returncode))
+
+ split = [line for line in stdout.split('\n') if line.strip()]
+ if not ignore_error and not split[-1].startswith('RETURN CODE: 0'):
+ raise Exception('adb shell "%s" did not complete successfully. Output:\n%s' % (cmd, stdout))
+
+ # Don't return the "RETURN CODE: 0" line!
+ return split[0:-1]
+
+
+def compute_remote_hashes():
+ hashes = {}
+ adb_out = adb_shell('cd /data/local && find . -type f | xargs sha1sum')
+ for (hash, filename) in [line.split() for line in adb_out]:
+ # Strip off './' from the filename.
+ if filename.startswith('./'):
+ filename = filename[2:]
+ else:
+ raise Exception('Unexpected filename %s' % filename)
+
+ hashes[filename] = hash
+ return hashes
+
+INDEXED_DB_FOLDER = 'indexedDB/'
+
+def remove_from_remote(local_hashes, remote_hashes):
+ """Remove any files from the remote device which don't appear in
+ local_hashes.
+
+ """
+
+ # Keep indexedDB content
+ to_keep = set()
+ for path in remote_hashes:
+ if path[:len(INDEXED_DB_FOLDER)] == INDEXED_DB_FOLDER:
+ to_keep.add(path)
+
+ to_remove = list(set(remote_hashes.keys()) - set(local_hashes.keys()) - to_keep)
+
+ if not to_remove:
+ return
+
+ print 'Removing from device:\n%s\n' % '\n'.join(to_remove)
+ # Chunk to_remove into 25 files at a time so we don't send too much over
+ # adb_shell at once.
+ for files in [to_remove[pos:pos + 25] for pos in xrange(0, len(to_remove), 25)]:
+ adb_shell('cd /data/local && rm -f %s' % ' '.join(files))
+
+def push_to_remote(local_hashes, remote_hashes):
+ global adb_cmd
+
+ to_push = set()
+ for (k, v) in local_hashes.items():
+ if k not in remote_hashes or remote_hashes[k] != local_hashes[k]:
+ to_push.add(k)
+
+ if not to_push:
+ return
+
+ print 'Pushing to device:\n%s' % '\n'.join(list(to_push))
+
+ tmpfile, tmpfilename = mkstemp()
+ try:
+ subprocess.check_call(['tar', '-czf', tmpfilename] + list(to_push))
+ adb_push(tmpfilename, '/data/local')
+ basename = os.path.basename(tmpfilename)
+ adb_shell('cd /data/local && tar -xzf %s && rm %s' % (basename, basename))
+ finally:
+ os.remove(tmpfilename)
+
+def install_gaia_fast():
+ os.chdir('profile')
+ try:
+ local_hashes = compute_local_hashes()
+ remote_hashes = compute_remote_hashes()
+ remove_from_remote(local_hashes, remote_hashes)
+ push_to_remote(local_hashes, remote_hashes)
+ finally:
+ os.chdir('..')
+
+def install_gaia_slow():
+ global adb_cmd, remote_path
+ webapps_path = remote_path + '/webapps'
+ adb_shell("rm -r " + webapps_path, ignore_error=True)
+ adb_shell("rm /data/local/user.js", ignore_error=True)
+ adb_push('profile/webapps', webapps_path)
+ adb_push('profile/user.js', '/data/local')
+
+def install_gaia():
+ global remote_path
+ try:
+ if remote_path == "/system/b2g":
+ # XXX Force slow method until we fix the fast one to support
+ # files in both /system/b2g and /data/local
+ # install_gaia_fast()
+ install_gaia_slow()
+ else:
+ install_gaia_fast()
+ except:
+ # If anything goes wrong, fall back to the slow method.
+ install_gaia_slow()
+
+if __name__ == '__main__':
+ if len(sys.argv) > 3:
+ print >>sys.stderr, 'Too many arguments!\n'
+ print >>sys.stderr, \
+ 'Usage: python %s [ADB_PATH] [REMOTE_PATH]\n' % __FILE__
+ sys.exit(1)
+
+ adb_cmd = 'adb'
+ remote_path = '/data/local/webapps'
+ if len(sys.argv) >= 2:
+ adb_cmd = sys.argv[1]
+ if len(sys.argv) >= 3:
+ remote_path = sys.argv[2]
+
+ install_gaia()
diff --git a/build/multilocale.py b/build/multilocale.py
new file mode 100644
index 0000000..60387b3
--- /dev/null
+++ b/build/multilocale.py
@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+
+import os
+import shutil
+import fnmatch
+import json
+import re
+from optparse import OptionParser
+import logging
+
+section_line = re.compile('\[(?P<section>.*)\]')
+import_line = re.compile('@import url\((?P<filename>.*)\)')
+property_line = re.compile('(?P<id>.*)\s*[:=]\s*(?P<value>.*)')
+
+def _get_locales(filename):
+ locales_list = json.load(open(filename), encoding="utf-8")
+ return locales_list.keys()
+
+
+def find_files(dirs, pattern):
+ matches = []
+ for dir in dirs:
+ for current, dirnames, filenames in os.walk(dir):
+ for filename in fnmatch.filter(filenames, pattern):
+ matches.append(os.path.join(current, filename))
+ return matches
+
+
+def parse_manifest_properties(filename):
+ with open(filename) as f:
+ data = f.readlines()
+ strings = {
+ "default": {},
+ "entry_points": {},
+ }
+ for line in data:
+ m = property_line.search(line)
+ if not m or line.strip().startswith('#'):
+ continue
+ value = m.group('value').strip()
+ if '.' in m.group('id'):
+ entry_point, key = m.group('id').split('.',1)
+ if entry_point not in strings["entry_points"]:
+ strings["entry_points"][entry_point] = {}
+ strings["entry_points"][entry_point][key.strip()] = value
+ else:
+ key = m.group('id')
+ strings["default"][key.strip()] = value
+ return strings
+
+
+def parse_ini(filename):
+ log = logging.getLogger(__name__)
+ with open(filename) as f:
+ data = f.readlines()
+ section = 'default'
+ imports = { section: [] }
+ for line in data:
+ if line.strip() == "" or line.startswith('!') or line.startswith('#'):
+ continue
+ elif line.strip().startswith('['): # Section header
+ section = section_line.search(line).group('section')
+ imports[section] = []
+ elif '@import' in line: # Import lines
+ property_file = import_line.search(line).group('filename')
+ imports[section].append(property_file)
+ else:
+ log.warn('parse_ini - found a line with contents '
+ 'unaccounted for "%s"', line.strip())
+ return imports
+
+
+def serialize_ini(outfile, imports):
+ def _section(locale):
+ return "[%s]" % locale
+ def _import(path):
+ return "@import url(%s)" % path
+ output = []
+ for locale, paths in imports.items():
+ if locale == "default":
+ for path in paths:
+ output.insert(0, _import(path))
+ continue
+ output.append(_section(locale))
+ for path in paths:
+ output.append(_import(path))
+ with open(outfile, 'w') as o:
+ o.write("\n".join(output))
+
+
+def add_locale_imports(locales, ini_file):
+ """Recreate an ini file with all locales sections"""
+ log = logging.getLogger(__name__)
+ imports = {
+ "default": parse_ini(ini_file)["default"]
+ }
+ for locale in locales:
+ log.info("adding %s to %s" % (locale, ini_file))
+ imports[locale] = []
+ for path in imports["default"]:
+ locale_path = path.replace("en-US", locale)
+ imports[locale].append(locale_path)
+ log.debug("added %s" % locale_path)
+ serialize_ini(ini_file, imports)
+ log.info("updated %s saved" % ini_file)
+
+
+def copy_properties(source, locales, ini_file):
+ log = logging.getLogger(__name__)
+ ini_dirname = os.path.dirname(ini_file)
+ imports = parse_ini(ini_file)
+ for locale in locales:
+ log.info("copying %s files as per %s" % (locale, ini_file))
+ for path in imports[locale]:
+ target_path = os.path.join(ini_dirname, path)
+ # apps/browser/locales/browser.fr.properties becomes
+ # apps/browser/browser.properties
+ source_path = target_path.replace('/locales', '') \
+ .replace('.%s' % locale, '')
+ source_path = os.path.join(source, locale, source_path)
+ if not os.path.exists(source_path):
+ log.warn('%s does not exist' % source_path)
+ continue
+ shutil.copy(source_path, target_path)
+ log.debug("copied %s to %s" % (source_path, target_path))
+
+
+def add_locale_manifest(source, locales, manifest_file):
+ log = logging.getLogger(__name__)
+ with open(manifest_file) as f:
+ manifest = json.load(f, encoding="utf-8")
+ for locale in locales:
+ log.info("adding %s to %s" % (locale, manifest_file))
+ manifest_properties = os.path.join(source, locale,
+ os.path.dirname(manifest_file),
+ 'manifest.properties')
+ log.debug("getting strings from %s" % manifest_properties)
+ if not os.path.exists(manifest_properties):
+ log.warn("%s does not exist" % manifest_properties)
+ continue
+ strings = parse_manifest_properties(manifest_properties)
+ if "entry_points" in manifest:
+ for name, ep in manifest["entry_points"].items():
+ if "locales" not in ep:
+ continue
+ log.debug("adding to entry_points.%s.locales" % name)
+ if name not in strings["entry_points"]:
+ log.warn("%s.* strings are missing from %s" %
+ (name, manifest_properties))
+ continue
+ ep["locales"][locale] = {}
+ ep["locales"][locale].update(strings["entry_points"][name])
+ if "locales" in manifest:
+ log.debug("adding to locales")
+ manifest["locales"][locale] = {}
+ manifest["locales"][locale].update(strings["default"])
+ f.close()
+ with open(manifest_file, 'w') as o:
+ json.dump(manifest, o, encoding="utf-8", indent=2)
+ log.debug("updated %s saved" % manifest_file)
+
+
+def setup_logging(volume=1, console=True, filename=None):
+ logger = logging.getLogger(__name__)
+ levels = [logging.DEBUG,
+ logging.INFO,
+ logging.WARNING,
+ logging.ERROR,
+ logging.CRITICAL][::1]
+ if volume > len(levels):
+ volume = len(levels) - 1
+ elif volume < 0:
+ volume = 0
+ logger.setLevel(levels[len(levels)-volume])
+ if console:
+ console_handler = logging.StreamHandler()
+ console_formatter = logging.Formatter('%(levelname)s: %(message)s')
+ console_handler.setFormatter(console_formatter)
+ logger.addHandler(console_handler)
+ if filename:
+ file_handler = logging.FileHandler(filename)
+ file_formatter = logging.Formatter('%(asctime) - %(levelname)s: %(message)s')
+ file_handler.addFormatter(file_formatter)
+ logger.addHandler(file_handler)
+
+
+def main():
+ parser = OptionParser("%prog [OPTIONS] [LOCALES...] - create multilocale Gaia")
+ parser.add_option("-v", "--verbose",
+ action="count", dest="verbose", default=2,
+ help="use more to make louder")
+ parser.add_option("-i", "--ini",
+ action="store_true", dest="onlyini", default=False,
+ help=("just edit the ini files and exit; "
+ "use this with DEBUG=1 make profile"))
+ parser.add_option("--target",
+ action="append", dest="target",
+ help=("path to directory to make changes in "
+ "(more than one is fine)"))
+ parser.add_option("--source",
+ action="store", dest="source",
+ help="path to the l10n basedir")
+ parser.add_option("--config",
+ action="store", dest="config_file",
+ help=("path to the languages.json config file; "
+ "will be used instead of LOCALES"))
+
+ options, locales = parser.parse_args()
+
+ setup_logging(volume=options.verbose)
+ log = logging.getLogger(__name__)
+
+ if options.config_file is not None:
+ locales = _get_locales(options.config_file)
+ log.debug("config file specified; ignoring any locales passed as args")
+ elif len(locales) == 0:
+ parser.error("You need to specify --config or pass the list of locales")
+ if options.target is None:
+ parser.error("You need to specify at least one --target")
+ if options.source is None and not options.onlyini:
+ parser.error("You need to specify --source (unless you meant --ini)")
+
+ if "en-US" in locales:
+ locales.remove("en-US")
+ ini_files = find_files(options.target, "*.ini")
+
+ # 1. link properties files from the inis
+ for ini_file in ini_files:
+ log.info("########## adding locale import rules to %s" % ini_file)
+ add_locale_imports(locales, ini_file)
+
+ if options.onlyini:
+ parser.exit(1)
+
+ # 2. copy properties files as per the inis
+ for ini_file in ini_files:
+ log.info("########## copying locale files as per %s" % ini_file)
+ copy_properties(options.source, locales, ini_file)
+
+ # 3. edit manifests
+ manifest_files = find_files(options.target, 'manifest.webapp')
+ for manifest_file in manifest_files:
+ log.info("########## adding localized names to %s" % manifest_file)
+ add_locale_manifest(options.source, locales, manifest_file)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/build/offline-cache.js b/build/offline-cache.js
new file mode 100644
index 0000000..abec495
--- /dev/null
+++ b/build/offline-cache.js
@@ -0,0 +1,159 @@
+let Namespace = CC('@mozilla.org/network/application-cache-namespace;1',
+ 'nsIApplicationCacheNamespace',
+ 'init');
+const nsICache = Ci.nsICache;
+const nsIApplicationCache = Ci.nsIApplicationCache;
+const applicationCacheService = Cc['@mozilla.org/network/application-cache-service;1']
+ .getService(Ci.nsIApplicationCacheService);
+
+function log(str) {
+ dump(' +-+ OfflineCache: ' + str + '\n');
+}
+
+/*
+ * Compile Gaia into an offline cache sqlite database.
+ */
+function storeCache(applicationCache, url, file, itemType) {
+ let session = Services.cache.createSession(applicationCache.clientID,
+ nsICache.STORE_OFFLINE, true);
+ session.asyncOpenCacheEntry(url, nsICache.ACCESS_WRITE, {
+ onCacheEntryAvailable: function (cacheEntry, accessGranted, status) {
+ cacheEntry.setMetaDataElement('request-method', 'GET');
+ cacheEntry.setMetaDataElement('response-head', 'HTTP/1.1 200 OK\r\n');
+ // Force an update. the default expiration time is way too far in the future:
+ //cacheEntry.setExpirationTime(0);
+
+ let outputStream = cacheEntry.openOutputStream(0);
+
+ // Input-Output stream machinery in order to push nsIFile content into cache
+ let inputStream = Cc['@mozilla.org/network/file-input-stream;1']
+ .createInstance(Ci.nsIFileInputStream);
+ inputStream.init(file, 1, -1, null);
+ let bufferedOutputStream = Cc['@mozilla.org/network/buffered-output-stream;1']
+ .createInstance(Ci.nsIBufferedOutputStream);
+ bufferedOutputStream.init(outputStream, 1024);
+ bufferedOutputStream.writeFrom(inputStream, inputStream.available());
+ bufferedOutputStream.flush();
+ bufferedOutputStream.close();
+ outputStream.close();
+ inputStream.close();
+
+ cacheEntry.markValid();
+ log (file.path + ' -> ' + url + ' (' + itemType + ')');
+ applicationCache.markEntry(url, itemType);
+ cacheEntry.close();
+ }
+ });
+}
+
+function getCachedURLs(origin, appcacheFile) {
+ let urls = [];
+ let lines = getFileContent(appcacheFile).split(/\r?\n/);
+ for (let i = 0; i < lines.length; i++) {
+ let line = lines[i];
+ if (/^#/.test(line) || !line.length)
+ continue;
+ if (line == 'CACHE MANIFEST')
+ continue;
+ if (line == 'CACHE:')
+ continue;
+ if (line == 'NETWORK:')
+ break;
+ // Prepend webapp origin in case of absolute path
+ if (line[0] == '/')
+ urls.push(origin + line.substring(1));
+ // Just pass along the url, if we have one
+ else if (line.substr(0, 4) == 'http')
+ urls.push(line);
+ else
+ throw new Error('Invalid line in appcache manifest:\n' + line +
+ '\nFrom: ' + appcacheFile.path);
+ }
+ return urls;
+}
+
+let webapps = getJSON(getFile(PROFILE_DIR, 'webapps', 'webapps.json'));
+
+Gaia.externalWebapps.forEach(function (webapp) {
+ // Process only webapp with a `appcache_path` field in their manifest.
+ if (!('appcache_path' in webapp.manifest))
+ return;
+
+ // Get the nsIFile for the appcache file by using `origin` file and
+ // `appcache_path` field of the manifest in order to find it in `cache/`.
+ let originDomain = webapp.origin.replace(/^https?:\/\//, '');
+ let appcachePath = 'cache/' + originDomain + webapp.manifest.appcache_path;
+ let appcacheURL = webapp.origin +
+ webapp.manifest.appcache_path.replace(/^\//, '');
+ let appcacheFile = webapp.sourceDirectoryFile.clone();
+ appcachePath.split('/').forEach(function (name) {
+ appcacheFile.append(name);
+ });
+ if (!appcacheFile.exists())
+ throw new Error('Unable to find application cache manifest: ' +
+ appcacheFile.path);
+
+ // Retrieve generated webapp id from platform profile file build by
+ // webapp-manifest.js; in order to allow the webapp to use offline cache.
+ let appId = webapps[webapp.sourceDirectoryName].localId;
+ let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
+ Services.io.newURI(webapp.origin, null, null),
+ appId, false);
+ Services.perms.addFromPrincipal(principal, 'offline-app',
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ // Get the url for the manifest. At some points the root
+ // domain should be extracted from manifest.webapp.
+ // See netwerk/cache/nsDiskCacheDeviceSQL.cpp : AppendJARIdentifier
+ // The group ID contains application id and 'f' for not being hosted in
+ // a browser element, but a mozbrowser iframe.
+ let groupID = appcacheURL + '#' + appId + '+f';
+ let applicationCache = applicationCacheService.createApplicationCache(groupID);
+ applicationCache.activate();
+
+ log ('Compiling (' + webapp.domain + ')');
+
+ let urls = getCachedURLs(webapp.origin, appcacheFile);
+ urls.forEach(function appendFile(url) {
+ // Get this nsIFile out of its relative path
+ let path = url.replace(/https?:\/\//, '');
+ let file = webapp.sourceDirectoryFile.clone();
+ file.append('cache');
+ let paths = path.split('/');
+ for (let i = 0; i < paths.length; i++) {
+ file.append(paths[i]);
+ }
+
+ if (!file.exists()) {
+ let msg = 'File ' + file.path + ' exists in the manifest but does not ' +
+ 'points to a real file.';
+ throw new Error(msg);
+ }
+
+ // TODO: use ITEM_IMPLICIT for launch_path, if it occurs to be important.
+ let itemType = nsIApplicationCache.ITEM_EXPLICIT;
+ storeCache(applicationCache, url, file, itemType);
+ });
+
+ // Store the appcache file
+ storeCache(applicationCache, appcacheURL, appcacheFile,
+ nsIApplicationCache.ITEM_MANIFEST);
+
+ // NETWORK:
+ // http://*
+ // https://*
+ let array = Cc['@mozilla.org/array;1'].createInstance(Ci.nsIMutableArray);
+ let bypass = Ci.nsIApplicationCacheNamespace.NAMESPACE_BYPASS;
+ array.appendElement(new Namespace(bypass, 'http://*/', ''), false);
+ array.appendElement(new Namespace(bypass, 'https://*/', ''), false);
+ applicationCache.addNamespaces(array);
+});
+
+
+// Wait for cache to be filled before quitting
+if (Gaia.engine === 'xpcshell') {
+ let thread = Services.tm.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
diff --git a/build/optimize-clean.js b/build/optimize-clean.js
new file mode 100644
index 0000000..14dbcf0
--- /dev/null
+++ b/build/optimize-clean.js
@@ -0,0 +1,28 @@
+
+function debug(str) {
+ //dump(' -*- l10n-clean.js: ' + str + '\n');
+}
+
+debug('Begin');
+
+Gaia.webapps.forEach(function(webapp) {
+ // if BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ debug(webapp.sourceDirectoryName);
+
+ let re = new RegExp('\\.html\\.' + GAIA_DEFAULT_LOCALE + '$');
+ let files = ls(webapp.sourceDirectoryFile, true);
+ files.forEach(function(file) {
+ if (
+ re.test(file.leafName) ||
+ file.leafName.indexOf(Gaia.aggregatePrefix) === 0
+ ) {
+ file.remove(false);
+ }
+ });
+});
+
+debug('End');
+
diff --git a/build/otoro-install-busybox.sh b/build/otoro-install-busybox.sh
new file mode 100755
index 0000000..8fa138b
--- /dev/null
+++ b/build/otoro-install-busybox.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# This shell script installs busybox on the device.
+# This lets us take the fast path in install-gaia.py.
+
+# Remount file system with read/write permissions
+adb shell "mount -o rw,remount -t rootfs /"
+adb shell "mkdir -p /system/vendor/bin"
+adb push busybox-armv6l /vendor/bin/busybox
+adb shell "chmod 555 /vendor/bin/busybox"
+
+# Perform the symbolic links
+adb shell "for x in \`busybox --list\`; do ln -s /vendor/bin/busybox /vendor/bin/\$x; done"
+
+# Remount file system with read-only permissions
+adb shell "mount -o ro,remount -t rootfs /"
+
diff --git a/build/payment-prefs.js b/build/payment-prefs.js
new file mode 100644
index 0000000..214fd4e
--- /dev/null
+++ b/build/payment-prefs.js
@@ -0,0 +1,5 @@
+pref("dom.payment.provider.0.name", "firefoxmarket");
+pref("dom.payment.provider.0.description", "marketplace.firefox.com");
+pref("dom.payment.provider.0.uri", "https://marketplace.firefox.com/mozpay/?req=");
+pref("dom.payment.provider.0.type", "mozilla/payments/pay/v1");
+pref("dom.payment.provider.0.requestMethod", "GET");
diff --git a/build/preferences.js b/build/preferences.js
new file mode 100644
index 0000000..8787a3b
--- /dev/null
+++ b/build/preferences.js
@@ -0,0 +1,100 @@
+
+'use strict';
+
+function debug(msg) {
+ //dump('-*- preferences.js ' + msg + '\n');
+}
+
+const prefs = [];
+
+let homescreen = HOMESCREEN + (GAIA_PORT ? GAIA_PORT : '');
+prefs.push(["browser.manifestURL", homescreen + "/manifest.webapp"]);
+if (homescreen.substring(0,6) == "app://") { // B2G bug 773884
+ homescreen += "/index.html";
+}
+prefs.push(["browser.homescreenURL", homescreen]);
+
+let domains = [];
+domains.push(GAIA_DOMAIN);
+
+Gaia.webapps.forEach(function (webapp) {
+ domains.push(webapp.domain);
+});
+
+prefs.push(["network.http.max-connections-per-server", 15]);
+
+// for https://bugzilla.mozilla.org/show_bug.cgi?id=811605 to let user know what prefs is for ril debugging
+prefs.push(["ril.debugging.enabled", false]);
+
+if (LOCAL_DOMAINS) {
+ prefs.push(["network.dns.localDomains", domains.join(",")]);
+}
+
+if (DEBUG) {
+ prefs.push(["marionette.defaultPrefs.enabled", true]);
+ prefs.push(["b2g.remote-js.enabled", true]);
+ prefs.push(["b2g.remote-js.port", 4242]);
+ prefs.push(["javascript.options.showInConsole", true]);
+ prefs.push(["nglayout.debug.disable_xul_cache", true]);
+ prefs.push(["browser.dom.window.dump.enabled", true]);
+ prefs.push(["javascript.options.strict", true]);
+ prefs.push(["dom.report_all_js_exceptions", true]);
+ prefs.push(["nglayout.debug.disable_xul_fastload", true]);
+ prefs.push(["extensions.autoDisableScopes", 0]);
+ prefs.push(["browser.startup.homepage", homescreen]);
+
+ prefs.push(["dom.mozBrowserFramesEnabled", true]);
+ prefs.push(["b2g.ignoreXFrameOptions", true]);
+ prefs.push(["dom.sms.enabled", true]);
+ prefs.push(["dom.mozContacts.enabled", true]);
+ prefs.push(["dom.mozSettings.enabled", true]);
+ prefs.push(["device.storage.enabled", true]);
+ prefs.push(["devtools.chrome.enabled", true]);
+ prefs.push(["webgl.verbose", true]);
+
+ // Preferences for httpd
+ // (Use JSON.stringify in order to avoid taking care of `\` escaping)
+ prefs.push(["extensions.gaia.dir", GAIA_DIR]);
+ prefs.push(["extensions.gaia.domain", GAIA_DOMAIN]);
+ prefs.push(["extensions.gaia.port", parseInt(GAIA_PORT.replace(/:/g, ""))]);
+ prefs.push(["extensions.gaia.app_src_dirs", GAIA_APP_SRCDIRS]);
+ prefs.push(["extensions.gaia.locales_debug_path", GAIA_LOCALES_PATH]);
+ let appPathList = [];
+ Gaia.webapps.forEach(function (webapp) {
+ appPathList.push(webapp.sourceAppDirectoryName + '/' +
+ webapp.sourceDirectoryName);
+ });
+ prefs.push(["extensions.gaia.app_relative_path", appPathList.join(' ')]);
+
+ // Identity debug messages
+ prefs.push(["toolkit.identity.debug", true]);
+}
+
+function writePrefs() {
+ let userJs = getFile(GAIA_DIR, 'profile', 'user.js');
+ let content = prefs.map(function (entry) {
+ return 'user_pref("' + entry[0] + '", ' + JSON.stringify(entry[1]) + ');';
+ }).join('\n');
+ writeContent(userJs, content + "\n");
+ debug("\n" + content);
+}
+
+function setPrefs() {
+ prefs.forEach(function(entry) {
+ if (typeof entry[1] == "string") {
+ Services.prefs.setCharPref(entry[0], entry[1]);
+ } else if (typeof entry[1] == "boolean") {
+ Services.prefs.setBoolPref(entry[0], entry[1]);
+ } else if (typeof entry[1] == "number") {
+ Services.prefs.setIntPref(entry[0], entry[1]);
+ } else {
+ throw new Error("Unsupported pref type: " + typeof entry[1]);
+ }
+ });
+}
+
+if (Gaia.engine === "xpcshell") {
+ writePrefs();
+} else if (Gaia.engine === "b2g") {
+ setPrefs();
+}
diff --git a/build/settings.py b/build/settings.py
new file mode 100644
index 0000000..dd8c1b6
--- /dev/null
+++ b/build/settings.py
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+#
+# This script generates the settings.json file which is stored into the b2g profile
+
+import base64
+import json
+import optparse
+import os
+import sys
+
+settings = {
+ "accessibility.invert": False,
+ "accessibility.screenreader": False,
+ "alarm.enabled": False,
+ "alert-sound.enabled": True,
+ "alert-vibration.enabled": True,
+ "app.reportCrashes": "ask",
+ "app.update.interval": 86400,
+ "audio.volume.alarm": 15,
+ "audio.volume.bt_sco": 15,
+ "audio.volume.dtmf": 15,
+ "audio.volume.content": 15,
+ "audio.volume.notification": 15,
+ "audio.volume.tts": 15,
+ "audio.volume.telephony": 5,
+ "bluetooth.enabled": False,
+ "bluetooth.debugging.enabled": False,
+ "camera.shutter.enabled": True,
+ "clear.remote-windows.data": False,
+ "debug.grid.enabled": False,
+ "debug.oop.disabled": False,
+ "debug.fps.enabled": False,
+ "debug.ttl.enabled": False,
+ "debug.log-animations.enabled": False,
+ "debug.paint-flashing.enabled": False,
+ "debug.peformancedata.shared": False,
+ "deviceinfo.firmware_revision": "",
+ "deviceinfo.hardware": "",
+ "deviceinfo.os": "",
+ "deviceinfo.platform_build_id": "",
+ "deviceinfo.platform_version": "",
+ "deviceinfo.software": "",
+ "deviceinfo.update_channel": "",
+ "gaia.system.checkForUpdates": False,
+ "geolocation.enabled": True,
+ "keyboard.layouts.english": True,
+ "keyboard.layouts.dvorak": False,
+ "keyboard.layouts.otherlatins": False,
+ "keyboard.layouts.cyrillic": False,
+ "keyboard.layouts.arabic": False,
+ "keyboard.layouts.hebrew": False,
+ "keyboard.layouts.zhuyin": False,
+ "keyboard.layouts.pinyin": False,
+ "keyboard.layouts.greek": False,
+ "keyboard.layouts.japanese": False,
+ "keyboard.layouts.portuguese": False,
+ "keyboard.layouts.spanish": False,
+ "keyboard.vibration": False,
+ "keyboard.clicksound": False,
+ "keyboard.wordsuggestion": False,
+ "keyboard.current": "en",
+ "language.current": "en-US",
+ "lockscreen.passcode-lock.code": "0000",
+ "lockscreen.passcode-lock.timeout": 0,
+ "lockscreen.passcode-lock.enabled": False,
+ "lockscreen.notifications-preview.enabled": True,
+ "lockscreen.enabled": True,
+ "lockscreen.locked": True,
+ "lockscreen.unlock-sound.enabled": False,
+ "mail.sent-sound.enabled": True,
+ "operatorvariant.mcc": 0,
+ "operatorvariant.mnc": 0,
+ "ril.iccInfo.mbdn":"",
+ "ril.sms.strict7BitEncoding.enabled": False,
+ "ril.cellbroadcast.searchlist": "",
+ "debug.console.enabled": False,
+ "phone.ring.keypad": True,
+ "powersave.enabled": False,
+ "powersave.threshold": 0,
+ "privacy.donottrackheader.enabled": False,
+ "ril.callwaiting.enabled": None,
+ "ril.cf.enabled": False,
+ "ril.data.enabled": False,
+ "ril.data.apn": "",
+ "ril.data.carrier": "",
+ "ril.data.passwd": "",
+ "ril.data.httpProxyHost": "",
+ "ril.data.httpProxyPort": 0,
+ "ril.data.mmsc": "",
+ "ril.data.mmsproxy": "",
+ "ril.data.mmsport": 0,
+ "ril.data.roaming_enabled": False,
+ "ril.data.user": "",
+ "ril.mms.apn": "",
+ "ril.mms.carrier": "",
+ "ril.mms.httpProxyHost": "",
+ "ril.mms.httpProxyPort": "",
+ "ril.mms.mmsc": "",
+ "ril.mms.mmsport": "",
+ "ril.mms.mmsproxy": "",
+ "ril.mms.passwd": "",
+ "ril.mms.user": "",
+ "ril.radio.preferredNetworkType": "",
+ "ril.radio.disabled": False,
+ "ril.supl.apn": "",
+ "ril.supl.carrier": "",
+ "ril.supl.httpProxyHost": "",
+ "ril.supl.httpProxyPort": "",
+ "ril.supl.passwd": "",
+ "ril.supl.user": "",
+ "ril.sms.strict7BitEncoding.enabled": False,
+ "ring.enabled": True,
+ "screen.automatic-brightness": True,
+ "screen.brightness": 1,
+ "screen.timeout": 60,
+ "tethering.usb.enabled": False,
+ "tethering.usb.ip": "192.168.0.1",
+ "tethering.usb.prefix": "24",
+ "tethering.usb.dhcpserver.startip": "192.168.0.10",
+ "tethering.usb.dhcpserver.endip": "192.168.0.30",
+ "tethering.wifi.enabled": False,
+ "tethering.wifi.ip": "192.168.1.1",
+ "tethering.wifi.prefix": "24",
+ "tethering.wifi.dhcpserver.startip": "192.168.1.10",
+ "tethering.wifi.dhcpserver.endip": "192.168.1.30",
+ "tethering.wifi.ssid": "FirefoxHotspot",
+ "tethering.wifi.security.type": "open",
+ "tethering.wifi.security.password": "1234567890",
+ "tethering.wifi.connectedClients": 0,
+ "tethering.usb.connectedClients": 0,
+ "time.nitz.automatic-update.enabled": True,
+ "time.timezone": None,
+ "ums.enabled": False,
+ "ums.mode": 0,
+ "vibration.enabled": True,
+ "wifi.enabled": True,
+ "wifi.disabled_by_wakelock": False,
+ "wifi.notification": False,
+ "icc.displayTextTimeout": 40000,
+ "icc.inputTextTimeout": 40000
+}
+
+def main():
+ parser = optparse.OptionParser(description="Generate initial settings.json file")
+ parser.add_option( "--override", help="JSON files for custom settings overrides")
+ parser.add_option( "--homescreen", help="specify the homescreen URL")
+ parser.add_option( "--ftu", help="specify the ftu manifest URL")
+ parser.add_option("-c", "--console", help="indicate if the console should be enabled", action="store_true")
+ parser.add_option("-o", "--output", help="specify the name of the output file")
+ parser.add_option("-w", "--wallpaper", help="specify the name of the wallpaper file")
+ parser.add_option("-v", "--verbose", help="increase output verbosity", action="store_true")
+ parser.add_option( "--noftu", help="bypass the ftu app", action="store_true")
+ parser.add_option( "--locale", help="specify the default locale to use")
+ parser.add_option( "--enable-debugger", help="enable remote debugger (and ADB for VARIANT=user builds)", action="store_true")
+ (options, args) = parser.parse_args(sys.argv[1:])
+
+ verbose = options.verbose
+
+ if options.homescreen:
+ homescreen_url = options.homescreen
+ else:
+ homescreen_url = "app://homescreen.gaiamobile.org/manifest.webapp"
+
+ if options.ftu:
+ ftu_url = options.ftu
+ else:
+ ftu_url = "app://communications.gaiamobile.org/manifest.webapp"
+
+ if options.output:
+ settings_filename = options.output
+ else:
+ settings_filename = "profile/settings.json"
+
+ if options.wallpaper:
+ wallpaper_filename = options.wallpaper
+ else:
+ wallpaper_filename = "build/wallpaper.jpg"
+
+ enable_debugger = (options.enable_debugger == True)
+
+ if verbose:
+ print "Console:", options.console
+ print "Homescreen URL:", homescreen_url
+ print "Ftu URL:", ftu_url
+ print "Setting Filename:",settings_filename
+ print "Wallpaper Filename:", wallpaper_filename
+ print "Enable Debugger:", enable_debugger
+
+ # Set the default console output
+ if options.console:
+ settings["debug.console.enabled"] = options.console
+
+ # Set the homescreen URL
+ settings["homescreen.manifestURL"] = homescreen_url
+
+ # Set the ftu manifest URL
+ if not options.noftu:
+ settings["ftu.manifestURL"] = ftu_url
+
+ # Set the default locale
+ if options.locale:
+ settings["language.current"] = options.locale
+
+ settings["devtools.debugger.remote-enabled"] = enable_debugger;
+
+ # Grab wallpaper.jpg and convert it into a base64 string
+ wallpaper_file = open(wallpaper_filename, "rb")
+ wallpaper_base64 = base64.b64encode(wallpaper_file.read())
+ settings["wallpaper.image"] = "data:image/jpeg;base64," + wallpaper_base64.decode("utf-8")
+
+ # Grab ringer_classic_courier.opus and convert it into a base64 string
+ ringtone_name = "shared/resources/media/ringtones/ringer_classic_courier.opus"
+ ringtone_file = open(ringtone_name, "rb");
+ ringtone_base64 = base64.b64encode(ringtone_file.read())
+ settings["dialer.ringtone"] = "data:audio/ogg;base64," + ringtone_base64.decode("utf-8")
+ settings["dialer.ringtone.name"] = "ringer_classic_courier.opus"
+
+ # Grab notifier_bell.opus and convert it into a base64 string
+ notification_name = "shared/resources/media/notifications/notifier_bell.opus"
+ notification_file = open(notification_name, "rb");
+ notification_base64 = base64.b64encode(notification_file.read())
+ settings["notification.ringtone"] = "data:audio/ogg;base64," + notification_base64.decode("utf-8")
+ settings["notification.ringtone.name"] = "notifier_bell.opus"
+
+ if options.override and os.path.exists(options.override):
+ try:
+ overrides = json.load(open(options.override))
+ for key, val in overrides.items():
+ settings[key] = val
+ except Exception, e:
+ print "Error while applying override setting file: %s\n%s" % (options.override, e)
+
+ json.dump(settings, open(settings_filename, "wb"))
+
+if __name__ == "__main__":
+ main()
diff --git a/build/ua-override-prefs.js b/build/ua-override-prefs.js
new file mode 100644
index 0000000..af5c354
--- /dev/null
+++ b/build/ua-override-prefs.js
@@ -0,0 +1,102 @@
+
+// Send these sites a custom user-agent. Bugs to remove each override after
+// evangelism are included.
+pref("general.useragent.override.facebook.com", "\(Mobile#(Android; Mobile"); // bug 827635
+pref("general.useragent.override.youtube.com", "\(Mobile#(Android; Mobile"); // bug 827636
+pref("general.useragent.override.yelp.com", "\(Mobile#(Android; Mobile"); // bug 799884
+pref("general.useragent.override.dailymotion.com", "\(Mobile#(Android; Mobile"); // bug 827638
+pref("general.useragent.override.accounts.google.com", "\(Mobile#(Android; Mobile"); // bug 805164
+pref("general.useragent.override.maps.google.com", "\(Mobile#(Android; Mobile"); // bug 802981
+pref("general.useragent.override.uol.com.br", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826330
+pref("general.useragent.override.live.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826332
+pref("general.useragent.override.globo.com", "\(Mobile#(Android; Mobile"); // bug 826335
+pref("general.useragent.override.yahoo.com", "\(Mobile#(Android; Mobile"); // bug 826338
+pref("general.useragent.override.mercadolivre.com.br", "\(Mobile#(Android; Mobile"); // bug 826342
+pref("general.useragent.override.ig.com.br", "\(Mobile#(Android; Mobile"); // bug 826343
+pref("general.useragent.override.abril.com.br", "\(Mobile#(Android; Mobile"); // bug 826344
+pref("general.useragent.override.msn.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826347
+pref("general.useragent.override.linkedin.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826348
+pref("general.useragent.override.itau.com.br", "\(Mobile#(Android; Mobile"); // bug 826353
+pref("general.useragent.override.tumblr.com", "\(Mobile#(Android; Mobile"); // bug 826361
+pref("general.useragent.override.4shared.com", "\(Mobile#(Android; Mobile"); // bug 826502
+pref("general.useragent.override.orkut.com.br", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826504
+pref("general.useragent.override.r7.com", "\(Mobile#(Android; Mobile"); // bug 826510
+pref("general.useragent.override.amazon.com", "\(Mobile#(Android; Mobile"); // bug 826512
+pref("general.useragent.override.estadao.com.br", "\(Mobile#(Android; Mobile"); // bug 826514
+pref("general.useragent.override.letras.mus.br", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826517
+pref("general.useragent.override.bb.com.br", "\(Mobile#(Android; Mobile"); // bug 826711
+pref("general.useragent.override.orkut.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826712
+pref("general.useragent.override.noticias.uol.com.br", "\(Mobile#(Android; Mobile"); // bug 826715
+pref("general.useragent.override.olx.com.br", "\(Mobile#(Android; Mobile"); // bug 826720
+pref("general.useragent.override.bancobrasil.com.br", "\(Mobile#(Android; Mobile"); // bug 826736
+pref("general.useragent.override.techtudo.com.br", "\(Mobile#(Android; Mobile"); // bug 826845
+pref("general.useragent.override.clickjogos.uol.com.br", "\(Mobile#(Android; Mobile"); // bug 826949
+pref("general.useragent.override.ebay.com", "\(Mobile#(Android; Mobile");// bug 826958
+pref("general.useragent.override.bing.com", "\(Mobile#(Android; Mobile"); // bug 827622
+pref("general.useragent.override.tam.com.br", "\(Mobile#(Android; Mobile"); // bug 827623
+pref("general.useragent.override.pontofrio.com.br", "\(Mobile#(Android; Mobile"); // bug 827624
+pref("general.useragent.override.pagseguro.uol.com.br", "\(Mobile#(Android; Mobile"); // bug 827625
+pref("general.useragent.override.magazineluiza.com.br", "\(Mobile#(Android; Mobile"); // bug 827626
+pref("general.useragent.override.bol.uol.com.br", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 827627
+pref("general.useragent.override.groupon.com.br", "\(Mobile#(Android; Mobile"); // bug 827628
+pref("general.useragent.override.vagalume.com.br", "\(Mobile#(Android; Mobile"); // bug 827630
+pref("general.useragent.override.climatempo.com.br", "\(Mobile#(Android; Mobile"); // bug 827631
+pref("general.useragent.override.tecmundo.com.br", "\(Mobile#(Android; Mobile"); // bug 827632
+pref("general.useragent.override.hao123.com", "\(Mobile#(Android; Mobile"); // bug 827633
+pref("general.useragent.override.imdb.com", "\(Mobile#(Android; Mobile"); // bug 827634
+pref("general.useragent.override.lancenet.com.br", "\(Mobile#(Android; Mobile"); // bug 827576
+pref("general.useragent.override.webmotors.com.br", "\(Mobile#(Android; Mobile"); // bug 827573
+pref("general.useragent.override.mercadolibre.com.co", "\(Mobile#(Android; Mobile"); // bug 827661
+pref("general.useragent.override.elespectador.com", "\(Mobile#(Android; Mobile"); // bug 827664
+pref("general.useragent.override.slideshare.net", "\(Mobile#(Android; Mobile"); // bug 827666
+pref("general.useragent.override.scribd.com", "\(Mobile#(Android; Mobile"); // bug 827668
+pref("general.useragent.override.elpais.com.co", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 827670
+pref("general.useragent.override.olx.com.co", "\(Mobile#(Android; Mobile"); // bug 827672
+pref("general.useragent.override.avianca.com", "\(Mobile#(Android; Mobile"); // bug 827674
+pref("general.useragent.override.dropbox.com", "\(Mobile#(Android; Mobile"); // bug 827676
+pref("general.useragent.override.marca.com", "\(Mobile#(Android; Mobile"); // bug 827678
+pref("general.useragent.override.wp.pl", "\(Mobile#(Android; Mobile"); // bug 828351
+pref("general.useragent.override.gazeta.pl", "\(Mobile#(Android; Mobile"); // bug 828354
+pref("general.useragent.override.o2.pl", "\(Mobile#(Android; Mobile"); // bug 828356
+pref("general.useragent.override.ceneo.pl", "\(Mobile#(Android; Mobile"); // bug 828358
+pref("general.useragent.override.sport.pl", "\(Mobile#(Android; Mobile"); // bug 828360
+pref("general.useragent.override.tvn24.pl", "\(Mobile#(Android; Mobile"); // bug 828362
+pref("general.useragent.override.nk.pl", "\(Mobile#(Android; Mobile"); // bug 828364
+pref("general.useragent.override.wyborcza.biz", "\(Mobile#(Android; Mobile"); // bug 828366
+pref("general.useragent.override.money.pl", "\(Mobile#(Android; Mobile"); // bug 828369
+pref("general.useragent.override.ingbank.pl", "\(Mobile#(Android; Mobile"); // bug 828371
+pref("general.useragent.override.tablica.pl", "\(Mobile#(Android; Mobile"); // bug 828374
+pref("general.useragent.override.plotek.pl", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 828376
+pref("general.useragent.override.wyborcza.pl", "\(Mobile#(Android; Mobile"); // bug 828378
+pref("general.useragent.override.deser.pl", "\(Mobile#(Android; Mobile"); // bug 828380
+pref("general.useragent.override.as.com", "\(Mobile#(Android; Mobile"); // bug 828383
+pref("general.useragent.override.ebay.es", "\(Mobile#(Android; Mobile"); // bug 828386
+pref("general.useragent.override.amazon.es", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 828388
+pref("general.useragent.override.20minutos.es", "\(Mobile#(Android; Mobile"); // bug 828390
+pref("general.useragent.override.infojobs.net", "\(Mobile#(Android; Mobile"); // bug 828392
+pref("general.useragent.override.vimeo.com", "\(Mobile#(Android; Mobile"); // bug 828394
+pref("general.useragent.override.elconfidencial.com", "\(Mobile#(Android; Mobile"); // bug 828397
+pref("general.useragent.override.antena3.com", "\(Mobile#(Android; Mobile"); // bug 828399
+pref("general.useragent.override.ingdirect.es", "\(Mobile#(Android; Mobile"); // bug 828401
+pref("general.useragent.override.fotocasa.es", "\(Mobile#(Android; Mobile"); // bug 828403
+pref("general.useragent.override.orange.es", "\(Mobile#(Android; Mobile"); // bug 828406
+pref("general.useragent.override.stackoverflow.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 828408
+pref("general.useragent.override.amazon.co.uk", "\(Mobile#(Android; Mobile"); // bug 828412
+pref("general.useragent.override.paginasamarillas.es", "\(Mobile#(Android; Mobile"); // bug 828414
+pref("general.useragent.override.loteriasyapuestas.es", "\(Mobile#(Android; Mobile"); // bug 828416
+pref("general.useragent.override.bbva.es", "\(Mobile#(Android; Mobile"); // bug 828418
+pref("general.useragent.override.booking.com", "\(Mobile#(Android; Mobile"); // bug 828420
+pref("general.useragent.override.publico.es", "\(Mobile#(Android; Mobile"); // bug 828422
+pref("general.useragent.override.mercadolibre.com.ve", "\(Mobile#(Android; Mobile"); // bug 828425
+pref("general.useragent.override.lapatilla.com", "\(Mobile#(Android; Mobile"); // bug 828428
+pref("general.useragent.override.meridiano.com.ve", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 828430
+pref("general.useragent.override.espn.go.com", "\(Mobile#(Android; Mobile"); // bug 828431
+pref("general.useragent.override.olx.com.ve", "\(Mobile#(Android; Mobile"); // bug 828433
+pref("general.useragent.override.rincondelvago.com", "\(Mobile#(Android; Mobile"); // bug 828435
+pref("general.useragent.override.avn.info.ve", "\(Mobile#(Android; Mobile"); // bug 828437
+pref("general.useragent.override.movistar.com.ve", "\(Mobile#(Android; Mobile"); // bug 828439
+pref("general.useragent.override.laverdad.com", "\(Mobile#(Android; Mobile"); // bug 828441
+pref("general.useragent.override.despegar.com.ve", "\(Mobile#(Android; Mobile"); // bug 828443
+pref("general.useragent.override.bumeran.com.ve", "\(Mobile#(Android; Mobile"); // bug 828445
+pref("general.useragent.override.petardas.com", "\(Mobile#(Android; Mobile"); // bug 828448
+pref("general.useragent.override.mail.google.com", "\(Mobile#(Android; Mobile"); // bug 827869
diff --git a/build/utils.js b/build/utils.js
new file mode 100644
index 0000000..5b7a58b
--- /dev/null
+++ b/build/utils.js
@@ -0,0 +1,222 @@
+const { 'classes': Cc, 'interfaces': Ci, 'results': Cr, 'utils': Cu,
+ 'Constructor': CC } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/FileUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+function isSubjectToBranding(path) {
+ return /shared[\/\\][a-zA-Z]+[\/\\]branding$/.test(path) ||
+ /branding[\/\\]initlogo.png/.test(path);
+}
+
+function getSubDirectories(directory) {
+ let appsDir = new FileUtils.File(GAIA_DIR);
+ appsDir.append(directory);
+
+ let dirs = [];
+ let files = appsDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsILocalFile);
+ if (file.isDirectory()) {
+ dirs.push(file.leafName);
+ }
+ }
+ return dirs;
+}
+
+/**
+ * Returns an array of nsIFile's for a given directory
+ *
+ * @param {nsIFile} dir directory to read.
+ * @param {boolean} recursive set to true in order to walk recursively.
+ * @param {RegExp} exclude optional filter to exclude file/directories.
+ *
+ * @return {Array} list of nsIFile's.
+ */
+function ls(dir, recursive, exclude) {
+ let results = [];
+ let files = dir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsILocalFile);
+ if (!exclude || !exclude.test(file.leafName)) {
+ results.push(file);
+ if (recursive && file.isDirectory()) {
+ results = results.concat(ls(file, true, exclude));
+ }
+ }
+ }
+ return results;
+}
+
+function getFileContent(file) {
+ try {
+ let fileStream = Cc['@mozilla.org/network/file-input-stream;1']
+ .createInstance(Ci.nsIFileInputStream);
+ fileStream.init(file, 1, 0, false);
+
+ let converterStream = Cc['@mozilla.org/intl/converter-input-stream;1']
+ .createInstance(Ci.nsIConverterInputStream);
+ converterStream.init(fileStream, 'utf-8', fileStream.available(),
+ Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+ let out = {};
+ let count = fileStream.available();
+ converterStream.readString(count, out);
+
+ var content = out.value;
+ converterStream.close();
+ fileStream.close();
+ } catch (e) {
+ let msg = (file && file.path) ? '\nfile not found: ' + file.path : '';
+ throw new Error(' -*- build/utils.js: ' + e + msg + '\n');
+ }
+ return content;
+}
+
+function writeContent(file, content) {
+ var fileStream = Cc['@mozilla.org/network/file-output-stream;1']
+ .createInstance(Ci.nsIFileOutputStream);
+ fileStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
+
+ let converterStream = Cc['@mozilla.org/intl/converter-output-stream;1']
+ .createInstance(Ci.nsIConverterOutputStream);
+
+ converterStream.init(fileStream, 'utf-8', 0, 0);
+ converterStream.writeString(content);
+ converterStream.close();
+}
+
+// Return an nsIFile by joining paths given as arguments
+// First path has to be an absolute one
+function getFile() {
+ try {
+ let file = new FileUtils.File(arguments[0]);
+ if (arguments.length > 1) {
+ for (let i = 1; i < arguments.length; i++) {
+ file.append(arguments[i]);
+ }
+ }
+ return file;
+ } catch(e) {
+ throw new Error(' -*- build/utils.js: Invalid file path (' +
+ Array.slice(arguments).join(', ') + ')\n' + e + '\n');
+ }
+}
+
+function ensureFolderExists(file) {
+ if (!file.exists()) {
+ try {
+ file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0755', 8));
+ } catch (e if e.result == Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ // Bug 808513: Ignore races between `if exists() then create()`.
+ return;
+ }
+ }
+}
+
+function getJSON(file) {
+ try {
+ let content = getFileContent(file);
+ return JSON.parse(content);
+ } catch (e) {
+ dump('Invalid JSON file : ' + file.path + '\n');
+ throw e;
+ }
+}
+
+function makeWebappsObject(dirs) {
+ return {
+ forEach: function(fun) {
+ let appSrcDirs = dirs.split(' ');
+ appSrcDirs.forEach(function parseDirectory(directoryName) {
+ let directories = getSubDirectories(directoryName);
+ directories.forEach(function readManifests(dir) {
+ let manifestFile = getFile(GAIA_DIR, directoryName, dir,
+ 'manifest.webapp');
+ let updateFile = getFile(GAIA_DIR, directoryName, dir,
+ 'update.webapp');
+ // Ignore directories without manifest
+ if (!manifestFile.exists() && !updateFile.exists()) {
+ return;
+ }
+
+ let manifest = manifestFile.exists() ? manifestFile : updateFile;
+ let domain = dir + '.' + GAIA_DOMAIN;
+
+ let webapp = {
+ manifest: getJSON(manifest),
+ manifestFile: manifest,
+ url: GAIA_SCHEME + domain + (GAIA_PORT ? GAIA_PORT : ''),
+ domain: domain,
+ sourceDirectoryFile: manifestFile.parent,
+ sourceDirectoryName: dir,
+ sourceAppDirectoryName: directoryName
+ };
+
+ // External webapps have a `metadata.json` file
+ let metaData = webapp.sourceDirectoryFile.clone();
+ metaData.append('metadata.json');
+ if (metaData.exists()) {
+ webapp.metaData = getJSON(metaData);
+ }
+
+ fun(webapp);
+ });
+ });
+ }
+ };
+}
+
+let externalAppsDirs = ['external-apps'];
+
+// External apps are built differently from other apps by webapp-manifests.js,
+// and we need apps that are both external and dogfood to be treated like
+// external apps (to properly test external apps on dogfood devices), so we
+// segregate them into their own directory that we add to the list of external
+// apps dirs here when building a dogfood profile.
+if (DOGFOOD === '1') {
+ externalAppsDirs.push('external-dogfood-apps');
+}
+
+const Gaia = {
+ engine: GAIA_ENGINE,
+ sharedFolder: getFile(GAIA_DIR, 'shared'),
+ webapps: makeWebappsObject(GAIA_APP_SRCDIRS),
+ externalWebapps: makeWebappsObject(externalAppsDirs.join(' ')),
+ aggregatePrefix: 'gaia_build_'
+};
+
+function registerProfileDirectory() {
+ let directoryProvider = {
+ getFile: function provider_getFile(prop, persistent) {
+ persistent.value = true;
+ if (prop != 'ProfD' && prop != 'ProfLDS') {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ return new FileUtils.File(PROFILE_DIR);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
+ Ci.nsISupports])
+ };
+
+ Cc['@mozilla.org/file/directory_service;1']
+ .getService(Ci.nsIProperties)
+ .QueryInterface(Ci.nsIDirectoryService)
+ .registerProvider(directoryProvider);
+}
+
+if (Gaia.engine === 'xpcshell') {
+ registerProfileDirectory();
+}
+
+function gaiaOriginURL(name) {
+ return GAIA_SCHEME + name + '.' + GAIA_DOMAIN + (GAIA_PORT ? GAIA_PORT : '');
+}
+
+function gaiaManifestURL(name) {
+ return gaiaOriginURL(name) + '/manifest.webapp';
+}
+
diff --git a/build/wallpaper.jpg b/build/wallpaper.jpg
new file mode 100644
index 0000000..d60fc2e
--- /dev/null
+++ b/build/wallpaper.jpg
Binary files differ
diff --git a/build/webapp-manifests.js b/build/webapp-manifests.js
new file mode 100644
index 0000000..6bd79b9
--- /dev/null
+++ b/build/webapp-manifests.js
@@ -0,0 +1,179 @@
+const INSTALL_TIME = 132333986000; // Match this to value in applications-data.js
+
+function debug(msg) {
+ //dump('-*- webapp-manifest.js: ' + msg + '\n');
+}
+
+let webappsTargetDir = Cc['@mozilla.org/file/local;1']
+ .createInstance(Ci.nsILocalFile);
+webappsTargetDir.initWithPath(PROFILE_DIR);
+// Create profile folder if doesn't exists
+if (!webappsTargetDir.exists())
+ webappsTargetDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0755', 8));
+// Create webapps folder if doesn't exists
+webappsTargetDir.append('webapps');
+if (!webappsTargetDir.exists())
+ webappsTargetDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0755', 8));
+
+let manifests = {};
+let id = 1;
+
+function copyRec(source, target) {
+ let results = [];
+ let files = source.directoryEntries;
+ if (!target.exists())
+ target.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0755', 8));
+
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsILocalFile);
+ if (file.isDirectory()) {
+ let subFolder = target.clone();
+ subFolder.append(file.leafName);
+ copyRec(file, subFolder);
+ } else {
+ file.copyTo(target, file.leafName);
+ }
+ }
+}
+
+// Returns the nsIPrincipal compliant integer
+// from the "type" property in manifests.
+function getAppStatus(status) {
+ let appStatus = 1; // By default, apps are installed
+ switch (status) {
+ case "certified":
+ appStatus = 3;
+ break;
+ case "privileged":
+ appStatus = 2;
+ break;
+ case "web":
+ default:
+ appStatus = 1;
+ break;
+ }
+ return appStatus;
+}
+
+Gaia.webapps.forEach(function (webapp) {
+ // If BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ // Compute webapp folder name in profile
+ let webappTargetDirName = webapp.domain;
+
+ // Copy webapp's manifest to the profile
+ let webappTargetDir = webappsTargetDir.clone();
+ webappTargetDir.append(webappTargetDirName);
+ webapp.manifestFile.copyTo(webappTargetDir, 'manifest.webapp');
+
+ // Add webapp's entry to the webapps global manifest.
+ // appStatus == 3 means this is a certified app.
+ // appStatus == 2 means this is a privileged app.
+ // appStatus == 1 means this is an installed (unprivileged) app
+
+ let url = webapp.url;
+ manifests[webappTargetDirName] = {
+ origin: url,
+ installOrigin: url,
+ receipt: null,
+ installTime: INSTALL_TIME,
+ manifestURL: url + '/manifest.webapp',
+ appStatus: getAppStatus(webapp.manifest.type),
+ localId: id++
+ };
+
+});
+
+// Process external webapps from /gaia/external-app/ folder
+Gaia.externalWebapps.forEach(function (webapp) {
+ // If BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ if (!webapp.metaData) {
+ return;
+ }
+
+ // Compute webapp folder name in profile
+ let webappTargetDirName = webapp.sourceDirectoryName;
+
+ // Copy webapp's manifest to the profile
+ let webappTargetDir = webappsTargetDir.clone();
+ webappTargetDir.append(webappTargetDirName);
+
+ let origin;
+ let installOrigin;
+ let manifestURL;
+
+ let removable;
+
+ // In case of packaged app, just copy `application.zip` and `update.webapp`
+ let appPackage = webapp.sourceDirectoryFile.clone();
+ appPackage.append('application.zip');
+ if (appPackage.exists()) {
+ let updateManifest = webapp.sourceDirectoryFile.clone();
+ updateManifest.append('update.webapp');
+ if (!updateManifest.exists()) {
+ throw new Error('External packaged webapp `' + webapp.domain + ' is ' +
+ 'missing an `update.webapp` file. This JSON file ' +
+ 'contains a `package_path` attribute specifying where ' +
+ 'to download the application zip package from the origin ' +
+ 'specified in `metadata.json` file.');
+ }
+ appPackage.copyTo(webappTargetDir, 'application.zip');
+ updateManifest.copyTo(webappTargetDir, 'update.webapp');
+ removable = true;
+ origin = webapp.metaData.origin;
+ installOrigin = webapp.metaData.installOrigin;
+ manifestURL = webapp.metaData.manifestURL;
+ } else {
+ webapp.manifestFile.copyTo(webappTargetDir, 'manifest.webapp');
+ origin = webapp.metaData.origin;
+ installOrigin = webapp.metaData.origin;
+ manifestURL = webapp.metaData.origin + 'manifest.webapp';
+
+ // This is an hosted app. Check if there is an offline cache.
+ let srcCacheFolder = webapp.sourceDirectoryFile.clone();
+ srcCacheFolder.append('cache');
+ if (srcCacheFolder.exists()) {
+ let cacheManifest = srcCacheFolder.clone();
+ cacheManifest.append('manifest.appcache');
+ if (!cacheManifest.exists())
+ throw new Error('External webapp `' + webapp.domain + '` has a cache ' +
+ 'directory without `manifest.appcache` file.');
+
+ // Copy recursively the whole cache folder to webapp folder
+ let targetCacheFolder = webappTargetDir.clone();
+ targetCacheFolder.append('cache');
+ copyRec(srcCacheFolder, targetCacheFolder);
+ }
+ }
+
+ let etag = webapp.metaData.etag || null;
+ let packageEtag = webapp.metaData.packageEtag || null;
+
+ // Add webapp's entry to the webapps global manifest
+ manifests[webappTargetDirName] = {
+ origin: origin,
+ installOrigin: installOrigin,
+ receipt: null,
+ installTime: 132333986000,
+ manifestURL: manifestURL,
+ removable: removable,
+ localId: id++,
+ etag: etag,
+ packageEtag: packageEtag,
+ appStatus: getAppStatus(webapp.metaData.type || "web"),
+ };
+
+});
+
+// Write webapps global manifest
+let manifestFile = webappsTargetDir.clone();
+manifestFile.append('webapps.json');
+
+// stringify json with 2 spaces indentation
+writeContent(manifestFile, JSON.stringify(manifests, null, 2) + '\n');
+
diff --git a/build/webapp-optimize.js b/build/webapp-optimize.js
new file mode 100644
index 0000000..8258e3d
--- /dev/null
+++ b/build/webapp-optimize.js
@@ -0,0 +1,386 @@
+
+function debug(str) {
+ //dump(' -*- webapp-optimize.js: ' + str + '\n');
+}
+
+
+/**
+ * Expose a global `win' object and load `l10n.js' in it --
+ * note: the `?reload' trick ensures we don't load a cached `l10njs' library.
+ */
+
+var win = { navigator: {} };
+Services.scriptloader.loadSubScript('file:///' + GAIA_DIR +
+ '/shared/js/l10n.js?reload=' + new Date().getTime(), win);
+
+
+/**
+ * Locale list -- by default, only the default one
+ */
+
+var l10nLocales = [GAIA_DEFAULT_LOCALE];
+var l10nDictionary = {
+ locales: {},
+ default_locale: GAIA_DEFAULT_LOCALE
+};
+l10nDictionary.locales[GAIA_DEFAULT_LOCALE] = {};
+
+/**
+ * whitelist by app name for javascript asset aggregation.
+ */
+const JS_AGGREGATION_WHITELIST = [
+ 'calendar'
+];
+
+/**
+ * Helpers
+ */
+
+function optimize_getFileContent(webapp, htmlFile, relativePath) {
+ let paths = relativePath.split('/');
+ let file;
+
+ // get starting directory: webapp root, HTML file or /shared/
+ if (/^\//.test(relativePath)) {
+ paths.shift();
+ file = webapp.sourceDirectoryFile.clone();
+ } else {
+ file = htmlFile.parent.clone();
+ }
+ if (paths[0] == 'shared') {
+ file = getFile(GAIA_DIR);
+ }
+
+ paths.forEach(function appendPath(name) {
+ file.append(name);
+ if (isSubjectToBranding(file.path)) {
+ file.append((OFFICIAL == 1) ? 'official' : 'unofficial');
+ }
+ });
+
+ try {
+ return getFileContent(file);
+ } catch (e) {
+ dump(file.path + ' could not be found.\n');
+ return '';
+ }
+}
+
+/**
+ * Aggregates javascript files by type to reduce the IO overhead.
+ * Depending on the script tags there are two files made:
+ *
+ * - defered scripts (<script defer src= ...) :
+ * $(Gaia.aggregatePrefix)defer_$(html_filename).js
+ *
+ * - normal scripts (<script src=...) :
+ * $(Gaia.aggregatePrefix)$(html_filename).js
+ *
+ *
+ * Also it is possible to skip aggregation on a per script basis:
+ *
+ * <script src="..." data-skip-optimize defer></script>
+ *
+ *
+ * This function is somewhat conservative about what it will aggregate and will
+ * only group scripts found the documents <head> section.
+ *
+ * @param {HTMLDocument} doc DOM document of the file.
+ * @param {Object} webapp details of current web app.
+ * @param {NSFile} htmlFile filename/path of the document.
+ */
+function optimize_aggregateJsResources(doc, webapp, htmlFile) {
+ // Everyone should be putting their scripts in head with defer.
+ // The best case is that only l10n.js is put into a normal.
+ let scripts = Array.slice(
+ doc.head.querySelectorAll('script[src]')
+ );
+
+ let deferred = {
+ prefix: 'defer_',
+ content: '',
+ lastNode: null
+ };
+
+ let normal = {
+ prefix: '',
+ content: '',
+ lastNode: null
+ };
+
+ scripts.forEach(function(script, idx) {
+ let html = script.outerHTML;
+
+ // per-script out see comment in function header.
+ if ('skipOptimize' in script.dataset) {
+ // remove from scripts so it will not be commented out...
+ debug(
+ '[optimize ' + webapp.sourceDirectoryName + '] ' +
+ 'skipping script "' + html + '"'
+ );
+ scripts.splice(idx, 1);
+ return;
+ }
+
+ // we inject the whole outerHTML into the comment for debugging so
+ // if there is something valuable in the html that effects the script
+ // that broke the app it should be fairly easy to tell what happened.
+ let content = '; /* "' + html + ' "*/\n\n';
+
+ // fetch the whole file append it to the comment.
+ content += optimize_getFileContent(webapp, htmlFile, script.src);
+
+ let config = normal;
+
+ if (script.defer)
+ config = deferred;
+
+ config.content += content;
+ config.lastNode = script;
+
+ // some apps (email) use version in the script types
+ // (text/javascript;version=x).
+ //
+ // If we don't have the same version in the aggregate the
+ // app will not load correctly.
+ if (script.type.indexOf('version') !== -1) {
+ config.type = script.type;
+ }
+ });
+
+ // root name like index or oncall, etc...
+ let baseName = htmlFile.path.split('/').pop().split('.')[0];
+
+ // used as basis for aggregated scripts...
+ let rootDirectory = htmlFile.parent;
+
+ // find the absolute root of the app's html file.
+ let rootUrl = htmlFile.parent.path;
+ rootUrl = rootUrl.replace(webapp.manifestFile.parent.path, '');
+ // the above will yield something like: '', '/facebook/', '/contacts/', etc...
+
+ function writeAggregatedScript(config) {
+ // skip if we don't have any content to write.
+ if (!config.content)
+ return;
+
+ // prefix the file we are about to write content to.
+ let scriptBaseName =
+ Gaia.aggregatePrefix + config.prefix + baseName + '.js';
+
+ let target = rootDirectory.clone();
+ target.append(scriptBaseName);
+
+ debug('writing aggregated source file: ' + target.path);
+
+ // write the contents of the aggregated script
+ writeContent(target, config.content);
+
+ let script = doc.createElement('script');
+ let lastScript = config.lastNode;
+
+ script.src = rootUrl + '/' + scriptBaseName;
+ script.defer = lastScript.defer;
+ // use the config's type if given (for text/javascript;version=x)
+ script.type = config.type || lastScript.type;
+
+ debug('writing to path="' + target.path + '" src="' + script.src + '"');
+
+ // insert after the last script node of this type...
+ let parent = lastScript.parentNode;
+ parent.insertBefore(script, lastScript.nextSibling);
+ }
+
+ writeAggregatedScript(deferred);
+ writeAggregatedScript(normal);
+
+ function commentScript(script) {
+ script.outerHTML = '<!-- ' + script.outerHTML + ' -->';
+ }
+
+ // comment out all scripts
+ scripts.forEach(commentScript);
+}
+
+function optimize_embedl10nResources(doc, dictionary) {
+ // remove all external l10n resource nodes
+ var resources = doc.querySelectorAll('link[type="application/l10n"]');
+ for (let i = 0; i < resources.length; i++) {
+ let res = resources[i].outerHTML;
+ resources[i].outerHTML = '<!-- ' + res + ' -->';
+ }
+
+ // put the current dictionary in an inline JSON script
+ let script = doc.createElement('script');
+ script.type = 'application/l10n';
+ script.innerHTML = '\n ' + JSON.stringify(dictionary) + '\n';
+ doc.documentElement.appendChild(script);
+}
+
+function optimize_serializeHTMLDocument(doc, file) {
+ debug('saving: ' + file.path);
+
+ // the doctype string should always be '<!DOCTYPE html>' but just in case...
+ let doctypeStr = '';
+ let dt = doc.doctype;
+ if (dt && dt.name) {
+ doctypeStr = '<!DOCTYPE ' + dt.name;
+ if (dt.publicId) {
+ doctypeStr += ' PUBLIC ' + dt.publicId;
+ }
+ if (dt.systemId) {
+ doctypeStr += ' ' + dt.systemId;
+ }
+ doctypeStr += '>\n';
+ }
+
+ // outerHTML breaks the formating, so let's use innerHTML instead
+ let htmlStr = '<html';
+ let docElt = doc.documentElement;
+ let attrs = docElt.attributes;
+ for (let i = 0; i < attrs.length; i++) {
+ htmlStr += ' ' + attrs[i].nodeName.toLowerCase() +
+ '="' + attrs[i].nodeValue + '"';
+ }
+ let innerHTML = docElt.innerHTML.replace(/ \n*<\/body>\n*/, ' </body>\n');
+ htmlStr += '>\n ' + innerHTML + '\n</html>\n';
+
+ writeContent(file, doctypeStr + htmlStr);
+}
+
+function optimize_compile(webapp, file) {
+ let mozL10n = win.navigator.mozL10n;
+
+ let processedLocales = 0;
+ let dictionary = l10nDictionary;
+
+ // catch console.[log|warn|info] calls and redirect them to `dump()'
+ // XXX for some reason, this won't work if gDEBUG >= 2 in l10n.js
+ function optimize_dump(str) {
+ dump(file.path.replace(GAIA_DIR, '') + ': ' + str + '\n');
+ }
+
+ win.console = {
+ log: optimize_dump,
+ warn: optimize_dump,
+ info: optimize_dump
+ };
+
+ // catch the XHR in `loadResource' and use a local file reader instead
+ win.XMLHttpRequest = function() {
+ debug('loadResource');
+
+ function open(type, url, async) {
+ this.readyState = 4;
+ this.status = 200;
+ this.responseText = optimize_getFileContent(webapp, file, url);
+ }
+
+ function send() {
+ this.onreadystatechange();
+ }
+
+ return {
+ open: open,
+ send: send,
+ onreadystatechange: null
+ };
+ };
+
+ // catch the `localized' event dispatched by `fireL10nReadyEvent()'
+ win.dispatchEvent = function() {
+ processedLocales++;
+ debug('fireL10nReadyEvent - ' +
+ processedLocales + '/' + l10nLocales.length);
+
+ let docElt = win.document.documentElement;
+ dictionary.locales[mozL10n.language.code] = mozL10n.dictionary;
+
+ if (processedLocales < l10nLocales.length) {
+ // load next locale
+ mozL10n.language.code = l10nLocales[processedLocales];
+ } else {
+ // we expect the last locale to be the default one:
+ // set the lang/dir attributes of the current document
+ docElt.dir = mozL10n.language.direction;
+ docElt.lang = mozL10n.language.code;
+
+ // save localized document
+ let newPath = file.path + '.' + GAIA_DEFAULT_LOCALE;
+ let newFile = new FileUtils.File(newPath);
+ optimize_embedl10nResources(win.document, dictionary);
+
+ if (JS_AGGREGATION_WHITELIST.indexOf(webapp.sourceDirectoryName) !== -1) {
+ optimize_aggregateJsResources(win.document, webapp, newFile);
+ dump(
+ '[optimize] aggregating javascript for : "' +
+ webapp.sourceDirectoryName + '" \n'
+ );
+ }
+
+ optimize_serializeHTMLDocument(win.document, newFile);
+ }
+ };
+
+ // load and parse the HTML document
+ let DOMParser = CC('@mozilla.org/xmlextras/domparser;1', 'nsIDOMParser');
+ win.document = (new DOMParser()).
+ parseFromString(getFileContent(file), 'text/html');
+
+ // if this HTML document uses l10n.js, pre-localize it --
+ // selecting a language triggers `XMLHttpRequest' and `dispatchEvent' above
+ if (win.document.querySelector('script[src$="l10n.js"]')) {
+ debug('localizing: ' + file.path);
+ mozL10n.language.code = l10nLocales[processedLocales];
+ }
+}
+
+
+/**
+ * Pre-translate all HTML files for the default locale
+ */
+
+debug('Begin');
+
+if (GAIA_INLINE_LOCALES === '1') {
+ l10nLocales = [];
+ l10nDictionary.locales = {};
+
+ // LOCALES_FILE is a relative path by default: shared/resources/languages.json
+ // -- but it can be an absolute path when doing a multilocale build.
+ // LOCALES_FILE is using unix separator, ensure working fine on win32
+ let abs_path_chunks = [GAIA_DIR].concat(LOCALES_FILE.split('/'));
+ let file = getFile.apply(null, abs_path_chunks);
+ if (!file.exists()) {
+ file = getFile(LOCALES_FILE);
+ }
+ let locales = JSON.parse(getFileContent(file));
+
+ // we keep the default locale order for `l10nDictionary.locales',
+ // but we ensure the default locale comes last in `l10nLocales'.
+ for (let lang in locales) {
+ if (lang != GAIA_DEFAULT_LOCALE) {
+ l10nLocales.push(lang);
+ }
+ l10nDictionary.locales[lang] = {};
+ }
+ l10nLocales.push(GAIA_DEFAULT_LOCALE);
+}
+
+Gaia.webapps.forEach(function(webapp) {
+ // if BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ debug(webapp.sourceDirectoryName);
+
+ let files = ls(webapp.sourceDirectoryFile, true, /^(shared|tests?)$/);
+ files.forEach(function(file) {
+ if (/\.html$/.test(file.leafName)) {
+ optimize_compile(webapp, file);
+ }
+ });
+});
+
+debug('End');
+
diff --git a/build/webapp-zip.js b/build/webapp-zip.js
new file mode 100644
index 0000000..1c6df44
--- /dev/null
+++ b/build/webapp-zip.js
@@ -0,0 +1,293 @@
+
+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;
+
+/**
+ * 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) {
+ if (isSubjectToBranding(file.path)) {
+ file.append((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 + '.' + GAIA_DEFAULT_LOCALE);
+ if (l10nFile.exists()) {
+ zip.addEntryFile(pathInZip,
+ Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ l10nFile,
+ false);
+ return;
+ }
+ }
+
+ let re = new RegExp('\\.html\\.' + GAIA_DEFAULT_LOCALE);
+ if (!zip.hasEntry(pathInZip) && !re.test(file.leafName)) {
+ zip.addEntryFile(pathInZip,
+ Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ file,
+ false);
+ }
+ } 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, file.lastModifiedTime, false);
+
+ // Append a `/` at end of relative path if it isn't already here
+ if (pathInZip.substr(-1) !== '/')
+ pathInZip += '/';
+
+ let files = ls(file);
+ files.forEach(function(subFile) {
+ let subPath = pathInZip + subFile.leafName;
+ addToZip(zip, subPath, subFile);
+ });
+ }
+}
+
+/**
+ * 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) {
+ let dirPath = '/shared/' + dirName + '/';
+
+ // Compute the nsIFile for this shared style
+ let styleFolder = Gaia.sharedFolder.clone();
+ styleFolder.append(dirName);
+ let cssFile = styleFolder.clone();
+ if (!styleFolder.exists()) {
+ throw new Error('Using inexistent shared style: ' + blockName);
+ }
+
+ cssFile.append(blockName + '.css');
+ addToZip(zip, dirPath + blockName + '.css', cssFile);
+
+ // Copy everything but index.html and any other HTML page into the
+ // style/<block> folder.
+ let subFolder = styleFolder.clone();
+ subFolder.append(blockName);
+ 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);
+ });
+}
+
+let webappsTargetDir = Cc['@mozilla.org/file/local;1']
+ .createInstance(Ci.nsILocalFile);
+webappsTargetDir.initWithPath(PROFILE_DIR);
+
+// Create profile folder if doesn't exists
+ensureFolderExists(webappsTargetDir);
+
+// Create webapps folder if doesn't exists
+webappsTargetDir.append('webapps');
+ensureFolderExists(webappsTargetDir);
+
+Gaia.webapps.forEach(function(webapp) {
+ // If BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ // Compute webapp folder name in profile
+ let webappTargetDir = webappsTargetDir.clone();
+ webappTargetDir.append(webapp.domain);
+ 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 = ls(webapp.sourceDirectoryFile);
+ files.forEach(function(file) {
+ // Ignore l10n files if they have been inlined
+ if (GAIA_INLINE_LOCALES &&
+ (file.leafName === 'locales' || file.leafName === 'locales.ini'))
+ return;
+ // Ignore files from /shared directory (these files were created by
+ // Makefile code). Also ignore files in the /test directory.
+ if (file.leafName !== 'shared' && file.leafName !== 'test')
+ addToZip(zip, '/' + file.leafName, file);
+ });
+
+ // 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
+ };
+
+ let files = ls(webapp.sourceDirectoryFile, true);
+ files.filter(function(file) {
+ // Process only files that may require a shared file
+ let extension = file.leafName
+ .substr(file.leafName.lastIndexOf('.') + 1)
+ .toLowerCase();
+ return file.isFile() && EXTENSIONS_WHITELIST.indexOf(extension) != -1;
+ }).
+ forEach(function(file) {
+ // Grep files to find shared/* usages
+ let content = getFileContent(file);
+ while ((matches = SHARED_USAGE.exec(content)) !== null) {
+ let kind = matches[1]; // js | locales | resources | style
+ let path = matches[2];
+ switch (kind) {
+ case 'js':
+ if (used.js.indexOf(path) == -1)
+ used.js.push(path);
+ break;
+ case 'locales':
+ if (!GAIA_INLINE_LOCALES) {
+ 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;
+ }
+ }
+ });
+
+ 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);
+ }
+ addToZip(zip, '/shared/js/' + path, file);
+ });
+
+ 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.');
+
+ // Add the .ini file
+ addToZip(zip, '/shared/locales/' + name + '.ini', ini);
+ // And the locale folder itself
+ addToZip(zip, '/shared/locales/' + name, localeFolder);
+ });
+
+ 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 (isSubjectToBranding(file.path)) {
+ file.append((OFFICIAL == 1) ? 'official' : 'unofficial');
+ }
+ });
+ if (!file.exists()) {
+ throw new Error('Using inexistent shared resource: ' + path +
+ ' from: ' + webapp.domain + '\n');
+ return;
+ }
+ addToZip(zip, '/shared/resources/' + path, file);
+ });
+
+ used.styles.forEach(function(name) {
+ try {
+ copyBuildingBlock(zip, name, 'style');
+ } catch (e) {
+ throw new Error(e + ' from: ' + webapp.domain);
+ }
+ });
+
+ used.unstable_styles.forEach(function(name) {
+ try {
+ copyBuildingBlock(zip, name, 'style_unstable');
+ } catch (e) {
+ throw new Error(e + ' from: ' + webapp.domain);
+ }
+ });
+
+ zip.close();
+});