From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/python/README | 13 + comm/python/l10n/l10n_clone/l10n_clone.py | 96 ++++ comm/python/l10n/mach_commands.py | 264 ++++++++++ comm/python/l10n/missing_ftl/__init__.py | 59 +++ comm/python/l10n/tb_fluent_migrations/__init__.py | 0 .../bug_1827199_multi_message_view.py | 33 ++ .../bug_1831422_backupKeyPassword.py | 25 + .../bug_1833042_unfied_toolbar_button_style.py | 25 + .../bug_1838109_changeExpiryDlg.py | 27 ++ .../bug_1838770_properties_menu_item.py | 56 +++ .../bug_1703164_calendar_ics_file_dialog.py | 20 + .../bug_1703164_calendar_uri_redirect_dialog.py | 20 + .../completed/bug_1731837_delete_commands.py | 114 +++++ .../bug_1798509_unified_toolbar_customization.py | 52 ++ .../completed/bug_1803705_thread_pane.py | 111 +++++ .../completed/bug_1805746_calendar_view.py | 29 ++ .../bug_1805938_calendar_recurrence_ux.py | 25 + .../bug_1806567_quick_filter_bar_migration.py | 182 +++++++ .../bug_1809312_unified_toolbar_buttons.py | 218 +++++++++ .../bug_1811400_thread_pane_column_picker.py | 79 +++ ...g_1814393_unified_toolbar_customization_tabs.py | 36 ++ .../bug_1814664_unified_toolbar_calendar_items.py | 48 ++ .../bug_1815489_add_back_forward_and_stop.py | 33 ++ ...815605_calendar_context_menu_has_empty_items.py | 52 ++ .../bug_1816532_about_dialog_migration.py | 94 ++++ .../bug_1816593_flexbox_emulation_dialogs.py | 21 + .../completed/bug_1817914_tags_mode.py | 21 + .../completed/bug_1817915_get_new_messages.py | 22 + .../completed/bug_1820700_select_thread.py | 35 ++ .../completed/bug_1823033_activity_indicator.py | 23 + ...act_mode_options_to_folder_mode_context_menu.py | 26 + .../completed/bug_1827891_dnt_prefs_learn_more.py | 21 + .../bug_1828340_aboutdialog_layout_fixes.py | 42 ++ .../completed/bug_1830004_folder_quota.py | 28 ++ .../completed/bug_1834662_cal_enable78.py | 30 ++ .../completed/bug_1834662_extensions_to_fluent.py | 531 +++++++++++++++++++++ comm/python/l10n/tbxchannel/__init__.py | 47 ++ comm/python/l10n/tbxchannel/l10n_merge.py | 26 + .../l10n/tbxchannel/quarantine_to_strings.py | 194 ++++++++ comm/python/l10n/tbxchannel/tb_migration_test.py | 172 +++++++ comm/python/moz.build | 13 + comm/python/mutlh/README.md | 56 +++ comm/python/mutlh/mutlh/__init__.py | 0 comm/python/mutlh/mutlh/decorators.py | 197 ++++++++ comm/python/mutlh/mutlh/site.py | 120 +++++ comm/python/mutlh/mutlh/test/__init__.py | 0 comm/python/mutlh/mutlh/test/conftest.py | 11 + comm/python/mutlh/mutlh/test/python.ini | 11 + comm/python/mutlh/mutlh/test/test_decorators.py | 82 ++++ comm/python/mutlh/mutlh/test/test_site.py | 33 ++ .../mutlh/mutlh/test/test_site_compatibility.py | 194 ++++++++ comm/python/rocboot/README.rst | 20 + comm/python/rocboot/bin/bootstrap.py | 444 +++++++++++++++++ comm/python/rocbuild/rocbuild/__init__.py | 0 comm/python/rocbuild/rocbuild/notify.py | 34 ++ comm/python/sites/tb_common.txt | 7 + comm/python/thirdroc/rnp_generated.py | 116 +++++ comm/python/thirdroc/setup.py | 24 + comm/python/thirdroc/thirdroc/__init__.py | 0 .../python/thirdroc/thirdroc/cmake_define_files.py | 102 ++++ comm/python/thirdroc/thirdroc/rnp_symbols.py | 131 +++++ 61 files changed, 4545 insertions(+) create mode 100644 comm/python/README create mode 100644 comm/python/l10n/l10n_clone/l10n_clone.py create mode 100644 comm/python/l10n/mach_commands.py create mode 100644 comm/python/l10n/missing_ftl/__init__.py create mode 100644 comm/python/l10n/tb_fluent_migrations/__init__.py create mode 100644 comm/python/l10n/tb_fluent_migrations/bug_1827199_multi_message_view.py create mode 100644 comm/python/l10n/tb_fluent_migrations/bug_1831422_backupKeyPassword.py create mode 100644 comm/python/l10n/tb_fluent_migrations/bug_1833042_unfied_toolbar_button_style.py create mode 100644 comm/python/l10n/tb_fluent_migrations/bug_1838109_changeExpiryDlg.py create mode 100644 comm/python/l10n/tb_fluent_migrations/bug_1838770_properties_menu_item.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1703164_calendar_ics_file_dialog.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1703164_calendar_uri_redirect_dialog.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1731837_delete_commands.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1798509_unified_toolbar_customization.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1803705_thread_pane.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1805746_calendar_view.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1805938_calendar_recurrence_ux.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1806567_quick_filter_bar_migration.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1809312_unified_toolbar_buttons.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1811400_thread_pane_column_picker.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1814393_unified_toolbar_customization_tabs.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1814664_unified_toolbar_calendar_items.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1815489_add_back_forward_and_stop.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1815605_calendar_context_menu_has_empty_items.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1816532_about_dialog_migration.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1816593_flexbox_emulation_dialogs.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1817914_tags_mode.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1817915_get_new_messages.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1820700_select_thread.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1823033_activity_indicator.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1827261_add_enable_disable_compact_mode_options_to_folder_mode_context_menu.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1827891_dnt_prefs_learn_more.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1828340_aboutdialog_layout_fixes.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1830004_folder_quota.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1834662_cal_enable78.py create mode 100644 comm/python/l10n/tb_fluent_migrations/completed/bug_1834662_extensions_to_fluent.py create mode 100644 comm/python/l10n/tbxchannel/__init__.py create mode 100644 comm/python/l10n/tbxchannel/l10n_merge.py create mode 100644 comm/python/l10n/tbxchannel/quarantine_to_strings.py create mode 100644 comm/python/l10n/tbxchannel/tb_migration_test.py create mode 100644 comm/python/moz.build create mode 100644 comm/python/mutlh/README.md create mode 100644 comm/python/mutlh/mutlh/__init__.py create mode 100644 comm/python/mutlh/mutlh/decorators.py create mode 100644 comm/python/mutlh/mutlh/site.py create mode 100644 comm/python/mutlh/mutlh/test/__init__.py create mode 100644 comm/python/mutlh/mutlh/test/conftest.py create mode 100644 comm/python/mutlh/mutlh/test/python.ini create mode 100644 comm/python/mutlh/mutlh/test/test_decorators.py create mode 100644 comm/python/mutlh/mutlh/test/test_site.py create mode 100644 comm/python/mutlh/mutlh/test/test_site_compatibility.py create mode 100644 comm/python/rocboot/README.rst create mode 100755 comm/python/rocboot/bin/bootstrap.py create mode 100644 comm/python/rocbuild/rocbuild/__init__.py create mode 100644 comm/python/rocbuild/rocbuild/notify.py create mode 100644 comm/python/sites/tb_common.txt create mode 100644 comm/python/thirdroc/rnp_generated.py create mode 100644 comm/python/thirdroc/setup.py create mode 100644 comm/python/thirdroc/thirdroc/__init__.py create mode 100644 comm/python/thirdroc/thirdroc/cmake_define_files.py create mode 100644 comm/python/thirdroc/thirdroc/rnp_symbols.py (limited to 'comm/python') diff --git a/comm/python/README b/comm/python/README new file mode 100644 index 0000000000..67028b9bff --- /dev/null +++ b/comm/python/README @@ -0,0 +1,13 @@ +This directory contains common Python code for Thunderbird. + +The basic rule is that if Python code is cross-module (that's "module" in the +Mozilla meaning - as in "module ownership") and is MPL-compatible, AND it +applies only to applications build from comm-central derived repositories +(Thunderbird and Seamonkey), it should go here. + +What should not go here: + +- Vendored python modules (use third_party/python instead) +- Python that is not MPL-compatible (see other-licenses/) +- Python that has good reason to remain close to its "owning" (Mozilla) + module (e.g. it is only being consumed from there). diff --git a/comm/python/l10n/l10n_clone/l10n_clone.py b/comm/python/l10n/l10n_clone/l10n_clone.py new file mode 100644 index 0000000000..899ad4a212 --- /dev/null +++ b/comm/python/l10n/l10n_clone/l10n_clone.py @@ -0,0 +1,96 @@ +# 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/. +""" +Download and combine translations from l10n-central and comm-l10n for +use by mach build installers-$AB_CD and mach build langpack-$AB_CD. +""" + +import argparse +import json +import os +import sys +import tempfile +from pathlib import Path + +from mozpack.copier import FileCopier +from mozpack.files import FileFinder +from mozversioncontrol.repoupdate import update_mercurial_repo + +COMM_PATH = (Path(__file__).parent / "../../..").resolve() +GECKO_PATH = COMM_PATH.parent +COMM_PYTHON_L10N = os.path.join(COMM_PATH, "python/l10n") +sys.path.insert(1, COMM_PYTHON_L10N) + +from tbxchannel.l10n_merge import ( + COMM_L10N, + COMM_STRINGS_PATTERNS, + GECKO_STRINGS_PATTERNS, + L10N_CENTRAL, +) + +ALL_LOCALES = [l.rstrip() for l in (COMM_PATH / "mail/locales/all-locales").open().readlines()] + + +def tb_locale(locale): + if locale in ALL_LOCALES: + return locale + raise argparse.ArgumentTypeError("Locale {} invalid.".format(locale)) + + +def get_revision(project, locale): + json_file = { + "browser": GECKO_PATH / "browser/locales/l10n-changesets.json", + "mail": COMM_PATH / "mail/locales/l10n-changesets.json", + }.get(project) + if json_file is None: + raise Exception(f"Invalid project {project} for l10n-changesets.json!") + + with open(json_file) as fp: + changesets = json.load(fp) + + revision = changesets.get(locale, {}).get("revision") + if revision is None: + raise Exception(f"Locale {locale} not found in {project} l10n-changesets.json!") + + return revision + + +def get_strings_repos(locale, destination): + with tempfile.TemporaryDirectory() as tmproot: + central_url = "{}/{}".format(L10N_CENTRAL, locale) + l10n_central = Path(tmproot) / "l10n-central" + l10n_central.mkdir() + central_path = l10n_central / locale + central_revision = get_revision("browser", locale) + update_mercurial_repo("hg", central_url, central_path, revision=central_revision) + + comm_l10n = Path(tmproot) / "comm-l10n" + comm_revision = get_revision("mail", locale) + update_mercurial_repo("hg", COMM_L10N, comm_l10n, revision=comm_revision) + + file_copier = FileCopier() + + def add_to_registry(base_path, patterns): + finder = FileFinder(base_path) + for pattern in patterns: + for _filepath, _fileobj in finder.find(pattern.format(lang=locale)): + file_copier.add(_filepath, _fileobj) + + add_to_registry(l10n_central, GECKO_STRINGS_PATTERNS) + add_to_registry(comm_l10n, COMM_STRINGS_PATTERNS) + + file_copier.copy(str(destination)) + + +def main(): + parser = argparse.ArgumentParser(description="Download translated strings from comm-l10n") + parser.add_argument("locale", help="The locale to download", type=tb_locale) + parser.add_argument("dest_path", help="Path where locale will be downloaded to.", type=Path) + + args = parser.parse_args() + get_strings_repos(args.locale, args.dest_path) + + +if __name__ == "__main__": + main() diff --git a/comm/python/l10n/mach_commands.py b/comm/python/l10n/mach_commands.py new file mode 100644 index 0000000000..e4cfbfcf30 --- /dev/null +++ b/comm/python/l10n/mach_commands.py @@ -0,0 +1,264 @@ +# 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/. + +import argparse +import logging +import os.path +from pathlib import Path + +from mutlh.decorators import Command, CommandArgument + + +# https://stackoverflow.com/a/14117511 +def _positive_int(value): + value = int(value) + if value <= 0: + raise argparse.ArgumentTypeError(f"{value} must be a positive integer.") + return value + + +def _retry_run_process(command_context, *args, error_msg=None, **kwargs): + try: + return command_context.run_process(*args, **kwargs) + except Exception as exc: + raise Exception(error_msg or str(exc)) from exc + + +def _get_rev(command_context, strings_path): + result = [] + + def save_output(line): + result.append(line) + + status = _retry_run_process( + command_context, + [ + "hg", + "--cwd", + str(strings_path), + "log", + "-r", + ".", + "--template", + "{node}\n", + ], + line_handler=save_output, + ) + if status == 0: + return "\n".join(result) + raise Exception(f"Failed to get head revision: {status}") + + +@Command( + "tb-l10n-x-channel", + category="thunderbird", + description="Create cross-channel content for Thunderbird (comm-strings).", +) +@CommandArgument( + "--strings-path", + "-s", + metavar="en-US", + type=Path, + default=Path("en-US"), + help="Path to mercurial repository for comm-strings-quarantine", +) +@CommandArgument( + "--outgoing-path", + "-o", + type=Path, + help="create an outgoing() patch if there are changes", +) +@CommandArgument( + "--attempts", + type=_positive_int, + default=1, + help="Number of times to try (for automation)", +) +@CommandArgument( + "--ssh-secret", + action="store", + help="Taskcluster secret to use to push (for automation)", +) +@CommandArgument( + "actions", + choices=("prep", "create", "push", "clean"), + nargs="+", + # This help block will be poorly formatted until we fix bug 1714239 + help=""" + "prep": clone repos and pull heads. + "create": create the en-US strings commit an optionally create an + outgoing() patch. + "push": push the en-US strings to the quarantine repo. + "clean": clean up any sub-repos. + """, +) +def tb_cross_channel( + command_context, + strings_path, + outgoing_path, + actions, + attempts, + ssh_secret, + **kwargs, +): + """Run Thunderbird's l10n cross-channel content generation.""" + from tbxchannel import TB_XC_NOTIFICATION_TMPL, get_thunderbird_xc_config + from tbxchannel.l10n_merge import COMM_STRINGS_QUARANTINE + + from rocbuild.notify import email_notification + + kwargs.update( + { + "strings_path": strings_path, + "outgoing_path": outgoing_path, + "actions": actions, + "attempts": attempts, + "ssh_secret": ssh_secret, + "get_config": get_thunderbird_xc_config, + } + ) + command_context._mach_context.commands.dispatch( + "l10n-cross-channel", command_context._mach_context, **kwargs + ) + if os.path.exists(outgoing_path): + head_rev = _get_rev(command_context, strings_path) + rev_url = f"{COMM_STRINGS_QUARANTINE}/rev/{head_rev}" + + notification_body = TB_XC_NOTIFICATION_TMPL.format(rev_url=rev_url) + email_notification("X-channel comm-strings-quarantine updated", notification_body) + + +@Command( + "tb-add-missing-ftls", + category="thunderbird", + description="Add missing FTL files after l10n merge.", +) +@CommandArgument( + "--merge", + type=Path, + help="Merge path base", +) +@CommandArgument( + "locale", + type=str, + help="Locale code", +) +def tb_add_missing_ftls(command_context, merge, locale): + """ + Command to create empty FTL files for incomplete localizations to + avoid over-zealous en-US fallback as described in bug 1586984. This + mach command is based on the script used to update the l10n-central + repositories. It gets around the need to have write access to those + repositories in favor of creating the files during l10m-repackaging. + This code assumes that mach compare-locales --merge has already run. + """ + from missing_ftl import add_missing_ftls, get_lang_ftls, get_source_ftls + + print("Checking for missing .ftl files in locale {}".format(locale)) + comm_src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) + source_files = get_source_ftls(comm_src_dir) + + l10n_path = os.path.join(merge, locale) + locale_files = get_lang_ftls(l10n_path) + + add_missing_ftls(l10n_path, source_files, locale_files) + + +@Command( + "tb-fluent-migration-test", + category="thunderbird", + description="Test Fluent migration recipes.", +) +@CommandArgument("test_paths", nargs="*", metavar="N", help="Recipe paths to test.") +def run_migration_tests(command_context, test_paths=None, **kwargs): + if not test_paths: + test_paths = [] + command_context.activate_virtualenv() + + from tbxchannel.tb_migration_test import inspect_migration, prepare_object_dir, test_migration + + rv = 0 + with_context = [] + for to_test in test_paths: + try: + context = inspect_migration(to_test) + for issue in context["issues"]: + command_context.log( + logging.ERROR, + "tb-fluent-migration-test", + { + "error": issue["msg"], + "file": to_test, + }, + "ERROR in {file}: {error}", + ) + if context["issues"]: + continue + with_context.append( + { + "to_test": to_test, + "references": context["references"], + } + ) + except Exception as e: + command_context.log( + logging.ERROR, + "tb-fluent-migration-test", + {"error": str(e), "file": to_test}, + "ERROR in {file}: {error}", + ) + rv |= 1 + obj_dir = prepare_object_dir(command_context) + for context in with_context: + rv |= test_migration(command_context, obj_dir, **context) + return rv + + +from mutlh.decorators import Command, CommandArgument + + +@Command( + "tb-l10n-quarantine-to-strings", + category="thunderbird", + description="Publish quarantines strings to comm-l10n.", +) +@CommandArgument( + "--quarantine-path", + "-q", + type=Path, + help="Path to comm-strings-quarantine repo", +) +@CommandArgument( + "--comm-l10n-path", + "-l", + type=Path, + help="Path to comm-l10n repo", +) +@CommandArgument( + "actions", + choices=("clean", "prep", "migrate", "push"), + nargs="+", + # This help block will be poorly formatted until we fix bug 1714239 + help=""" + "clean": remove existing clones of quarantine and comm-l10n repos + "prep": clone a new repository or update an existing one to latest rev + "migrate": update comm-l10n en_US from quarantine + "push": push comm-l10n + """, +) +def quarantine_to_strings( + command_context, + quarantine_path, + comm_l10n_path, + actions, + **kwargs, +): + """Publish strings in Thunderbird's comm-l10n cross-channel repository from + comm-strings-quarantine.""" + from tbxchannel.quarantine_to_strings import publish_strings + + command_context._set_log_level(True) + command_context.activate_virtualenv() + command_context.log_manager.enable_unstructured() + publish_strings(command_context, quarantine_path, comm_l10n_path, actions, **kwargs) diff --git a/comm/python/l10n/missing_ftl/__init__.py b/comm/python/l10n/missing_ftl/__init__.py new file mode 100644 index 0000000000..1e90db6d5b --- /dev/null +++ b/comm/python/l10n/missing_ftl/__init__.py @@ -0,0 +1,59 @@ +# 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/. + +import os + + +def walk_path(folder_path, prefix): + file_list = [] + for root, dirs, files in os.walk(folder_path, followlinks=True): + for file_name in files: + if os.path.splitext(file_name)[1] == ".ftl": + file_name = os.path.relpath(os.path.join(root, file_name), folder_path) + file_name = os.path.join(prefix, file_name) + file_list.append(file_name) + file_list.sort() + + return file_list + + +def get_source_ftls(comm_src_dir): + """Find ftl files in en-US mail and calendar.""" + file_list = [] + for d in ["mail", "calendar"]: + folder_path = os.path.join(comm_src_dir, d, "locales/en-US") + file_list += walk_path(folder_path, d) + return file_list + + +def get_lang_ftls(l10n_path): + """Find ftl files in the merge directory.""" + file_list = [] + for d in ["mail", "calendar"]: + folder_path = os.path.join(l10n_path, d) + file_list += walk_path(folder_path, d) + return file_list + + +def add_missing_ftls(l10n_path, source_files, locale_files): + """ + For any ftl files that are in source_files but missing in locale_files, + create a placeholder file. + """ + for file_name in source_files: + if file_name not in locale_files: + full_file_name = os.path.join(l10n_path, file_name) + file_path = os.path.dirname(full_file_name) + if not os.path.isdir(file_path): + # Create missing folder + print("Creating missing folder: {}".format(os.path.relpath(file_path, l10n_path))) + os.makedirs(file_path) + + print("Adding missing file: {}".format(file_name)) + with open(full_file_name, "w") as f: + f.write( + "# This Source Code Form is subject to the terms of the Mozilla Public\n" + "# License, v. 2.0. If a copy of the MPL was not distributed with this\n" + "# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n" + ) diff --git a/comm/python/l10n/tb_fluent_migrations/__init__.py b/comm/python/l10n/tb_fluent_migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/comm/python/l10n/tb_fluent_migrations/bug_1827199_multi_message_view.py b/comm/python/l10n/tb_fluent_migrations/bug_1827199_multi_message_view.py new file mode 100644 index 0000000000..47cc910b67 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/bug_1827199_multi_message_view.py @@ -0,0 +1,33 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb import COPY_PATTERN +from fluent.migratetb.helpers import transforms_from + + +def migrate(ctx): + """Bug 1827199 - Message summary preview header buttons are not keyboard accessible""" + + target = reference = "mail/locales/en-US/messenger/multimessageview.ftl" + ctx.add_transforms( + target, + reference, + transforms_from( + """ +multi-message-window-title = + .title = {{COPY_PATTERN(from_path, "window.title")}} + +selected-messages-label = + .label = {{COPY_PATTERN(from_path, "selectedmessages.label")}} + +multi-message-archive-button = + .label = {{COPY_PATTERN(from_path, "archiveButton.label")}} + .tooltiptext = {{COPY_PATTERN(from_path, "archiveButton.label")}} + +multi-message-delete-button = + .label = {{COPY_PATTERN(from_path, "deleteButton.label")}} + .tooltiptext = {{COPY_PATTERN(from_path, "deleteButton.label")}} + """, + from_path="mail/locales/en-US/chrome/messenger/multimessageview.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/bug_1831422_backupKeyPassword.py b/comm/python/l10n/tb_fluent_migrations/bug_1831422_backupKeyPassword.py new file mode 100644 index 0000000000..1c86a338a4 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/bug_1831422_backupKeyPassword.py @@ -0,0 +1,25 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb import COPY_PATTERN +from fluent.migratetb.helpers import transforms_from + + +def migrate(ctx): + """Bug 1831422 - migrations in OpenPGP key backup dialog""" + + target = reference = "mail/messenger/openpgp/backupKeyPassword.ftl" + ctx.add_transforms( + target, + reference, + transforms_from( + """ +set-password-window-title = {{COPY_PATTERN(from_path, "set-password-window.title")}} + +set-password-backup-pw-label = {{COPY_PATTERN(from_path, "set-password-backup-pw.value")}} + +set-password-backup-pw2-label = {{COPY_PATTERN(from_path, "set-password-repeat-backup-pw.value")}} + """, + from_path="mail/messenger/openpgp/backupKeyPassword.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/bug_1833042_unfied_toolbar_button_style.py b/comm/python/l10n/tb_fluent_migrations/bug_1833042_unfied_toolbar_button_style.py new file mode 100644 index 0000000000..108f48474e --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/bug_1833042_unfied_toolbar_button_style.py @@ -0,0 +1,25 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate.helpers import transforms_from +from fluent.migrate import COPY_PATTERN + + +def migrate(ctx): + """Bug 1833042 - Don't automatically update the button style preference when changed in the unified toolbar customization panel, part {index}.""" + ctx.add_transforms( + "mail/messenger/unifiedToolbar.ftl", + "mail/messenger/unifiedToolbar.ftl", + transforms_from( + """ +customize-button-style-icons-beside-text-option = {COPY_PATTERN(from_path, "customize-button-style-icons-beside-text.label")} + +customize-button-style-icons-above-text-option = {COPY_PATTERN(from_path, "customize-button-style-icons-above-text.label")} + +customize-button-style-icons-only-option = {COPY_PATTERN(from_path, "customize-button-style-icons-only.label")} + +customize-button-style-text-only-option = {COPY_PATTERN(from_path, "customize-button-style-text-only.label")} + """, + from_path="mail/messenger/unifiedToolbar.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/bug_1838109_changeExpiryDlg.py b/comm/python/l10n/tb_fluent_migrations/bug_1838109_changeExpiryDlg.py new file mode 100644 index 0000000000..68566c4131 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/bug_1838109_changeExpiryDlg.py @@ -0,0 +1,27 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb import COPY_PATTERN +from fluent.migratetb.helpers import transforms_from + + +def migrate(ctx): + """Bug 1838109 - migrate some strings in changeExpiryDlg.xhtml""" + + target = reference = "mail/messenger/openpgp/changeExpiryDlg.ftl" + ctx.add_transforms( + target, + reference, + transforms_from( + """ +openpgp-change-expiry-title = {{COPY_PATTERN(from_path, "openpgp-change-key-expiry-title.title")}} + +expire-no-change-label = {{COPY_PATTERN(from_path, "expire-dont-change.label")}} + +expire-in-time-label = {{COPY_PATTERN(from_path, "expire-in-label.label")}} + +expire-never-expire-label = {{COPY_PATTERN(from_path, "expire-never-label.label")}} + """, + from_path="mail/messenger/openpgp/changeExpiryDlg.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/bug_1838770_properties_menu_item.py b/comm/python/l10n/tb_fluent_migrations/bug_1838770_properties_menu_item.py new file mode 100644 index 0000000000..04e15628b8 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/bug_1838770_properties_menu_item.py @@ -0,0 +1,56 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb import COPY + +import fluent.syntax.ast as FTL + + +def migrate(ctx): + """Bug 1838770 - Edit > Folder Properties doesn't work, part {index}.""" + source = "mail/chrome/messenger/messenger.dtd" + dest = "mail/messenger/messenger.ftl" + ctx.add_transforms( + dest, + dest, + [ + FTL.Message( + id=FTL.Identifier("menu-edit-properties"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), value=COPY(source, "folderPropsCmd2.label") + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "folderPropsCmd.accesskey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("menu-edit-folder-properties"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "folderPropsFolderCmd2.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "folderPropsCmd.accesskey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("menu-edit-newsgroup-properties"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "folderPropsNewsgroupCmd2.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "folderPropsCmd.accesskey"), + ), + ], + ), + ], + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1703164_calendar_ics_file_dialog.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1703164_calendar_ics_file_dialog.py new file mode 100644 index 0000000000..4e7df08f73 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1703164_calendar_ics_file_dialog.py @@ -0,0 +1,20 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate import COPY_PATTERN +from fluent.migrate.helpers import transforms_from + + +def migrate(ctx): + """Bug 1703164 - convert calendar/base/content/dialogs/calendar-ics-file-dialog.xhtml to html""" + + ctx.add_transforms( + "calendar/calendar/calendar-ics-file-dialog.ftl", + "calendar/calendar/calendar-ics-file-dialog.ftl", + transforms_from( + """ +calendar-ics-file-window-title = {{COPY_PATTERN(from_path, "calendar-ics-file-window-2.title")}} + """, + from_path="calendar/calendar/calendar-ics-file-dialog.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1703164_calendar_uri_redirect_dialog.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1703164_calendar_uri_redirect_dialog.py new file mode 100644 index 0000000000..cd4e681da0 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1703164_calendar_uri_redirect_dialog.py @@ -0,0 +1,20 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate import COPY_PATTERN +from fluent.migrate.helpers import transforms_from + + +def migrate(ctx): + """Bug 1703164 - convert calendar/base/content/dialogs/calendar-uri-redirect-dialog.xhtml to top level html""" + + ctx.add_transforms( + "calendar/calendar/calendar-uri-redirect-dialog.ftl", + "calendar/calendar/calendar-uri-redirect-dialog.ftl", + transforms_from( + """ +calendar-uri-redirect-window-title = {{COPY_PATTERN(from_path, "calendar-uri-redirect-window.title")}} + """, + from_path="calendar/calendar/calendar-uri-redirect-dialog.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1731837_delete_commands.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1731837_delete_commands.py new file mode 100644 index 0000000000..fc045d1ed6 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1731837_delete_commands.py @@ -0,0 +1,114 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import re + +from fluent.migratetb import COPY, COPY_PATTERN +from fluent.migratetb.helpers import VARIABLE_REFERENCE +from fluent.migratetb.transforms import Transform, TransformPattern + +import fluent.syntax.ast as FTL + + +def migrate(ctx): + """Bug 1731837 - Fix multiple problems with the labelling of Delete commands, part {index}.""" + source = "mail/chrome/messenger/messenger.dtd" + dest = "mail/messenger/messenger.ftl" + ctx.add_transforms( + dest, + dest, + [ + FTL.Message( + id=FTL.Identifier("menu-edit-delete-folder"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), value=COPY(source, "deleteFolderCmd.label") + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "deleteFolderCmd.accesskey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("menu-edit-delete-messages"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=Transform.pattern_of( + FTL.SelectExpression( + selector=VARIABLE_REFERENCE("count"), + variants=[ + FTL.Variant( + key=FTL.Identifier("one"), + value=COPY(source, "deleteMsgCmd.label"), + ), + FTL.Variant( + key=FTL.Identifier("other"), + default=True, + value=COPY(source, "deleteMsgsCmd.label"), + ), + ], + ) + ), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "deleteMsgCmd.accesskey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("menu-edit-undelete-messages"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=Transform.pattern_of( + FTL.SelectExpression( + selector=VARIABLE_REFERENCE("count"), + variants=[ + FTL.Variant( + key=FTL.Identifier("one"), + value=COPY(source, "undeleteMsgCmd.label"), + ), + FTL.Variant( + key=FTL.Identifier("other"), + default=True, + value=COPY(source, "undeleteMsgsCmd.label"), + ), + ], + ) + ), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "undeleteMsgCmd.accesskey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("mail-context-undelete-messages"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=Transform.pattern_of( + FTL.SelectExpression( + selector=VARIABLE_REFERENCE("count"), + variants=[ + FTL.Variant( + key=FTL.Identifier("one"), + value=COPY(source, "undeleteMsgCmd.label"), + ), + FTL.Variant( + key=FTL.Identifier("other"), + default=True, + value=COPY(source, "undeleteMsgsCmd.label"), + ), + ], + ) + ), + ) + ], + ), + ], + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1798509_unified_toolbar_customization.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1798509_unified_toolbar_customization.py new file mode 100644 index 0000000000..7ac296ec35 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1798509_unified_toolbar_customization.py @@ -0,0 +1,52 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate import COPY_PATTERN +from fluent.migrate.helpers import transforms_from + + +def migrate(ctx): + """Bug 1798509 - Unified toolbar customization fluent migration part {index}.""" + + ctx.add_transforms( + "mail/messenger/unifiedToolbar.ftl", + "mail/messenger/unifiedToolbar.ftl", + transforms_from( + """ +customize-menu-customize = + .label = { COPY(from_path, "customizeToolbar.label") } + """, + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) + ctx.add_transforms( + "mail/messenger/unifiedToolbar.ftl", + "mail/messenger/unifiedToolbar.ftl", + transforms_from( + """ +customize-space-mail = { COPY_PATTERN(from_path, "spaces-toolbar-button-mail2.title") } + +customize-space-addressbook = { COPY_PATTERN(from_path, "spaces-toolbar-button-address-book2.title") } + +customize-space-calendar = { COPY_PATTERN(from_path, "spaces-toolbar-button-calendar2.title") } + +customize-space-tasks = { COPY_PATTERN(from_path, "spaces-toolbar-button-tasks2.title") } + +customize-space-chat = { COPY_PATTERN(from_path, "spaces-toolbar-button-chat2.title") } + +customize-space-settings = { COPY_PATTERN(from_path, "spaces-toolbar-button-settings2.title") } + """, + from_path="mail/messenger/messenger.ftl", + ), + ) + ctx.add_transforms( + "mail/messenger/unifiedToolbar.ftl", + "mail/messenger/unifiedToolbar.ftl", + transforms_from( + """ +customize-button-style-icons-beside-text = + .label = { COPY(from_path, "iconsBesideText.label") } + """, + from_path="mail/chrome/messenger/customizeToolbar.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1803705_thread_pane.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1803705_thread_pane.py new file mode 100644 index 0000000000..b3f41b1c5a --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1803705_thread_pane.py @@ -0,0 +1,111 @@ +# coding=utf8 + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate.helpers import transforms_from +from fluent.migrate import COPY + + +def migrate(ctx): + """Bug 1803705 - Migrate thread tree strings to fluent, part {index}.""" + + ctx.add_transforms( + "mail/messenger/treeView.ftl", + "mail/messenger/treeView.ftl", + transforms_from( + """ +tree-list-view-column-picker = + .title = { COPY(from_path, "columnChooser2.tooltip") } +""", + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) + + ctx.add_transforms( + "mail/messenger/about3Pane.ftl", + "mail/messenger/about3Pane.ftl", + transforms_from( + """ +threadpane-column-header-select = + .title = { COPY(from_path, "selectColumn.tooltip") } +threadpane-column-label-select = + .label = { COPY(from_path, "selectColumn.label") } +threadpane-column-label-thread = + .label = { COPY(from_path, "threadColumn.label") } +threadpane-column-header-flagged = + .title = { COPY(from_path, "starredColumn2.tooltip") } +threadpane-column-label-flagged = + .label = { COPY(from_path, "starredColumn.label") } +threadpane-column-header-attachments = + .title = { COPY(from_path, "attachmentColumn2.tooltip") } +threadpane-column-label-attachments = + .label = { COPY(from_path, "attachmentColumn.label") } +threadpane-column-header-sender = { COPY(from_path, "fromColumn.label") } + .title = { COPY(from_path, "fromColumn2.tooltip") } +threadpane-column-label-sender = + .label = { COPY(from_path, "fromColumn.label") } +threadpane-column-header-recipient = { COPY(from_path, "recipientColumn.label") } + .title = { COPY(from_path, "recipientColumn2.tooltip") } +threadpane-column-label-recipient = + .label = { COPY(from_path, "recipientColumn.label") } +threadpane-column-header-correspondents = { COPY(from_path, "correspondentColumn.label") } + .title = { COPY(from_path, "correspondentColumn2.tooltip") } +threadpane-column-label-correspondents = + .label = { COPY(from_path, "correspondentColumn.label") } +threadpane-column-header-subject = { COPY(from_path, "subjectColumn.label") } + .title = { COPY(from_path, "subjectColumn2.tooltip") } +threadpane-column-label-subject = + .label = { COPY(from_path, "subjectColumn.label") } +threadpane-column-header-date = { COPY(from_path, "dateColumn.label") } + .title = { COPY(from_path, "dateColumn2.tooltip") } +threadpane-column-label-date = + .label = { COPY(from_path, "dateColumn.label") } +threadpane-column-header-received = { COPY(from_path, "receivedColumn.label") } + .title = { COPY(from_path, "receivedColumn2.tooltip") } +threadpane-column-label-received = + .label = { COPY(from_path, "receivedColumn.label") } +threadpane-column-header-status = { COPY(from_path, "statusColumn.label") } + .title = { COPY(from_path, "statusColumn2.tooltip") } +threadpane-column-label-status = + .label = { COPY(from_path, "statusColumn.label") } +threadpane-column-header-size = { COPY(from_path, "sizeColumn.label") } + .title = { COPY(from_path, "sizeColumn2.tooltip") } +threadpane-column-label-size = + .label = { COPY(from_path, "sizeColumn.label") } +threadpane-column-header-tags = { COPY(from_path, "tagsColumn.label") } + .title = { COPY(from_path, "tagsColumn2.tooltip") } +threadpane-column-label-tags = + .label = { COPY(from_path, "tagsColumn.label") } +threadpane-column-header-account = { COPY(from_path, "accountColumn.label") } + .title = { COPY(from_path, "accountColumn2.tooltip") } +threadpane-column-label-account = + .label = { COPY(from_path, "accountColumn.label") } +threadpane-column-header-priority = { COPY(from_path, "priorityColumn.label") } + .title = { COPY(from_path, "priorityColumn2.tooltip") } +threadpane-column-label-priority = + .label = { COPY(from_path, "priorityColumn.label") } +threadpane-column-header-unread = { COPY(from_path, "unreadColumn.label") } + .title = { COPY(from_path, "unreadColumn2.tooltip") } +threadpane-column-label-unread = + .label = { COPY(from_path, "unreadColumn.label") } +threadpane-column-header-total = { COPY(from_path, "totalColumn.label") } + .title = { COPY(from_path, "totalColumn2.tooltip") } +threadpane-column-label-total = + .label = { COPY(from_path, "totalColumn.label") } +threadpane-column-header-location = { COPY(from_path, "locationColumn.label") } + .title = { COPY(from_path, "locationColumn2.tooltip") } +threadpane-column-label-location = + .label = { COPY(from_path, "locationColumn.label") } +threadpane-column-header-id = { COPY(from_path, "idColumn.label") } + .title = { COPY(from_path, "idColumn2.tooltip") } +threadpane-column-label-id = + .label = { COPY(from_path, "idColumn.label") } +threadpane-column-header-delete = + .title = { COPY(from_path, "deleteColumn.tooltip") } +threadpane-column-label-delete = + .label = { COPY(from_path, "deleteColumn.label") } +""", + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1805746_calendar_view.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1805746_calendar_view.py new file mode 100644 index 0000000000..eec0fbe44e --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1805746_calendar_view.py @@ -0,0 +1,29 @@ +# coding=utf8 + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate.helpers import transforms_from +from fluent.migrate import COPY + + +def migrate(ctx): + """Bug 1805746 - Update Calendar View selection part {index}.""" + + ctx.add_transforms( + "calendar/calendar/calendar-widgets.ftl", + "calendar/calendar/calendar-widgets.ftl", + transforms_from( + """ +calendar-view-toggle-day = { COPY(from_path, "calendar.day.button.label") } + .title = { COPY(from_path, "calendar.day.button.tooltip") } +calendar-view-toggle-week = { COPY(from_path, "calendar.week.button.label") } + .title = { COPY(from_path, "calendar.week.button.tooltip") } +calendar-view-toggle-multiweek = { COPY(from_path, "calendar.multiweek.button.label") } + .title = { COPY(from_path, "calendar.multiweek.button.tooltip") } +calendar-view-toggle-month = { COPY(from_path, "calendar.month.button.label") } + .title = { COPY(from_path, "calendar.month.button.tooltip") } +""", + from_path="calendar/chrome/calendar/calendar.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1805938_calendar_recurrence_ux.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1805938_calendar_recurrence_ux.py new file mode 100644 index 0000000000..715b475015 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1805938_calendar_recurrence_ux.py @@ -0,0 +1,25 @@ +# coding=utf8 + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate.helpers import transforms_from +from fluent.migrate import COPY + + +def migrate(ctx): + """Bug 1805938 - Refactor recurrence calendar UX part {index}.""" + + source = "calendar/chrome/calendar/calendar-event-dialog.dtd" + reference = target = "calendar/calendar/calendar-recurrence-dialog.ftl" + + ctx.add_transforms( + target, + reference, + transforms_from( + """ +calendar-recurrence-preview-label = { COPY(from_path, "event.recurrence.preview.label") } +""", + from_path=source, + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1806567_quick_filter_bar_migration.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1806567_quick_filter_bar_migration.py new file mode 100644 index 0000000000..72967ef4d8 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1806567_quick_filter_bar_migration.py @@ -0,0 +1,182 @@ +# coding=utf8 + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import fluent.syntax.ast as FTL +from fluent.migrate.helpers import ( + MESSAGE_REFERENCE, + VARIABLE_REFERENCE, + transforms_from, +) +from fluent.migrate.transforms import ( + CONCAT, + PLURALS, + REPLACE, + REPLACE_IN_TEXT, + Transform, +) + + +def migrate(ctx): + """Bug 1806567 - Migrate Quick Filter Bar strings from DTD to FTL, part {index}""" + + source = "mail/chrome/messenger/quickFilterBar.dtd" + target1 = reference1 = "mail/messenger/about3Pane.ftl" + ctx.add_transforms( + target1, + reference1, + transforms_from( + """ + +quick-filter-bar-sticky = + .title = { COPY(from_path, "quickFilterBar.sticky.tooltip") } +quick-filter-bar-unread = + .title = { COPY(from_path, "quickFilterBar.unread.tooltip") } +quick-filter-bar-unread-label = { COPY(from_path, "quickFilterBar.unread.label") } +quick-filter-bar-starred = + .title = { COPY(from_path, "quickFilterBar.starred.tooltip") } +quick-filter-bar-starred-label = { COPY(from_path, "quickFilterBar.starred.label") } +quick-filter-bar-inaddrbook = + .title = { COPY(from_path, "quickFilterBar.inaddrbook.tooltip") } +quick-filter-bar-inaddrbook-label = { COPY(from_path, "quickFilterBar.inaddrbook.label") } +quick-filter-bar-tags = + .title = { COPY(from_path, "quickFilterBar.tags.tooltip") } +quick-filter-bar-tags-label = { COPY(from_path, "quickFilterBar.tags.label") } +quick-filter-bar-attachment = + .title = { COPY(from_path, "quickFilterBar.attachment.tooltip") } +quick-filter-bar-attachment-label = { COPY(from_path, "quickFilterBar.attachment.label") } +quick-filter-bar-no-results = { COPY(from_path, "quickFilterBar.resultsLabel.none") } +""", + from_path=source, + ), + ) + ctx.add_transforms( + target1, + reference1, + [ + FTL.Message( + id=FTL.Identifier("quick-filter-bar-results"), + value=PLURALS( + source, + "quickFilterBar.resultsLabel.some.formatString", + VARIABLE_REFERENCE("count"), + lambda text: REPLACE_IN_TEXT(text, {"#1": VARIABLE_REFERENCE("count")}), + ), + ), + FTL.Message( + id=FTL.Identifier("quick-filter-bar-textbox-shortcut"), + value=Transform.pattern_of( + FTL.SelectExpression( + selector=FTL.FunctionReference( + id=FTL.Identifier("PLATFORM"), + arguments=FTL.CallArguments(), + ), + variants=[ + FTL.Variant( + key=FTL.Identifier("macos"), + default=False, + value=REPLACE( + source, + "quickFilterBar.textbox.emptyText.keyLabel2.mac", + { + "<": FTL.TextElement(""), + "⇧⌘": FTL.TextElement("⇧ ⌘ "), + ">": FTL.TextElement(""), + }, + ), + ), + FTL.Variant( + key=FTL.Identifier("other"), + default=True, + value=REPLACE( + source, + "quickFilterBar.textbox.emptyText.keyLabel2.nonmac", + { + "<": FTL.TextElement(""), + ">": FTL.TextElement(""), + }, + ), + ), + ], + ) + ), + ), + FTL.Message( + id=FTL.Identifier("quick-filter-bar-textbox"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("placeholder"), + value=REPLACE( + source, + "quickFilterBar.textbox.emptyText.base1", + { + "#1": CONCAT( + FTL.TextElement("<"), + MESSAGE_REFERENCE("quick-filter-bar-textbox-shortcut"), + FTL.TextElement(">"), + ) + }, + ), + ), + ], + ), + ], + ) + ctx.add_transforms( + target1, + reference1, + transforms_from( + """ +quick-filter-bar-boolean-mode = + .title = { COPY(from_path, "quickFilterBar.booleanMode.tooltip") } +quick-filter-bar-boolean-mode-any = + .label = { COPY(from_path, "quickFilterBar.booleanModeAny.label") } + .title = { COPY(from_path, "quickFilterBar.booleanModeAny.tooltip") } +quick-filter-bar-boolean-mode-all = + .label = { COPY(from_path, "quickFilterBar.booleanModeAll.label") } + .title = { COPY(from_path, "quickFilterBar.booleanModeAll.tooltip") } +quick-filter-bar-text-filter-explanation = { COPY(from_path, "quickFilterBar.textFilter.explanation.label") } +quick-filter-bar-text-filter-sender = { COPY(from_path, "quickFilterBar.textFilter.sender.label") } +quick-filter-bar-text-filter-recipients = { COPY(from_path, "quickFilterBar.textFilter.recipients.label") } +quick-filter-bar-text-filter-subject = { COPY(from_path, "quickFilterBar.textFilter.subject.label") } +quick-filter-bar-text-filter-body = { COPY(from_path, "quickFilterBar.textFilter.body.label") } +quick-filter-bar-gloda-upsell-line1 = { COPY(from_path, "quickFilterBar.glodaUpsell.continueSearch") } +""", + from_path=source, + ), + ) + ctx.add_transforms( + target1, + reference1, + [ + FTL.Message( + id=FTL.Identifier("quick-filter-bar-gloda-upsell-line2"), + value=REPLACE( + source, + "quickFilterBar.glodaUpsell.pressEnterAndCurrent", + { + "#1": VARIABLE_REFERENCE("text"), + " '": FTL.TextElement(" ‘"), + "' ": FTL.TextElement("’ "), + }, + ), + ), + ], + ) + + target2 = reference2 = "mail/messenger/messenger.ftl" + ctx.add_transforms( + target2, + reference2, + transforms_from( + """ +quick-filter-bar-toggle = + .label = { COPY(from_path, "quickFilterBar.toggleBarVisibility.menu.label") } + .accesskey = { COPY(from_path, "quickFilterBar.toggleBarVisibility.menu.accesskey") } +quick-filter-bar-show = + .key = { COPY(from_path, "quickFilterBar.show.key2") } +""", + from_path=source, + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1809312_unified_toolbar_buttons.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1809312_unified_toolbar_buttons.py new file mode 100644 index 0000000000..6558f5d6a7 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1809312_unified_toolbar_buttons.py @@ -0,0 +1,218 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate import COPY_PATTERN +from fluent.migrate.helpers import transforms_from + + +def migrate(ctx): + """Bug 1809312 - Implement mail space actions for unified toolbar fluent migration part {index}.""" + + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +spacer-label = { COPY(from_path, "springTitle") } + """, + from_path="mail/chrome/messenger/customizeToolbar.properties", + ), + ) + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-write-message-label = { COPY(from_path, "newMsgButton.label") } + +toolbar-write-message = + .title = { COPY(from_path, "newMsgButton.tooltip") } + +toolbar-folder-location-label = { COPY(from_path, "folderLocationToolbarItem.title") } + +toolbar-get-messages-label = { COPY(from_path, "getMsgButton1.label") } + +toolbar-reply-label = { COPY(from_path, "replyButton.label") } + +toolbar-reply = + .title = { COPY(from_path, "replyButton.tooltip") } + +toolbar-reply-all-label = { COPY(from_path, "replyAllButton.label") } + +toolbar-reply-all = + .title = { COPY(from_path, "replyAllButton.tooltip") } + +toolbar-reply-to-list-label = { COPY(from_path, "replyListButton.label") } + +toolbar-reply-to-list = + .title = { COPY(from_path, "replyListButton.tooltip") } + +toolbar-archive-label = { COPY(from_path, "archiveButton.label") } + +toolbar-archive = + .title = { COPY(from_path, "archiveButton.tooltip") } + +toolbar-conversation-label = { COPY(from_path, "openConversationButton.label") } + +toolbar-conversation = + .title = { COPY(from_path, "openMsgConversationButton.tooltip") } + +toolbar-previous-unread-label = { COPY(from_path, "previousButtonToolbarItem.label") } + +toolbar-previous-unread = + .title = { COPY(from_path, "previousButton.tooltip") } + +toolbar-previous-label = { COPY(from_path, "previousButton.label") } + +toolbar-previous = + .title = { COPY(from_path, "previousMsgButton.tooltip") } + +toolbar-next-unread-label = { COPY(from_path, "nextButtonToolbarItem.label") } + +toolbar-next-unread = + .title = { COPY(from_path, "nextButton.tooltip") } + +toolbar-next-label = { COPY(from_path, "nextMsgButton.label") } + +toolbar-next = + .title = { COPY(from_path, "nextMsgButton.tooltip") } + +toolbar-compact-label = { COPY(from_path, "compactButton.label") } + +toolbar-compact = + .title = { COPY(from_path, "compactButton.tooltip") } + +toolbar-tag-message-label = { COPY(from_path, "tagButton.label") } + +toolbar-tag-message = + .title = { COPY(from_path, "tagButton.tooltip") } + +toolbar-forward-inline-label = { COPY(from_path, "forwardButton.label") } + +toolbar-forward-inline = + .title = { COPY(from_path, "forwardAsInline.tooltip") } + +toolbar-forward-attachment-label = { COPY(from_path, "buttonMenuForwardAsAttachment.label") } + +toolbar-forward-attachment = + .title = { COPY(from_path, "forwardAsAttachment.tooltip") } + +toolbar-mark-as-label = { COPY(from_path, "markButton.label") } + +toolbar-mark-as = + .title = { COPY(from_path, "markButton.tooltip") } + +toolbar-address-book-label = { COPY(from_path, "addressBookButton.title") } + +toolbar-address-book = + .title = { COPY(from_path, "addressBookButton.tooltip") } + +toolbar-chat-label = { COPY(from_path, "chatButton.label") } + +toolbar-chat = + .title = { COPY(from_path, "chatButton.tooltip") } + +toolbar-print-label = { COPY(from_path, "printButton.label") } + +toolbar-print = + .title = { COPY(from_path, "printButton.tooltip") } + """, + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-unifinder-label = { COPY(from_path, "showUnifinderCmd.label") } + +toolbar-unifinder = + .title = { COPY(from_path, "showUnifinderCmd.tooltip") } + """, + from_path="calendar/chrome/calendar/menuOverlay.dtd", + ), + ) + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-edit-event-label = { COPY(from_path, "lightning.toolbar.edit.label") } + +toolbar-edit-event = + .title = { COPY(from_path, "lightning.toolbar.edit.tooltip") } + +toolbar-calendar-label = { COPY(from_path, "lightning.toolbar.calendar.label") } + +toolbar-calendar = + .title = { COPY(from_path, "lightning.toolbar.calendar.tooltip") } + +toolbar-tasks-label = { COPY(from_path, "lightning.toolbar.task.label") } + +toolbar-tasks = + .title = { COPY(from_path, "lightning.toolbar.task.tooltip") } + """, + from_path="calendar/chrome/lightning/lightning-toolbar.dtd", + ), + ) + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-redirect-label = { COPY_PATTERN(from_path, "redirect-msg-button.label") } + +toolbar-redirect = + .title = { COPY_PATTERN(from_path, "redirect-msg-button.tooltiptext") } + +toolbar-add-ons-and-themes-label = { COPY_PATTERN(from_path, "addons-and-themes-toolbarbutton.label") } + +toolbar-add-ons-and-themes = + .title = { COPY_PATTERN(from_path, "addons-and-themes-toolbarbutton.tooltiptext") } + + +toolbar-quick-filter-bar-label = { COPY_PATTERN(from_path, "quick-filter-toolbarbutton.label") } + +toolbar-quick-filter-bar = + .title = { COPY_PATTERN(from_path, "quick-filter-toolbarbutton.tooltiptext") } + """, + from_path="mail/messenger/messenger.ftl", + ), + ) + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-junk-label = { COPY_PATTERN(from_path, "toolbar-junk-button.label") } + +toolbar-junk = + .title = { COPY_PATTERN(from_path, "toolbar-junk-button.tooltiptext") } + +toolbar-delete-label = { COPY_PATTERN(from_path, "toolbar-delete-button.label") } + +toolbar-delete = + .title = { COPY_PATTERN(from_path, "toolbar-delete-button.tooltiptext") } + """, + from_path="mail/messenger/menubar.ftl", + ), + ) + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-add-as-event-label = { COPY(from_path, "calendar.extract.event.button") } + +toolbar-add-as-event = + .title = { COPY(from_path, "calendar.extract.event.button.tooltip") } + +toolbar-add-as-task-label = { COPY(from_path, "calendar.extract.task.button") } + +toolbar-add-as-task = + .title = { COPY(from_path, "calendar.extract.task.button.tooltip") } + """, + from_path="calendar/chrome/calendar/calendar.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1811400_thread_pane_column_picker.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1811400_thread_pane_column_picker.py new file mode 100644 index 0000000000..925a4cbbb7 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1811400_thread_pane_column_picker.py @@ -0,0 +1,79 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import fluent.syntax.ast as FTL +from fluent.migrate.helpers import VARIABLE_REFERENCE, transforms_from +from fluent.migrate.transforms import REPLACE + + +def migrate(ctx): + """Bug 1811400 - Migrate thread pane strings, part {index}.""" + + ctx.add_transforms( + "mail/messenger/about3Pane.ftl", + "mail/messenger/about3Pane.ftl", + transforms_from( + """ +apply-columns-to-menu = + .label = { COPY(from_path, "columnPicker.applyTo.label") } + +apply-current-view-to-folder = + .label = { COPY(from_path, "columnPicker.applyToFolder.label") } + +apply-current-view-to-folder-children = + .label = { COPY(from_path, "columnPicker.applyToFolderAndChildren.label") } + """, + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) + ctx.add_transforms( + "mail/messenger/about3Pane.ftl", + "mail/messenger/about3Pane.ftl", + transforms_from( + """ +apply-current-view-to-menu = + .label = { COPY_PATTERN(from_path, "apply-current-view-to-menu.label") } + +apply-changes-to-folder-title = { + COPY_PATTERN(from_path, "threadpane-apply-changes-prompt-title") +} + +apply-current-view-to-folder-message = { + COPY_PATTERN(from_path, "threadpane-apply-changes-prompt-no-children-text") +} + +apply-current-view-to-folder-with-children-message = { + COPY_PATTERN(from_path, "threadpane-apply-changes-prompt-with-children-text") +} + """, + from_path="mail/messenger/mailWidgets.ftl", + ), + ) + ctx.add_transforms( + "mail/messenger/about3Pane.ftl", + "mail/messenger/about3Pane.ftl", + [ + FTL.Message( + id=FTL.Identifier("apply-current-columns-to-folder-message"), + value=REPLACE( + "mail/chrome/messenger/messenger.properties", + "threadPane.columnPicker.confirmFolder.noChildren.message", + { + "%1$S": VARIABLE_REFERENCE("name"), + }, + normalize_printf=True, + ), + ), + FTL.Message( + id=FTL.Identifier("apply-current-columns-to-folder-with-children-message"), + value=REPLACE( + "mail/chrome/messenger/messenger.properties", + "threadPane.columnPicker.confirmFolder.withChildren.message", + { + "%1$S": VARIABLE_REFERENCE("name"), + }, + normalize_printf=True, + ), + ), + ], + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1814393_unified_toolbar_customization_tabs.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1814393_unified_toolbar_customization_tabs.py new file mode 100644 index 0000000000..5f4c3b469b --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1814393_unified_toolbar_customization_tabs.py @@ -0,0 +1,36 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate import COPY_PATTERN +from fluent.migrate.helpers import transforms_from + + +def migrate(ctx): + """Bug 1814393 - Unified toolbar customization tab titles part {index}.""" + + ctx.add_transforms( + "mail/messenger/unifiedToolbar.ftl", + "mail/messenger/unifiedToolbar.ftl", + transforms_from( + """ +customize-space-tab-mail = { COPY_PATTERN(from_path, "customize-space-mail") } + .title = { COPY_PATTERN(from_path, "customize-space-mail") } + +customize-space-tab-addressbook = { COPY_PATTERN(from_path, "customize-space-addressbook") } + .title = { COPY_PATTERN(from_path, "customize-space-addressbook") } + +customize-space-tab-calendar = { COPY_PATTERN(from_path, "customize-space-calendar") } + .title = { COPY_PATTERN(from_path, "customize-space-calendar") } + +customize-space-tab-tasks = { COPY_PATTERN(from_path, "customize-space-tasks") } + .title = { COPY_PATTERN(from_path, "customize-space-tasks") } + +customize-space-tab-chat = { COPY_PATTERN(from_path, "customize-space-chat") } + .title = { COPY_PATTERN(from_path, "customize-space-chat") } + +customize-space-tab-settings = { COPY_PATTERN(from_path, "customize-space-settings") } + .title = { COPY_PATTERN(from_path, "customize-space-settings") } + """, + from_path="mail/messenger/unifiedToolbar.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1814664_unified_toolbar_calendar_items.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1814664_unified_toolbar_calendar_items.py new file mode 100644 index 0000000000..5985124c42 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1814664_unified_toolbar_calendar_items.py @@ -0,0 +1,48 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate import COPY_PATTERN +from fluent.migrate.helpers import transforms_from + + +def migrate(ctx): + """Bug 1814664 - Add calendar items to unified toolbar fluent migration part {index}.""" + + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-synchronize-label = { COPY(from_path, "lightning.toolbar.sync.label") } + +toolbar-synchronize = + .title = { COPY(from_path, "lightning.toolbar.sync.tooltip") } + +toolbar-delete-event-label = { COPY(from_path, "lightning.toolbar.delete.label") } + +toolbar-delete-event = + .title = { COPY(from_path, "lightning.toolbar.delete.tooltip") } + +toolbar-go-to-today-label = { COPY(from_path, "lightning.toolbar.gototoday.label") } + +toolbar-go-to-today = + .title = { COPY(from_path, "lightning.toolbar.gototoday.tooltip") } + +toolbar-print-event-label = { COPY(from_path, "lightning.toolbar.print.label") } + +toolbar-print-event = + .title = { COPY(from_path, "lightning.toolbar.print.tooltip") } + +toolbar-new-event-label = { COPY(from_path, "lightning.toolbar.newevent.label") } + +toolbar-new-event = + .title = { COPY(from_path, "lightning.toolbar.newevent.tooltip") } + +toolbar-new-task-label = { COPY(from_path, "lightning.toolbar.newtask.label") } + +toolbar-new-task = + .title = { COPY(from_path, "lightning.toolbar.newtask.tooltip") } + """, + from_path="calendar/chrome/lightning/lightning-toolbar.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1815489_add_back_forward_and_stop.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1815489_add_back_forward_and_stop.py new file mode 100644 index 0000000000..8dab08d9ab --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1815489_add_back_forward_and_stop.py @@ -0,0 +1,33 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb import COPY +from fluent.migratetb.helpers import transforms_from + + +def migrate(ctx): + """Bug 1815489 - Add back, forward and stop to unified toolbar fluent migration part {index}.""" + + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-go-back-label = { COPY(from_path, "backButton1.label") } + +toolbar-go-back = + .title = { COPY(from_path, "goBackButton.tooltip") } + +toolbar-go-forward-label = { COPY(from_path, "goForwardButton1.label") } + +toolbar-go-forward = + .title = { COPY(from_path, "goForwardButton.tooltip") } + +toolbar-stop-label = { COPY(from_path, "stopButton.label") } + +toolbar-stop = + .title = { COPY(from_path, "stopButton.tooltip") } + """, + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1815605_calendar_context_menu_has_empty_items.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1815605_calendar_context_menu_has_empty_items.py new file mode 100644 index 0000000000..de0157ac47 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1815605_calendar_context_menu_has_empty_items.py @@ -0,0 +1,52 @@ +# coding=utf8 + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate.helpers import transforms_from +from fluent.migrate import COPY + + +def migrate(ctx): + """Bug 1815605 - calendar context menu has empty items, part {index}.""" + + ctx.add_transforms( + "calendar/calendar/calendar-widgets.ftl", + "calendar/calendar/calendar-widgets.ftl", + transforms_from( + """ +calendar-context-menu-previous-day = + .label = { COPY(from_path, "calendar.prevday.label") } + .accesskey = { COPY(from_path, "calendar.prevday.accesskey") } + +calendar-context-menu-previous-week = + .label = { COPY(from_path, "calendar.prevweek.label") } + .accesskey = { COPY(from_path, "calendar.prevweek.accesskey") } + +calendar-context-menu-previous-multiweek = + .label = { COPY(from_path, "calendar.prevweek.label") } + .accesskey = { COPY(from_path, "calendar.prevweek.accesskey") } + +calendar-context-menu-previous-month = + .label = { COPY(from_path, "calendar.prevmonth.label") } + .accesskey = { COPY(from_path, "calendar.prevmonth.accesskey") } + +calendar-context-menu-next-day = + .label = { COPY(from_path, "calendar.nextday.label") } + .accesskey = { COPY(from_path, "calendar.nextday.accesskey") } + +calendar-context-menu-next-week = + .label = { COPY(from_path, "calendar.nextweek.label") } + .accesskey = { COPY(from_path, "calendar.nextweek.accesskey") } + +calendar-context-menu-next-multiweek = + .label = { COPY(from_path, "calendar.nextweek.label") } + .accesskey = { COPY(from_path, "calendar.nextweek.accesskey") } + +calendar-context-menu-next-month = + .label = { COPY(from_path, "calendar.nextmonth.label") } + .accesskey = { COPY(from_path, "calendar.nextmonth.accesskey") } +""", + from_path="calendar/chrome/calendar/calendar.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1816532_about_dialog_migration.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1816532_about_dialog_migration.py new file mode 100644 index 0000000000..cc47261099 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1816532_about_dialog_migration.py @@ -0,0 +1,94 @@ +# coding=utf8 + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +from fluent.migratetb.helpers import TERM_REFERENCE +from fluent.migratetb.helpers import transforms_from + + +# This can't just be a straight up literal dict (eg: {"a":"b"}) because the +# validator fails... so make it a function call that returns a dict.. it works +about_replacements = dict( + { + "&brandShorterName;": TERM_REFERENCE("brand-shorter-name"), + "&brandShortName;": TERM_REFERENCE("brand-short-name"), + "&vendorShortName;": TERM_REFERENCE("vendor-short-name"), + } +) + + +def migrate(ctx): + """Bug 1816532 - Migrate aboutDialog.dtd strings to Fluent, part {index}""" + target = reference = "mail/messenger/aboutDialog.ftl" + source = "mail/chrome/messenger/aboutDialog.dtd" + + ctx.add_transforms( + target, + reference, + transforms_from( + """ +release-notes-link = { COPY(source, "releaseNotes.link") } + +update-check-for-updates-button = { COPY(source, "update.checkForUpdatesButton.label") } + .accesskey = { COPY(source, "update.checkForUpdatesButton.accesskey") } + +update-update-button = { REPLACE(source, "update.updateButton.label3", about_replacements) } + .accesskey = { COPY(source, "update.updateButton.accesskey") } + +update-checking-for-updates = { COPY(source, "update.checkingForUpdates") } + +update-downloading-message = { COPY(source, "update.downloading.start") } + +update-applying = { COPY(source, "update.applying") } + +update-downloading = { COPY(source, "update.downloading.start") } + +update-failed = { COPY(source, "update.failed.start") }{ COPY(source, "update.failed.linkText") } + +update-admin-disabled = { COPY(source, "update.adminDisabled") } + +update-no-updates-found = { REPLACE(source, "update.noUpdatesFound", about_replacements) } + +update-other-instance-handling-updates = { REPLACE(source, "update.otherInstanceHandlingUpdates", about_replacements) } + +update-unsupported = { COPY(source, "update.unsupported.start") }{ COPY(source, "update.unsupported.linkText") } + +update-restarting = { COPY(source, "update.restarting") } + +channel-description = { COPY(source, "channel.description.start") }{ $channel } { COPY(source, "channel.description.end", trim: "True") } + +warning-desc-version = { REPLACE(source, "warningDesc.version", about_replacements) } + +warning-desc-telemetry = { REPLACE(source, "warningDesc.telemetryDesc", about_replacements) } + +community-exp = + { REPLACE(source, "community.exp.mozillaLink", about_replacements) } + { COPY(source, "community.exp.middle") } + { COPY(source, "community.exp.creditsLink") } + { COPY(source, "community.exp.end") } + +community-2 = { REPLACE(source, "community.start2", about_replacements) } + { REPLACE(source, "community.mozillaLink", about_replacements) } + { COPY(source, "community.middle2") } + { COPY(source, "community.creditsLink") } + { COPY(source, "community.end3") } + +about-helpus = { COPY(source, "helpus.start") } + { COPY(source, "helpus.donateLink") } or + { COPY(source, "helpus.getInvolvedLink") } + +bottom-links-license = { COPY(source, "bottomLinks.license") } + +bottom-links-rights = { COPY(source, "bottomLinks.rights") } + +bottom-links-privacy = { COPY(source, "bottomLinks.privacy") } + +cmd-close-mac-command-key = + .key = { COPY(source, "cmdCloseMac.commandKey") } +""", + source=source, + about_replacements=about_replacements, + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1816593_flexbox_emulation_dialogs.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1816593_flexbox_emulation_dialogs.py new file mode 100644 index 0000000000..a920b9c0fd --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1816593_flexbox_emulation_dialogs.py @@ -0,0 +1,21 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate import COPY_PATTERN +from fluent.migrate.helpers import transforms_from + + +def migrate(ctx): + """Bug 1816593 - Fix wrong sized dialogs due to flexbox emulation, part {index}""" + + ctx.add_transforms( + "mail/messenger/compactFoldersDialog.ftl", + "mail/messenger/compactFoldersDialog.ftl", + transforms_from( + """ +compact-dialog-window-title = + .title = {{COPY_PATTERN(from_path, "compact-dialog-window.title")}} + """, + from_path="mail/messenger/compactFoldersDialog.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1817914_tags_mode.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1817914_tags_mode.py new file mode 100644 index 0000000000..d8b3c80d0c --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1817914_tags_mode.py @@ -0,0 +1,21 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb.helpers import transforms_from + + +def migrate(ctx): + """Bug 1820700 - Implement a Tags folder pane mode, part {index}.""" + + ctx.add_transforms( + "mail/messenger/messenger.ftl", + "mail/messenger/messenger.ftl", + transforms_from( + """ +show-tags-folders-label = + .label = {COPY(from_path, "viewTags.label")} + .accesskey = {COPY(from_path, "viewTags.accesskey")} +""", + from_path="mail/chrome/messenger/msgViewPickerOverlay.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1817915_get_new_messages.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1817915_get_new_messages.py new file mode 100644 index 0000000000..2e2c1ac497 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1817915_get_new_messages.py @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb.helpers import transforms_from +from fluent.migratetb import COPY + + +def migrate(ctx): + """Bug 1817915 - Add context menu to Get Messages folder pane button to fetch all messages or per account, part {index}.""" + + ctx.add_transforms( + "mail/messenger/about3Pane.ftl", + "mail/messenger/about3Pane.ftl", + transforms_from( + """ +folder-pane-get-all-messages-menuitem = + .label = { COPY(from_path, "getAllNewMsgCmd.label") } + .accesskey = { COPY(from_path, "getAllNewMsgCmd.accesskey") } + """, + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1820700_select_thread.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1820700_select_thread.py new file mode 100644 index 0000000000..ae65f694d1 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1820700_select_thread.py @@ -0,0 +1,35 @@ +# 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 https://mozilla.org/MPL/2.0/. + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import fluent.syntax.ast as FTL +from fluent.migratetb.helpers import VARIABLE_REFERENCE, transforms_from +from fluent.migratetb.transforms import REPLACE + + +def migrate(ctx): + """Bug 1820700 - Migrate thread img strings to button, part {index}.""" + + ctx.add_transforms( + "mail/messenger/treeView.ftl", + "mail/messenger/treeView.ftl", + transforms_from( + """ +tree-list-view-row-thread-button = + .title = {COPY_PATTERN(from_path, "tree-list-view-row-thread-icon.title")} + +tree-list-view-row-ignored-thread-button = + .title = {COPY_PATTERN(from_path, "tree-list-view-row-ignored-thread-icon.title")} + +tree-list-view-row-ignored-subthread-button = + .title = {COPY_PATTERN(from_path, "tree-list-view-row-ignored-subthread-icon.title")} + +tree-list-view-row-watched-thread-button = + .title = {COPY_PATTERN(from_path, "tree-list-view-row-watched-thread-icon.title")} +""", + from_path="mail/messenger/treeView.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1823033_activity_indicator.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1823033_activity_indicator.py new file mode 100644 index 0000000000..4cfaec4695 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1823033_activity_indicator.py @@ -0,0 +1,23 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb import COPY +from fluent.migratetb.helpers import transforms_from + + +def migrate(ctx): + """Bug 1823033 - Add activity indicator to unified toolbar fluent migration part {index}.""" + + ctx.add_transforms( + "mail/messenger/unifiedToolbarItems.ftl", + "mail/messenger/unifiedToolbarItems.ftl", + transforms_from( + """ +toolbar-throbber-label = { COPY(from_path, "throbberItem.title") } + +toolbar-throbber = + .title = { COPY(from_path, "throbberItem.title") } + """, + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1827261_add_enable_disable_compact_mode_options_to_folder_mode_context_menu.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1827261_add_enable_disable_compact_mode_options_to_folder_mode_context_menu.py new file mode 100644 index 0000000000..7ea090e903 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1827261_add_enable_disable_compact_mode_options_to_folder_mode_context_menu.py @@ -0,0 +1,26 @@ +# 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 https://mozilla.org/MPL/2.0/. + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb.helpers import transforms_from +from fluent.migratetb import COPY + + +def migrate(ctx): + """Bug 1827261 - Add enable/disable compact mode options to Folder Mode context menu, part {index}.""" + + ctx.add_transforms( + "mail/messenger/about3Pane.ftl", + "mail/messenger/about3Pane.ftl", + transforms_from( + """ +folder-pane-mode-context-toggle-compact-mode = + .label = { COPY(from_path, "compactVersion.label") } + .accesskey = { COPY(from_path, "compactVersion.accesskey") } + """, + from_path="mail/chrome/messenger/messenger.dtd", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1827891_dnt_prefs_learn_more.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1827891_dnt_prefs_learn_more.py new file mode 100644 index 0000000000..4861e428bc --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1827891_dnt_prefs_learn_more.py @@ -0,0 +1,21 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migratetb import COPY_PATTERN +from fluent.migratetb.helpers import transforms_from + + +def migrate(ctx): + """Bug 1827891 - DNT Prefs 'learn more' link fix, part {index}.""" + + ctx.add_transforms( + "mail/messenger/preferences/preferences.ftl", + "mail/messenger/preferences/preferences.ftl", + transforms_from( + """ +dnt-learn-more-button = + .value = {{ COPY_PATTERN(from_path, "learn-button.label") }} + """, + from_path="mail/messenger/preferences/preferences.ftl", + ), + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1828340_aboutdialog_layout_fixes.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1828340_aboutdialog_layout_fixes.py new file mode 100644 index 0000000000..d162e94659 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1828340_aboutdialog_layout_fixes.py @@ -0,0 +1,42 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import re + +from fluent.migratetb import COPY_PATTERN +from fluent.migratetb.transforms import TransformPattern + +import fluent.syntax.ast as FTL + + +class STRIP_NEWLINES(TransformPattern): + def visit_TextElement(self, node): + node.value = re.sub("\n", "", node.value) + return node + + +def migrate(ctx): + """Bug 1828340 - Fix aboutDialog layout issues, part {index}.""" + path = "mail/messenger/aboutDialog.ftl" + ctx.add_transforms( + path, + path, + [ + FTL.Message( + id=FTL.Identifier("about-dialog-title"), + value=COPY_PATTERN(path, "aboutDialog-title.title"), + ), + FTL.Message( + id=FTL.Identifier("community-experimental"), + value=STRIP_NEWLINES(path, "community-exp"), + ), + FTL.Message( + id=FTL.Identifier("community-desc"), + value=STRIP_NEWLINES(path, "community-2"), + ), + FTL.Message( + id=FTL.Identifier("about-donation"), + value=STRIP_NEWLINES(path, "about-helpus"), + ), + ], + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1830004_folder_quota.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1830004_folder_quota.py new file mode 100644 index 0000000000..07f4505912 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1830004_folder_quota.py @@ -0,0 +1,28 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import fluent.syntax.ast as FTL +from fluent.migratetb.helpers import VARIABLE_REFERENCE, transforms_from +from fluent.migratetb.transforms import REPLACE + + +def migrate(ctx): + """Bug 1830004 - Migrate a quota string, part {index}.""" + + ctx.add_transforms( + "mail/messenger/folderprops.ftl", + "mail/messenger/folderprops.ftl", + [ + FTL.Message( + id=FTL.Identifier("quota-percent-used"), + value=REPLACE( + "mail/chrome/messenger/messenger.properties", + "quotaPercentUsed", + { + "%1$S": VARIABLE_REFERENCE("percent"), + }, + normalize_printf=True, + ), + ), + ], + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1834662_cal_enable78.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1834662_cal_enable78.py new file mode 100644 index 0000000000..b3f6c420c6 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1834662_cal_enable78.py @@ -0,0 +1,30 @@ +# 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 https://mozilla.org/MPL/2.0/. + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import fluent.syntax.ast as FTL +from fluent.migratetb.helpers import VARIABLE_REFERENCE, transforms_from +from fluent.migratetb.transforms import REPLACE +from fluent.migratetb.transforms import COPY + + +def migrate(ctx): + """Bug 1834662 - Migrate calendar enable button, part {index}.""" + target = reference = "calendar/calendar/calendar-widgets.ftl" + + ctx.add_transforms( + target, + reference, + [ + FTL.Message( + id=FTL.Identifier("calendar-enable-button"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.sideloadEnable.label", + ), + ), + ], + ) diff --git a/comm/python/l10n/tb_fluent_migrations/completed/bug_1834662_extensions_to_fluent.py b/comm/python/l10n/tb_fluent_migrations/completed/bug_1834662_extensions_to_fluent.py new file mode 100644 index 0000000000..cdd8736b67 --- /dev/null +++ b/comm/python/l10n/tb_fluent_migrations/completed/bug_1834662_extensions_to_fluent.py @@ -0,0 +1,531 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import fluent.syntax.ast as FTL +from fluent.migratetb.helpers import TERM_REFERENCE, VARIABLE_REFERENCE +from fluent.migratetb.transforms import ( + COPY, + COPY_PATTERN, + PLURALS, + REPLACE, + REPLACE_IN_TEXT, +) + + +def migrate(ctx): + """Bug 1834662 - Migrate addon/extension stuff, part {index}.""" + + # extensionPermissions.ftl - from addons.properties + ctx.add_transforms( + "mail/messenger/extensionPermissions.ftl", + "mail/messenger/extensionPermissions.ftl", + [ + FTL.Message( + id=FTL.Identifier("webext-perms-description-accountsFolders"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.accountsFolders", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-accountsIdentities"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.accountsIdentities", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-accountsRead"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.accountsRead2", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-addressBooks"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.addressBooks", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-compose"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.compose", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-compose"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.compose", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-compose-send"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.compose.send", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-compose-save"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.compose.save", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-experiment"), + value=REPLACE( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.experiment", + {"%1$S": TERM_REFERENCE("brand-short-name")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-messagesImport"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.messagesImport", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-messagesModify"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.messagesModify", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-messagesMove"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.messagesMove2", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-messagesDelete"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.messagesDelete", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-messagesRead"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.messagesRead", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-messagesTags"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.messagesTags", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-sensitiveDataUpload"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.description.sensitiveDataUpload", + ), + ), + ], + ) + + # extensionsUI.ftl - from here and there + ctx.add_transforms( + "mail/messenger/extensionsUI.ftl", + "mail/messenger/extensionsUI.ftl", + [ + FTL.Message( + id=FTL.Identifier("webext-experiment-warning"), + value=COPY( + "mail/chrome/messenger/addons.properties", + "webextPerms.experimentWarning", + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-learn-more"), + value=COPY("mail/chrome/messenger/addons.properties", "webextPerms.learnMore2"), + ), + ], + ) + + # addonNotifications.ftl - copied from browser/ migration script + + addons_properties = "mail/chrome/messenger/addons.properties" + notifications = "mail/messenger/addonNotifications.ftl" + + ctx.add_transforms( + notifications, + notifications, + [ + FTL.Message( + id=FTL.Identifier("xpinstall-prompt"), + value=REPLACE( + addons_properties, + "xpinstallPromptMessage", + {"%1$S": TERM_REFERENCE("brand-short-name")}, + ), + ), + FTL.Message( + id=FTL.Identifier("xpinstall-prompt-header"), + value=REPLACE( + addons_properties, + "xpinstallPromptMessage.header", + {"%1$S": VARIABLE_REFERENCE("host")}, + ), + ), + FTL.Message( + id=FTL.Identifier("xpinstall-prompt-message"), + value=REPLACE( + addons_properties, + "xpinstallPromptMessage.message", + {"%1$S": VARIABLE_REFERENCE("host")}, + ), + ), + FTL.Message( + id=FTL.Identifier("xpinstall-prompt-header-unknown"), + value=COPY(addons_properties, "xpinstallPromptMessage.header.unknown"), + ), + FTL.Message( + id=FTL.Identifier("xpinstall-prompt-message-unknown"), + value=COPY(addons_properties, "xpinstallPromptMessage.message.unknown"), + ), + FTL.Message( + id=FTL.Identifier("xpinstall-prompt-dont-allow"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(addons_properties, "xpinstallPromptMessage.dontAllow"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY( + addons_properties, + "xpinstallPromptMessage.dontAllow.accesskey", + ), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("xpinstall-prompt-never-allow"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(addons_properties, "xpinstallPromptMessage.neverAllow"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY( + addons_properties, + "xpinstallPromptMessage.neverAllow.accesskey", + ), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("xpinstall-prompt-never-allow-and-report"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY( + addons_properties, + "xpinstallPromptMessage.neverAllowAndReport", + ), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY( + addons_properties, + "xpinstallPromptMessage.neverAllowAndReport.accesskey", + ), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("site-permission-install-first-prompt-midi-header"), + value=COPY(addons_properties, "sitePermissionInstallFirstPrompt.midi.header"), + ), + FTL.Message( + id=FTL.Identifier("site-permission-install-first-prompt-midi-message"), + value=COPY(addons_properties, "sitePermissionInstallFirstPrompt.midi.message"), + ), + FTL.Message( + id=FTL.Identifier("xpinstall-prompt-install"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(addons_properties, "xpinstallPromptMessage.install"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY( + addons_properties, + "xpinstallPromptMessage.install.accesskey", + ), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("xpinstall-disabled-locked"), + value=COPY(addons_properties, "xpinstallDisabledMessageLocked"), + ), + FTL.Message( + id=FTL.Identifier("xpinstall-disabled"), + value=COPY(addons_properties, "xpinstallDisabledMessage"), + ), + FTL.Message( + id=FTL.Identifier("xpinstall-disabled-button"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(addons_properties, "xpinstallDisabledButton"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(addons_properties, "xpinstallDisabledButton.accesskey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("addon-install-blocked-by-policy"), + value=REPLACE( + addons_properties, + "addonInstallBlockedByPolicy", + { + "%1$S": VARIABLE_REFERENCE("addonName"), + "%2$S": VARIABLE_REFERENCE("addonId"), + "%3$S": FTL.TextElement(""), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-domain-blocked-by-policy"), + value=COPY(addons_properties, "addonDomainBlockedByPolicy"), + ), + FTL.Message( + id=FTL.Identifier("addon-install-full-screen-blocked"), + value=COPY(addons_properties, "addonInstallFullScreenBlocked"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-sideload-menu-item"), + value=REPLACE( + addons_properties, + "webextPerms.sideloadMenuItem", + { + "%1$S": VARIABLE_REFERENCE("addonName"), + "%2$S": TERM_REFERENCE("brand-short-name"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-update-menu-item"), + value=REPLACE( + addons_properties, + "webextPerms.updateMenuItem", + {"%1$S": VARIABLE_REFERENCE("addonName")}, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-removal-message"), + value=REPLACE( + addons_properties, + "webext.remove.confirmation.message", + { + "%1$S": VARIABLE_REFERENCE("name"), + "%2$S": TERM_REFERENCE("brand-shorter-name"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-removal-button"), + value=COPY(addons_properties, "webext.remove.confirmation.button"), + ), + FTL.Message( + id=FTL.Identifier("addon-downloading-and-verifying"), + value=PLURALS( + addons_properties, + "addonDownloadingAndVerifying", + VARIABLE_REFERENCE("addonCount"), + foreach=lambda n: REPLACE_IN_TEXT( + n, + {"#1": VARIABLE_REFERENCE("addonCount")}, + ), + ), + ), + FTL.Message( + id=FTL.Identifier("addon-download-verifying"), + value=COPY(addons_properties, "addonDownloadVerifying"), + ), + FTL.Message( + id=FTL.Identifier("addon-install-cancel-button"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(addons_properties, "addonInstall.cancelButton.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(addons_properties, "addonInstall.cancelButton.accesskey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("addon-install-accept-button"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(addons_properties, "addonInstall.acceptButton2.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(addons_properties, "addonInstall.acceptButton2.accesskey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("addon-confirm-install-message"), + value=PLURALS( + addons_properties, + "addonConfirmInstall.message", + VARIABLE_REFERENCE("addonCount"), + foreach=lambda n: REPLACE_IN_TEXT( + n, + { + "#1": TERM_REFERENCE("brand-short-name"), + "#2": VARIABLE_REFERENCE("addonCount"), + }, + ), + ), + ), + FTL.Message( + id=FTL.Identifier("addon-confirm-install-unsigned-message"), + value=PLURALS( + addons_properties, + "addonConfirmInstallUnsigned.message", + VARIABLE_REFERENCE("addonCount"), + foreach=lambda n: REPLACE_IN_TEXT( + n, + { + "#1": TERM_REFERENCE("brand-short-name"), + "#2": VARIABLE_REFERENCE("addonCount"), + }, + ), + ), + ), + FTL.Message( + id=FTL.Identifier("addon-confirm-install-some-unsigned-message"), + value=PLURALS( + addons_properties, + "addonConfirmInstallSomeUnsigned.message", + VARIABLE_REFERENCE("addonCount"), + foreach=lambda n: REPLACE_IN_TEXT( + n, + { + "#1": TERM_REFERENCE("brand-short-name"), + "#2": VARIABLE_REFERENCE("addonCount"), + }, + ), + ), + ), + FTL.Message( + id=FTL.Identifier("addon-install-error-network-failure"), + value=COPY(addons_properties, "addonInstallError-1"), + ), + FTL.Message( + id=FTL.Identifier("addon-install-error-incorrect-hash"), + value=REPLACE( + addons_properties, + "addonInstallError-2", + {"%1$S": TERM_REFERENCE("brand-short-name")}, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-install-error-corrupt-file"), + value=COPY(addons_properties, "addonInstallError-3"), + ), + FTL.Message( + id=FTL.Identifier("addon-install-error-file-access"), + value=REPLACE( + addons_properties, + "addonInstallError-4", + { + "%2$S": VARIABLE_REFERENCE("addonName"), + "%1$S": TERM_REFERENCE("brand-short-name"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-install-error-not-signed"), + value=REPLACE( + addons_properties, + "addonInstallError-5", + {"%1$S": TERM_REFERENCE("brand-short-name")}, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-install-error-invalid-domain"), + value=REPLACE( + addons_properties, + "addonInstallError-8", + {"%2$S": VARIABLE_REFERENCE("addonName")}, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-local-install-error-network-failure"), + value=COPY(addons_properties, "addonLocalInstallError-1"), + ), + FTL.Message( + id=FTL.Identifier("addon-local-install-error-incorrect-hash"), + value=REPLACE( + addons_properties, + "addonLocalInstallError-2", + {"%1$S": TERM_REFERENCE("brand-short-name")}, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-local-install-error-corrupt-file"), + value=COPY(addons_properties, "addonLocalInstallError-3"), + ), + FTL.Message( + id=FTL.Identifier("addon-local-install-error-file-access"), + value=REPLACE( + addons_properties, + "addonLocalInstallError-4", + { + "%2$S": VARIABLE_REFERENCE("addonName"), + "%1$S": TERM_REFERENCE("brand-short-name"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-local-install-error-not-signed"), + value=COPY(addons_properties, "addonLocalInstallError-5"), + ), + FTL.Message( + id=FTL.Identifier("addon-install-error-incompatible"), + value=REPLACE( + addons_properties, + "addonInstallErrorIncompatible", + { + "%3$S": VARIABLE_REFERENCE("addonName"), + "%1$S": TERM_REFERENCE("brand-short-name"), + "%2$S": VARIABLE_REFERENCE("appVersion"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("addon-install-error-blocklisted"), + value=REPLACE( + addons_properties, + "addonInstallErrorBlocklisted", + {"%1$S": VARIABLE_REFERENCE("addonName")}, + ), + ), + ], + ) diff --git a/comm/python/l10n/tbxchannel/__init__.py b/comm/python/l10n/tbxchannel/__init__.py new file mode 100644 index 0000000000..b6599032bc --- /dev/null +++ b/comm/python/l10n/tbxchannel/__init__.py @@ -0,0 +1,47 @@ +# 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/. + +from pathlib import Path + +from .l10n_merge import COMM_STRINGS_QUARANTINE, COMM_STRINGS_QUARANTINE_PUSH + +TB_XC_NOTIFICATION_TMPL = """\ +**Thunderbird L10n Cross Channel** + +Changes pushed to `comm-strings-quarantine`: {rev_url} +""" + + +def get_thunderbird_xc_config(topsrcdir, strings_path): + assert isinstance(topsrcdir, Path) + assert isinstance(strings_path, Path) + return { + "strings": { + "path": strings_path, + "url": COMM_STRINGS_QUARANTINE, + "heads": {"default": "default"}, + "update_on_pull": True, + "push_url": COMM_STRINGS_QUARANTINE_PUSH, + }, + "source": { + "comm-central": { + "path": topsrcdir / "comm", + "url": "https://hg.mozilla.org/comm-central/", + "heads": { + # This list of repositories is ordered, starting with the + # one with the most recent content (central) to the oldest + # (ESR). In case two ESR versions are supported, the oldest + # ESR goes last (e.g. esr102 goes after esr115). + "comm": "comm-central", + "comm-beta": "releases/comm-beta", + "comm-esr102": "releases/comm-esr102", + }, + "config_files": [ + "comm/calendar/locales/l10n.toml", + "comm/mail/locales/l10n.toml", + "comm/suite/locales/l10n.toml", + ], + }, + }, + } diff --git a/comm/python/l10n/tbxchannel/l10n_merge.py b/comm/python/l10n/tbxchannel/l10n_merge.py new file mode 100644 index 0000000000..b202e7dfe0 --- /dev/null +++ b/comm/python/l10n/tbxchannel/l10n_merge.py @@ -0,0 +1,26 @@ +# 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/. + +L10N_CENTRAL = "https://hg.mozilla.org/l10n-central" +COMM_L10N = "https://hg.mozilla.org/projects/comm-l10n" +COMM_L10N_PUSH = f"ssh{COMM_L10N[5:]}" +COMM_STRINGS_QUARANTINE = "https://hg.mozilla.org/projects/comm-strings-quarantine" +COMM_STRINGS_QUARANTINE_PUSH = f"ssh{COMM_STRINGS_QUARANTINE[5:]}" + + +GECKO_STRINGS_PATTERNS = ( + "{lang}/browser/pdfviewer/**", + "{lang}/devtools/**", + "{lang}/dom/**", + "{lang}/extensions/spellcheck/**", + "{lang}/netwerk/**", + "{lang}/security/**", + "{lang}/toolkit/**", +) + +COMM_STRINGS_PATTERNS = ( + "{lang}/calendar/**", + "{lang}/chat/**", + "{lang}/mail/**", +) diff --git a/comm/python/l10n/tbxchannel/quarantine_to_strings.py b/comm/python/l10n/tbxchannel/quarantine_to_strings.py new file mode 100644 index 0000000000..def3f59e5e --- /dev/null +++ b/comm/python/l10n/tbxchannel/quarantine_to_strings.py @@ -0,0 +1,194 @@ +# 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 https://mozilla.org/MPL/2.0/. + +import logging +import os +import shutil +import subprocess +import tempfile +from pathlib import Path + +from typing_extensions import Literal + +from mozversioncontrol import HgRepository +from mozversioncontrol.repoupdate import update_mercurial_repo + +from .l10n_merge import COMM_L10N, COMM_L10N_PUSH, COMM_STRINGS_QUARANTINE + +ACTIONS = Literal["clean", "prep", "migrate", "push"] + + +class HgL10nRepository(HgRepository): + log_trans_table = str.maketrans({"{": "{{", "}": "}}"}) + + def __init__(self, path: Path, check_url=None, logger=print): + super(HgL10nRepository, self).__init__(path, hg="hg") + self._logger = logger + if check_url is not None: + self._check_hg_url(check_url) + + def logger(self, *args): + # Escape python-style format string substitutions because Sentry is annoying + self._logger(*args[:-1], args[-1].translate(self.log_trans_table)) + + def _check_hg_url(self, repo_url): + configured_url = self._run("config", "paths.default").strip() + if configured_url != repo_url: + raise Exception(f"Repository does not match {repo_url}.") + + def check_status(self): + if not self.working_directory_clean() or self.get_outgoing_files(): + raise Exception(f"Repository at {self.path} is not clean, run with 'clean'.") + + def last_convert_rev(self): + args = ( + "log", + "-r", + "last(extra('convert_source', 'comm-strings-quarantine'))", + "--template", + "{get(extras,'convert_revision')}\n", + ) + self.logger(logging.INFO, "last_convert_rev", {}, " ".join(args)) + rv = self._run(*args).strip() + self.logger(logging.INFO, "last_convert_rev", {}, rv) + return rv + + def next_convert_rev(self, last_converted): + args = ("log", "-r", f"first(children({last_converted}))", "--template", "{node}\n") + self.logger(logging.INFO, "next_convert_rev", {}, " ".join(args)) + rv = self._run(*args).strip() + self.logger(logging.INFO, "next_convert_rev", {}, rv) + return rv + + def convert_quarantine(self, strings_path, filemap_path, splicemap_path, next_converted_rev): + args = ( + "convert", + "--config", + "convert.hg.saverev=True", + "--config", + "convert.hg.sourcename=comm-strings-quarantine", + "--config", + f"convert.hg.revs={next_converted_rev}:tip", + "--filemap", + filemap_path, + "--splicemap", + splicemap_path, + "--datesort", + str(self.path), + str(strings_path.absolute()), + ) + self.logger(logging.INFO, "convert_quarantine", {}, " ".join(args)) + rv = self._run(*args) + self.logger(logging.INFO, "convert_quarantine", {}, rv) + return rv + + def push(self, push_url): + popen_kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "cwd": self.path, + "env": self._env, + "universal_newlines": True, + "bufsize": 1, + } + cmd = ("hg", "push", "-r", ".", push_url) + self.logger(logging.INFO, "push", {}, " ".join(cmd)) + # This function doesn't really push to try... + self._push_to_try_with_log_capture(cmd, popen_kwargs) + + +def _nuke_hg_repos(*paths: Path): + failed = {} + for path in paths: + try: + if path.exists(): + shutil.rmtree(str(path)) + except Exception as e: + failed[str(path)] = e + + if failed: + for f in failed: + print(f"Unable to nuke '{f}': {failed[f]}") + raise Exception() + + +def publish_strings( + command_context, + quarantine_path: Path, + comm_l10n_path: Path, + actions: ACTIONS, + **kwargs, +): + if "clean" in actions: + command_context.log(logging.INFO, "clean", {}, "Removing old repository clones.") + _nuke_hg_repos(quarantine_path, comm_l10n_path) + + if "prep" in actions: + # update_mercurial_repo also will clone if a repo is not already there + command_context.log( + logging.INFO, "prep", {}, f"Updating comm-strings-quarantine at {quarantine_path}." + ) + update_mercurial_repo("hg", COMM_STRINGS_QUARANTINE, quarantine_path) + command_context.log(logging.INFO, "prep", {}, f"Updating comm-l10n at {comm_l10n_path}.") + update_mercurial_repo("hg", COMM_L10N, comm_l10n_path) + + local_quarantine = HgL10nRepository( + quarantine_path, COMM_STRINGS_QUARANTINE, command_context.log + ) + local_comm_l10n = HgL10nRepository(comm_l10n_path, COMM_L10N, command_context.log) + + if "prep" not in actions: + local_quarantine.update("tip") + local_comm_l10n.update("tip") + + if "migrate" in actions: + local_quarantine.check_status() + local_comm_l10n.check_status() + + command_context.log( + logging.INFO, "migrate", {}, "Starting string migration from quarantine." + ) + head_rev = local_comm_l10n.head_ref + last_convert_rev = local_comm_l10n.last_convert_rev() + first_convert_rev = local_quarantine.next_convert_rev(last_convert_rev) + command_context.log( + logging.INFO, "migrate", {}, f" Last converted rev: {last_convert_rev}" + ) + command_context.log( + logging.INFO, "migrate", {}, f" First converted rev: {first_convert_rev}" + ) + + with tempfile.NamedTemporaryFile( + prefix="splicemap", suffix=".txt", delete=False + ) as splice_fp: + splicemap = splice_fp.name + command_context.log( + logging.INFO, "migrate", {}, f" Writing splicemap to: {splicemap}" + ) + splice_fp.write(f"{first_convert_rev} {head_rev}\n".encode("utf-8")) + + with tempfile.NamedTemporaryFile(prefix="filemap", suffix=".txt", delete=False) as file_fp: + filemap = file_fp.name + command_context.log(logging.INFO, "migrate", {}, f" Writing filemap to: {filemap}") + file_fp.writelines( + ["exclude _configs\n".encode("utf-8"), "rename . en-US\n".encode("utf-8")] + ) + + command_context.log(logging.INFO, "migrate", {}, " Running hg convert...") + local_quarantine.convert_quarantine(comm_l10n_path, filemap, splicemap, first_convert_rev) + try: + os.unlink(splicemap) + os.unlink(filemap) + except Exception: + pass + + local_comm_l10n.update("tip") + command_context.log(logging.INFO, "migrate", {}, " Finished!") + + if "push" in actions: + if local_comm_l10n.get_outgoing_files(): + command_context.log(logging.INFO, "push", {}, " Pushing to comm-l10n.") + local_comm_l10n.push(COMM_L10N_PUSH) + else: + command_context.log(logging.INFO, "push", {}, "Skipping empty push.") diff --git a/comm/python/l10n/tbxchannel/tb_migration_test.py b/comm/python/l10n/tbxchannel/tb_migration_test.py new file mode 100644 index 0000000000..8e01e1d889 --- /dev/null +++ b/comm/python/l10n/tbxchannel/tb_migration_test.py @@ -0,0 +1,172 @@ +# 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/. +""" +Test comm-l10n Fluent migrations +""" + +import logging +import os +import re +import shutil + +import hglib +from compare_locales.merge import merge_channels +from compare_locales.paths.configparser import TOMLParser +from compare_locales.paths.files import ProjectFiles +from fluent.migratetb import validator +from test_fluent_migrations.fmt import diff_resources + +import mozpack.path as mozpath +from mach.util import get_state_dir +from mozversioncontrol.repoupdate import update_mercurial_repo + +from .l10n_merge import COMM_L10N + + +def inspect_migration(path): + """Validate recipe and extract some metadata.""" + return validator.Validator.validate(path) + + +def prepare_object_dir(cmd): + """Prepare object dir to have an up-to-date clone of comm-l10n. + + We run this once per mach invocation, for all tested migrations. + """ + obj_dir = mozpath.join(cmd.topobjdir, "comm", "python", "l10n") + if not os.path.exists(obj_dir): + os.makedirs(obj_dir) + state_dir = get_state_dir() + update_mercurial_repo("hg", COMM_L10N, mozpath.join(state_dir, "comm-strings")) + return obj_dir + + +def test_migration(cmd, obj_dir, to_test, references): + """Test the given recipe. + + This creates a workdir by merging comm-strings-quarantine and the c-c source, + to mimic comm-strings-quarantine after the patch to test landed. + It then runs the recipe with a comm-strings-quarantine clone as localization, both + dry and wet. + It inspects the generated commits, and shows a diff between the merged + reference and the generated content. + The diff is intended to be visually inspected. Some changes might be + expected, in particular when formatting of the en-US strings is different. + """ + rv = 0 + migration_name = os.path.splitext(os.path.split(to_test)[1])[0] + l10n_lib = os.path.abspath(os.path.dirname(os.path.dirname(to_test))) + work_dir = mozpath.join(obj_dir, migration_name) + + paths = os.path.normpath(to_test).split(os.sep) + # Migration modules should be in a sub-folder of l10n. + migration_module = ".".join(paths[paths.index("l10n") + 1 : -1]) + "." + migration_name + + if os.path.exists(work_dir): + shutil.rmtree(work_dir) + os.makedirs(mozpath.join(work_dir, "reference")) + l10n_toml = mozpath.join(cmd.topsrcdir, cmd.substs["MOZ_BUILD_APP"], "locales", "l10n.toml") + pc = TOMLParser().parse(l10n_toml, env={"l10n_base": work_dir}) + pc.set_locales(["reference"]) + files = ProjectFiles("reference", [pc]) + for ref in references: + if ref != mozpath.normpath(ref): + cmd.log( + logging.ERROR, + "tb-fluent-migration-test", + { + "file": to_test, + "ref": ref, + }, + 'Reference path "{ref}" needs to be normalized for {file}', + ) + rv = 1 + continue + full_ref = mozpath.join(work_dir, "reference", ref) + m = files.match(full_ref) + if m is None: + raise ValueError(f"Bad reference path: {ref} - {full_ref}") + m_c_path = m[1] + g_s_path = mozpath.join(work_dir, "comm-strings", ref) + resources = [ + b"" if not os.path.exists(f) else open(f, "rb").read() for f in (g_s_path, m_c_path) + ] + ref_dir = os.path.dirname(full_ref) + if not os.path.exists(ref_dir): + os.makedirs(ref_dir) + open(full_ref, "wb").write(merge_channels(ref, resources)) + client = hglib.clone( + source=mozpath.join(get_state_dir(), "comm-strings"), + dest=mozpath.join(work_dir, "comm-strings"), + ) + client.open() + old_tip = client.tip().node + run_migration = [ + cmd._virtualenv_manager.python_path, + "-m", + "fluent.migratetb.tool", + "--locale", + "en-US", + "--reference-dir", + mozpath.join(work_dir, "reference"), + "--localization-dir", + mozpath.join(work_dir, "comm-strings"), + "--dry-run", + migration_module, + ] + append_env = {"PYTHONPATH": l10n_lib} + cmd.run_process( + run_migration, + append_env=append_env, + cwd=work_dir, + line_handler=print, + ) + # drop --dry-run + run_migration.pop(-2) + cmd.run_process( + run_migration, + append_env=append_env, + cwd=work_dir, + line_handler=print, + ) + tip = client.tip().node + if old_tip == tip: + cmd.log( + logging.WARN, + "tb-fluent-migration-test", + { + "file": to_test, + }, + "No migration applied for {file}", + ) + return rv + for ref in references: + diff_resources( + mozpath.join(work_dir, "reference", ref), + mozpath.join(work_dir, "comm-strings", "en-US", ref), + ) + messages = [l.desc.decode("utf-8") for l in client.log(b"::%s - ::%s" % (tip, old_tip))] + bug = re.search("[0-9]{5,}", migration_name) + # Just check first message for bug number, they're all following the same pattern + if bug is None or bug.group() not in messages[0]: + rv = 1 + cmd.log( + logging.ERROR, + "tb-fluent-migration-test", + { + "file": to_test, + }, + "Missing or wrong bug number for {file}", + ) + if any("part {}".format(n + 1) not in msg for n, msg in enumerate(messages)): + rv = 1 + cmd.log( + logging.ERROR, + "tb-fluent-migration-test", + { + "file": to_test, + }, + 'Commit messages should have "part {{index}}" for {file}', + ) + return rv diff --git a/comm/python/moz.build b/comm/python/moz.build new file mode 100644 index 0000000000..5c0dac54b9 --- /dev/null +++ b/comm/python/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +# Default extra components to build config +with Files("**"): + BUG_COMPONENT = ("Thunderbird", "Build Config") + +PYTHON_UNITTEST_MANIFESTS += [ + "mutlh/mutlh/test/python.ini", +] diff --git a/comm/python/mutlh/README.md b/comm/python/mutlh/README.md new file mode 100644 index 0000000000..0c3d3c4387 --- /dev/null +++ b/comm/python/mutlh/README.md @@ -0,0 +1,56 @@ +# mutlh + +Mutlh (Klingon for *construct, assemble, put together*) is an extension of +Mozilla's Mach to meet the needs of Thunderbird developers. + +### Why you might need this + +When implementing a `mach` command in comm-central, you may need to utilize +some of the Python code in the repository. Often, `mach` commands have difficulty +importing these modules as they're not in sys.path usually. + +### Use case: mach command needs to import a library not on sys.path + +- In your `mach_commands.py` file, instead of importing from `mach.decorators`, + import from `mutlh.decorators`. +- Implement your command as usual. `@Command`, `@CommandArgument`, `@SubCommand`, + and `@CommandArgumentGroup` are available and work just like `mach.decorators` + equivalents. +- By default, the "tb_common" site is used for MutlhCommands. + +```python +from mutlh.decorators import Command, CommandArgument + +@Command( + "tb-add-missing-ftls", + category="thunderbird", + description="Add missing FTL files after l10n merge.", +) +@CommandArgument( + "--merge", + type=Path, + help="Merge path base", +) +@CommandArgument( + "locale", + type=str, + help="Locale code", +) +def tb_add_missing_ftls(command_context, merge, locale): + """implementation""" +``` + +- The default "tb_common" virtualenv can be overridden by passing `virtualenv_name` + to `@Command`. + +```python +@Command( + "crazytb", + category="thunderbird", + description="Something insane", + virtualenv_name="crazyenv" +) +def crazytb(command_context, *args, **kwargs): + command_context.activate_virtualenv() + """Do stuff""" +``` diff --git a/comm/python/mutlh/mutlh/__init__.py b/comm/python/mutlh/mutlh/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/comm/python/mutlh/mutlh/decorators.py b/comm/python/mutlh/mutlh/decorators.py new file mode 100644 index 0000000000..9a5f537009 --- /dev/null +++ b/comm/python/mutlh/mutlh/decorators.py @@ -0,0 +1,197 @@ +# 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 https://mozilla.org/MPL/2.0/. + +import argparse +import os + +from mach.decorators import _MachCommand +from mozbuild.base import MachCommandBase + + +class MutlhCommandBase(MachCommandBase): + @property + def virtualenv_manager(self): + from mozboot.util import get_state_dir + + from .site import MutlhCommandSiteManager + + if self._virtualenv_manager is None: + self._virtualenv_manager = MutlhCommandSiteManager.from_environment( + self.topsrcdir, + lambda: get_state_dir(specific_to_topsrcdir=True, topsrcdir=self.topsrcdir), + self._virtualenv_name, + os.path.join(self.topobjdir, "_virtualenvs"), + ) + + return self._virtualenv_manager + + +class _MutlhCommand(_MachCommand): + def create_instance(self, context, virtualenv_name): + metrics = None + if self.metrics_path: + metrics = context.telemetry.metrics(self.metrics_path) + + # This ensures the resulting class is defined inside `mach` so that logging + # works as expected, and has a meaningful name + subclass = type(self.name, (MutlhCommandBase,), {}) + + if virtualenv_name is None: + virtualenv_name = "tb_common" + + return subclass( + context, + virtualenv_name=virtualenv_name, + metrics=metrics, + no_auto_log=self.no_auto_log, + ) + + +class Command(object): + def __init__(self, name, metrics_path=None, **kwargs): + self._mach_command = _MutlhCommand(name=name, **kwargs) + self._mach_command.metrics_path = metrics_path + + def __call__(self, func): + if not hasattr(func, "_mach_command"): + func._mach_command = _MutlhCommand() + + func._mach_command |= self._mach_command + func._mach_command.register(func) + + return func + + +class SubCommand(object): + global_order = 0 + + def __init__( + self, + command, + subcommand, + description=None, + parser=None, + metrics_path=None, + virtualenv_name=None, + ): + self._mach_command = _MutlhCommand( + name=command, + subcommand=subcommand, + description=description, + parser=parser, + virtualenv_name=virtualenv_name, + ) + self._mach_command.decl_order = SubCommand.global_order + SubCommand.global_order += 1 + + self._mach_command.metrics_path = metrics_path + + def __call__(self, func): + if not hasattr(func, "_mach_command"): + func._mach_command = _MutlhCommand() + + func._mach_command |= self._mach_command + func._mach_command.register(func) + + return func + + +class CommandArgument(object): + def __init__(self, *args, **kwargs): + if kwargs.get("nargs") == argparse.REMAINDER: + # These are the assertions we make in dispatcher.py about + # those types of CommandArguments. + assert len(args) == 1 + assert all(k in ("default", "nargs", "help", "group", "metavar") for k in kwargs) + self._command_args = (args, kwargs) + + def __call__(self, func): + if not hasattr(func, "_mach_command"): + func._mach_command = _MutlhCommand() + + func._mach_command.arguments.insert(0, self._command_args) + + return func + + +class CommandArgumentGroup(object): + def __init__(self, group_name): + self._group_name = group_name + + def __call__(self, func): + if not hasattr(func, "_mach_command"): + func._mach_command = _MutlhCommand() + + func._mach_command.argument_group_names.insert(0, self._group_name) + + return func + + +def mach2MutlhCommand(cmd: str, new_func=None, **replacekws): + """ + Change a registered _MachCommand to a _MutlhCommand + + :param str cmd: The name of the existing command + :param function new_func: New implementation function + :param dict replacekws: keyword arguments to replace + :return _MutlhCommand: replacement + """ + from mach.registrar import Registrar + + def get_mach_command(cmd_name): + mach_cmd = Registrar.command_handlers.get(cmd_name) + if mach_cmd: + del Registrar.command_handlers[cmd_name] + return mach_cmd + raise Exception(f"{cmd_name} unknown!") + + mach_cmd = get_mach_command(cmd) + + if mach_cmd.subcommand_handlers: + raise Exception("Commands with SubCommands not implemented!") + + if "parser" in replacekws: + replacekws["_parser"] = replacekws["parser"] + del replacekws["parser"] + + arg_names = ( + "name", + "subcommand", + "category", + "description", + "conditions", + "_parser", + "virtualenv_name", + "ok_if_tests_disabled", + "order", + "no_auto_log", + ) + kwargs = dict([(k, getattr(cmd, k)) for k in arg_names]) + kwargs.update(dict([(k, v) for k, v in replacekws.items() if k in arg_names])) + if "_parser" in kwargs: + kwargs["parser"] = kwargs["_parser"] + del kwargs["_parser"] + + mutlh_cmd = _MutlhCommand(**kwargs) + post_args = ( + "arguments", + "argument_group_names", + "metrics_path", + "subcommand_handlers", + "decl_order", + ) + for arg in post_args: + value = replacekws.get(arg, getattr(mach_cmd, arg)) + setattr(mutlh_cmd, arg, value) + + if new_func is None: + new_func = mach_cmd.func + delattr(new_func, "_mach_command") + if not hasattr(new_func, "_mach_command"): + new_func._mach_command = _MutlhCommand() + + new_func._mach_command |= mutlh_cmd + mutlh_cmd.register(new_func) + + return mutlh_cmd diff --git a/comm/python/mutlh/mutlh/site.py b/comm/python/mutlh/mutlh/site.py new file mode 100644 index 0000000000..ab93e874e9 --- /dev/null +++ b/comm/python/mutlh/mutlh/site.py @@ -0,0 +1,120 @@ +# 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 https://mozilla.org/MPL/2.0/. + +import functools +import os +from typing import Callable, Optional + +from mach.requirements import MachEnvRequirements, UnexpectedFlexibleRequirementException +from mach.site import ( + PIP_NETWORK_INSTALL_RESTRICTED_VIRTUALENVS, + CommandSiteManager, + MozSiteMetadata, + SitePackagesSource, + _mach_virtualenv_root, +) + + +class SiteNotFoundException(Exception): + def __init__(self, site_name, manifest_paths): + self.site_name = site_name + self.manifest_paths = manifest_paths + self.args = (site_name, manifest_paths) + + +@functools.lru_cache(maxsize=None) +def find_manifest(topsrcdir, site_name): + manifest_paths = ( + os.path.join(topsrcdir, "comm", "python", "sites", f"{site_name}.txt"), + os.path.join(topsrcdir, "python", "sites", f"{site_name}.txt"), + ) + + for check_path in manifest_paths: + if os.path.exists(check_path): + return check_path + + raise SiteNotFoundException(site_name, manifest_paths) + + +@functools.lru_cache(maxsize=None) +def resolve_requirements(topsrcdir, site_name): + try: + manifest_path = find_manifest(topsrcdir, site_name) + except SiteNotFoundException as e: + raise Exception( + f'The current command is using the "{e.site_name}" ' + "site. However, that site is missing its associated " + f"requirements definition file in one of the supported " + f"paths: {e.manifest_paths}." + ) + is_thunderbird = True + try: + return MachEnvRequirements.from_requirements_definition( + topsrcdir, + is_thunderbird, + site_name not in PIP_NETWORK_INSTALL_RESTRICTED_VIRTUALENVS, + manifest_path, + ) + except UnexpectedFlexibleRequirementException as e: + raise Exception( + f'The "{site_name}" site does not have all pypi packages pinned ' + f'in the format "package==version" (found "{e.raw_requirement}").\n' + f"Only the {PIP_NETWORK_INSTALL_RESTRICTED_VIRTUALENVS} sites are " + "allowed to have unpinned packages." + ) + + +class MutlhCommandSiteManager(CommandSiteManager): + @classmethod + def from_environment( + cls, + topsrcdir: str, + get_state_dir: Callable[[], Optional[str]], + site_name: str, + command_virtualenvs_dir: str, + ): + """ + Args: + topsrcdir: The path to the Firefox repo + get_state_dir: A function that resolves the path to the checkout-scoped + state_dir, generally ~/.mozbuild/srcdirs// + site_name: The name of this site, such as "build" + command_virtualenvs_dir: The location under which this site's virtualenv + should be created + """ + active_metadata = MozSiteMetadata.from_runtime() + assert ( + active_metadata + ), "A Mach-managed site must be active before doing work with command sites" + + mach_site_packages_source = active_metadata.mach_site_packages_source + pip_restricted_site = site_name in PIP_NETWORK_INSTALL_RESTRICTED_VIRTUALENVS + if not pip_restricted_site and mach_site_packages_source == SitePackagesSource.SYSTEM: + # Sites that aren't pip-network-install-restricted are likely going to be + # incompatible with the system. Besides, this use case shouldn't exist, since + # using the system packages is supposed to only be needed to lower risk of + # important processes like building Firefox. + raise Exception( + 'Cannot use MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE="system" for any ' + f"sites other than {PIP_NETWORK_INSTALL_RESTRICTED_VIRTUALENVS}. The " + f'current attempted site is "{site_name}".' + ) + + mach_virtualenv_root = ( + _mach_virtualenv_root(get_state_dir()) + if mach_site_packages_source == SitePackagesSource.VENV + else None + ) + populate_virtualenv = ( + mach_site_packages_source == SitePackagesSource.VENV or not pip_restricted_site + ) + return cls( + topsrcdir, + mach_virtualenv_root, + os.path.join(command_virtualenvs_dir, site_name), + site_name, + active_metadata, + populate_virtualenv, + resolve_requirements(topsrcdir, site_name), + ) diff --git a/comm/python/mutlh/mutlh/test/__init__.py b/comm/python/mutlh/mutlh/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/comm/python/mutlh/mutlh/test/conftest.py b/comm/python/mutlh/mutlh/test/conftest.py new file mode 100644 index 0000000000..6fcac64308 --- /dev/null +++ b/comm/python/mutlh/mutlh/test/conftest.py @@ -0,0 +1,11 @@ +# 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 https://mozilla.org/MPL/2.0/. + +import os +import sys + +HERE = os.path.dirname(__file__) +EXT_PATH = os.path.abspath(os.path.join(HERE, "..", "..")) + +sys.path.insert(0, EXT_PATH) diff --git a/comm/python/mutlh/mutlh/test/python.ini b/comm/python/mutlh/mutlh/test/python.ini new file mode 100644 index 0000000000..3dafe825cd --- /dev/null +++ b/comm/python/mutlh/mutlh/test/python.ini @@ -0,0 +1,11 @@ +[DEFAULT] +subsuite = mutlh + +[test_decorators.py] +[test_site.py] +[test_site_compatibility.py] +# The Windows and Mac workers only use the internal PyPI mirror, +# which will be missing packages required for this test. +skip-if = + os == "win" + os == "mac" diff --git a/comm/python/mutlh/mutlh/test/test_decorators.py b/comm/python/mutlh/mutlh/test/test_decorators.py new file mode 100644 index 0000000000..7e27cd6383 --- /dev/null +++ b/comm/python/mutlh/mutlh/test/test_decorators.py @@ -0,0 +1,82 @@ +# 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/. + +from pathlib import Path +from unittest import mock +from unittest.mock import Mock, patch + +import conftest # noqa: F401 +import pytest +from mozunit import main + +import mach.decorators +import mach.registrar +from mach.requirements import MachEnvRequirements +from mach.site import MozSiteMetadata, SitePackagesSource +from mutlh.decorators import Command, CommandArgument, MutlhCommandBase +from mutlh.site import MutlhCommandSiteManager + + +@pytest.fixture +def registrar(monkeypatch): + test_registrar = mach.registrar.MachRegistrar() + test_registrar.register_category("testing", "Mach unittest", "Testing for mach decorators") + monkeypatch.setattr(mach.decorators, "Registrar", test_registrar) + return test_registrar + + +def test_register_command_with_argument(registrar): + inner_function = Mock() + context = Mock() + context.cwd = "." + + @Command("cmd_foo", category="testing") + @CommandArgument("--arg", default=None, help="Argument help.") + def run_foo(command_context, arg): + inner_function(arg) + + registrar.dispatch("cmd_foo", context, arg="argument") + + inner_function.assert_called_with("argument") + + +def test_register_command_sets_up_class_at_runtime(registrar): + inner_function = Mock() + + context = Mock() + context.cwd = "." + + # We test that the virtualenv is set up properly dynamically on + # the instance that actually runs the command. + @Command("cmd_foo", category="testing", virtualenv_name="env_foo") + def run_foo(command_context): + assert Path(command_context.virtualenv_manager.virtualenv_root).name == "env_foo" + inner_function("foo") + + @Command("cmd_bar", category="testing", virtualenv_name="env_bar") + def run_bar(command_context): + assert Path(command_context.virtualenv_manager.virtualenv_root).name == "env_bar" + inner_function("bar") + + def from_environment_patch(topsrcdir: str, state_dir: str, virtualenv_name, directory: str): + return MutlhCommandSiteManager( + "", + "", + virtualenv_name, + virtualenv_name, + MozSiteMetadata(0, "mach", SitePackagesSource.VENV, "", ""), + True, + MachEnvRequirements(), + ) + + with mock.patch.object(MutlhCommandSiteManager, "from_environment", from_environment_patch): + with patch.object(MutlhCommandBase, "activate_virtualenv"): + registrar.dispatch("cmd_foo", context) + inner_function.assert_called_with("foo") + registrar.dispatch("cmd_bar", context) + inner_function.assert_called_with("bar") + + +if __name__ == "__main__": + main() diff --git a/comm/python/mutlh/mutlh/test/test_site.py b/comm/python/mutlh/mutlh/test/test_site.py new file mode 100644 index 0000000000..f5c11cadd7 --- /dev/null +++ b/comm/python/mutlh/mutlh/test/test_site.py @@ -0,0 +1,33 @@ +# 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/. + +import os +from contextlib import nullcontext as does_not_raise + +import conftest # noqa: F401 +import mozunit +import pytest + +from buildconfig import topsrcdir +from mutlh.site import SiteNotFoundException, find_manifest + + +@pytest.mark.parametrize( + "site_name,expected", + [ + ("tb_common", does_not_raise("comm/python/sites/tb_common.txt")), + ("lint", does_not_raise("python/sites/lint.txt")), + ("not_a_real_site_name", pytest.raises(SiteNotFoundException)), + ], +) +def test_find_manifest(site_name, expected): + def get_path(result): + return os.path.relpath(result, topsrcdir) + + with expected: + assert get_path(find_manifest(topsrcdir, site_name)) == expected.enter_result + + +if __name__ == "__main__": + mozunit.main() diff --git a/comm/python/mutlh/mutlh/test/test_site_compatibility.py b/comm/python/mutlh/mutlh/test/test_site_compatibility.py new file mode 100644 index 0000000000..c316e54240 --- /dev/null +++ b/comm/python/mutlh/mutlh/test/test_site_compatibility.py @@ -0,0 +1,194 @@ +# 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/. + +import os +import shutil +import subprocess +import sys +from pathlib import Path +from textwrap import dedent + +import mozunit + +from buildconfig import topsrcdir +from mach.requirements import MachEnvRequirements +from mach.site import PythonVirtualenv + +MUTLH_REQUIREMENTS_PATH = Path(topsrcdir) / "comm" / "python" / "sites" +MACH_REQUIREMENTS_PATH = Path(topsrcdir) / "python" / "sites" + + +def _resolve_command_site_names(): + site_names = [] + for child in MUTLH_REQUIREMENTS_PATH.iterdir(): + if not child.is_file(): + continue + + if child.suffix != ".txt": + continue + + if child.name == "mach.txt": + continue + + site_names.append(child.stem) + return site_names + + +def _requirement_definition_to_pip_format(site_name, cache, is_mach_or_build_env): + """Convert from parsed requirements object to pip-consumable format""" + if site_name == "mach": + requirements_path = MACH_REQUIREMENTS_PATH / f"{site_name}.txt" + else: + requirements_path = MUTLH_REQUIREMENTS_PATH / f"{site_name}.txt" + is_thunderbird = True + + requirements = MachEnvRequirements.from_requirements_definition( + topsrcdir, is_thunderbird, not is_mach_or_build_env, requirements_path + ) + + lines = [] + for pypi in requirements.pypi_requirements + requirements.pypi_optional_requirements: + lines.append(str(pypi.requirement)) + + for vendored in requirements.vendored_requirements: + lines.append(str(cache.package_for_vendor_dir(Path(vendored.path)))) + + for pth in requirements.pth_requirements: + path = Path(pth.path) + + if "third_party" not in (p.name for p in path.parents): + continue + + for child in path.iterdir(): + if child.name.endswith(".dist-info"): + raise Exception( + f'In {requirements_path}, the "pth:" pointing to "{path}" has a ' + '".dist-info" file.\n' + 'Perhaps it should change to start with "vendored:" instead of ' + '"pth:".' + ) + if child.name.endswith(".egg-info"): + raise Exception( + f'In {requirements_path}, the "pth:" pointing to "{path}" has an ' + '".egg-info" file.\n' + 'Perhaps it should change to start with "vendored:" instead of ' + '"pth:".' + ) + + return "\n".join(lines) + + +class PackageCache: + def __init__(self, storage_dir: Path): + self._cache = {} + self._storage_dir = storage_dir + + def package_for_vendor_dir(self, vendor_path: Path): + if vendor_path in self._cache: + return self._cache[vendor_path] + + if not any((p for p in vendor_path.iterdir() if p.name.endswith(".dist-info"))): + # This vendored package is not a wheel. It may be a source package (with + # a setup.py), or just some Python code that was manually copied into the + # tree. If it's a source package, the setup.py file may be up a few levels + # from the referenced Python module path. + package_dir = vendor_path + while True: + if (package_dir / "setup.py").exists(): + break + elif package_dir.parent == package_dir: + raise Exception( + f'Package "{vendor_path}" is not a wheel and does not have a ' + 'setup.py file. Perhaps it should be "pth:" instead of ' + '"vendored:"?' + ) + package_dir = package_dir.parent + + self._cache[vendor_path] = package_dir + return package_dir + + # Pip requires that wheels have a version number in their name, even if + # it ignores it. We should parse out the version and put it in here + # so that failure debugging is easier, but that's non-trivial work. + # So, this "0" satisfies pip's naming requirement while being relatively + # obvious that it's a placeholder. + output_path = self._storage_dir / f"{vendor_path.name}-0-py3-none-any" + shutil.make_archive(str(output_path), "zip", vendor_path) + + whl_path = output_path.parent / (output_path.name + ".whl") + (output_path.parent / (output_path.name + ".zip")).rename(whl_path) + self._cache[vendor_path] = whl_path + + return whl_path + + +def test_sites_compatible(tmpdir: str): + command_site_names = _resolve_command_site_names() + work_dir = Path(tmpdir) + cache = PackageCache(work_dir) + mach_requirements = _requirement_definition_to_pip_format("mach", cache, True) + + # Create virtualenv to try to install all dependencies into. + virtualenv = PythonVirtualenv(str(work_dir / "env")) + subprocess.check_call( + [ + sys.executable, + "-m", + "venv", + "--without-pip", + virtualenv.prefix, + ] + ) + platlib_dir = virtualenv.resolve_sysconfig_packages_path("platlib") + third_party = Path(topsrcdir) / "third_party" / "python" + with open(os.path.join(platlib_dir, "site.pth"), "w") as pthfile: + pthfile.write( + "\n".join( + [ + str(third_party / "pip"), + str(third_party / "wheel"), + str(third_party / "setuptools"), + ] + ) + ) + + for name in command_site_names: + print(f'Checking compatibility of "{name}" site') + command_requirements = _requirement_definition_to_pip_format(name, cache, False) + with open(work_dir / "requirements.txt", "w") as requirements_txt: + requirements_txt.write(mach_requirements) + requirements_txt.write("\n") + requirements_txt.write(command_requirements) + + # Attempt to install combined set of dependencies (global Mach + current + # command) + proc = subprocess.run( + [ + virtualenv.python_path, + "-m", + "pip", + "install", + "-r", + str(work_dir / "requirements.txt"), + ], + cwd=topsrcdir, + ) + if proc.returncode != 0: + print( + dedent( + f""" + Error: The '{name}' site contains dependencies that are not + compatible with the 'mach' site. Check the following files for + any conflicting packages mentioned in the prior error message: + + python/sites/mach.txt + comm/python/sites/{name}.txt + """ + ) + ) + assert False + + +if __name__ == "__main__": + mozunit.main() diff --git a/comm/python/rocboot/README.rst b/comm/python/rocboot/README.rst new file mode 100644 index 0000000000..64db75f5a5 --- /dev/null +++ b/comm/python/rocboot/README.rst @@ -0,0 +1,20 @@ +rocboot - Bootstrap your system to build Mozilla Thunderbird! +============================================================= + +This package contains code used for bootstrapping a system to build from +comm-central. + +This code is not part of the build system per se. Instead, it is related +to everything up to invoking the actual build system. + +If you have a copy of the source tree, you run: + + python bin/bootstrap.py + +If you don't have a copy of the source tree, you can run: + + curl https://hg.mozilla.org/comm-central/raw-file/default/python/rocboot/bin/bootstrap.py -o bootstrap.py + python bootstrap.py + +The bootstrap script will download everything it needs from hg.mozilla.org +automagically! diff --git a/comm/python/rocboot/bin/bootstrap.py b/comm/python/rocboot/bin/bootstrap.py new file mode 100755 index 0000000000..3d1470a96a --- /dev/null +++ b/comm/python/rocboot/bin/bootstrap.py @@ -0,0 +1,444 @@ +#!/usr/bin/env python +# 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 provides one-line bootstrap support to configure systems to build +# the tree. It does so by cloning the repo before calling directly into `mach +# bootstrap`. + +# mozboot bootstrap.py was mangled and maimed in the creation of this script. + +# Note that this script can't assume anything in particular about the host +# Python environment (except that it's run with a sufficiently recent version of +# Python 3), so we are restricted to stdlib modules. + +import sys + +major, minor = sys.version_info[:2] +if (major < 3) or (major == 3 and minor < 5): + print("Bootstrap currently only runs on Python 3.5+." "Please try re-running with python3.5+.") + sys.exit(1) + +import ctypes +import os +import shutil +import subprocess +import tempfile +from optparse import OptionParser +from pathlib import Path + +CLONE_MERCURIAL_PULL_FAIL = """ +Failed to pull from hg.mozilla.org. + +This is most likely because of unstable network connection. +Try running `cd %s && hg pull https://hg.mozilla.org/comm-central` manually, +or download a mercurial bundle and use it: +https://firefox-source-docs.mozilla.org/contributing/vcs/mercurial_bundles.html""" + +WINDOWS = sys.platform.startswith("win32") or sys.platform.startswith("msys") +VCS_HUMAN_READABLE = { + "hg": "Mercurial", + "git": "Git", +} + + +def which(name): + """Python implementation of which. + + It returns the path of an executable or None if it couldn't be found. + """ + # git-cinnabar.exe doesn't exist, but .exe versions of the other executables + # do. + if WINDOWS and name != "git-cinnabar": + name += ".exe" + search_dirs = os.environ["PATH"].split(os.pathsep) + + for path in search_dirs: + test = Path(path) / name + if test.is_file() and os.access(test, os.X_OK): + return test + + return None + + +def validate_clone_dest(dest: Path): + dest = dest.resolve() + + if not dest.exists(): + return dest + + if not dest.is_dir(): + print(f"ERROR! Destination {dest} exists but is not a directory.") + return None + + if not any(dest.iterdir()): + return dest + else: + print(f"ERROR! Destination directory {dest} exists but is nonempty.") + print( + f"To re-bootstrap the existing checkout, go into '{dest}' and run './mach bootstrap'." + ) + return None + + +def input_clone_dest(vcs, no_interactive): + repo_name = "mozilla-unified" + print(f"Cloning into {repo_name} using {VCS_HUMAN_READABLE[vcs]}...") + while True: + dest = None + if not no_interactive: + dest = input( + f"Destination directory for clone (leave empty to use " + f"default destination of {repo_name}): " + ).strip() + if not dest: + dest = repo_name + dest = validate_clone_dest(Path(dest).expanduser()) + if dest: + return dest + if no_interactive: + return None + + +def hg_clone(hg: Path, repo, dest: Path, watchman: Path): + print(f"Cloning {repo} to {dest}...") + # We create an empty repo then modify the config before adding data. + # This is necessary to ensure storage settings are optimally + # configured. + args = [ + str(hg), + # The unified repo is generaldelta, so ensure the client is as + # well. + "--config", + "format.generaldelta=true", + "init", + str(dest), + ] + res = subprocess.call(args) + if res: + print("unable to create destination repo; please try cloning manually") + return None + + # Strictly speaking, this could overwrite a config based on a template + # the user has installed. Let's pretend this problem doesn't exist + # unless someone complains about it. + with open(dest / ".hg" / "hgrc", "a") as fh: + fh.write("[paths]\n") + fh.write("default = https://hg.mozilla.org/{}\n".format(repo)) + fh.write("\n") + + # The server uses aggressivemergedeltas which can blow up delta chain + # length. This can cause performance to tank due to delta chains being + # too long. Limit the delta chain length to something reasonable + # to bound revlog read time. + fh.write("[format]\n") + fh.write("# This is necessary to keep performance in check\n") + fh.write("maxchainlen = 10000\n") + + res = subprocess.call( + [str(hg), "pull", "https://hg.mozilla.org/{}".format(repo)], cwd=str(dest) + ) + print("") + if res: + print(CLONE_MERCURIAL_PULL_FAIL % dest) + return None + + update_rev = {"mozilla-unified": "central", "comm-central": "tip"}[repo] + print("updating to {}".format(update_rev)) + res = subprocess.call([str(hg), "update", "-r", update_rev], cwd=str(dest)) + if res: + print( + f"error updating; you will need to `cd {dest} && hg update -r {update_rev}` " + "manually" + ) + return dest + + +def git_clone(git: Path, repo, dest: Path, watchman: Path): + print(f"Cloning {repo} to {dest}...") + tempdir = None + cinnabar = None + env = dict(os.environ) + try: + cinnabar = which("git-cinnabar") + if not cinnabar: + cinnabar_url = "https://github.com/glandium/git-cinnabar/" + # If git-cinnabar isn't installed already, that's fine; we can + # download a temporary copy. `mach bootstrap` will clone a full copy + # of the repo in the state dir; we don't want to copy all that logic + # to this tiny bootstrapping script. + tempdir = Path(tempfile.mkdtemp()) + cinnabar_dir = tempdir / "git-cinnabar-master" + subprocess.check_call( + [str(git), "clone", "--depth=1", str(cinnabar_url), str(cinnabar_dir)], + cwd=str(tempdir), + env=env, + ) + env["PATH"] = str(cinnabar_dir) + os.pathsep + env["PATH"] + subprocess.check_call( + [sys.executable, str(cinnabar_dir / "download.py")], + cwd=str(cinnabar_dir), + env=env, + ) + print( + "WARNING! git-cinnabar is required for Firefox development " + "with git. After the clone is complete, the bootstrapper " + "will ask if you would like to configure git; answer yes, " + "and be sure to add git-cinnabar to your PATH according to " + "the bootstrapper output." + ) + + # We're guaranteed to have `git-cinnabar` installed now. + # Configure git per the git-cinnabar requirements. + cmd = [ + str(git), + "clone", + ] + if repo == "mozilla-unified": + cmd += [ + "-b", + "bookmarks/central", + ] + cmd += [ + "hg::https://hg.mozilla.org/{}".format(repo), + str(dest), + ] + subprocess.check_call(cmd, env=env) + subprocess.check_call([str(git), "config", "fetch.prune", "true"], cwd=str(dest), env=env) + subprocess.check_call([str(git), "config", "pull.ff", "only"], cwd=str(dest), env=env) + + watchman_sample = dest / ".git/hooks/fsmonitor-watchman.sample" + # Older versions of git didn't include fsmonitor-watchman.sample. + if watchman and watchman_sample.exists(): + print("Configuring watchman") + watchman_config = dest / ".git/hooks/query-watchman" + if not watchman_config.exists(): + print(f"Copying {watchman_sample} to {watchman_config}") + copy_args = [ + "cp", + ".git/hooks/fsmonitor-watchman.sample", + ".git/hooks/query-watchman", + ] + subprocess.check_call(copy_args, cwd=str(dest)) + + config_args = [ + str(git), + "config", + "core.fsmonitor", + ".git/hooks/query-watchman", + ] + subprocess.check_call(config_args, cwd=str(dest), env=env) + return dest + finally: + if not cinnabar: + print( + "Failed to install git-cinnabar. Try performing a manual " + "installation: https://github.com/glandium/git-cinnabar/wiki/" + "Mozilla:-A-git-workflow-for-Gecko-development" + ) + if tempdir: + shutil.rmtree(str(tempdir)) + + +def add_microsoft_defender_antivirus_exclusions(dest, no_system_changes): + if no_system_changes: + return + + if not WINDOWS: + return + + powershell_exe = which("powershell") + + if not powershell_exe: + return + + def print_attempt_exclusion(path): + print(f"Attempting to add exclusion path to Microsoft Defender Antivirus for: {path}") + + powershell_exe = str(powershell_exe) + paths = [] + + # mozilla-unified / clone dest + repo_dir = Path.cwd() / dest + paths.append(repo_dir) + print_attempt_exclusion(repo_dir) + + # MOZILLABUILD + mozillabuild_dir = os.getenv("MOZILLABUILD") + if mozillabuild_dir: + paths.append(mozillabuild_dir) + print_attempt_exclusion(mozillabuild_dir) + + # .mozbuild + mozbuild_dir = Path.home() / ".mozbuild" + paths.append(mozbuild_dir) + print_attempt_exclusion(mozbuild_dir) + + args = ";".join(f"Add-MpPreference -ExclusionPath '{path}'" for path in paths) + command = f'-Command "{args}"' + + # This will attempt to run as administrator by triggering a UAC prompt + # for admin credentials. If "No" is selected, no exclusions are added. + ctypes.windll.shell32.ShellExecuteW(None, "runas", powershell_exe, command, None, 0) + + +def clone(options): + vcs = options.vcs + no_interactive = options.no_interactive + no_system_changes = options.no_system_changes + + hg = which("hg") + if not hg: + print( + "Mercurial is not installed. Mercurial is required to clone " + "Thunderbird%s." % (", even when cloning with Git" if vcs == "git" else "") + ) + try: + # We're going to recommend people install the Mercurial package with + # pip3. That will work if `pip3` installs binaries to a location + # that's in the PATH, but it might not be. To help out, if we CAN + # import "mercurial" (in which case it's already been installed), + # offer that as a solution. + import mercurial # noqa: F401 + + print( + "Hint: have you made sure that Mercurial is installed to a " + "location in your PATH?" + ) + except ImportError: + print("Try installing hg with `pip3 install Mercurial`.") + return None + + if vcs == "hg": + binary = hg + else: + binary = which(vcs) + if not binary: + print("Git is not installed.") + print("Try installing git using your system package manager.") + return None + + dest = input_clone_dest(vcs, no_interactive) + if not dest: + return None + + add_microsoft_defender_antivirus_exclusions(dest, no_system_changes) + + if vcs == "hg": + clone_func = hg_clone + else: + clone_func = git_clone + watchman = which("watchman") + + mc = clone_func(binary, "mozilla-unified", dest, watchman) + # Funny logic... the return value if successful needs to be the path + # to mozilla-central. Only return "cc" if cloning comm-central + # fails. + if mc == dest: + cc_dest = Path(dest) / "comm" + cc = clone_func(binary, "comm-central", cc_dest, watchman) + if cc: + return mc + else: + return cc + return mc + + +def bootstrap(srcdir: Path, artifact_mode, no_interactive, no_system_changes): + args = [sys.executable, "mach"] + + if no_interactive: + # --no-interactive is a global argument, not a command argument, + # so it needs to be specified before "bootstrap" is appended. + args += ["--no-interactive"] + + args += ["bootstrap"] + + if artifact_mode: + args += ["--application-choice", "Firefox for Desktop Artifact Mode"] + else: + args += ["--application-choice", "Firefox for Desktop"] + if no_system_changes: + args += ["--no-system-changes"] + + print("Running `%s`" % " ".join(args)) + return subprocess.call(args, cwd=str(srcdir)) + + +def mozconfig(srcdir): + """Build Thunderbird, not Firefox!""" + mozconfig = os.path.join(srcdir, "mozconfig") + with open(mozconfig, "a") as mfp: + mfp.write("ac_add_options --enable-project=comm/mail") + return True + + +def main(args): + parser = OptionParser() + parser.add_option( + "--artifact-mode", + dest="artifact_mode", + help="Build Thunderbird in Artifact mode. " + "See https://firefox-source-docs.mozilla.org/contributing/build" + "/artifact_builds.html for details.", + ) + parser.add_option( + "--vcs", + dest="vcs", + default="hg", + choices=["git", "hg"], + help="VCS (hg or git) to use for downloading the source code, " + "instead of using the default interactive prompt.", + ) + parser.add_option( + "--no-interactive", + dest="no_interactive", + action="store_true", + help="Answer yes to any (Y/n) interactive prompts.", + ) + parser.add_option( + "--no-system-changes", + dest="no_system_changes", + action="store_true", + help="Only executes actions that leave the system " "configuration alone.", + ) + + options, leftover = parser.parse_args(args) + + try: + srcdir = clone(options) + if not srcdir: + return 1 + print("Clone complete.") + print( + "If you need to run the tooling bootstrapping again, " + "then consider running './mach bootstrap' instead." + ) + if not options.no_interactive: + remove_bootstrap_file = input( + "Unless you are going to have more local copies of Firefox source code, " + "this 'bootstrap.py' file is no longer needed and can be deleted. " + "Clean up the bootstrap.py file? (Y/n)" + ) + if not remove_bootstrap_file: + remove_bootstrap_file = "y" + if options.no_interactive or remove_bootstrap_file == "y": + try: + Path(sys.argv[0]).unlink() + except FileNotFoundError: + print("File could not be found !") + bootstrap( + srcdir, + options.artifact_mode, + options.no_interactive, + options.no_system_changes, + ) + return mozconfig(srcdir) + except Exception: + print("Could not bootstrap Thunderbird! Consider filing a bug.") + raise + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/comm/python/rocbuild/rocbuild/__init__.py b/comm/python/rocbuild/rocbuild/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/comm/python/rocbuild/rocbuild/notify.py b/comm/python/rocbuild/rocbuild/notify.py new file mode 100644 index 0000000000..b7bc135ea4 --- /dev/null +++ b/comm/python/rocbuild/rocbuild/notify.py @@ -0,0 +1,34 @@ +# 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/. + +""" +Notification utility functions +""" + +import os + +from taskcluster import Notify, optionsFromEnvironment + +TB_BUILD_ADDR = "tb-builds@thunderbird.net" + + +def email_notification(subject, content, recipients=None): + # use proxy if configured, otherwise local credentials from env vars + if recipients is None: + recipients = [TB_BUILD_ADDR] + + if "TASKCLUSTER_PROXY_URL" in os.environ: + notify_options = {"rootUrl": os.environ["TASKCLUSTER_PROXY_URL"]} + else: + notify_options = optionsFromEnvironment() + + notify = Notify(notify_options) + for address in recipients: + notify.email( + { + "address": address, + "subject": subject, + "content": content, + } + ) diff --git a/comm/python/sites/tb_common.txt b/comm/python/sites/tb_common.txt new file mode 100644 index 0000000000..932cf6a177 --- /dev/null +++ b/comm/python/sites/tb_common.txt @@ -0,0 +1,7 @@ +pth:comm/python/l10n +pth:comm/python/mutlh +pth:comm/python/rocbuild +pth:comm/python/thirdroc +pth:comm/taskcluster +pth:comm/testing/marionette +vendored:comm/third_party/python/fluent.migratetb diff --git a/comm/python/thirdroc/rnp_generated.py b/comm/python/thirdroc/rnp_generated.py new file mode 100644 index 0000000000..9c3390e4b9 --- /dev/null +++ b/comm/python/thirdroc/rnp_generated.py @@ -0,0 +1,116 @@ +# 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 https://mozilla.org/MPL/2.0/. + +# 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/. + +import argparse +import os +import sys + +from packaging.version import parse + +from mozbuild.preprocessor import Preprocessor +from mozbuild.util import FileAvoidWrite, ensureParentDir + +from thirdroc.cmake_define_files import define_type, process_cmake_define_file + + +def rnp_version(version_file, thunderbird_version): + """ + Update RNP source files: generate version.h + :param string version_file: + """ + with open(version_file) as fp: + version_str = fp.readline(512).strip() + + version = parse(version_str) + version_major = version.major + version_minor = version.minor + version_patch = version.micro + + version_full = f"{version_str}.MZLA.{thunderbird_version}" + + defines = dict( + RNP_VERSION_MAJOR=version_major, + RNP_VERSION_MINOR=version_minor, + RNP_VERSION_PATCH=version_patch, + RNP_VERSION=version_str, + RNP_VERSION_FULL=version_full, + # Follow upstream's example when commit info is unavailable + RNP_VERSION_COMMIT_TIMESTAMP="0", + PACKAGE_STRING=f'"rnp {version_full}"', + ) + + return defines + + +def rnp_preprocess(tmpl, dest, defines): + """ + Generic preprocessing + :param BinaryIO tmpl: open filehandle (read) input + :param BinaryIO dest: open filehandle (write) output + :param dict defines: result of get_defines() + :return boolean: + """ + pp = Preprocessor() + pp.setMarker("%") + pp.addDefines(defines) + pp.do_filter("substitution") + pp.out = dest + pp.do_include(tmpl, True) + return True + + +def generate_version_h(output, template, defines): + """ + Generate version.h for rnp from a the template file, write the + result to destination. + :param string template: path to template file (version.h.in) + :param string destination: path to write generated file (version.h) + :param dict defines: result of get_defines() + """ + with open(template) as tmpl: + rnp_preprocess(tmpl, output, defines) + + +def main(output, *argv): + parser = argparse.ArgumentParser(description="Preprocess RNP files.") + + parser.add_argument("version_h_in", help="version.h.in") + parser.add_argument("config_h_in", help="config.h.in") + parser.add_argument("-m", type=str, dest="thunderbird_version", help="Thunderbird version") + parser.add_argument("-V", type=str, dest="version_file", help="Path to RNP version.txt") + parser.add_argument( + "-D", + type=define_type, + action="append", + dest="extra_defines", + default=[], + help="Additional defines not set at configure time.", + ) + + args = parser.parse_args(argv) + + defines = rnp_version(args.version_file, args.thunderbird_version) + + # "output" is an open filedescriptor for version.h + generate_version_h(output, args.version_h_in, defines) + + # We must create the remaining output files ourselves. This requires + # creating the output directory directly if it doesn't already exist. + ensureParentDir(output.name) + parent_dir = os.path.dirname(output.name) + + # For config.h, include defines set for version.h and extra -D args + config_h_defines = dict(args.extra_defines) + config_h_defines.update(defines) + + with FileAvoidWrite(os.path.join(parent_dir, "config.h")) as fd: + process_cmake_define_file(fd, args.config_h_in, config_h_defines) + + +if __name__ == "__main__": + sys.exit(main(*sys.argv)) diff --git a/comm/python/thirdroc/setup.py b/comm/python/thirdroc/setup.py new file mode 100644 index 0000000000..2a4cf6d3b5 --- /dev/null +++ b/comm/python/thirdroc/setup.py @@ -0,0 +1,24 @@ +# 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/. + +from setuptools import find_packages, setup + +VERSION = "0.1" + +setup( + author="MZLA Technologies", + author_email="MZLA Technologies Release Engineering", + name="thirdroc", + description="Utility for maintaining third party source code in Thunderbird", + license="MPL 2.0", + packages=find_packages(), + version=VERSION, + classifiers=[ + "Development Status :: 3 - Alpha", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: Implementation :: CPython", + ], +) diff --git a/comm/python/thirdroc/thirdroc/__init__.py b/comm/python/thirdroc/thirdroc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/comm/python/thirdroc/thirdroc/cmake_define_files.py b/comm/python/thirdroc/thirdroc/cmake_define_files.py new file mode 100644 index 0000000000..2c53bf2d54 --- /dev/null +++ b/comm/python/thirdroc/thirdroc/cmake_define_files.py @@ -0,0 +1,102 @@ +# 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/. + +import argparse +import os +import re +import sys + +from buildconfig import topobjdir +from mozbuild.backend.configenvironment import PartialConfigEnvironment + + +def define_type(string): + vals = string.split("=", 1) + if len(vals) == 1: + vals.append(1) + elif vals[1].isdecimal(): + vals[1] = int(vals[1]) + return tuple(vals) + + +def process_cmake_define_file(output, input_file, extra_defines): + """Creates the given config header. A config header is generated by + taking the corresponding source file and replacing some #define/#undef + occurrences: + "#undef NAME" is turned into "#define NAME VALUE" + "#cmakedefine NAME" is turned into "#define NAME VALUE" + "#define NAME" is unchanged + "#define NAME ORIGINAL_VALUE" is turned into "#define NAME VALUE" + "#undef UNKNOWN_NAME" is turned into "/* #undef UNKNOWN_NAME */" + "#cmakedefine UNKNOWN_NAME" is turned into "/* #undef UNKNOWN_NAME */" + Whitespaces are preserved. + """ + + path = os.path.abspath(input_file) + + config = PartialConfigEnvironment(topobjdir) + + defines = dict(config.defines.iteritems()) + defines.update(extra_defines) + + with open(path, "r") as input_file: + r = re.compile( + r'^\s*#\s*(?P[a-z]+)(?:\s+(?P\S+)(?:\s+(?P("[^"]+"|\S+)))?)?', + re.U, + ) + for line in input_file: + m = r.match(line) + if m: + cmd = m.group("cmd") + name = m.group("name") + value = m.group("value") + if name: + if cmd == "define": + if value and name in defines: + line = ( + line[: m.start("value")] + + str(defines[name]) + + line[m.end("value") :] + ) + elif cmd in ("undef", "cmakedefine"): + if name in defines: + line = ( + line[: m.start("cmd")] + + "define" + + line[m.end("cmd") : m.end("name")] + + " " + + str(defines[name]) + + line[m.end("name") :] + ) + else: + line = ( + "/* #undef " + + line[m.start("name") : m.end("name")] + + " */" + + line[m.end("name") :] + ) + + output.write(line) + + +def main(output, *argv): + parser = argparse.ArgumentParser(description="Process define files.") + + parser.add_argument("input", help="Input define file.") + parser.add_argument( + "-D", + type=define_type, + action="append", + dest="extra_defines", + default=[], + help="Additional defines not set at configure time.", + ) + + args = parser.parse_args(argv) + + return process_cmake_define_file(output, args.input, args.extra_defines) + + +if __name__ == "__main__": + sys.exit(main(*sys.argv)) diff --git a/comm/python/thirdroc/thirdroc/rnp_symbols.py b/comm/python/thirdroc/thirdroc/rnp_symbols.py new file mode 100644 index 0000000000..95f430f59c --- /dev/null +++ b/comm/python/thirdroc/thirdroc/rnp_symbols.py @@ -0,0 +1,131 @@ +#!/usr/bin/python3 +# 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/. +""" +Parse rnp/rnp.h header file and build a symbols file suitable +for use with mozbuild. + +This script is meant to be run when the public C API of librnp adds or removes functions so that +they can be exported by the shared library. + +Limitations: The regex that captures the function name is very basic and may need adjusting if +the third_party/rnp/include/rnp/rnp.h format changes too much. +Also note that APIs that are marked deprecated are not checked for. + +Dependencies: Only Python 3 + +Running: + python3 rnp_symbols.py [-h] [rnp.h path] [rnp.symbols path] + +Both file path arguments are optional. By default, the header file will be +read from "comm/third_party/rnp/include/rnp/rnp.h" and the symbols file will +be written to "comm/third_party/rnp/rnp.symbols". + +Path arguments are relative to the current working directory, the defaults +will be determined based on the location of this script. + +Either path argument can be '-' to use stdin or stdout respectively. +""" + +import argparse +import os +import re +import sys + +HERE = os.path.dirname(__file__) +TOPSRCDIR = os.path.abspath(os.path.join(HERE, "../../../../")) +THIRD_SRCDIR = os.path.join(TOPSRCDIR, "comm/third_party") +HEADER_FILE_REL = "rnp/include/rnp/rnp.h" +HEADER_FILE = os.path.join(THIRD_SRCDIR, HEADER_FILE_REL) +SYMBOLS_FILE_REL = "rnp/rnp.symbols" +SYMBOLS_FILE = os.path.join(THIRD_SRCDIR, SYMBOLS_FILE_REL) + + +FUNC_DECL_RE = re.compile(r"^RNP_API\s+.*?([a-zA-Z0-9_]+)\(.*$") + + +class FileArg: + """Based on argparse.FileType from the Python standard library. + Modified to not open the filehandles until the open() method is + called. + """ + + def __init__(self, mode="r"): + self._mode = mode + self._fp = None + self._file = None + + def __call__(self, string): + # the special argument "-" means sys.std{in,out} + if string == "-": + if "r" in self._mode: + self._fp = sys.stdin.buffer if "b" in self._mode else sys.stdin + elif "w" in self._mode: + self._fp = sys.stdout.buffer if "b" in self._mode else sys.stdout + else: + raise ValueError(f"Invalid mode {self._mode} for stdin/stdout") + else: + if "r" in self._mode: + if not os.path.isfile(string): + raise ValueError(f"Cannot read file {string}, does not exist.") + elif "w" in self._mode: + if not os.access(string, os.W_OK): + raise ValueError(f"Cannot write file {string}, permission denied.") + self._file = string + return self + + def open(self): + if self._fp: + return self._fp + return open(self._file, self._mode) + + +def get_func_name(line): + """ + Extract the function name from a RNP_API function declaration. + Examples: + RNP_API rnp_result_t rnp_enable_debug(const char *file); + + RNP_API rnp_result_t rnp_ffi_create(rnp_ffi_t * ffi, + """ + m = FUNC_DECL_RE.match(line) + return m.group(1) + + +def extract_func_defs(filearg): + """ + Look for RNP_API in the header file to find the names of the symbols that should be exported + """ + with filearg.open() as fp: + for line in fp: + if line.startswith("RNP_API") and "RNP_DEPRECATED" not in line: + func_name = get_func_name(line) + yield func_name + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Update rnp.symbols file from rnp.h", + epilog="To use stdin or stdout pass '-' for the argument.", + ) + parser.add_argument( + "header_file", + default=HEADER_FILE, + type=FileArg("r"), + nargs="?", + help=f"input path to rnp.h header file (default: {HEADER_FILE_REL})", + ) + parser.add_argument( + "symbols_file", + default=SYMBOLS_FILE, + type=FileArg("w"), + nargs="?", + help=f"output path to symbols file (default: {SYMBOLS_FILE_REL})", + ) + + args = parser.parse_args() + + with args.symbols_file.open() as out_fp: + for symbol in sorted(list(extract_func_defs(args.header_file))): + out_fp.write(f"{symbol}\n") -- cgit v1.2.3