Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Carlos de Paula
2026-06-22 20:11:07 -03:00
400 changed files with 13578 additions and 4077 deletions
+2 -2
View File
@@ -45,7 +45,7 @@ def strip_multiline_comment(string):
def c_source_files(dir_names):
"""Returns a list of all *.c, *.h, and *.cpp files for a given list of directories
"""Returns a list of all *.c, *.h, *.cpp, and *.hpp files for a given list of directories
Args:
@@ -54,7 +54,7 @@ def c_source_files(dir_names):
"""
files = []
for dir in dir_names:
files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp'])
files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp', '.hpp'])
return files
+1
View File
@@ -56,6 +56,7 @@ safe_commands = [
subcommands = [
'qmk.cli.ci.validate_aliases',
'qmk.cli.ci.validate_keyboard_targets',
'qmk.cli.bux',
'qmk.cli.c2json',
'qmk.cli.cd',
@@ -0,0 +1,28 @@
"""Validates the list of keyboard targets.
"""
from milc import cli
from pathlib import Path
@cli.subcommand('Validates the list of keyboard targets.', hidden=True)
def ci_validate_keyboard_targets(cli):
errors = set()
for rules_mk in Path('keyboards').glob('**/rules.mk'):
if any({'keymaps', 'common', 'lib'} & set(rules_mk.parts)):
continue
folder = rules_mk.parent
if not any(folder.glob('**/keyboard.json')):
errors.add(folder)
for keymap in Path('keyboards').glob('**/keymaps/'):
folder = keymap.parent
if not any(folder.glob('**/keyboard.json')):
errors.add(folder)
for error in errors:
print(f"{error}::Legacy target detected")
exit(min(len(errors), 255))
+2
View File
@@ -41,6 +41,7 @@ def compile(cli):
cli.args.filter = []
cli.config.mass_compile.keymap = cli.config.compile.keymap
cli.config.mass_compile.parallel = cli.config.compile.parallel
cli.args.print_failures = False
cli.args.no_temp = False
return mass_compile(cli)
@@ -51,6 +52,7 @@ def compile(cli):
cli.args.filter = []
cli.config.mass_compile.keymap = None
cli.config.mass_compile.parallel = cli.config.compile.parallel
cli.args.print_failures = False
cli.args.no_temp = False
return mass_compile(cli)
+20 -96
View File
@@ -1,62 +1,23 @@
"""OS-specific functions for: Linux
"""
import platform
import shutil
from pathlib import Path
from milc import cli
from qmk.constants import QMK_FIRMWARE, BOOTLOADER_VIDS_PIDS
from qmk.constants import QMK_FIRMWARE
from .check import CheckStatus, release_info
QMK_UDEV_INSTALL_SCRIPT = 'util/install_udev.sh'
def _is_wsl():
return 'microsoft' in platform.uname().release.lower()
def _udev_rule(vid, pid=None, *args):
""" Helper function that return udev rules
"""
rule = ""
if pid:
rule = 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", ATTRS{idProduct}=="%s", TAG+="uaccess"' % (
vid,
pid,
)
else:
rule = 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", TAG+="uaccess"' % vid
if args:
rule = ', '.join([rule, *args])
return rule
def _generate_desired_rules(bootloader_vids_pids):
rules = dict()
for bl in bootloader_vids_pids.keys():
rules[bl] = set()
for vid_pid in bootloader_vids_pids[bl]:
if bl == 'caterina' or bl == 'md-boot':
rules[bl].add(_udev_rule(vid_pid[0], vid_pid[1], 'ENV{ID_MM_DEVICE_IGNORE}="1"'))
else:
rules[bl].add(_udev_rule(vid_pid[0], vid_pid[1]))
return rules
def _deprecated_udev_rule(vid, pid=None):
""" Helper function that return udev rules
Note: these are no longer the recommended rules, this is just used to check for them
"""
if pid:
return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", ATTRS{idProduct}=="%s", MODE:="0666"' % (vid, pid)
else:
return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", MODE:="0666"' % vid
def check_udev_rules():
"""Make sure the udev rules look good.
"""
rc = CheckStatus.OK
udev_dirs = [
Path("/usr/lib/udev/rules.d/"),
Path("/usr/local/lib/udev/rules.d/"),
@@ -64,24 +25,15 @@ def check_udev_rules():
Path("/etc/udev/rules.d/"),
]
desired_rules = _generate_desired_rules(BOOTLOADER_VIDS_PIDS)
if not any(udev_dir.exists() for udev_dir in udev_dirs):
cli.log.warning("{fg_yellow}Can't find udev rules directories, skipping udev rule checking...")
cli.log.debug("Checked directories: %s", ', '.join(str(udev_dir) for udev_dir in udev_dirs))
return CheckStatus.WARNING
# These rules are no longer recommended, only use them to check for their presence.
deprecated_rules = {
'atmel-dfu': {_deprecated_udev_rule("03eb", "2ff4"), _deprecated_udev_rule("03eb", "2ffb"), _deprecated_udev_rule("03eb", "2ff0")},
'kiibohd': {_deprecated_udev_rule("1c11")},
'stm32': {_deprecated_udev_rule("1eaf", "0003"), _deprecated_udev_rule("0483", "df11")},
'bootloadhid': {_deprecated_udev_rule("16c0", "05df")},
'caterina': {'ATTRS{idVendor}=="2a03", ENV{ID_MM_DEVICE_IGNORE}="1"', 'ATTRS{idVendor}=="2341", ENV{ID_MM_DEVICE_IGNORE}="1"'},
'tmk': {_deprecated_udev_rule("feed")}
}
if any(udev_dir.exists() for udev_dir in udev_dirs):
udev_rules = [rule_file for udev_dir in udev_dirs for rule_file in udev_dir.glob('*.rules')]
current_rules = set()
# Collect all rules from the config files
for rule_file in udev_rules:
# Collect all non-comment lines from QMK-related rules files
current_rules = set()
for udev_dir in udev_dirs:
for rule_file in udev_dir.glob('*qmk*'):
try:
for line in rule_file.read_text(encoding='utf-8').split('\n'):
line = line.strip()
@@ -90,45 +42,17 @@ def check_udev_rules():
except (PermissionError, FileNotFoundError):
cli.log.debug("Failed to read: %s", rule_file)
# Check if the desired rules are among the currently present rules
for bootloader, rules in desired_rules.items():
if not rules.issubset(current_rules):
deprecated_rule = deprecated_rules.get(bootloader)
if deprecated_rule and deprecated_rule.issubset(current_rules):
cli.log.warning("{fg_yellow}Found old, deprecated udev rules for '%s' boards. The new rules on https://docs.qmk.fm/#/faq_build?id=linux-udev-rules offer better security with the same functionality.", bootloader)
else:
# For caterina, check if ModemManager is running
if bootloader == "caterina" and check_modem_manager():
cli.log.warning("{fg_yellow}Detected ModemManager without the necessary udev rules. Please either disable it or set the appropriate udev rules if you are using a Pro Micro.")
if not current_rules:
cli.log.warning("{fg_yellow}Missing udev rules for QMK boards. Please run '%s' to install the rules", QMK_UDEV_INSTALL_SCRIPT)
return CheckStatus.WARNING
rc = CheckStatus.WARNING
cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE)
# Check for the qmk_udev ID_QMK marker
if any('ID_QMK' in rule for rule in current_rules):
return CheckStatus.OK
else:
cli.log.warning("{fg_yellow}Can't find udev rules, skipping udev rule checking...")
cli.log.debug("Checked directories: %s", ', '.join(str(udev_dir) for udev_dir in udev_dirs))
return rc
def check_systemd():
"""Check if it's a systemd system
"""
return bool(shutil.which("systemctl"))
def check_modem_manager():
"""Returns True if ModemManager is running.
"""
if check_systemd():
mm_check = cli.run(["systemctl", "--quiet", "is-active", "ModemManager.service"], timeout=10)
if mm_check.returncode == 0:
return True
else:
"""(TODO): Add check for non-systemd systems
"""
return False
# Legacy rules found (TAG+="uaccess" without ID_QMK)
cli.log.warning("{fg_yellow}Found legacy udev rules. Please run '%s' to install the latest rules", QMK_UDEV_INSTALL_SCRIPT)
return CheckStatus.WARNING
def os_test_linux():
+1 -1
View File
@@ -95,7 +95,7 @@ def generate_config_items(kb_info_json, config_h_lines):
try:
config_value = kb_info_json[info_key]
except KeyError:
except (KeyError, IndexError):
continue
if key_type.startswith('array.array'):
+1 -1
View File
@@ -31,7 +31,7 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
try:
rules_value = kb_info_json[info_key]
except KeyError:
except (KeyError, IndexError):
return None
if key_type in ['array', 'list']:
+10 -21
View File
@@ -22,15 +22,10 @@ INVALID_KM_NAMES = ['via', 'vial']
def _list_defaultish_keymaps(kb):
"""Return default like keymaps for a given keyboard
"""
defaultish = ['ansi', 'iso']
keymaps = set(list_keymaps(kb, include_userspace=False, include_community=False))
# This is only here to flag it as "testable", so it doesn't fly under the radar during PR
defaultish.extend(INVALID_KM_NAMES)
keymaps = set()
for x in list_keymaps(kb, include_userspace=False):
if x in defaultish or x.startswith('default'):
keymaps.add(x)
# Ensure that at least a 'default' keymap always exists
keymaps.add('default')
return keymaps
@@ -173,14 +168,6 @@ def _handle_invalid_features(kb, info):
return ok
def _handle_invalid_config(kb, info):
"""Check for invalid keyboard level config
"""
if info.get('url') == "":
cli.log.warning(f'{kb}: Invalid keyboard level config detected - Optional field "url" should not be empty.')
return True
def _chibios_conf_includenext_check(target):
"""Check the ChibiOS conf.h for the correct inclusion of the next conf.h
"""
@@ -297,9 +284,6 @@ def keyboard_check(kb): # noqa C901
if not _handle_invalid_features(kb, kb_info):
ok = False
if not _handle_invalid_config(kb, kb_info):
ok = False
if not _handle_duplicating_code_defaults(kb, kb_info):
ok = False
@@ -366,6 +350,11 @@ def lint(cli):
cli.print_help()
return False
# milc config handling of user.keymap breaks running lint without keymap argument
# so we have to disable that while still allowing a default to be set with lint.keymap
if 'keymap' not in cli.config_source.lint.keys() and cli.config.lint.keymap:
cli.config.lint.keymap = None
if isinstance(cli.config.lint.keyboard, str):
# if provided via config - string not array
keyboard_list = [cli.config.lint.keyboard]
@@ -381,12 +370,12 @@ def lint(cli):
# Determine keymaps to also check
if cli.args.keymap == 'all':
keymaps = list_keymaps(kb)
elif cli.args.keymap:
keymaps = {cli.args.keymap}
elif cli.config.lint.keymap:
keymaps = {cli.config.lint.keymap}
else:
keymaps = _list_defaultish_keymaps(kb)
# Ensure that at least a 'default' keymap always exists
keymaps.add('default')
ok = True
+18 -19
View File
@@ -18,8 +18,8 @@ from qmk.makefile import parse_rules_mk_file
from qmk.math_ops import compute
from qmk.util import maybe_exit, truthy
true_values = ['1', 'on', 'yes']
false_values = ['0', 'off', 'no']
TRUE_VALUES = ['true', '1', 'on', 'yes']
FALSE_VALUES = ['false', '0', 'off', 'no']
class LedFlags(IntFlag):
@@ -319,7 +319,7 @@ def _extract_features(info_data, rules):
for key, value in rules.items():
if key.endswith('_ENABLE'):
key = '_'.join(key.split('_')[:-1]).lower()
value = True if value.lower() in true_values else False if value.lower() in false_values else value
value = True if value.lower() in TRUE_VALUES else False if value.lower() in FALSE_VALUES else value
if key in ['lto']:
continue
@@ -420,19 +420,6 @@ def _extract_direct_matrix(direct_pins):
return direct_pin_array
def _extract_audio(info_data, config_c):
"""Populate data about the audio configuration
"""
audio_pins = []
for pin in 'B5', 'B6', 'B7', 'C4', 'C5', 'C6':
if config_c.get(f'{pin}_AUDIO'):
audio_pins.append(pin)
if audio_pins:
info_data['audio'] = {'pins': audio_pins}
def _extract_encoders_values(config_c, postfix=''):
"""Common encoder extraction logic
"""
@@ -657,7 +644,7 @@ def _config_to_json(key_type, config_value):
elif key_type in ['bool', 'flag']:
if isinstance(config_value, bool):
return config_value
return config_value in true_values
return config_value in TRUE_VALUES
elif key_type == 'hex':
return '0x' + config_value[2:].upper()
@@ -718,7 +705,6 @@ def _extract_config_h(info_data, config_c):
# Pull data that easily can't be mapped in json
_extract_matrix_info(info_data, config_c)
_extract_audio(info_data, config_c)
_extract_secure_unlock(info_data, config_c)
_extract_split_handedness(info_data, config_c)
_extract_split_serial(info_data, config_c)
@@ -1151,4 +1137,17 @@ def get_modules(keyboard, keymap_filename):
if keymap_json:
modules.extend(keymap_json.get('modules', []))
return list(dict.fromkeys(modules)) # remove dupes
# remove duplicates while maintaining the current order
ret = list(dict.fromkeys(modules))
# We currently do not support duplicate module names
# e.g.: ['foo/hello_world', 'bar/hello_world'] will fail
seen = set()
for module in ret:
module_slug = Path(module).name.lower()
if module_slug in seen:
duplicates = list(filter(lambda m: module_slug == Path(m).name.lower(), ret))
raise Exception(f'Duplicate module name detected: "{module_slug}" - {duplicates}')
seen.add(module_slug)
return ret
+18 -14
View File
@@ -389,7 +389,7 @@ def is_keymap_target(keyboard, keymap):
return False
def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False, include_userspace=True):
def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False, include_userspace=True, include_community=True):
"""List the available keymaps for a keyboard.
Args:
@@ -411,6 +411,9 @@ def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=Fa
include_userspace
When set to True, also search userspace for available keymaps
include_community
When set to True, also search community layouts folder for available keymaps
Returns:
a sorted list of valid keymap names.
"""
@@ -434,21 +437,22 @@ def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=Fa
kb_path = kb_path.parent
# Check community layouts as a fallback
info = info_json(keyboard)
if include_community:
# Check community layouts as a fallback
info = info_json(keyboard)
community_parents = list(Path('layouts').glob('*/'))
if has_userspace and (Path(QMK_USERSPACE) / "layouts").exists():
community_parents.append(Path(QMK_USERSPACE) / "layouts")
community_parents = list(Path('layouts').glob('*/'))
if has_userspace and (Path(QMK_USERSPACE) / "layouts").exists():
community_parents.append(Path(QMK_USERSPACE) / "layouts")
for community_parent in community_parents:
for layout in info.get("community_layouts", []):
cl_path = community_parent / layout
if cl_path.is_dir():
for keymap in cl_path.iterdir():
if is_keymap_dir(keymap, c, json, additional_files):
keymap = keymap if fullpath else keymap.name
names.add(keymap)
for community_parent in community_parents:
for layout in info.get("community_layouts", []):
cl_path = community_parent / layout
if cl_path.is_dir():
for keymap in cl_path.iterdir():
if is_keymap_dir(keymap, c, json, additional_files):
keymap = keymap if fullpath else keymap.name
names.add(keymap)
return sorted(names)