Merge commit 'd9e077468ab3446cbd7306a453a73dad2c1403e8' into firmware_21

This commit is contained in:
Drashna Jael're
2021-10-11 20:36:47 -07:00
831 changed files with 23110 additions and 8122 deletions

View File

@@ -74,7 +74,6 @@ def find_layouts(file):
key['matrix'] = matrix_locations[key['label']]
parsed_layouts[macro_name] = {
'key_count': len(parsed_layout),
'layout': parsed_layout,
'filename': str(file),
}

View File

@@ -16,12 +16,14 @@ import_names = {
# A mapping of package name to importable name
'pep8-naming': 'pep8ext_naming',
'pyusb': 'usb.core',
'qmk-dotty-dict': 'dotty_dict'
}
safe_commands = [
# A list of subcommands we always run, even when the module imports fail
'clone',
'config',
'doctor',
'env',
'setup',
]
@@ -38,7 +40,10 @@ subcommands = [
'qmk.cli.doctor',
'qmk.cli.fileformat',
'qmk.cli.flash',
'qmk.cli.format.c',
'qmk.cli.format.json',
'qmk.cli.format.python',
'qmk.cli.format.text',
'qmk.cli.generate.api',
'qmk.cli.generate.config_h',
'qmk.cli.generate.dfu_header',
@@ -48,12 +53,14 @@ subcommands = [
'qmk.cli.generate.layouts',
'qmk.cli.generate.rgb_breathe_table',
'qmk.cli.generate.rules_mk',
'qmk.cli.generate.version_h',
'qmk.cli.hello',
'qmk.cli.info',
'qmk.cli.json2c',
'qmk.cli.lint',
'qmk.cli.list.keyboards',
'qmk.cli.list.keymaps',
'qmk.cli.list.layouts',
'qmk.cli.kle2json',
'qmk.cli.multibuild',
'qmk.cli.new.keyboard',
@@ -63,6 +70,26 @@ subcommands = [
]
def _install_deps(requirements):
"""Perform the installation of missing requirements.
If we detect that we are running in a virtualenv we can't write into we'll use sudo to perform the pip install.
"""
command = [sys.executable, '-m', 'pip', 'install']
if sys.prefix != sys.base_prefix:
# We are in a virtualenv, check to see if we need to use sudo to write to it
if not os.access(sys.prefix, os.W_OK):
print('Notice: Using sudo to install modules to location owned by root:', sys.prefix)
command.insert(0, 'sudo')
elif not os.access(sys.prefix, os.W_OK):
# We can't write to sys.prefix, attempt to install locally
command.append('--local')
return _run_cmd(*command, '-r', requirements)
def _run_cmd(*command):
"""Run a command in a subshell.
"""
@@ -155,8 +182,14 @@ if int(milc_version[0]) < 2 and int(milc_version[1]) < 4:
print(f'Your MILC library is too old! Please upgrade: python3 -m pip install -U -r {str(requirements)}')
exit(127)
# Make sure we can run binaries in the same directory as our Python interpreter
python_dir = os.path.dirname(sys.executable)
if python_dir not in os.environ['PATH'].split(':'):
os.environ['PATH'] = ":".join((python_dir, os.environ['PATH']))
# Check to make sure we have all our dependencies
msg_install = 'Please run `python3 -m pip install -r %s` to install required python dependencies.'
msg_install = f'Please run `{sys.executable} -m pip install -r %s` to install required python dependencies.'
args = sys.argv[1:]
while args and args[0][0] == '-':
del args[0]
@@ -166,7 +199,7 @@ safe_command = args and args[0] in safe_commands
if not safe_command:
if _broken_module_imports('requirements.txt'):
if yesno('Would you like to install the required Python modules?'):
_run_cmd(sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt')
_install_deps('requirements.txt')
else:
print()
print(msg_install % (str(Path('requirements.txt').resolve()),))
@@ -175,7 +208,7 @@ if not safe_command:
if cli.config.user.developer and _broken_module_imports('requirements-dev.txt'):
if yesno('Would you like to install the required developer Python modules?'):
_run_cmd(sys.executable, '-m', 'pip', 'install', '-r', 'requirements-dev.txt')
_install_deps('requirements-dev.txt')
elif yesno('Would you like to disable developer mode?'):
_run_cmd(sys.argv[0], 'config', 'user.developer=None')
else:
@@ -190,7 +223,7 @@ for subcommand in subcommands:
try:
__import__(subcommand)
except ModuleNotFoundError as e:
except (ImportError, ModuleNotFoundError) as e:
if safe_command:
print(f'Warning: Could not import {subcommand}: {e.__class__.__name__}, {e}')
else:

139
lib/python/qmk/cli/cformat.py Normal file → Executable file
View File

@@ -1,137 +1,28 @@
"""Format C code according to QMK's style.
"""Point people to the new command name.
"""
from os import path
from shutil import which
from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
import sys
from pathlib import Path
from argcomplete.completers import FilesCompleter
from milc import cli
from qmk.path import normpath
from qmk.c_parse import c_source_files
c_file_suffixes = ('c', 'h', 'cpp')
core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
def find_clang_format():
"""Returns the path to clang-format.
"""
for clang_version in range(20, 6, -1):
binary = f'clang-format-{clang_version}'
if which(binary):
return binary
return 'clang-format'
def find_diffs(files):
"""Run clang-format and diff it against a file.
"""
found_diffs = False
for file in files:
cli.log.debug('Checking for changes in %s', file)
clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
if diff.returncode != 0:
print(diff.stdout)
found_diffs = True
return found_diffs
def cformat_run(files):
"""Spawn clang-format subprocess with proper arguments
"""
# Determine which version of clang-format to use
clang_format = [find_clang_format(), '-i']
try:
cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
cli.log.info('Successfully formatted the C code.')
return True
except CalledProcessError as e:
cli.log.error('Error formatting C code!')
cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
cli.log.debug('STDOUT:')
cli.log.debug(e.stdout)
cli.log.debug('STDERR:')
cli.log.debug(e.stderr)
return False
def filter_files(files, core_only=False):
"""Yield only files to be formatted and skip the rest
"""
if core_only:
# Filter non-core files
for index, file in enumerate(files):
# The following statement checks each file to see if the file path is
# - in the core directories
# - not in the ignored directories
if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
files[index] = None
cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
for file in files:
if file and file.name.split('.')[-1] in c_file_suffixes:
yield file
else:
cli.log.debug('Skipping file %s', file)
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
@cli.argument('files', nargs='*', arg_only=True, help='Filename(s) to format.')
@cli.subcommand('Pointer to the new command name: qmk format-c.', hidden=True)
def cformat(cli):
"""Format C code according to QMK's style.
"""Pointer to the new command name: qmk format-c.
"""
# Find the list of files to format
if cli.args.files:
files = list(filter_files(cli.args.files, cli.args.core_only))
cli.log.warning('"qmk cformat" has been renamed to "qmk format-c". Please use the new command in the future.')
argv = [sys.executable, *sys.argv]
argv[argv.index('cformat')] = 'format-c'
script_path = Path(argv[1])
script_path_exe = Path(f'{argv[1]}.exe')
if not files:
cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
exit(0)
if not script_path.exists() and script_path_exe.exists():
# For reasons I don't understand ".exe" is stripped from the script name on windows.
argv[1] = str(script_path_exe)
if cli.args.all_files:
cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
elif cli.args.all_files:
all_files = c_source_files(core_dirs)
files = list(filter_files(all_files, True))
else:
git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
if git_diff.returncode != 0:
cli.log.error("Error running %s", git_diff_cmd)
print(git_diff.stderr)
return git_diff.returncode
files = []
for file in git_diff.stdout.strip().split('\n'):
if not any([file.startswith(ignore) for ignore in ignored]):
if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
files.append(file)
# Sanity check
if not files:
cli.log.error('No changed files detected. Use "qmk cformat -a" to format all core files')
return False
# Run clang-format on the files we've found
if cli.args.dry_run:
return not find_diffs(files)
else:
return cformat_run(files)
return cli.run(argv, capture_output=False).returncode

View File

@@ -18,7 +18,7 @@ from qmk.keymap import keymap_completer
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
@cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
@cli.subcommand('Compile a QMK Firmware.')

View File

@@ -34,8 +34,8 @@ KNOWN_BOOTLOADERS = {
('03EB', '2FFB'): 'atmel-dfu: AT90USB128',
('03EB', '6124'): 'Microchip SAM-BA',
('0483', 'DF11'): 'stm32-dfu: STM32 BOOTLOADER',
('16C0', '05DC'): 'USBasp: USBaspLoader',
('16C0', '05DF'): 'bootloadHID: HIDBoot',
('16C0', '05DC'): 'usbasploader: USBaspLoader',
('16C0', '05DF'): 'bootloadhid: HIDBoot',
('16C0', '0478'): 'halfkay: Teensy Halfkay',
('1B4F', '9203'): 'caterina: Pro Micro 3.3V',
('1B4F', '9205'): 'caterina: Pro Micro 5V',
@@ -48,10 +48,11 @@ KNOWN_BOOTLOADERS = {
('239A', '000C'): 'caterina: Adafruit Feather 32U4',
('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v',
('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
('2A03', '0036'): 'caterina: Arduino Leonardo',
('2A03', '0037'): 'caterina: Arduino Micro',
('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode'
('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode',
('03EB', '2067'): 'qmk-hid: HID Bootloader',
('03EB', '2045'): 'lufa-ms: LUFA Mass Storage Bootloader'
}

View File

@@ -2,11 +2,13 @@
"""
import http.server
import os
import webbrowser
from milc import cli
@cli.argument('-p', '--port', default=8936, type=int, help='Port number to use.')
@cli.argument('-b', '--browser', action='store_true', help='Open the docs in the default browser.')
@cli.subcommand('Run a local webserver for QMK documentation.', hidden=False if cli.config.user.developer else True)
def docs(cli):
"""Spin up a local HTTPServer instance for the QMK docs.
@@ -14,9 +16,12 @@ def docs(cli):
os.chdir('docs')
with http.server.HTTPServer(('', cli.config.docs.port), http.server.SimpleHTTPRequestHandler) as httpd:
cli.log.info("Serving QMK docs at http://localhost:%d/", cli.config.docs.port)
cli.log.info(f"Serving QMK docs at http://localhost:{cli.config.docs.port}/")
cli.log.info("Press Control+C to exit.")
if cli.config.docs.browser:
webbrowser.open(f'http://localhost:{cli.config.docs.port}')
try:
httpd.serve_forever()
except KeyboardInterrupt:

View File

@@ -0,0 +1,5 @@
"""QMK Doctor
Check out the user's QMK environment and make sure it's ready to compile.
"""
from .main import doctor

View File

@@ -1,4 +1,4 @@
"""OS-agnostic helper functions
"""Check for specific programs.
"""
from enum import Enum
import re
@@ -30,7 +30,7 @@ ESSENTIAL_BINARIES = {
}
def parse_gcc_version(version):
def _parse_gcc_version(version):
m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
return {
@@ -40,7 +40,7 @@ def parse_gcc_version(version):
}
def check_arm_gcc_version():
def _check_arm_gcc_version():
"""Returns True if the arm-none-eabi-gcc version is not known to cause problems.
"""
if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']:
@@ -50,7 +50,7 @@ def check_arm_gcc_version():
return CheckStatus.OK # Right now all known arm versions are ok
def check_avr_gcc_version():
def _check_avr_gcc_version():
"""Returns True if the avr-gcc version is not known to cause problems.
"""
rc = CheckStatus.ERROR
@@ -60,7 +60,7 @@ def check_avr_gcc_version():
cli.log.info('Found avr-gcc version %s', version_number)
rc = CheckStatus.OK
parsed_version = parse_gcc_version(version_number)
parsed_version = _parse_gcc_version(version_number)
if parsed_version['major'] > 8:
cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.')
rc = CheckStatus.WARNING
@@ -68,7 +68,7 @@ def check_avr_gcc_version():
return rc
def check_avrdude_version():
def _check_avrdude_version():
if 'output' in ESSENTIAL_BINARIES['avrdude']:
last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2]
version_number = last_line.split()[2][:-1]
@@ -77,7 +77,7 @@ def check_avrdude_version():
return CheckStatus.OK
def check_dfu_util_version():
def _check_dfu_util_version():
if 'output' in ESSENTIAL_BINARIES['dfu-util']:
first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]
version_number = first_line.split()[1]
@@ -86,7 +86,7 @@ def check_dfu_util_version():
return CheckStatus.OK
def check_dfu_programmer_version():
def _check_dfu_programmer_version():
if 'output' in ESSENTIAL_BINARIES['dfu-programmer']:
first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]
version_number = first_line.split()[1]
@@ -111,7 +111,7 @@ def check_binary_versions():
"""Check the versions of ESSENTIAL_BINARIES
"""
versions = []
for check in (check_arm_gcc_version, check_avr_gcc_version, check_avrdude_version, check_dfu_util_version, check_dfu_programmer_version):
for check in (_check_arm_gcc_version, _check_avr_gcc_version, _check_avrdude_version, _check_dfu_util_version, _check_dfu_programmer_version):
versions.append(check())
return versions
@@ -159,6 +159,6 @@ def check_git_repo():
This is a decent enough indicator that the qmk_firmware directory is a
proper Git repository, rather than a .zip download from GitHub.
"""
dot_git_dir = QMK_FIRMWARE / '.git'
dot_git = QMK_FIRMWARE / '.git'
return CheckStatus.OK if dot_git_dir.is_dir() else CheckStatus.WARNING
return CheckStatus.OK if dot_git.exists() else CheckStatus.WARNING

View File

@@ -1,11 +1,13 @@
"""OS-specific functions for: Linux
"""
from pathlib import Path
import platform
import shutil
from pathlib import Path
from milc import cli
from qmk.constants import QMK_FIRMWARE
from qmk.os_helpers import CheckStatus
from .check import CheckStatus
def _udev_rule(vid, pid=None, *args):
@@ -39,7 +41,12 @@ def check_udev_rules():
"""Make sure the udev rules look good.
"""
rc = CheckStatus.OK
udev_dir = Path("/etc/udev/rules.d/")
udev_dirs = [
Path("/usr/lib/udev/rules.d/"),
Path("/usr/local/lib/udev/rules.d/"),
Path("/run/udev/rules.d/"),
Path("/etc/udev/rules.d/"),
]
desired_rules = {
'atmel-dfu': {
_udev_rule("03eb", "2fef"), # ATmega16U2
@@ -78,6 +85,10 @@ def check_udev_rules():
# dog hunter AG
_udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
_udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro
},
'hid-bootloader': {
_udev_rule("03eb", "2067"), # QMK HID
_udev_rule("16c0", "0478") # PJRC halfkay
}
}
@@ -91,8 +102,8 @@ def check_udev_rules():
'tmk': {_deprecated_udev_rule("feed")}
}
if udev_dir.exists():
udev_rules = [rule_file for rule_file in udev_dir.glob('*.rules')]
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
@@ -114,15 +125,15 @@ def check_udev_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":
if check_modem_manager():
rc = CheckStatus.WARNING
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 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.")
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)
else:
cli.log.warning("{fg_yellow}'%s' does not exist. Skipping udev rule checking...", udev_dir)
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
@@ -149,3 +160,22 @@ def check_modem_manager():
"""(TODO): Add check for non-systemd systems
"""
return False
def os_test_linux():
"""Run the Linux specific tests.
"""
# Don't bother with udev on WSL, for now
if 'microsoft' in platform.uname().release.lower():
cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
# https://github.com/microsoft/WSL/issues/4197
if QMK_FIRMWARE.as_posix().startswith("/mnt"):
cli.log.warning("I/O performance on /mnt may be extremely slow.")
return CheckStatus.WARNING
return CheckStatus.OK
else:
cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
return check_udev_rules()

View File

@@ -0,0 +1,13 @@
import platform
from milc import cli
from .check import CheckStatus
def os_test_macos():
"""Run the Mac specific tests.
"""
cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0])
return CheckStatus.OK

View File

@@ -7,9 +7,11 @@ from subprocess import DEVNULL
from milc import cli
from milc.questions import yesno
from qmk import submodules
from qmk.constants import QMK_FIRMWARE
from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo
from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM
from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv
def os_tests():
@@ -18,51 +20,48 @@ def os_tests():
platform_id = platform.platform().lower()
if 'darwin' in platform_id or 'macos' in platform_id:
from .macos import os_test_macos
return os_test_macos()
elif 'linux' in platform_id:
from .linux import os_test_linux
return os_test_linux()
elif 'windows' in platform_id:
from .windows import os_test_windows
return os_test_windows()
else:
cli.log.warning('Unsupported OS detected: %s', platform_id)
return CheckStatus.WARNING
def os_test_linux():
"""Run the Linux specific tests.
def git_tests():
"""Run Git-related checks
"""
# Don't bother with udev on WSL, for now
if 'microsoft' in platform.uname().release.lower():
cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
status = CheckStatus.OK
# https://github.com/microsoft/WSL/issues/4197
if QMK_FIRMWARE.as_posix().startswith("/mnt"):
cli.log.warning("I/O performance on /mnt may be extremely slow.")
return CheckStatus.WARNING
return CheckStatus.OK
# Make sure our QMK home is a Git repo
git_ok = git_check_repo()
if not git_ok:
cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)")
status = CheckStatus.WARNING
else:
cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
from qmk.os_helpers.linux import check_udev_rules
git_branch = git_get_branch()
if git_branch:
cli.log.info('Git branch: %s', git_branch)
git_dirty = git_is_dirty()
if git_dirty:
cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.')
status = CheckStatus.WARNING
git_remotes = git_get_remotes()
if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''):
cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".')
status = CheckStatus.WARNING
else:
git_deviation = git_check_deviation(git_branch)
if git_branch in ['master', 'develop'] and git_deviation:
cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch)
status = CheckStatus.WARNING
return check_udev_rules()
def os_test_macos():
"""Run the Mac specific tests.
"""
cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0])
return CheckStatus.OK
def os_test_windows():
"""Run the Windows specific tests.
"""
win32_ver = platform.win32_ver()
cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
return CheckStatus.OK
return status
@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
@@ -77,16 +76,17 @@ def doctor(cli):
* [ ] Compile a trivial program with each compiler
"""
cli.log.info('QMK Doctor is checking your environment.')
cli.log.info('CLI version: %s', cli.version)
cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE)
status = os_tests()
status = os_status = os_tests()
git_status = git_tests()
# Make sure our QMK home is a Git repo
git_ok = check_git_repo()
if git_status == CheckStatus.ERROR or (os_status == CheckStatus.OK and git_status == CheckStatus.WARNING):
status = git_status
if git_ok == CheckStatus.WARNING:
cli.log.warning("QMK home does not appear to be a Git repository! (no .git folder)")
status = CheckStatus.WARNING
if in_virtualenv():
cli.log.info('CLI installed in virtualenv.')
# Make sure the basic CLI tools we need are available and can be executed.
bin_ok = check_binaries()

View File

@@ -0,0 +1,14 @@
import platform
from milc import cli
from .check import CheckStatus
def os_test_windows():
"""Run the Windows specific tests.
"""
win32_ver = platform.win32_ver()
cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
return CheckStatus.OK

24
lib/python/qmk/cli/fileformat.py Normal file → Executable file
View File

@@ -1,13 +1,23 @@
"""Format files according to QMK's style.
"""Point people to the new command name.
"""
import sys
from pathlib import Path
from milc import cli
import subprocess
@cli.subcommand("Format files according to QMK's style.", hidden=True)
@cli.subcommand('Pointer to the new command name: qmk format-text.', hidden=True)
def fileformat(cli):
"""Run several general formatting commands.
"""Pointer to the new command name: qmk format-text.
"""
dos2unix = subprocess.run(['bash', '-c', 'git ls-files -z | xargs -0 dos2unix'], stdout=subprocess.DEVNULL)
return dos2unix.returncode
cli.log.warning('"qmk fileformat" has been renamed to "qmk format-text". Please use the new command in the future.')
argv = [sys.executable, *sys.argv]
argv[argv.index('fileformat')] = 'format-text'
script_path = Path(argv[1])
script_path_exe = Path(f'{argv[1]}.exe')
if not script_path.exists() and script_path_exe.exists():
# For reasons I don't understand ".exe" is stripped from the script name on windows.
argv[1] = str(script_path_exe)
return cli.run(argv, capture_output=False).returncode

View File

@@ -38,7 +38,7 @@ def print_bootloader_help():
@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
@cli.subcommand('QMK Flash.')

View File

@@ -0,0 +1,137 @@
"""Format C code according to QMK's style.
"""
from os import path
from shutil import which
from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
from argcomplete.completers import FilesCompleter
from milc import cli
from qmk.path import normpath
from qmk.c_parse import c_source_files
c_file_suffixes = ('c', 'h', 'cpp')
core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards')
def find_clang_format():
"""Returns the path to clang-format.
"""
for clang_version in range(20, 6, -1):
binary = f'clang-format-{clang_version}'
if which(binary):
return binary
return 'clang-format'
def find_diffs(files):
"""Run clang-format and diff it against a file.
"""
found_diffs = False
for file in files:
cli.log.debug('Checking for changes in %s', file)
clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
if diff.returncode != 0:
print(diff.stdout)
found_diffs = True
return found_diffs
def cformat_run(files):
"""Spawn clang-format subprocess with proper arguments
"""
# Determine which version of clang-format to use
clang_format = [find_clang_format(), '-i']
try:
cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
cli.log.info('Successfully formatted the C code.')
return True
except CalledProcessError as e:
cli.log.error('Error formatting C code!')
cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
cli.log.debug('STDOUT:')
cli.log.debug(e.stdout)
cli.log.debug('STDERR:')
cli.log.debug(e.stderr)
return False
def filter_files(files, core_only=False):
"""Yield only files to be formatted and skip the rest
"""
if core_only:
# Filter non-core files
for index, file in enumerate(files):
# The following statement checks each file to see if the file path is
# - in the core directories
# - not in the ignored directories
if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
files[index] = None
cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
for file in files:
if file and file.name.split('.')[-1] in c_file_suffixes:
yield file
else:
cli.log.debug('Skipping file %s', file)
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
def format_c(cli):
"""Format C code according to QMK's style.
"""
# Find the list of files to format
if cli.args.files:
files = list(filter_files(cli.args.files, cli.args.core_only))
if not files:
cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
exit(0)
if cli.args.all_files:
cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
elif cli.args.all_files:
all_files = c_source_files(core_dirs)
files = list(filter_files(all_files, True))
else:
git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
if git_diff.returncode != 0:
cli.log.error("Error running %s", git_diff_cmd)
print(git_diff.stderr)
return git_diff.returncode
files = []
for file in git_diff.stdout.strip().split('\n'):
if not any([file.startswith(ignore) for ignore in ignored]):
if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
files.append(file)
# Sanity check
if not files:
cli.log.error('No changed files detected. Use "qmk format-c -a" to format all core files')
return False
# Run clang-format on the files we've found
if cli.args.dry_run:
return not find_diffs(files)
else:
return cformat_run(files)

View File

@@ -8,7 +8,7 @@ from jsonschema import ValidationError
from milc import cli
from qmk.info import info_json
from qmk.json_schema import json_load, keyboard_validate
from qmk.json_schema import json_load, validate
from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder
from qmk.path import normpath
@@ -23,14 +23,13 @@ def format_json(cli):
if cli.args.format == 'auto':
try:
keyboard_validate(json_file)
validate(json_file, 'qmk.keyboard.v1')
json_encoder = InfoJSONEncoder
except ValidationError as e:
cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e)
cli.log.info('Treating %s as a keymap file.', cli.args.json_file)
json_encoder = KeymapJSONEncoder
elif cli.args.format == 'keyboard':
json_encoder = InfoJSONEncoder
elif cli.args.format == 'keymap':

View File

@@ -0,0 +1,26 @@
"""Format python code according to QMK's style.
"""
from subprocess import CalledProcessError, DEVNULL
from milc import cli
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True)
def format_python(cli):
"""Format python code according to QMK's style.
"""
edit = '--diff' if cli.args.dry_run else '--in-place'
yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python']
try:
cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.')
return True
except CalledProcessError:
if cli.args.dry_run:
cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!')
else:
cli.log.error('Error formatting python code!')
return False

View File

@@ -0,0 +1,27 @@
"""Ensure text files have the proper line endings.
"""
from subprocess import CalledProcessError
from milc import cli
@cli.subcommand("Ensure text files have the proper line endings.", hidden=True)
def format_text(cli):
"""Ensure text files have the proper line endings.
"""
try:
file_list_cmd = cli.run(['git', 'ls-files', '-z'], check=True)
except CalledProcessError as e:
cli.log.error('Could not get file list: %s', e)
exit(1)
except Exception as e:
cli.log.error('Unhandled exception: %s: %s', e.__class__.__name__, e)
cli.log.exception(e)
exit(1)
dos2unix = cli.run(['xargs', '-0', 'dos2unix'], stdin=None, input=file_list_cmd.stdout)
if dos2unix.returncode != 0:
print(dos2unix.stderr)
return dos2unix.returncode

View File

@@ -5,14 +5,14 @@ from pathlib import Path
from dotty_dict import dotty
from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.json_schema import json_load
from qmk.json_schema import json_load, validate
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import is_keyboard, normpath
from qmk.keymap import locate_keymap
from qmk.path import normpath
def direct_pins(direct_pins):
def direct_pins(direct_pins, postfix):
"""Return the config.h lines that set the direct pins.
"""
rows = []
@@ -24,81 +24,60 @@ def direct_pins(direct_pins):
col_count = len(direct_pins[0])
row_count = len(direct_pins)
return """
#ifndef MATRIX_COLS
# define MATRIX_COLS %s
#endif // MATRIX_COLS
return f"""
#ifndef MATRIX_COLS{postfix}
# define MATRIX_COLS{postfix} {col_count}
#endif // MATRIX_COLS{postfix}
#ifndef MATRIX_ROWS
# define MATRIX_ROWS %s
#endif // MATRIX_ROWS
#ifndef MATRIX_ROWS{postfix}
# define MATRIX_ROWS{postfix} {row_count}
#endif // MATRIX_ROWS{postfix}
#ifndef DIRECT_PINS
# define DIRECT_PINS {%s}
#endif // DIRECT_PINS
""" % (col_count, row_count, ','.join(rows))
#ifndef DIRECT_PINS{postfix}
# define DIRECT_PINS{postfix} {{ {", ".join(rows)} }}
#endif // DIRECT_PINS{postfix}
"""
def pin_array(define, pins):
def pin_array(define, pins, postfix):
"""Return the config.h lines that set a pin array.
"""
pin_num = len(pins)
pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
return f"""
#ifndef {define}S
# define {define}S {pin_num}
#endif // {define}S
#ifndef {define}S{postfix}
# define {define}S{postfix} {pin_num}
#endif // {define}S{postfix}
#ifndef {define}_PINS
# define {define}_PINS {{ {pin_array} }}
#endif // {define}_PINS
#ifndef {define}_PINS{postfix}
# define {define}_PINS{postfix} {{ {pin_array} }}
#endif // {define}_PINS{postfix}
"""
def matrix_pins(matrix_pins):
def matrix_pins(matrix_pins, postfix=''):
"""Add the matrix config to the config.h.
"""
pins = []
if 'direct' in matrix_pins:
pins.append(direct_pins(matrix_pins['direct']))
pins.append(direct_pins(matrix_pins['direct'], postfix))
if 'cols' in matrix_pins:
pins.append(pin_array('MATRIX_COL', matrix_pins['cols']))
pins.append(pin_array('MATRIX_COL', matrix_pins['cols'], postfix))
if 'rows' in matrix_pins:
pins.append(pin_array('MATRIX_ROW', matrix_pins['rows']))
pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'], postfix))
return '\n'.join(pins)
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
@automagic_keyboard
@automagic_keymap
def generate_config_h(cli):
"""Generates the info_config.h file.
def generate_config_items(kb_info_json, config_h_lines):
"""Iterate through the info_config map to generate basic config values.
"""
# Determine our keyboard(s)
if not cli.config.generate_config_h.keyboard:
cli.log.error('Missing parameter: --keyboard')
cli.subcommands['info'].print_help()
return False
if not is_keyboard(cli.config.generate_config_h.keyboard):
cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
return False
# Build the info_config.h file.
kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
info_config_map = json_load(Path('data/mappings/info_config.json'))
config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
# Iterate through the info_config map to generate basic things
for config_key, info_dict in info_config_map.items():
info_key = info_dict['info_key']
key_type = info_dict.get('value_type', 'str')
@@ -135,9 +114,75 @@ def generate_config_h(cli):
config_h_lines.append(f'# define {config_key} {config_value}')
config_h_lines.append(f'#endif // {config_key}')
def generate_split_config(kb_info_json, config_h_lines):
"""Generate the config.h lines for split boards."""
if 'primary' in kb_info_json['split']:
if kb_info_json['split']['primary'] in ('left', 'right'):
config_h_lines.append('')
config_h_lines.append('#ifndef MASTER_LEFT')
config_h_lines.append('# ifndef MASTER_RIGHT')
if kb_info_json['split']['primary'] == 'left':
config_h_lines.append('# define MASTER_LEFT')
elif kb_info_json['split']['primary'] == 'right':
config_h_lines.append('# define MASTER_RIGHT')
config_h_lines.append('# endif // MASTER_RIGHT')
config_h_lines.append('#endif // MASTER_LEFT')
elif kb_info_json['split']['primary'] == 'pin':
config_h_lines.append('')
config_h_lines.append('#ifndef SPLIT_HAND_PIN')
config_h_lines.append('# define SPLIT_HAND_PIN')
config_h_lines.append('#endif // SPLIT_HAND_PIN')
elif kb_info_json['split']['primary'] == 'matrix_grid':
config_h_lines.append('')
config_h_lines.append('#ifndef SPLIT_HAND_MATRIX_GRID')
config_h_lines.append('# define SPLIT_HAND_MATRIX_GRID {%s}' % (','.join(kb_info_json["split"]["matrix_grid"],)))
config_h_lines.append('#endif // SPLIT_HAND_MATRIX_GRID')
elif kb_info_json['split']['primary'] == 'eeprom':
config_h_lines.append('')
config_h_lines.append('#ifndef EE_HANDS')
config_h_lines.append('# define EE_HANDS')
config_h_lines.append('#endif // EE_HANDS')
if 'protocol' in kb_info_json['split'].get('transport', {}):
if kb_info_json['split']['transport']['protocol'] == 'i2c':
config_h_lines.append('')
config_h_lines.append('#ifndef USE_I2C')
config_h_lines.append('# define USE_I2C')
config_h_lines.append('#endif // USE_I2C')
if 'right' in kb_info_json['split'].get('matrix_pins', {}):
config_h_lines.append(matrix_pins(kb_info_json['split']['matrix_pins']['right'], '_RIGHT'))
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate config.h for.')
@cli.argument('-km', '--keymap', arg_only=True, help='Keymap to generate config.h for.')
@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
def generate_config_h(cli):
"""Generates the info_config.h file.
"""
# Determine our keyboard/keymap
if cli.args.keymap:
km = locate_keymap(cli.args.keyboard, cli.args.keymap)
km_json = json_load(km)
validate(km_json, 'qmk.keymap.v1')
kb_info_json = dotty(km_json.get('config', {}))
else:
kb_info_json = dotty(info_json(cli.args.keyboard))
# Build the info_config.h file.
config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.', ' */', '', '#pragma once']
generate_config_items(kb_info_json, config_h_lines)
if 'matrix_pins' in kb_info_json:
config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
if 'split' in kb_info_json:
generate_split_config(kb_info_json, config_h_lines)
# Show the results
config_h = '\n'.join(config_h_lines)

View File

@@ -30,7 +30,7 @@ def generate_dfu_header(cli):
# Build the Keyboard.h file.
kb_info_json = dotty(info_json(cli.config.generate_dfu_header.keyboard))
keyboard_h_lines = ['/* This file was generated by `qmk generate-dfu-header`. Do not edit or copy.' ' */', '', '#pragma once']
keyboard_h_lines = ['/* This file was generated by `qmk generate-dfu-header`. Do not edit or copy.', ' */', '', '#pragma once']
keyboard_h_lines.append(f'#define MANUFACTURER {kb_info_json["manufacturer"]}')
keyboard_h_lines.append(f'#define PRODUCT {cli.config.generate_dfu_header.keyboard} Bootloader')

View File

@@ -4,15 +4,17 @@ Compile an info.json for a particular keyboard and pretty-print it.
"""
import json
from jsonschema import Draft7Validator, validators
from argcomplete.completers import FilesCompleter
from jsonschema import Draft7Validator, RefResolver, validators
from milc import cli
from pathlib import Path
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.json_encoders import InfoJSONEncoder
from qmk.json_schema import load_jsonschema
from qmk.json_schema import compile_schema_store
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import is_keyboard
from qmk.path import is_keyboard, normpath
def pruning_validator(validator_class):
@@ -34,15 +36,19 @@ def pruning_validator(validator_class):
def strip_info_json(kb_info_json):
"""Remove the API-only properties from the info.json.
"""
schema_store = compile_schema_store()
pruning_draft_7_validator = pruning_validator(Draft7Validator)
schema = load_jsonschema('keyboard')
validator = pruning_draft_7_validator(schema).validate
schema = schema_store['qmk.keyboard.v1']
resolver = RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
validator = pruning_draft_7_validator(schema, resolver=resolver).validate
return validator(kb_info_json)
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.')
@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
@cli.argument('-o', '--output', arg_only=True, completer=FilesCompleter, help='Write the output the specified file, overwriting if necessary.')
@cli.argument('-ow', '--overwrite', arg_only=True, action='store_true', help='Overwrite the existing info.json. (Overrides the location of --output)')
@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
@automagic_keyboard
@automagic_keymap
@@ -59,9 +65,29 @@ def generate_info_json(cli):
cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
return False
if cli.args.overwrite:
output_path = (Path('keyboards') / cli.config.generate_info_json.keyboard / 'info.json').resolve()
if cli.args.output:
cli.log.warning('Overwriting user supplied --output with %s', output_path)
cli.args.output = output_path
# Build the info.json file
kb_info_json = info_json(cli.config.generate_info_json.keyboard)
strip_info_json(kb_info_json)
info_json_text = json.dumps(kb_info_json, indent=4, cls=InfoJSONEncoder)
# Display the results
print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder))
if cli.args.output:
# Write to a file
output_path = normpath(cli.args.output)
if output_path.exists():
cli.log.warning('Overwriting output file %s', output_path)
output_path.write_text(info_json_text + '\n')
cli.log.info('Wrote info.json to %s.', output_path)
else:
# Display the results
print(info_json_text)

