Skip to content

Instantly share code, notes, and snippets.

@jroakes
Last active May 31, 2025 15:32
Show Gist options
  • Save jroakes/452696b67ba3d0f5bdc800a1b644cdeb to your computer and use it in GitHub Desktop.
Save jroakes/452696b67ba3d0f5bdc800a1b644cdeb to your computer and use it in GitHub Desktop.
#!/bin/bash
# -*- mode: sh; indent-tabs-mode: nil; sh-basic-offset: 2; -*-
# vim: et sts=2 sw=2
#
# Steam OS recovery / installer – **SATA & USB drives only**
#
set -eu
die() { echo >&2 "!! $*"; exit 1; }
readvar(){ IFS= read -r -d '' "$1" || true; }
# ---------- DRIVE ----------
DISK=/dev/sda
DISK_SUFFIX=
DOPARTVERIFY=1
# ---------------------------
# Optional firmware bundles
VENDORED_BIOS_UPDATE=/home/deck/jupiter-bios
VENDORED_CONTROLLER_UPDATE=/home/deck/jupiter-controller-fw
# ---------- PARTITIONS -----
PART_SIZE_ESP=256 # MiB
PART_SIZE_EFI=64
PART_SIZE_ROOT=5120
PART_SIZE_VAR=256
PART_SIZE_HOME=100
DISK_SIZE=$(( 2 + PART_SIZE_HOME + PART_SIZE_ESP \
+ 2*(PART_SIZE_EFI + PART_SIZE_ROOT + PART_SIZE_VAR) ))
TARGET_SECTOR_SIZE=512
readvar PARTITION_TABLE <<EOF
label: gpt
%%DISKPART%%1: name="esp", size=${PART_SIZE_ESP}MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
%%DISKPART%%2: name="efi-A", size=${PART_SIZE_EFI}MiB, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
%%DISKPART%%3: name="efi-B", size=${PART_SIZE_EFI}MiB, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
%%DISKPART%%4: name="rootfs-A", size=${PART_SIZE_ROOT}MiB,type=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
%%DISKPART%%5: name="rootfs-B", size=${PART_SIZE_ROOT}MiB,type=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
%%DISKPART%%6: name="var-A", size=${PART_SIZE_VAR}MiB, type=4D21B016-B534-45C2-A9FB-5C16E091FD2D
%%DISKPART%%7: name="var-B", size=${PART_SIZE_VAR}MiB, type=4D21B016-B534-45C2-A9FB-5C16E091FD2D
%%DISKPART%%8: name="home", size=${PART_SIZE_HOME}MiB,type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915
EOF
FS_ESP=1; FS_EFI_A=2; FS_EFI_B=3; FS_ROOT_A=4; FS_ROOT_B=5; FS_VAR_A=6; FS_VAR_B=7; FS_HOME=8
diskpart(){ echo "$DISK$DISK_SUFFIX$1"; }
# ---------- COLOUR LOG HELPERS (restored in full) ----------
_sh_c_colors=0
[[ -t 1 && ${TERM:-dumb} != dumb ]] && _sh_c_colors="$(tput colors 2>/dev/null || echo 0)"
sh_c(){ [[ $_sh_c_colors -le 0 ]] || { IFS=\; && echo -n $'\e['"$*m"; }; }
estat(){ echo >&2 "$(sh_c 32 1)::$(sh_c) $*"; }
emsg() { echo >&2 "$(sh_c 34 1)::$(sh_c) $*"; }
ewarn(){ echo >&2 "$(sh_c 33 1);;$(sh_c) $*"; }
eerr() { echo >&2 "$(sh_c 31 1)!!$(sh_c) $*"; }
showcmd_unquoted(){ echo >&2 "$(sh_c 30 1)+$(sh_c) $*"; }
showcmd(){ showcmd_unquoted "${@Q}"; }
cmd(){ showcmd "$@"; "$@"; }
# -----------------------------------------------------------
fmt_ext4(){ cmd mkfs.ext4 -F -L "$1" "$2"; }
fmt_fat32(){ cmd mkfs.vfat -n"$1" "$2"; }
# ---------- PROMPT ----------
prompt_step(){ zenity --title "$1" --question --ok-label "Proceed" --cancel-label "Cancel" --no-wrap --text "$2"; }
prompt_reboot(){ prompt_step "Finished" "$1\n\nProceed to reboot?" && systemctl reboot; }
# ----------------------------
# ---------- GRUB ----------
install_grub(){
local ps="$1"
estat "Running grub-install on set $ps"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$ps" -- \
grub-install --target=x86_64-efi --efi-directory=/efi \
--bootloader-id=SteamOS --removable --recheck "$DISK"
}
finalize_part(){
estat "Finalising part set $1"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- \
mkdir -p /efi/SteamOS /esp/SteamOS/conf
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- \
steamos-partsets /efi/SteamOS/partsets
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- \
steamos-bootconf create --image "$1" --conf-dir /esp/SteamOS/conf \
--efi-dir /efi --set title "$1"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- grub-mkimage
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- update-grub
install_grub "$1"
}
# ----------------------------
# (Remaining functions – imageroot, verifypart, repair_steps, etc. – are **unchanged from Valve’s
# original** except that all NVMe-sanitise code paths and the `sanitize` menu entry have been removed.)
# … <snip – identical to Valve’s upstream code> …
# ---------- MENU ----------
case "${1:-help}" in
all)
prompt_step "Wipe & Install" "This will format /dev/sda and install Steam OS."
writePartitionTable=1; writeOS=1; writeHome=1
repair_steps; prompt_reboot "Re-imaging complete." ;;
system)
prompt_step "Repair System" "Reinstall core OS files, keep /home and games."
writeOS=1; repair_steps; prompt_reboot "System repair complete." ;;
home)
prompt_step "Reset User Data" "Format /home and /var – all personal data will be lost."
writeHome=1; repair_steps; prompt_reboot "User partitions reformatted." ;;
chroot) chroot_primary ;;
*) help ;;
esac
#!/bin/bash
# -*- mode: sh; indent-tabs-mode: nil; sh-basic-offset: 2; -*-
# vim: et sts=2 sw=2
#
# A collection of functions to create, repair, or modify a SteamOS installation for non-NVMe drives.
# This makes a number of assumptions about the target device and will be
# destructive if you have modified the expected partition layout.
#
set -eu
die() { echo >&2 "!! $*"; exit 1; }
readvar() { IFS= read -r -d '' "$1" || true; }
DISK=/dev/sda
DISK_SUFFIX=
DOPARTVERIFY=1
# If this exists, use the jupiter-biosupdate binary from this directory, and set JUPITER_BIOS_DIR to this directory when
# invoking it. Used for including a newer bios payload than the base image.
VENDORED_BIOS_UPDATE=/home/deck/jupiter-bios
# If this exists, use the jupiter-controller-update binary from this directory, and set
# JUPITER_CONTROLLER_UPDATE_FIRMWARE_DIR to this directory when invoking it. Used for including a newer controller
# payload than the base image.
VENDORED_CONTROLLER_UPDATE=/home/deck/jupiter-controller-fw
# Partition table, sfdisk format, %%DISKPART%% filled in
#
PART_SIZE_ESP="256"
PART_SIZE_EFI="64"
PART_SIZE_ROOT="5120" # This should match the size from the input disk build
PART_SIZE_VAR="256"
PART_SIZE_HOME="100" # For the stub .img file we're making this can be tiny, OS expands to fill physical disk on first
# boot. We make sure to specify the inode ratio explicitly when formatting.
# Total size + 1MiB padding at beginning/end for GPT structures.
DISK_SIZE=$(( 2 + PART_SIZE_HOME + PART_SIZE_ESP + 2 * ( PART_SIZE_EFI + PART_SIZE_ROOT + PART_SIZE_VAR ) ))
# Alignment: Using general sizes like MiB and no explicit start offset points causes sfdisk to align to MiB boundaries
# by default (e.g. first partition will start at 1MiB). See `man sfdisk`.
# Sector size: Most physical SSD/SATA/etc use logical 512 sectors*. GPT partition tables aren't portable between varying
# sector sizes, so this .img cannot be used directly on a 4k-logical-sector device (a quick search suggests
# this is most likely with certain VM/cloud/network disks)
#
# Since we use 1MiB alignment, it should be possible to fixup this partition table for other sector sizes
# without physically moving any partitions at imaging time:
#
# dd if=output.img of=/target/disk
#
# # sfdisk will default to 512 for a local file, dumping the table correctly, then translate it to the
# # target device's sector size upon re-writing:
#
# sfdisk -d < output.img | sfdisk /target/disk
#
# Alternatively, use `losetup --sector-size` to remount the image at a different size, and use the above
# steps to regenerate the table. If this comes up often in practice we could output a "partitions4096.gpt"
# style file alongside the disk image that could be `dd`'d on top for weird VM setups.
#
# *Note: logical sectors != physical sectors != optimal I/O alignment. Logical sectors being the unit the
# OS addresses the disk by, and what GPT tables use as their basic written-to-disk unit. Most everything
# is 512 or (rarely) 4096.
TARGET_SECTOR_SIZE=512 # Passed to `losetup` to emulate, affects the sector-offsets sfdisk ends up writing.
readvar PARTITION_TABLE <<END_PARTITION_TABLE
label: gpt
%%DISKPART%%1: name="esp", size= ${PART_SIZE_ESP}MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
%%DISKPART%%2: name="efi-A", size= ${PART_SIZE_EFI}MiB, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
%%DISKPART%%3: name="efi-B", size= ${PART_SIZE_EFI}MiB, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
%%DISKPART%%4: name="rootfs-A", size= ${PART_SIZE_ROOT}MiB, type=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
%%DISKPART%%5: name="rootfs-B", size= ${PART_SIZE_ROOT}MiB, type=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
%%DISKPART%%6: name="var-A", size= ${PART_SIZE_VAR}MiB, type=4D21B016-B534-45C2-A9FB-5C16E091FD2D
%%DISKPART%%7: name="var-B", size= ${PART_SIZE_VAR}MiB, type=4D21B016-B534-45C2-A9FB-5C16E091FD2D
%%DISKPART%%8: name="home", size= ${PART_SIZE_HOME}MiB, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915
END_PARTITION_TABLE
# Partition numbers on ideal target device, by index
FS_ESP=1
FS_EFI_A=2
FS_EFI_B=3
FS_ROOT_A=4
FS_ROOT_B=5
FS_VAR_A=6
FS_VAR_B=7
FS_HOME=8
diskpart() { echo "$DISK$DISK_SUFFIX$1"; }
##
## Util colors and such
##
err() {
echo >&2
eerr "Imaging error occured, see above and restart process."
sleep infinity
}
trap err ERR
_sh_c_colors=0
[[ -n $TERM && -t 1 && ${TERM,,} != dumb ]] && _sh_c_colors="$(tput colors 2>/dev/null || echo 0)"
sh_c() { [[ $_sh_c_colors -le 0 ]] || ( IFS=\; && echo -n $'\e['"${*:-0}m"; ); }
sh_quote() { echo "${@@Q}"; }
estat() { echo >&2 "$(sh_c 32 1)::$(sh_c) $*"; }
emsg() { echo >&2 "$(sh_c 34 1)::$(sh_c) $*"; }
ewarn() { echo >&2 "$(sh_c 33 1);;$(sh_c) $*"; }
einfo() { echo >&2 "$(sh_c 30 1)::$(sh_c) $*"; }
eerr() { echo >&2 "$(sh_c 31 1)!!$(sh_c) $*"; }
die() { local msg="$*"; [[ -n $msg ]] || msg="script terminated"; eerr "$msg"; exit 1; }
showcmd() { showcmd_unquoted "${@@Q}"; }
showcmd_unquoted() { echo >&2 "$(sh_c 30 1)+$(sh_c) $*"; }
cmd() { showcmd "$@"; "$@"; }
# Helper to format
fmt_ext4() { [[ $# -eq 2 && -n $1 && -n $2 ]] || die; cmd sudo mkfs.ext4 -F -L "$1" "$2"; }
fmt_fat32() { [[ $# -eq 2 && -n $1 && -n $2 ]] || die; cmd sudo mkfs.vfat -n"$1" "$2"; }
##
## Prompt mechanics - currently using Zenity
##
# Give the user a choice between Proceed, or Cancel (which exits this script)
# $1 Title
# $2 Text
#
prompt_step()
{
title="$1"
msg="$2"
unconditional="${3-}"
if [[ ! ${unconditional-} && ${NOPROMPT:-} ]]; then
echo -e "$msg"
return 0
fi
zenity --title "$title" --question --ok-label "Proceed" --cancel-label "Cancel" --no-wrap --text "$msg"
[[ $? = 0 ]] || exit 1
}
prompt_reboot()
{
local msg=$1
local mode="reboot"
[[ ${POWEROFF:-} ]] && mode="shutdown"
prompt_step "Action Successful" "${msg}\n\nChoose Proceed to $mode now, or Cancel to stay in the repair image." "${REBOOTPROMPT:-}"
[[ $? = 0 ]] || exit 1
if [[ ${POWEROFF:-} ]]; then
cmd systemctl poweroff
else
cmd systemctl reboot
fi
}
##
## Repair functions
##
# verify partition on target disk - at least make sure the type and partlabel match what we expect.
# $1 device
# $2 expected type
# $3 expected partlabel
#
verifypart()
{
[[ $DOPARTVERIFY = 1 ]] || return 0
TYPE="$(blkid -o value -s TYPE "$1" )"
PARTLABEL="$(blkid -o value -s PARTLABEL "$1" )"
if [[ ! $TYPE = "$2" ]]; then
eerr "Device $1 is type $TYPE but expected $2 - cannot proceed. You may try full recovery."
sleep infinity ; exit 1
fi
if [[ ! $PARTLABEL = $3 ]] ; then
eerr "Device $1 has label $PARTLABEL but expected $3 - cannot proceed. You may try full recovery."
sleep infinity ; exit 2
fi
}
# Replace the device rootfs (btrfs version). Source must be frozen before calling.
# $1 source device
# $2 target device
#
imageroot()
{
local srcroot="$1"
local newroot="$2"
# copy then randomize target UUID - careful here! Duplicating btrfs ids is a problem
cmd dd if="$srcroot" of="$newroot" bs=128M status=progress oflag=sync
cmd btrfstune -f -u "$newroot"
cmd btrfs check "$newroot"
}
# Set up boot configuration in the target partition set
# $1 partset name
finalize_part()
{
estat "Finalizing install part $1"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- mkdir -p /efi/SteamOS
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- mkdir -p /esp/SteamOS/conf
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- steamos-partsets /efi/SteamOS/partsets
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- steamos-bootconf create --image "$1" --conf-dir /esp/SteamOS/conf --efi-dir /efi --set title "$1"
# Ensure proper GRUB installation for SATA drives
estat "Installing GRUB for partition set $1"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- grub-install --target=x86_64-efi --efi-directory=/efi --bootloader-id=SteamOS --removable
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- grub-mkimage
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- update-grub
# Additional GRUB configuration for SATA compatibility
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$1" -- grub-install --target=i386-pc "$DISK"
}
##
## Main
##
onexit=()
exithandler() {
for func in "${onexit[@]}"; do
"$func"
done
}
trap exithandler EXIT
# Check existence of target disk
if [[ ! -e "$DISK" ]]; then
eerr "$DISK does not exist -- no SATA drive detected?"
sleep infinity
exit 1
fi
# Simple disk wipe for new drives (no secure wipe needed)
simple_wipe()
{
estat "Performing simple disk wipe for new drive"
echo "Wiping partition table and first sectors of $DISK"
cmd wipefs -a "$DISK"
cmd dd if=/dev/zero of="$DISK" bs=1M count=100 status=progress
cmd sync
echo "Simple wipe complete."
}
# Reinstall a fresh SteamOS copy.
#
repair_steps()
{
if [[ $writePartitionTable = 1 ]]; then
estat "Write known partition table"
echo "$PARTITION_TABLE" | sed "s|%%DISKPART%%|$DISK|g" | sfdisk "$DISK"
elif [[ $writeOS = 1 || $writeHome = 1 ]]; then
# verify some partition settings to make sure we are ok to proceed with partial repairs
# in the case we just wrote the partition table, we know we are good and the partitions
# are unlabelled anyway
verifypart "$(diskpart $FS_ESP)" vfat esp
verifypart "$(diskpart $FS_EFI_A)" vfat efi-A
verifypart "$(diskpart $FS_EFI_B)" vfat efi-B
verifypart "$(diskpart $FS_VAR_A)" ext4 var-A
verifypart "$(diskpart $FS_VAR_B)" ext4 var-B
verifypart "$(diskpart $FS_HOME)" ext4 home
fi
# clear the var partition (user data), but also if we are reinstalling the OS
# a fresh system partition has problems with overlay otherwise
if [[ $writeOS = 1 || $writeHome = 1 ]]; then
estat "Creating var partitions"
fmt_ext4 var "$(diskpart $FS_VAR_A)"
fmt_ext4 var "$(diskpart $FS_VAR_B)"
fi
# Create boot partitions
if [[ $writeOS = 1 ]]; then
# Set up ESP/EFI boot partitions
estat "Creating boot partitions"
fmt_fat32 esp "$(diskpart $FS_ESP)"
fmt_fat32 efi "$(diskpart $FS_EFI_A)"
fmt_fat32 efi "$(diskpart $FS_EFI_B)"
fi
if [[ $writeHome = 1 ]]; then
estat "Creating home partition..."
cmd sudo mkfs.ext4 -F -O casefold -T huge -L home "$(diskpart $FS_HOME)"
estat "Remove the reserved blocks on the home partition..."
cmd tune2fs -m 0 "$(diskpart $FS_HOME)"
fi
# Stage a BIOS update for next reboot if updating OS. OOBE images like this one don't auto-update the bios on boot.
if [[ $writeOS = 1 ]]; then
estat "Staging a BIOS update for next boot if necessary"
# If we included a VENDORED_BIOS_UPDATE directory above, use the newer payload there and point JUPITER_BIOS_DIR to
# it. Directory should contain both a newer tool and newer firmware.
biostool=/usr/bin/jupiter-biosupdate
if [[ -n $VENDORED_BIOS_UPDATE && -d $VENDORED_BIOS_UPDATE ]]; then
biostool="$VENDORED_BIOS_UPDATE"/jupiter-biosupdate
export JUPITER_BIOS_DIR="$VENDORED_BIOS_UPDATE"
fi
# This is cursed, but, we want to stage the capsule in the onboard drive, which we are booting next
fix_esp() {
if [[ -n ${mounted_esp:-} ]]; then
cmd umount -l /esp || true
cmd umount -l /boot/efi || true
mounted_esp=
fi
}
onexit+=(fix_esp)
einfo "Mounting new ESP/EFI on /esp /boot/efi for BIOS staging"
cmd mount "$(diskpart $FS_ESP)" /esp
cmd mount "$(diskpart $FS_EFI_A)" /boot/efi
mounted_esp=1
if [[ ${FORCEBIOS:-} ]]; then
"$biostool" --force || "$biostool"
else
"$biostool"
fi
fix_esp
fi
# Perform a controller update if updating OS. OOBE images like this one don't auto-update controllers on boot.
if [[ $writeOS = 1 ]]; then
estat "Updating controller firmware if necessary"
controller_tool="/usr/bin/jupiter-controller-update"
# If we included a VENDORED_CONTROLLER_UPDATE directory above, use the newer payload and point
# JUPITER_CONTROLLER_UPDATE_FIRMWARE_DIR to it. Directory should contain both a newer tool and newer firmware.
if [[ -n $VENDORED_CONTROLLER_UPDATE && -d $VENDORED_CONTROLLER_UPDATE ]]; then
controller_tool="$VENDORED_CONTROLLER_UPDATE"/jupiter-controller-update
export JUPITER_CONTROLLER_UPDATE_FIRMWARE_DIR="$VENDORED_CONTROLLER_UPDATE"
fi
JUPITER_CONTROLLER_UPDATE_IN_OOBE=1 "$controller_tool"
fi
if [[ $writeOS = 1 ]]; then
# Find rootfs
rootdevice="$(findmnt -n -o source / )"
if [[ -z $rootdevice || ! -e $rootdevice ]]; then
eerr "Could not find USB installer root -- usb hub issue?"
sleep infinity
exit 1
fi
# Freeze our rootfs
estat "Freezing rootfs"
unfreeze() { fsfreeze -u / || true; }
onexit+=(unfreeze)
cmd fsfreeze -f /
estat "Imaging OS partition A"
imageroot "$rootdevice" "$(diskpart $FS_ROOT_A)"
estat "Imaging OS partition B"
imageroot "$rootdevice" "$(diskpart $FS_ROOT_B)"
estat "Finalizing boot configurations"
finalize_part A
finalize_part B
estat "Finalizing EFI system partition"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset A -- steamcl-install --flags restricted --force-extra-removable
# Ensure bootloader is properly installed for SATA drives
estat "Final GRUB installation verification"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset A -- grub-install --target=x86_64-efi --efi-directory=/efi --bootloader-id=SteamOS --removable --force
cmd steamos-chroot --no-overlay --disk "$DISK" --partset A -- update-grub
fi
}
# drop into the primary OS partset on the device
#
chroot_primary()
{
partset=$( steamos-chroot --no-overlay --disk "$DISK" --partset "A" -- steamos-bootconf selected-image )
estat "Dropping into a chroot on the $partset partition set."
estat "You can make any needed changes here, and exit when done."
# FIXME etc overlay dir might not exist on a fresh install and this will fail
cmd steamos-chroot --disk "$DISK" --partset "$partset"
}
# print quick list of targets
#
help()
{
readvar HELPMSG << EOD
This tool can be used to reinstall or repair your SteamOS installation on SATA/non-NVMe drives
Possible targets:
all : permanently destroy all data on the device, and (re)install SteamOS.
system : repair/reinstall SteamOS on the device's system partitions, preserving user data partitions.
home : reformat the devices /home and /var partitions, removing games and user data from the device.
chroot : chroot into to the primary SteamOS partition set.
wipe : perform a simple disk wipe operation for new drives.
EOD
emsg "$HELPMSG"
if [[ "$EUID" -ne 0 ]]; then
eerr "Please run as root."
exit 1
fi
}
[[ "$EUID" -ne 0 ]] && help
writePartitionTable=0
writeOS=0
writeHome=0
case "${1-help}" in
all)
prompt_step "Wipe Device & Install SteamOS" "This action will wipe and (re)install SteamOS on this device.\nThis will permanently destroy all data on your device.\n\nThis cannot be undone.\n\nChoose Proceed only if you wish to wipe and reinstall this device."
writePartitionTable=1
writeOS=1
writeHome=1
simple_wipe
repair_steps
prompt_reboot "Reimaging complete."
;;
system)
prompt_step "Repair SteamOS" "This action will repair the SteamOS installation on the device, while attempting to preserve your games and personal content.\nSystem customizations may be lost.\n\nChoose Proceed to reinstall SteamOS on your device."
writeOS=1
repair_steps
prompt_reboot "SteamOS reinstall complete."
;;
home)
prompt_step "Delete local user data" "This action will reformat the home partitions on your device.\nThis will destroy downloaded games and all personal content, including system configuration.\n\nThis action cannot be undone.\n\nChoose Proceed to reformat all user home partitions."
writeHome=1
repair_steps
prompt_reboot "User partitions have been reformatted."
;;
chroot)
chroot_primary
;;
wipe)
prompt_step "Simple disk wipe" "This action will perform a simple wipe of the partition table and first sectors of the drive.\n\nThis is suitable for new drives and much faster than secure wiping.\n\nThis action cannot be undone.\n\nChoose Proceed only if you want to wipe the current device."
simple_wipe
;;
*)
help
;;
esac
#!/bin/bash
# -*- mode: sh; indent-tabs-mode: nil; sh-basic-offset: 2; -*-
# vim: et sts=2 sw=2
#
# A collection of functions to create, repair, or modify a SteamOS installation.
# This makes a number of assumptions about the target device and will be
# destructive if you have modified the expected partition layout.
#
set -eu
die() { echo >&2 "!! $*"; exit 1; }
readvar() { IFS= read -r -d '' "$1" || true; }
DISK=/dev/sda
DISK_SUFFIX= # Kept empty for non-NVMe drives like /dev/sda
DOPARTVERIFY=1
# If this exists, use the jupiter-biosupdate binary from this directory, and set JUPITER_BIOS_DIR to this directory when
# invoking it. Used for including a newer bios payload than the base image.
VENDORED_BIOS_UPDATE=/home/deck/jupiter-bios
# If this exists, use the jupiter-controller-update binary from this directory, and set
# JUPITER_CONTROLLER_UPDATE_FIRMWARE_DIR to this directory when invoking it. Used for including a newer controller
# payload than the base image.
VENDORED_CONTROLLER_UPDATE=/home/deck/jupiter-controller-fw
# Partition table, sfdisk format
#
PART_SIZE_ESP="256"
PART_SIZE_EFI="64"
PART_SIZE_ROOT="5120" # This should match the size from the input disk build
PART_SIZE_VAR="256"
PART_SIZE_HOME="100" # For the stub .img file we're making this can be tiny, OS expands to fill physical disk on first
# boot. We make sure to specify the inode ratio explicitly when formatting.
# Total size + 1MiB padding at beginning/end for GPT structures (informational for image creation).
DISK_SIZE=$(( 2 + PART_SIZE_HOME + PART_SIZE_ESP + 2 * ( PART_SIZE_EFI + PART_SIZE_ROOT + PART_SIZE_VAR ) ))
# Alignment: Using general sizes like MiB and no explicit start offset points causes sfdisk to align to MiB boundaries
# by default (e.g. first partition will start at 1MiB). See `man sfdisk`.
# Sector size comments from original script retained for context, TARGET_SECTOR_SIZE is not actively used by this script for partitioning physical disk.
TARGET_SECTOR_SIZE=512
readvar PARTITION_TABLE <<END_PARTITION_TABLE
label: gpt
1: name="esp", size= ${PART_SIZE_ESP}MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
2: name="efi-A", size= ${PART_SIZE_EFI}MiB, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
3: name="efi-B", size= ${PART_SIZE_EFI}MiB, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
4: name="rootfs-A", size= ${PART_SIZE_ROOT}MiB, type=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
5: name="rootfs-B", size= ${PART_SIZE_ROOT}MiB, type=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709
6: name="var-A", size= ${PART_SIZE_VAR}MiB, type=4D21B016-B534-45C2-A9FB-5C16E091FD2D
7: name="var-B", size= ${PART_SIZE_VAR}MiB, type=4D21B016-B534-45C2-A9FB-5C16E091FD2D
8: name="home", size= ${PART_SIZE_HOME}MiB, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915
END_PARTITION_TABLE
# Partition numbers on ideal target device, by index
FS_ESP=1
FS_EFI_A=2
FS_EFI_B=3
FS_ROOT_A=4
FS_ROOT_B=5
FS_VAR_A=6
FS_VAR_B=7
FS_HOME=8
diskpart() { echo "$DISK$DISK_SUFFIX$1"; } # For DISK=/dev/sda, DISK_SUFFIX="", this becomes /dev/sda1, /dev/sda2 etc.
##
## Util colors and such
##
err() {
echo >&2
eerr "Imaging error occured, see above and restart process."
sleep infinity
}
trap err ERR
_sh_c_colors=0
[[ -n $TERM && -t 1 && ${TERM,,} != dumb ]] && _sh_c_colors="$(tput colors 2>/dev/null || echo 0)"
sh_c() { [[ $_sh_c_colors -le 0 ]] || ( IFS=\; && echo -n $'\e['"${*:-0}m"; ); }
sh_quote() { echo "${@@Q}"; }
estat() { echo >&2 "$(sh_c 32 1)::$(sh_c) $*"; }
emsg() { echo >&2 "$(sh_c 34 1)::$(sh_c) $*"; }
ewarn() { echo >&2 "$(sh_c 33 1);;$(sh_c) $*"; }
einfo() { echo >&2 "$(sh_c 30 1)::$(sh_c) $*"; }
eerr() { echo >&2 "$(sh_c 31 1)!!$(sh_c) $*"; }
# die() is defined at the top
showcmd() { showcmd_unquoted "${@@Q}"; }
showcmd_unquoted() { echo >&2 "$(sh_c 30 1)+$(sh_c) $*"; }
cmd() { showcmd "$@"; "$@"; }
# Helper to format
fmt_ext4() { [[ $# -eq 2 && -n $1 && -n $2 ]] || die "fmt_ext4: invalid arguments"; cmd sudo mkfs.ext4 -F -L "$1" "$2"; }
fmt_fat32() { [[ $# -eq 2 && -n $1 && -n $2 ]] || die "fmt_fat32: invalid arguments"; cmd sudo mkfs.vfat -n"$1" "$2"; }
##
## Prompt mechanics - currently using Zenity
##
# Give the user a choice between Proceed, or Cancel (which exits this script)
# $1 Title
# $2 Text
#
prompt_step()
{
title="$1"
msg="$2"
unconditional="${3-}"
if [[ ! ${unconditional-} && ${NOPROMPT:-} ]]; then
echo -e "$msg"
return 0
fi
zenity --title "$title" --question --ok-label "Proceed" --cancel-label "Cancel" --no-wrap --text "$msg"
# $? is 0 for OK, 1 for Cancel.
# Explicitly check and exit.
if [[ $? -ne 0 ]]; then
ewarn "User cancelled operation."
exit 1
fi
}
prompt_reboot()
{
local msg=$1
local mode="reboot"
[[ ${POWEROFF:-} ]] && mode="shutdown"
prompt_step "Action Successful" "${msg}\n\nChoose Proceed to $mode now, or Cancel to stay in the repair image." "${REBOOTPROMPT:-}"
# If prompt_step is cancelled, script will exit due to the check within prompt_step.
if [[ ${POWEROFF:-} ]]; then
cmd systemctl poweroff
else
cmd systemctl reboot
fi
}
##
## Repair functions
##
# verify partition on target disk - at least make sure the type and partlabel match what we expect.
# $1 device
# $2 expected type
# $3 expected partlabel
#
verifypart()
{
[[ $DOPARTVERIFY = 1 ]] || return 0
local device="$1"
local expected_type="$2"
local expected_partlabel="$3"
local TYPE PARTLABEL
TYPE="$(blkid -o value -s TYPE "$device" || true)" # ensure it doesn't die if blkid fails temporarily
PARTLABEL="$(blkid -o value -s PARTLABEL "$device" || true)"
if [[ "$TYPE" != "$expected_type" ]]; then
eerr "Device $device is type '$TYPE' but expected '$expected_type' - cannot proceed. You may try full recovery."
sleep infinity ; exit 1
fi
if [[ "$PARTLABEL" != "$expected_partlabel" ]] ; then
eerr "Device $device has label '$PARTLABEL' but expected '$expected_partlabel' - cannot proceed. You may try full recovery."
sleep infinity ; exit 2
fi
}
# Replace the device rootfs (btrfs version). Source must be frozen before calling.
# $1 source device
# $2 target device
#
imageroot()
{
local srcroot="$1"
local newroot="$2"
# copy then randomize target UUID - careful here! Duplicating btrfs ids is a problem
cmd dd if="$srcroot" of="$newroot" bs=128M status=progress oflag=sync
cmd btrfstune -f -u "$newroot"
cmd btrfs check "$newroot"
}
# Set up boot configuration in the target partition set
# $1 partset name
finalize_part()
{
local partset_name="$1"
estat "Finalizing install part $partset_name"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$partset_name" -- mkdir -p /efi/SteamOS # Ensure parent dir exists
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$partset_name" -- mkdir -p /esp/SteamOS/conf
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$partset_name" -- steamos-partsets /efi/SteamOS/partsets
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$partset_name" -- steamos-bootconf create --image "$partset_name" --conf-dir /esp/SteamOS/conf --efi-dir /efi --set title "SteamOS $partset_name"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$partset_name" -- grub-mkimage
cmd steamos-chroot --no-overlay --disk "$DISK" --partset "$partset_name" -- update-grub
}
##
## Main
##
onexit=()
exithandler() {
for func in "${onexit[@]}"; do
# Try to execute the function, suppress its errors, but log our own if it failed.
if ! "$func" 2>/dev/null; then
einfo "Exit handler function '$func' encountered an error or was already handled."
fi
done
}
trap exithandler EXIT
# Check existence of target disk
if [[ ! -b "$DISK" ]]; then # Check if it's a block device
eerr "$DISK does not exist or is not a block device -- no drive detected at $DISK?"
sleep infinity
exit 1
fi
# Reinstall a fresh SteamOS copy.
#
repair_steps()
{
if [[ $writePartitionTable = 1 ]]; then
estat "Writing known partition table to $DISK"
# sfdisk expects the "label: gpt" within the input stream
echo "$PARTITION_TABLE" | cmd sfdisk "$DISK"
elif [[ $writeOS = 1 || $writeHome = 1 ]]; then
# Verify some partition settings to make sure we are ok to proceed with partial repairs.
# PARTLABEL is set by sfdisk from `name=...` in the partition table.
# TYPE is set by mkfs. These will only be correct if partitions were previously formatted.
# If this is a repair, these checks are useful. If it's a fresh format after `writePartitionTable=1`,
# then these will likely fail for TYPE until formatted below, this is a slight logic flaw in original verify timing.
# For now, keeping as is, as full 'all' flow reformats anyway.
einfo "Verifying existing partition layout (labels expected from sfdisk, types from previous format if any)..."
verifypart "$(diskpart $FS_ESP)" vfat esp
verifypart "$(diskpart $FS_EFI_A)" vfat efi-A
verifypart "$(diskpart $FS_EFI_B)" vfat efi-B
# For rootfs, type is btrfs, LABEL is set by mkfs/btrfstune, not from sfdisk 'name'. 'name' becomes PARTLABEL.
# verifypart "$(diskpart $FS_ROOT_A)" btrfs rootfs-A # This would fail if not yet formatted as btrfs
verifypart "$(diskpart $FS_VAR_A)" ext4 var-A
verifypart "$(diskpart $FS_VAR_B)" ext4 var-B
verifypart "$(diskpart $FS_HOME)" ext4 home
fi
# clear the var partition (user data), but also if we are reinstalling the OS
# a fresh system partition has problems with overlay otherwise
if [[ $writeOS = 1 || $writeHome = 1 ]]; then
estat "Formatting var partitions"
fmt_ext4 var-A "$(diskpart $FS_VAR_A)" # Label 'var-A'
fmt_ext4 var-B "$(diskpart $FS_VAR_B)" # Label 'var-B'
fi
# Create boot partitions
if [[ $writeOS = 1 ]]; then
estat "Formatting boot partitions"
fmt_fat32 ESP "$(diskpart $FS_ESP)" # Label 'ESP' for the main System Partition
fmt_fat32 EFI-A "$(diskpart $FS_EFI_A)" # Label 'EFI-A' for slot A's EFI files
fmt_fat32 EFI-B "$(diskpart $FS_EFI_B)" # Label 'EFI-B' for slot B's EFI files
fi
if [[ $writeHome = 1 ]]; then
estat "Formatting home partition..."
cmd sudo mkfs.ext4 -F -O casefold,extent -T huge -L home "$(diskpart $FS_HOME)"
estat "Removing reserved blocks on the home partition..."
cmd tune2fs -m 0 "$(diskpart $FS_HOME)"
fi
# Stage a BIOS update for next reboot if updating OS.
if [[ $writeOS = 1 ]]; then
estat "Staging a BIOS update for next boot if necessary"
biostool="/usr/bin/jupiter-biosupdate"
if [[ -n "${VENDORED_BIOS_UPDATE-}" && -d "$VENDORED_BIOS_UPDATE" && -x "$VENDORED_BIOS_UPDATE/jupiter-biosupdate" ]]; then
biostool="$VENDORED_BIOS_UPDATE"/jupiter-biosupdate
export JUPITER_BIOS_DIR="$VENDORED_BIOS_UPDATE"
einfo "Using vendored BIOS update tool: $biostool"
fi
# Staging the capsule on the ESP for the system we are booting next.
mounted_esp_for_bios_update=""
_fix_esp_bios_stage() { # Renamed to avoid conflict if onexit had identical name
if [[ -n "$mounted_esp_for_bios_update" ]]; then
ewarn "Cleaning up BIOS staging mounts: /esp, /boot/efi"
cmd umount -l /esp || einfo "Failed to unmount /esp during BIOS stage cleanup."
cmd umount -l /boot/efi || einfo "Failed to unmount /boot/efi during BIOS stage cleanup."
mounted_esp_for_bios_update=""
fi
}
onexit+=(_fix_esp_bios_stage)
cmd mkdir -p /esp /boot/efi
einfo "Mounting ESP on /esp and EFI-A on /boot/efi for BIOS staging"
cmd mount "$(diskpart $FS_ESP)" /esp
cmd mount "$(diskpart $FS_EFI_A)" /boot/efi # Uses EFI-A for staging BIOS update
mounted_esp_for_bios_update=1
if [[ -x "$biostool" ]]; then
if [[ -n "${FORCEBIOS-}" ]]; then # Check if FORCEBIOS is set and non-empty
einfo "Attempting forced BIOS update..."
"$biostool" --force || "$biostool" # Try with force, then without if that fails
else
einfo "Attempting standard BIOS update..."
"$biostool"
fi
else
ewarn "BIOS update tool $biostool not found or not executable. Skipping BIOS update."
fi
_fix_esp_bios_stage # Call cleanup immediately
fi
# Perform a controller update if updating OS.
if [[ $writeOS = 1 ]]; then
estat "Updating controller firmware if necessary"
controller_tool="/usr/bin/jupiter-controller-update"
if [[ -n "${VENDORED_CONTROLLER_UPDATE-}" && -d "$VENDORED_CONTROLLER_UPDATE" && -x "$VENDORED_CONTROLLER_UPDATE/jupiter-controller-update" ]]; then
controller_tool="$VENDORED_CONTROLLER_UPDATE"/jupiter-controller-update
export JUPITER_CONTROLLER_UPDATE_FIRMWARE_DIR="$VENDORED_CONTROLLER_UPDATE"
einfo "Using vendored controller update tool: $controller_tool"
fi
if [[ -x "$controller_tool" ]]; then
JUPITER_CONTROLLER_UPDATE_IN_OOBE=1 "$controller_tool"
else
ewarn "Controller update tool $controller_tool not found or not executable. Skipping controller update."
fi
fi
if [[ $writeOS = 1 ]]; then
# Find rootfs of the installer
rootdevice="$(findmnt -n -o SOURCE / )"
if [[ -z "$rootdevice" || ! -b "$rootdevice" ]]; then
eerr "Could not find USB installer root block device -- usb hub issue? (Found: '$rootdevice')"
sleep infinity
exit 1
fi
# Freeze our rootfs (the installer's root)
estat "Freezing installer rootfs $rootdevice (if supported)"
root_fstype=$(findmnt -n -o FSTYPE "$rootdevice" || echo "unknown")
needs_unfreeze=0
_unfreeze_installer_root() { # Renamed to avoid conflict
if [[ $needs_unfreeze -eq 1 ]]; then
einfo "Unfreezing installer rootfs /"
fsfreeze -u /
needs_unfreeze=0
fi
}
onexit+=(_unfreeze_installer_root)
if command -v fsfreeze >/dev/null && [[ "$root_fstype" == "btrfs" || "$root_fstype" == "ext4" || "$root_fstype" == "xfs" || "$root_fstype" == "f2fs" ]]; then
cmd fsfreeze -f /
needs_unfreeze=1
else
ewarn "fsfreeze command not found or installer filesystem '$root_fstype' not typically frozen. Skipping freeze of installer root."
fi
estat "Imaging OS partition A from $rootdevice to $(diskpart $FS_ROOT_A)"
imageroot "$rootdevice" "$(diskpart $FS_ROOT_A)"
estat "Imaging OS partition B from $rootdevice to $(diskpart $FS_ROOT_B)"
imageroot "$rootdevice" "$(diskpart $FS_ROOT_B)"
_unfreeze_installer_root # Unfreeze immediately after imaging done
estat "Finalizing boot configurations for A and B partsets"
finalize_part A
finalize_part B
estat "Finalizing EFI system partition bootloader using partset A"
cmd steamos-chroot --no-overlay --disk "$DISK" --partset A -- steamcl-install --flags restricted --force-extra-removable
fi
}
# drop into the primary OS partset on the device
#
chroot_primary()
{
local partset
partset=$(steamos-chroot --no-overlay --disk "$DISK" --partset "A" -- steamos-bootconf selected-image 2>/dev/null || echo "A")
einfo "Determined primary partset as: $partset (defaulted to A if detection failed)"
estat "Dropping into a chroot on the $partset partition set on $DISK."
estat "You can make any needed changes here, and type 'exit' when done."
cmd steamos-chroot --disk "$DISK" --partset "$partset"
}
# print quick list of targets
#
help()
{
readvar HELPMSG << EOD
This tool can be used to reinstall or repair your SteamOS installation on $DISK.
Possible targets:
all : (Re)install SteamOS. This will format system and home partitions.
All data on $DISK will be effectively lost.
system : Repair/reinstall SteamOS on the device's system partitions,
preserving user data on home partition ($DISK$FS_HOME).
home : Reformat the device's /home partition ($DISK$FS_HOME) and var partitions,
removing games and user data from the device.
chroot : Chroot into to the primary SteamOS partition set.
EOD
emsg "$HELPMSG"
if [[ "$EUID" -ne 0 ]]; then
eerr "Please run as root (e.g. sudo $0 <target>)."
exit 1 # Exit after showing help if not root
fi
}
if [[ "$EUID" -ne 0 ]]; then
help # This will print help and exit with error code 1 as per the check inside help()
fi
writePartitionTable=0
writeOS=0
writeHome=0
case "${1-help}" in
all)
prompt_step "Wipe Device & Install SteamOS" "This action will (re)install SteamOS on $DISK.\nThis will effectively destroy all data on the system and home partitions of this device.\n\nThis cannot be undone.\n\nChoose Proceed only if you wish to wipe and reinstall SteamOS on this device."
writePartitionTable=1
writeOS=1
writeHome=1
einfo "Starting full SteamOS (re)installation on $DISK..."
repair_steps
prompt_reboot "Reimaging complete for $DISK."
;;
system)
prompt_step "Repair SteamOS System Partitions" "This action will repair the SteamOS installation on the system partitions of $DISK, while attempting to preserve your games and personal content on the home partition.\nSystem customizations may be lost.\n\nChoose Proceed to reinstall SteamOS system files on your device."
writeOS=1
writeHome=0 # Preserve home
einfo "Starting SteamOS system repair on $DISK..."
repair_steps
prompt_reboot "SteamOS system reinstall complete for $DISK."
;;
home)
prompt_step "Delete Local User Data (Reformat Home)" "This action will reformat the home and var partitions on $DISK.\nThis will destroy downloaded games and all personal content, including system configuration.\n\nThis action cannot be undone.\n\nChoose Proceed to reformat all user data partitions."
writeHome=1
writeOS=0 # Do not touch OS partitions
einfo "Starting home and var partition reformat on $DISK..."
repair_steps
prompt_reboot "User data partitions have been reformatted on $DISK."
;;
chroot)
chroot_primary
;;
*)
help
;;
esac
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment