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
This commit is contained in:
@@ -39,6 +39,13 @@ on:
|
||||
description: Optional SSH public key for the admin user (ssh-ed25519 AAAA...)
|
||||
type: string
|
||||
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:
|
||||
description: Days to keep the ISO artifact
|
||||
type: number
|
||||
@@ -57,11 +64,12 @@ jobs:
|
||||
- name: Install build tools
|
||||
run: |
|
||||
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
|
||||
env:
|
||||
MODE: ${{ inputs.mode }}
|
||||
OFFLINE: ${{ inputs.offline }}
|
||||
ADMIN_USERNAME: ${{ inputs.username }}
|
||||
ADMIN_HOSTNAME: ${{ inputs.hostname }}
|
||||
SSH_PUBLIC_KEY: ${{ inputs.ssh_public_key }}
|
||||
@@ -84,19 +92,22 @@ jobs:
|
||||
printf '%s\n' "$SSH_PUBLIC_KEY" > "$RUNNER_TEMP/ssh_key.pub"
|
||||
args+=(--ssh-key "$RUNNER_TEMP/ssh_key.pub")
|
||||
fi
|
||||
[[ "$OFFLINE" == "true" ]] && args+=(--offline)
|
||||
iso/build-iso.sh "${args[@]}"
|
||||
# Name the file after the Ubuntu point release the script picked.
|
||||
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"
|
||||
# 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")
|
||||
echo "ISO_NAME=$name" >> "$GITHUB_ENV"
|
||||
{
|
||||
echo "### $name"
|
||||
echo
|
||||
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 "- Built from: \`${GITHUB_REF_NAME}\` @ \`${GITHUB_SHA::7}\`"
|
||||
echo "- SHA-256: \`$(cut -d' ' -f1 "$RUNNER_TEMP/iso/$name.sha256")\`"
|
||||
|
||||
@@ -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. |
|
||||
| `--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.** |
|
||||
| `--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. |
|
||||
|
||||
### 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
|
||||
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).
|
||||
@@ -107,9 +145,10 @@ because its inputs show up in the run log.
|
||||
Notes:
|
||||
- The ISO carries a copy of this checkout's current commit (uncommitted
|
||||
changes are not included).
|
||||
- The kiosk machine still needs internet on first boot (packages, Node.js,
|
||||
Electron). If there's none, the console waits for it and offers a shell to
|
||||
fix networking (`/etc/netplan/*.yaml`, then `sudo netplan apply`).
|
||||
- Without `--offline`, the kiosk machine needs internet on first boot
|
||||
(packages, Node.js, Electron). If there's none, the console waits for it
|
||||
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
|
||||
now with `sudo systemctl start kiosk-firstboot`. Its log is
|
||||
`/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.
|
||||
|
||||
> **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:**
|
||||
|
||||
|
||||
+28
-2
@@ -26,6 +26,8 @@
|
||||
#
|
||||
# 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]
|
||||
@@ -65,6 +67,15 @@
|
||||
# --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).
|
||||
@@ -91,6 +102,7 @@ AUTO_STORAGE=0
|
||||
AUTO_NETWORK=0
|
||||
NO_CONFIRM=0
|
||||
UNATTENDED=0
|
||||
OFFLINE=0
|
||||
REPO_URL=""
|
||||
|
||||
# Usernames the Ubuntu installer rejects (system users/groups) - copied
|
||||
@@ -136,6 +148,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--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
|
||||
@@ -212,8 +225,9 @@ fi
|
||||
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
src_base="$(basename "$SRC_ISO" .iso)"
|
||||
OUTPUT="$BUILD_DIR/${src_base/-live-server/-kiosk}.iso"
|
||||
[[ "$OUTPUT" != "$BUILD_DIR/$src_base.iso" ]] || OUTPUT="$BUILD_DIR/${src_base}-kiosk.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
|
||||
|
||||
################################################################################
|
||||
@@ -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 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
|
||||
################################################################################
|
||||
@@ -305,9 +325,14 @@ interactive=()
|
||||
- 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"
|
||||
|
||||
################################################################################
|
||||
@@ -345,6 +370,7 @@ 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"
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
# KIOSK_UNATTENDED=1 take every prompt's default, skip the Core
|
||||
# Settings menu, reboot into the kiosk when done.
|
||||
# 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
|
||||
@@ -31,13 +34,23 @@ SUDOERS_DROPIN=/etc/sudoers.d/90-kiosk-firstboot
|
||||
APT_DROPIN=/etc/apt/apt.conf.d/90-kiosk-firstboot
|
||||
REPO_SRC=/opt/ubuntu-based-kiosk
|
||||
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_OFFLINE_WAIT=90
|
||||
# shellcheck source=/dev/null
|
||||
[[ -f "$CONF" ]] && source "$CONF"
|
||||
|
||||
cleanup() {
|
||||
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
|
||||
rm -f "$PENDING"
|
||||
fi
|
||||
@@ -86,6 +99,8 @@ offer_retry() {
|
||||
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() {
|
||||
local waited=0
|
||||
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
|
||||
echo "Waiting for an internet connection (needed for packages,"
|
||||
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
|
||||
sleep 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
|
||||
echo "Still no internet after ${waited}s. Network status:"
|
||||
networkctl list --no-pager 2>/dev/null || ip -brief addr
|
||||
@@ -153,7 +176,22 @@ fi
|
||||
mkdir -p "$STATE_DIR"
|
||||
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.
|
||||
echo "Installing installer prerequisites (jq, git)..."
|
||||
|
||||
Executable
+63
@@ -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
|
||||
Executable
+213
@@ -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)"
|
||||
Executable
+104
@@ -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()
|
||||
@@ -41,6 +41,16 @@
|
||||
# other addon's own scripts.
|
||||
: "${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 kiosk's own restricted account) - used where an addon needs to grant
|
||||
# *this* user a group membership (e.g. lpadmin for CUPS).
|
||||
@@ -74,6 +84,26 @@ AUTHELIA_URL=""
|
||||
AUTHELIA_USERNAME=""
|
||||
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() {
|
||||
id "$KIOSK_USER" &>/dev/null
|
||||
}
|
||||
|
||||
+29
-12
@@ -54,23 +54,31 @@ provision_install_file() {
|
||||
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() {
|
||||
echo "[1/10] Installing packages..."
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
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
|
||||
sudo apt install -y "${PROVISION_APT_PACKAGES[@]}"
|
||||
|
||||
if lspci | grep -i "VGA.*Intel" >/dev/null 2>&1; then
|
||||
sudo apt install -y intel-gpu-tools xserver-xorg-video-intel \
|
||||
i965-va-driver intel-media-va-driver
|
||||
sudo apt install -y "${PROVISION_INTEL_APT_PACKAGES[@]}"
|
||||
provision_install_file "etc/X11/xorg.conf.d/20-intel.conf" /etc/X11/xorg.conf.d/20-intel.conf
|
||||
fi
|
||||
|
||||
@@ -101,9 +109,14 @@ provision_create_kiosk_user() {
|
||||
provision_install_nodejs() {
|
||||
echo "[3/10] Installing Node.js..."
|
||||
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 -
|
||||
sudo apt install -y nodejs
|
||||
fi
|
||||
fi
|
||||
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 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)..."
|
||||
sudo -u "$KIOSK_USER" bash -lc "
|
||||
npm config set fetch-timeout 600000
|
||||
@@ -124,6 +140,7 @@ provision_install_app() {
|
||||
log_error "npm install failed"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
electron_install_binary
|
||||
}
|
||||
|
||||
+8
-3
@@ -14,6 +14,13 @@
|
||||
# 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() {
|
||||
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"
|
||||
return 1
|
||||
fi
|
||||
if ! sudo apt install -y cups cups-client cups-filters printer-driver-all \
|
||||
printer-driver-cups-pdf hplip printer-driver-gutenprint \
|
||||
foomatic-db-compressed-ppds openprinting-ppds; then
|
||||
if ! sudo apt install -y "${CUPS_APT_PACKAGES[@]}"; then
|
||||
log_error "CUPS package installation failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -113,7 +113,9 @@ webui_install_app_files() {
|
||||
return 1
|
||||
fi
|
||||
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"
|
||||
return 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user