From 128b1b7e5e185f2e5c3ffdb30746cc596a3bc333 Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Mon, 6 Feb 2023 01:51:35 +0100 Subject: [PATCH 1/8] config/state: remove `field_name` from missing device/flavour hint as it gets used as the profile name --- config/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 2c1584ab12a301653f458bf1a4ff9f510b59a8c2 Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Sun, 5 Mar 2023 18:33:13 +0100 Subject: [PATCH 2/8] wrapper: add needs_wrap(), typehint return values --- wrapper/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/wrapper/__init__.py b/wrapper/__init__.py index f7ce80a..f6f519f 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() From 07d084fd76099ff9a3cc2d90ceebffe3c00a5295 Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Mon, 13 Mar 2023 01:16:41 +0100 Subject: [PATCH 3/8] config/cli: warn when saving config in container --- config/cli.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/cli.py b/config/cli.py index dd82202..1d753ea 100644 --- a/config/cli.py +++ b/config/cli.py @@ -176,7 +176,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): @@ -333,7 +338,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}!') From da1c3b22b3c428524d3e66935609823359511f26 Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Mon, 13 Mar 2023 01:39:32 +0100 Subject: [PATCH 4/8] config/cli: save main config body separately from profiles to support flavour and device listing --- config/cli.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config/cli.py b/config/cli.py index 1d753ea..92d798c 100644 --- a/config/cli.py +++ b/config/cli.py @@ -206,6 +206,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 @@ -229,6 +231,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: @@ -244,7 +247,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) @@ -271,6 +281,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('=') @@ -328,6 +339,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] From 01dc62aa926c83e125743c2fb4352876bbc0eecc Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Mon, 13 Mar 2023 05:32:37 +0100 Subject: [PATCH 5/8] wrapper: add Wrapper.argv_override --- wrapper/docker.py | 7 ++++++- wrapper/wrapper.py | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/wrapper/docker.py b/wrapper/docker.py index 2112548..cac3088 100644 --- a/wrapper/docker.py +++ b/wrapper/docker.py @@ -86,7 +86,12 @@ 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 diff --git a/wrapper/wrapper.py b/wrapper/wrapper.py index 619c299..1c7ba01 100644 --- a/wrapper/wrapper.py +++ b/wrapper/wrapper.py @@ -31,15 +31,17 @@ class Wrapper(Protocol): """ -class BaseWrapper(Wrapper): +class Wrapper(WrapperProtocol): uuid: str identifier: str type: str wrapped_config_path: str + argv_override: Optional[list[str]] 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 def filter_args_wrapper(self, args): """filter out -c/--config since it doesn't apply in wrapper""" From a86f2d3cbb12b51aa12b12df940948a77a2b868e Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Mon, 13 Mar 2023 05:33:21 +0100 Subject: [PATCH 6/8] wrapper: add Wrapper.should_exit --- wrapper/docker.py | 9 +++++---- wrapper/wrapper.py | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/wrapper/docker.py b/wrapper/docker.py index cac3088..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): @@ -98,8 +98,9 @@ class DockerWrapper(BaseWrapper): 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 1c7ba01..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): @@ -37,11 +37,13 @@ class Wrapper(WrapperProtocol): 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""" From bc3fa84f5818206b6e1b1f3e5dea0f18350aa79b Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Mon, 13 Mar 2023 05:54:10 +0100 Subject: [PATCH 7/8] wrapper: add execute_without_exit() --- wrapper/__init__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/wrapper/__init__.py b/wrapper/__init__.py index f6f519f..a86f879 100644 --- a/wrapper/__init__.py +++ b/wrapper/__init__.py @@ -56,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', From 2408f00132cbf198897c646c8b7485e14a5bc524 Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Mon, 13 Mar 2023 05:54:36 +0100 Subject: [PATCH 8/8] config/cli: use wrapper.execute_without_exit() for prompt_profile_{flavour,device}() to avoid prompting in docker --- config/cli.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/config/cli.py b/config/cli.py index 92d798c..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())