Skip to content

Commit

Permalink
Interactive USB Installer
Browse files Browse the repository at this point in the history
This makes it significantly easier for custom hardware users to install umbrelOS.

It also auto-reflashes internal storage for Umbrel Home users on boot, providing full factory reset functionality.

A new USB installer image will be produced in CI for every release.
  • Loading branch information
lukechilds authored May 4, 2024
1 parent 6e2dfcc commit 135f62e
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/create-release-on-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ jobs:
if: matrix.task == 'build:amd64'
run: cd build && sudo xz --keep --threads=0 umbrelos-amd64.img

- name: Create USB installer
if: matrix.task == 'build:amd64'
run: npm run build:amd64:usb-installer

- name: Create GitHub Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
with:
Expand All @@ -46,3 +50,4 @@ jobs:
packages/os/build/*.update
packages/os/build/*.img.zip
packages/os/build/*.img.xz
packages/os/usb-installer/build/*.img.xz
1 change: 1 addition & 0 deletions packages/os/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"scripts": {
"build": "./build.sh",
"build:amd64": "SKIP_ARM64=true npm run build",
"build:amd64:usb-installer": "cd usb-installer && ./run.sh",
"build:arm64": "SKIP_AMD64=true npm run build",
"build:pi5": "SKIP_AMD64=true SKIP_PI4=true npm run build"
}
Expand Down
1 change: 1 addition & 0 deletions packages/os/usb-installer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
65 changes: 65 additions & 0 deletions packages/os/usb-installer/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash

echo "Creating disk image..."
rootfs_tar_size="$(du --block-size 1M /data/build/rootfs.tar | awk '{print $1}')"
rootfs_buffer="512"
disk_size_mb="$((rootfs_tar_size + rootfs_buffer))"
disk_size_sector=$(expr $disk_size_mb \* 1024 \* 1024 / 512)
disk_image="/data/build/umbrelos-amd64-usb-installer.img"
dd if=/dev/zero of="${disk_image}" bs="${disk_size_sector}" count=512

echo Creating disk partitions...
gpt_efi="ef00"
gpt_root_amd64="8304"
sgdisk \
--new 1:2048:+200M \
--typecode 1:"${gpt_efi}" \
--change-name 1:ESP \
--new 2:0:0 \
--typecode 2:"${gpt_root_amd64}" \
--change-name 1:ROOTFS \
"${disk_image}"

disk_layout=$(fdisk -l "${disk_image}")
echo "${disk_layout}"

echo Attaching partitions to loopback devices...
efi_start=$(echo "${disk_layout}" -l "${disk_image}" | grep EFI | awk '{print $2}')
efi_size=$(echo "${disk_layout}" -l "${disk_image}" | grep EFI | awk '{print $4}')
root_start=$(echo "${disk_layout}" -l "${disk_image}" | grep root | awk '{print $2}')
root_size=$(echo "${disk_layout}" -l "${disk_image}" | grep root | awk '{print $4}')
efi_device=$(losetup --offset $((512*efi_start)) --sizelimit $((512*efi_size)) --show --find "${disk_image}")
root_device=$(losetup --offset $((512*root_start)) --sizelimit $((512*root_size)) --show --find "${disk_image}")

echo Formatting partitions...
mkfs.vfat -n "ESP" "${efi_device}"
mkfs.ext4 -L "ROOTFS" "${root_device}"

echo Mounting partitions...
efi_mount_point="/mnt/efi"
root_mount_point="/mnt/root"
mkdir -p "${efi_mount_point}"
mkdir -p "${root_mount_point}"
mount "${efi_device}" "${efi_mount_point}"
mount -t ext4 "${root_device}" "${root_mount_point}"

echo Extracting rootfs...
tar -xf /data/build/rootfs.tar --directory "${root_mount_point}"

echo Copying boot directory over to ESP partition...
cp -r "${root_mount_point}/boot/." "${efi_mount_point}"
tree "${efi_mount_point}"
echo

echo Unmounting partitions...
umount "${root_mount_point}"
umount "${efi_mount_point}"

echo Detaching loopback devices...
losetup --detach "${efi_device}"
losetup --detach "${root_device}"

echo "Compressing image..."
xz --keep --threads 0 --force "${disk_image}"

echo Done!
4 changes: 4 additions & 0 deletions packages/os/usb-installer/builder.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM debian:bookworm

RUN apt-get -y update
RUN apt-get -y install fdisk gdisk qemu-utils dosfstools tree xz-utils
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Custom TTY
After=multi-user.target

[Service]
ExecStart=/opt/custom-tty
StandardInput=tty
StandardOutput=tty
StandardError=tty
TTYPath=/dev/tty1
Restart=on-failure

[Install]
WantedBy=multi-user.target
55 changes: 55 additions & 0 deletions packages/os/usb-installer/overlay/opt/custom-tty
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash

sleep 1

clear

cat << 'EOF'
,;###GGGGGGGGGGl#Sp
,##GGGlW""^' '`""%GGGG#S,
,#GGG" "lGG#o
#GGl^ '$GG#
,#GGb \GGG,
lGG" "GGG
#GGGlGGGl##p,,p##lGGl##p,,p###ll##GGGG
!GGGlW"""*GGGGGGG#""""WlGGGGG#W""*WGGGGS
"" "^ '" ""
umbrelOS USB Installer
EOF
echo

# If device is an Umbrel Home auto flash and shutdown non-interactively since we don't have video output.
if dmidecode -t system | grep --silent 'Umbrel Home'
then
echo "Umbrel Home detected."
echo "Automatically flashing internal storage..."
xz --decompress --stdout /umbrelos-amd64.img.xz | dd of=/dev/nvme0n1 bs=4M status=progress
echo "umbrelOS has been installed, shutting down."
poweroff
exit 1
fi

# For all other devices, run the interactive installer.
echo "Installing umbrelOS will wipe your entire storage device."
echo
readarray -t devices < <(lsblk --nodeps --output NAME,VENDOR,MODEL,SIZE | sed '1d')
PS3="Select a storage device by number to install umbrelOS on: "
select device in "${devices[@]}"
do
if [[ -n "$device" ]]
then
echo "installing umbrelOS on: $device"
device_path="/dev/$(echo $device | awk '{print $1}')"
xz --decompress --stdout /umbrelos-amd64.img.xz | dd of=$device_path bs=4M status=progress
sync
echo
echo "umbrelOS has been installed!"
printf "Press any key to shutdown, remember to remove the USB drive before turning the device back on."
read -n 1 -s
poweroff
break
else
echo "Invalid choice, please try again."
fi
done
10 changes: 10 additions & 0 deletions packages/os/usb-installer/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail

mkdir -p build
docker build -f usb-installer.Dockerfile --platform linux/amd64 -t usb-installer ../
docker export -o build/rootfs.tar $(docker run -d usb-installer /bin/true)
docker build -f builder.Dockerfile -t usb-installer:builder .
docker run --entrypoint /data/build.sh -v $PWD:/data --privileged usb-installer:builder

# qemu-system-x86_64 -net nic -net user -machine accel=tcg -m 2048 -hda build/umbrelos-amd64-usb-installer.img -bios OVMF.fd
57 changes: 57 additions & 0 deletions packages/os/usb-installer/usb-installer.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
FROM debian:bookworm-slim

RUN echo "root:root" | chpasswd

RUN apt-get -y update

# Install Linux kernel, systemd, bootloader and script deps
RUN apt-get install --yes --no-install-recommends linux-image-amd64 systemd-sysv systemd-boot xz-utils dmidecode

# We can't install the bootloader via `bootctl install` from Docker because it complains
# about an invalid ESP partition. We can't easily fix it with loopback mounts from a Docker
# build environment. Instead we just manually install the bootloader to /boot and
# migrate /boot to an ESP partition in a post processing step outside of Docker.
RUN mkdir -p "/boot/EFI/systemd/"
RUN mkdir -p "/boot/EFI/BOOT/"
RUN cp "/usr/lib/systemd/boot/efi/systemd-bootx64.efi" "/boot/EFI/systemd/systemd-bootx64.efi"
RUN cp "/usr/lib/systemd/boot/efi/systemd-bootx64.efi" "/boot/EFI/BOOT/bootx64.efi"

# Generate boot config
RUN mkdir -p "/boot/loader/entries"

RUN echo " \n\
title Debian \n\
linux $(ls /boot/vmlinuz-* | sed 's/\/boot//') \n\
initrd $(ls /boot/initrd.img* | sed 's/\/boot//') \n\
options root=LABEL=ROOTFS rw quiet loglevel=1" | tee "/boot/loader/entries/debian.conf"

RUN echo " \n\
default debian \n\
timeout 0 \n\
console-mode max \n\
editor no" | tee "/boot/loader/loader.conf"

# Verify boot status
RUN bootctl --esp-path=/boot status

# Reduce size
# We have to do this extremely aggreseively because we're close to GitHub's 2GB release asset limit
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* /tmp/* /usr/share/man /usr/share/doc /usr/share/info /var/log/*
RUN find / -name '*.a' -delete && \
find / -name '*.so*' -exec strip --strip-debug {} \;
RUN rm -rf /usr/lib/modules/6.1.0-20-amd64/kernel/drivers/gpu
RUN rm -rf /usr/lib/modules/6.1.0-20-amd64/kernel/drivers/net
RUN rm -rf /usr/lib/modules/6.1.0-20-amd64/kernel/drivers/infiniband
RUN rm -rf /usr/lib/modules/6.1.0-20-amd64/kernel/net
RUN rm -rf /usr/lib/modules/6.1.0-20-amd64/kernel/sound

# Copy in umbrelOS image
COPY build/umbrelos-amd64.img.xz /

# Copy in filesystem overlay
COPY usb-installer/overlay /

# Configure TTY services
RUN systemctl enable custom-tty.service
RUN systemctl mask console-getty.service
RUN systemctl mask [email protected]

0 comments on commit 135f62e

Please sign in to comment.