diff --git a/config/cli.py b/config/cli.py index dd82202..c1e54dc 100644 --- a/config/cli.py +++ b/config/cli.py @@ -7,6 +7,7 @@ from typing import Any, Iterable, Optional, Union from devices.device import get_devices from flavours.flavour import get_flavours +from wrapper import execute_without_exit from .scheme import Profile from .profile import PROFILE_EMPTY, PROFILE_DEFAULTS @@ -132,16 +133,22 @@ def prompt_choice(current: Optional[Any], key: str, choices: Iterable[Any], allo def prompt_profile_device(current: Optional[str], profile_name: str) -> tuple[str, bool]: - devices = get_devices() print(click.style("Pick your device!\nThese are the available devices:", bold=True)) + devices = execute_without_exit(get_devices, ['devices']) + if devices is None: + print("(wrapper mode, input for this field will not be checked for correctness)") + return prompt_config(text=f'{profile_name}.device', default=current) for dev in sorted(devices.keys()): print(devices[dev]) return prompt_choice(current, f'profiles.{profile_name}.device', devices.keys()) def prompt_profile_flavour(current: Optional[str], profile_name: str) -> tuple[str, bool]: - flavours = get_flavours() print(click.style("Pick your flavour!\nThese are the available flavours:", bold=True)) + flavours = execute_without_exit(get_flavours, ['flavours']) + if flavours is None: + print("(wrapper mode, input for this field will not be checked for correctness)") + return prompt_config(text=f'{profile_name}.flavour', default=current) for f in sorted(flavours.keys()): print(flavours[f]) return prompt_choice(current, f'profiles.{profile_name}.flavour', flavours.keys()) @@ -176,7 +183,12 @@ def prompt_for_save(retry_ctx: Optional[click.Context] = None): If `retry_ctx` is passed, the context's command will be reexecuted with the same arguments if the user chooses to retry. False will still be returned as the retry is expected to either save, perform another retry or arbort. """ + from wrapper import is_wrapped if click.confirm(f'Do you want to save your changes to {config.runtime.config_file}?', default=True): + if is_wrapped(): + logging.warning("Writing to config file inside wrapper." + "This is pointless and probably a bug." + "Your host config file will not be modified.") return True if retry_ctx: if click.confirm('Retry? ("n" to quit without saving)', default=True): @@ -201,6 +213,8 @@ noninteractive_flag = click.option('-N', '--non-interactive', is_flag=True) noop_flag = click.option('--noop', '-n', help="Don't write changes to file", is_flag=True) noparse_flag = click.option('--no-parse', help="Don't search PKGBUILDs for devices and flavours", is_flag=True) +CONFIG_MSG = ("Leave fields empty to leave them at their currently displayed value.") + @cmd_config.command(name='init') @noninteractive_flag @@ -224,6 +238,7 @@ def cmd_config_init( ): """Initialize the config file""" if not non_interactive: + logging.info(CONFIG_MSG) results: dict[str, dict] = {} for section in sections: if section not in CONFIG_SECTIONS: @@ -239,7 +254,14 @@ def cmd_config_init( results[section][key] = result config.update(results) + print("Main configuration complete") + if not noop: + if prompt_for_save(ctx): + config.write() + else: + return if 'profiles' in sections: + print("Configuring profiles") current_profile = 'default' if 'current' not in config.file.profiles else config.file.profiles.current new_current, _ = prompt_config('profiles.current', default=current_profile, field_type=str) profile, changed = prompt_profile(new_current, create=True, no_parse=no_parse) @@ -266,6 +288,7 @@ def cmd_config_set(ctx, key_vals: list[str], non_interactive: bool = False, noop like `build.clean_mode=false` or alternatively just keys to get prompted if run interactively. """ config.enforce_config_loaded() + logging.info(CONFIG_MSG) config_copy = deepcopy(config.file) for pair in key_vals: split_pair = pair.split('=') @@ -323,6 +346,7 @@ def cmd_profile_init(ctx, name: Optional[str] = None, non_interactive: bool = Fa profile = deepcopy(PROFILE_EMPTY) if name == 'current': raise Exception("profile name 'current' not allowed") + logging.info(CONFIG_MSG) name = name or config.file.profiles.current if name in config.file.profiles: profile |= config.file.profiles[name] @@ -333,7 +357,9 @@ def cmd_profile_init(ctx, name: Optional[str] = None, non_interactive: bool = Fa config.update_profile(name, profile) if not noop: if not prompt_for_save(ctx): + logging.info("Not saving.") return + config.write() else: logging.info(f'--noop passed, not writing to {config.runtime.config_file}!') diff --git a/config/state.py b/config/state.py index 0395baa..6825b9e 100644 --- a/config/state.py +++ b/config/state.py @@ -255,7 +255,7 @@ class ConfigStateHolder: profile = self.get_profile(profile_name) if field not in profile or not profile[field]: m = (f'Profile "{profile_name}" has no {field.upper()} configured.\n' - f'Please run `kupferbootstrap config profile init {field}`{arch_hint}') + f'Please run `kupferbootstrap config profile init {profile_name}`{arch_hint}') raise Exception(m) return profile diff --git a/wrapper/__init__.py b/wrapper/__init__.py index f7ce80a..a86f879 100644 --- a/wrapper/__init__.py +++ b/wrapper/__init__.py @@ -14,7 +14,7 @@ wrapper_impls: dict[str, Wrapper] = { } -def get_wrapper_type(wrapper_type: Optional[str] = None): +def get_wrapper_type(wrapper_type: Optional[str] = None) -> str: return wrapper_type or config.file.wrapper.type @@ -28,14 +28,19 @@ def wrap(wrapper_type: Optional[str] = None): get_wrapper_impl(wrapper_type).wrap() -def is_wrapped(wrapper_type: Optional[str] = None): +def is_wrapped(wrapper_type: Optional[str] = None) -> bool: wrapper_type = get_wrapper_type(wrapper_type) return wrapper_type != 'none' and get_wrapper_impl(wrapper_type).is_wrapped() +def needs_wrap(wrapper_type: Optional[str] = None) -> bool: + wrapper_type = wrapper_type or get_wrapper_type() + return wrapper_type != 'none' and not is_wrapped(wrapper_type) and not config.runtime.no_wrap + + def enforce_wrap(no_wrapper=False): wrapper_type = get_wrapper_type() - if wrapper_type != 'none' and not is_wrapped(wrapper_type) and not config.runtime.no_wrap and not no_wrapper: + if needs_wrap(wrapper_type) and not no_wrapper: logging.info(f'Wrapping in {wrapper_type}') wrap() @@ -51,6 +56,26 @@ def wrap_if_foreign_arch(arch: Arch): enforce_wrap() +def execute_without_exit(f, argv_override: Optional[list[str]], *args, **kwargs): + """If no wrap is needed, executes and returns f(*args, **kwargs). + If a wrap is determined to be necessary, force a wrap with argv_override applied. + If a wrap was forced, None is returned. + WARNING: No protection against f() returning None is taken.""" + if not needs_wrap(): + return f(*args, **kwargs) + assert get_wrapper_type() != 'none', "needs_wrap() should've returned False" + w = get_wrapper_impl() + w_cmd = w.argv_override + # we need to avoid throwing and catching SystemExit due to FDs getting closed otherwise + w_should_exit = w.should_exit + w.argv_override = argv_override + w.should_exit = False + w.wrap() + w.argv_override = w_cmd + w.should_exit = w_should_exit + return None + + nowrapper_option = click.option( '-w/-W', '--force-wrapper/--no-wrapper', diff --git a/wrapper/docker.py b/wrapper/docker.py index 2112548..d61a35b 100644 --- a/wrapper/docker.py +++ b/wrapper/docker.py @@ -7,7 +7,7 @@ import sys from config.state import config from exec.file import makedir -from .wrapper import BaseWrapper, WRAPPER_PATHS +from .wrapper import Wrapper, WRAPPER_PATHS DOCKER_PATHS = WRAPPER_PATHS.copy() @@ -19,7 +19,7 @@ def docker_volumes_args(volume_mappings: dict[str, str]) -> list[str]: return result -class DockerWrapper(BaseWrapper): +class DockerWrapper(Wrapper): type: str = 'docker' def wrap(self): @@ -86,15 +86,21 @@ class DockerWrapper(BaseWrapper): '--privileged', ] + docker_volumes_args(volumes) + [tag] - kupfer_cmd = ['kupferbootstrap', '--config', volumes[wrapped_config]] + self.filter_args_wrapper(sys.argv[1:]) + kupfer_cmd = [ + 'kupferbootstrap', + '--config', + volumes[wrapped_config], + ] + kupfer_cmd += self.argv_override or self.filter_args_wrapper(sys.argv[1:]) if config.runtime.uid: kupfer_cmd = ['wrapper_su_helper', '--uid', str(config.runtime.uid), '--username', 'kupfer', '--'] + kupfer_cmd cmd = docker_cmd + kupfer_cmd logging.debug('Wrapping in docker:' + repr(cmd)) result = subprocess.run(cmd) - - exit(result.returncode) + if self.should_exit: + exit(result.returncode) + return result.returncode def stop(self): subprocess.run( diff --git a/wrapper/wrapper.py b/wrapper/wrapper.py index 619c299..61bc197 100644 --- a/wrapper/wrapper.py +++ b/wrapper/wrapper.py @@ -15,7 +15,7 @@ WRAPPER_PATHS = CHROOT_PATHS | { } -class Wrapper(Protocol): +class WrapperProtocol(Protocol): """Wrappers wrap kupferbootstrap in some form of isolation from the host OS, i.e. docker or chroots""" def wrap(self): @@ -31,15 +31,19 @@ class Wrapper(Protocol): """ -class BaseWrapper(Wrapper): +class Wrapper(WrapperProtocol): uuid: str identifier: str type: str wrapped_config_path: str + argv_override: Optional[list[str]] + should_exit: bool def __init__(self, random_id: Optional[str] = None, name: Optional[str] = None): self.uuid = str(random_id or uuid.uuid4()) self.identifier = name or f'kupferbootstrap-{self.uuid}' + self.argv_override = None + self.should_exit = True def filter_args_wrapper(self, args): """filter out -c/--config since it doesn't apply in wrapper"""