2 Commits
Author SHA1 Message Date
Claude a170dc6228 ISO: add --offline bundle so the kiosk installs with no internet
`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
2026-10-03 04:34:32 +00:00
Outis 125622cd74 Merge pull request #114 from outis1one/claude/happy-knuth-hwnvbn
Claude/happy knuth hwnvbn
2026-10-02 23:20:05 -04:00
11 changed files with 586 additions and 38 deletions
+14 -3
View File
@@ -39,6 +39,13 @@ on:
description: Optional SSH public key for the admin user (ssh-ed25519 AAAA...) description: Optional SSH public key for the admin user (ssh-ed25519 AAAA...)
type: string type: string
default: "" default: ""
offline:
description: >-
Bundle everything install.sh downloads (~420 MB: all packages incl.
CUPS printing, Node.js, Electron) so the kiosk installs with no
internet on first boot.
type: boolean
default: true
retention_days: retention_days:
description: Days to keep the ISO artifact description: Days to keep the ISO artifact
type: number type: number
@@ -57,11 +64,12 @@ jobs:
- name: Install build tools - name: Install build tools
run: | run: |
sudo apt-get update -q sudo apt-get update -q
sudo apt-get install -y -q xorriso sudo apt-get install -y -q xorriso apt-utils ubuntu-keyring
- name: Build ISO - name: Build ISO
env: env:
MODE: ${{ inputs.mode }} MODE: ${{ inputs.mode }}
OFFLINE: ${{ inputs.offline }}
ADMIN_USERNAME: ${{ inputs.username }} ADMIN_USERNAME: ${{ inputs.username }}
ADMIN_HOSTNAME: ${{ inputs.hostname }} ADMIN_HOSTNAME: ${{ inputs.hostname }}
SSH_PUBLIC_KEY: ${{ inputs.ssh_public_key }} SSH_PUBLIC_KEY: ${{ inputs.ssh_public_key }}
@@ -84,19 +92,22 @@ jobs:
printf '%s\n' "$SSH_PUBLIC_KEY" > "$RUNNER_TEMP/ssh_key.pub" printf '%s\n' "$SSH_PUBLIC_KEY" > "$RUNNER_TEMP/ssh_key.pub"
args+=(--ssh-key "$RUNNER_TEMP/ssh_key.pub") args+=(--ssh-key "$RUNNER_TEMP/ssh_key.pub")
fi fi
[[ "$OFFLINE" == "true" ]] && args+=(--offline)
iso/build-iso.sh "${args[@]}" iso/build-iso.sh "${args[@]}"
# Name the file after the Ubuntu point release the script picked. # Name the file after the Ubuntu point release the script picked.
src="$(find iso/build/cache -name "*.iso" -print -quit)" src="$(find iso/build/cache -name "*.iso" -print -quit)"
name="$(basename "$src" .iso | sed 's/-live-server/-kiosk/')-${MODE}-${GITHUB_SHA::7}.iso" kind="kiosk"; [[ "$OFFLINE" == "true" ]] && kind="kiosk-offline"
name="$(basename "$src" .iso | sed "s/-live-server/-$kind/")-${MODE}-${GITHUB_SHA::7}.iso"
mv "$RUNNER_TEMP/iso/ubuntu-kiosk-amd64.iso" "$RUNNER_TEMP/iso/$name" mv "$RUNNER_TEMP/iso/ubuntu-kiosk-amd64.iso" "$RUNNER_TEMP/iso/$name"
# Free the ~3 GB source ISO before the upload step needs scratch space. # Free the ~3 GB source ISO before the upload step needs scratch space.
rm -rf iso/build/cache rm -rf iso/build/cache iso/build/offline-cache
(cd "$RUNNER_TEMP/iso" && sha256sum "$name" > "$name.sha256") (cd "$RUNNER_TEMP/iso" && sha256sum "$name" > "$name.sha256")
echo "ISO_NAME=$name" >> "$GITHUB_ENV" echo "ISO_NAME=$name" >> "$GITHUB_ENV"
{ {
echo "### $name" echo "### $name"
echo echo
echo "- Mode: \`$MODE\`" echo "- Mode: \`$MODE\`"
echo "- Offline bundle: $([[ "$OFFLINE" == "true" ]] && echo "yes - installs with no internet" || echo "no - kiosk needs internet on first boot")"
echo "- Admin user: ${ADMIN_USERNAME:-chosen during install}" echo "- Admin user: ${ADMIN_USERNAME:-chosen during install}"
echo "- Built from: \`${GITHUB_REF_NAME}\` @ \`${GITHUB_SHA::7}\`" echo "- Built from: \`${GITHUB_REF_NAME}\` @ \`${GITHUB_SHA::7}\`"
echo "- SHA-256: \`$(cut -d' ' -f1 "$RUNNER_TEMP/iso/$name.sha256")\`" echo "- SHA-256: \`$(cut -d' ' -f1 "$RUNNER_TEMP/iso/$name.sha256")\`"
+43 -4
View File
@@ -94,8 +94,46 @@ Useful options (`iso/build-iso.sh --help` for all):
| `--auto-network` / `--auto-storage` | Skip the network screen (DHCP on ethernet) / wipe the largest disk without asking. | | `--auto-network` / `--auto-storage` | Skip the network screen (DHCP on ethernet) / wipe the largest disk without asking. |
| `--no-confirm` | Skip the "Continue with autoinstall?" prompt at boot. | | `--no-confirm` | Skip the "Continue with autoinstall?" prompt at boot. |
| `--fully-automatic` | All of the above (needs `--username`): boot it and walk away. **Wipes a disk with no questions — label the USB stick.** | | `--fully-automatic` | All of the above (needs `--username`): boot it and walk away. **Wipes a disk with no questions — label the USB stick.** |
| `--offline` | Bundle everything the kiosk install downloads, so it works with **no internet** (see below). |
| `--iso PATH` | Use an ISO you already have instead of downloading. | | `--iso PATH` | Use an ISO you already have instead of downloading. |
### Offline installs (`--offline`)
```bash
sudo apt install apt-utils python3 gnupg ubuntu-keyring # once, on top of the tools above
iso/build-iso.sh --offline
# -> iso/build/ubuntu-26.04.x-kiosk-offline-amd64.iso (~3.2 GB)
```
This adds a ~420 MB bundle to the ISO:
- Every Ubuntu package the kiosk install and the **CUPS printing addon**
need (all printer drivers included), with their dependencies, as a local
package repository.
- Node.js 22 (NodeSource).
- Prebuilt Electron and Web UI modules.
On first boot it still installs online when there's internet. With none,
it waits 90 seconds and then installs entirely from the bundle. The bundle
stays on the kiosk at `/opt/kiosk-offline`, so CUPS can be added later
with no internet too:
```bash
sudo kiosk-offline-apt on # apt uses the bundle only
./install.sh # Addons -> CUPS Printing
sudo kiosk-offline-apt off # back to normal updates
```
Notes:
- Build it on an **amd64** Ubuntu/Debian machine, or let the GitHub
workflow do it (its `offline` box is ticked by default). Rebuilds are
quick because downloads are cached in `iso/build/offline-cache`.
- Bundled versions are whatever was current when the ISO was built.
Once the kiosk is online, normal updates and Advanced → Upgrade bring it
current.
- Addons that download their own software (LMS/Squeezelite, Tailscale,
Netbird, Asterisk Intercom, VNC) still need internet.
- `sudo rm -rf /opt/kiosk-offline` frees the space if you don't need it.
**No Linux machine handy?** Build it on GitHub instead: **Actions → Build **No Linux machine handy?** Build it on GitHub instead: **Actions → Build
kiosk ISO → Run workflow**, pick a mode, and download the ISO from the run's kiosk ISO → Run workflow**, pick a mode, and download the ISO from the run's
**Artifacts** once it finishes (about 10 minutes; kept for 3 days by default). **Artifacts** once it finishes (about 10 minutes; kept for 3 days by default).
@@ -107,9 +145,10 @@ because its inputs show up in the run log.
Notes: Notes:
- The ISO carries a copy of this checkout's current commit (uncommitted - The ISO carries a copy of this checkout's current commit (uncommitted
changes are not included). changes are not included).
- The kiosk machine still needs internet on first boot (packages, Node.js, - Without `--offline`, the kiosk machine needs internet on first boot
Electron). If there's none, the console waits for it and offers a shell to (packages, Node.js, Electron). If there's none, the console waits for it
fix networking (`/etc/netplan/*.yaml`, then `sudo netplan apply`). and offers a shell to fix networking (`/etc/netplan/*.yaml`, then
`sudo netplan apply`).
- A failed or interrupted first-boot install runs again on the next boot, or - A failed or interrupted first-boot install runs again on the next boot, or
now with `sudo systemctl start kiosk-firstboot`. Its log is now with `sudo systemctl start kiosk-firstboot`. Its log is
`/var/lib/kiosk-firstboot/install.log`. `/var/lib/kiosk-firstboot/install.log`.
@@ -120,7 +159,7 @@ Notes:
If the kiosk machine can't reach GitHub directly (no browser, restrictive proxy, or you just prefer to grab the script on another computer and carry it over via USB), download it ahead of time instead of using the `curl`/`wget` one-liner above. If the kiosk machine can't reach GitHub directly (no browser, restrictive proxy, or you just prefer to grab the script on another computer and carry it over via USB), download it ahead of time instead of using the `curl`/`wget` one-liner above.
> **Note:** This only avoids needing internet access *to fetch the script*. The installer itself still requires the kiosk machine to have internet access while it runs — it uses `apt` to install packages, pulls Node.js from NodeSource, and runs `npm install` to fetch Electron (~120MB). There is currently no fully air-gapped/offline package bundle. > **Note:** This only avoids needing internet access *to fetch the script*. For a truly offline install, use the [All-in-One Install ISO](#all-in-one-install-iso-ubuntu-server-2604--kiosk) built with `--offline` instead. The installer itself still requires the kiosk machine to have internet access while it runs — it uses `apt` to install packages, pulls Node.js from NodeSource, and runs `npm install` to fetch Electron (~120MB). There is currently no fully air-gapped/offline package bundle.
**On a machine with internet access:** **On a machine with internet access:**
+28 -2
View File
@@ -26,6 +26,8 @@
# #
# Requirements (on the build machine, any recent Ubuntu/Debian): # Requirements (on the build machine, any recent Ubuntu/Debian):
# sudo apt install xorriso curl git openssl # sudo apt install xorriso curl git openssl
# plus, for --offline (amd64 build machine only):
# sudo apt install apt-utils python3 gnupg ubuntu-keyring
# #
# Usage: # Usage:
# iso/build-iso.sh [options] # iso/build-iso.sh [options]
@@ -65,6 +67,15 @@
# --repo-url URL git remote recorded in the copied repo, used by # --repo-url URL git remote recorded in the copied repo, used by
# Advanced -> Upgrade (default: this checkout's # Advanced -> Upgrade (default: this checkout's
# origin, else the upstream GitHub repo). # 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 # The repo copied onto the ISO is a clone of this checkout's current HEAD
# (committed changes only - uncommitted edits are NOT included). # (committed changes only - uncommitted edits are NOT included).
@@ -91,6 +102,7 @@ AUTO_STORAGE=0
AUTO_NETWORK=0 AUTO_NETWORK=0
NO_CONFIRM=0 NO_CONFIRM=0
UNATTENDED=0 UNATTENDED=0
OFFLINE=0
REPO_URL="" REPO_URL=""
# Usernames the Ubuntu installer rejects (system users/groups) - copied # Usernames the Ubuntu installer rejects (system users/groups) - copied
@@ -136,6 +148,7 @@ while [[ $# -gt 0 ]]; do
--unattended) UNATTENDED=1; shift ;; --unattended) UNATTENDED=1; shift ;;
--fully-automatic) AUTO_STORAGE=1; AUTO_NETWORK=1; NO_CONFIRM=1; UNATTENDED=1; shift ;; --fully-automatic) AUTO_STORAGE=1; AUTO_NETWORK=1; NO_CONFIRM=1; UNATTENDED=1; shift ;;
--repo-url) REPO_URL="$2"; shift 2 ;; --repo-url) REPO_URL="$2"; shift 2 ;;
--offline) OFFLINE=1; shift ;;
-h|--help) usage 0 ;; -h|--help) usage 0 ;;
*) echo "Unknown option: $1" >&2; usage 1 ;; *) echo "Unknown option: $1" >&2; usage 1 ;;
esac esac
@@ -212,8 +225,9 @@ fi
if [[ -z "$OUTPUT" ]]; then if [[ -z "$OUTPUT" ]]; then
src_base="$(basename "$SRC_ISO" .iso)" src_base="$(basename "$SRC_ISO" .iso)"
OUTPUT="$BUILD_DIR/${src_base/-live-server/-kiosk}.iso" kind="kiosk"; (( OFFLINE )) && kind="kiosk-offline"
[[ "$OUTPUT" != "$BUILD_DIR/$src_base.iso" ]] || OUTPUT="$BUILD_DIR/${src_base}-kiosk.iso" OUTPUT="$BUILD_DIR/${src_base/-live-server/-$kind}.iso"
[[ "$OUTPUT" != "$BUILD_DIR/$src_base.iso" ]] || OUTPUT="$BUILD_DIR/${src_base}-$kind.iso"
fi fi
################################################################################ ################################################################################
@@ -244,12 +258,18 @@ git -C "$stage_repo" config "branch.$BRANCH.merge" "refs/heads/$BRANCH"
install -m 755 "$SCRIPT_DIR/firstboot/kiosk-firstboot" "$WORK/kiosk/firstboot/" 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 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 cat > "$WORK/kiosk/firstboot/kiosk-firstboot.conf" <<EOF
# Read by /usr/local/sbin/kiosk-firstboot. Written by iso/build-iso.sh. # 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. # 1 = take every install.sh default and reboot into the kiosk; 0 = interactive.
KIOSK_UNATTENDED=$UNATTENDED KIOSK_UNATTENDED=$UNATTENDED
EOF 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 # autoinstall.yaml
################################################################################ ################################################################################
@@ -305,9 +325,14 @@ interactive=()
- install -m 755 /cdrom/kiosk/firstboot/kiosk-firstboot /target/usr/local/sbin/kiosk-firstboot - 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.service /target/etc/systemd/system/kiosk-firstboot.service
- install -m 644 /cdrom/kiosk/firstboot/kiosk-firstboot.conf /target/etc/kiosk-firstboot.conf - 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 - touch /target/var/lib/kiosk-firstboot/pending
- curtin in-target --target=/target -- systemctl enable kiosk-firstboot.service - curtin in-target --target=/target -- systemctl enable kiosk-firstboot.service
EOF 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" } > "$WORK/autoinstall.yaml"
################################################################################ ################################################################################
@@ -345,6 +370,7 @@ echo
echo "Installer asks for : ${interactive[*]:-nothing}$( (( NO_CONFIRM )) || echo " (plus a 'Continue with autoinstall?' confirmation)")" echo "Installer asks for : ${interactive[*]:-nothing}$( (( NO_CONFIRM )) || echo " (plus a 'Continue with autoinstall?' confirmation)")"
echo "Admin user : ${ADMIN_USER:-chosen during install}" 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 "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
echo "Write it to a USB stick (replace /dev/sdX - this erases it):" 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" echo " sudo dd if='$OUTPUT' of=/dev/sdX bs=4M status=progress oflag=sync"
+39 -1
View File
@@ -19,6 +19,9 @@
# KIOSK_UNATTENDED=1 take every prompt's default, skip the Core # KIOSK_UNATTENDED=1 take every prompt's default, skip the Core
# Settings menu, reboot into the kiosk when done. # Settings menu, reboot into the kiosk when done.
# KIOSK_UNATTENDED=0 (default) normal interactive install.sh on tty1. # KIOSK_UNATTENDED=0 (default) normal interactive install.sh on tty1.
# KIOSK_OFFLINE_WAIT=N on an ISO built with --offline: seconds to wait
# for internet before installing from the bundled
# packages instead (default 90).
################################################################################ ################################################################################
set -uo pipefail set -uo pipefail
@@ -31,13 +34,23 @@ SUDOERS_DROPIN=/etc/sudoers.d/90-kiosk-firstboot
APT_DROPIN=/etc/apt/apt.conf.d/90-kiosk-firstboot APT_DROPIN=/etc/apt/apt.conf.d/90-kiosk-firstboot
REPO_SRC=/opt/ubuntu-based-kiosk REPO_SRC=/opt/ubuntu-based-kiosk
LOG="$STATE_DIR/install.log" LOG="$STATE_DIR/install.log"
OFFLINE_BUNDLE=/opt/kiosk-offline
OFFLINE_APT=/usr/local/sbin/kiosk-offline-apt
OFFLINE_FLAG="$STATE_DIR/offline-mode"
KIOSK_UNATTENDED=0 KIOSK_UNATTENDED=0
KIOSK_OFFLINE_WAIT=90
# shellcheck source=/dev/null # shellcheck source=/dev/null
[[ -f "$CONF" ]] && source "$CONF" [[ -f "$CONF" ]] && source "$CONF"
cleanup() { cleanup() {
rm -f "$SUDOERS_DROPIN" "$APT_DROPIN" rm -f "$SUDOERS_DROPIN" "$APT_DROPIN"
# Offline mode was only for this install - hand apt back to the real
# archive so normal updates work once the kiosk is online.
if [[ -f "$OFFLINE_FLAG" ]]; then
"$OFFLINE_APT" off >/dev/null 2>&1 || true
rm -f "$OFFLINE_FLAG"
fi
if [[ -f "$DONE_MARKER" ]]; then if [[ -f "$DONE_MARKER" ]]; then
rm -f "$PENDING" rm -f "$PENDING"
fi fi
@@ -86,6 +99,8 @@ offer_retry() {
done done
} }
# Returns 0 once online, 2 to install from the offline bundle instead
# (only when there is one), 1 to give up.
wait_for_internet() { wait_for_internet() {
local waited=0 local waited=0
until curl -fsS --max-time 10 -o /dev/null https://deb.nodesource.com/ 2>/dev/null \ until curl -fsS --max-time 10 -o /dev/null https://deb.nodesource.com/ 2>/dev/null \
@@ -93,9 +108,17 @@ wait_for_internet() {
if (( waited == 0 )); then if (( waited == 0 )); then
echo "Waiting for an internet connection (needed for packages," echo "Waiting for an internet connection (needed for packages,"
echo "Node.js and Electron)..." echo "Node.js and Electron)..."
if [[ -f "$OFFLINE_BUNDLE/apt/Packages" ]]; then
echo "This disk has the offline bundle: installing from it instead"
echo "if there's still no internet after ${KIOSK_OFFLINE_WAIT}s."
fi
fi fi
sleep 10 sleep 10
waited=$((waited + 10)) waited=$((waited + 10))
if [[ -f "$OFFLINE_BUNDLE/apt/Packages" ]] && (( waited >= KIOSK_OFFLINE_WAIT )); then
echo "No internet after ${waited}s - installing from the offline bundle on this disk."
return 2
fi
if (( waited % 120 == 0 )); then if (( waited % 120 == 0 )); then
echo "Still no internet after ${waited}s. Network status:" echo "Still no internet after ${waited}s. Network status:"
networkctl list --no-pager 2>/dev/null || ip -brief addr networkctl list --no-pager 2>/dev/null || ip -brief addr
@@ -153,7 +176,22 @@ fi
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
printf 'DPkg::Lock::Timeout "900";\n' > "$APT_DROPIN" printf 'DPkg::Lock::Timeout "900";\n' > "$APT_DROPIN"
wait_for_internet || { release_tty; exit 1; } wait_for_internet
case $? in
0) ;;
2)
# install.sh notices this by itself (lib/config.sh's
# offline_mode_active) and unpacks prebuilt node_modules instead
# of running npm; cleanup() switches it back off.
touch "$OFFLINE_FLAG"
if ! "$OFFLINE_APT" on; then
echo "ERROR: couldn't switch apt to the offline bundle."
release_tty
exit 1
fi
;;
*) release_tty; exit 1 ;;
esac
# install.sh's own preflight needs jq; Advanced -> Upgrade needs git. # install.sh's own preflight needs jq; Advanced -> Upgrade needs git.
echo "Installing installer prerequisites (jq, git)..." echo "Installing installer prerequisites (jq, git)..."
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
################################################################################
# kiosk-offline-apt - point apt at the offline install bundle, or back.
#
# sudo kiosk-offline-apt on apt uses ONLY /opt/kiosk-offline/apt
# sudo kiosk-offline-apt off back to the normal Ubuntu archive
# kiosk-offline-apt status
#
# Installed by the custom ISO (iso/build-iso.sh); the bundle itself is
# only there on ISOs built with --offline. kiosk-firstboot turns this on
# by itself when first boot finds no internet, and back off afterwards.
# By hand it's for installing things later with no internet - e.g. the
# CUPS addon: `sudo kiosk-offline-apt on`, ./install.sh -> Addons -> CUPS,
# then `sudo kiosk-offline-apt off`. While it's on, install.sh also
# unpacks the bundle's prebuilt node_modules instead of running npm
# (lib/config.sh's offline_mode_active checks for the file this writes).
#
# "Only" matters: with the normal archive still listed, apt would pick
# newer versions from it that it then can't download.
################################################################################
set -euo pipefail
BUNDLE_DIR=/opt/kiosk-offline
APT_CONF=/etc/apt/apt.conf.d/90-kiosk-offline
case "${1:-status}" in
on)
[[ $EUID -eq 0 ]] || { echo "Run with sudo" >&2; exit 1; }
if [[ ! -f "$BUNDLE_DIR/apt/Packages" ]]; then
echo "No offline bundle at $BUNDLE_DIR (ISO not built with --offline)" >&2
exit 1
fi
mkdir -p "$BUNDLE_DIR/sources.d"
echo "deb [trusted=yes] file:$BUNDLE_DIR/apt ./" > "$BUNDLE_DIR/offline.list"
cat > "$APT_CONF" <<EOF
// Written by kiosk-offline-apt - remove with: sudo kiosk-offline-apt off
Dir::Etc::SourceList "$BUNDLE_DIR/offline.list";
Dir::Etc::SourceParts "$BUNDLE_DIR/sources.d";
Acquire::Languages "none";
EOF
apt-get update -q >/dev/null
echo "apt now installs from the offline bundle only ($BUNDLE_DIR)."
;;
off)
[[ $EUID -eq 0 ]] || { echo "Run with sudo" >&2; exit 1; }
rm -f "$APT_CONF"
echo "apt is back on the normal Ubuntu sources."
echo "Run 'sudo apt update' once there's internet."
;;
status)
if [[ -f "$APT_CONF" ]]; then
echo "on (apt uses $BUNDLE_DIR/apt only)"
else
echo "off"
fi
[[ -f "$BUNDLE_DIR/bundle-info.txt" ]] && sed -n '1,3p;5,6p' "$BUNDLE_DIR/bundle-info.txt"
;;
*)
echo "Usage: kiosk-offline-apt on|off|status" >&2
exit 2
;;
esac
+213
View File
@@ -0,0 +1,213 @@
#!/bin/bash
################################################################################
# iso/offline/build-bundle.sh - Build the offline install bundle that
# `iso/build-iso.sh --offline` puts on the ISO, so a kiosk can be installed
# with no internet at all. Called by build-iso.sh; not normally run by hand.
#
# Usage: build-bundle.sh OUT_DIR SOURCE_ISO REPO_DIR CACHE_DIR
#
# OUT_DIR ends up as /opt/kiosk-offline on the installed system:
# apt/ Local apt repository (flat, [trusted=yes]) with every
# package install.sh's provisioning and the CUPS addon
# install ask for, NodeSource's nodejs, and all their
# dependencies that a fresh Ubuntu Server install from
# SOURCE_ISO doesn't already have.
# npm/kiosk-app-node_modules.tar.gz Prebuilt node_modules (Electron
# npm/webui-node_modules.tar.gz included) in place of `npm install`.
# bundle-info.txt What was bundled, from where, when.
#
# Package lists are read from REPO_DIR's own lib/provision.sh and
# menus/addon_cups.sh (PROVISION_APT_PACKAGES, PROVISION_INTEL_APT_PACKAGES,
# CUPS_APT_PACKAGES), so the bundle always matches what install.sh asks for.
#
# Dependencies are resolved against SOURCE_ISO's own package manifest - a
# fake dpkg status built from it (fake-dpkg-status.py) - so apt only
# downloads what's actually missing and makes the same choices (virtual
# packages, alternatives) it will make on the real target.
#
# CACHE_DIR keeps apt's downloaded .debs, npm's cache and Electron's
# download between builds, so a rebuild only fetches what changed.
#
# Build host: Ubuntu or Debian, amd64 (Node.js from the bundled .deb is
# run here to build node_modules). Needs: apt-get, apt-ftparchive
# (apt-utils), dpkg-deb, python3, gpg, curl, xorriso, and
# /usr/share/keyrings/ubuntu-archive-keyring.gpg (ubuntu-keyring).
################################################################################
set -euo pipefail
OUT_DIR="$1"
SRC_ISO="$2"
REPO_DIR="$3"
CACHE_DIR="$4"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
UBUNTU_MIRROR="${UBUNTU_MIRROR:-http://archive.ubuntu.com/ubuntu}"
NODESOURCE_REPO="https://deb.nodesource.com/node_22.x"
NODESOURCE_KEY="https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key"
UBUNTU_KEYRING=/usr/share/keyrings/ubuntu-archive-keyring.gpg
# Needed on the target beyond install.sh's own lists: kiosk-firstboot's
# prerequisites, wget (lib/electron.sh's fallback), and openssh-server
# (installed by the Ubuntu installer only when it had a network).
EXTRA_APT_PACKAGES=(nodejs jq git curl wget unzip openssh-server)
die() { echo "ERROR: $*" >&2; exit 1; }
info() { echo " $*"; }
for cmd in apt-get apt-ftparchive dpkg-deb dpkg python3 gpg curl xorriso tar gzip; do
command -v "$cmd" &>/dev/null \
|| die "--offline needs '$cmd' - on Ubuntu/Debian: sudo apt install apt-utils dpkg python3 gnupg curl xorriso"
done
[[ "$(dpkg --print-architecture)" == "amd64" ]] \
|| die "--offline must be built on an amd64 machine (it runs the bundled Node.js to build node_modules)"
[[ -r "$UBUNTU_KEYRING" ]] \
|| die "missing $UBUNTU_KEYRING - install it: sudo apt install ubuntu-keyring"
rm -rf "$OUT_DIR/apt" "$OUT_DIR/npm"
mkdir -p "$OUT_DIR/apt" "$OUT_DIR/npm" "$CACHE_DIR"
# apt's Dir= and Signed-By need absolute paths.
OUT_DIR="$(cd "$OUT_DIR" && pwd)"
CACHE_DIR="$(cd "$CACHE_DIR" && pwd)"
REPO_DIR="$(cd "$REPO_DIR" && pwd)"
SRC_ISO="$(cd "$(dirname "$SRC_ISO")" && pwd)/$(basename "$SRC_ISO")"
################################################################################
# Which Ubuntu release, and what a default server install already has
################################################################################
codename="$(xorriso -indev "$SRC_ISO" -ls /dists 2>/dev/null \
| tr -d "'" | awk '{print $NF}' | grep -E '^[a-z]+$' | head -1)"
[[ -n "$codename" ]] || die "couldn't tell the Ubuntu release of $SRC_ISO (no /dists on it)"
manifest="$CACHE_DIR/server.manifest"
xorriso -osirrox on -indev "$SRC_ISO" \
-extract /casper/ubuntu-server-minimal.ubuntu-server.manifest.full "$manifest" 2>/dev/null \
|| die "no server package manifest on $SRC_ISO (casper/ubuntu-server-minimal.ubuntu-server.manifest.full)"
chmod u+w "$manifest"
################################################################################
# Standalone apt environment for $codename (never touches the host's apt)
################################################################################
R="$CACHE_DIR/aptroot-$codename"
mkdir -p "$R/etc/apt/apt.conf.d" "$R/etc/apt/preferences.d" "$R/etc/apt/sources.list.d" \
"$R/var/lib/apt/lists/partial" "$R/var/cache/apt/archives/partial" "$R/var/lib/dpkg" "$R/keys"
: > "$R/etc/apt/sources.list"
cp "$UBUNTU_KEYRING" "$R/keys/ubuntu.gpg"
curl -fsSL "$NODESOURCE_KEY" | gpg --dearmor --yes -o "$R/keys/nodesource.gpg"
cat > "$R/etc/apt/sources.list.d/bundle.sources" <<EOF
Types: deb
URIs: $UBUNTU_MIRROR
Suites: $codename $codename-updates $codename-security
Components: main restricted universe multiverse
Signed-By: $R/keys/ubuntu.gpg
Types: deb
URIs: $NODESOURCE_REPO
Suites: nodistro
Components: main
Signed-By: $R/keys/nodesource.gpg
EOF
APT=(apt-get
-o "Dir=$R" -o "Dir::State::status=$R/var/lib/dpkg/status"
-o APT::Architecture=amd64 -o APT::Architectures::=amd64
-o Acquire::Languages=none -o Acquire::GzipIndexes=false
-o Debug::NoLocking=1 -q)
info "Fetching $codename package indexes..."
: > "$R/var/lib/dpkg/status"
"${APT[@]}" update >/dev/null
python3 "$HERE/fake-dpkg-status.py" "$manifest" "$R/var/lib/apt/lists" > "$R/var/lib/dpkg/status"
"${APT[@]}" check >/dev/null || die "baseline from the ISO manifest is inconsistent"
################################################################################
# Packages
################################################################################
# shellcheck disable=SC1091,SC2034
mapfile -t packages < <(
SCRIPT_DIR="$REPO_DIR"
source "$REPO_DIR/lib/provision.sh"
source "$REPO_DIR/menus/addon_cups.sh"
printf '%s\n' "${PROVISION_APT_PACKAGES[@]}" "${PROVISION_INTEL_APT_PACKAGES[@]}" \
"${CUPS_APT_PACKAGES[@]}" "${EXTRA_APT_PACKAGES[@]}"
)
(( ${#packages[@]} > 20 )) || die "couldn't read the package lists from $REPO_DIR"
info "Resolving ${#packages[@]} packages (with recommends, as install.sh installs them)..."
# --print-uris against an empty archive dir: it leaves out anything already
# in the cache, which on a rebuild would be everything.
rm -rf "$R/empty-archives"; mkdir -p "$R/empty-archives/partial"
mapfile -t debs < <("${APT[@]}" -qq -o "Dir::Cache::archives=$R/empty-archives" \
install --print-uris -y "${packages[@]}" | awk '{print $2}')
(( ${#debs[@]} )) || die "apt resolved nothing to download - check the output above"
info "Downloading ${#debs[@]} .debs (cached between builds)..."
"${APT[@]}" install --download-only -y "${packages[@]}" >/dev/null
for f in "${debs[@]}"; do
cp "$R/var/cache/apt/archives/$f" "$OUT_DIR/apt/"
done
(
cd "$OUT_DIR/apt"
apt-ftparchive packages . > Packages 2>/dev/null
gzip -9 -k Packages
apt-ftparchive release . > Release
)
################################################################################
# node_modules, built with the exact Node.js being bundled
################################################################################
node_deb="$(printf '%s\n' "${debs[@]}" | grep -E '^nodejs_' | head -1 || true)"
[[ -n "$node_deb" ]] || node_deb="$(cd "$R/var/cache/apt/archives" && ls nodejs_*_amd64.deb 2>/dev/null | sort -V | tail -1)"
[[ -n "$node_deb" ]] || die "no nodejs .deb downloaded"
node_root="$CACHE_DIR/node-root"
rm -rf "$node_root"
dpkg-deb -x "$R/var/cache/apt/archives/$node_deb" "$node_root"
export PATH="$node_root/usr/bin:$PATH"
export npm_config_cache="$CACHE_DIR/npm-cache"
export electron_config_cache="$CACHE_DIR/electron-cache"
export npm_config_update_notifier=false npm_config_fund=false npm_config_audit=false
info "Building node_modules with Node.js $(node -v)..."
# build_modules NAME SRC_DIR NPM_ARGS...
build_modules() {
local name="$1" src="$2"; shift 2
local tmp="$CACHE_DIR/build-$name"
rm -rf "$tmp"; mkdir -p "$tmp"
cp "$src/package.json" "$tmp/"
[[ -f "$src/package-lock.json" ]] && cp "$src/package-lock.json" "$tmp/"
(cd "$tmp" && npm install --no-progress "$@" >/dev/null) || die "npm install failed for $name"
# Electron's package no longer fetches its binary at install time (no
# postinstall since ~v42) - lib/electron.sh runs this same install.js
# on the target when online; offline it has to already be in dist/.
if [[ -f "$tmp/node_modules/electron/install.js" ]]; then
(cd "$tmp" && node node_modules/electron/install.js) || die "Electron binary download failed"
fi
tar -czf "$OUT_DIR/npm/${name}-node_modules.tar.gz" --owner=0 --group=0 -C "$tmp" node_modules
info "$name: $(du -sh "$tmp/node_modules" | cut -f1) -> $(du -h "$OUT_DIR/npm/${name}-node_modules.tar.gz" | cut -f1) compressed"
rm -rf "$tmp"
}
build_modules kiosk-app "$REPO_DIR/kiosk-app"
tar -tzf "$OUT_DIR/npm/kiosk-app-node_modules.tar.gz" node_modules/electron/dist/electron >/dev/null 2>&1 \
|| die "Electron binary missing from the built kiosk-app modules (download blocked?)"
build_modules webui "$REPO_DIR/webui" --omit=dev
################################################################################
electron_ver="$(tar -xzOf "$OUT_DIR/npm/kiosk-app-node_modules.tar.gz" node_modules/electron/package.json \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["version"])')"
{
echo "Ubuntu Based Kiosk offline bundle"
echo "Built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Ubuntu: $codename (resolved against $(basename "$SRC_ISO"))"
echo "Packages: ${#debs[@]} .debs for: ${packages[*]}"
echo "Node.js: ${node_deb}"
echo "Electron: $electron_ver"
} > "$OUT_DIR/bundle-info.txt"
info "Bundle: $(du -sh "$OUT_DIR" | cut -f1) (${#debs[@]} packages, Node.js $(node -v), Electron $electron_ver)"
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Write a dpkg status file claiming a set of packages is installed.
Usage: fake-dpkg-status.py MANIFEST LISTS_DIR > status
MANIFEST is an Ubuntu ISO's casper/*.manifest.full ("name[:arch]<TAB>version"
per line) - the packages a default Ubuntu Server install ends up with.
LISTS_DIR is an apt lists directory holding uncompressed *_Packages indexes.
iso/offline/build-bundle.sh resolves the kiosk's packages against this, so
apt only downloads what a freshly installed server is actually missing
(and makes the same alternative/virtual-package choices the real target
will), instead of the whole dependency closure down to libc6.
"""
import glob
import os
import sys
KEEP = ("Package", "Essential", "Priority", "Section", "Architecture",
"Multi-Arch", "Source", "Version", "Replaces", "Provides", "Depends",
"Pre-Depends", "Recommends", "Suggests", "Breaks", "Conflicts")
def stanzas(path):
with open(path, encoding="utf-8", errors="replace") as f:
cur = []
for line in f:
if line.strip() == "":
if cur:
yield cur
cur = []
else:
cur.append(line.rstrip("\n"))
if cur:
yield cur
def parse(lines):
fields, order, key = {}, [], None
for line in lines:
if line[:1] in (" ", "\t") and key:
fields[key] += "\n" + line
else:
key, _, val = line.partition(":")
fields[key] = val.strip()
order.append(key)
return fields
def main():
manifest, lists_dir = sys.argv[1], sys.argv[2]
wanted = {}
with open(manifest) as f:
for line in f:
parts = line.rstrip("\n").split("\t")
if len(parts) != 2 or parts[0].startswith("snap:"):
continue
wanted[parts[0].split(":")[0]] = parts[1]
exact, any_version = {}, {}
for path in glob.glob(os.path.join(lists_dir, "*_Packages")):
for st in stanzas(path):
p = parse(st)
name = p.get("Package")
if name not in wanted:
continue
any_version.setdefault(name, p)
if p.get("Version") == wanted[name]:
exact[name] = p
missing = 0
for name, version in sorted(wanted.items()):
p = exact.get(name) or any_version.get(name)
if p is None:
missing += 1
continue
out = [f"Package: {name}", "Status: install ok installed"]
for k in KEEP[1:]:
if k == "Version":
out.append(f"Version: {version}")
elif k in p:
val = p[k]
# The archive no longer carries this exact version (an
# update superseded it), so these fields came from a
# different one and its dependencies (version pins on
# siblings, a newer kernel ABI...) don't describe what's
# installed. Already-installed packages' own dependencies
# are assumed satisfied anyway, so just leave them out.
if name not in exact and k in ("Depends", "Pre-Depends", "Recommends"):
continue
# Same for versioned Breaks/Conflicts - drop them outright
# (unversioned ones still hold for any version).
if name not in exact and k in ("Breaks", "Conflicts"):
val = ", ".join(c.strip() for c in val.split(",") if "(" not in c)
if not val:
continue
out.append(f"{k}: {val}")
print("\n".join(out) + "\n")
print(f"fake-dpkg-status: {len(wanted) - missing} packages marked installed,"
f" {missing} not in the archive indexes (skipped)", file=sys.stderr)
if __name__ == "__main__":
main()
+30
View File
@@ -41,6 +41,16 @@
# other addon's own scripts. # other addon's own scripts.
: "${WEBUI_HELPER_PATH:=$BIN_DIR/kiosk-webui-helper}" : "${WEBUI_HELPER_PATH:=$BIN_DIR/kiosk-webui-helper}"
# Offline install bundle - only present on systems installed from an ISO
# built with `iso/build-iso.sh --offline`: a local apt repository (every
# package provisioning and the CUPS addon install) plus prebuilt
# node_modules for the kiosk app and Web UI. Offline mode is switched on
# by /usr/local/sbin/kiosk-offline-apt (which writes $KIOSK_OFFLINE_APT_CONF
# pointing apt at that repository only) - the first-boot service does that
# automatically when there's no internet; by hand: `sudo kiosk-offline-apt on`.
: "${KIOSK_OFFLINE_DIR:=/opt/kiosk-offline}"
: "${KIOSK_OFFLINE_APT_CONF:=/etc/apt/apt.conf.d/90-kiosk-offline}"
# The admin account actually running this tool (as opposed to $KIOSK_USER, # The admin account actually running this tool (as opposed to $KIOSK_USER,
# the kiosk's own restricted account) - used where an addon needs to grant # the kiosk's own restricted account) - used where an addon needs to grant
# *this* user a group membership (e.g. lpadmin for CUPS). # *this* user a group membership (e.g. lpadmin for CUPS).
@@ -74,6 +84,26 @@ AUTHELIA_URL=""
AUTHELIA_USERNAME="" AUTHELIA_USERNAME=""
AUTHELIA_ENCRYPTED_PASSWORD="" AUTHELIA_ENCRYPTED_PASSWORD=""
offline_mode_active() {
[[ -f "$KIOSK_OFFLINE_APT_CONF" && -d "$KIOSK_OFFLINE_DIR/apt" ]]
}
# offline_install_node_modules NAME DEST_DIR
# Unpacks the bundle's prebuilt node_modules for NAME (kiosk-app, webui)
# into DEST_DIR as $KIOSK_USER, in place of `npm install` - which can't
# reach the npm registry (or Electron's GitHub download) offline.
offline_install_node_modules() {
local name="$1" dest="$2"
local tarball="$KIOSK_OFFLINE_DIR/npm/${name}-node_modules.tar.gz"
if [[ ! -f "$tarball" ]]; then
log_error "Offline bundle has no prebuilt modules for $name ($tarball)"
return 1
fi
echo "Offline mode: unpacking prebuilt $name modules from $KIOSK_OFFLINE_DIR..."
sudo -u "$KIOSK_USER" rm -rf "$dest/node_modules"
sudo -u "$KIOSK_USER" tar -xzf "$tarball" -C "$dest"
}
kiosk_user_exists() { kiosk_user_exists() {
id "$KIOSK_USER" &>/dev/null id "$KIOSK_USER" &>/dev/null
} }
+29 -12
View File
@@ -54,23 +54,31 @@ provision_install_file() {
sudo install -D -m "$mode" "$PROVISION_FILES/$src_rel" "$dest" sudo install -D -m "$mode" "$PROVISION_FILES/$src_rel" "$dest"
} }
# Package lists live in variables, not inline, so iso/offline/build-bundle.sh
# (the offline install ISO) can read the exact same lists to bundle - add a
# package here and the offline bundle picks it up on its next build.
PROVISION_APT_PACKAGES=(
xorg openbox lightdm unclutter screen curl git build-essential
ca-certificates gnupg lsb-release jq ufw x11-xserver-utils xinput
vainfo mesa-utils libgl1-mesa-dri libglx-mesa0 mesa-vulkan-drivers
libva2 libva-drm2 libva-x11-2 mesa-va-drivers
libegl-mesa0 libegl1-mesa-dev libgles2-mesa-dev
pipewire pipewire-pulse pipewire-alsa wireplumber pipewire-audio-client-libraries alsa-utils libnotify-bin
gstreamer1.0-pipewire libspa-0.2-bluetooth
systemd-timesyncd acpid xbindkeys xdotool python3-evdev unzip
net-tools ncdu evtest
)
PROVISION_INTEL_APT_PACKAGES=(
intel-gpu-tools xserver-xorg-video-intel i965-va-driver intel-media-va-driver
)
provision_install_packages() { provision_install_packages() {
echo "[1/10] Installing packages..." echo "[1/10] Installing packages..."
sudo apt update sudo apt update
sudo apt install -y \ sudo apt install -y "${PROVISION_APT_PACKAGES[@]}"
xorg openbox lightdm unclutter screen curl git build-essential \
ca-certificates gnupg lsb-release jq ufw x11-xserver-utils xinput \
vainfo mesa-utils libgl1-mesa-dri libglx-mesa0 mesa-vulkan-drivers \
libva2 libva-drm2 libva-x11-2 mesa-va-drivers \
libegl-mesa0 libegl1-mesa-dev libgles2-mesa-dev \
pipewire pipewire-pulse pipewire-alsa wireplumber pipewire-audio-client-libraries alsa-utils libnotify-bin \
gstreamer1.0-pipewire libspa-0.2-bluetooth \
systemd-timesyncd acpid xbindkeys xdotool python3-evdev unzip \
net-tools ncdu evtest
if lspci | grep -i "VGA.*Intel" >/dev/null 2>&1; then if lspci | grep -i "VGA.*Intel" >/dev/null 2>&1; then
sudo apt install -y intel-gpu-tools xserver-xorg-video-intel \ sudo apt install -y "${PROVISION_INTEL_APT_PACKAGES[@]}"
i965-va-driver intel-media-va-driver
provision_install_file "etc/X11/xorg.conf.d/20-intel.conf" /etc/X11/xorg.conf.d/20-intel.conf provision_install_file "etc/X11/xorg.conf.d/20-intel.conf" /etc/X11/xorg.conf.d/20-intel.conf
fi fi
@@ -101,9 +109,14 @@ provision_create_kiosk_user() {
provision_install_nodejs() { provision_install_nodejs() {
echo "[3/10] Installing Node.js..." echo "[3/10] Installing Node.js..."
if ! command -v node &>/dev/null; then if ! command -v node &>/dev/null; then
if offline_mode_active; then
# The bundle carries NodeSource's own nodejs package.
sudo apt install -y nodejs
else
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs sudo apt install -y nodejs
fi fi
fi
echo "Node.js: $(node -v)" echo "Node.js: $(node -v)"
} }
@@ -113,6 +126,9 @@ provision_install_app() {
sudo chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_DIR"/*.js "$KIOSK_DIR"/*.html "$KIOSK_DIR/package.json" "$KIOSK_DIR/start.sh" sudo chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_DIR"/*.js "$KIOSK_DIR"/*.html "$KIOSK_DIR/package.json" "$KIOSK_DIR/start.sh"
sudo chmod +x "$KIOSK_DIR/start.sh" sudo chmod +x "$KIOSK_DIR/start.sh"
if offline_mode_active; then
offline_install_node_modules kiosk-app "$KIOSK_DIR"
else
echo "Installing npm dependencies (Electron ~120MB - may take several minutes)..." echo "Installing npm dependencies (Electron ~120MB - may take several minutes)..."
sudo -u "$KIOSK_USER" bash -lc " sudo -u "$KIOSK_USER" bash -lc "
npm config set fetch-timeout 600000 npm config set fetch-timeout 600000
@@ -124,6 +140,7 @@ provision_install_app() {
log_error "npm install failed" log_error "npm install failed"
return 1 return 1
fi fi
fi
electron_install_binary electron_install_binary
} }
+8 -3
View File
@@ -14,6 +14,13 @@
# Depends on: lib/menu.sh, lib/config.sh being sourced first. # Depends on: lib/menu.sh, lib/config.sh being sourced first.
################################################################################ ################################################################################
# Also read by iso/offline/build-bundle.sh to bundle these for offline installs.
CUPS_APT_PACKAGES=(
cups cups-client cups-filters printer-driver-all
printer-driver-cups-pdf hplip printer-driver-gutenprint
foomatic-db-compressed-ppds openprinting-ppds
)
cups_is_installed() { cups_is_installed() {
dpkg -l 2>/dev/null | grep -q "^ii\s\+cups\s" dpkg -l 2>/dev/null | grep -q "^ii\s\+cups\s"
} }
@@ -62,9 +69,7 @@ action_install_cups() {
log_error "apt update failed - check network/package sources and try again" log_error "apt update failed - check network/package sources and try again"
return 1 return 1
fi fi
if ! sudo apt install -y cups cups-client cups-filters printer-driver-all \ if ! sudo apt install -y "${CUPS_APT_PACKAGES[@]}"; then
printer-driver-cups-pdf hplip printer-driver-gutenprint \
foomatic-db-compressed-ppds openprinting-ppds; then
log_error "CUPS package installation failed" log_error "CUPS package installation failed"
return 1 return 1
fi fi
+3 -1
View File
@@ -113,7 +113,9 @@ webui_install_app_files() {
return 1 return 1
fi fi
sudo chown -R "$KIOSK_USER:$KIOSK_USER" "$WEBUI_DIR" sudo chown -R "$KIOSK_USER:$KIOSK_USER" "$WEBUI_DIR"
if ! sudo -u "$KIOSK_USER" bash -lc "cd '$WEBUI_DIR' && npm install --omit=dev --unsafe-perm"; then if offline_mode_active; then
offline_install_node_modules webui "$WEBUI_DIR" || return 1
elif ! sudo -u "$KIOSK_USER" bash -lc "cd '$WEBUI_DIR' && npm install --omit=dev --unsafe-perm"; then
log_error "npm install failed" log_error "npm install failed"
return 1 return 1
fi fi