From 47b9b110097a864d6ab76516b2213afd59948527 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Wed, 30 Dec 2020 10:27:37 -0800
Subject: Configure keyboard matrix from info.json (#10817)

* Make parameters from info.json available to the build system

* move all clueboard settings to info.json

* code formatting

* make flake8 happy

* make flake8 happy

* make qmk lint happy

* Add support for specifying led indicators in json

* move led indicators to the clueboard info.json

* Apply suggestions from code review

Co-authored-by: Erovia <Erovia@users.noreply.github.com>

* add missing docstring

Co-authored-by: Erovia <Erovia@users.noreply.github.com>
---
 lib/python/qmk/cli/c2json.py              |   3 +-
 lib/python/qmk/cli/chibios/confmigrate.py |   8 +-
 lib/python/qmk/cli/generate/__init__.py   |   4 +
 lib/python/qmk/cli/generate/api.py        |  14 +-
 lib/python/qmk/cli/generate/config_h.py   | 277 +++++++++++++++++++++++
 lib/python/qmk/cli/generate/info_json.py  |  49 +++++
 lib/python/qmk/cli/generate/layouts.py    |  93 ++++++++
 lib/python/qmk/cli/generate/rules_mk.py   |  59 +++++
 lib/python/qmk/cli/info.py                |   7 +-
 lib/python/qmk/cli/kle2json.py            |  51 ++---
 lib/python/qmk/constants.py               |  11 +
 lib/python/qmk/info.py                    | 355 +++++++++++++++++++++++++-----
 lib/python/qmk/info_json_encoder.py       |  96 ++++++++
 13 files changed, 920 insertions(+), 107 deletions(-)
 create mode 100755 lib/python/qmk/cli/generate/config_h.py
 create mode 100755 lib/python/qmk/cli/generate/info_json.py
 create mode 100755 lib/python/qmk/cli/generate/layouts.py
 create mode 100755 lib/python/qmk/cli/generate/rules_mk.py
 create mode 100755 lib/python/qmk/info_json_encoder.py

(limited to 'lib')

diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py
index 2b3bb774f7..8f9d8dc383 100644
--- a/lib/python/qmk/cli/c2json.py
+++ b/lib/python/qmk/cli/c2json.py
@@ -6,6 +6,7 @@ from milc import cli
 
 import qmk.keymap
 import qmk.path
+from qmk.info_json_encoder import InfoJSONEncoder
 
 
 @cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c')
@@ -47,7 +48,7 @@ def c2json(cli):
         cli.args.output.parent.mkdir(parents=True, exist_ok=True)
         if cli.args.output.exists():
             cli.args.output.replace(cli.args.output.name + '.bak')
-        cli.args.output.write_text(json.dumps(keymap_json))
+        cli.args.output.write_text(json.dumps(keymap_json, cls=InfoJSONEncoder))
 
         if not cli.args.quiet:
             cli.log.info('Wrote keymap to %s.', cli.args.output)
diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py
index eae294a0c6..b9cfda9614 100644
--- a/lib/python/qmk/cli/chibios/confmigrate.py
+++ b/lib/python/qmk/cli/chibios/confmigrate.py
@@ -13,7 +13,7 @@ def eprint(*args, **kwargs):
     print(*args, file=sys.stderr, **kwargs)
 
 
-fileHeader = """\
+file_header = """\
 /* Copyright 2020 QMK
  *
  * This program is free software: you can redistribute it and/or modify
@@ -77,7 +77,7 @@ def check_diffs(input_defs, reference_defs):
 
 
 def migrate_chconf_h(to_override, outfile):
-    print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
+    print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
 
     for override in to_override:
         print("#define %s %s" % (override[0], override[1]), file=outfile)
@@ -87,7 +87,7 @@ def migrate_chconf_h(to_override, outfile):
 
 
 def migrate_halconf_h(to_override, outfile):
-    print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
+    print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
 
     for override in to_override:
         print("#define %s %s" % (override[0], override[1]), file=outfile)
@@ -97,7 +97,7 @@ def migrate_halconf_h(to_override, outfile):
 
 
 def migrate_mcuconf_h(to_override, outfile):
-    print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
+    print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
 
     print("#include_next <mcuconf.h>\n", file=outfile)
 
diff --git a/lib/python/qmk/cli/generate/__init__.py b/lib/python/qmk/cli/generate/__init__.py
index f9585bfb5c..bd75b044c5 100644
--- a/lib/python/qmk/cli/generate/__init__.py
+++ b/lib/python/qmk/cli/generate/__init__.py
@@ -1,3 +1,7 @@
 from . import api
+from . import config_h
 from . import docs
+from . import info_json
+from . import layouts
 from . import rgb_breathe_table
+from . import rules_mk
diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
index 66db37cb52..6d111f244c 100755
--- a/lib/python/qmk/cli/generate/api.py
+++ b/lib/python/qmk/cli/generate/api.py
@@ -8,6 +8,7 @@ from milc import cli
 
 from qmk.datetime import current_datetime
 from qmk.info import info_json
+from qmk.info_json_encoder import InfoJSONEncoder
 from qmk.keyboard import list_keyboards
 
 
@@ -44,15 +45,16 @@ def generate_api(cli):
         if 'usb' in kb_all['keyboards'][keyboard_name]:
             usb = kb_all['keyboards'][keyboard_name]['usb']
 
-            if usb['vid'] not in usb_list['devices']:
+            if 'vid' in usb and usb['vid'] not in usb_list['devices']:
                 usb_list['devices'][usb['vid']] = {}
 
-            if usb['pid'] not in usb_list['devices'][usb['vid']]:
+            if 'pid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]:
                 usb_list['devices'][usb['vid']][usb['pid']] = {}
 
-            usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb
+            if 'vid' in usb and 'pid' in usb:
+                usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb
 
     # Write the global JSON files
-    keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])}))
-    keyboard_all.write_text(json.dumps(kb_all))
-    usb_file.write_text(json.dumps(usb_list))
+    keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])}, cls=InfoJSONEncoder))
+    keyboard_all.write_text(json.dumps(kb_all, cls=InfoJSONEncoder))
+    usb_file.write_text(json.dumps(usb_list, cls=InfoJSONEncoder))
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
new file mode 100755
index 0000000000..4d734017a1
--- /dev/null
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -0,0 +1,277 @@
+"""Used by the make system to generate info_config.h from info.json.
+"""
+from milc import cli
+
+from qmk.constants import LED_INDICATORS
+from qmk.decorators import automagic_keyboard, automagic_keymap
+from qmk.info import info_json, rgblight_animations, rgblight_properties, rgblight_toggles
+from qmk.path import is_keyboard, normpath
+
+usb_properties = {
+    'vid': 'VENDOR_ID',
+    'pid': 'PRODUCT_ID',
+    'device_ver': 'DEVICE_VER',
+}
+
+
+def debounce(debounce):
+    """Return the config.h lines that set debounce
+    """
+    return """
+#ifndef DEBOUNCE
+#   define DEBOUNCE %s
+#endif // DEBOUNCE
+""" % debounce
+
+
+def diode_direction(diode_direction):
+    """Return the config.h lines that set diode direction
+    """
+    return """
+#ifndef DIODE_DIRECTION
+#   define DIODE_DIRECTION %s
+#endif // DIODE_DIRECTION
+""" % diode_direction
+
+
+def keyboard_name(keyboard_name):
+    """Return the config.h lines that set the keyboard's name.
+    """
+    return """
+#ifndef DESCRIPTION
+#    define DESCRIPTION %s
+#endif // DESCRIPTION
+
+#ifndef PRODUCT
+#    define PRODUCT %s
+#endif // PRODUCT
+""" % (keyboard_name, keyboard_name)
+
+
+def manufacturer(manufacturer):
+    """Return the config.h lines that set the manufacturer.
+    """
+    return """
+#ifndef MANUFACTURER
+#    define MANUFACTURER %s
+#endif // MANUFACTURER
+""" % (manufacturer)
+
+
+def direct_pins(direct_pins):
+    """Return the config.h lines that set the direct pins.
+    """
+    rows = []
+
+    for row in direct_pins:
+        cols = ','.join([col or 'NO_PIN' for col in row])
+        rows.append('{' + cols + '}')
+
+    col_count = len(direct_pins[0])
+    row_count = len(direct_pins)
+
+    return """
+#ifndef MATRIX_COLS
+#    define MATRIX_COLS %s
+#endif // MATRIX_COLS
+
+#ifndef MATRIX_ROWS
+#    define MATRIX_ROWS %s
+#endif // MATRIX_ROWS
+
+#ifndef DIRECT_PINS
+#    define DIRECT_PINS {%s}
+#endif // DIRECT_PINS
+""" % (col_count, row_count, ','.join(rows))
+
+
+def col_pins(col_pins):
+    """Return the config.h lines that set the column pins.
+    """
+    cols = ','.join(col_pins)
+    col_num = len(col_pins)
+
+    return """
+#ifndef MATRIX_COLS
+#    define MATRIX_COLS %s
+#endif // MATRIX_COLS
+
+#ifndef MATRIX_COL_PINS
+#    define MATRIX_COL_PINS {%s}
+#endif // MATRIX_COL_PINS
+""" % (col_num, cols)
+
+
+def row_pins(row_pins):
+    """Return the config.h lines that set the row pins.
+    """
+    rows = ','.join(row_pins)
+    row_num = len(row_pins)
+
+    return """
+#ifndef MATRIX_ROWS
+#    define MATRIX_ROWS %s
+#endif // MATRIX_ROWS
+
+#ifndef MATRIX_ROW_PINS
+#    define MATRIX_ROW_PINS {%s}
+#endif // MATRIX_ROW_PINS
+""" % (row_num, rows)
+
+
+def indicators(config):
+    """Return the config.h lines that setup LED indicators.
+    """
+    defines = []
+
+    for led, define in LED_INDICATORS.items():
+        if led in config:
+            defines.append('')
+            defines.append('#ifndef %s' % (define,))
+            defines.append('#   define %s %s' % (define, config[led]))
+            defines.append('#endif // %s' % (define,))
+
+    return '\n'.join(defines)
+
+
+def layout_aliases(layout_aliases):
+    """Return the config.h lines that setup layout aliases.
+    """
+    defines = []
+
+    for alias, layout in layout_aliases.items():
+        defines.append('')
+        defines.append('#ifndef %s' % (alias,))
+        defines.append('#   define %s %s' % (alias, layout))
+        defines.append('#endif // %s' % (alias,))
+
+    return '\n'.join(defines)
+
+
+def matrix_pins(matrix_pins):
+    """Add the matrix config to the config.h.
+    """
+    pins = []
+
+    if 'direct' in matrix_pins:
+        pins.append(direct_pins(matrix_pins['direct']))
+
+    if 'cols' in matrix_pins:
+        pins.append(col_pins(matrix_pins['cols']))
+
+    if 'rows' in matrix_pins:
+        pins.append(row_pins(matrix_pins['rows']))
+
+    return '\n'.join(pins)
+
+
+def rgblight(config):
+    """Return the config.h lines that setup rgblight.
+    """
+    rgblight_config = []
+
+    for json_key, config_key in rgblight_properties.items():
+        if json_key in config:
+            rgblight_config.append('')
+            rgblight_config.append('#ifndef %s' % (config_key,))
+            rgblight_config.append('#   define %s %s' % (config_key, config[json_key]))
+            rgblight_config.append('#endif // %s' % (config_key,))
+
+    for json_key, config_key in rgblight_toggles.items():
+        if config.get(json_key):
+            rgblight_config.append('')
+            rgblight_config.append('#ifndef %s' % (config_key,))
+            rgblight_config.append('#   define %s' % (config_key,))
+            rgblight_config.append('#endif // %s' % (config_key,))
+
+    for json_key, config_key in rgblight_animations.items():
+        if 'animations' in config and config['animations'].get(json_key):
+            rgblight_config.append('')
+            rgblight_config.append('#ifndef %s' % (config_key,))
+            rgblight_config.append('#   define %s' % (config_key,))
+            rgblight_config.append('#endif // %s' % (config_key,))
+
+    return '\n'.join(rgblight_config)
+
+
+def usb_properties(usb_props):
+    """Return the config.h lines that setup USB params.
+    """
+    usb_lines = []
+
+    for info_name, config_name in usb_props.items():
+        if info_name in usb_props:
+            usb_lines.append('')
+            usb_lines.append('#ifndef ' + config_name)
+            usb_lines.append('#    define %s %s' % (config_name, usb_props[info_name]))
+            usb_lines.append('#endif // ' + config_name)
+
+    return '\n'.join(usb_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', help='Keyboard to generate config.h for.')
+@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
+@automagic_keyboard
+@automagic_keymap
+def generate_config_h(cli):
+    """Generates the info_config.h file.
+    """
+    # Determine our keyboard(s)
+    if not cli.config.generate_config_h.keyboard:
+        cli.log.error('Missing paramater: --keyboard')
+        cli.subcommands['info'].print_help()
+        return False
+
+    if not is_keyboard(cli.config.generate_config_h.keyboard):
+        cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
+        return False
+
+    # Build the info.json file
+    kb_info_json = info_json(cli.config.generate_config_h.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']
+
+    if 'debounce' in kb_info_json:
+        config_h_lines.append(debounce(kb_info_json['debounce']))
+
+    if 'diode_direction' in kb_info_json:
+        config_h_lines.append(diode_direction(kb_info_json['diode_direction']))
+
+    if 'indicators' in kb_info_json:
+        config_h_lines.append(indicators(kb_info_json['indicators']))
+
+    if 'keyboard_name' in kb_info_json:
+        config_h_lines.append(keyboard_name(kb_info_json['keyboard_name']))
+
+    if 'layout_aliases' in kb_info_json:
+        config_h_lines.append(layout_aliases(kb_info_json['layout_aliases']))
+
+    if 'manufacturer' in kb_info_json:
+        config_h_lines.append(manufacturer(kb_info_json['manufacturer']))
+
+    if 'rgblight' in kb_info_json:
+        config_h_lines.append(rgblight(kb_info_json['rgblight']))
+
+    if 'matrix_pins' in kb_info_json:
+        config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
+
+    if 'usb' in kb_info_json:
+        config_h_lines.append(usb_properties(kb_info_json['usb']))
+
+    # Show the results
+    config_h = '\n'.join(config_h_lines)
+
+    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.name + '.bak')
+        cli.args.output.write_text(config_h)
+
+        if not cli.args.quiet:
+            cli.log.info('Wrote info_config.h to %s.', cli.args.output)
+
+    else:
+        print(config_h)
diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
new file mode 100755
index 0000000000..7e6654e45d
--- /dev/null
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -0,0 +1,49 @@
+"""Keyboard information script.
+
+Compile an info.json for a particular keyboard and pretty-print it.
+"""
+import json
+
+from milc import cli
+
+from qmk.info_json_encoder import InfoJSONEncoder
+from qmk.decorators import automagic_keyboard, automagic_keymap
+from qmk.info import info_json
+from qmk.path import is_keyboard
+
+
+@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')
+@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
+@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
+@automagic_keyboard
+@automagic_keymap
+def generate_info_json(cli):
+    """Generate an info.json file for a keyboard
+    """
+    # Determine our keyboard(s)
+    if not cli.config.generate_info_json.keyboard:
+        cli.log.error('Missing paramater: --keyboard')
+        cli.subcommands['info'].print_help()
+        return False
+
+    if not is_keyboard(cli.config.generate_info_json.keyboard):
+        cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
+        return False
+
+    # Build the info.json file
+    kb_info_json = info_json(cli.config.generate_info_json.keyboard)
+    pared_down_json = {}
+
+    for key in ('manufacturer', 'maintainer', 'usb', 'keyboard_name', 'width', 'height', 'debounce', 'diode_direction', 'features', 'community_layouts', 'layout_aliases', 'matrix_pins', 'rgblight', 'url'):
+        if key in kb_info_json:
+            pared_down_json[key] = kb_info_json[key]
+
+    pared_down_json['layouts'] = {}
+    if 'layouts' in pared_down_json:
+        for layout_name, layout in kb_info_json['layouts'].items():
+            pared_down_json['layouts'][layout_name] = {}
+            pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))
+            pared_down_json['layouts'][layout_name]['layout'] = layout['layout']
+
+    # Display the results
+    print(json.dumps(pared_down_json, indent=2, cls=InfoJSONEncoder))
diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py
new file mode 100755
index 0000000000..809f0ef7ec
--- /dev/null
+++ b/lib/python/qmk/cli/generate/layouts.py
@@ -0,0 +1,93 @@
+"""Used by the make system to generate layouts.h from info.json.
+"""
+from milc import cli
+
+from qmk.constants import COL_LETTERS, ROW_LETTERS
+from qmk.decorators import automagic_keyboard, automagic_keymap
+from qmk.info import info_json
+from qmk.path import is_keyboard, normpath
+
+usb_properties = {
+    'vid': 'VENDOR_ID',
+    'pid': 'PRODUCT_ID',
+    'device_ver': 'DEVICE_VER',
+}
+
+
+@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', help='Keyboard to generate config.h for.')
+@cli.subcommand('Used by the make system to generate layouts.h from info.json', hidden=True)
+@automagic_keyboard
+@automagic_keymap
+def generate_layouts(cli):
+    """Generates the layouts.h file.
+    """
+    # Determine our keyboard(s)
+    if not cli.config.generate_layouts.keyboard:
+        cli.log.error('Missing paramater: --keyboard')
+        cli.subcommands['info'].print_help()
+        return False
+
+    if not is_keyboard(cli.config.generate_layouts.keyboard):
+        cli.log.error('Invalid keyboard: "%s"', cli.config.generate_layouts.keyboard)
+        return False
+
+    # Build the info.json file
+    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']
+
+    if 'direct' in kb_info_json['matrix_pins']:
+        col_num = len(kb_info_json['matrix_pins']['direct'][0])
+        row_num = len(kb_info_json['matrix_pins']['direct'])
+    elif 'cols' in kb_info_json['matrix_pins'] and 'rows' in kb_info_json['matrix_pins']:
+        col_num = len(kb_info_json['matrix_pins']['cols'])
+        row_num = len(kb_info_json['matrix_pins']['rows'])
+    else:
+        cli.log.error('%s: Invalid matrix config.', cli.config.generate_layouts.keyboard)
+        return False
+
+    for layout_name in kb_info_json['layouts']:
+        if kb_info_json['layouts'][layout_name]['c_macro']:
+            continue
+
+        layout_keys = []
+        layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)]
+
+        for i, key in enumerate(kb_info_json['layouts'][layout_name]['layout']):
+            row = key['matrix'][0]
+            col = key['matrix'][1]
+            identifier = 'k%s%s' % (ROW_LETTERS[row], COL_LETTERS[col])
+
+            try:
+                layout_matrix[row][col] = identifier
+                layout_keys.append(identifier)
+            except IndexError:
+                key_name = key.get('label', identifier)
+                cli.log.error('Matrix data out of bounds for layout %s at index %s (%s): %s, %s', layout_name, i, key_name, row, col)
+                return False
+
+        layouts_h_lines.append('')
+        layouts_h_lines.append('#define %s(%s) {\\' % (layout_name, ', '.join(layout_keys)))
+
+        rows = ', \\\n'.join(['\t {' + ', '.join(row) + '}' for row in layout_matrix])
+        rows += ' \\'
+        layouts_h_lines.append(rows)
+        layouts_h_lines.append('}')
+
+    # Show the results
+    layouts_h = '\n'.join(layouts_h_lines) + '\n'
+
+    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.name + '.bak')
+        cli.args.output.write_text(layouts_h)
+
+        if not cli.args.quiet:
+            cli.log.info('Wrote info_config.h to %s.', cli.args.output)
+
+    else:
+        print(layouts_h)
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
new file mode 100755
index 0000000000..4268ae047b
--- /dev/null
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -0,0 +1,59 @@
+"""Used by the make system to generate a rules.mk
+"""
+from milc import cli
+
+from qmk.decorators import automagic_keyboard, automagic_keymap
+from qmk.info import info_json
+from qmk.path import is_keyboard, normpath
+
+
+@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
+@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
+@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
+@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
+@automagic_keyboard
+@automagic_keymap
+def generate_rules_mk(cli):
+    """Generates a rules.mk file from info.json.
+    """
+    # Determine our keyboard(s)
+    if not cli.config.generate_rules_mk.keyboard:
+        cli.log.error('Missing paramater: --keyboard')
+        cli.subcommands['info'].print_help()
+        return False
+
+    if not is_keyboard(cli.config.generate_rules_mk.keyboard):
+        cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard)
+        return False
+
+    # Build the info.json file
+    kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
+    rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
+
+    # Find features that should be enabled
+    if 'features' in kb_info_json:
+        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}')
+
+    # Add community layouts
+    if 'community_layouts' in kb_info_json:
+        rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')
+
+    # Show the results
+    rules_mk = '\n'.join(rules_mk_lines) + '\n'
+
+    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.name + '.bak')
+        cli.args.output.write_text(rules_mk)
+
+        if cli.args.quiet:
+            print(cli.args.output)
+        else:
+            cli.log.info('Wrote info_config.h to %s.', cli.args.output)
+
+    else:
+        print(rules_mk)
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 9ab299a21e..87d7253d4b 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -7,6 +7,8 @@ import platform
 
 from milc import cli
 
+from qmk.info_json_encoder import InfoJSONEncoder
+from qmk.constants import COL_LETTERS, ROW_LETTERS
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.keyboard import render_layouts, render_layout
 from qmk.keymap import locate_keymap
@@ -15,9 +17,6 @@ from qmk.path import is_keyboard
 
 platform_id = platform.platform().lower()
 
-ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
-COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
-
 
 def show_keymap(kb_info_json, title_caps=True):
     """Render the keymap in ascii art.
@@ -149,7 +148,7 @@ def info(cli):
 
     # Output in the requested format
     if cli.args.format == 'json':
-        print(json.dumps(kb_info_json))
+        print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
     elif cli.args.format == 'text':
         print_text_output(kb_info_json)
     elif cli.args.format == 'friendly':
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
index 3d1bb8c43c..66d504bfc2 100755
--- a/lib/python/qmk/cli/kle2json.py
+++ b/lib/python/qmk/cli/kle2json.py
@@ -3,25 +3,12 @@
 import json
 import os
 from pathlib import Path
-from decimal import Decimal
-from collections import OrderedDict
 
 from milc import cli
 from kle2xy import KLE2xy
 
 from qmk.converter import kle2qmk
-
-
-class CustomJSONEncoder(json.JSONEncoder):
-    def default(self, obj):
-        try:
-            if isinstance(obj, Decimal):
-                if obj % 2 in (Decimal(0), Decimal(1)):
-                    return int(obj)
-                return float(obj)
-        except TypeError:
-            pass
-        return json.JSONEncoder.default(self, obj)
+from qmk.info_json_encoder import InfoJSONEncoder
 
 
 @cli.argument('filename', help='The KLE raw txt to convert')
@@ -52,24 +39,22 @@ def kle2json(cli):
         cli.log.error('Could not parse KLE raw data: %s', raw_code)
         cli.log.exception(e)
         return False
-    keyboard = OrderedDict(
-        keyboard_name=kle.name,
-        url='',
-        maintainer='qmk',
-        width=kle.columns,
-        height=kle.rows,
-        layouts={'LAYOUT': {
-            'layout': 'LAYOUT_JSON_HERE'
-        }},
-    )
-    # Initialize keyboard with json encoded from ordered dict
-    keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=CustomJSONEncoder)
-    # Initialize layout with kle2qmk from converter module
-    layout = json.dumps(kle2qmk(kle), separators=(', ', ':'), cls=CustomJSONEncoder)
-    # Replace layout in keyboard json
-    keyboard = keyboard.replace('"LAYOUT_JSON_HERE"', layout)
+    keyboard = {
+        'keyboard_name': kle.name,
+        'url': '',
+        'maintainer': 'qmk',
+        'width': kle.columns,
+        'height': kle.rows,
+        'layouts': {
+            'LAYOUT': {
+                'layout': kle2qmk(kle)
+            }
+        },
+    }
+
     # Write our info.json
-    file = open(out_path / "info.json", "w")
-    file.write(keyboard)
-    file.close()
+    keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=InfoJSONEncoder)
+    info_json_file = out_path / 'info.json'
+
+    info_json_file.write_text(keyboard)
     cli.log.info('Wrote out {fg_cyan}%s/info.json', out_path)
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 94ab68e5e4..675832c506 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -17,3 +17,14 @@ VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
 DATE_FORMAT = '%Y-%m-%d'
 DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
 TIME_FORMAT = '%H:%M:%S'
+
+# Used when generating matrix locations
+COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
+ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
+
+# Mapping between info.json and config.h keys
+LED_INDICATORS = {
+    'caps_lock': 'LED_CAPS_LOCK_PIN',
+    'num_lock': 'LED_NUM_LOCK_PIN',
+    'scrol_lock': 'LED_SCROLL_LOCK_PIN'
+}
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index f476dc666d..d7b128aa66 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -6,13 +6,45 @@ from pathlib import Path
 
 from milc import cli
 
-from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
+from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
 from qmk.c_parse import find_layouts
 from qmk.keyboard import config_h, rules_mk
 from qmk.keymap import list_keymaps
 from qmk.makefile import parse_rules_mk_file
 from qmk.math import compute
 
+rgblight_properties = {
+    'led_count': 'RGBLED_NUM',
+    'pin': 'RGB_DI_PIN',
+    'split_count': 'RGBLED_SPLIT',
+    'max_brightness': 'RGBLIGHT_LIMIT_VAL',
+    'hue_steps': 'RGBLIGHT_HUE_STEP',
+    'saturation_steps': 'RGBLIGHT_SAT_STEP',
+    'brightness_steps': 'RGBLIGHT_VAL_STEP'
+}
+
+rgblight_toggles = {
+    'sleep': 'RGBLIGHT_SLEEP',
+    'split': 'RGBLIGHT_SPLIT',
+}
+
+rgblight_animations = {
+    'all': 'RGBLIGHT_ANIMATIONS',
+    'alternating': 'RGBLIGHT_EFFECT_ALTERNATING',
+    'breathing': 'RGBLIGHT_EFFECT_BREATHING',
+    'christmas': 'RGBLIGHT_EFFECT_CHRISTMAS',
+    'knight': 'RGBLIGHT_EFFECT_KNIGHT',
+    'rainbow_mood': 'RGBLIGHT_EFFECT_RAINBOW_MOOD',
+    'rainbow_swirl': 'RGBLIGHT_EFFECT_RAINBOW_SWIRL',
+    'rgb_test': 'RGBLIGHT_EFFECT_RGB_TEST',
+    'snake': 'RGBLIGHT_EFFECT_SNAKE',
+    'static_gradient': 'RGBLIGHT_EFFECT_STATIC_GRADIENT',
+    'twinkle': 'RGBLIGHT_EFFECT_TWINKLE'
+}
+
+true_values = ['1', 'on', 'yes']
+false_values = ['0', 'off', 'no']
+
 
 def info_json(keyboard):
     """Generate the info.json data for a specific keyboard.
@@ -38,8 +70,9 @@ def info_json(keyboard):
         info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
 
     # Populate layout data
-    for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items():
+    for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items():
         if not layout_name.startswith('LAYOUT_kc'):
+            layout_json['c_macro'] = True
             info_data['layouts'][layout_name] = layout_json
 
     # Merge in the data from info.json, config.h, and rules.mk
@@ -47,34 +80,179 @@ def info_json(keyboard):
     info_data = _extract_config_h(info_data)
     info_data = _extract_rules_mk(info_data)
 
+    # Make sure we have at least one layout
+    if not info_data.get('layouts'):
+        _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
+
+    # Make sure we supply layout macros for the community layouts we claim to support
+    # FIXME(skullydazed): This should be populated into info.json and read from there instead
+    if 'LAYOUTS' in rules and info_data.get('layouts'):
+        # Match these up against the supplied layouts
+        supported_layouts = rules['LAYOUTS'].strip().split()
+        for layout_name in sorted(info_data['layouts']):
+            layout_name = layout_name[7:]
+
+            if layout_name in supported_layouts:
+                supported_layouts.remove(layout_name)
+
+        if supported_layouts:
+            for supported_layout in supported_layouts:
+                _log_error(info_data, 'Claims to support community layout %s but no LAYOUT_%s() macro found' % (supported_layout, supported_layout))
+
     return info_data
 
 
-def _extract_config_h(info_data):
-    """Pull some keyboard information from existing rules.mk files
+def _extract_debounce(info_data, config_c):
+    """Handle debounce.
+    """
+    if 'debounce' in info_data and 'DEBOUNCE' in config_c:
+        _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
+
+    if 'DEBOUNCE' in config_c:
+        info_data['debounce'] = config_c.get('DEBOUNCE')
+
+    return info_data
+
+
+def _extract_diode_direction(info_data, config_c):
+    """Handle the diode direction.
+    """
+    if 'diode_direction' in info_data and 'DIODE_DIRECTION' in config_c:
+        _log_warning(info_data, 'Diode direction is specified in both info.json and config.h, the config.h value wins.')
+
+    if 'DIODE_DIRECTION' in config_c:
+        info_data['diode_direction'] = config_c.get('DIODE_DIRECTION')
+
+    return info_data
+
+
+def _extract_indicators(info_data, config_c):
+    """Find the LED indicator information.
+    """
+    for json_key, config_key in LED_INDICATORS.items():
+        if json_key in info_data.get('indicators', []) and config_key in config_c:
+            _log_warning(info_data, f'Indicator {json_key} is specified in both info.json and config.h, the config.h value wins.')
+
+        if config_key in config_c:
+            info_data['indicators'][json_key] = config_c.get(config_key)
+
+    return info_data
+
+
+def _extract_community_layouts(info_data, rules):
+    """Find the community layouts in rules.mk.
+    """
+    community_layouts = rules['LAYOUTS'].split() if 'LAYOUTS' in rules else []
+
+    if 'community_layouts' in info_data:
+        for layout in community_layouts:
+            if layout not in info_data['community_layouts']:
+                community_layouts.append(layout)
+
+    else:
+        info_data['community_layouts'] = community_layouts
+
+    return info_data
+
+
+def _extract_features(info_data, rules):
+    """Find all the features enabled in rules.mk.
+    """
+    for key, value in rules.items():
+        if key.endswith('_ENABLE'):
+            key = '_'.join(key.split('_')[:-1]).lower()
+            value = True if value in true_values else False if value in false_values else value
+
+            if 'config_h_features' not in info_data:
+                info_data['config_h_features'] = {}
+
+            if 'features' not in info_data:
+                info_data['features'] = {}
+
+            if key in info_data['features']:
+                _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
+
+            info_data['features'][key] = value
+            info_data['config_h_features'][key] = value
+
+    return info_data
+
+
+def _extract_rgblight(info_data, config_c):
+    """Handle the rgblight configuration
+    """
+    rgblight = info_data.get('rgblight', {})
+    animations = rgblight.get('animations', {})
+
+    for json_key, config_key in rgblight_properties.items():
+        if config_key in config_c:
+            if json_key in rgblight:
+                _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
+
+            rgblight[json_key] = config_c[config_key]
+
+    for json_key, config_key in rgblight_toggles.items():
+        if config_key in config_c:
+            if json_key in rgblight:
+                _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key)
+
+            rgblight[json_key] = config_c[config_key]
+
+    for json_key, config_key in rgblight_animations.items():
+        if config_key in config_c:
+            if json_key in animations:
+                _log_warning(info_data, 'RGB Light: animations: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
+
+            animations[json_key] = config_c[config_key]
+
+    if animations:
+        rgblight['animations'] = animations
+
+    if rgblight:
+        info_data['rgblight'] = rgblight
+
+    return info_data
+
+
+def _extract_matrix_info(info_data, config_c):
+    """Populate the matrix information.
     """
-    config_c = config_h(info_data['keyboard_folder'])
     row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
     col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
     direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
 
-    info_data['diode_direction'] = config_c.get('DIODE_DIRECTION')
-    info_data['matrix_size'] = {
-        'rows': compute(config_c.get('MATRIX_ROWS', '0')),
-        'cols': compute(config_c.get('MATRIX_COLS', '0')),
-    }
-    info_data['matrix_pins'] = {}
+    if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
+        if 'matrix_size' in info_data:
+            _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
 
-    if row_pins:
-        info_data['matrix_pins']['rows'] = row_pins.split(',')
-    if col_pins:
-        info_data['matrix_pins']['cols'] = col_pins.split(',')
+        info_data['matrix_size'] = {
+            'rows': compute(config_c.get('MATRIX_ROWS', '0')),
+            'cols': compute(config_c.get('MATRIX_COLS', '0')),
+        }
+
+    if row_pins and col_pins:
+        if 'matrix_pins' in info_data:
+            _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
+
+        info_data['matrix_pins'] = {}
+
+        if row_pins:
+            info_data['matrix_pins']['rows'] = row_pins.split(',')
+
+        if col_pins:
+            info_data['matrix_pins']['cols'] = col_pins.split(',')
 
     if direct_pins:
+        if 'matrix_pins' in info_data:
+            _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
+
+        info_data['matrix_pins'] = {}
         direct_pin_array = []
+
         for row in direct_pins.split('},{'):
             if row.startswith('{'):
                 row = row[1:]
+
             if row.endswith('}'):
                 row = row[:-1]
 
@@ -86,15 +264,43 @@ def _extract_config_h(info_data):
 
                 direct_pin_array[-1].append(pin)
 
-        info_data['matrix_pins']['direct'] = direct_pin_array
+            info_data['matrix_pins']['direct'] = direct_pin_array
 
-    info_data['usb'] = {
-        'vid': config_c.get('VENDOR_ID'),
-        'pid': config_c.get('PRODUCT_ID'),
-        'device_ver': config_c.get('DEVICE_VER'),
-        'manufacturer': config_c.get('MANUFACTURER'),
-        'product': config_c.get('PRODUCT'),
-    }
+    return info_data
+
+
+def _extract_usb_info(info_data, config_c):
+    """Populate the USB information.
+    """
+    usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'}
+
+    if 'usb' not in info_data:
+        info_data['usb'] = {}
+
+    for info_name, config_name in usb_properties.items():
+        if config_name in config_c:
+            if info_name in info_data['usb']:
+                _log_warning(info_data, '%s in config.h is overwriting usb.%s in info.json' % (config_name, info_name))
+
+            info_data['usb'][info_name] = config_c[config_name]
+
+        elif info_name not in info_data['usb']:
+            _log_error(info_data, '%s not specified in config.h, and %s not specified in info.json. One is required.' % (config_name, info_name))
+
+    return info_data
+
+
+def _extract_config_h(info_data):
+    """Pull some keyboard information from existing config.h files
+    """
+    config_c = config_h(info_data['keyboard_folder'])
+
+    _extract_debounce(info_data, config_c)
+    _extract_diode_direction(info_data, config_c)
+    _extract_indicators(info_data, config_c)
+    _extract_matrix_info(info_data, config_c)
+    _extract_usb_info(info_data, config_c)
+    _extract_rgblight(info_data, config_c)
 
     return info_data
 
@@ -106,16 +312,52 @@ def _extract_rules_mk(info_data):
     mcu = rules.get('MCU')
 
     if mcu in CHIBIOS_PROCESSORS:
-        return arm_processor_rules(info_data, rules)
+        arm_processor_rules(info_data, rules)
 
     elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
-        return avr_processor_rules(info_data, rules)
+        avr_processor_rules(info_data, rules)
+
+    else:
+        cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu))
+        unknown_processor_rules(info_data, rules)
+
+    _extract_community_layouts(info_data, rules)
+    _extract_features(info_data, rules)
+
+    return info_data
 
-    msg = "Unknown MCU: " + str(mcu)
 
-    _log_warning(info_data, msg)
+def _merge_layouts(info_data, new_info_data):
+    """Merge new_info_data into info_data in an intelligent way.
+    """
+    for layout_name, layout_json in new_info_data['layouts'].items():
+        if layout_name in info_data['layouts']:
+            # Pull in layouts we have a macro for
+            if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
+                msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
+                _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
+            else:
+                for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
+                    key.update(layout_json['layout'][i])
+        else:
+            # Pull in layouts that have matrix data
+            missing_matrix = False
+            for key in layout_json['layout']:
+                if 'matrix' not in key:
+                    missing_matrix = True
+
+            if not missing_matrix:
+                if layout_name in info_data['layouts']:
+                    # Update an existing layout with new data
+                    for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
+                        key.update(layout_json['layout'][i])
 
-    return unknown_processor_rules(info_data, rules)
+                else:
+                    # Copy in the new layout wholesale
+                    layout_json['c_macro'] = False
+                    info_data['layouts'][layout_name] = layout_json
+
+    return info_data
 
 
 def _search_keyboard_h(path):
@@ -131,34 +373,21 @@ def _search_keyboard_h(path):
     return layouts
 
 
-def _find_all_layouts(info_data, keyboard, rules):
+def _find_all_layouts(info_data, keyboard):
     """Looks for layout macros associated with this keyboard.
     """
     layouts = _search_keyboard_h(Path(keyboard))
 
     if not layouts:
-        # If we didn't find any layouts above we widen our search. This is error
-        # prone which is why we want to encourage people to follow the standard above.
-        _log_warning(info_data, 'Falling back to searching for KEYMAP/LAYOUT macros.')
+        # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
+        info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
+
         for file in glob('keyboards/%s/*.h' % keyboard):
             if file.endswith('.h'):
                 these_layouts = find_layouts(file)
                 if these_layouts:
                     layouts.update(these_layouts)
 
-    if 'LAYOUTS' in rules:
-        # Match these up against the supplied layouts
-        supported_layouts = rules['LAYOUTS'].strip().split()
-        for layout_name in sorted(layouts):
-            if not layout_name.startswith('LAYOUT_'):
-                continue
-            layout_name = layout_name[7:]
-            if layout_name in supported_layouts:
-                supported_layouts.remove(layout_name)
-
-        if supported_layouts:
-            _log_error(info_data, 'Missing LAYOUT() macro for %s' % (', '.join(supported_layouts)))
-
     return layouts
 
 
@@ -231,32 +460,40 @@ def merge_info_jsons(keyboard, info_data):
     for info_file in find_info_json(keyboard):
         # Load and validate the JSON data
         try:
-            with info_file.open('r') as info_fd:
-                new_info_data = json.load(info_fd)
+            new_info_data = json.load(info_file.open('r'))
         except Exception as e:
             _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
-            continue
+            new_info_data = {}
 
         if not isinstance(new_info_data, dict):
             _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
             continue
 
         # Copy whitelisted keys into `info_data`
-        for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'):
+        for key in ('debounce', 'diode_direction', 'indicators', 'keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'):
             if key in new_info_data:
                 info_data[key] = new_info_data[key]
 
-        # Merge the layouts in
+        # Deep merge certain keys
+        # FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something.
+        for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'):
+            if key in new_info_data:
+                if key not in info_data:
+                    info_data[key] = {}
+
+                info_data[key].update(new_info_data[key])
+
+        # Merge the layouts
+        if 'community_layouts' in new_info_data:
+            if 'community_layouts' in info_data:
+                for layout in new_info_data['community_layouts']:
+                    if layout not in info_data['community_layouts']:
+                        info_data['community_layouts'].append(layout)
+            else:
+                info_data['community_layouts'] = new_info_data['community_layouts']
+
         if 'layouts' in new_info_data:
-            for layout_name, json_layout in new_info_data['layouts'].items():
-                # Only pull in layouts we have a macro for
-                if layout_name in info_data['layouts']:
-                    if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']):
-                        msg = '%s: Number of elements in info.json does not match! info.json:%s != %s:%s'
-                        _log_error(info_data, msg % (layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
-                    else:
-                        for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
-                            key.update(json_layout['layout'][i])
+            _merge_layouts(info_data, new_info_data)
 
     return info_data
 
diff --git a/lib/python/qmk/info_json_encoder.py b/lib/python/qmk/info_json_encoder.py
new file mode 100755
index 0000000000..60dae7247f
--- /dev/null
+++ b/lib/python/qmk/info_json_encoder.py
@@ -0,0 +1,96 @@
+"""Class that pretty-prints QMK info.json files.
+"""
+import json
+from decimal import Decimal
+
+
+class InfoJSONEncoder(json.JSONEncoder):
+    """Custom encoder to make info.json's a little nicer to work with.
+    """
+    container_types = (list, tuple, dict)
+    indentation_char = " "
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.indentation_level = 0
+
+        if not self.indent:
+            self.indent = 4
+
+    def encode(self, obj):
+        """Encode JSON objects for QMK.
+        """
+        if isinstance(obj, Decimal):
+            if obj == int(obj):  # I can't believe Decimal objects don't have .is_integer()
+                return int(obj)
+            return float(obj)
+
+        elif isinstance(obj, (list, tuple)):
+            if self._primitives_only(obj):
+                return "[" + ", ".join(self.encode(element) for element in obj) + "]"
+
+            else:
+                self.indentation_level += 1
+                output = [self.indent_str + self.encode(element) for element in obj]
+                self.indentation_level -= 1
+                return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]"
+
+        elif isinstance(obj, dict):
+            if obj:
+                if self.indentation_level == 4:
+                    # These are part of a layout, put them on a single line.
+                    return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }"
+
+                else:
+                    self.indentation_level += 1
+                    output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_root_dict)]
+                    self.indentation_level -= 1
+                    return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"
+            else:
+                return "{}"
+        else:
+            return super().encode(obj)
+
+    def _primitives_only(self, obj):
+        """Returns true if the object doesn't have any container type objects (list, tuple, dict).
+        """
+        if isinstance(obj, dict):
+            obj = obj.values()
+
+        return not any(isinstance(element, self.container_types) for element in obj)
+
+    def sort_root_dict(self, key):
+        """Forces layout to the back of the sort order.
+        """
+        key = key[0]
+
+        if self.indentation_level == 1:
+            if key == 'manufacturer':
+                return '10keyboard_name'
+
+            elif key == 'keyboard_name':
+                return '11keyboard_name'
+
+            elif key == 'maintainer':
+                return '12maintainer'
+
+            elif key in ('height', 'width'):
+                return '40' + str(key)
+
+            elif key == 'community_layouts':
+                return '97community_layouts'
+
+            elif key == 'layout_aliases':
+                return '98layout_aliases'
+
+            elif key == 'layouts':
+                return '99layouts'
+
+            else:
+                return '50' + str(key)
+
+        return key
+
+    @property
+    def indent_str(self):
+        return self.indentation_char * (self.indentation_level * self.indent)
-- 
cgit v1.2.3


From f27d8d94489342d4ce7ba9955cd062c725350db9 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Sat, 2 Jan 2021 12:05:56 -0800
Subject: Fix compiling on develop (#11409)

---
 lib/python/qmk/cli/generate/config_h.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 4d734017a1..e160ffea83 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -7,7 +7,7 @@ from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.info import info_json, rgblight_animations, rgblight_properties, rgblight_toggles
 from qmk.path import is_keyboard, normpath
 
-usb_properties = {
+usb_prop_map = {
     'vid': 'VENDOR_ID',
     'pid': 'PRODUCT_ID',
     'device_ver': 'DEVICE_VER',
@@ -199,7 +199,7 @@ def usb_properties(usb_props):
     """
     usb_lines = []
 
-    for info_name, config_name in usb_props.items():
+    for info_name, config_name in usb_prop_map.items():
         if info_name in usb_props:
             usb_lines.append('')
             usb_lines.append('#ifndef ' + config_name)
-- 
cgit v1.2.3


From 11bd98f684148ed9577b263189121e52027d66d9 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Sat, 2 Jan 2021 18:08:17 -0800
Subject: Fix broken keyboards (#11412)

* Fix a couple errors

* add a dependency for the generated headers
---
 lib/python/qmk/cli/generate/layouts.py | 19 ++++++++++---------
 lib/python/qmk/info.py                 |  3 +++
 2 files changed, 13 insertions(+), 9 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py
index 809f0ef7ec..273870e15c 100755
--- a/lib/python/qmk/cli/generate/layouts.py
+++ b/lib/python/qmk/cli/generate/layouts.py
@@ -39,15 +39,16 @@ def generate_layouts(cli):
     # Build the layouts.h file.
     layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.' ' */', '', '#pragma once']
 
-    if 'direct' in kb_info_json['matrix_pins']:
-        col_num = len(kb_info_json['matrix_pins']['direct'][0])
-        row_num = len(kb_info_json['matrix_pins']['direct'])
-    elif 'cols' in kb_info_json['matrix_pins'] and 'rows' in kb_info_json['matrix_pins']:
-        col_num = len(kb_info_json['matrix_pins']['cols'])
-        row_num = len(kb_info_json['matrix_pins']['rows'])
-    else:
-        cli.log.error('%s: Invalid matrix config.', cli.config.generate_layouts.keyboard)
-        return False
+    if 'matrix_pins' in kb_info_json:
+        if 'direct' in kb_info_json['matrix_pins']:
+            col_num = len(kb_info_json['matrix_pins']['direct'][0])
+            row_num = len(kb_info_json['matrix_pins']['direct'])
+        elif 'cols' in kb_info_json['matrix_pins'] and 'rows' in kb_info_json['matrix_pins']:
+            col_num = len(kb_info_json['matrix_pins']['cols'])
+            row_num = len(kb_info_json['matrix_pins']['rows'])
+        else:
+            cli.log.error('%s: Invalid matrix config.', cli.config.generate_layouts.keyboard)
+            return False
 
     for layout_name in kb_info_json['layouts']:
         if kb_info_json['layouts'][layout_name]['c_macro']:
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index d7b128aa66..2954a17e09 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -134,6 +134,9 @@ def _extract_indicators(info_data, config_c):
             _log_warning(info_data, f'Indicator {json_key} is specified in both info.json and config.h, the config.h value wins.')
 
         if config_key in config_c:
+            if 'indicators' not in info_data:
+                info_data['indicators'] = {}
+
             info_data['indicators'][json_key] = config_c.get(config_key)
 
     return info_data
-- 
cgit v1.2.3


From 6daa204363dfaa7f31d935a9913d6b35ec22e971 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Sun, 3 Jan 2021 15:45:06 -0800
Subject: fix keyboards with apostrophes in their names

---
 lib/python/qmk/cli/generate/config_h.py | 4 ++--
 lib/python/qmk/info.py                  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index e160ffea83..09aa0041f4 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -45,7 +45,7 @@ def keyboard_name(keyboard_name):
 #ifndef PRODUCT
 #    define PRODUCT %s
 #endif // PRODUCT
-""" % (keyboard_name, keyboard_name)
+""" % (keyboard_name.replace("'", ""), keyboard_name.replace("'", ""))
 
 
 def manufacturer(manufacturer):
@@ -55,7 +55,7 @@ def manufacturer(manufacturer):
 #ifndef MANUFACTURER
 #    define MANUFACTURER %s
 #endif // MANUFACTURER
-""" % (manufacturer)
+""" % (manufacturer.replace("'", ""))
 
 
 def direct_pins(direct_pins):
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 2954a17e09..4611874e85 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -345,7 +345,7 @@ def _merge_layouts(info_data, new_info_data):
         else:
             # Pull in layouts that have matrix data
             missing_matrix = False
-            for key in layout_json['layout']:
+            for key in layout_json.get('layout', {}):
                 if 'matrix' not in key:
                     missing_matrix = True
 
-- 
cgit v1.2.3


From ededff8556daff544633cb143cb6d939afd09014 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Tue, 1 Dec 2020 12:52:02 -0800
Subject: validate keyboard data with jsonschema

---
 lib/python/qmk/cli/generate/info_json.py |   2 +-
 lib/python/qmk/cli/generate/rules_mk.py  |  13 +++
 lib/python/qmk/info.py                   | 151 ++++++++++++++++++++++++++++---
 3 files changed, 154 insertions(+), 12 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
index 7e6654e45d..fba4b1c014 100755
--- a/lib/python/qmk/cli/generate/info_json.py
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -39,7 +39,7 @@ def generate_info_json(cli):
             pared_down_json[key] = kb_info_json[key]
 
     pared_down_json['layouts'] = {}
-    if 'layouts' in pared_down_json:
+    if 'layouts' in kb_info_json:
         for layout_name, layout in kb_info_json['layouts'].items():
             pared_down_json['layouts'][layout_name] = {}
             pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 4268ae047b..72ed3c45fa 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -6,6 +6,10 @@ from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.info import info_json
 from qmk.path import is_keyboard, normpath
 
+info_to_rules = {
+    'bootloader': 'BOOTLOADER',
+    'processor': 'MCU'
+}
 
 @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")
@@ -30,6 +34,10 @@ def generate_rules_mk(cli):
     kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
     rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
 
+    # Bring in settings
+    for info_key, rule_key in info_to_rules.items():
+        rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}')
+
     # Find features that should be enabled
     if 'features' in kb_info_json:
         for feature, enabled in kb_info_json['features'].items():
@@ -37,6 +45,11 @@ def generate_rules_mk(cli):
             enabled = 'yes' if enabled else 'no'
             rules_mk_lines.append(f'{feature}_ENABLE := {enabled}')
 
+    # Set the LED driver
+    if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
+        driver = kb_info_json['led_matrix']['driver']
+        rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}')
+
     # Add community layouts
     if 'community_layouts' in kb_info_json:
         rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 4611874e85..1cf12190d6 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -4,6 +4,7 @@ import json
 from glob import glob
 from pathlib import Path
 
+import jsonschema
 from milc import cli
 
 from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
@@ -13,6 +14,17 @@ from qmk.keymap import list_keymaps
 from qmk.makefile import parse_rules_mk_file
 from qmk.math import compute
 
+led_matrix_properties = {
+    'driver_count': 'LED_DRIVER_COUNT',
+    'driver_addr1': 'LED_DRIVER_ADDR_1',
+    'driver_addr2': 'LED_DRIVER_ADDR_2',
+    'driver_addr3': 'LED_DRIVER_ADDR_3',
+    'driver_addr4': 'LED_DRIVER_ADDR_4',
+    'led_count': 'LED_DRIVER_LED_COUNT',
+    'timeout': 'ISSI_TIMEOUT',
+    'persistence': 'ISSI_PERSISTENCE'
+}
+
 rgblight_properties = {
     'led_count': 'RGBLED_NUM',
     'pin': 'RGB_DI_PIN',
@@ -80,6 +92,15 @@ def info_json(keyboard):
     info_data = _extract_config_h(info_data)
     info_data = _extract_rules_mk(info_data)
 
+    # Validate against the jsonschema
+    try:
+        keyboard_api_validate(info_data)
+
+    except jsonschema.ValidationError as e:
+        cli.log.error('Invalid info.json data: %s', e.message)
+        print(dir(e))
+        exit()
+
     # Make sure we have at least one layout
     if not info_data.get('layouts'):
         _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
@@ -102,6 +123,50 @@ def info_json(keyboard):
     return info_data
 
 
+def _json_load(json_file):
+    """Load a json file from disk.
+
+    Note: file must be a Path object.
+    """
+    try:
+        return json.load(json_file.open())
+
+    except json.decoder.JSONDecodeError as e:
+        cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
+        exit(1)
+
+
+def _jsonschema(schema_name):
+    """Read a jsonschema file from disk.
+    """
+    schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
+
+    if not schema_path.exists():
+        schema_path = Path('data/schemas/false.jsonschema')
+
+    return _json_load(schema_path)
+
+
+def keyboard_validate(data):
+    """Validates data against the keyboard jsonschema.
+    """
+    schema = _jsonschema('keyboard')
+    validator = jsonschema.Draft7Validator(schema).validate
+
+    return validator(data)
+
+
+def keyboard_api_validate(data):
+    """Validates data against the api_keyboard jsonschema.
+    """
+    base = _jsonschema('keyboard')
+    relative = _jsonschema('api_keyboard')
+    resolver = jsonschema.RefResolver.from_schema(base)
+    validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
+
+    return validator(data)
+
+
 def _extract_debounce(info_data, config_c):
     """Handle debounce.
     """
@@ -109,7 +174,7 @@ def _extract_debounce(info_data, config_c):
         _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
 
     if 'DEBOUNCE' in config_c:
-        info_data['debounce'] = config_c.get('DEBOUNCE')
+        info_data['debounce'] = int(config_c['DEBOUNCE'])
 
     return info_data
 
@@ -181,8 +246,36 @@ def _extract_features(info_data, rules):
     return info_data
 
 
+def _extract_led_drivers(info_data, rules):
+    """Find all the LED drivers set in rules.mk.
+    """
+    if 'LED_MATRIX_DRIVER' in rules:
+        if 'led_matrix' not in info_data:
+            info_data['led_matrix'] = {}
+
+        if info_data['led_matrix'].get('driver'):
+            _log_warning(info_data, 'LED Matrix driver is specified in both info.json and rules.mk, the rules.mk value wins.')
+
+        info_data['led_matrix']['driver'] = rules['LED_MATRIX_DRIVER']
+
+    return info_data
+
+
+def _extract_led_matrix(info_data, config_c):
+    """Handle the led_matrix configuration.
+    """
+    led_matrix = info_data.get('led_matrix', {})
+
+    for json_key, config_key in led_matrix_properties.items():
+        if config_key in config_c:
+            if json_key in led_matrix:
+                _log_warning(info_data, 'LED Matrix: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
+
+            led_matrix[json_key] = config_c[config_key]
+
+
 def _extract_rgblight(info_data, config_c):
-    """Handle the rgblight configuration
+    """Handle the rgblight configuration.
     """
     rgblight = info_data.get('rgblight', {})
     animations = rgblight.get('animations', {})
@@ -303,6 +396,7 @@ def _extract_config_h(info_data):
     _extract_indicators(info_data, config_c)
     _extract_matrix_info(info_data, config_c)
     _extract_usb_info(info_data, config_c)
+    _extract_led_matrix(info_data, config_c)
     _extract_rgblight(info_data, config_c)
 
     return info_data
@@ -326,6 +420,7 @@ def _extract_rules_mk(info_data):
 
     _extract_community_layouts(info_data, rules)
     _extract_features(info_data, rules)
+    _extract_led_drivers(info_data, rules)
 
     return info_data
 
@@ -412,13 +507,28 @@ def arm_processor_rules(info_data, rules):
     """Setup the default info for an ARM board.
     """
     info_data['processor_type'] = 'arm'
-    info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
-    info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
     info_data['protocol'] = 'ChibiOS'
 
-    if info_data['bootloader'] == 'unknown':
+    if 'MCU' in rules:
+        if 'processor' in info_data:
+            _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
+
+        info_data['processor'] = rules['MCU']
+
+    elif 'processor' not in info_data:
+        info_data['processor'] = 'unknown'
+
+    if 'BOOTLOADER' in rules:
+        if 'bootloader' in info_data:
+            _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
+
+        info_data['bootloader'] = rules['BOOTLOADER']
+
+    else:
         if 'STM32' in info_data['processor']:
             info_data['bootloader'] = 'stm32-dfu'
+        else:
+            info_data['bootloader'] = 'unknown'
 
     if 'STM32' in info_data['processor']:
         info_data['platform'] = 'STM32'
@@ -436,9 +546,25 @@ def avr_processor_rules(info_data, rules):
     info_data['processor_type'] = 'avr'
     info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
     info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
-    info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
     info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
 
+    if 'MCU' in rules:
+        if 'processor' in info_data:
+            _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
+
+        info_data['processor'] = rules['MCU']
+
+    elif 'processor' not in info_data:
+        info_data['processor'] = 'unknown'
+
+    if 'BOOTLOADER' in rules:
+        if 'bootloader' in info_data:
+            _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
+
+        info_data['bootloader'] = rules['BOOTLOADER']
+    else:
+        info_data['bootloader'] = 'atmel-dfu'
+
     # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
     # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
 
@@ -463,10 +589,13 @@ def merge_info_jsons(keyboard, info_data):
     for info_file in find_info_json(keyboard):
         # Load and validate the JSON data
         try:
-            new_info_data = json.load(info_file.open('r'))
-        except Exception as e:
-            _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
-            new_info_data = {}
+            new_info_data = _json_load(info_file)
+            keyboard_validate(new_info_data)
+
+        except jsonschema.ValidationError as e:
+            cli.log.error('Invalid info.json data: %s', e.message)
+            cli.log.error('Not including file %s', info_file)
+            continue
 
         if not isinstance(new_info_data, dict):
             _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
@@ -479,7 +608,7 @@ def merge_info_jsons(keyboard, info_data):
 
         # Deep merge certain keys
         # FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something.
-        for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'):
+        for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'):
             if key in new_info_data:
                 if key not in info_data:
                     info_data[key] = {}
-- 
cgit v1.2.3


From b2c26f7cdd4b268e80f98cae7f444956559436ec Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Tue, 1 Dec 2020 16:04:22 -0800
Subject: get qmk generate-api into a good state

---
 lib/python/qmk/c_parse.py               | 26 ++++++++---
 lib/python/qmk/cli/generate/api.py      |  2 +-
 lib/python/qmk/cli/generate/rules_mk.py |  9 ++--
 lib/python/qmk/info.py                  | 82 +++++++++++++++++++++++----------
 4 files changed, 84 insertions(+), 35 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index e41e271a43..67e196f0ea 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -1,12 +1,27 @@
 """Functions for working with config.h files.
 """
 from pathlib import Path
+import re
 
 from milc import cli
 
 from qmk.comment_remover import comment_remover
 
 default_key_entry = {'x': -1, 'y': 0, 'w': 1}
+single_comment_regex = re.compile(r' */[/*].*$')
+multi_comment_regex = re.compile(r'/\*(.|\n)*\*/', re.MULTILINE)
+
+
+def strip_line_comment(string):
+    """Removes comments from a single line string.
+    """
+    return single_comment_regex.sub('', string)
+
+
+def strip_multiline_comment(string):
+    """Removes comments from a single line string.
+    """
+    return multi_comment_regex.sub('', string)
 
 
 def c_source_files(dir_names):
@@ -53,7 +68,8 @@ def find_layouts(file):
             parsed_layout = [_default_key(key) for key in layout.split(',')]
 
             for key in parsed_layout:
-                key['matrix'] = matrix_locations.get(key['label'])
+                if key['label'] in matrix_locations:
+                    key['matrix'] = matrix_locations[key['label']]
 
             parsed_layouts[macro_name] = {
                 'key_count': len(parsed_layout),
@@ -88,12 +104,10 @@ def parse_config_h_file(config_h_file, config_h=None):
     if config_h_file.exists():
         config_h_text = config_h_file.read_text()
         config_h_text = config_h_text.replace('\\\n', '')
+        config_h_text = strip_multiline_comment(config_h_text)
 
         for linenum, line in enumerate(config_h_text.split('\n')):
-            line = line.strip()
-
-            if '//' in line:
-                line = line[:line.index('//')].strip()
+            line = strip_line_comment(line).strip()
 
             if not line:
                 continue
@@ -156,6 +170,6 @@ def _parse_matrix_locations(matrix, file, macro_name):
         row = row.replace('{', '').replace('}', '')
         for col_num, identifier in enumerate(row.split(',')):
             if identifier != 'KC_NO':
-                matrix_locations[identifier] = (row_num, col_num)
+                matrix_locations[identifier] = [row_num, col_num]
 
     return matrix_locations
diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
index 6d111f244c..2fda38fc9a 100755
--- a/lib/python/qmk/cli/generate/api.py
+++ b/lib/python/qmk/cli/generate/api.py
@@ -48,7 +48,7 @@ def generate_api(cli):
             if 'vid' in usb and usb['vid'] not in usb_list['devices']:
                 usb_list['devices'][usb['vid']] = {}
 
-            if 'pid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]:
+            if 'vid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]:
                 usb_list['devices'][usb['vid']][usb['pid']] = {}
 
             if 'vid' in usb and 'pid' in usb:
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 72ed3c45fa..570ef5a0d6 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -41,9 +41,12 @@ def generate_rules_mk(cli):
     # Find features that should be enabled
     if 'features' in kb_info_json:
         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}')
+            if feature == 'bootmagic_lite' and enabled:
+                rules_mk_lines.append(f'BOOTMAGIC_ENABLE := lite')
+            else:
+                feature = feature.upper()
+                enabled = 'yes' if enabled else 'no'
+                rules_mk_lines.append(f'{feature}_ENABLE := {enabled}')
 
     # Set the LED driver
     if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 1cf12190d6..39af88f790 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -26,13 +26,12 @@ led_matrix_properties = {
 }
 
 rgblight_properties = {
-    'led_count': 'RGBLED_NUM',
-    'pin': 'RGB_DI_PIN',
-    'split_count': 'RGBLED_SPLIT',
-    'max_brightness': 'RGBLIGHT_LIMIT_VAL',
-    'hue_steps': 'RGBLIGHT_HUE_STEP',
-    'saturation_steps': 'RGBLIGHT_SAT_STEP',
-    'brightness_steps': 'RGBLIGHT_VAL_STEP'
+    'led_count': ('RGBLED_NUM', int),
+    'pin': ('RGB_DI_PIN', str),
+    'max_brightness': ('RGBLIGHT_LIMIT_VAL', int),
+    'hue_steps': ('RGBLIGHT_HUE_STEP', int),
+    'saturation_steps': ('RGBLIGHT_SAT_STEP', int),
+    'brightness_steps': ('RGBLIGHT_VAL_STEP', int)
 }
 
 rgblight_toggles = {
@@ -54,6 +53,8 @@ rgblight_animations = {
     'twinkle': 'RGBLIGHT_EFFECT_TWINKLE'
 }
 
+usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'}
+
 true_values = ['1', 'on', 'yes']
 false_values = ['0', 'off', 'no']
 
@@ -97,7 +98,8 @@ def info_json(keyboard):
         keyboard_api_validate(info_data)
 
     except jsonschema.ValidationError as e:
-        cli.log.error('Invalid info.json data: %s', e.message)
+        json_path = '.'.join([str(p) for p in e.absolute_path])
+        cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
         print(dir(e))
         exit()
 
@@ -198,6 +200,9 @@ def _extract_indicators(info_data, config_c):
         if json_key in info_data.get('indicators', []) and config_key in config_c:
             _log_warning(info_data, f'Indicator {json_key} is specified in both info.json and config.h, the config.h value wins.')
 
+        if 'indicators' not in info_data:
+            info_data['indicators'] = {}
+
         if config_key in config_c:
             if 'indicators' not in info_data:
                 info_data['indicators'] = {}
@@ -226,10 +231,23 @@ def _extract_community_layouts(info_data, rules):
 def _extract_features(info_data, rules):
     """Find all the features enabled in rules.mk.
     """
+    # Special handling for bootmagic which also supports a "lite" mode.
+    if rules.get('BOOTMAGIC_ENABLE') == 'lite':
+        rules['BOOTMAGIC_LITE_ENABLE'] = 'on'
+        del(rules['BOOTMAGIC_ENABLE'])
+    if rules.get('BOOTMAGIC_ENABLE') == 'full':
+        rules['BOOTMAGIC_ENABLE'] = 'on'
+
+    # Skip non-boolean features we haven't implemented special handling for
+    for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
+        if rules.get(feature):
+            del(rules[feature])
+
+    # Process the rest of the rules as booleans
     for key, value in rules.items():
         if key.endswith('_ENABLE'):
             key = '_'.join(key.split('_')[:-1]).lower()
-            value = True if value in true_values else False if value in false_values else value
+            value = True if value.lower() in true_values else False if value.lower() in false_values else value
 
             if 'config_h_features' not in info_data:
                 info_data['config_h_features'] = {}
@@ -280,12 +298,21 @@ def _extract_rgblight(info_data, config_c):
     rgblight = info_data.get('rgblight', {})
     animations = rgblight.get('animations', {})
 
-    for json_key, config_key in rgblight_properties.items():
+    if 'RGBLED_SPLIT' in config_c:
+        raw_split = config_c.get('RGBLED_SPLIT', '').replace('{', '').replace('}', '').strip()
+        rgblight['split_count'] = [int(i) for i in raw_split.split(',')]
+
+    for json_key, config_key_type in rgblight_properties.items():
+        config_key, config_type = config_key_type
+
         if config_key in config_c:
             if json_key in rgblight:
                 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
 
-            rgblight[json_key] = config_c[config_key]
+            try:
+                rgblight[json_key] = config_type(config_c[config_key])
+            except ValueError as e:
+                cli.log.error('%s: config.h: Could not convert "%s" to %s: %s', info_data['keyboard_folder'], config_c[config_key], config_type.__name__, e)
 
     for json_key, config_key in rgblight_toggles.items():
         if config_key in config_c:
@@ -332,11 +359,16 @@ def _extract_matrix_info(info_data, config_c):
 
         info_data['matrix_pins'] = {}
 
+        # FIXME(skullydazed/anyone): Should really check every pin, not just the first
         if row_pins:
-            info_data['matrix_pins']['rows'] = row_pins.split(',')
+            row_pins = [pin.strip() for pin in row_pins.split(',') if pin]
+            if row_pins[0][0] in 'ABCDEFGHIJK' and row_pins[0][1].isdigit():
+                info_data['matrix_pins']['rows'] = row_pins
 
         if col_pins:
-            info_data['matrix_pins']['cols'] = col_pins.split(',')
+            col_pins = [pin.strip() for pin in col_pins.split(',') if pin]
+            if col_pins[0][0] in 'ABCDEFGHIJK' and col_pins[0][1].isdigit():
+                info_data['matrix_pins']['cols'] = col_pins
 
     if direct_pins:
         if 'matrix_pins' in info_data:
@@ -345,6 +377,9 @@ def _extract_matrix_info(info_data, config_c):
         info_data['matrix_pins'] = {}
         direct_pin_array = []
 
+        while direct_pins[-1] != '}':
+            direct_pins = direct_pins[:-1]
+
         for row in direct_pins.split('},{'):
             if row.startswith('{'):
                 row = row[1:]
@@ -368,8 +403,6 @@ def _extract_matrix_info(info_data, config_c):
 def _extract_usb_info(info_data, config_c):
     """Populate the USB information.
     """
-    usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'}
-
     if 'usb' not in info_data:
         info_data['usb'] = {}
 
@@ -378,10 +411,7 @@ def _extract_usb_info(info_data, config_c):
             if info_name in info_data['usb']:
                 _log_warning(info_data, '%s in config.h is overwriting usb.%s in info.json' % (config_name, info_name))
 
-            info_data['usb'][info_name] = config_c[config_name]
-
-        elif info_name not in info_data['usb']:
-            _log_error(info_data, '%s not specified in config.h, and %s not specified in info.json. One is required.' % (config_name, info_name))
+            info_data['usb'][info_name] = '0x' + config_c[config_name][2:].upper()
 
     return info_data
 
@@ -519,8 +549,9 @@ def arm_processor_rules(info_data, rules):
         info_data['processor'] = 'unknown'
 
     if 'BOOTLOADER' in rules:
-        if 'bootloader' in info_data:
-            _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
+        # FIXME(skullydazed/anyone): need to remove the massive amounts of duplication first
+        # if 'bootloader' in info_data:
+        #     _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
 
         info_data['bootloader'] = rules['BOOTLOADER']
 
@@ -558,8 +589,9 @@ def avr_processor_rules(info_data, rules):
         info_data['processor'] = 'unknown'
 
     if 'BOOTLOADER' in rules:
-        if 'bootloader' in info_data:
-            _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
+        # FIXME(skullydazed/anyone): need to remove the massive amounts of duplication first
+        # if 'bootloader' in info_data:
+        #     _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
 
         info_data['bootloader'] = rules['BOOTLOADER']
     else:
@@ -593,8 +625,8 @@ def merge_info_jsons(keyboard, info_data):
             keyboard_validate(new_info_data)
 
         except jsonschema.ValidationError as e:
-            cli.log.error('Invalid info.json data: %s', e.message)
-            cli.log.error('Not including file %s', info_file)
+            json_path = '.'.join([str(p) for p in e.absolute_path])
+            cli.log.error('Invalid info.json data: %s: %s: %s', info_file, json_path, e.message)
             continue
 
         if not isinstance(new_info_data, dict):
-- 
cgit v1.2.3


From 56ef80216ae4c67e2a70857c61d1e62eec1ab380 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Wed, 30 Dec 2020 11:21:18 -0800
Subject: make flake8 happy

---
 lib/python/qmk/cli/generate/rules_mk.py |  5 +-
 lib/python/qmk/info.py                  | 84 ++++++++++++++++++---------------
 2 files changed, 50 insertions(+), 39 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 570ef5a0d6..2a7e918569 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -8,9 +8,10 @@ from qmk.path import is_keyboard, normpath
 
 info_to_rules = {
     'bootloader': 'BOOTLOADER',
-    'processor': 'MCU'
+    'processor': 'MCU',
 }
 
+
 @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', help='Keyboard to generate config.h for.')
@@ -42,7 +43,7 @@ def generate_rules_mk(cli):
     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(f'BOOTMAGIC_ENABLE := lite')
+                rules_mk_lines.append('BOOTMAGIC_ENABLE := lite')
             else:
                 feature = feature.upper()
                 enabled = 'yes' if enabled else 'no'
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 39af88f790..efd339115b 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -234,14 +234,14 @@ def _extract_features(info_data, rules):
     # Special handling for bootmagic which also supports a "lite" mode.
     if rules.get('BOOTMAGIC_ENABLE') == 'lite':
         rules['BOOTMAGIC_LITE_ENABLE'] = 'on'
-        del(rules['BOOTMAGIC_ENABLE'])
+        del rules['BOOTMAGIC_ENABLE']
     if rules.get('BOOTMAGIC_ENABLE') == 'full':
         rules['BOOTMAGIC_ENABLE'] = 'on'
 
     # Skip non-boolean features we haven't implemented special handling for
     for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
         if rules.get(feature):
-            del(rules[feature])
+            del rules[feature]
 
     # Process the rest of the rules as booleans
     for key, value in rules.items():
@@ -337,6 +337,45 @@ def _extract_rgblight(info_data, config_c):
     return info_data
 
 
+def _extract_pins(pins):
+    """Returns a list of pins from a comma separated string of pins.
+    """
+    pins = [pin.strip() for pin in pins.split(',') if pin]
+
+    for pin in pins:
+        if pin[0] not in 'ABCDEFGHIJK' or not pin[1].isdigit():
+            raise ValueError(f'Invalid pin: {pin}')
+
+    return pins
+
+
+def _extract_direct_matrix(info_data, direct_pins):
+    """
+    """
+    info_data['matrix_pins'] = {}
+    direct_pin_array = []
+
+    while direct_pins[-1] != '}':
+        direct_pins = direct_pins[:-1]
+
+    for row in direct_pins.split('},{'):
+        if row.startswith('{'):
+            row = row[1:]
+
+        if row.endswith('}'):
+            row = row[:-1]
+
+        direct_pin_array.append([])
+
+        for pin in row.split(','):
+            if pin == 'NO_PIN':
+                pin = None
+
+            direct_pin_array[-1].append(pin)
+
+    return direct_pin_array
+
+
 def _extract_matrix_info(info_data, config_c):
     """Populate the matrix information.
     """
@@ -349,53 +388,24 @@ def _extract_matrix_info(info_data, config_c):
             _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
 
         info_data['matrix_size'] = {
-            'rows': compute(config_c.get('MATRIX_ROWS', '0')),
             'cols': compute(config_c.get('MATRIX_COLS', '0')),
+            'rows': compute(config_c.get('MATRIX_ROWS', '0')),
         }
 
     if row_pins and col_pins:
         if 'matrix_pins' in info_data:
             _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
 
-        info_data['matrix_pins'] = {}
-
-        # FIXME(skullydazed/anyone): Should really check every pin, not just the first
-        if row_pins:
-            row_pins = [pin.strip() for pin in row_pins.split(',') if pin]
-            if row_pins[0][0] in 'ABCDEFGHIJK' and row_pins[0][1].isdigit():
-                info_data['matrix_pins']['rows'] = row_pins
-
-        if col_pins:
-            col_pins = [pin.strip() for pin in col_pins.split(',') if pin]
-            if col_pins[0][0] in 'ABCDEFGHIJK' and col_pins[0][1].isdigit():
-                info_data['matrix_pins']['cols'] = col_pins
+        info_data['matrix_pins'] = {
+            'cols': _extract_pins(col_pins),
+            'rows': _extract_pins(row_pins),
+        }
 
     if direct_pins:
         if 'matrix_pins' in info_data:
             _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
 
-        info_data['matrix_pins'] = {}
-        direct_pin_array = []
-
-        while direct_pins[-1] != '}':
-            direct_pins = direct_pins[:-1]
-
-        for row in direct_pins.split('},{'):
-            if row.startswith('{'):
-                row = row[1:]
-
-            if row.endswith('}'):
-                row = row[:-1]
-
-            direct_pin_array.append([])
-
-            for pin in row.split(','):
-                if pin == 'NO_PIN':
-                    pin = None
-
-                direct_pin_array[-1].append(pin)
-
-            info_data['matrix_pins']['direct'] = direct_pin_array
+        info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
 
     return info_data
 
-- 
cgit v1.2.3


From 9a178f6826a30007f84983daf1c87ded1525c9cd Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Wed, 6 Jan 2021 10:13:25 -0800
Subject: fix rgblight properties

---
 lib/python/qmk/cli/generate/config_h.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 09aa0041f4..15d4fbf2dd 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -173,9 +173,9 @@ def rgblight(config):
     for json_key, config_key in rgblight_properties.items():
         if json_key in config:
             rgblight_config.append('')
-            rgblight_config.append('#ifndef %s' % (config_key,))
-            rgblight_config.append('#   define %s %s' % (config_key, config[json_key]))
-            rgblight_config.append('#endif // %s' % (config_key,))
+            rgblight_config.append('#ifndef %s' % (config_key[0],))
+            rgblight_config.append('#   define %s %s' % (config_key[0], config[json_key]))
+            rgblight_config.append('#endif // %s' % (config_key[0],))
 
     for json_key, config_key in rgblight_toggles.items():
         if config.get(json_key):
-- 
cgit v1.2.3


From b44532aa3dbdce702d662e5b1cf043e461846d8b Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Wed, 6 Jan 2021 10:28:58 -0800
Subject: change invalid pin from error to warning

---
 lib/python/qmk/info.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'lib')

diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index efd339115b..89c08809ba 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -344,7 +344,7 @@ def _extract_pins(pins):
 
     for pin in pins:
         if pin[0] not in 'ABCDEFGHIJK' or not pin[1].isdigit():
-            raise ValueError(f'Invalid pin: {pin}')
+            cli.log.warning(f'Nonstandard pin format: {pin}')
 
     return pins
 
-- 
cgit v1.2.3


From e2e793c1c3efeb96108072c2c8a57555c0fa76a0 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Wed, 6 Jan 2021 10:37:31 -0800
Subject: Revert "change invalid pin from error to warning"

This reverts commit 68da9cd19658071f0c2f22e97956303281703176.
---
 lib/python/qmk/info.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'lib')

diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 89c08809ba..efd339115b 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -344,7 +344,7 @@ def _extract_pins(pins):
 
     for pin in pins:
         if pin[0] not in 'ABCDEFGHIJK' or not pin[1].isdigit():
-            cli.log.warning(f'Nonstandard pin format: {pin}')
+            raise ValueError(f'Invalid pin: {pin}')
 
     return pins
 
-- 
cgit v1.2.3


From 30331b383f9ef4620e47aa07e4f9af7fae9d30b3 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Fri, 8 Jan 2021 00:00:15 -0800
Subject: fix bugs triggered by certain boards

---
 lib/python/qmk/c_parse.py               |  4 ++--
 lib/python/qmk/cli/generate/config_h.py |  6 +++---
 lib/python/qmk/info.py                  | 35 ++++++++++++++++++++++-----------
 3 files changed, 29 insertions(+), 16 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index 67e196f0ea..0338484ec7 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -9,7 +9,7 @@ from qmk.comment_remover import comment_remover
 
 default_key_entry = {'x': -1, 'y': 0, 'w': 1}
 single_comment_regex = re.compile(r' */[/*].*$')
-multi_comment_regex = re.compile(r'/\*(.|\n)*\*/', re.MULTILINE)
+multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
 
 
 def strip_line_comment(string):
@@ -103,7 +103,7 @@ def parse_config_h_file(config_h_file, config_h=None):
 
     if config_h_file.exists():
         config_h_text = config_h_file.read_text()
-        config_h_text = config_h_text.replace('\\\n', '')
+        config_h_text = config_h_text.replace('\\\n', '') # Why are you here?
         config_h_text = strip_multiline_comment(config_h_text)
 
         for linenum, line in enumerate(config_h_text.split('\n')):
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 15d4fbf2dd..1de84de7a9 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -64,7 +64,7 @@ def direct_pins(direct_pins):
     rows = []
 
     for row in direct_pins:
-        cols = ','.join([col or 'NO_PIN' for col in row])
+        cols = ','.join(map(str, [col or 'NO_PIN' for col in row]))
         rows.append('{' + cols + '}')
 
     col_count = len(direct_pins[0])
@@ -88,7 +88,7 @@ def direct_pins(direct_pins):
 def col_pins(col_pins):
     """Return the config.h lines that set the column pins.
     """
-    cols = ','.join(col_pins)
+    cols = ','.join(map(str, [pin or 'NO_PIN' for pin in col_pins]))
     col_num = len(col_pins)
 
     return """
@@ -105,7 +105,7 @@ def col_pins(col_pins):
 def row_pins(row_pins):
     """Return the config.h lines that set the row pins.
     """
-    rows = ','.join(row_pins)
+    rows = ','.join(map(str, [pin or 'NO_PIN' for pin in row_pins]))
     row_num = len(row_pins)
 
     return """
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index efd339115b..28c281a4bc 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -315,11 +315,10 @@ def _extract_rgblight(info_data, config_c):
                 cli.log.error('%s: config.h: Could not convert "%s" to %s: %s', info_data['keyboard_folder'], config_c[config_key], config_type.__name__, e)
 
     for json_key, config_key in rgblight_toggles.items():
-        if config_key in config_c:
-            if json_key in rgblight:
-                _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key)
+        if config_key in config_c and json_key in rgblight:
+            _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key)
 
-            rgblight[json_key] = config_c[config_key]
+        rgblight[json_key] = config_key in config_c
 
     for json_key, config_key in rgblight_animations.items():
         if config_key in config_c:
@@ -337,16 +336,30 @@ def _extract_rgblight(info_data, config_c):
     return info_data
 
 
-def _extract_pins(pins):
-    """Returns a list of pins from a comma separated string of pins.
+def _pin_name(pin):
+    """Returns the proper representation for a pin.
     """
-    pins = [pin.strip() for pin in pins.split(',') if pin]
+    pin = pin.strip()
+
+    if not pin:
+        return None
+
+    elif pin.isdigit():
+        return int(pin)
 
-    for pin in pins:
-        if pin[0] not in 'ABCDEFGHIJK' or not pin[1].isdigit():
-            raise ValueError(f'Invalid pin: {pin}')
+    elif pin == 'NO_PIN':
+        return None
 
-    return pins
+    elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit():
+        return pin
+
+    raise ValueError(f'Invalid pin: {pin}')
+
+
+def _extract_pins(pins):
+    """Returns a list of pins from a comma separated string of pins.
+    """
+    return [_pin_name(pin) for pin in pins.split(',')]
 
 
 def _extract_direct_matrix(info_data, direct_pins):
-- 
cgit v1.2.3


From 58fcdf8c07e5c1363b6b3eaf23883853dfd12f53 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Fri, 8 Jan 2021 00:21:51 -0800
Subject: remove extraneous comment

---
 lib/python/qmk/c_parse.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'lib')

diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index 0338484ec7..ade3e38059 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -103,7 +103,7 @@ def parse_config_h_file(config_h_file, config_h=None):
 
     if config_h_file.exists():
         config_h_text = config_h_file.read_text()
-        config_h_text = config_h_text.replace('\\\n', '') # Why are you here?
+        config_h_text = config_h_text.replace('\\\n', '')
         config_h_text = strip_multiline_comment(config_h_text)
 
         for linenum, line in enumerate(config_h_text.split('\n')):
-- 
cgit v1.2.3


From e2c7f893470465f986bd6849c18606be0f2b7842 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Fri, 8 Jan 2021 08:46:51 -0800
Subject: typo fix

---
 lib/python/qmk/cli/generate/api.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
index 2fda38fc9a..6d111f244c 100755
--- a/lib/python/qmk/cli/generate/api.py
+++ b/lib/python/qmk/cli/generate/api.py
@@ -48,7 +48,7 @@ def generate_api(cli):
             if 'vid' in usb and usb['vid'] not in usb_list['devices']:
                 usb_list['devices'][usb['vid']] = {}
 
-            if 'vid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]:
+            if 'pid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]:
                 usb_list['devices'][usb['vid']][usb['pid']] = {}
 
             if 'vid' in usb and 'pid' in usb:
-- 
cgit v1.2.3


From 962bc8d9dd413690dbeadeaac971a5389697210f Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Sat, 9 Jan 2021 13:34:14 -0800
Subject: Use the schema to eliminate custom code (#11108)

* use the schema to eliminate custom code

* Update docs/reference_info_json.md

Co-authored-by: Ryan <fauxpark@gmail.com>

* make flake8 happy

* bugfix

* do not overwrite make vars from json

Co-authored-by: Ryan <fauxpark@gmail.com>
---
 lib/python/qmk/cli/generate/info_json.py | 48 ++++++++++++++++---------
 lib/python/qmk/cli/generate/layouts.py   |  4 +++
 lib/python/qmk/cli/generate/rules_mk.py  | 10 +++---
 lib/python/qmk/constants.py              |  2 +-
 lib/python/qmk/info.py                   | 60 +++++++++++++++-----------------
 5 files changed, 71 insertions(+), 53 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
index fba4b1c014..f3fc54ddcf 100755
--- a/lib/python/qmk/cli/generate/info_json.py
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -4,14 +4,41 @@ Compile an info.json for a particular keyboard and pretty-print it.
 """
 import json
 
+from jsonschema import Draft7Validator, validators
 from milc import cli
 
-from qmk.info_json_encoder import InfoJSONEncoder
 from qmk.decorators import automagic_keyboard, automagic_keymap
-from qmk.info import info_json
+from qmk.info import info_json, _jsonschema
+from qmk.info_json_encoder import InfoJSONEncoder
 from qmk.path import is_keyboard
 
 
+def pruning_validator(validator_class):
+    """Extends Draft7Validator to remove properties that aren't specified in the schema.
+    """
+    validate_properties = validator_class.VALIDATORS["properties"]
+
+    def remove_additional_properties(validator, properties, instance, schema):
+        for prop in list(instance.keys()):
+            if prop not in properties:
+                del instance[prop]
+
+        for error in validate_properties(validator, properties, instance, schema):
+            yield error
+
+    return validators.extend(validator_class, {"properties": remove_additional_properties})
+
+
+def strip_info_json(kb_info_json):
+    """Remove the API-only properties from the info.json.
+    """
+    pruning_draft_7_validator = pruning_validator(Draft7Validator)
+    schema = _jsonschema('keyboard')
+    validator = pruning_draft_7_validator(schema).validate
+
+    return validator(kb_info_json)
+
+
 @cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')
 @cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
 @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
@@ -22,7 +49,7 @@ def generate_info_json(cli):
     """
     # Determine our keyboard(s)
     if not cli.config.generate_info_json.keyboard:
-        cli.log.error('Missing paramater: --keyboard')
+        cli.log.error('Missing parameter: --keyboard')
         cli.subcommands['info'].print_help()
         return False
 
@@ -32,18 +59,7 @@ def generate_info_json(cli):
 
     # Build the info.json file
     kb_info_json = info_json(cli.config.generate_info_json.keyboard)
-    pared_down_json = {}
-
-    for key in ('manufacturer', 'maintainer', 'usb', 'keyboard_name', 'width', 'height', 'debounce', 'diode_direction', 'features', 'community_layouts', 'layout_aliases', 'matrix_pins', 'rgblight', 'url'):
-        if key in kb_info_json:
-            pared_down_json[key] = kb_info_json[key]
-
-    pared_down_json['layouts'] = {}
-    if 'layouts' in kb_info_json:
-        for layout_name, layout in kb_info_json['layouts'].items():
-            pared_down_json['layouts'][layout_name] = {}
-            pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))
-            pared_down_json['layouts'][layout_name]['layout'] = layout['layout']
+    strip_info_json(kb_info_json)
 
     # Display the results
