Initial implementation of XAP protocol.

This commit is contained in:
Nick Brassel 2021-08-11 21:08:32 +10:00
parent f4c447f2df
commit eba91c6e28
34 changed files with 1934 additions and 4 deletions

65
lib/python/qmk/casing.py Executable file
View file

@ -0,0 +1,65 @@
"""This script handles conversion between snake and camel casing.
"""
import re
_words_expr = re.compile(r"([a-zA-Z][^A-Z0-9]*|[0-9]+)")
_lower_snake_case_expr = re.compile(r'^[a-z][a-z0-9_]*$')
_upper_snake_case_expr = re.compile(r'^[A-Z][A-Z0-9_]*$')
def _is_snake_case(str):
"""Checks if the supplied string is already in snake case.
"""
match = _lower_snake_case_expr.match(str)
if match:
return True
match = _upper_snake_case_expr.match(str)
if match:
return True
return False
def _split_snake_case(str):
"""Splits up a string based on underscores, if it's in snake casing.
"""
if _is_snake_case(str):
return [s.lower() for s in str.split("_")]
return str
def _split_camel_case(str):
"""Splits up a string based on capitalised camel casing.
"""
return _words_expr.findall(str)
def _split_cased_words(str):
return _split_snake_case(str) if _is_snake_case(str) else _split_camel_case(str)
def to_snake(str):
str = "_".join([word.strip().lower() for word in _split_cased_words(str)])
# Fix acronyms
str = str.replace('i_d', 'id')
str = str.replace('x_a_p', 'xap')
str = str.replace('q_m_k', 'qmk')
return str
def to_upper_snake(str):
return to_snake(str).upper()
def to_camel(str):
def _acronym(w):
if w.strip().lower() == 'qmk':
return 'QMK'
elif w.strip().lower() == 'xap':
return 'XAP'
elif w.strip().lower() == 'id':
return 'ID'
return w.title()
return "".join([_acronym(word) for word in _split_cased_words(str)])

View file

@ -66,6 +66,9 @@ subcommands = [
'qmk.cli.new.keymap',
'qmk.cli.pyformat',
'qmk.cli.pytest',
'qmk.cli.xap.generate_docs',
'qmk.cli.xap.generate_json',
'qmk.cli.xap.generate_qmk',
]

View file

View file

@ -0,0 +1,11 @@
"""This script generates the XAP protocol documentation.
"""
from milc import cli
from qmk.xap.gen_docs.generator import generate_docs
@cli.subcommand('Generates the XAP protocol documentation.', hidden=False if cli.config.user.developer else True)
def xap_generate_docs(cli):
"""Generates the XAP protocol documentation by merging the definitions files, and producing the corresponding Markdown document under `/docs/`.
"""
generate_docs()

View file

@ -0,0 +1,13 @@
"""This script generates the consolidated XAP protocol definitions.
"""
import hjson
from milc import cli
from qmk.xap.common import latest_xap_defs
@cli.subcommand('Generates the consolidated XAP protocol definitions.', hidden=False if cli.config.user.developer else True)
def xap_generate_json(cli):
"""Generates the consolidated XAP protocol definitions.
"""
defs = latest_xap_defs()
print(hjson.dumps(defs))

View file

@ -0,0 +1,24 @@
"""This script generates the XAP protocol generated sources to be compiled into QMK firmware.
"""
from milc import cli
from qmk.path import normpath
from qmk.xap.gen_firmware.inline_generator import generate_inline
from qmk.xap.gen_firmware.header_generator import generate_header
@cli.argument('-o', '--output', type=normpath, help='File to write to')
@cli.subcommand('Generates the XAP protocol include.', hidden=False if cli.config.user.developer else True)
def xap_generate_qmk_inc(cli):
"""Generates the XAP protocol inline codegen file, generated during normal build.
"""
generate_inline(cli.args.output)
@cli.argument('-o', '--output', type=normpath, help='File to write to')
@cli.argument('-kb', '--keyboard', help='Name of the keyboard')
@cli.subcommand('Generates the XAP protocol include.', hidden=False if cli.config.user.developer else True)
def xap_generate_qmk_h(cli):
"""Generates the XAP protocol header file, generated during normal build.
"""
generate_header(cli.args.output, cli.args.keyboard)

