1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
#!/usr/bin/python3
# Author: Benjamin Drung <bdrung@ubuntu.com>
"""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/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/Godthab",
"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()
|