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 devices.device import get_devices
from flavours.flavour import get_flavours from flavours.flavour import get_flavours
from wrapper import execute_without_exit
from .scheme import Profile from .scheme import Profile
from .profile import PROFILE_EMPTY, PROFILE_DEFAULTS 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]: 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)) 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()): for dev in sorted(devices.keys()):
print(devices[dev]) print(devices[dev])
return prompt_choice(current, f'profiles.{profile_name}.device', devices.keys()) return prompt_choice(current, f'profiles.{profile_name}.device', devices.keys())
def prompt_profile_flavour(current: Optional[str], profile_name: str) -> tuple[str, bool]: 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)) 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()): for f in sorted(flavours.keys()):
print(flavours[f]) print(flavours[f])
return prompt_choice(current, f'profiles.{profile_name}.flavour', flavours.keys()) 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. 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. 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 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 return True
if retry_ctx: if retry_ctx:
if click.confirm('Retry? ("n" to quit without saving)', default=True): 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) 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) 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') @cmd_config.command(name='init')
@noninteractive_flag @noninteractive_flag
@ -224,6 +238,7 @@ def cmd_config_init(
): ):
"""Initialize the config file""" """Initialize the config file"""
if not non_interactive: if not non_interactive:
logging.info(CONFIG_MSG)
results: dict[str, dict] = {} results: dict[str, dict] = {}
for section in sections: for section in sections:
if section not in CONFIG_SECTIONS: if section not in CONFIG_SECTIONS:
@ -239,7 +254,14 @@ def cmd_config_init(
results[section][key] = result results[section][key] = result
config.update(results) config.update(results)
print("Main configuration complete")
if not noop:
if prompt_for_save(ctx):
config.write()
else:
return
if 'profiles' in sections: if 'profiles' in sections:
print("Configuring profiles")
current_profile = 'default' if 'current' not in config.file.profiles else config.file.profiles.current 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) new_current, _ = prompt_config('profiles.current', default=current_profile, field_type=str)
profile, changed = prompt_profile(new_current, create=True, no_parse=no_parse) 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. like `build.clean_mode=false` or alternatively just keys to get prompted if run interactively.
""" """
config.enforce_config_loaded() config.enforce_config_loaded()
logging.info(CONFIG_MSG)
config_copy = deepcopy(config.file) config_copy = deepcopy(config.file)
for pair in key_vals: for pair in key_vals:
split_pair = pair.split('=') 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) profile = deepcopy(PROFILE_EMPTY)
if name == 'current': if name == 'current':
raise Exception("profile name 'current' not allowed") raise Exception("profile name 'current' not allowed")
logging.info(CONFIG_MSG)
name = name or config.file.profiles.current name = name or config.file.profiles.current
if name in config.file.profiles: if name in config.file.profiles:
profile |= config.file.profiles[name] 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) config.update_profile(name, profile)
if not noop: if not noop:
if not prompt_for_save(ctx): if not prompt_for_save(ctx):
logging.info("Not saving.")
return return
config.write() config.write()
else: else:
logging.info(f'--noop passed, not writing to {config.runtime.config_file}!') 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) profile = self.get_profile(profile_name)
if field not in profile or not profile[field]: if field not in profile or not profile[field]:
m = (f'Profile "{profile_name}" has no {field.upper()} configured.\n' 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) raise Exception(m)
return profile 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 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() 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) wrapper_type = get_wrapper_type(wrapper_type)
return wrapper_type != 'none' and get_wrapper_impl(wrapper_type).is_wrapped() 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): def enforce_wrap(no_wrapper=False):
wrapper_type = get_wrapper_type() 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}') logging.info(f'Wrapping in {wrapper_type}')
wrap() wrap()
@ -51,6 +56,26 @@ def wrap_if_foreign_arch(arch: Arch):
enforce_wrap() 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( nowrapper_option = click.option(
'-w/-W', '-w/-W',
'--force-wrapper/--no-wrapper', '--force-wrapper/--no-wrapper',

View file

@ -7,7 +7,7 @@ import sys
from config.state import config from config.state import config
from exec.file import makedir from exec.file import makedir
from .wrapper import BaseWrapper, WRAPPER_PATHS from .wrapper import Wrapper, WRAPPER_PATHS
DOCKER_PATHS = WRAPPER_PATHS.copy() DOCKER_PATHS = WRAPPER_PATHS.copy()
@ -19,7 +19,7 @@ def docker_volumes_args(volume_mappings: dict[str, str]) -> list[str]:
return result return result
class DockerWrapper(BaseWrapper): class DockerWrapper(Wrapper):
type: str = 'docker' type: str = 'docker'
def wrap(self): def wrap(self):
@ -86,15 +86,21 @@ class DockerWrapper(BaseWrapper):
'--privileged', '--privileged',
] + docker_volumes_args(volumes) + [tag] ] + 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: if config.runtime.uid:
kupfer_cmd = ['wrapper_su_helper', '--uid', str(config.runtime.uid), '--username', 'kupfer', '--'] + kupfer_cmd kupfer_cmd = ['wrapper_su_helper', '--uid', str(config.runtime.uid), '--username', 'kupfer', '--'] + kupfer_cmd
cmd = docker_cmd + kupfer_cmd cmd = docker_cmd + kupfer_cmd
logging.debug('Wrapping in docker:' + repr(cmd)) logging.debug('Wrapping in docker:' + repr(cmd))
result = subprocess.run(cmd) result = subprocess.run(cmd)
if self.should_exit:
exit(result.returncode) exit(result.returncode)
return result.returncode
def stop(self): def stop(self):
subprocess.run( 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""" """Wrappers wrap kupferbootstrap in some form of isolation from the host OS, i.e. docker or chroots"""
def wrap(self): def wrap(self):
@ -31,15 +31,19 @@ class Wrapper(Protocol):
""" """
class BaseWrapper(Wrapper): class Wrapper(WrapperProtocol):
uuid: str uuid: str
identifier: str identifier: str
type: str type: str
wrapped_config_path: 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): def __init__(self, random_id: Optional[str] = None, name: Optional[str] = None):
self.uuid = str(random_id or uuid.uuid4()) self.uuid = str(random_id or uuid.uuid4())
self.identifier = name or f'kupferbootstrap-{self.uuid}' self.identifier = name or f'kupferbootstrap-{self.uuid}'
self.argv_override = None
self.should_exit = True
def filter_args_wrapper(self, args): def filter_args_wrapper(self, args):
"""filter out -c/--config since it doesn't apply in wrapper""" """filter out -c/--config since it doesn't apply in wrapper"""