Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,43 @@ Alternatively to running the whole browser-based GUI, you can run the `installer
The end result will be exactly the same.
Just don't forget to edit the configuration options (especially the `DISK` variable) before running it.

### Building Locally Or In A VM

Build compiled components with:

./build-compiled-components.sh

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

./internal-tools/build-compiled-components.sh


or manually:

cd frontend && npm run build
cd ../backend && go build -o opinionated-installer

For installer image work (`make_image.sh`, `installer.sh`), keep in mind:

- The safest default is to build a raw image file first and only write that image to a USB stick as a separate, explicit step.

Example flow using the new native image-file mode in `make_image.sh`:

sudo IMAGE_FILE=/tmp/odin-test-disk.img IMAGE_SIZE=20G ./make_image.sh --non-interactive

This creates the raw image file, attaches it to a loop device automatically, builds the installer image into it and detaches the loop device on exit.

To write the resulting image to a USB stick afterwards:

sudo dd if=/tmp/odin-test-disk.img of=/dev/sdX bs=256M oflag=dsync status=progress
sync

### Creating Your Own Installer Image

1. Insert a blank storage device
2. Edit the **DISK** and other variables at the top of `make_image.sh`
3. Execute `make_image.sh` as root
1. Build to a raw image file first (recommended): `sudo IMAGE_FILE=/tmp/opinionated.img IMAGE_SIZE=20G ./make_image.sh --non-interactive`
2. If you need interactive prompts, remove `--non-interactive`
3. If you really want to build directly to a block device, set **DISK** and other variables at the top of `make_image.sh` or pass them via environment
4. Optionally write the generated image file to removable media with `dd`

Minimal host/VM package set for `make_image.sh`:

sudo apt update
sudo apt install -y btrfs-progs debootstrap dosfstools golang-go kpartx npm systemd-repart udev uuid-runtime

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to point this out for the user; the script will install these


In the first stage of image generation, you will get a _tasksel_ prompt where you can select a different set of packages for your image.

Expand Down Expand Up @@ -295,4 +327,4 @@ Please set up your torrent client to follow the RSS feed below and seed all new

Tell your friends about the installer.
If you are active on social media, please share!
Follow the author on [mastodon](https://mas.to/@r0b0).
Follow the author on [mastodon](https://mas.to/@r0b0).
3 changes: 3 additions & 0 deletions installer-files/etc/dracut.conf.d/10-no-hostonly.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Build full initramfs with all drivers instead of host-only
# This ensures the initramfs works across different hardware configurations
hostonly="no"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There already is a hostonly="no" in 90-odin.conf

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was needed, as during my tests installing a new system it didn't work when this wasn't added

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is weird because your are not in fact copying your 10-no-hostonly.conf file to the actual image (using install_file()), are you?

12 changes: 10 additions & 2 deletions installer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ top_level_mount=/mnt/top_level_mount
target=/target
kernel_params="rw quiet rootfstype=btrfs rootflags=${FSFLAGS},subvol=@ rd.auto=1 splash"
if [ "${DISABLE_LUKS}" != "true" ]; then
kernel_params="rd.luks.options=tpm2-device=auto ${kernel_params}"
luks_device_name=root
root_device=/dev/mapper/${luks_device_name}
else
Expand Down Expand Up @@ -171,10 +170,15 @@ if [ "${DISABLE_LUKS}" != "true" ]; then
rm -f /tmp/passwd
cryptsetup luksUUID "${main_partition}" > luks.uuid
root_uuid=$(cat luks.uuid)
# Add LUKS parameters to kernel cmdline
kernel_params="rd.luks.uuid=${root_uuid} rd.luks.name=${root_uuid}=${luks_device_name} rd.luks.options=tpm2-device=auto root=${root_device} ${kernel_params}"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can leave rd.luks.uuid out when you use rd.luks.name. No biggie, just reduces the number of options.

if [ ! -e ${root_device} ]; then
notify open luks on root
cryptsetup luksOpen ${main_partition} ${luks_device_name} --key-file $KEYFILE
fi
else
# Without LUKS, just set the root device
kernel_params="root=${root_device} ${kernel_params}"
fi

btrfs_uuid=$(lsblk -no UUID ${root_device})
Expand Down Expand Up @@ -381,6 +385,9 @@ fi
cat <<EOF > ${target}/tmp/run1.sh
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive

# Ensure package lists are updated before installing from backports
apt update -y
apt install -y locales tasksel network-manager sudo
apt install -y -t ${BACKPORTS_VERSION} systemd shim-signed systemd-boot systemd-boot-efi-amd64-signed systemd-ukify sbsigntool dracut btrfs-progs cryptsetup tpm2-tools tpm-udev

Expand Down Expand Up @@ -462,7 +469,6 @@ firmware-misc-nonfree
firmware-myricom
firmware-netronome
firmware-netxen
firmware-qcom-soc
firmware-qlogic
firmware-realtek
firmware-ti-connectivity
Expand All @@ -481,6 +487,8 @@ cat <<EOF > ${target}/tmp/run2.sh
set -euo pipefail

export DEBIAN_FRONTEND=noninteractive
apt update -y || echo "Warning: apt update failed, continuing anyway"

xargs apt install -y < /tmp/packages.txt
apt install -t ${BACKPORTS_VERSION} -y dracut initramfs-tools- initramfs-tools-core- initramfs-tools-bin- \
busybox- klibc-utils- libklibc-
Expand Down
198 changes: 171 additions & 27 deletions make_image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,50 +19,182 @@
set -euo pipefail

# edit this:
DISK=/dev/vdb
USERNAME=live
DEBIAN_VERSION=trixie
DISK=${DISK:-/dev/vdb}
IMAGE_FILE=${IMAGE_FILE:-}
IMAGE_SIZE=${IMAGE_SIZE:-5G}
USERNAME=${USERNAME:-live}
DEBIAN_VERSION=${DEBIAN_VERSION:-trixie}
BACKPORTS_VERSION=${DEBIAN_VERSION}-backports
FSFLAGS="compress=zstd:15"
BOOTSTRAP_IMAGE=/var/cache/opinionated-debian-installer/bootstrap.btrfs
FSFLAGS=${FSFLAGS:-"compress=zstd:15"}
BOOTSTRAP_IMAGE=${BOOTSTRAP_IMAGE:-/var/cache/opinionated-debian-installer/bootstrap.btrfs}
NON_INTERACTIVE=${NON_INTERACTIVE:-false}
TASKSEL_TASKS=${TASKSEL_TASKS:-"task-ssh-server"}

for arg in "$@"; do
case "$arg" in
--non-interactive|-y)
NON_INTERACTIVE=true
;;
-h|--help)
cat <<EOF
Usage: $(basename "$0") [--non-interactive|-y]

Options:
--non-interactive, -y Accept defaults and skip Enter prompts.
--help, -h Show this help.

Configuration values are set via environment variables:
DISK Target disk (default: /dev/vdb)
IMAGE_FILE Output image file path (if set, creates loop image)
IMAGE_SIZE Image size (default: 3G)
USERNAME Default user (default: live)
DEBIAN_VERSION Debian version (default: trixie)
TASKSEL_TASKS Space-separated tasksel tasks (default: task-ssh-server)
Available: task-desktop, task-kde-desktop, task-gnome-desktop,
task-xfce-desktop, task-ssh-server, task-web-server, etc.
EOF
exit 0
;;
*)
echo "Unknown argument: $arg" >&2
exit 2
;;
esac
done

target=/target
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
LOOP_DEVICE=
STATE_DIR=${STATE_DIR:-/var/tmp/opinionated-debian-installer}
REPART_DIR="${STATE_DIR}/repart.d"
EFI_UUID_FILE="${STATE_DIR}/efi-part.uuid"
INSTALLER_IMAGE_UUID_FILE="${STATE_DIR}/installer-image-part.uuid"
DISK_WIPED_MARKER="${STATE_DIR}/disk_wiped.txt"
FIRST_PHASE_DONE_MARKER="${STATE_DIR}/first_phase_done.txt"

mkdir -p "${STATE_DIR}"

function cleanup() {
if mountpoint -q "${target}/home"; then
umount "${target}/home"
fi
if mountpoint -q "${target}"; then
umount -R "${target}"
fi
if mountpoint -q "/mnt/btrfs1"; then
umount -R /mnt/btrfs1
fi
if [ -n "${LOOP_DEVICE}" ] && losetup "${LOOP_DEVICE}" >/dev/null 2>&1; then
losetup -d "${LOOP_DEVICE}"
fi
}

trap cleanup EXIT

function notify {
echo -en "\033[32m$*\033[0m> "
read -r
if [ "${NON_INTERACTIVE}" = "true" ]; then
echo "$*"
else
echo -en "\033[32m$*\033[0m> "
read -r
fi
}

function attach_loop_image() {
local image_file="$1"
local loop_dev

# In containers, /dev/loopN nodes can be sparse (for example missing loop1).
# Try explicitly to attach using existing block devices.
for loop_dev in /dev/loop[0-9]*; do
if [[ "${loop_dev}" =~ p[0-9]+$ ]]; then

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry, what does this do?

continue
fi
if [ ! -b "${loop_dev}" ]; then
continue
fi
# Check if the device is free
if losetup "${loop_dev}" >/dev/null 2>&1; then
continue
fi
# Try to attach the image to this device
if losetup -P --show "${loop_dev}" "${image_file}"; then
return 0
fi
done

# If no explicit device works, fall back to losetup -f
# but do NOT try to create missing nodes (leads to permission errors in containers).
losetup -fP --show "${image_file}"
}

if [ "$(id -u)" -ne 0 ]; then
echo 'This script must be run by root' >&2
exit 1
fi

if [ -n "${IMAGE_FILE}" ]; then
mkdir -p "$(dirname "${IMAGE_FILE}")"
if [ ! -f "${FIRST_PHASE_DONE_MARKER}" ]; then
# Phase 1 not completed - always start with a clean image to avoid
# stale partition tables / corrupt btrfs from a previous failed run.
if [ -f "${IMAGE_FILE}" ]; then
notify removing stale image file ${IMAGE_FILE} to start fresh
rm -f "${IMAGE_FILE}"
fi
rm -f "${DISK_WIPED_MARKER}"
fi
if [ ! -f "${IMAGE_FILE}" ]; then
notify creating raw image file ${IMAGE_FILE} with size ${IMAGE_SIZE}
truncate -s "${IMAGE_SIZE}" "${IMAGE_FILE}"
fi
notify attaching ${IMAGE_FILE} as a loop device
if ! LOOP_DEVICE=$(attach_loop_image "${IMAGE_FILE}"); then
cat >&2 <<EOF
Failed to attach ${IMAGE_FILE} to a loop device.

If running in a devcontainer, ensure it has the needed privileges/devices:
- privileged mode (or CAP_SYS_ADMIN + CAP_MKNOD)
- /dev/loop-control
- /dev/loop* device nodes
EOF
exit 1
fi
DISK=${LOOP_DEVICE}
udevadm settle

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this block after the install required packages block to make sure udevadm is actually installed at this point

fi

notify install required packages
apt update -y
DEBIAN_FRONTEND=noninteractive apt install -y \
btrfs-progs \
debootstrap \
dosfstools \
golang-go \
kpartx \
npm \
systemd-repart \
udev \
uuid-runtime

if [ ! -f efi-part.uuid ]; then
if [ ! -f "${EFI_UUID_FILE}" ]; then
echo generate uuid for efi partition
uuidgen > efi-part.uuid
uuidgen > "${EFI_UUID_FILE}"
fi
if [ ! -f installer-image-part.uuid ]; then
if [ ! -f "${INSTALLER_IMAGE_UUID_FILE}" ]; then
echo generate uuid for installer image partition
uuidgen > installer-image-part.uuid
uuidgen > "${INSTALLER_IMAGE_UUID_FILE}"
fi
efi_uuid=$(cat efi-part.uuid)
installer_image_uuid=$(cat installer-image-part.uuid)
efi_uuid=$(cat "${EFI_UUID_FILE}")
installer_image_uuid=$(cat "${INSTALLER_IMAGE_UUID_FILE}")

notify setting up partitions on ${DISK}
mkdir -p /mnt/btrfs1
mkdir -p ${target}/home
rm -rf repart.d
mkdir -p repart.d
rm -rf "${REPART_DIR}"
mkdir -p "${REPART_DIR}"

cat <<EOF > repart.d/01_efi.conf
cat <<EOF > "${REPART_DIR}/01_efi.conf"
[Partition]
Type=esp
UUID=${efi_uuid}
Expand All @@ -71,7 +203,7 @@ SizeMaxBytes=300M
Format=vfat
EOF

cat <<EOF > repart.d/02_baseImage.conf
cat <<EOF > "${REPART_DIR}/02_baseImage.conf"
[Partition]
Type=root
Label=Opinionated Debian Installer
Expand All @@ -84,14 +216,20 @@ GrowFileSystem=on
Encrypt=off
EOF

if [ ! -f disk_wiped.txt ]; then
if [ ! -f "${DISK_WIPED_MARKER}" ]; then
wipefs --all ${DISK}
touch disk_wiped.txt
touch "${DISK_WIPED_MARKER}"
fi

# sector-size: see https://github.com/systemd/systemd/issues/37801
# remove with systemd 258
systemd-repart --sector-size=512 --empty=allow --no-pager --definitions=repart.d --dry-run=no ${DISK}
systemd-repart --sector-size=512 --empty=allow --no-pager --definitions="${REPART_DIR}" --dry-run=no ${DISK}

# Wait for kernel to recognize new partitions and create device symlinks
notify waiting for kernel to probe partitions
sleep 2
blockdev --rereadpt ${DISK} || true
udevadm settle --timeout=30

root_device=/dev/disk/by-partuuid/${installer_image_uuid}
efi_device=/dev/disk/by-partuuid/${efi_uuid}
Expand Down Expand Up @@ -228,7 +366,6 @@ firmware-misc-nonfree
firmware-myricom
firmware-netronome
firmware-netxen
firmware-qcom-soc
firmware-qlogic
firmware-realtek
firmware-ti-connectivity
Expand All @@ -252,8 +389,10 @@ xargs apt install -t ${BACKPORTS_VERSION} -y < /tmp/packages_backports.txt
EOF
chroot ${target}/ bash /tmp/run2.sh

notify running tasksel
chroot ${target}/ tasksel
notify installing tasksel selections: ${TASKSEL_TASKS}
for task in ${TASKSEL_TASKS}; do
chroot ${target}/ apt install -y "${task}" || echo "Warning: failed to install ${task}"
done

if mountpoint -q "${target}/var/cache/apt/archives" ; then
notify unmounting apt cache directory from target
Expand Down Expand Up @@ -290,15 +429,15 @@ rm -f ${target}/etc/crypttab
rm -f ${target}/var/log/*log
rm -f ${target}/var/log/apt/*log

if [ ! -f first_phase_done.txt ]; then
if [ ! -f "${FIRST_PHASE_DONE_MARKER}" ]; then
notify create snapshot after first phase
(cd /mnt/btrfs1; btrfs subvolume snapshot -r @ opinionated_installer_bootstrap)
mkdir -p $(dirname $BOOTSTRAP_IMAGE)
if [ ! -f $BOOTSTRAP_IMAGE ]; then
notify storing bootstrap data to $BOOTSTRAP_IMAGE
btrfs send --compressed-data /mnt/btrfs1/opinionated_installer_bootstrap > $BOOTSTRAP_IMAGE
fi
touch first_phase_done.txt
touch "${FIRST_PHASE_DONE_MARKER}"
fi

function install_file() {
Expand Down Expand Up @@ -424,7 +563,8 @@ cat <<EOF > ${target}/tmp/run1.sh
set -euo pipefail

# see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1095646
ln -s /dev/null /etc/kernel/install.d/50-dracut.install
mkdir -p /etc/kernel/install.d
ln -sf /dev/null /etc/kernel/install.d/50-dracut.install

export DEBIAN_FRONTEND=noninteractive
apt -t ${BACKPORTS_VERSION} install linux-image-amd64 -y
Expand Down Expand Up @@ -493,4 +633,8 @@ sync
umount -R ${target}
umount -R /mnt/btrfs1

echo "INSTALLATION FINISHED"
if [ -n "${IMAGE_FILE}" ]; then
echo "INSTALLATION FINISHED: ${IMAGE_FILE}"
else
echo "INSTALLATION FINISHED"
fi
Loading