diff --git a/README.md b/README.md index 89e6c99..c76b526 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,7 @@ Edit `/etc/atomic.conf`: | `MAPPER_NAME` | `root_crypt` | dm-crypt mapper name (fallback if auto-detection fails) | | `KERNEL_PKG` | `linux` | Kernel package (linux/linux-lts/linux-zen) | | `KERNEL_PARAMS` | *(security defaults)* | Kernel command line parameters | +| `CHROOT_COMMAND` | *(none)* | Default command in snapshot chroot (overrides built-in `pacman -Syu`) | | `SBCTL_SIGN` | `0` | Sign UKI files with sbctl for Secure Boot (`0`=off, `1`=on) | | `UPGRADE_GUARD` | `1` | Upgrade guard: block direct `pacman -Syu` (`0`=off, `1`=on) | | `HOME_COPY_FILES` | *(empty)* | Files to copy into isolated home subvolumes (see [Home isolation](#home-isolation)) | diff --git a/bin/atomic-upgrade b/bin/atomic-upgrade index f6ed9ac..1e3c163 100644 --- a/bin/atomic-upgrade +++ b/bin/atomic-upgrade @@ -18,7 +18,7 @@ show_help() { cat << EOF atomic-upgrade v${VERSION} - Atomic system upgrades for Arch Linux -Usage: atomic-upgrade [OPTIONS] [-- COMMAND...] +Usage: atomic-upgrade [OPTIONS] [-- CHROOT_COMMAND...] Options: -n, --dry-run Show what would be done @@ -106,9 +106,18 @@ if [[ -n "$CUSTOM_TAG" ]]; then fi fi -# Default chroot command +# Default chroot command: use CHROOT_COMMAND from config (or default in common.sh). +# Note: CHROOT_COMMAND uses simple word splitting; args with spaces inside +# are not supported (same limitation as HOME_COPY_FILES). if [[ ${#CHROOT_CMD[@]} -eq 0 ]]; then - CHROOT_CMD=(/usr/bin/pacman -Syu) + [[ -z "${CHROOT_COMMAND:-}" ]] && CHROOT_COMMAND="/usr/bin/pacman -Syu" + read -ra CHROOT_CMD <<< "$CHROOT_COMMAND" +fi + +# Warn if CHROOT_COMMAND contains quotes (word splitting doesn't process them) +if [[ "$CHROOT_COMMAND" =~ [\'\"] ]]; then + echo "WARN: CHROOT_COMMAND contains quotes, but word splitting doesn't process them." >&2 + echo "WARN: Use CLI instead: atomic-upgrade -- pacman -S 'nvidia dkms'" >&2 fi # Verify required variables are set diff --git a/etc/atomic.conf b/etc/atomic.conf index 2758349..e5b04de 100644 --- a/etc/atomic.conf +++ b/etc/atomic.conf @@ -1,4 +1,8 @@ # /etc/atomic.conf + +# Note: inline comments start at ' #' (space + hash). +# Values containing a literal ' #' will be truncated at that point. + #BTRFS_MOUNT="/run/atomic/temp_root" #NEW_ROOT="/run/atomic/newroot" #ESP="/efi" @@ -7,8 +11,10 @@ #KERNEL_PKG="linux" #KERNEL_PARAMS="rw slab_nomerge init_on_alloc=1 page_alloc.shuffle=1 pti=on vsyscall=none randomize_kstack_offset=on debugfs=off" -# Note: inline comments start at ' #' (space + hash). -# Values containing a literal ' #' will be truncated at that point. +# Default command to run in the snapshot chroot (overrides built-in default) +# Command-line -- CHROOT_COMMAND... overrides this setting +# Note: args with spaces inside are not supported (simple word splitting) +#CHROOT_COMMAND="/usr/bin/pacman -Syu" # Sign UKI files with sbctl for Secure Boot (requires sbctl) #SBCTL_SIGN=1 diff --git a/lib/atomic/common.sh b/lib/atomic/common.sh index 3ffbeab..1de176f 100644 --- a/lib/atomic/common.sh +++ b/lib/atomic/common.sh @@ -29,6 +29,8 @@ UPGRADE_GUARD=1 HOME_COPY_FILES="" # Kernel security parameters KERNEL_PARAMS="rw slab_nomerge init_on_alloc=1 page_alloc.shuffle=1 pti=on vsyscall=none randomize_kstack_offset=on debugfs=off" +# Default chroot command (overridden by config or CLI -- CHROOT_COMMAND...) +CHROOT_COMMAND="/usr/bin/pacman -Syu" # ── Config loading (safe parser, no arbitrary code execution) ─────── @@ -46,7 +48,7 @@ load_config() { fi # Whitelist of allowed config keys - local -a allowed=(BTRFS_MOUNT NEW_ROOT ESP KEEP_GENERATIONS MAPPER_NAME KERNEL_PARAMS KERNEL_PKG SBCTL_SIGN UPGRADE_GUARD HOME_COPY_FILES) + local -a allowed=(BTRFS_MOUNT NEW_ROOT ESP KEEP_GENERATIONS MAPPER_NAME KERNEL_PARAMS KERNEL_PKG CHROOT_COMMAND SBCTL_SIGN UPGRADE_GUARD HOME_COPY_FILES) while IFS='=' read -r key value; do # Strip leading/trailing whitespace from key diff --git a/man/atomic.conf.5 b/man/atomic.conf.5 index 1365639..1713c64 100644 --- a/man/atomic.conf.5 +++ b/man/atomic.conf.5 @@ -71,6 +71,15 @@ device arguments. Default: \f[B]rw slab_nomerge init_on_alloc=1 page_alloc.shuffle=1 pti=on vsyscall=none randomize_kstack_offset=on debugfs=off\f[R]. .TP +\f[B]COMMAND\f[R] +Default command to run in the snapshot chroot, overriding the built\-in +default of \f[B]pacman \-Syu\f[R]. +Command\-line \f[B]\-\-\f[R] \f[I]COMMAND...\f[R] takes priority over +this setting. +Note: arguments with spaces inside are not supported due to simple word +splitting. +Default: \f[I](none)\f[R]. +.TP \f[B]SBCTL_SIGN\f[R] Enable UKI signing with \f[B]sbctl\f[R](8) for Secure Boot. Set to \f[B]1\f[R] to enable, \f[B]0\f[R] to disable. diff --git a/man/atomic.conf.5.md b/man/atomic.conf.5.md index da180bc..66dc31f 100644 --- a/man/atomic.conf.5.md +++ b/man/atomic.conf.5.md @@ -66,6 +66,12 @@ rejected entirely. page_alloc.shuffle=1 pti=on vsyscall=none randomize_kstack_offset=on debugfs=off**. +**CHROOT_COMMAND** +: Default command to run in the snapshot chroot, overriding the built-in + default of **pacman -Syu**. Command-line **-- CHROOT_COMMAND...** takes priority + over this setting. Note: arguments with spaces inside are not supported + due to simple word splitting. Default: *(none)*. + **SBCTL_SIGN** : Enable UKI signing with **sbctl**(8) for Secure Boot. Set to **1** to enable, **0** to disable. When disabled, UKI files are built unsigned. diff --git a/tests/test_config.sh b/tests/test_config.sh index 5f67ff3..f8444f8 100644 --- a/tests/test_config.sh +++ b/tests/test_config.sh @@ -276,7 +276,40 @@ assert_eq "HOME_COPY_FILES absent → stays empty" "" "$HOME_COPY_FILES" # Restore HOME_COPY_FILES="" +# ── CHROOT_COMMAND config ─────────────────────────────────── +section "CHROOT_COMMAND config" + +# Test: CHROOT_COMMAND loaded from config +CONFIG_FILE="${TESTDIR}/chroot_command.conf" +cat > "$CONFIG_FILE" <<'EOF' +CHROOT_COMMAND=/usr/bin/pacman -Syu +EOF +CHROOT_COMMAND="" +load_config +assert_eq "CHROOT_COMMAND loaded from config" "/usr/bin/pacman -Syu" "$CHROOT_COMMAND" +CHROOT_COMMAND="" + +# Test: CHROOT_COMMAND with quotes +CONFIG_FILE="${TESTDIR}/chroot_command_quoted.conf" +cat > "$CONFIG_FILE" <<'EOF' +CHROOT_COMMAND="/usr/bin/pacman -S nvidia" +EOF +CHROOT_COMMAND="" +load_config +assert_eq "CHROOT_COMMAND with double quotes" "/usr/bin/pacman -S nvidia" "$CHROOT_COMMAND" +CHROOT_COMMAND="" + +# Test: CHROOT_COMMAND absent → stays empty +CONFIG_FILE="${TESTDIR}/chroot_command_absent.conf" +cat > "$CONFIG_FILE" <<'EOF' +KEEP_GENERATIONS=3 +EOF +CHROOT_COMMAND="" +load_config +assert_eq "CHROOT_COMMAND absent → stays empty" "" "$CHROOT_COMMAND" + +# # ── config key whitespace trimming ───────────── section "Config key whitespace trimming" diff --git a/tests/test_upgrade.sh b/tests/test_upgrade.sh index 0ebc0f1..112a09a 100644 --- a/tests/test_upgrade.sh +++ b/tests/test_upgrade.sh @@ -196,6 +196,82 @@ assert_eq "default cmd count" "CHROOT_CMD_COUNT=2" "$(echo "$_out" | grep '^CHRO assert_eq "default cmd[0]" "CHROOT_CMD_0=/usr/bin/pacman" "$(echo "$_out" | grep '^CHROOT_CMD_0=')" assert_eq "default cmd[1]" "CHROOT_CMD_1=-Syu" "$(echo "$_out" | grep '^CHROOT_CMD_1=')" +# ── Word splitting limitation: args with spaces ──────────── + +section "Word splitting limitation (space in args)" + +CONFIG_FILE="${TESTDIR}/word_split.conf" +cat > "$CONFIG_FILE" <<'EOF' +CHROOT_COMMAND=/usr/bin/pacman -S nvidia driver +EOF + +_TEST_SCRIPT_WS="${TESTDIR}/atomic-upgrade-word-split" +sed \ + -e 's|^\(export ATOMIC_UPGRADE=1\)$|\1\nCONFIG_FILE="${CONFIG_FILE:-/etc/atomic.conf}"|' \ + -e 's/^\(\[\[ \$EUID -eq 0 \]\]\)/# \1/' \ + -e '/^# Verify required variables are set$/i\ +if [[ "${ATOMIC_EXIT_AFTER_PARSE:-}" == "1" ]]; then\ + echo "PARSE_OK"\ + echo "CHROOT_CMD_COUNT=${#CHROOT_CMD[@]}"\ + for i in "${!CHROOT_CMD[@]}"; do\ + echo "CHROOT_CMD_${i}=${CHROOT_CMD[$i]}"\ + done\ + exit 0\ +fi\ +' \ + "$SCRIPT" > "$_TEST_SCRIPT_WS" +chmod +x "$_TEST_SCRIPT_WS" + +run_cmd env CONFIG_FILE="$CONFIG_FILE" ATOMIC_EXIT_AFTER_PARSE=1 bash "$_TEST_SCRIPT_WS" +assert_eq "word split: PARSE_OK" "0" "$_rc" +assert_eq "word split: count is 4" "CHROOT_CMD_COUNT=4" "$(echo "$_out" | grep '^CHROOT_CMD_COUNT=')" +assert_eq "word split: arg[2]" "CHROOT_CMD_2=nvidia" "$(echo "$_out" | grep '^CHROOT_CMD_2=')" +assert_eq "word split: arg[3]" "CHROOT_CMD_3=driver" "$(echo "$_out" | grep '^CHROOT_CMD_3=')" + +rm -f "$_TEST_SCRIPT_WS" + +# ── CLI -- CHROOT_COMMAND priority over config CHROOT_COMMAND= ─────────── + +section "CLI -- CHROOT_COMMAND priority over config CHROOT_COMMAND=" + +CONFIG_FILE="${TESTDIR}/cli_priority.conf" +cat > "$CONFIG_FILE" <<'EOF' +CHROOT_COMMAND=/usr/bin/pacman -Syu +EOF + +_TEST_SCRIPT_CLI="${TESTDIR}/atomic-upgrade-cli-priority" +sed \ + -e 's|^\(export ATOMIC_UPGRADE=1\)$|\1\nCONFIG_FILE="${CONFIG_FILE:-/etc/atomic.conf}"|' \ + -e 's/^\(\[\[ \$EUID -eq 0 \]\]\)/# \1/' \ + -e '/^# Verify required variables are set$/i\ +if [[ "${ATOMIC_EXIT_AFTER_PARSE:-}" == "1" ]]; then\ + echo "PARSE_OK"\ + echo "DRY_RUN=${DRY_RUN}"\ + echo "CUSTOM_TAG=${CUSTOM_TAG}"\ + echo "NO_GC=${NO_GC}"\ + echo "SEPARATE_HOME=${SEPARATE_HOME}"\ + echo "COPY_FILES=${COPY_FILES}"\ + echo "CHROOT_CMD_COUNT=${#CHROOT_CMD[@]}"\ + for i in "${!CHROOT_CMD[@]}"; do\ + echo "CHROOT_CMD_${i}=${CHROOT_CMD[$i]}"\ + done\ + echo "CHROOT_COMMAND_FROM_CONFIG=${CHROOT_COMMAND:-}"\ + exit 0\ +fi\ +' \ + "$SCRIPT" > "$_TEST_SCRIPT_CLI" +chmod +x "$_TEST_SCRIPT_CLI" + +run_cmd env CONFIG_FILE="$CONFIG_FILE" ATOMIC_EXIT_AFTER_PARSE=1 bash "$_TEST_SCRIPT_CLI" -- /usr/bin/pacman -S nvidia +assert_eq "CLI priority: PARSE_OK" "0" "$_rc" +assert_contains "CLI priority: uses CLI args" "CHROOT_CMD_0=/usr/bin/pacman" "$_out" +assert_contains "CLI priority: CLI arg[1]" "CHROOT_CMD_1=-S" "$_out" +assert_contains "CLI priority: CLI arg[2]" "CHROOT_CMD_2=nvidia" "$_out" +assert_eq "CLI priority: count is 3" "CHROOT_CMD_COUNT=3" "$(echo "$_out" | grep '^CHROOT_CMD_COUNT=')" +assert_contains "CLI priority: CHROOT_COMMAND loaded from config" "CHROOT_COMMAND_FROM_CONFIG=/usr/bin/pacman -Syu" "$_out" + +rm -f "$_TEST_SCRIPT_CLI" + # ── GEN_ID format validation ──────────────────────────────── section "GEN_ID format validation"