Bug 1365440 - Generate langpacks as webextensions. r?mshal draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Tue, 30 May 2017 11:33:59 +0200
changeset 646016 693e69ce60e0610e145afca9166ddc7486fc12d1
parent 645074 80ff3f300e05f38f96c385b03d1973a966a2bd35
child 726111 ee5759bc0b0aba97936971471fdcf47bcc3b969b
push id73975
push userbmo:gandalf@aviary.pl
push dateMon, 14 Aug 2017 18:50:54 +0000
reviewersmshal
bugs1365440
milestone57.0a1
Bug 1365440 - Generate langpacks as webextensions. r?mshal Introduce a new make command to produce new type of language packs based on web extensions. MozReview-Commit-ID: EltYtzT7ZRR
python/mozbuild/mozbuild/action/langpack_manifest.py
python/mozbuild/mozbuild/action/zip.py
toolkit/locales/l10n.mk
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/action/langpack_manifest.py
@@ -0,0 +1,268 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+###
+# This script generates a web manifest JSON file based on the xpi-stage
+# directory structure. It extracts the data from defines.inc files from
+# the locale directory, chrome registry entries and other information
+# necessary to produce the complete manifest file for a language pack.
+###
+from __future__ import absolute_import
+
+import argparse
+import sys
+import os
+import json
+import io
+from mozpack.chrome.manifest import (
+    Manifest,
+    ManifestLocale,
+    ManifestOverride,
+    ManifestResource,
+    parse_manifest,
+)
+from mozbuild.preprocessor import Preprocessor
+
+
+def write_file(path, content):
+    with io.open(path, 'w', encoding='utf-8') as out:
+        out.write(content + '\n')
+
+
+###
+# Parses multiple defines files into a single key-value pair object.
+#
+# Args:
+#    paths (str) - a comma separated list of paths to defines files
+#
+# Returns:
+#    (dict) - a key-value dict with defines
+#
+# Example:
+#    res = parse_defines('./toolkit/defines.inc,./browser/defines.inc')
+#    res == {
+#        'MOZ_LANG_TITLE': 'Polski',
+#        'MOZ_LANGPACK_CREATOR': 'Aviary.pl',
+#        'MOZ_LANGPACK_CONTRIBUTORS': 'Marek Stepien, Marek Wawoczny'
+#    }
+###
+def parse_defines(paths):
+    pp = Preprocessor()
+    for path in paths:
+        pp.do_include(path)
+
+    return pp.context
+
+
+###
+# Converts the list of contributors from the old RDF based list
+# of entries, into a comma separated list.
+#
+# Args:
+#    str (str) - a string with an RDF list of contributors entries
+#
+# Returns:
+#    (str) - a comma separated list of contributors
+#
+# Example:
+#    s = convert_contributors('
+#        <em:contributor>Marek Wawoczny</em:contributor>
+#        <em:contributor>Marek Stepien</em:contributor>
+#    ')
+#    s == 'Marek Wawoczny, Marek Stepien'
+###
+def convert_contributors(str):
+    str = str.replace('<em:contributor>', '')
+    tokens = str.split('</em:contributor>')
+    tokens = map(lambda t: t.strip(), tokens)
+    tokens = filter(lambda t: t != '', tokens)
+    return ', '.join(tokens)
+
+
+###
+# Recursively parse a chrome manifest file appending new entries
+# to the result list
+#
+# The function can handle three entry types: 'locale', 'override' and 'resource'
+#
+# Args:
+#    path           (str)  - a path to a chrome manifest
+#    base_path      (str)  - a path to the base directory all chrome registry
+#                            entries will be relative to
+#    chrome_entries (list) - a list to which entries will be appended to
+#
+# Example:
+#
+#    chrome_entries = {}
+#    parse_manifest('./chrome.manifest', './', chrome_entries)
+#
+#    chrome_entries == [
+#        {
+#            'type': 'locale',
+#            'alias': 'devtools',
+#            'locale': 'pl',
+#            'path': 'chrome/pl/locale/pl/devtools/'
+#        },
+#        {
+#            'type': 'locale',
+#            'alias': 'autoconfig',
+#            'locale': 'pl',
+#            'path': 'chrome/pl/locale/pl/autoconfig/'
+#        },
+#    ]
+###
+def parse_chrome_manifest(path, base_path, chrome_entries):
+    for entry in parse_manifest(None, path):
+        if isinstance(entry, Manifest):
+            parse_chrome_manifest(
+                os.path.join(os.path.dirname(path), entry.relpath),
+                base_path,
+                chrome_entries
+            )
+        elif isinstance(entry, ManifestLocale):
+            chrome_entries.append({
+                'type': 'locale',
+                'alias': entry.name,
+                'locale': entry.id,
+                'path': os.path.join(
+                    os.path.relpath(
+                        os.path.dirname(path),
+                        base_path
+                    ),
+                    entry.relpath
+                )
+            })
+        elif isinstance(entry, ManifestOverride):
+            chrome_entries.append({
+                'type': 'override',
+                'real-path': entry.overloaded,
+                'overlay-path': entry.overload
+            })
+        elif isinstance(entry, ManifestResource):
+            chrome_entries.append({
+                'type': 'resource',
+                'alias': entry.name,
+                'path': entry.target
+            })
+        else:
+            raise Exception('Unknown type %s' % entry[0])
+
+
+###
+# Generates a new web manifest dict with values specific for a language pack.
+#
+# Args:
+#    locstr         (str)  - A string with a comma separated list of locales
+#                            for which resources are embedded in the
+#                            language pack
+#    appver         (str)  - A version of the application the language
+#                            resources are for
+#    defines        (dict) - A dictionary of defines entries
+#    chrome_entries (dict) - A dictionary of chrome registry entries
+#
+# Returns:
+#    (dict) - a web manifest
+#
+# Example:
+#    manifest = create_webmanifest(
+#      ['pl'],
+#      '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}',
+#      '55.0a1',
+#      {'MOZ_LANG_TITLE': 'Polski'},
+#      chrome_entries
+#    )
+#    manifest == {
+#        'languages': ['pl'],
+#        'applications': {
+#            'gecko':  {
+#                'strict_min_version': '55.0a1',
+#                'id': '',
+#            }
+#        },
+#        'version': '55.0a1',
+#        'name': 'Polski Language Pack',
+#        ...
+#    }
+###
+def create_webmanifest(locstr, appver, defines, chrome_entries):
+    locales = map(lambda loc: loc.strip(), locstr.split(','))
+    main_locale = locales[0]
+
+    contributors = convert_contributors(defines['MOZ_LANGPACK_CONTRIBUTORS'])
+
+    manifest = {
+        'langpack-id': main_locale,
+        'manifest_version': 2,
+        'applications': {
+            'gecko': {
+                'id': "langpack-" + main_locale + "@mozilla.org",
+                'strict_min_version': appver
+            }
+        },
+        'name': defines['MOZ_LANG_TITLE'] + ' Language Pack',
+        'description': 'Language pack for Firefox for ' + main_locale,
+        'version': appver,
+        'languages': locales,
+        'author': '%s (contributors: %s)' % (defines['MOZ_LANGPACK_CREATOR'], contributors),
+        'chrome_entries': [
+        ]
+    }
+
+    for entry in chrome_entries:
+        line = ''
+        if entry['type'] == 'locale':
+            line = '%s %s %s %s' % (
+                entry['type'],
+                entry['alias'],
+                entry['locale'],
+                entry['path']
+            )
+        elif entry['type'] == 'override':
+            line = '%s %s %s' % (
+                entry['type'],
+                entry['real-path'],
+                entry['overlay-path']
+            )
+        elif entry['type'] == 'resource':
+            line = '%s %s %s' % (
+                entry['type'],
+                entry['alias'],
+                entry['path']
+            )
+        else:
+            raise Exception('Unknown type %s' % entry['type'])
+        manifest['chrome_entries'].append(line)
+    return json.dumps(manifest, indent=2, ensure_ascii=False, encoding='utf8')
+
+
+def main(args):
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--locales',
+                        help='List of language codes provided by the langpack')
+    parser.add_argument('--appver',
+                        help='Version of the application the langpack is for')
+    parser.add_argument('--defines', default=[], nargs='+',
+                        help='List of defines files to load data from')
+    parser.add_argument('--input',
+                        help='Langpack directory.')
+
+    args = parser.parse_args(args)
+
+    chrome_entries = []
+    parse_chrome_manifest(
+        os.path.join(args.input, 'chrome.manifest'), args.input, chrome_entries)
+
+    defines = parse_defines(args.defines)
+
+    res = create_webmanifest(
+        args.locales,
+        args.appver,
+        defines,
+        chrome_entries
+    )
+    write_file(os.path.join(args.input, 'manifest.json'), res)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
--- a/python/mozbuild/mozbuild/action/zip.py
+++ b/python/mozbuild/mozbuild/action/zip.py
@@ -5,37 +5,42 @@
 # This script creates a zip file, but will also strip any binaries
 # it finds before adding them to the zip.
 
 from __future__ import absolute_import
 
 from mozpack.files import FileFinder
 from mozpack.copier import Jarrer
 from mozpack.errors import errors
