1
0

xkeyboard-config-test.py.in 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. #!/usr/bin/env python3
  2. from __future__ import annotations
  3. import argparse
  4. import gzip
  5. import itertools
  6. import json
  7. import multiprocessing
  8. import os
  9. import shlex
  10. import subprocess
  11. import sys
  12. import xml.etree.ElementTree as ET
  13. from abc import ABCMeta, abstractmethod
  14. from dataclasses import dataclass
  15. from enum import StrEnum, auto, unique
  16. from functools import partial
  17. from pathlib import Path
  18. from typing import (
  19. TYPE_CHECKING,
  20. Any,
  21. BinaryIO,
  22. ClassVar,
  23. Iterable,
  24. Iterator,
  25. NoReturn,
  26. Protocol,
  27. Sequence,
  28. TextIO,
  29. TypeVar,
  30. cast,
  31. )
  32. try:
  33. import yaml
  34. except ImportError:
  35. yaml = None
  36. # TODO: import unconditionally Self from typing once we raise Python requirement to 3.11+
  37. if TYPE_CHECKING:
  38. from typing_extensions import Self
  39. WILDCARD = "*"
  40. DEFAULT_XKB_ROOT = Path("@XKB_CONFIG_ROOT@")
  41. # Meson needs to fill this in so we can call the tool in the buildir.
  42. EXTRA_PATH = "@MESON_BUILD_ROOT@"
  43. os.environ["PATH"] = ":".join(filter(bool, (EXTRA_PATH, os.getenv("PATH"))))
  44. # Environment variable to get the right level of log
  45. os.environ["XKB_LOG_LEVEL"] = "warning"
  46. os.environ["XKB_LOG_VERBOSITY"] = "10"
  47. @dataclass
  48. class RMLVO:
  49. DEFAULT_RULES: ClassVar[str] = "evdev"
  50. DEFAULT_MODEL: ClassVar[str] = "pc105"
  51. DEFAULT_LAYOUT: ClassVar[str] = "us"
  52. rules: str
  53. model: str
  54. layout: str
  55. variant: str | None
  56. option: str | None
  57. def __iter__(self) -> Iterator[str | None]:
  58. yield self.rules
  59. yield self.model
  60. yield self.layout
  61. yield self.variant
  62. yield self.option
  63. @property
  64. def rmlvo(self) -> Iterator[tuple[str, str]]:
  65. yield ("rules", self.rules)
  66. yield ("model", self.model)
  67. yield ("layout", self.layout)
  68. # Keep only defined and non-empty values
  69. if self.variant is not None:
  70. yield ("variant", self.variant)
  71. if self.option is not None:
  72. yield ("option", self.option)
  73. @classmethod
  74. def from_rmlvo(
  75. cls,
  76. rules: str | None = None,
  77. model: str | None = None,
  78. layout: str | None = None,
  79. variant: str | None = None,
  80. option: str | None = None,
  81. ) -> Self:
  82. return cls(
  83. # We need to force a value for RML components
  84. rules or cls.DEFAULT_RULES,
  85. model or cls.DEFAULT_MODEL,
  86. layout or cls.DEFAULT_LAYOUT,
  87. variant,
  88. option,
  89. )
  90. @dataclass
  91. class Invocation(RMLVO, metaclass=ABCMeta):
  92. exitstatus: int = 77 # default to “skipped”
  93. error: str | None = None
  94. keymap: bytes = b""
  95. command: str = "" # The fully compiled keymap
  96. def __str_iter(self) -> Iterator[str]:
  97. yield f"- rmlvo: {self.to_yaml(self.rmlvo)}"
  98. yield f" cmd: {json.dumps(self.command)}"
  99. yield f" status: {self.exitstatus}"
  100. if self.error:
  101. yield f" error: {json.dumps(self.error.strip())}"
  102. def __str__(self) -> str:
  103. return "\n".join(self.__str_iter())
  104. @property
  105. def short(self) -> Iterator[tuple[str, str | int]]:
  106. yield from self.rmlvo
  107. yield ("status", self.exitstatus)
  108. if self.error is not None:
  109. yield ("error", self.error)
  110. @staticmethod
  111. def to_yaml(xs: Iterable[tuple[str, str | int]]) -> str:
  112. fields = ", ".join(f"{k}: {json.dumps(v)}" for k, v in xs)
  113. return f"{{ {fields} }}"
  114. def _write(self, fd: BinaryIO) -> None:
  115. fd.write(f"// {self.to_yaml(self.rmlvo)}\n".encode("utf-8"))
  116. fd.write(self.keymap)
  117. def _write_keymap(self, output_dir: Path, compress: int) -> None:
  118. layout = self.layout
  119. if self.variant:
  120. layout += f"({self.variant})"
  121. if self.option:
  122. layout += f"+{self.option}"
  123. (output_dir / self.model).mkdir(exist_ok=True)
  124. keymap_file = output_dir / self.model / layout
  125. # Handle subdirs
  126. keymap_file.parent.mkdir(parents=True, exist_ok=True)
  127. if compress:
  128. keymap_file = keymap_file.with_suffix(".gz")
  129. with gzip.open(keymap_file, "wb", compresslevel=compress) as fd:
  130. self._write(fd)
  131. fd.close()
  132. else:
  133. with keymap_file.open("wb") as fd:
  134. self._write(fd)
  135. def _print_result(self, short: bool, verbose: bool) -> None:
  136. if self.exitstatus != 0:
  137. target = sys.stderr
  138. else:
  139. target = sys.stdout if verbose else None
  140. if target:
  141. if short:
  142. print("-", self.to_yaml(self.short), file=target)
  143. else:
  144. print(self, file=target)
  145. @classmethod
  146. @abstractmethod
  147. def run(
  148. cls,
  149. i: Self,
  150. xkb_root: Path,
  151. output_dir: Path | None,
  152. compress: int,
  153. *args,
  154. **kwargs,
  155. ) -> Self: ...
  156. @classmethod
  157. def run_all(
  158. cls,
  159. xkb_root: Path,
  160. combos: Iterable[Self],
  161. combos_count: int,
  162. njobs: int,
  163. keymap_output_dir: Path | None,
  164. verbose: bool,
  165. short: bool,
  166. progress_bar: ProgressBar[Iterable[Self]],
  167. chunksize: int,
  168. compress: int,
  169. **kwargs,
  170. ) -> bool:
  171. if keymap_output_dir:
  172. try:
  173. keymap_output_dir.mkdir(parents=True)
  174. except FileExistsError as e:
  175. print(e, file=sys.stderr)
  176. return False
  177. failed = False
  178. with multiprocessing.Pool(njobs) as p:
  179. f = partial(
  180. cls.run,
  181. xkb_root=xkb_root,
  182. output_dir=keymap_output_dir,
  183. compress=compress,
  184. )
  185. results = p.imap_unordered(f, combos, chunksize=chunksize)
  186. for invocation in progress_bar(
  187. results, total=combos_count, file=sys.stdout
  188. ):
  189. if invocation.exitstatus != 0:
  190. failed = True
  191. invocation._print_result(short, verbose)
  192. return failed
  193. @dataclass
  194. class XkbCompInvocation(Invocation):
  195. @classmethod
  196. def run(
  197. cls,
  198. i: Self,
  199. xkb_root: Path,
  200. output_dir: Path | None,
  201. compress: int,
  202. *args,
  203. **kwargs,
  204. ) -> Self:
  205. i._run(xkb_root, not output_dir)
  206. if output_dir:
  207. i._write_keymap(output_dir, compress)
  208. return i
  209. def _run(self, xkb_root: Path, test: bool) -> None:
  210. setxkbmap_args = (
  211. "setxkbmap",
  212. "-print",
  213. # Informative only; we set CWD to ensure proper rules are loaded
  214. "-I",
  215. str(xkb_root),
  216. *itertools.chain.from_iterable((f"-{k}", v) for k, v in self.rmlvo),
  217. )
  218. xkbcomp_args = ("xkbcomp", "-I", f"-I{xkb_root}", "-xkb", "-", "-")
  219. self.command = shlex.join(itertools.chain(setxkbmap_args, "|", xkbcomp_args))
  220. setxkbmap = subprocess.Popen(
  221. setxkbmap_args,
  222. stdout=subprocess.PIPE,
  223. stderr=subprocess.PIPE,
  224. universal_newlines=True,
  225. cwd=xkb_root,
  226. )
  227. stdout, stderr = setxkbmap.communicate()
  228. if "Cannot open display" in stderr:
  229. self.error = stderr
  230. self.exitstatus = 90
  231. else:
  232. xkbcomp = subprocess.Popen(
  233. xkbcomp_args,
  234. stdin=subprocess.PIPE,
  235. stdout=subprocess.PIPE,
  236. stderr=subprocess.PIPE,
  237. universal_newlines=True,
  238. )
  239. stdout, stderr = xkbcomp.communicate(stdout)
  240. if xkbcomp.returncode != 0:
  241. self.error = (
  242. "failed to compile keymap:\n"
  243. f"------\nstderr:\n{stderr}\n"
  244. f"------\nstdout:\n{stdout}\n"
  245. )
  246. self.exitstatus = xkbcomp.returncode
  247. else:
  248. self.keymap = stdout.encode("utf-8")
  249. self.exitstatus = 0
  250. @dataclass
  251. class XkbCompToXkbcommonInvocation(XkbCompInvocation):
  252. """
  253. Use setxkbcomp & xkbcomp, then pipe the result to xkbcli in order to enable
  254. comparison with direct xkbcli compilation with the same format.
  255. """
  256. def _run(self, xkb_root: Path, test: bool) -> None:
  257. super()._run(xkb_root, test)
  258. if self.exitstatus:
  259. return
  260. if test:
  261. return
  262. args = (
  263. "xkbcli-compile-keymap", # this is run in the builddir
  264. # Not used: keymap is already compiled
  265. # "--include", xkb_root,
  266. # Not needed, because we set XKB_LOG_LEVEL and XKB_LOG_VERBOSITY in env
  267. # "--verbose",
  268. "--keymap",
  269. )
  270. try:
  271. completed = subprocess.run(
  272. args,
  273. check=True,
  274. capture_output=True,
  275. input=self.keymap,
  276. )
  277. except subprocess.CalledProcessError as err:
  278. self.error = (
  279. "failed to compile keymap:\n"
  280. f"------\nstderr:\n{err.stderr}\n"
  281. f"------\nstdout:\n{err.stdout}\n"
  282. f"------\nstdin:\n{self.keymap.decode('utf-8')}\n"
  283. )
  284. self.exitstatus = err.returncode
  285. else:
  286. self.keymap = completed.stdout
  287. @dataclass
  288. class XkbcommonInvocation(Invocation):
  289. UNRECOGNIZED_KEYSYM_ERROR: ClassVar[str] = "XKB-107"
  290. def _check_stderr(self, stderr: str) -> bool:
  291. if self.UNRECOGNIZED_KEYSYM_ERROR in stderr:
  292. for line in stderr.splitlines():
  293. if self.UNRECOGNIZED_KEYSYM_ERROR in line:
  294. self.error = line
  295. break
  296. self.exitstatus = 99 # tool doesn't generate this one
  297. return False
  298. else:
  299. self.exitstatus = 0
  300. return True
  301. @classmethod
  302. def run(
  303. cls,
  304. i: Self,
  305. xkb_root: Path,
  306. output_dir: Path | None,
  307. compress: int,
  308. *args,
  309. **kwargs,
  310. ) -> Self:
  311. i._run(xkb_root, output_dir, not output_dir, compress)
  312. return i
  313. def _run(
  314. self, xkb_root: Path, output_dir: Path | None, test: bool, compress: int
  315. ) -> None:
  316. args = (
  317. "xkbcli-compile-keymap", # this is run in the builddir
  318. "--include",
  319. str(xkb_root),
  320. # Not needed, because we set XKB_LOG_LEVEL and XKB_LOG_VERBOSITY in env
  321. # "--verbose",
  322. *itertools.chain.from_iterable((f"--{k}", v) for k, v in self.rmlvo),
  323. )
  324. if test:
  325. args += ("--test",)
  326. self.command = shlex.join(args)
  327. try:
  328. completed = subprocess.run(args, text=True, check=True, capture_output=True)
  329. except subprocess.CalledProcessError as err:
  330. self.error = (
  331. "failed to compile keymap:\n"
  332. f"------\nstderr:\n{err.stderr}\n"
  333. f"------\nstdout:\n{err.stdout}\n"
  334. )
  335. self.exitstatus = err.returncode
  336. else:
  337. if self._check_stderr(completed.stderr):
  338. self.keymap = completed.stdout.encode("utf-8")
  339. if output_dir:
  340. self._write_keymap(output_dir, compress)
  341. @dataclass
  342. class XkbcommonToXkbcompInvocation(XkbcommonInvocation):
  343. """
  344. Use xkbcli, then pipe the result to xkbcomp in order to check
  345. that xkbcomp can parse xkbcommon output.
  346. """
  347. def _run(
  348. self, xkb_root: Path, output_dir: Path | None, test: bool, compress: int
  349. ) -> None:
  350. super()._run(xkb_root, output_dir, False, compress)
  351. if self.exitstatus:
  352. return
  353. args = ("xkbcomp", "-xkb", "-opt", "g", "-", "-")
  354. try:
  355. completed = subprocess.run(
  356. args,
  357. check=True,
  358. capture_output=True,
  359. input=self.keymap,
  360. )
  361. except subprocess.CalledProcessError as err:
  362. self.error = (
  363. "failed to compile keymap:\n"
  364. f"------\nstderr:\n{err.stderr}\n"
  365. f"------\nstdout:\n{err.stdout}\n"
  366. f"------\nstdin:\n{self.keymap.decode('utf-8')}\n"
  367. )
  368. self.exitstatus = err.returncode
  369. else:
  370. self.keymap = completed.stdout
  371. if output_dir:
  372. self._write_keymap(output_dir, compress)
  373. @dataclass
  374. class Layout:
  375. name: str
  376. variants: list[str | None]
  377. @classmethod
  378. def parse(cls, e: ET.Element, variant: list[str] | None = None) -> Self:
  379. if (name_elem := e.find("configItem/name")) is None or name_elem is None:
  380. raise ValueError("Layout name not found")
  381. if not variant:
  382. variants = [None] + [
  383. cls.parse_text(v)
  384. for v in e.findall("variantList/variant/configItem/name")
  385. ]
  386. else:
  387. variants = cast(list[str | None], variant)
  388. return cls(cls.parse_text(e.find("configItem/name")), variants)
  389. @staticmethod
  390. def parse_text(e: ET.Element | None) -> str:
  391. if e is None or not e.text:
  392. raise ValueError("Name not found")
  393. return e.text
  394. class Registry:
  395. @classmethod
  396. def parse_path(cls, xkb_root: Path, path: Path) -> Path:
  397. if path.is_file():
  398. # File exists: return unchanged
  399. return path
  400. elif len(path.parts) == 1:
  401. # Lookup XML file in XKB root
  402. _path = (
  403. path
  404. if path.suffix == ".xml"
  405. # NOTE: If we got evdev.extras, we want to keep the current suffix
  406. else path.with_suffix(f"{path.suffix}.xml")
  407. )
  408. _path = xkb_root / "rules" / _path
  409. if _path.is_file():
  410. return _path
  411. raise ValueError(f"Cannot resolve registry file: {path}")
  412. @classmethod
  413. def parse(
  414. cls,
  415. paths: Sequence[Path],
  416. tool: type[Invocation],
  417. rules: str | None,
  418. model: str | None,
  419. layout: str | None,
  420. variant: str | None,
  421. option: str | None,
  422. ) -> tuple[int, Iterator[Invocation]]:
  423. models: tuple[str, ...] = ()
  424. layouts: tuple[Layout, ...] = ()
  425. options: tuple[str, ...] = ()
  426. if variant and not layout:
  427. raise ValueError("Variant must be set together with layout")
  428. for path in paths:
  429. root = ET.fromstring(path.read_text(encoding="utf-8"))
  430. # Models
  431. if model is None:
  432. models += tuple(
  433. e.text
  434. for e in root.findall("modelList/model/configItem/name")
  435. if e.text
  436. )
  437. elif not models:
  438. models += (model,)
  439. # Layouts/variants
  440. if layout:
  441. if variant is None:
  442. layouts += tuple(
  443. map(
  444. Layout.parse,
  445. (
  446. e
  447. for e in root.findall("layoutList/layout")
  448. if e.find(f"configItem/name[.='{layout}']") is not None
  449. ),
  450. )
  451. )
  452. elif not layouts:
  453. layouts += (
  454. Layout(layout, cast(list[str | None], variant.split(":"))),
  455. )
  456. else:
  457. layouts += tuple(map(Layout.parse, root.findall("layoutList/layout")))
  458. # Options
  459. if option is None:
  460. options += tuple(
  461. e.text
  462. for e in root.findall("optionList/group/option/configItem/name")
  463. if e.text
  464. )
  465. elif not options and option:
  466. options += (option,)
  467. # Some registry may be only partial, e.g.: *.extras.xml
  468. if not models:
  469. models = (RMLVO.DEFAULT_MODEL,)
  470. if not layouts:
  471. layouts = (Layout(RMLVO.DEFAULT_LAYOUT, [None]),)
  472. count = len(models) * sum(len(l.variants) for l in layouts) * (1 + len(options))
  473. # The list of combos can be huge, so better to use a generator instead
  474. def iter_combos() -> Iterator[Invocation]:
  475. for m in models:
  476. for l in layouts:
  477. for v in l.variants:
  478. yield tool.from_rmlvo(
  479. rules=rules, model=m, layout=l.name, variant=v, option=None
  480. )
  481. for opt in options:
  482. yield tool.from_rmlvo(
  483. rules=rules,
  484. model=m,
  485. layout=l.name,
  486. variant=v,
  487. option=opt,
  488. )
  489. return count, iter_combos()
  490. class Introspection:
  491. """
  492. Enable listing symbols files from an xkbcommon YAML introspection file
  493. """
  494. @unique
  495. class SectionFlag(StrEnum):
  496. DEFAULT = auto()
  497. PARTIAL = auto()
  498. HIDDEN = auto()
  499. ALPHANUMERIC = auto()
  500. MODIFIERS = auto()
  501. KEYPAD = auto()
  502. FN = auto()
  503. ALTGR = auto()
  504. @classmethod
  505. def parse(cls, raw: str) -> Self:
  506. for f in cls:
  507. if raw == f:
  508. return f
  509. raise ValueError(raw)
  510. @dataclass
  511. class Section:
  512. root: Path
  513. file: Path
  514. name: str
  515. flags: tuple[Introspection.SectionFlag]
  516. @property
  517. def file_ref(self) -> str:
  518. if self.name:
  519. return f"{self.file}({self.name})"
  520. else:
  521. return str(self.file)
  522. @classmethod
  523. def parse_path(cls, path: Path) -> Path:
  524. file = path
  525. path = path.parent
  526. while path.name != "symbols":
  527. path = path.parent
  528. return path.parent, file.relative_to(path)
  529. @classmethod
  530. def parse(cls, path: Path, raw: Any) -> Self:
  531. flags = tuple(map(Introspection.SectionFlag.parse, raw["flags"]))
  532. root, file = cls.parse_path(path)
  533. return cls(root=root, file=file, name=raw["section"], flags=flags)
  534. @classmethod
  535. def parse_all(cls, doc: Any) -> Iterable[Self]:
  536. for s in doc["sections"]:
  537. if s["type"] == "symbols":
  538. yield cls.parse(Path(doc["path"]), s)
  539. @classmethod
  540. def _parse(cls, path: Path) -> Iterable[Introspection.Section]:
  541. with path.open("rt", encoding="utf-8") as fd:
  542. docs = tuple(yaml.safe_load_all(fd))
  543. yield from map(cls.Section.parse_all, docs)
  544. @classmethod
  545. def parse(
  546. cls,
  547. paths: Sequence[Path],
  548. tool: type[Invocation],
  549. ) -> tuple[int, Iterator[Invocation]]:
  550. symbols = tuple(
  551. itertools.chain.from_iterable(
  552. itertools.chain.from_iterable(cls._parse(p) for p in paths)
  553. )
  554. )
  555. count = len(symbols)
  556. return count, (tool.from_rmlvo(layout=s.file_ref) for s in symbols)
  557. T = TypeVar("T")
  558. # Needed because Callable does not handle keywords args
  559. class ProgressBar(Protocol[T]):
  560. def __call__(self, x: T, total: int, file: TextIO | None) -> T: ...
  561. # The function generating the progress bar (if any).
  562. def create_progress_bar(verbose: bool) -> ProgressBar[T]:
  563. def noop_progress_bar(x: T, total: int, file: TextIO | None = None) -> T:
  564. return x
  565. progress_bar: ProgressBar[T] = noop_progress_bar
  566. if not verbose and os.isatty(sys.stdout.fileno()):
  567. try:
  568. from tqdm import tqdm
  569. progress_bar = cast(ProgressBar[T], tqdm)
  570. except ImportError:
  571. pass
  572. return progress_bar
  573. def main() -> NoReturn:
  574. parser = argparse.ArgumentParser(
  575. description="""
  576. This tool compiles a keymap for each layout, variant and
  577. options combination in the given rules XML file. The output
  578. of this tool is YAML, use your favorite YAML parser to
  579. extract error messages. Errors are printed to stderr.
  580. """
  581. )
  582. parser.add_argument(
  583. "paths",
  584. metavar="/path/to/rules.xml",
  585. nargs="*",
  586. type=Path,
  587. default=(),
  588. help="Path to xkeyboard-config's XML registry file",
  589. )
  590. parser.add_argument(
  591. "--xkb-root",
  592. default=DEFAULT_XKB_ROOT,
  593. type=Path,
  594. help="XKB root directory",
  595. )
  596. if yaml:
  597. parser.add_argument(
  598. "--from-introspection",
  599. action="store_true",
  600. help="Interpret input files as YAML instrospection files instead of XML registry files",
  601. )
  602. DEFAULT_TOOL = "xkbcommon"
  603. tools: dict[str, type[Invocation]] = {
  604. DEFAULT_TOOL: XkbcommonInvocation,
  605. "xkbcommon-xkbcomp": XkbcommonToXkbcompInvocation,
  606. "xkbcomp": XkbCompInvocation,
  607. "xkbcomp-xkbcommon": XkbCompToXkbcommonInvocation,
  608. }
  609. parser.add_argument(
  610. "--tool",
  611. choices=tools.keys(),
  612. type=str,
  613. default=DEFAULT_TOOL,
  614. help="parsing tool to use",
  615. )
  616. parser.add_argument(
  617. "--jobs",
  618. "-j",
  619. type=int,
  620. default=4 * (os.cpu_count() or 1),
  621. help="number of processes to use",
  622. )
  623. parser.add_argument("--chunksize", default=1, type=int)
  624. parser.add_argument("--verbose", "-v", default=False, action="store_true")
  625. parser.add_argument(
  626. "--short", default=False, action="store_true", help="Concise output"
  627. )
  628. parser.add_argument(
  629. "--keymap-output-dir",
  630. default=None,
  631. type=Path,
  632. help="Directory to print compiled keymaps to",
  633. )
  634. parser.add_argument(
  635. "--compress", type=int, default=0, help="Compression level of keymaps files"
  636. )
  637. parser.add_argument(
  638. "--rules", default=RMLVO.DEFAULT_RULES, type=str, help="Rule set to use"
  639. )
  640. parser.add_argument(
  641. "--model", default="", type=str, help="Only test the given model"
  642. )
  643. parser.add_argument(
  644. "--layout", default=WILDCARD, type=str, help="Only test the given layout"
  645. )
  646. parser.add_argument(
  647. "--variant",
  648. default=WILDCARD,
  649. type=str,
  650. help="Only test the given variants (colon-separated list)",
  651. )
  652. parser.add_argument(
  653. "--option", default=WILDCARD, type=str, help="Only test the given option"
  654. )
  655. parser.add_argument(
  656. "--no-iterations", "-1", action="store_true", help="Only test one combo"
  657. )
  658. args = parser.parse_args()
  659. xkb_root: Path = args.xkb_root
  660. verbose: bool = args.verbose
  661. short = args.short
  662. keymapdir = args.keymap_output_dir
  663. progress_bar: ProgressBar[Iterable[Invocation]] = create_progress_bar(verbose)
  664. tool = tools[args.tool]
  665. # NOTE: We test only one set of rules; handle wild card only for consistency
  666. # with other components.
  667. rules: str | None = None if args.rules == WILDCARD else args.rules
  668. model: str | None = None if args.model == WILDCARD else args.model
  669. layout: str | None = None if args.layout == WILDCARD else args.layout
  670. variant: str | None = None if args.variant == WILDCARD else args.variant
  671. option: str | None = None if args.option == WILDCARD else args.option
  672. if yaml and args.from_introspection:
  673. paths = args.paths
  674. count, iter_combos = Introspection.parse(paths=paths, tool=tool)
  675. else:
  676. if not args.paths:
  677. # If there is no given registry, fallback to the given rules,
  678. # else fallback to the default rules.
  679. path = xkb_root / "rules" / (rules or RMLVO.DEFAULT_RULES)
  680. paths = (path.with_suffix(f"{path.suffix}.xml"),)
  681. else:
  682. paths = tuple(Registry.parse_path(xkb_root, p) for p in args.paths)
  683. if args.no_iterations:
  684. combos = (
  685. tool.from_rmlvo(
  686. rules=rules,
  687. model=model,
  688. layout=layout,
  689. variant=variant,
  690. option=option,
  691. ),
  692. )
  693. count = len(combos)
  694. iter_combos = iter(combos)
  695. else:
  696. count, iter_combos = Registry.parse(
  697. paths, tool, rules, model, layout, variant, option
  698. )
  699. # This need to be valid YAML. Currently unused, so left as comments
  700. print("# xkb root:", json.dumps(str(xkb_root)), file=sys.stderr)
  701. print("# paths:", json.dumps(tuple(map(str, paths))), file=sys.stderr)
  702. failed = tool.run_all(
  703. xkb_root=xkb_root,
  704. combos=iter_combos,
  705. combos_count=count,
  706. njobs=args.jobs,
  707. keymap_output_dir=keymapdir,
  708. verbose=verbose,
  709. short=short,
  710. progress_bar=progress_bar,
  711. chunksize=args.chunksize,
  712. compress=args.compress,
  713. )
  714. sys.exit(failed)
  715. if __name__ == "__main__":
  716. try:
  717. main()
  718. except KeyboardInterrupt:
  719. print("# Exiting after Ctrl+C")