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):
115 UNRECOGNIZED_KEYSYM_ERROR = "XKB-107"
118 r, m, l, v, o = self.rmlvo
120 'xkbcli-compile-keymap', # this is run in the builddir
127 args += ['--variant', v]
129 args += ['--options', o]
131 self.command = " ".join(args)
133 output = subprocess.check_output(args, stderr=subprocess.STDOUT,
134 universal_newlines=True)
135 if self.UNRECOGNIZED_KEYSYM_ERROR in output:
136 for line in output.split('\n'):
137 if self.UNRECOGNIZED_KEYSYM_ERROR in line:
139 self.exitstatus = 99 # tool doesn't generate this one
143 except subprocess.CalledProcessError as err:
144 self.error = "failed to compile keymap"
145 self.exitstatus = err.returncode
148 def xkbcommontool(rmlvo):
150 r = rmlvo.get('r', 'evdev')
151 m = rmlvo.get('m', 'pc105')
152 l = rmlvo.get('l', 'us')
153 v = rmlvo.get('v', None)
154 o = rmlvo.get('o', None)
155 tool = XkbcommonInvocation(r, m, l, v, o)
158 except KeyboardInterrupt:
164 r = rmlvo.get('r', 'evdev')
165 m = rmlvo.get('m', 'pc105')
166 l = rmlvo.get('l', 'us')
167 v = rmlvo.get('v', None)
168 o = rmlvo.get('o', None)
169 tool = XkbCompInvocation(r, m, l, v, o)
172 except KeyboardInterrupt:
177 root = ET.fromstring(open(path).read())
178 layouts = root.findall('layoutList/layout')
182 for e in root.findall('optionList/group/option/configItem/name')
187 layout = l.find('configItem/name').text
188 combos.append({'l': layout})
190 variants = l.findall('variantList/variant')
192 variant = v.find('configItem/name').text
194 combos.append({'l': layout, 'v': variant})
195 for option in options:
196 combos.append({'l': layout, 'v': variant, 'o': option})
201 def run(combos, tool, njobs, keymap_output_dir):
202 if keymap_output_dir:
203 keymap_output_dir = Path(keymap_output_dir)
205 keymap_output_dir.mkdir()
206 except FileExistsError as e:
207 print(e, file=sys.stderr)
211 keymap_file_fd = None
214 with multiprocessing.Pool(njobs) as p:
215 results = p.imap_unordered(tool, combos)
216 for invocation in progress_bar(results, total=len(combos), file=sys.stdout):
217 if invocation.exitstatus != 0:
221 target = sys.stdout if verbose else None
224 print(invocation, file=target)
226 if keymap_output_dir:
227 # we're running through the layouts in a somewhat sorted manner,
228 # so let's keep the fd open until we switch layouts
229 layout = invocation.layout
230 if invocation.variant:
231 layout += f"({invocation.variant})"
232 fname = keymap_output_dir / layout
233 if fname != keymap_file:
236 keymap_file_fd.close()
237 keymap_file_fd = open(keymap_file, 'a')
239 rmlvo = ', '.join([x or '' for x in invocation.rmlvo])
240 print(f"// {rmlvo}", file=keymap_file_fd)
241 print(invocation.keymap, file=keymap_file_fd)
242 keymap_file_fd.flush()
252 'libxkbcommon': xkbcommontool,
256 parser = argparse.ArgumentParser(
258 This tool compiles a keymap for each layout, variant and
259 options combination in the given rules XML file. The output
260 of this tool is YAML, use your favorite YAML parser to
261 extract error messages. Errors are printed to stderr.
264 parser.add_argument('path', metavar='/path/to/evdev.xml',
266 default=DEFAULT_RULES_XML,
267 help='Path to xkeyboard-config\'s evdev.xml')
268 parser.add_argument('--tool', choices=tools.keys(),
269 type=str, default='libxkbcommon',
270 help='parsing tool to use')
271 parser.add_argument('--jobs', '-j', type=int,
272 default=os.cpu_count() * 4,
273 help='number of processes to use')
274 parser.add_argument('--verbose', '-v', default=False, action="store_true")
275 parser.add_argument('--keymap-output-dir', default=None, type=str,
276 help='Directory to print compiled keymaps to')
277 parser.add_argument('--layout', default=None, type=str,
278 help='Only test the given layout')
279 parser.add_argument('--variant', default=None, type=str,
280 help='Only test the given variant')
281 parser.add_argument('--option', default=None, type=str,
282 help='Only test the given option')
284 args = parser.parse_args()
286 verbose = args.verbose
287 keymapdir = args.keymap_output_dir
288 progress_bar = create_progress_bar(verbose)
290 tool = tools[args.tool]
292 if any([args.layout, args.variant, args.option]):
299 combos = parse(args.path)
300 failed = run(combos, tool, args.jobs, keymapdir)
304 if __name__ == '__main__':
307 except KeyboardInterrupt:
308 print('# Exiting after Ctrl+C')