feedBack/build-proxmox-ct.sh
Bret Mogilefsky af2949677a
rename: slopsmith → feedBack, byron → got-feedBack (#537)
* Update GitHub repo references from feedback* to feedBack*

* rename: slopsmith -> feedBack, byron -> got-feedBack

Renames across the entire codebase:
- slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack
- byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack
- /home/byron/ -> /opt/got-feedBack/
- byron@ougsoft.com -> hi@got-feedBack.org
- github.com/byrongamatos/ -> github.com/got-feedback/
- com.byron. -> com.got-feedback.
- SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback
- Protocol/storage strings migrated with read-old/write-new pattern
- window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias

Refs: #rename-slopsmith

* rename: complete regen against current main + fix backward-compat alias

Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of
current main (3 commits had landed since the branch: #572/#554/#574),
resolving the four content conflicts in favour of main's newer content
(autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge).

Completion fixes on top of the mechanical rename:
- Re-apply rename to post-branch content the original rename never saw:
  window.slopsmith(.Tour) consumers in lessons.js / notifications.js /
  onboarding-tour.js, and the matching JS + python tests (autoplay_exit,
  progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env
  vars now match server.py (which reads FEEDBACK_SYNC_STARTUP /
  FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real
  path again.
- Restore the window.slopsmith backward-compat alias dropped during conflict
  resolution, and move the bus aliases to AFTER the _feedBackExisting merge
  block so they reference the fully-assembled object (also fixes the
  loop_api.test.js API-surface regex, which the original PR latently broke).
- Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR)
  and gitignore it.
- Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped
  source labels.

Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669
passed / 1683 collected with 0 import errors; zero residual slopsmith/byron
except the two intentional window.slopsmith aliases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* rename: implement advertised backward-compat + prune dead community plugins

Address gaps where PR #537's "Backward compatibility" section was advertised
but not implemented, and clean up the community plugin list.

Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured):
- New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py
  (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and
  tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI /
  SLOPSMITH_PLUGINS_DIR / etc. deployments keep working.
- Fix the rename collapsing plugins/__init__.py and minigames/routes.py from
  `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant
  `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost).

Storage (app.js update-channel):
- Read feedBack-update-channel, fall back to legacy slopsmith-update-channel,
  and clear the legacy key on write — so a user's update-channel preference
  survives the rename instead of resetting to "stable".

Community plugin list (README): the rename rewrote third-party repo URLs we
don't own. Probed every one; their owners never renamed, so:
- Restore the 13 live community plugins to their real slopsmith-* names.
- Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex
  tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle).
- Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory).

Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors,
rename-sensitive + startup suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:01 +02:00