-    print(json.dumps(pared_down_json, indent=2, cls=InfoJSONEncoder))
+    print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder))
diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py
index 273870e15c..b7baae0651 100755
--- a/lib/python/qmk/cli/generate/layouts.py
+++ b/lib/python/qmk/cli/generate/layouts.py
@@ -54,6 +54,10 @@ def generate_layouts(cli):
         if kb_info_json['layouts'][layout_name]['c_macro']:
             continue
 
+        if 'matrix' not in kb_info_json['layouts'][layout_name]['layout'][0]:
+            cli.log.debug('%s/%s: No matrix data!', cli.config.generate_layouts.keyboard, layout_name)
+            continue
+
         layout_keys = []
         layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)]
 
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 2a7e918569..0fdccb4048 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -37,26 +37,26 @@ def generate_rules_mk(cli):
 
     # Bring in settings
     for info_key, rule_key in info_to_rules.items():
-        rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}')
+        rules_mk_lines.append(f'{rule_key} ?= {kb_info_json[info_key]}')
 
     # Find features that should be enabled
     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')
+                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}')
+                rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
 
     # Set the LED driver
     if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
         driver = kb_info_json['led_matrix']['driver']
-        rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}')
+        rules_mk_lines.append(f'LED_MATRIX_DRIVER ?= {driver}')
 
     # Add community layouts
     if 'community_layouts' in kb_info_json:
