143 lines
4.7 KiB
Python

import json
import logging
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Sequence, Callable, Optional, Literal, TypeAlias, TypeVar
_invalid_file_char_regex = re.compile(r'[^a-zA-Z0-9_]')
_invalid_path_char_regex = re.compile(r'[^a-zA-Z0-9_/]')
_variable_regex = re.compile(r'@([A-Z_]+)@')
_logger = logging.getLogger(__name__)
_root : Path
T = TypeVar('T')
_Validator : TypeAlias = Callable[[T], bool]
def get_root() -> Path:
return _root
def script_preamble() -> None:
global _root
logging.basicConfig(level=logging.DEBUG, format='%(message)s')
_root = Path(__file__).parent.parent.parent
os.chdir(_root)
def run_script(main_fn: Callable[[],None]) -> None:
script_preamble()
try:
main_fn()
except KeyboardInterrupt:
_logger.warning('Cancelled.')
sys.exit(2)
except Exception as e:
_logger.error('There was an error running the script: %s', e)
sys.exit(1)
def exec_checked(args: Sequence[str], **kwargs) -> None:
subprocess.run(args, stdout=sys.stdout, stderr=sys.stderr, check=True, **kwargs)
def exec_get_output(args: Sequence[str], **kwargs) -> str:
return subprocess.run(args, text=True, check=True, capture_output=True).stdout
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())}]')
val = prompt(prefix).strip().lower()
for name, value in choices.items():
if name.lower() == val:
return value
def prompt_yesno(message: str, prefix: str = '> ') -> bool:
while True:
print(f'{message} [(y)es, (n)o]')
val = prompt(prefix).strip().lower()
if val in ('y', 'yes'):
return True
if val in ('n', 'no'):
return False
def prompt_path(message: str,
prefix: str = '> ',
mode: Literal['save']|Literal['load']|None = None,
allow_slash: bool = False,
allow_absolute: bool = False,
relative_to: Optional[Path] = None,
validator: Optional[_Validator[Path]] = None) -> Path:
while True:
print(message)
val = prompt(prefix).strip()
if val == '':
continue
if mode != 'load': # no verification needed when loading a file
regex = _invalid_path_char_regex if allow_slash else _invalid_file_char_regex
invalid_chars = regex.findall(val)
if len(invalid_chars) > 0:
print(f'Path contains the following invalid chars: {", ".join(invalid_chars)}')
continue
path = Path(val)
if not allow_absolute and path.is_absolute():
print('Absolute paths are not allowed.')
continue
if relative_to is not None:
path = relative_to / path
if mode == 'load':
if not path.exists():
print('File does not exist.')
continue
elif mode == 'save':
if path.exists():
if not prompt_yesno(f'File {path} already exists. Continue?'):
continue
if validator is not None and not validator(path):
continue
return path
def escape_filename(name: str) -> str:
return _invalid_file_char_regex.sub('_', name)
def replace_in_file(file: Path, **replacements) -> None:
_logger.debug('Patching file "%s"...', file)
bakfile = file.with_suffix(f'{file.suffix}.bak')
bakfile.unlink(missing_ok=True)
file.rename(bakfile)
with bakfile.open('r') as fin, file.open('w') as fout:
for line in fin:
def _repl(match: re.Match) -> str:
return replacements.get(match.group(1), '<invalid variable>')
fout.write(_variable_regex.sub(_repl, line))
bakfile.unlink()