summaryrefslogtreecommitdiff
path: root/.gitlab-ci
diff options
context:
space:
mode:
authorPeter Hutterer <peter.hutterer@who-t.net>2021-01-08 14:33:56 +1000
committerSergey Udaltsov <sergey.udaltsov@gmail.com>2021-02-16 16:16:32 +0000
commit4265882fd2293cebea4980f7df0f2c53357bf78b (patch)
treed14e35498481a418332976d88ebbeb09ad08a7c1 /.gitlab-ci
parent6307c44a2d813350d90183f426de2174e3a4d6e3 (diff)
downloadxkeyboard-config-4265882fd2293cebea4980f7df0f2c53357bf78b.tar.gz
gitlab CI: generate the evdev keycodes (v2)
The various <I123> keycodes in keycodes/evdev simply match the kernel defines + offset 8. There is no need to maintain these manually, let's generate them instead. Keycodes update rarely and irregularly (on average maybe every second kernel release) so there's no need to integrate this into the build itself, let's add it to our CI instead. The script here uses python-libevdev which has a list of the various key codes and their names (compile-time built-in in libevdev itself so it's advisable that a recent libevdev is used). The script is hooked up to a custom job that will fail if there are key codes with a #define in the kernel that are not listed in our evdev file. We allow that job to fail, it's not that urgent to block any merge requests. Changes to v1, see commit 5dc9b48c and its revert 8fa3b314: - Parse the template for existing defines and alias those keys. e.g. alias <I121> = <MUTE>; - Kernel v5.10 keycodes are now included in the file - The script defaults to the correct template/keycode file, no commandline arguments needed for the default run. Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Diffstat (limited to '.gitlab-ci')
-rwxr-xr-x.gitlab-ci/generate-evdev-keycodes.py212
1 files changed, 212 insertions, 0 deletions
diff --git a/.gitlab-ci/generate-evdev-keycodes.py b/.gitlab-ci/generate-evdev-keycodes.py
new file mode 100755
index 0000000..fd8b0af
--- /dev/null
+++ b/.gitlab-ci/generate-evdev-keycodes.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+#
+# Generate the keycodes/evdev file from the names defined in
+# linux/input-event-codes.h
+#
+# Note that this script relies on libevdev to provide the key names and
+# those are compiled in. Ensure you have a recent-enough libevdev to
+# generate this list.
+#
+
+import argparse
+import contextlib
+import re
+import sys
+try:
+ import libevdev
+except ImportError:
+ print('WARNING: python-libevdev not available, cannot check for new evdev keycodes', file=sys.stderr)
+ sys.exit(77)
+
+
+# The marker to search for in the template file, replaced with our generated
+# codes.
+replacement_marker = '@evdevkeys@'
+
+# These markers are put into the result file and are used to detect
+# the section that we added when parsing an existing file.
+section_header = 'Key codes below are autogenerated'
+section_footer = 'End of autogenerated key codes'
+
+
+def evdev_codes():
+ '''
+ Return the dict {code, name} for all known evdev codes.
+
+ The list of names is compiled into libevdev.so, use a recent libevdev
+ release to get the most up-to-date list.
+ '''
+ codes = {}
+ for c in libevdev.EV_KEY.codes:
+ # 112 because that's where our 1:1 keycode entries historically
+ # started.
+ # Undefined keys are those with a code < KEY_MAX but without a
+ # #define in the kernel header file
+ if c.value < 112 or not c.is_defined:
+ continue
+
+ if c.name.startswith('BTN_') or c.name == 'KEY_MAX':
+ continue
+
+ codes[c.value] = c.name
+
+ return codes
+
+
+def existing_keys(lines):
+ '''
+ Return the dict {code, name} for all existing keycodes in the templates
+ file.
+
+ This is a very simple parser, good enough for the keycodes/evdev file
+ but that's about it.
+ '''
+ pattern = re.compile(r'\s+\<([^>]+)\>\s+=\s+(\d+);')
+ keys = {}
+ for line in lines:
+ match = re.match(pattern, line)
+ if not match:
+ continue
+ keys[int(match.group(2))] = match.group(1)
+
+ return keys
+
+
+def generate_keycodes_file(template, codes):
+ '''
+ Generate a new keycodes/evdev file with line containing @evdevkeys@
+ replaced by the full list of known evdev key codes, including our
+ section_header/footer. Expected output:
+
+ ::
+
+ // $section_header
+ <I$keycode> = <$keycode + 8> // #define $kernel_name
+ ...
+ // $section_footer
+
+ '''
+ lines = template.readlines()
+ existing = existing_keys(lines)
+
+ output = []
+ for line in lines:
+ if replacement_marker not in line:
+ output.append(line)
+ continue
+
+ output.append(f'\t// {section_header}\n')
+
+ warned = False
+ for code, name in codes.items():
+ xkeycode = code + 8
+
+ if xkeycode > 255 and not warned:
+ warned = True
+ output.append('\n')
+ output.append('\t// Key codes below cannot be used in X\n')
+ output.append('\n')
+
+ if xkeycode in existing:
+ output.append(f'\talias <I{xkeycode}> = <{existing[xkeycode]}>; // #define {name:23s} {code}\n')
+ continue
+
+ # Special keys that need a comment
+ special_keys = {
+ 211: 'conflicts with AB11',
+ }
+
+ comment = special_keys.get(xkeycode, '')
+ if comment:
+ comment = f' {comment}'
+
+ output.append(f'\t<I{xkeycode}> = {xkeycode};\t\t// #define {name:23s} {code}{comment}\n')
+ output.append(f'\t// {section_footer}\n')
+
+ return output
+
+
+def extract_generated_keycodes(fp):
+ """
+ Return an iterator the keycode of any <I123> keys between the section
+ header and footer.
+ """
+ in_generated_section = False
+ pattern = re.compile('.*<I([0-9]*)>.*')
+
+ for line in fp:
+ if section_header in line:
+ in_generated_section = True
+ continue
+ elif section_footer in line:
+ return
+ elif in_generated_section:
+ match = pattern.match(line)
+ if match:
+ yield int(match[1])
+
+
+def compare_with(codes, oldfile):
+ '''
+ Extract the <I123> keycodes from between the section_header/footer of
+ oldfile and return a list of keycodes that are in codes but not in
+ oldfile.
+ '''
+ old_keycodes = extract_generated_keycodes(oldfile)
+ keycodes = [c + 8 for c in codes] # X keycode offset
+
+ # This does not detect keycodes in old_keycode but not in the new
+ # generated list - should never happen anyway.
+ return sorted(set(keycodes).difference(old_keycodes))
+
+
+def log_msg(msg):
+ print(msg, file=sys.stderr)
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Generate the evdev keycode lists.')
+ parser.add_argument('--template', type=argparse.FileType('r'),
+ default=open('keycodes/evdev.in'),
+ help='The template file (default: keycodes/evdev.in)')
+ parser.add_argument('--output', type=str, default='keycodes/evdev', required=False,
+ help='The file to be written to (default: keycodes/evdev)')
+ parser.add_argument('--compare-with', type=argparse.FileType('r'),
+ default=open('keycodes/evdev'),
+ help='Compare generated output with the given file (default: keycodes/evdev)')
+ parser.add_argument('--verbose', action=argparse.BooleanOptionalAction,
+ help='Print verbose output to stderr')
+ ns = parser.parse_args()
+
+ codes = evdev_codes()
+ rc = 0
+ if ns.verbose:
+ kmin, kmax = min(codes.keys()), max(codes.keys())
+ log_msg(f'evdev keycode range: {kmin} ({kmin:#x}) → {kmax} ({kmax:#x})')
+
+ # We compare before writing so we can use the same filename for
+ # --compare-with and --output. That's also why --output has to be type
+ # str instead of FileType('w').
+ if ns.compare_with:
+ diff = compare_with(codes, ns.compare_with)
+ if diff:
+ rc = 1
+ if ns.verbose:
+ log_msg(f'File {ns.compare_with.name} is out of date, missing keycodes:')
+ for k in diff:
+ name = codes[k - 8] # remove the X offset
+ log_msg(f' <I{k}> // #define {name}')
+
+ with contextlib.ExitStack() as stack:
+ if ns.output == '-':
+ fd = sys.stdout
+ else:
+ fd = stack.enter_context(open(ns.output, 'w'))
+ output = generate_keycodes_file(ns.template, codes)
+ fd.write(''.join(output))
+
+ sys.exit(rc)
+
+
+if __name__ == '__main__':
+ main()