at master 16 kB view raw
1import contextlib 2from argcomplete.completers import FilesCompleter 3from pathlib import Path 4 5from milc import cli 6 7import qmk.path 8from qmk.info import get_modules 9from qmk.keyboard import keyboard_completer, keyboard_folder 10from qmk.commands import dump_lines 11from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE 12from qmk.community_modules import module_api_list, load_module_jsons, find_module_path 13 14 15@contextlib.contextmanager 16def _render_api_guard(lines, api): 17 if api.guard: 18 lines.append(f'#if {api.guard}') 19 yield 20 if api.guard: 21 lines.append(f'#endif // {api.guard}') 22 23 24def _render_api_header(api): 25 lines = [] 26 if api.header: 27 lines.append('') 28 with _render_api_guard(lines, api): 29 lines.append(f'#include <{api.header}>') 30 return lines 31 32 33def _render_keycodes(module_jsons): 34 lines = [] 35 lines.append('') 36 lines.append('enum {') 37 first = True 38 for module_json in module_jsons: 39 module_name = Path(module_json['module']).name 40 keycodes = module_json.get('keycodes', []) 41 if len(keycodes) > 0: 42 lines.append(f' // From module: {module_name}') 43 for keycode in keycodes: 44 key = keycode.get('key', None) 45 if first: 46 lines.append(f' {key} = QK_COMMUNITY_MODULE,') 47 first = False 48 else: 49 lines.append(f' {key},') 50 for alias in keycode.get('aliases', []): 51 lines.append(f' {alias} = {key},') 52 lines.append('') 53 lines.append(' LAST_COMMUNITY_MODULE_KEY') 54 lines.append('};') 55 lines.append('STATIC_ASSERT((int)LAST_COMMUNITY_MODULE_KEY <= (int)(QK_COMMUNITY_MODULE_MAX+1), "Too many community module keycodes");') 56 return lines 57 58 59def _render_api_declarations(api, module, user_kb=True): 60 lines = [] 61 lines.append('') 62 with _render_api_guard(lines, api): 63 if user_kb: 64 lines.append(f'{api.ret_type} {api.name}_{module}_user({api.args});') 65 lines.append(f'{api.ret_type} {api.name}_{module}_kb({api.args});') 66 lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') 67 return lines 68 69 70def _render_api_implementations(api, module): 71 module_name = Path(module).name 72 lines = [] 73 lines.append('') 74 with _render_api_guard(lines, api): 75 # _user 76 lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_user({api.args}) {{') 77 if api.ret_type == 'bool': 78 lines.append(' return true;') 79 elif api.ret_type in ['layer_state_t', 'report_mouse_t']: 80 lines.append(f' return {api.call_params};') 81 else: 82 pass 83 lines.append('}') 84 lines.append('') 85 86 # _kb 87 lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_kb({api.args}) {{') 88 if api.ret_type == 'bool': 89 lines.append(f' if(!{api.name}_{module_name}_user({api.call_params})) {{ return false; }}') 90 lines.append(' return true;') 91 elif api.ret_type in ['layer_state_t', 'report_mouse_t']: 92 lines.append(f' return {api.name}_{module_name}_user({api.call_params});') 93 else: 94 lines.append(f' {api.name}_{module_name}_user({api.call_params});') 95 lines.append('}') 96 lines.append('') 97 98 # module (non-suffixed) 99 lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}({api.args}) {{') 100 if api.ret_type == 'bool': 101 lines.append(f' if(!{api.name}_{module_name}_kb({api.call_params})) {{ return false; }}') 102 lines.append(' return true;') 103 elif api.ret_type in ['layer_state_t', 'report_mouse_t']: 104 lines.append(f' return {api.name}_{module_name}_kb({api.call_params});') 105 else: 106 lines.append(f' {api.name}_{module_name}_kb({api.call_params});') 107 lines.append('}') 108 return lines 109 110 111def _render_core_implementation(api, modules): 112 lines = [] 113 lines.append('') 114 with _render_api_guard(lines, api): 115 lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') 116 if api.ret_type == 'bool': 117 lines.append(' return true') 118 for module in modules: 119 module_name = Path(module).name 120 if api.ret_type == 'bool': 121 lines.append(f' && {api.name}_{module_name}({api.call_params})') 122 elif api.ret_type in ['layer_state_t', 'report_mouse_t']: 123 lines.append(f' {api.call_params} = {api.name}_{module_name}({api.call_params});') 124 else: 125 lines.append(f' {api.name}_{module_name}({api.call_params});') 126 if api.ret_type == 'bool': 127 lines.append(' ;') 128 elif api.ret_type in ['layer_state_t', 'report_mouse_t']: 129 lines.append(f' return {api.call_params};') 130 lines.append('}') 131 return lines 132 133 134def _generate_features_rules(features_dict): 135 lines = [] 136 for feature, enabled in features_dict.items(): 137 feature = feature.upper() 138 enabled = 'yes' if enabled else 'no' 139 lines.append(f'{feature}_ENABLE={enabled}') 140 return lines 141 142 143def _generate_modules_rules(keyboard, filename): 144 lines = [] 145 modules = get_modules(keyboard, filename) 146 if len(modules) > 0: 147 lines.append('') 148 lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLE=TRUE') 149 for module in modules: 150 module_path = qmk.path.unix_style_path(find_module_path(module)) 151 if not module_path: 152 raise FileNotFoundError(f"Module '{module}' not found.") 153 lines.append('') 154 lines.append(f'COMMUNITY_MODULES += {module_path.name}') # use module_path here instead of module as it may be a subdirectory 155 lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module_path.name.upper()}_ENABLE=TRUE') 156 lines.append(f'COMMUNITY_MODULE_PATHS += {module_path}') 157 lines.append(f'VPATH += {module_path}') 158 lines.append(f'SRC += $(wildcard {module_path}/{module_path.name}.c)') 159 lines.append(f'MODULE_NAME_{module_path.name.upper()} := {module_path.name}') 160 lines.append(f'MODULE_PATH_{module_path.name.upper()} := {module_path}') 161 lines.append(f'-include {module_path}/rules.mk') 162 163 module_jsons = load_module_jsons(modules) 164 for module_json in module_jsons: 165 if 'features' in module_json: 166 lines.append('') 167 lines.append(f'# Module: {module_json["module_name"]}') 168 lines.extend(_generate_features_rules(module_json['features'])) 169 return lines 170 171 172@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') 173@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 174@cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode") 175@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate rules.mk for.') 176@cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.') 177@cli.subcommand('Creates a community_modules_rules_mk from a keymap.json file.') 178def generate_community_modules_rules_mk(cli): 179 180 rules_mk_lines = [GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE] 181 182 rules_mk_lines.extend(_generate_modules_rules(cli.args.keyboard, cli.args.filename)) 183 184 # Show the results 185 dump_lines(cli.args.output, rules_mk_lines) 186 187 if cli.args.output: 188 if cli.args.quiet: 189 if cli.args.escape: 190 print(cli.args.output.as_posix().replace(' ', '\\ ')) 191 else: 192 print(cli.args.output) 193 else: 194 cli.log.info('Wrote rules.mk to %s.', cli.args.output) 195 196 197@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') 198@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 199@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.h for.') 200@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') 201@cli.subcommand('Creates a community_modules.h from a keymap.json file.') 202def generate_community_modules_h(cli): 203 """Creates a community_modules.h from a keymap.json file 204 """ 205 if cli.args.output and cli.args.output.name == '-': 206 cli.args.output = None 207 208 api_list, api_version, ver_major, ver_minor, ver_patch = module_api_list() 209 210 lines = [ 211 GPL2_HEADER_C_LIKE, 212 GENERATED_HEADER_C_LIKE, 213 '#pragma once', 214 '#include <stdint.h>', 215 '#include <stdbool.h>', 216 '#include <keycodes.h>', 217 '', 218 '#include "compiler_support.h"', 219 '', 220 '#define COMMUNITY_MODULES_API_VERSION_BUILDER(ver_major,ver_minor,ver_patch) (((((uint32_t)(ver_major))&0xFF) << 24) | ((((uint32_t)(ver_minor))&0xFF) << 16) | (((uint32_t)(ver_patch))&0xFF))', 221 f'#define COMMUNITY_MODULES_API_VERSION COMMUNITY_MODULES_API_VERSION_BUILDER({ver_major},{ver_minor},{ver_patch})', 222 f'#define ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(ver_major,ver_minor,ver_patch) STATIC_ASSERT(COMMUNITY_MODULES_API_VERSION_BUILDER(ver_major,ver_minor,ver_patch) <= COMMUNITY_MODULES_API_VERSION, "Community module requires a newer version of QMK modules API -- needs: " #ver_major "." #ver_minor "." #ver_patch ", current: {api_version}.")', 223 '', 224 'typedef struct keyrecord_t keyrecord_t; // forward declaration so we don\'t need to include quantum.h', 225 '', 226 ] 227 228 modules = get_modules(cli.args.keyboard, cli.args.filename) 229 module_jsons = load_module_jsons(modules) 230 if len(modules) > 0: 231 lines.extend(_render_keycodes(module_jsons)) 232 233 for api in api_list: 234 lines.extend(_render_api_header(api)) 235 236 for module in modules: 237 lines.append('') 238 lines.append(f'// From module: {module}') 239 for api in api_list: 240 lines.extend(_render_api_declarations(api, Path(module).name)) 241 lines.append('') 242 243 lines.append('// Core wrapper') 244 for api in api_list: 245 lines.extend(_render_api_declarations(api, 'modules', user_kb=False)) 246 247 dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) 248 249 250@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') 251@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 252@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') 253@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') 254@cli.subcommand('Creates a community_modules.c from a keymap.json file.') 255def generate_community_modules_c(cli): 256 """Creates a community_modules.c from a keymap.json file 257 """ 258 if cli.args.output and cli.args.output.name == '-': 259 cli.args.output = None 260 261 api_list, _, _, _, _ = module_api_list() 262 263 lines = [ 264 GPL2_HEADER_C_LIKE, 265 GENERATED_HEADER_C_LIKE, 266 '', 267 '#include "community_modules.h"', 268 ] 269 270 modules = get_modules(cli.args.keyboard, cli.args.filename) 271 if len(modules) > 0: 272 273 for module in modules: 274 for api in api_list: 275 lines.extend(_render_api_implementations(api, Path(module).name)) 276 277 for api in api_list: 278 lines.extend(_render_core_implementation(api, modules)) 279 280 dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) 281 282 283def _generate_include_per_module(cli, include_file_name): 284 """Generates C code to include "<module_path>/include_file_name" for each module.""" 285 if cli.args.output and cli.args.output.name == '-': 286 cli.args.output = None 287 288 lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE] 289 290 for module in get_modules(cli.args.keyboard, cli.args.filename): 291 full_path = f'{find_module_path(module)}/{include_file_name}' 292 lines.append('') 293 lines.append(f'#if __has_include("{full_path}")') 294 lines.append(f'#include "{full_path}"') 295 lines.append(f'#endif // __has_include("{full_path}")') 296 297 dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) 298 299 300@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') 301@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 302@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules_introspection.h for.') 303@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') 304@cli.subcommand('Creates a community_modules_introspection.h from a keymap.json file.') 305def generate_community_modules_introspection_h(cli): 306 """Creates a community_modules_introspection.h from a keymap.json file 307 """ 308 _generate_include_per_module(cli, 'introspection.h') 309 310 311@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') 312@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 313@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') 314@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') 315@cli.subcommand('Creates a community_modules_introspection.c from a keymap.json file.') 316def generate_community_modules_introspection_c(cli): 317 """Creates a community_modules_introspection.c from a keymap.json file 318 """ 319 _generate_include_per_module(cli, 'introspection.c') 320 321 322@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') 323@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 324@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate led_matrix_community_modules.inc for.') 325@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') 326@cli.subcommand('Creates an led_matrix_community_modules.inc from a keymap.json file.') 327def generate_led_matrix_community_modules_inc(cli): 328 """Creates an led_matrix_community_modules.inc from a keymap.json file 329 """ 330 _generate_include_per_module(cli, 'led_matrix_module.inc') 331 332 333@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') 334@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 335@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate rgb_matrix_community_modules.inc for.') 336@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') 337@cli.subcommand('Creates an rgb_matrix_community_modules.inc from a keymap.json file.') 338def generate_rgb_matrix_community_modules_inc(cli): 339 """Creates an rgb_matrix_community_modules.inc from a keymap.json file 340 """ 341 _generate_include_per_module(cli, 'rgb_matrix_module.inc')