143 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			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()
 |