-        rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')
+        rules_mk_lines.append(f'LAYOUTS ?= {" ".join(kb_info_json["community_layouts"])}')
 
     # Show the results
     rules_mk = '\n'.join(rules_mk_lines) + '\n'
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 675832c506..6a643070fd 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -26,5 +26,5 @@ ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
 LED_INDICATORS = {
     'caps_lock': 'LED_CAPS_LOCK_PIN',
     'num_lock': 'LED_NUM_LOCK_PIN',
-    'scrol_lock': 'LED_SCROLL_LOCK_PIN'
+    'scrol_lock': 'LED_SCROLL_LOCK_PIN',
 }
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 28c281a4bc..0ea5136a97 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -1,6 +1,7 @@
 """Functions that help us generate and use info.json files.
 """
 import json
+from collections.abc import Mapping
 from glob import glob
 from pathlib import Path
 
@@ -140,6 +141,8 @@ def _json_load(json_file):
 
 def _jsonschema(schema_name):
     """Read a jsonschema file from disk.
+
+    FIXME(skullydazed/anyone): Refactor to make this a public function.
     """
     schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
 
@@ -638,49 +641,44 @@ def unknown_processor_rules(info_data, rules):
     return info_data
 
 
+def deep_update(origdict, newdict):
+    """Update a dictionary in place, recursing to do a deep copy.
+    """
+    for key, value in newdict.items():
+        if isinstance(value, Mapping):
+            origdict[key] = deep_update(origdict.get(key, {}), value)
+
+        else:
+            origdict[key] = value
+
+    return origdict
+
+
 def merge_info_jsons(keyboard, info_data):
     """Return a merged copy of all the info.json files for a keyboard.
     """
     for info_file in find_info_json(keyboard):
         # Load and validate the JSON data
