1#!/usr/bin/env python3 2import argparse 3import multiprocessing 4import sys 5import subprocess 6import os 7import xml.etree.ElementTree as ET 8from pathlib import Path 9 10 11verbose = False 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 escape(s): 21 return s.replace('"', '\\"') 22 23 24# The function generating the progress bar (if any). 25def create_progress_bar(verbose): 26 def noop_progress_bar(x, total, file=None): 27 return x 28 29 progress_bar = noop_progress_bar 30 if not verbose and os.isatty(sys.stdout.fileno()): 31 try: 32 from tqdm import tqdm 33 progress_bar = tqdm 34 except ImportError: 35 pass 36 37 return progress_bar 38 39 40class Invocation: 41 def __init__(self, r, m, l, v, o): 42 self.command = "" 43 self.rules = r 44 self.model = m 45 self.layout = l 46 self.variant = v 47 self.option = o 48 self.exitstatus = 77 # default to skipped 49 self.error = None 50 self.keymap = None # The fully compiled keymap 51 52 @property 53 def rmlvo(self): 54 return self.rules, self.model, self.layout, self.variant, self.option 55 56 def __str__(self): 57 s = [] 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}') 63 if self.error: 64 s.append(f' error: "{escape(self.error.strip())}"') 65 return '\n'.join(s) 66 67 def run(self): 68 raise NotImplementedError 69 70 71class XkbCompInvocation(Invocation): 72 def run(self): 73 r, m, l, v, o = self.rmlvo 74 args = ['setxkbmap', '-print'] 75 if r is not None: 76 args.append('-rules') 77 args.append('{}'.format(r)) 78 if m is not None: 79 args.append('-model') 80 args.append('{}'.format(m)) 81 if l is not None: 82 args.append('-layout') 83 args.append('{}'.format(l)) 84 if v is not None: 85 args.append('-variant') 86 args.append('{}'.format(v)) 87 if o is not None: 88 args.append('-option') 89 args.append('{}'.format(o)) 90 91 xkbcomp_args = ['xkbcomp', '-xkb', '-', '-'] 92 93 self.command = " ".join(args + ["|"] + xkbcomp_args) 94 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: 99 self.error = stderr 100 self.exitstatus = 90 101 else: 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 109 else: 110 self.keymap = stdout 111 self.exitstatus = 0 112 113 114class XkbcommonInvocation(Invocation): 115 def run(self): 116 r, m, l, v, o = self.rmlvo 117 args = [ 118 'xkbcli-compile-keymap', # this is run in the builddir 119 '--verbose', 120 '--rules', r, 121 '--model', m, 122 '--layout', l, 123 ] 124 if v is not None: 125 args += ['--variant', v] 126 if o is not None: 127 args += ['--options', o] 128 129 self.command = " ".join(args) 130 try: 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: 136 self.error = line 137 self.exitstatus = 99 # tool doesn't generate this one 138 else: 139 self.exitstatus = 0 140 self.keymap = output 141 except subprocess.CalledProcessError as err: 142 self.error = "failed to compile keymap" 143 self.exitstatus = err.returncode 144 145 146def xkbcommontool(rmlvo): 147 try: 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) 154 tool.run() 155 return tool 156 except KeyboardInterrupt: 157 pass 158 159 160def xkbcomp(rmlvo): 161 try: 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) 168 tool.run() 169 return tool 170 except KeyboardInterrupt: 171 pass 172 173 174def parse(path): 175 root = ET.fromstring(open(path).read()) 176 layouts = root.findall('layoutList/layout') 177 178 options = [ 179 e.text 180 for e in root.findall('optionList/group/option/configItem/name') 181 ] 182 183 combos = [] 184 for l in layouts: 185 layout = l.find('configItem/name').text 186 combos.append({'l': layout}) 187 188 variants = l.findall('variantList/variant') 189 for v in variants: 190 variant = v.find('configItem/name').text 191 192 combos.append({'l': layout, 'v': variant}) 193 for option in options: 194 combos.append({'l': layout, 'v': variant, 'o': option}) 195 196 return combos 197 198 199def run(combos, tool, njobs, keymap_output_dir): 200 if keymap_output_dir: 201 keymap_output_dir = Path(keymap_output_dir) 202 try: 203 keymap_output_dir.mkdir() 204 except FileExistsError as e: 205 print(e, file=sys.stderr) 206 return False 207 208 keymap_file = None 209 keymap_file_fd = None 210 211 failed = False 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: 216 failed = True 217 target = sys.stderr 218 else: 219 target = sys.stdout if verbose else None 220 221 if target: 222 print(invocation, file=target) 223 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: 232 keymap_file = fname 233 if keymap_file_fd: 234 keymap_file_fd.close() 235 keymap_file_fd = open(keymap_file, 'a') 236 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() 241 242 return failed 243 244 245def main(args): 246 global progress_bar 247 global verbose 248 249 tools = { 250 'libxkbcommon': xkbcommontool, 251 'xkbcomp': xkbcomp, 252 } 253 254 parser = argparse.ArgumentParser( 255 description=''' 256 This tool compiles a keymap for each layout, variant and 257 options combination in the given rules XML file. The output 258 of this tool is YAML, use your favorite YAML parser to 259 extract error messages. Errors are printed to stderr. 260 ''' 261 ) 262 parser.add_argument('path', metavar='/path/to/evdev.xml', 263 nargs='?', type=str, 264 default=DEFAULT_RULES_XML, 265 help='Path to xkeyboard-config\'s evdev.xml') 266 parser.add_argument('--tool', choices=tools.keys(), 267 type=str, default='libxkbcommon', 268 help='parsing tool to use') 269 parser.add_argument('--jobs', '-j', type=int, 270 default=os.cpu_count() * 4, 271 help='number of processes to use') 272 parser.add_argument('--verbose', '-v', default=False, action="store_true") 273 parser.add_argument('--keymap-output-dir', default=None, type=str, 274 help='Directory to print compiled keymaps to') 275 parser.add_argument('--layout', default=None, type=str, 276 help='Only test the given layout') 277 parser.add_argument('--variant', default=None, type=str, 278 help='Only test the given variant') 279 parser.add_argument('--option', default=None, type=str, 280 help='Only test the given option') 281 282 args = parser.parse_args() 283 284 verbose = args.verbose 285 keymapdir = args.keymap_output_dir 286 progress_bar = create_progress_bar(verbose) 287 288 tool = tools[args.tool] 289 290 if any([args.layout, args.variant, args.option]): 291 combos = [{ 292 'l': args.layout, 293 'v': args.variant, 294 'o': args.option, 295 }] 296 else: 297 combos = parse(args.path) 298 failed = run(combos, tool, args.jobs, keymapdir) 299 sys.exit(failed) 300 301 302if __name__ == '__main__': 303 try: 304 main(sys.argv) 305 except KeyboardInterrupt: 306 print('# Exiting after Ctrl+C') 307