Compare commits

...
Sign in to create a new pull request.

8 commits

5 changed files with 74 additions and 13 deletions

View file

@ -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}!')

View file

@ -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

View file

@ -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',

View file

@ -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(

View file

@ -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"""