225 lines
7.6 KiB
Python
225 lines
7.6 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Optional, Literal, Any, Callable
|
|
|
|
sys.path.append(os.path.dirname(__file__))
|
|
|
|
from common import exec_checked, prompt, prompt_and_validate, prompt_choices, run_script, escape_filename, prompt_yesno
|
|
from common.jinja import is_jinja_installed, generate_file, render_string
|
|
from new_source import run_new_source
|
|
|
|
class _TemplateCustomizable: ...
|
|
@dataclass
|
|
class _TemplateOption:
|
|
name: str
|
|
message: str
|
|
|
|
@staticmethod
|
|
def from_json(type: str, **kwargs) -> '_TemplateOption':
|
|
tp = {
|
|
'choices': _TemplateOptionChoices
|
|
}[type]
|
|
return tp.from_json(**kwargs)
|
|
|
|
@dataclass
|
|
class _TemplateOptionChoices(_TemplateOption):
|
|
choices: list[str]
|
|
|
|
@staticmethod
|
|
def from_json(**kwargs) -> '_TemplateOptionChoices':
|
|
return _TemplateOptionChoices(**kwargs)
|
|
|
|
|
|
@dataclass
|
|
class _TemplateNewSource(_TemplateCustomizable):
|
|
header: Literal['public', 'private', 'no']
|
|
source: bool
|
|
path: str
|
|
|
|
@staticmethod
|
|
def from_json(**kwargs) -> '_TemplateNewSource':
|
|
return _TemplateNewSource(**kwargs)
|
|
|
|
@dataclass
|
|
class _TemplateConfig(_TemplateCustomizable):
|
|
options: list[_TemplateOption]
|
|
new_sources: list[_TemplateNewSource]
|
|
|
|
@staticmethod
|
|
def from_json(data: dict) -> '_TemplateConfig':
|
|
return _TemplateConfig(
|
|
options=[_TemplateOption.from_json(**ele) for ele in data.get('options', [])],
|
|
new_sources=[_TemplateNewSource.from_json(**ele) for ele in data.get('new_sources', [])]
|
|
)
|
|
|
|
|
|
_PRIVATE_PATH = Path('private')
|
|
_PUBLIC_PATH = Path('public')
|
|
_TEMPLATE_BASE_PATH = Path(__file__).parent / 'templates/module'
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
_module_folder_name: str
|
|
_module_name: str
|
|
_template_folder: Path
|
|
_template_config: _TemplateConfig
|
|
_template_options: dict[str, Any] = {}
|
|
|
|
def verify_tools() -> None:
|
|
success = True
|
|
if not is_jinja_installed():
|
|
_logger.error('Python module Jinja2 is not installed.')
|
|
success = False
|
|
if not success:
|
|
raise RuntimeError('one or more required tools could not be found')
|
|
|
|
def query_common_params() -> None:
|
|
global _module_folder_name, _module_name, _template_folder
|
|
|
|
def _validate_name(name: str) -> bool:
|
|
return name.strip() != ''
|
|
|
|
_module_name = prompt_and_validate('Enter module name.', _validate_name).strip()
|
|
|
|
_module_folder_name = escape_filename(_module_name.lower())
|
|
while True:
|
|
print(f'Please enter a folder name for the module. Leave empty for "{_module_folder_name}". Anything but [A-Za-z0-9_] will be replaced with underscores.')
|
|
new_name = prompt().strip()
|
|
if new_name != '':
|
|
_module_folder_name = escape_filename(new_name) # just enforce nice names
|
|
module_path = _PRIVATE_PATH / _module_folder_name
|
|
if not module_path.exists():
|
|
break
|
|
print(f'Module folder at {module_path} already exists.')
|
|
|
|
|
|
template_choices = {}
|
|
for subpath in sorted(_TEMPLATE_BASE_PATH.iterdir()):
|
|
if not subpath.is_dir():
|
|
continue
|
|
template_choices[subpath.name] = subpath
|
|
_template_folder = prompt_choices('Please select a module template.', template_choices)
|
|
|
|
# sanity check
|
|
if not (_template_folder / 'private/SModule.jinja').exists():
|
|
raise RuntimeError('module template appears to be invalid')
|
|
|
|
def load_template_config() -> None:
|
|
global _template_config
|
|
config_file = _template_folder / 'config.json'
|
|
if not config_file.exists():
|
|
_template_config = _TemplateConfig(
|
|
options=[],
|
|
new_sources=[]
|
|
)
|
|
return
|
|
with config_file.open('r') as f:
|
|
try:
|
|
_template_config = _TemplateConfig.from_json(json.load(f))
|
|
except Exception as e:
|
|
raise RuntimeError(f'Error while parsing template config: {e}')
|
|
|
|
def query_template_params() -> None:
|
|
def _query_choices(option: _TemplateOptionChoices) -> str:
|
|
return prompt_choices(option.message, {ele: ele for ele in option.choices})
|
|
|
|
_QUERY_FUNCS : dict[type[_TemplateOption], Callable] = {
|
|
_TemplateOptionChoices: _query_choices
|
|
}
|
|
for option in _template_config.options:
|
|
_template_options[option.name] = _QUERY_FUNCS[option.__class__](option)
|
|
|
|
def generate_module() -> None:
|
|
# firest prepare the template context
|
|
context = {
|
|
'name': _module_name,
|
|
'target_name': _module_folder_name,
|
|
'folder_name': _module_folder_name,
|
|
**_template_options
|
|
}
|
|
|
|
# next parse templates in the config
|
|
def _replace_options(obj: _TemplateCustomizable) -> None:
|
|
def _process_value(value: Any, setter: Optional[Callable[[str], None]]) -> None:
|
|
if isinstance(value, str):
|
|
setter(render_string(value, context))
|
|
elif isinstance(value, list):
|
|
for idx in range(len(value)):
|
|
def __set(v: str) -> None:
|
|
value[idx] = v
|
|
_process_value(value[idx], __set)
|
|
elif isinstance(value, dict):
|
|
for key in list(value.keys()):
|
|
def __set(v: str) -> None:
|
|
value[key] = v
|
|
_process_value(value[key], __set)
|
|
elif isinstance(value, _TemplateCustomizable):
|
|
_process_value(value.__dict__, setter)
|
|
_process_value(obj.__dict__, None)
|
|
_replace_options(_template_config)
|
|
|
|
# then copy the files over
|
|
def _copy_template(src: Path, dst: Path) -> None:
|
|
dst.mkdir(parents=True, exist_ok=True)
|
|
for ele in src.iterdir():
|
|
dstele = dst / ele.name
|
|
if ele.is_dir():
|
|
_copy_template(ele, dstele)
|
|
continue
|
|
if ele.suffix == '.jinja':
|
|
generate_file(str(ele.relative_to(_template_folder)), dstele.with_suffix(''), context, templates_path=_template_folder)
|
|
else:
|
|
shutil.copyfile(ele, dstele)
|
|
|
|
template_private = _template_folder / 'private'
|
|
template_public = _template_folder / 'public'
|
|
|
|
_copy_template(template_private, _PRIVATE_PATH / _module_folder_name)
|
|
|
|
if template_public.exists():
|
|
_copy_template(template_public, _PUBLIC_PATH / _module_folder_name)
|
|
|
|
# and then generate sources
|
|
for new_source in _template_config.new_sources:
|
|
run_new_source(new_source.header, new_source.source, Path(new_source.path))
|
|
|
|
def append_module() -> None:
|
|
print('Appending module to SConstruct file. You might want to adjust it.')
|
|
|
|
sconstruct_file = Path('SConstruct')
|
|
bak_file = sconstruct_file.with_suffix('.bak')
|
|
|
|
shutil.copyfile(sconstruct_file, bak_file)
|
|
|
|
with bak_file.open('r') as fin, sconstruct_file.open('w') as fout:
|
|
for line in fin:
|
|
if line.strip() == 'env.Finalize()':
|
|
fout.write(f'# {_module_name}\n')
|
|
fout.write(f"env = env.Module('{_PRIVATE_PATH / _module_folder_name}/SModule')\n")
|
|
fout.write('\n\n')
|
|
fout.write(line)
|
|
|
|
def refresh_projects() -> None:
|
|
if Path('.idea').exists():
|
|
if prompt_yesno('CLion project found. Regenerate it?'):
|
|
exec_checked(('scons', '--generate_project=clion'))
|
|
if Path('.vscode').exists():
|
|
if prompt_yesno('VS Code project found. Regenerate it?'):
|
|
exec_checked(('scons', '--generate_project=vscode'))
|
|
|
|
def script_main() -> None:
|
|
verify_tools()
|
|
query_common_params()
|
|
load_template_config()
|
|
query_template_params()
|
|
generate_module()
|
|
append_module()
|
|
refresh_projects()
|
|
|
|
if __name__ == '__main__':
|
|
run_script(script_main)
|