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:
Claude
2026-10-03 04:34:32 +00:00
parent 125622cd74
commit a170dc6228
11 changed files with 586 additions and 38 deletions
+28 -2
View File
@@ -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"
+39 -1
View File
@@ -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)..."
+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()