- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
818 lines
29 KiB
Python
818 lines
29 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright Pionix GmbH and Contributors to EVerest
|
|
#
|
|
"""
|
|
author: aw@pionix.de
|
|
FIXME (aw): Module documentation.
|
|
"""
|
|
|
|
from .type_parsing import TypeParser
|
|
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import re
|
|
from typing import Dict, List, Tuple
|
|
import keyword
|
|
|
|
import json
|
|
import jsonschema
|
|
|
|
import yaml
|
|
|
|
from uuid import uuid4
|
|
|
|
import stringcase
|
|
|
|
|
|
everest_dirs: List[Path] = []
|
|
|
|
|
|
class EVerestParsingException(SystemExit):
|
|
pass
|
|
|
|
|
|
def snake_case(word: str) -> str:
|
|
"""Convert capital case to snake case
|
|
Only alphanumerical characters are allowed. Only inserts camelcase
|
|
between a consecutive lower and upper alphabetical character and
|
|
lowers first letter
|
|
"""
|
|
|
|
out = ''
|
|
if len(word) == 0:
|
|
return out
|
|
cur_char: str = ''
|
|
for i in range(len(word)):
|
|
if i == 0:
|
|
cur_char = word[i]
|
|
if not cur_char.isalnum():
|
|
raise Exception('Non legal character in: ' + word)
|
|
out += cur_char.lower()
|
|
continue
|
|
last_char: str = cur_char
|
|
cur_char = word[i]
|
|
if (last_char.islower() and last_char.isalpha() and cur_char.isupper() and cur_char.isalpha):
|
|
out += '_'
|
|
if not cur_char.isalnum():
|
|
out += '_'
|
|
else:
|
|
out += cur_char.lower()
|
|
|
|
return out
|
|
|
|
|
|
def create_dummy_result(json_type) -> str:
|
|
def primitive_to_sample_value(type):
|
|
if type == 'boolean':
|
|
return 'true'
|
|
elif type == 'integer':
|
|
return '42'
|
|
elif type == 'number':
|
|
return '3.14'
|
|
elif type == 'string':
|
|
return '"everest"'
|
|
elif type == 'object':
|
|
return '{}'
|
|
elif type == 'array':
|
|
return '{}'
|
|
else:
|
|
raise Exception(f'This json type "{type}" is not known or not implemented')
|
|
|
|
if isinstance(json_type, list):
|
|
return '{}' # default initialization for variant
|
|
else:
|
|
return primitive_to_sample_value(json_type)
|
|
|
|
|
|
cpp_type_map = {
|
|
'null': 'std::nullptr_t', # FIXME (aw): who gets the null, json? or the variant
|
|
'integer': 'int',
|
|
'number': 'double',
|
|
'string': 'std::string',
|
|
'boolean': 'bool',
|
|
'array': 'Array',
|
|
'object': 'Object',
|
|
}
|
|
|
|
def clang_format(config_file_path, file_info):
|
|
# check if we handle cpp and hpp files
|
|
if not file_info['path'].suffix in ('.hpp', '.cpp'):
|
|
return
|
|
|
|
clang_format_path = shutil.which('clang-format')
|
|
if clang_format_path is None:
|
|
raise RuntimeError('Could not find clang-format executable - needed when passing clang-format config file')
|
|
|
|
config_file_path = Path(config_file_path)
|
|
if not config_file_path.is_dir():
|
|
raise RuntimeError(f'Supplied directory for the clang-format file ({config_file_path}) does not exist')
|
|
|
|
if not (config_file_path / '.clang-format').exists():
|
|
raise RuntimeError(f'Supplied directory for the clang-format file '
|
|
f'({config_file_path}) does not contain a .clang-format file')
|
|
|
|
content = file_info['content']
|
|
|
|
run_parms = {'capture_output': True, 'cwd': config_file_path, 'encoding': 'utf-8', 'input': content}
|
|
|
|
format_cmd = subprocess.run([clang_format_path, '--style=file'], **run_parms)
|
|
|
|
if format_cmd.returncode != 0:
|
|
raise RuntimeError(f'clang-format failed with:\n{format_cmd.stderr}')
|
|
|
|
file_info['content'] = format_cmd.stdout
|
|
|
|
|
|
def resolve_everest_dir_path(postfix):
|
|
resolved_path = None
|
|
for everest_dir in everest_dirs:
|
|
path = everest_dir / postfix
|
|
if path.exists():
|
|
resolved_path = path
|
|
break
|
|
|
|
if not resolved_path:
|
|
raise EVerestParsingException(
|
|
f'Could not resolve "{postfix}" in any of the provided everest-dir ({everest_dirs}).')
|
|
|
|
return resolved_path
|
|
|
|
|
|
def build_type_info(name, json_type):
|
|
ti = {
|
|
'name': name,
|
|
'is_variant': False,
|
|
'cpp_type': None,
|
|
'json_type': json_type
|
|
}
|
|
|
|
if isinstance(json_type, list):
|
|
ti['is_variant'] = True
|
|
ti['cpp_type'] = [cpp_type_map[e] for e in json_type if e != 'null']
|
|
ti['cpp_type'].sort() # sort, so template generation might get reduced
|
|
# prepend boost::blank if type 'null' exists, so the variant
|
|
# gets default initialized with blank
|
|
if 'null' in json_type:
|
|
ti['cpp_type'].insert(0, cpp_type_map['null'])
|
|
else:
|
|
ti['cpp_type'] = cpp_type_map[json_type]
|
|
|
|
return ti
|
|
|
|
|
|
type_headers = set()
|
|
parsed_types: List = []
|
|
parsed_enums: List = []
|
|
current_defs: Dict = {}
|
|
|
|
format_types = dict()
|
|
# format_types['date-time'] = 'DateTime'
|
|
|
|
|
|
def object_exists(name: str) -> bool:
|
|
"""Check if an object already exists."""
|
|
for el in parsed_types:
|
|
if el['name'] == name:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def add_enum_type(name: str, enums: Tuple[str], description: str):
|
|
"""Add enum type to parsed_types."""
|
|
for el in parsed_enums:
|
|
if el['name'] == name:
|
|
raise Exception('Warning: enum ' + name + ' already exists')
|
|
parsed_enums.append({
|
|
'name': name,
|
|
'enums': enums,
|
|
'description': description
|
|
})
|
|
|
|
|
|
def parse_ref(ref: str, prop_type, prop_info: Dict) -> Tuple[str, dict]:
|
|
if ref not in TypeParser.all_types:
|
|
TypeParser.all_types[ref] = TypeParser.parse_type_url(type_url=ref)
|
|
type_dict = TypeParser.all_types[ref]
|
|
|
|
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
|
|
if not type_path or not type_path.exists():
|
|
raise EVerestParsingException('$ref: ' + ref + f' referenced type file "{type_path}" does not exist.')
|
|
|
|
(td, _mod) = TypeParser.load_type_definition(type_path)
|
|
if 'types' in td and type_dict['type_name'] in td['types']:
|
|
local_type_info = td['types'][type_dict['type_name']]
|
|
if local_type_info['type'] == 'string' and 'enum' in local_type_info:
|
|
prop_info['enum'] = True
|
|
prop_type = type_dict['namespaced_type']
|
|
prop_info['prop']['type'] = prop_type
|
|
prop_info['type_dict'] = type_dict
|
|
|
|
path = Path('generated/types') / \
|
|
type_dict['type_relative_path'].with_suffix('.hpp')
|
|
type_headers.add(path.as_posix())
|
|
|
|
return (prop_type, prop_info)
|
|
|
|
|
|
def parse_property(prop_name: str, prop: Dict, depends_on: List[str], type_file: bool) -> Tuple[str, dict]:
|
|
"""Determine type of property and proceed with it.
|
|
In case it is a $ref, look it up in the TypeParser
|
|
Currently, the following property types are supported:
|
|
- string (and enum as a special case)
|
|
- integer
|
|
- number
|
|
- boolean
|
|
- array
|
|
- object (will be parsed recursivly)
|
|
"""
|
|
|
|
prop_type = None
|
|
prop_info = {
|
|
'description': prop.get('description', 'TODO: description'),
|
|
'prop': prop,
|
|
'enum': False
|
|
}
|
|
if '$ref' in prop:
|
|
return parse_ref(prop['$ref'], prop_type, prop_info)
|
|
|
|
if 'type' not in prop:
|
|
raise EVerestParsingException(f'{prop_name} does not contain a type property')
|
|
|
|
if prop['type'] == 'string':
|
|
if 'enum' in prop and type_file:
|
|
prop_type = stringcase.capitalcase(prop_name)
|
|
add_enum_type(prop_type, prop['enum'], prop_info['description'])
|
|
elif 'format' in prop:
|
|
if prop['format'] in format_types:
|
|
prop_type = format_types[prop['format']]
|
|
else:
|
|
# unsupported format type
|
|
prop_type = 'std::string'
|
|
prop_info['unsupported_format'] = True
|
|
else:
|
|
prop_type = 'std::string'
|
|
elif prop['type'] == 'integer':
|
|
prop_type = 'int32_t'
|
|
elif prop['type'] == 'number':
|
|
prop_type = 'float'
|
|
elif prop['type'] == 'boolean':
|
|
prop_type = 'bool'
|
|
elif prop['type'] == 'array':
|
|
if 'items' in prop:
|
|
prop_type = 'std::vector<' + parse_property(prop_name, prop['items'], depends_on, type_file)[0] + '>'
|
|
else:
|
|
raise EVerestParsingException(f'Property items of array {prop_name} does not contain a type property')
|
|
elif prop['type'] == 'object':
|
|
prop_type = stringcase.capitalcase(prop_name)
|
|
depends_on.append(prop_type)
|
|
if not object_exists(prop_type):
|
|
parse_object(prop_type, prop, type_file)
|
|
else:
|
|
raise Exception('Unknown type: ' + prop['type'])
|
|
|
|
return (prop_type, prop_info)
|
|
|
|
|
|
def parse_object(ob_name: str, json_schema: Dict, type_file: bool):
|
|
"""Parse a JSON object.
|
|
Iterates over the properties of this object, parses their type
|
|
and puts these information into the global dict parsed_types.
|
|
"""
|
|
|
|
ob_dict = {'name': ob_name, 'properties': [], 'depends_on': []}
|
|
parsed_types.insert(0, ob_dict)
|
|
|
|
if 'properties' not in json_schema:
|
|
# object has no properties, probably not a complex object
|
|
if '$ref' in json_schema:
|
|
if json_schema['$ref'] not in TypeParser.all_types:
|
|
TypeParser.all_types[json_schema['$ref']] = TypeParser.parse_type_url(type_url=json_schema['$ref'])
|
|
type_dict = TypeParser.all_types[json_schema['$ref']]
|
|
|
|
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'].with_suffix('.yaml'))
|
|
if not type_path or not type_path.exists():
|
|
raise EVerestParsingException(
|
|
'$ref: ' + json_schema['$ref'] + f' referenced type file "{type_path}" does not exist.')
|
|
TypeParser.does_type_exist(type_url=json_schema['$ref'], json_type=json_schema['type'])
|
|
|
|
prop_type = type_dict['namespaced_type']
|
|
ob_dict['name'] = prop_type
|
|
path = Path('generated/types') / \
|
|
type_dict['type_relative_path'].with_suffix('.hpp')
|
|
type_headers.add(path.as_posix())
|
|
return ob_dict
|
|
return
|
|
|
|
if not type_file:
|
|
return
|
|
|
|
for prop_name, prop in json_schema['properties'].items():
|
|
if not prop_name.isidentifier() or keyword.iskeyword(prop_name):
|
|
raise Exception(prop_name + ' can\'t be used as an identifier!')
|
|
(prop_type, prop_info) = parse_property(prop_name, prop, ob_dict['depends_on'], type_file)
|
|
ob_dict['properties'].append({
|
|
'name': prop_name,
|
|
'json_name': prop_name,
|
|
'type': prop_type,
|
|
'info': prop_info,
|
|
'enum': 'enum' in prop or prop_info['enum'],
|
|
'required': prop_name in json_schema.get('required', {}),
|
|
})
|
|
|
|
ob_dict['properties'].sort(key=lambda x: x.get('required'), reverse=True)
|
|
|
|
return ob_dict
|
|
|
|
|
|
def generate_header_for_type(type_name: str) -> Path:
|
|
return (Path('generated/types') / type_name).with_suffix('.hpp')
|
|
|
|
|
|
def extended_build_type_info(name: str, info: dict, type_file=False) -> Tuple[dict, dict]:
|
|
"""Extend build_type_info with enum and object type handling."""
|
|
type_info = build_type_info(name, info['type'])
|
|
enum_info = None
|
|
|
|
if type_info['json_type'] == 'string':
|
|
if 'enum' in info and type_file:
|
|
enum_info = {
|
|
'name': name,
|
|
'description': info.get('description', 'TODO: description'),
|
|
'enum_type': stringcase.capitalcase(name),
|
|
'enum': info['enum']
|
|
}
|
|
|
|
type_info['enum_type'] = enum_info['enum_type']
|
|
elif '$ref' in info:
|
|
if info['$ref'] not in TypeParser.all_types:
|
|
TypeParser.all_types[info['$ref']] = TypeParser.parse_type_url(type_url=info['$ref'])
|
|
type_dict = TypeParser.all_types[info['$ref']]
|
|
|
|
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
|
|
if not type_path or not type_path.exists():
|
|
raise EVerestParsingException('$ref: ' + info['$ref'] +
|
|
f' referenced type file "{type_path}" does not exist.')
|
|
|
|
(td, _mod) = TypeParser.load_type_definition(type_path)
|
|
if 'types' in td and type_dict['type_name'] in td['types']:
|
|
local_type_info = td['types'][type_dict['type_name']]
|
|
if local_type_info['type'] == 'string' and 'enum' in local_type_info:
|
|
enum_info = {
|
|
'name': name,
|
|
'description': local_type_info.get('description', 'TODO: description'),
|
|
'enum_type': type_dict['namespaced_type'],
|
|
'enum': local_type_info['enum']
|
|
}
|
|
|
|
type_info['enum_type'] = enum_info['enum_type']
|
|
path = generate_header_for_type(type_dict['type_relative_path'])
|
|
type_headers.add(path.as_posix())
|
|
elif type_info['json_type'] == 'object':
|
|
try:
|
|
ob = parse_object(name, info, type_file)
|
|
if ob and 'name' in ob:
|
|
type_info['object_type'] = ob['name']
|
|
except EVerestParsingException as e:
|
|
raise EVerestParsingException(f'Error parsing object {name}: {e}')
|
|
elif type_info['json_type'] == 'array':
|
|
if '$ref' in info['items']:
|
|
if info['items']['$ref'] not in TypeParser.all_types:
|
|
TypeParser.all_types[info['items']['$ref']] = TypeParser.parse_type_url(type_url=info['items']['$ref'])
|
|
type_dict = TypeParser.all_types[info['items']['$ref']]
|
|
|
|
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
|
|
if not type_path or not type_path.exists():
|
|
raise EVerestParsingException(
|
|
'$ref: ' + info['items']['$ref'] + f' referenced type file "{type_path}" does not exist.')
|
|
|
|
(td, _mod) = TypeParser.load_type_definition(type_path)
|
|
if 'types' in td and type_dict['type_name'] in td['types']:
|
|
local_type_info = td['types'][type_dict['type_name']]
|
|
if 'enum' in local_type_info:
|
|
type_info['array_type_contains_enum'] = True
|
|
type_info['array_type'] = type_dict['namespaced_type']
|
|
path = generate_header_for_type(type_dict['type_relative_path'])
|
|
type_headers.add(path.as_posix())
|
|
|
|
return (type_info, enum_info)
|
|
|
|
|
|
def load_validators(schema_path: Path):
|
|
# FIXME (aw): we should also patch the schemas like in everest-framework
|
|
validators = {}
|
|
for validator, filename in zip(
|
|
['interface', 'module', 'config', 'type', 'error_declaration_list'],
|
|
['interface', 'manifest', 'config', 'type', 'error-declaration-list']):
|
|
try:
|
|
schema = yaml.safe_load((schema_path / f'{filename}.yaml').read_text())
|
|
jsonschema.Draft7Validator.check_schema(schema)
|
|
validators[validator] = jsonschema.Draft7Validator(schema)
|
|
except OSError as err:
|
|
print(f'Could not open schema file {err.filename}: {err.strerror}')
|
|
exit(1)
|
|
except jsonschema.SchemaError as err:
|
|
print(f'Schema error in schema file {filename}.yaml')
|
|
raise
|
|
except yaml.YAMLError as err:
|
|
raise Exception(f'Could not parse interface definition file {schema_path}') from err
|
|
|
|
return validators
|
|
|
|
|
|
def load_validated_interface_def(if_def_path: Path, validator):
|
|
if_def = {}
|
|
try:
|
|
if_def = yaml.safe_load(if_def_path.read_text())
|
|
# validating interface
|
|
validator.validate(if_def)
|
|
# validate var/cmd subparts
|
|
if 'vars' in if_def:
|
|
for _var_name, var_def in if_def['vars'].items():
|
|
jsonschema.Draft7Validator.check_schema(var_def)
|
|
if 'cmds' in if_def:
|
|
for _cmd_name, cmd_def in if_def['cmds'].items():
|
|
if 'arguments' in cmd_def:
|
|
for _arg_name, arg_def in cmd_def['arguments'].items():
|
|
jsonschema.Draft7Validator.check_schema(arg_def)
|
|
if 'result' in cmd_def:
|
|
jsonschema.Draft7Validator.check_schema(cmd_def['result'])
|
|
except OSError as err:
|
|
raise Exception(f'Could not open interface definition file {err.filename}: {err.strerror}') from err
|
|
except jsonschema.ValidationError as err:
|
|
raise Exception(f'Validation error in interface definition file {if_def_path}: {err}') from err
|
|
except yaml.YAMLError as err:
|
|
raise Exception(f'Could not parse interface definition file {if_def_path}') from err
|
|
|
|
return if_def
|
|
|
|
|
|
def load_validated_type_def(type_def_path: Path, validator):
|
|
"""Load a type definition from the provided path and validate it with the provided validator."""
|
|
|
|
try:
|
|
type_def = yaml.safe_load(type_def_path.read_text())
|
|
# validating type definition
|
|
validator.validate(type_def)
|
|
|
|
return type_def
|
|
except OSError as err:
|
|
raise Exception(f'Could not open type definition file {err.filename}: {err.strerror}') from err
|
|
except jsonschema.ValidationError as err:
|
|
raise Exception(f'Validation error in type definition file {type_def_path}') from err
|
|
except yaml.YAMLError as err:
|
|
raise Exception(f'Could not parse interface definition file {type_def_path}') from err
|
|
|
|
return type_def
|
|
|
|
|
|
def load_validated_module_def(module_path: Path, validator):
|
|
try:
|
|
module_def = yaml.safe_load(module_path.read_text())
|
|
validator.validate(module_def)
|
|
except OSError as err:
|
|
raise Exception(f'Could not open type definition file {err.filename}: {err.strerror}') from err
|
|
except jsonschema.ValidationError as err:
|
|
raise Exception(f'Validation error in module definition file {module_path}') from err
|
|
except yaml.YAMLError as err:
|
|
raise Exception(f'Could not parse interface definition file {module_path}') from err
|
|
|
|
return module_def
|
|
|
|
|
|
def generate_some_uuids(count):
|
|
for i in range(count):
|
|
print(uuid4())
|
|
|
|
|
|
def yaml2json(yaml_file: Path, json_file: Path):
|
|
if not yaml_file.exists():
|
|
print(f'The input file ({yaml_file}) does not exist')
|
|
exit(1)
|
|
|
|
with open(yaml_file, 'r') as yaml_content:
|
|
content_as_dict = yaml.safe_load(yaml_content)
|
|
|
|
with open(json_file, 'w') as json_content:
|
|
json.dump(content_as_dict, json_content, indent=2)
|
|
|
|
|
|
def json2yaml(json_file: Path, yaml_file: Path):
|
|
if not json_file.exists():
|
|
print(f'The input file ({json_file}) does not exist')
|
|
exit(1)
|
|
|
|
with open(json_file, 'r') as json_content:
|
|
content_as_dict = json.load(json_content)
|
|
|
|
with open(yaml_file, 'w') as yaml_content:
|
|
yaml.safe_dump(content_as_dict, yaml_content, indent=2, sort_keys=False, width=120)
|
|
|
|
|
|
def __check_for_match(blocks_def, line, line_no, file_path):
|
|
match = re.search(blocks_def['regex_str'], line)
|
|
if not match:
|
|
return None
|
|
|
|
# mb = match_block
|
|
mb = {
|
|
'id': match.group('uuid'),
|
|
'version': match.group('version'),
|
|
'tag': blocks_def['format_str'].format(
|
|
uuid=match.group('uuid'),
|
|
version=match.group('version')
|
|
)
|
|
}
|
|
|
|
# check if uuid and version exists
|
|
if blocks_def['version'] != mb['version']:
|
|
raise ValueError(
|
|
f'Error while parsing {file_path}:\n'
|
|
f' matched line {line_no}: {line}\n'
|
|
f' contains version "{mb["version"]}", which is different from the blocks definition version "{blocks_def["version"]}"'
|
|
)
|
|
|
|
for block, block_info in blocks_def['definitions'].items():
|
|
if block_info['id'] != mb['id']:
|
|
continue
|
|
|
|
mb['name'] = block
|
|
mb['block'] = block_info
|
|
|
|
if not 'block' in mb:
|
|
raise ValueError(
|
|
f'Error while parsing {file_path}:\n'
|
|
f' matched line {line_no}: {line}\n'
|
|
f' contains uuid "{mb["id"]}", which doesn\'t exist in the block definition'
|
|
)
|
|
|
|
return mb
|
|
|
|
|
|
def generate_tmpl_blocks(blocks_def, file_path=None):
|
|
|
|
tmpl_block = {}
|
|
for block_name, block_def in blocks_def['definitions'].items():
|
|
tmpl_block[block_name] = {
|
|
'tag': blocks_def['format_str'].format(
|
|
uuid=block_def['id'],
|
|
version=blocks_def['version']
|
|
),
|
|
'content': block_def['content'],
|
|
'first_use': True
|
|
}
|
|
|
|
if not file_path:
|
|
return tmpl_block
|
|
|
|
try:
|
|
file_data = file_path.read_text()
|
|
except OSError as err:
|
|
print(f'Could not open file {err.filename} for parsing blocks: {err.strerror}')
|
|
exit(1)
|
|
|
|
line_no = 0
|
|
matched_block = None
|
|
content = None
|
|
|
|
for line in file_data.splitlines(True):
|
|
line_no += 1
|
|
|
|
if not matched_block:
|
|
matched_block = __check_for_match(blocks_def, line.rstrip(), line_no, file_path)
|
|
content = None
|
|
continue
|
|
|
|
if (line.strip() == matched_block['tag']):
|
|
if (content):
|
|
tmpl_block[matched_block['name']]['content'] = content.rstrip()
|
|
tmpl_block[matched_block['name']]['first_use'] = False
|
|
matched_block = None
|
|
else:
|
|
content = (content + line) if content else line
|
|
|
|
if matched_block:
|
|
raise ValueError(
|
|
f'Error while parsing {file_path}:\n'
|
|
f' matched tag line {matched_block["tag"]}\n'
|
|
f' could not find closing tag'
|
|
)
|
|
|
|
return tmpl_block
|
|
|
|
|
|
def load_tmpl_blocks(blocks_def, file_path, update):
|
|
if update and file_path.exists():
|
|
return generate_tmpl_blocks(blocks_def, file_path)
|
|
else:
|
|
return generate_tmpl_blocks(blocks_def)
|
|
|
|
|
|
def __show_diff_for(file_info):
|
|
diff_path = shutil.which('diff')
|
|
if diff_path == None:
|
|
raise Exception('Can\'t generate diff, because "diff" executable not found')
|
|
|
|
file_path = file_info['path']
|
|
|
|
diff_ignore = ''
|
|
if file_path.suffix in ('.hpp', '.cpp'):
|
|
diff_ignore = '^//.*'
|
|
elif file_path.name == 'CMakeLists.txt':
|
|
diff_ignore = '^#.*'
|
|
diff_ignore_args = ['-I', diff_ignore] if diff_ignore else []
|
|
|
|
run_parms = {'input': file_info['content'], 'capture_output': True, 'encoding': 'utf-8'}
|
|
|
|
diff = subprocess.run([
|
|
diff_path,
|
|
'-ruN',
|
|
*diff_ignore_args,
|
|
'--label', file_info['printable_name'],
|
|
'--color=always',
|
|
file_path,
|
|
'-'
|
|
], **run_parms).stdout
|
|
if diff:
|
|
print(diff)
|
|
|
|
|
|
def filter_mod_files(only, mod_files):
|
|
if not only:
|
|
return
|
|
|
|
filter_files = set([c.strip() for c in only.split(',')])
|
|
not_filtered_files = filter_files.copy()
|
|
|
|
# first check if all selected file filters are valid
|
|
for category_files in mod_files.values():
|
|
for file_info in category_files:
|
|
if file_info['abbr'] in not_filtered_files:
|
|
not_filtered_files.remove(file_info['abbr'])
|
|
|
|
if not_filtered_files:
|
|
raise Exception(f'Unknown file filters for --only option: {not_filtered_files}\n'
|
|
'Use "--only which" to show available file filters')
|
|
|
|
# now do the filtering
|
|
for category, category_files in mod_files.items():
|
|
mod_files[category] = list(filter(lambda x: x['abbr'] in filter_files, category_files))
|
|
|
|
|
|
def print_available_mod_files(mod_files):
|
|
for category, category_files in mod_files.items():
|
|
print(f'Available files for category "{category}"')
|
|
for file_info in category_files:
|
|
print(f' {file_info["abbr"]}')
|
|
|
|
def get_mtime(filename: str | Path) -> float:
|
|
if isinstance(filename, str):
|
|
filename = Path(filename)
|
|
|
|
return filename.stat().st_mtime
|
|
|
|
|
|
def is_template_newer(file_info) -> Tuple[bool, str]:
|
|
template_path = file_info['template_path']
|
|
generated_path = file_info['path']
|
|
|
|
if not generated_path.exists():
|
|
return (True, ' (Generated file did not exist)')
|
|
|
|
if get_mtime(template_path) > get_mtime(generated_path):
|
|
return (True, ' (Template file has changed since last generation)')
|
|
|
|
return (False, '')
|
|
|
|
|
|
def write_content_to_file(file_info, strategy, only_diff=False, reason = '', check_license_header=False):
|
|
# strategy:
|
|
# update: update only if dest older or not existent
|
|
# force-update: update, even if dest newer
|
|
# update-if-non-existent: update only if file does not exists
|
|
# create: create file only if it does not exist
|
|
# force-create: create file, even if it exists
|
|
# FIXME (aw): we should have this as an enum
|
|
|
|
strategies = ['update', 'force-update', 'update-if-non-existent', 'create', 'force-create']
|
|
|
|
file_path = file_info['path']
|
|
file_dir = file_path.parent
|
|
printable_name = file_info['printable_name']
|
|
|
|
method = ''
|
|
|
|
if only_diff:
|
|
return __show_diff_for(file_info)
|
|
|
|
if strategy == 'update':
|
|
if file_path.exists() and file_path.stat().st_mtime > file_info['last_mtime']:
|
|
print(f'Skipping {printable_name} (up-to-date)')
|
|
return
|
|
method = 'Updating'
|
|
elif strategy == 'force-update':
|
|
method = 'Force-updating' if file_path.exists() else 'Creating'
|
|
elif strategy == 'force-create':
|
|
method = 'Overwriting' if file_path.exists() else 'Creating'
|
|
elif strategy == 'update-if-non-existent' or strategy == 'create':
|
|
if file_path.exists():
|
|
print(f'Skipping {printable_name} (use create --force to recreate)')
|
|
return
|
|
method = 'Creating'
|
|
else:
|
|
raise Exception(f'Invalid strategy "{strategy}"\nSupported strategies: {strategies}')
|
|
|
|
print(f'{method} file {printable_name}{reason}')
|
|
|
|
if not file_dir.exists():
|
|
file_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# check if file header is different from license header
|
|
if check_license_header:
|
|
if 'license_header' in file_info and file_path.exists():
|
|
original_content = file_path.read_text()
|
|
if not original_content.startswith(file_info['license_header']):
|
|
# determine likely end of license header
|
|
search_terms = ['#ifndef', '#pragma once', '#include']
|
|
original_license_header = ''
|
|
for search in search_terms:
|
|
index = original_content.find(search)
|
|
if index >= 0:
|
|
original_license_header = original_content[0:index]
|
|
break
|
|
print(f'Keeping the existing licence header:\n{original_license_header}')
|
|
file_info['content'] = file_info['content'].replace(
|
|
file_info['license_header'], original_license_header.strip())
|
|
|
|
file_path.write_text(file_info['content'])
|
|
|
|
|
|
def write_content_to_file_and_check_template(file_info, strategy, only_diff=False):
|
|
# check if template is newer and force-update file if it is
|
|
update_strategy = strategy
|
|
(newer, reason) = is_template_newer(file_info)
|
|
if newer:
|
|
update_strategy = 'force-update'
|
|
write_content_to_file(file_info, update_strategy, only_diff, reason)
|
|
|
|
|
|
def get_license_header(license_dirs, license_url):
|
|
url_schemas = ['http://', 'https://']
|
|
for url_schema in url_schemas:
|
|
if license_url.startswith(url_schema):
|
|
license_url = license_url.replace(url_schema, '', 1)
|
|
license_path = None
|
|
for license_dir in license_dirs:
|
|
check_license_path = license_dir / license_url
|
|
print(f'Checking if license "{check_license_path}" exists...')
|
|
if check_license_path.exists():
|
|
license_path = check_license_path
|
|
|
|
if not license_path:
|
|
return None
|
|
with open(license_path, 'r') as custom_license_file:
|
|
return custom_license_file.read().strip()
|
|
|
|
|
|
def get_path_from_cmake_cache(variable_prefix, cmake_cache_path, option_name):
|
|
print(f'Searching for {variable_prefix} in: {cmake_cache_path}')
|
|
print(f'You can either provide the {variable_prefix} directory with {option_name} or influence the'
|
|
' automatic search path by setting --build-dir (default: ./build)')
|
|
if not cmake_cache_path.exists():
|
|
print(f'CMakeCache.txt does not exist: {cmake_cache_path}')
|
|
return None
|
|
with open(cmake_cache_path, 'r') as cmake_cache_file:
|
|
search = f'{variable_prefix}_SOURCE_DIR:STATIC='
|
|
for line in cmake_cache_file:
|
|
if line.startswith(search):
|
|
found_dir = Path(line.replace(search, '', 1).strip(' \t\n\r'))
|
|
if found_dir.exists():
|
|
print(f'Found {variable_prefix} directory: {found_dir}')
|
|
user_choice = input('Do you want to use this? [Y/n] ').lower()
|
|
if user_choice == 'y' or not user_choice:
|
|
return found_dir
|
|
break
|
|
return None
|
|
|
|
|
|
def detect_everest_projects(everest_projects, build_dir):
|
|
detected_everest_project = False
|
|
for everest_dir in everest_dirs:
|
|
if everest_dir.exists() and everest_dir.name in everest_projects:
|
|
detected_everest_project = True
|
|
|
|
found_dirs = []
|
|
|
|
if not detected_everest_project:
|
|
print('Could not detect ' + ", ".join(everest_projects) + ' path in --everest-dir')
|
|
cmake_cache_path = Path(build_dir) / 'CMakeCache.txt'
|
|
for everest_project in everest_projects:
|
|
found_dir = get_path_from_cmake_cache(everest_project, cmake_cache_path, '--everest-dir')
|
|
if found_dir:
|
|
found_dirs.append(found_dir)
|
|
|
|
return found_dirs
|