+from mozpack.path import match
 
 import argparse
 import mozpack.path as mozpath
 import sys
 
+
 def main(args):
     parser = argparse.ArgumentParser()
     parser.add_argument("-C", metavar='DIR', default=".",
                         help="Change to given directory before considering "
                         "other paths")
     parser.add_argument("--strip", action='store_true',
                         help="Strip executables")
+    parser.add_argument("-x", metavar='EXCLUDE', default=[], action='append',
+                        help="Exclude files that match the pattern")
     parser.add_argument("zip", help="Path to zip file to write")
     parser.add_argument("input", nargs="+",
                         help="Path to files to add to zip")
     args = parser.parse_args(args)
 
     jarrer = Jarrer(optimize=False)
 
     with errors.accumulate():
         finder = FileFinder(args.C, find_executables=args.strip)
         for path in args.input:
             for p, f in finder.find(path):
-                jarrer.add(p, f)
+                if not any([match(p, exclude) for exclude in args.x]):
+                    jarrer.add(p, f)
         jarrer.copy(mozpath.join(args.C, args.zip))
 
 
 if __name__ == '__main__':
     main(sys.argv[1:])
--- a/toolkit/locales/l10n.mk
+++ b/toolkit/locales/l10n.mk
@@ -162,16 +162,19 @@ endif
 	mv -f '$(DIST)/l10n-stage/$(PACKAGE)' '$(ZIP_OUT)'
 	if test -f '$(DIST)/l10n-stage/$(PACKAGE).asc'; then mv -f '$(DIST)/l10n-stage/$(PACKAGE).asc' '$(ZIP_OUT).asc'; fi
 
 repackage-zip-%: unpack
 	@$(MAKE) repackage-zip AB_CD=$* ZIP_IN='$(ZIP_IN)'
 
 APP_DEFINES = $(firstword $(wildcard $(LOCALE_SRCDIR)/defines.inc) \
                           $(srcdir)/en-US/defines.inc)
+
+NEW_APP_DEFINES = $(TK_DEFINES) $(firstword $(wildcard $(LOCALE_SRCDIR)/defines.inc) \
+                          $(srcdir)/en-US/defines.inc)
 TK_DEFINES = $(firstword \
    $(wildcard $(call EXPAND_LOCALE_SRCDIR,toolkit/locales)/defines.inc) \
    $(MOZILLA_DIR)/toolkit/locales/en-US/defines.inc)
 
 # Dealing with app sub dirs: If DIST_SUBDIRS is defined it contains a
 # listing of app sub-dirs we should include in langpack xpis. If not,
 # check DIST_SUBDIR, and if that isn't present, just package the default
 # chrome directory.
@@ -211,16 +214,25 @@ langpack-%: IS_LANGUAGE_REPACK=1
 langpack-%: IS_LANGPACK=1
 langpack-%: libs-%
 	@echo 'Making langpack $(LANGPACK_FILE)'
 	$(NSINSTALL) -D $(DIST)/$(PKG_LANGPACK_PATH)
 	$(call py_action,preprocessor,$(DEFINES) $(ACDEFINES) \
 	  -DTK_DEFINES=$(TK_DEFINES) -DAPP_DEFINES=$(APP_DEFINES) $(MOZILLA_DIR)/toolkit/locales/generic/install.rdf -o $(DIST)/xpi-stage/$(XPI_NAME)/install.rdf)
 	$(call py_action,zip,-C $(DIST)/xpi-stage/locale-$(AB_CD) $(LANGPACK_FILE) install.rdf $(PKG_ZIP_DIRS) chrome.manifest)
 
+langpack-webext-%: LANGPACK_FILE=$(ABS_DIST)/$(PKG_LANGPACK_PATH)$(PKG_LANGPACK_BASENAME).xpi
+langpack-webext-%: AB_CD=$*
+langpack-webext-%: XPI_NAME=locale-$*
+langpack-webext-%: libs-%
+	@echo 'Making new-langpack $(LANGPACK_FILE)'
+	$(NSINSTALL) -D $(DIST)/$(PKG_LANGPACK_PATH)
+	$(call py_action,langpack_manifest,--locales $(AB_CD) --appver $(MOZ_APP_VERSION) --defines $(NEW_APP_DEFINES) --input $(DIST)/xpi-stage/locale-$(AB_CD))
+	$(call py_action,zip,-C $(DIST)/xpi-stage/locale-$(AB_CD) -x **/*.manifest -x **/*.js -x **/*.ini $(LANGPACK_FILE) $(PKG_ZIP_DIRS) manifest.json)
+
 # This variable is to allow the wget-en-US target to know which ftp server to download from
 ifndef EN_US_BINARY_URL 
 EN_US_BINARY_URL = $(error You must set EN_US_BINARY_URL)
 endif
 # In taskcluster the installer comes from another location
 ifndef EN_US_INSTALLER_BINARY_URL
 EN_US_INSTALLER_BINARY_URL = $(EN_US_BINARY_URL)
 endif