michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: import os michael@0: import zipfile michael@0: import simplejson as json michael@0: from cuddlefish.util import filter_filenames, filter_dirnames michael@0: michael@0: class HarnessOptionAlreadyDefinedError(Exception): michael@0: """You cannot use --harness-option on keys that already exist in michael@0: harness-options.json""" michael@0: michael@0: ZIPSEP = "/" # always use "/" in zipfiles michael@0: michael@0: def make_zipfile_path(localroot, localpath): michael@0: return ZIPSEP.join(localpath[len(localroot)+1:].split(os.sep)) michael@0: michael@0: def mkzipdir(zf, path): michael@0: dirinfo = zipfile.ZipInfo(path) michael@0: dirinfo.external_attr = int("040755", 8) << 16L michael@0: zf.writestr(dirinfo, "") michael@0: michael@0: def build_xpi(template_root_dir, manifest, xpi_path, michael@0: harness_options, limit_to=None, extra_harness_options={}, michael@0: bundle_sdk=True, pkgdir=""): michael@0: IGNORED_FILES = [".hgignore", ".DS_Store", "install.rdf", michael@0: "application.ini", xpi_path] michael@0: michael@0: files_to_copy = {} # maps zipfile path to local-disk abspath michael@0: dirs_to_create = set() # zipfile paths, no trailing slash michael@0: michael@0: zf = zipfile.ZipFile(xpi_path, "w", zipfile.ZIP_DEFLATED) michael@0: michael@0: open('.install.rdf', 'w').write(str(manifest)) michael@0: zf.write('.install.rdf', 'install.rdf') michael@0: os.remove('.install.rdf') michael@0: michael@0: # Handle add-on icon michael@0: if 'icon' in harness_options: michael@0: zf.write(str(harness_options['icon']), 'icon.png') michael@0: del harness_options['icon'] michael@0: michael@0: if 'icon64' in harness_options: michael@0: zf.write(str(harness_options['icon64']), 'icon64.png') michael@0: del harness_options['icon64'] michael@0: michael@0: # chrome.manifest michael@0: if os.path.isfile(os.path.join(pkgdir, 'chrome.manifest')): michael@0: files_to_copy['chrome.manifest'] = os.path.join(pkgdir, 'chrome.manifest') michael@0: michael@0: # chrome folder (would contain content, skin, and locale folders typically) michael@0: folder = 'chrome' michael@0: if os.path.exists(os.path.join(pkgdir, folder)): michael@0: dirs_to_create.add('chrome') michael@0: # cp -r folder michael@0: abs_dirname = os.path.join(pkgdir, folder) michael@0: for dirpath, dirnames, filenames in os.walk(abs_dirname): michael@0: goodfiles = list(filter_filenames(filenames, IGNORED_FILES)) michael@0: dirnames[:] = filter_dirnames(dirnames) michael@0: for dirname in dirnames: michael@0: arcpath = make_zipfile_path(template_root_dir, michael@0: os.path.join(dirpath, dirname)) michael@0: dirs_to_create.add(arcpath) michael@0: for filename in goodfiles: michael@0: abspath = os.path.join(dirpath, filename) michael@0: arcpath = ZIPSEP.join( michael@0: [folder, michael@0: make_zipfile_path(abs_dirname, os.path.join(dirpath, filename)), michael@0: ]) michael@0: files_to_copy[str(arcpath)] = str(abspath) michael@0: michael@0: # Handle simple-prefs michael@0: if 'preferences' in harness_options: michael@0: from options_xul import parse_options, validate_prefs michael@0: michael@0: validate_prefs(harness_options["preferences"]) michael@0: michael@0: opts_xul = parse_options(harness_options["preferences"], michael@0: harness_options["jetpackID"], michael@0: harness_options["preferencesBranch"]) michael@0: open('.options.xul', 'wb').write(opts_xul.encode("utf-8")) michael@0: zf.write('.options.xul', 'options.xul') michael@0: os.remove('.options.xul') michael@0: michael@0: from options_defaults import parse_options_defaults michael@0: prefs_js = parse_options_defaults(harness_options["preferences"], michael@0: harness_options["preferencesBranch"]) michael@0: open('.prefs.js', 'wb').write(prefs_js.encode("utf-8")) michael@0: michael@0: else: michael@0: open('.prefs.js', 'wb').write("") michael@0: michael@0: zf.write('.prefs.js', 'defaults/preferences/prefs.js') michael@0: os.remove('.prefs.js') michael@0: michael@0: michael@0: for dirpath, dirnames, filenames in os.walk(template_root_dir): michael@0: filenames = list(filter_filenames(filenames, IGNORED_FILES)) michael@0: dirnames[:] = filter_dirnames(dirnames) michael@0: for dirname in dirnames: michael@0: arcpath = make_zipfile_path(template_root_dir, michael@0: os.path.join(dirpath, dirname)) michael@0: dirs_to_create.add(arcpath) michael@0: for filename in filenames: michael@0: abspath = os.path.join(dirpath, filename) michael@0: arcpath = make_zipfile_path(template_root_dir, abspath) michael@0: files_to_copy[arcpath] = abspath michael@0: michael@0: # `packages` attribute contains a dictionnary of dictionnary michael@0: # of all packages sections directories michael@0: for packageName in harness_options['packages']: michael@0: base_arcpath = ZIPSEP.join(['resources', packageName]) michael@0: # Eventually strip sdk files. We need to do that in addition to the michael@0: # whilelist as the whitelist is only used for `cfx xpi`: michael@0: if not bundle_sdk and packageName == 'addon-sdk': michael@0: continue michael@0: # Always write the top directory, even if it contains no files, since michael@0: # the harness will try to access it. michael@0: dirs_to_create.add(base_arcpath) michael@0: for sectionName in harness_options['packages'][packageName]: michael@0: abs_dirname = harness_options['packages'][packageName][sectionName] michael@0: base_arcpath = ZIPSEP.join(['resources', packageName, sectionName]) michael@0: # Always write the top directory, even if it contains no files, since michael@0: # the harness will try to access it. michael@0: dirs_to_create.add(base_arcpath) michael@0: # cp -r stuff from abs_dirname/ into ZIP/resources/RESOURCEBASE/ michael@0: for dirpath, dirnames, filenames in os.walk(abs_dirname): michael@0: goodfiles = list(filter_filenames(filenames, IGNORED_FILES)) michael@0: dirnames[:] = filter_dirnames(dirnames) michael@0: for filename in goodfiles: michael@0: abspath = os.path.join(dirpath, filename) michael@0: if limit_to is not None and abspath not in limit_to: michael@0: continue # strip unused files michael@0: arcpath = ZIPSEP.join( michael@0: ['resources', michael@0: packageName, michael@0: sectionName, michael@0: make_zipfile_path(abs_dirname, michael@0: os.path.join(dirpath, filename)), michael@0: ]) michael@0: files_to_copy[str(arcpath)] = str(abspath) michael@0: del harness_options['packages'] michael@0: michael@0: locales_json_data = {"locales": []} michael@0: mkzipdir(zf, "locale/") michael@0: for language in sorted(harness_options['locale']): michael@0: locales_json_data["locales"].append(language) michael@0: locale = harness_options['locale'][language] michael@0: # Be carefull about strings, we need to always ensure working with UTF-8 michael@0: jsonStr = json.dumps(locale, indent=1, sort_keys=True, ensure_ascii=False) michael@0: info = zipfile.ZipInfo('locale/' + language + '.json') michael@0: info.external_attr = 0644 << 16L michael@0: zf.writestr(info, jsonStr.encode( "utf-8" )) michael@0: del harness_options['locale'] michael@0: michael@0: jsonStr = json.dumps(locales_json_data, ensure_ascii=True) +"\n" michael@0: info = zipfile.ZipInfo('locales.json') michael@0: info.external_attr = 0644 << 16L michael@0: zf.writestr(info, jsonStr.encode("utf-8")) michael@0: michael@0: # now figure out which directories we need: all retained files parents michael@0: for arcpath in files_to_copy: michael@0: bits = arcpath.split("/") michael@0: for i in range(1,len(bits)): michael@0: parentpath = ZIPSEP.join(bits[0:i]) michael@0: dirs_to_create.add(parentpath) michael@0: michael@0: # Create zipfile in alphabetical order, with each directory before its michael@0: # files michael@0: for name in sorted(dirs_to_create.union(set(files_to_copy))): michael@0: if name in dirs_to_create: michael@0: mkzipdir(zf, name+"/") michael@0: if name in files_to_copy: michael@0: zf.write(files_to_copy[name], name) michael@0: michael@0: # Add extra harness options michael@0: harness_options = harness_options.copy() michael@0: for key,value in extra_harness_options.items(): michael@0: if key in harness_options: michael@0: msg = "Can't use --harness-option for existing key '%s'" % key michael@0: raise HarnessOptionAlreadyDefinedError(msg) michael@0: harness_options[key] = value michael@0: michael@0: # Write harness-options.json michael@0: open('.options.json', 'w').write(json.dumps(harness_options, indent=1, michael@0: sort_keys=True)) michael@0: zf.write('.options.json', 'harness-options.json') michael@0: os.remove('.options.json') michael@0: michael@0: zf.close()