View File

@@ -2,7 +2,6 @@
"""
from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import normpath
@@ -29,17 +28,15 @@ def would_populate_layout_h(keyboard):
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.h for.')
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.h for.')
@cli.subcommand('Used by the make system to generate keyboard.h from info.json', hidden=True)
@automagic_keyboard
@automagic_keymap
def generate_keyboard_h(cli):
"""Generates the keyboard.h file.
"""
has_layout_h = would_populate_layout_h(cli.config.generate_keyboard_h.keyboard)
has_layout_h = would_populate_layout_h(cli.args.keyboard)
# Build the layouts.h file.
keyboard_h_lines = ['/* This file was generated by `qmk generate-keyboard-h`. Do not edit or copy.' ' */', '', '#pragma once', '#include "quantum.h"']
keyboard_h_lines = ['/* This file was generated by `qmk generate-keyboard-h`. Do not edit or copy.', ' */', '', '#pragma once', '#include "quantum.h"']
if not has_layout_h:
keyboard_h_lines.append('#pragma error("<keyboard>.h is only optional for data driven keyboards - kb.h == bad times")')

View File

@@ -38,7 +38,7 @@ def generate_layouts(cli):
kb_info_json = info_json(cli.config.generate_layouts.keyboard)
# Build the layouts.h file.
layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.' ' */', '', '#pragma once']
layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.', ' */', '', '#pragma once']
if 'matrix_pins' in kb_info_json:
if 'direct' in kb_info_json['matrix_pins']:

View File

@@ -5,11 +5,11 @@ from pathlib import Path
from dotty_dict import dotty
from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.json_schema import json_load
from qmk.json_schema import json_load, validate
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import is_keyboard, normpath
from qmk.keymap import locate_keymap
from qmk.path import normpath
def process_mapping_rule(kb_info_json, rules_key, info_dict):
@@ -26,7 +26,7 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
except KeyError:
return None
if key_type == 'array':
if key_type in ['array', 'list']:
return f'{rules_key} ?= {" ".join(rules_value)}'
elif key_type == 'bool':
return f'{rules_key} ?= {"on" if rules_value else "off"}'
@@ -39,23 +39,21 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode")
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
@automagic_keyboard
@automagic_keymap
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate rules.mk for.')
@cli.argument('-km', '--keymap', arg_only=True, help='Keymap to generate rules.mk for.')
@cli.subcommand('Used by the make system to generate rules.mk from info.json', hidden=True)
def generate_rules_mk(cli):
"""Generates a rules.mk file from info.json.
"""
if not cli.config.generate_rules_mk.keyboard:
cli.log.error('Missing parameter: --keyboard')
cli.subcommands['info'].print_help()
return False
# Determine our keyboard/keymap
if cli.args.keymap:
km = locate_keymap(cli.args.keyboard, cli.args.keymap)
km_json = json_load(km)
validate(km_json, 'qmk.keymap.v1')
kb_info_json = dotty(km_json.get('config', {}))
else:
kb_info_json = dotty(info_json(cli.args.keyboard))
if not is_keyboard(cli.config.generate_rules_mk.keyboard):
cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard)
return False
kb_info_json = dotty(info_json(cli.config.generate_rules_mk.keyboard))
info_rules_map = json_load(Path('data/mappings/info_rules.json'))
rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
@@ -76,6 +74,17 @@ def generate_rules_mk(cli):
enabled = 'yes' if enabled else 'no'
rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
# Set SPLIT_TRANSPORT, if needed
if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom':
rules_mk_lines.append('SPLIT_TRANSPORT ?= custom')
# Set CUSTOM_MATRIX, if needed
if kb_info_json.get('matrix_pins', {}).get('custom'):
if kb_info_json.get('matrix_pins', {}).get('custom_lite'):
rules_mk_lines.append('CUSTOM_MATRIX ?= lite')
else:
rules_mk_lines.append('CUSTOM_MATRIX ?= yes')
# Show the results
rules_mk = '\n'.join(rules_mk_lines) + '\n'

View File

@@ -0,0 +1,28 @@
"""Used by the make system to generate version.h for use in code.
"""
from milc import cli
from qmk.commands import create_version_h
from qmk.path import normpath
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('--skip-git', arg_only=True, action='store_true', help='Skip Git operations')
@cli.argument('--skip-all', arg_only=True, action='store_true', help='Use placeholder values for all defines (implies --skip-git)')
@cli.subcommand('Used by the make system to generate version.h for use in code', hidden=True)
def generate_version_h(cli):
"""Generates the version.h file.
"""
if cli.args.skip_all:
cli.args.skip_git = True
version_h = create_version_h(cli.args.skip_git, cli.args.skip_all)
if cli.args.output:
cli.args.output.write_text(version_h)
if not cli.args.quiet:
cli.log.info('Wrote version.h to %s.', cli.args.output)
else:
print(version_h)

View File

@@ -24,19 +24,15 @@ def show_keymap(kb_info_json, title_caps=True):
keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
if keymap_path and keymap_path.suffix == '.json':
if title_caps:
cli.echo('{fg_blue}Keymap "%s"{fg_reset}:', cli.config.info.keymap)
else:
cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
keymap_data = json.load(keymap_path.open(encoding='utf-8'))
layout_name = keymap_data['layout']
layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names
for layer_num, layer in enumerate(keymap_data['layers']):
if title_caps:
cli.echo('{fg_cyan}Layer %s{fg_reset}:', layer_num)
cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num)
else:
cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num)
cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num)
print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer))
@@ -45,7 +41,7 @@ def show_layouts(kb_info_json, title_caps=True):
"""Render the layouts with info.json labels.
"""
for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items():
title = layout_name.title() if title_caps else layout_name
title = f'Layout {layout_name.title()}' if title_caps else f'layouts.{layout_name}'
cli.echo('{fg_cyan}%s{fg_reset}:', title)
print(layout_art) # Avoid passing dirty data to cli.echo()
@@ -87,23 +83,12 @@ def print_friendly_output(kb_info_json):
cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])
cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))
cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
if 'width' in kb_info_json and 'height' in kb_info_json:
cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
if 'layout_aliases' in kb_info_json:
aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]
cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
if cli.config.info.layouts:
show_layouts(kb_info_json, True)
if cli.config.info.matrix:
show_matrix(kb_info_json, True)
if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
show_keymap(kb_info_json, True)
def print_text_output(kb_info_json):
"""Print the info.json in a plain text format.
@@ -124,6 +109,24 @@ def print_text_output(kb_info_json):
show_keymap(kb_info_json, False)
def print_dotted_output(kb_info_json, prefix=''):
"""Print the info.json in a plain text format with dot-joined keys.
"""
for key in sorted(kb_info_json):
new_prefix = f'{prefix}.{key}' if prefix else key
if key in ['parse_errors', 'parse_warnings']:
continue
elif key == 'layouts' and prefix == '':
cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
elif isinstance(kb_info_json[key], dict):
print_dotted_output(kb_info_json[key], new_prefix)
elif isinstance(kb_info_json[key], list):
cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, ', '.join(map(str, sorted(kb_info_json[key]))))
else:
cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, kb_info_json[key])
def print_parsed_rules_mk(keyboard_name):
rules = rules_mk(keyboard_name)
for k in sorted(rules.keys()):
@@ -164,10 +167,22 @@ def info(cli):
# Output in the requested format
if cli.args.format == 'json':
print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
return True
elif cli.args.format == 'text':
print_text_output(kb_info_json)
print_dotted_output(kb_info_json)
title_caps = False
elif cli.args.format == 'friendly':
print_friendly_output(kb_info_json)
title_caps = True
else:
cli.log.error('Unknown format: %s', cli.args.format)
return False
if cli.config.info.layouts:
show_layouts(kb_info_json, title_caps)
if cli.config.info.matrix:
show_matrix(kb_info_json, title_caps)
if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
show_keymap(kb_info_json, title_caps)

