7 import xml.etree.ElementTree as ET
8 from pathlib import Path
13 DEFAULT_RULES_XML = '@XKB_CONFIG_ROOT@/rules/evdev.xml'
15 # Meson needs to fill this in so we can call the tool in the buildir.
16 EXTRA_PATH = '@MESON_BUILD_ROOT@'
17 os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')])
21 return s.replace('"', '\\"')
24 # The function generating the progress bar (if any).
25 def create_progress_bar(verbose):
26 def noop_progress_bar(x, total, file=None):
29 progress_bar = noop_progress_bar
30 if not verbose and os.isatty(sys.stdout.fileno()):
41 def __init__(self, r, m, l, v, o):
48 self.exitstatus = 77 # default to skipped
50 self.keymap = None # The fully compiled keymap
54 return self.rules, self.model, self.layout, self.variant, self.option
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}')
64 s.append(f' error: "{escape(self.error.strip())}"')
68 raise NotImplementedError
71 class XkbCompInvocation(Invocation):
73 r, m, l, v, o = self.rmlvo
74 args = ['setxkbmap', '-print']
77 args.append('{}'.format(r))
80 args.append('{}'.format(m))
82 args.append('-layout')
83 args.append('{}'.format(l))
85 args.append('-variant')
86 args.append('{}'.format(v))
88 args.append('-option')
89 args.append('{}'.format(o))
91 xkbcomp_args = ['xkbcomp', '-xkb', '-', '-']
93 self.command = " ".join(args + ["|"] + xkbcomp_args)
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:
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
114 class XkbcommonInvocation(Invocation):
116 r, m, l, v, o = self.rmlvo
118 'xkbcli-compile-keymap', # this is run in the builddir
125 args += ['--variant', v]
127 args += ['--options', o]
129 self.command = " ".join(args)
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:
137 self.exitstatus = 99 # tool doesn't generate this one
141 except subprocess.CalledProcessError as err:
142 self.error = "failed to compile keymap"
143 self.exitstatus = err.returncode
146 def xkbcommontool(rmlvo):
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)
156 except KeyboardInterrupt:
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)
170 except KeyboardInterrupt:
175 root = ET.fromstring(open(path).read())
176 layouts = root.findall('layoutList/layout')
180 for e in root.findall('optionList/group/option/configItem/name')
185 layout = l.find('configItem/name').text
186 combos.append({'l': layout})
188 variants = l.findall('variantList/variant')
190 variant = v.find('configItem/name').text
192 combos.append({'l': layout, 'v': variant})
193 for option in options:
194 combos.append({'l': layout, 'v': variant, 'o': option})
199 def run(combos, tool, njobs, keymap_output_dir):
200 if keymap_output_dir:
201 keymap_output_dir = Path(keymap_output_dir)
203 keymap_output_dir.mkdir()
204 except FileExistsError as e:
205 print(e, file=sys.stderr)
209 keymap_file_fd = None
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:
219 target = sys.stdout if verbose else None
222 print(invocation, file=target)
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:
234 keymap_file_fd.close()
235 keymap_file_fd = open(keymap_file, 'a')
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()
250 'libxkbcommon': xkbcommontool,
254 parser = argparse.ArgumentParser(
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.
262 parser.add_argument('path', metavar='/path/to/evdev.xml',
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')
282 args = parser.parse_args()
284 verbose = args.verbose
285 keymapdir = args.keymap_output_dir
286 progress_bar = create_progress_bar(verbose)
288 tool = tools[args.tool]
290 if any([args.layout, args.variant, args.option]):
297 combos = parse(args.path)
298 failed = run(combos, tool, args.jobs, keymapdir)
302 if __name__ == '__main__':
305 except KeyboardInterrupt:
306 print('# Exiting after Ctrl+C')