View file

@ -87,11 +87,14 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
return create_make_target(':'.join(make_args), parallel, **env_vars)
def get_git_version(current_time, repo_dir='.', check_dir='.'):
def get_git_version(current_time=None, repo_dir='.', check_dir='.'):
"""Returns the current git version for a repo, or the current time.
"""
git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
if current_time is None:
current_time = strftime(time_fmt)
if repo_dir != '.':
repo_dir = Path('lib') / repo_dir
@ -118,7 +121,7 @@ def create_version_h(skip_git=False, skip_all=False):
if skip_all:
current_time = "1970-01-01-00:00:00"
else:
current_time = strftime(time_fmt)
current_time = None
if skip_git:
git_version = "NA"

View file

@ -1,6 +1,7 @@
"""Information that should be available to the python library.
"""
from os import environ
from datetime import date
from pathlib import Path
# The root of the qmk_firmware tree.
@ -36,3 +37,92 @@ LED_INDICATORS = {
# Constants that should match their counterparts in make
BUILD_DIR = environ.get('BUILD_DIR', '.build')
KEYBOARD_OUTPUT_PREFIX = f'{BUILD_DIR}/obj_'
# Headers for generated files
this_year = date.today().year
GPL2_HEADER_C_LIKE = f'''\
/* Copyright {this_year} QMK
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'''
GPL2_HEADER_SH_LIKE = f'''\
# Copyright {this_year} QMK
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
GENERATED_HEADER_C_LIKE = '''\
/*******************************************************************************
88888888888 888 d8b .d888 d8b 888 d8b
888 888 Y8P d88P" Y8P 888 Y8P
888 888 888 888
888 88888b. 888 .d8888b 888888 888 888 .d88b. 888 .d8888b
888 888 "88b 888 88K 888 888 888 d8P Y8b 888 88K
888 888 888 888 "Y8888b. 888 888 888 88888888 888 "Y8888b.
888 888 888 888 X88 888 888 888 Y8b. 888 X88
888 888 888 888 88888P' 888 888 888 "Y8888 888 88888P'
888 888
888 888
888 888
.d88b. .d88b. 88888b. .d88b. 888d888 8888b. 888888 .d88b. .d88888
d88P"88b d8P Y8b 888 "88b d8P Y8b 888P" "88b 888 d8P Y8b d88" 888
888 888 88888888 888 888 88888888 888 .d888888 888 88888888 888 888
Y88b 888 Y8b. 888 888 Y8b. 888 888 888 Y88b. Y8b. Y88b 888
"Y88888 "Y8888 888 888 "Y8888 888 "Y888888 "Y888 "Y8888 "Y88888
888
Y8b d88P
"Y88P"
*******************************************************************************/
'''
GENERATED_HEADER_SH_LIKE = '''\
################################################################################
#
# 88888888888 888 d8b .d888 d8b 888 d8b
# 888 888 Y8P d88P" Y8P 888 Y8P
# 888 888 888 888
# 888 88888b. 888 .d8888b 888888 888 888 .d88b. 888 .d8888b
# 888 888 "88b 888 88K 888 888 888 d8P Y8b 888 88K
# 888 888 888 888 "Y8888b. 888 888 888 88888888 888 "Y8888b.
# 888 888 888 888 X88 888 888 888 Y8b. 888 X88
# 888 888 888 888 88888P' 888 888 888 "Y8888 888 88888P'
#
# 888 888
# 888 888
# 888 888
# .d88b. .d88b. 88888b. .d88b. 888d888 8888b. 888888 .d88b. .d88888
# d88P"88b d8P Y8b 888 "88b d8P Y8b 888P" "88b 888 d8P Y8b d88" 888
# 888 888 88888888 888 888 88888888 888 .d888888 888 88888888 888 888
# Y88b 888 Y8b. 888 888 Y8b. 888 888 888 Y88b. Y8b. Y88b 888
# "Y88888 "Y8888 888 888 "Y8888 888 "Y888888 "Y888 "Y8888 "Y88888
# 888
# Y8b d88P
# "Y88P"
#
################################################################################
'''

View file

81
lib/python/qmk/xap/common.py Executable file
View file

@ -0,0 +1,81 @@
"""This script handles the XAP protocol data files.
"""
import re
import hjson
from typing import OrderedDict
from qmk.constants import QMK_FIRMWARE
def _merge_ordered_dicts(dicts):
"""Merges nested OrderedDict objects resulting from reading a hjson file.
Later input dicts overrides earlier dicts for plain values.
Arrays will be appended. If the first entry of an array is "!reset!", the contents of the array will be cleared and replaced with RHS.
Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
"""
result = OrderedDict()
def add_entry(target, k, v):
if k in target and isinstance(v, OrderedDict):
if "!reset!" in v:
target[k] = v
else:
target[k] = _merge_ordered_dicts([target[k], v])
if "!reset!" in target[k]:
del target[k]["!reset!"]
elif k in target and isinstance(v, list):
if v[0] == '!reset!':
target[k] = v[1:]
else:
target[k] = target[k] + v
else:
target[k] = v
for d in dicts:
for (k, v) in d.items():
add_entry(result, k, v)
return result
def get_xap_definition_files():
"""Get the sorted list of XAP definition files, from <QMK>/data/xap.
"""
xap_defs = QMK_FIRMWARE / "data" / "xap"
return list(sorted(xap_defs.glob('**/xap_*.hjson')))
def update_xap_definitions(original, new):
"""Creates a new XAP definition object based on an original and the new supplied object.
Both inputs must be of type OrderedDict.
Later input dicts overrides earlier dicts for plain values.
Arrays will be appended. If the first entry of an array is "!reset!", the contents of the array will be cleared and replaced with RHS.
Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
"""
if original is None:
original = OrderedDict()
return _merge_ordered_dicts([original, new])
def latest_xap_defs():
"""Gets the latest version of the XAP definitions.
"""
definitions = [hjson.load(file.open(encoding='utf-8')) for file in get_xap_definition_files()]
return _merge_ordered_dicts(definitions)
def route_conditions(route_stack):
"""Handles building the C preprocessor conditional based on the current route.
"""
conditions = []
for route in route_stack:
if 'enable_if_preprocessor' in route:
conditions.append(route['enable_if_preprocessor'])
if len(conditions) == 0:
return None
return "(" + ' && '.join([f'({c})' for c in conditions]) + ")"

View file

View file

@ -0,0 +1,103 @@
"""This script generates the XAP protocol documentation.
"""
import hjson
from qmk.constants import QMK_FIRMWARE
from qmk.xap.common import get_xap_definition_files, update_xap_definitions
def _update_type_docs(overall):
defs = overall['type_docs']
type_docs = []
for (k, v) in sorted(defs.items(), key=lambda x: x[0]):
type_docs.append(f'| _{k}_ | {v} |')
desc_str = "\n".join(type_docs)
overall['documentation']['!type_docs!'] = f'''\
| Name | Definition |
| -- | -- |
{desc_str}
'''
def _update_term_definitions(overall):
defs = overall['term_definitions']
term_descriptions = []
for (k, v) in sorted(defs.items(), key=lambda x: x[0]):
term_descriptions.append(f'| _{k}_ | {v} |')
desc_str = "\n".join(term_descriptions)
overall['documentation']['!term_definitions!'] = f'''\
| Name | Definition |
| -- | -- |
{desc_str}
'''
def _update_response_flags(overall):
flags = overall['response_flags']['bits']
for n in range(0, 8):
if str(n) not in flags:
flags[str(n)] = {"name": "-", "description": "-"}
header = '| ' + " | ".join([f'Bit {n}' for n in range(7, -1, -1)]) + ' |'
dividers = '|' + "|".join(['--' for n in range(7, -1, -1)]) + '|'
bit_names = '| ' + " | ".join([flags[str(n)]['name'] for n in range(7, -1, -1)]) + ' |'
bit_descriptions = ''
for n in range(7, -1, -1):
bit_desc = flags[str(n)]
if bit_desc['name'] != '-':
desc = bit_desc['description']
bit_descriptions = bit_descriptions + f'\n* `Bit {n}`: {desc}'
overall['documentation']['!response_flags!'] = f'''\
{header}
{dividers}
{bit_names}
{bit_descriptions}
'''
def generate_docs():
"""Generates the XAP protocol documentation by merging the definitions files, and producing the corresponding Markdown document under `/docs/`.
"""
docs_list = []
overall = None
for file in get_xap_definition_files():
overall = update_xap_definitions(overall, hjson.load(file.open(encoding='utf-8')))
try:
if 'type_docs' in overall:
_update_type_docs(overall)
if 'term_definitions' in overall:
_update_term_definitions(overall)
if 'response_flags' in overall:
_update_response_flags(overall)
except:
print(hjson.dumps(overall))
exit(1)
output_doc = QMK_FIRMWARE / "docs" / f"{file.stem}.md"
docs_list.append(output_doc)
with open(output_doc, "w", encoding='utf-8') as out_file:
for e in overall['documentation']['order']:
out_file.write(overall['documentation'][e].strip())
out_file.write('\n\n')
output_doc = QMK_FIRMWARE / "docs" / f"xap_protocol.md"
with open(output_doc, "w", encoding='utf-8') as out_file:
out_file.write('''\
# XAP Protocol Reference
''')
for file in reversed(sorted(docs_list)):
ver = file.stem[4:]
out_file.write(f'* [XAP Version {ver}]({file.name})\n')

View file

@ -0,0 +1,136 @@
"""This script generates the XAP protocol generated header to be compiled into QMK.
"""
import re
import pyhash
from qmk.commands import get_git_version
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
from qmk.xap.common import latest_xap_defs, route_conditions
def _append_route_defines(lines, container, container_id=None, route_stack=None):
"""Handles building the list of the XAP routes, combining parent and child names together, as well as the route number.
"""
if route_stack is None:
route_stack = [container]
else:
route_stack.append(container)
route_name = '_'.join([r['define'] for r in route_stack])
if container_id:
lines.append(f'#define {route_name} {container_id}')
if 'routes' in container:
for route_id in container['routes']:
route = container['routes'][route_id]
_append_route_defines(lines, route, route_id, route_stack)
route_stack.pop()
def _append_route_masks(lines, container, container_id=None, route_stack=None):
"""Handles creating the equivalent XAP route masks, for capabilities checks. Forces value of `0` if disabled in the firmware.
"""
if route_stack is None:
route_stack = [container]
else:
route_stack.append(container)
route_name = '_'.join([r['define'] for r in route_stack])
condition = route_conditions(route_stack)
if container_id:
if condition:
lines.append('')
lines.append(f'#if {condition}')
lines.append(f'#define {route_name}_MASK (1ul << ({route_name}))')
if condition:
lines.append(f'#else // {condition}')
lines.append(f'#define {route_name}_MASK 0')
lines.append(f'#endif // {condition}')
lines.append('')
if 'routes' in container:
for route_id in container['routes']:
route = container['routes'][route_id]
_append_route_masks(lines, route, route_id, route_stack)
route_stack.pop()
def _append_route_capabilities(lines, container, container_id=None, route_stack=None):
"""Handles creating the equivalent XAP route masks, for capabilities checks. Forces value of `0` if disabled in the firmware.
"""
if route_stack is None:
route_stack = [container]
else:
route_stack.append(container)
route_name = '_'.join([r['define'] for r in route_stack])
if 'routes' in container:
lines.append('')
lines.append(f'#define {route_name}_CAPABILITIES (0 \\')
if 'routes' in container:
for route_id in container['routes']:
route = container['routes'][route_id]
route_stack.append(route)
child_name = '_'.join([r['define'] for r in route_stack])
lines.append(f' | ({child_name}_MASK) \\')
route_stack.pop()
lines.append(' )')
if 'routes' in container:
for route_id in container['routes']:
route = container['routes'][route_id]
_append_route_capabilities(lines, route, route_id, route_stack)
route_stack.pop()
def generate_header(output_file, keyboard):
"""Generates the XAP protocol header file, generated during normal build.
"""
xap_defs = latest_xap_defs()
# Preamble
lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '']
# Versions
prog = re.compile(r'^(\d+)\.(\d+)\.(\d+)')
b = prog.match(xap_defs['version'])
lines.append(f'#define XAP_BCD_VERSION 0x{int(b.group(1)):02d}{int(b.group(2)):02d}{int(b.group(3)):04d}ul')
b = prog.match(get_git_version())
lines.append(f'#define QMK_BCD_VERSION 0x{int(b.group(1)):02d}{int(b.group(2)):02d}{int(b.group(3)):04d}ul')
keyboard_id = pyhash.murmur3_32()(keyboard)
lines.append(f'#define XAP_KEYBOARD_IDENTIFIER 0x{keyboard_id:08X}ul')
lines.append('')
# Append the route and command defines
_append_route_defines(lines, xap_defs)
lines.append('')
_append_route_masks(lines, xap_defs)
lines.append('')
_append_route_capabilities(lines, xap_defs)
lines.append('')
# Generate the full output
xap_generated_inl = '\n'.join(lines)
# Clean up newlines
while "\n\n\n" in xap_generated_inl:
xap_generated_inl = xap_generated_inl.replace("\n\n\n", "\n\n")
if output_file:
if output_file.name == '-':
print(xap_generated_inl)
else:
output_file.parent.mkdir(parents=True, exist_ok=True)
if output_file.exists():
output_file.replace(output_file.parent / (output_file.name + '.bak'))
output_file.write_text(xap_generated_inl)

View file

@ -0,0 +1,222 @@
"""This script generates the XAP protocol generated header to be compiled into QMK.
"""
import pyhash
from qmk.casing import to_snake
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
from qmk.xap.common import latest_xap_defs, route_conditions
def _get_c_type(xap_type):
if xap_type == 'bool':
return 'bool'
elif xap_type == 'u8':
return 'uint8_t'
elif xap_type == 'u16':
return 'uint16_t'
elif xap_type == 'u32':
return 'uint32_t'
elif xap_type == 'u64':
return 'uint64_t'
elif xap_type == 'struct':
return 'struct'
elif xap_type == 'string':
return 'const char *'
return 'unknown'
def _get_route_type(container):
if 'routes' in container:
return 'XAP_ROUTE'
elif 'return_constant' in container:
if container['return_type'] == 'u32':
return 'XAP_VALUE'
elif container['return_type'] == 'struct':
return 'XAP_CONST_MEM'
elif container['return_type'] == 'string':
return 'XAP_CONST_MEM'
elif 'return_getter' in container:
if container['return_type'] == 'u32':
return 'XAP_GETTER'
return 'UNSUPPORTED'
def _append_routing_table_declaration(lines, container, container_id, route_stack):
route_stack.append(container)
route_name = to_snake('_'.join([r['define'] for r in route_stack]))
if 'routes' in container:
pass
elif 'return_constant' in container:
if container['return_type'] == 'u32':
pass
elif container['return_type'] == 'struct':
lines.append('')
lines.append(f'static const struct {route_name}_t {{')
for member in container['return_struct_members']:
member_type = _get_c_type(member['type'])
member_name = to_snake(member['name'])
lines.append(f' const {member_type} {member_name};')
lines.append(f'}} {route_name}_data PROGMEM = {{')
for constant in container['return_constant']:
lines.append(f' {constant},')
lines.append(f'}};')
elif container['return_type'] == 'string':
constant = container['return_constant']
lines.append('')
lines.append(f'static const char {route_name}_str[] PROGMEM = {constant};')
elif 'return_getter' in container:
if container['return_type'] == 'u32':
lines.append('')
lines.append(f'extern uint32_t {route_name}_getter(void);')
elif container['return_type'] == 'struct':
pass
route_stack.pop()
def _append_routing_table_entry_flags(lines, container, container_id, route_stack):
is_secure = 1 if ('secure' in container and container['secure'] is True) else 0
lines.append(f' .flags = {{')
lines.append(f' .type = {_get_route_type(container)},')
lines.append(f' .is_secure = {is_secure},')
lines.append(f' }},')
def _append_routing_table_entry_route(lines, container, container_id, route_stack):
route_name = to_snake('_'.join([r['define'] for r in route_stack]))
lines.append(f' .child_routes = {route_name}_table,')
lines.append(f' .child_routes_len = sizeof({route_name}_table)/sizeof(xap_route_t),')
def _append_routing_table_entry_u32value(lines, container, container_id, route_stack):
value = container['return_constant']
lines.append(f' .u32value = {value},')
def _append_routing_table_entry_u32getter(lines, container, container_id, route_stack):
route_name = to_snake('_'.join([r['define'] for r in route_stack]))
lines.append(f' .u32getter = &{route_name}_getter,')
def _append_routing_table_entry_const_data(lines, container, container_id, route_stack):
route_name = to_snake('_'.join([r['define'] for r in route_stack]))
lines.append(f' .const_data = &{route_name}_data,')
lines.append(f' .const_data_len = sizeof({route_name}_data),')
def _append_routing_table_entry_string(lines, container, container_id, route_stack):
route_name = to_snake('_'.join([r['define'] for r in route_stack]))
lines.append(f' .const_data = {route_name}_str,')
lines.append(f' .const_data_len = sizeof({route_name}_str) - 1,')
def _append_routing_table_entry(lines, container, container_id, route_stack):
route_stack.append(container)
route_name = '_'.join([r['define'] for r in route_stack])
condition = route_conditions(route_stack)
if condition:
lines.append(f'#if {condition}')
lines.append(f' [{route_name}] = {{')
_append_routing_table_entry_flags(lines, container, container_id, route_stack)
if 'routes' in container:
_append_routing_table_entry_route(lines, container, container_id, route_stack)
elif 'return_constant' in container:
if container['return_type'] == 'u32':
_append_routing_table_entry_u32value(lines, container, container_id, route_stack)
elif container['return_type'] == 'struct':
_append_routing_table_entry_const_data(lines, container, container_id, route_stack)
elif container['return_type'] == 'string':
_append_routing_table_entry_string(lines, container, container_id, route_stack)
elif 'return_getter' in container:
if container['return_type'] == 'u32':
_append_routing_table_entry_u32getter(lines, container, container_id, route_stack)
lines.append(f' }},')
if condition:
lines.append(f'#endif // {condition}')
route_stack.pop()
def _append_routing_tables(lines, container, container_id=None, route_stack=None):
"""Handles building the list of the XAP routes, combining parent and child names together, as well as the route number.
"""
if route_stack is None:
route_stack = [container]
else:
route_stack.append(container)
route_name = to_snake('_'.join([r['define'] for r in route_stack]))
condition = route_conditions(route_stack)
if 'routes' in container:
for route_id in container['routes']:
route = container['routes'][route_id]
_append_routing_tables(lines, route, route_id, route_stack)
for route_id in container['routes']:
route = container['routes'][route_id]
_append_routing_table_declaration(lines, route, route_id, route_stack)
lines.append('')
if condition:
lines.append(f'#if {condition}')
lines.append(f'static const xap_route_t {route_name}_table[] PROGMEM = {{')
for route_id in container['routes']:
route = container['routes'][route_id]
_append_routing_table_entry(lines, route, route_id, route_stack)
lines.append('};')
if condition:
lines.append(f'#endif // {condition}')
lines.append('')
route_stack.pop()
def generate_inline(output_file):
"""Generates the XAP protocol header file, generated during normal build.
"""
xap_defs = latest_xap_defs()
# Preamble
lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '']
# Add all the generated code
_append_routing_tables(lines, xap_defs)
# Generate the full output
xap_generated_inl = '\n'.join(lines)
# Clean up newlines
while "\n\n\n" in xap_generated_inl:
xap_generated_inl = xap_generated_inl.replace("\n\n\n", "\n\n")
if output_file:
if output_file.name == '-':
print(xap_generated_inl)
else:
output_file.parent.mkdir(parents=True, exist_ok=True)
if output_file.exists():
output_file.replace(output_file.parent / (output_file.name + '.bak'))
output_file.write_text(xap_generated_inl)