• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2import argparse
3import sys
4import subprocess
5import os
6import io
7import xml.etree.ElementTree as ET
8from multiprocessing import Pool
9
10
11verbose = True
12
13DEFAULT_RULES_XML = '@XKB_CONFIG_ROOT@/rules/evdev.xml'
14
15# Meson needs to fill this in so we can call the tool in the buildir.
16EXTRA_PATH = '@MESON_BUILD_ROOT@'
17os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')])
18
19
20def noop_progress_bar(x, total):
21    return x
22
23
24# The function generating the progress bar (if any).
25progress_bar = noop_progress_bar
26if os.isatty(sys.stdout.fileno()):
27    try:
28        from tqdm import tqdm
29        progress_bar = tqdm
30
31        verbose = False
32    except ImportError:
33        pass
34
35
36def xkbcommontool(rmlvo):
37    try:
38        r = rmlvo.get('r', 'evdev')
39        m = rmlvo.get('m', 'pc105')
40        l = rmlvo.get('l', 'us')
41        v = rmlvo.get('v', None)
42        o = rmlvo.get('o', None)
43        args = [
44            'xkbcli-compile-keymap',  # this is run in the builddir
45            '--verbose',
46            '--rules', r,
47            '--model', m,
48            '--layout', l,
49        ]
50        if v is not None:
51            args += ['--variant', v]
52        if o is not None:
53            args += ['--options', o]
54
55        success = True
56        out = io.StringIO()
57        if verbose:
58            print(':: {}'.format(' '.join(args)), file=out)
59
60        try:
61            output = subprocess.check_output(args, stderr=subprocess.STDOUT,
62                                             universal_newlines=True)
63            if verbose:
64                print(output, file=out)
65
66            if "unrecognized keysym" in output:
67                for line in output.split('\n'):
68                    if "unrecognized keysym" in line:
69                        print('ERROR: {}'.format(line))
70                success = False
71        except subprocess.CalledProcessError as err:
72            print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out)
73            print(err.output, file=out)
74            success = False
75
76        return success, out.getvalue()
77    except KeyboardInterrupt:
78        pass
79
80
81def xkbcomp(rmlvo):
82    try:
83        r = rmlvo.get('r', 'evdev')
84        m = rmlvo.get('m', 'pc105')
85        l = rmlvo.get('l', 'us')
86        v = rmlvo.get('v', None)
87        o = rmlvo.get('o', None)
88        args = ['setxkbmap', '-print']
89        if r is not None:
90            args.append('-rules')
91            args.append('{}'.format(r))
92        if m is not None:
93            args.append('-model')
94            args.append('{}'.format(m))
95        if l is not None:
96            args.append('-layout')
97            args.append('{}'.format(l))
98        if v is not None:
99            args.append('-variant')
100            args.append('{}'.format(v))
101        if o is not None:
102            args.append('-option')
103            args.append('{}'.format(o))
104
105        success = True
106        out = io.StringIO()
107        if verbose:
108            print(':: {}'.format(' '.join(args)), file=out)
109
110        try:
111            xkbcomp_args = ['xkbcomp', '-xkb', '-', '-']
112
113            setxkbmap = subprocess.Popen(args, stdout=subprocess.PIPE)
114            xkbcomp = subprocess.Popen(xkbcomp_args, stdin=setxkbmap.stdout,
115                                       stdout=subprocess.PIPE, stderr=subprocess.PIPE,
116                                       universal_newlines=True)
117            setxkbmap.stdout.close()
118            stdout, stderr = xkbcomp.communicate()
119            if xkbcomp.returncode != 0:
120                print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out)
121                success = False
122            if xkbcomp.returncode != 0 or verbose:
123                print(stdout, file=out)
124                print(stderr, file=out)
125
126        # This catches setxkbmap errors.
127        except subprocess.CalledProcessError as err:
128            print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out)
129            print(err.output, file=out)
130            success = False
131
132        return success, out.getvalue()
133    except KeyboardInterrupt:
134        pass
135
136
137def parse(path):
138    root = ET.fromstring(open(path).read())
139    layouts = root.findall('layoutList/layout')
140
141    options = [
142        e.text
143        for e in root.findall('optionList/group/option/configItem/name')
144    ]
145
146    combos = []
147    for l in layouts:
148        layout = l.find('configItem/name').text
149        combos.append({'l': layout})
150
151        variants = l.findall('variantList/variant')
152        for v in variants:
153            variant = v.find('configItem/name').text
154
155            combos.append({'l': layout, 'v': variant})
156            for option in options:
157                combos.append({'l': layout, 'v': variant, 'o': option})
158
159    return combos
160
161
162def run(combos, tool, njobs):
163    failed = False
164    with Pool(njobs) as p:
165        results = p.imap_unordered(tool, combos)
166        for success, output in progress_bar(results, total=len(combos)):
167            if not success:
168                failed = True
169            if output:
170                print(output, file=sys.stdout if success else sys.stderr)
171    return failed
172
173
174def main(args):
175    tools = {
176        'libxkbcommon': xkbcommontool,
177        'xkbcomp': xkbcomp,
178    }
179
180    parser = argparse.ArgumentParser(
181        description='Tool to test all layout/variant/option combinations.'
182    )
183    parser.add_argument('path', metavar='/path/to/evdev.xml',
184                        nargs='?', type=str,
185                        default=DEFAULT_RULES_XML,
186                        help='Path to xkeyboard-config\'s evdev.xml')
187    parser.add_argument('--tool', choices=tools.keys(),
188                        type=str, default='libxkbcommon',
189                        help='parsing tool to use')
190    parser.add_argument('--jobs', '-j', type=int,
191                        default=os.cpu_count() * 4,
192                        help='number of processes to use')
193    args = parser.parse_args()
194
195    tool = tools[args.tool]
196
197    combos = parse(args.path)
198    failed = run(combos, tool, args.jobs)
199    sys.exit(failed)
200
201
202if __name__ == '__main__':
203    try:
204        main(sys.argv)
205    except KeyboardInterrupt:
206        print('Exiting after Ctrl+C')
207