index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
author | Anton Hvornum <anton@hvornum.se> | 2021-04-17 08:02:46 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-17 08:02:46 +0000 |
commit | 9ed29df8528688132a192959fafc0899fdca6194 (patch) | |
tree | adbc75a03ad664f03f85689d76af7a4c92fe6292 | |
parent | e5b0468384ba70398b91c5418399b0118ece0bc5 (diff) | |
parent | c63d2140a46ee4001f855a575499cd51b7d0c0ee (diff) |
-rw-r--r-- | .github/workflows/iso-build.yaml | 11 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | archinstall/lib/general.py | 10 | ||||
-rw-r--r-- | archinstall/lib/installer.py | 12 | ||||
-rw-r--r-- | archinstall/lib/locale_helpers.py | 14 | ||||
-rw-r--r-- | archinstall/lib/mirrors.py | 9 | ||||
-rw-r--r-- | archinstall/lib/profiles.py | 3 | ||||
-rw-r--r-- | archinstall/lib/services.py | 2 | ||||
-rw-r--r-- | archinstall/lib/user_interaction.py | 38 | ||||
-rw-r--r-- | examples/guided.py | 99 | ||||
-rw-r--r-- | profiles/applications/awesome.py | 6 | ||||
-rw-r--r-- | profiles/awesome.py | 4 |
diff --git a/.github/workflows/iso-build.yaml b/.github/workflows/iso-build.yaml index 229a37be..d08e68ff 100644 --- a/.github/workflows/iso-build.yaml +++ b/.github/workflows/iso-build.yaml @@ -2,7 +2,16 @@ name: Build Arch ISO with ArchInstall Commit -on: pull_request +on: + pull_request: + paths-ignore: + - '.github/**' + - 'docs/**' + - '**.editorconfig' + - '**.gitignore' + - '**.md' + - 'LICENSE' + - 'PKGBUILD' jobs: build: @@ -1,4 +1,9 @@ -# <img src="https://github.com/archlinux/archinstall/raw/master/docs/logo.png" alt="drawing" width="200"/> +<!-- <div align="center"> --> +<img src="https://github.com/archlinux/archinstall/raw/master/docs/logo.png" alt="drawing" width="200"/> + +# Arch Installer +<!-- </div> --> + Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist. The installer also doubles as a python library to install Arch Linux and manage services, packages and other things inside the installed system *(Usually from a live medium)*. @@ -51,7 +56,7 @@ disk_password = getpass.getpass(prompt='Disk password (won\'t echo): ') harddrive.keep_partitions = False # First, we configure the basic filesystem layout -with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs: +with archinstall.Filesystem(harddrive, archinstall.GPT) as fs: # We create a filesystem layout that will use the entire drive # (this is a helper function, you can partition manually as well) fs.use_entire_disk(root_filesystem_type='btrfs') @@ -61,9 +66,9 @@ with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) boot.format('vfat') - # Set the flat for encrypted to allow for encryption and then encrypt + # Set the flag for encrypted to allow for encryption and then encrypt root.encrypted = True - root.encrypt(password=archinstall.arguments.get('!encryption-password', None)) + root.encrypt(password=disk_password) with archinstall.luks2(root, 'luksloop', disk_password) as unlocked_root: unlocked_root.format(root.filesystem) diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 5b1b3c2a..dc0f018a 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -76,7 +76,7 @@ class sys_command():#Thread): """ Stolen from archinstall_gui """ - def __init__(self, cmd, callback=None, start_callback=None, peak_output=False, *args, **kwargs): + def __init__(self, cmd, callback=None, start_callback=None, peak_output=False, environment_vars={}, *args, **kwargs): kwargs.setdefault("worker_id", gen_uid()) kwargs.setdefault("emulate", False) kwargs.setdefault("suppress_errors", False) @@ -102,6 +102,7 @@ class sys_command():#Thread): self.args = args self.kwargs = kwargs self.peak_output = peak_output + self.environment_vars = environment_vars self.kwargs.setdefault("worker", None) self.callback = callback @@ -200,7 +201,7 @@ class sys_command():#Thread): # Replace child process with our main process if not self.kwargs['emulate']: try: - os.execv(self.cmd[0], self.cmd) + os.execve(self.cmd[0], self.cmd, {**os.environ, **self.environment_vars}) except FileNotFoundError: self.status = 'done' self.log(f"{self.cmd[0]} does not exist.", level=LOG_LEVELS.Debug) @@ -304,6 +305,11 @@ class sys_command():#Thread): with open(f'{self.cwd}/trace.log', 'wb') as fh: fh.write(self.trace_log) + try: + os.close(child_fd) + except: + pass + def prerequisite_check(): if not os.path.isdir("/sys/firmware/efi"): diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 2effc7f7..75ef9c4e 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -86,6 +86,7 @@ class Installer(): if not (missing_steps := self.post_install_check()): self.log('Installation completed without any errors. You may now reboot.', bg='black', fg='green', level=LOG_LEVELS.Info) self.sync_log_to_install_medium() + return True else: self.log('Some required steps were not successfully installed/configured before leaving the installer:', bg='black', fg='red', level=LOG_LEVELS.Warning) @@ -195,6 +196,9 @@ class Installer(): def arch_chroot(self, cmd, *args, **kwargs): return self.run_command(cmd) + def drop_to_shell(self): + subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True) + def configure_nic(self, nic, dhcp=True, ip=None, gateway=None, dns=None, *args, **kwargs): if dhcp: conf = Networkd(Match={"Name": nic}, Network={"DHCP": "yes"}) @@ -474,10 +478,18 @@ class Installer(): o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{user}:{password}' | chpasswd\"")) pass + + def user_set_shell(self, user, shell): + self.log(f'Setting shell for {user} to {shell}', level=LOG_LEVELS.Info) + + o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.target} sh -c \"chsh -s {shell} {user}\"")) + pass def set_keyboard_language(self, language): if len(language.strip()): with open(f'{self.target}/etc/vconsole.conf', 'w') as vconsole: vconsole.write(f'KEYMAP={language}\n') vconsole.write(f'FONT=lat9w-16\n') + else: + self.log(f'Keyboard language was not changed from default (no language specified).', fg="yellow", level=LOG_LEVELS.Info) return True diff --git a/archinstall/lib/locale_helpers.py b/archinstall/lib/locale_helpers.py index 523a23d5..736bfc47 100644 --- a/archinstall/lib/locale_helpers.py +++ b/archinstall/lib/locale_helpers.py @@ -1,29 +1,25 @@ +import subprocess import os from .exceptions import * # from .general import sys_command -def list_keyboard_languages(layout='qwerty'): +def list_keyboard_languages(): locale_dir = '/usr/share/kbd/keymaps/' if not os.path.isdir(locale_dir): raise RequirementError(f'Directory containing locales does not exist: {locale_dir}') for root, folders, files in os.walk(locale_dir): - # Since qwerty is under /i386/ but other layouts are - # in different spots, we'll need to filter the last foldername - # of the path to verify against the desired layout. - if os.path.basename(root) != layout: - continue for file in files: if os.path.splitext(file)[1] == '.gz': yield file.strip('.gz').strip('.map') -def search_keyboard_layout(filter, layout='qwerty'): - for language in list_keyboard_languages(layout): +def search_keyboard_layout(filter): + for language in list_keyboard_languages(): if filter.lower() in language.lower(): yield language def set_keyboard_language(locale): - return os.system(f'loadkeys {locale}') == 0 + return subprocess.call(['loadkeys', locale]) == 0 diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index d7d35782..04f47c0d 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -74,10 +74,15 @@ def re_rank_mirrors(top=10, *positionals, **kwargs): def list_mirrors(): url = f"https://archlinux.org/mirrorlist/?protocol=https&ip_version=4&ip_version=6&use_mirror_status=on" - - response = urllib.request.urlopen(url) regions = {} + try: + response = urllib.request.urlopen(url) + except urllib.error.URLError as err: + log(f'Could not fetch an active mirror-list: {err}', level=LOG_LEVELS.Warning, fg="yellow") + return regions + + region = 'Unknown region' for line in response.readlines(): if len(line.strip()) == 0: diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py index 94d9f6ee..c5f63e72 100644 --- a/archinstall/lib/profiles.py +++ b/archinstall/lib/profiles.py @@ -177,7 +177,7 @@ class Profile(Script): if hasattr(imported, '_prep_function'): return True return False - """ + def has_post_install(self): with open(self.path, 'r') as source: source_data = source.read() @@ -193,7 +193,6 @@ class Profile(Script): with self.load_instructions(namespace=f"{self.namespace}.py") as imported: if hasattr(imported, '_post_install'): return True - """ def is_top_level_profile(self): with open(self.path, 'r') as source: diff --git a/archinstall/lib/services.py b/archinstall/lib/services.py index 8fcdd296..bb6f64f2 100644 --- a/archinstall/lib/services.py +++ b/archinstall/lib/services.py @@ -7,6 +7,6 @@ def service_state(service_name: str): if os.path.splitext(service_name)[1] != '.service': service_name += '.service' # Just to be safe - state = b''.join(sys_command(f'systemctl show -p SubState --value {service_name}')) + state = b''.join(sys_command(f'systemctl show --no-pager -p SubState --value {service_name}', environment_vars={'SYSTEMD_COLORS' : '0'})) return state.strip().decode('UTF-8') diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index 114c7c6a..822f63be 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -132,7 +132,9 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan return users, super_users def ask_for_a_timezone(): - timezone = input('Enter a valid timezone (Example: Europe/Stockholm): ').strip() + timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip() + if timezone == '': + timezone = 'UTC' if (pathlib.Path("/usr")/"share"/"zoneinfo"/timezone).exists(): return timezone else: @@ -186,7 +188,7 @@ def ask_to_configure_network(): elif nic: return nic - return None + return {} def ask_for_disk_layout(): options = { @@ -323,6 +325,8 @@ def select_language(options, show_only_country_codes=True): :return: The language/dictionary key of the selected language :rtype: str """ + DEFAULT_KEYBOARD_LANGUAGE = 'us' + if show_only_country_codes: languages = sorted([language for language in list(options) if len(language) == 2]) else: @@ -332,9 +336,12 @@ def select_language(options, show_only_country_codes=True): for index, language in enumerate(languages): print(f"{index}: {language}") - print(' -- You can enter ? or help to search for more languages --') + print(' -- You can enter ? or help to search for more languages, or skip to use US layout --') selected_language = input('Select one of the above keyboard languages (by number or full name): ') - if selected_language.lower() in ('?', 'help'): + + if len(selected_language.strip()) == 0: + return DEFAULT_KEYBOARD_LANGUAGE + elif selected_language.lower() in ('?', 'help'): while True: filter_string = input('Search for layout containing (example: "sv-"): ') new_options = list(search_keyboard_layout(filter_string)) @@ -347,6 +354,7 @@ def select_language(options, show_only_country_codes=True): elif selected_language.isdigit() and (pos := int(selected_language)) <= len(languages)-1: selected_language = languages[pos] + return selected_language # I'm leaving "options" on purpose here. # Since languages possibly contains a filtered version of # all possible language layouts, and we might want to write @@ -354,9 +362,9 @@ def select_language(options, show_only_country_codes=True): # go through the search step. elif selected_language in options: selected_language = options[options.index(selected_language)] + return selected_language else: - RequirementError("Selected language does not exist.") - return selected_language + raise RequirementError("Selected language does not exist.") raise RequirementError("Selecting languages require a least one language to be given as an option.") @@ -380,25 +388,27 @@ def select_mirror_regions(mirrors, show_top_mirrors=True): selected_mirrors = {} if len(regions) >= 1: - print_large_list(regions, margin_bottom=2) + print_large_list(regions, margin_bottom=4) print(' -- You can skip this step by leaving the option blank --') selected_mirror = input('Select one of the above regions to download packages from (by number or full name): ') if len(selected_mirror.strip()) == 0: + # Returning back empty options which can be both used to + # do "if x:" logic as well as do `x.get('mirror', {}).get('sub', None)` chaining return {} - elif selected_mirror.isdigit() and (pos := int(selected_mirror)) <= len(regions)-1: + elif selected_mirror.isdigit() and int(selected_mirror) <= len(regions)-1: + # I'm leaving "mirrors" on purpose here. + # Since region possibly contains a known region of + # all possible regions, and we might want to write + # for instance Sweden (if we know that exists) without having to + # go through the search step. region = regions[int(selected_mirror)] selected_mirrors[region] = mirrors[region] - # I'm leaving "mirrors" on purpose here. - # Since region possibly contains a known region of - # all possible regions, and we might want to write - # for instance Sweden (if we know that exists) without having to - # go through the search step. elif selected_mirror in mirrors: selected_mirrors[selected_mirror] = mirrors[selected_mirror] else: - RequirementError("Selected region does not exist.") + raise RequirementError("Selected region does not exist.") return selected_mirrors diff --git a/examples/guided.py b/examples/guided.py index 07d8c0af..df708ac1 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -3,24 +3,6 @@ import archinstall from archinstall.lib.hardware import hasUEFI from archinstall.lib.profiles import Profile -""" -This signal-handler chain (and global variable) -is used to trigger the "Are you sure you want to abort?" question further down. -It might look a bit odd, but have a look at the line: "if SIG_TRIGGER:" -""" -SIG_TRIGGER = False -def kill_handler(sig, frame): - print() - exit(0) - -def sig_handler(sig, frame): - global SIG_TRIGGER - SIG_TRIGGER = True - signal.signal(signal.SIGINT, kill_handler) - -original_sigint_handler = signal.getsignal(signal.SIGINT) -signal.signal(signal.SIGINT, sig_handler) - if archinstall.arguments.get('help'): print("See `man archinstall` for help.") exit(0) @@ -32,7 +14,12 @@ def ask_user_questions(): will we continue with the actual installation steps. """ if not archinstall.arguments.get('keyboard-language', None): - archinstall.arguments['keyboard-language'] = archinstall.select_language(archinstall.list_keyboard_languages()).strip() + while True: + try: + archinstall.arguments['keyboard-language'] = archinstall.select_language(archinstall.list_keyboard_languages()).strip() + break + except archinstall.RequirementError as err: + archinstall.log(err, fg="red") # Before continuing, set the preferred keyboard layout/language in the current terminal. # This will just help the user with the next following questions. @@ -41,7 +28,12 @@ def ask_user_questions(): # Set which region to download packages from during the installation if not archinstall.arguments.get('mirror-region', None): - archinstall.arguments['mirror-region'] = archinstall.select_mirror_regions(archinstall.list_mirrors()) + while True: + try: + archinstall.arguments['mirror-region'] = archinstall.select_mirror_regions(archinstall.list_mirrors()) + break + except archinstall.RequirementError as e: + archinstall.log(e, fg="red") else: selected_region = archinstall.arguments['mirror-region'] archinstall.arguments['mirror-region'] = {selected_region : archinstall.list_mirrors()[selected_region]} @@ -193,22 +185,27 @@ def ask_user_questions(): else: # packages installed by a profile may depend on audio and something may get installed anyways, not much we can do about that. # we will not try to remove packages post-installation to not have audio, as that may cause multiple issues - archinstall.arguments['audio'] = 'none' + archinstall.arguments['audio'] = None # Additional packages (with some light weight error handling for invalid package names) - if not archinstall.arguments.get('packages', None): - print("Packages not part of the desktop environment are not installed by default.") - print("If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.") - archinstall.arguments['packages'] = [package for package in input('Write additional packages to install (space separated, leave blank to skip): ').split(' ') if len(package)] - - if len(archinstall.arguments['packages']): - # Verify packages that were given - try: - archinstall.log(f"Verifying that additional packages exist (this might take a few seconds)") - archinstall.validate_package_list(archinstall.arguments['packages']) - except archinstall.RequirementError as e: - archinstall.log(e, fg='red') - exit(1) + while True: + if not archinstall.arguments.get('packages', None): + print("Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.") + print("If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.") + archinstall.arguments['packages'] = [package for package in input('Write additional packages to install (space separated, leave blank to skip): ').split(' ') if len(package)] + + if len(archinstall.arguments['packages']): + # Verify packages that were given + try: + archinstall.log(f"Verifying that additional packages exist (this might take a few seconds)") + archinstall.validate_package_list(archinstall.arguments['packages']) + break + except archinstall.RequirementError as e: + archinstall.log(e, fg='red') + archinstall.arguments['packages'] = None # Clear the packages to trigger a new input question + else: + # no additional packages were selected, which we'll allow + break # Ask or Call the helper function that asks the user to optionally configure a network. if not archinstall.arguments.get('nic', None): @@ -299,32 +296,36 @@ def perform_installation(mountpoint): # Certain services might be running that affects the system during installation. # Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist # We need to wait for it before we continue since we opted in to use a custom mirror/region. - installation.log(f'Waiting for automatic mirror selection has completed before using custom mirrors.') - while 'dead' not in (status := archinstall.service_state('reflector')): + installation.log(f'Waiting for automatic mirror selection (reflector) to complete.', level=archinstall.LOG_LEVELS.Info) + while archinstall.service_state('reflector') not in ('dead', 'failed'): time.sleep(1) - archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + # Set mirrors used by pacstrap (outside of installation) + if archinstall.arguments.get('mirror-region', None): + archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium + if installation.minimal_installation(): installation.set_hostname(archinstall.arguments['hostname']) - installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if archinstall.arguments['mirror-region'].get("mirrors",{})!= None: + installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium installation.set_keyboard_language(archinstall.arguments['keyboard-language']) installation.add_bootloader() # If user selected to copy the current ISO network configuration # Perform a copy of the config - if archinstall.arguments.get('nic', None) == 'Copy ISO network configuration to installation': + if archinstall.arguments.get('nic', {}) == 'Copy ISO network configuration to installation': installation.copy_ISO_network_config(enable_services=True) # Sources the ISO network configuration to the install medium. - elif archinstall.arguments.get('nic',{}).get('NetworkManager',False): + elif archinstall.arguments.get('nic', {}).get('NetworkManager',False): installation.add_additional_packages("networkmanager") installation.enable_service('NetworkManager.service') # Otherwise, if a interface was selected, configure that interface - elif archinstall.arguments.get('nic', None): + elif archinstall.arguments.get('nic', {}): installation.configure_nic(**archinstall.arguments.get('nic', {})) installation.enable_service('systemd-networkd') installation.enable_service('systemd-resolved') - if archinstall.arguments.get('audio', None) != None: - installation.log(f"The {archinstall.arguments.get('audio', None)} audio server will be used.", level=archinstall.LOG_LEVELS.Info) + if archinstall.arguments.get('audio', None) != None: + installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}", level=archinstall.LOG_LEVELS.Info) if archinstall.arguments.get('audio', None) == 'pipewire': print('Installing pipewire ...') @@ -332,6 +333,8 @@ def perform_installation(mountpoint): elif archinstall.arguments.get('audio', None) == 'pulseaudio': print('Installing pulseaudio ...') installation.add_additional_packages("pulseaudio") + else: + installation.log("No audio server will be installed.", level=archinstall.LOG_LEVELS.Info) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': installation.add_additional_packages(archinstall.arguments.get('packages', None)) @@ -350,6 +353,7 @@ def perform_installation(mountpoint): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) + if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_post_install(): with archinstall.arguments['profile'].load_instructions(namespace=f"{archinstall.arguments['profile'].namespace}.py") as imported: if not imported._post_install(): @@ -359,5 +363,14 @@ def perform_installation(mountpoint): ) exit(1) + installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") + choice = input("Would you like to chroot into the newly created installation and perform post-installation configuration? [Y/n] ") + if choice.lower() in ("y", ""): + try: + installation.drop_to_shell() + except: + pass + ask_user_questions() perform_installation_steps() + diff --git a/profiles/applications/awesome.py b/profiles/applications/awesome.py index b8d779c0..a63c707b 100644 --- a/profiles/applications/awesome.py +++ b/profiles/applications/awesome.py @@ -6,7 +6,7 @@ installation.install_profile('xorg') installation.add_additional_packages(__packages__) -with open(f'{installation.mountpoint}/etc/X11/xinit/xinitrc', 'r') as xinitrc: +with open(f'{installation.target}/etc/X11/xinit/xinitrc', 'r') as xinitrc: xinitrc_data = xinitrc.read() for line in xinitrc_data.split('\n'): @@ -20,5 +20,5 @@ for line in xinitrc_data.split('\n'): xinitrc_data += '\n' xinitrc_data += 'exec awesome\n' -with open(f'{installation.mountpoint}/etc/X11/xinit/xinitrc', 'w') as xinitrc: - xinitrc.write(xinitrc_data)
\ No newline at end of file +with open(f'{installation.target}/etc/X11/xinit/xinitrc', 'w') as xinitrc: + xinitrc.write(xinitrc_data) diff --git a/profiles/awesome.py b/profiles/awesome.py index 0b00a424..cbd52a3c 100644 --- a/profiles/awesome.py +++ b/profiles/awesome.py @@ -39,13 +39,13 @@ if __name__ == 'awesome': alacritty.install() # TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead. - with open(f'{installation.mountpoint}/etc/xdg/awesome/rc.lua', 'r') as fh: + with open(f'{installation.target}/etc/xdg/awesome/rc.lua', 'r') as fh: awesome_lua = fh.read() ## Replace xterm with alacritty for a smoother experience. awesome_lua = awesome_lua.replace('"xterm"', '"alacritty"') - with open(f'{installation.mountpoint}/etc/xdg/awesome/rc.lua', 'w') as fh: + with open(f'{installation.target}/etc/xdg/awesome/rc.lua', 'w') as fh: fh.write(awesome_lua) ## TODO: Configure the right-click-menu to contain the above packages that were installed. (as a user config) |