#!/usr/bin/python3 # Author: Benjamin Drung """Generate tzdata debconf templates file.""" import argparse import logging import os import pathlib import sys import natsort LOG_FORMAT = "%(name)s %(levelname)s: %(message)s" __script_name__ = os.path.basename(sys.argv[0]) if __name__ == "__main__" else __name__ TEMPLATES_AREAS = [ "Africa", "America", "Antarctica", "Arctic", "Asia", "Atlantic", "Australia", "Europe", "Indian", "Pacific", "Etc", ] # List of backward compatability symlinks that should not be selectable EXCLUDE_SYMLINKS = { "Africa/Asmera", "America/Argentina/ComodRivadavia", "America/Buenos_Aires", "America/Catamarca", "America/Cordoba", "America/Fort_Wayne", "America/Godthab", "America/Indianapolis", "America/Jujuy", "America/Knox_IN", "America/Louisville", "America/Mendoza", "America/Rosario", "Antarctica/South_Pole", "Asia/Ashkhabad", "Asia/Calcutta", "Asia/Chungking", "Asia/Dacca", "Asia/Katmandu", "Asia/Macao", "Asia/Rangoon", "Asia/Saigon", "Asia/Thimbu", "Asia/Ujung_Pandang", "Asia/Ulan_Bator", "Atlantic/Faeroe", "Australia/ACT", "Australia/LHI", "Australia/NSW", "Australia/North", "Australia/Queensland", "Australia/South", "Australia/Tasmania", "Australia/Victoria", "Australia/West", "Europe/Kiev", "Europe/Uzhgorod", "Europe/Zaporozhye", "Pacific/Enderbury", "Pacific/Ponape", "Pacific/Truk", "US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa", } # List of symlinks that should be selectable INCLUDE_SYMLINKS = { "Africa/Timbuktu", "America/Atka", "America/Coral_Harbour", "America/Ensenada", "America/Kralendijk", "America/Lower_Princes", "America/Marigot", "America/Montreal", "America/Nipigon", "America/Pangnirtung", "America/Porto_Acre", "America/Rainy_River", "America/Santa_Isabel", "America/Shiprock", "America/St_Barthelemy", "America/Thunder_Bay", "America/Virgin", "America/Yellowknife", "Arctic/Longyearbyen", "Asia/Chongqing", "Asia/Harbin", "Asia/Istanbul", "Asia/Kashgar", "Asia/Tel_Aviv", "Atlantic/Jan_Mayen", "Australia/Canberra", "Australia/Currie", "Australia/Yancowinna", "Europe/Belfast", "Europe/Bratislava", "Europe/Busingen", "Europe/Mariehamn", "Europe/Nicosia", "Europe/Podgorica", "Europe/San_Marino", "Europe/Tiraspol", "Europe/Vatican", "Pacific/Johnston", "Pacific/Samoa", "Pacific/Yap", "Etc/GMT+0", "Etc/GMT-0", "Etc/GMT0", "Etc/Greenwich", "Etc/UCT", "Etc/Universal", "Etc/Zulu", } def get_timezones(base_dir: pathlib.Path, subdir: pathlib.Path) -> list[str]: """Return list of timezone files relative to the base_dir.""" timezones = [] for path in natsort.natsorted(subdir.iterdir(), key=str): if path.is_dir(): timezones += get_timezones(base_dir, path) continue timezone = str(path.relative_to(base_dir)) if path.is_symlink(): if timezone in EXCLUDE_SYMLINKS: continue if timezone not in INCLUDE_SYMLINKS: logger = logging.getLogger(__script_name__) logger.error( "Timezone '%s' is a symlink, but neither in EXCLUDE_SYMLINKS" " nor INCLUDE_SYMLINKS. Please add it explicitly.", timezone, ) sys.exit(1) timezones.append(timezone) return timezones def generate_debconf_templates_area(zoneinfo_dir: pathlib.Path, area: str) -> str: """Generate tzdata debconf templates paragraph for given area.""" timezones = get_timezones(zoneinfo_dir, zoneinfo_dir / area) choices = [timezone.split("/", maxsplit=1)[1] for timezone in timezones] if area == "Etc": choices_key = "Choices" description = ( "Please select your time zone. Contrary to modern conventions, these" " POSIX-compatible zones use positive values to refer to zones west of" " Greenwich and negative values for those east of Greenwich" " (e.g., 'Etc/GMT+6' refers to 6 hours west of Greenwich," " commonly called 'UTC-6')." ) else: choices_key = "__Choices" description = ( "Please select the city or region corresponding to your time zone." ) return f"""\ Template: tzdata/Zones/{area} Type: select # Translators: This is a city name. # Do not translate underscores. You can use spaces instead. #flag:partial {choices_key}: {", ".join(choices)} _Description: Time zone: {description} """ def generate_debconf_templates(zoneinfo_dir: pathlib.Path) -> str: """Generate tzdata debconf templates file content.""" debconf_templates = f"""\ # This file was generated by {pathlib.Path(__file__).relative_to(os.getcwd())} # Template: tzdata/Areas Type: select # Note to translators: # - This is a continent or ocean (except for "Etc") # - "Etc" will present users with a list of "GMT+xx" or "GMT-xx" timezones __Choices: {", ".join(TEMPLATES_AREAS)} _Description: Geographic area: Please select the geographic area in which you live. Subsequent configuration questions will narrow this down by presenting a list of cities, representing the time zones in which they are located. """ for area in TEMPLATES_AREAS: debconf_templates += "\n" + generate_debconf_templates_area(zoneinfo_dir, area) return debconf_templates def existing_dir_path(string: str) -> pathlib.Path: """Convert string to existing dir path or raise ArgumentTypeError.""" path = pathlib.Path(string) if not path.is_dir(): raise argparse.ArgumentTypeError(f"Directory {string} does not exist") return path def main() -> None: """Generate tzdata debconf templates file.""" parser = argparse.ArgumentParser() parser.add_argument( "-d", "--directory", default=".", type=existing_dir_path, help="Directory containing the generated zoneinfo files", ) args = parser.parse_args() logging.basicConfig(format=LOG_FORMAT) print(generate_debconf_templates(args.directory), end="") if __name__ == "__main__": main()