+        new_info_data = _json_load(info_file)
+
+        if not isinstance(new_info_data, dict):
+            _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
+            continue
+
         try:
-            new_info_data = _json_load(info_file)
             keyboard_validate(new_info_data)
-
         except jsonschema.ValidationError as e:
             json_path = '.'.join([str(p) for p in e.absolute_path])
-            cli.log.error('Invalid info.json data: %s: %s: %s', info_file, json_path, e.message)
+            cli.log.error('Not including data from file: %s', info_file)
+            cli.log.error('\t%s: %s', json_path, e.message)
             continue
 
-        if not isinstance(new_info_data, dict):
-            _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
-            continue
-
-        # Copy whitelisted keys into `info_data`
-        for key in ('debounce', 'diode_direction', 'indicators', 'keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'):
-            if key in new_info_data:
-                info_data[key] = new_info_data[key]
-
-        # Deep merge certain keys
-        # FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something.
-        for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'):
-            if key in new_info_data:
-                if key not in info_data:
-                    info_data[key] = {}
-
-                info_data[key].update(new_info_data[key])
-
-        # Merge the layouts
-        if 'community_layouts' in new_info_data:
-            if 'community_layouts' in info_data:
-                for layout in new_info_data['community_layouts']:
-                    if layout not in info_data['community_layouts']:
-                        info_data['community_layouts'].append(layout)
-            else:
-                info_data['community_layouts'] = new_info_data['community_layouts']
+        # Mark the layouts as coming from json
+        for layout in new_info_data.get('layouts', {}).values():
+            layout['c_macro'] = False
 
-        if 'layouts' in new_info_data:
-            _merge_layouts(info_data, new_info_data)
+        # Update info_data with the new data
+        deep_update(info_data, new_info_data)
 
     return info_data
 
-- 
cgit v1.2.3


