-#!/usr/bin/env python
+#!/usr/bin/env python3
+import argparse
+import multiprocessing
import sys
import subprocess
import os
import xml.etree.ElementTree as ET
+from pathlib import Path
-verbose = True
+verbose = False
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@'
+EXTRA_PATH = '@MESON_BUILD_ROOT@'
os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')])
+def escape(s):
+ return s.replace('"', '\\"')
+
+
# The function generating the progress bar (if any).
-progress_bar = lambda x, desc: x
-if os.isatty(sys.stdout.fileno()):
- try:
- from tqdm import tqdm
- progress_bar = tqdm
+def create_progress_bar(verbose):
+ def noop_progress_bar(x, total, file=None):
+ return x
- verbose = False
- except ImportError:
- pass
+ progress_bar = noop_progress_bar
+ if not verbose and os.isatty(sys.stdout.fileno()):
+ try:
+ from tqdm import tqdm
+ progress_bar = tqdm
+ except ImportError:
+ pass
+ return progress_bar
-def xkbcommontool(r='evdev', m='pc105', l='us', v=None, 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]
- if verbose:
- print(':: {}'.format(' '.join(args)))
+class Invocation:
+ def __init__(self, r, m, l, v, o):
+ self.command = ""
+ self.rules = r
+ self.model = m
+ self.layout = l
+ self.variant = v
+ self.option = o
+ self.exitstatus = 77 # default to skipped
+ self.error = None
+ self.keymap = None # The fully compiled keymap
- try:
- output = subprocess.check_output(args, stderr=subprocess.STDOUT)
- if verbose:
- print(output.decode('utf-8'))
- except subprocess.CalledProcessError as err:
- print('ERROR: Failed to compile: {}'.format(' '.join(args)))
- print(err.output.decode('utf-8'))
- sys.exit(1)
-
-
-def xkbcomp(r='evdev', m='pc105', l='us', v='', o=''):
- 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 o is not None:
- args.append('-option')
- args.append('{}'.format(o))
-
- if verbose:
- print(':: {}'.format(' '.join(args)))
+ @property
+ def rmlvo(self):
+ return self.rules, self.model, self.layout, self.variant, self.option
+
+ def __str__(self):
+ s = []
+ rmlvo = [x or "" for x in self.rmlvo]
+ rmlvo = ', '.join([f'"{x}"' for x in rmlvo])
+ s.append(f'- rmlvo: [{rmlvo}]')
+ s.append(f' cmd: "{escape(self.command)}"')
+ s.append(f' status: {self.exitstatus}')
+ if self.error:
+ s.append(f' error: "{escape(self.error.strip())}"')
+ return '\n'.join(s)
+
+ def run(self):
+ raise NotImplementedError
+
+
+class XkbCompInvocation(Invocation):
+ def run(self):
+ r, m, l, v, o = self.rmlvo
+ 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))
- 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)
- setxkbmap.stdout.close()
- stdout, stderr = xkbcomp.communicate()
- if xkbcomp.returncode != 0:
- print('ERROR: Failed to compile: {}'.format(' '.join(args)))
- if xkbcomp.returncode != 0 or verbose:
- print(stdout.decode('utf-8'))
- print(stderr.decode('utf-8'))
+ self.command = " ".join(args + ["|"] + xkbcomp_args)
+
+ setxkbmap = subprocess.Popen(args, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, universal_newlines=True)
+ stdout, stderr = setxkbmap.communicate()
+ if "Cannot open display" in stderr:
+ self.error = stderr
+ self.exitstatus = 90
+ else:
+ xkbcomp = subprocess.Popen(xkbcomp_args, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ universal_newlines=True)
+ stdout, stderr = xkbcomp.communicate(stdout)
+ if xkbcomp.returncode != 0:
+ self.error = "failed to compile keymap"
+ self.exitstatus = xkbcomp.returncode
+ else:
+ self.keymap = stdout
+ self.exitstatus = 0
+
+
+class XkbcommonInvocation(Invocation):
+ UNRECOGNIZED_KEYSYM_ERROR = "XKB-107"
+
+ def run(self):
+ r, m, l, v, o = self.rmlvo
+ args = [
+ 'xkbcli-compile-keymap', # this is run in the builddir
+ '--verbose',
+ '--rules', r,
+ '--model', m,
+ '--layout', l,
+ ]
+ if v is not None:
+ args += ['--variant', v]
+ if o is not None:
+ args += ['--options', o]
+
+ self.command = " ".join(args)
+ try:
+ output = subprocess.check_output(args, stderr=subprocess.STDOUT,
+ universal_newlines=True)
+ if self.UNRECOGNIZED_KEYSYM_ERROR in output:
+ for line in output.split('\n'):
+ if self.UNRECOGNIZED_KEYSYM_ERROR in line:
+ self.error = line
+ self.exitstatus = 99 # tool doesn't generate this one
+ else:
+ self.exitstatus = 0
+ self.keymap = output
+ except subprocess.CalledProcessError as err:
+ self.error = "failed to compile keymap"
+ self.exitstatus = err.returncode
+
+
+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)
+ tool = XkbcommonInvocation(r, m, l, v, o)
+ tool.run()
+ return tool
+ except KeyboardInterrupt:
+ pass
+
- # This catches setxkbmap errors.
- except subprocess.CalledProcessError as err:
- print('ERROR: Failed to compile: {}'.format(' '.join(args)))
- print(err.output.decode('utf-8'))
+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)
+ tool = XkbCompInvocation(r, m, l, v, o)
+ tool.run()
+ return tool
+ except KeyboardInterrupt:
+ pass
-def parse(root):
+def parse(path):
+ root = ET.fromstring(open(path).read())
layouts = root.findall('layoutList/layout')
options = [
for e in root.findall('optionList/group/option/configItem/name')
]
- # Switch this to xkbcomp if needed.
- tool = xkbcommontool
- # tool = xkbcomp
-
- for l in progress_bar(layouts, 'layout '):
+ combos = []
+ for l in layouts:
layout = l.find('configItem/name').text
- tool(l=layout)
+ combos.append({'l': layout})
variants = l.findall('variantList/variant')
- for v in progress_bar(variants, 'variant'):
+ for v in variants:
variant = v.find('configItem/name').text
- tool(l=layout, v=variant)
- for option in progress_bar(options, 'option '):
- tool(l=layout, v=variant, o=option)
+ 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, keymap_output_dir):
+ if keymap_output_dir:
+ keymap_output_dir = Path(keymap_output_dir)
+ try:
+ keymap_output_dir.mkdir()
+ except FileExistsError as e:
+ print(e, file=sys.stderr)
+ return False
+
+ keymap_file = None
+ keymap_file_fd = None
+
+ failed = False
+ with multiprocessing.Pool(njobs) as p:
+ results = p.imap_unordered(tool, combos)
+ for invocation in progress_bar(results, total=len(combos), file=sys.stdout):
+ if invocation.exitstatus != 0:
+ failed = True
+ target = sys.stderr
+ else:
+ target = sys.stdout if verbose else None
+
+ if target:
+ print(invocation, file=target)
+
+ if keymap_output_dir:
+ # we're running through the layouts in a somewhat sorted manner,
+ # so let's keep the fd open until we switch layouts
+ layout = invocation.layout
+ if invocation.variant:
+ layout += f"({invocation.variant})"
+ fname = keymap_output_dir / layout
+ if fname != keymap_file:
+ keymap_file = fname
+ if keymap_file_fd:
+ keymap_file_fd.close()
+ keymap_file_fd = open(keymap_file, 'a')
+
+ rmlvo = ', '.join([x or '' for x in invocation.rmlvo])
+ print(f"// {rmlvo}", file=keymap_file_fd)
+ print(invocation.keymap, file=keymap_file_fd)
+ keymap_file_fd.flush()
+
+ return failed
def main(args):
- try:
- path = args[1]
- except IndexError:
- path = DEFAULT_RULES_XML
+ global progress_bar
+ global verbose
+
+ tools = {
+ 'libxkbcommon': xkbcommontool,
+ 'xkbcomp': xkbcomp,
+ }
- with open(path) as f:
- root = ET.fromstring(f.read())
- parse(root)
+ parser = argparse.ArgumentParser(
+ description='''
+ This tool compiles a keymap for each layout, variant and
+ options combination in the given rules XML file. The output
+ of this tool is YAML, use your favorite YAML parser to
+ extract error messages. Errors are printed to stderr.
+ '''
+ )
+ 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')
+ parser.add_argument('--verbose', '-v', default=False, action="store_true")
+ parser.add_argument('--keymap-output-dir', default=None, type=str,
+ help='Directory to print compiled keymaps to')
+ parser.add_argument('--layout', default=None, type=str,
+ help='Only test the given layout')
+ parser.add_argument('--variant', default=None, type=str,
+ help='Only test the given variant')
+ parser.add_argument('--option', default=None, type=str,
+ help='Only test the given option')
+
+ args = parser.parse_args()
+
+ verbose = args.verbose
+ keymapdir = args.keymap_output_dir
+ progress_bar = create_progress_bar(verbose)
+
+ tool = tools[args.tool]
+
+ if any([args.layout, args.variant, args.option]):
+ combos = [{
+ 'l': args.layout,
+ 'v': args.variant,
+ 'o': args.option,
+ }]
+ else:
+ combos = parse(args.path)
+ failed = run(combos, tool, args.jobs, keymapdir)
+ sys.exit(failed)
if __name__ == '__main__':
- main(sys.argv)
+ try:
+ main(sys.argv)
+ except KeyboardInterrupt:
+ print('# Exiting after Ctrl+C')