1*2b949d04SAndroid Build Coastguard Worker#!/usr/bin/env python3 2*2b949d04SAndroid Build Coastguard Workerimport argparse 3*2b949d04SAndroid Build Coastguard Workerimport multiprocessing 4*2b949d04SAndroid Build Coastguard Workerimport sys 5*2b949d04SAndroid Build Coastguard Workerimport subprocess 6*2b949d04SAndroid Build Coastguard Workerimport os 7*2b949d04SAndroid Build Coastguard Workerimport xml.etree.ElementTree as ET 8*2b949d04SAndroid Build Coastguard Workerfrom pathlib import Path 9*2b949d04SAndroid Build Coastguard Worker 10*2b949d04SAndroid Build Coastguard Worker 11*2b949d04SAndroid Build Coastguard Workerverbose = False 12*2b949d04SAndroid Build Coastguard Worker 13*2b949d04SAndroid Build Coastguard WorkerDEFAULT_RULES_XML = '@XKB_CONFIG_ROOT@/rules/evdev.xml' 14*2b949d04SAndroid Build Coastguard Worker 15*2b949d04SAndroid Build Coastguard Worker# Meson needs to fill this in so we can call the tool in the buildir. 16*2b949d04SAndroid Build Coastguard WorkerEXTRA_PATH = '@MESON_BUILD_ROOT@' 17*2b949d04SAndroid Build Coastguard Workeros.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')]) 18*2b949d04SAndroid Build Coastguard Worker 19*2b949d04SAndroid Build Coastguard Worker 20*2b949d04SAndroid Build Coastguard Workerdef escape(s): 21*2b949d04SAndroid Build Coastguard Worker return s.replace('"', '\\"') 22*2b949d04SAndroid Build Coastguard Worker 23*2b949d04SAndroid Build Coastguard Worker 24*2b949d04SAndroid Build Coastguard Worker# The function generating the progress bar (if any). 25*2b949d04SAndroid Build Coastguard Workerdef create_progress_bar(verbose): 26*2b949d04SAndroid Build Coastguard Worker def noop_progress_bar(x, total, file=None): 27*2b949d04SAndroid Build Coastguard Worker return x 28*2b949d04SAndroid Build Coastguard Worker 29*2b949d04SAndroid Build Coastguard Worker progress_bar = noop_progress_bar 30*2b949d04SAndroid Build Coastguard Worker if not verbose and os.isatty(sys.stdout.fileno()): 31*2b949d04SAndroid Build Coastguard Worker try: 32*2b949d04SAndroid Build Coastguard Worker from tqdm import tqdm 33*2b949d04SAndroid Build Coastguard Worker progress_bar = tqdm 34*2b949d04SAndroid Build Coastguard Worker except ImportError: 35*2b949d04SAndroid Build Coastguard Worker pass 36*2b949d04SAndroid Build Coastguard Worker 37*2b949d04SAndroid Build Coastguard Worker return progress_bar 38*2b949d04SAndroid Build Coastguard Worker 39*2b949d04SAndroid Build Coastguard Worker 40*2b949d04SAndroid Build Coastguard Workerclass Invocation: 41*2b949d04SAndroid Build Coastguard Worker def __init__(self, r, m, l, v, o): 42*2b949d04SAndroid Build Coastguard Worker self.command = "" 43*2b949d04SAndroid Build Coastguard Worker self.rules = r 44*2b949d04SAndroid Build Coastguard Worker self.model = m 45*2b949d04SAndroid Build Coastguard Worker self.layout = l 46*2b949d04SAndroid Build Coastguard Worker self.variant = v 47*2b949d04SAndroid Build Coastguard Worker self.option = o 48*2b949d04SAndroid Build Coastguard Worker self.exitstatus = 77 # default to skipped 49*2b949d04SAndroid Build Coastguard Worker self.error = None 50*2b949d04SAndroid Build Coastguard Worker self.keymap = None # The fully compiled keymap 51*2b949d04SAndroid Build Coastguard Worker 52*2b949d04SAndroid Build Coastguard Worker @property 53*2b949d04SAndroid Build Coastguard Worker def rmlvo(self): 54*2b949d04SAndroid Build Coastguard Worker return self.rules, self.model, self.layout, self.variant, self.option 55*2b949d04SAndroid Build Coastguard Worker 56*2b949d04SAndroid Build Coastguard Worker def __str__(self): 57*2b949d04SAndroid Build Coastguard Worker s = [] 58*2b949d04SAndroid Build Coastguard Worker rmlvo = [x or "" for x in self.rmlvo] 59*2b949d04SAndroid Build Coastguard Worker rmlvo = ', '.join([f'"{x}"' for x in rmlvo]) 60*2b949d04SAndroid Build Coastguard Worker s.append(f'- rmlvo: [{rmlvo}]') 61*2b949d04SAndroid Build Coastguard Worker s.append(f' cmd: "{escape(self.command)}"') 62*2b949d04SAndroid Build Coastguard Worker s.append(f' status: {self.exitstatus}') 63*2b949d04SAndroid Build Coastguard Worker if self.error: 64*2b949d04SAndroid Build Coastguard Worker s.append(f' error: "{escape(self.error.strip())}"') 65*2b949d04SAndroid Build Coastguard Worker return '\n'.join(s) 66*2b949d04SAndroid Build Coastguard Worker 67*2b949d04SAndroid Build Coastguard Worker def run(self): 68*2b949d04SAndroid Build Coastguard Worker raise NotImplementedError 69*2b949d04SAndroid Build Coastguard Worker 70*2b949d04SAndroid Build Coastguard Worker 71*2b949d04SAndroid Build Coastguard Workerclass XkbCompInvocation(Invocation): 72*2b949d04SAndroid Build Coastguard Worker def run(self): 73*2b949d04SAndroid Build Coastguard Worker r, m, l, v, o = self.rmlvo 74*2b949d04SAndroid Build Coastguard Worker args = ['setxkbmap', '-print'] 75*2b949d04SAndroid Build Coastguard Worker if r is not None: 76*2b949d04SAndroid Build Coastguard Worker args.append('-rules') 77*2b949d04SAndroid Build Coastguard Worker args.append('{}'.format(r)) 78*2b949d04SAndroid Build Coastguard Worker if m is not None: 79*2b949d04SAndroid Build Coastguard Worker args.append('-model') 80*2b949d04SAndroid Build Coastguard Worker args.append('{}'.format(m)) 81*2b949d04SAndroid Build Coastguard Worker if l is not None: 82*2b949d04SAndroid Build Coastguard Worker args.append('-layout') 83*2b949d04SAndroid Build Coastguard Worker args.append('{}'.format(l)) 84*2b949d04SAndroid Build Coastguard Worker if v is not None: 85*2b949d04SAndroid Build Coastguard Worker args.append('-variant') 86*2b949d04SAndroid Build Coastguard Worker args.append('{}'.format(v)) 87*2b949d04SAndroid Build Coastguard Worker if o is not None: 88*2b949d04SAndroid Build Coastguard Worker args.append('-option') 89*2b949d04SAndroid Build Coastguard Worker args.append('{}'.format(o)) 90*2b949d04SAndroid Build Coastguard Worker 91*2b949d04SAndroid Build Coastguard Worker xkbcomp_args = ['xkbcomp', '-xkb', '-', '-'] 92*2b949d04SAndroid Build Coastguard Worker 93*2b949d04SAndroid Build Coastguard Worker self.command = " ".join(args + ["|"] + xkbcomp_args) 94*2b949d04SAndroid Build Coastguard Worker 95*2b949d04SAndroid Build Coastguard Worker setxkbmap = subprocess.Popen(args, stdout=subprocess.PIPE, 96*2b949d04SAndroid Build Coastguard Worker stderr=subprocess.PIPE, universal_newlines=True) 97*2b949d04SAndroid Build Coastguard Worker stdout, stderr = setxkbmap.communicate() 98*2b949d04SAndroid Build Coastguard Worker if "Cannot open display" in stderr: 99*2b949d04SAndroid Build Coastguard Worker self.error = stderr 100*2b949d04SAndroid Build Coastguard Worker self.exitstatus = 90 101*2b949d04SAndroid Build Coastguard Worker else: 102*2b949d04SAndroid Build Coastguard Worker xkbcomp = subprocess.Popen(xkbcomp_args, stdin=subprocess.PIPE, 103*2b949d04SAndroid Build Coastguard Worker stdout=subprocess.PIPE, stderr=subprocess.PIPE, 104*2b949d04SAndroid Build Coastguard Worker universal_newlines=True) 105*2b949d04SAndroid Build Coastguard Worker stdout, stderr = xkbcomp.communicate(stdout) 106*2b949d04SAndroid Build Coastguard Worker if xkbcomp.returncode != 0: 107*2b949d04SAndroid Build Coastguard Worker self.error = "failed to compile keymap" 108*2b949d04SAndroid Build Coastguard Worker self.exitstatus = xkbcomp.returncode 109*2b949d04SAndroid Build Coastguard Worker else: 110*2b949d04SAndroid Build Coastguard Worker self.keymap = stdout 111*2b949d04SAndroid Build Coastguard Worker self.exitstatus = 0 112*2b949d04SAndroid Build Coastguard Worker 113*2b949d04SAndroid Build Coastguard Worker 114*2b949d04SAndroid Build Coastguard Workerclass XkbcommonInvocation(Invocation): 115*2b949d04SAndroid Build Coastguard Worker def run(self): 116*2b949d04SAndroid Build Coastguard Worker r, m, l, v, o = self.rmlvo 117*2b949d04SAndroid Build Coastguard Worker args = [ 118*2b949d04SAndroid Build Coastguard Worker 'xkbcli-compile-keymap', # this is run in the builddir 119*2b949d04SAndroid Build Coastguard Worker '--verbose', 120*2b949d04SAndroid Build Coastguard Worker '--rules', r, 121*2b949d04SAndroid Build Coastguard Worker '--model', m, 122*2b949d04SAndroid Build Coastguard Worker '--layout', l, 123*2b949d04SAndroid Build Coastguard Worker ] 124*2b949d04SAndroid Build Coastguard Worker if v is not None: 125*2b949d04SAndroid Build Coastguard Worker args += ['--variant', v] 126*2b949d04SAndroid Build Coastguard Worker if o is not None: 127*2b949d04SAndroid Build Coastguard Worker args += ['--options', o] 128*2b949d04SAndroid Build Coastguard Worker 129*2b949d04SAndroid Build Coastguard Worker self.command = " ".join(args) 130*2b949d04SAndroid Build Coastguard Worker try: 131*2b949d04SAndroid Build Coastguard Worker output = subprocess.check_output(args, stderr=subprocess.STDOUT, 132*2b949d04SAndroid Build Coastguard Worker universal_newlines=True) 133*2b949d04SAndroid Build Coastguard Worker if "unrecognized keysym" in output: 134*2b949d04SAndroid Build Coastguard Worker for line in output.split('\n'): 135*2b949d04SAndroid Build Coastguard Worker if "unrecognized keysym" in line: 136*2b949d04SAndroid Build Coastguard Worker self.error = line 137*2b949d04SAndroid Build Coastguard Worker self.exitstatus = 99 # tool doesn't generate this one 138*2b949d04SAndroid Build Coastguard Worker else: 139*2b949d04SAndroid Build Coastguard Worker self.exitstatus = 0 140*2b949d04SAndroid Build Coastguard Worker self.keymap = output 141*2b949d04SAndroid Build Coastguard Worker except subprocess.CalledProcessError as err: 142*2b949d04SAndroid Build Coastguard Worker self.error = "failed to compile keymap" 143*2b949d04SAndroid Build Coastguard Worker self.exitstatus = err.returncode 144*2b949d04SAndroid Build Coastguard Worker 145*2b949d04SAndroid Build Coastguard Worker 146*2b949d04SAndroid Build Coastguard Workerdef xkbcommontool(rmlvo): 147*2b949d04SAndroid Build Coastguard Worker try: 148*2b949d04SAndroid Build Coastguard Worker r = rmlvo.get('r', 'evdev') 149*2b949d04SAndroid Build Coastguard Worker m = rmlvo.get('m', 'pc105') 150*2b949d04SAndroid Build Coastguard Worker l = rmlvo.get('l', 'us') 151*2b949d04SAndroid Build Coastguard Worker v = rmlvo.get('v', None) 152*2b949d04SAndroid Build Coastguard Worker o = rmlvo.get('o', None) 153*2b949d04SAndroid Build Coastguard Worker tool = XkbcommonInvocation(r, m, l, v, o) 154*2b949d04SAndroid Build Coastguard Worker tool.run() 155*2b949d04SAndroid Build Coastguard Worker return tool 156*2b949d04SAndroid Build Coastguard Worker except KeyboardInterrupt: 157*2b949d04SAndroid Build Coastguard Worker pass 158*2b949d04SAndroid Build Coastguard Worker 159*2b949d04SAndroid Build Coastguard Worker 160*2b949d04SAndroid Build Coastguard Workerdef xkbcomp(rmlvo): 161*2b949d04SAndroid Build Coastguard Worker try: 162*2b949d04SAndroid Build Coastguard Worker r = rmlvo.get('r', 'evdev') 163*2b949d04SAndroid Build Coastguard Worker m = rmlvo.get('m', 'pc105') 164*2b949d04SAndroid Build Coastguard Worker l = rmlvo.get('l', 'us') 165*2b949d04SAndroid Build Coastguard Worker v = rmlvo.get('v', None) 166*2b949d04SAndroid Build Coastguard Worker o = rmlvo.get('o', None) 167*2b949d04SAndroid Build Coastguard Worker tool = XkbCompInvocation(r, m, l, v, o) 168*2b949d04SAndroid Build Coastguard Worker tool.run() 169*2b949d04SAndroid Build Coastguard Worker return tool 170*2b949d04SAndroid Build Coastguard Worker except KeyboardInterrupt: 171*2b949d04SAndroid Build Coastguard Worker pass 172*2b949d04SAndroid Build Coastguard Worker 173*2b949d04SAndroid Build Coastguard Worker 174*2b949d04SAndroid Build Coastguard Workerdef parse(path): 175*2b949d04SAndroid Build Coastguard Worker root = ET.fromstring(open(path).read()) 176*2b949d04SAndroid Build Coastguard Worker layouts = root.findall('layoutList/layout') 177*2b949d04SAndroid Build Coastguard Worker 178*2b949d04SAndroid Build Coastguard Worker options = [ 179*2b949d04SAndroid Build Coastguard Worker e.text 180*2b949d04SAndroid Build Coastguard Worker for e in root.findall('optionList/group/option/configItem/name') 181*2b949d04SAndroid Build Coastguard Worker ] 182*2b949d04SAndroid Build Coastguard Worker 183*2b949d04SAndroid Build Coastguard Worker combos = [] 184*2b949d04SAndroid Build Coastguard Worker for l in layouts: 185*2b949d04SAndroid Build Coastguard Worker layout = l.find('configItem/name').text 186*2b949d04SAndroid Build Coastguard Worker combos.append({'l': layout}) 187*2b949d04SAndroid Build Coastguard Worker 188*2b949d04SAndroid Build Coastguard Worker variants = l.findall('variantList/variant') 189*2b949d04SAndroid Build Coastguard Worker for v in variants: 190*2b949d04SAndroid Build Coastguard Worker variant = v.find('configItem/name').text 191*2b949d04SAndroid Build Coastguard Worker 192*2b949d04SAndroid Build Coastguard Worker combos.append({'l': layout, 'v': variant}) 193*2b949d04SAndroid Build Coastguard Worker for option in options: 194*2b949d04SAndroid Build Coastguard Worker combos.append({'l': layout, 'v': variant, 'o': option}) 195*2b949d04SAndroid Build Coastguard Worker 196*2b949d04SAndroid Build Coastguard Worker return combos 197*2b949d04SAndroid Build Coastguard Worker 198*2b949d04SAndroid Build Coastguard Worker 199*2b949d04SAndroid Build Coastguard Workerdef run(combos, tool, njobs, keymap_output_dir): 200*2b949d04SAndroid Build Coastguard Worker if keymap_output_dir: 201*2b949d04SAndroid Build Coastguard Worker keymap_output_dir = Path(keymap_output_dir) 202*2b949d04SAndroid Build Coastguard Worker try: 203*2b949d04SAndroid Build Coastguard Worker keymap_output_dir.mkdir() 204*2b949d04SAndroid Build Coastguard Worker except FileExistsError as e: 205*2b949d04SAndroid Build Coastguard Worker print(e, file=sys.stderr) 206*2b949d04SAndroid Build Coastguard Worker return False 207*2b949d04SAndroid Build Coastguard Worker 208*2b949d04SAndroid Build Coastguard Worker keymap_file = None 209*2b949d04SAndroid Build Coastguard Worker keymap_file_fd = None 210*2b949d04SAndroid Build Coastguard Worker 211*2b949d04SAndroid Build Coastguard Worker failed = False 212*2b949d04SAndroid Build Coastguard Worker with multiprocessing.Pool(njobs) as p: 213*2b949d04SAndroid Build Coastguard Worker results = p.imap_unordered(tool, combos) 214*2b949d04SAndroid Build Coastguard Worker for invocation in progress_bar(results, total=len(combos), file=sys.stdout): 215*2b949d04SAndroid Build Coastguard Worker if invocation.exitstatus != 0: 216*2b949d04SAndroid Build Coastguard Worker failed = True 217*2b949d04SAndroid Build Coastguard Worker target = sys.stderr 218*2b949d04SAndroid Build Coastguard Worker else: 219*2b949d04SAndroid Build Coastguard Worker target = sys.stdout if verbose else None 220*2b949d04SAndroid Build Coastguard Worker 221*2b949d04SAndroid Build Coastguard Worker if target: 222*2b949d04SAndroid Build Coastguard Worker print(invocation, file=target) 223*2b949d04SAndroid Build Coastguard Worker 224*2b949d04SAndroid Build Coastguard Worker if keymap_output_dir: 225*2b949d04SAndroid Build Coastguard Worker # we're running through the layouts in a somewhat sorted manner, 226*2b949d04SAndroid Build Coastguard Worker # so let's keep the fd open until we switch layouts 227*2b949d04SAndroid Build Coastguard Worker layout = invocation.layout 228*2b949d04SAndroid Build Coastguard Worker if invocation.variant: 229*2b949d04SAndroid Build Coastguard Worker layout += f"({invocation.variant})" 230*2b949d04SAndroid Build Coastguard Worker fname = keymap_output_dir / layout 231*2b949d04SAndroid Build Coastguard Worker if fname != keymap_file: 232*2b949d04SAndroid Build Coastguard Worker keymap_file = fname 233*2b949d04SAndroid Build Coastguard Worker if keymap_file_fd: 234*2b949d04SAndroid Build Coastguard Worker keymap_file_fd.close() 235*2b949d04SAndroid Build Coastguard Worker keymap_file_fd = open(keymap_file, 'a') 236*2b949d04SAndroid Build Coastguard Worker 237*2b949d04SAndroid Build Coastguard Worker rmlvo = ', '.join([x or '' for x in invocation.rmlvo]) 238*2b949d04SAndroid Build Coastguard Worker print(f"// {rmlvo}", file=keymap_file_fd) 239*2b949d04SAndroid Build Coastguard Worker print(invocation.keymap, file=keymap_file_fd) 240*2b949d04SAndroid Build Coastguard Worker keymap_file_fd.flush() 241*2b949d04SAndroid Build Coastguard Worker 242*2b949d04SAndroid Build Coastguard Worker return failed 243*2b949d04SAndroid Build Coastguard Worker 244*2b949d04SAndroid Build Coastguard Worker 245*2b949d04SAndroid Build Coastguard Workerdef main(args): 246*2b949d04SAndroid Build Coastguard Worker global progress_bar 247*2b949d04SAndroid Build Coastguard Worker global verbose 248*2b949d04SAndroid Build Coastguard Worker 249*2b949d04SAndroid Build Coastguard Worker tools = { 250*2b949d04SAndroid Build Coastguard Worker 'libxkbcommon': xkbcommontool, 251*2b949d04SAndroid Build Coastguard Worker 'xkbcomp': xkbcomp, 252*2b949d04SAndroid Build Coastguard Worker } 253*2b949d04SAndroid Build Coastguard Worker 254*2b949d04SAndroid Build Coastguard Worker parser = argparse.ArgumentParser( 255*2b949d04SAndroid Build Coastguard Worker description=''' 256*2b949d04SAndroid Build Coastguard Worker This tool compiles a keymap for each layout, variant and 257*2b949d04SAndroid Build Coastguard Worker options combination in the given rules XML file. The output 258*2b949d04SAndroid Build Coastguard Worker of this tool is YAML, use your favorite YAML parser to 259*2b949d04SAndroid Build Coastguard Worker extract error messages. Errors are printed to stderr. 260*2b949d04SAndroid Build Coastguard Worker ''' 261*2b949d04SAndroid Build Coastguard Worker ) 262*2b949d04SAndroid Build Coastguard Worker parser.add_argument('path', metavar='/path/to/evdev.xml', 263*2b949d04SAndroid Build Coastguard Worker nargs='?', type=str, 264*2b949d04SAndroid Build Coastguard Worker default=DEFAULT_RULES_XML, 265*2b949d04SAndroid Build Coastguard Worker help='Path to xkeyboard-config\'s evdev.xml') 266*2b949d04SAndroid Build Coastguard Worker parser.add_argument('--tool', choices=tools.keys(), 267*2b949d04SAndroid Build Coastguard Worker type=str, default='libxkbcommon', 268*2b949d04SAndroid Build Coastguard Worker help='parsing tool to use') 269*2b949d04SAndroid Build Coastguard Worker parser.add_argument('--jobs', '-j', type=int, 270*2b949d04SAndroid Build Coastguard Worker default=os.cpu_count() * 4, 271*2b949d04SAndroid Build Coastguard Worker help='number of processes to use') 272*2b949d04SAndroid Build Coastguard Worker parser.add_argument('--verbose', '-v', default=False, action="store_true") 273*2b949d04SAndroid Build Coastguard Worker parser.add_argument('--keymap-output-dir', default=None, type=str, 274*2b949d04SAndroid Build Coastguard Worker help='Directory to print compiled keymaps to') 275*2b949d04SAndroid Build Coastguard Worker parser.add_argument('--layout', default=None, type=str, 276*2b949d04SAndroid Build Coastguard Worker help='Only test the given layout') 277*2b949d04SAndroid Build Coastguard Worker parser.add_argument('--variant', default=None, type=str, 278*2b949d04SAndroid Build Coastguard Worker help='Only test the given variant') 279*2b949d04SAndroid Build Coastguard Worker parser.add_argument('--option', default=None, type=str, 280*2b949d04SAndroid Build Coastguard Worker help='Only test the given option') 281*2b949d04SAndroid Build Coastguard Worker 282*2b949d04SAndroid Build Coastguard Worker args = parser.parse_args() 283*2b949d04SAndroid Build Coastguard Worker 284*2b949d04SAndroid Build Coastguard Worker verbose = args.verbose 285*2b949d04SAndroid Build Coastguard Worker keymapdir = args.keymap_output_dir 286*2b949d04SAndroid Build Coastguard Worker progress_bar = create_progress_bar(verbose) 287*2b949d04SAndroid Build Coastguard Worker 288*2b949d04SAndroid Build Coastguard Worker tool = tools[args.tool] 289*2b949d04SAndroid Build Coastguard Worker 290*2b949d04SAndroid Build Coastguard Worker if any([args.layout, args.variant, args.option]): 291*2b949d04SAndroid Build Coastguard Worker combos = [{ 292*2b949d04SAndroid Build Coastguard Worker 'l': args.layout, 293*2b949d04SAndroid Build Coastguard Worker 'v': args.variant, 294*2b949d04SAndroid Build Coastguard Worker 'o': args.option, 295*2b949d04SAndroid Build Coastguard Worker }] 296*2b949d04SAndroid Build Coastguard Worker else: 297*2b949d04SAndroid Build Coastguard Worker combos = parse(args.path) 298*2b949d04SAndroid Build Coastguard Worker failed = run(combos, tool, args.jobs, keymapdir) 299*2b949d04SAndroid Build Coastguard Worker sys.exit(failed) 300*2b949d04SAndroid Build Coastguard Worker 301*2b949d04SAndroid Build Coastguard Worker 302*2b949d04SAndroid Build Coastguard Workerif __name__ == '__main__': 303*2b949d04SAndroid Build Coastguard Worker try: 304*2b949d04SAndroid Build Coastguard Worker main(sys.argv) 305*2b949d04SAndroid Build Coastguard Worker except KeyboardInterrupt: 306*2b949d04SAndroid Build Coastguard Worker print('# Exiting after Ctrl+C') 307