`iso/build-iso.sh --offline` (and the workflow's new `offline` box, on by default) adds ~420 MB to the ISO and copies it to /opt/kiosk-offline: - apt/: local repository with every package install.sh's provisioning and the CUPS addon install ask for, NodeSource's nodejs, and all dependencies a fresh server install doesn't already have. Resolved against the source ISO's own package manifest via a synthesized dpkg status (iso/offline/fake-dpkg-status.py), so apt downloads only what's missing and makes the same virtual/alternative choices as the target. - npm/: prebuilt kiosk-app (Electron 42, binary included - it no longer downloads at npm install time) and Web UI node_modules. On first boot, kiosk-firstboot installs online as before when there's internet; with none it waits KIOSK_OFFLINE_WAIT (90s), then runs `kiosk-offline-apt on` (apt.conf.d drop-in: bundle is the ONLY source) and turns it off again afterwards. install.sh notices offline mode via offline_mode_active (lib/config.sh) and installs nodejs from the bundle and unpacks prebuilt node_modules instead of npm install. Package lists moved into PROVISION_APT_PACKAGES / PROVISION_INTEL_APT_PACKAGES / CUPS_APT_PACKAGES so the bundler reads the same lists install.sh installs from. Tested: built against the 26.04.1 ISO, then in a network-less chroot of the ISO's own server filesystem installed every package set, ran provision_create_kiosk_user/install_nodejs/install_app and webui_install_app_files in offline mode (Electron runs, no missing libraries, chrome-sandbox setuid), and ran kiosk-firstboot through the offline switch with a stub install.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
381 lines
18 KiB
Bash
Executable File
381 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
|
################################################################################
|
|
# iso/build-iso.sh - Build an Ubuntu Server 26.04 install ISO with the
|
|
# kiosk built in, so there's no separate "install Ubuntu, then clone and
|
|
# run ./install.sh" step.
|
|
#
|
|
# What the ISO does:
|
|
# 1. Boots the stock Ubuntu Server installer (Subiquity), driven by an
|
|
# autoinstall.yaml at the root of the ISO. By default it still asks
|
|
# for network, disk and your admin username/password - everything
|
|
# else (language, keyboard, SSH server, snaps...) is answered for
|
|
# you. Flags below make any of those automatic too.
|
|
# 2. At the end of the install (autoinstall late-commands), copies this
|
|
# repo to /opt/ubuntu-based-kiosk on the new system and enables a
|
|
# one-shot first-boot service (firstboot/kiosk-firstboot.service).
|
|
# 3. On the first boot of the installed system, that service takes over
|
|
# tty1 and runs ./install.sh as your admin user from
|
|
# ~/ubuntu-based-kiosk - interactively by default (same prompts as a
|
|
# manual install), or with --unattended taking every default and
|
|
# rebooting straight into the kiosk. Configure sites afterwards via
|
|
# the Web UI (http://<ip>:8090) or ./install.sh.
|
|
#
|
|
# Nothing in the original ISO is changed except /boot/grub/grub.cfg
|
|
# (menu title, plus the `autoinstall` kernel argument with --no-confirm);
|
|
# boot records (BIOS + UEFI) are replayed from the original as-is.
|
|
#
|
|
# Requirements (on the build machine, any recent Ubuntu/Debian):
|
|
# sudo apt install xorriso curl git openssl
|
|
# plus, for --offline (amd64 build machine only):
|
|
# sudo apt install apt-utils python3 gnupg ubuntu-keyring
|
|
#
|
|
# Usage:
|
|
# iso/build-iso.sh [options]
|
|
#
|
|
# --iso PATH Use this Ubuntu Server ISO instead of downloading.
|
|
# --release VER Ubuntu release to download (default: 26.04 - picks
|
|
# the newest 26.04.x live-server-amd64 point release).
|
|
# --output PATH Output ISO (default: iso/build/ubuntu-<ver>-kiosk-amd64.iso)
|
|
#
|
|
# Installed system:
|
|
# --username NAME Admin username (the account you SSH in with and run
|
|
# ./install.sh as - must not be "kiosk"). Giving this
|
|
# makes the installer's identity screen automatic.
|
|
# --password PASS Admin password (prompted for if --username is given
|
|
# without it, or read from $KIOSK_ADMIN_PASSWORD -
|
|
# better than this flag on shared machines/CI, where
|
|
# command lines are visible). Stored on the ISO only
|
|
# as a SHA-512 crypt hash.
|
|
# --hostname NAME Hostname (default: kiosk).
|
|
# --realname NAME Full name for the admin account (default: Kiosk Admin).
|
|
# --ssh-key FILE Add this public key file to the admin user's
|
|
# authorized_keys.
|
|
# --locale LOCALE Default: en_US.UTF-8
|
|
# --keyboard LAYOUT Default: us
|
|
# --auto-storage Don't ask about disks: wipe the largest disk and use
|
|
# all of it (LVM). DESTROYS DATA on that disk.
|
|
# --auto-network Don't show the network screen (DHCP on wired
|
|
# interfaces). Leave this off if you need WiFi.
|
|
# --no-confirm Don't ask "Continue with autoinstall?" at boot.
|
|
# --fully-automatic Shorthand for --auto-storage --auto-network
|
|
# --no-confirm --unattended; needs --username.
|
|
# Booting this ISO will wipe a disk with NO questions.
|
|
#
|
|
# Kiosk install on first boot:
|
|
# --unattended Run install.sh with all defaults, no prompts
|
|
# (skips the Core Settings menu), then reboot.
|
|
# --repo-url URL git remote recorded in the copied repo, used by
|
|
# Advanced -> Upgrade (default: this checkout's
|
|
# origin, else the upstream GitHub repo).
|
|
# --offline Also bundle everything install.sh downloads (~420 MB:
|
|
# every apt package incl. the CUPS printing addon,
|
|
# Node.js, prebuilt Electron/Web UI node_modules), so
|
|
# first boot can install the kiosk with NO internet.
|
|
# With internet it still installs online as usual;
|
|
# offline it waits 90s, then uses the bundle. The
|
|
# bundle stays on the kiosk at /opt/kiosk-offline
|
|
# (see iso/firstboot/kiosk-offline-apt for using it
|
|
# later, e.g. adding CUPS while offline).
|
|
#
|
|
# The repo copied onto the ISO is a clone of this checkout's current HEAD
|
|
# (committed changes only - uncommitted edits are NOT included).
|
|
################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
BUILD_DIR="$SCRIPT_DIR/build"
|
|
DEFAULT_REPO_URL="https://github.com/outis1one/ubuntu-based-kiosk.git"
|
|
|
|
SRC_ISO=""
|
|
RELEASE="26.04"
|
|
OUTPUT=""
|
|
ADMIN_USER=""
|
|
ADMIN_PASS="${KIOSK_ADMIN_PASSWORD:-}"
|
|
HOSTNAME_="kiosk"
|
|
REALNAME="Kiosk Admin"
|
|
SSH_KEY_FILE=""
|
|
LOCALE="en_US.UTF-8"
|
|
KEYBOARD="us"
|
|
AUTO_STORAGE=0
|
|
AUTO_NETWORK=0
|
|
NO_CONFIRM=0
|
|
UNATTENDED=0
|
|
OFFLINE=0
|
|
REPO_URL=""
|
|
|
|
# Usernames the Ubuntu installer rejects (system users/groups) - copied
|
|
# from subiquity's reserved-usernames so a bad --username fails here
|
|
# instead of at boot with "Username is reserved by the system".
|
|
RESERVED_USERNAMES="
|
|
root daemon bin sys sync games man lp mail news uucp proxy www-data
|
|
backup list irc gnats nobody adm tty disk kmem dialout fax voice cdrom
|
|
floppy tape sudo audio dip operator src shadow utmp video sasl plugdev
|
|
staff users nogroup netplan ftn mysql tac-plus alias qmail qmaild
|
|
qmails qmailr qmailq qmaill qmailp asterisk vpopmail vchkpw slurm
|
|
hacluster haclient grsec-tpe grsec-sock-all grsec-sock-clt
|
|
grsec-sock-srv grsec-proc ceph opensrf libvirt-qemu admin Debian-exim
|
|
bind crontab cupsys dcc dhcp dictd dnsmasq dovecot fetchmail firebird
|
|
ftp fuse gdm haldaemon hplilp identd input jwhois klog kvm lpadmin maas
|
|
messagebus mythtv netdev powerdev radvd render saned sbuild scanner sgx
|
|
slocate ssh sshd ssl-cert sslwrap statd syslog telnetd tftpd
|
|
"
|
|
|
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
info() { echo "==> $*"; }
|
|
|
|
usage() {
|
|
sed -n '/^# Usage:/,/^################/p' "$0" | sed '$d; s/^# \{0,1\}//'
|
|
exit "${1:-0}"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--iso) SRC_ISO="$2"; shift 2 ;;
|
|
--release) RELEASE="$2"; shift 2 ;;
|
|
--output) OUTPUT="$2"; shift 2 ;;
|
|
--username) ADMIN_USER="$2"; shift 2 ;;
|
|
--password) ADMIN_PASS="$2"; shift 2 ;;
|
|
--hostname) HOSTNAME_="$2"; shift 2 ;;
|
|
--realname) REALNAME="$2"; shift 2 ;;
|
|
--ssh-key) SSH_KEY_FILE="$2"; shift 2 ;;
|
|
--locale) LOCALE="$2"; shift 2 ;;
|
|
--keyboard) KEYBOARD="$2"; shift 2 ;;
|
|
--auto-storage) AUTO_STORAGE=1; shift ;;
|
|
--auto-network) AUTO_NETWORK=1; shift ;;
|
|
--no-confirm) NO_CONFIRM=1; shift ;;
|
|
--unattended) UNATTENDED=1; shift ;;
|
|
--fully-automatic) AUTO_STORAGE=1; AUTO_NETWORK=1; NO_CONFIRM=1; UNATTENDED=1; shift ;;
|
|
--repo-url) REPO_URL="$2"; shift 2 ;;
|
|
--offline) OFFLINE=1; shift ;;
|
|
-h|--help) usage 0 ;;
|
|
*) echo "Unknown option: $1" >&2; usage 1 ;;
|
|
esac
|
|
done
|
|
|
|
################################################################################
|
|
# Validate
|
|
################################################################################
|
|
|
|
for cmd in xorriso curl git openssl sha256sum; do
|
|
command -v "$cmd" &>/dev/null || die "'$cmd' not found - install it: sudo apt install xorriso curl git openssl"
|
|
done
|
|
|
|
if [[ -n "$ADMIN_USER" ]]; then
|
|
[[ "$ADMIN_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "invalid username: $ADMIN_USER"
|
|
[[ "$ADMIN_USER" != "kiosk" && "$ADMIN_USER" != "root" ]] \
|
|
|| die "username '$ADMIN_USER' is reserved (install.sh creates its own 'kiosk' user)"
|
|
[[ " $(echo $RESERVED_USERNAMES) " != *" $ADMIN_USER "* ]] \
|
|
|| die "username '$ADMIN_USER' is reserved by Ubuntu (a system user/group) - pick another"
|
|
if [[ -z "$ADMIN_PASS" ]]; then
|
|
[[ -t 0 ]] || die "--username needs a password: pass --password or set KIOSK_ADMIN_PASSWORD"
|
|
read -r -s -p "Password for $ADMIN_USER: " ADMIN_PASS; echo
|
|
read -r -s -p "Confirm password: " pass2; echo
|
|
[[ "$ADMIN_PASS" == "$pass2" ]] || die "passwords don't match"
|
|
[[ -n "$ADMIN_PASS" ]] || die "empty password"
|
|
fi
|
|
elif [[ -n "$ADMIN_PASS" ]]; then
|
|
die "--password needs --username"
|
|
fi
|
|
|
|
if (( AUTO_STORAGE && AUTO_NETWORK && NO_CONFIRM )) && [[ -z "$ADMIN_USER" ]]; then
|
|
die "--fully-automatic (or all of --auto-storage --auto-network --no-confirm) needs --username"
|
|
fi
|
|
|
|
[[ "$HOSTNAME_" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]] || die "invalid hostname: $HOSTNAME_"
|
|
[[ -z "$SSH_KEY_FILE" || -r "$SSH_KEY_FILE" ]] || die "can't read --ssh-key file: $SSH_KEY_FILE"
|
|
|
|
if [[ -z "$REPO_URL" ]]; then
|
|
REPO_URL="$(git -C "$REPO_ROOT" remote get-url origin 2>/dev/null || true)"
|
|
# Local proxies/mirrors and ssh remotes aren't reachable from a kiosk.
|
|
[[ "$REPO_URL" == https://github.com/* ]] || REPO_URL="$DEFAULT_REPO_URL"
|
|
fi
|
|
|
|
mkdir -p "$BUILD_DIR"
|
|
WORK="$(mktemp -d "$BUILD_DIR/work.XXXXXX")"
|
|
trap 'rm -rf "$WORK"' EXIT
|
|
|
|
################################################################################
|
|
# Source ISO - download and verify unless one was given
|
|
################################################################################
|
|
|
|
if [[ -z "$SRC_ISO" ]]; then
|
|
base="https://releases.ubuntu.com/$RELEASE"
|
|
info "Looking up the latest Ubuntu $RELEASE live-server ISO..."
|
|
sums="$(curl -fsSL "$base/SHA256SUMS")" || die "couldn't fetch $base/SHA256SUMS"
|
|
iso_name="$(grep -oE "ubuntu-${RELEASE//./\\.}(\.[0-9]+)?-live-server-amd64\.iso" <<<"$sums" | sort -V | tail -1)"
|
|
[[ -n "$iso_name" ]] || die "no live-server-amd64 ISO listed in $base/SHA256SUMS"
|
|
expected="$(grep -F "*$iso_name" <<<"$sums" | awk '{print $1}')"
|
|
|
|
mkdir -p "$BUILD_DIR/cache"
|
|
SRC_ISO="$BUILD_DIR/cache/$iso_name"
|
|
if [[ -f "$SRC_ISO" ]] && echo "$expected $SRC_ISO" | sha256sum -c --status; then
|
|
info "Using cached $iso_name"
|
|
else
|
|
info "Downloading $iso_name (~3 GB)..."
|
|
curl -fL --retry 5 -C - -o "$SRC_ISO" "$base/$iso_name" \
|
|
|| curl -fL --retry 5 -o "$SRC_ISO" "$base/$iso_name"
|
|
info "Verifying checksum..."
|
|
echo "$expected $SRC_ISO" | sha256sum -c --status \
|
|
|| { rm -f "$SRC_ISO"; die "checksum mismatch for $iso_name - deleted, re-run to retry"; }
|
|
fi
|
|
fi
|
|
[[ -f "$SRC_ISO" ]] || die "ISO not found: $SRC_ISO"
|
|
|
|
if [[ -z "$OUTPUT" ]]; then
|
|
src_base="$(basename "$SRC_ISO" .iso)"
|
|
kind="kiosk"; (( OFFLINE )) && kind="kiosk-offline"
|
|
OUTPUT="$BUILD_DIR/${src_base/-live-server/-$kind}.iso"
|
|
[[ "$OUTPUT" != "$BUILD_DIR/$src_base.iso" ]] || OUTPUT="$BUILD_DIR/${src_base}-$kind.iso"
|
|
fi
|
|
|
|
################################################################################
|
|
# Stage the repo + first-boot files that go on the ISO under /kiosk
|
|
################################################################################
|
|
|
|
# Branch the copied repo is on - what Advanced -> Upgrade pulls. CI
|
|
# checkouts (actions/checkout) are a detached HEAD, so fall back to the
|
|
# branch CI says it built, then main.
|
|
BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
|
|
[[ "$BRANCH" != "HEAD" ]] || BRANCH="${GITHUB_REF_NAME:-main}"
|
|
COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
|
|
|
info "Staging repo ($BRANCH @ ${COMMIT:0:7})..."
|
|
if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then
|
|
echo " WARNING: uncommitted changes in $REPO_ROOT are NOT included - commit them first if you need them."
|
|
fi
|
|
mkdir -p "$WORK/kiosk/firstboot"
|
|
# init + fetch by commit rather than `git clone --branch`, which can't
|
|
# clone a detached HEAD.
|
|
stage_repo="$WORK/kiosk/ubuntu-based-kiosk"
|
|
git init --quiet "$stage_repo"
|
|
git -C "$stage_repo" fetch --quiet --depth 1 "file://$REPO_ROOT" "$COMMIT"
|
|
git -C "$stage_repo" checkout --quiet -B "$BRANCH" FETCH_HEAD
|
|
git -C "$stage_repo" remote add origin "$REPO_URL"
|
|
git -C "$stage_repo" config "branch.$BRANCH.remote" origin
|
|
git -C "$stage_repo" config "branch.$BRANCH.merge" "refs/heads/$BRANCH"
|
|
|
|
install -m 755 "$SCRIPT_DIR/firstboot/kiosk-firstboot" "$WORK/kiosk/firstboot/"
|
|
install -m 644 "$SCRIPT_DIR/firstboot/kiosk-firstboot.service" "$WORK/kiosk/firstboot/"
|
|
install -m 755 "$SCRIPT_DIR/firstboot/kiosk-offline-apt" "$WORK/kiosk/firstboot/"
|
|
cat > "$WORK/kiosk/firstboot/kiosk-firstboot.conf" <<EOF
|
|
# Read by /usr/local/sbin/kiosk-firstboot. Written by iso/build-iso.sh.
|
|
# 1 = take every install.sh default and reboot into the kiosk; 0 = interactive.
|
|
KIOSK_UNATTENDED=$UNATTENDED
|
|
EOF
|
|
|
|
if (( OFFLINE )); then
|
|
info "Building offline bundle (packages, Node.js, Electron)..."
|
|
"$SCRIPT_DIR/offline/build-bundle.sh" "$WORK/kiosk/offline" "$SRC_ISO" "$stage_repo" "$BUILD_DIR/offline-cache"
|
|
fi
|
|
|
|
################################################################################
|
|
# autoinstall.yaml
|
|
################################################################################
|
|
|
|
# Single-quoted YAML scalar.
|
|
yq() { printf "'%s'" "${1//\'/\'\'}"; }
|
|
|
|
interactive=()
|
|
(( AUTO_NETWORK )) || interactive+=(network)
|
|
(( AUTO_STORAGE )) || interactive+=(storage)
|
|
[[ -n "$ADMIN_USER" ]] || interactive+=(identity)
|
|
|
|
{
|
|
echo "#cloud-config"
|
|
echo "# Generated by iso/build-iso.sh - see that file for what this does."
|
|
echo "autoinstall:"
|
|
echo " version: 1"
|
|
if (( ${#interactive[@]} )); then
|
|
echo " interactive-sections:"
|
|
for s in "${interactive[@]}"; do echo " - $s"; done
|
|
fi
|
|
echo " locale: $(yq "$LOCALE")"
|
|
echo " keyboard:"
|
|
echo " layout: $(yq "$KEYBOARD")"
|
|
# Pre-selected answer for the storage screen when it's interactive;
|
|
# the whole answer when --auto-storage.
|
|
echo " storage:"
|
|
echo " layout:"
|
|
echo " name: lvm"
|
|
echo " sizing-policy: all"
|
|
if [[ -n "$ADMIN_USER" ]]; then
|
|
echo " identity:"
|
|
echo " hostname: $(yq "$HOSTNAME_")"
|
|
echo " realname: $(yq "$REALNAME")"
|
|
echo " username: $(yq "$ADMIN_USER")"
|
|
echo " password: $(yq "$(openssl passwd -6 -stdin <<<"$ADMIN_PASS")")"
|
|
fi
|
|
echo " ssh:"
|
|
echo " install-server: true"
|
|
echo " allow-pw: true"
|
|
if [[ -n "$SSH_KEY_FILE" ]]; then
|
|
echo " authorized-keys:"
|
|
while IFS= read -r key; do
|
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
echo " - $(yq "$key")"
|
|
done < "$SSH_KEY_FILE"
|
|
fi
|
|
cat <<'EOF'
|
|
late-commands:
|
|
- mkdir -p /target/opt /target/usr/local/sbin /target/var/lib/kiosk-firstboot
|
|
- cp -a /cdrom/kiosk/ubuntu-based-kiosk /target/opt/ubuntu-based-kiosk
|
|
- chmod -R u+w /target/opt/ubuntu-based-kiosk
|
|
- install -m 755 /cdrom/kiosk/firstboot/kiosk-firstboot /target/usr/local/sbin/kiosk-firstboot
|
|
- install -m 644 /cdrom/kiosk/firstboot/kiosk-firstboot.service /target/etc/systemd/system/kiosk-firstboot.service
|
|
- install -m 644 /cdrom/kiosk/firstboot/kiosk-firstboot.conf /target/etc/kiosk-firstboot.conf
|
|
- install -m 755 /cdrom/kiosk/firstboot/kiosk-offline-apt /target/usr/local/sbin/kiosk-offline-apt
|
|
- touch /target/var/lib/kiosk-firstboot/pending
|
|
- curtin in-target --target=/target -- systemctl enable kiosk-firstboot.service
|
|
EOF
|
|
if (( OFFLINE )); then
|
|
echo " - cp -a /cdrom/kiosk/offline /target/opt/kiosk-offline"
|
|
echo " - chmod -R u+w,a+rX /target/opt/kiosk-offline"
|
|
fi
|
|
} > "$WORK/autoinstall.yaml"
|
|
|
|
################################################################################
|
|
# grub.cfg - same file serves BIOS and UEFI boot on the server ISO
|
|
################################################################################
|
|
|
|
xorriso -osirrox on -indev "$SRC_ISO" -extract /boot/grub/grub.cfg "$WORK/grub.cfg" 2>/dev/null \
|
|
|| die "couldn't read /boot/grub/grub.cfg from $SRC_ISO - is it an Ubuntu Server ISO?"
|
|
chmod u+w "$WORK/grub.cfg"
|
|
grep -q '/casper/vmlinuz' "$WORK/grub.cfg" || die "unexpected grub.cfg layout in $SRC_ISO"
|
|
|
|
sed -i 's/Try or Install Ubuntu Server/Install Ubuntu Based Kiosk/' "$WORK/grub.cfg"
|
|
if (( NO_CONFIRM )); then
|
|
sed -i -E '/linux[[:space:]]+\/casper\/vmlinuz/ s/[[:space:]]+---/ autoinstall ---/' "$WORK/grub.cfg"
|
|
fi
|
|
|
|
################################################################################
|
|
# Write the new ISO
|
|
################################################################################
|
|
|
|
info "Writing $OUTPUT..."
|
|
rm -f "$OUTPUT"
|
|
xorriso -indev "$SRC_ISO" -outdev "$OUTPUT" \
|
|
-map "$WORK/autoinstall.yaml" /autoinstall.yaml \
|
|
-map "$WORK/kiosk" /kiosk \
|
|
-map "$WORK/grub.cfg" /boot/grub/grub.cfg \
|
|
-boot_image any replay \
|
|
-padding included \
|
|
> "$WORK/xorriso.log" 2>&1 \
|
|
|| { cat "$WORK/xorriso.log" >&2; rm -f "$OUTPUT"; die "xorriso failed"; }
|
|
|
|
echo
|
|
info "Done: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
|
echo
|
|
echo "Installer asks for : ${interactive[*]:-nothing}$( (( NO_CONFIRM )) || echo " (plus a 'Continue with autoinstall?' confirmation)")"
|
|
echo "Admin user : ${ADMIN_USER:-chosen during install}"
|
|
echo "First boot : $( (( UNATTENDED )) && echo "unattended install.sh, then reboots into the kiosk" || echo "interactive install.sh on the console")"
|
|
echo "Offline bundle : $( (( OFFLINE )) && echo "yes - installs with no internet (used after 90s offline)" || echo "no - first boot needs internet (add --offline to bundle everything)")"
|
|
echo
|
|
echo "Write it to a USB stick (replace /dev/sdX - this erases it):"
|
|
echo " sudo dd if='$OUTPUT' of=/dev/sdX bs=4M status=progress oflag=sync"
|
|
if (( AUTO_STORAGE && NO_CONFIRM )); then
|
|
echo
|
|
echo "WARNING: this ISO wipes the target's largest disk without asking. Label the stick."
|
|
fi
|