keysym-generator.py revision d63b911f
1d63b911fSmrg#!/usr/bin/env python3
2d63b911fSmrg#
3d63b911fSmrg# SPDX-License-Identifier: MIT
4d63b911fSmrg#
5d63b911fSmrg# This script checks XF86keysym.h for the reserved evdev keysym range and/or
6d63b911fSmrg# appends new keysym to that range. An up-to-date libevdev must be
7d63b911fSmrg# available to guarantee the correct keycode ranges and names.
8d63b911fSmrg#
9d63b911fSmrg# Run with --help for usage information.
10d63b911fSmrg#
11d63b911fSmrg#
12d63b911fSmrg# File is formatted with Python Black
13d63b911fSmrg
14d63b911fSmrgimport argparse
15d63b911fSmrgimport logging
16d63b911fSmrgimport sys
17d63b911fSmrgimport re
18d63b911fSmrgimport libevdev
19d63b911fSmrgimport subprocess
20d63b911fSmrgfrom pathlib import Path
21d63b911fSmrg
22d63b911fSmrglogging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
23d63b911fSmrglogger = logging.getLogger("ksgen")
24d63b911fSmrg
25d63b911fSmrgstart_token = re.compile(r"#define _EVDEVK.*")
26d63b911fSmrgend_token = re.compile(r"#undef _EVDEVK\n")
27d63b911fSmrg
28d63b911fSmrg
29d63b911fSmrgdef die(msg):
30d63b911fSmrg    logger.critical(msg)
31d63b911fSmrg    sys.exit(1)
32d63b911fSmrg
33d63b911fSmrg
34d63b911fSmrgdef all_keysyms(directory):
35d63b911fSmrg    """
36d63b911fSmrg    Extract the key names for all keysyms we have in our repo and return
37d63b911fSmrg    them as list.
38d63b911fSmrg    """
39d63b911fSmrg    keysym_names = []
40d63b911fSmrg    pattern = re.compile(r"^#define\s+(?P<name>\w+)\s+(0x[0-9A-Fa-f]+)")
41d63b911fSmrg    for path in directory.glob("*keysym*.h"):
42d63b911fSmrg        with open(path) as fd:
43d63b911fSmrg            for line in fd:
44d63b911fSmrg                match = re.match(pattern, line)
45d63b911fSmrg                if match:
46d63b911fSmrg                    keysym_names.append(match.group("name"))
47d63b911fSmrg    return keysym_names
48d63b911fSmrg
49d63b911fSmrg
50d63b911fSmrgclass Kernel(object):
51d63b911fSmrg    """
52d63b911fSmrg    Wrapper around the kernel git tree to simplify searching for when a
53d63b911fSmrg    particular keycode was introduced.
54d63b911fSmrg    """
55d63b911fSmrg
56d63b911fSmrg    def __init__(self, repo):
57d63b911fSmrg        self.repo = repo
58d63b911fSmrg
59d63b911fSmrg        exitcode, stdout, stderr = self.git_command("git branch --show-current")
60d63b911fSmrg        if exitcode != 0:
61d63b911fSmrg            die(f"{stderr}")
62d63b911fSmrg        if stdout.strip() != "master":
63d63b911fSmrg            die(f"Kernel repo must be on the master branch (current: {stdout.strip()})")
64d63b911fSmrg
65d63b911fSmrg        exitcode, stdout, stderr = self.git_command("git tag --sort=version:refname")
66d63b911fSmrg        tags = stdout.split("\n")
67d63b911fSmrg        self.versions = list(
68d63b911fSmrg            filter(lambda v: re.match(r"^v[2-6]\.[0-9]+(\.[0-9]+)?$", v), tags)
69d63b911fSmrg        )
70d63b911fSmrg        logger.debug(f"Kernel versions: {', '.join(self.versions)}")
71d63b911fSmrg
72d63b911fSmrg    def git_command(self, cmd):
73d63b911fSmrg        """
74d63b911fSmrg        Takes a single-string git command and runs it in the repo.
75d63b911fSmrg
76d63b911fSmrg        Returns the tuple (exitcode, stdout, stderr)
77d63b911fSmrg        """
78d63b911fSmrg        # logger.debug(f"git command: {cmd}")
79d63b911fSmrg        try:
80d63b911fSmrg            result = subprocess.run(
81d63b911fSmrg                cmd.split(" "), cwd=self.repo, capture_output=True, encoding="utf8"
82d63b911fSmrg            )
83d63b911fSmrg            if result.returncode == 128:
84d63b911fSmrg                die(f"{result.stderr}")
85d63b911fSmrg
86d63b911fSmrg            return result.returncode, result.stdout, result.stderr
87d63b911fSmrg        except FileNotFoundError:
88d63b911fSmrg            die(f"{self.repo} is not a git repository")
89d63b911fSmrg
90d63b911fSmrg    def introduced_in_version(self, string):
91d63b911fSmrg        """
92d63b911fSmrg        Search this repo for the first version with string in the headers.
93d63b911fSmrg
94d63b911fSmrg        Returns the kernel version number (e.g. "v5.10") or None
95d63b911fSmrg        """
96d63b911fSmrg
97d63b911fSmrg        # The fastest approach is to git grep every version for the string
98d63b911fSmrg        # and return the first. Using git log -G and then git tag --contains
99d63b911fSmrg        # is an order of magnitude slower.
100d63b911fSmrg        def found_in_version(v):
101d63b911fSmrg            cmd = f"git grep -E \\<{string}\\> {v} -- include/"
102d63b911fSmrg            exitcode, _, _ = self.git_command(cmd)
103d63b911fSmrg            return exitcode == 0
104d63b911fSmrg
105d63b911fSmrg        def bisect(iterable, func):
106d63b911fSmrg            """
107d63b911fSmrg            Return the first element in iterable for which func
108d63b911fSmrg            returns True.
109d63b911fSmrg            """
110d63b911fSmrg            # bias to speed things up: most keycodes will be in the first
111d63b911fSmrg            # kernel version
112d63b911fSmrg            if func(iterable[0]):
113d63b911fSmrg                return iterable[0]
114d63b911fSmrg
115d63b911fSmrg            lo, hi = 0, len(iterable)
116d63b911fSmrg            while lo < hi:
117d63b911fSmrg                mid = (lo + hi) // 2
118d63b911fSmrg                if func(iterable[mid]):
119d63b911fSmrg                    hi = mid
120d63b911fSmrg                else:
121d63b911fSmrg                    lo = mid + 1
122d63b911fSmrg            return iterable[hi]
123d63b911fSmrg
124d63b911fSmrg        version = bisect(self.versions, found_in_version)
125d63b911fSmrg        logger.debug(f"Bisected {string} to {version}")
126d63b911fSmrg        # 2.6.11 doesn't count, that's the start of git
127d63b911fSmrg        return version if version != self.versions[0] else None
128d63b911fSmrg
129d63b911fSmrg
130d63b911fSmrgdef generate_keysym_line(code, kernel, kver_list=[]):
131d63b911fSmrg    """
132d63b911fSmrg    Generate the line to append to the keysym file.
133d63b911fSmrg
134d63b911fSmrg    This format is semi-ABI, scripts rely on the format of this line (e.g. in
135d63b911fSmrg    xkeyboard-config).
136d63b911fSmrg    """
137d63b911fSmrg    evcode = libevdev.evbit(libevdev.EV_KEY.value, code)
138d63b911fSmrg    if not evcode.is_defined:  # codes without a #define in the kernel
139d63b911fSmrg        return None
140d63b911fSmrg    if evcode.name.startswith("BTN_"):
141d63b911fSmrg        return None
142d63b911fSmrg
143d63b911fSmrg    name = "".join([s.capitalize() for s in evcode.name[4:].lower().split("_")])
144d63b911fSmrg    keysym = f"XF86XK_{name}"
145d63b911fSmrg    tabs = 4 - len(keysym) // 8
146d63b911fSmrg    kver = kernel.introduced_in_version(evcode.name) or " "
147d63b911fSmrg    if kver_list:
148d63b911fSmrg        from fnmatch import fnmatch
149d63b911fSmrg
150d63b911fSmrg        allowed_kvers = [v.strip() for v in kver_list.split(",")]
151d63b911fSmrg        for allowed in allowed_kvers:
152d63b911fSmrg            if fnmatch(kver, allowed):
153d63b911fSmrg                break
154d63b911fSmrg        else:  # no match
155d63b911fSmrg            return None
156d63b911fSmrg
157d63b911fSmrg    return f"#define {keysym}{'	' * tabs}_EVDEVK(0x{code:03X})		/* {kver:5s} {evcode.name} */"
158d63b911fSmrg
159d63b911fSmrg
160d63b911fSmrgdef verify(ns):
161d63b911fSmrg    """
162d63b911fSmrg    Verify that the XF86keysym.h file follows the requirements. Since we expect
163d63b911fSmrg    the header file to be parsed by outside scripts, the requirements for the format
164d63b911fSmrg    are quite strict, including things like correct-case hex codes.
165d63b911fSmrg    """
166d63b911fSmrg
167d63b911fSmrg    # No other keysym must use this range
168d63b911fSmrg    reserved_range = re.compile(r"#define.*0x10081.*")
169d63b911fSmrg    normal_range = re.compile(r"#define.*0x1008.*")
170d63b911fSmrg
171d63b911fSmrg    # This is the full pattern we expect.
172d63b911fSmrg    expected_pattern = re.compile(
173d63b911fSmrg        r"#define XF86XK_\w+\t+_EVDEVK\(0x([0-9A-F]{3})\)\t+/\* (v[2-6]\.[0-9]+(\.[0-9]+)?)? +KEY_\w+ \*/"
174d63b911fSmrg    )
175d63b911fSmrg    # This is the comment pattern we expect
176d63b911fSmrg    expected_comment_pattern = re.compile(
177d63b911fSmrg        r"/\* Use: (?P<name>\w+)\t+_EVDEVK\(0x(?P<value>[0-9A-F]{3})\)\t+   (v[2-6]\.[0-9]+(\.[0-9]+)?)? +KEY_\w+ \*/"
178d63b911fSmrg    )
179d63b911fSmrg
180d63b911fSmrg    # Some patterns to spot specific errors, just so we can print useful errors
181d63b911fSmrg    define = re.compile(r"^#define .*")
182d63b911fSmrg    name_pattern = re.compile(r"#define (XF86XK_[^\s]*)")
183d63b911fSmrg    tab_check = re.compile(r"#define \w+(\s+)[^\s]+(\s+)")
184d63b911fSmrg    hex_pattern = re.compile(r".*0x([a-f0-9]+).*", re.I)
185d63b911fSmrg    comment_format = re.compile(r".*/\* ([^\s]+)?\s+(\w+)")
186d63b911fSmrg    kver_format = re.compile(r"v[2-6]\.[0-9]+(\.[0-9]+)?")
187d63b911fSmrg
188d63b911fSmrg    in_evdev_codes_section = False
189d63b911fSmrg    had_evdev_codes_section = False
190d63b911fSmrg    success = True
191d63b911fSmrg
192d63b911fSmrg    all_defines = []
193d63b911fSmrg
194d63b911fSmrg    all_keysym_names = all_keysyms(ns.header.parent)
195d63b911fSmrg
196d63b911fSmrg    class ParserError(Exception):
197d63b911fSmrg        pass
198d63b911fSmrg
199d63b911fSmrg    def error(msg, line):
200d63b911fSmrg        raise ParserError(f"{msg} in '{line.strip()}'")
201d63b911fSmrg
202d63b911fSmrg    last_keycode = 0
203d63b911fSmrg    for line in open(ns.header):
204d63b911fSmrg        try:
205d63b911fSmrg            if not in_evdev_codes_section:
206d63b911fSmrg                if re.match(start_token, line):
207d63b911fSmrg                    in_evdev_codes_section = True
208d63b911fSmrg                    had_evdev_codes_section = True
209d63b911fSmrg                    continue
210d63b911fSmrg
211d63b911fSmrg                if re.match(reserved_range, line):
212d63b911fSmrg                    error("Using reserved range", line)
213d63b911fSmrg                match = re.match(name_pattern, line)
214d63b911fSmrg                if match:
215d63b911fSmrg                    all_defines.append(match.group(1))
216d63b911fSmrg            else:
217d63b911fSmrg                # Within the evdev defines section
218d63b911fSmrg                if re.match(end_token, line):
219d63b911fSmrg                    in_evdev_codes_section = False
220d63b911fSmrg                    continue
221d63b911fSmrg
222d63b911fSmrg                # Comments we only search for a hex pattern and where there is one present
223d63b911fSmrg                # we only check for uppercase format, ordering and update our last_keycode.
224d63b911fSmrg                if not re.match(define, line):
225d63b911fSmrg                    match = re.match(expected_comment_pattern, line)
226d63b911fSmrg                    if match:
227d63b911fSmrg                        hexcode = match.group("value")
228d63b911fSmrg                        if hexcode != hexcode.upper():
229d63b911fSmrg                            error(f"Hex code 0x{hexcode} must be uppercase", line)
230d63b911fSmrg                        if hexcode:
231d63b911fSmrg                            keycode = int(hexcode, 16)
232d63b911fSmrg                            if keycode < last_keycode:
233d63b911fSmrg                                error("Keycode must be ascending", line)
234d63b911fSmrg                            if keycode == last_keycode:
235d63b911fSmrg                                error("Duplicate keycode", line)
236d63b911fSmrg                            last_keycode = keycode
237d63b911fSmrg
238d63b911fSmrg                        name = match.group("name")
239d63b911fSmrg                        if name not in all_keysym_names:
240d63b911fSmrg                            error(f"Unknown keysym {name}", line)
241d63b911fSmrg                    elif re.match(hex_pattern, line):
242d63b911fSmrg                        logger.warning(f"Unexpected hex code in {line}")
243d63b911fSmrg                    continue
244d63b911fSmrg
245d63b911fSmrg                # Anything below here is a #define line
246d63b911fSmrg                # Let's check for specific errors
247d63b911fSmrg                if re.match(normal_range, line):
248d63b911fSmrg                    error("Define must use _EVDEVK", line)
249d63b911fSmrg
250d63b911fSmrg                match = re.match(name_pattern, line)
251d63b911fSmrg                if match:
252d63b911fSmrg                    if match.group(1) in all_defines:
253d63b911fSmrg                        error("Duplicate define", line)
254d63b911fSmrg                    all_defines.append(match.group(1))
255d63b911fSmrg                else:
256d63b911fSmrg                    error("Typo", line)
257d63b911fSmrg
258d63b911fSmrg                match = re.match(hex_pattern, line)
259d63b911fSmrg                if not match:
260d63b911fSmrg                    error("No hex code", line)
261d63b911fSmrg                if match.group(1) != match.group(1).upper():
262d63b911fSmrg                    error(f"Hex code 0x{match.group(1)} must be uppercase", line)
263d63b911fSmrg
264d63b911fSmrg                tabs = re.match(tab_check, line)
265d63b911fSmrg                if not tabs:  # bug
266d63b911fSmrg                    error("Matching error", line)
267d63b911fSmrg                if " " in tabs.group(1) or " " in tabs.group(2):
268d63b911fSmrg                    error("Use tabs, not spaces", line)
269d63b911fSmrg
270d63b911fSmrg                comment = re.match(comment_format, line)
271d63b911fSmrg                if not comment:
272d63b911fSmrg                    error("Invalid comment format", line)
273d63b911fSmrg                kver = comment.group(1)
274d63b911fSmrg                if kver and not re.match(kver_format, kver):
275d63b911fSmrg                    error("Invalid kernel version format", line)
276d63b911fSmrg
277d63b911fSmrg                keyname = comment.group(2)
278d63b911fSmrg                if not keyname.startswith("KEY_") or keyname.upper() != keyname:
279d63b911fSmrg                    error("Kernel keycode name invalid", line)
280d63b911fSmrg
281d63b911fSmrg                # This could be an old libevdev
282d63b911fSmrg                if keyname not in [c.name for c in libevdev.EV_KEY.codes]:
283d63b911fSmrg                    logger.warning(f"Unknown kernel keycode name {keyname}")
284d63b911fSmrg
285d63b911fSmrg                # Check the full expected format, no better error messages
286d63b911fSmrg                # available if this fails
287d63b911fSmrg                match = re.match(expected_pattern, line)
288d63b911fSmrg                if not match:
289d63b911fSmrg                    error("Failed match", line)
290d63b911fSmrg
291d63b911fSmrg                keycode = int(match.group(1), 16)
292d63b911fSmrg                if keycode < last_keycode:
293d63b911fSmrg                    error("Keycode must be ascending", line)
294d63b911fSmrg                if keycode == last_keycode:
295d63b911fSmrg                    error("Duplicate keycode", line)
296d63b911fSmrg
297d63b911fSmrg                # May cause a false positive for old libevdev if KEY_MAX is bumped
298d63b911fSmrg                if keycode < 0x0A0 or keycode > libevdev.EV_KEY.KEY_MAX.value:
299d63b911fSmrg                    error("Keycode outside range", line)
300d63b911fSmrg
301d63b911fSmrg                last_keycode = keycode
302d63b911fSmrg        except ParserError as e:
303d63b911fSmrg            logger.error(e)
304d63b911fSmrg            success = False
305d63b911fSmrg
306d63b911fSmrg    if not had_evdev_codes_section:
307d63b911fSmrg        logger.error("Unable to locate EVDEVK section")
308d63b911fSmrg        success = False
309d63b911fSmrg    elif in_evdev_codes_section:
310d63b911fSmrg        logger.error("Unterminated EVDEVK section")
311d63b911fSmrg        success = False
312d63b911fSmrg
313d63b911fSmrg    if success:
314d63b911fSmrg        logger.info("Verification succeeded")
315d63b911fSmrg
316d63b911fSmrg    return 0 if success else 1
317d63b911fSmrg
318d63b911fSmrg
319d63b911fSmrgdef add_keysyms(ns):
320d63b911fSmrg    """
321d63b911fSmrg    Print a new XF86keysym.h file, adding any *missing* keycodes to the existing file.
322d63b911fSmrg    """
323d63b911fSmrg    if verify(ns) != 0:
324d63b911fSmrg        die("Header file verification failed")
325d63b911fSmrg
326d63b911fSmrg    # If verification succeeds, we can be a bit more lenient here because we already know
327d63b911fSmrg    # what the format of the field is. Specifically, we're searching for
328d63b911fSmrg    # 3-digit hexcode in brackets and use that as keycode.
329d63b911fSmrg    pattern = re.compile(r".*_EVDEVK\((0x[a-fA-F0-9]{3})\).*")
330d63b911fSmrg    max_code = max(
331d63b911fSmrg        [
332d63b911fSmrg            c.value
333d63b911fSmrg            for c in libevdev.EV_KEY.codes
334d63b911fSmrg            if c.is_defined
335d63b911fSmrg            and c != libevdev.EV_KEY.KEY_MAX
336d63b911fSmrg            and not c.name.startswith("BTN")
337d63b911fSmrg        ]
338d63b911fSmrg    )
339d63b911fSmrg
340d63b911fSmrg    def defined_keycodes(path):
341d63b911fSmrg        """
342d63b911fSmrg        Returns an iterator to the next #defined (or otherwise mentioned)
343d63b911fSmrg        keycode, all other lines (including the returned one) are passed
344d63b911fSmrg        through to printf.
345d63b911fSmrg        """
346d63b911fSmrg        with open(path) as fd:
347d63b911fSmrg            in_evdev_codes_section = False
348d63b911fSmrg
349d63b911fSmrg            for line in fd:
350d63b911fSmrg                if not in_evdev_codes_section:
351d63b911fSmrg                    if re.match(start_token, line):
352d63b911fSmrg                        in_evdev_codes_section = True
353d63b911fSmrg                    # passthrough for all other lines
354d63b911fSmrg                    print(line, end="")
355d63b911fSmrg                else:
356d63b911fSmrg                    if re.match(r"#undef _EVDEVK\n", line):
357d63b911fSmrg                        in_evdev_codes_section = False
358d63b911fSmrg                        yield max_code
359d63b911fSmrg                    else:
360d63b911fSmrg                        match = re.match(pattern, line)
361d63b911fSmrg                        if match:
362d63b911fSmrg                            logger.debug(f"Found keycode in {line.strip()}")
363d63b911fSmrg                            yield int(match.group(1), 16)
364d63b911fSmrg                    print(line, end="")
365d63b911fSmrg
366d63b911fSmrg    kernel = Kernel(ns.kernel_git_tree)
367d63b911fSmrg    prev_code = 255 - 8  # the last keycode we can map directly in X
368d63b911fSmrg    for code in defined_keycodes(ns.header):
369d63b911fSmrg        for missing in range(prev_code + 1, code):
370d63b911fSmrg            newline = generate_keysym_line(
371d63b911fSmrg                missing, kernel, kver_list=ns.kernel_versions
372d63b911fSmrg            )
373d63b911fSmrg            if newline:
374d63b911fSmrg                print(newline)
375d63b911fSmrg        prev_code = code
376d63b911fSmrg
377d63b911fSmrg    return 0
378d63b911fSmrg
379d63b911fSmrg
380d63b911fSmrgdef find_xf86keysym_header():
381d63b911fSmrg    """
382d63b911fSmrg    Search for the XF86keysym.h file in the current tree or use the system one
383d63b911fSmrg    as last resort. This is a convenience function for running the script
384d63b911fSmrg    locally, it should not be relied on in the CI.
385d63b911fSmrg    """
386d63b911fSmrg    paths = tuple(Path.cwd().glob("**/XF86keysym.h"))
387d63b911fSmrg    if not paths:
388d63b911fSmrg        path = Path("/usr/include/X11/XF86keysym.h")
389d63b911fSmrg        if not path.exists():
390d63b911fSmrg            die("Unable to find XF86keysym.h in CWD or /usr")
391d63b911fSmrg    else:
392d63b911fSmrg        if len(paths) > 1:
393d63b911fSmrg            die("Multiple XF86keysym.h in CWD, please use --header")
394d63b911fSmrg        path = paths[0]
395d63b911fSmrg
396d63b911fSmrg    logger.info(f"Using header file {path}")
397d63b911fSmrg    return path
398d63b911fSmrg
399d63b911fSmrg
400d63b911fSmrgdef main():
401d63b911fSmrg    parser = argparse.ArgumentParser(description="Keysym parser script")
402d63b911fSmrg    parser.add_argument("--verbose", "-v", action="count", default=0)
403d63b911fSmrg    parser.add_argument(
404d63b911fSmrg        "--header",
405d63b911fSmrg        type=str,
406d63b911fSmrg        default=None,
407d63b911fSmrg        help="Path to the XF86Keysym.h header file (default: search $CWD)",
408d63b911fSmrg    )
409d63b911fSmrg
410d63b911fSmrg    subparsers = parser.add_subparsers(help="command-specific help", dest="command")
411d63b911fSmrg    parser_verify = subparsers.add_parser(
412d63b911fSmrg        "verify", help="Verify the XF86keysym.h matches requirements"
413d63b911fSmrg    )
414d63b911fSmrg    parser_verify.set_defaults(func=verify)
415d63b911fSmrg
416d63b911fSmrg    parser_generate = subparsers.add_parser(
417d63b911fSmrg        "add-keysyms", help="Add missing keysyms to the existing ones"
418d63b911fSmrg    )
419d63b911fSmrg    parser_generate.add_argument(
420d63b911fSmrg        "--kernel-git-tree",
421d63b911fSmrg        type=str,
422d63b911fSmrg        default=None,
423d63b911fSmrg        required=True,
424d63b911fSmrg        help="Path to a kernel git repo, required to find git tags",
425d63b911fSmrg    )
426d63b911fSmrg    parser_generate.add_argument(
427d63b911fSmrg        "--kernel-versions",
428d63b911fSmrg        type=str,
429d63b911fSmrg        default=[],
430d63b911fSmrg        required=False,
431d63b911fSmrg        help="Comma-separated list of kernel versions to limit ourselves to (e.g. 'v5.10,v5.9'). Supports fnmatch.",
432d63b911fSmrg    )
433d63b911fSmrg    parser_generate.set_defaults(func=add_keysyms)
434d63b911fSmrg    ns = parser.parse_args()
435d63b911fSmrg
436d63b911fSmrg    logger.setLevel(
437d63b911fSmrg        {2: logging.DEBUG, 1: logging.INFO, 0: logging.WARNING}.get(ns.verbose, 2)
438d63b911fSmrg    )
439d63b911fSmrg
440d63b911fSmrg    if not ns.header:
441d63b911fSmrg        ns.header = find_xf86keysym_header()
442d63b911fSmrg    else:
443d63b911fSmrg        ns.header = Path(ns.header)
444d63b911fSmrg
445d63b911fSmrg    if ns.command is None:
446d63b911fSmrg        parser.error("Invalid or missing command")
447d63b911fSmrg
448d63b911fSmrg    sys.exit(ns.func(ns))
449d63b911fSmrg
450d63b911fSmrg
451d63b911fSmrgif __name__ == "__main__":
452d63b911fSmrg    main()
453