7 import xml.etree.ElementTree as ET
8 from pathlib import Path
13 DEFAULT_RULES_XML = '@XKB_CONFIG_ROOT@/rules/evdev.xml'
15 # Meson needs to fill this in so we can call the tool in the buildir.
16 EXTRA_PATH = '@MESON_BUILD_ROOT@'
17 os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')])
21 return s.replace('"', '\\"')
24 # The function generating the progress bar (if any).
25 def create_progress_bar(verbose):
26 def noop_progress_bar(x, total, file=None):
29 progress_bar = noop_progress_bar
30 if not verbose and os.isatty(sys.stdout.fileno()):
41 def __init__(self, r, m, l, v, o):
48 self.exitstatus = 77 # default to skipped
50 self.keymap = None # The fully compiled keymap
54 return self.rules, self.model, self.layout, self.variant, self.option
58 rmlvo = [x or "" for x in self.rmlvo]
59 rmlvo = ', '.join([f'"{x}"' for x in rmlvo])
60 s.append(f'- rmlvo: [{rmlvo}]')
61 s.append(f' cmd: "{escape(self.command)}"')
62 s.append(f' status: {self.exitstatus}')
64 s.append(f' error: "{escape(self.error.strip())}"')
68 raise NotImplementedError
71 class XkbCompInvocation(Invocation):
73 r, m, l, v, o = self.rmlvo
74 args = ['setxkbmap', '-print']
77 args.append('{}'.format(r))
80 args.append('{}'.format(m))
82 args.append('-layout')
83 args.append('{}'.format(l))
85 args.append('-variant')
86 args.append('{}'.format(v))
88 args.append('-option')
89 args.append('{}'.format(o))
91 xkbcomp_args = ['xkbcomp', '-xkb', '-', '-']
93 self.command = " ".join(args + ["|"] + xkbcomp_args)
95 setxkbmap = subprocess.Popen(args, stdout=subprocess.PIPE,
96 stderr=subprocess.PIPE, universal_newlines=True)
97 stdout, stderr = setxkbmap.communicate()
98 if "Cannot open display" in stderr:
102 xkbcomp = subprocess.Popen(xkbcomp_args, stdin=subprocess.PIPE,
103 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
104 universal_newlines=True)
105 stdout, stderr = xkbcomp.communicate(stdout)
106 if xkbcomp.returncode != 0:
107 self.error = "failed to compile keymap"
108 self.exitstatus = xkbcomp.returncode
114 class XkbcommonInvocation(Invocation):
116 r, m, l, v, o = self.rmlvo
118 'xkbcli-compile-keymap', # this is run in the builddir
125 args += ['--variant', v]
127 args += ['--options', o]
129 self.command = " ".join(args)
131 output = subprocess.check_output(args, stderr=subprocess.STDOUT,
132 universal_newlines=True)
133 if "unrecognized keysym" in output:
134 for line in output.split('\n'):
135 if "unrecognized keysym" in line:
137 self.exitstatus = 99 # tool doesn't generate this one
141 except subprocess.CalledProcessError as err:
142 self.error = "failed to compile keymap"
143 self.exitstatus = err.returncode
146 def xkbcommontool(rmlvo):
148 r = rmlvo.get('r', 'evdev')
149 m = rmlvo.get('m', 'pc105')
150 l = rmlvo.get('l', 'us')
151 v = rmlvo.get('v', None)
152 o = rmlvo.get('o', None)
153 tool = XkbcommonInvocation(r, m, l, v, o)
156 except KeyboardInterrupt:
162 r = rmlvo.get('r', 'evdev')
163 m = rmlvo.get('m', 'pc105')
164 l = rmlvo.get('l', 'us')
165 v = rmlvo.get('v', None)
166 o = rmlvo.get('o', None)
167 tool = XkbCompInvocation(r, m, l, v, o)
170 except KeyboardInterrupt:
175 root = ET.fromstring(open(path).read())
176 layouts = root.findall('layoutList/layout')
180 for e in root.findall('optionList/group/option/configItem/name')
185 layout = l.find('configItem/name').text
186 combos.append({'l': layout})
188 variants = l.findall('variantList/variant')
190 variant = v.find('configItem/name').text
192 combos.append({'l': layout, 'v': variant})
193 for option in options:
194 combos.append({'l': layout, 'v': variant, 'o': option})
199 def run(combos, tool, njobs, keymap_output_dir):
200 if keymap_output_dir:
201 keymap_output_dir = Path(keymap_output_dir)
203 keymap_output_dir.mkdir()
204 except FileExistsError as e:
205 print(e, file=sys.stderr)
209 keymap_file_fd = None
212 with multiprocessing.Pool(njobs) as p:
213 results = p.imap_unordered(tool, combos)
214 for invocation in progress_bar(results, total=len(combos), file=sys.stdout):
215 if invocation.exitstatus != 0:
219 target = sys.stdout if verbose else None
222 print(invocation, file=target)
224 if keymap_output_dir:
225 # we're running through the layouts in a somewhat sorted manner,
226 # so let's keep the fd open until we switch layouts
227 layout = invocation.layout
228 if invocation.variant:
229 layout += f"({invocation.variant})"
230 fname = keymap_output_dir / layout
231 if fname != keymap_file:
234 keymap_file_fd.close()
235 keymap_file_fd = open(keymap_file, 'a')
237 rmlvo = ', '.join([x or '' for x in invocation.rmlvo])
238 print(f"// {rmlvo}", file=keymap_file_fd)
239 print(invocation.keymap, file=keymap_file_fd)
240 keymap_file_fd.flush()
250 'libxkbcommon': xkbcommontool,
254 parser = argparse.ArgumentParser(
255 description='Tool to test all layout/variant/option combinations.'
257 parser.add_argument('path', metavar='/path/to/evdev.xml',
259 default=DEFAULT_RULES_XML,
260 help='Path to xkeyboard-config\'s evdev.xml')
261 parser.add_argument('--tool', choices=tools.keys(),
262 type=str, default='libxkbcommon',
263 help='parsing tool to use')
264 parser.add_argument('--jobs', '-j', type=int,
265 default=os.cpu_count() * 4,
266 help='number of processes to use')
267 parser.add_argument('--verbose', '-v', default=False, action="store_true")
268 parser.add_argument('--keymap-output-dir', default=None, type=str,
269 help='Directory to print compiled keymaps to')
270 parser.add_argument('--layout', default=None, type=str,
271 help='Only test the given layout')
272 parser.add_argument('--variant', default=None, type=str,
273 help='Only test the given variant')
274 parser.add_argument('--option', default=None, type=str,
275 help='Only test the given option')
277 args = parser.parse_args()
279 verbose = args.verbose
280 keymapdir = args.keymap_output_dir
281 progress_bar = create_progress_bar(verbose)
283 tool = tools[args.tool]
285 if any([args.layout, args.variant, args.option]):
292 combos = parse(args.path)
293 failed = run(combos, tool, args.jobs, keymapdir)
297 if __name__ == '__main__':
300 except KeyboardInterrupt:
301 print('# Exiting after Ctrl+C')