From c4e09ea7f16f2d310fe92edc6176aa82b575cb9d Mon Sep 17 00:00:00 2001 From: Denis Benato Date: Sat, 22 Jun 2024 14:59:36 +0200 Subject: [PATCH] refactor: Complete rewrite of the FRZR project Co-authored-by: Matthew Anderson --- README.md | 60 ++- __frzr | 863 ++++++++++++++++++++++++++++++++++++++ __frzr-bootloader | 259 ++++++++++++ __frzr-bootstrap | 351 ++++++++++++++++ __frzr-deploy | 1026 +++++++++++++++++++++++++-------------------- __frzr-envars | 46 ++ __frzr-kernel | 424 +++++++++++++++++++ __frzr-unlock | 168 ++++++++ __frzr-version | 10 + frzr | 154 +++++++ frzr-bootloader | 10 + frzr-bootstrap | 112 +---- frzr-deploy | 18 +- frzr-extras | 30 ++ frzr-initramfs | 132 ------ frzr-kernel | 10 + frzr-release | 8 +- frzr-source | 8 + frzr-tweaks | 24 -- frzr-unlock | 57 +-- frzr-version | 10 + mkinitcpio.conf | 8 + test/run.sh | 36 +- 23 files changed, 2999 insertions(+), 825 deletions(-) create mode 100644 __frzr create mode 100644 __frzr-bootloader create mode 100644 __frzr-bootstrap create mode 100644 __frzr-envars create mode 100644 __frzr-kernel create mode 100644 __frzr-unlock create mode 100644 __frzr-version create mode 100755 frzr create mode 100755 frzr-bootloader create mode 100755 frzr-extras delete mode 100755 frzr-initramfs create mode 100644 frzr-kernel create mode 100755 frzr-source delete mode 100755 frzr-tweaks create mode 100644 frzr-version create mode 100644 mkinitcpio.conf diff --git a/README.md b/README.md index 108059f..e17e5a3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,62 @@ # frzr -frzr is a deployment and automatic update mechanism for operating systems. It deploys pre-built systems via read-only btrfs subvolumes, thus ensuring safe and atomic updates that never interrupt the user. +frzr is a deployment and automatic update mechanism for operating systems. +It deploys pre-built systems that have been generated using btrfs send to snapshot a rootfs image. + +## Features +Despite frzr being the deployment software for chimeraos it has been designed to handle every linux distribution, +even embedded ones or the more traditional ones. + +Each distibution can either be immutable or read/write, immutable distros can choose how they want to achieve the goal +and implement a distro-specific way of unlocking the filesystem. + +frzr aims at: +- ensuring safe and atomic updates that never interrupt the user +- distributing a known tested and working copy of a system +- allows easier transitioning among different operating systems and versions +- allows installing one kernel that can be shared among all installed systems + +## How Updated system images are downloaded at boot time and deployed to an entirely separate subvolume so as not to interfere with the currently running system. Upon next boot, the newly installed system is started and the old one is deleted, completely seamlessly and invisibly. -Persistence is handled by making `/home` and `/var` separate subvolumes mounted into the read-only root. -`/etc` is made read-write via overlayfs. +Actions to be performed when installing the system, unlocking it (if required) and uninstalling it are distro-specific and are provided +via scripts that frzr runs when performing the requested action. + +## More +To ease system management and system debuggability frzr can be used to keep a primary distro on the main btrfs subvolume and +manage deployments in a completely separate way so that your beloved archlinux install will remain usable whenever you want to. + +Also frzr ships with utilities that are meant to regenerate bootloader entries whenever a kernel gets installed or uninstalled. + +## Q&A + +__Q__ Are there limitations on what systems can be deployed? + +__A__ Yes, supported systems must support booting from a btrfs subvolume. + + +__Q__ Is the distro limited in what it can do once installed? + +__A__ Not by default, every limitation is decided by maintainers of that distro. + + +__Q__ Is it easy to create a proof-of-concept distro that is compatible with frzr? + +__A__ Absolutely: frzr wants to be hackable and easy to work with in every aspect: just create a btrfs subvolume and run your preferred command to write a rootfs (debootstrap, pacstrap or whatever). Then create a snapshot of it with btrfs send and that file can be deployed on real hardware! + + +__Q__ Can a distro be shipped with its own kernel? + +__A__ Yes and this is the preferred method. The user can boot a deployment only if such deployment comes with its own kernel or the user has installed one +already via the dedicated *frzr kernel* tool! + + +__Q__ Are there rules to follow? + +__A__ There are conventions: for example a distro should __NOT__ assume it's the only distro installed, and *SHOULD NOT* cause harm to other deployments for example by writing to the efi partition. + + +__Q__ Can data be shared among different distributions? + +__A__ This depends on the distro: the easiest way probably is using systemd-homed and mount in /home the home directory. \ No newline at end of file diff --git a/__frzr b/__frzr new file mode 100644 index 0000000..8117e93 --- /dev/null +++ b/__frzr @@ -0,0 +1,863 @@ +#! /bin/bash + +# All global variables and signals are sourced from __frzr-envars +source "${BASH_SOURCE%/*}/__frzr-envars" + +frzr_status() { + # Output frzr status tracker using the JSON format + cat < "${TRACKER_FILE_PATH}" +} + +send_data(){ + sleep 1 + write_tracker_file +} + +frzr_check_bootenv() { + if [ -d /tmp/frzr_root ]; then + UPGRADE=0 + else + UPGRADE=1 + fi +} + +# Perform required mounts to have a working chroot environment +# $1 the rootfs to be chrooted +# stdout OK, an error otherwise +frzr_mount_chroot() { + local CHROOT_PATH=$1 + + if mount -t proc /proc "${CHROOT_PATH}/proc"; then + if mount -t sysfs /sys "${CHROOT_PATH}/sys"; then + if mount --rbind /dev "${CHROOT_PATH}/dev"; then + echo "OK" + else + echo "ERROR: Could not bind-mount /dev to '${CHROOT_PATH}/dev'" + fi + else + echo "ERROR: Could not mount sysfs to '${CHROOT_PATH}/sys'" + fi + else + echo "ERROR: Could not mount proc to '${CHROOT_PATH}/proc'" + fi +} + +frzr_exec_chroot() { + local CHROOT_PATH=$1 + local CHROOT_CMD=$2 + + chroot ${CHROOT_PATH} /bin/bash </dev/null; then + for m in "${deployment}"/usr/lib/frzr.d/*.migration; do + unset frzr_migration_version + + unset -f post_install + + # source the migration + . $m + + # only execute migrations marked for newer frzr versions + if [ ! -z "$frzr_migration_version" ] && [ $frzr_migration_version -gt 0 ]; then + if [ "$(type -t post_install)" == function ] ; then + + # Run migration and check for errors + local install_migration_first_error=$(post_install "${frzr_root}" "${deployment}" "${deployment_version}" "${frzr_version}") + if echo "${install_migration_first_error}" | grep -Fq "ERROR"; then + break + fi + fi + fi + + unset -f post_install + + unset frzr_migration_version + done + + if echo "${install_migration_first_error}" | grep -Fq "ERROR"; then + echo "ERROR: Could not execute migration '$m': ${install_migration_first_error}" + else + echo "OK" + fi + else + echo "OK" + fi +} + +# Check if there are unlock scripts available in the image to be unlocked and run them one by one +# Note: post_unlock scripts are functions named unlock inside (executable) files with .unlock extension +# stored inside $2/usr/lib/frzr.d/ +# +# Every post_unlock function will be run in a subshell. +# +# PRE=$2/usr/lib/frzr.d is a directory +# POST= +# +# $1 the deployment version; this is also the deployment name (the name of the subvolume to be used as rootfs) +# $2 the mounted subvolume +# $3 frzr_root the mounted path to the main btrfs subvolume (the one that contains home as a subvolume) +# $4 frzr version string +# stdout "OK" on success, the error otherwise +execute_unlock() { + local version=$1 + local deployment=$2 + local frzr_root=$3 + local frzr_version=$4 + + local error="" + + if compgen -G "${deployment}"/usr/lib/frzr.d/*.unlock >/dev/null; then + for m in "${deployment}"/usr/lib/frzr.d/*.unlock; do + unset frzr_migration_version + + unset -f post_unlock + + # source the migration + . $m + + # only execute unlock scripts marked for newer frzr versions + if [ -z "${error}" ] && [ ! -z "${frzr_migration_version}" ] && [ $frzr_migration_version -gt 0 ]; then + if [ "$(type -t post_unlock)" == function ] ; then + + # Run migration and check for errors + local unlock_script_result=$(post_unlock "${frzr_root}" "${deployment}" "${version}") + if echo "${unlock_script_result}" | grep -Fq "ERROR"; then + error="${unlock_script_result}" + fi + else + echo "$m has no post_unlock function" + fi + fi + + unset -f post_unlock + + unset frzr_migration_version + done + fi + + if [ -z "${error}" ]; then + echo "OK" + else + echo "${error}" + fi +} + +# Check if there are removal scripts available in the image to be removed and run them one by one +# Note: removal scripts are functions named pre_removal inside (executable) files with .removal extension +# stored inside $2/usr/lib/frzr.d/ +# +# Every pre_removal function will be run in a subshell. +# +# PRE=$2/usr/lib/frzr.d is a directory +# POST= +# +# $1 the deployment version; this is also the deployment name (the name of the subvolume to be used as rootfs) +# $2 the deployment subvolume +# $3 frzr_root the mounted path to the main btrfs subvolume (the one that contains home as a subvolume) +# $4 frzr version string +# stdout "OK" on success, the error otherwise +execute_removal() { + local version=$1 + local deployment=$2 + local frzr_root=$3 + local frzr_version=$4 + + local error="" + + if compgen -G "${deployment}"/usr/lib/frzr.d/*.removal >/dev/null; then + for m in "${deployment}"/usr/lib/frzr.d/*.removal; do + unset frzr_migration_version + + unset -f pre_removal + + # source the migration + . $m + + # only execute unlock scripts marked for newer frzr versions + if [ -z "${error}" ] && [ ! -z "${frzr_migration_version}" ] && [ $frzr_migration_version -gt 0 ]; then + if [ "$(type -t pre_removal)" == function ] ; then + echo "Running $m" + local unlock_script_result=$(pre_removal "${frzr_root}" "${deployment}" "${version}" "${frzr_version}") + if echo "${unlock_script_result}" | grep -Fq "ERROR"; then + error="${unlock_script_result}" + fi + else + echo "$m has no pre_removal function" + fi + fi + + unset -f pre_removal + + unset frzr_migration_version + done + fi + + if [ -z "${error}" ]; then + echo "OK" + else + echo "${error}" + fi +} + +# Write the systemd-boot entry needed to boot the specified deployment +# Note: this function can ignore amd-ucode and intel-ucode if those are not found since +# either dracut or mkinitcpio will place those in the initramfs and including +# them on systemd-boot is now being deprecated. +# $1 the deployment version +# $2 the config file name +# $3 the boot entry (visible) name +# $4 /efi mount path, the vfat partition (mind not inserting a final /) +# $5 amd_ucode.img path relative to $2/$3/ (mind not inserting a final /) +# $6 intel_ucode.img path relative to $2/$3/ (mind not inserting a final /) +# $7 vmlinuz-linux path relative to $2/$3/ (mind not inserting a final /) +# $8 initramfs-linux.img path relative to $2/$3/ (mind not inserting a final /) +# $9 additional arguments to be used in the kernel cmdline +# $10 the UUID of the btrfs rootfs partition containing the deployment to boot +# $11 the btrfs subvolume ID of the deployment to boot +# stdout the configuration to be written to a systemd-boot entry to make the given deployment version bootable +# return "OK" for success, an error string otherwise +generate_systemd_boot_cfg() { + local version=${1} + local entry_conf_filename=${2} + local entry_name=${3} + local efi_mount_path=${4} + local amd_ucode=${5} + local intel_ucode=${6} + local vmlinuz=${7} + local initramfs=${8} + local additional_arguments=${9} + local btrfs_rootfs_uuid=${10} + local btrfs_rootfs_subvolid=${11} + + local entry_conf="${efi_mount_path}/loader/entries/${entry_conf_filename}" + + # search for the actual kernel to boot + if [ -f "${efi_mount_path}/${version}/${vmlinuz}" ]; then + # search fot the initramfs + if [ -f "${efi_mount_path}/${version}/${initramfs}" ]; then + # write title chimeraos-46_fcc653a3 (also creating the boot entry file if it doesn't exists yet) + echo "title ${entry_name}" > "${entry_conf}" + + # write linux /chimeraos-46_fcc653a3/vmlinuz-linux + if [ -z "${version}" ]; then + echo "linux /${vmlinuz}" >> "${entry_conf}" + else + echo "linux /${version}/${vmlinuz}" >> "${entry_conf}" + fi + + # write /chimeraos-46_fcc653a3/amd-ucode.img if necessary + if [ -f "${efi_mount_path}/${version}/${amd_ucode}" ]; then + if [ -z "${version}" ]; then + echo "initrd /${amd_ucode}" >> "${entry_conf}" + else + echo "initrd /${version}/${amd_ucode}" >> "${entry_conf}" + fi + else + echo "# amd-ucode.img omitted" >> "${entry_conf}" + fi + + # write /chimeraos-46_fcc653a3/intel-ucode.img if necessary + if [ -f "${efi_mount_path}/${version}/${intel_ucode}" ]; then + if [ -z "${version}" ]; then + echo "initrd /${intel_ucode}" >> "${entry_conf}" + else + echo "initrd /${version}/${intel_ucode}" >> "${entry_conf}" + fi + else + echo "# intel-ucode.img omitted" >> "${entry_conf}" + fi + + # write /chimeraos-46_fcc653a3/initrams-linuz.img + if [ -z "${version}" ]; then + echo "initrd /${initramfs}" >> "${entry_conf}" + else + echo "initrd /${version}/${initramfs}" >> "${entry_conf}" + fi + + local options="" + if [ ! -z "${btrfs_rootfs_uuid}" ]; then + options="${options} root=UUID=${btrfs_rootfs_uuid} rw" + else + options="${options} root=gpt-auto rw" + fi + + if [ ! -z "${btrfs_rootfs_subvolid}" ]; then + options="${options} rootflags=subvolid=${btrfs_rootfs_subvolid},noatime" + else + options="${options}" + fi + + # write kernel cmdline + echo "options ${options} quiet splash loglevel=3 rd.systemd.show_status=auto rd.udev.log_priority=3 ${additional_arguments}" >> "${entry_conf}" + + # return success + echo "OK" + else + # return the error to be printed out + echo "ERROR: initramfs not found (searched at: '$efi_mount_path/$version/$initramfs')" + fi + else + # return the error to be printed out + echo "ERROR: kernel not found (searched at: '$mount_path/$version/$vmlinuz')" + fi +} + +# Write the systemd-boot entry needed to make the specified deployment bootable. +# Note: this function can ignore amd-ucode and intel-ucode if those are not found since mkinitcpio will place those +# in the initramfs and including them on systemd-boot is now deprecated. +# PRE=the deployment image is available to be read from +# POST=the installed bootloader will automatically start the new deployment at boot +# $1 the deployment version; this is also the deployment name (the name of the subvolume to be used as rootfs) +# $2 /efi mount path +# $3 the path to the deployment to be booted +# $4 additional arguments to be used in the kernel cmdline +# $5 the UUID of the btrfs rootfs partition containing the deployment to boot +# $6 the btrfs subvolume ID of the deployment to boot +# stdout "OK" for success, and error string otherwise +prepare_efi_partition() { + local version=${1} + local efi_mount_path=${2} + local boot_dir=${3} + local additional_arguments=${4} + local btrfs_rootfs_uuid=${5} + local btrfs_rootfs_subvolid=${6} + + local default_config_entry="" + + # create /loader/entries in the EFI system partition (if it does not exists already) + mkdir -p ${efi_mount_path}/loader/entries + + # this will hold the default .conf file + local default_boot_cfg="" + + # Clear out old initramfs for the current deployment + if [ -d "${efi_mount_path}/${version}" ]; then + rm -rf "${efi_mount_path}/${version}" + fi + + if [ -d "${efi_mount_path}/loader/entries" ]; then + # Clear out old boot configuration + for config_file_path in "${efi_mount_path}/loader/entries"/frzr-*; do + if echo "${config_file_path}" | grep -Fq "${version}"; then + rm -f "${config_file_path}" + fi + done + + # Iterate over each pair of files + for vmlinuz_file_path in "${boot_dir}"/vmlinuz-*; do + # Extract the matching part of the filename and remove the .img extension + kernel_version=$(basename "${vmlinuz_file_path}" | sed 's/^vmlinuz-//') + + # Check if the file really is a linux kernel + if ! file -bL "${vmlinuz_file_path}" | grep -Fq "Linux kernel"; then + continue + fi + + # $initramfs_file is the initramfs name of the file in /boot: find the corresponding vmlinuz file + initramfs_file="initramfs-${kernel_version}.img" + + # $vmlinuz_file is the kernel name of the file in /boot: find the corresponding initramfs file + vmlinuz_file="vmlinuz-${kernel_version}" + + # Check if the corresponding vmlinuz file exists + if [ -f "${boot_dir}/${vmlinuz_file}" ] && [ -f "${boot_dir}/${initramfs_file}" ]; then + # each deployment will need its own kernel(s) as well as initramfs to boot: place them in a EFI subdirectory + mkdir -p "${efi_mount_path}/${version}" + + # If the initramfs contains the AMD microcode spare space in /efi and do not copy it + if cat "${boot_dir}/${initramfs_file}" | cpio -itv 2>/dev/null | grep -Fq "AuthenticAMD.bin"; then + local amd_ucode="amd-ucode.img_excluded" + else + local amd_ucode="amd-ucode.img" + fi + + if [ -e "${boot_dir}/${amd_ucode}" ]; then + cp "${boot_dir}/${amd_ucode}" "${efi_mount_path}/${version}/${amd_ucode}" + fi + + # If the initramfs contains the Intel microcode spare space in /efi and do not copy it + if cat "${boot_dir}/${initramfs_file}" | cpio -itv 2>/dev/null | grep -Fq "GenuineIntel.bin"; then + local intel_ucode="intel-ucode.img_excluded" + else + local intel_ucode="intel-ucode.img" + fi + + if [ -e "${boot_dir}/${intel_ucode}" ]; then + cp "${boot_dir}/${intel_ucode}" "${efi_mount_path}/${version}/${intel_ucode}" + fi + + if cp "${boot_dir}/${vmlinuz_file}" "${efi_mount_path}/${version}/${vmlinuz_file}"; then + if cp "${boot_dir}/${initramfs_file}" "${efi_mount_path}/${version}/${initramfs_file}"; then + local boot_entry="frzr-${version}-${kernel_version}.conf" + + # Write the configuration entry + local systemd_boot_update_result=$(generate_systemd_boot_cfg "${version}" "${boot_entry}" "${version} (${kernel_version})" "${efi_mount_path}" "${amd_ucode}" "${intel_ucode}" "${vmlinuz_file}" "${initramfs_file}" "${additional_arguments}" "${btrfs_rootfs_uuid}" "${btrfs_rootfs_subvolid}") + if echo "${systemd_boot_update_result}" | grep -q 'ERROR'; then + echo "ERROR: Could not add bootloader entry: ${systemd_boot_update_result}" + else + default_boot_cfg="${boot_entry}" + fi + else + echo "ERROR: Could not copy '${boot_dir}/${initramfs_file}' to '${efi_mount_path}/${version}/${initramfs_file}'" + fi + else + echo "ERROR: Could not copy '${boot_dir}/${vmlinuz_file}' to '${efi_mount_path}/${version}/${vmlinuz_file}'" + fi + fi + done + + # override the default deployment boot with the user-provided one + if [ -f "${efi_mount_path}/loader/entries/frzr_kernel.conf" ]; then + default_boot_cfg="frzr_kernel.conf" + fi + + # write the default boot entry + if [ -z "$default_boot_cfg" ]; then + echo "ERROR: no bootable kernel found" + else + echo "default ${default_boot_cfg}" > "${efi_mount_path}/loader/loader.conf" + + # If frzr-kernel has been used this is a development machine: add a boot timeout so that different kernels can be used + if [ "${default_boot_cfg}" = "frzr_kernel.conf" ]; then + echo "timeout 3" >> "${efi_mount_path}/loader/loader.conf" + fi + + echo "OK" + fi + fi +} diff --git a/__frzr-bootloader b/__frzr-bootloader new file mode 100644 index 0000000..b6b0814 --- /dev/null +++ b/__frzr-bootloader @@ -0,0 +1,259 @@ +#! /bin/bash + +# import methods +source "${BASH_SOURCE%/*}/__frzr" "$@" + +frzr_bootloader() { + NAME=${1} + + # by default the deployment is the running one + DEPLOY_PATH="/" + SUBVOL="/" + + local RUNNING=true + RUNNING=true + STATE="BEGIN" + while $RUNNING; do + case "$STATE" in + "BEGIN") + # If the image is being built skip the bootloader generation + if [ ! -z "${FRZR_IMAGE_GENERATION}" ]; then + STATE="SUCCESS" + continue + fi + + TASK_MSG="Checking for root privileges" + send_data + if [ $EUID -ne 0 ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="$(basename $0) not run as root" + STATE="FAIL" + send_data + continue + fi + + MOUNT_PATH="/frzr_root" + # Make sure the frzr_root is mounted during the deployment procedure + # this code is based on the fact that when a btrfs filesystem is created + # the default subvolid that is created contextually has the ID set to 256 + # also as a matter of fact in btrfs is impossible to change subvolumes IDs + if mount | grep -Fq "${MOUNT_PATH}"; then + local MOUNTED_MOUNT_PATH="no" + else + MOUNT_PATH="/tmp/frzr_root" + TASK_MSG="Preparing '${MOUNT_PATH}' to be used as the main subvolume mount path" + send_data + mkdir -p "${MOUNT_PATH}" + if mount -L frzr_root -t btrfs -o subvolid=5,rw "${MOUNT_PATH}"; then + local MOUNTED_MOUNT_PATH="yes" + else + local MOUNTED_MOUNT_PATH="no" + TASK_ERROR=1 + TASK_ERROR_MSG="Could not bind ${MOUNT_PATH} to frzr_root main subvolume" + STATE="FAIL" + send_data + continue + fi + sleep 5 + fi + + # Make sure the EFI partition is mounted during the deployment procedure + EFI_MOUNT_PATH="${MOUNT_PATH}/efi" + + if mount | grep -Fq "${EFI_MOUNT_PATH}"; then + local MOUNTED_EFI_MOUNT_PATH="no" + else + mkdir -p "${EFI_MOUNT_PATH}" + if mount -L frzr_efi -o uid=0,gid=0,fmask=0077,dmask=0077 "${EFI_MOUNT_PATH}"; then + local MOUNTED_EFI_MOUNT_PATH="yes" + else + local MOUNTED_EFI_MOUNT_PATH="no" + TASK_ERROR=1 + TASK_ERROR_MSG="Could not bind ${EFI_MOUNT_PATH} to frzr_efi (boot) partition" + STATE="FAIL" + send_data + continue + fi + sleep 5 + fi + + STATE="CHECK" + ;; + + "CHECK") + if [ -z "$NAME" ]; then + if [ -e "/build_info" ]; then + NAME=$(cat "/build_info" | head -1) + fi + + if [ -z "$NAME" ]; then + TASK_ERROR_MSG="Could not fetch deployment name" + STATE="FAIL" + send_data + continue + fi + else + DEPLOY_PATH="${MOUNT_PATH}/deployments" + SUBVOL="${DEPLOY_PATH}/${NAME}" + + # Make sure DEPLOY_PATH exists + mkdir -p "${DEPLOY_PATH}" + if [ ! -d "${DEPLOY_PATH}" ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not create ${DEPLOY_PATH} to to store deployments" + STATE="FAIL" + send_data + continue + fi + + # Make sure SUBVOL exists + if [ ! -d "${SUBVOL}" ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not find deployment '${NAME}', '${SUBVOL}' was searched" + STATE="FAIL" + send_data + continue + fi + fi + + STATE="BOOTLOADER" + ;; + + "BOOTLOADER") + + # Check if a (supported) bootloader is present and if not install it + + # Install systemd-boot as the bootloader + if [ ! -f "${EFI_MOUNT_PATH}/EFI/systemd/systemd-bootx64.efi" ]; then + TASK_MSG="Installing systemd-boot to '${EFI_MOUNT_PATH}' as the bootloader" + if ! bootctl --esp-path="${EFI_MOUNT_PATH}/" install; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not install systemd-boot to '${EFI_MOUNT_PATH}'" + STATE="FAIL" + send_data + continue + fi + fi + + STATE="CONFIGURE" + ;; + + "CONFIGURE") + # Get the rootfs UUID: this will be used in generating the systemd-boot entry + TASK_MSG="Finding deployment partition UUID" + local rootfs_uuid=$(get_uuid "${SUBVOL}") + if echo "$rootfs_uuid" | grep -Fq "ERROR"; then + local rootfs_uuid="" + TASK_WARNING=1 + TASK_WARNING_MSG="Could not find rootfs UUID of the new deployment -- gpt-auto will be used" + send_data + fi + + # Get the subvolid: this will be used in generating the systemd-boot entry and setting is as the default for the automatic boot + TASK_MSG="Finding deployment subvolume ID" + local rootfs_subvolid=$(btrfs_subvol_get_id "${SUBVOL}") + if echo "$subvolid" | grep -Fq "ERROR"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not find subvolume ID of the new deployment" + STATE="FAIL" + send_data + continue + elif [ "${rootfs_subvolid}" = "5" ]; then + TASK_ERROR_MSG="Could not identify the correct subvolid of the running deployment" + STATE="FAIL" + send_data + continue + fi + + # Read additional boot arguments (to be added to the kernel cmdline) from the deployed image + local deployment_arguments="" + if [ -f "${SUBVOL}/usr/lib/frzr.d/bootconfig.conf" ]; then + local bootconf_args=$(cat "${SUBVOL}/usr/lib/frzr.d/bootconfig.conf") + deployment_arguments="$deployment_arguments ${bootconf_args}" + else + TASK_WARNING=1 + TASK_WARNING_MSG="Could not read '${SUBVOL}/usr/lib/frzr.d/bootconfig.conf': default kernel cmdline will be used" + send_data + fi + + # Read additional boot arguments (to be added to the kernel cmdline) from the user file + local additional_arguments="$deployment_arguments" + if [ -f "${EFI_MOUNT_PATH}/frzr_bootconfig.conf" ]; then + local user_bootconf_args=$(cat "${EFI_MOUNT_PATH}/frzr_bootconfig.conf") + additional_arguments="$additional_arguments ${user_bootconf_args}" + else + TASK_WARNING=1 + TASK_WARNING_MSG="Could not read '${EFI_MOUNT_PATH}/frzr_bootconfig.conf': deployment cmdline will be used" + send_data + fi + + # Make sure the deployment has a /boot directory + if [ ! -d "${SUBVOL}/boot" ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not find the deployment /boot directory: '${SUBVOL}/boot' not found" + STATE="FAIL" + send_data + continue + fi + + # This is used to update the EFI partition: setting up systemd-boot (or whatever else bootlader might be supported) to boot the new deployment + local efi_update_result=$(prepare_efi_partition "${NAME}" "${EFI_MOUNT_PATH}" "${SUBVOL}/boot" "${additional_arguments}" "${rootfs_uuid}" "${rootfs_subvolid}") + if echo "${efi_update_result}" | grep -q 'ERROR'; then + # bootloader configuration could not be updated + TASK_ERROR=1 + TASK_ERROR_MSG="Could not update the EFI partition: ${efi_update_result}" + STATE="FAIL" + send_data + continue + fi + + STATE="SUCCESS" + ;; + + "SUCCESS") + # This state should only be used if the unlock completed without errors + #TASK_STATE="SUCCESS" + + if [ ! -z "${FRZR_IMAGE_GENERATION}" ]; then + echo "bootloader configuration update skipped" + else + echo "bootloader configuration update success" + fi + + RUNNING=false + ;; + "FAIL") + # This state should only be used if the unlock failed + + #TASK_STATE="FAIL" + + echo "ERROR: frzr-bootloader failed: ${TASK_ERROR_MSG}" + TASK_ERROR=1 + send_data + RUNNING=false + ;; + *) + TASK_STATE="UNKNOWN_ERROR" + + echo "ERROR: Something went terribly wrong in $(basename $0)" + TASK_ERROR=1 + send_data + RUNNING=false + ;; + esac + done + + # umount the efi path + if [ "${MOUNTED_EFI_MOUNT_PATH}" = "yes" ]; then + if mountpoint -q "${EFI_MOUNT_PATH}"; then + umount -l "${EFI_MOUNT_PATH}" + fi + fi + + # umount the frzr_root subvolume (if it was mounted by this tool and not externally) + if [ "${MOUNTED_MOUNT_PATH}" = "yes" ]; then + if mountpoint -q "${MOUNT_PATH}"; then + umount -l "${MOUNT_PATH}" + fi + fi +} diff --git a/__frzr-bootstrap b/__frzr-bootstrap new file mode 100644 index 0000000..0d7b0c6 --- /dev/null +++ b/__frzr-bootstrap @@ -0,0 +1,351 @@ +#! /bin/bash + +set -e + +# import methods +source "${BASH_SOURCE%/*}/__frzr" "$@" + +# Perform the repair install +# $1 target (physical) disk +# $2 frzr_root main subvolume mount path (the one that contains the home subvolume) +repair_install() { + local disk=${1} + local frzr_root=${2} + + # 1st partition is always EFI + local boot_efi=$(fdisk -o Device --list ${disk} | grep "^${disk}.*1$") + + # start frzr_root search from partition 2 (1st partition is always EFI) + local frzr_root_part_number=2 + local install_mount=$(fdisk -o Device --list ${disk} | grep "^${disk}.*${frzr_root_part_number}$") + local install_mount_label=$(blkid -s LABEL -o value "${install_mount}") + + while [ $frzr_root_part_number -le 10 ] && [[ $install_mount_label != "frzr_root" ]] + do + ((frzr_root_part_number++)) + install_mount=$(fdisk -o Device --list ${disk} | grep "^${disk}.*${frzr_root_part_number}$") + install_mount_label=$(blkid -s LABEL -o value "${install_mount}") + done + + if [[ $install_mount_label != "frzr_root" ]]; then + # make sure the directory to be used as the frzr_root mountpoint does exists + mkdir -p "${frzr_root}" + + # mount the frzr_root partition + mount -t btrfs -o subvolid=5,rw "${install_mount}" "${frzr_root}" + + # set the default subvol back to 5 as the fresh installation did + btrfs subvolume set-default 5 "${frzr_root}" + + # mkdir required directories + mkdir -p "${frzr_root}/boot" + mkdir -p "${frzr_root}/efi" + + # clear out the etc overlay upperdir + if [ -d "${frzr_root}/etc/" ]; then + rm -rf ${frzr_root}/etc/* + else + mkdir -p "${frzr_root}/etc/" + fi + + # clear out the etc overlay workdir + if [ -d "${frzr_root}/.etc/" ]; then + rm -rf "${frzr_root}"/.etc/* + else + mkdir -p "${frzr_root}/.etc/" + fi + + # clear out the /var subvolume + if [ ! -d "${frzr_root}/var" ]; then + btrfs subvolume create ${frzr_root}/var + elif is_btrfs_subvolume "${frzr_root}/var"; then + rm -rf "${frzr_root}"/var/* + fi + + # mount the EFI partition + mount -t vfat "${boot_efi}" "${frzr_root}/efi/" + + # remove everything from the EFI partition + rm -rf "${frzr_root}"/efi/* + + # Delete every installed system + if [ -d "${frzr_root}"/deployments ]; then + echo "deleting subvolume(s)..." + btrfs subvolume delete ${frzr_root}/deployments/* || true + fi + + # TODO: if a minimal/recovery system has to installed in the main partition here is the place to do just that. + + echo "OK" + else + echo "ERROR: frzr_root not found" + fi +} + +# Perform the fresh install +# $1 target (physical) disk +# $2 frzr_root main subvolume mount path (the one that contains the home subvolume) +fresh_install() { + local disk=${1} + local frzr_root=${2} + + mkdir -p ${frzr_root} + + if [ -z "${SWAP_GIB:-}" ]; then + declare -i SWAP_GIB=2 # A 2GiB swap will prevent some games crashing on 16GB handhelds + fi + + if [ -z "${ROOT_GIB:-}" ]; then + declare -i ROOT_GIB=0 + fi + + # $SEPARATE_HOME_FS can be either btrfs or ext4 + if [ -z "${SEPARATE_HOME_FS:-}" ]; then + SEPARATE_HOME_FS="ext4" + fi + + if [[ $SWAP_GIB -eq 0 ]]; then + SWAP_PART_NUMBER=0 + ROOT_PART_NUMBER=2 + if [[ $ROOT_GIB -eq 0 ]]; then + HOME_PART_NUMBER=0 + else + HOME_PART_NUMBER=3 + fi + else + SWAP_PART_NUMBER=2 + ROOT_PART_NUMBER=3 + if [[ $ROOT_GIB -eq 0 ]]; then + HOME_PART_NUMBER=0 + else + HOME_PART_NUMBER=4 + fi + fi + + declare -i SWAP_START=1 + declare -i SWAP_END=$((SWAP_GIB + SWAP_START)) + + declare -i ROOT_START=$((SWAP_END)) + declare -i ROOT_END=$((ROOT_START + ROOT_GIB)) + + declare -i HOME_START=$((ROOT_END)) + + # create the GPT partition table and set the correct GPT type for each partition + parted --script ${disk} \ + mklabel gpt + + # create the FAT32 EFI boot partition + parted --script ${disk} \ + mkpart primary fat32 1MiB 1GiB \ + type 1 "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" \ + set 1 esp on + + if [[ $SWAP_PART_NUMBER -ne 0 ]]; then + parted --script ${disk} \ + mkpart primary linux-swap ${SWAP_START}GiB ${SWAP_END}GiB \ + type $SWAP_PART_NUMBER "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f" + fi + + if [[ $HOME_PART_NUMBER -ne 0 ]]; then + # split / and /home into two partitions + + parted --script ${disk} \ + mkpart primary btrfs ${ROOT_START}GiB ${ROOT_END}GiB \ + type $ROOT_PART_NUMBER "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709" + + parted --script ${disk} \ + mkpart primary $SEPARATE_HOME_FS ${HOME_START}GiB 100% \ + type $HOME_PART_NUMBER "933ac7e1-2eb4-4f13-b844-0e14e2aef915" \ + set $HOME_PART_NUMBER linux-home on + else + # /home will be a subvolume of / (default) + parted --script ${disk} \ + mkpart primary btrfs ${ROOT_START}GiB 100% \ + type $ROOT_PART_NUMBER "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709" + fi + + # this creates the subvolid 5 + local root_part=$(fdisk -o Device --list ${disk} | grep "^${disk}.*${ROOT_PART_NUMBER}$") + mkfs.btrfs -L frzr_root -f ${root_part} + local root_uuid=$(blkid -s UUID -o value "${root_part}") + mount ${root_part} ${frzr_root} # mount rootfs + mkdir -p ${frzr_root}/efi # prepare to mount vfat + #echo "${root_uuid}" > "${frzr_root}/root_uuid" + btrfs subvolume create ${frzr_root}/var + mkdir -p "${frzr_root}/etc" + mkdir -p "${frzr_root}/boot" + mkdir -p "${frzr_root}/efi" + mkdir -p "${frzr_root}/.etc" + + # setup boot partition (the bootloader will be installed by frzr-bootloader utility) + local efi_part=$(fdisk -o Device --list ${disk} | grep "^${disk}.*1$") + mkfs.vfat "${efi_part}" + dosfslabel "${efi_part}" frzr_efi + mount -t vfat "${efi_part}" "${frzr_root}/efi/" + + # setup the swap partition + if [[ $SWAP_PART_NUMBER -ne 0 ]]; then + local swap_part=$(fdisk -o Device --list ${disk} | grep "^${disk}.*${SWAP_PART_NUMBER}$") + mkswap "${swap_part}" + local swap_uuid=$(blkid -s UUID -o value "${swap_part}") + local swap_partuuid=$(blkid -s PARTUUID -o value "${swap_part}") + if [ ! -z "$swap_uuid" ]; then + echo "resume=PARTUUID=${swap_partuuid}" > "${frzr_root}/efi//frzr_bootconfig.conf" + fi + #echo "${swap_uuid}" > "${frzr_root}/swap_uuid" + swapon "${swap_part}" # activate swap for the installer + fi + + # create /home subvolume + btrfs subvolume create "${frzr_root}/home" + + # set the /home as no copy-on-write + chattr +C "${frzr_root}/home" + + # setup home partition + if [[ $HOME_PART_NUMBER -ne 0 ]]; then + # Delete the /home subvolume so that it can't be mounted (subvolume ids are never reused by btrfs) + btrfs subvolume delete "${frzr_root}/home" + + local home_part=$(fdisk -o Device --list ${disk} | grep "^${disk}.*${HOME_PART_NUMBER}$") + + if [[ SEPARATE_HOME_FS == "ext4" ]]; then + mkfs.ext4 -L frzr_home -f "${home_part}" + else + mkfs.btrfs -L frzr_home -f "${home_part}" + fi + + mkdir -p "${frzr_root}/home" + mount "${home_part}" "${frzr_root}/home" + + # set the /home as no copy-on-write if that is a btrfs filesystem + if [[ SEPARATE_HOME_FS == "btrfs" ]]; then + chattr +C "${frzr_root}/home" + fi + + local home_uuid=$(blkid -s UUID -o value "${home_part}") + echo "${home_uuid}" >${frzr_root}/home_uuid + fi + + # create folder for the gamer user + mkdir -p "${frzr_root}/home/${USERNAME}" + chown 1000:1000 "${frzr_root}/home/${USERNAME}" + + # TODO: if a minimal/recovery system has to installed in the main partition here is the place to do just that. +} + +frzr_bootstrap() { + RUNNING=true + + # Check for the GUI installer + if [ -n "$1" ] && [ -n "$2" ]; then + STATE="FORMAT" + MOUNT_PATH=/tmp/frzr_root + else + STATE="FRZR_BOOTSTRAP_CHECK" + fi + + TASK_TRACKER=0 + TASK_MSG="Bootstrapping the drive for the FRZR system" + send_data + while $RUNNING; do + case "$STATE" in + "FRZR_BOOTSTRAP_CHECK") + + TASK_MSG="Checking for root privileges" + send_data + if [ $EUID -ne 0 ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="$(basename $0) not run as root" + STATE="FAIL" + send_data + continue + fi + + echo "Checking for drives connected to the system" + MOUNT_PATH=/tmp/frzr_root + device_list=() + device_output=$(lsblk --list -n -o name,model,size,type | grep disk | tr -s ' ' '\t') + while read -r line; do + name=/dev/$(echo "$line" | cut -f 1) + model=$(echo "$line" | cut -f 2) + size=$(echo "$line" | cut -f 3) + device_list+=($name) + device_list+=("$model ($size)") + done <<<"$device_output" + DISK=$(whiptail --nocancel --menu "Choose a disk to install to:" 20 50 5 "${device_list[@]}" 3>&1 1>&2 2>&3) + echo "Checking for existing FRZR deployments" + # Checking for existing installation + + if (lsblk -o label ${DISK} | grep -q frzr_efi); then + echo "Existing installation found" + + if (whiptail --yesno --yes-button "Repair" --no-button "Clean" "WARNING: $DISK appears to already have a system installed. Would you like to repair it or do a clean install?\n\nNOTE: A clean install will delete everything on the disk, but a repair install will preserve your user data." 13 70); then + echo "User chose to do a repair install" + REPAIR_INSTALL=1 + STATE="REPAIR" + else + echo "User chose to do a clean install" + STATE="FORMAT" + fi + else + echo "Existing installation not found" + STATE="FORMAT" + fi + ;; + "FORMAT") + echo "We are doing a fresh install" + USERNAME=user + + if [ ! -z $1 ]; then + USERNAME=$1 + fi + + if [ ! -z $2 ]; then + DISK=$2 + fi + + fresh_install "${DISK}" "${MOUNT_PATH}" + RESULT=$? + if [ $RESULT -eq 0 ]; then + STATE="SUCCESS" + else + STATE="FAIL" + TASK_ERROR_MSG="Fresh installation has failed" + continue + fi + ;; + "REPAIR") + echo "We are doing a repair install" + local repair_install_result=$(repair_install "${DISK}" "${MOUNT_PATH}") + if echo "$repair_install_result" | grep -Fq "OK"; then + STATE="SUCCESS" + else + STATE="FAIL" + TASK_ERROR_MSG="Repair installation has failed" + continue + fi + ;; + "SUCCESS") + echo "Successfully prepared the drive for an FRZR installation" + TASK_MSG="Successfully prepared the drive for an FRZR installation" + TASK_TRACKER=1 + send_data + RUNNING=false + ;; + "FAIL") + echo "The bootstrap failed" + TASK_ERROR=1 + send_data + RUNNING=false + ;; + *) + echo "Something went terribly wrong" + TASK_ERROR_MSG="Bootstrap hit an unexpected state" + TASK_ERROR=1 + send_data + RUNNING=false + ;; + esac + done +} diff --git a/__frzr-deploy b/__frzr-deploy index 3ccc5f3..5b59713 100644 --- a/__frzr-deploy +++ b/__frzr-deploy @@ -1,506 +1,616 @@ #! /bin/bash -set -e set -o pipefail -get_img_url() { - CHANNEL=$1 - - # Order by creation date in reverse - result=$(jq 'sort_by(.created_at) | reverse') - - # Remove entries which have not been completed uploading - result=$(echo "${result}" | jq 'del(.[] | select(.assets[].state != "uploaded"))') - - # Always check for stable date - if stable_release_date=$(echo "${result}" | jq -er '[ .[] | - select(.prerelease==false) ] | - first | - .created_at' - ); then - # Check for stable url, this is the latest that have prerelease == false - stable_download_url=$(echo "${result}" | jq -r '[ .[] | - select(.prerelease==false) ] | - first | - .assets[] | - select(.browser_download_url | test("img")) | - .browser_download_url' - ) - else - # No stable channel found, pick some (old) values - # For testing/ channel selection - stable_release_date="1970-01-01T00:00:00Z" - stable_download_url="" - fi - - - # Filter channels by release naming conventions - if [[ "$CHANNEL" =~ ^[0-9]+\-?[0-9]*$ ]] ; then - # Check first for explicit version numbers between stable releases - # Useful for downgrading - result=$(echo "${result}" | jq -r "[ .[] | - select(.prerelease==false) | - select(.name|test(\" ${CHANNEL}\$\")) ] | - first | - .assets[] | - select(.browser_download_url | test(\"img\")) | - .browser_download_url" - ) - elif [ "$CHANNEL" == "stable" ]; then - result=$stable_download_url - elif [ "$CHANNEL" == "testing" ]; then - # Testing channel have prerelease = true and no other tags - if testing_release_date=$(echo "${result}" | jq -er '[ .[] | - select(.prerelease==true) | - select(.name|test("\\[.*\\]")|not) ] | - first | - .created_at' - ); then - testing_url=$(echo "${result}" | jq -r '[ .[] | - select(.prerelease==true) | - select(.name|test("\\[.*\\]")|not) ] | - first | - .assets[] | - select(.browser_download_url | test("img")) | - .browser_download_url' - ) - if [ $(date -d $testing_release_date +%s) -le $(date -d $stable_release_date +%s) ]; then - result=$stable_download_url - else - result=$testing_url - fi - else - result=$stable_download_url - fi - else - # Match any release with CHANNEL as a tag (including unstable) - result=$(echo ${result} | jq "[ .[] | select(.prerelease==true) | select(.name|test(\"\\\[${CHANNEL}\\\]\" ; \"i\")) ]") - if unstable_release_date=$(echo "${result}" | jq -er "[ .[] | - select(.prerelease==true) | - select(.name|test(\"\\\[${CHANNEL}\\\]\" ; \"i\")) ] | - first | - .created_at" - ); then - unstable_url=$(echo "${result}" | jq -r "[ .[] | - select(.prerelease==true) | - select(.name|test(\"\\\[${CHANNEL}\\\]\" ; \"i\")) ] | - first | - .assets[] | - select(.browser_download_url | test(\"img\")) | - .browser_download_url" - ) - if [ $(date -d $unstable_release_date +%s) -le $(date -d $stable_release_date +%s) ]; then - result=$stable_download_url - else - result=$unstable_url - fi - else - result=$stable_download_url - fi - fi - - echo $result -} - -get_boot_cfg() { - local version=${1} - local amd_ucode=${2} - local intel_ucode=${3} - local additional_arguments=${4} - -echo "title ${version} -linux /${version}/vmlinuz-linux -${amd_ucode} -${intel_ucode} -initrd /${version}/initramfs-linux.img -options root=LABEL=frzr_root rw rootflags=subvol=deployments/${version} quiet splash loglevel=3 rd.systemd.show_status=auto rd.udev.log_priority=3 ${additional_arguments}" - -} - -get_deployment_slot() { - local current_version=${1} - local slot_A_path=${2} - local slot_B_path=${3} - if [ -f "${slot_A_path}" ] && grep "^title" "${slot_A_path}" > /dev/null; then - SLOT_A=`grep ^title ${slot_A_path} | sed 's/title //'` - fi - if [ -f "${slot_B_path}" ] && grep "^title" "${slot_B_path}" > /dev/null; then - SLOT_B=`grep ^title ${slot_B_path} | sed 's/title //'` - fi - if [ "$SLOT_A" == "$current_version" ] ; then - echo "slot-A" - elif [ "$SLOT_B" == "$current_version" ] ; then - echo "slot-B" - fi -} - -get_deployment_to_delete() { - local current_version=${1} - local boot_cfg_path=${2} - local deployment_path=${3} - - local TO_BOOT=`get_next_boot_deployment ${current_version} ${boot_cfg_path}` - - ls -1 ${deployment_path} | grep -v ${current_version} | grep -v ${TO_BOOT} | head -1 || echo -} - -get_next_boot_deployment() { - local current_version=${1} - local boot_cfg_path=${2} - - local TO_BOOT='this-is-not-a-valid-version-string' - if [ -f "${boot_cfg_path}" ] && grep "^title" "${boot_cfg_path}" > /dev/null; then - TO_BOOT=`grep ^title ${boot_cfg_path} | sed 's/title //'` - fi - - echo ${TO_BOOT} -} - -clean_progress() { - local scale=$1 - local postfix=$2 - local last_value=$scale - while IFS= read -r line; do - value=$(( ${line}*${scale}/100 )) - if [ "$last_value" != "$value" ]; then - echo ${value}${postfix} - last_value=$value - fi - done -} - +# import methods +#source "${BASH_SOURCE%/*}/__frzr" "$@" -main() { - if [ $EUID -ne 0 ]; then - echo "$(basename $0) must be run as root" - exit 1 - fi +frzr_deploy() { + #TODO Utilize the state machine to manage the individual proccesses to send signals to external monitors + ## Signals + #TASK_STATE="" # The current state of the state machine + #TASK_MSG="" # TASK_MSG will be read by external tools. EX TASK_MSG="Preparing user directory" + #TASK_TRACKER=0 # TASK_TRACKER will be used to signal back that the current TASK_MSG in queue is handled or not 0 = Pending 1 = Completed + #TASK_ERROR=0 # Signal to let listeners know that the task had an error. + #TASK_ERROR_MSG="" # Error message. EX: "Unexpected I/O errors found during write" + #TASK_WARNING=0 # Signal to let listeners know there is a warning + #TASK_WARNING_MSG="" # Warning message. EX: "BTRFS filesystem was supposed to be locked, but it was already unlocked. Continuing.." FRZR_CHECK_UPDATE=0 FRZR_STEAM_PROGRESS=0 FRZR_SOURCE="" FRZR_PARAMS="" - while (( "$#" )); do - case $1 in - --check) - FRZR_CHECK_UPDATE=1 - shift - ;; - --steam-progress) - FRZR_STEAM_PROGRESS=1 - shift - ;; - -*|--*) - echo "Unknown argument $1" - exit 1 - ;; - *) # preserve positional arguments - FRZR_PARAMS="${FRZR_PARAMS}$1 " # Use trailing space for the match below - shift - ;; - esac - done - - if [ ! -d /sys/firmware/efi/efivars ]; then - echo "Legacy BIOS installs are not supported. Aborting." - exit 1 - fi - - # keep only the first param as source - FRZR_SOURCE="${FRZR_PARAMS%% *}" - - MOUNT_PATH=/frzr_root - - if ! mountpoint -q ${MOUNT_PATH}; then - MOUNT_PATH=/tmp/frzr_root - fi + local RUNNING=true + STATE="FRZR_DEPLOY_CHECK" + TASK_TRACKER=0 + while $RUNNING; do + case "$STATE" in + "FRZR_DEPLOY_CHECK") + TASK_STATE="CHECK" + TASK_MSG="Checking for root privileges" + send_data + if [ $EUID -ne 0 ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="$(basename $0) not run as root" + STATE="FAIL" + send_data + continue + fi - if ! mountpoint -q ${MOUNT_PATH}; then - mkdir -p ${MOUNT_PATH} - mount -L frzr_root ${MOUNT_PATH} - sleep 5 - fi + TASK_MSG="Checking if the system has booted via UEFI" + send_data + check_uefi_result=$(check_uefi) + if echo "${check_uefi_result}" | grep -Fq 'ERROR'; then + TASK_ERROR=1 + TASK_ERROR_MSG="UEFI boot check failed: ${check_uefi_result}" + STATE="FAIL" + send_data + continue + fi + + # Check if any updates are available and preserve FRZR parameters + while (("$#")); do + case $1 in + --check) + FRZR_CHECK_UPDATE=1 + shift + ;; + --steam-progress) + FRZR_STEAM_PROGRESS=1 + shift + ;; + -* | --*) + TASK_ERROR=1 + TASK_ERROR_MSG="Unknown argument $1" + STATE="FAIL" + send_data + ;; + *) # preserve positional arguments + FRZR_PARAMS="${FRZR_PARAMS}$1 " # Use trailing space for the match below + shift + ;; + esac + done + + # keep only the first param as source + FRZR_SOURCE="${FRZR_PARAMS%% *}" + if frzr-release > /dev/null; then + CURRENT=`frzr-release` + fi - if ! mountpoint -q ${MOUNT_PATH}/boot && ls -1 /dev/disk/by-label | grep frzr_efi > /dev/null; then - mkdir -p ${MOUNT_PATH}/boot - mount -L frzr_efi ${MOUNT_PATH}/boot - sleep 5 - fi + STATE="BEGIN" + ;; + "BEGIN") + TASK_STATE="BEGIN" + send_data + + local version=$(frzr-version) + if echo "${version}" | grep -Fq "ERROR"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not fetch frzr version: ${version}" + STATE="FAIL" + send_data + continue + fi + + # Make sure the frzr_root is mounted during the deployment procedure + # this code is based on the fact that when a btrfs filesystem is created + # the default subvolid that is created contextually has the ID set to 256 + # also as a matter of fact in btrfs is impossible to change subvolumes IDs + MOUNT_PATH="/frzr_root" + if mount | grep -Fq "${MOUNT_PATH}"; then + local MOUNTED_MOUNT_PATH="no" + else + MOUNT_PATH="/tmp/frzr_root" + TASK_MSG="Preparing '${MOUNT_PATH}' to be used as the main subvolume mount path" + send_data + mkdir -p "${MOUNT_PATH}" + if mount -L frzr_root -t btrfs -o subvolid=5,rw "${MOUNT_PATH}"; then + local MOUNTED_MOUNT_PATH="yes" + else + local MOUNTED_MOUNT_PATH="no" + TASK_ERROR=1 + TASK_ERROR_MSG="Could not bind ${MOUNT_PATH} to frzr_root main subvolume" + STATE="FAIL" + send_data + continue + fi + sleep 5 + fi - DEPLOY_PATH=${MOUNT_PATH}/deployments - mkdir -p ${DEPLOY_PATH} - - BOOT_CFG_SLOT_A="${MOUNT_PATH}/boot/loader/entries/frzr-slot-A.conf" - BOOT_CFG_SLOT_B="${MOUNT_PATH}/boot/loader/entries/frzr-slot-B.conf" - mkdir -p ${MOUNT_PATH}/boot/loader/entries - - # default to no slot - SLOT="" - - # delete deployments under these conditions: - # - we are currently running inside a frzr deployment (i.e. not during install) - # - the deployment is not currently running - # - the deployment is not configured to be run on next boot - if frzr-release > /dev/null; then - CURRENT=`frzr-release` - SLOT=`get_deployment_slot ${CURRENT} ${BOOT_CFG_SLOT_A} ${BOOT_CFG_SLOT_B}` - BOOT_CFG="${MOUNT_PATH}/boot/loader/entries/frzr-${SLOT}.conf" - TO_DELETE=`get_deployment_to_delete ${CURRENT} ${BOOT_CFG} ${DEPLOY_PATH}` - - if [ ! -z ${TO_DELETE} ]; then - echo "deleting ${TO_DELETE}..." - btrfs subvolume delete ${DEPLOY_PATH}/${TO_DELETE} || true - rm -rf ${MOUNT_PATH}/boot/${TO_DELETE} - fi - fi + DEPLOY_PATH="${MOUNT_PATH}/deployments" + + TASK_MSG="Preparing '${DEPLOY_PATH}' to be used as the deployment destination path" + send_data + + # Make sure DEPLOY_PATH exists + mkdir -p "${DEPLOY_PATH}" + if [ ! -d "${DEPLOY_PATH}" ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not create ${DEPLOY_PATH} to to store deployments" + STATE="FAIL" + send_data + continue + fi - if [ ! -z "$FRZR_SOURCE" ] && [ "$FRZR_SOURCE" != " " ] && [ $FRZR_CHECK_UPDATE -eq 0 ] && [ $FRZR_STEAM_PROGRESS -eq 0 ]; then - echo "$FRZR_SOURCE" > "${MOUNT_PATH}/source" - fi + # Make sure the EFI partition is mounted during the deployment procedure + EFI_MOUNT_PATH="${MOUNT_PATH}/efi" - if [ -e "${MOUNT_PATH}/source" ]; then - SOURCE=`cat "${MOUNT_PATH}/source" | head -1` - else - echo "WARNING: source wasn't specified" - fi + TASK_MSG="Preparing '${EFI_MOUNT_PATH}' to be used as the EFI System path" + send_data - if [ "${local_install}" == true ]; then - mkdir tmp_source - mount -o rw -L FRZR_UPDATE /root/tmp_source - FILE_NAME=$(basename /root/tmp_source/*.img.tar.xz*) - NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') - SUBVOL="${DEPLOY_PATH}/${NAME}" - IMG_FILE="/root/tmp_source/${FILE_NAME}" - elif [[ "$FRZR_SOURCE" == *".img.tar.xz" ]]; then - FILE_NAME=$(basename ${FRZR_SOURCE}) - NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') - SUBVOL="${DEPLOY_PATH}/${NAME}" - IMG_FILE=${FRZR_SOURCE} - elif [[ "$FRZR_SOURCE" == *".img.xz" ]]; then - FILE_NAME=$(basename ${FRZR_SOURCE}) - NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') - SUBVOL="${DEPLOY_PATH}/${NAME}" - IMG_FILE=${FRZR_SOURCE} - elif [[ "$FRZR_SOURCE" == *".img.zst" ]]; then - FILE_NAME=$(basename ${FRZR_SOURCE}) - NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') - SUBVOL="${DEPLOY_PATH}/${NAME}" - IMG_FILE=${FRZR_SOURCE} - elif [[ "$FRZR_SOURCE" == *".img" ]]; then - FILE_NAME=$(basename ${FRZR_SOURCE}) - NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') - SUBVOL="${DEPLOY_PATH}/${NAME}" - IMG_FILE=${FRZR_SOURCE} - else - REPO=$(echo "${SOURCE}" | cut -f 1 -d ':') - CHANNEL=$(echo "${SOURCE}" | cut -f 2 -d ':') - - RELEASES_URL="https://api.github.com/repos/${REPO}/releases" - - IMG_URL=$(curl --http1.1 -L -s "${RELEASES_URL}" | get_img_url "${CHANNEL}") - - if [ -z "$IMG_URL" ] || [ "$IMG_URL" == "null" ]; then - echo "No matching source found" - if curl --http1.1 -L -s "${RELEASES_URL}" | grep "rate limit" > /dev/null; then - echo "GitHub API rate limit exceeded" - exit 29 + if mount | grep -Fq "${EFI_MOUNT_PATH}"; then + local MOUNTED_EFI_MOUNT_PATH="no" + else + mkdir -p "${EFI_MOUNT_PATH}" + if mount -L frzr_efi -o uid=0,gid=0,fmask=0077,dmask=0077 "${EFI_MOUNT_PATH}"; then + local MOUNTED_EFI_MOUNT_PATH="yes" + else + local MOUNTED_EFI_MOUNT_PATH="no" + TASK_ERROR=1 + TASK_ERROR_MSG="Could not bind ${EFI_MOUNT_PATH} to frzr_efi (boot) partition" + STATE="FAIL" + send_data + continue + fi + sleep 5 fi - exit 1 - fi - - FILE_NAME=$(basename ${IMG_URL}) - NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') - BASE_URL=$(dirname "${IMG_URL}") - CHECKSUM=$(curl --http1.1 -L -s "${BASE_URL}/sha256sum.txt" | cut -f -1 -d ' ') - SUBVOL="${DEPLOY_PATH}/${NAME}" - IMG_FILE="${MOUNT_PATH}/${FILE_NAME}" - if [ -e ${SUBVOL} ]; then - echo "${NAME} already installed; aborting" - exit 7 # let Steam know there is no update available - fi + STATE="PREPARE" + ;; + "PREPARE") + TASK_STATE="PREPARE" + send_data + # Before the new image is deployed + # delete old deployments under these conditions: + # - the current deployment is known + # - the deployment is not currently running + if echo "${CURRENT}" | grep -Fq 'ERROR'; then + TASK_WARNING=1 + TASK_WARNING_MSG="Could not retrieve deployment to keep: ${CURRENT}" + send_data + continue + fi - if [ $FRZR_CHECK_UPDATE -eq 1 ]; then - echo "Update available: ${NAME}" - exit 0 # let Steam know there is an update available - fi + local default_btrfs_subvolid_cmd_res=$(btrfs subvolume get-default "${MOUNT_PATH}") + if [ $? -eq 0 ]; then + local default_btrfs_subvolid=$(echo "$default_btrfs_subvolid_cmd_res" | awk '{print $2}') + for deployment_to_be_removed in "${DEPLOY_PATH}"/*; do + if [ ! -z "${deployment_to_be_removed}" ] && [ -d "${deployment_to_be_removed}" ]; then + if is_btrfs_subvolume "${deployment_to_be_removed}"; then + local removal_deployment=$(basename "${deployment_to_be_removed}") + local removal_deployment_subvolid=$(btrfs_subvol_get_id "${deployment_to_be_removed}") + + if echo "${removal_deployment}" | grep -Fq "${CURRENT}"; then + TASK_MSG="Deployment '${removal_deployment}' will be kept: is the current deployment" + send_data + elif echo "${removal_deployment_subvolid}" | grep -Fq "ERROR"; then + TASK_MSG="Deployment '${removal_deployment}' will be kept: cannot get the subvolid and it's too risky to delete it" + send_data + elif [ "$removal_deployment_subvolid" = "$default_btrfs_subvolid" ]; then + TASK_MSG="Deployment '${removal_deployment}' will be kept: Default subvolid '${default_btrfs_subvolid}' is the subvolid of the deployment" + send_data + else + TASK_MSG="Deleting deployment ${removal_deployment}: it is not the current '${CURRENT}'" + send_data + + # Delete deployment boot files taking up space in /efi + if [ -d "${EFI_MOUNT_PATH}/${removal_deployment}" ]; then + TASK_MSG="deleting deployment boot files from '${EFI_MOUNT_PATH}/${removal_deployment}'" + send_data + rm -rf "${EFI_MOUNT_PATH}/${removal_deployment}" + fi + + # Delete deployment entries (this code is based on the fact that names of entries are generated as they are in prepare_efi_partition) + for entry_path in "${EFI_MOUNT_PATH}"/loader/entries/*.conf; do + if echo "${entry_path}" | grep -Fq "${removal_deployment}"; then + TASK_MSG="deleting deployment boot entry '${entry_path}'" + send_data + rm -f "${entry_path}" + fi + done + + # Execute deployment removal scripts + TASK_MSG="Executing removal hooks" + send_data + local deployment_removal_hooks=$(execute_removal "${removal_deployment}" "${deployment_to_be_removed}" "${MOUNT_PATH}" "${version}") + if echo "${deployment_removal_hooks}" | grep -Fq "ERROR"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Removal hook on subvolume '${deployment_to_be_removed}' failed: ${deployment_removal_hooks}" + STATE="FAIL" + send_data + continue + fi + + # Delete the deployment btrfs subvolume + local deployment_removal_result=$(btrfs_subvolume_simple_delete "${DEPLOY_PATH}/${removal_deployment}") + if echo "${deployment_removal_result}" | grep -Fq "ERROR"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not delete the old deployment '${removal_deployment}'" + STATE="FAIL" + send_data + continue + fi + fi + fi + fi + done + else + TASK_WARNING=1 + TASK_WARNING_MSG="Could not get the default soubvolid for the mountpoint '${MOUNT_PATH}' -- no deployments will be deleted" + send_data + fi - if [ $FRZR_STEAM_PROGRESS -eq 1 ]; then - curl --http1.1 -# -L -o "${IMG_FILE}" -C - "${IMG_URL}" 2>&1 | \ - stdbuf -oL tr '\r' '\n' | grep --line-buffered -oP '[0-9]*+(?=.[0-9])' | clean_progress 91 % - elif [ -z ${SHOW_UI} ]; then - echo "downloading ${NAME}..." - curl --http1.1 -L -o "${IMG_FILE}" -C - "${IMG_URL}" - else - curl --http1.1 -# -L -o "${IMG_FILE}" -C - "${IMG_URL}" 2>&1 | \ - stdbuf -oL tr '\r' '\n' | grep --line-buffered -oP '[0-9]*+(?=.[0-9])' | clean_progress 100 | \ - whiptail --gauge "Downloading system image (${NAME})" 10 50 0 - fi + STATE="DOWNLOAD" + ;; + "DOWNLOAD") + TASK_STATE="DOWNLOAD" - CHECKSUM2=`sha256sum "${IMG_FILE}" | cut -d' ' -f 1` - if [ "$CHECKSUM" != "$CHECKSUM2" ]; then - rm -f "${IMG_FILE}" - echo "checksum does not match; aborting" - exit 1 - fi - fi + # Check if source needs updated or needs to be created for the first time + if [ ! -z "$FRZR_SOURCE" ] && [ "$FRZR_SOURCE" != " " ] && [ $FRZR_CHECK_UPDATE -eq 0 ] && [ $FRZR_STEAM_PROGRESS -eq 0 ]; then + echo "$FRZR_SOURCE" >"${MOUNT_PATH}/source" + fi - if [ -z ${SHOW_UI} ]; then - echo "installing ${NAME}..." - else - whiptail --infobox "Extracting and installing system image (${NAME}). This may take some time." 10 50 - fi + if [ -e "${MOUNT_PATH}/source" ]; then + SOURCE=$(cat "${MOUNT_PATH}/source" | head -1) + else + echo "WARNING: source wasn't specified" + fi + TASK_MSG="Determining what source image we should use" + if [ "${local_install}" == true ]; then + mkdir tmp_source + mount -o rw -L FRZR_UPDATE /root/tmp_source + FILE_NAME=$(basename /root/tmp_source/*.img.tar.xz*) + NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') + SUBVOL="${DEPLOY_PATH}/${NAME}" + IMG_FILE="/root/tmp_source/${FILE_NAME}" + elif [[ "$FRZR_SOURCE" == *".img.tar.xz" ]]; then + FILE_NAME=$(basename ${FRZR_SOURCE}) + NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') + SUBVOL="${DEPLOY_PATH}/${NAME}" + IMG_FILE="${FRZR_SOURCE}" + elif [[ "$FRZR_SOURCE" == *".img" ]]; then + FILE_NAME=$(basename ${FRZR_SOURCE}) + NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') + SUBVOL="${DEPLOY_PATH}/${NAME}" + IMG_FILE="${FRZR_SOURCE}" + else + TASK_MSG="We will be downloading the image from the specified repo" + REPO=$(echo "${SOURCE}" | cut -f 1 -d ':') + CHANNEL=$(echo "${SOURCE}" | cut -f 2 -d ':') + + RELEASES_URL="https://api.github.com/repos/${REPO}/releases" + + IMG_URL=$(curl --http1.1 -L -s "${RELEASES_URL}" | get_img_url "${CHANNEL}") + + if [ -z "$IMG_URL" ] || [ "$IMG_URL" == "null" ]; then + if curl --http1.1 -L -s "${RELEASES_URL}" | grep "rate limit" >/dev/null; then + TASK_ERROR=1 + TASK_ERROR_MSG="GitHub API rate limit exceeded" + STATE="FAIL" + else + TASK_ERROR=1 + TASK_ERROR_MSG="No matching source found" + STATE="FAIL" + fi + + continue + fi + + FILE_NAME=$(basename ${IMG_URL}) + NAME=$(echo "${FILE_NAME}" | cut -f 1 -d '.') + BASE_URL=$(dirname "${IMG_URL}") + EXPECTED_CHECKSUM=$(curl --http1.1 -L -s "${BASE_URL}/sha256sum.txt" | cut -f -1 -d ' ') + SUBVOL="${DEPLOY_PATH}/${NAME}" + IMG_FILE="${MOUNT_PATH}/${FILE_NAME}" + + if [ -e "${SUBVOL}" ]; then + # Only allow deleting the subvolume if we are booted in the installer. + if [ "$UPGRADE" -eq 0 ]; then + TASK_MSG="An already existing install was found" + send_data + MSG="${NAME} is already installed, would you like to delete this and re-deploy?" + if (whiptail --yesno "${MSG}" 10 50); then + echo "deleting ${NAME}" + btrfs subvolume delete "${SUBVOL}" + rm -rf "${EFI_MOUNT_PATH}/${NAME}" + fi + else + TASK_ERROR=1 + TASK_ERROR_MSG="${NAME} is already installed" + STATE="FAIL" + send_data + continue + fi + fi + + if [ $FRZR_CHECK_UPDATE -eq 1 ]; then + echo "Update available: ${NAME}" + fi + + if [ $FRZR_STEAM_PROGRESS -eq 1 ]; then + curl --http1.1 -# -L -o "${IMG_FILE}" -C - "${IMG_URL}" 2>&1 | + stdbuf -oL tr '\r' '\n' | grep --line-buffered -oP '[0-9]*+(?=.[0-9])' | clean_progress 91 % + elif [ -z ${SHOW_UI} ]; then + # This will need to be put under a new variable if it passes testing because we want to have different + # logic depending if there is a Modern UI, legecy UI support, or if it is ran via CLI + TASK_MSG="Downloading Image" + send_data + touch /tmp/wget_output.log # Create log to be used for recording progress + wget ${IMG_URL} -O ${IMG_FILE} -o /tmp/wget_output.log & pid=$!; + previous_percentage=-1; + while [ -e /proc/$pid ]; do + percentage=$(awk '/[0-9]+%/{print substr($7, 1, length($7)-1)}' /tmp/wget_output.log | tail -n 1); + if [[ $percentage =~ ^[0-9]+$ && $percentage -ne $previous_percentage ]]; then + previous_percentage=$percentage; + TASK_PROGRESS=$percentage + send_data + fi; + sleep 1; + done + else + TASK_MSG="Using Whiptail to show download progress" + send_data + curl --http1.1 -# -L -o "${IMG_FILE}" -C - "${IMG_URL}" 2>&1 | + stdbuf -oL tr '\r' '\n' | grep --line-buffered -oP '[0-9]*+(?=.[0-9])' | clean_progress 100 | + whiptail --gauge "Downloading system image (${NAME})" 10 50 0 + fi + fi - if [[ "${IMG_FILE##*.}" == "img" ]]; then - btrfs receive --quiet ${DEPLOY_PATH} < ${IMG_FILE} - elif [[ "${IMG_FILE##*.}" == "zst" ]]; then - zstd -d -c ${IMG_FILE} | btrfs receive --quiet ${DEPLOY_PATH} - elif [[ "${IMG_FILE##*.}" == "xz" ]]; then - if [[ "${IMG_FILE}" == *".tar.xz" ]]; then - tar xfO ${IMG_FILE} | btrfs receive --quiet ${DEPLOY_PATH} - else - xz -dc ${IMG_FILE} | btrfs receive --quiet ${DEPLOY_PATH} - fi - else - # Handle other cases or provide an error message - echo "Unsupported file format: ${IMG_FILE}" - fi + STATE="CHECKSUM" + ;; + "CHECKSUM") + TASK_STATE="CHECKSUM" + TASK_MSG="Verifying if checksums match" + send_data + ACTUAL_CHECKSUM=$(sha256sum "${IMG_FILE}" | cut -d' ' -f 1) + if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then + rm -f "${IMG_FILE}" + + # We can attempt to redownload the image again here instead of aborting like the original behavior + TASK_ERROR=1 + TASK_ERROR_MSG="Checksum does not match" + STATE="FAIL" + send_data + continue + fi + TASK_TRACKER=1 # Tell any listeners this task was completed, I'm not sure this variable will be needed in the end + send_data + + STATE="EXTRACT" + ;; + "EXTRACT") + TASK_STATE="EXTRACT" + send_data + # Extract tar of system image + # This step might not ever need to be used, but left here depending on the occasion it may be needed. + + # Write the deployment image to disk (if it was not being done already by the download step) + if [ -d "$SUBVOL" ]; then + if is_btrfs_subvolume "$SUBVOL"; then + # skip this step: the subvolume has been created already + STATE="INSTALL" + continue + fi + fi - if [ -d "${SUBVOL}/etc" ]; then - echo "System image ${NAME} will use the /etc overlay" - else - echo "System image ${NAME} will use an /etc subvolume" + # Use BTRFS receive to install the image + if [ -z ${SHOW_UI} ]; then + TASK_MSG="Installing image ${NAME}" + send_data + else + # TODO: verify if this really works + whiptail --infobox "Extracting and installing system image (${NAME}). This may take some time." 10 50 + fi - # unlock the subvolume to be able to create a nested subvolume - btrfs property set -f -ts "${SUBVOL}/" ro false + # Install the deployment from the downloaded file $IMG_FILE and place the deployed image in $DEPLOY_PATH + TASK_MSG="Extracting Image" + send_data + local install_result=$(install_image "${IMG_FILE}" "${DEPLOY_PATH}") + if echo "${install_result}" | grep -Fq 'ERROR'; then + TASK_ERROR=1 + TASK_ERROR_MSG="Error extracting the deployment image: ${install_result}" + STATE="FAIL" + send_data + continue + fi - # create the nested subvolume for /etc - btrfs subvolume create "${SUBVOL}/etc" + STATE="INSTALL" + ;; + "INSTALL") + TASK_STATE="INSTALL" + send_data + + # mount home into the new deployment + if [ -d "${SUBVOL}/home" ]; then + local SUBVOL_HOME_MOUNT="${SUBVOL}/home" + if is_btrfs_subvolume "${MOUNT_PATH}/home"; then + mount --bind "${MOUNT_PATH}/home" "${SUBVOL_HOME_MOUNT}" + elif mountpoint -q "/home"; then + mount --bind "/home" "${SUBVOL_HOME_MOUNT}" + else + TASK_WARNING=1 + TASK_WARNING_MSG="Could not mount /home directory" + send_data + fi + fi - # create the nested subvolume for /snapshots - btrfs subvolume create "${SUBVOL}/snapshots" + # bind-mount /efi of the newly deployed image + if [ -d "${SUBVOL}/efi" ]; then + local SUBVOL_EFI_MOUNT="${SUBVOL}/efi" + if [ -d "${SUBVOL_EFI_MOUNT}" ]; then + mount --bind "${EFI_MOUNT_PATH}" "${SUBVOL_EFI_MOUNT}" + else + TASK_WARNING=1 + TASK_WARNING_MSG="Could not mount ESP partition because image does not have a /efi directory" + send_data + fi + fi - # re-lock the subvolume. ${SUBVOL}/etc will remain R/W - btrfs property set -f -ts "${SUBVOL}/" ro true + # Execute any migrations available in the image to be deployed + if [ -d "${SUBVOL}"/usr/lib/frzr.d ]; then + TASK_MSG="Executing deployment migrations" + local migrations_result=$(execute_migrations "${NAME}" "${SUBVOL}" "${MOUNT_PATH}" "${version}") + if echo "${migrations_result}" | grep -Fq "ERROR"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Migrations on subvolume '$SUBVOL' failed: ${migrations_result}" + STATE="FAIL" + send_data + continue + fi + else + TASK_WARNING=1 + TASK_WARNING_MSG="Could not find migrations scripts inside the deployed image" + send_data + fi - btrfs subvolume create "${SUBVOL}/snapshots/etc" + STATE="VERIFY" + ;; + "VERIFY") + TASK_STATE="VERIFY" + + # verify the subvolume integrity + TASK_MSG="Checking integrity of deployed image" + send_data + if [ -z "$FRZR_SKIP_CHECK" ]; then + if ! btrfs scrub start -Bdr "${SUBVOL}"; then + TASK_ERROR=1 + TASK_ERROR_MSG="FS check on btrfs subvolume '$SUBVOL' failed: image integrity compromised" + STATE="FAIL" + send_data + fi + fi - # copy every stock /etc (that has been moved to /usr/etc) file to the actual /etc - cp -a ${SUBVOL}/usr/etc/* ${SUBVOL}/etc/ + STATE="CLEANUP" + ;; + "CLEANUP") + TASK_STATE="CLEANUP" + send_data + + # This is used to update the EFI partition: setting up systemd-boot (or whatever else bootlader might be supported) to boot the new deployment + local efi_update_result=$(frzr-bootloader "${NAME}") + if echo "${efi_update_result}" | grep -Fq 'ERROR'; then + # bootloader configuration could not be updated + TASK_ERROR=1 + TASK_ERROR_MSG="Could not update the EFI partition: ${efi_update_result}" + STATE="FAIL" + send_data + continue + fi - # copy the machine-id file: this was created by systemd the very first boot and identify the machine: - # changing this will also make ssh warn about machine not matching. - if [ -f "/etc/machine-id" ]; then - cp -a /etc/machine-id ${SUBVOL}/etc/ - else - echo "WARNING: no /etc/machine-id -- new machine-id will be regenerated at next boot" - fi + # Remove download artifacts (if any) + rm -f ${MOUNT_PATH}/*.img.* + + # Lazy umount the deployed system (find all mounted subdirectories under the parent directory and unmount the mounted subdirectories) + TASK_MSG="Post-install umount of '${SUBVOL}'" + send_data + mounted_subdirectories=$(find "${SUBVOL}" -mindepth 1 -maxdepth 1 -type d -exec sh -c 'findmnt -M "$1" > /dev/null' sh {} \; -print) + echo "$mounted_subdirectories" | while read -r subdirectory; do + umount -l -R "$subdirectory" + done + + # Get the subvolid: this will be used in generating the systemd-boot entry and setting is as the default for the automatic boot + TASK_MSG="Finding deployment subvolume ID" + send_data + local subvolid=$(btrfs_subvol_get_id "${SUBVOL}") + if echo "$subvolid" | grep -Fq "ERROR"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not find subvolume ID of the new deployment" + STATE="FAIL" + send_data + continue + elif [ "${subvolid}" = "5" ]; then + TASK_ERROR_MSG="Invalid deployment subvolid" + STATE="FAIL" + send_data + continue + elif [ -z "${subvolid}" ]; then + TASK_ERROR_MSG="Could not identify the correct subvolid of the deployment" + STATE="FAIL" + send_data + continue + fi - # Make a snapshot of /etc so that this state can be restored - btrfs subvolume snapshot ${SUBVOL}/etc ${SUBVOL}/snapshots/etc/0 - fi + # Activates the new deployed image by making it the default btrfs subvolume + # systemd-equipped initramfs images will automount the default subvolume as the rootfs + TASK_MSG="Activating the new deployment (subvolid=${subvolid}) as the default subvolume" + send_data + if ! btrfs subvolume set-default "${subvolid}" "${MOUNT_PATH}"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not activate the new deployment" + STATE="FAIL" + send_data + continue + fi + + STATE="SUCCESS" + ;; + "SUCCESS") + # This state should only be used if the installation completed without errors + TASK_STATE="SUCCESS" - mkdir -p ${MOUNT_PATH}/boot/${NAME} - cp ${SUBVOL}/boot/vmlinuz-linux ${MOUNT_PATH}/boot/${NAME} - cp ${SUBVOL}/boot/initramfs-linux.img ${MOUNT_PATH}/boot/${NAME} + TASK_MSG="Deployment is successful: reboot to boot into ${NAME}" + TASK_TRACKER=1 + send_data - RESUME_ARGS="" - if [ -f "/home/swapfile" ]; then - RESUME_OFFSET=$(btrfs inspect-internal map-swapfile -r /home/swapfile) - - RESUME_DISK=$(mount | grep "/home" | awk '{print $1}') - PART_UUID=$(blkid -o value -s UUID ${RESUME_DISK}) + RUNNING=false - RESUME_ARGS="resume=UUID=${PART_UUID} resume_offset=${RESUME_OFFSET}" + if [ -z ${SHOW_UI} ]; then + echo "Deployment is successful: reboot to boot into ${NAME}" + else + whiptail --msgbox "Deployment is successful: reboot to boot into ${NAME}" 8 48 + fi + + ;; + "FAIL") + TASK_STATE="FAIL" + TASK_ERROR=1 + send_data + RUNNING=false + + if [ -z ${SHOW_UI} ]; then + echo "Deployment failed: ${TASK_ERROR_MSG}" + else + whiptail --msgbox "Deployment failed: ${TASK_ERROR_MSG}" 8 48 + fi - echo "Add resume hook to boot boot parameters: ${RESUME_ARGS}" - fi + ;; + *) + TASK_STATE="UNKNOWN_ERROR" + TASK_ERROR_MSG="Deploy hit an unexpected state" + TASK_ERROR=1 + send_data + RUNNING=false + ;; + esac + done - if [ ! -z "$SLOT" ] ; then - # Copy current running config to other slot - if [ "${BOOT_CFG}" == "${BOOT_CFG_SLOT_A}" ] ; then - cp ${BOOT_CFG_SLOT_A} ${BOOT_CFG_SLOT_B} - elif [ "${BOOT_CFG}" == "${BOOT_CFG_SLOT_B}" ] ; then - cp ${BOOT_CFG_SLOT_B} ${BOOT_CFG_SLOT_A} + # umount the subvol /home bind-mount + if [ ! -z "$SUBVOL_HOME_MOUNT" ]; then + if mountpoint -q "${SUBVOL_HOME_MOUNT}"; then + umount -l "${SUBVOL_HOME_MOUNT}" fi - else - # No slot seems booted, write to slot A - echo "Warning: no slot currently set, will start from A" - BOOT_CFG="${BOOT_CFG_SLOT_A}" - SLOT="slot-A" fi - # Check if there are migrations available - if compgen -G "${SUBVOL}"/usr/lib/frzr.d/*.migration > /dev/null ; then - for m in "${SUBVOL}"/usr/lib/frzr.d/*.migration ; - do - unset -f post_install - . $m - if [ "$(type -t post_install)" == function ] ; then - post_install "${MOUNT_PATH}" "${SUBVOL}" "${NAME}" - fi - unset -f post_install - done - fi - - # Export variables to be used by child processes for frzr-tweaks and frzr-initramfs - export MOUNT_PATH - export SUBVOL - export NAME - - # Check if the FIRMWARE_OVERRIDE variable is set by the install media, if so enable firmware overrides - if [ -n "${FIRMWARE_OVERRIDE}" ]; then - echo "export USE_FIRMWARE_OVERRIDES=1" > ${MOUNT_PATH}/etc/device-quirks.conf + # umount the subvol /efi bind-mount + if [ ! -z "$SUBVOL_EFI_MOUNT" ]; then + if mountpoint -q "${SUBVOL_EFI_MOUNT}"; then + umount -l "${SUBVOL_EFI_MOUNT}" + fi fi - # Run frzr-tweaks to execute the device-quirks to be supplied by the deployed images - frzr-tweaks - - # Run frzr-initramfs to create mkinicpio.conf and build an initramfs - frzr-initramfs - - # now that the initramfs has been built determine if that includes microcode - AMD_UCODE="# missing intel-ucode" - INTEL_UCODE="# missing intel-ucode" - if grep -q "microcode" "/etc/mkinitcpio.conf"; then - echo "microcode hook present: will skip systemd-boot initrd." - - AMD_UCODE="# amd-ucode in initramfs" - INTEL_UCODE="# intel-ucode in initramfs" - else - echo "microcode hook not found: will use systemd-bood initrd." - - if [ -e ${SUBVOL}/boot/amd-ucode.img ] ; then - cp ${SUBVOL}/boot/amd-ucode.img ${MOUNT_PATH}/boot/${NAME} - AMD_UCODE="initrd /${NAME}/amd-ucode.img" - fi - - if [ -e ${SUBVOL}/boot/intel-ucode.img ] ; then - cp ${SUBVOL}/boot/intel-ucode.img ${MOUNT_PATH}/boot/${NAME} - INTEL_UCODE="initrd /${NAME}/intel-ucode.img" + # umount the efi path + if [ "${MOUNTED_EFI_MOUNT_PATH}" = "yes" ]; then + if mountpoint -q "${EFI_MOUNT_PATH}"; then + umount -l "${EFI_MOUNT_PATH}" fi fi - ADDITIONAL_ARGUMENTS="" - if [ -e ${SUBVOL}/usr/lib/frzr.d/bootconfig.conf ] ; then - ADDITIONAL_ARGUMENTS="$ADDITIONAL_ARGUMENTS $(cat ${SUBVOL}/usr/lib/frzr.d/bootconfig.conf)" + # umount the frzr_root subvolume (if it was mounted by this tool and not externally) + if [ "${MOUNTED_MOUNT_PATH}" = "yes" ]; then + if mountpoint -q "${MOUNT_PATH}"; then + umount -l "${MOUNT_PATH}" + fi fi - - # write down the kernel cmdline as the last step: shall a blackout happen the prevous deployment will get booted - get_boot_cfg "${NAME}" "${AMD_UCODE}" "${INTEL_UCODE}" "${ADDITIONAL_ARGUMENTS}" > ${BOOT_CFG} - echo "default frzr-$SLOT.conf" > ${MOUNT_PATH}/boot/loader/loader.conf - - rm -f ${MOUNT_PATH}/*.img.* - - rm -rf /var/lib/pacman # undo frzr-unlock - - echo "deployment complete; restart to boot into ${NAME}" - - umount -R -l ${MOUNT_PATH} } - - -if [ "$0" = "$BASH_SOURCE" ] ; then - main "$@" -fi diff --git a/__frzr-envars b/__frzr-envars new file mode 100644 index 0000000..bf6df0e --- /dev/null +++ b/__frzr-envars @@ -0,0 +1,46 @@ +#! /bin/bash + +# This is the FRZR global variable manifest to be used for early declarlation of variables to be logged + +# Define all public variables to be used here and in all sourced scripts +# Check for exported variables provided by an installer script +if [ -z $TARGET ]; then + TARGET="" # Set to the target deployment channel. This should only be set by the installer script +fi + +if [ -z $LOCAL_INSTALL ]; then + LOCAL_INSTALL=0 # Set to 1 to perform a local media install. This should be set by the installer script +fi + +UPGRADE=0 +REPAIR_INSTALL=0 # Set to 1 if you want to do a repair install. This should be set by frzr-bootstrap later if an existing install is found. +NAME="" # Name of the OS to be deployed +EFI_MOUNT_PATH="" # EFI mount path +MOUNT_PATH="" # This is set as /tmp/frzr_root/ in the installer and /frzr_root during upgrades +SUBVOL="" +DEPLOY_PATH="" # ${MOUNT_PATH}/deployments +FRZR_CHECK_UPDATE=0 +FRZR_PARAMS="" +FILE_NAME="" # The file name downloaded from the repo +IMG_NAME="" # Path to the downloaded image to be deployed. It gets set by "${MOUNT_PATH}/${FILE_NAME}" +BASE_URL="" +REPO="" # Github repo +CHANNEL="" # The target channel to download the image from. Stable/Testing/Unstable +RELEASES_URL="" +EXPECTED_CHECKSUM="" # Expected checksum value of downloaded file +ACTUAL_CHECKSUM="" # Actual checksum of the file downloaded +STATE="" + +# Tracker file directory +TRACKER_FILE_DIR="/tmp" +TRACKER_FILE_PATH="${TRACKER_FILE_DIR}/frzr.tracker" + +# Signals +TASK_STATE="" +TASK_MSG="" # TASK_MSG will be read by external tools. EX TASK_MSG="Preparing user directory" +TASK_TRACKER=0 # TASK_TRACKER will be used to signal back that the current TASK_MSG in queue is handled or not 0 = Pending 1 = Completed +TASK_ERROR=0 # Signal to let listeners know that the task had an error. +TASK_ERROR_MSG="" # Error message. EX: "Unexpected I/O errors found during write" +TASK_WARNING=0 # Signal to let listeners know there is a warning +TASK_WARNING_MSG="" # Warning message. EX: "BTRFS filesystem was supposed to be locked, but it was already unlocked. Continuing.." +TASK_PROGRESS=0 # Use this whenever we want to send progress to the GUI in the form of whole number percentages diff --git a/__frzr-kernel b/__frzr-kernel new file mode 100644 index 0000000..ae7486e --- /dev/null +++ b/__frzr-kernel @@ -0,0 +1,424 @@ +#! /bin/bash + +set -o pipefail + +# import methods +#source "${BASH_SOURCE%/*}/__frzr" "$@" + +frzr_kernel() { + local KERNEL_NAME="" + + RUNNING=true + STATE="BEGIN" + while $RUNNING; do + case "$STATE" in + "BEGIN") + MOUNT_PATH="/frzr_root" + + # Make sure the frzr_root is mounted during the deployment procedure + # this code is based on the fact that when a btrfs filesystem is created + # the default subvolid that is created contextually has the ID set to 256 + # also as a matter of fact in btrfs is impossible to change subvolumes IDs + if mount | grep -Fq "${MOUNT_PATH}"; then + local MOUNTED_MOUNT_PATH="no" + else + MOUNT_PATH="/tmp/frzr_root" + #TASK_MSG="Preparing '${MOUNT_PATH}' to be used as the main subvolume mount path" + mkdir -p "${MOUNT_PATH}" + if sudo mount -L frzr_root -t btrfs -o subvolid=5,rw "${MOUNT_PATH}"; then + local MOUNTED_MOUNT_PATH="yes" + else + echo "frzr-kernel failed: could not mount frzr_root" + STATE="FAIL" + send_data + continue + fi + sleep 5 + fi + + # Make sure the EFI partition is mounted during the deployment procedure + EFI_MOUNT_PATH="${MOUNT_PATH}/efi" + + if mount | grep -Fq "${EFI_MOUNT_PATH}"; then + local MOUNTED_EFI_MOUNT_PATH="no" + else + sudo mkdir -p "${EFI_MOUNT_PATH}" + if sudo mount -L frzr_efi -o rw,noauto,noexec,nosuid,nodev,uid=0,gid=379,dmask=007,fmask=117 "${EFI_MOUNT_PATH}"; then + local MOUNTED_EFI_MOUNT_PATH="yes" + else + TASK_ERROR=1 + TASK_ERROR_MSG="Could not bind ${EFI_MOUNT_PATH} to frzr_efi (boot) partition" + STATE="FAIL" + send_data + continue + fi + sleep 5 + fi + + if [ ! -f "/usr/bin/depmod" ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not find depmod at '/usr/bin/depmod'" + STATE="FAIL" + send_data + continue + fi + + if ! type -P make >/dev/null 2>&1; then + TASK_ERROR=1 + TASK_ERROR_MSG="make is unavailable, did you forget to install development packages?" + STATE="FAIL" + send_data + continue + fi + + if [ -d "${MOUNT_PATH}/kernels/usr" ]; then + local KERNEL_OVERLAY_DIR="${MOUNT_PATH}/kernels" + local MODULES_DIR="${KERNEL_OVERLAY_DIR}/usr" + else + TASK_ERROR=1 + TASK_ERROR_MSG="Could not find kernels overlay in '${MOUNT_PATH}/kernels/usr'" + STATE="FAIL" + send_data + continue + fi + + STATE="DOWNLOAD" + ;; + + "DOWNLOAD") + if [ ! -f ".config" ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not find kernel configuration" + STATE="FAIL" + send_data + continue + else + cp .config .config.pre + fi + + STATE="BUILD" + ;; + + "BUILD") + # This is important as otherwise ccache will always have 100% misses + export KBUILD_BUILD_TIMESTAMP="" + export KBUILD_BUILD_USER="root" + export KBUILD_BUILD_HOST="frzr" + + if [ -f "/usr/bin/clang" ] && [ -f "/usr/bin/llvm-ar" ] && [ -f "/usr/bin/lld" ]; then + echo "Compiling with clang" + export LLVM=1 + + if [ -f "/usr/bin/ccache" ]; then + export CC="ccache clang" + fi + else + echo "Couldn't set clang as the compiler" + fi + + # Fetch the name of the kernel (uname -r) + if ! make -s kernelrelease > version; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not execute defconfig (1)" + STATE="FAIL" + send_data + continue + else + local KERNEL_NAME=$(cat version) + fi + + if [ ! -f "include/config/auto.conf" ]; then + + if ! make defconfig; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not execute defconfig (1)" + STATE="FAIL" + send_data + continue + fi + + if ! make defconfig; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not execute defconfig (2)" + STATE="FAIL" + send_data + continue + fi + + # This will perform a clean of previous build artifacts + #if ! make KERNELRELEASE="${KERNEL_NAME}" mrproper; then + # TASK_ERROR=1 + # TASK_ERROR_MSG="Could not execute mrproper" + # STATE="FAIL" + # send_data + # continue + #fi + + cp .config.pre .config + + #echo "Checking for pre-installed kernel" + #if [ -d "/usr/lib/modules/${KERNEL_NAME}" ]; then + # TASK_ERROR=1 + # TASK_ERROR_MSG="ERROR: Kernel ${KERNEL_NAME} already exists" + # STATE="FAIL" + # continue + #fi + fi + + echo "Building ${KERNEL_NAME}..." + + # Get the number of available cores + local CORES=$(nproc) + local CORES_INT=$(echo $CORES | awk -F. '{print $1}') + + # Decide the number of cores to use based on the condition + if [ $CORES_INT -le 16 ]; then + MAKE_CORES=$CORES_INT + else + MAKE_CORES=$((CORES_INT - 2)) + fi + + # Invoke make with the determined number of cores + if ! make -j$MAKE_CORES all; then + TASK_ERROR=1 + TASK_ERROR_MSG="ERROR: Compilation of linux-${KERNEL_NAME} failed" + STATE="FAIL" + send_data + continue + fi + + STATE="INSTALL" + ;; + "INSTALL") + + # Install kernel modules (taken from _package) + local modulesdir="${MODULES_DIR}/lib/modules/${KERNEL_NAME}" + mkdir -p -m 755 "${modulesdir}" + + # systemd expects to find the kernel here to allow hibernation + # https://github.com/systemd/systemd/commit/edda44605f06a41fb86b7ab8128dcf99161d2344 + install -Dm775 "$(make -s KERNELRELEASE="${KERNEL_NAME}" image_name)" "$modulesdir/vmlinuz" + + # Used by mkinitcpio to name the kernel + echo "${KERNEL_NAME}" > pkgbase + install -Dm775 pkgbase "$modulesdir/pkgbase" + rm pkgbase + + # Install modules suppressing depmod + ZSTD_CLEVEL=19 make -j$MAKE_CORES KERNELRELEASE="${KERNEL_NAME}" INSTALL_MOD_PATH="${MODULES_DIR}" INSTALL_MOD_STRIP=1 DEPMOD=/usr/bin/depmod modules_install + + # remove build links + rm -rf "${modulesdir}/build" + + # Install api-headers (taken from _package-api-headers) + make -j$MAKE_CORES KERNELRELEASE="${KERNEL_NAME}" INSTALL_HDR_PATH="${MODULES_DIR}" headers_install + + # Install kernel headers (taken from _package-headers) + local builddir="${MODULES_DIR}/lib/modules/${KERNEL_NAME}/build" + mkdir -p -m 755 "${builddir}" + + # Install build files + install -Dt "$builddir" -m775 .config Makefile Module.symvers System.map version vmlinux + + if ls -lah | grep -Fq "localversion."; then + for localversion in localversion.*; do + install -Dt "$builddir" -m775 "${localversion}" + done + fi + + install -Dt "$builddir/kernel" -m775 kernel/Makefile + install -Dt "$builddir/arch/x86" -m775 arch/x86/Makefile + cp -t "$builddir" -a scripts + + # required when STACK_VALIDATION is enabled + install -Dt "$builddir/tools/objtool" tools/objtool/objtool + + # required when DEBUG_INFO_BTF_MODULES is enabled + install -Dt "$builddir/tools/bpf/resolve_btfids" tools/bpf/resolve_btfids/resolve_btfids + + # Install headers + cp -t "$builddir" -a include + cp -t "$builddir/arch/x86" -a arch/x86/include + install -Dt "$builddir/arch/x86/kernel" -m775 arch/x86/kernel/asm-offsets.s + + install -Dt "$builddir/drivers/md" -m775 drivers/md/*.h + + install -Dt "$builddir/net/mac80211" -m775 net/mac80211/*.h + + # https://bugs.archlinux.org/task/13146 + install -Dt "$builddir/drivers/media/i2c" -m775 drivers/media/i2c/msp3400-driver.h + + # https://bugs.archlinux.org/task/20402 + install -Dt "$builddir/drivers/media/usb/dvb-usb" -m775 drivers/media/usb/dvb-usb/*.h + install -Dt "$builddir/drivers/media/dvb-frontends" -m775 drivers/media/dvb-frontends/*.h + install -Dt "$builddir/drivers/media/tuners" -m775 drivers/media/tuners/*.h + + # https://bugs.archlinux.org/task/71392 + install -Dt "$builddir/drivers/iio/common/hid-sensors" -m775 drivers/iio/common/hid-sensors/*.h + + # Install Kconfig files + find . -name 'Kconfig*' -exec install -Dm775 {} "$builddir/{}" \; + + # Remove unneeded architectures + local arch + for arch in "$builddir"/arch/*/; do + [[ $arch = */x86/ ]] && continue + echo "Removing $(basename "$arch")" + rm -r "$arch" + done + + # Remove documentation + rm -r "$builddir/Documentation" + + # Remove broken symlinks + find -L "$builddir" -type l -printf 'Removing %P\n' -delete + + # Remove loose objects + find "$builddir" -type f -name '*.o' -printf 'Removing %P\n' -delete + + if [ -z "${STRIP_STATIC}" ]; then + local STRIP_STATIC="--strip-debug" + fi + + if [ -z "${STRIP_SHARED}" ]; then + local STRIP_SHARED="--strip-unneeded" + fi + + if [ -z "${STRIP_BINARIES}" ]; then + local STRIP_BINARIES="--strip-all" + fi + + # Strip build tools + local file + while read -rd '' file; do + case "$(file -Sib "$file")" in + application/x-sharedlib\;*) # Libraries (.so) + strip -v $STRIP_SHARED "$file" ;; + application/x-archive\;*) # Libraries (.a) + strip -v $STRIP_STATIC "$file" ;; + application/x-executable\;*) # Binaries + strip -v $STRIP_BINARIES "$file" ;; + application/x-pie-executable\;*) # Relocatable binaries + strip -v $STRIP_SHARED "$file" ;; + esac + done < <(find "$builddir" -type f -perm -u+x ! -name vmlinux -print0) + + # Strip vmlinux + strip -v $STRIP_STATIC "$builddir/vmlinux" + + # Add symlinks + mkdir -p -m 755 "${MODULES_DIR}/src" + + ln -sr "$builddir" "${MODULES_DIR}/src/${KERNEL_NAME}" + + STATE="INITRAMFS" + ;; + "INITRAMFS") + + # This has worked: + # dracut --force --add-drivers " xhci_pci usbhid " --filesystems "overlay btrfs" --strip --aggressive-strip --reproducible --kver ${KERNEL_NAME} --kmoddir "${MODULES_DIR}/lib/modules/${KERNEL_NAME}" "${EFI_MOUNT_PATH}/initramfs-frzr.img" + + if ! dracut --force --filesystems "overlay btrfs" --strip --aggressive-strip --fstab --reproducible --kver ${KERNEL_NAME} --kmoddir "${MODULES_DIR}/lib/modules/${KERNEL_NAME}" "${EFI_MOUNT_PATH}/initramfs-frzr.img"; then + #TASK_ERROR=1 + #TASK_ERROR_MSG="ERROR: Could not generate a valid initramfs" + #STATE="FAIL" + #send_data + #continue + echo "WARNING: initramfs might not be complete" + fi + + #if ! mkinitcpio -c "${BASH_SOURCE%/*}/../lib/frzr/mkinitcpio.conf" -g "${EFI_MOUNT_PATH}/initramfs-frzr.img" -k "${KERNEL_NAME}" -r "${MODULES_DIR}"; then + # #TASK_ERROR=1 + # #TASK_ERROR_MSG="ERROR: Could not generate a valid initramfs" + # #STATE="FAIL" + # #send_data + # #continue + # echo "WARNING: initramfs might not be complete" + #fi + + if ! cp "$modulesdir/vmlinuz" "${EFI_MOUNT_PATH}/vmlinuz-frzr"; then + TASK_ERROR=1 + TASK_ERROR_MSG="ERROR: Could not copy '$modulesdir/vmlinuz' to '${EFI_MOUNT_PATH}/vmlinuz-frzr'" + STATE="FAIL" + send_data + continue + fi + + STATE="BOOTLOADER" + ;; + "BOOTLOADER") + # Read additional boot arguments (to be added to the kernel cmdline) from the user file + local additional_arguments="" + if [ -f "${EFI_MOUNT_PATH}/frzr_bootconfig.conf" ]; then + local user_bootconf_args=$(cat "${EFI_MOUNT_PATH}/frzr_bootconfig.conf") + additional_arguments="$additional_arguments ${user_bootconf_args}" + else + TASK_WARNING=1 + TASK_WARNING_MSG="Could not read '${EFI_MOUNT_PATH}/frzr_bootconfig.conf': deployment cmdline will be used" + send_data + fi + + # Write "${EFI_MOUNT_PATH}/loader/entries/frzr_kernel.conf" so that frzr will make use of the new kernel + # WARNING: version being empty has special meaning + # WARNING: btrfs_rootfs_uuid being empty means gpt-auto will be used + local boot_entry_generation_res=$(generate_systemd_boot_cfg "" "frzr_kernel.conf" "frzr-kernel" "${EFI_MOUNT_PATH}" "no_ucode" "no_ucode" "vmlinuz-frzr" "initramfs-frzr.img" "" "") + if echo "${boot_entry_generation_res}" | grep -Fq "ERROR"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not generate systemd-boot entry: ${boot_entry_generation_res}" + STATE="FAIL" + send_data + continue + fi + + # This is used to update the EFI partition: setting up systemd-boot (or whatever else bootlader might be supported) to boot the new deployment + local efi_update_result=$(frzr-bootloader) + if echo "${efi_update_result}" | grep -Fq 'ERROR'; then + # bootloader configuration could not be updated + TASK_ERROR=1 + TASK_ERROR_MSG="Could not update the EFI partition: ${efi_update_result}" + STATE="FAIL" + send_data + continue + fi + + STATE="SUCCESS" + ;; + "SUCCESS") + # This state should only be used if the unlock completed without errors + #TASK_STATE="SUCCESS" + + echo "frzr kernel deployment succeeded" + + RUNNING=false + ;; + "FAIL") + # This state should only be used if the unlock failed + + #TASK_STATE="FAIL" + + echo "ERROR: frzr-kernel failed: ${TASK_ERROR_MSG}" + + RUNNING=false + ;; + *) + TASK_STATE="UNKNOWN_ERROR" + echo "ERROR: Something went terribly wrong in $(basename $0)" + RUNNING=false + ;; + esac + done + + # umount the efi path + if [ "${MOUNTED_EFI_MOUNT_PATH}" = "yes" ]; then + if mountpoint -q "${EFI_MOUNT_PATH}"; then + sudo umount -l "${EFI_MOUNT_PATH}" + fi + fi + + # umount the frzr_root subvolume (if it was mounted by this tool and not externally) + if [ "${MOUNTED_MOUNT_PATH}" = "yes" ]; then + if mountpoint -q "${MOUNT_PATH}"; then + sudo umount -l "${MOUNT_PATH}" + fi + fi +} diff --git a/__frzr-unlock b/__frzr-unlock new file mode 100644 index 0000000..f5244f3 --- /dev/null +++ b/__frzr-unlock @@ -0,0 +1,168 @@ +#! /bin/bash + +set -o pipefail + +# import methods +#source "${BASH_SOURCE%/*}/__frzr" "$@" + +frzr_unlock() { + # by default the deployment is the running one + # and NAME will be the result of frzr-release + DEPLOY_PATH="/" + SUBVOL="/" + NAME="" + + RUNNING=true + STATE="FRZR_DEPLOY_CHECK" + while $RUNNING; do + case "$STATE" in + "FRZR_DEPLOY_CHECK") + TASK_STATE="CHECK" + + TASK_MSG="Checking for root privileges" + send_data + if [ $EUID -ne 0 ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="$(basename $0) not run as root" + STATE="FAIL" + send_data + continue + fi + + while (("$#")); do + case $1 in + #--check) + # FRZR_CHECK_UPDATE=1 + # shift + # ;; + #--steam-progress) + # FRZR_STEAM_PROGRESS=1 + # shift + # ;; + -* | --*) + TASK_ERROR=1 + TASK_ERROR_MSG="Unknown argument $1" + STATE="FAIL" + ;; + *) # preserve positional arguments + FRZR_PARAMS="${FRZR_PARAMS}$1 " # Use trailing space for the match below + shift + ;; + esac + done + + # keep only the first param as source + FRZR_SOURCE="${FRZR_PARAMS%% *}" + if frzr-release > /dev/null; then + CURRENT=`frzr-release` + fi + + STATE="BEGIN" + ;; + "BEGIN") + FRZR_VERSION=$(frzr-version) + if echo "${FRZR_VERSION}" | grep -Fq "ERROR"; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not fetch frzr version: ${FRZR_VERSION}" + STATE="FAIL" + send_data + continue + fi + + # Make sure the frzr_root is mounted during the deployment procedure + # this code is based on the fact that when a btrfs filesystem is created + # the default subvolid that is created contextually has the ID set to 256 + # also as a matter of fact in btrfs is impossible to change subvolumes IDs + MOUNT_PATH="/frzr_root" + if ! mountpoint -q "${MOUNT_PATH}" && ls -1 /dev/disk/by-label | grep frzr_root > /dev/null; then + MOUNT_PATH="/tmp/frzr_root" + #TASK_MSG="Preparing '${MOUNT_PATH}' to be used as the main subvolume mount path" + mkdir -p ${MOUNT_PATH} + if mount -L frzr_root -t btrfs -o subvolid=5,rw "${MOUNT_PATH}"; then + MOUNTED_MOUNT_PATH="yes" + fi + sleep 5 + fi + + if mountpoint -q "${MOUNT_PATH}" && ls -1 /dev/disk/by-label | grep frzr_root > /dev/null; then + STATE="RELEASE_CHECK" + else + echo "frzr-unlock failed: could not mount frzr_root" + STATE="FAIL" + continue + fi + ;; + "RELEASE_CHECK") + # If this is user-specified we should unlock that deployment in particular + NAME="${FRZR_SOURCE}" + if [ -z "${NAME}" ]; then + NAME="${CURRENT}" + else + DEPLOY_PATH="${MOUNT_PATH}/deployments" + SUBVOL="${DEPLOY_PATH}/${NAME}" + + # Make sure DEPLOY_PATH exists + mkdir -p "${DEPLOY_PATH}" + if [ ! -d "${DEPLOY_PATH}" ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not create ${DEPLOY_PATH} to to store deployments" + STATE="FAIL" + continue + fi + + # Make sure SUBVOL exists + if [ ! -d "${SUBVOL}" ]; then + TASK_ERROR=1 + TASK_ERROR_MSG="Could not find deployment '${NAME}', '${SUBVOL}' was searched" + STATE="FAIL" + continue + fi + fi + + STATE="UNLOCK" + ;; + "UNLOCK") + # set rootfs btrfs subvolume to read-write mode + UNLOCK_RESULT=$(execute_unlock "${NAME}" "${SUBVOL}" "${MOUNT_PATH}" "${FRZR_VERSION}") + if echo "${UNLOCK_RESULT}" | grep -Fq 'ERROR'; then + echo "frzr deployment ${NAME} unlock failed: ${UNLOCK_RESULT}" + STATE="FAIL" + continue + fi + + systemctl daemon-reload + + STATE="SUCCESS" + ;; + "SUCCESS") + # This state should only be used if the unlock completed without errors + #TASK_STATE="SUCCESS" + + echo "frzr deployment ${NAME} unlock succeeded, please reboot to use the unlocked deployment" + + RUNNING=false + ;; + "FAIL") + # This state should only be used if the unlock failed + + #TASK_STATE="FAIL" + + echo "frzr-unlock failed for deployment ${NAME}: ${UNLOCK_RESULT}" + + RUNNING=false + ;; + *) + TASK_STATE="UNKNOWN_ERROR" + echo "ERROR: Something went terribly wrong" + RUNNING=false + ;; + esac + done + + # umount the frzr_root subvolume (if it was mounted by this tool and not externally) + if [ "${MOUNTED_MOUNT_PATH}" = "yes" ]; then + if mountpoint -q "${MOUNT_PATH}"; then + umount -l "${MOUNT_PATH}" + fi + fi +} \ No newline at end of file diff --git a/__frzr-version b/__frzr-version new file mode 100644 index 0000000..492ba46 --- /dev/null +++ b/__frzr-version @@ -0,0 +1,10 @@ +#! /bin/bash + +set -e + +# import methods +#source "${BASH_SOURCE%/*}/__frzr" "$@" + +frzr_version() { + echo "1.0.0" +} \ No newline at end of file diff --git a/frzr b/frzr new file mode 100755 index 0000000..b1b7830 --- /dev/null +++ b/frzr @@ -0,0 +1,154 @@ +#! /bin/bash + +set -Ee + +if ! groups | grep -Fq "frzr"; then + if [ "$EUID" -ne 0 ]; then + echo "Current user is not in the frzr group" + exit 1 + else + echo "Running frzr as root, but root is not a member of frzr group" + fi +fi + +# Import methods +source "${BASH_SOURCE%/*}/__frzr" "$@" + +# Create the tracker file before we start +write_tracker_file + +usage(){ + echo "[Usage] +frzr deploy [Check for system updates and update the system if available] +frzr bootloader [Install the bootloader and create entries for every deployed image] +frzr unlock (deployment) [Unlock the specified deployment, or the running one if deployment is not specified] +frzr kernel [Deploy a custom kernel] +frzr set-channel [Set the update channel stable/testing/unstable] +frzr get-channel [Get the update channel currently in use] +frzr version [Get the version of FRZR] +frzr build-initramfs [Build the initramfs for the kernel] +frzr configure-tweaks [Configure system specific quirks] +frzr bootstrap [Format and configure a drive to be used with FRZR]" +} + +# Catch unexpected errors and give feedback +handle_error() { + local function_name="$1" + local line_number="$2" + local command="$3" + + echo "Error occurred in function '${function_name}' on line ${line_number}. Command: '${command}'" + + frzr_status + + if [ -f /tmp/frzr.lock ]; then + rm /tmp/frzr.lock + fi +} + +# Set up trap to catch errors and call handle_error function +trap 'handle_error "${FUNCNAME[0]}" "$LINENO" "$BASH_COMMAND"' ERR + +if [ $# -eq 0 ]; then + usage + exit 1 +fi + +function=$1 +arg1=$2 +arg2=$3 +arg3=$4 + +if [ $function == "-h" ] || [ $function == "help" ]; then + usage +elif [ $function == "bootstrap" ]; then + source "${BASH_SOURCE%/*}/frzr-bootstrap" "${arg1}" "${arg2}" "${arg3}" # username, disk, clean/repair install + RESULT=$? + exit $RESULT +elif [ $function == "deploy" ]; then + frzr_check_bootenv + # We don't want to pass the function parameter to __frzr-deploy + shift + #flock -E 255 -n /tmp/frzr.lock "frzr-deploy" "$@" + source "${BASH_SOURCE%/*}/frzr-deploy" "$@" + RESULT=$TASK_ERROR + #if [ $RESULT == 255 ]; then + # echo "ERROR: $(basename $0) is already running" + #fi + exit $RESULT +elif [ $function == "unlock" ]; then + frzr_check_bootenv + # We don't want to pass the function parameter to __frzr-unlock + shift + #flock -E 255 -n /tmp/frzr.lock "frzr-depunlockloy" "$@" + source "${BASH_SOURCE%/*}/frzr-unlock" "$@" + RESULT=$TASK_ERROR + #if [ $RESULT == 255 ]; then + # echo "ERROR: $(basename $0) is already running" + #fi + exit $RESULT +elif [ $function == "bootloader" ]; then + frzr_check_bootenv + # We don't want to pass the function parameter to __frzr-bootloader + shift + + #flock -E 255 -n /tmp/frzr.lock "frzr-bootloader" "$@" + source "${BASH_SOURCE%/*}/frzr-bootloader" "$@" + RESULT=$? + + #if [ $RESULT == 255 ]; then + # echo "ERROR: $(basename $0) is already running" + #fi + + exit $RESULT +elif [ $function == "kernel" ]; then + frzr_check_bootenv + # We don't want to pass the function parameter to __frzr-kernel + shift + source "${BASH_SOURCE%/*}/frzr-kernel" "$@" + RESULT=$? + + #if [ $RESULT == 255 ]; then + # echo "ERROR: $(basename $0) is already running" + #fi + + exit $RESULT +elif [ $function == "release" ]; then + source "${BASH_SOURCE%/*}/frzr-release" "$@" +elif [ $function == "version" ]; then + frzr_check_bootenv + # We don't want to pass the function parameter to __frzr-version + shift + + #flock -E 255 -n /tmp/frzr.lock "frzr-version" "$@" + source "${BASH_SOURCE%/*}/frzr-version" "$@" + RESULT=$? + + #if [ $RESULT == 255 ]; then + # echo "ERROR: $(basename $0) is already running" + #fi + + exit $RESULT +elif [ $function == "set-channel" ]; then + echo "set channel" + #TODO create frzr-channel to set target channel + #frzr-channel $arg1 +elif [ $function == "get-channel" ]; then + echo "get-channel" + #TODO create frzr-channel to get target channel + #echo ${FRZR_ROOT}/source +elif [ $function == "build-initramfs" ]; then + source frzr-initramfs +elif [ $function == "configure-tweaks" ]; then + source frzr-tweaks +elif [ $function == "package-options" ]; then + #User selected packages to be added to the install + source frzr-extras +else + echo "invalid argument" +fi + +# print out the latest error in stderr (this is meant for debugging) +if [ ! -z "${TASK_ERROR_MSG}" ]; then + echo "${TASK_ERROR_MSG}" 1>&2 +fi diff --git a/frzr-bootloader b/frzr-bootloader new file mode 100755 index 0000000..8a49f81 --- /dev/null +++ b/frzr-bootloader @@ -0,0 +1,10 @@ +#! /bin/bash + +source "${BASH_SOURCE%/*}/__frzr-bootloader" + +# TODO: check the file lock exists + +frzr_bootloader "$@" +RESULT=$? + +exit $RESULT diff --git a/frzr-bootstrap b/frzr-bootstrap index 349ef7f..083f0d0 100755 --- a/frzr-bootstrap +++ b/frzr-bootstrap @@ -1,110 +1,10 @@ #! /bin/bash -set -e +source "${BASH_SOURCE%/*}/__frzr-bootstrap" -if [ $EUID -ne 0 ]; then - echo "$(basename $0) must be run as root" - exit 1 -fi +# TODO: check the file lock exists -MOUNT_PATH=/tmp/frzr_root - -device_list=() -device_output=`lsblk --list -n -o name,model,size,type | grep disk | tr -s ' ' '\t'` - -while read -r line; do - name=/dev/`echo "$line" | cut -f 1` - model=`echo "$line" | cut -f 2` - size=`echo "$line" | cut -f 3` - device_list+=($name) - device_list+=("$model ($size)") -done <<< "$device_output" - -DISK=$(whiptail --nocancel --menu "Choose a disk to install to:" 20 50 5 "${device_list[@]}" 3>&1 1>&2 2>&3) - -# Checking for existing installation - -REPAIR=false - -if (lsblk -o label ${DISK} | grep -q frzr_efi); then - echo "Existing installation found" - - if (whiptail --yesno --yes-button "Repair" --no-button "Clean" "WARNING: $DISK appears to already have a system installed. Would you like to repair it or do a clean install?\n\nNOTE: A clean install will delete everything on the disk, but a repair install will preserve your user data." 13 70); then - echo "User chose to do a repair install" - REPAIR=true - else - echo "User chose to do a clean install" - fi -else - echo "Existing installation not found" -fi - - - -########## Doing a repair install - -if [ "${REPAIR}" == true ]; then - - mkdir -p ${MOUNT_PATH} - INSTALL_MOUNT=$(fdisk -o Device --list ${DISK} | grep "^${DISK}.*2$") - BOOT_EFI=$(fdisk -o Device --list ${DISK} | grep "^${DISK}.*1$") - mount ${INSTALL_MOUNT} ${MOUNT_PATH} - mount -t vfat ${BOOT_EFI} ${MOUNT_PATH}/boot/ - rm -rf ${MOUNT_PATH}/boot/* - bootctl --esp-path=${MOUNT_PATH}/boot/ install - - echo "deleting subvolume..." - btrfs subvolume delete ${MOUNT_PATH}/deployments/* || true - - rm -rf ${MOUNT_PATH}/etc/* - - exit 0 -fi - - - -########## Doing a fresh install - -if ! (whiptail --yesno "WARNING: $DISK will now be formatted. All data on the disk will be lost. Do you wish to proceed?" 10 50); then - echo "installation aborted" - exit 1 -fi - -USERNAME=user - -if [ ! -z $1 ]; then - USERNAME=$1 -fi - -mkdir -p ${MOUNT_PATH} - -# create partition table and create and mount the btrfs filesystem -parted --script ${DISK} \ - mklabel gpt \ - mkpart primary fat32 1MiB 512MiB \ - set 1 esp on \ - mkpart primary 512MiB 100% - -PART1=$(fdisk -o Device --list ${DISK} | grep "^${DISK}.*1$") -PART2=$(fdisk -o Device --list ${DISK} | grep "^${DISK}.*2$") - -mkfs.btrfs -L frzr_root -f ${PART2} -mount -t btrfs -o nodatacow ${PART2} ${MOUNT_PATH} - -btrfs subvolume create ${MOUNT_PATH}/var -btrfs subvolume create ${MOUNT_PATH}/home - -mkdir -p ${MOUNT_PATH}/home/${USERNAME} -chown 1000:1000 ${MOUNT_PATH}/home/${USERNAME} - -mkdir ${MOUNT_PATH}/boot -mkdir -p ${MOUNT_PATH}/etc -mkdir -p ${MOUNT_PATH}/.etc - - -# setup boot partition & install bootloader -mkfs.vfat ${PART1} -dosfslabel ${PART1} frzr_efi -mount -t vfat ${PART1} ${MOUNT_PATH}/boot/ -bootctl --esp-path=${MOUNT_PATH}/boot/ install -parted ${DISK} set 1 boot on +frzr_bootstrap "$@" +# We don't need to check the exit status here because this is being ran and handled in frzr +# RESULT=$? +# exit $RESULT diff --git a/frzr-deploy b/frzr-deploy index 88f4efc..7edb6f4 100755 --- a/frzr-deploy +++ b/frzr-deploy @@ -1,16 +1,10 @@ #! /bin/bash -if [ $EUID -ne 0 ]; then - echo "$(basename $0) must be run as root" - exit 1 -fi +source "${BASH_SOURCE%/*}/__frzr-deploy" +# TODO: check the file lock exists -flock -E 255 -n /tmp/frzr.lock ${BASH_SOURCE%/*}/__frzr-deploy $@ -RESULT=$? - -if [ $RESULT == 255 ]; then - echo "$(basename $0) is already running" -fi - -exit $RESULT +frzr_deploy "$@" +# We don't need to check the exit status here because this is being ran and handled in frzr +# RESULT=$? +# exit $RESULT diff --git a/frzr-extras b/frzr-extras new file mode 100755 index 0000000..42db2c8 --- /dev/null +++ b/frzr-extras @@ -0,0 +1,30 @@ +#! /bin/bash + +if [ $EUID -ne 0 ]; then + echo "$(basename $0) must be run as root" + exit 1 +fi + +# Define the list of packages to be installed +PACKAGES=( + "DECKY" + "EMUDECK" + # Add more packages here as needed +) + +# Iterate over each package +for PACKAGE in "${PACKAGES[@]}"; do + case "$PACKAGE" in + "DECKY") + COMMAND=' +curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/install_release.sh | sh +' + ;; + "EMUDECK") + COMMAND='' + # Add more cases for other packages here + ;; + esac + + frzr_chroot "${SUBVOL}" "$COMMAND" +done diff --git a/frzr-initramfs b/frzr-initramfs deleted file mode 100755 index 13afcff..0000000 --- a/frzr-initramfs +++ /dev/null @@ -1,132 +0,0 @@ -#! /bin/bash - -if [ $EUID -ne 0 ]; then - echo "$(basename $0) must be run as root" - exit 1 -fi - -# Check if script is being ran frmo the install media -if [ -d /tmp/frzr_root ]; then - - if [ -d "${SUBVOL}" ]; then - - cd ${SUBVOL} - # Mount necessary file systems - mount -t proc /proc proc/ - mount -t sysfs /sys sys/ - mount --rbind /dev dev/ - - # Set R/W permissions - btrfs property set -fts ${SUBVOL} ro false - chroot ${SUBVOL} /bin/bash < /etc/mkinitcpio.d/\${NAME%%-*}.preset - -### Rebuild Initramfs with custom preset -mkinitcpio -p \${NAME%%-*} -EOF - # Set back to R/O permissions - btrfs property set -fts ${SUBVOL} ro true - # unmount filesystems - umount -l ${SUBVOL}/proc - umount -l ${SUBVOL}/sys - mount --make-rslave ${SUBVOL}/dev - umount -l ${SUBVOL}/dev - else - echo "No deployment directory found" - exit 1 - fi -else - echo "We don't appear to be running from an arch install media" -fi - - -# Build initramfs from within a deployed system -if [ -d /frzr_root ]; then - - if [ -n "$SUBVOL" ]; then - ### Grabbing name of the new deployed system - ID=$(grep '^ID=' "$SUBVOL/etc/os-release" | awk -F= '{ print $2 }' | sed 's/"//g') - VERSIONID=$(grep '^VERSION_ID=' "$SUBVOL/etc/os-release" | awk -F= '{ print $2 }' | sed 's/"//g') - BUILDID=$(grep '^BUILD_ID=' "$SUBVOL/etc/os-release" | awk -F= '{ print $2 }' | sed 's/"//g') - else - ### Grabbing name of the currently deployed system - ID=$(grep '^ID=' /etc/os-release | awk -F= '{ print $2 }' | sed 's/"//g') - VERSIONID=$(grep '^VERSION_ID=' /etc/os-release | awk -F= '{ print $2 }' | sed 's/"//g') - BUILDID=$(grep '^BUILD_ID=' /etc/os-release | awk -F= '{ print $2 }' | sed 's/"//g') - fi - - BUILD="$ID"-"$VERSIONID"_"$BUILDID" - DEPLOYMENT_PATH="/frzr_root/deployments/$BUILD" - - # Get locked state - RELOCK=0 - LOCK_STATE=$(btrfs property get -fts "$DEPLOYMENT_PATH") - if [[ $LOCK_STATE == *"ro=true"* ]]; then - btrfs property set -fts ${DEPLOYMENT_PATH} ro false - RELOCK=1 - else - echo "Filesystem appears to be unlocked" - fi - - echo "Generating configuration for $BUILD" - - ### Rebuild Initramfs with custom preset - - if [ -n "$SUBVOL" ]; then - cd ${SUBVOL} - # Mount necessary file systems - mount -t proc /proc proc/ - mount -t sysfs /sys sys/ - mount --rbind /dev dev/ - - # We have to chroot for new images or else the kernel version can cause initramfs building to fail - chroot ${SUBVOL} /bin/bash < /etc/mkinitcpio.d/\${NAME%%-*}.preset - -### Rebuild Initramfs with custom preset -mkinitcpio -p \${NAME%%-*} -EOF - umount -l ${SUBVOL}/proc - umount -l ${SUBVOL}/sys - mount --make-rslave ${SUBVOL}/dev - umount -l ${SUBVOL}/dev - else - echo ' -ALL_config="/etc/mkinitcpio.conf" -ALL_kver="/boot/'$BUILD'/vmlinuz-linux" - -PRESETS="default" - -default_image="/boot/'$BUILD'/initramfs-linux.img" -' > /etc/mkinitcpio.d/$ID.preset - # If we are not doing a deployment then this will be used for local installs to rebuild initramfs - mkinitcpio -p $ID - fi - - if [[ $RELOCK == 1 ]]; then - btrfs property set -fts ${DEPLOYMENT_PATH} ro true - else - # Move rebuilt images to the unlocked location if system was unlocked prior - cp /boot/$BUILD/* /boot - fi -fi diff --git a/frzr-kernel b/frzr-kernel new file mode 100644 index 0000000..6307757 --- /dev/null +++ b/frzr-kernel @@ -0,0 +1,10 @@ +#! /bin/bash + +source "${BASH_SOURCE%/*}/__frzr-kernel" + +# TODO: check the file lock exists + +frzr_kernel "$@" +# We don't need to check the exit status here because this is being ran and handled in frzr +# RESULT=$? +# exit $RESULT diff --git a/frzr-release b/frzr-release index aa7abb6..791bfb3 100755 --- a/frzr-release +++ b/frzr-release @@ -2,9 +2,9 @@ set -e -if [ ! -e /build_info ]; then - echo "Not currently running a frzr deployment" +if [ -e "/build_info" ]; then + cat "/build_info" | head -1 +else + echo "ERROR: not currently running a frzr deployment" exit 1 fi - -cat /build_info | head -1 diff --git a/frzr-source b/frzr-source new file mode 100755 index 0000000..af037c9 --- /dev/null +++ b/frzr-source @@ -0,0 +1,8 @@ +#! /bin/bash + +if [ $EUID -ne 0 ]; then + echo "$(basename $0) must be run as root" + exit 1 +fi + +#TODO Set up logic to get and set the target source for FRZR to use diff --git a/frzr-tweaks b/frzr-tweaks deleted file mode 100755 index becaf0d..0000000 --- a/frzr-tweaks +++ /dev/null @@ -1,24 +0,0 @@ -#! /bin/bash - -if [ $EUID -ne 0 ]; then - echo "$(basename $0) must be run as root" - exit 1 -fi - -# Check if device quirks exist in the new image when frzr-deploy is used -if [ -v SUBVOL ]; then - echo "Checking newly deployed system for device-quirks" - if [ -e "${SUBVOL}/usr/share/device-quirks/id-device" ]; then - ${SUBVOL}/usr/share/device-quirks/id-device - else - echo "Device-quirks package was not found, skipping..." - fi -else -# Check if device quirks exist when frzr-tweaks is ran directly - echo "Checking for device-quirks" - if [ -e "/usr/share/device-quirks/id-device" ]; then - /usr/share/device-quirks/id-device - else - echo "Device-quirks packages was not found, skipping..." - fi -fi diff --git a/frzr-unlock b/frzr-unlock index e8cdb72..509e3e3 100755 --- a/frzr-unlock +++ b/frzr-unlock @@ -1,55 +1,10 @@ #! /bin/bash -set -e +source "${BASH_SOURCE%/*}/__frzr-unlock" +# TODO: check the file lock exists -if [ $EUID -ne 0 ]; then - echo "$(basename $0) must be run as root" - exit 1 -fi - -if ! frzr-release > /dev/null; then - echo "Not currently running a frzr deployment" - exit 1 -fi - -MOUNT_PATH=/frzr_root - -if ! mountpoint -q ${MOUNT_PATH}; then - mkdir -p ${MOUNT_PATH} - mount -L frzr_root ${MOUNT_PATH} - sleep 5 -fi - -if ! mountpoint -q ${MOUNT_PATH}/boot && ls -1 /dev/disk/by-label | grep frzr_efi > /dev/null; then - mkdir -p ${MOUNT_PATH}/boot - mount -L frzr_efi ${MOUNT_PATH}/boot - sleep 5 -fi - -DEPLOYMENT=$(frzr-release) - -# set to read-write mode -mount -o remount,rw / -btrfs property set -fts /frzr_root/deployments/${DEPLOYMENT} ro false -sed -i -e 's/,ro,/,rw,/' /etc/fstab -systemctl daemon-reload - - -# move kernel/initrd and ucode to standard location -BOOT_CFG="${MOUNT_PATH}/boot/loader/entries/frzr.conf" -if [ -f "${BOOT_CFG}" ]; then - # guard is for compatibility with systems still using syslinux during the transition to systemd-boot - cp ${MOUNT_PATH}/boot/${DEPLOYMENT}/* ${MOUNT_PATH}/boot/ - sed -i ${BOOT_CFG} -e s,/${DEPLOYMENT}/,/,g -fi - - -# copy package database and refresh -if [ -d /usr/var/lib/pacman/local ] && [ ! -d /var/lib/pacman/local ]; then - mkdir -p /var/lib/pacman - cp -r /usr/var/lib/pacman/local /var/lib/pacman/ - pacman -Sy -fi - -echo "frzr deployment ${DEPLOYMENT} unlocked" +frzr_unlock "$@" +# We don't need to check the exit status here because this is being ran and handled in frzr +# RESULT=$? +# exit $RESULT diff --git a/frzr-version b/frzr-version new file mode 100644 index 0000000..aa992ee --- /dev/null +++ b/frzr-version @@ -0,0 +1,10 @@ +#! /bin/bash + +source "${BASH_SOURCE%/*}/__frzr-version" + +# TODO: check the file lock exists + +frzr_version "$@" +# We don't need to check the exit status here because this is being ran and handled in frzr +# RESULT=$? +# exit $RESULT diff --git a/mkinitcpio.conf b/mkinitcpio.conf new file mode 100644 index 0000000..7150c0e --- /dev/null +++ b/mkinitcpio.conf @@ -0,0 +1,8 @@ +# vim:set ft=sh + +MODULES=(dm_mod btrfs ext4 sha256 sha512 overlay) +BINARIES=() +FILES=() +HOOKS=(microcode acpi_override systemd modconf kms resume keyboard sd-vconsole block filesystems fsck) +COMPRESSION="xz" +COMPRESSION_OPTIONS=(-v -9e) diff --git a/test/run.sh b/test/run.sh index 7d30f75..aff4bff 100755 --- a/test/run.sh +++ b/test/run.sh @@ -1,6 +1,6 @@ #! /bin/bash -source ../__frzr-deploy +source ../__frzr check() { if [ "$2" == "$3" ]; then @@ -55,37 +55,3 @@ check 'should be able to select older point versions' $(cat test4.dat check 'should select latest matching asset' $(cat test4a.dat | get_img_url 1) 'chimeraos-1_0000002.img.tar.xz' echo -echo '== get_boot_cfg' -check 'should return expected boot config' "$(get_boot_cfg '12_abcdef' 'initrd /12_abcdef/amd-ucode.img' 'initrd /12_abcdef/intel-ucode.img' 'ibt=off split_lock_detect=off')" \ -'title 12_abcdef -linux /12_abcdef/vmlinuz-linux -initrd /12_abcdef/amd-ucode.img -initrd /12_abcdef/intel-ucode.img -initrd /12_abcdef/initramfs-linux.img -options root=LABEL=frzr_root rw rootflags=subvol=deployments/12_abcdef quiet splash loglevel=3 rd.systemd.show_status=auto rd.udev.log_priority=3 ibt=off split_lock_detect=off' - - - -echo -echo '== get_deployment_to_delete' - -base='/tmp/frzr_test/get_deployment_to_delete' -mkdir -p "${base}/config" -mkdir -p "${base}/deployments" - -mkdir "${base}/deployments/3_a" -check 'should return empty if a valid deployment to delete is not found (no other deployments, no deployments in config)' $(get_deployment_to_delete '3_a' "${base}/config/boot.cfg" "${base}/deployments") "" - -mkdir "${base}/deployments/4_a" -check 'should return a deployment if it is not the current version and not referenced in the boot config (one other deployment, no deployments in config)' $(get_deployment_to_delete '3_a' "${base}/config/boot.cfg" "${base}/deployments") '4_a' - -get_boot_cfg '4_a' > "${base}/config/boot.cfg" -check 'should return empty if a valid deployment to delete is not found (one other deployment which is referenced in the config)' $(get_deployment_to_delete '3_a' "${base}/config/boot.cfg" "${base}/deployments") "" - -mkdir "${base}/deployments/1_a" -mkdir "${base}/deployments/2_a" -check 'should select a single deployment which is not active and will not become active on next boot (three other deployments, one which is in config)' $(get_deployment_to_delete '3_a' "${base}/config/boot.cfg" "${base}/deployments") '1_a' - -rm -rf "${base}" - -echo