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