Merge remote-tracking branch 'upstream/develop' into xap
This commit is contained in:
commit
bf66b91433
5591 changed files with 131128 additions and 54530 deletions
|
@ -31,6 +31,7 @@ safe_commands = [
|
|||
subcommands = [
|
||||
'qmk.cli.bux',
|
||||
'qmk.cli.c2json',
|
||||
'qmk.cli.cd',
|
||||
'qmk.cli.cformat',
|
||||
'qmk.cli.chibios.confmigrate',
|
||||
'qmk.cli.clean',
|
||||
|
@ -44,7 +45,9 @@ subcommands = [
|
|||
'qmk.cli.format.python',
|
||||
'qmk.cli.format.text',
|
||||
'qmk.cli.generate.api',
|
||||
'qmk.cli.generate.compilation_database',
|
||||
'qmk.cli.generate.config_h',
|
||||
'qmk.cli.generate.develop_pr_list',
|
||||
'qmk.cli.generate.dfu_header',
|
||||
'qmk.cli.generate.docs',
|
||||
'qmk.cli.generate.info_json',
|
||||
|
@ -59,6 +62,7 @@ subcommands = [
|
|||
'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',
|
||||
|
|
46
lib/python/qmk/cli/cd.py
Executable file
46
lib/python/qmk/cli/cd.py
Executable file
|
@ -0,0 +1,46 @@
|
|||
"""Open a shell in the QMK Home directory
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
from milc import cli
|
||||
|
||||
from qmk.path import under_qmk_firmware
|
||||
|
||||
|
||||
@cli.subcommand('Go to QMK Home')
|
||||
def cd(cli):
|
||||
"""Go to QMK Home
|
||||
"""
|
||||
if not sys.stdout.isatty():
|
||||
cli.log.error("This command is for interactive usage only. For non-interactive usage, 'cd $(qmk env QMK_HOME)' is more robust.")
|
||||
sys.exit(1)
|
||||
|
||||
if not under_qmk_firmware():
|
||||
# Only do anything if the user is not under qmk_firmware already
|
||||
# in order to reduce the possibility of starting multiple shells
|
||||
cli.log.info("Spawning a subshell in your QMK_HOME directory.")
|
||||
cli.log.info("Type 'exit' to get back to the parent shell.")
|
||||
if not cli.platform.lower().startswith('windows'):
|
||||
# For Linux/Mac/etc
|
||||
# Check the user's login shell from 'passwd'
|
||||
# alternatively fall back to $SHELL env var
|
||||
# and finally to '/bin/bash'.
|
||||
import getpass
|
||||
import pwd
|
||||
shell = pwd.getpwnam(getpass.getuser()).pw_shell
|
||||
if not shell:
|
||||
shell = os.environ.get('SHELL', '/bin/bash')
|
||||
# Start the new subshell
|
||||
os.execl(shell, shell)
|
||||
else:
|
||||
# For Windows
|
||||
# Check the $SHELL env var
|
||||
# and fall back to '/usr/bin/bash'.
|
||||
qmk_env = os.environ.copy()
|
||||
# Set the prompt for the new shell
|
||||
qmk_env['MSYS2_PS1'] = qmk_env['PS1']
|
||||
# Start the new subshell
|
||||
cli.run([os.environ.get('SHELL', '/usr/bin/bash')], env=qmk_env)
|
||||
else:
|
||||
cli.log.info("Already within qmk_firmware directory.")
|
|
@ -2,6 +2,7 @@
|
|||
"""
|
||||
import http.server
|
||||
import os
|
||||
import shutil
|
||||
import webbrowser
|
||||
|
||||
from milc import cli
|
||||
|
@ -11,20 +12,33 @@ from milc import cli
|
|||
@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.
|
||||
"""Spin up a local HTTP server for the QMK docs.
|
||||
"""
|
||||
os.chdir('docs')
|
||||
|
||||
with http.server.HTTPServer(('', cli.config.docs.port), http.server.SimpleHTTPRequestHandler) as httpd:
|
||||
cli.log.info(f"Serving QMK docs at http://localhost:{cli.config.docs.port}/")
|
||||
# If docsify-cli is installed, run that instead so we get live reload
|
||||
if shutil.which('docsify'):
|
||||
command = ['docsify', 'serve', '--port', f'{cli.config.docs.port}', '--open' if cli.config.docs.browser else '']
|
||||
|
||||
cli.log.info(f"Running {{fg_cyan}}{str.join(' ', command)}{{fg_reset}}")
|
||||
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()
|
||||
cli.run(command, capture_output=False)
|
||||
except KeyboardInterrupt:
|
||||
cli.log.info("Stopping HTTP server...")
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
else:
|
||||
# Fall back to Python HTTPServer
|
||||
with http.server.HTTPServer(('', cli.config.docs.port), http.server.SimpleHTTPRequestHandler) as httpd:
|
||||
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:
|
||||
cli.log.info("Stopping HTTP server...")
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
|
|
|
@ -118,10 +118,9 @@ 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)
|
||||
|
||||
|
@ -167,6 +166,5 @@ def os_test_linux():
|
|||
return CheckStatus.OK
|
||||
else:
|
||||
cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
|
||||
from .linux import check_udev_rules
|
||||
|
||||
return check_udev_rules()
|
||||
|
|
|
@ -79,12 +79,13 @@ def doctor(cli):
|
|||
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()
|
||||
|
||||
status = git_tests()
|
||||
if git_status == CheckStatus.ERROR or (os_status == CheckStatus.OK and git_status == CheckStatus.WARNING):
|
||||
status = git_status
|
||||
|
||||
venv = in_virtualenv()
|
||||
if venv:
|
||||
if in_virtualenv():
|
||||
cli.log.info('CLI installed in virtualenv.')
|
||||
|
||||
# Make sure the basic CLI tools we need are available and can be executed.
|
||||
|
|
|
@ -18,17 +18,21 @@ def print_bootloader_help():
|
|||
"""Prints the available bootloaders listed in docs.qmk.fm.
|
||||
"""
|
||||
cli.log.info('Here are the available bootloaders:')
|
||||
cli.echo('\tavrdude')
|
||||
cli.echo('\tbootloadhid')
|
||||
cli.echo('\tdfu')
|
||||
cli.echo('\tdfu-util')
|
||||
cli.echo('\tmdloader')
|
||||
cli.echo('\tst-flash')
|
||||
cli.echo('\tst-link-cli')
|
||||
cli.log.info('Enhanced variants for split keyboards:')
|
||||
cli.echo('\tavrdude-split-left')
|
||||
cli.echo('\tavrdude-split-right')
|
||||
cli.echo('\tdfu-ee')
|
||||
cli.echo('\tdfu-split-left')
|
||||
cli.echo('\tdfu-split-right')
|
||||
cli.echo('\tavrdude')
|
||||
cli.echo('\tBootloadHID')
|
||||
cli.echo('\tdfu-util')
|
||||
cli.echo('\tdfu-util-split-left')
|
||||
cli.echo('\tdfu-util-split-right')
|
||||
cli.echo('\tst-link-cli')
|
||||
cli.echo('\tst-flash')
|
||||
cli.echo('For more info, visit https://docs.qmk.fm/#/flashing')
|
||||
|
||||
|
||||
|
|
|
@ -4,23 +4,66 @@ from subprocess import CalledProcessError, DEVNULL
|
|||
|
||||
from milc import cli
|
||||
|
||||
from qmk.path import normpath
|
||||
|
||||
py_file_suffixes = ('py',)
|
||||
py_dirs = ['lib/python']
|
||||
|
||||
|
||||
def yapf_run(files):
|
||||
edit = '--diff' if cli.args.dry_run else '--in-place'
|
||||
yapf_cmd = ['yapf', '-vv', '--recursive', edit, *files]
|
||||
try:
|
||||
cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
|
||||
cli.log.info('Successfully formatted the python code.')
|
||||
|
||||
except CalledProcessError:
|
||||
cli.log.error(f'Python code in {",".join(py_dirs)} incorrectly formatted!')
|
||||
return False
|
||||
|
||||
|
||||
def filter_files(files):
|
||||
"""Yield only files to be formatted and skip the rest
|
||||
"""
|
||||
for file in files:
|
||||
if file and normpath(file).name.split('.')[-1] in py_file_suffixes:
|
||||
yield file
|
||||
else:
|
||||
cli.log.debug('Skipping file %s', file)
|
||||
|
||||
|
||||
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually 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 files.')
|
||||
@cli.argument('files', nargs='*', arg_only=True, type=normpath, help='Filename(s) to 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, 'lib/python']
|
||||
try:
|
||||
cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
|
||||
cli.log.info('Python code in `lib/python` is correctly formatted.')
|
||||
return True
|
||||
# Find the list of files to format
|
||||
if cli.args.files:
|
||||
files = list(filter_files(cli.args.files))
|
||||
|
||||
except CalledProcessError:
|
||||
if cli.args.dry_run:
|
||||
cli.log.error('Python code in `lib/python` is incorrectly formatted!')
|
||||
else:
|
||||
cli.log.error('Error formatting python code!')
|
||||
if not files:
|
||||
cli.log.error('No Python files in filelist: %s', ', '.join(map(str, cli.args.files)))
|
||||
exit(0)
|
||||
|
||||
return False
|
||||
if cli.args.all_files:
|
||||
cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
|
||||
|
||||
elif cli.args.all_files:
|
||||
git_ls_cmd = ['git', 'ls-files', *py_dirs]
|
||||
git_ls = cli.run(git_ls_cmd, stdin=DEVNULL)
|
||||
files = list(filter_files(git_ls.stdout.split('\n')))
|
||||
|
||||
else:
|
||||
git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *py_dirs]
|
||||
git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
|
||||
files = list(filter_files(git_diff.stdout.split('\n')))
|
||||
|
||||
# Sanity check
|
||||
if not files:
|
||||
cli.log.error('No changed files detected. Use "qmk format-python -a" to format all files')
|
||||
return False
|
||||
|
||||
return yapf_run(files)
|
||||
|
|
|
@ -1,27 +1,57 @@
|
|||
"""Ensure text files have the proper line endings.
|
||||
"""
|
||||
from subprocess import CalledProcessError
|
||||
from itertools import islice
|
||||
from subprocess import DEVNULL
|
||||
|
||||
from milc import cli
|
||||
|
||||
from qmk.path import normpath
|
||||
|
||||
|
||||
def _get_chunks(it, size):
|
||||
"""Break down a collection into smaller parts
|
||||
"""
|
||||
it = iter(it)
|
||||
return iter(lambda: tuple(islice(it, size)), ())
|
||||
|
||||
|
||||
def dos2unix_run(files):
|
||||
"""Spawn multiple dos2unix subprocess avoiding too long commands on formatting everything
|
||||
"""
|
||||
for chunk in _get_chunks(files, 10):
|
||||
dos2unix = cli.run(['dos2unix', *chunk])
|
||||
|
||||
if dos2unix.returncode:
|
||||
return False
|
||||
|
||||
|
||||
@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 files.')
|
||||
@cli.argument('files', nargs='*', arg_only=True, type=normpath, help='Filename(s) to format.')
|
||||
@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)
|
||||
# Find the list of files to format
|
||||
if cli.args.files:
|
||||
files = list(cli.args.files)
|
||||
|
||||
dos2unix = cli.run(['xargs', '-0', 'dos2unix'], stdin=None, input=file_list_cmd.stdout)
|
||||
if cli.args.all_files:
|
||||
cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
|
||||
|
||||
if dos2unix.returncode != 0:
|
||||
print(dos2unix.stderr)
|
||||
elif cli.args.all_files:
|
||||
git_ls_cmd = ['git', 'ls-files']
|
||||
git_ls = cli.run(git_ls_cmd, stdin=DEVNULL)
|
||||
files = list(filter(None, git_ls.stdout.split('\n')))
|
||||
|
||||
return dos2unix.returncode
|
||||
else:
|
||||
git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch]
|
||||
git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
|
||||
files = list(filter(None, git_diff.stdout.split('\n')))
|
||||
|
||||
# Sanity check
|
||||
if not files:
|
||||
cli.log.error('No changed files detected. Use "qmk format-text -a" to format all files')
|
||||
return False
|
||||
|
||||
return dos2unix_run(files)
|
||||
|
|
133
lib/python/qmk/cli/generate/compilation_database.py
Executable file
133
lib/python/qmk/cli/generate/compilation_database.py
Executable file
|
@ -0,0 +1,133 @@
|
|||
"""Creates a compilation database for the given keyboard build.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, List, Union
|
||||
|
||||
from milc import cli, MILC
|
||||
|
||||
from qmk.commands import create_make_command
|
||||
from qmk.constants import QMK_FIRMWARE
|
||||
from qmk.decorators import automagic_keyboard, automagic_keymap
|
||||
|
||||
|
||||
@lru_cache(maxsize=10)
|
||||
def system_libs(binary: str) -> List[Path]:
|
||||
"""Find the system include directory that the given build tool uses.
|
||||
"""
|
||||
cli.log.debug("searching for system library directory for binary: %s", binary)
|
||||
bin_path = shutil.which(binary)
|
||||
|
||||
# Actually query xxxxxx-gcc to find its include paths.
|
||||
if binary.endswith("gcc") or binary.endswith("g++"):
|
||||
result = cli.run([binary, '-E', '-Wp,-v', '-'], capture_output=True, check=True, input='\n')
|
||||
paths = []
|
||||
for line in result.stderr.splitlines():
|
||||
if line.startswith(" "):
|
||||
paths.append(Path(line.strip()).resolve())
|
||||
return paths
|
||||
|
||||
return list(Path(bin_path).resolve().parent.parent.glob("*/include")) if bin_path else []
|
||||
|
||||
|
||||
file_re = re.compile(r'printf "Compiling: ([^"]+)')
|
||||
cmd_re = re.compile(r'LOG=\$\((.+?)&&')
|
||||
|
||||
|
||||
def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]:
|
||||
"""parse the output of `make -n <target>`
|
||||
|
||||
This function makes many assumptions about the format of your build log.
|
||||
This happens to work right now for qmk.
|
||||
"""
|
||||
|
||||
state = 'start'
|
||||
this_file = None
|
||||
records = []
|
||||
for line in f:
|
||||
if state == 'start':
|
||||
m = file_re.search(line)
|
||||
if m:
|
||||
this_file = m.group(1)
|
||||
state = 'cmd'
|
||||
|
||||
if state == 'cmd':
|
||||
assert this_file
|
||||
m = cmd_re.search(line)
|
||||
if m:
|
||||
# we have a hit!
|
||||
this_cmd = m.group(1)
|
||||
args = shlex.split(this_cmd)
|
||||
for s in system_libs(args[0]):
|
||||
args += ['-isystem', '%s' % s]
|
||||
new_cmd = ' '.join(shlex.quote(s) for s in args if s != '-mno-thumb-interwork')
|
||||
records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file})
|
||||
state = 'start'
|
||||
|
||||
return records
|
||||
|
||||
|
||||
@cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
|
||||
@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
|
||||
@cli.subcommand('Create a compilation database.')
|
||||
@automagic_keyboard
|
||||
@automagic_keymap
|
||||
def generate_compilation_database(cli: MILC) -> Union[bool, int]:
|
||||
"""Creates a compilation database for the given keyboard build.
|
||||
|
||||
Does a make clean, then a make -n for this target and uses the dry-run output to create
|
||||
a compilation database (compile_commands.json). This file can help some IDEs and
|
||||
IDE-like editors work better. For more information about this:
|
||||
|
||||
https://clang.llvm.org/docs/JSONCompilationDatabase.html
|
||||
"""
|
||||
command = None
|
||||
# check both config domains: the magic decorator fills in `generate_compilation_database` but the user is
|
||||
# more likely to have set `compile` in their config file.
|
||||
current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard
|
||||
current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap
|
||||
|
||||
if current_keyboard and current_keymap:
|
||||
# Generate the make command for a specific keyboard/keymap.
|
||||
command = create_make_command(current_keyboard, current_keymap, dry_run=True)
|
||||
elif not current_keyboard:
|
||||
cli.log.error('Could not determine keyboard!')
|
||||
elif not current_keymap:
|
||||
cli.log.error('Could not determine keymap!')
|
||||
|
||||
if not command:
|
||||
cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
|
||||
cli.echo('usage: qmk compiledb [-kb KEYBOARD] [-km KEYMAP]')
|
||||
return False
|
||||
|
||||
# remove any environment variable overrides which could trip us up
|
||||
env = os.environ.copy()
|
||||
env.pop("MAKEFLAGS", None)
|
||||
|
||||
# re-use same executable as the main make invocation (might be gmake)
|
||||
clean_command = [command[0], 'clean']
|
||||
cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command))
|
||||
cli.run(clean_command, capture_output=False, check=True, env=env)
|
||||
|
||||
cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command))
|
||||
|
||||
result = cli.run(command, capture_output=True, check=True, env=env)
|
||||
db = parse_make_n(result.stdout.splitlines())
|
||||
if not db:
|
||||
cli.log.error("Failed to parse output from make output:\n%s", result.stdout)
|
||||
return False
|
||||
|
||||
cli.log.info("Found %s compile commands", len(db))
|
||||
|
||||
dbpath = QMK_FIRMWARE / 'compile_commands.json'
|
||||
|
||||
cli.log.info(f"Writing build database to {dbpath}")
|
||||
dbpath.write_text(json.dumps(db, indent=4))
|
||||
|
||||
return True
|
|
@ -173,7 +173,7 @@ def generate_config_h(cli):
|
|||
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']
|
||||
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)
|
||||
|
||||
|
|
119
lib/python/qmk/cli/generate/develop_pr_list.py
Executable file
119
lib/python/qmk/cli/generate/develop_pr_list.py
Executable file
|
@ -0,0 +1,119 @@
|
|||
"""Export the initial list of PRs associated with a `develop` merge to `master`.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from subprocess import DEVNULL
|
||||
|
||||
from milc import cli
|
||||
|
||||
cache_timeout = 7 * 86400
|
||||
fix_expr = re.compile(r'fix', flags=re.IGNORECASE)
|
||||
clean1_expr = re.compile(r'\[(develop|keyboard|keymap|core|cli|bug|docs|feature)\]', flags=re.IGNORECASE)
|
||||
clean2_expr = re.compile(r'^(develop|keyboard|keymap|core|cli|bug|docs|feature):', flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def _get_pr_info(cache, gh, pr_num):
|
||||
pull = cache.get(f'pull:{pr_num}')
|
||||
if pull is None:
|
||||
print(f'Retrieving info for PR #{pr_num}')
|
||||
pull = gh.pulls.get(owner='qmk', repo='qmk_firmware', pull_number=pr_num)
|
||||
cache.set(f'pull:{pr_num}', pull, cache_timeout)
|
||||
return pull
|
||||
|
||||
|
||||
def _try_open_cache(cli):
|
||||
# These dependencies are manually handled because people complain. Fun.
|
||||
try:
|
||||
from sqlite_cache.sqlite_cache import SqliteCache
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
cache_loc = Path(cli.config_file).parent
|
||||
return SqliteCache(cache_loc)
|
||||
|
||||
|
||||
def _get_github():
|
||||
try:
|
||||
from ghapi.all import GhApi
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
return GhApi()
|
||||
|
||||
|
||||
@cli.argument('-f', '--from-ref', default='0.11.0', help='Git revision/tag/reference/branch to begin search')
|
||||
@cli.argument('-b', '--branch', default='upstream/develop', help='Git branch to iterate (default: "upstream/develop")')
|
||||
@cli.subcommand('Creates the develop PR list.', hidden=False if cli.config.user.developer else True)
|
||||
def generate_develop_pr_list(cli):
|
||||
"""Retrieves information from GitHub regarding the list of PRs associated
|
||||
with a merge of `develop` branch into `master`.
|
||||
|
||||
Requires environment variable GITHUB_TOKEN to be set.
|
||||
"""
|
||||
|
||||
if 'GITHUB_TOKEN' not in os.environ or os.environ['GITHUB_TOKEN'] == '':
|
||||
cli.log.error('Environment variable "GITHUB_TOKEN" is not set.')
|
||||
return 1
|
||||
|
||||
cache = _try_open_cache(cli)
|
||||
gh = _get_github()
|
||||
|
||||
git_args = ['git', 'rev-list', '--oneline', '--no-merges', '--reverse', f'{cli.args.from_ref}...{cli.args.branch}', '^upstream/master']
|
||||
commit_list = cli.run(git_args, capture_output=True, stdin=DEVNULL)
|
||||
|
||||
if cache is None or gh is None:
|
||||
cli.log.error('Missing one or more dependent python packages: "ghapi", "python-sqlite-cache"')
|
||||
return 1
|
||||
|
||||
pr_list_bugs = []
|
||||
pr_list_dependencies = []
|
||||
pr_list_core = []
|
||||
pr_list_keyboards = []
|
||||
pr_list_keyboard_fixes = []
|
||||
pr_list_cli = []
|
||||
pr_list_others = []
|
||||
|
||||
def _categorise_commit(commit_info):
|
||||
def fix_or_normal(info, fixes_collection, normal_collection):
|
||||
if "bug" in info['pr_labels'] or fix_expr.search(info['title']):
|
||||
fixes_collection.append(info)
|
||||
else:
|
||||
normal_collection.append(info)
|
||||
|
||||
if "dependencies" in commit_info['pr_labels']:
|
||||
fix_or_normal(commit_info, pr_list_bugs, pr_list_dependencies)
|
||||
elif "core" in commit_info['pr_labels']:
|
||||
fix_or_normal(commit_info, pr_list_bugs, pr_list_core)
|
||||
elif "keyboard" in commit_info['pr_labels'] or "keymap" in commit_info['pr_labels'] or "via" in commit_info['pr_labels']:
|
||||
fix_or_normal(commit_info, pr_list_keyboard_fixes, pr_list_keyboards)
|
||||
elif "cli" in commit_info['pr_labels']:
|
||||
fix_or_normal(commit_info, pr_list_bugs, pr_list_cli)
|
||||
else:
|
||||
fix_or_normal(commit_info, pr_list_bugs, pr_list_others)
|
||||
|
||||
git_expr = re.compile(r'^(?P<hash>[a-f0-9]+) (?P<title>.*) \(#(?P<pr>[0-9]+)\)$')
|
||||
for line in commit_list.stdout.split('\n'):
|
||||
match = git_expr.search(line)
|
||||
if match:
|
||||
pr_info = _get_pr_info(cache, gh, match.group("pr"))
|
||||
commit_info = {'hash': match.group("hash"), 'title': match.group("title"), 'pr_num': int(match.group("pr")), 'pr_labels': [label.name for label in pr_info.labels.items]}
|
||||
_categorise_commit(commit_info)
|
||||
|
||||
def _dump_commit_list(name, collection):
|
||||
if len(collection) == 0:
|
||||
return
|
||||
print("")
|
||||
print(f"{name}:")
|
||||
for commit in sorted(collection, key=lambda x: x['pr_num']):
|
||||
title = clean1_expr.sub('', clean2_expr.sub('', commit['title'])).strip()
|
||||
pr_num = commit['pr_num']
|
||||
print(f'* {title} ([#{pr_num}](https://github.com/qmk/qmk_firmware/pull/{pr_num}))')
|
||||
|
||||
_dump_commit_list("Core", pr_list_core)
|
||||
_dump_commit_list("CLI", pr_list_cli)
|
||||
_dump_commit_list("Submodule updates", pr_list_dependencies)
|
||||
_dump_commit_list("Keyboards", pr_list_keyboards)
|
||||
_dump_commit_list("Keyboard fixes", pr_list_keyboard_fixes)
|
||||
_dump_commit_list("Others", pr_list_others)
|
||||
_dump_commit_list("Bugs", pr_list_bugs)
|
|
@ -30,9 +30,9 @@ 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')
|
||||
keyboard_h_lines.append(f'#define PRODUCT {kb_info_json["keyboard_name"]} Bootloader')
|
||||
|
||||
# Optional
|
||||
if 'qmk_lufa_bootloader.esc_output' in kb_info_json:
|
||||
|
|
|
@ -36,7 +36,7 @@ def generate_keyboard_h(cli):
|
|||
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")')
|
||||
|
|
|
@ -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']:
|
||||
|
|
|
@ -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"}'
|
||||
|
@ -67,12 +67,9 @@ def generate_rules_mk(cli):
|
|||
# Iterate through features to enable/disable them
|
||||
if 'features' in kb_info_json:
|
||||
for feature, enabled in kb_info_json['features'].items():
|
||||
if feature == 'bootmagic_lite' and enabled:
|
||||
rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite')
|
||||
else:
|
||||
feature = feature.upper()
|
||||
enabled = 'yes' if enabled else 'no'
|
||||
rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
|
||||
feature = feature.upper()
|
||||
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':
|
||||
|
|
|
@ -33,7 +33,7 @@ def json2c(cli):
|
|||
cli.args.output = None
|
||||
|
||||
# Generate the keymap
|
||||
keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
|
||||
keymap_c = qmk.keymap.generate_c(user_keymap)
|
||||
|
||||
if cli.args.output:
|
||||
cli.args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
|
|
@ -13,5 +13,10 @@ from qmk.keyboard import keyboard_completer, keyboard_folder
|
|||
def list_keymaps(cli):
|
||||
"""List the keymaps for a specific keyboard
|
||||
"""
|
||||
if not cli.config.list_keymaps.keyboard:
|
||||
cli.log.error('Missing required arguments: --keyboard')
|
||||
cli.subcommands['list-keymaps'].print_help()
|
||||
return False
|
||||
|
||||
for name in qmk.keymap.list_keymaps(cli.config.list_keymaps.keyboard):
|
||||
print(name)
|
||||
|
|
23
lib/python/qmk/cli/list/layouts.py
Normal file
23
lib/python/qmk/cli/list/layouts.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
"""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
|
||||
"""
|
||||
if not cli.config.list_layouts.keyboard:
|
||||
cli.log.error('Missing required arguments: --keyboard')
|
||||
cli.subcommands['list-layouts'].print_help()
|
||||
return False
|
||||
|
||||
info_data = info_json(cli.config.list_layouts.keyboard)
|
||||
for name in sorted(info_data.get('community_layouts', [])):
|
||||
print(name)
|
|
@ -1,10 +1,8 @@
|
|||
"""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
|
||||
|
@ -32,6 +30,7 @@ def validate_keyboard_name(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.argument('-n', '--realname', help='Specify your real name if you want to use that. Defaults to username', arg_only=True)
|
||||
@cli.subcommand('Creates a new keyboard directory')
|
||||
def new_keyboard(cli):
|
||||
"""Creates a new keyboard.
|
||||
|
@ -69,7 +68,7 @@ def new_keyboard(cli):
|
|||
# Get username
|
||||
user_name = None
|
||||
while not user_name:
|
||||
user_name = question('Your Name:', default=find_user_name())
|
||||
user_name = question('Your GitHub User 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.')
|
||||
|
@ -78,26 +77,21 @@ def new_keyboard(cli):
|
|||
if cli.args.username:
|
||||
return False
|
||||
|
||||
# Copy all the files
|
||||
copy_templates(keyboard_type, keyboard_path)
|
||||
real_name = None
|
||||
while not real_name:
|
||||
real_name = question('Your real name:', default=user_name)
|
||||
|
||||
# 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)
|
||||
replacements = {
|
||||
"YEAR": str(date.today().year),
|
||||
"KEYBOARD": keyboard_basename,
|
||||
"USER_NAME": user_name,
|
||||
"YOUR_NAME": real_name,
|
||||
}
|
||||
|
||||
template_dir = Path('data/templates')
|
||||
template_tree(template_dir / 'base', keyboard_path, replacements)
|
||||
template_tree(template_dir / keyboard_type, keyboard_path, replacements)
|
||||
|
||||
cli.echo('')
|
||||
cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{new_keyboard_name}{{fg_green}}.{{fg_reset}}')
|
||||
|
@ -114,29 +108,32 @@ def find_user_name():
|
|||
return git_get_username()
|
||||
|
||||
|
||||
def copy_templates(keyboard_type, keyboard_path):
|
||||
"""Copies the template files from data/templates to the new keyboard directory.
|
||||
def template_tree(src: Path, dst: Path, replacements: dict):
|
||||
"""Recursively copy template and replace placeholders
|
||||
|
||||
Args:
|
||||
src (Path)
|
||||
The source folder to copy from
|
||||
dst (Path)
|
||||
The destination folder to copy to
|
||||
replacements (dict)
|
||||
a dictionary with "key":"value" pairs to replace.
|
||||
|
||||
Raises:
|
||||
FileExistsError
|
||||
When trying to overwrite existing files
|
||||
"""
|
||||
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)
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
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)
|
||||
for child in src.iterdir():
|
||||
if child.is_dir():
|
||||
template_tree(child, dst / child.name, replacements=replacements)
|
||||
|
||||
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')
|
||||
if child.is_file():
|
||||
file_name = dst / (child.name % replacements)
|
||||
|
||||
|
||||
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='')
|
||||
with file_name.open(mode='x') as dst_f:
|
||||
with child.open() as src_f:
|
||||
template = src_f.read()
|
||||
dst_f.write(template % replacements)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue