aboutsummaryrefslogtreecommitdiff
path: root/lib/python/qmk/cli
diff options
context:
space:
mode:
authorNick Brassel <[email protected]>2023-11-28 16:39:48 +1100
committerNick Brassel <[email protected]>2023-11-28 16:40:47 +1100
commit0115951de12bd4c798a43a380651c34552552e85 (patch)
tree62999011ae782d93aa6dc9c889615db2a840cafc /lib/python/qmk/cli
parent0379d1f59e58cefa18cdb72ba1b77507d1108ae6 (diff)
parent4d99e0a23cff02f4c6e55e093b5de08a9be0df7c (diff)
downloadqmk_firmware-0.23.0.tar.gz
qmk_firmware-0.23.0.zip
`develop` -> `master`, 2023q4 edition0.23.0
Diffstat (limited to 'lib/python/qmk/cli')
-rw-r--r--lib/python/qmk/cli/__init__.py5
-rw-r--r--lib/python/qmk/cli/c2json.py59
-rw-r--r--lib/python/qmk/cli/ci/validate_aliases.py7
-rw-r--r--lib/python/qmk/cli/clean.py4
-rwxr-xr-xlib/python/qmk/cli/compile.py62
-rwxr-xr-xlib/python/qmk/cli/doctor/main.py25
-rw-r--r--lib/python/qmk/cli/find.py14
-rw-r--r--lib/python/qmk/cli/flash.py45
-rwxr-xr-xlib/python/qmk/cli/format/json.py70
-rwxr-xr-xlib/python/qmk/cli/generate/api.py4
-rwxr-xr-xlib/python/qmk/cli/generate/compilation_database.py16
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py59
-rwxr-xr-xlib/python/qmk/cli/generate/keyboard_c.py27
-rw-r--r--lib/python/qmk/cli/generate/keycodes.py8
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py26
-rwxr-xr-xlib/python/qmk/cli/mass_compile.py31
-rwxr-xr-xlib/python/qmk/cli/new/keymap.py8
-rw-r--r--lib/python/qmk/cli/userspace/__init__.py5
-rw-r--r--lib/python/qmk/cli/userspace/add.py51
-rw-r--r--lib/python/qmk/cli/userspace/compile.py38
-rw-r--r--lib/python/qmk/cli/userspace/doctor.py11
-rw-r--r--lib/python/qmk/cli/userspace/list.py51
-rw-r--r--lib/python/qmk/cli/userspace/remove.py37
23 files changed, 470 insertions, 193 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 695a180066..cf60903687 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -81,6 +81,11 @@ subcommands = [
'qmk.cli.new.keymap',
'qmk.cli.painter',
'qmk.cli.pytest',
+ 'qmk.cli.userspace.add',
+ 'qmk.cli.userspace.compile',
+ 'qmk.cli.userspace.doctor',
+ 'qmk.cli.userspace.list',
+ 'qmk.cli.userspace.remove',
'qmk.cli.via2json',
]
diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py
index 7f6aca070a..f7f1f2ffba 100644
--- a/lib/python/qmk/cli/c2json.py
+++ b/lib/python/qmk/cli/c2json.py
@@ -1,45 +1,57 @@
"""Generate a keymap.json from a keymap.c file.
"""
+import re
import json
from argcomplete.completers import FilesCompleter
from milc import cli
-import qmk.keymap
import qmk.path
from qmk.json_encoders import InfoJSONEncoder
+from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.keyboard import keyboard_completer, keyboard_folder
+from qmk.keymap import locate_keymap, find_keymap_from_dir, generate_json, c2json as c2json_impl
from qmk.errors import CppError
+from qmk.commands import dump_lines
@cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c')
@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
[email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='The keyboard\'s name')
[email protected]('-km', '--keymap', arg_only=True, required=True, help='The keymap\'s name')
[email protected]('filename', arg_only=True, completer=FilesCompleter('.c'), help='keymap.c file')
[email protected]('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard\'s name')
[email protected]('-km', '--keymap', help='The keymap\'s name')
[email protected]('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.c'), help='keymap.c file')
@cli.subcommand('Creates a keymap.json from a keymap.c file.')
+@automagic_keyboard
+@automagic_keymap
def c2json(cli):
"""Generate a keymap.json from a keymap.c file.
This command uses the `qmk.keymap` module to generate a keymap.json from a keymap.c file. The generated keymap is written to stdout, or to a file if -o is provided.
"""
- if cli.args.filename != '-':
- cli.args.filename = qmk.path.normpath(cli.args.filename)
+ filename = cli.args.filename
+ keyboard = cli.config.c2json.keyboard
+ keymap = cli.config.c2json.keymap
- # Error checking
- if not cli.args.filename.exists():
- cli.log.error('C file does not exist!')
- cli.print_usage()
- return False
+ if filename:
+ if not keyboard and not keymap:
+ # fallback to inferring keyboard/keymap from path
+ (keymap, found_type) = find_keymap_from_dir(filename)
+ if found_type == 'keymap_directory':
+ keyboard = re.search(fr"keyboards/(.+)/keymaps/{keymap}/.*", filename.as_posix()).group(1)
- # Environment processing
- if cli.args.output == ('-'):
- cli.args.output = None
+ elif keyboard and keymap:
+ if not filename:
+ # fallback to inferring keyboard/keymap from path
+ filename = locate_keymap(keyboard, keymap)
+
+ if not all((filename, keyboard, keymap)):
+ cli.log.error('You must supply keyboard and keymap, a path to a keymap.c within qmk_firmware, or absolute filename and keyboard and keymap')
+ cli.print_help()
+ return False
- # Parse the keymap.c
try:
- keymap_json = qmk.keymap.c2json(cli.args.keyboard, cli.args.keymap, cli.args.filename, use_cpp=cli.args.no_cpp)
+ keymap_json = c2json_impl(keyboard, keymap, filename, use_cpp=cli.args.no_cpp)
except CppError as e:
if cli.config.general.verbose:
cli.log.debug('The C pre-processor ran into a fatal error: %s', e)
@@ -48,19 +60,14 @@ def c2json(cli):
# Generate the keymap.json
try:
- keymap_json = qmk.keymap.generate_json(keymap_json['keymap'], keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers'])
+ keymap_json = generate_json(keymap_json['keymap'], keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers'])
except KeyError:
cli.log.error('Something went wrong. Try to use --no-cpp.')
return False
if cli.args.output:
- cli.args.output.parent.mkdir(parents=True, exist_ok=True)
- if cli.args.output.exists():
- cli.args.output.replace(cli.args.output.parent / (cli.args.output.name + '.bak'))
- cli.args.output.write_text(json.dumps(keymap_json, cls=InfoJSONEncoder, sort_keys=True))
-
- if not cli.args.quiet:
- cli.log.info('Wrote keymap to %s.', cli.args.output)
-
+ keymap_lines = [json.dumps(keymap_json, cls=InfoJSONEncoder, sort_keys=True)]
else:
- print(json.dumps(keymap_json))
+ keymap_lines = [json.dumps(keymap_json)]
+
+ dump_lines(cli.args.output, keymap_lines, cli.args.quiet)
diff --git a/lib/python/qmk/cli/ci/validate_aliases.py b/lib/python/qmk/cli/ci/validate_aliases.py
index a205d03cff..7f781d4397 100644
--- a/lib/python/qmk/cli/ci/validate_aliases.py
+++ b/lib/python/qmk/cli/ci/validate_aliases.py
@@ -1,11 +1,8 @@
"""Validates the list of keyboard aliases.
"""
-from pathlib import Path
-
from milc import cli
-from qmk.json_schema import json_load
-from qmk.keyboard import resolve_keyboard, keyboard_folder
+from qmk.keyboard import resolve_keyboard, keyboard_folder, keyboard_alias_definitions
def _safe_keyboard_folder(target):
@@ -34,7 +31,7 @@ def _target_keyboard_exists(target):
@cli.subcommand('Validates the list of keyboard aliases.', hidden=True)
def ci_validate_aliases(cli):
- aliases = json_load(Path('data/mappings/keyboard_aliases.hjson'))
+ aliases = keyboard_alias_definitions()
success = True
for alias in aliases.keys():
diff --git a/lib/python/qmk/cli/clean.py b/lib/python/qmk/cli/clean.py
index 72b7ffe810..bdec01e4b6 100644
--- a/lib/python/qmk/cli/clean.py
+++ b/lib/python/qmk/cli/clean.py
@@ -2,7 +2,7 @@
"""
from subprocess import DEVNULL
-from qmk.commands import create_make_target
+from qmk.commands import find_make
from milc import cli
@@ -11,4 +11,4 @@ from milc import cli
def clean(cli):
"""Runs `make clean` (or `make distclean` if --all is passed)
"""
- cli.run(create_make_target('distclean' if cli.args.all else 'clean'), capture_output=False, stdin=DEVNULL)
+ cli.run([find_make(), 'distclean' if cli.args.all else 'clean'], capture_output=False, stdin=DEVNULL)
diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py
index 2478ea1fed..3c8f3664ea 100755
--- a/lib/python/qmk/cli/compile.py
+++ b/lib/python/qmk/cli/compile.py
@@ -7,22 +7,11 @@ from argcomplete.completers import FilesCompleter
from milc import cli
import qmk.path
-from qmk.constants import QMK_FIRMWARE
from qmk.decorators import automagic_keyboard, automagic_keymap
-from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json, build_environment
+from qmk.commands import build_environment
from qmk.keyboard import keyboard_completer, keyboard_folder_or_all, is_all_keyboards
from qmk.keymap import keymap_completer, locate_keymap
-from qmk.cli.generate.compilation_database import write_compilation_database
-
-
-def _is_keymap_target(keyboard, keymap):
- if keymap == 'all':
- return True
-
- if locate_keymap(keyboard, keymap):
- return True
-
- return False
+from qmk.build_targets import KeyboardKeymapBuildTarget, JsonKeymapBuildTarget
@cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), completer=FilesCompleter('.json'), help='The configurator export to compile')
@@ -32,6 +21,7 @@ def _is_keymap_target(keyboard, keymap):
@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.")
[email protected]('-t', '--target', type=str, default=None, help="Intended alternative build target, such as `production` in `make planck/rev4:default:production`.")
@cli.argument('--compiledb', arg_only=True, action='store_true', help="Generates the clang compile_commands.json file during build. Implies --clean.")
@cli.subcommand('Compile a QMK Firmware.')
@automagic_keyboard
@@ -47,53 +37,35 @@ def compile(cli):
from .mass_compile import mass_compile
cli.args.builds = []
cli.args.filter = []
- cli.args.no_temp = False
+ cli.config.mass_compile.keymap = cli.config.compile.keymap
+ cli.config.mass_compile.parallel = cli.config.compile.parallel
+ cli.config.mass_compile.no_temp = False
return mass_compile(cli)
# Build the environment vars
envs = build_environment(cli.args.env)
- # Determine the compile command
- commands = []
-
- current_keyboard = None
- current_keymap = None
+ # Handler for the build target
+ target = None
if cli.args.filename:
- # If a configurator JSON was provided generate a keymap and compile it
- user_keymap = parse_configurator_json(cli.args.filename)
- commands = [compile_configurator_json(user_keymap, parallel=cli.config.compile.parallel, clean=cli.args.clean, **envs)]
+ # if we were given a filename, assume we have a json build target
+ target = JsonKeymapBuildTarget(cli.args.filename)
elif cli.config.compile.keyboard and cli.config.compile.keymap:
- # Generate the make command for a specific keyboard/keymap.
- if not _is_keymap_target(cli.config.compile.keyboard, cli.config.compile.keymap):
+ # if we got a keyboard and keymap, attempt to find it
+ if not locate_keymap(cli.config.compile.keyboard, cli.config.compile.keymap):
cli.log.error('Invalid keymap argument.')
cli.print_help()
return False
- if cli.args.clean:
- commands.append(create_make_command(cli.config.compile.keyboard, cli.config.compile.keymap, 'clean', **envs))
- commands.append(create_make_command(cli.config.compile.keyboard, cli.config.compile.keymap, parallel=cli.config.compile.parallel, **envs))
+ # If we got here, then we have a valid keyboard and keymap for a build target
+ target = KeyboardKeymapBuildTarget(cli.config.compile.keyboard, cli.config.compile.keymap)
- current_keyboard = cli.config.compile.keyboard
- current_keymap = cli.config.compile.keymap
-
- if not commands:
+ if not target:
cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
cli.print_help()
return False
- if cli.args.compiledb:
- if current_keyboard is None or current_keymap is None:
- cli.log.error('You must supply both `--keyboard` and `--keymap` or be in a directory with a keymap to generate a compile_commands.json file.')
- cli.print_help()
- return False
- write_compilation_database(current_keyboard, current_keymap, QMK_FIRMWARE / 'compile_commands.json')
-
- cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(commands[-1]))
- if not cli.args.dry_run:
- cli.echo('\n')
- for command in commands:
- ret = cli.run(command, capture_output=False)
- if ret.returncode:
- return ret.returncode
+ target.configure(parallel=cli.config.compile.parallel, clean=cli.args.clean, compiledb=cli.args.compiledb)
+ target.compile(cli.args.target, dry_run=cli.args.dry_run, **envs)
diff --git a/lib/python/qmk/cli/doctor/main.py b/lib/python/qmk/cli/doctor/main.py
index 6a6feb87d1..dd8b58b2c7 100755
--- a/lib/python/qmk/cli/doctor/main.py
+++ b/lib/python/qmk/cli/doctor/main.py
@@ -9,10 +9,11 @@ from milc import cli
from milc.questions import yesno
from qmk import submodules
-from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM
+from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM, QMK_USERSPACE, HAS_QMK_USERSPACE
from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
from qmk.git import git_check_repo, git_get_branch, git_get_tag, git_get_last_log_entry, git_get_common_ancestor, git_is_dirty, git_get_remotes, git_check_deviation
from qmk.commands import in_virtualenv
+from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError
def os_tests():
@@ -92,6 +93,25 @@ def output_submodule_status():
cli.log.error(f'- {sub_name}: <<< missing or unknown >>>')
+def userspace_tests(qmk_firmware):
+ if qmk_firmware:
+ cli.log.info(f'QMK home: {{fg_cyan}}{qmk_firmware}')
+
+ for path in qmk_userspace_paths():
+ try:
+ qmk_userspace_validate(path)
+ cli.log.info(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_green}}Valid `qmk.json`')
+ except FileNotFoundError:
+ cli.log.warn(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Missing `qmk.json`')
+ except UserspaceValidationError as err:
+ cli.log.warn(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Invalid `qmk.json`')
+ cli.log.warn(f' -- {{fg_cyan}}{path}/qmk.json{{fg_reset}} validation error: {err}')
+
+ if QMK_USERSPACE is not None:
+ cli.log.info(f'QMK userspace: {{fg_cyan}}{QMK_USERSPACE}')
+ cli.log.info(f'Userspace enabled: {{fg_cyan}}{HAS_QMK_USERSPACE}')
+
+
@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
@cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.')
@cli.subcommand('Basic QMK environment checks')
@@ -108,6 +128,9 @@ def doctor(cli):
cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE)
status = os_status = os_tests()
+
+ userspace_tests(None)
+
git_status = git_tests()
if git_status == CheckStatus.ERROR or (os_status == CheckStatus.OK and git_status == CheckStatus.WARNING):
diff --git a/lib/python/qmk/cli/find.py b/lib/python/qmk/cli/find.py
index f2135bbc16..55a0530092 100644
--- a/lib/python/qmk/cli/find.py
+++ b/lib/python/qmk/cli/find.py
@@ -19,13 +19,9 @@ from qmk.search import search_keymap_targets
def find(cli):
"""Search through all keyboards and keymaps for a given search criteria.
"""
+ targets = search_keymap_targets([('all', cli.config.find.keymap)], cli.args.filter)
+ for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)):
+ print(f'{target}')
- if len(cli.args.filter) == 0 and len(cli.args.print) > 0:
- cli.log.warning('No filters supplied -- keymaps not parsed, unable to print requested values.')
-
- targets = search_keymap_targets([('all', cli.config.find.keymap)], cli.args.filter, cli.args.print)
- for keyboard, keymap, print_vals in targets:
- print(f'{keyboard}:{keymap}')
-
- for key, val in print_vals:
- print(f' {key}={val}')
+ for key in cli.args.print:
+ print(f' {key}={target.dotty.get(key, None)}')
diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py
index 8724f26889..5df31e4781 100644
--- a/lib/python/qmk/cli/flash.py
+++ b/lib/python/qmk/cli/flash.py
@@ -4,25 +4,17 @@ You can compile a keymap already in the repo or using a QMK Configurator export.
A bootloader must be specified.
"""
from argcomplete.completers import FilesCompleter
+from pathlib import Path
from milc import cli
import qmk.path
from qmk.decorators import automagic_keyboard, automagic_keymap
-from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json, build_environment
+from qmk.commands import build_environment
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.keymap import keymap_completer, locate_keymap
from qmk.flashers import flasher
-
-
-def _is_keymap_target(keyboard, keymap):
- if keymap == 'all':
- return True
-
- if locate_keymap(keyboard, keymap):
- return True
-
- return False
+from qmk.build_targets import KeyboardKeymapBuildTarget, JsonKeymapBuildTarget
def _list_bootloaders():
@@ -89,7 +81,7 @@ def flash(cli):
If bootloader is omitted the make system will use the configured bootloader for that keyboard.
"""
- if cli.args.filename and cli.args.filename.suffix in ['.bin', '.hex', '.uf2']:
+ if cli.args.filename and isinstance(cli.args.filename, Path) and cli.args.filename.suffix in ['.bin', '.hex', '.uf2']:
return _flash_binary(cli.args.filename, cli.args.mcu)
if cli.args.bootloaders:
@@ -98,34 +90,27 @@ def flash(cli):
# Build the environment vars
envs = build_environment(cli.args.env)
- # Determine the compile command
- commands = []
+ # Handler for the build target
+ target = None
if cli.args.filename:
- # If a configurator JSON was provided generate a keymap and compile it
- user_keymap = parse_configurator_json(cli.args.filename)
- commands = [compile_configurator_json(user_keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, clean=cli.args.clean, **envs)]
+ # if we were given a filename, assume we have a json build target
+ target = JsonKeymapBuildTarget(cli.args.filename)
elif cli.config.flash.keyboard and cli.config.flash.keymap:
- # Generate the make command for a specific keyboard/keymap.
- if not _is_keymap_target(cli.config.flash.keyboard, cli.config.flash.keymap):
+ # if we got a keyboard and keymap, attempt to find it
+ if not locate_keymap(cli.config.flash.keyboard, cli.config.flash.keymap):
cli.log.error('Invalid keymap argument.')
cli.print_help()
return False
- if cli.args.clean:
- commands.append(create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, 'clean', **envs))
- commands.append(create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs))
+ # If we got here, then we have a valid keyboard and keymap for a build target
+ target = KeyboardKeymapBuildTarget(cli.config.flash.keyboard, cli.config.flash.keymap)
- if not commands:
+ if not target:
cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
cli.print_help()
return False
- cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(commands[-1]))
- if not cli.args.dry_run:
- cli.echo('\n')
- for command in commands:
- ret = cli.run(command, capture_output=False)
- if ret.returncode:
- return ret.returncode
+ target.configure(parallel=cli.config.flash.parallel, clean=cli.args.clean)
+ target.compile(cli.args.bootloader, dry_run=cli.args.dry_run, **envs)
diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py
index 3299a0d807..283513254c 100755
--- a/lib/python/qmk/cli/format/json.py
+++ b/lib/python/qmk/cli/format/json.py
@@ -9,48 +9,74 @@ from milc import cli
from qmk.info import info_json
from qmk.json_schema import json_load, validate
-from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder
+from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder
from qmk.path import normpath
[email protected]('json_file', arg_only=True, type=normpath, help='JSON file to format')
[email protected]('-f', '--format', choices=['auto', 'keyboard', 'keymap'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)')
[email protected]('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file')
[email protected]('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ')
[email protected]('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
-def format_json(cli):
- """Format a json file.
+def _detect_json_format(file, json_data):
+ """Detect the format of a json file.
"""
- json_file = json_load(cli.args.json_file)
-
- if cli.args.format == 'auto':
+ json_encoder = None
+ try:
+ validate(json_data, 'qmk.user_repo.v1')
+ json_encoder = UserspaceJSONEncoder
+ except ValidationError:
+ pass
+
+ if json_encoder is None:
try:
- validate(json_file, 'qmk.keyboard.v1')
+ validate(json_data, '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)
+ cli.log.warning('File %s did not validate as a keyboard info.json or userspace qmk.json:\n\t%s', file, e)
+ cli.log.info('Treating %s as a keymap file.', file)
json_encoder = KeymapJSONEncoder
+
+ return json_encoder
+
+
+def _get_json_encoder(file, json_data):
+ """Get the json encoder for a file.
+ """
+ json_encoder = None
+ if cli.args.format == 'auto':
+ json_encoder = _detect_json_format(file, json_data)
elif cli.args.format == 'keyboard':
json_encoder = InfoJSONEncoder
elif cli.args.format == 'keymap':
json_encoder = KeymapJSONEncoder
+ elif cli.args.format == 'userspace':
+ json_encoder = UserspaceJSONEncoder
else:
# This should be impossible
cli.log.error('Unknown format: %s', cli.args.format)
+ return json_encoder
+
+
[email protected]('json_file', arg_only=True, type=normpath, help='JSON file to format')
[email protected]('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)')
[email protected]('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file')
[email protected]('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ')
[email protected]('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
+def format_json(cli):
+ """Format a json file.
+ """
+ json_data = json_load(cli.args.json_file)
+
+ json_encoder = _get_json_encoder(cli.args.json_file, json_data)
+ if json_encoder is None:
return False
- if json_encoder == KeymapJSONEncoder and 'layout' in json_file:
+ if json_encoder == KeymapJSONEncoder and 'layout' in json_data:
# Attempt to format the keycodes.
- layout = json_file['layout']
- info_data = info_json(json_file['keyboard'])
+ layout = json_data['layout']
+ info_data = info_json(json_data['keyboard'])
if layout in info_data.get('layout_aliases', {}):
- layout = json_file['layout'] = info_data['layout_aliases'][layout]
+ layout = json_data['layout'] = info_data['layout_aliases'][layout]
if layout in info_data.get('layouts'):
- for layer_num, layer in enumerate(json_file['layers']):
+ for layer_num, layer in enumerate(json_data['layers']):
current_layer = []
last_row = 0
@@ -61,9 +87,9 @@ def format_json(cli):
current_layer.append(keymap_key)
- json_file['layers'][layer_num] = current_layer
+ json_data['layers'][layer_num] = current_layer
- output = json.dumps(json_file, cls=json_encoder, sort_keys=True)
+ output = json.dumps(json_data, cls=json_encoder, sort_keys=True)
if cli.args.inplace:
with open(cli.args.json_file, 'w+', encoding='utf-8') as outfile:
diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
index 61a2f9f732..1948b483a1 100755
--- a/lib/python/qmk/cli/generate/api.py
+++ b/lib/python/qmk/cli/generate/api.py
@@ -10,7 +10,7 @@ from qmk.datetime import current_datetime
from qmk.info import info_json
from qmk.json_schema import json_load
from qmk.keymap import list_keymaps
-from qmk.keyboard import find_readme, list_keyboards
+from qmk.keyboard import find_readme, list_keyboards, keyboard_alias_definitions
from qmk.keycodes import load_spec, list_versions, list_languages
DATA_PATH = Path('data')
@@ -166,7 +166,7 @@ def generate_api(cli):
# Generate data for the global files
keyboard_list = sorted(kb_all)
- keyboard_aliases = json_load(Path('data/mappings/keyboard_aliases.hjson'))
+ keyboard_aliases = keyboard_alias_definitions()
keyboard_metadata = {
'last_updated': current_datetime(),
'keyboards': keyboard_list,
diff --git a/lib/python/qmk/cli/generate/compilation_database.py b/lib/python/qmk/cli/generate/compilation_database.py
index 9e5c266516..5100d2b6d2 100755
--- a/lib/python/qmk/cli/generate/compilation_database.py
+++ b/lib/python/qmk/cli/generate/compilation_database.py
@@ -12,7 +12,7 @@ from typing import Dict, Iterator, List, Union
from milc import cli, MILC
-from qmk.commands import create_make_command
+from qmk.commands import find_make
from qmk.constants import QMK_FIRMWARE
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.keyboard import keyboard_completer, keyboard_folder
@@ -76,9 +76,12 @@ def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]:
return records
-def write_compilation_database(keyboard: str, keymap: str, output_path: Path) -> bool:
+def write_compilation_database(keyboard: str = None, keymap: str = None, output_path: Path = QMK_FIRMWARE / 'compile_commands.json', skip_clean: bool = False, command: List[str] = None, **env_vars) -> bool:
# Generate the make command for a specific keyboard/keymap.
- command = create_make_command(keyboard, keymap, dry_run=True)
+ if not command:
+ from qmk.build_targets import KeyboardKeymapBuildTarget # Lazy load due to circular references
+ target = KeyboardKeymapBuildTarget(keyboard, keymap)
+ command = target.compile_command(dry_run=True, **env_vars)
if not command:
cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
@@ -90,9 +93,10 @@ def write_compilation_database(keyboard: str, keymap: str, output_path: Path) ->
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)
+ if not skip_clean:
+ clean_command = [find_make(), "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))
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 64d4db6ffe..00fb1d9585 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -15,7 +15,12 @@ from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
def generate_define(define, value=None):
+ is_keymap = cli.args.filename
value = f' {value}' if value is not None else ''
+ if is_keymap:
+ return f"""
+#undef {define}
+#define {define}{value}"""
return f"""
#ifndef {define}
# define {define}{value}
@@ -67,6 +72,19 @@ def generate_matrix_size(kb_info_json, config_h_lines):
config_h_lines.append(generate_define('MATRIX_ROWS', kb_info_json['matrix_size']['rows']))
+def generate_matrix_masked(kb_info_json, config_h_lines):
+ """"Enable matrix mask if required"""
+ mask_required = False
+
+ if 'matrix_grid' in kb_info_json.get('dip_switch', {}):
+ mask_required = True
+ if 'matrix_grid' in kb_info_json.get('split', {}).get('handedness', {}):
+ mask_required = True
+
+ if mask_required:
+ config_h_lines.append(generate_define('MATRIX_MASKED'))
+
+
def generate_config_items(kb_info_json, config_h_lines):
"""Iterate through the info_config map to generate basic config values.
"""
@@ -75,9 +93,9 @@ def generate_config_items(kb_info_json, config_h_lines):
for config_key, info_dict in info_config_map.items():
info_key = info_dict['info_key']
key_type = info_dict.get('value_type', 'raw')
- to_config = info_dict.get('to_config', True)
+ to_c = info_dict.get('to_c', True)
- if not to_config:
+ if not to_c:
continue
try:
@@ -130,23 +148,11 @@ def generate_encoder_config(encoder_json, config_h_lines, postfix=''):
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(generate_define('SPLIT_HAND_PIN'))
- elif kb_info_json['split']['primary'] == 'matrix_grid':
- config_h_lines.append(generate_define('SPLIT_HAND_MATRIX_GRID', f'{{ {",".join(kb_info_json["split"]["matrix_grid"])} }}'))
- elif kb_info_json['split']['primary'] == 'eeprom':
- config_h_lines.append(generate_define('EE_HANDS'))
+ if 'handedness' in kb_info_json['split']:
+ # TODO: change SPLIT_HAND_MATRIX_GRID to require brackets
+ handedness = kb_info_json['split']['handedness']
+ if 'matrix_grid' in handedness:
+ config_h_lines.append(generate_define('SPLIT_HAND_MATRIX_GRID', ', '.join(handedness['matrix_grid'])))
if 'protocol' in kb_info_json['split'].get('transport', {}):
if kb_info_json['split']['transport']['protocol'] == 'i2c':
@@ -159,10 +165,13 @@ def generate_split_config(kb_info_json, config_h_lines):
generate_encoder_config(kb_info_json['split']['encoder']['right'], config_h_lines, '_RIGHT')
-def generate_led_animations_config(led_feature_json, config_h_lines, prefix):
+def generate_led_animations_config(feature, led_feature_json, config_h_lines, enable_prefix, animation_prefix):
+ if 'animation' in led_feature_json.get('default', {}):
+ config_h_lines.append(generate_define(f'{feature.upper()}_DEFAULT_MODE', f'{animation_prefix}{led_feature_json["default"]["animation"].upper()}'))
+
for animation in led_feature_json.get('animations', {}):
if led_feature_json['animations'][animation]:
- config_h_lines.append(generate_define(f'{prefix}{animation.upper()}'))
+ config_h_lines.append(generate_define(f'{enable_prefix}{animation.upper()}'))
@cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.')
@@ -191,6 +200,8 @@ def generate_config_h(cli):
generate_matrix_size(kb_info_json, config_h_lines)
+ generate_matrix_masked(kb_info_json, config_h_lines)
+
if 'matrix_pins' in kb_info_json:
config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
@@ -201,13 +212,13 @@ def generate_config_h(cli):
generate_split_config(kb_info_json, config_h_lines)
if 'led_matrix' in kb_info_json:
- generate_led_animations_config(kb_info_json['led_matrix'], config_h_lines, 'ENABLE_LED_MATRIX_')
+ generate_led_animations_config('led_matrix', kb_info_json['led_matrix'], config_h_lines, 'ENABLE_LED_MATRIX_', 'LED_MATRIX_')
if 'rgb_matrix' in kb_info_json:
- generate_led_animations_config(kb_info_json['rgb_matrix'], config_h_lines, 'ENABLE_RGB_MATRIX_')
+ generate_led_animations_config('rgb_matrix', kb_info_json['rgb_matrix'], config_h_lines, 'ENABLE_RGB_MATRIX_', 'RGB_MATRIX_')
if 'rgblight' in kb_info_json:
- generate_led_animations_config(kb_info_json['rgblight'], config_h_lines, 'RGBLIGHT_EFFECT_')
+ generate_led_animations_config('rgblight', kb_info_json['rgblight'], config_h_lines, 'RGBLIGHT_EFFECT_', 'RGBLIGHT_MODE_')
# Show the results
dump_lines(cli.args.output, config_h_lines, cli.args.quiet)
diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py
index 9004b41abb..f8a2372cf3 100755
--- a/lib/python/qmk/cli/generate/keyboard_c.py
+++ b/lib/python/qmk/cli/generate/keyboard_c.py
@@ -57,6 +57,32 @@ def _gen_led_config(info_data):
return lines
+def _gen_matrix_mask(info_data):
+ """Convert info.json content to matrix_mask
+ """
+ cols = info_data['matrix_size']['cols']
+ rows = info_data['matrix_size']['rows']
+
+ # Default mask to everything disabled
+ mask = [['0'] * cols for i in range(rows)]
+
+ # Mirror layout macros squashed on top of each other
+ for layout_data in info_data['layouts'].values():
+ for key_data in layout_data['layout']:
+ row, col = key_data['matrix']
+ mask[row][col] = '1'
+
+ lines = []
+ lines.append('#ifdef MATRIX_MASKED')
+ lines.append('__attribute__((weak)) const matrix_row_t matrix_mask[] = {')
+ for i in range(rows):
+ lines.append(f' 0b{"".join(reversed(mask[i]))},')
+ lines.append('};')
+ lines.append('#endif')
+
+ return lines
+
+
@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 keyboard.c for.')
@@ -70,6 +96,7 @@ def generate_keyboard_c(cli):
keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', '']
keyboard_h_lines.extend(_gen_led_config(kb_info_json))
+ keyboard_h_lines.extend(_gen_matrix_mask(kb_info_json))
# Show the results
dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet)
diff --git a/lib/python/qmk/cli/generate/keycodes.py b/lib/python/qmk/cli/generate/keycodes.py
index ed8b6827bd..719fced5d5 100644
--- a/lib/python/qmk/cli/generate/keycodes.py
+++ b/lib/python/qmk/cli/generate/keycodes.py
@@ -94,6 +94,14 @@ def _generate_helpers(lines, keycodes):
hi = keycodes["keycodes"][f'0x{codes[1]:04X}']['key']
lines.append(f'#define IS_{ _translate_group(group).upper() }_KEYCODE(code) ((code) >= {lo} && (code) <= {hi})')
+ lines.append('')
+ lines.append('// Switch statement Helpers')
+ for group, codes in temp.items():
+ lo = keycodes["keycodes"][f'0x{codes[0]:04X}']['key']
+ hi = keycodes["keycodes"][f'0x{codes[1]:04X}']['key']
+ name = f'{ _translate_group(group).upper() }_KEYCODE_RANGE'
+ lines.append(f'#define { name.ljust(35) } {lo} ... {hi}')
+
def _generate_aliases(lines, keycodes):
# Work around ChibiOS ch.h include guard
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index fc272da6c6..5291556109 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -14,6 +14,12 @@ from qmk.path import normpath, FileType
from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE
+def generate_rule(rules_key, rules_value):
+ is_keymap = cli.args.filename
+ rule_assignment_operator = '=' if is_keymap else '?='
+ return f'{rules_key} {rule_assignment_operator} {rules_value}'
+
+
def process_mapping_rule(kb_info_json, rules_key, info_dict):
"""Return the rules.mk line(s) for a mapping rule.
"""
@@ -29,15 +35,15 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
return None
if key_type in ['array', 'list']:
- return f'{rules_key} ?= {" ".join(rules_value)}'
+ return generate_rule(rules_key, " ".join(rules_value))
elif key_type == 'bool':
- return f'{rules_key} ?= {"yes" if rules_value else "no"}'
+ return generate_rule(rules_key, "yes" if rules_value else "no")
elif key_type == 'mapping':
- return '\n'.join([f'{key} ?= {value}' for key, value in rules_value.items()])
+ return '\n'.join([generate_rule(key, value) for key, value in rules_value.items()])
elif key_type == 'str':
- return f'{rules_key} ?= "{rules_value}"'
+ return generate_rule(rules_key, f'"{rules_value}"')
- return f'{rules_key} ?= {rules_value}'
+ return generate_rule(rules_key, rules_value)
@cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.')
@@ -77,21 +83,21 @@ def generate_rules_mk(cli):
for feature, enabled in kb_info_json['features'].items():
feature = feature.upper()
enabled = 'yes' if enabled else 'no'
- rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
+ rules_mk_lines.append(generate_rule(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')
+ rules_mk_lines.append(generate_rule('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')
+ rules_mk_lines.append(generate_rule('CUSTOM_MATRIX', 'lite'))
else:
- rules_mk_lines.append('CUSTOM_MATRIX ?= yes')
+ rules_mk_lines.append(generate_rule('CUSTOM_MATRIX', 'yes'))
if converter:
- rules_mk_lines.append(f'CONVERT_TO ?= {converter}')
+ rules_mk_lines.append(generate_rule('CONVERT_TO', converter))
# Show the results
dump_lines(cli.args.output, rules_mk_lines)
diff --git a/lib/python/qmk/cli/mass_compile.py b/lib/python/qmk/cli/mass_compile.py
index 06e6e411a7..b025f85701 100755
--- a/lib/python/qmk/cli/mass_compile.py
+++ b/lib/python/qmk/cli/mass_compile.py
@@ -3,26 +3,28 @@
This will compile everything in parallel, for testing purposes.
"""
import os
+from typing import List
from pathlib import Path
from subprocess import DEVNULL
from milc import cli
from qmk.constants import QMK_FIRMWARE
-from qmk.commands import _find_make, get_make_parallel_args
+from qmk.commands import find_make, get_make_parallel_args, build_environment
from qmk.search import search_keymap_targets, search_make_targets
+from qmk.build_targets import BuildTarget, JsonKeymapBuildTarget
-def mass_compile_targets(targets, clean, dry_run, no_temp, parallel, env):
+def mass_compile_targets(targets: List[BuildTarget], clean: bool, dry_run: bool, no_temp: bool, parallel: int, **env):
if len(targets) == 0:
return
- make_cmd = _find_make()
+ make_cmd = find_make()
builddir = Path(QMK_FIRMWARE) / '.build'
makefile = builddir / 'parallel_kb_builds.mk'
if dry_run:
cli.log.info('Compilation targets:')
- for target in sorted(targets):
+ for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)):
cli.log.info(f"{{fg_cyan}}qmk compile -kb {target[0]} -km {target[1]}{{fg_reset}}")
else:
if clean:
@@ -30,9 +32,13 @@ def mass_compile_targets(targets, clean, dry_run, no_temp, parallel, env):
builddir.mkdir(parents=True, exist_ok=True)
with open(makefile, "w") as f:
- for target in sorted(targets):
- keyboard_name = target[0]
- keymap_name = target[1]
+ for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)):
+ keyboard_name = target.keyboard
+ keymap_name = target.keymap
+ target.configure(parallel=1) # We ignore parallelism on a per-build basis as we defer to the parent make invocation
+ target.prepare_build(**env) # If we've got json targets, allow them to write out any extra info to .build before we kick off `make`
+ command = target.compile_command(**env)
+ command[0] = '+@$(MAKE)' # Override the make so that we can use jobserver to handle parallelism
keyboard_safe = keyboard_name.replace('/', '_')
build_log = f"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}"
failed_log = f"{QMK_FIRMWARE}/.build/failed.log.{os.getpid()}.{keyboard_safe}.{keymap_name}"
@@ -43,7 +49,7 @@ all: {keyboard_safe}_{keymap_name}_binary
{keyboard_safe}_{keymap_name}_binary:
@rm -f "{build_log}" || true
@echo "Compiling QMK Firmware for target: '{keyboard_name}:{keymap_name}'..." >>"{build_log}"
- +@$(MAKE) -C "{QMK_FIRMWARE}" -f "{QMK_FIRMWARE}/builddefs/build_keyboard.mk" KEYBOARD="{keyboard_name}" KEYMAP="{keymap_name}" COLOR=true SILENT=false {' '.join(env)} \\
+ {' '.join(command)} \\
>>"{build_log}" 2>&1 \\
|| cp "{build_log}" "{failed_log}"
@{{ grep '\[ERRORS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;31m[ERRORS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
@@ -66,7 +72,7 @@ all: {keyboard_safe}_{keymap_name}_binary
# yapf: enable
f.write('\n')
- cli.run([make_cmd, *get_make_parallel_args(parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
+ cli.run([find_make(), *get_make_parallel_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()}.*')]
@@ -95,8 +101,11 @@ def mass_compile(cli):
"""Compile QMK Firmware against all keyboards.
"""
if len(cli.args.builds) > 0:
- targets = search_make_targets(cli.args.builds, cli.args.filter)
+ json_like_targets = list([Path(p) for p in filter(lambda e: Path(e).exists() and Path(e).suffix == '.json', cli.args.builds)])
+ make_like_targets = list(filter(lambda e: Path(e) not in json_like_targets, cli.args.builds))
+ targets = search_make_targets(make_like_targets)
+ targets.extend([JsonKeymapBuildTarget(e) for e in json_like_targets])
else:
targets = search_keymap_targets([('all', cli.config.mass_compile.keymap)], cli.args.filter)
- return mass_compile_targets(targets, cli.args.clean, cli.args.dry_run, cli.config.mass_compile.no_temp, cli.config.mass_compile.parallel, cli.args.env)
+ return mass_compile_targets(targets, cli.args.clean, cli.args.dry_run, cli.config.mass_compile.no_temp, cli.config.mass_compile.parallel, **build_environment(cli.args.env))
diff --git a/lib/python/qmk/cli/new/keymap.py b/lib/python/qmk/cli/new/keymap.py
index 9b0ac221a4..d4339bc9ef 100755
--- a/lib/python/qmk/cli/new/keymap.py
+++ b/lib/python/qmk/cli/new/keymap.py
@@ -5,10 +5,12 @@ import shutil
from milc import cli
from milc.questions import question
+from qmk.constants import HAS_QMK_USERSPACE, QMK_USERSPACE
from qmk.path import is_keyboard, keymaps, keymap
from qmk.git import git_get_username
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.keyboard import keyboard_completer, keyboard_folder
+from qmk.userspace import UserspaceDefs
def prompt_keyboard():
@@ -68,3 +70,9 @@ def new_keymap(cli):
# end message to user
cli.log.info(f'{{fg_green}}Created a new keymap called {{fg_cyan}}{user_name}{{fg_green}} in: {{fg_cyan}}{keymap_path_new}.{{fg_reset}}')
cli.log.info(f"Compile a firmware with your new keymap by typing: {{fg_yellow}}qmk compile -kb {kb_name} -km {user_name}{{fg_reset}}.")
+
+ # Add to userspace compile if we have userspace available
+ if HAS_QMK_USERSPACE:
+ userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json')
+ userspace.add_target(keyboard=kb_name, keymap=user_name, do_print=False)
+ return userspace.save()
diff --git a/lib/python/qmk/cli/userspace/__init__.py b/lib/python/qmk/cli/userspace/__init__.py
new file mode 100644
index 0000000000..5757d3a4c9
--- /dev/null
+++ b/lib/python/qmk/cli/userspace/__init__.py
@@ -0,0 +1,5 @@
+from . import doctor
+from . import add
+from . import remove
+from . import list
+from . import compile
diff --git a/lib/python/qmk/cli/userspace/add.py b/lib/python/qmk/cli/userspace/add.py
new file mode 100644
index 0000000000..8993d54dba
--- /dev/null
+++ b/lib/python/qmk/cli/userspace/add.py
@@ -0,0 +1,51 @@
+# Copyright 2023 Nick Brassel (@tzarc)
+# SPDX-License-Identifier: GPL-2.0-or-later
+from pathlib import Path
+from milc import cli
+
+from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE
+from qmk.keyboard import keyboard_completer, keyboard_folder_or_all
+from qmk.keymap import keymap_completer, is_keymap_target
+from qmk.userspace import UserspaceDefs
+
+
[email protected]('builds', nargs='*', arg_only=True, help="List of builds in form <keyboard>:<keymap>, or path to a keymap JSON file.")
[email protected]('-kb', '--keyboard', type=keyboard_folder_or_all, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
[email protected]('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
[email protected]('Adds a build target to userspace `qmk.json`.')
+def userspace_add(cli):
+ if not HAS_QMK_USERSPACE:
+ cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.')
+ return False
+
+ userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json')
+
+ if len(cli.args.builds) > 0:
+ json_like_targets = list([Path(p) for p in filter(lambda e: Path(e).exists() and Path(e).suffix == '.json', cli.args.builds)])
+ make_like_targets = list(filter(lambda e: Path(e) not in json_like_targets, cli.args.builds))
+
+ for e in json_like_targets:
+ userspace.add_target(json_path=e)
+
+ for e in make_like_targets:
+ s = e.split(':')
+ userspace.add_target(keyboard=s[0], keymap=s[1])
+
+ else:
+ failed = False
+ try:
+ if not is_keymap_target(cli.args.keyboard, cli.args.keymap):
+ failed = True
+ except KeyError:
+ failed = True
+
+ if failed:
+ from qmk.cli.new.keymap import new_keymap
+ cli.config.new_keymap.keyboard = cli.args.keyboard
+ cli.config.new_keymap.keymap = cli.args.keymap
+ if new_keymap(cli) is not False:
+ userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap)
+ else:
+ userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap)
+
+ return userspace.save()
diff --git a/lib/python/qmk/cli/userspace/compile.py b/lib/python/qmk/cli/userspace/compile.py
new file mode 100644
index 0000000000..0a42dd5bf5
--- /dev/null
+++ b/lib/python/qmk/cli/userspace/compile.py
@@ -0,0 +1,38 @@
+# Copyright 2023 Nick Brassel (@tzarc)
+# SPDX-License-Identifier: GPL-2.0-or-later
+from pathlib import Path
+from milc import cli
+
+from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE
+from qmk.commands import build_environment
+from qmk.userspace import UserspaceDefs
+from qmk.build_targets import JsonKeymapBuildTarget
+from qmk.search import search_keymap_targets
+from qmk.cli.mass_compile import mass_compile_targets
+
+
[email protected]('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.")
[email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
[email protected]('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
[email protected]('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the commands to be run.")
[email protected]('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
[email protected]('Compiles the build targets specified in userspace `qmk.json`.')
+def userspace_compile(cli):
+ if not HAS_QMK_USERSPACE:
+ cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.')
+ return False
+
+ userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json')
+
+ build_targets = []
+ keyboard_keymap_targets = []
+ for e in userspace.build_targets:
+ if isinstance(e, Path):
+ build_targets.append(JsonKeymapBuildTarget(e))
+ elif isinstance(e, dict):
+ keyboard_keymap_targets.append((e['keyboard'], e['keymap']))
+
+ if len(keyboard_keymap_targets) > 0:
+ build_targets.extend(search_keymap_targets(keyboard_keymap_targets))
+
+ mass_compile_targets(list(set(build_targets)), cli.args.clean, cli.args.dry_run, cli.config.userspace_compile.no_temp, cli.config.userspace_compile.parallel, **build_environment(cli.args.env))
diff --git a/lib/python/qmk/cli/userspace/doctor.py b/lib/python/qmk/cli/userspace/doctor.py
new file mode 100644
index 0000000000..2b7e29aa7e
--- /dev/null
+++ b/lib/python/qmk/cli/userspace/doctor.py
@@ -0,0 +1,11 @@
+# Copyright 2023 Nick Brassel (@tzarc)
+# SPDX-License-Identifier: GPL-2.0-or-later
+from milc import cli
+
+from qmk.constants import QMK_FIRMWARE
+from qmk.cli.doctor.main import userspace_tests
+
+
[email protected]('Checks userspace configuration.')
+def userspace_doctor(cli):
+ userspace_tests(QMK_FIRMWARE)
diff --git a/lib/python/qmk/cli/userspace/list.py b/lib/python/qmk/cli/userspace/list.py
new file mode 100644
index 0000000000..a63f669dd7
--- /dev/null
+++ b/lib/python/qmk/cli/userspace/list.py
@@ -0,0 +1,51 @@
+# Copyright 2023 Nick Brassel (@tzarc)
+# SPDX-License-Identifier: GPL-2.0-or-later
+from pathlib import Path
+from dotty_dict import Dotty
+from milc import cli
+
+from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE
+from qmk.userspace import UserspaceDefs
+from qmk.build_targets import BuildTarget
+from qmk.keyboard import is_all_keyboards, keyboard_folder
+from qmk.keymap import is_keymap_target
+from qmk.search import search_keymap_targets
+
+
[email protected]('-e', '--expand', arg_only=True, action='store_true', help="Expands any use of `all` for either keyboard or keymap.")
[email protected]('Lists the build targets specified in userspace `qmk.json`.')
+def userspace_list(cli):
+ if not HAS_QMK_USERSPACE:
+ cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.')
+ return False
+
+ userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json')
+
+ if cli.args.expand:
+ build_targets = []
+ for e in userspace.build_targets:
+ if isinstance(e, Path):
+ build_targets.append(e)
+ elif isinstance(e, dict) or isinstance(e, Dotty):
+ build_targets.extend(search_keymap_targets([(e['keyboard'], e['keymap'])]))
+ else:
+ build_targets = userspace.build_targets
+
+ for e in build_targets:
+ if isinstance(e, Path):
+ # JSON keymap from userspace
+ cli.log.info(f'JSON keymap: {{fg_cyan}}{e}{{fg_reset}}')
+ continue
+ elif isinstance(e, dict) or isinstance(e, Dotty):
+ # keyboard/keymap dict from userspace
+ keyboard = e['keyboard']
+ keymap = e['keymap']
+ elif isinstance(e, BuildTarget):
+ # BuildTarget from search_keymap_targets()
+ keyboard = e.keyboard
+ keymap = e.keymap
+
+ if is_all_keyboards(keyboard) or is_keymap_target(keyboard_folder(keyboard), keymap):
+ cli.log.info(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}}')
+ else:
+ cli.log.warn(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}} -- not found!')
diff --git a/lib/python/qmk/cli/userspace/remove.py b/lib/python/qmk/cli/userspace/remove.py
new file mode 100644
index 0000000000..c7d180bfd1
--- /dev/null
+++ b/lib/python/qmk/cli/userspace/remove.py
@@ -0,0 +1,37 @@
+# Copyright 2023 Nick Brassel (@tzarc)
+# SPDX-License-Identifier: GPL-2.0-or-later
+from pathlib import Path
+from milc import cli
+
+from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE
+from qmk.keyboard import keyboard_completer, keyboard_folder_or_all
+from qmk.keymap import keymap_completer
+from qmk.userspace import UserspaceDefs
+
+
[email protected]('builds', nargs='*', arg_only=True, help="List of builds in form <keyboard>:<keymap>, or path to a keymap JSON file.")
[email protected]('-kb', '--keyboard', type=keyboard_folder_or_all, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
[email protected]('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
[email protected]('Removes a build target from userspace `qmk.json`.')
+def userspace_remove(cli):
+ if not HAS_QMK_USERSPACE:
+ cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.')
+ return False
+
+ userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json')
+
+ if len(cli.args.builds) > 0:
+ json_like_targets = list([Path(p) for p in filter(lambda e: Path(e).exists() and Path(e).suffix == '.json', cli.args.builds)])
+ make_like_targets = list(filter(lambda e: Path(e) not in json_like_targets, cli.args.builds))
+
+ for e in json_like_targets:
+ userspace.remove_target(json_path=e)
+
+ for e in make_like_targets:
+ s = e.split(':')
+ userspace.remove_target(keyboard=s[0], keymap=s[1])
+
+ else:
+ userspace.remove_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap)
+
+ return userspace.save()