#!/usr/bin/env python3 import argparse import sys import subprocess import os import io import xml.etree.ElementTree as ET from multiprocessing import Pool verbose = True DEFAULT_RULES_XML = '@XKB_CONFIG_ROOT@/rules/evdev.xml' # Meson needs to fill this in so we can call the tool in the buildir. EXTRA_PATH = '@MESON_BUILD_ROOT@' os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')]) def noop_progress_bar(x, total): return x # The function generating the progress bar (if any). progress_bar = noop_progress_bar if os.isatty(sys.stdout.fileno()): try: from tqdm import tqdm progress_bar = tqdm verbose = False except ImportError: pass def xkbcommontool(rmlvo): try: r = rmlvo.get('r', 'evdev') m = rmlvo.get('m', 'pc105') l = rmlvo.get('l', 'us') v = rmlvo.get('v', None) o = rmlvo.get('o', None) args = [ 'rmlvo-to-keymap', '--rules', r, '--model', m, '--layout', l, ] if v is not None: args += ['--variant', v] if o is not None: args += ['--options', o] success = True out = io.StringIO() if verbose: print(':: {}'.format(' '.join(args)), file=out) try: output = subprocess.check_output(args, stderr=subprocess.STDOUT, universal_newlines=True) if verbose: print(output, file=out) except subprocess.CalledProcessError as err: print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out) print(err.output, file=out) success = False return success, out.getvalue() except KeyboardInterrupt: pass def xkbcomp(rmlvo): try: r = rmlvo.get('r', 'evdev') m = rmlvo.get('m', 'pc105') l = rmlvo.get('l', 'us') v = rmlvo.get('v', None) o = rmlvo.get('o', None) args = ['setxkbmap', '-print'] if r is not None: args.append('-rules') args.append('{}'.format(r)) if m is not None: args.append('-model') args.append('{}'.format(m)) if l is not None: args.append('-layout') args.append('{}'.format(l)) if v is not None: args.append('-variant') args.append('{}'.format(v)) if o is not None: args.append('-option') args.append('{}'.format(o)) success = True out = io.StringIO() if verbose: print(':: {}'.format(' '.join(args)), file=out) try: xkbcomp_args = ['xkbcomp', '-xkb', '-', '-'] setxkbmap = subprocess.Popen(args, stdout=subprocess.PIPE) xkbcomp = subprocess.Popen(xkbcomp_args, stdin=setxkbmap.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) setxkbmap.stdout.close() stdout, stderr = xkbcomp.communicate() if xkbcomp.returncode != 0: print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out) success = False if xkbcomp.returncode != 0 or verbose: print(stdout, file=out) print(stderr, file=out) # This catches setxkbmap errors. except subprocess.CalledProcessError as err: print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out) print(err.output, file=out) success = False return success, out.getvalue() except KeyboardInterrupt: pass def parse(path): root = ET.fromstring(open(path).read()) layouts = root.findall('layoutList/layout') options = [ e.text for e in root.findall('optionList/group/option/configItem/name') ] combos = [] for l in layouts: layout = l.find('configItem/name').text combos.append({'l': layout}) variants = l.findall('variantList/variant') for v in variants: variant = v.find('configItem/name').text combos.append({'l': layout, 'v': variant}) for option in options: combos.append({'l': layout, 'v': variant, 'o': option}) return combos def run(combos, tool, njobs): failed = False with Pool(njobs) as p: results = p.imap_unordered(tool, combos) for success, output in progress_bar(results, total=len(combos)): if not success: failed = True if output: print(output, file=sys.stdout if success else sys.stderr) return failed def main(args): tools = { 'libxkbcommon': xkbcommontool, 'xkbcomp': xkbcomp, } parser = argparse.ArgumentParser( description='Tool to test all layout/variant/option combinations.' ) parser.add_argument('path', metavar='/path/to/evdev.xml', nargs='?', type=str, default=DEFAULT_RULES_XML, help='Path to xkeyboard-config\'s evdev.xml') parser.add_argument('--tool', choices=tools.keys(), type=str, default='libxkbcommon', help='parsing tool to use') parser.add_argument('--jobs', '-j', type=int, default=os.cpu_count() * 4, help='number of processes to use') args = parser.parse_args() tool = tools[args.tool] combos = parse(args.path) failed = run(combos, tool, args.jobs) sys.exit(failed) if __name__ == '__main__': try: main(sys.argv) except KeyboardInterrupt: print('Exiting after Ctrl+C')