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()):
42 def __init__(self, r, m, l, v, o):
49 self.exitstatus = 77 # default to skipped
51 self.keymap = None # The fully compiled keymap
55 return self.rules, self.model, self.layout, self.variant, self.option
59 rmlvo = [x or "" for x in self.rmlvo]
60 rmlvo = ", ".join([f'"{x}"' for x in rmlvo])
61 s.append(f"- rmlvo: [{rmlvo}]")
62 s.append(f' cmd: "{escape(self.command)}"')
63 s.append(f" status: {self.exitstatus}")
65 s.append(f' error: "{escape(self.error.strip())}"')
69 raise NotImplementedError
72 class XkbCompInvocation(Invocation):
74 r, m, l, v, o = self.rmlvo
75 args = ["setxkbmap", "-print"]
78 args.append("{}".format(r))
81 args.append("{}".format(m))
83 args.append("-layout")
84 args.append("{}".format(l))
86 args.append("-variant")
87 args.append("{}".format(v))
89 args.append("-option")
90 args.append("{}".format(o))
92 xkbcomp_args = ["xkbcomp", "-xkb", "-", "-"]
94 self.command = " ".join(args + ["|"] + xkbcomp_args)
96 setxkbmap = subprocess.Popen(
98 stdout=subprocess.PIPE,
99 stderr=subprocess.PIPE,
100 universal_newlines=True,
102 stdout, stderr = setxkbmap.communicate()
103 if "Cannot open display" in stderr:
107 xkbcomp = subprocess.Popen(
109 stdin=subprocess.PIPE,
110 stdout=subprocess.PIPE,
111 stderr=subprocess.PIPE,
112 universal_newlines=True,
114 stdout, stderr = xkbcomp.communicate(stdout)
115 if xkbcomp.returncode != 0:
116 self.error = "failed to compile keymap"
117 self.exitstatus = xkbcomp.returncode
123 class XkbcommonInvocation(Invocation):
124 UNRECOGNIZED_KEYSYM_ERROR = "XKB-107"
127 r, m, l, v, o = self.rmlvo
129 "xkbcli-compile-keymap", # this is run in the builddir
139 args += ["--variant", v]
141 args += ["--options", o]
143 self.command = " ".join(args)
145 output = subprocess.check_output(
146 args, stderr=subprocess.STDOUT, universal_newlines=True
148 if self.UNRECOGNIZED_KEYSYM_ERROR in output:
149 for line in output.split("\n"):
150 if self.UNRECOGNIZED_KEYSYM_ERROR in line:
152 self.exitstatus = 99 # tool doesn't generate this one
156 except subprocess.CalledProcessError as err:
157 self.error = "failed to compile keymap"
158 self.exitstatus = err.returncode
161 def xkbcommontool(rmlvo):
163 r = rmlvo.get("r", "evdev")
164 m = rmlvo.get("m", "pc105")
165 l = rmlvo.get("l", "us")
166 v = rmlvo.get("v", None)
167 o = rmlvo.get("o", None)
168 tool = XkbcommonInvocation(r, m, l, v, o)
171 except KeyboardInterrupt:
177 r = rmlvo.get("r", "evdev")
178 m = rmlvo.get("m", "pc105")
179 l = rmlvo.get("l", "us")
180 v = rmlvo.get("v", None)
181 o = rmlvo.get("o", None)
182 tool = XkbCompInvocation(r, m, l, v, o)
185 except KeyboardInterrupt:
190 root = ET.fromstring(open(path).read())
191 layouts = root.findall("layoutList/layout")
193 options = [e.text for e in root.findall("optionList/group/option/configItem/name")]
197 layout = l.find("configItem/name").text
198 combos.append({"l": layout})
200 variants = l.findall("variantList/variant")
202 variant = v.find("configItem/name").text
204 combos.append({"l": layout, "v": variant})
205 for option in options:
206 combos.append({"l": layout, "v": variant, "o": option})
211 def run(combos, tool, njobs, keymap_output_dir):
212 if keymap_output_dir:
213 keymap_output_dir = Path(keymap_output_dir)
215 keymap_output_dir.mkdir()
216 except FileExistsError as e:
217 print(e, file=sys.stderr)
221 keymap_file_fd = None
224 with multiprocessing.Pool(njobs) as p:
225 results = p.imap_unordered(tool, combos)
226 for invocation in progress_bar(results, total=len(combos), file=sys.stdout):
227 if invocation.exitstatus != 0:
231 target = sys.stdout if verbose else None
234 print(invocation, file=target)
236 if keymap_output_dir:
237 # we're running through the layouts in a somewhat sorted manner,
238 # so let's keep the fd open until we switch layouts
239 layout = invocation.layout
240 if invocation.variant:
241 layout += f"({invocation.variant})"
242 fname = keymap_output_dir / layout
243 if fname != keymap_file:
246 keymap_file_fd.close()
247 keymap_file_fd = open(keymap_file, "a")
249 rmlvo = ", ".join([x or "" for x in invocation.rmlvo])
250 print(f"// {rmlvo}", file=keymap_file_fd)
251 print(invocation.keymap, file=keymap_file_fd)
252 keymap_file_fd.flush()
262 "libxkbcommon": xkbcommontool,
266 parser = argparse.ArgumentParser(
268 This tool compiles a keymap for each layout, variant and
269 options combination in the given rules XML file. The output
270 of this tool is YAML, use your favorite YAML parser to
271 extract error messages. Errors are printed to stderr.
276 metavar="/path/to/evdev.xml",
279 default=DEFAULT_RULES_XML,
280 help="Path to xkeyboard-config's evdev.xml",
284 choices=tools.keys(),
286 default="libxkbcommon",
287 help="parsing tool to use",
293 default=os.cpu_count() * 4,
294 help="number of processes to use",
296 parser.add_argument("--verbose", "-v", default=False, action="store_true")
298 "--keymap-output-dir",
301 help="Directory to print compiled keymaps to",
304 "--layout", default=None, type=str, help="Only test the given layout"
307 "--variant", default=None, type=str, help="Only test the given variant"
310 "--option", default=None, type=str, help="Only test the given option"
313 args = parser.parse_args()
315 verbose = args.verbose
316 keymapdir = args.keymap_output_dir
317 progress_bar = create_progress_bar(verbose)
319 tool = tools[args.tool]
321 if any([args.layout, args.variant, args.option]):
330 combos = parse(args.path)
331 failed = run(combos, tool, args.jobs, keymapdir)
335 if __name__ == "__main__":
338 except KeyboardInterrupt:
339 print("# Exiting after Ctrl+C")