Compare commits
2 Commits
34730454ae
...
b1c461387f
Author | SHA1 | Date | |
---|---|---|---|
b1c461387f | |||
9bfbd44e34 |
2
external/scons-plus-plus
vendored
2
external/scons-plus-plus
vendored
@ -1 +1 @@
|
||||
Subproject commit c3b5244eac5187a64b6f46a1a4dc171416fff313
|
||||
Subproject commit e583c5ef6c1a8dbce941014aa3ce995f2d53d05b
|
@ -43,12 +43,29 @@ def get_project_config() -> dict:
|
||||
raw = exec_get_output(('scons', '-s', '--disable_auto_update', '--dump=config', '--dump_format=json'))
|
||||
return json.loads(raw)
|
||||
|
||||
def get_module_config() -> dict:
|
||||
raw = exec_get_output(('scons', '-s', '--disable_auto_update', '--dump=modules', '--dump_format=json'))
|
||||
return json.loads(raw)
|
||||
|
||||
def find_module_folder(subpath: Path) -> Optional[Path]:
|
||||
for parent in subpath.parents:
|
||||
if (parent / 'SModule').exists():
|
||||
return parent
|
||||
return None
|
||||
|
||||
def prompt(prefix: str = '> ') -> str:
|
||||
sys.stdout.write(prefix)
|
||||
sys.stdout.flush()
|
||||
return sys.stdin.readline()
|
||||
|
||||
|
||||
def prompt_and_validate(message: str, validator: _Validator[str], prefix: str = '> ') -> str:
|
||||
while True:
|
||||
print(message)
|
||||
input = prompt(prefix)
|
||||
if validator(input):
|
||||
return input
|
||||
|
||||
def prompt_choices[T](message: str, choices: dict[str, T], prefix: str = '> ') -> T:
|
||||
while True:
|
||||
print(f'{message} [{", ".join(choices.keys())}]')
|
||||
|
@ -34,11 +34,16 @@ def make_env(filters: Optional[dict[str, Callable]] = None,
|
||||
return jinja_env
|
||||
|
||||
|
||||
def generate_file(template: str, target: Path, context: dict[str, Any], **kwargs) -> None:
|
||||
def generate_file(template_name: str, target: Path, context: dict[str, Any], **kwargs) -> None:
|
||||
jinja_env = make_env(**kwargs)
|
||||
template = jinja_env.get_template(template)
|
||||
template = jinja_env.get_template(template_name)
|
||||
result = template.render(**context)
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with target.open('w') as f:
|
||||
f.write(result)
|
||||
|
||||
def render_string(input: str, context: dict[str, Any], **kwargs) -> str:
|
||||
jinja_env = make_env(**kwargs)
|
||||
template = jinja_env.from_string(input)
|
||||
return template.render(**context)
|
||||
|
224
tools/new_module.py
Normal file
224
tools/new_module.py
Normal file
@ -0,0 +1,224 @@
|
||||
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)
|
@ -4,11 +4,11 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, Literal
|
||||
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
from common import prompt_choices, prompt_path, prompt_yesno, run_script
|
||||
from common import find_module_folder, get_module_config, prompt_choices, prompt_path, prompt_yesno, run_script
|
||||
from common.jinja import is_jinja_installed, generate_file
|
||||
|
||||
|
||||
@ -16,6 +16,11 @@ _PRIVATE_PATH = Path('private')
|
||||
_PUBLIC_PATH = Path('public')
|
||||
_HEADER_TEMPLATE_NAME = 'header.hpp.jinja'
|
||||
_SOURCE_TEMPLATE_NAME = 'source.cpp.jinja'
|
||||
_HEADER_CHOICES = {
|
||||
'public': _PUBLIC_PATH,
|
||||
'private': _PRIVATE_PATH,
|
||||
'no': None
|
||||
}
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_header_path: Optional[Path] = None
|
||||
@ -30,27 +35,51 @@ def verify_tools() -> None:
|
||||
if not success:
|
||||
raise RuntimeError('one or more required tools could not be found')
|
||||
|
||||
def query_params() -> None:
|
||||
def _make_header_path(header_folder: Path, base_path: Path) -> Path:
|
||||
return header_folder / base_path.with_suffix('.hpp')
|
||||
|
||||
def _make_source_path(base_path: Path) -> Path:
|
||||
return _PRIVATE_PATH / base_path.with_suffix('.cpp')
|
||||
|
||||
def _apply_params(header_folder: Path, do_create_source: bool, base_path: Path) -> None:
|
||||
global _header_path, _source_path, _namespace
|
||||
header_folder = prompt_choices('Create Header?', {
|
||||
'public': _PUBLIC_PATH,
|
||||
'private': _PRIVATE_PATH,
|
||||
'no': None
|
||||
})
|
||||
|
||||
source_path = _make_source_path(base_path)
|
||||
module_folder = find_module_folder(source_path)
|
||||
|
||||
namespace = None
|
||||
if module_folder is not None:
|
||||
module_subfolder = module_folder.relative_to(_PRIVATE_PATH)
|
||||
module_key = str(module_subfolder)
|
||||
if os.path.sep != '/':
|
||||
module_key = module_key.replace(os.pathsep, '/')
|
||||
|
||||
all_module_conf = get_module_config()
|
||||
module_conf = all_module_conf.get(module_key)
|
||||
if module_conf is not None:
|
||||
namespace = module_conf.get('cxx_namespace')
|
||||
if namespace is None:
|
||||
namespace = module_key.replace('/', '_')
|
||||
|
||||
if namespace is None:
|
||||
namespace = base_path.parts[0]
|
||||
_namespace = namespace
|
||||
|
||||
if header_folder is not None:
|
||||
_header_path = _make_header_path(header_folder, base_path)
|
||||
if do_create_source:
|
||||
_source_path = source_path
|
||||
|
||||
def query_params() -> None:
|
||||
header_folder = prompt_choices('Create Header?', _HEADER_CHOICES)
|
||||
do_create_source = prompt_yesno('Create source?')
|
||||
if header_folder is None and not do_create_source:
|
||||
raise RuntimeError('Neither header nor source selected for creation.')
|
||||
|
||||
def _make_header_path(base_path: Path) -> Path:
|
||||
return header_folder / base_path.with_suffix('.hpp')
|
||||
|
||||
def _make_source_path(base_path: Path) -> Path:
|
||||
return _PRIVATE_PATH / base_path.with_suffix('.cpp')
|
||||
|
||||
def _validate_path(path: Path) -> bool:
|
||||
result = True
|
||||
if header_folder is not None:
|
||||
header_path = _make_header_path(path)
|
||||
header_path = _make_header_path(header_folder, path)
|
||||
if header_path.exists():
|
||||
print(f'Header file {header_path} already exists.')
|
||||
result = False
|
||||
@ -63,12 +92,7 @@ def query_params() -> None:
|
||||
|
||||
input_path = prompt_path('Enter basename for the source (relative to public/private folder, no file extension).',
|
||||
allow_slash=True, validator=_validate_path)
|
||||
_namespace = input_path.parts[0]
|
||||
|
||||
if header_folder is not None:
|
||||
_header_path = _make_header_path(input_path)
|
||||
if do_create_source:
|
||||
_source_path = _make_source_path(input_path)
|
||||
_apply_params(header_folder, do_create_source, input_path)
|
||||
|
||||
|
||||
def create_header() -> None:
|
||||
@ -94,11 +118,18 @@ def create_source() -> None:
|
||||
'namespace': _namespace
|
||||
})
|
||||
|
||||
def script_main() -> None:
|
||||
verify_tools()
|
||||
query_params()
|
||||
def _run() -> None:
|
||||
create_header()
|
||||
create_source()
|
||||
|
||||
def run_new_source(header: Literal['private', 'public', 'no'], source: bool, path: Path) -> None:
|
||||
_apply_params(_HEADER_CHOICES[header], source, path)
|
||||
_run()
|
||||
|
||||
def script_main() -> None:
|
||||
verify_tools()
|
||||
query_params()
|
||||
_run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_script(script_main)
|
||||
|
23
tools/templates/module/app/private/SModule.jinja
Normal file
23
tools/templates/module/app/private/SModule.jinja
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
Import('env')
|
||||
|
||||
env.ModuleConfig(
|
||||
name = '{{name}}',
|
||||
description = '',
|
||||
cxx_namespace = '{{folder_name}}'
|
||||
)
|
||||
|
||||
src_files = Split("""
|
||||
main.cpp
|
||||
""")
|
||||
|
||||
prog_app = env.UnityProgram(
|
||||
name = '{{name}}',
|
||||
target = env['BIN_DIR'] + '/{{target_name}}',
|
||||
source = src_files,
|
||||
dependencies = {
|
||||
'mijin': {}
|
||||
}
|
||||
)
|
||||
|
||||
Return('env')
|
5
tools/templates/module/app/private/main.cpp
Normal file
5
tools/templates/module/app/private/main.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
int main(int, char**)
|
||||
{
|
||||
return 0;
|
||||
}
|
17
tools/templates/module/library/config.json
Normal file
17
tools/templates/module/library/config.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"name": "library_type",
|
||||
"message": "Enter library type.",
|
||||
"type": "choices",
|
||||
"choices": ["static", "shared"]
|
||||
}
|
||||
],
|
||||
"new_sources": [
|
||||
{
|
||||
"header": "public",
|
||||
"source": true,
|
||||
"path": "{{folder_name}}/{{target_name}}"
|
||||
}
|
||||
]
|
||||
}
|
17
tools/templates/module/library/private/SModule.jinja
Normal file
17
tools/templates/module/library/private/SModule.jinja
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
Import('env')
|
||||
|
||||
src_files = Split("""
|
||||
{{target_name}}.cpp
|
||||
""")
|
||||
|
||||
env.Unity{% if library_type == "shared" %}Shared{% else %}Static{% endif %}Library(
|
||||
name = '{{name}}',
|
||||
target = env['LIB_DIR'] + '/{{target_name}}',
|
||||
source = src_files,
|
||||
dependencies = {
|
||||
'mijin': {}
|
||||
}
|
||||
)
|
||||
|
||||
Return('env')
|
Loading…
x
Reference in New Issue
Block a user