From eaa9106ec74591593e638ac015a5c90d17b30612 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Sat, 9 Jan 2021 20:18:47 -0800
Subject: Add support for specifying BOARD in info.json (#11492)

* add support for specifying BOARD in info.json

* move BOARD from rules.mk to info.json for clueboard

* fix keyboards that do not require board

* remove out of compliance values
---
 lib/python/qmk/cli/generate/rules_mk.py | 4 +++-
 lib/python/qmk/info.py                  | 8 +++++++-
 2 files changed, 10 insertions(+), 2 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 0fdccb4048..b262e3c666 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -7,6 +7,7 @@ from qmk.info import info_json
 from qmk.path import is_keyboard, normpath
 
 info_to_rules = {
+    'board': 'BOARD',
     'bootloader': 'BOOTLOADER',
     'processor': 'MCU',
 }
@@ -37,7 +38,8 @@ def generate_rules_mk(cli):
 
     # Bring in settings
     for info_key, rule_key in info_to_rules.items():
-        rules_mk_lines.append(f'{rule_key} ?= {kb_info_json[info_key]}')
+        if info_key in kb_info_json:
+            rules_mk_lines.append(f'{rule_key} ?= {kb_info_json[info_key]}')
 
     # Find features that should be enabled
     if 'features' in kb_info_json:
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 0ea5136a97..cc81f7a086 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -462,7 +462,7 @@ def _extract_rules_mk(info_data):
     """Pull some keyboard information from existing rules.mk files
     """
     rules = rules_mk(info_data['keyboard_folder'])
-    mcu = rules.get('MCU')
+    mcu = rules.get('MCU', info_data.get('processor'))
 
     if mcu in CHIBIOS_PROCESSORS:
         arm_processor_rules(info_data, rules)
@@ -594,6 +594,12 @@ def arm_processor_rules(info_data, rules):
     elif 'ARM_ATSAM' in rules:
         info_data['platform'] = 'ARM_ATSAM'
 
+    if 'BOARD' in rules:
+        if 'board' in info_data:
+            _log_warning(info_data, 'Board is specified in both info.json and rules.mk, the rules.mk value wins.')
+
+        info_data['board'] = rules['BOARD']
+
     return info_data
 
 
-- 
cgit v1.2.3


From a7c45daeb8022097a0dbb3859bfcd56b73d65d14 Mon Sep 17 00:00:00 2001
From: Nick Brassel <nick@tzarc.org>
Date: Wed, 20 Jan 2021 00:59:12 +1100
Subject: ChibiOS 20.3.2 (#11495)

* Update ChibiOS submodule

* Update ChibiOS-Contrib submodule

* Update ChibiOS config files
---
 lib/chibios         | 2 +-
 lib/chibios-contrib | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

(limited to 'lib')

diff --git a/lib/chibios b/lib/chibios
index 313416b8fd..ffe54d63cb 160000
--- a/lib/chibios
+++ b/lib/chibios
@@ -1 +1 @@
-Subproject commit 313416b8fda90d9973a749a0a35970956852c286
+Subproject commit ffe54d63cb10a355add318f8e922e39f1c3d4bfd
diff --git a/lib/chibios-contrib b/lib/chibios-contrib
index e3a3a24047..61baa6b036 160000
--- a/lib/chibios-contrib
+++ b/lib/chibios-contrib
@@ -1 +1 @@
-Subproject commit e3a3a24047717af33b098266e11c2e4e5102206a
+Subproject commit 61baa6b036138c155f7cfc5646d833d9423f3243
-- 
cgit v1.2.3


From 78fe155b7241900988f26479b8e417623b137320 Mon Sep 17 00:00:00 2001
From: Nick Brassel <nick@tzarc.org>
Date: Wed, 20 Jan 2021 02:19:36 +1100
Subject: Fix up comments showing how to execute config migration. (#11621)

---
 lib/python/qmk/cli/chibios/confmigrate.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py
index b9cfda9614..9d25488586 100644
--- a/lib/python/qmk/cli/chibios/confmigrate.py
+++ b/lib/python/qmk/cli/chibios/confmigrate.py
@@ -32,7 +32,7 @@ file_header = """\
 
 /*
  * This file was auto-generated by:
- *    `qmk chibios-confupdate -i {0} -r {1}`
+ *    `qmk chibios-confmigrate -i {0} -r {1}`
  */
 
 #pragma once
-- 
cgit v1.2.3


From bdb757e189bf72c0ef382a924328642a629e06d5 Mon Sep 17 00:00:00 2001
From: Nick Brassel <nick@tzarc.org>
Date: Thu, 21 Jan 2021 17:00:53 +1100
Subject: ChibiOS conf migrations... take 11 (#11646)

* Allow forcing re-migration.

* ChibiOS conf upgrade for acheron/arctic

 acheron/arctic - 2aedbe9103fff6c37e596c33c9ed337957647368

* ChibiOS conf upgrade for acheron/austin

 acheron/austin - c2f4e3b7fc9f1c3d64f47d139bc58367afeca1b7

* ChibiOS conf upgrade for acheron/keebspcb

 acheron/keebspcb - 1ba976e409732bfa5c0487dd33e20bec06852ce4

* ChibiOS conf upgrade for acheron/lasgweloth

 acheron/lasgweloth - 01a2e70d0d86de0ff05d0b898a6e3b4428ee1581

* ChibiOS conf upgrade for aeboards/ext65/rev2

 aeboards/ext65/rev2 - 3c9a45cafb4ed6d9672aaff4548b105193633a87

* ChibiOS conf upgrade for ai03/vega

 ai03/vega - 1bd0dfccb99baa69bacd2d55f2bfa72019b8bf80

* ChibiOS conf upgrade for akegata_denki/device_one

 akegata_denki/device_one - a013823188660f5fca37c5763f160f8646aed7a7

* ChibiOS conf upgrade for at_at/660m

 at_at/660m - 9999583e8bec2772046132a22818482d24e18c84

* ChibiOS conf upgrade for box75

 box75 - 1126206109a942237eea96f3a9608e3c9ed55f8e

* ChibiOS conf upgrade for bt66tech/bt66tech60

 bt66tech/bt66tech60 - b69120638a8b2c86c008fd0592be918383d8a454

* ChibiOS conf upgrade for cannonkeys/an_c

 cannonkeys/an_c - c755f6caaccbe2b30d95661a3d441b836534c29f

* ChibiOS conf upgrade for cannonkeys/atlas

 cannonkeys/atlas - 1a18c55492a834b82715516ea4cd4d3d94508743

* ChibiOS conf upgrade for cannonkeys/chimera65

 cannonkeys/chimera65 - 6bf226ef353da9106c381a8ac95a9b90529327e5

* ChibiOS conf upgrade for cannonkeys/db60

 cannonkeys/db60 - 07e3246f8e3adc5b6918809e6a5aa1ad064b9a09

* ChibiOS conf upgrade for cannonkeys/devastatingtkl

 cannonkeys/devastatingtkl - 907d258e29eb7e35b1f868c1ea6148bfd1e3a3f2

* ChibiOS conf upgrade for cannonkeys/instant60

 cannonkeys/instant60 - ac876ff6746b08839a3936dd4126b52683b763f4

* ChibiOS conf upgrade for cannonkeys/instant65

 cannonkeys/instant65 - 3b8c89ec5fecbdb35cccb00c45e64a798528dbda

* ChibiOS conf upgrade for cannonkeys/iron165

 cannonkeys/iron165 - 0814ec4067d9857b0134e38384f272cf7172fb03

* ChibiOS conf upgrade for cannonkeys/obliterated75

 cannonkeys/obliterated75 - 0f376e5d9a232c62e9c60f8fdc81d12732352ddc

* ChibiOS conf upgrade for cannonkeys/onyx

 cannonkeys/onyx - d35134ebe09046b91ab176035d3742d80b23ed8e

* ChibiOS conf upgrade for cannonkeys/ortho48

 cannonkeys/ortho48 - 828dbfbb013ff4fefe75044b3c9fd31fec5e95a1

* ChibiOS conf upgrade for cannonkeys/ortho60

 cannonkeys/ortho60 - 2aca4dd4234e0dac828f5fa15ae5b3bc3c0605dc

* ChibiOS conf upgrade for cannonkeys/ortho75

 cannonkeys/ortho75 - 4af8aaaded0ff53e9c01473f5968cc169f268647

* ChibiOS conf upgrade for cannonkeys/practice60

 cannonkeys/practice60 - e01df551d9483eeb3f825fcc453317fe65f674b5

* ChibiOS conf upgrade for cannonkeys/practice65

 cannonkeys/practice65 - 156163789d4c96cdd4787895788c1d02a2367f5c

* ChibiOS conf upgrade for cannonkeys/rekt1800

 cannonkeys/rekt1800 - 15882afb6373dea3e5f7ed13c19744ee2463f3bf

* ChibiOS conf upgrade for cannonkeys/sagittarius

 cannonkeys/sagittarius - f49fee611ffcc02cb3dc7f249fc8c83f136907b9

* ChibiOS conf upgrade for cannonkeys/satisfaction75

 cannonkeys/satisfaction75 - 2c9fbbea3a38820774f6ff436ef51017b545475a

* ChibiOS conf upgrade for cannonkeys/savage65

 cannonkeys/savage65 - ddd764be363c46f3e9b1a16151bd02d8f66b2dd4

* ChibiOS conf upgrade for cannonkeys/tmov2

 cannonkeys/tmov2 - 7af36cd2a42015838b77697624e90008ffa72778

* ChibiOS conf upgrade for cannonkeys/tsukuyomi

 cannonkeys/tsukuyomi - a120bea5dacb2cd6f143458cece46d2860ea196d

* ChibiOS conf upgrade for cheshire/curiosity

 cheshire/curiosity - 93d8fe3b7a8e028d4b015941ee71b50afe86bc8a

* ChibiOS conf upgrade for coarse/cordillera

 coarse/cordillera - 7d986d3f534786f302cbf74a5e6b7e51ffe60093

* ChibiOS conf upgrade for converter/siemens_tastatur

 converter/siemens_tastatur - 3f79f1467bbc56f284aceafd76ab5c6a1cbb68f4

* ChibiOS conf upgrade for cutie_club/borsdorf

 cutie_club/borsdorf - f8c6015b7aacfd5edef4f22f719eea30660005e7

* ChibiOS conf upgrade for dekunukem/duckypad

 dekunukem/duckypad - d9c162d0867d3925cca2e653a3a3870eac2d23d2

* ChibiOS conf upgrade for ebastler/isometria_75/rev1

 ebastler/isometria_75/rev1 - 2ecdd00b8c6cc794a7014747185b88374d5766c0

* ChibiOS conf upgrade for ergodox_infinity

 ergodox_infinity - 76736c701db22e890764481c25bd38badf32a1cf

* ChibiOS conf upgrade for ergodox_stm32

 ergodox_stm32 - a7bdb0b7822617ca0f9d316a082874ac0fef5964

* ChibiOS conf upgrade for function96

 function96 - 1b9e394a86dab24b85d160afe9281b5e7652afe2

* ChibiOS conf upgrade for geekboards/macropad_v2

 geekboards/macropad_v2 - de261e46a42cc7b7ff793e26200669f94b1dbb18

* ChibiOS conf upgrade for generic_panda/panda65_01

 generic_panda/panda65_01 - 592d2443033875e0309506aa8cb5212389054458

* ChibiOS conf upgrade for handwired/ck4x4

 handwired/ck4x4 - 9d7d4af7f2d787afd638e976334cd37c490040d7

* ChibiOS conf upgrade for handwired/onekey/blackpill_f401

 handwired/onekey/blackpill_f401 - c9257a30203b3a0e5aa18b35e07281fcd043ab5d

* ChibiOS conf upgrade for handwired/onekey/blackpill_f411

 handwired/onekey/blackpill_f411 - dfccc2c6b245cfb561faba60c9f3fc7af8c3db30

* ChibiOS conf upgrade for handwired/onekey/bluepill

 handwired/onekey/bluepill - 393f4b94e9a0fbc9cd00de1acde346ad72250b4a

* ChibiOS conf upgrade for handwired/onekey/stm32f0_disco

 handwired/onekey/stm32f0_disco - d827881f9bed09fd41c7c4e9d066e96b6643a099

* ChibiOS conf upgrade for handwired/onekey/teensy_32

 handwired/onekey/teensy_32 - dd90f6c823da80ba43afa41249a2bdbbab98b04c

* ChibiOS conf upgrade for handwired/onekey/teensy_lc

 handwired/onekey/teensy_lc - 34596f2eeb62403cce532d20cf893ebd77fe2d05

* ChibiOS conf upgrade for handwired/pill60/blackpill_f401

 handwired/pill60/blackpill_f401 - 20bbccb5058085076357f4fa956eede2c0af86e8

* ChibiOS conf upgrade for handwired/pill60/blackpill_f411

 handwired/pill60/blackpill_f411 - e03a4c3ec3b9ba65b8067126a039a8eb2cd10e51

* ChibiOS conf upgrade for handwired/pill60/bluepill

 handwired/pill60/bluepill - 2fb721fb4376cde2067eed1c0c8acbf3bf5851c3

* ChibiOS conf upgrade for handwired/riblee_f401

 handwired/riblee_f401 - 2823cd98d64c91b146dae343a32176d51772952b

* ChibiOS conf upgrade for handwired/riblee_f411

 handwired/riblee_f411 - 5cf7589c9a11c4ed14e7d28676ac836fbb07d013

* ChibiOS conf upgrade for handwired/sono1

 handwired/sono1 - d972acee03efb4917fc42f7d72c0a416b67ebd4a

* ChibiOS conf upgrade for handwired/t111

 handwired/t111 - f947c1d59025fe04e7d7b999d80e20277be4366c

* ChibiOS conf upgrade for handwired/twadlee/tp69

 handwired/twadlee/tp69 - 48ab62464ba1f2651f4468cbec7c6058e3b8c158

* ChibiOS conf upgrade for handwired/z150

 handwired/z150 - 35e85696845e965d7f2dcdb372ed20c17d42c2bb

* ChibiOS conf upgrade for hub20

 hub20 - f094036a182169b5a73f89f09f2ccaa3e5fc9e7e

* ChibiOS conf upgrade for iron180

 iron180 - 5efb6f21c848b3c964795d9debddceac7628933d

* ChibiOS conf upgrade for jm60

 jm60 - dbbdbe8b27b3c1d0a25981b2f75728d163d5d299

* ChibiOS conf upgrade for keebio/bdn9/rev2

 keebio/bdn9/rev2 - 0031632523dddfc8f7b3a02cf9c5990ebbb3856d

* ChibiOS conf upgrade for keebio/choconum

 keebio/choconum - 4484ef6936a497610ca5ae4d378bc07b2bc1b1c7

* ChibiOS conf upgrade for matrix/m20add

 matrix/m20add - 6068aeff5b14c26de8838180f4397800abe9f1aa

* ChibiOS conf upgrade for matrix/noah

 matrix/noah - fcde175fb1d3f5bf0716ac5a880c2cc9ab4bbf7f

* ChibiOS conf upgrade for misterknife/knife66

 misterknife/knife66 - e7f3ceffb94fb8e680da2af27fa7cb1a0a52b699

* ChibiOS conf upgrade for misterknife/knife66_iso

 misterknife/knife66_iso - ee027db939c098f908f70f02ffa9cc3952139f70

* ChibiOS conf upgrade for nebula12

 nebula12 - 2f3a25b6675dcd0fabe75e7bb63038d3cac19610

* ChibiOS conf upgrade for nemui

 nemui - 4030faa372512f766fd747876cb28176302453f9

* ChibiOS conf upgrade for nibiria/stream15

 nibiria/stream15 - fd7b09affa208403099ef9bff1b756fac0d4f918

* ChibiOS conf upgrade for nightingale_studios/hailey

 nightingale_studios/hailey - 496a44b2f789cc59a6110c7c1c41d57e59c722be

* ChibiOS conf upgrade for planck/rev6

 planck/rev6 - 6fb8f0bc21b4ab774ce3d339fdf6f25d96059f10

* ChibiOS conf upgrade for primekb/meridian

 primekb/meridian - 8021ed99e8bdf92faab806780186cc924dd59e50

* ChibiOS conf upgrade for projectkb/alice

 projectkb/alice - ad7678d475a14c54f28b62d1c1b15f5b4c4448bc

* ChibiOS conf upgrade for projectkb/signature87

 projectkb/signature87 - de61338b0a5c477f39c000df8117dd3017c2643d

* ChibiOS conf upgrade for ramonimbao/squishy65

 ramonimbao/squishy65 - 2ea1a8b18c39d0e193bbbf5836191874d3aafc53

* ChibiOS conf upgrade for ramonimbao/wete

 ramonimbao/wete - a80b3b309f06566adcdf20234d81c1d5aa599b00

* ChibiOS conf upgrade for rart/rartlice

 rart/rartlice - c8c80c302428d62bb8b08e3185509a233a94f5f0

* ChibiOS conf upgrade for retro_75

 retro_75 - 7434e266bba43ded8ca1ff75d77faa506473ca91

* ChibiOS conf upgrade for rocketboard_16

 rocketboard_16 - 1182a8730a84b58eba5a470286b6db6134d36f54

* ChibiOS conf upgrade for satt/vision

 satt/vision - c85a28a8d7e321511a17a7e7084d0a7876d1779a

* ChibiOS conf upgrade for technika

 technika - 7c59de348f0ec5f6ac9e260806a87aaf16fbafa2

* ChibiOS conf upgrade for tkc/godspeed75

 tkc/godspeed75 - e447b39dca393736fbf289049699d5b4748bb8c6

* ChibiOS conf upgrade for tkw/stoutgat/v2/f411

 tkw/stoutgat/v2/f411 - 58e9b909379ad107affbff0508e3923264b75426

* ChibiOS conf upgrade for viktus/styrka

 viktus/styrka - 36bf0921be79a8b037fdc25343584574a312be60

* ChibiOS conf upgrade for westfoxtrot/prophet

 westfoxtrot/prophet - 80036c8f986c8d3261d6b0748d2057eccab6430b

* ChibiOS conf upgrade for whitefox

 whitefox - c9eccac0196c3e2e2ea6c90d3036283a3eabff2c

* ChibiOS conf upgrade for wolfmarkclub/wm1

 wolfmarkclub/wm1 - 0d22a426255b829a7805bd6a3ff927198b1e31b4

* ChibiOS conf upgrade for xelus/kangaroo

 xelus/kangaroo - fdb1ffb66f4841f662e968929f058d4fd403c97e

* ChibiOS conf upgrade for xelus/trinityxttkl

 xelus/trinityxttkl - a9641e5b39b7bb38351b9109f0f381837b2e8e79

* ChibiOS conf upgrade for xelus/valor_frl_tkl

 xelus/valor_frl_tkl - 6c3b577852736a68727ec319c30712b3088e65cd

* ChibiOS conf upgrade for xiaomi/mk02

 xiaomi/mk02 - 4c1406ebfee73551a844ef7ab29fc5788d7e875c

* ChibiOS conf upgrade for zoo/wampus

 zoo/wampus - 930c903e85478e220a235f45593c03512c66bc2a

* ChibiOS conf upgrade for zvecr/split_blackpill

 zvecr/split_blackpill - c0e3e1583262e4cb0ebfe2efa6356ed6c5c957ca
---
 lib/python/qmk/cli/chibios/confmigrate.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py
index 9d25488586..3e348b2b07 100644
--- a/lib/python/qmk/cli/chibios/confmigrate.py
+++ b/lib/python/qmk/cli/chibios/confmigrate.py
@@ -111,6 +111,7 @@ def migrate_mcuconf_h(to_override, outfile):
 @cli.argument('-r', '--reference', type=normpath, arg_only=True, help='Specify the reference file to compare against')
 @cli.argument('-o', '--overwrite', arg_only=True, action='store_true', help='Overwrites the input file during migration.')
 @cli.argument('-d', '--delete', arg_only=True, action='store_true', help='If the file has no overrides, migration will delete the input file.')
+@cli.argument('-f', '--force', arg_only=True, action='store_true', help='Re-migrates an already migrated file, even if it doesn\'t detect a full ChibiOS config.')
 @cli.subcommand('Generates a migrated ChibiOS configuration file, as a result of comparing the input against a reference')
 def chibios_confmigrate(cli):
     """Generates a usable ChibiOS replacement configuration file, based on a fully-defined conf and a reference config.
@@ -142,19 +143,19 @@ def chibios_confmigrate(cli):
 
         eprint('--------------------------------------')
 
-        if "CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"]:
+        if cli.args.input.name == "chconf.h" and ("CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_chconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
                 with open(cli.args.input, "w") as out_file:
                     migrate_chconf_h(to_override, outfile=out_file)
 
-        elif "HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"]:
+        elif cli.args.input.name == "halconf.h" and ("HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_halconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
                 with open(cli.args.input, "w") as out_file:
                     migrate_halconf_h(to_override, outfile=out_file)
 
-        elif "MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"]:
+        elif cli.args.input.name == "mcuconf.h" and ("MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_mcuconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
                 with open(cli.args.input, "w") as out_file:
-- 
cgit v1.2.3


From d92ffd1157e3ecc4ae2dbf8548c45c8b0269f664 Mon Sep 17 00:00:00 2001
From: Dasky <32983009+daskygit@users.noreply.github.com>
Date: Sat, 30 Jan 2021 03:53:56 +0000
Subject: Adds AT90USB162 support (#11570)

* at90usb162 support

* fix missing bracket

* Apply suggestions from code review

Co-authored-by: Ryan <fauxpark@gmail.com>

Co-authored-by: Ryan <fauxpark@gmail.com>
---
 lib/python/qmk/constants.py                 | 2 +-
 lib/python/qmk/os_helpers/linux/__init__.py | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

(limited to 'lib')

diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 404a58a7e5..cb94613562 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -11,7 +11,7 @@ MAX_KEYBOARD_SUBFOLDERS = 5
 
 # Supported processor types
 CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411'
-LUFA_PROCESSORS = 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
+LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
 VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
 
 # Common format strings
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/os_helpers/linux/__init__.py
index 86850bf284..a04ac4f8a9 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/os_helpers/linux/__init__.py
@@ -48,6 +48,7 @@ def check_udev_rules():
             _udev_rule("03eb", "2ff3"),  # ATmega16U4
             _udev_rule("03eb", "2ff4"),  # ATmega32U4
             _udev_rule("03eb", "2ff9"),  # AT90USB64
+            _udev_rule("03eb", "2ffa"),  # AT90USB162
             _udev_rule("03eb", "2ffb")  # AT90USB128
         },
         'kiibohd': {_udev_rule("1c11", "b007")},
-- 
cgit v1.2.3


From ef6329af7c7be77b537fbfc5a5cc7105acc679f7 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Sun, 31 Jan 2021 12:46:00 -0800
Subject: Create a system to map between info.json and config.h/rules.mk
 (#11548)

* generate rules.mk from a json mapping

* generate rules.mk from a json mapping

* support for config.h from json maps

* improve the mapping system

* document the mapping system

* move data/maps to data/mappings

* fix flake8 errors

* fixup LED_MATRIX_DRIVER

* remove product and description from the vision_division keymap level

* reduce the complexity of generate-rules-mk

* add tests for the generate commands

* fix qmk doctor when submodules are not clean
---
 lib/python/qmk/cli/doctor.py              |   4 +-
 lib/python/qmk/cli/generate/config_h.py   | 245 ++++++---------------
 lib/python/qmk/cli/generate/rules_mk.py   |  61 +++---
 lib/python/qmk/constants.py               |   2 +-
 lib/python/qmk/info.py                    | 343 +++++++++---------------------
 lib/python/qmk/tests/test_cli_commands.py |  29 +++
 6 files changed, 231 insertions(+), 453 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor.py
index 70f32911a4..28d480707c 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor.py
@@ -107,9 +107,9 @@ def doctor(cli):
             submodules.update()
             sub_ok = check_submodules()
 
-        if CheckStatus.ERROR in sub_ok:
+        if sub_ok == CheckStatus.ERROR:
             status = CheckStatus.ERROR
-        elif CheckStatus.WARNING in sub_ok and status == CheckStatus.OK:
+        elif sub_ok == CheckStatus.WARNING and status == CheckStatus.OK:
             status = CheckStatus.WARNING
 
     # Report a summary of our findings to the user
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 1de84de7a9..7ddad745d1 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -1,62 +1,14 @@
 """Used by the make system to generate info_config.h from info.json.
 """
+from pathlib import Path
+
+from dotty_dict import dotty
 from milc import cli
 
-from qmk.constants import LED_INDICATORS
 from qmk.decorators import automagic_keyboard, automagic_keymap
-from qmk.info import info_json, rgblight_animations, rgblight_properties, rgblight_toggles
+from qmk.info import _json_load, info_json
 from qmk.path import is_keyboard, normpath
 
-usb_prop_map = {
-    'vid': 'VENDOR_ID',
-    'pid': 'PRODUCT_ID',
-    'device_ver': 'DEVICE_VER',
-}
-
-
-def debounce(debounce):
-    """Return the config.h lines that set debounce
-    """
-    return """
-#ifndef DEBOUNCE
-#   define DEBOUNCE %s
-#endif // DEBOUNCE
-""" % debounce
-
-
-def diode_direction(diode_direction):
-    """Return the config.h lines that set diode direction
-    """
-    return """
-#ifndef DIODE_DIRECTION
-#   define DIODE_DIRECTION %s
-#endif // DIODE_DIRECTION
-""" % diode_direction
-
-
-def keyboard_name(keyboard_name):
-    """Return the config.h lines that set the keyboard's name.
-    """
-    return """
-#ifndef DESCRIPTION
-#    define DESCRIPTION %s
-#endif // DESCRIPTION
-
-#ifndef PRODUCT
-#    define PRODUCT %s
-#endif // PRODUCT
-""" % (keyboard_name.replace("'", ""), keyboard_name.replace("'", ""))
-
-
-def manufacturer(manufacturer):
-    """Return the config.h lines that set the manufacturer.
-    """
-    return """
-#ifndef MANUFACTURER
-#    define MANUFACTURER %s
-#endif // MANUFACTURER
-""" % (manufacturer.replace("'", ""))
-
 
 def direct_pins(direct_pins):
     """Return the config.h lines that set the direct pins.
@@ -72,80 +24,34 @@ def direct_pins(direct_pins):
 
     return """
 #ifndef MATRIX_COLS
-#    define MATRIX_COLS %s
+#   define MATRIX_COLS %s
 #endif // MATRIX_COLS
 
 #ifndef MATRIX_ROWS
-#    define MATRIX_ROWS %s
+#   define MATRIX_ROWS %s
 #endif // MATRIX_ROWS
 
 #ifndef DIRECT_PINS
-#    define DIRECT_PINS {%s}
+#   define DIRECT_PINS {%s}
 #endif // DIRECT_PINS
 """ % (col_count, row_count, ','.join(rows))
 
 
-def col_pins(col_pins):
-    """Return the config.h lines that set the column pins.
-    """
-    cols = ','.join(map(str, [pin or 'NO_PIN' for pin in col_pins]))
-    col_num = len(col_pins)
-
-    return """
-#ifndef MATRIX_COLS
-#    define MATRIX_COLS %s
-#endif // MATRIX_COLS
-
-#ifndef MATRIX_COL_PINS
-#    define MATRIX_COL_PINS {%s}
-#endif // MATRIX_COL_PINS
-""" % (col_num, cols)
-
-
-def row_pins(row_pins):
-    """Return the config.h lines that set the row pins.
-    """
-    rows = ','.join(map(str, [pin or 'NO_PIN' for pin in row_pins]))
-    row_num = len(row_pins)
-
-    return """
-#ifndef MATRIX_ROWS
-#    define MATRIX_ROWS %s
-#endif // MATRIX_ROWS
-
-#ifndef MATRIX_ROW_PINS
-#    define MATRIX_ROW_PINS {%s}
-#endif // MATRIX_ROW_PINS
-""" % (row_num, rows)
-
-
-def indicators(config):
-    """Return the config.h lines that setup LED indicators.
+def pin_array(define, pins):
+    """Return the config.h lines that set a pin array.
     """
-    defines = []
+    pin_num = len(pins)
+    pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
 
-    for led, define in LED_INDICATORS.items():
-        if led in config:
-            defines.append('')
-            defines.append('#ifndef %s' % (define,))
-            defines.append('#   define %s %s' % (define, config[led]))
-            defines.append('#endif // %s' % (define,))
+    return f"""
+#ifndef {define}S
+#   define {define}S {pin_num}
+#endif // {define}S
 
-    return '\n'.join(defines)
-
-
-def layout_aliases(layout_aliases):
-    """Return the config.h lines that setup layout aliases.
-    """
-    defines = []
-
-    for alias, layout in layout_aliases.items():
-        defines.append('')
-        defines.append('#ifndef %s' % (alias,))
-        defines.append('#   define %s %s' % (alias, layout))
-        defines.append('#endif // %s' % (alias,))
-
-    return '\n'.join(defines)
+#ifndef {define}_PINS
+#   define {define}_PINS {{ {pin_array} }}
+#endif // {define}_PINS
+"""
 
 
 def matrix_pins(matrix_pins):
@@ -157,58 +63,14 @@ def matrix_pins(matrix_pins):
         pins.append(direct_pins(matrix_pins['direct']))
 
     if 'cols' in matrix_pins:
-        pins.append(col_pins(matrix_pins['cols']))
+        pins.append(pin_array('MATRIX_COL', matrix_pins['cols']))
 
     if 'rows' in matrix_pins:
-        pins.append(row_pins(matrix_pins['rows']))
+        pins.append(pin_array('MATRIX_ROW', matrix_pins['rows']))
 
     return '\n'.join(pins)
 
 
-def rgblight(config):
-    """Return the config.h lines that setup rgblight.
-    """
-    rgblight_config = []
-
-    for json_key, config_key in rgblight_properties.items():
-        if json_key in config:
-            rgblight_config.append('')
-            rgblight_config.append('#ifndef %s' % (config_key[0],))
-            rgblight_config.append('#   define %s %s' % (config_key[0], config[json_key]))
-            rgblight_config.append('#endif // %s' % (config_key[0],))
-
-    for json_key, config_key in rgblight_toggles.items():
-        if config.get(json_key):
-            rgblight_config.append('')
-            rgblight_config.append('#ifndef %s' % (config_key,))
-            rgblight_config.append('#   define %s' % (config_key,))
-            rgblight_config.append('#endif // %s' % (config_key,))
-
-    for json_key, config_key in rgblight_animations.items():
-        if 'animations' in config and config['animations'].get(json_key):
-            rgblight_config.append('')
-            rgblight_config.append('#ifndef %s' % (config_key,))
-            rgblight_config.append('#   define %s' % (config_key,))
-            rgblight_config.append('#endif // %s' % (config_key,))
-
-    return '\n'.join(rgblight_config)
-
-
-def usb_properties(usb_props):
-    """Return the config.h lines that setup USB params.
-    """
-    usb_lines = []
-
-    for info_name, config_name in usb_prop_map.items():
-        if info_name in usb_props:
-            usb_lines.append('')
-            usb_lines.append('#ifndef ' + config_name)
-            usb_lines.append('#    define %s %s' % (config_name, usb_props[info_name]))
-            usb_lines.append('#endif // ' + config_name)
-
-    return '\n'.join(usb_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', help='Keyboard to generate config.h for.')
@@ -228,39 +90,52 @@ def generate_config_h(cli):
         cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
         return False
 
-    # Build the info.json file
-    kb_info_json = info_json(cli.config.generate_config_h.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']
+    kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
+    info_config_map = _json_load(Path('data/mappings/info_config.json'))
 
-    if 'debounce' in kb_info_json:
-        config_h_lines.append(debounce(kb_info_json['debounce']))
-
-    if 'diode_direction' in kb_info_json:
-        config_h_lines.append(diode_direction(kb_info_json['diode_direction']))
-
-    if 'indicators' in kb_info_json:
-        config_h_lines.append(indicators(kb_info_json['indicators']))
-
-    if 'keyboard_name' in kb_info_json:
-        config_h_lines.append(keyboard_name(kb_info_json['keyboard_name']))
-
-    if 'layout_aliases' in kb_info_json:
-        config_h_lines.append(layout_aliases(kb_info_json['layout_aliases']))
-
-    if 'manufacturer' in kb_info_json:
-        config_h_lines.append(manufacturer(kb_info_json['manufacturer']))
+    config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
 
-    if 'rgblight' in kb_info_json:
-        config_h_lines.append(rgblight(kb_info_json['rgblight']))
+    # Iterate through the info_config map to generate basic things
+    for config_key, info_dict in info_config_map.items():
+        info_key = info_dict['info_key']
+        key_type = info_dict.get('value_type', 'str')
+        to_config = info_dict.get('to_config', True)
+
+        if not to_config:
+            continue
+
+        try:
+            config_value = kb_info_json[info_key]
+        except KeyError:
+            continue
+
+        if key_type.startswith('array'):
+            config_h_lines.append('')
+            config_h_lines.append(f'#ifndef {config_key}')
+            config_h_lines.append(f'#   define {config_key} {{ {", ".join(map(str, config_value))} }}')
+            config_h_lines.append(f'#endif // {config_key}')
+        elif key_type == 'bool':
+            if config_value:
+                config_h_lines.append('')
+                config_h_lines.append(f'#ifndef {config_key}')
+                config_h_lines.append(f'#   define {config_key}')
+                config_h_lines.append(f'#endif // {config_key}')
+        elif key_type == 'mapping':
+            for key, value in config_value.items():
+                config_h_lines.append('')
+                config_h_lines.append(f'#ifndef {key}')
+                config_h_lines.append(f'#   define {key} {value}')
+                config_h_lines.append(f'#endif // {key}')
+        else:
+            config_h_lines.append('')
+            config_h_lines.append(f'#ifndef {config_key}')
+            config_h_lines.append(f'#   define {config_key} {config_value}')
+            config_h_lines.append(f'#endif // {config_key}')
 
     if 'matrix_pins' in kb_info_json:
         config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
 
-    if 'usb' in kb_info_json:
-        config_h_lines.append(usb_properties(kb_info_json['usb']))
-
     # Show the results
     config_h = '\n'.join(config_h_lines)
 
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index b262e3c666..af740f341d 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -1,16 +1,37 @@
 """Used by the make system to generate a rules.mk
 """
+from pathlib import Path
+
+from dotty_dict import dotty
 from milc import cli
 
 from qmk.decorators import automagic_keyboard, automagic_keymap
-from qmk.info import info_json
+from qmk.info import _json_load, info_json
 from qmk.path import is_keyboard, normpath
 
-info_to_rules = {
-    'board': 'BOARD',
-    'bootloader': 'BOOTLOADER',
-    'processor': 'MCU',
-}
+
+def process_mapping_rule(kb_info_json, rules_key, info_dict):
+    """Return the rules.mk line(s) for a mapping rule.
+    """
+    if not info_dict.get('to_c', True):
+        return None
+
+    info_key = info_dict['info_key']
+    key_type = info_dict.get('value_type', 'str')
+
+    try:
+        rules_value = kb_info_json[info_key]
+    except KeyError:
+        return None
+
+    if key_type == 'array':
+        return f'{rules_key} ?= {" ".join(rules_value)}'
+    elif key_type == 'bool':
+        return f'{rules_key} ?= {"on" if rules_value else "off"}'
+    elif key_type == 'mapping':
+        return '\n'.join([f'{key} ?= {value}' for key, value in rules_value.items()])
+
+    return f'{rules_key} ?= {rules_value}'
 
 
 @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@@ -22,7 +43,6 @@ info_to_rules = {
 def generate_rules_mk(cli):
     """Generates a rules.mk file from info.json.
     """
-    # Determine our keyboard(s)
     if not cli.config.generate_rules_mk.keyboard:
         cli.log.error('Missing paramater: --keyboard')
         cli.subcommands['info'].print_help()
@@ -32,16 +52,18 @@ def generate_rules_mk(cli):
         cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard)
         return False
 
-    # Build the info.json file
-    kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
+    kb_info_json = dotty(info_json(cli.config.generate_rules_mk.keyboard))
+    info_rules_map = _json_load(Path('data/mappings/info_rules.json'))
     rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
 
-    # Bring in settings
-    for info_key, rule_key in info_to_rules.items():
-        if info_key in kb_info_json:
-            rules_mk_lines.append(f'{rule_key} ?= {kb_info_json[info_key]}')
+    # Iterate through the info_rules map to generate basic rules
+    for rules_key, info_dict in info_rules_map.items():
+        new_entry = process_mapping_rule(kb_info_json, rules_key, info_dict)
+
+        if new_entry:
+            rules_mk_lines.append(new_entry)
 
-    # Find features that should be enabled
+    # 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:
@@ -51,15 +73,6 @@ def generate_rules_mk(cli):
                 enabled = 'yes' if enabled else 'no'
                 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
 
-    # Set the LED driver
-    if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
-        driver = kb_info_json['led_matrix']['driver']
-        rules_mk_lines.append(f'LED_MATRIX_DRIVER ?= {driver}')
-
-    # Add community layouts
-    if 'community_layouts' in kb_info_json:
-        rules_mk_lines.append(f'LAYOUTS ?= {" ".join(kb_info_json["community_layouts"])}')
-
     # Show the results
     rules_mk = '\n'.join(rules_mk_lines) + '\n'
 
@@ -72,7 +85,7 @@ def generate_rules_mk(cli):
         if cli.args.quiet:
             print(cli.args.output)
         else:
-            cli.log.info('Wrote info_config.h to %s.', cli.args.output)
+            cli.log.info('Wrote rules.mk to %s.', cli.args.output)
 
     else:
         print(rules_mk)
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index cb94613562..8a177eb6b5 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -27,7 +27,7 @@ ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
 LED_INDICATORS = {
     'caps_lock': 'LED_CAPS_LOCK_PIN',
     'num_lock': 'LED_NUM_LOCK_PIN',
-    'scrol_lock': 'LED_SCROLL_LOCK_PIN',
+    'scroll_lock': 'LED_SCROLL_LOCK_PIN',
 }
 
 # Constants that should match their counterparts in make
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index cc81f7a086..2accaba9e4 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -5,57 +5,18 @@ from collections.abc import Mapping
 from glob import glob
 from pathlib import Path
 
+import hjson
 import jsonschema
+from dotty_dict import dotty
 from milc import cli
 
-from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
+from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
 from qmk.c_parse import find_layouts
 from qmk.keyboard import config_h, rules_mk
 from qmk.keymap import list_keymaps
 from qmk.makefile import parse_rules_mk_file
 from qmk.math import compute
 
-led_matrix_properties = {
-    'driver_count': 'LED_DRIVER_COUNT',
-    'driver_addr1': 'LED_DRIVER_ADDR_1',
-    'driver_addr2': 'LED_DRIVER_ADDR_2',
-    'driver_addr3': 'LED_DRIVER_ADDR_3',
-    'driver_addr4': 'LED_DRIVER_ADDR_4',
-    'led_count': 'LED_DRIVER_LED_COUNT',
-    'timeout': 'ISSI_TIMEOUT',
-    'persistence': 'ISSI_PERSISTENCE'
-}
-
-rgblight_properties = {
-    'led_count': ('RGBLED_NUM', int),
-    'pin': ('RGB_DI_PIN', str),
-    'max_brightness': ('RGBLIGHT_LIMIT_VAL', int),
-    'hue_steps': ('RGBLIGHT_HUE_STEP', int),
-    'saturation_steps': ('RGBLIGHT_SAT_STEP', int),
-    'brightness_steps': ('RGBLIGHT_VAL_STEP', int)
-}
-
-rgblight_toggles = {
-    'sleep': 'RGBLIGHT_SLEEP',
-    'split': 'RGBLIGHT_SPLIT',
-}
-
-rgblight_animations = {
-    'all': 'RGBLIGHT_ANIMATIONS',
-    'alternating': 'RGBLIGHT_EFFECT_ALTERNATING',
-    'breathing': 'RGBLIGHT_EFFECT_BREATHING',
-    'christmas': 'RGBLIGHT_EFFECT_CHRISTMAS',
-    'knight': 'RGBLIGHT_EFFECT_KNIGHT',
-    'rainbow_mood': 'RGBLIGHT_EFFECT_RAINBOW_MOOD',
-    'rainbow_swirl': 'RGBLIGHT_EFFECT_RAINBOW_SWIRL',
-    'rgb_test': 'RGBLIGHT_EFFECT_RGB_TEST',
-    'snake': 'RGBLIGHT_EFFECT_SNAKE',
-    'static_gradient': 'RGBLIGHT_EFFECT_STATIC_GRADIENT',
-    'twinkle': 'RGBLIGHT_EFFECT_TWINKLE'
-}
-
-usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'}
-
 true_values = ['1', 'on', 'yes']
 false_values = ['0', 'off', 'no']
 
@@ -101,7 +62,6 @@ def info_json(keyboard):
     except jsonschema.ValidationError as e:
         json_path = '.'.join([str(p) for p in e.absolute_path])
         cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
-        print(dir(e))
         exit()
 
     # Make sure we have at least one layout
@@ -132,7 +92,7 @@ def _json_load(json_file):
     Note: file must be a Path object.
     """
     try:
-        return json.load(json_file.open())
+        return hjson.load(json_file.open())
 
     except json.decoder.JSONDecodeError as e:
         cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
@@ -172,65 +132,6 @@ def keyboard_api_validate(data):
     return validator(data)
 
 
-def _extract_debounce(info_data, config_c):
-    """Handle debounce.
-    """
-    if 'debounce' in info_data and 'DEBOUNCE' in config_c:
-        _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
-
-    if 'DEBOUNCE' in config_c:
-        info_data['debounce'] = int(config_c['DEBOUNCE'])
-
-    return info_data
-
-
-def _extract_diode_direction(info_data, config_c):
-    """Handle the diode direction.
-    """
-    if 'diode_direction' in info_data and 'DIODE_DIRECTION' in config_c:
-        _log_warning(info_data, 'Diode direction is specified in both info.json and config.h, the config.h value wins.')
-
-    if 'DIODE_DIRECTION' in config_c:
-        info_data['diode_direction'] = config_c.get('DIODE_DIRECTION')
-
-    return info_data
-
-
-def _extract_indicators(info_data, config_c):
-    """Find the LED indicator information.
-    """
-    for json_key, config_key in LED_INDICATORS.items():
-        if json_key in info_data.get('indicators', []) and config_key in config_c:
-            _log_warning(info_data, f'Indicator {json_key} is specified in both info.json and config.h, the config.h value wins.')
-
-        if 'indicators' not in info_data:
-            info_data['indicators'] = {}
-
-        if config_key in config_c:
-            if 'indicators' not in info_data:
-                info_data['indicators'] = {}
-
-            info_data['indicators'][json_key] = config_c.get(config_key)
-
-    return info_data
-
-
-def _extract_community_layouts(info_data, rules):
-    """Find the community layouts in rules.mk.
-    """
-    community_layouts = rules['LAYOUTS'].split() if 'LAYOUTS' in rules else []
-
-    if 'community_layouts' in info_data:
-        for layout in community_layouts:
-            if layout not in info_data['community_layouts']:
-                community_layouts.append(layout)
-
-    else:
-        info_data['community_layouts'] = community_layouts
-
-    return info_data
-
-
 def _extract_features(info_data, rules):
     """Find all the features enabled in rules.mk.
     """
@@ -267,78 +168,6 @@ def _extract_features(info_data, rules):
     return info_data
 
 
-def _extract_led_drivers(info_data, rules):
-    """Find all the LED drivers set in rules.mk.
-    """
-    if 'LED_MATRIX_DRIVER' in rules:
-        if 'led_matrix' not in info_data:
-            info_data['led_matrix'] = {}
-
-        if info_data['led_matrix'].get('driver'):
-            _log_warning(info_data, 'LED Matrix driver is specified in both info.json and rules.mk, the rules.mk value wins.')
-
-        info_data['led_matrix']['driver'] = rules['LED_MATRIX_DRIVER']
-
-    return info_data
-
-
-def _extract_led_matrix(info_data, config_c):
-    """Handle the led_matrix configuration.
-    """
-    led_matrix = info_data.get('led_matrix', {})
-
-    for json_key, config_key in led_matrix_properties.items():
-        if config_key in config_c:
-            if json_key in led_matrix:
-                _log_warning(info_data, 'LED Matrix: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
-
-            led_matrix[json_key] = config_c[config_key]
-
-
-def _extract_rgblight(info_data, config_c):
-    """Handle the rgblight configuration.
-    """
-    rgblight = info_data.get('rgblight', {})
-    animations = rgblight.get('animations', {})
-
-    if 'RGBLED_SPLIT' in config_c:
-        raw_split = config_c.get('RGBLED_SPLIT', '').replace('{', '').replace('}', '').strip()
-        rgblight['split_count'] = [int(i) for i in raw_split.split(',')]
-
-    for json_key, config_key_type in rgblight_properties.items():
-        config_key, config_type = config_key_type
-
-        if config_key in config_c:
-            if json_key in rgblight:
-                _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
-
-            try:
-                rgblight[json_key] = config_type(config_c[config_key])
-            except ValueError as e:
-                cli.log.error('%s: config.h: Could not convert "%s" to %s: %s', info_data['keyboard_folder'], config_c[config_key], config_type.__name__, e)
-
-    for json_key, config_key in rgblight_toggles.items():
-        if config_key in config_c and json_key in rgblight:
-            _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key)
-
-        rgblight[json_key] = config_key in config_c
-
-    for json_key, config_key in rgblight_animations.items():
-        if config_key in config_c:
-            if json_key in animations:
-                _log_warning(info_data, 'RGB Light: animations: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
-
-            animations[json_key] = config_c[config_key]
-
-    if animations:
-        rgblight['animations'] = animations
-
-    if rgblight:
-        info_data['rgblight'] = rgblight
-
-    return info_data
-
-
 def _pin_name(pin):
     """Returns the proper representation for a pin.
     """
@@ -426,34 +255,59 @@ def _extract_matrix_info(info_data, config_c):
     return info_data
 
 
-def _extract_usb_info(info_data, config_c):
-    """Populate the USB information.
+def _extract_config_h(info_data):
+    """Pull some keyboard information from existing config.h files
     """
-    if 'usb' not in info_data:
-        info_data['usb'] = {}
+    config_c = config_h(info_data['keyboard_folder'])
 
-    for info_name, config_name in usb_properties.items():
-        if config_name in config_c:
-            if info_name in info_data['usb']:
-                _log_warning(info_data, '%s in config.h is overwriting usb.%s in info.json' % (config_name, info_name))
+    # Pull in data from the json map
+    dotty_info = dotty(info_data)
+    info_config_map = _json_load(Path('data/mappings/info_config.json'))
 
-            info_data['usb'][info_name] = '0x' + config_c[config_name][2:].upper()
+    for config_key, info_dict in info_config_map.items():
+        info_key = info_dict['info_key']
+        key_type = info_dict.get('value_type', 'str')
 
-    return info_data
+        try:
+            if config_key in config_c and info_dict.get('to_json', True):
+                if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
+                    _log_warning(info_data, '%s in config.h is overwriting %s in info.json' % (config_key, info_key))
 
+                if key_type.startswith('array'):
+                    if '.' in key_type:
+                        key_type, array_type = key_type.split('.', 1)
+                    else:
+                        array_type = None
 
-def _extract_config_h(info_data):
-    """Pull some keyboard information from existing config.h files
-    """
-    config_c = config_h(info_data['keyboard_folder'])
+                    config_value = config_c[config_key].replace('{', '').replace('}', '').strip()
+
+                    if array_type == 'int':
+                        dotty_info[info_key] = list(map(int, config_value.split(',')))
+                    else:
+                        dotty_info[info_key] = config_value.split(',')
+
+                elif key_type == 'bool':
+                    dotty_info[info_key] = config_c[config_key] in true_values
+
+                elif key_type == 'hex':
+                    dotty_info[info_key] = '0x' + config_c[config_key][2:].upper()
 
-    _extract_debounce(info_data, config_c)
-    _extract_diode_direction(info_data, config_c)
-    _extract_indicators(info_data, config_c)
+                elif key_type == 'list':
+                    dotty_info[info_key] = config_c[config_key].split()
+
+                elif key_type == 'int':
+                    dotty_info[info_key] = int(config_c[config_key])
+
+                else:
+                    dotty_info[info_key] = config_c[config_key]
+
+        except Exception as e:
+            _log_warning(info_data, f'{config_key}->{info_key}: {e}')
+
+    info_data.update(dotty_info)
+
+    # Pull data that easily can't be mapped in json
     _extract_matrix_info(info_data, config_c)
-    _extract_usb_info(info_data, config_c)
-    _extract_led_matrix(info_data, config_c)
-    _extract_rgblight(info_data, config_c)
 
     return info_data
 
@@ -462,21 +316,66 @@ def _extract_rules_mk(info_data):
     """Pull some keyboard information from existing rules.mk files
     """
     rules = rules_mk(info_data['keyboard_folder'])
-    mcu = rules.get('MCU', info_data.get('processor'))
+    info_data['processor'] = rules.get('MCU', info_data.get('processor', 'atmega32u4'))
 
-    if mcu in CHIBIOS_PROCESSORS:
+    if info_data['processor'] in CHIBIOS_PROCESSORS:
         arm_processor_rules(info_data, rules)
 
-    elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
+    elif info_data['processor'] in LUFA_PROCESSORS + VUSB_PROCESSORS:
         avr_processor_rules(info_data, rules)
 
     else:
-        cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu))
+        cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], info_data['processor']))
         unknown_processor_rules(info_data, rules)
 
-    _extract_community_layouts(info_data, rules)
+    # Pull in data from the json map
+    dotty_info = dotty(info_data)
+    info_rules_map = _json_load(Path('data/mappings/info_rules.json'))
+
+    for rules_key, info_dict in info_rules_map.items():
+        info_key = info_dict['info_key']
+        key_type = info_dict.get('value_type', 'str')
+
+        try:
+            if rules_key in rules and info_dict.get('to_json', True):
+                if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
+                    _log_warning(info_data, '%s in rules.mk is overwriting %s in info.json' % (rules_key, info_key))
+
+                if key_type.startswith('array'):
+                    if '.' in key_type:
+                        key_type, array_type = key_type.split('.', 1)
+                    else:
+                        array_type = None
+
+                    rules_value = rules[rules_key].replace('{', '').replace('}', '').strip()
+
+                    if array_type == 'int':
+                        dotty_info[info_key] = list(map(int, rules_value.split(',')))
+                    else:
+                        dotty_info[info_key] = rules_value.split(',')
+
+                elif key_type == 'list':
+                    dotty_info[info_key] = rules[rules_key].split()
+
+                elif key_type == 'bool':
+                    dotty_info[info_key] = rules[rules_key] in true_values
+
+                elif key_type == 'hex':
+                    dotty_info[info_key] = '0x' + rules[rules_key][2:].upper()
+
+                elif key_type == 'int':
+                    dotty_info[info_key] = int(rules[rules_key])
+
+                else:
+                    dotty_info[info_key] = rules[rules_key]
+
+        except Exception as e:
+            _log_warning(info_data, f'{rules_key}->{info_key}: {e}')
+
+    info_data.update(dotty_info)
+
+    # Merge in config values that can't be easily mapped
     _extract_features(info_data, rules)
-    _extract_led_drivers(info_data, rules)
 
     return info_data
 
@@ -565,23 +464,7 @@ def arm_processor_rules(info_data, rules):
     info_data['processor_type'] = 'arm'
     info_data['protocol'] = 'ChibiOS'
 
-    if 'MCU' in rules:
-        if 'processor' in info_data:
-            _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
-
-        info_data['processor'] = rules['MCU']
-
-    elif 'processor' not in info_data:
-        info_data['processor'] = 'unknown'
-
-    if 'BOOTLOADER' in rules:
-        # FIXME(skullydazed/anyone): need to remove the massive amounts of duplication first
-        # if 'bootloader' in info_data:
-        #     _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
-
-        info_data['bootloader'] = rules['BOOTLOADER']
-
-    else:
+    if 'bootloader' not in info_data:
         if 'STM32' in info_data['processor']:
             info_data['bootloader'] = 'stm32-dfu'
         else:
@@ -594,12 +477,6 @@ def arm_processor_rules(info_data, rules):
     elif 'ARM_ATSAM' in rules:
         info_data['platform'] = 'ARM_ATSAM'
 
-    if 'BOARD' in rules:
-        if 'board' in info_data:
-            _log_warning(info_data, 'Board is specified in both info.json and rules.mk, the rules.mk value wins.')
-
-        info_data['board'] = rules['BOARD']
-
     return info_data
 
 
@@ -607,26 +484,10 @@ def avr_processor_rules(info_data, rules):
     """Setup the default info for an AVR board.
     """
     info_data['processor_type'] = 'avr'
-    info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
     info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
     info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
 
-    if 'MCU' in rules:
-        if 'processor' in info_data:
-            _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
-
-        info_data['processor'] = rules['MCU']
-
-    elif 'processor' not in info_data:
-        info_data['processor'] = 'unknown'
-
-    if 'BOOTLOADER' in rules:
-        # FIXME(skullydazed/anyone): need to remove the massive amounts of duplication first
-        # if 'bootloader' in info_data:
-        #     _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
-
-        info_data['bootloader'] = rules['BOOTLOADER']
-    else:
+    if 'bootloader' not in info_data:
         info_data['bootloader'] = 'atmel-dfu'
 
     # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index f889833d0b..3efeddb85e 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -230,3 +230,32 @@ def test_generate_rgb_breathe_table():
     check_returncode(result)
     assert 'Breathing center: 1.2' in result.stdout
     assert 'Breathing max:    127' in result.stdout
+
+
+def test_generate_config_h():
+    result = check_subcommand('generate-config-h', '-kb', 'handwired/pytest/basic')
+    check_returncode(result)
+    assert '#   define DEVICE_VER 0x0001' in result.stdout
+    assert '#   define DESCRIPTION handwired/pytest/basic' in result.stdout
+    assert '#   define DIODE_DIRECTION COL2ROW' in result.stdout
+    assert '#   define MANUFACTURER none' in result.stdout
+    assert '#   define PRODUCT handwired/pytest/basic' in result.stdout
+    assert '#   define PRODUCT_ID 0x6465' in result.stdout
+    assert '#   define VENDOR_ID 0xFEED' in result.stdout
+    assert '#   define MATRIX_COLS 1' in result.stdout
+    assert '#   define MATRIX_COL_PINS { F4 }' in result.stdout
+    assert '#   define MATRIX_ROWS 1' in result.stdout
+    assert '#   define MATRIX_ROW_PINS { F5 }' in result.stdout
+
+
+def test_generate_rules_mk():
+    result = check_subcommand('generate-rules-mk', '-kb', 'handwired/pytest/basic')
+    check_returncode(result)
+    assert 'BOOTLOADER ?= atmel-dfu' in result.stdout
+    assert 'MCU ?= atmega32u4' in result.stdout
+
+
+def test_generate_layouts():
+    result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic')
+    check_returncode(result)
+    assert '#define LAYOUT_custom(k0A) {' in result.stdout
-- 
cgit v1.2.3


From 620a946d01477b64ee2f719141aa35400c0188c6 Mon Sep 17 00:00:00 2001
From: Nick Brassel <nick@tzarc.org>
Date: Sat, 6 Feb 2021 11:27:46 +1100
Subject: Add STM32G431 and STM32G474 board definitions. (#11793)

* Add STM32G431 and STM32G474 board definitions.

* Add docs.
---
 lib/python/qmk/constants.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'lib')

diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 8a177eb6b5..3ed69f3bf9 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -10,7 +10,7 @@ QMK_FIRMWARE = Path.cwd()
 MAX_KEYBOARD_SUBFOLDERS = 5
 
 # Supported processor types
-CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411'
+CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32G431', 'STM32G474'
 LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
 VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
 
-- 
cgit v1.2.3


From 448cc1acd5f107f9679a48c605861654523432e7 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Mon, 8 Feb 2021 03:03:08 -0800
Subject: remove deprecated qmk json-keymap (#11823)

---
 lib/python/qmk/cli/__init__.py      |  1 -
 lib/python/qmk/cli/json/__init__.py |  5 -----
 lib/python/qmk/cli/json/keymap.py   | 16 ----------------
 3 files changed, 22 deletions(-)
 delete mode 100644 lib/python/qmk/cli/json/__init__.py
 delete mode 100755 lib/python/qmk/cli/json/keymap.py

(limited to 'lib')

diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 372c40921a..1732287da0 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -19,7 +19,6 @@ from . import flash
 from . import generate
 from . import hello
 from . import info
-from . import json
 from . import json2c
 from . import lint
 from . import list
diff --git a/lib/python/qmk/cli/json/__init__.py b/lib/python/qmk/cli/json/__init__.py
deleted file mode 100644
index f4ebfc45b4..0000000000
--- a/lib/python/qmk/cli/json/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""QMK CLI JSON Subcommands
-
-We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
-"""
-from . import keymap
diff --git a/lib/python/qmk/cli/json/keymap.py b/lib/python/qmk/cli/json/keymap.py
deleted file mode 100755
index 2af9faaa72..0000000000
--- a/lib/python/qmk/cli/json/keymap.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Generate a keymap.c from a configurator export.
-"""
-from pathlib import Path
-
-from milc import cli
-
-
-@cli.argument('-o', '--output', arg_only=True, type=Path, help='File to write to')
-@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
-@cli.argument('filename', arg_only=True, help='Configurator JSON file')
-@cli.subcommand('Creates a keymap.c from a QMK Configurator export.', hidden=True)
-def json_keymap(cli):
-    """Renamed to `qmk json2c`.
-    """
-    cli.log.error('This command has been renamed to `qmk json2c`.')
-    return False
-- 
cgit v1.2.3


From cd336b2b545798269405e8ffef0fe8958d4d27d4 Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Sat, 13 Feb 2021 10:26:45 -0800
Subject: bump to python 3.7 (#11408)

---
 lib/python/qmk/cli/__init__.py | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 1732287da0..a5f1f47679 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -27,6 +27,24 @@ from . import new
 from . import pyformat
 from . import pytest
 
-if sys.version_info[0] != 3 or sys.version_info[1] < 6:
-    cli.log.error('Your Python is too old! Please upgrade to Python 3.6 or later.')
+# Supported version information
+#
+# Based on the OSes we support these are the minimum python version available by default.
+# Last update: 2021 Jan 02
+#
+# Arch: 3.9
+# Debian: 3.7
+# Fedora 31: 3.7
+# Fedora 32: 3.8
+# Fedora 33: 3.9
+# FreeBSD: 3.7
+# Gentoo: 3.7
+# macOS: 3.9 (from homebrew)
+# msys2: 3.8
+# Slackware: 3.7
+# solus: 3.7
+# void: 3.9
+
+if sys.version_info[0] != 3 or sys.version_info[1] < 7:
+    cli.log.error('Your Python is too old! Please upgrade to Python 3.7 or later.')
     exit(127)
-- 
cgit v1.2.3


From 23ef327e118307d276677d30e3fda064ace6713b Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Wed, 24 Feb 2021 10:35:08 -0800
Subject: make LAYOUT parsing more robust

---
 lib/python/qmk/c_parse.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index ade3e38059..89dd278b7e 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -67,8 +67,10 @@ def find_layouts(file):
             layout = layout.strip()
             parsed_layout = [_default_key(key) for key in layout.split(',')]
 
-            for key in parsed_layout:
-                if key['label'] in matrix_locations:
+            for i, key in enumerate(parsed_layout):
+                if 'label' not in key:
+                    cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i)
+                elif key['label'] in matrix_locations:
                     key['matrix'] = matrix_locations[key['label']]
 
             parsed_layouts[macro_name] = {
-- 
cgit v1.2.3


From 1581ea48dcd48d0d3f42cc09b388c468aedec45d Mon Sep 17 00:00:00 2001
From: Zach White <skullydazed@gmail.com>
Date: Sat, 27 Feb 2021 12:00:50 -0800
Subject: Fix develop (#12039)

Fixes file encoding errors on Windows, and layouts not correctly merging into info.json.

* force utf8 encoding

* correctly merge layouts and layout aliases

* show what aliases point to
---
 lib/python/qmk/c_parse.py                   | 11 ++------
 lib/python/qmk/cli/chibios/confmigrate.py   |  8 +++---
 lib/python/qmk/cli/generate/layouts.py      |  4 +++
 lib/python/qmk/cli/info.py                  |  7 +++--
 lib/python/qmk/cli/kle2json.py              |  2 +-
 lib/python/qmk/info.py                      | 44 ++++++++++++++++++++++-------
 lib/python/qmk/keymap.py                    |  6 ++--
 lib/python/qmk/os_helpers/linux/__init__.py |  2 +-
 lib/python/qmk/tests/test_cli_commands.py   |  2 +-
 9 files changed, 56 insertions(+), 30 deletions(-)

(limited to 'lib')

diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index 89dd278b7e..d4f39c8839 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -46,7 +46,7 @@ def find_layouts(file):
     parsed_layouts = {}
 
     # Search the file for LAYOUT macros and aliases
-    file_contents = file.read_text()
+    file_contents = file.read_text(encoding='utf-8')
     file_contents = comment_remover(file_contents)
     file_contents = file_contents.replace('\\\n', '')
 
@@ -87,12 +87,7 @@ def find_layouts(file):
             except ValueError:
                 continue
 
-    # Populate our aliases
-    for alias, text in aliases.items():
-        if text in parsed_layouts and 'KEYMAP' not in alias:
-            parsed_layouts[alias] = parsed_layouts[text]
-
-    return parsed_layouts
+    return parsed_layouts, aliases
 
 
 def parse_config_h_file(config_h_file, config_h=None):
@@ -104,7 +99,7 @@ def parse_config_h_file(config_h_file, config_h=None):
     config_h_file = Path(config_h_file)
 
     if config_h_file.exists():
-        config_h_text = config_h_file.read_text()
+        config_h_text = config_h_file.read_text(encoding='utf-8')
         config_h_text = config_h_text.replace('\\\n', '')
         config_h_text = strip_multiline_comment(config_h_text)
 
diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py
index 3e348b2b07..89995931a4 100644
--- a/lib/python/qmk/cli/chibios/confmigrate.py
+++ b/lib/python/qmk/cli/chibios/confmigrate.py
@@ -40,7 +40,7 @@ file_header = """\
 
 
 def collect_defines(filepath):
-    with open(filepath, 'r') as f:
+    with open(filepath, 'r', encoding='utf-8') as f:
         content = f.read()
         define_search = re.compile(r'(?m)^#\s*define\s+(?:.*\\\r?\n)*.*$', re.MULTILINE)
         value_search = re.compile(r'^#\s*define\s+(?P<name>[a-zA-Z0-9_]+(\([^\)]*\))?)\s*(?P<value>.*)', re.DOTALL)
@@ -146,17 +146,17 @@ def chibios_confmigrate(cli):
         if cli.args.input.name == "chconf.h" and ("CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_chconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
-                with open(cli.args.input, "w") as out_file:
+                with open(cli.args.input, "w", encoding='utf-8') as out_file:
                     migrate_chconf_h(to_override, outfile=out_file)
 
         elif cli.args.input.name == "halconf.h" and ("HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_halconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
-                with open(cli.args.input, "w") as out_file:
+                with open(cli.args.input, "w", encoding='utf-8') as out_file:
                     migrate_halconf_h(to_override, outfile=out_file)
 
         elif cli.args.input.name == "mcuconf.h" and ("MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_mcuconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
-                with open(cli.args.input, "w") as out_file:
+                with open(cli.args.input, "w", encoding='utf-8') as out_file:
                     migrate_mcuconf_h(to_override, outfile=out_file)
diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py
index b7baae0651..15b289522e 100755
--- a/lib/python/qmk/cli/generate/layouts.py
+++ b/lib/python/qmk/cli/generate/layouts.py
@@ -82,6 +82,10 @@ def generate_layouts(cli):
         layouts_h_lines.append(rows)
         layouts_h_lines.append('}')
 
+    for alias, target in kb_info_json.get('layout_aliases', {}).items():
+        layouts_h_lines.append('')
+        layouts_h_lines.append('#define %s %s' % (alias, target))
+
     # Show the results
     layouts_h = '\n'.join(layouts_h_lines) + '\n'
 
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 87d7253d4b..a7ce8abf03 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -29,7 +29,7 @@ def show_keymap(kb_info_json, title_caps=True):
         else:
             cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
 
-        keymap_data = json.load(keymap_path.open())
+        keymap_data = json.load(keymap_path.open(encoding='utf-8'))
         layout_name = keymap_data['layout']
 
         for layer_num, layer in enumerate(keymap_data['layers']):
@@ -57,7 +57,7 @@ def show_matrix(kb_info_json, title_caps=True):
         # Build our label list
         labels = []
         for key in layout['layout']:
-            if key['matrix']:
+            if 'matrix' in key:
                 row = ROW_LETTERS[key['matrix'][0]]
                 col = COL_LETTERS[key['matrix'][1]]
 
@@ -91,6 +91,9 @@ def print_friendly_output(kb_info_json):
         cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
     cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
     cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
+    if 'layout_aliases' in kb_info_json:
+        aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]
+        cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
 
     if cli.config.info.layouts:
         show_layouts(kb_info_json, True)
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
index 66d504bfc2..3bb7443582 100755
--- a/lib/python/qmk/cli/kle2json.py
+++ b/lib/python/qmk/cli/kle2json.py
@@ -27,7 +27,7 @@ def kle2json(cli):
         cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', file_path)
         return False
     out_path = file_path.parent
-    raw_code = file_path.open().read()
+    raw_code = file_path.read_text(encoding='utf-8')
     # Check if info.json exists, allow overwrite with force
     if Path(out_path, "info.json").exists() and not cli.args.force:
         cli.log.error('File {fg_cyan}%s/info.json{style_reset_all} already exists, use -f or --force to overwrite.', out_path)
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 2accaba9e4..cf5dc6640b 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -45,7 +45,12 @@ def info_json(keyboard):
         info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
 
     # Populate layout data
-    for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items():
+    layouts, aliases = _find_all_layouts(info_data, keyboard)
+
+    if aliases:
+        info_data['layout_aliases'] = aliases
+
+    for layout_name, layout_json in layouts.items():
         if not layout_name.startswith('LAYOUT_kc'):
             layout_json['c_macro'] = True
             info_data['layouts'][layout_name] = layout_json
@@ -92,7 +97,7 @@ def _json_load(json_file):
     Note: file must be a Path object.
     """
     try:
-        return hjson.load(json_file.open())
+        return hjson.load(json_file.open(encoding='utf-8'))
 
     except json.decoder.JSONDecodeError as e:
         cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
@@ -415,21 +420,28 @@ def _merge_layouts(info_data, new_info_data):
 
 def _search_keyboard_h(path):
     current_path = Path('keyboards/')
+    aliases = {}
     layouts = {}
+
     for directory in path.parts:
         current_path = current_path / directory
         keyboard_h = '%s.h' % (directory,)
         keyboard_h_path = current_path / keyboard_h
         if keyboard_h_path.exists():
-            layouts.update(find_layouts(keyboard_h_path))
+            new_layouts, new_aliases = find_layouts(keyboard_h_path)
+            layouts.update(new_layouts)
+
+            for alias, alias_text in new_aliases.items():
+                if alias_text in layouts:
+                    aliases[alias] = alias_text
 
-    return layouts
+    return layouts, aliases
 
 
 def _find_all_layouts(info_data, keyboard):
     """Looks for layout macros associated with this keyboard.
     """
-    layouts = _search_keyboard_h(Path(keyboard))
+    layouts, aliases = _search_keyboard_h(Path(keyboard))
 
     if not layouts:
         # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
@@ -437,11 +449,15 @@ def _find_all_layouts(info_data, keyboard):
 
         for file in glob('keyboards/%s/*.h' % keyboard):
             if file.endswith('.h'):
-                these_layouts = find_layouts(file)
+                these_layouts, these_aliases = find_layouts(file)
+
                 if these_layouts:
                     layouts.update(these_layouts)
 
-    return layouts
+                if these_aliases:
+                    aliases.update(these_aliases)
+
+    return layouts, aliases
 
 
 def _log_error(info_data, message):
@@ -540,11 +556,19 @@ def merge_info_jsons(keyboard, info_data):
             cli.log.error('\t%s: %s', json_path, e.message)
             continue
 
-        # Mark the layouts as coming from json
-        for layout in new_info_data.get('layouts', {}).values():
-            layout['c_macro'] = False
+        # Merge layout data in
+        for layout_name, layout in new_info_data.get('layouts', {}).items():
+            if layout_name in info_data['layouts']:
+                for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
+                    existing_key.update(new_key)
+            else:
+                layout['c_macro'] = False
+                info_data['layouts'][layout_name] = layout
 
         # Update info_data with the new data
+        if 'layouts' in new_info_data:
+            del (new_info_data['layouts'])
+
         deep_update(info_data, new_info_data)
 
     return info_data
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
index 266532f503..d8495c38bc 100644
--- a/lib/python/qmk/keymap.py
+++ b/lib/python/qmk/keymap.py
@@ -42,7 +42,7 @@ def template_json(keyboard):
     template_file = Path('keyboards/%s/templates/keymap.json' % keyboard)
     template = {'keyboard': keyboard}
     if template_file.exists():
-        template.update(json.loads(template_file.read_text()))
+        template.update(json.load(template_file.open(encoding='utf-8')))
 
     return template
 
@@ -58,7 +58,7 @@ def template_c(keyboard):
     """
     template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
     if template_file.exists():
-        template = template_file.read_text()
+        template = template_file.read_text(encoding='utf-8')
     else:
         template = DEFAULT_KEYMAP_C
 
@@ -469,7 +469,7 @@ def parse_keymap_c(keymap_file, use_cpp=True):
         if use_cpp:
             keymap_file = _c_preprocess(keymap_file)
         else:
-            keymap_file = keymap_file.read_text()
+            keymap_file = keymap_file.read_text(encoding='utf-8')
 
     keymap = dict()
     keymap['layers'] = _get_layers(keymap_file)
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/os_helpers/linux/__init__.py
index a04ac4f8a9..9e73964e47 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/os_helpers/linux/__init__.py
@@ -95,7 +95,7 @@ def check_udev_rules():
 
         # Collect all rules from the config files
         for rule_file in udev_rules:
-            for line in rule_file.read_text().split('\n'):
+            for line in rule_file.read_text(encoding='utf-8').split('\n'):
                 line = line.strip()
                 if not line.startswith("#") and len(line):
                     current_rules.add(line)
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index 3efeddb85e..82c42a20e8 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -16,7 +16,7 @@ def check_subcommand(command, *args):
 def check_subcommand_stdin(file_to_read, command, *args):
     """Pipe content of a file to a command and return output.
     """
-    with open(file_to_read) as my_file:
+    with open(file_to_read, encoding='utf-8') as my_file:
         cmd = ['bin/qmk', command, *args]
         result = run(cmd, stdin=my_file, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
     return result
-- 
cgit v1.2.3