summaryrefslogtreecommitdiffstats
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/cli/__init__.py4
-rwxr-xr-x[-rw-r--r--]lib/python/qmk/cli/cformat.py139
-rwxr-xr-xlib/python/qmk/cli/compile.py2
-rw-r--r--lib/python/qmk/cli/console.py5
-rwxr-xr-xlib/python/qmk/cli/doctor/__init__.py5
-rw-r--r--lib/python/qmk/cli/doctor/check.py (renamed from lib/python/qmk/os_helpers/__init__.py)22
-rw-r--r--lib/python/qmk/cli/doctor/linux.py (renamed from lib/python/qmk/os_helpers/linux/__init__.py)44
-rw-r--r--lib/python/qmk/cli/doctor/macos.py13
-rwxr-xr-xlib/python/qmk/cli/doctor/main.py (renamed from lib/python/qmk/cli/doctor.py)76
-rw-r--r--lib/python/qmk/cli/doctor/windows.py14
-rwxr-xr-x[-rw-r--r--]lib/python/qmk/cli/fileformat.py24
-rw-r--r--lib/python/qmk/cli/flash.py2
-rw-r--r--lib/python/qmk/cli/format/c.py137
-rwxr-xr-xlib/python/qmk/cli/format/json.py5
-rwxr-xr-xlib/python/qmk/cli/format/python.py26
-rw-r--r--lib/python/qmk/cli/format/text.py27
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py143
-rwxr-xr-xlib/python/qmk/cli/generate/info_json.py40
-rwxr-xr-xlib/python/qmk/cli/generate/keyboard_h.py7
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py41
-rw-r--r--lib/python/qmk/cli/generate/version_h.py28
-rwxr-xr-xlib/python/qmk/cli/info.py55
-rwxr-xr-xlib/python/qmk/cli/kle2json.py2
-rwxr-xr-xlib/python/qmk/cli/multibuild.py6
-rw-r--r--lib/python/qmk/cli/new/keyboard.py141
-rwxr-xr-xlib/python/qmk/cli/pyformat.py32
-rw-r--r--lib/python/qmk/commands.py165
-rw-r--r--lib/python/qmk/constants.py5
-rw-r--r--lib/python/qmk/info.py284
-rwxr-xr-xlib/python/qmk/json_encoders.py3
-rw-r--r--lib/python/qmk/json_schema.py45
-rw-r--r--lib/python/qmk/tests/minimal_info.json2
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py22
33 files changed, 1156 insertions, 410 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index de71a5d1e7..b22f1c0d2d 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -40,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',
@@ -50,6 +53,7 @@ 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',
diff --git a/lib/python/qmk/cli/cformat.py b/lib/python/qmk/cli/cformat.py
index efeb459676..9d0ecaeba3 100644..100755
--- a/lib/python/qmk/cli/cformat.py
+++ b/lib/python/qmk/cli/cformat.py
@@ -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))
-
- 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)
+ 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')
- # Sanity check
- if not files:
- cli.log.error('No changed files detected. Use "qmk cformat -a" to format all core files')
- return False
+ 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)
- # 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
diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py
index 7a45e77214..acbd778649 100755
--- a/lib/python/qmk/cli/compile.py
+++ b/lib/python/qmk/cli/compile.py
@@ -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.')
diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py
index 45ff0c8bee..3c508160e3 100644
--- a/lib/python/qmk/cli/console.py
+++ b/lib/python/qmk/cli/console.py
@@ -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'
}
diff --git a/lib/python/qmk/cli/doctor/__init__.py b/lib/python/qmk/cli/doctor/__init__.py
new file mode 100755
index 0000000000..272e042023
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/__init__.py
@@ -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
diff --git a/lib/python/qmk/os_helpers/__init__.py b/lib/python/qmk/cli/doctor/check.py
index 3e98db3c32..0807f41518 100644
--- a/lib/python/qmk/os_helpers/__init__.py
+++ b/lib/python/qmk/cli/doctor/check.py
@@ -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
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/cli/doctor/linux.py
index 008654ab0f..6ce00f6ef1 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/cli/doctor/linux.py
@@ -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
@@ -75,6 +82,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
}
}
@@ -88,8 +99,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
@@ -115,7 +126,8 @@ def check_udev_rules():
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
@@ -138,3 +150,23 @@ 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}.")
+ from .linux import check_udev_rules
+
+ return check_udev_rules()
diff --git a/lib/python/qmk/cli/doctor/macos.py b/lib/python/qmk/cli/doctor/macos.py
new file mode 100644
index 0000000000..00fb272858
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/macos.py
@@ -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
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor/main.py
index 327bc9cb30..6a31ccdfdd 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor/main.py
@@ -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}.")
-
- # 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
+ status = CheckStatus.OK
- 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
-
- 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
+ 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 status
@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
@@ -82,12 +81,11 @@ def doctor(cli):
status = os_tests()
- # Make sure our QMK home is a Git repo
- git_ok = check_git_repo()
+ status = git_tests()
- if git_ok == CheckStatus.WARNING:
- cli.log.warning("QMK home does not appear to be a Git repository! (no .git folder)")
- status = CheckStatus.WARNING
+ venv = in_virtualenv()
+ if venv:
+ 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()
diff --git a/lib/python/qmk/cli/doctor/windows.py b/lib/python/qmk/cli/doctor/windows.py
new file mode 100644
index 0000000000..381ab36fde
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/windows.py
@@ -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
diff --git a/lib/python/qmk/cli/fileformat.py b/lib/python/qmk/cli/fileformat.py
index 112d8d59da..cee4ba1acd 100644..100755
--- a/lib/python/qmk/cli/fileformat.py
+++ b/lib/python/qmk/cli/fileformat.py
@@ -1,13 +1,23 @@
-"""Format files according to QMK's style.
+"""Point people to the new command name.
"""
-from milc import cli
+import sys
+from pathlib import Path
-import subprocess
+from milc import cli
-@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
diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py
index 1b2932a5b2..c2d9e09c69 100644
--- a/lib/python/qmk/cli/flash.py
+++ b/lib/python/qmk/cli/flash.py
@@ -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.')
diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py
new file mode 100644
index 0000000000..b7263e19f3
--- /dev/null
+++ b/lib/python/qmk/cli/format/c.py
@@ -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', '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)