668 lines
28 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# shellcheck shell=bash
# =============================================================================
# build-proxmox-ct.sh Build a Proxmox LXC rootfs from WSL2 (no lxc-start)
#
# Run this from your project root (where server.py, lib/, etc. live).
#
# Usage:
# sudo bash build-proxmox-ct.sh [TARGETARCH] [OUTPUT_NAME]
#
# Examples:
# sudo bash build-proxmox-ct.sh amd64 feedBack-ct
# sudo bash build-proxmox-ct.sh arm64 feedBack-ct
#
# The resulting container ships empty; mount or copy your .sloppak /
# loose-folder library into /dlc inside the CT after import.
#
# Environment variables:
# SKIP_HASH_CHECK=1 Bypass SHA256 verification — for unpinned hashes OR
# to override mismatches when an upstream artifact rolls.
# Use with caution.
# KEEP_BUILD_DIR=1 Retain ${BUILD_BASE} after a successful build
# FORCE_REBUILD=1 Delete an existing rootfs without prompting (for CI)
#
# Prerequisites (install in WSL):
# sudo apt install debootstrap systemd-container tar zstd curl unzip git
#
# On Proxmox, after transfer:
# pct restore <VMID> feedBack-ct.tar.zst --storage local-lvm --rootfs 8 --unprivileged 1
# =============================================================================
set -euo pipefail
TARGETARCH="${1:-amd64}"
OUTPUT_NAME="${2:-feedBack-ct}"
# OUTPUT_NAME is a positional arg that flows into BUILD_BASE (interpolated into
# `mkdir -p` / `rm -rf` paths) and into the final tarball name. Reject anything
# outside a safe filename charset so an input like `../../etc` can't escape
# /tmp or shape the tarball path.
if [[ ! "$OUTPUT_NAME" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "[ERROR] OUTPUT_NAME must match ^[A-Za-z0-9._-]+\$ (got: '${OUTPUT_NAME}')" >&2
exit 1
fi
# debootstrap requires a real Linux filesystem (ext4/tmpfs/etc.) — it creates
# device nodes that NTFS/FUSE mounts (/mnt/c, /mnt/d …) cannot represent.
# We default the build dir to /tmp (a Linux fs on every WSL2 setup we've
# seen, even when /tmp isn't strictly tmpfs) and copy the final tarball back.
# Namespace BUILD_BASE by OUTPUT_NAME + TARGETARCH so concurrent invocations
# (or stale leftovers from a prior build of a different artifact) don't
# collide on /tmp/proxmox-ct-build/rootfs. BUILD_BASE can still be overridden
# via the environment for users who want a known, reusable path.
BUILD_BASE="${BUILD_BASE:-/tmp/proxmox-ct-build-${OUTPUT_NAME}-${TARGETARCH}}"
# Safety net: BUILD_BASE feeds rm -rf in the cleanup trap, the rebuild flow,
# and (indirectly) the rootfs build. Refuse obviously dangerous values up
# front so a stray BUILD_BASE=/ or BUILD_BASE='' can never `rm -rf` the host.
case "$BUILD_BASE" in
""|/|//|/.*|.|./*|../*) echo "[ERROR] Refusing dangerous BUILD_BASE='${BUILD_BASE}'." >&2; exit 1 ;;
esac
if [[ "${BUILD_BASE}" != /* ]]; then
echo "[ERROR] BUILD_BASE must be an absolute path (got: '${BUILD_BASE}')." >&2
exit 1
fi
# Normalize so that things like /tmp/../etc resolve against the real prefix
# check below — without this, a path-traversal payload would slip past the
# /tmp/* match and the cleanup branches could rm-rf an unintended host path.
BUILD_BASE=$(realpath -m -- "$BUILD_BASE")
ROOTFS="${BUILD_BASE}/rootfs"
if (( ${#BUILD_BASE} < 6 )); then
echo "[ERROR] BUILD_BASE='${BUILD_BASE}' is too short — refusing for safety." >&2
exit 1
fi
if [[ "${BUILD_BASE}" != /tmp/* && "${I_KNOW_WHAT_IM_DOING:-0}" != "1" ]]; then
echo "[ERROR] BUILD_BASE='${BUILD_BASE}' resolves outside /tmp." >&2
echo " Re-run with I_KNOW_WHAT_IM_DOING=1 to use a non-/tmp path." >&2
exit 1
fi
mkdir -p "$BUILD_BASE"
# vgmstream tag and the immutable commit it MUST resolve to. A tag is a
# movable Git ref upstream — pinning the expected SHA and verifying after
# clone catches a re-tag without losing the shallow-clone optimisation.
VGMSTREAM_REF="r2083"
VGMSTREAM_COMMIT="57df2e179d929532094f4e4dd42ce5395514622b"
VGMSTREAM_REPO="https://github.com/vgmstream/vgmstream.git"
# Static ffmpeg binaries from BtbN/FFmpeg-Builds (GPL, 7.1 series).
# To bump: pick a new autobuild-* tag from
# https://github.com/BtbN/FFmpeg-Builds/releases
# download the two linux gpl-7.1 tarballs, re-run
# sha256sum ffmpeg-*-linux{64,arm64}-gpl-7.1.tar.xz
# and update FFMPEG_RELEASE + both builds + hashes below.
FFMPEG_RELEASE="autobuild-2026-06-01-15-02"
FFMPEG_BUILD_AMD64="ffmpeg-n7.1.4-7-gadcf20da26-linux64-gpl-7.1.tar.xz"
FFMPEG_BUILD_ARM64="ffmpeg-n7.1.4-7-gadcf20da26-linuxarm64-gpl-7.1.tar.xz"
FFMPEG_SHA256_AMD64=afde55344990650c117fbb7cb36b38d2ab6790b06beb06a9c43a9300c9ce277a
FFMPEG_SHA256_ARM64=03c8a7d9a7cf48d017a22a7c31acfdc8e76c5cb193923f883b0338c7baf0bd28
APP_DIR="/app"
VENV_DIR="/opt/app-venv"
PIP_VERSION="26.1.1"
DLC_DIR="/dlc"
CONFIG_DIR="/config"
SVC_USER="feedBack"
# Coloured logging
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
ok() { echo -e "\033[1;32m[OK]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
die() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
cleanup() {
local rc=$?
if [[ $rc -ne 0 && -d "${BUILD_BASE:-}" ]]; then
warn "Build failed (exit $rc). Partial rootfs left at ${BUILD_BASE} for inspection."
# Use printf directly: warn() pipes through `echo -e`, which would
# re-interpret the backslash escapes that `printf %q` emits and
# silently break the suggested cleanup command.
printf "\033[1;33m[WARN]\033[0m Run: sudo rm -rf %q\n" "${BUILD_BASE}"
elif [[ $rc -eq 0 && -d "${BUILD_BASE:-}" && "${KEEP_BUILD_DIR:-0}" != "1" ]]; then
info "Removing build directory ${BUILD_BASE} (set KEEP_BUILD_DIR=1 to retain)."
rm -rf "${BUILD_BASE}"
fi
}
trap cleanup EXIT
# Verify a downloaded file against a pinned SHA256 hash.
# Skips verification when the expected hash is empty (not yet pinned).
verify_sha256() {
local file="$1" expected="$2" label="${3:-$1}"
if [[ -z "$expected" ]]; then
if [[ "${SKIP_HASH_CHECK:-0}" != "1" ]]; then
die "No SHA256 pinned for ${label}. Pin the hash or set SKIP_HASH_CHECK=1 to proceed."
fi
warn "No SHA256 pinned for ${label} — skipping verification (SKIP_HASH_CHECK=1)."
return 0
fi
local actual
actual=$(sha256sum "$file" | awk '{print $1}')
if [[ "$actual" != "$expected" ]]; then
if [[ "${SKIP_HASH_CHECK:-0}" == "1" ]]; then
warn "SHA256 mismatch for ${label} (expected ${expected}, got ${actual}) — continuing because SKIP_HASH_CHECK=1."
return 0
fi
die "SHA256 mismatch for ${label}:\n" \
" expected: ${expected}\n" \
" got: ${actual}\n" \
" Refresh the pinned hash, or set SKIP_HASH_CHECK=1 to bypass."
fi
ok "SHA256 verified for ${label}."
}
[[ $EUID -eq 0 ]] || die "Run as root: sudo bash $0"
case "$TARGETARCH" in
arm64) RID="linux-arm64" ; DEBIAN_ARCH="arm64" ;;
amd64) RID="linux-x64" ; DEBIAN_ARCH="amd64" ;;
*) die "Unsupported TARGETARCH: ${TARGETARCH}. Expected: amd64 | arm64" ;;
esac
# arm64 cross-builds require qemu-user-static + a registered binfmt handler.
# Checking the binary alone isn't enough — without a registered+enabled handler
# debootstrap/nspawn fail later with "exec format error" after significant
# wasted setup time.
if [[ "$TARGETARCH" == "arm64" && "$(uname -m)" != "aarch64" ]]; then
if ! command -v qemu-aarch64-static &>/dev/null; then
die "arm64 builds on a non-arm64 host require qemu-user-static.\n" \
" Install with: sudo apt install qemu-user-static binfmt-support\n" \
" Then re-run this script."
fi
binfmt_reg=""
for f in /proc/sys/fs/binfmt_misc/qemu-aarch64 \
/proc/sys/fs/binfmt_misc/qemu-aarch64-static; do
[[ -f "$f" ]] && grep -q '^enabled' "$f" 2>/dev/null && { binfmt_reg="$f"; break; }
done
if [[ -z "$binfmt_reg" ]]; then
die "arm64 binfmt handler not registered or not enabled.\n" \
" Register with: sudo apt install qemu-user-static binfmt-support\n" \
" Or: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes\n" \
" Then verify: grep ^enabled /proc/sys/fs/binfmt_misc/qemu-aarch64*"
fi
fi
# Confirm required tools
for cmd in debootstrap systemd-nspawn curl unzip git tar zstd; do
command -v "$cmd" &>/dev/null || die "'$cmd' not found. Run: sudo apt install debootstrap systemd-container curl unzip git tar zstd"
done
# =============================================================================
# Pre-flight: verify the pinned BtbN FFmpeg release still exists
# =============================================================================
# BtbN only keeps ~10 days of autobuilds. A stale FFMPEG_RELEASE means
# the build will 404 deep into step 5b after significant setup work.
# Fail fast with actionable instructions instead.
info "Checking ffmpeg release availability …"
case "$TARGETARCH" in
arm64) _preflight_tarball="${FFMPEG_BUILD_ARM64}" ;;
amd64) _preflight_tarball="${FFMPEG_BUILD_AMD64}" ;;
esac
_preflight_url="https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_RELEASE}/${_preflight_tarball}"
_http_code=$(curl -sL -o /dev/null -w '%{http_code}' --head "${_preflight_url}" || true)
if [[ ! "$_http_code" =~ ^2[0-9]{2}$ ]]; then
die "Pinned ffmpeg release is no longer available (HTTP ${_http_code}).
URL: ${_preflight_url}
BtbN/FFmpeg-Builds only keeps ~10 days of autobuilds.
To fix, update these variables in build-proxmox-ct.sh:
1. Pick a current release tag from:
https://github.com/BtbN/FFmpeg-Builds/releases
2. Update FFMPEG_RELEASE to the new tag
(e.g. autobuild-YYYY-MM-DD-HH-MM)
3. Update FFMPEG_BUILD_AMD64 and FFMPEG_BUILD_ARM64
to the new *-linux64-gpl-7.1.tar.xz and
*-linuxarm64-gpl-7.1.tar.xz filenames
4. Update FFMPEG_SHA256_AMD64 and FFMPEG_SHA256_ARM64
from the checksums.sha256 file in that release
Also update the same ARGs in Dockerfile"
fi
ok "ffmpeg release verified (HTTP ${_http_code})."
# =============================================================================
# Helper: run a command inside the rootfs via systemd-nspawn
# --quiet suppresses nspawn chatter so apt/cmake output is the only
# thing we see during the build. The host's /etc/resolv.conf is
# bind-mounted read-only so DNS works inside nspawn.
# =============================================================================
r() {
systemd-nspawn \
--quiet \
--directory="$ROOTFS" \
--bind-ro=/etc/resolv.conf:/etc/resolv.conf \
-- bash -c "set -e; $1"
}
# =============================================================================
# 1. Bootstrap a minimal Debian Trixie rootfs
# =============================================================================
info "Bootstrapping Debian Trixie (${DEBIAN_ARCH}) rootfs at ${ROOTFS}"
if [[ -d "$ROOTFS" ]]; then
if [[ "${FORCE_REBUILD:-0}" == "1" ]]; then
info "FORCE_REBUILD=1 — removing existing rootfs at ${ROOTFS}."
rm -rf "$ROOTFS" || die "Failed to remove existing rootfs at ${ROOTFS}."
elif [[ -t 0 ]]; then
warn "Existing rootfs found at ${ROOTFS} remove it to rebuild from scratch."
read -rp " Delete and rebuild? [y/N] " yn
if [[ "$yn" =~ ^[Yy]$ ]]; then
rm -rf "$ROOTFS" || die "Failed to remove existing rootfs at ${ROOTFS}."
else
die "Aborting."
fi
else
die "Existing rootfs at ${ROOTFS}; rerun with FORCE_REBUILD=1 to overwrite."
fi
fi
debootstrap \
--arch="$DEBIAN_ARCH" \
--include=ca-certificates,curl,gnupg \
trixie \
"$ROOTFS" \
https://deb.debian.org/debian
ok "Bootstrap complete."
# DNS during the build is supplied by the host's /etc/resolv.conf, which
# r() bind-mounts read-only into nspawn. The rootfs's own /etc/resolv.conf
# gets replaced with a systemd-resolved stub symlink in step 10(d).
# =============================================================================
# 2. System packages (mirrors Stage 2 apt block)
# =============================================================================
info "Installing system packages …"
# systemd-sysv + systemd-resolved are explicit because the final container
# enables systemd-networkd/systemd-resolved units in step 10 and rewrites
# /etc/resolv.conf to the resolved stub — a minimal debootstrap does not
# guarantee these binaries on its own, which would yield broken DNS in the
# imported CT.
#
# NOTE: ffmpeg is NOT installed via apt — a static binary is copied in
# step 5b instead, avoiding the huge codec + TLS dependency tree.
# vgmstream-cli is built from source in step 5, needing runtime libs:
# libmpg123-0, libvorbisfile3, libspeex1, libopus0.
#
# Node.js: Debian Trixie ships Node 20 by default. We use extrepo to enable
# the official NodeSource Node 22.x repository for LTS support.
r "apt-get update -qq \
&& apt-get -y upgrade \
&& apt-get install -y --no-install-recommends \
systemd-sysv systemd-resolved \
python3 python3-pip python3-venv \
fluidsynth \
fluid-soundfont-gm \
libsndfile1 \
libmpg123-0 \
libvorbisfile3 \
libspeex1 \
libopus0 \
libstdc++6 \
libgcc-s1 \
extrepo \
&& apt-get clean && rm -rf /var/lib/apt/lists/*"
ok "System packages installed."
info "Enabling Node.js 22.x repository via extrepo …"
r "extrepo enable node_22.x \
&& apt-get update -qq \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get clean && rm -rf /var/lib/apt/lists/*"
ok "Node.js 22.x installed."
# Build-time dependencies for compiling vgmstream-cli from source (step 5).
# Purged after the build in step 5c to keep the rootfs lean.
info "Installing vgmstream build dependencies (temporary) …"
# `git` intentionally absent — vgmstream is cloned host-side and the build
# itself uses cmake only, so the container never invokes git. Adding it back
# would pull in libcurl/libexpat unnecessarily.
r "apt-get update -qq && apt-get install -y --no-install-recommends \
build-essential cmake pkg-config yasm \
libmpg123-dev libvorbis-dev libspeex-dev libopus-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*"
ok "Build dependencies installed."
# =============================================================================
# 5. Build vgmstream-cli from source (mirrors Dockerfile stage 1b)
# =============================================================================
info "Building vgmstream-cli from source (${VGMSTREAM_REF}) …"
# Clone into /root/ — systemd-nspawn mounts a private tmpfs over /tmp,
# hiding anything placed at ${ROOTFS}/tmp/ by the host.
rm -rf "${ROOTFS}/root/vgmstream"
git clone --depth 1 --branch "${VGMSTREAM_REF}" "${VGMSTREAM_REPO}" "${ROOTFS}/root/vgmstream"
# Verify the tag still points at the pinned commit — a re-tag upstream
# would otherwise silently change what we ship.
_vgmstream_head=$(git -C "${ROOTFS}/root/vgmstream" rev-parse HEAD)
if [[ "${_vgmstream_head}" != "${VGMSTREAM_COMMIT}" ]]; then
die "vgmstream tag ${VGMSTREAM_REF} resolves to ${_vgmstream_head}, expected ${VGMSTREAM_COMMIT}. Tag may have moved upstream — verify and update VGMSTREAM_COMMIT."
fi
r "cmake -S /root/vgmstream -B /root/vgmstream/build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_V123=OFF \
-DBUILD_AUDACIOUS=OFF \
-DBUILD_SHARED_LIBS=OFF \
-DUSE_FFMPEG=OFF \
&& cmake --build /root/vgmstream/build --config Release --target vgmstream_cli -j\$(nproc) \
&& cp /root/vgmstream/build/cli/vgmstream-cli /usr/local/bin/vgmstream-cli \
&& chmod +x /usr/local/bin/vgmstream-cli"
rm -rf "${ROOTFS}/root/vgmstream"
ok "vgmstream-cli built."
# =============================================================================
# 5b. Static ffmpeg (mirrors Dockerfile stage 1c — BtbN GPL build)
# =============================================================================
info "Installing static ffmpeg …"
case "$TARGETARCH" in
arm64) FFMPEG_TARBALL="${FFMPEG_BUILD_ARM64}"; FFMPEG_SHA256="${FFMPEG_SHA256_ARM64}" ;;
amd64) FFMPEG_TARBALL="${FFMPEG_BUILD_AMD64}"; FFMPEG_SHA256="${FFMPEG_SHA256_AMD64}" ;;
esac
curl -fsSL "https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_RELEASE}/${FFMPEG_TARBALL}" \
-o "${BUILD_BASE}/ffmpeg.tar.xz"
verify_sha256 "${BUILD_BASE}/ffmpeg.tar.xz" "${FFMPEG_SHA256}" "ffmpeg-static (${TARGETARCH})"
mkdir -p "${BUILD_BASE}/ffmpeg-extract"
tar -xJf "${BUILD_BASE}/ffmpeg.tar.xz" -C "${BUILD_BASE}/ffmpeg-extract" --strip-components=1
cp "${BUILD_BASE}/ffmpeg-extract/bin/ffmpeg" "${ROOTFS}/usr/local/bin/ffmpeg"
cp "${BUILD_BASE}/ffmpeg-extract/bin/ffprobe" "${ROOTFS}/usr/local/bin/ffprobe"
chmod +x "${ROOTFS}/usr/local/bin/ffmpeg" "${ROOTFS}/usr/local/bin/ffprobe"
# GPL compliance — ship the license text AND a written offer pointing
# at where the corresponding source can be obtained, so redistributing
# the resulting CT template doesn't strand recipients without access to
# the source for the GPL-licensed binary. LICENSE.txt alone covers the
# license terms; the SOURCE file satisfies the "corresponding source"
# availability requirement of GPLv3 §6 / GPLv2 §3.
mkdir -p "${ROOTFS}/usr/share/doc/ffmpeg"
cp "${BUILD_BASE}/ffmpeg-extract/LICENSE.txt" "${ROOTFS}/usr/share/doc/ffmpeg/LICENSE.txt"
cat > "${ROOTFS}/usr/share/doc/ffmpeg/SOURCE" <<EOF
This ffmpeg/ffprobe binary is a static GPL build from the BtbN/FFmpeg-Builds
project (https://github.com/BtbN/FFmpeg-Builds). The exact build artefact
shipped here is:
Release: ${FFMPEG_RELEASE}
Tarball: ${FFMPEG_TARBALL}
SHA-256: ${FFMPEG_SHA256}
Corresponding source code (per GPL):
- Build recipe + scripts: https://github.com/BtbN/FFmpeg-Builds/tree/${FFMPEG_RELEASE}
- Upstream FFmpeg source: https://github.com/FFmpeg/FFmpeg (the FFmpeg
commit baked into this build is identified by the n7.1.x version tag
in the tarball filename, e.g. n7.1.4-5-ged860ef7d9 → upstream commit
ed860ef7d9).
If either URL becomes unavailable, the maintainer of this CT template will
make the corresponding source available on request for at least three years,
per GPLv2 §3(b) / GPLv3 §6(b).
EOF
rm -rf "${BUILD_BASE}/ffmpeg-extract" "${BUILD_BASE}/ffmpeg.tar.xz"
ok "Static ffmpeg installed."
# =============================================================================
# 5c. Clean up build-time dependencies
# =============================================================================
info "Removing build-time dependencies …"
r "apt-get purge -y --auto-remove \
build-essential cmake pkg-config yasm \
libmpg123-dev libvorbis-dev libspeex-dev libopus-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*"
ok "Build dependencies removed."
# =============================================================================
# 5d. Tailwind CLI for runtime stylesheet regeneration
# =============================================================================
# When a plugin is installed into FEEDBACK_PLUGINS_DIR at runtime (or
# discovered there on startup), the server rebuilds static/tailwind.min.css
# so the plugin's classes are styled — the image-baked sheet only covers
# in-tree plugins (see lib/tailwind_rebuild.py). tailwindcss is installed
# globally so the rebuild runs offline, with no npx fetch at install time.
# nodejs + npm were installed in step 2 via apt.
info "Installing Tailwind CLI globally …"
r "npm install -g tailwindcss@3.4.19 \
&& npm cache clean --force"
ok "Tailwind CLI installed."
# =============================================================================
# 6. Python application
# =============================================================================
info "Setting up Python application …"
mkdir -p \
"${ROOTFS}${APP_DIR}/lib" \
"${ROOTFS}${APP_DIR}/static" \
"${ROOTFS}${APP_DIR}/plugins"
for d in lib static plugins; do
if [[ -d "$d" ]]; then
cp -r "${d}/." "${ROOTFS}${APP_DIR}/${d}/"
info " Copied ${d}/"
else
# main.py imports logging_setup from lib/; server.py imports plugins.
# Without these the rootfs boots and the service crashes immediately.
if [[ "$d" == "lib" || "$d" == "plugins" ]]; then
die " '${d}/' not found — required for the service to import."
fi
warn " Local '${d}/' not found skipping."
fi
done
for f in requirements.txt server.py VERSION main.py tailwind.config.js; do
if [[ -f "$f" ]]; then
cp "$f" "${ROOTFS}${APP_DIR}/"
info " Copied ${f}"
else
# main.py imports `server:app`, so without server.py the service unit
# would boot but fail on first request — make it fail-fast at build.
if [[ "$f" == "requirements.txt" || "$f" == "main.py" || "$f" == "server.py" ]]; then
die " '${f}' not found — required for the service to start."
fi
warn " '${f}' not found skipping."
fi
done
info "Creating Python venv and installing dependencies …"
r "python3 -m venv ${VENV_DIR} \
&& ${VENV_DIR}/bin/pip install --no-cache-dir 'pip==${PIP_VERSION}' \
&& ${VENV_DIR}/bin/pip install --no-cache-dir -r ${APP_DIR}/requirements.txt"
ok "Python venv + dependencies installed."
# =============================================================================
# 6b. Build Tailwind stylesheet over the full plugin set
# =============================================================================
# The committed static/tailwind.min.css is generated against only the in-tree
# plugins. Rebuild it here after static/ + plugins/ are in place so the sheet
# covers whatever plugins are baked into the rootfs. Mirrors Dockerfile
# stage 1d. Runtime-installed plugins are handled by the server's rebuild.
info "Building Tailwind stylesheet …"
r "cd ${APP_DIR} \
&& tailwindcss \
-c tailwind.config.js \
-i static/_tailwind.src.css \
-o static/tailwind.min.css \
--minify"
ok "Tailwind stylesheet built."
# =============================================================================
# 7. Data directories + assets
# =============================================================================
info "Populating data directories …"
# The library folder (${DLC_DIR}) is created empty — mount or copy your
# .sloppak / loose-folder library into it after the CT is imported.
mkdir -p "${ROOTFS}${CONFIG_DIR}" "${ROOTFS}${DLC_DIR}"
if [[ -d "config" ]]; then
cp -r config/. "${ROOTFS}${CONFIG_DIR}/"
info " Copied config/"
else
warn " config/ not found."
fi
# =============================================================================
# 8. Environment variables
# =============================================================================
info "Writing /etc/environment …"
cat > "${ROOTFS}/etc/environment" <<EOF
PATH=${VENV_DIR}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PYTHONPATH=${APP_DIR}/lib:${APP_DIR}
DLC_DIR=${DLC_DIR}
CONFIG_DIR=${CONFIG_DIR}
EOF
# =============================================================================
# 9. systemd service for uvicorn
# =============================================================================
info "Creating service user '${SVC_USER}' …"
r "useradd --system --home-dir ${APP_DIR} --shell /usr/sbin/nologin ${SVC_USER}"
ok "User '${SVC_USER}' created."
info "Installing feedBack-server.service …"
mkdir -p "${ROOTFS}/etc/systemd/system"
cat > "${ROOTFS}/etc/systemd/system/feedBack-server.service" <<EOF
[Unit]
Description=FeedBack uvicorn server
After=network.target
[Service]
User=${SVC_USER}
# Default port (8000) is non-privileged; uncomment the next line only if
# you set PORT<1024 in /etc/environment so the unit can bind it.
# AmbientCapabilities=CAP_NET_BIND_SERVICE
WorkingDirectory=${APP_DIR}
EnvironmentFile=/etc/environment
ExecStart=${VENV_DIR}/bin/python3 main.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Enable by symlinking (avoids running systemctl inside nspawn)
mkdir -p "${ROOTFS}/etc/systemd/system/multi-user.target.wants"
ln -sf /etc/systemd/system/feedBack-server.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/feedBack-server.service"
ok "Service enabled."
# =============================================================================
# 10. Proxmox-specific tweaks
# =============================================================================
info "Applying Proxmox CT compatibility tweaks …"
# (a) Ensure a working /etc/hostname and /etc/hosts.
# Use OUTPUT_NAME (already validated to a safe filename charset) so the
# template's identity matches the artifact name. This is just a sane
# fallback — `pct restore --hostname …` (or the Proxmox UI) will overwrite
# /etc/hostname when the CT is created from this template.
DEFAULT_HOSTNAME="${OUTPUT_NAME//_/-}" # underscores aren't valid in hostnames
echo "${DEFAULT_HOSTNAME}" > "${ROOTFS}/etc/hostname"
cat > "${ROOTFS}/etc/hosts" <<EOF
127.0.0.1 localhost
127.0.1.1 ${DEFAULT_HOSTNAME}
::1 localhost ip6-localhost ip6-loopback
EOF
# (b) Clear machine-id so Proxmox generates a fresh one on first boot
# A pre-filled machine-id can cause network/systemd conflicts across clones.
echo -n > "${ROOTFS}/etc/machine-id"
[[ -f "${ROOTFS}/var/lib/dbus/machine-id" ]] && echo -n > "${ROOTFS}/var/lib/dbus/machine-id"
# (c) DHCP networking via systemd-networkd (Proxmox expects this for unprivileged CTs)
mkdir -p "${ROOTFS}/etc/systemd/network"
cat > "${ROOTFS}/etc/systemd/network/20-eth0.network" <<EOF
[Match]
Name=eth0
[Network]
DHCP=yes
EOF
# Enable via symlinks on the host systemctl inside nspawn needs a running
# init which WSL doesn't provide.
mkdir -p "${ROOTFS}/etc/systemd/system/multi-user.target.wants"
for svc in systemd-networkd systemd-resolved; do
unit_src=""
for d in /lib/systemd/system /usr/lib/systemd/system; do
if [[ -e "${ROOTFS}${d}/${svc}.service" ]]; then
unit_src="${d}/${svc}.service"
break
fi
done
if [[ -z "$unit_src" ]]; then
die "${svc}.service unit not found in rootfs — DNS/networking would be broken in the imported CT."
fi
ln -sf "${unit_src}" "${ROOTFS}/etc/systemd/system/multi-user.target.wants/${svc}.service"
done
# (d) Ensure correct permissions on key dirs.
# -h preserves symlinks: a Python venv keeps /opt/app-venv/bin/python3 as a
# symlink to /usr/bin/python3, and a plain `chown -R` would chase it and
# rewrite the system interpreter's ownership inside the rootfs.
SVC_UID="$(r "id -u ${SVC_USER}" | tr -d '\r')"
SVC_GID="$(r "id -g ${SVC_USER}" | tr -d '\r')"
chown -hR "${SVC_UID}:${SVC_GID}" \
"${ROOTFS}${APP_DIR}" "${ROOTFS}${CONFIG_DIR}" \
"${ROOTFS}${DLC_DIR}" "${ROOTFS}${VENV_DIR}"
# (e) Fix resolv.conf to use the systemd-resolved stub. MUST run after the
# last r() invocation: r() bind-mounts the host /etc/resolv.conf onto the
# rootfs path, and systemd-nspawn follows symlinks when resolving the bind
# target — pointing /etc/resolv.conf at /run/systemd/resolve/stub-resolv.conf
# before that would make subsequent r() calls try to bind onto a path that
# doesn't exist during the build.
rm -f "${ROOTFS}/etc/resolv.conf"
ln -sf /run/systemd/resolve/stub-resolv.conf "${ROOTFS}/etc/resolv.conf"
ok "Proxmox tweaks applied."
# =============================================================================
# 11. Package as a Proxmox-importable .tar.zst
# =============================================================================
OUTPUT_FILE="${OUTPUT_NAME}.tar.zst"
info "Creating ${OUTPUT_FILE}"
# Proxmox pct restore expects a plain rootfs tarball (no ./rootfs/ prefix).
tar \
--numeric-owner \
--xattrs \
--acls \
-C "$ROOTFS" \
-c . \
| zstd -T0 -9 > "$OUTPUT_FILE"
ok "Template ready: $(pwd)/${OUTPUT_FILE} ($(du -sh "$OUTPUT_FILE" | cut -f1))"
# =============================================================================
# Done
# =============================================================================
cat <<DONE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Build complete!
Transfer to Proxmox:
scp ${OUTPUT_FILE} root@<proxmox-host>:/var/lib/vz/template/cache/
Import on Proxmox (pick an unused VMID, e.g. 200):
pct restore 200 /var/lib/vz/template/cache/${OUTPUT_FILE} \\
--storage local-lvm \\
--rootfs 8 \\
--memory 2048 \\
--cores 2 \\
--net0 name=eth0,bridge=vmbr0,ip=dhcp \\
--unprivileged 1 \\
--start 1
Then check the server:
pct exec 200 -- systemctl status feedBack-server
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DONE