View File

@@ -44,8 +44,6 @@ def kle2json(cli):
'keyboard_name': kle.name,
'url': '',
'maintainer': 'qmk',
'width': kle.columns,
'height': kle.rows,
'layouts': {
'LAYOUT': {
'layout': kle2qmk(kle)

View File

@@ -4,7 +4,7 @@ from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.keyboard import keyboard_completer
from qmk.keyboard import find_readme, keyboard_completer
from qmk.keymap import locate_keymap
from qmk.path import is_keyboard, keyboard
@@ -31,7 +31,8 @@ def lint(cli):
ok = True
keyboard_path = keyboard(cli.config.lint.keyboard)
keyboard_info = info_json(cli.config.lint.keyboard)
readme_path = keyboard_path / 'readme.md'
readme_path = find_readme(cli.config.lint.keyboard)
missing_readme_path = keyboard_path / 'readme.md'
# Check for errors in the info.json
if keyboard_info['parse_errors']:
@@ -43,9 +44,9 @@ def lint(cli):
cli.log.error('Warnings found when generating info.json (Strict mode enabled.)')
# Check for a readme.md and warn if it doesn't exist
if not readme_path.exists():
if not readme_path:
ok = False
cli.log.error('Missing %s', readme_path)
cli.log.error('Missing %s', missing_readme_path)
# Keymap specific checks
if cli.config.lint.keymap:

View File

@@ -0,0 +1,18 @@
"""List the keymaps for a specific keyboard
"""
from milc import cli
from qmk.decorators import automagic_keyboard
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.info import info_json
@cli.argument("-kb", "--keyboard", type=keyboard_folder, completer=keyboard_completer, help="Specify keyboard name. Example: monarch")
@cli.subcommand("List the layouts for a specific keyboard")
@automagic_keyboard
def list_layouts(cli):
"""List the layouts for a specific keyboard
"""
info_data = info_json(cli.config.list_layouts.keyboard)
for name in sorted(info_data.get('community_layouts', [])):
print(name)

View File

@@ -10,7 +10,7 @@ from subprocess import DEVNULL
from milc import cli
from qmk.constants import QMK_FIRMWARE
from qmk.commands import _find_make
from qmk.commands import _find_make, get_make_parallel_args
import qmk.keyboard
import qmk.keymap
@@ -28,7 +28,7 @@ def _is_split(keyboard_name):
return True if 'SPLIT_KEYBOARD' in rules_mk and rules_mk['SPLIT_KEYBOARD'].lower() == 'yes' else False
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
@cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on the supplied value in rules.mk. Supported format is 'SPLIT_KEYBOARD=yes'. May be passed multiple times.")
@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.")
@@ -80,7 +80,7 @@ all: {keyboard_safe}_binary
)
# yapf: enable
cli.run([make_cmd, '-j', str(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
cli.run([make_cmd, *get_make_parallel_args(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
# Check for failures
failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')]

View File

@@ -1,11 +1,142 @@
"""This script automates the creation of keyboards.
"""This script automates the creation of new keyboard directories using a starter template.
"""
from datetime import date
import fileinput
from pathlib import Path
import re
import shutil
from qmk.commands import git_get_username
import qmk.path
from milc import cli
from milc.questions import choice, question
KEYBOARD_TYPES = ['avr', 'ps2avrgb']
@cli.subcommand('Creates a new keyboard')
def new_keyboard(cli):
"""Creates a new keyboard
def keyboard_name(name):
"""Callable for argparse validation.
"""
# TODO: replace this bodge to the existing script
cli.run(['util/new_keyboard.sh'], stdin=None, capture_output=False)
if not validate_keyboard_name(name):
raise ValueError
return name
def validate_keyboard_name(name):
"""Returns True if the given keyboard name contains only lowercase a-z, 0-9 and underscore characters.
"""
regex = re.compile(r'^[a-z0-9][a-z0-9/_]+$')
return bool(regex.match(name))
@cli.argument('-kb', '--keyboard', help='Specify the name for the new keyboard directory', arg_only=True, type=keyboard_name)
@cli.argument('-t', '--type', help='Specify the keyboard type', arg_only=True, choices=KEYBOARD_TYPES)
@cli.argument('-u', '--username', help='Specify your username (default from Git config)', arg_only=True)
@cli.subcommand('Creates a new keyboard directory')
def new_keyboard(cli):
"""Creates a new keyboard.
"""
cli.log.info('{style_bright}Generating a new QMK keyboard directory{style_normal}')
cli.echo('')
# Get keyboard name
new_keyboard_name = None
while not new_keyboard_name:
new_keyboard_name = cli.args.keyboard if cli.args.keyboard else question('Keyboard Name:')
if not validate_keyboard_name(new_keyboard_name):
cli.log.error('Keyboard names must contain only {fg_cyan}lowercase a-z{fg_reset}, {fg_cyan}0-9{fg_reset}, and {fg_cyan}_{fg_reset}! Please choose a different name.')
# Exit if passed by arg
if cli.args.keyboard:
return False
new_keyboard_name = None
continue
keyboard_path = qmk.path.keyboard(new_keyboard_name)
if keyboard_path.exists():
cli.log.error(f'Keyboard {{fg_cyan}}{new_keyboard_name}{{fg_reset}} already exists! Please choose a different name.')
# Exit if passed by arg
if cli.args.keyboard:
return False
new_keyboard_name = None
# Get keyboard type
keyboard_type = cli.args.type if cli.args.type else choice('Keyboard Type:', KEYBOARD_TYPES, default=0)
# Get username
user_name = None
while not user_name:
user_name = question('Your Name:', default=find_user_name())
if not user_name:
cli.log.error('You didn\'t provide a username, and we couldn\'t find one set in your QMK or Git configs. Please try again.')
# Exit if passed by arg
if cli.args.username:
return False
# Copy all the files
copy_templates(keyboard_type, keyboard_path)
# Replace all the placeholders
keyboard_basename = keyboard_path.name
replacements = [
('%YEAR%', str(date.today().year)),
('%KEYBOARD%', keyboard_basename),
('%YOUR_NAME%', user_name),
]
filenames = [
keyboard_path / 'config.h',
keyboard_path / 'info.json',
keyboard_path / 'readme.md',
keyboard_path / f'{keyboard_basename}.c',
keyboard_path / f'{keyboard_basename}.h',
keyboard_path / 'keymaps/default/readme.md',
keyboard_path / 'keymaps/default/keymap.c',
]
replace_placeholders(replacements, filenames)
cli.echo('')
cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{new_keyboard_name}{{fg_green}}.{{fg_reset}}')
cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}{keyboard_path}{{fg_reset}},')
cli.log.info('or open the directory in your preferred text editor.')
def find_user_name():
if cli.args.username:
return cli.args.username
elif cli.config.user.name:
return cli.config.user.name
else:
return git_get_username()
def copy_templates(keyboard_type, keyboard_path):
"""Copies the template files from data/templates to the new keyboard directory.
"""
template_base_path = Path('data/templates')
keyboard_basename = keyboard_path.name
cli.log.info('Copying base template files...')
shutil.copytree(template_base_path / 'base', keyboard_path)
cli.log.info(f'Copying {{fg_cyan}}{keyboard_type}{{fg_reset}} template files...')
shutil.copytree(template_base_path / keyboard_type, keyboard_path, dirs_exist_ok=True)
cli.log.info(f'Renaming {{fg_cyan}}keyboard.[ch]{{fg_reset}} to {{fg_cyan}}{keyboard_basename}.[ch]{{fg_reset}}...')
shutil.move(keyboard_path / 'keyboard.c', keyboard_path / f'{keyboard_basename}.c')
shutil.move(keyboard_path / 'keyboard.h', keyboard_path / f'{keyboard_basename}.h')
def replace_placeholders(replacements, filenames):
"""Replaces the given placeholders in each template file.
"""
for replacement in replacements:
cli.log.info(f'Replacing {{fg_cyan}}{replacement[0]}{{fg_reset}} with {{fg_cyan}}{replacement[1]}{{fg_reset}}...')
with fileinput.input(files=filenames, inplace=True) as file:
for line in file:
print(line.replace(replacement[0], replacement[1]), end='')

View File

@@ -1,26 +1,24 @@
"""Format python code according to QMK's style.
"""Point people to the new command name.
"""
from subprocess import CalledProcessError, DEVNULL
import sys
from pathlib import Path
from milc import cli
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True)
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
@cli.subcommand('Pointer to the new command name: qmk format-python.', hidden=False if cli.config.user.developer else True)
def pyformat(cli):
"""Format python code according to QMK's style.
"""Pointer to the new command name: qmk format-python.
"""
edit = '--diff' if cli.args.dry_run else '--in-place'
yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python']
try:
cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.')
return True
cli.log.warning('"qmk pyformat" has been renamed to "qmk format-python". Please use the new command in the future.')
argv = [sys.executable, *sys.argv]
argv[argv.index('pyformat')] = 'format-python'
script_path = Path(argv[1])
script_path_exe = Path(f'{argv[1]}.exe')
except CalledProcessError:
if cli.args.dry_run:
cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!')
else:
cli.log.error('Error formatting python code!')
if not script_path.exists() and script_path_exe.exists():
# For reasons I don't understand ".exe" is stripped from the script name on windows.
argv[1] = str(script_path_exe)
return False
return cli.run(argv, capture_output=False).returncode

View File

@@ -2,6 +2,7 @@
"""
import json
import os
import sys
import shutil
from pathlib import Path
from subprocess import DEVNULL
@@ -10,7 +11,7 @@ from time import strftime
from milc import cli
import qmk.keymap
from qmk.constants import KEYBOARD_OUTPUT_PREFIX
from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX
from qmk.json_schema import json_load
time_fmt = '%Y-%m-%d-%H:%M:%S'
@@ -51,7 +52,7 @@ def create_make_target(target, parallel=1, **env_vars):
for key, value in env_vars.items():
env.append(f'{key}={value}')
return [make_cmd, '-j', str(parallel), *env, target]
return [make_cmd, *get_make_parallel_args(parallel), *env, target]
def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
@@ -86,11 +87,17 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
return create_make_target(':'.join(make_args), parallel, **env_vars)
def get_git_version(repo_dir='.', check_dir='.'):
def get_git_version(current_time, repo_dir='.', check_dir='.'):
"""Returns the current git version for a repo, or the current time.
"""
git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
if repo_dir != '.':
repo_dir = Path('lib') / repo_dir
if check_dir != '.':
check_dir = repo_dir / check_dir
if Path(check_dir).exists():
git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
@@ -100,23 +107,58 @@ def get_git_version(repo_dir='.', check_dir='.'):
else:
cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')
print(git_describe.stderr)
return strftime(time_fmt)
return current_time
return strftime(time_fmt)
return current_time
def write_version_h(git_version, build_date, chibios_version, chibios_contrib_version):
"""Generate and write quantum/version.h
def get_make_parallel_args(parallel=1):
"""Returns the arguments for running the specified number of parallel jobs.
"""
version_h = [
f'#define QMK_VERSION "{git_version}"',
f'#define QMK_BUILDDATE "{build_date}"',
f'#define CHIBIOS_VERSION "{chibios_version}"',
f'#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"',
]
parallel_args = []
version_h_file = Path('quantum/version.h')
version_h_file.write_text('\n'.join(version_h))
if int(parallel) <= 0:
# 0 or -1 means -j without argument (unlimited jobs)
parallel_args.append('--jobs')
else:
parallel_args.append('--jobs=' + str(parallel))
if int(parallel) != 1:
# If more than 1 job is used, synchronize parallel output by target
parallel_args.append('--output-sync=target')
return parallel_args
def create_version_h(skip_git=False, skip_all=False):
"""Generate version.h contents
"""
if skip_all:
current_time = "1970-01-01-00:00:00"
else:
current_time = strftime(time_fmt)
if skip_git:
git_version = "NA"
chibios_version = "NA"
chibios_contrib_version = "NA"
else:
git_version = get_git_version(current_time)
chibios_version = get_git_version(current_time, "chibios", "os")
chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os")
version_h_lines = f"""/* This file was automatically generated. Do not edit or copy.
*/
#pragma once
#define QMK_VERSION "{git_version}"
#define QMK_BUILDDATE "{current_time}"
#define CHIBIOS_VERSION "{chibios_version}"
#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"
"""
return version_h_lines
def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars):
@@ -149,13 +191,8 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
keymap_dir.mkdir(exist_ok=True, parents=True)
keymap_c.write_text(c_text)
# Write the version.h file
git_version = get_git_version()
build_date = strftime('%Y-%m-%d-%H:%M:%S')
chibios_version = get_git_version("lib/chibios", "lib/chibios/os")
chibios_contrib_version = get_git_version("lib/chibios-contrib", "lib/chibios-contrib/os")
write_version_h(git_version, build_date, chibios_version, chibios_contrib_version)
version_h = Path('quantum/version.h')
version_h.write_text(create_version_h())
# Return a command that can be run to make the keymap and flash if given
verbose = 'true' if cli.config.general.verbose else 'false'
@@ -166,8 +203,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
make_command.append('-s')
make_command.extend([
'-j',
str(parallel),
*get_make_parallel_args(parallel),
'-r',
'-R',
'-f',
@@ -181,10 +217,6 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
make_command.append(f'{key}={value}')
make_command.extend([
f'GIT_VERSION={git_version}',
f'BUILD_DATE={build_date}',
f'CHIBIOS_VERSION={chibios_version}',
f'CHIBIOS_CONTRIB_VERSION={chibios_contrib_version}',
f'KEYBOARD={user_keymap["keyboard"]}',
f'KEYMAP={user_keymap["keymap"]}',
f'KEYBOARD_FILESAFE={keyboard_filesafe}',
@@ -223,3 +255,80 @@ def parse_configurator_json(configurator_file):
user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']]
return user_keymap
def git_get_username():
"""Retrieves user's username from Git config, if set.
"""
git_username = cli.run(['git', 'config', '--get', 'user.name'])
if git_username.returncode == 0 and git_username.stdout:
return git_username.stdout.strip()
def git_check_repo():
"""Checks that the .git directory exists inside QMK_HOME.
This is a decent enough indicator that the qmk_firmware directory is a
proper Git repository, rather than a .zip download from GitHub.
"""
dot_git_dir = QMK_FIRMWARE / '.git'
return dot_git_dir.is_dir()
def git_get_branch():
"""Returns the current branch for a repo, or None.
"""
git_branch = cli.run(['git', 'branch', '--show-current'])
if not git_branch.returncode != 0 or not git_branch.stdout:
# Workaround for Git pre-2.22
git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
if git_branch.returncode == 0:
return git_branch.stdout.strip()
def git_is_dirty():
"""Returns 1 if repo is dirty, or 0 if clean
"""
git_diff_staged_cmd = ['git', 'diff', '--quiet']
git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached']
unstaged = cli.run(git_diff_staged_cmd)
staged = cli.run(git_diff_unstaged_cmd)
return unstaged.returncode != 0 or staged.returncode != 0
def git_get_remotes():
"""Returns the current remotes for a repo.
"""
remotes = {}
git_remote_show_cmd = ['git', 'remote', 'show']
git_remote_get_cmd = ['git', 'remote', 'get-url']
git_remote_show = cli.run(git_remote_show_cmd)
if git_remote_show.returncode == 0:
for name in git_remote_show.stdout.splitlines():
git_remote_name = cli.run([*git_remote_get_cmd, name])
remotes[name.strip()] = {"url": git_remote_name.stdout.strip()}
return remotes
def git_check_deviation(active_branch):
"""Return True if branch has custom commits
"""
cli.run(['git', 'fetch', 'upstream', active_branch])
deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}'])
return bool(deviations.returncode)
def in_virtualenv():
"""Check if running inside a virtualenv.
Based on https://stackoverflow.com/a/1883251
"""
active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
return active_prefix != sys.prefix

View File

@@ -6,11 +6,14 @@ from pathlib import Path
# The root of the qmk_firmware tree.
QMK_FIRMWARE = Path.cwd()
# Upstream repo url
QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware'
# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system.
MAX_KEYBOARD_SUBFOLDERS = 5
# Supported processor types
CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L433', 'STM32L443'
CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443'
LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'

View File

@@ -9,7 +9,7 @@ from milc import cli
from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
from qmk.c_parse import find_layouts
from qmk.json_schema import deep_update, json_load, keyboard_validate, keyboard_api_validate
from qmk.json_schema import deep_update, json_load, validate
from qmk.keyboard import config_h, rules_mk
from qmk.keymap import list_keymaps
from qmk.makefile import parse_rules_mk_file
@@ -29,10 +29,10 @@ def info_json(keyboard):
"""Generate the info.json data for a specific keyboard.
"""
cur_dir = Path('keyboards')
rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
if 'DEFAULT_FOLDER' in rules:
keyboard = rules['DEFAULT_FOLDER']
rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk', rules)
root_rules_mk = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
if 'DEFAULT_FOLDER' in root_rules_mk:
keyboard = root_rules_mk['DEFAULT_FOLDER']
info_data = {
'keyboard_name': str(keyboard),
@@ -49,7 +49,7 @@ def info_json(keyboard):
info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
# Populate layout data
layouts, aliases = _find_all_layouts(info_data, keyboard)
layouts, aliases = _search_keyboard_h(keyboard)
if aliases:
info_data['layout_aliases'] = aliases
@@ -61,19 +61,25 @@ def info_json(keyboard):
# Merge in the data from info.json, config.h, and rules.mk
info_data = merge_info_jsons(keyboard, info_data)
info_data = _extract_config_h(info_data)
info_data = _extract_rules_mk(info_data)
info_data = _extract_config_h(info_data)
# Ensure that we have matrix row and column counts
info_data = _matrix_size(info_data)
# Validate against the jsonschema
try:
keyboard_api_validate(info_data)
validate(info_data, 'qmk.api.keyboard.v1')
except jsonschema.ValidationError as e:
json_path = '.'.join([str(p) for p in e.absolute_path])
cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
exit()
exit(1)
# Make sure we have at least one layout
if not info_data.get('layouts'):
_find_missing_layouts(info_data, keyboard)
if not info_data.get('layouts'):
_log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
@@ -90,6 +96,9 @@ def info_json(keyboard):
if layout_name not in info_data.get('layouts', {}) and layout_name not in info_data.get('layout_aliases', {}):
_log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))
# Check that the reported matrix size is consistent with the actual matrix size
_check_matrix(info_data)
return info_data
@@ -143,10 +152,7 @@ def _pin_name(pin):
elif pin == 'NO_PIN':
return None
elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit():
return pin
raise ValueError(f'Invalid pin: {pin}')
return pin
def _extract_pins(pins):
@@ -155,10 +161,9 @@ def _extract_pins(pins):
return [_pin_name(pin) for pin in pins.split(',')]
def _extract_direct_matrix(info_data, direct_pins):
def _extract_direct_matrix(direct_pins):
"""
"""
info_data['matrix_pins'] = {}
direct_pin_array = []
while direct_pins[-1] != '}':
@@ -182,12 +187,157 @@ def _extract_direct_matrix(info_data, 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_split_main(info_data, config_c):
"""Populate data about the split configuration
"""
# Figure out how the main half is determined
if config_c.get('SPLIT_HAND_PIN') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_PIN) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'pin'
if config_c.get('SPLIT_HAND_MATRIX_GRID'):
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_MATRIX_GRID) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'matrix_grid'
info_data['split']['matrix_grid'] = _extract_pins(config_c['SPLIT_HAND_MATRIX_GRID'])
if config_c.get('EE_HANDS') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (EE_HANDS) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'eeprom'
if config_c.get('MASTER_RIGHT') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_RIGHT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'right'
if config_c.get('MASTER_LEFT') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_LEFT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'left'
def _extract_split_transport(info_data, config_c):
# Figure out the transport method
if config_c.get('USE_I2C') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'transport' not in info_data['split']:
info_data['split']['transport'] = {}
if 'protocol' in info_data['split']['transport']:
_log_warning(info_data, 'Split transport is specified in both config.h (USE_I2C) and info.json (split.transport.protocol) (Value: %s), the config.h value wins.' % info_data['split']['transport'])
info_data['split']['transport']['protocol'] = 'i2c'
elif 'protocol' not in info_data.get('split', {}).get('transport', {}):
if 'split' not in info_data:
info_data['split'] = {}
if 'transport' not in info_data['split']:
info_data['split']['transport'] = {}
info_data['split']['transport']['protocol'] = 'serial'
def _extract_split_right_pins(info_data, config_c):
# Figure out the right half matrix pins
row_pins = config_c.get('MATRIX_ROW_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
col_pins = config_c.get('MATRIX_COL_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
unused_pin_text = config_c.get('UNUSED_PINS_RIGHT')
unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None
direct_pins = config_c.get('DIRECT_PINS_RIGHT', '').replace(' ', '')[1:-1]
if row_pins and col_pins:
if info_data.get('split', {}).get('matrix_pins', {}).get('right') in info_data:
_log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
if 'split' not in info_data:
info_data['split'] = {}
if 'matrix_pins' not in info_data['split']:
info_data['split']['matrix_pins'] = {}
if 'right' not in info_data['split']['matrix_pins']:
info_data['split']['matrix_pins']['right'] = {}
info_data['split']['matrix_pins']['right'] = {
'cols': _extract_pins(col_pins),
'rows': _extract_pins(row_pins),
}
if direct_pins:
if info_data.get('split', {}).get('matrix_pins', {}).get('right', {}):
_log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
if 'split' not in info_data:
info_data['split'] = {}
if 'matrix_pins' not in info_data['split']:
info_data['split']['matrix_pins'] = {}
if 'right' not in info_data['split']['matrix_pins']:
info_data['split']['matrix_pins']['right'] = {}
info_data['split']['matrix_pins']['right']['direct'] = _extract_direct_matrix(direct_pins)
if unused_pins:
if 'split' not in info_data:
info_data['split'] = {}
if 'matrix_pins' not in info_data['split']:
info_data['split']['matrix_pins'] = {}
if 'right' not in info_data['split']['matrix_pins']:
info_data['split']['matrix_pins']['right'] = {}
info_data['split']['matrix_pins']['right']['unused'] = _extract_pins(unused_pins)
def _extract_matrix_info(info_data, config_c):
"""Populate the matrix information.
"""
row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
unused_pin_text = config_c.get('UNUSED_PINS')
unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None
direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
info_snippet = {}
if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
if 'matrix_size' in info_data:
@@ -199,19 +349,35 @@ def _extract_matrix_info(info_data, config_c):
}
if row_pins and col_pins:
if 'matrix_pins' in info_data:
if 'matrix_pins' in info_data and 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
_log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
info_data['matrix_pins'] = {
'cols': _extract_pins(col_pins),
'rows': _extract_pins(row_pins),
}
info_snippet['cols'] = _extract_pins(col_pins)
info_snippet['rows'] = _extract_pins(row_pins)
if direct_pins:
if 'matrix_pins' in info_data:
if 'matrix_pins' in info_data and 'direct' in info_data['matrix_pins']:
_log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
info_snippet['direct'] = _extract_direct_matrix(direct_pins)
if unused_pins:
if 'matrix_pins' not in info_data:
info_data['matrix_pins'] = {}
info_snippet['unused'] = _extract_pins(unused_pins)
if config_c.get('CUSTOM_MATRIX', 'no') != 'no':
if 'matrix_pins' in info_data and 'custom' in info_data['matrix_pins']:
_log_warning(info_data, 'Custom Matrix is specified in both info.json and config.h, the config.h values win.')
info_snippet['custom'] = True
if config_c['CUSTOM_MATRIX'] == 'lite':
info_snippet['custom_lite'] = True
if info_snippet:
info_data['matrix_pins'] = info_snippet
return info_data
@@ -269,6 +435,10 @@ def _extract_config_h(info_data):
# Pull data that easily can't be mapped in json
_extract_matrix_info(info_data, config_c)
_extract_audio(info_data, config_c)
_extract_split_main(info_data, config_c)
_extract_split_transport(info_data, config_c)
_extract_split_right_pins(info_data, config_c)
return info_data
@@ -341,45 +511,53 @@ def _extract_rules_mk(info_data):
return info_data
def _merge_layouts(info_data, new_info_data):
"""Merge new_info_data into info_data in an intelligent way.
def _matrix_size(info_data):
"""Add info_data['matrix_size'] if it doesn't exist.
"""
for layout_name, layout_json in new_info_data['layouts'].items():
if layout_name in info_data['layouts']:
# Pull in layouts we have a macro for
if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
_log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
else:
for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
key.update(layout_json['layout'][i])
else:
# Pull in layouts that have matrix data
missing_matrix = False
for key in layout_json.get('layout', {}):
if 'matrix' not in key:
missing_matrix = True
if 'matrix_size' not in info_data and 'matrix_pins' in info_data:
info_data['matrix_size'] = {}
if not missing_matrix:
if layout_name in info_data['layouts']:
# Update an existing layout with new data
for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
key.update(layout_json['layout'][i])
else:
# Copy in the new layout wholesale
layout_json['c_macro'] = False
info_data['layouts'][layout_name] = layout_json
if 'direct' in info_data['matrix_pins']:
info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0])
info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct'])
elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols'])
info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows'])
return info_data
def _search_keyboard_h(path):
def _check_matrix(info_data):
"""Check the matrix to ensure that row/column count is consistent.
"""
if 'matrix_pins' in info_data and 'matrix_size' in info_data:
actual_col_count = info_data['matrix_size'].get('cols', 0)
actual_row_count = info_data['matrix_size'].get('rows', 0)
col_count = row_count = 0
if 'direct' in info_data['matrix_pins']:
col_count = len(info_data['matrix_pins']['direct'][0])
row_count = len(info_data['matrix_pins']['direct'])
elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
col_count = len(info_data['matrix_pins']['cols'])
row_count = len(info_data['matrix_pins']['rows'])
if col_count != actual_col_count and col_count != (actual_col_count / 2):
# FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check.
_log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}')
if row_count != actual_row_count and row_count != (actual_row_count / 2):
# FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check.
_log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}')
def _search_keyboard_h(keyboard):
keyboard = Path(keyboard)
current_path = Path('keyboards/')
aliases = {}
layouts = {}
for directory in path.parts:
for directory in keyboard.parts:
current_path = current_path / directory
keyboard_h = '%s.h' % (directory,)
keyboard_h_path = current_path / keyboard_h
@@ -394,27 +572,28 @@ def _search_keyboard_h(path):
return layouts, aliases
def _find_all_layouts(info_data, keyboard):
"""Looks for layout macros associated with this keyboard.
def _find_missing_layouts(info_data, keyboard):
"""Looks for layout macros when they aren't found other places.
If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
"""
layouts, aliases = _search_keyboard_h(Path(keyboard))
_log_warning(info_data, '%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
if not layouts:
# If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
for file in glob('keyboards/%s/*.h' % keyboard):
these_layouts, these_aliases = find_layouts(file)
for file in glob('keyboards/%s/*.h' % keyboard):
if file.endswith('.h'):
these_layouts, these_aliases = find_layouts(file)
if these_layouts:
for layout_name, layout_json in these_layouts.items():
if not layout_name.startswith('LAYOUT_kc'):
layout_json['c_macro'] = True
info_data['layouts'][layout_name] = layout_json
if these_layouts:
layouts.update(these_layouts)
for alias, alias_text in these_aliases.items():
if alias_text in these_layouts:
if 'layout_aliases' not in info_data:
info_data['layout_aliases'] = {}
for alias, alias_text in these_aliases.items():
if alias_text in layouts:
aliases[alias] = alias_text
return layouts, aliases
info_data['layout_aliases'][alias] = alias_text
def _log_error(info_data, message):
@@ -493,7 +672,7 @@ def merge_info_jsons(keyboard, info_data):
continue
try:
keyboard_validate(new_info_data)
validate(new_info_data, 'qmk.keyboard.v1')
except jsonschema.ValidationError as e:
json_path = '.'.join([str(p) for p in e.absolute_path])
cli.log.error('Not including data from file: %s', info_file)
@@ -511,8 +690,12 @@ def merge_info_jsons(keyboard, info_data):
layout_name = info_data['layout_aliases'][layout_name]
if layout_name in info_data['layouts']:
for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
existing_key.update(new_key)
if len(info_data['layouts'][layout_name]['layout']) != len(layout['layout']):
msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
_log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
else:
for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
existing_key.update(new_key)
else:
layout['c_macro'] = False
info_data['layouts'][layout_name] = layout

View File

@@ -102,9 +102,6 @@ class InfoJSONEncoder(QMKJSONEncoder):
elif key == 'maintainer':
return '12maintainer'
elif key in ('height', 'width'):
return '40' + str(key)
elif key == 'community_layouts':
return '97community_layouts'

View File

@@ -2,6 +2,7 @@
"""
import json
from collections.abc import Mapping
from functools import lru_cache
from pathlib import Path
import hjson
@@ -17,16 +18,21 @@ def json_load(json_file):
try:
return hjson.load(json_file.open(encoding='utf-8'))
except json.decoder.JSONDecodeError as e:
except (json.decoder.JSONDecodeError, hjson.HjsonDecodeError) as e:
cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
exit(1)
except Exception as e:
cli.log.error('Unknown error attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
exit(1)
@lru_cache(maxsize=0)
def load_jsonschema(schema_name):
"""Read a jsonschema file from disk.
FIXME(skullydazed/anyone): Refactor to make this a public function.
"""
if Path(schema_name).exists():
return json_load(schema_name)
schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
if not schema_path.exists():
@@ -35,28 +41,42 @@ def load_jsonschema(schema_name):
return json_load(schema_path)
def keyboard_validate(data):
"""Validates data against the keyboard jsonschema.
@lru_cache(maxsize=0)
def compile_schema_store():
"""Compile all our schemas into a schema store.
"""
schema = load_jsonschema('keyboard')
validator = jsonschema.Draft7Validator(schema).validate
schema_store = {}
return validator(data)
for schema_file in Path('data/schemas').glob('*.jsonschema'):
schema_data = load_jsonschema(schema_file)
if not isinstance(schema_data, dict):
cli.log.debug('Skipping schema file %s', schema_file)
continue
schema_store[schema_data['$id']] = schema_data
return schema_store
def keyboard_api_validate(data):
"""Validates data against the api_keyboard jsonschema.
@lru_cache(maxsize=0)
def create_validator(schema):
"""Creates a validator for the given schema id.
"""
base = load_jsonschema('keyboard')
relative = load_jsonschema('api_keyboard')
resolver = jsonschema.RefResolver.from_schema(base)
validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
schema_store = compile_schema_store()
resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate
def validate(data, schema):
"""Validates data against a schema.
"""
validator = create_validator(schema)
return validator(data)
def deep_update(origdict, newdict):
"""Update a dictionary in place, recursing to do a deep copy.
"""Update a dictionary in place, recursing to do a depth-first deep copy.
"""
for key, value in newdict.items():
if isinstance(value, Mapping):

View File

@@ -160,7 +160,7 @@ def rules_mk(keyboard):
def render_layout(layout_data, render_ascii, key_labels=None):
"""Renders a single layout.
"""
textpad = [array('u', ' ' * 200) for x in range(50)]
textpad = [array('u', ' ' * 200) for x in range(100)]
style = 'ascii' if render_ascii else 'unicode'
box_chars = BOX_DRAWING_CHARACTERS[style]

View File

@@ -149,8 +149,8 @@ def is_keymap_dir(keymap, c=True, json=True, additional_files=None):
for file in files:
if (keymap / file).is_file():
if additional_files:
for file in additional_files:
if not (keymap / file).is_file():
for additional_file in additional_files:
if not (keymap / additional_file).is_file():
return False
return True

View File

@@ -1,8 +1,6 @@
{
"keyboard_name": "tester",
"maintainer": "qmk",
"height": 5,
"width": 15,
"layouts": {
"LAYOUT": {
"layout": [

View File

@@ -31,13 +31,13 @@ def check_returncode(result, expected=[0]):
assert result.returncode in expected
def test_cformat():
result = check_subcommand('cformat', '-n', 'quantum/matrix.c')
def test_format_c():
result = check_subcommand('format-c', '-n', 'quantum/matrix.c')
check_returncode(result)
def test_cformat_all():
result = check_subcommand('cformat', '-n', '-a')
def test_format_c_all():
result = check_subcommand('format-c', '-n', '-a')
check_returncode(result, [0, 1])
@@ -80,8 +80,8 @@ def test_hello():
assert 'Hello,' in result.stdout
def test_pyformat():
result = check_subcommand('pyformat', '--dry-run')
def test_format_python():
result = check_subcommand('format-python', '--dry-run')
check_returncode(result)
assert 'Python code in `bin/qmk` and `lib/python` is correctly formatted.' in result.stdout
@@ -258,6 +258,12 @@ def test_generate_rules_mk():
assert 'MCU ?= atmega32u4' in result.stdout
def test_generate_version_h():
result = check_subcommand('generate-version-h')
check_returncode(result)
assert '#define QMK_VERSION' in result.stdout
def test_generate_layouts():
result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic')
check_returncode(result)
@@ -267,7 +273,7 @@ def test_generate_layouts():
def test_format_json_keyboard():
result = check_subcommand('format-json', '--format', 'keyboard', 'lib/python/qmk/tests/minimal_info.json')
check_returncode(result)
assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "height": 5,\n "width": 15,\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
def test_format_json_keymap():
@@ -279,7 +285,7 @@ def test_format_json_keymap():
def test_format_json_keyboard_auto():
result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_info.json')
check_returncode(result)
assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "height": 5,\n "width": 15,\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
def test_format_json_keymap_auto():