The trigger copies osa-mother-2026 from the build host into any ISO as long as the key file exists (which it does permanently on OSA). A BUILD_CHANNEL=release build would embed the private key into a publicly hosted image = mother compromise. Add a fail-closed guard: release builds exit with an error before copying the key. Dev builds (including personalized sticks) are unaffected.
2522 lines
111 KiB
Bash
Executable file
2522 lines
111 KiB
Bash
Executable file
#!/bin/sh
|
|
# clawdie-iso operator USB build script
|
|
# Produces a bootable FreeBSD XFCE operator USB image with Clawdie-AI pre-bundled.
|
|
# All packages are fetched and bundled for fully offline installation.
|
|
#
|
|
# Runtime detection handles configuration (display, GPU, etc.)
|
|
#
|
|
# Usage:
|
|
# ./build.sh # full build (fetch + assemble)
|
|
# ./build.sh --fetch-only # fetch packages/memstick only (no root needed)
|
|
# ./build.sh --skip-fetch # assemble only (use cached packages)
|
|
# ./build.sh --skip-memstick-fetch # fetch packages, reuse cached FreeBSD memstick
|
|
# ./build.sh --live-default-password # set live clawdie password to quindecim
|
|
# ./build.sh --clawdie-password ... # set an explicit live clawdie password
|
|
# ./build.sh --clawdie-version 1.0.2 # pin Clawdie-AI release tag v1.0.2
|
|
# ./build.sh --clawdie-ref main # bundle a branch/tag/commit ref
|
|
#
|
|
# Requirements (run on FreeBSD host):
|
|
# pkg install curl # for fetching
|
|
# pkg install node24 npm-node24 # to bundle clawdie-ai node_modules for offline firstboot
|
|
# pkg install (root) # for step 5-6 (mdconfig, mount)
|
|
#
|
|
# The tmp/packages/ directory produced here is dual-purpose:
|
|
# 1. Bundled into the ISO for offline installation
|
|
# 2. Used to seed zroot/pkg-cache on the installed system for offline jail provisioning
|
|
|
|
set -e
|
|
|
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|
# Make build host command lookup independent from the invoking user's login PATH.
|
|
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
|
export PATH
|
|
NPM_CONFIG_UPDATE_NOTIFIER=false
|
|
export NPM_CONFIG_UPDATE_NOTIFIER
|
|
TMP_DIR="${SCRIPT_DIR}/tmp"
|
|
NPM_CONFIG_GLOBALCONFIG="${TMP_DIR}/npm-globalconfig"
|
|
NPM_CONFIG_USERCONFIG="${TMP_DIR}/npm-userconfig"
|
|
export NPM_CONFIG_GLOBALCONFIG NPM_CONFIG_USERCONFIG
|
|
PKG_LIST_DIR="${SCRIPT_DIR}/packages"
|
|
PKG_REPO_DIR="${TMP_DIR}/packages"
|
|
NPM_GLOBALS_DIR="${TMP_DIR}/npm-globals"
|
|
CACHE_DIR="${TMP_DIR}/cache"
|
|
OUTPUT_DIR="${TMP_DIR}/output"
|
|
LIVE_SESSION_DIR="${SCRIPT_DIR}/live/operator-session"
|
|
mkdir -p "$TMP_DIR"
|
|
|
|
. "${SCRIPT_DIR}/build.cfg"
|
|
|
|
BUILD_HOST_USER=""
|
|
BUILD_HOST_HOME=""
|
|
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
|
|
BUILD_HOST_USER="$SUDO_USER"
|
|
BUILD_HOST_HOME="$(getent passwd "$BUILD_HOST_USER" | cut -d: -f6)"
|
|
fi
|
|
if [ "$(id -u)" -eq 0 ] && [ -z "${BUILD_HOST_USER}" ]; then
|
|
echo "WARN: build is running as root without SUDO_USER; npm stages cannot drop to a non-root build user."
|
|
fi
|
|
|
|
# --- argument parsing ---
|
|
SKIP_FETCH=0
|
|
SKIP_MEMSTICK_FETCH=0
|
|
FETCH_ONLY=0
|
|
LIVE_DEFAULT_PASSWORD=0
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--clawdie-version) CLAWDIE_VERSION="$2"; CLAWDIE_REF="v$2"; shift 2 ;;
|
|
--clawdie-ref) CLAWDIE_REF="$2"; CLAWDIE_VERSION="$2"; shift 2 ;;
|
|
--skip-fetch) SKIP_FETCH=1; shift ;;
|
|
--skip-memstick-fetch) SKIP_MEMSTICK_FETCH=1; shift ;;
|
|
--fetch-only) FETCH_ONLY=1; shift ;;
|
|
--live-default-password) LIVE_DEFAULT_PASSWORD=1; shift ;;
|
|
--assistant-name) ASSISTANT_NAME="$2"; shift 2 ;;
|
|
--domain) AGENT_DOMAIN="$2"; shift 2 ;;
|
|
--tz) TZ="$2"; shift 2 ;;
|
|
--ssh-key) SSH_PUBLIC_KEY="$2"; shift 2 ;;
|
|
--tailscale-auth-key) TAILSCALE_AUTHKEY="$2"; shift 2 ;;
|
|
--root-password) ROOT_PASSWORD="$2"; shift 2 ;;
|
|
--clawdie-password) CLAWDIE_USER_PASSWORD="$2"; shift 2 ;;
|
|
*) echo "Unknown arg: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [ "$LIVE_DEFAULT_PASSWORD" -eq 1 ] && [ -z "${CLAWDIE_USER_PASSWORD:-}" ]; then
|
|
CLAWDIE_USER_PASSWORD="quindecim"
|
|
fi
|
|
|
|
TAILSCALE_AUTH_KEY_BAKED=false
|
|
if [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
|
|
TAILSCALE_AUTH_KEY_BAKED=true
|
|
fi
|
|
|
|
LIVE_SSH_PUBKEY_FP=""
|
|
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then
|
|
_ssh_fp_tmp=$(mktemp "${TMP_DIR}/live-ssh-pubkey.XXXXXX")
|
|
printf '%s\n' "${SSH_PUBLIC_KEY}" > "${_ssh_fp_tmp}"
|
|
LIVE_SSH_PUBKEY_FP=$(ssh-keygen -lf "${_ssh_fp_tmp}" 2>/dev/null | awk '{print $2}')
|
|
rm -f "${_ssh_fp_tmp}"
|
|
if [ -z "${LIVE_SSH_PUBKEY_FP:-}" ]; then
|
|
echo "ERROR: could not derive fingerprint from --ssh-key input"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# The release gate runs in the preflight section below — it depends on the
|
|
# resolve_* helpers, which are defined later in this script.
|
|
|
|
# The ISO carries its own product version (ISO_VERSION in build.cfg). It does not
|
|
# track any single component. Component versions are resolved below purely for
|
|
# provenance in build-manifest.json. A milestone build must declare a version.
|
|
if [ -z "${ISO_VERSION:-}" ] || [ "${ISO_VERSION}" = "auto" ]; then
|
|
echo "ERROR: ISO_VERSION must be an explicit product version (e.g. 0.10.0)."
|
|
echo " The image no longer tracks zot — set ISO_VERSION in build.cfg."
|
|
exit 1
|
|
fi
|
|
_iso_zot_repo="${ZOT_REPO:-${SCRIPT_DIR}/../zot}"
|
|
case "${_iso_zot_repo}" in /*) ;; *) _iso_zot_repo="${SCRIPT_DIR}/${_iso_zot_repo}" ;; esac
|
|
ZOT_RESOLVED_VERSION=""
|
|
ZOT_RESOLVED_COMMIT=""
|
|
if [ -d "${_iso_zot_repo}/.git" ]; then
|
|
ZOT_RESOLVED_VERSION="$(git -C "${_iso_zot_repo}" describe --tags --always 2>/dev/null || true)"
|
|
ZOT_RESOLVED_COMMIT="$(git -C "${_iso_zot_repo}" rev-parse --short=12 HEAD 2>/dev/null || true)"
|
|
fi
|
|
[ -n "${ZOT_RESOLVED_VERSION}" ] || ZOT_RESOLVED_VERSION="${ZOT_VERSION}"
|
|
|
|
echo "==> clawdie-iso build"
|
|
echo " ISO : ${ISO_VERSION}-${BUILD_CHANNEL} (zot ${ZOT_RESOLVED_VERSION})"
|
|
echo " FreeBSD : ${FREEBSD_VERSION} ${FREEBSD_ARCH}"
|
|
echo " Clawdie : ${CLAWDIE_REF}"
|
|
echo " Desktop : ${DEFAULT_DESKTOP}"
|
|
echo " Pkg : ${DEFAULT_PKG_BRANCH}"
|
|
echo " GPU : ${GPU_DRIVER:-auto-detect}"
|
|
echo " NVIDIA universal : ${NVIDIA_UNIVERSAL:-NO}"
|
|
echo " Target : ${TARGET:-baremetal}"
|
|
echo " Colibri : ${FEATURE_COLIBRI:-NO} (agent ${ZOT_VERSION:-} / stage ${COLIBRI_STAGE_AGENT:-YES} / test-agent ${COLIBRI_STAGE_TEST_AGENT:-NO})"
|
|
echo ""
|
|
|
|
# Name the output: clawdie-<freebsd-codename>-<version>.img, where the version is
|
|
# the ISO product version (see ISO_VERSION above). Per-build provenance — date,
|
|
# clawdie-iso/colibri/zot/clawdie-ai commits — lives in build-manifest.json.
|
|
BUILD_FREEBSD_MAJOR="${FREEBSD_RELEASE_SERIES%%.*}"
|
|
BUILD_FREEBSD_STAMP="$(printf '%s' "${FREEBSD_RELEASE_SERIES}" | tr 'A-Z' 'a-z')"
|
|
case "${BUILD_FREEBSD_MAJOR}" in
|
|
15) BUILD_FREEBSD_CODENAME="quindecim" ;;
|
|
*) BUILD_FREEBSD_CODENAME="fbsd${BUILD_FREEBSD_STAMP}" ;;
|
|
esac
|
|
IMAGE_NAME="clawdie-${BUILD_FREEBSD_CODENAME}-${ISO_VERSION}.img"
|
|
: > "$NPM_CONFIG_GLOBALCONFIG"
|
|
: > "$NPM_CONFIG_USERCONFIG"
|
|
|
|
# --- helper: read package lists into a single deduplicated list ---
|
|
pkg_list_all() {
|
|
# Operator USB: include host, jail, desktop, GPU, and disk-install extras
|
|
# for offline use. Runtime detection handles configuration.
|
|
|
|
echo "==> Operator USB: including packages (host + jails + XFCE desktop + GPU drivers + disk extras)" >&2
|
|
|
|
cat \
|
|
"${PKG_LIST_DIR}/pkg-list-host.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-jails.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-desktop-base.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-xfce.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-xfce-theming.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-live-operator.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-disk-install-extras.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-nvidia-all.txt" \
|
|
| grep -v '^#' | grep -v '^$' | sort -u
|
|
}
|
|
|
|
pkg_list_live_installer() {
|
|
cat \
|
|
"${PKG_LIST_DIR}/pkg-list-desktop-base.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-xfce.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-xfce-theming.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-live-operator.txt" \
|
|
| grep -v '^#' \
|
|
| grep -v '^$' \
|
|
| sort -u
|
|
}
|
|
|
|
pkg_list_live_nvidia() {
|
|
case "${GPU_DRIVER:-}" in
|
|
nvidia-390)
|
|
cat "${PKG_LIST_DIR}/pkg-list-nvidia-390.txt"
|
|
;;
|
|
nvidia-470)
|
|
cat "${PKG_LIST_DIR}/pkg-list-nvidia-470.txt"
|
|
;;
|
|
nvidia-590)
|
|
cat "${PKG_LIST_DIR}/pkg-list-nvidia-590.txt"
|
|
;;
|
|
*)
|
|
return 0
|
|
;;
|
|
esac | grep -v '^#' | grep -v '^$' | sort -u
|
|
}
|
|
|
|
set_config_line() {
|
|
_file="$1"
|
|
_assignment="$2"
|
|
_name=$(echo "$_assignment" | cut -d= -f1)
|
|
mkdir -p "$(dirname "$_file")"
|
|
touch "$_file"
|
|
if grep -q "^${_name}=" "$_file" 2>/dev/null; then
|
|
sed -i '' "s|^${_name}=.*|${_assignment}|" "$_file"
|
|
else
|
|
echo "$_assignment" >> "$_file"
|
|
fi
|
|
}
|
|
|
|
append_rc_list_values() {
|
|
_file="$1"
|
|
_name="$2"
|
|
shift 2
|
|
|
|
mkdir -p "$(dirname "$_file")"
|
|
touch "$_file"
|
|
|
|
_current=""
|
|
if grep -q "^${_name}=" "$_file" 2>/dev/null; then
|
|
_current=$(sed -n "s/^${_name}=\"\\(.*\\)\"/\\1/p" "$_file" | head -1)
|
|
fi
|
|
|
|
_merged="$_current"
|
|
for _value in "$@"; do
|
|
if [ -z "$_merged" ]; then
|
|
_merged="$_value"
|
|
continue
|
|
fi
|
|
case " ${_merged} " in
|
|
*" ${_value} "*) ;;
|
|
*)
|
|
_merged="${_merged} ${_value}"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
set_config_line "$_file" "${_name}=\"${_merged}\""
|
|
}
|
|
|
|
ensure_fstab_line() {
|
|
_file="$1"
|
|
_pattern="$2"
|
|
_line="$3"
|
|
mkdir -p "$(dirname "$_file")"
|
|
touch "$_file"
|
|
if ! grep -Eq "$_pattern" "$_file" 2>/dev/null; then
|
|
echo "$_line" >> "$_file"
|
|
fi
|
|
}
|
|
|
|
set_fstab_line() {
|
|
_file="$1"
|
|
_pattern="$2"
|
|
_line="$3"
|
|
mkdir -p "$(dirname "$_file")"
|
|
touch "$_file"
|
|
_tmp="${_file}.tmp.$$"
|
|
awk -v pattern="$_pattern" -v line="$_line" '
|
|
BEGIN { replaced = 0 }
|
|
$0 ~ pattern {
|
|
if (!replaced) {
|
|
print line
|
|
replaced = 1
|
|
}
|
|
next
|
|
}
|
|
{ print }
|
|
END {
|
|
if (!replaced) {
|
|
print line
|
|
}
|
|
}
|
|
' "$_file" > "$_tmp"
|
|
mv "$_tmp" "$_file"
|
|
}
|
|
|
|
pkg_archive_for() {
|
|
_pkg_name="$1"
|
|
# Match exact package names only. A loose `${name}-*.pkg` pattern lets a
|
|
# missing package such as `xfce` resolve to `xfce-icons-elementary`, which
|
|
# silently builds an image without the requested desktop session.
|
|
{
|
|
find "${PKG_REPO_DIR}/All" -type f -name "${_pkg_name}-[0-9]*.pkg" 2>/dev/null
|
|
find "${PKG_REPO_DIR}" -maxdepth 2 -type f -name "${_pkg_name}-[0-9]*.pkg" 2>/dev/null
|
|
} | sort -u | tail -1
|
|
}
|
|
|
|
resolve_colibri_paths() {
|
|
_resolved_colibri_repo="${COLIBRI_REPO:-/home/clawdie/ai/colibri}"
|
|
case "${_resolved_colibri_repo}" in
|
|
/*) ;;
|
|
*) _resolved_colibri_repo="${SCRIPT_DIR}/${_resolved_colibri_repo}" ;;
|
|
esac
|
|
if [ -n "${COLIBRI_ARTIFACT_DIR:-}" ]; then
|
|
_resolved_colibri_artifact_dir="${COLIBRI_ARTIFACT_DIR}"
|
|
case "${_resolved_colibri_artifact_dir}" in
|
|
/*) ;;
|
|
*) _resolved_colibri_artifact_dir="${SCRIPT_DIR}/${_resolved_colibri_artifact_dir}" ;;
|
|
esac
|
|
else
|
|
_resolved_colibri_artifact_dir="${_resolved_colibri_repo}/target/release"
|
|
fi
|
|
}
|
|
|
|
resolve_clawdie_ai_repo() {
|
|
_resolved_clawdie_ai_repo="${CLAWDIE_AI_REPO:-/home/clawdie/ai/clawdie-ai}"
|
|
case "${_resolved_clawdie_ai_repo}" in
|
|
/*) ;;
|
|
*) _resolved_clawdie_ai_repo="${SCRIPT_DIR}/${_resolved_clawdie_ai_repo}" ;;
|
|
esac
|
|
}
|
|
|
|
preflight_colibri_artifacts() {
|
|
[ "${FEATURE_COLIBRI:-NO}" = "YES" ] || return 0
|
|
[ "${FETCH_ONLY:-0}" -eq 0 ] || return 0
|
|
|
|
resolve_colibri_paths
|
|
_colibri_rc="${_resolved_colibri_repo}/packaging/freebsd/colibri_daemon.in"
|
|
_colibri_newsyslog="${_resolved_colibri_repo}/packaging/freebsd/newsyslog-colibri.conf"
|
|
if [ ! -f "${_colibri_rc}" ]; then
|
|
echo "ERROR: Colibri rc.d source missing: ${_colibri_rc}"
|
|
echo " Set COLIBRI_REPO=/path/to/colibri or FEATURE_COLIBRI=NO."
|
|
exit 1
|
|
fi
|
|
if ! grep -q '^command="/usr/sbin/daemon"' "${_colibri_rc}"; then
|
|
echo "ERROR: Colibri rc.d source does not supervise with daemon(8): ${_colibri_rc}"
|
|
echo " Update the Colibri checkout before building; a foreground daemon blocks live boot."
|
|
exit 1
|
|
fi
|
|
if [ ! -f "${_colibri_newsyslog}" ]; then
|
|
echo "ERROR: Colibri newsyslog source missing: ${_colibri_newsyslog}"
|
|
echo " Set COLIBRI_REPO=/path/to/colibri or FEATURE_COLIBRI=NO."
|
|
exit 1
|
|
fi
|
|
for _colibri_bin in colibri-daemon colibri colibri-mcp; do
|
|
if [ ! -x "${_resolved_colibri_artifact_dir}/${_colibri_bin}" ]; then
|
|
echo "ERROR: Colibri release binary missing: ${_resolved_colibri_artifact_dir}/${_colibri_bin}"
|
|
command -v cargo >/dev/null 2>&1 || \
|
|
echo " NOTE: rust toolchain not found on this host — install it: pkg install rust"
|
|
echo " Build first: (cd ${_resolved_colibri_repo} && cargo build --workspace --release)"
|
|
echo " Or set FEATURE_COLIBRI=NO to skip Colibri staging."
|
|
exit 1
|
|
fi
|
|
done
|
|
if [ "${COLIBRI_STAGE_TEST_AGENT:-NO}" = "YES" ] && \
|
|
[ ! -x "${_resolved_colibri_artifact_dir}/colibri-test-agent" ]; then
|
|
echo "ERROR: Colibri test-agent staging requested, but binary is missing: ${_resolved_colibri_artifact_dir}/colibri-test-agent"
|
|
echo " Set COLIBRI_STAGE_TEST_AGENT=NO for production images, or rebuild Colibri with cargo build --workspace --release."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
resolve_zot_paths() {
|
|
_resolved_zot_repo="${ZOT_REPO:-${SCRIPT_DIR}/../zot}"
|
|
case "${_resolved_zot_repo}" in
|
|
/*) ;;
|
|
*) _resolved_zot_repo="${SCRIPT_DIR}/${_resolved_zot_repo}" ;;
|
|
esac
|
|
if [ -n "${ZOT_ARTIFACT_DIR:-}" ]; then
|
|
_resolved_zot_artifact_dir="${ZOT_ARTIFACT_DIR}"
|
|
case "${_resolved_zot_artifact_dir}" in
|
|
/*) ;;
|
|
*) _resolved_zot_artifact_dir="${SCRIPT_DIR}/${_resolved_zot_artifact_dir}" ;;
|
|
esac
|
|
else
|
|
_resolved_zot_artifact_dir="${_resolved_zot_repo}/bin"
|
|
fi
|
|
}
|
|
|
|
preflight_zot_artifacts() {
|
|
[ "${FEATURE_COLIBRI:-NO}" = "YES" ] || return 0
|
|
[ "${COLIBRI_STAGE_AGENT:-YES}" = "YES" ] || return 0
|
|
[ "${FETCH_ONLY:-0}" -eq 0 ] || return 0
|
|
|
|
resolve_zot_paths
|
|
if [ ! -x "${_resolved_zot_artifact_dir}/zot" ]; then
|
|
echo "ERROR: Colibri agent binary missing: ${_resolved_zot_artifact_dir}/zot"
|
|
command -v go >/dev/null 2>&1 || \
|
|
echo " NOTE: go toolchain not found on this host — install it: pkg install go"
|
|
echo " The agent has no FreeBSD release — build it first:"
|
|
echo " (cd ${_resolved_zot_repo} && git checkout ${ZOT_VERSION:-v0.2.42} && \\"
|
|
echo " ZOT_BUILD_VERSION=\"\${ZOT_VERSION:-v0.2.42}\" && VERSION=\"\${ZOT_BUILD_VERSION#v}\" make build)"
|
|
echo " Or set COLIBRI_STAGE_AGENT=NO to skip agent staging."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
override_networkmgr_package() {
|
|
_networkmgr_pkg=$(find "${PKG_REPO_DIR}/All/Hashed" -maxdepth 1 -type f -name 'networkmgr-*.pkg' | sort | tail -1)
|
|
if [ -z "${_networkmgr_pkg:-}" ]; then
|
|
echo "ERROR: networkmgr package not found under ${PKG_REPO_DIR}/All/Hashed"
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> [2c/7] Repacking NetworkMgr for mac_do/mdo policy..."
|
|
_networkmgr_stage=$(mktemp -d "${TMP_DIR}/networkmgr-override.XXXXXX")
|
|
_networkmgr_log="${TMP_DIR}/networkmgr-repack.log"
|
|
if ! node "${SCRIPT_DIR}/scripts/repack-networkmgr-for-mdo.mjs" "${_networkmgr_pkg}" "${_networkmgr_stage}" >"${_networkmgr_log}" 2>&1; then
|
|
cat "${_networkmgr_log}"
|
|
rm -rf "${_networkmgr_stage}"
|
|
echo "ERROR: failed to rebuild local networkmgr package"
|
|
exit 1
|
|
fi
|
|
|
|
rm -f "${PKG_REPO_DIR}/All/Hashed"/networkmgr-*.pkg
|
|
mv "${_networkmgr_stage}"/networkmgr-*.pkg "${PKG_REPO_DIR}/All/Hashed/"
|
|
|
|
# After replacing NetworkMgr's sudo dependency with the local mac_do/mdo
|
|
# package, remove any fetched sudo archive from the offline repo that will
|
|
# be copied onto the USB image.
|
|
find "${PKG_REPO_DIR}/All" -type f -name 'sudo-*.pkg' -delete
|
|
|
|
rm -rf "${_networkmgr_stage}"
|
|
}
|
|
|
|
json_escape() {
|
|
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
}
|
|
|
|
resolve_clawdie_commit() {
|
|
_ref="$1"
|
|
_repo="https://code.smilepowered.org/clawdie/clawdie-ai.git"
|
|
if printf '%s' "$_ref" | grep -Eq '^[0-9a-fA-F]{40}$'; then
|
|
printf '%s\n' "$_ref"
|
|
return 0
|
|
fi
|
|
if command -v git >/dev/null 2>&1; then
|
|
git ls-remote "$_repo" \
|
|
"refs/heads/${_ref}" \
|
|
"refs/tags/${_ref}^{}" \
|
|
"refs/tags/${_ref}" 2>/dev/null \
|
|
| awk '
|
|
$2 ~ /\^\{\}$/ { print $1; found = 1; exit }
|
|
first == "" { first = $1 }
|
|
END { if (!found && first != "") print first }
|
|
'
|
|
fi
|
|
}
|
|
|
|
resolve_latest_clawdie_tag() {
|
|
_repo_api="https://code.smilepowered.org/api/v1/repos/clawdie/clawdie-ai"
|
|
_repo_git="https://code.smilepowered.org/clawdie/clawdie-ai.git"
|
|
|
|
_tag=$(
|
|
curl -fsS "${_repo_api}/releases?limit=20" 2>/dev/null \
|
|
| grep -o '"tag_name":"[^"]*"' \
|
|
| head -1 \
|
|
| cut -d'"' -f4
|
|
)
|
|
if [ -n "$_tag" ]; then
|
|
printf '%s\n' "$_tag"
|
|
return 0
|
|
fi
|
|
|
|
git ls-remote --tags "$_repo_git" 2>/dev/null \
|
|
| awk -F/ '$2 == "tags" && $3 ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ { print $3 }' \
|
|
| sed 's/^v//' \
|
|
| sort -t. -k1,1n -k2,2n -k3,3n \
|
|
| tail -n 1 \
|
|
| sed 's/^/v/'
|
|
}
|
|
|
|
is_pinned_clawdie_ref() {
|
|
_ref="$1"
|
|
printf '%s' "$_ref" | grep -Eq '^[0-9a-fA-F]{40}$|^v[0-9]+\.[0-9]+\.[0-9]+$'
|
|
}
|
|
|
|
# Assert a repo has no uncommitted *or untracked* changes. `git status --porcelain`
|
|
# is used (not `git diff`) so stray untracked files — which would change the build
|
|
# yet pass a diff-only check — also fail the gate. Increments _release_errors on a
|
|
# modified/untracked tree; a non-git path is skipped (nothing to assert).
|
|
assert_clean_repo() {
|
|
_acr_name="$1"
|
|
_acr_path="$2"
|
|
command -v git >/dev/null 2>&1 || return 0
|
|
git -C "${_acr_path}" rev-parse --git-dir >/dev/null 2>&1 || return 0
|
|
if [ -n "$(git -C "${_acr_path}" status --porcelain 2>/dev/null)" ]; then
|
|
echo "ERROR: release builds require a clean ${_acr_name} repo (no uncommitted or untracked files)"
|
|
echo " Modified tree at: ${_acr_path}"
|
|
_release_errors=$((_release_errors + 1))
|
|
fi
|
|
}
|
|
|
|
# Release builds must be reproducible from the recorded provenance. We do not
|
|
# require components to be on a named tag — a recorded commit is just as pinned —
|
|
# but every staged source must be a clean, committed tree so the manifest's
|
|
# commits fully describe the artifact. (build-manifest.json records each commit.)
|
|
check_release_gate() {
|
|
_release_errors=0
|
|
|
|
assert_clean_repo "clawdie-iso" "${SCRIPT_DIR}"
|
|
|
|
resolve_clawdie_ai_repo
|
|
assert_clean_repo "clawdie-ai" "${_resolved_clawdie_ai_repo}"
|
|
|
|
if [ "${FEATURE_COLIBRI:-NO}" = "YES" ]; then
|
|
resolve_colibri_paths
|
|
assert_clean_repo "colibri" "${_resolved_colibri_repo}"
|
|
|
|
if [ "${COLIBRI_STAGE_AGENT:-YES}" = "YES" ]; then
|
|
resolve_zot_paths
|
|
assert_clean_repo "zot" "${_resolved_zot_repo}"
|
|
fi
|
|
fi
|
|
|
|
# Guard against a re-introduced duplicate port. The canonical Colibri FreeBSD
|
|
# port lives in the colibri repo (packaging/freebsd/port/sysutils/colibri) and
|
|
# is consumed from there; a copy in this repo would silently drift from
|
|
# Colibri's Cargo.lock / binaries / license.
|
|
if [ -e "${SCRIPT_DIR}/ports/sysutils/colibri" ]; then
|
|
echo "ERROR: duplicate Colibri port at ports/sysutils/colibri — the canonical port lives in the colibri repo (packaging/freebsd/port/sysutils/colibri); remove this copy."
|
|
_release_errors=$(( _release_errors + 1 ))
|
|
fi
|
|
|
|
if [ "${_release_errors}" -gt 0 ]; then
|
|
echo "ERROR: release build aborted — ${_release_errors} modified repo(s). Use BUILD_CHANNEL=dev for iteration builds."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
write_build_manifest() {
|
|
_manifest_path="$1"
|
|
_iso_repo_commit="unknown"
|
|
_iso_repo_modified="null"
|
|
_live_ssh_pubkey_fp_json="null"
|
|
_tailscale_auth_key_baked="${TAILSCALE_AUTH_KEY_BAKED:-false}"
|
|
if command -v git >/dev/null 2>&1 && git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1; then
|
|
_iso_repo_commit=$(git -C "$SCRIPT_DIR" rev-parse HEAD 2>/dev/null || echo unknown)
|
|
if [ -z "$(git -C "$SCRIPT_DIR" status --porcelain 2>/dev/null)" ]; then
|
|
_iso_repo_modified="false"
|
|
else
|
|
_iso_repo_modified="true"
|
|
fi
|
|
fi
|
|
# Clawdie-AI provenance: the image stages a git checkout of the AI source,
|
|
# so record whether the tree is modified at build time.
|
|
_clawdie_ai_modified="null"
|
|
if command -v git >/dev/null 2>&1; then
|
|
resolve_clawdie_ai_repo
|
|
if git -C "${_resolved_clawdie_ai_repo}" rev-parse --git-dir >/dev/null 2>&1; then
|
|
if [ -z "$(git -C "${_resolved_clawdie_ai_repo}" status --porcelain 2>/dev/null)" ]; then
|
|
_clawdie_ai_modified="false"
|
|
else
|
|
_clawdie_ai_modified="true"
|
|
fi
|
|
fi
|
|
fi
|
|
if [ -n "${LIVE_SSH_PUBKEY_FP:-}" ]; then
|
|
_live_ssh_pubkey_fp_json="\"$(json_escape "${LIVE_SSH_PUBKEY_FP}")\""
|
|
fi
|
|
# Colibri provenance: the image stages adjacent colibri binaries, so record
|
|
# which commit produced them (the biggest gap for a reproducible release).
|
|
_colibri_commit="unknown"
|
|
_colibri_modified="null"
|
|
if [ "${FEATURE_COLIBRI:-NO}" = "YES" ]; then
|
|
resolve_colibri_paths
|
|
if git -C "${_resolved_colibri_repo}" rev-parse --git-dir >/dev/null 2>&1; then
|
|
_colibri_commit=$(git -C "${_resolved_colibri_repo}" rev-parse HEAD 2>/dev/null || echo unknown)
|
|
if [ -z "$(git -C "${_resolved_colibri_repo}" status --porcelain 2>/dev/null)" ]; then
|
|
_colibri_modified="false"
|
|
else
|
|
_colibri_modified="true"
|
|
fi
|
|
fi
|
|
fi
|
|
# Zot provenance (agent binary built from source — record if repo is modified).
|
|
_zot_modified="null"
|
|
if [ "${FEATURE_COLIBRI:-NO}" = "YES" ] && [ "${COLIBRI_STAGE_AGENT:-YES}" = "YES" ]; then
|
|
resolve_zot_paths
|
|
if command -v git >/dev/null 2>&1 && git -C "${_resolved_zot_repo}" rev-parse --git-dir >/dev/null 2>&1; then
|
|
if [ -z "$(git -C "${_resolved_zot_repo}" status --porcelain 2>/dev/null)" ]; then
|
|
_zot_modified="false"
|
|
else
|
|
_zot_modified="true"
|
|
fi
|
|
fi
|
|
fi
|
|
mkdir -p "$(dirname "$_manifest_path")"
|
|
cat > "$_manifest_path" <<EOF
|
|
{
|
|
"iso_version": "$(json_escape "${ISO_VERSION}")",
|
|
"version_scheme": "product",
|
|
"zot_version": "$(json_escape "${ZOT_RESOLVED_VERSION:-${ZOT_VERSION}}")",
|
|
"zot_commit": "$(json_escape "${ZOT_RESOLVED_COMMIT:-unknown}")",
|
|
"zot_modified": ${_zot_modified:-null},
|
|
"colibri_commit": "$(json_escape "${_colibri_commit:-unknown}")",
|
|
"colibri_modified": ${_colibri_modified:-null},
|
|
"build_channel": "$(json_escape "${BUILD_CHANNEL}")",
|
|
"freebsd_version": "$(json_escape "${FREEBSD_VERSION}")",
|
|
"freebsd_arch": "$(json_escape "${FREEBSD_ARCH}")",
|
|
"clawdie_ai_ref": "$(json_escape "${CLAWDIE_REF}")",
|
|
"clawdie_ai_commit": "$(json_escape "${CLAWDIE_AI_COMMIT:-unknown}")",
|
|
"clawdie_ai_modified": ${_clawdie_ai_modified:-null},
|
|
"live_ssh_pubkey_fp": ${_live_ssh_pubkey_fp_json},
|
|
"tailscale_auth_key_baked": ${_tailscale_auth_key_baked},
|
|
"iso_repo_commit": "$(json_escape "${_iso_repo_commit}")",
|
|
"iso_repo_modified": ${_iso_repo_modified},
|
|
"built_at": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
mount_memstick_rootfs() {
|
|
_memstick_mount="$1"
|
|
_memstick_slice_img="${CACHE_DIR}/memstick-freebsd-slice.img"
|
|
_memstick_ufs_img="${CACHE_DIR}/memstick-rootfs.img"
|
|
|
|
mkdir -p "$_memstick_mount"
|
|
rm -f "$_memstick_slice_img" "$_memstick_ufs_img"
|
|
|
|
MD_SRC=$(mdconfig -a -t vnode -f "$MEMSTICK")
|
|
|
|
_slice_meta=$(
|
|
gpart show -p "/dev/${MD_SRC}" \
|
|
| awk '$4 == "freebsd" { print $1 " " $2; exit }'
|
|
)
|
|
_slice_start=$(echo "$_slice_meta" | awk '{print $1}')
|
|
_slice_size=$(echo "$_slice_meta" | awk '{print $2}')
|
|
if [ -z "${_slice_start:-}" ] || [ -z "${_slice_size:-}" ]; then
|
|
echo "ERROR: could not determine FreeBSD slice start in ${MEMSTICK}"
|
|
mdconfig -d -u "${MD_SRC}" 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
|
|
truncate -s "$((_slice_size * 512))" "$_memstick_slice_img"
|
|
dd if="$MEMSTICK" of="$_memstick_slice_img" bs=512 skip="$_slice_start" conv=sparse status=none
|
|
|
|
MD_SLICE=$(mdconfig -a -t vnode -f "$_memstick_slice_img")
|
|
_ufs_meta=$(
|
|
bsdlabel "/dev/${MD_SLICE}" 2>/dev/null \
|
|
| awk '$1 == "a:" { print $2 " " $3; exit }'
|
|
)
|
|
_ufs_size=$(echo "$_ufs_meta" | awk '{print $1}')
|
|
_ufs_offset=$(echo "$_ufs_meta" | awk '{print $2}')
|
|
if [ -z "${_ufs_size:-}" ] || [ -z "${_ufs_offset:-}" ]; then
|
|
echo "ERROR: could not determine UFS root partition offset in ${MEMSTICK}"
|
|
mdconfig -d -u "${MD_SLICE}" 2>/dev/null || true
|
|
mdconfig -d -u "${MD_SRC}" 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
|
|
truncate -s "$((_ufs_size * 512))" "$_memstick_ufs_img"
|
|
dd if="$_memstick_slice_img" of="$_memstick_ufs_img" bs=512 skip="$_ufs_offset" conv=sparse status=none
|
|
|
|
MD_ROOTFS=$(mdconfig -a -t vnode -f "$_memstick_ufs_img")
|
|
mount -r -t ufs "/dev/${MD_ROOTFS}" "$_memstick_mount"
|
|
}
|
|
|
|
cleanup_memstick_rootfs() {
|
|
_memstick_mount="$1"
|
|
|
|
umount "$_memstick_mount" 2>/dev/null || true
|
|
[ -n "${MD_ROOTFS:-}" ] && mdconfig -d -u "${MD_ROOTFS}" 2>/dev/null || true
|
|
[ -n "${MD_SLICE:-}" ] && mdconfig -d -u "${MD_SLICE}" 2>/dev/null || true
|
|
[ -n "${MD_SRC:-}" ] && mdconfig -d -u "${MD_SRC}" 2>/dev/null || true
|
|
rm -f "${CACHE_DIR}/memstick-freebsd-slice.img" "${CACHE_DIR}/memstick-rootfs.img"
|
|
MD_ROOTFS=""
|
|
MD_SLICE=""
|
|
MD_SRC=""
|
|
}
|
|
|
|
verify_memstick_cache() {
|
|
if [ ! -f "$MEMSTICK" ] || [ ! -f "${MEMSTICK}.SHA256" ]; then
|
|
return 1
|
|
fi
|
|
_expected_sha=$(
|
|
awk -v image="$(basename "$MEMSTICK")" '
|
|
$2 == "(" image ")" { print $4; exit }
|
|
' "${MEMSTICK}.SHA256"
|
|
)
|
|
[ -n "${_expected_sha:-}" ] || return 1
|
|
[ "$(/sbin/sha256 -q "$MEMSTICK")" = "$_expected_sha" ]
|
|
}
|
|
|
|
install_image_bootcode() {
|
|
_image_md="$1"
|
|
_image_root="$2"
|
|
_freebsd_slice_index="${3:-}"
|
|
|
|
if [ -z "${_freebsd_slice_index}" ]; then
|
|
if [ -e "/dev/${_image_md}s2" ]; then
|
|
_freebsd_slice_index="2"
|
|
else
|
|
_freebsd_slice_index="1"
|
|
fi
|
|
fi
|
|
|
|
if [ ! -f "${_image_root}/boot/mbr" ] || [ ! -f "${_image_root}/boot/boot" ]; then
|
|
echo "ERROR: missing bootcode files in ${_image_root}/boot"
|
|
exit 1
|
|
fi
|
|
|
|
gpart bootcode -b "${_image_root}/boot/mbr" "/dev/${_image_md}"
|
|
gpart bootcode -b "${_image_root}/boot/boot" "/dev/${_image_md}s${_freebsd_slice_index}"
|
|
}
|
|
|
|
install_image_uefi_bootcode() {
|
|
_image_md="$1"
|
|
_image_root="$2"
|
|
_efi_slice_index="${3:-1}"
|
|
_efi_mount="${CACHE_DIR}/efi-mnt"
|
|
|
|
if [ ! -f "${_image_root}/boot/loader.efi" ]; then
|
|
echo "ERROR: missing UEFI loader at ${_image_root}/boot/loader.efi"
|
|
exit 1
|
|
fi
|
|
|
|
newfs_msdos -L EFISYS "/dev/${_image_md}s${_efi_slice_index}"
|
|
mkdir -p "${_efi_mount}"
|
|
mount_msdosfs "/dev/${_image_md}s${_efi_slice_index}" "${_efi_mount}"
|
|
mkdir -p "${_efi_mount}/EFI/BOOT"
|
|
cp "${_image_root}/boot/loader.efi" "${_efi_mount}/EFI/BOOT/BOOTX64.EFI"
|
|
sync
|
|
umount "${_efi_mount}"
|
|
rmdir "${_efi_mount}" 2>/dev/null || true
|
|
}
|
|
|
|
run_live_chroot() {
|
|
chroot "$MOUNT_POINT" /usr/bin/env \
|
|
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
|
|
LD_LIBRARY_PATH="/usr/local/lib:/usr/local/lib/compat:/lib:/usr/lib" \
|
|
"$@"
|
|
}
|
|
|
|
refresh_live_desktop_caches() {
|
|
echo " Refreshing live desktop schema/icon/mime caches..."
|
|
|
|
if [ -x "${MOUNT_POINT}/usr/local/bin/glib-compile-schemas" ]; then
|
|
run_live_chroot /usr/local/bin/glib-compile-schemas /usr/local/share/glib-2.0/schemas
|
|
fi
|
|
if [ ! -f "${MOUNT_POINT}/usr/local/share/glib-2.0/schemas/gschemas.compiled" ]; then
|
|
echo "ERROR: GLib GSettings schema cache missing from live image"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -x "${MOUNT_POINT}/usr/local/bin/gdk-pixbuf-query-loaders" ]; then
|
|
run_live_chroot /usr/local/bin/gdk-pixbuf-query-loaders --update-cache
|
|
fi
|
|
if [ ! -f "${MOUNT_POINT}/usr/local/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" ]; then
|
|
echo "ERROR: gdk-pixbuf loader cache missing from live image"
|
|
exit 1
|
|
fi
|
|
if ! grep -qi 'png' "${MOUNT_POINT}/usr/local/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"; then
|
|
echo "ERROR: gdk-pixbuf PNG loader missing from live image cache"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -x "${MOUNT_POINT}/usr/local/bin/update-mime-database" ] && [ -d "${MOUNT_POINT}/usr/local/share/mime" ]; then
|
|
run_live_chroot /usr/local/bin/update-mime-database /usr/local/share/mime
|
|
fi
|
|
if [ -x "${MOUNT_POINT}/usr/local/bin/update-desktop-database" ] && [ -d "${MOUNT_POINT}/usr/local/share/applications" ]; then
|
|
run_live_chroot /usr/local/bin/update-desktop-database /usr/local/share/applications
|
|
fi
|
|
if [ -x "${MOUNT_POINT}/usr/local/bin/gtk-update-icon-cache" ]; then
|
|
# Regenerate every installed icon theme cache. FreeBSD packages and
|
|
# Clawdie branding install icons into hicolor and other theme
|
|
# directories; missed caches show up as blank/dot panel icons.
|
|
for _icon_theme_dir in "${MOUNT_POINT}/usr/local/share/icons"/*/; do
|
|
[ -d "$_icon_theme_dir" ] || continue
|
|
_theme_name=$(basename "$_icon_theme_dir")
|
|
run_live_chroot /usr/local/bin/gtk-update-icon-cache -f -t "/usr/local/share/icons/${_theme_name}" || true
|
|
done
|
|
fi
|
|
|
|
if [ -x "${MOUNT_POINT}/usr/local/bin/fc-cache" ] && [ -d "${MOUNT_POINT}/usr/local/share/fonts" ]; then
|
|
run_live_chroot /usr/local/bin/fc-cache -f
|
|
fi
|
|
}
|
|
|
|
ensure_sshd_include_line() {
|
|
_sshd_config="$1"
|
|
_include_line='Include /etc/ssh/sshd_config.d/*.conf'
|
|
|
|
if [ ! -f "$_sshd_config" ]; then
|
|
echo "ERROR: sshd_config missing from live image"
|
|
exit 1
|
|
fi
|
|
|
|
if grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "$_sshd_config"; then
|
|
return 0
|
|
fi
|
|
|
|
_tmp="${_sshd_config}.tmp.$$"
|
|
{
|
|
printf '%s\n' "${_include_line}"
|
|
cat "$_sshd_config"
|
|
} > "$_tmp"
|
|
mv "$_tmp" "$_sshd_config"
|
|
}
|
|
|
|
configure_nsswitch_hosts_line() {
|
|
_file="$1"
|
|
_line='hosts: files mdns_minimal [NOTFOUND=return] dns mdns'
|
|
if [ ! -f "$_file" ]; then
|
|
echo "ERROR: nsswitch.conf missing from live image"
|
|
exit 1
|
|
fi
|
|
set_fstab_line "$_file" '^[[:space:]]*hosts:' "$_line"
|
|
}
|
|
|
|
install_live_runtime_packages() {
|
|
echo " Installing live operator runtime packages into image..."
|
|
|
|
_pkg_files=""
|
|
for _pkg in $(pkg_list_live_installer); do
|
|
_archive=$(pkg_archive_for "$_pkg")
|
|
if [ -z "${_archive:-}" ]; then
|
|
echo "ERROR: missing package archive for ${_pkg}"
|
|
exit 1
|
|
fi
|
|
_pkg_files="${_pkg_files} ${_archive}"
|
|
done
|
|
for _pkg in $(pkg_list_live_nvidia); do
|
|
_archive=$(pkg_archive_for "$_pkg")
|
|
if [ -z "${_archive:-}" ]; then
|
|
echo "ERROR: missing NVIDIA package archive for ${_pkg}"
|
|
exit 1
|
|
fi
|
|
_pkg_files="${_pkg_files} ${_archive}"
|
|
done
|
|
|
|
mkdir -p "${MOUNT_POINT}/dev" "${MOUNT_POINT}/proc"
|
|
_mounted_devfs=0
|
|
_mounted_procfs=0
|
|
|
|
if mount -t devfs devfs "${MOUNT_POINT}/dev"; then
|
|
_mounted_devfs=1
|
|
fi
|
|
if mount -t procfs proc "${MOUNT_POINT}/proc"; then
|
|
_mounted_procfs=1
|
|
fi
|
|
|
|
if ! env ASSUME_ALWAYS_YES=yes HANDLE_RC_SCRIPTS=no \
|
|
/usr/local/sbin/pkg-static -o PKG_TRIGGERS_ENABLE=false -r "$MOUNT_POINT" add -f ${_pkg_files}; then
|
|
[ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
|
|
[ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
|
|
echo "ERROR: failed to install live GUI runtime packages into image"
|
|
exit 1
|
|
fi
|
|
|
|
if ! chroot "${MOUNT_POINT}" /usr/sbin/pw usershow sddm >/dev/null 2>&1; then
|
|
[ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
|
|
[ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
|
|
echo "ERROR: sddm user missing from live image after package install"
|
|
exit 1
|
|
fi
|
|
|
|
# webcamd creates /dev/videoN with mode 0660 root:webcamd. Add clawdie
|
|
# to that group so the operator can open the camera without sudo.
|
|
if chroot "${MOUNT_POINT}" /usr/sbin/pw groupshow webcamd >/dev/null 2>&1; then
|
|
chroot "${MOUNT_POINT}" /usr/sbin/pw groupmod webcamd -m clawdie 2>/dev/null || true
|
|
else
|
|
[ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
|
|
[ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
|
|
echo "ERROR: webcamd group missing from live image after package install"
|
|
exit 1
|
|
fi
|
|
|
|
refresh_live_desktop_caches
|
|
|
|
[ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
|
|
[ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
|
|
}
|
|
|
|
install_colibri_service() {
|
|
[ "${FEATURE_COLIBRI:-NO}" = "YES" ] || {
|
|
echo " Colibri service staging disabled (FEATURE_COLIBRI=${FEATURE_COLIBRI:-NO})"
|
|
return 0
|
|
}
|
|
|
|
echo " Staging Colibri service..."
|
|
|
|
resolve_colibri_paths
|
|
|
|
env \
|
|
COLIBRI_REPO="${_resolved_colibri_repo}" \
|
|
COLIBRI_ARTIFACT_DIR="${_resolved_colibri_artifact_dir}" \
|
|
COLIBRI_STAGE_ENABLE="${COLIBRI_DAEMON_ENABLE:-YES}" \
|
|
COLIBRI_STAGE_TEST_AGENT="${COLIBRI_STAGE_TEST_AGENT:-NO}" \
|
|
COLIBRI_COST_MODE="${COLIBRI_COST_MODE:-smart}" \
|
|
"${SCRIPT_DIR}/scripts/stage-colibri-iso.sh" "${MOUNT_POINT}"
|
|
|
|
if ! /usr/sbin/pw -R "${MOUNT_POINT}" groupshow colibri >/dev/null 2>&1; then
|
|
/usr/sbin/pw -R "${MOUNT_POINT}" groupadd colibri
|
|
fi
|
|
if ! /usr/sbin/pw -R "${MOUNT_POINT}" usershow colibri >/dev/null 2>&1; then
|
|
/usr/sbin/pw -R "${MOUNT_POINT}" useradd colibri \
|
|
-g colibri \
|
|
-d /var/db/colibri \
|
|
-s /usr/sbin/nologin \
|
|
-c "Colibri Daemon"
|
|
fi
|
|
|
|
mkdir -p \
|
|
"${MOUNT_POINT}/var/db/colibri" \
|
|
"${MOUNT_POINT}/var/run/colibri" \
|
|
"${MOUNT_POINT}/var/log/colibri"
|
|
chroot "${MOUNT_POINT}" chown -R colibri:colibri \
|
|
/var/db/colibri \
|
|
/var/run/colibri \
|
|
/var/log/colibri
|
|
# 0750 matches the rc.d prestart (install -d -m 0750); the daemon dirs hold
|
|
# the SQLite DB and logs and should not be world-readable. The operator
|
|
# reaches them via the colibri group, not "other".
|
|
chmod 0750 \
|
|
"${MOUNT_POINT}/var/db/colibri" \
|
|
"${MOUNT_POINT}/var/run/colibri" \
|
|
"${MOUNT_POINT}/var/log/colibri"
|
|
|
|
# Allow operator to use 'colibri' CLI without root
|
|
if /usr/sbin/pw -R "${MOUNT_POINT}" usershow clawdie >/dev/null 2>&1; then
|
|
/usr/sbin/pw -R "${MOUNT_POINT}" groupmod colibri -m clawdie
|
|
fi
|
|
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" "colibri_daemon_enable=\"${COLIBRI_DAEMON_ENABLE:-YES}\""
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_user="colibri"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_group="colibri"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_data_dir="/var/db/colibri"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_run_dir="/var/run/colibri"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_socket="/var/run/colibri/colibri.sock"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_db_path="/var/db/colibri/colibri.sqlite"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_logfile="/var/log/colibri/daemon.log"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_provider_env="/usr/local/etc/colibri/provider.env"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_host="$(/bin/hostname)"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" "colibri_daemon_cost_mode=\"${COLIBRI_COST_MODE:-smart}\""
|
|
|
|
if [ ! -x "${MOUNT_POINT}/usr/local/bin/colibri-daemon" ] || \
|
|
[ ! -x "${MOUNT_POINT}/usr/local/bin/colibri" ] || \
|
|
[ ! -x "${MOUNT_POINT}/usr/local/bin/colibri-mcp" ]; then
|
|
echo "ERROR: Colibri binaries missing from live image"
|
|
exit 1
|
|
fi
|
|
if [ ! -x "${MOUNT_POINT}/usr/local/etc/rc.d/colibri_daemon" ]; then
|
|
echo "ERROR: Colibri rc.d script missing from live image"
|
|
exit 1
|
|
fi
|
|
if ! /usr/sbin/pw -R "${MOUNT_POINT}" usershow colibri >/dev/null 2>&1; then
|
|
echo "ERROR: colibri service user missing from live image"
|
|
exit 1
|
|
fi
|
|
|
|
# Operator helper: one-command live rebuild/redeploy of Colibri from source
|
|
# (automates docs/LIVE-COLIBRI-REBUILD.md). Only staged with the service.
|
|
install -m 0755 "${LIVE_SESSION_DIR}/colibri-live-rebuild" \
|
|
"${MOUNT_POINT}/usr/local/bin/colibri-live-rebuild"
|
|
|
|
# Seed the skills catalog with operator-useful entries. The daemon
|
|
# creates the SQLite DB on first start; pre-populate it so skills
|
|
# are available immediately without a first-boot script.
|
|
_colibri_db="${MOUNT_POINT}/var/db/colibri/colibri.sqlite"
|
|
if command -v sqlite3 >/dev/null 2>&1; then
|
|
sqlite3 "${_colibri_db}" "CREATE TABLE IF NOT EXISTS skills (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
category TEXT,
|
|
created_at TEXT NOT NULL
|
|
);" 2>/dev/null || true
|
|
_now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
sqlite3 "${_colibri_db}" "INSERT OR IGNORE INTO skills (id, name, description, category, created_at) VALUES
|
|
('$(uuidgen || echo 00000000-0000-0000-0000-000000000001)', 'freebsd-live-usb', 'FreeBSD live USB operator workstation procedures', 'freebsd', '${_now}'),
|
|
('$(uuidgen || echo 00000000-0000-0000-0000-000000000002)', 'colibri-test', 'Colibri daemon startup check and validation', 'colibri', '${_now}'),
|
|
('$(uuidgen || echo 00000000-0000-0000-0000-000000000003)', 'iso-build', 'Clawdie ISO build and staging workflow', 'iso', '${_now}'),
|
|
('$(uuidgen || echo 00000000-0000-0000-0000-000000000004)', 'tailscale-join', 'Tailscale mesh join procedure for operator USB', 'networking', '${_now}'),
|
|
('$(uuidgen || echo 00000000-0000-0000-0000-000000000005)', 'disk-deploy', 'Deploy from USB live to permanent disk install. Provisions ZFS pool, installs FreeBSD boot environment, migrates config, and prepares for the future deployed-system clawdie service.', 'clawdie', '${_now}'),
|
|
('$(uuidgen || echo 00000000-0000-0000-0000-000000000006)', 'deployed-clawdie-health', 'Future post-deploy health check for service clawdie once the deployed-system service implementation lands.', 'clawdie', '${_now}');" 2>/dev/null || true
|
|
chroot "${MOUNT_POINT}" chown colibri:colibri /var/db/colibri/colibri.sqlite 2>/dev/null || true
|
|
echo " colibri skills seeded: 6 entries"
|
|
fi
|
|
|
|
# Import clawdie-ai skill definitions into the catalog.
|
|
# Reads .agent/skills/*/SKILL.md and registers name + description.
|
|
resolve_clawdie_ai_repo
|
|
_clawdie_ai_dir="${_resolved_clawdie_ai_repo}"
|
|
if [ -d "${_clawdie_ai_dir}/.agent/skills" ]; then
|
|
"${SCRIPT_DIR}/scripts/import-clawdie-skills.sh" \
|
|
"${_clawdie_ai_dir}" "${MOUNT_POINT}"
|
|
else
|
|
echo " clawdie-ai checkout not found, skipping skill import"
|
|
fi
|
|
}
|
|
|
|
install_zot_agent() {
|
|
[ "${FEATURE_COLIBRI:-NO}" = "YES" ] || {
|
|
echo " Colibri agent staging disabled (FEATURE_COLIBRI=${FEATURE_COLIBRI:-NO})"
|
|
return 0
|
|
}
|
|
[ "${COLIBRI_STAGE_AGENT:-YES}" = "YES" ] || {
|
|
echo " Colibri agent staging disabled (COLIBRI_STAGE_AGENT=${COLIBRI_STAGE_AGENT:-YES})"
|
|
return 0
|
|
}
|
|
|
|
echo " Staging Colibri agent (${ZOT_VERSION:-})..."
|
|
resolve_zot_paths
|
|
|
|
env \
|
|
ZOT_ARTIFACT_DIR="${_resolved_zot_artifact_dir}" \
|
|
ZOT_OPERATOR="clawdie" \
|
|
ZOT_DEEPSEEK_KEY="${ZOT_DEEPSEEK_KEY:-}" \
|
|
"${SCRIPT_DIR}/scripts/stage-zot-iso.sh" "${MOUNT_POINT}"
|
|
|
|
# zot state lives in the operator's home; own it as the operator.
|
|
if /usr/sbin/pw -R "${MOUNT_POINT}" usershow clawdie >/dev/null 2>&1; then
|
|
chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/.local 2>/dev/null || true
|
|
fi
|
|
|
|
if [ ! -x "${MOUNT_POINT}/usr/local/bin/zot" ]; then
|
|
echo "ERROR: zot binary missing from live image"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Stage an on-image NVIDIA pkg repo (all branches) so clawdie_live_gpu can
|
|
# `pkg install` the detected branch at boot (NVIDIA_UNIVERSAL lane).
|
|
#
|
|
# FreeBSD-build-host step (authored on Linux; runs + must be validated on
|
|
# FreeBSD). Verify on the build host: (1) the `pkg fetch -o` layout matches what
|
|
# `pkg repo` expects, (2) the dependency closure is complete for offline boot
|
|
# install, (3) image size headroom, (4) `pkg install -r clawdie-nvidia` resolves
|
|
# from file:// at boot. See doc handoff.
|
|
install_nvidia_universal_repo() {
|
|
[ "${NVIDIA_UNIVERSAL:-NO}" = "YES" ] || {
|
|
echo " NVIDIA universal repo staging disabled (NVIDIA_UNIVERSAL=${NVIDIA_UNIVERSAL:-NO})"
|
|
return 0
|
|
}
|
|
|
|
echo " Staging universal NVIDIA repo (all branches) into image..."
|
|
_nv_repo="${MOUNT_POINT}/usr/local/share/clawdie/nvidia-repo"
|
|
mkdir -p "${_nv_repo}"
|
|
|
|
# Fetch all three branches + deps into the on-image repo dir, then build repo
|
|
# metadata. Uses pkg's own dependency resolution so the closure is complete.
|
|
if ! env ASSUME_ALWAYS_YES=yes pkg fetch -y -o "${_nv_repo}" -d \
|
|
nvidia-driver-390 nvidia-driver-470 nvidia-driver-580 nvidia-settings nvidia-xconfig; then
|
|
echo "ERROR: failed to fetch NVIDIA packages for universal repo"
|
|
exit 1
|
|
fi
|
|
if ! pkg repo "${_nv_repo}"; then
|
|
echo "ERROR: failed to generate NVIDIA universal repo metadata"
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/pkg/repos"
|
|
cat > "${MOUNT_POINT}/usr/local/etc/pkg/repos/clawdie-nvidia.conf" <<'EOF'
|
|
# On-image NVIDIA package repo for the universal GPU lane. Consumed at boot by
|
|
# clawdie_live_gpu (nvidia-auto mode) via: pkg install -r clawdie-nvidia ...
|
|
clawdie-nvidia: {
|
|
url: "file:///usr/local/share/clawdie/nvidia-repo",
|
|
enabled: yes
|
|
}
|
|
EOF
|
|
echo " NVIDIA universal repo staged at /usr/local/share/clawdie/nvidia-repo"
|
|
}
|
|
|
|
install_live_npm_globals() {
|
|
echo " Installing bundled npm globals into live image..."
|
|
|
|
if [ ! -d "$NPM_GLOBALS_DIR" ]; then
|
|
echo " WARN: ${NPM_GLOBALS_DIR} not found — skipping live npm globals"
|
|
return 0
|
|
fi
|
|
|
|
_live_prefix="${MOUNT_POINT}/opt/clawdie/npm-global"
|
|
mkdir -p "${_live_prefix}/bin" "${_live_prefix}/lib" "${MOUNT_POINT}/usr/local/bin"
|
|
|
|
_live_npm_home="${CACHE_DIR}/npm-live-home"
|
|
rm -rf "${_live_npm_home}"
|
|
mkdir -p "${_live_npm_home}"
|
|
|
|
for _tgz in "${NPM_GLOBALS_DIR}"/*.tgz; do
|
|
[ -f "${_tgz}" ] || continue
|
|
# Live bundle policy: ship pi only via npm tarball. codex is covered
|
|
# separately via the FreeBSD pkg in pkg-list-live-operator.txt.
|
|
case "$(basename "${_tgz}")" in
|
|
*claude-code*)
|
|
echo " skip $(basename "${_tgz}") (broken on FreeBSD live image)"
|
|
continue
|
|
;;
|
|
*gemini-cli*)
|
|
echo " skip $(basename "${_tgz}") (not part of current live CLI policy)"
|
|
continue
|
|
;;
|
|
esac
|
|
echo " npm install -g $(basename "${_tgz}")"
|
|
env HOME="${_live_npm_home}" npm_config_prefix="${_live_prefix}" npm install -g --ignore-scripts --no-audit --no-fund "${_tgz}" >/dev/null
|
|
done
|
|
|
|
for _bin in "${_live_prefix}/bin"/*; do
|
|
[ -e "${_bin}" ] || continue
|
|
ln -sf "/opt/clawdie/npm-global/bin/$(basename "${_bin}")" "${MOUNT_POINT}/usr/local/bin/$(basename "${_bin}")"
|
|
done
|
|
|
|
patch_live_pi_footer_hostname "${_live_prefix}"
|
|
|
|
# npm runs as root on the build host and writes everything as
|
|
# root:wheel. configure_live_operator_session() used to do this
|
|
# chown but ran BEFORE install_live_npm_globals (caller order at
|
|
# bottom of build.sh), so it was chowning two empty directories
|
|
# before the package tree was written. Do it here, after the files
|
|
# exist, then validate — fail the build if ownership didn't take
|
|
# so this regression cannot ship silently again.
|
|
if ! chroot "${MOUNT_POINT}" /usr/sbin/pw usershow clawdie >/dev/null 2>&1; then
|
|
echo "ERROR: clawdie user missing — cannot chown npm-global tree"
|
|
exit 1
|
|
fi
|
|
chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /opt/clawdie
|
|
_npm_owner=$(chroot "${MOUNT_POINT}" /usr/bin/stat -f '%Su:%Sg' /opt/clawdie/npm-global)
|
|
if [ "${_npm_owner}" != "clawdie:clawdie" ]; then
|
|
echo "ERROR: /opt/clawdie/npm-global ownership is ${_npm_owner}, expected clawdie:clawdie"
|
|
exit 1
|
|
fi
|
|
|
|
# Fix pi HTTP/2: the bundled http-dispatcher sets allowH2:false to
|
|
# work around a Node 26.0 undici decompression bug. We run Node 24
|
|
# with undici 8.3, where H2 works correctly. Enabling HTTP/2 gives
|
|
# HPACK header compression and multiplexing for API calls.
|
|
_pi_dispatcher="${_live_prefix}/lib/node_modules/@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js"
|
|
if [ -f "${_pi_dispatcher}" ]; then
|
|
sed -i '' 's/allowH2: false/allowH2: true/' "${_pi_dispatcher}"
|
|
echo " pi http-dispatcher: allowH2 → true"
|
|
fi
|
|
}
|
|
|
|
patch_live_pi_footer_hostname() {
|
|
_npm_prefix="$1"
|
|
_patch_script="${SCRIPT_DIR}/firstboot/patch-pi-footer-hostname.js"
|
|
_patched=0
|
|
|
|
if [ ! -f "${_patch_script}" ]; then
|
|
echo "ERROR: Pi footer patch script missing: ${_patch_script}"
|
|
exit 1
|
|
fi
|
|
|
|
for _footer in \
|
|
"${_npm_prefix}/lib/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js" \
|
|
"${_npm_prefix}/lib/node_modules/@mariozechner/pi-coding-agent/dist/modes/interactive/components/footer.js"
|
|
do
|
|
[ -f "${_footer}" ] || continue
|
|
echo " patch Pi footer hostname: ${_footer}"
|
|
node "${_patch_script}" "${_footer}"
|
|
node --check "${_footer}"
|
|
_patched=1
|
|
done
|
|
|
|
if [ "${_patched}" -ne 1 ]; then
|
|
echo "ERROR: Pi footer.js not found under ${_npm_prefix}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
seed_live_ai_source_repo() {
|
|
_repo_src="$1"
|
|
_repo_name="$2"
|
|
_repo_dest="${MOUNT_POINT}/home/clawdie/ai/${_repo_name}"
|
|
|
|
if [ ! -d "${_repo_src}" ]; then
|
|
echo " Skipping AI source seed ${_repo_name}: ${_repo_src} not found"
|
|
return 0
|
|
fi
|
|
if ! command -v git >/dev/null 2>&1 || ! git -C "${_repo_src}" rev-parse --git-dir >/dev/null 2>&1; then
|
|
echo " Skipping AI source seed ${_repo_name}: not a git worktree"
|
|
return 0
|
|
fi
|
|
|
|
_repo_branch=$(git -C "${_repo_src}" symbolic-ref --short -q HEAD 2>/dev/null || echo detached)
|
|
_repo_commit=$(git -C "${_repo_src}" rev-parse HEAD 2>/dev/null || echo unknown)
|
|
_repo_origin=$(git -C "${_repo_src}" remote get-url origin 2>/dev/null || echo unknown)
|
|
_repo_src_real=$(cd "${_repo_src}" && pwd -P)
|
|
|
|
echo " Seeding AI source checkout: ${_repo_name} (${_repo_commit})"
|
|
rm -rf "${_repo_dest}"
|
|
if [ "${_repo_branch}" != "detached" ]; then
|
|
git clone --quiet --depth 1 --branch "${_repo_branch}" "file://${_repo_src_real}" "${_repo_dest}"
|
|
else
|
|
git clone --quiet "file://${_repo_src_real}" "${_repo_dest}"
|
|
git -C "${_repo_dest}" checkout --quiet --detach "${_repo_commit}"
|
|
fi
|
|
git -C "${_repo_dest}" remote set-url origin "${_repo_origin}" 2>/dev/null || true
|
|
printf '%s\n' '.clawdie-source.json' >> "${_repo_dest}/.git/info/exclude"
|
|
|
|
_repo_modified=false
|
|
if [ -n "$(git -C "${_repo_src}" status --porcelain 2>/dev/null)" ]; then
|
|
_repo_modified=true
|
|
fi
|
|
|
|
cat > "${_repo_dest}/.clawdie-source.json" <<EOF
|
|
{
|
|
"name": "$(json_escape "${_repo_name}")",
|
|
"source_path": "$(json_escape "${_repo_src}")",
|
|
"origin": "$(json_escape "${_repo_origin}")",
|
|
"branch": "$(json_escape "${_repo_branch}")",
|
|
"commit": "$(json_escape "${_repo_commit}")",
|
|
"modified_at_build": ${_repo_modified},
|
|
"iso_version": "$(json_escape "${ISO_VERSION}")",
|
|
"build_channel": "$(json_escape "${BUILD_CHANNEL}")",
|
|
"snapshot_kind": "shallow git checkout",
|
|
"snapshot_note": "shallow git checkout of HEAD; uncommitted changes and ignored/private files are not included"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
install_live_ai_source_snapshots() {
|
|
echo " Installing live AI source checkouts..."
|
|
resolve_colibri_paths
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie/ai"
|
|
cat > "${MOUNT_POINT}/home/clawdie/ai/README.txt" <<'EOF'
|
|
Clawdie live AI source checkouts
|
|
|
|
These directories are included so the operator can inspect the shipped source
|
|
beside the running system and fetch follow-up commits from Forgejo when network
|
|
access is available.
|
|
|
|
No API keys, .env files, SSH private keys, build caches, package caches, tmp/
|
|
directories, or uncommitted worktree changes are included. Each checkout keeps a
|
|
.git directory plus a .clawdie-source.json file recording the source remote,
|
|
branch, commit, modified state, ISO version, and build channel at image build time.
|
|
|
|
clawdie-ai (TypeScript) is no longer included — it is being phased out in favor
|
|
of the colibri (Rust) control plane. The `clawdie` name is retained as the brand
|
|
and bare-metal installer identity.
|
|
EOF
|
|
seed_live_ai_source_repo "${SCRIPT_DIR}" "clawdie-iso"
|
|
seed_live_ai_source_repo "${_resolved_colibri_repo}" "colibri"
|
|
# zot source so the live rebuild lane can rebuild the agent (Go) too, not
|
|
# just Colibri (Rust). Skipped automatically if the zot checkout is absent.
|
|
resolve_zot_paths
|
|
seed_live_ai_source_repo "${_resolved_zot_repo}" "zot"
|
|
chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/ai
|
|
}
|
|
|
|
configure_live_operator_session() {
|
|
echo " Configuring live operator session..."
|
|
|
|
mkdir -p "${MOUNT_POINT}/usr/local/bin"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-bootstrap-launch.sh" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-bootstrap-launch.sh"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-noblank-guard.sh" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-noblank-guard.sh"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/hw-report" \
|
|
"${MOUNT_POINT}/usr/local/bin/hw-report"
|
|
# Vaultwarden secret bridge (bw -> .env). Always staged: needs only the
|
|
# bundled `bw` CLI and a 0600 bootstrap drop; absent bootstrap = no-op, so
|
|
# the manual setup wizard stays the floor. See docs/VAULTWARDEN-SETUP.md.
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-vault-fetch" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-vault-fetch"
|
|
|
|
# The stock FreeBSD memstick starts bsdinstall from /etc/rc.local before
|
|
# our graphical live session can own the USB workflow. Preserve a copy for
|
|
# debugging, but disable the automatic text installer.
|
|
if [ -f "${MOUNT_POINT}/etc/rc.local" ]; then
|
|
cp "${MOUNT_POINT}/etc/rc.local" "${MOUNT_POINT}/etc/rc.local.freebsd-installer"
|
|
rm -f "${MOUNT_POINT}/etc/rc.local"
|
|
fi
|
|
|
|
# The installer memstick may leave /etc/resolv.conf as a symlink into
|
|
# /tmp/bsdinstall_etc. The live image mounts /tmp as tmpfs, so that link
|
|
# becomes dangling on boot and DNS fails despite working DHCP/routing.
|
|
# Ship a real file, then let DHCP/NetworkMgr replace it at runtime.
|
|
if [ -L "${MOUNT_POINT}/etc/resolv.conf" ] && \
|
|
readlink "${MOUNT_POINT}/etc/resolv.conf" | grep -q '^/tmp/bsdinstall_etc/'; then
|
|
rm -f "${MOUNT_POINT}/etc/resolv.conf"
|
|
fi
|
|
if [ ! -e "${MOUNT_POINT}/etc/resolv.conf" ]; then
|
|
{
|
|
echo '# Created by the Clawdie live image build.'
|
|
echo '# DHCP/NetworkMgr may replace this file after interfaces come up.'
|
|
} > "${MOUNT_POINT}/etc/resolv.conf"
|
|
chmod 0644 "${MOUNT_POINT}/etc/resolv.conf"
|
|
fi
|
|
|
|
# python3 → 3.11 (FreeBSD's PYTHON_DEFAULT — don't fight it). python312 is
|
|
# also installed and stays available as python3.12 for anything needing newer.
|
|
# FreeBSD ships no bare python3 symlink; scripts use /usr/bin/env python3.
|
|
if [ ! -e "${MOUNT_POINT}/usr/local/bin/python3" ]; then
|
|
if [ -x "${MOUNT_POINT}/usr/local/bin/python3.11" ]; then
|
|
py_bin=python3.11
|
|
else
|
|
py_bin=$(ls "${MOUNT_POINT}/usr/local/bin"/python3.* 2>/dev/null \
|
|
| sed 's@.*/@@' | grep -E '^python3\.[0-9]+$' | sort -V | head -1)
|
|
fi
|
|
if [ -n "${py_bin}" ]; then
|
|
ln -sf "${py_bin}" "${MOUNT_POINT}/usr/local/bin/python3"
|
|
ln -sf "${py_bin}" "${MOUNT_POINT}/usr/local/bin/python"
|
|
fi
|
|
fi
|
|
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/sddm.conf.d"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/sddm.conf" \
|
|
"${MOUNT_POINT}/usr/local/etc/sddm.conf.d/50-clawdie-live.conf"
|
|
mkdir -p "${MOUNT_POINT}/var/lib/sddm" "${MOUNT_POINT}/var/log"
|
|
cat > "${MOUNT_POINT}/var/lib/sddm/state.conf" <<'EOF'
|
|
[Last]
|
|
Session=clawdie-xfce.desktop
|
|
User=clawdie
|
|
EOF
|
|
chmod 0755 "${MOUNT_POINT}/var/lib/sddm"
|
|
chmod 0644 "${MOUNT_POINT}/var/lib/sddm/state.conf"
|
|
if chroot "${MOUNT_POINT}" /usr/sbin/pw usershow sddm >/dev/null 2>&1; then
|
|
chroot "${MOUNT_POINT}" chown -R sddm:sddm /var/lib/sddm
|
|
fi
|
|
mkdir -p "${MOUNT_POINT}/usr/local/share/xsessions"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-xfce.desktop" \
|
|
"${MOUNT_POINT}/usr/local/share/xsessions/clawdie-xfce.desktop"
|
|
|
|
# Strip any Wayland session .desktop files dropped by transitive
|
|
# packages. The operator USB has no working Wayland compositor
|
|
# (XFCE on FreeBSD is X11-only here), so offering a Wayland entry
|
|
# in the SDDM greeter just hands the operator a non-booting session.
|
|
if [ -d "${MOUNT_POINT}/usr/local/share/wayland-sessions" ]; then
|
|
rm -f "${MOUNT_POINT}/usr/local/share/wayland-sessions/"*.desktop
|
|
rmdir "${MOUNT_POINT}/usr/local/share/wayland-sessions" 2>/dev/null || true
|
|
fi
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/xdg/autostart"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/autostart/clawdie-bootstrap.desktop" \
|
|
"${MOUNT_POINT}/usr/local/etc/xdg/autostart/clawdie-bootstrap.desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/autostart/clawdie-noblank-guard.desktop" \
|
|
"${MOUNT_POINT}/usr/local/etc/xdg/autostart/clawdie-noblank-guard.desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/autostart/volumeicon.desktop" \
|
|
"${MOUNT_POINT}/usr/local/etc/xdg/autostart/volumeicon.desktop"
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/xorg.conf.d/30-keyboard.conf" \
|
|
"${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d/30-keyboard.conf"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/xorg.conf.d/40-clawdie-noblank.conf" \
|
|
"${MOUNT_POINT}/usr/local/etc/X11/xorg.conf.d/40-clawdie-noblank.conf"
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/rc.d"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-gpu" \
|
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_gpu"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-wifi" \
|
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_wifi"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-seed" \
|
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_seed"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-resolver" \
|
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_resolver"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-audio" \
|
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_audio"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-power" \
|
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_power"
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/polkit-1/rules.d"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/49-clawdie-power.rules" \
|
|
"${MOUNT_POINT}/usr/local/etc/polkit-1/rules.d/49-clawdie-power.rules"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-xfce-session" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-xfce-session"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-xfce-session-inner" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-xfce-session-inner"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-touchpad-guard" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-live-touchpad-guard"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-startx" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-startx"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-startx" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-gui"
|
|
for _xfce_xdg_dir in \
|
|
"${MOUNT_POINT}/usr/local/etc" \
|
|
"${MOUNT_POINT}/usr/local/etc/xdg" \
|
|
"${MOUNT_POINT}/usr/local/etc/xdg/xfce4"; do
|
|
if [ ! -d "$_xfce_xdg_dir" ]; then
|
|
echo "ERROR: XFCE XDG directory missing from live image: ${_xfce_xdg_dir#${MOUNT_POINT}}"
|
|
exit 1
|
|
fi
|
|
chmod 0755 "$_xfce_xdg_dir"
|
|
done
|
|
if [ -f "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xinitrc" ]; then
|
|
chmod 0755 "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xinitrc"
|
|
else
|
|
echo "ERROR: XFCE xinitrc missing from live image"
|
|
exit 1
|
|
fi
|
|
if [ ! -x "${MOUNT_POINT}/usr/local/bin/xinit" ] || [ ! -x "${MOUNT_POINT}/usr/local/bin/startx" ]; then
|
|
echo "ERROR: xinit/startx missing from live image"
|
|
exit 1
|
|
fi
|
|
if [ ! -x "${MOUNT_POINT}/usr/local/bin/clawdie-startx" ]; then
|
|
echo "ERROR: clawdie-startx rescue launcher missing from live image"
|
|
exit 1
|
|
fi
|
|
if [ ! -x "${MOUNT_POINT}/usr/local/bin/bash" ]; then
|
|
echo "ERROR: bash missing from live image"
|
|
exit 1
|
|
fi
|
|
if [ ! -x "${MOUNT_POINT}/usr/local/bin/zsh" ]; then
|
|
echo "ERROR: zsh missing from live image"
|
|
exit 1
|
|
fi
|
|
if ! grep -qx '/usr/local/bin/bash' "${MOUNT_POINT}/etc/shells" 2>/dev/null; then
|
|
printf '%s\n' '/usr/local/bin/bash' >> "${MOUNT_POINT}/etc/shells"
|
|
fi
|
|
if ! grep -qx '/usr/local/bin/zsh' "${MOUNT_POINT}/etc/shells" 2>/dev/null; then
|
|
printf '%s\n' '/usr/local/bin/zsh' >> "${MOUNT_POINT}/etc/shells"
|
|
fi
|
|
|
|
if ! /usr/sbin/pw -R "$MOUNT_POINT" usershow clawdie >/dev/null 2>&1; then
|
|
/usr/sbin/pw -R "$MOUNT_POINT" useradd clawdie \
|
|
-m \
|
|
-s /usr/local/bin/bash \
|
|
-c "Clawdie Operator"
|
|
fi
|
|
/usr/sbin/pw -R "$MOUNT_POINT" usermod clawdie -s /usr/local/bin/bash
|
|
/usr/sbin/pw -R "$MOUNT_POINT" groupmod wheel -m clawdie
|
|
if /usr/sbin/pw -R "$MOUNT_POINT" groupshow video >/dev/null 2>&1; then
|
|
/usr/sbin/pw -R "$MOUNT_POINT" groupmod video -m clawdie
|
|
fi
|
|
CLAWDIE_UID="$(/usr/sbin/pw -R "$MOUNT_POINT" usershow clawdie | cut -d: -f3)"
|
|
# NOTE: chown of /opt/clawdie/npm-global was moved into
|
|
# install_live_npm_globals (called later in the build). The previous
|
|
# chown here ran before npm globals were installed and silently
|
|
# chowned two empty directories, leaving the package tree root:wheel
|
|
# at runtime. Do not re-introduce a chown here.
|
|
if [ -n "${CLAWDIE_USER_PASSWORD:-}" ]; then
|
|
printf '%s\n' "${CLAWDIE_USER_PASSWORD}" | /usr/sbin/pw -R "$MOUNT_POINT" usermod clawdie -h 0
|
|
fi
|
|
|
|
# Do not force vendor-specific DRM firmware or KMS drivers through
|
|
# loader.conf. The live image must boot Intel, AMD, VMware, NVIDIA and
|
|
# fallback framebuffer hosts from the same rootfs. GPU KMS is selected at
|
|
# boot by /usr/local/etc/rc.d/clawdie_live_gpu after inspecting display PCI
|
|
# vendor IDs.
|
|
|
|
# Live root is on USB. Avoid USB autosuspend/resume surprises while the
|
|
# root filesystem is attached to the same bus.
|
|
set_config_line "${MOUNT_POINT}/boot/loader.conf" 'hw.usb.no_suspend="1"'
|
|
|
|
# The stock installer memstick brands the loader menu as "Installer".
|
|
# This artifact is an operator live USB, so use generic FreeBSD branding
|
|
# plus an explicit Clawdie live prompt.
|
|
set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_brand="orb"'
|
|
set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_menu_title="Clawdie Operator USB"'
|
|
set_config_line "${MOUNT_POINT}/boot/loader.conf" 'loader_menu_multi_user_prompt="Start Clawdie Operator USB"'
|
|
|
|
# Enable FreeBSD mac_do for kernel-enforced privilege escalation.
|
|
# Base system only — no sudo package needed. Wheel group members can
|
|
# become root via mdo(1). mdo -u root changes the primary gid to wheel,
|
|
# so the target rule must explicitly allow gid=0 as well as uid=0.
|
|
set_config_line "${MOUNT_POINT}/boot/loader.conf" 'mac_do_load="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/sysctl.conf" 'security.mac.do.rules=gid=0>uid=0,gid=0,+gid=*'
|
|
|
|
if ! grep -q '^\[clawdie_live=' "${MOUNT_POINT}/etc/devfs.rules" 2>/dev/null; then
|
|
cat >> "${MOUNT_POINT}/etc/devfs.rules" <<'EOF'
|
|
|
|
[clawdie_live=10]
|
|
add path 'dri' mode 0755 group video
|
|
add path 'dri/*' mode 0660 group video
|
|
add path 'drm/*' mode 0660 group video
|
|
EOF
|
|
fi
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'devfs_system_ruleset="clawdie_live"'
|
|
|
|
_nm_autostart="${MOUNT_POINT}/usr/local/etc/xdg/autostart/networkmgr.desktop"
|
|
if [ ! -f "$_nm_autostart" ]; then
|
|
echo "ERROR: networkmgr.desktop missing from live image"
|
|
exit 1
|
|
fi
|
|
if /usr/local/sbin/pkg -r "$MOUNT_POINT" info -e sudo 2>/dev/null; then
|
|
echo "ERROR: sudo is installed in the live image"
|
|
exit 1
|
|
fi
|
|
if [ -e "${MOUNT_POINT}/usr/local/etc/sudoers.d/networkmgr" ]; then
|
|
echo "ERROR: NetworkMgr sudoers policy is still present in the live image"
|
|
exit 1
|
|
fi
|
|
if ! grep -q 'Exec=mdo -u root networkmgr' "$_nm_autostart"; then
|
|
echo "ERROR: networkmgr.desktop does not use mdo -u root networkmgr"
|
|
exit 1
|
|
fi
|
|
if /usr/local/sbin/pkg -r "$MOUNT_POINT" info -d networkmgr 2>/dev/null | grep -q '^.*sudo-'; then
|
|
echo "ERROR: networkmgr still depends on sudo in the live image"
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie" "${MOUNT_POINT}/etc/profile.d" "${MOUNT_POINT}/etc/skel"
|
|
|
|
# Install the canonical npm-global profile snippet (shared with agent-jail-bootstrap.sh).
|
|
# The snippet lives in colibri; resolve its repo the same way every other
|
|
# colibri consumer here does so COLIBRI_REPO overrides are honored.
|
|
resolve_colibri_paths
|
|
_npm_profile_src="${_resolved_colibri_repo}/packaging/freebsd/clawdie-npm-profile.sh"
|
|
if [ ! -f "${_npm_profile_src}" ]; then
|
|
echo "ERROR: shared npm profile snippet missing: ${_npm_profile_src}"
|
|
echo " It ships in colibri packaging/freebsd/. Set COLIBRI_REPO=/path/to/colibri."
|
|
exit 1
|
|
fi
|
|
{
|
|
printf 'NPM_PREFIX="/opt/clawdie/npm-global"\n'
|
|
cat "${_npm_profile_src}"
|
|
} > "${MOUNT_POINT}/etc/profile.d/clawdie-npm.sh"
|
|
chmod 0644 "${MOUNT_POINT}/etc/profile.d/clawdie-npm.sh"
|
|
|
|
# ISO-specific environment (PATH, locale, colibri socket). Sources the
|
|
# shared npm snippet rather than duplicating it.
|
|
cat > "${MOUNT_POINT}/etc/profile.d/clawdie.sh" <<'EOF'
|
|
# Clawdie live operator environment.
|
|
_clawdie_base_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
_clawdie_path_has() {
|
|
case ":${PATH:-}:" in
|
|
*:"$1":*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
export LANG="${LANG:-en_US.UTF-8}"
|
|
export LC_ALL="${LC_ALL:-en_US.UTF-8}"
|
|
if [ -z "${PATH:-}" ]; then
|
|
PATH="${_clawdie_base_path}"
|
|
else
|
|
for _clawdie_dir in /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin; do
|
|
_clawdie_path_has "${_clawdie_dir}" || PATH="${PATH}:${_clawdie_dir}"
|
|
done
|
|
fi
|
|
# npm-global bin + npm config — canonical snippet shared with agent jails
|
|
[ -r /etc/profile.d/clawdie-npm.sh ] && . /etc/profile.d/clawdie-npm.sh
|
|
export PATH
|
|
|
|
# Colibri daemon socket — needed for 'colibri' CLI without --socket
|
|
export COLIBRI_DAEMON_SOCKET="/var/run/colibri/colibri.sock"
|
|
|
|
unset _clawdie_base_path
|
|
unset -f _clawdie_path_has 2>/dev/null || true
|
|
EOF
|
|
chmod 0644 "${MOUNT_POINT}/etc/profile.d/clawdie.sh"
|
|
cat > "${MOUNT_POINT}/home/clawdie/.profile" <<'EOF'
|
|
# Clawdie operator POSIX shell profile.
|
|
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
|
|
EOF
|
|
cat > "${MOUNT_POINT}/home/clawdie/.bash_profile" <<'EOF'
|
|
# Clawdie operator bash login profile.
|
|
[ -r /etc/profile ] && . /etc/profile
|
|
[ -L "${HOME}/.cache" ] && {
|
|
mkdir -p /tmp/clawdie/cache 2>/dev/null || true
|
|
chown "$(id -u):$(id -g)" /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
|
|
chmod 0700 /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
|
|
}
|
|
[ -r "${HOME}/.bashrc" ] && . "${HOME}/.bashrc"
|
|
EOF
|
|
cat > "${MOUNT_POINT}/home/clawdie/.bashrc" <<'EOF'
|
|
# Clawdie operator interactive bash profile.
|
|
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
|
|
if [ -n "${PS1:-}" ]; then
|
|
export HISTFILE="${HISTFILE:-/tmp/clawdie/bash_history}"
|
|
mkdir -p /tmp/clawdie 2>/dev/null || true
|
|
fi
|
|
EOF
|
|
cat > "${MOUNT_POINT}/home/clawdie/.zprofile" <<'EOF'
|
|
# Clawdie operator zsh login profile.
|
|
[ -r /etc/profile ] && . /etc/profile
|
|
[ -L "${HOME}/.cache" ] && {
|
|
mkdir -p /tmp/clawdie/cache 2>/dev/null || true
|
|
chown "$(id -u):$(id -g)" /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
|
|
chmod 0700 /tmp/clawdie /tmp/clawdie/cache 2>/dev/null || true
|
|
}
|
|
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
|
|
[ -r "${HOME}/.zshrc" ] && . "${HOME}/.zshrc"
|
|
EOF
|
|
cat > "${MOUNT_POINT}/home/clawdie/.zshrc" <<'EOF'
|
|
# Clawdie operator interactive zsh profile.
|
|
[ -r /etc/profile.d/clawdie.sh ] && . /etc/profile.d/clawdie.sh
|
|
export HISTFILE="${HISTFILE:-/tmp/clawdie/zsh_history}"
|
|
mkdir -p /tmp/clawdie 2>/dev/null || true
|
|
|
|
# Keep zsh optional, but ready: use packaged oh-my-zsh when present.
|
|
if [ -d /usr/local/share/ohmyzsh ]; then
|
|
export ZSH="/usr/local/share/ohmyzsh"
|
|
elif [ -d /usr/local/share/oh-my-zsh ]; then
|
|
export ZSH="/usr/local/share/oh-my-zsh"
|
|
fi
|
|
if [ -n "${ZSH:-}" ] && [ -r "${ZSH}/oh-my-zsh.sh" ]; then
|
|
ZSH_THEME="${ZSH_THEME:-robbyrussell}"
|
|
plugins=(git)
|
|
source "${ZSH}/oh-my-zsh.sh"
|
|
fi
|
|
EOF
|
|
cat > "${MOUNT_POINT}/home/clawdie/.tmux.conf" <<'EOF'
|
|
# Clawdie operator tmux defaults.
|
|
# Pi uses modified Enter/key chords; tmux must pass CSI-u extended keys or Pi
|
|
# warns that modified Enter keys may not work.
|
|
set -g extended-keys on
|
|
set -g extended-keys-format csi-u
|
|
set -g escape-time 10
|
|
set -g mouse on
|
|
set -g base-index 1
|
|
setw -g pane-base-index 1
|
|
set -g renumber-windows on
|
|
EOF
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc"
|
|
cp "${MOUNT_POINT}/home/clawdie/.tmux.conf" "${MOUNT_POINT}/usr/local/etc/tmux.conf"
|
|
cp "${MOUNT_POINT}/home/clawdie/.profile" "${MOUNT_POINT}/etc/skel/.profile"
|
|
cp "${MOUNT_POINT}/home/clawdie/.bash_profile" "${MOUNT_POINT}/etc/skel/.bash_profile"
|
|
cp "${MOUNT_POINT}/home/clawdie/.bashrc" "${MOUNT_POINT}/etc/skel/.bashrc"
|
|
cp "${MOUNT_POINT}/home/clawdie/.zprofile" "${MOUNT_POINT}/etc/skel/.zprofile"
|
|
cp "${MOUNT_POINT}/home/clawdie/.zshrc" "${MOUNT_POINT}/etc/skel/.zshrc"
|
|
cp "${MOUNT_POINT}/home/clawdie/.tmux.conf" "${MOUNT_POINT}/etc/skel/.tmux.conf"
|
|
chmod 0644 \
|
|
"${MOUNT_POINT}/home/clawdie/.profile" \
|
|
"${MOUNT_POINT}/home/clawdie/.bash_profile" \
|
|
"${MOUNT_POINT}/home/clawdie/.bashrc" \
|
|
"${MOUNT_POINT}/home/clawdie/.zprofile" \
|
|
"${MOUNT_POINT}/home/clawdie/.zshrc" \
|
|
"${MOUNT_POINT}/home/clawdie/.tmux.conf" \
|
|
"${MOUNT_POINT}/usr/local/etc/tmux.conf" \
|
|
"${MOUNT_POINT}/etc/skel/.profile" \
|
|
"${MOUNT_POINT}/etc/skel/.bash_profile" \
|
|
"${MOUNT_POINT}/etc/skel/.bashrc" \
|
|
"${MOUNT_POINT}/etc/skel/.zprofile" \
|
|
"${MOUNT_POINT}/etc/skel/.zshrc" \
|
|
"${MOUNT_POINT}/etc/skel/.tmux.conf"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/xprofile" \
|
|
"${MOUNT_POINT}/home/clawdie/.xprofile"
|
|
cat > "${MOUNT_POINT}/home/clawdie/.xinitrc" <<'EOF'
|
|
#!/bin/sh
|
|
exec /usr/local/bin/clawdie-xfce-session
|
|
EOF
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie/.config/xfce4" "${MOUNT_POINT}/etc/skel/.config/xfce4"
|
|
cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/home/clawdie/.config/xfce4/xinitrc"
|
|
cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/etc/skel/.xinitrc"
|
|
cp "${MOUNT_POINT}/home/clawdie/.xinitrc" "${MOUNT_POINT}/etc/skel/.config/xfce4/xinitrc"
|
|
|
|
# Pre-stage mother connectivity key if present on the build host.
|
|
# Lets the live USB node SSH into the mother server (osa) without
|
|
# manual key exchange. Public key is already in mother authorized_keys.
|
|
_mother_key_src="/home/clawdie/.ssh/osa-mother-2026"
|
|
if [ -f "${_mother_key_src}" ]; then
|
|
[ "${BUILD_CHANNEL}" = "release" ] && { echo "ERROR: refusing to bake mother SSH key into a release image"; exit 1; }
|
|
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie/.ssh"
|
|
cp "${_mother_key_src}" "${MOUNT_POINT}/home/clawdie/.ssh/osa-mother-2026"
|
|
chmod 0600 "${MOUNT_POINT}/home/clawdie/.ssh/osa-mother-2026"
|
|
echo " Staged mother SSH key for USB→mother connectivity."
|
|
fi
|
|
chmod 0755 \
|
|
"${MOUNT_POINT}/home/clawdie/.xinitrc" \
|
|
"${MOUNT_POINT}/home/clawdie/.config/xfce4/xinitrc" \
|
|
"${MOUNT_POINT}/etc/skel/.xinitrc" \
|
|
"${MOUNT_POINT}/etc/skel/.config/xfce4/xinitrc"
|
|
if [ -f "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" ]; then
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie/.config/tumbler" "${MOUNT_POINT}/etc/skel/.config/tumbler"
|
|
install -m 0644 "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" \
|
|
"${MOUNT_POINT}/home/clawdie/.config/tumbler/tumbler.rc"
|
|
install -m 0644 "${MOUNT_POINT}/usr/local/etc/xdg/tumbler/tumbler.rc" \
|
|
"${MOUNT_POINT}/etc/skel/.config/tumbler/tumbler.rc"
|
|
fi
|
|
chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie
|
|
|
|
mkdir -p "${MOUNT_POINT}/usr/local/share/applications"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-bootstrap.desktop" \
|
|
"${MOUNT_POINT}/usr/local/share/applications/Clawdie Start Here.desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/colibri-dashboard.desktop" \
|
|
"${MOUNT_POINT}/usr/local/share/applications/Colibri Dashboard.desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/hw-report.desktop" \
|
|
"${MOUNT_POINT}/usr/local/share/applications/Clawdie Hardware Report.desktop"
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie/Desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-bootstrap.desktop" \
|
|
"${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Start Here.desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/colibri-dashboard.desktop" \
|
|
"${MOUNT_POINT}/home/clawdie/Desktop/Colibri Dashboard.desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/hw-report.desktop" \
|
|
"${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Hardware Report.desktop"
|
|
mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/START-HERE.txt" \
|
|
"${MOUNT_POINT}/usr/local/share/clawdie-iso/START-HERE.txt"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/colibri-panel-indicator.sh" \
|
|
"${MOUNT_POINT}/usr/local/bin/colibri-panel-indicator"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-wallpaper-gen.sh" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-wallpaper-gen"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-join-hive.sh" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-join-hive"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-join-hive.desktop" \
|
|
"${MOUNT_POINT}/usr/local/share/applications/Clawdie Join Hive.desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-join-hive.desktop" \
|
|
"${MOUNT_POINT}/home/clawdie/Desktop/Join Hive.desktop"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-enable-mother.sh" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-enable-mother"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-enable-mother.desktop" \
|
|
"${MOUNT_POINT}/usr/local/share/applications/Clawdie Enable Mother.desktop"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-enable-mother.desktop" \
|
|
"${MOUNT_POINT}/home/clawdie/Desktop/Enable Mother Link.desktop"
|
|
mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/bootstrap"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/bootstrap.html" \
|
|
"${MOUNT_POINT}/usr/local/share/clawdie-iso/bootstrap/index.html"
|
|
mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/seed"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-live-seed.README.txt" \
|
|
"${MOUNT_POINT}/usr/local/share/clawdie-iso/seed/README.txt"
|
|
if [ -d "${LIVE_SESSION_DIR}/mcp-examples" ]; then
|
|
mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/mcp-examples"
|
|
cp -R "${LIVE_SESSION_DIR}/mcp-examples/." \
|
|
"${MOUNT_POINT}/usr/local/share/clawdie-iso/mcp-examples/"
|
|
find "${MOUNT_POINT}/usr/local/share/clawdie-iso/mcp-examples" \
|
|
-type f -exec chmod 0644 {} +
|
|
fi
|
|
chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/Desktop
|
|
chmod 0755 "${MOUNT_POINT}/home/clawdie/Desktop"
|
|
chmod 0644 \
|
|
"${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Start Here.desktop" \
|
|
"${MOUNT_POINT}/home/clawdie/Desktop/Colibri Dashboard.desktop" \
|
|
"${MOUNT_POINT}/home/clawdie/Desktop/Clawdie Hardware Report.desktop"
|
|
|
|
install_live_ai_source_snapshots
|
|
|
|
# The stock FreeBSD install memstick is intentionally read-only. This live
|
|
# operator USB needs a writable root so SDDM, Xorg, NetworkMgr, logs, and
|
|
# operator diagnostics can create runtime state on the flashed stick.
|
|
set_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*/dev/ufs/FreeBSD_Install[[:space:]]+/[[:space:]]+ufs[[:space:]]' '/dev/ufs/FreeBSD_Install / ufs rw,noatime 1 1'
|
|
if ! grep -Eq '^[[:space:]]*/dev/ufs/FreeBSD_Install[[:space:]]+/[[:space:]]+ufs[[:space:]]+([^[:space:]]*,)?rw(,|[[:space:]])' "${MOUNT_POINT}/etc/fstab"; then
|
|
echo "ERROR: live USB root filesystem is not configured read-write in /etc/fstab"
|
|
exit 1
|
|
fi
|
|
ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*proc[[:space:]]+/proc[[:space:]]+procfs[[:space:]]' 'proc /proc procfs rw 0 0'
|
|
ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*tmpfs[[:space:]]+/tmp[[:space:]]+tmpfs[[:space:]]' 'tmpfs /tmp tmpfs rw,mode=1777 0 0'
|
|
ensure_fstab_line "${MOUNT_POINT}/etc/fstab" '^[[:space:]]*tmpfs[[:space:]]+/var/log[[:space:]]+tmpfs[[:space:]]' 'tmpfs /var/log tmpfs rw,mode=755 0 0'
|
|
|
|
# No swap on the live USB. USB write wear, unpredictable behaviour, and
|
|
# it would defeat the tmpfs work above (under memory pressure the kernel
|
|
# would page tmpfs contents to swap, putting writes back on the stick).
|
|
# On an 8 GB RAM baseline, "fail visibly under memory pressure" is the
|
|
# right behaviour, not "silently grind the USB". Strip any inherited
|
|
# swap entries and assert clean.
|
|
sed -i '' '/^[[:space:]]*[^#].*[[:space:]]swap[[:space:]]/d' "${MOUNT_POINT}/etc/fstab"
|
|
if grep -qE '^[[:space:]]*[^#].*[[:space:]]swap[[:space:]]' "${MOUNT_POINT}/etc/fstab"; then
|
|
echo "ERROR: swap entry remains in live image /etc/fstab"
|
|
exit 1
|
|
fi
|
|
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'root_rw_mount="YES"'
|
|
# FreeBSD default dumpdev_enable="AUTO" scans for swap on boot to use as
|
|
# the kernel crash-dump device. With no swap and no desire to crash-dump
|
|
# to the USB anyway, set NO explicitly so the scan never runs.
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'dumpdev_enable="NO"'
|
|
# The stock installer memstick overlays /tmp and /var with tiny tmpfs
|
|
# filesystems. That hides image content such as /var/lib/xkb and leaves
|
|
# too little runtime space for a graphical desktop. The operator USB has a
|
|
# writable root filesystem, so keep /tmp and /var on the root UFS image.
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'tmpmfs="NO"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'varmfs="NO"'
|
|
if ! grep -Eq '^root_rw_mount="YES"' "${MOUNT_POINT}/etc/rc.conf"; then
|
|
echo "ERROR: live USB root_rw_mount is not enabled in /etc/rc.conf"
|
|
exit 1
|
|
fi
|
|
if ! grep -Eq '^tmpmfs="NO"' "${MOUNT_POINT}/etc/rc.conf" || ! grep -Eq '^varmfs="NO"' "${MOUNT_POINT}/etc/rc.conf"; then
|
|
echo "ERROR: live USB tmpmfs/varmfs overlays are not disabled in /etc/rc.conf"
|
|
exit 1
|
|
fi
|
|
# Keep a real hostname so startx/xauth do not derive invalid display names
|
|
# such as bare :0 or /unix:0 on the installer-derived "Amnesiac" profile.
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'hostname="usb.clawdie.home.arpa"'
|
|
if ! grep -Eq '^hostname="usb.clawdie.home.arpa"' "${MOUNT_POINT}/etc/rc.conf"; then
|
|
echo "ERROR: live USB hostname is not configured in /etc/rc.conf"
|
|
exit 1
|
|
fi
|
|
ensure_fstab_line "${MOUNT_POINT}/etc/hosts" '^[[:space:]]*127\.0\.0\.1[[:space:]].*clawdie-live' '127.0.0.1 clawdie-live'
|
|
ensure_fstab_line "${MOUNT_POINT}/etc/hosts" '^[[:space:]]*::1[[:space:]].*clawdie-live' '::1 clawdie-live'
|
|
configure_nsswitch_hosts_line "${MOUNT_POINT}/etc/nsswitch.conf"
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'dbus_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'sshd_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'tailscaled_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'avahi_daemon_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'sddm_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'display_manager="sddm"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="auto"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_nvidia_branch=""'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_wifi_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_seed_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_resolver_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_audio_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_power_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'webcamd_enable="YES"'
|
|
# CPU power management. powerdxx is a drop-in replacement for base
|
|
# powerd with moving-average load sampling and better multi-core
|
|
# behavior (matters on Ryzen 7 5700U-class chips). Do NOT also set
|
|
# powerd_enable — both daemons use the same pidfile and one would
|
|
# fail to start. Base powerd defaults to NO when unset, so leaving
|
|
# it unset is the right way to keep it off.
|
|
# The bigger battery lever is the C-state pair. Use C3 rather than Cmax
|
|
# on the live USB because root is USB-backed; deeper C6/C7-style states
|
|
# can expose USB controller resume bugs on some Ryzen mobile systems.
|
|
# Installed systems can choose Cmax once root is on internal storage.
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'powerdxx_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'powerdxx_flags="-a hiadaptive -b adaptive -n adaptive"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'performance_cx_lowest="C3"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'economy_cx_lowest="C3"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'linux_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'zfs_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'pf_enable="YES"'
|
|
# GPU KMS is selected at boot by /usr/local/etc/rc.d/clawdie_live_gpu so
|
|
# the live USB does not force the Intel driver on non-Intel hardware.
|
|
# kld_list breakdown:
|
|
# linux / linux64 — Linuxulator binary compatibility.
|
|
# zfs — runtime ZFS support (pool import on demand).
|
|
# cuse — userspace character devices; required by webcamd.
|
|
# hidbus / iichid — HID-over-I²C bridge for modern touchpads / keyboards.
|
|
# hms / hmt / hkbd — HID mouse / multitouch / keyboard class drivers.
|
|
# acpi_video — backlight control sysctls (hw.acpi.video.lcd0.brightness).
|
|
append_rc_list_values "${MOUNT_POINT}/etc/rc.conf" kld_list \
|
|
linux linux64 zfs \
|
|
cuse \
|
|
hidbus iichid hms hmt hkbd \
|
|
acpi_video
|
|
case "${GPU_DRIVER:-}" in
|
|
nvidia-390|nvidia-470|nvidia-590)
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="nvidia"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" "clawdie_live_gpu_nvidia_branch=\"${GPU_DRIVER#nvidia-}\""
|
|
append_rc_list_values "${MOUNT_POINT}/etc/rc.conf" kld_list \
|
|
nvidia-modeset nvidia
|
|
;;
|
|
esac
|
|
|
|
# Universal NVIDIA lane: no branch is baked; clawdie_live_gpu installs the
|
|
# matching one from the on-image repo at boot. Overrides the mode set above.
|
|
# kld_list intentionally does NOT preload nvidia (it is not installed yet at
|
|
# first boot); the rc.d service kldloads it after install.
|
|
if [ "${NVIDIA_UNIVERSAL:-NO}" = "YES" ]; then
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_mode="nvidia-auto"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_nvidia_branch=""'
|
|
fi
|
|
|
|
mkdir -p "${MOUNT_POINT}/etc/ssh/sshd_config.d"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/sshd-live.conf" \
|
|
"${MOUNT_POINT}/etc/ssh/sshd_config.d/clawdie-live.conf"
|
|
ensure_sshd_include_line "${MOUNT_POINT}/etc/ssh/sshd_config"
|
|
rm -rf "${MOUNT_POINT}/home/clawdie/.ssh"
|
|
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie/.ssh"
|
|
chmod 0700 "${MOUNT_POINT}/home/clawdie/.ssh"
|
|
printf '%s\n' "${SSH_PUBLIC_KEY}" > "${MOUNT_POINT}/home/clawdie/.ssh/authorized_keys"
|
|
chmod 0600 "${MOUNT_POINT}/home/clawdie/.ssh/authorized_keys"
|
|
chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/.ssh
|
|
fi
|
|
if grep -q '^sshd_enable="YES"' "${MOUNT_POINT}/etc/rc.conf"; then
|
|
_drop_in="${MOUNT_POINT}/etc/ssh/sshd_config.d/clawdie-live.conf"
|
|
if [ ! -s "$_drop_in" ]; then
|
|
echo "ERROR: sshd_enable=YES but ${_drop_in#${MOUNT_POINT}} missing or empty"
|
|
exit 1
|
|
fi
|
|
if ! grep -qE '^PasswordAuthentication[[:space:]]+no' "$_drop_in" \
|
|
|| ! grep -qE '^PermitRootLogin[[:space:]]+no' "$_drop_in" \
|
|
|| ! grep -qE '^KbdInteractiveAuthentication[[:space:]]+no' "$_drop_in"; then
|
|
echo "ERROR: sshd drop-in does not disable password, keyboard-interactive, and root login"
|
|
exit 1
|
|
fi
|
|
if ! grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "${MOUNT_POINT}/etc/ssh/sshd_config"; then
|
|
echo "ERROR: sshd_config does not include /etc/ssh/sshd_config.d/*.conf"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
install -m 0644 "${LIVE_SESSION_DIR}/pf-live.conf" "${MOUNT_POINT}/etc/pf.conf"
|
|
if grep -q '^[^#].*\<log\>' "${MOUNT_POINT}/etc/pf.conf"; then
|
|
echo "ERROR: live PF ruleset unexpectedly enables logging"
|
|
exit 1
|
|
fi
|
|
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-tailscale-up" \
|
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_tailscale_up"
|
|
rm -f "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey"
|
|
if [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
|
|
mkdir -p "${MOUNT_POINT}/var/lib/clawdie-iso"
|
|
printf '%s\n' "${TAILSCALE_AUTHKEY}" > "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey"
|
|
chmod 0600 "${MOUNT_POINT}/var/lib/clawdie-iso/tailscale-authkey"
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_tailscale_up_enable="YES"'
|
|
else
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_tailscale_up_enable="NO"'
|
|
fi
|
|
|
|
# Keep ZFS tools/modules ready on the live USB, but do not let a copied or
|
|
# stale zpool cache auto-import disks from the machine being rescued. The
|
|
# operator can still import pools explicitly with mdo when needed.
|
|
mkdir -p "${MOUNT_POINT}/etc/zfs" "${MOUNT_POINT}/boot/zfs"
|
|
rm -f "${MOUNT_POINT}/etc/zfs/zpool.cache" "${MOUNT_POINT}/boot/zfs/zpool.cache"
|
|
|
|
# Xorg's XKB compiler must be able to chdir into the XKB root and write a
|
|
# compiled keymap cache. The stock memstick/rootless-X path is unforgiving:
|
|
# if /tmp, /var/tmp, /var/lib/xkb, or any XKB parent directory is missing or
|
|
# not traversable, xkbcomp reports a misleading missing keycodes file.
|
|
mkdir -p "${MOUNT_POINT}/tmp" "${MOUNT_POINT}/var/tmp" "${MOUNT_POINT}/var/lib/xkb"
|
|
chmod 1777 "${MOUNT_POINT}/tmp" "${MOUNT_POINT}/var/tmp" "${MOUNT_POINT}/var/lib/xkb"
|
|
mkdir -p "${MOUNT_POINT}/var/run/user" "${MOUNT_POINT}/var/run/user/${CLAWDIE_UID}"
|
|
chroot "${MOUNT_POINT}" chown clawdie:clawdie "/var/run/user/${CLAWDIE_UID}"
|
|
chmod 0755 "${MOUNT_POINT}/var/run/user"
|
|
chmod 0700 "${MOUNT_POINT}/var/run/user/${CLAWDIE_UID}"
|
|
for _xkb_dir in \
|
|
"${MOUNT_POINT}/usr" \
|
|
"${MOUNT_POINT}/usr/local" \
|
|
"${MOUNT_POINT}/usr/local/share" \
|
|
"${MOUNT_POINT}/usr/local/share/X11" \
|
|
"${MOUNT_POINT}/usr/local/share/X11/xkb" \
|
|
"${MOUNT_POINT}/usr/local/share/X11/xkb/keycodes" \
|
|
"${MOUNT_POINT}/usr/local/share/X11/xkb/rules" \
|
|
"${MOUNT_POINT}/usr/local/share/X11/xkb/symbols" \
|
|
"${MOUNT_POINT}/usr/local/share/X11/xkb/types" \
|
|
"${MOUNT_POINT}/usr/local/share/X11/xkb/compat"; do
|
|
if [ ! -d "$_xkb_dir" ]; then
|
|
echo "ERROR: XKB directory missing from live image: ${_xkb_dir#${MOUNT_POINT}}"
|
|
exit 1
|
|
fi
|
|
chmod 0755 "$_xkb_dir"
|
|
done
|
|
if [ ! -f "${MOUNT_POINT}/usr/local/share/X11/xkb/keycodes/xfree86" ]; then
|
|
echo "ERROR: XKB keycodes missing from live image"
|
|
exit 1
|
|
fi
|
|
if [ ! -x "${MOUNT_POINT}/usr/local/bin/xkbcomp" ]; then
|
|
echo "ERROR: xkbcomp missing from live image"
|
|
exit 1
|
|
fi
|
|
_xkb_test="${CACHE_DIR}/xkb-default-test.xkb"
|
|
_xkb_out="${CACHE_DIR}/xkb-default-test.xkm"
|
|
cat > "$_xkb_test" <<'EOF'
|
|
xkb_keymap {
|
|
xkb_keycodes { include "xfree86+aliases(qwerty)" };
|
|
xkb_types { include "complete" };
|
|
xkb_compat { include "complete" };
|
|
xkb_symbols { include "pc+us+inet(evdev)" };
|
|
xkb_geometry { include "pc(pc105)" };
|
|
};
|
|
EOF
|
|
if ! "${MOUNT_POINT}/usr/local/bin/xkbcomp" -w 0 -R"${MOUNT_POINT}/usr/local/share/X11/xkb" "$_xkb_test" "$_xkb_out" >/dev/null 2>&1; then
|
|
echo "ERROR: XKB default keymap does not compile against the live image xkeyboard-config"
|
|
exit 1
|
|
fi
|
|
rm -f "$_xkb_test" "$_xkb_out"
|
|
|
|
_xkb_si_test="${CACHE_DIR}/xkb-si-test.xkb"
|
|
_xkb_si_out="${CACHE_DIR}/xkb-si-test.xkm"
|
|
cat > "$_xkb_si_test" <<'EOF'
|
|
xkb_keymap {
|
|
xkb_keycodes { include "xfree86+aliases(qwerty)" };
|
|
xkb_types { include "complete" };
|
|
xkb_compat { include "complete" };
|
|
xkb_symbols { include "pc+si+inet(evdev)" };
|
|
xkb_geometry { include "pc(pc105)" };
|
|
};
|
|
EOF
|
|
if ! "${MOUNT_POINT}/usr/local/bin/xkbcomp" -w 0 -R"${MOUNT_POINT}/usr/local/share/X11/xkb" "$_xkb_si_test" "$_xkb_si_out" >/dev/null 2>&1; then
|
|
echo "ERROR: Slovenian (si) XKB keymap does not compile — missing xkeyboard-config layout?"
|
|
exit 1
|
|
fi
|
|
rm -f "$_xkb_si_test" "$_xkb_si_out"
|
|
|
|
# Preseed XFCE panel layout into /etc/skel so the clawdie user gets it
|
|
# on first login. pw useradd -m copies /etc/skel into /home/clawdie, but
|
|
# the user was already created above, so copy directly into both locations.
|
|
PANEL_SKEL="${LIVE_SESSION_DIR}/panel-skel"
|
|
if [ -d "$PANEL_SKEL" ]; then
|
|
echo " Seeding XFCE panel layout..."
|
|
mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/panel-skel"
|
|
cp -R "${PANEL_SKEL}/." "${MOUNT_POINT}/usr/local/share/clawdie-iso/panel-skel/"
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml"
|
|
cp "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml" \
|
|
"${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml"
|
|
for _xfconf_xml in xsettings.xml xfwm4.xml xfce4-desktop.xml xfce4-power-manager.xml displays.xml; do
|
|
if [ -f "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}" ]; then
|
|
cp "${PANEL_SKEL}/.config/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}" \
|
|
"${MOUNT_POINT}/usr/local/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/${_xfconf_xml}"
|
|
fi
|
|
done
|
|
# Install into /etc/skel for any future users
|
|
_skel_etc="${MOUNT_POINT}/etc/skel"
|
|
mkdir -p "${_skel_etc}/.config"
|
|
cp -R "${PANEL_SKEL}/.config/." "${_skel_etc}/.config/"
|
|
if [ -d "${PANEL_SKEL}/.local/share/applications" ]; then
|
|
mkdir -p "${_skel_etc}/.local/share/applications"
|
|
cp "${PANEL_SKEL}/.local/share/applications/mimeapps.list" \
|
|
"${_skel_etc}/.local/share/applications/" 2>/dev/null || true
|
|
fi
|
|
# Also copy directly into clawdie's home (user already exists)
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie/.config"
|
|
cp -R "${PANEL_SKEL}/.config/." "${MOUNT_POINT}/home/clawdie/.config/"
|
|
if [ -d "${PANEL_SKEL}/.local/share/applications" ]; then
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie/.local/share/applications"
|
|
cp "${PANEL_SKEL}/.local/share/applications/mimeapps.list" \
|
|
"${MOUNT_POINT}/home/clawdie/.local/share/applications/" 2>/dev/null || true
|
|
fi
|
|
chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/.config
|
|
chroot "$MOUNT_POINT" chown -R clawdie:clawdie /home/clawdie/.local
|
|
chroot "$MOUNT_POINT" chown -R root:wheel /etc/skel
|
|
fi
|
|
|
|
_wallpapers_dir="${LIVE_SESSION_DIR}/wallpapers"
|
|
if [ -d "$_wallpapers_dir" ]; then
|
|
echo " Installing wallpapers..."
|
|
mkdir -p "${MOUNT_POINT}/usr/local/share/clawdie-iso/wallpapers"
|
|
cp "${_wallpapers_dir}"/* "${MOUNT_POINT}/usr/local/share/clawdie-iso/wallpapers/" 2>/dev/null || true
|
|
fi
|
|
|
|
# Brand icons (e.g. the Whisker Start-button triangle). Install both the
|
|
# raw files for direct diagnostics and hicolor theme names for XFCE plugins
|
|
# that do not reliably load absolute icon paths.
|
|
_icons_dir="${LIVE_SESSION_DIR}/icons"
|
|
if [ -d "$_icons_dir" ]; then
|
|
echo " Installing brand icons..."
|
|
mkdir -p \
|
|
"${MOUNT_POINT}/usr/local/share/clawdie-iso/icons" \
|
|
"${MOUNT_POINT}/usr/local/share/icons/hicolor/48x48/apps" \
|
|
"${MOUNT_POINT}/usr/local/share/icons/hicolor/64x64/apps" \
|
|
"${MOUNT_POINT}/usr/local/share/icons/hicolor/scalable/apps"
|
|
cp "${_icons_dir}"/* "${MOUNT_POINT}/usr/local/share/clawdie-iso/icons/" 2>/dev/null || true
|
|
if [ -f "${_icons_dir}/clawdie-start-48.png" ]; then
|
|
install -m 0644 "${_icons_dir}/clawdie-start-48.png" \
|
|
"${MOUNT_POINT}/usr/local/share/icons/hicolor/48x48/apps/clawdie-start.png"
|
|
fi
|
|
if [ -f "${_icons_dir}/clawdie-start.png" ]; then
|
|
install -m 0644 "${_icons_dir}/clawdie-start.png" \
|
|
"${MOUNT_POINT}/usr/local/share/icons/hicolor/64x64/apps/clawdie-start.png"
|
|
fi
|
|
if [ -f "${_icons_dir}/clawdie-start.svg" ]; then
|
|
install -m 0644 "${_icons_dir}/clawdie-start.svg" \
|
|
"${MOUNT_POINT}/usr/local/share/icons/hicolor/scalable/apps/clawdie-start.svg"
|
|
fi
|
|
if [ -x "${MOUNT_POINT}/usr/local/bin/gtk-update-icon-cache" ]; then
|
|
run_live_chroot /usr/local/bin/gtk-update-icon-cache -f -t /usr/local/share/icons/hicolor || true
|
|
fi
|
|
fi
|
|
|
|
rm -rf "${MOUNT_POINT}/home/clawdie/.cache" "${MOUNT_POINT}/etc/skel/.cache"
|
|
ln -s /tmp/clawdie/cache "${MOUNT_POINT}/home/clawdie/.cache"
|
|
ln -s /tmp/clawdie/cache "${MOUNT_POINT}/etc/skel/.cache"
|
|
chroot "${MOUNT_POINT}" chown -h clawdie:clawdie /home/clawdie/.cache
|
|
}
|
|
|
|
if [ "${BUILD_CHANNEL}" = "release" ]; then
|
|
check_release_gate
|
|
fi
|
|
preflight_colibri_artifacts
|
|
preflight_zot_artifacts
|
|
|
|
# --- step 1: fetch FreeBSD memstick ---
|
|
MEMSTICK="${CACHE_DIR}/FreeBSD-${FREEBSD_VERSION}-${FREEBSD_ARCH}-memstick.img"
|
|
if [ "$SKIP_MEMSTICK_FETCH" -eq 1 ]; then
|
|
if [ ! -f "$MEMSTICK" ]; then
|
|
echo "ERROR: cached FreeBSD memstick not found: ${MEMSTICK}"
|
|
echo "Rerun without --skip-memstick-fetch to download it."
|
|
exit 1
|
|
fi
|
|
if ! verify_memstick_cache; then
|
|
echo "ERROR: cached FreeBSD memstick failed checksum verification."
|
|
echo "Remove ${MEMSTICK} and rerun without --skip-memstick-fetch."
|
|
exit 1
|
|
fi
|
|
echo "==> [1/7] FreeBSD memstick cached; fetch skipped."
|
|
elif [ "$SKIP_FETCH" -eq 0 ] || [ ! -f "$MEMSTICK" ]; then
|
|
echo "==> [1/7] Fetching FreeBSD memstick..."
|
|
mkdir -p "$CACHE_DIR"
|
|
curl -L --progress-bar -o "$MEMSTICK" "$FREEBSD_MEMSTICK_URL"
|
|
curl -L -o "${MEMSTICK}.SHA256" "$FREEBSD_MEMSTICK_SHA256_URL"
|
|
verify_memstick_cache || { echo "ERROR: checksum mismatch on memstick"; exit 1; }
|
|
else
|
|
if ! verify_memstick_cache; then
|
|
echo "ERROR: cached FreeBSD memstick failed checksum verification."
|
|
echo "Remove ${MEMSTICK} and rerun without --skip-fetch."
|
|
exit 1
|
|
fi
|
|
echo "==> [1/7] FreeBSD memstick cached."
|
|
fi
|
|
|
|
# --- step 2: fetch all packages (no root needed) ---
|
|
if [ "$SKIP_FETCH" -eq 0 ]; then
|
|
echo "==> [2/7] Fetching packages to tmp/packages/..."
|
|
mkdir -p "$PKG_REPO_DIR"
|
|
|
|
# Set pkg repo to configured branch before fetching
|
|
# Use temporary user-level config to avoid requiring root
|
|
PKG_CONFIG_DIR=$(mktemp -d "${TMP_DIR}/pkg-repo.XXXXXX")
|
|
trap "rm -rf $PKG_CONFIG_DIR" EXIT
|
|
# Avoid calling pkg config (needs privilege) — hardcode ABI and use cache
|
|
ABI="FreeBSD:15:amd64"
|
|
PKG_CACHE_HOME="${HOME}/.pkg-cache"
|
|
mkdir -p "$PKG_CONFIG_DIR" "$PKG_CACHE_HOME"
|
|
cat > "$PKG_CONFIG_DIR/FreeBSD.conf" <<EOF
|
|
FreeBSD: {
|
|
url: "pkg+https://pkg.FreeBSD.org/${ABI}/${DEFAULT_PKG_BRANCH}",
|
|
mirror_type: "srv",
|
|
enabled: yes
|
|
}
|
|
EOF
|
|
|
|
PKGS=$(pkg_list_all)
|
|
PKG_COUNT=$(echo "$PKGS" | wc -l | tr -d ' ')
|
|
echo " Fetching ${PKG_COUNT} packages (with dependencies)..."
|
|
|
|
# pkg fetch downloads .pkg files and all dependencies to tmp/packages/
|
|
# Use PKG_REPOS_DIR and PKG_CACHEDIR to minimize privilege requirements
|
|
export PKG_REPOS_DIR="$PKG_CONFIG_DIR"
|
|
export PKG_CACHEDIR="$PKG_CACHE_HOME"
|
|
|
|
# Try unprivileged fetch first
|
|
_PKG_FETCH_ERROR="${TMP_DIR}/pkg-fetch-error.log"
|
|
if ! echo "$PKGS" | xargs pkg fetch --yes --dependencies --output "$PKG_REPO_DIR" 2>"$_PKG_FETCH_ERROR"; then
|
|
# If privilege error, offer to re-run with sudo
|
|
if grep -q "Insufficient privileges" "$_PKG_FETCH_ERROR" 2>/dev/null; then
|
|
echo " ⚠️ pkg fetch requires elevated privileges."
|
|
echo " Re-running with sudo..."
|
|
sudo sh -c "export PKG_REPOS_DIR='$PKG_CONFIG_DIR' PKG_CACHEDIR='$PKG_CACHE_HOME'; echo '$PKGS' | xargs pkg fetch --yes --dependencies --output '$PKG_REPO_DIR'" || {
|
|
echo " ERROR: sudo pkg fetch failed"
|
|
exit 1
|
|
}
|
|
else
|
|
cat "$_PKG_FETCH_ERROR"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo " Fetch complete."
|
|
|
|
# Fetch pinned npm-global CLI tarballs for the live image bundle.
|
|
echo "==> [2b/7] Fetching npm-global CLI tarballs..."
|
|
OUT_DIR="$NPM_GLOBALS_DIR" "${SCRIPT_DIR}/scripts/fetch-npm-globals.sh" || {
|
|
echo " ERROR: fetch-npm-globals.sh failed"
|
|
exit 1
|
|
}
|
|
else
|
|
echo "==> [2/7] Skipping package fetch."
|
|
fi
|
|
|
|
if [ -d "${PKG_REPO_DIR}/Hashed" ]; then
|
|
mkdir -p "${PKG_REPO_DIR}/All/Hashed"
|
|
find "${PKG_REPO_DIR}/Hashed" -maxdepth 1 -type f -name '*.pkg' -exec mv {} "${PKG_REPO_DIR}/All/Hashed/" \;
|
|
rmdir "${PKG_REPO_DIR}/Hashed" 2>/dev/null || true
|
|
fi
|
|
|
|
if [ -d "${PKG_REPO_DIR}/All/Hashed" ]; then
|
|
override_networkmgr_package
|
|
fi
|
|
|
|
# --- step 3: generate offline pkg repo metadata ---
|
|
echo "==> [3/7] Generating offline pkg repo metadata..."
|
|
if [ -d "$PKG_REPO_DIR/All" ]; then
|
|
# pkg repo may also need privilege — try unprivileged first
|
|
_PKG_REPO_ERROR="${TMP_DIR}/pkg-repo-error.log"
|
|
if ! pkg repo "$PKG_REPO_DIR" 2>"$_PKG_REPO_ERROR"; then
|
|
if grep -q "Insufficient privileges" "$_PKG_REPO_ERROR" 2>/dev/null; then
|
|
echo " ⚠️ pkg repo requires elevated privileges. Re-running with sudo..."
|
|
sudo pkg repo "$PKG_REPO_DIR" || {
|
|
echo " ERROR: sudo pkg repo failed"
|
|
exit 1
|
|
}
|
|
else
|
|
cat "$_PKG_REPO_ERROR"
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo " Repo metadata written to tmp/packages/"
|
|
else
|
|
echo " WARN: tmp/packages/All/ not found — run without --skip-fetch first"
|
|
fi
|
|
|
|
# --- step 4: fetch + prepare Clawdie-AI tarball (offline-ready) ---
|
|
# Resolve "latest" from Forgejo releases first, then tags.
|
|
if [ "${CLAWDIE_REF:-${CLAWDIE_VERSION:-}}" = "latest" ] || [ -z "${CLAWDIE_REF:-}" ]; then
|
|
echo "==> [4/7] Resolving latest Clawdie-AI version..."
|
|
CLAWDIE_VERSION=$(resolve_latest_clawdie_tag | sed 's/^v//')
|
|
if [ -z "$CLAWDIE_VERSION" ]; then
|
|
echo "ERROR: could not resolve latest Clawdie-AI release/tag from Forgejo."
|
|
echo " Pin --clawdie-ref main or --clawdie-version X.Y.Z explicitly."
|
|
exit 1
|
|
fi
|
|
CLAWDIE_REF="v${CLAWDIE_VERSION}"
|
|
echo " Resolved: ${CLAWDIE_REF}"
|
|
fi
|
|
|
|
CLAWDIE_AI_COMMIT=$(resolve_clawdie_commit "$CLAWDIE_REF" | head -n 1)
|
|
CLAWDIE_AI_COMMIT="${CLAWDIE_AI_COMMIT:-unknown}"
|
|
echo " Clawdie commit: ${CLAWDIE_AI_COMMIT}"
|
|
|
|
if [ "$CLAWDIE_AI_COMMIT" = "unknown" ] && [ "$SKIP_FETCH" -eq 1 ] && ! is_pinned_clawdie_ref "$CLAWDIE_REF"; then
|
|
echo "ERROR: cannot safely use --skip-fetch for moving Clawdie-AI ref '${CLAWDIE_REF}' without resolving its commit."
|
|
echo " Run without --skip-fetch or pin --clawdie-version / --clawdie-ref to a commit."
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$CLAWDIE_AI_COMMIT" != "unknown" ]; then
|
|
CLAWDIE_ARCHIVE_REF="$CLAWDIE_AI_COMMIT"
|
|
CLAWDIE_CACHE_KEY="$CLAWDIE_AI_COMMIT"
|
|
else
|
|
CLAWDIE_ARCHIVE_REF="$CLAWDIE_REF"
|
|
CLAWDIE_CACHE_KEY=$(printf '%s' "$CLAWDIE_REF" | tr -c 'A-Za-z0-9._-' '_')
|
|
fi
|
|
|
|
CLAWDIE_TARBALL="${CACHE_DIR}/clawdie-ai-${CLAWDIE_CACHE_KEY}.tar.gz"
|
|
CLAWDIE_TARBALL_URL="https://code.smilepowered.org/clawdie/clawdie-ai/archive/${CLAWDIE_ARCHIVE_REF}.tar.gz"
|
|
if [ "$SKIP_FETCH" -eq 0 ] || [ ! -f "$CLAWDIE_TARBALL" ]; then
|
|
echo "==> [4/7] Fetching Clawdie-AI ${CLAWDIE_REF} (${CLAWDIE_ARCHIVE_REF})..."
|
|
mkdir -p "$CACHE_DIR"
|
|
curl -L --progress-bar -o "$CLAWDIE_TARBALL" "$CLAWDIE_TARBALL_URL"
|
|
else
|
|
echo "==> [4/7] Clawdie-AI ${CLAWDIE_REF} (${CLAWDIE_ARCHIVE_REF}) cached."
|
|
fi
|
|
|
|
# Build an ISO-ready tarball that includes node_modules for offline firstboot.
|
|
CLAWDIE_TARBALL_ISO="${CACHE_DIR}/clawdie-ai-${CLAWDIE_CACHE_KEY}-iso.tar.gz"
|
|
if [ "$SKIP_FETCH" -eq 0 ] || [ ! -f "$CLAWDIE_TARBALL_ISO" ]; then
|
|
echo "==> [4/7] Preparing Clawdie-AI offline tarball (node_modules)..."
|
|
|
|
if ! command -v npm >/dev/null 2>&1; then
|
|
echo "ERROR: npm not found on build host."
|
|
echo "Install Node.js + npm on the ISO build machine, then rerun build.sh."
|
|
echo "Example (FreeBSD): sudo pkg install -y node24 npm-node24"
|
|
exit 1
|
|
fi
|
|
|
|
BUILD_AI_DIR="${CACHE_DIR}/clawdie-ai-build"
|
|
STAGE_AI_DIR="${CACHE_DIR}/clawdie-ai-stage"
|
|
rm -rf "$BUILD_AI_DIR"
|
|
rm -rf "$STAGE_AI_DIR"
|
|
mkdir -p "$BUILD_AI_DIR"
|
|
mkdir -p "$STAGE_AI_DIR"
|
|
|
|
tar -xzf "$CLAWDIE_TARBALL" -C "$BUILD_AI_DIR"
|
|
|
|
AI_SRC_DIR="$(find "$BUILD_AI_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
|
|
if [ -z "${AI_SRC_DIR:-}" ]; then
|
|
echo "ERROR: Could not locate extracted Clawdie-AI directory."
|
|
exit 1
|
|
fi
|
|
|
|
_bundle_script="${CACHE_DIR}/bundle-clawdie-ai.sh"
|
|
cat > "$_bundle_script" <<'EOF'
|
|
#!/bin/sh
|
|
set -eu
|
|
cd "$BUILD_AI_SRC_DIR"
|
|
# Never let package-manager lifecycle hooks run the Clawdie installer on
|
|
# the ISO build host. Older Clawdie-AI refs used an npm "install" script
|
|
# as an operator shortcut; remove only that root lifecycle hook in the
|
|
# staged copy while still allowing dependency install scripts.
|
|
node -e '
|
|
const fs = require("fs");
|
|
const path = "package.json";
|
|
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
if (pkg.scripts?.install) delete pkg.scripts.install;
|
|
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
|
|
'
|
|
if ! npm ci --no-audit --no-fund --legacy-peer-deps; then
|
|
echo " WARN: npm ci failed for Clawdie-AI; falling back to npm install."
|
|
echo " WARN: This usually means the published release tarball is out of sync with package-lock.json."
|
|
npm install --no-audit --no-fund --legacy-peer-deps
|
|
fi
|
|
EOF
|
|
chmod 700 "$_bundle_script"
|
|
if [ -n "${BUILD_HOST_USER}" ]; then
|
|
chown -R "$BUILD_HOST_USER":"$BUILD_HOST_USER" "$BUILD_AI_DIR" "$STAGE_AI_DIR" "$_bundle_script" 2>/dev/null || true
|
|
su -m "$BUILD_HOST_USER" -c "env HOME='${BUILD_HOST_HOME}' PATH='${PATH}' BUILD_AI_SRC_DIR='${AI_SRC_DIR}' sh '${_bundle_script}'"
|
|
else
|
|
env BUILD_AI_SRC_DIR="${AI_SRC_DIR}" sh "$_bundle_script"
|
|
fi
|
|
|
|
mv "$AI_SRC_DIR" "${STAGE_AI_DIR}/Clawdie-AI"
|
|
|
|
tar -czf "$CLAWDIE_TARBALL_ISO" -C "$STAGE_AI_DIR" Clawdie-AI
|
|
else
|
|
echo "==> [4/7] Clawdie-AI offline tarball cached."
|
|
fi
|
|
|
|
# Exit here if --fetch-only (CI package pre-fetch step, no root required)
|
|
if [ "$FETCH_ONLY" -eq 1 ]; then
|
|
echo ""
|
|
echo "==> Fetch complete. Run ./build.sh --skip-fetch to assemble ISO."
|
|
exit 0
|
|
fi
|
|
|
|
# --- step 5: prepare working image (requires root) ---
|
|
echo "==> [5/7] Preparing working image (${IMAGE_SIZE} for offline packages)..."
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "ERROR: steps 5-7 require root (mdconfig/mount)"
|
|
exit 1
|
|
fi
|
|
|
|
WORK_IMG="${CACHE_DIR}/work.img"
|
|
|
|
# Recreate the working image on every assembly. Package/profile changes must not
|
|
# inherit stale packages or desktop files from a previous branch build.
|
|
rm -f "$WORK_IMG"
|
|
|
|
# Create image with configured size for offline packages
|
|
if [ ! -f "$WORK_IMG" ]; then
|
|
echo " Creating ${IMAGE_SIZE} image..."
|
|
truncate -s "${IMAGE_SIZE}" "$WORK_IMG"
|
|
|
|
# Attach to mdconfig and partition
|
|
MD=$(mdconfig -a -t vnode -f "$WORK_IMG")
|
|
echo " Attached as /dev/${MD}"
|
|
|
|
# Initialize an MBR layout compatible with modern UEFI removable boot:
|
|
# s1: EFI System Partition (FAT, contains /EFI/BOOT/BOOTX64.EFI)
|
|
# s2: FreeBSD slice with BSD label + UFS root
|
|
# s3: CLAWDIESEED FAT32 partition for operator seed files (SSH keys,
|
|
# future hostname / Tailscale / Wi-Fi overrides). Lives at the
|
|
# tail of the disk so Linux pre-flash tooling can mount and edit
|
|
# it without touching the UFS root. Imported on every boot by
|
|
# /usr/local/etc/rc.d/clawdie_live_seed.
|
|
_seed_size_m=64
|
|
_efi_size_m=64
|
|
_image_size_m=$(( $(stat -f %z "$WORK_IMG") / 1024 / 1024 ))
|
|
# 4 MiB headroom covers MBR overhead + alignment rounding. Without this
|
|
# the trailing 'gpart add -s ${_seed_size_m}M' can fail when the EFI
|
|
# slice rounds up past the requested 64M.
|
|
_freebsd_size_m=$(( _image_size_m - _efi_size_m - _seed_size_m - 4 ))
|
|
|
|
gpart create -s MBR /dev/${MD}
|
|
gpart add -t efi -s ${_efi_size_m}M /dev/${MD}
|
|
gpart add -t freebsd -s ${_freebsd_size_m}M /dev/${MD}
|
|
gpart add -t '!12' -s ${_seed_size_m}M /dev/${MD} # !12 = FAT32 LBA (0x0c)
|
|
gpart set -a active -i 2 /dev/${MD}
|
|
|
|
# Create BSD label (partition a) in the FreeBSD slice
|
|
gpart create -s BSD /dev/${MD}s2
|
|
gpart add -t freebsd-ufs /dev/${MD}s2
|
|
|
|
# Create UFS filesystem on partition a. Keep the label expected by the
|
|
# stock FreeBSD memstick loader configuration.
|
|
newfs -U -L FreeBSD_Install /dev/${MD}s2a
|
|
|
|
# Format and seed the CLAWDIESEED partition. Operators mount this on
|
|
# Linux (sudo mount -t vfat /dev/sdX3 /mnt/...) pre-flash to drop
|
|
# authorized_keys. The README.txt explains the allowlisted contract.
|
|
#
|
|
# -c 1 forces 1 sector/cluster (512 B). FAT32 requires >=65525 clusters
|
|
# by spec; at default cluster size (4 KiB+) a 64 MiB partition falls
|
|
# well below that and newfs_msdos rejects it ("too few clusters for
|
|
# FAT32, need 65525"). 512 B clusters give us 131072 clusters with a
|
|
# ~512 KiB FAT table — <1% overhead, invisible for the few-KiB text
|
|
# files this partition is meant to hold.
|
|
newfs_msdos -F 32 -c 1 -L CLAWDIESEED /dev/${MD}s3
|
|
_seed_mount="${CACHE_DIR}/seed-mnt"
|
|
mkdir -p "${_seed_mount}"
|
|
mount_msdosfs /dev/${MD}s3 "${_seed_mount}"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/clawdie-live-seed.README.txt" \
|
|
"${_seed_mount}/README.txt"
|
|
sync
|
|
umount "${_seed_mount}"
|
|
rmdir "${_seed_mount}" 2>/dev/null || true
|
|
|
|
# Mount the new filesystem
|
|
MOUNT_POINT="${CACHE_DIR}/mnt"
|
|
mkdir -p "$MOUNT_POINT"
|
|
mount /dev/${MD}s2a "$MOUNT_POINT"
|
|
echo " Mounted /dev/${MD}s2a at ${MOUNT_POINT}"
|
|
|
|
# Mount memstick read-only to extract base system
|
|
MEMSTICK_MNT="${CACHE_DIR}/memstick-src"
|
|
mkdir -p "$MEMSTICK_MNT"
|
|
mount_memstick_rootfs "$MEMSTICK_MNT"
|
|
echo " Copying base system from memstick..."
|
|
|
|
# Copy all files from memstick to new image (excluding package cache)
|
|
(
|
|
cd "$MEMSTICK_MNT"
|
|
pax -rw -pe . "$MOUNT_POINT"
|
|
)
|
|
|
|
# Cleanup memstick mount
|
|
cleanup_memstick_rootfs "$MEMSTICK_MNT"
|
|
rm -rf "$MEMSTICK_MNT"
|
|
|
|
install_image_bootcode "$MD" "$MOUNT_POINT" "2"
|
|
install_image_uefi_bootcode "$MD" "$MOUNT_POINT" "1"
|
|
|
|
# Store MD device for later cleanup
|
|
echo "$MD" > "${CACHE_DIR}/.md_device"
|
|
else
|
|
# Reattach existing image (but extract base system if empty)
|
|
MD=$(mdconfig -a -t vnode -f "$WORK_IMG")
|
|
echo " Reattached as /dev/${MD}"
|
|
|
|
MOUNT_POINT="${CACHE_DIR}/mnt"
|
|
mkdir -p "$MOUNT_POINT"
|
|
if [ -e "/dev/${MD}s2a" ]; then
|
|
ROOT_PART="/dev/${MD}s2a"
|
|
ROOT_SLICE_INDEX="2"
|
|
else
|
|
ROOT_PART="/dev/${MD}s1a"
|
|
ROOT_SLICE_INDEX="1"
|
|
fi
|
|
mount "$ROOT_PART" "$MOUNT_POINT"
|
|
echo " Mounted ${ROOT_PART} at ${MOUNT_POINT}"
|
|
|
|
# Check if base system is missing and extract if needed
|
|
if [ ! -d "$MOUNT_POINT/etc" ]; then
|
|
echo " Extracting base system from memstick..."
|
|
MEMSTICK_MNT="${CACHE_DIR}/memstick-src"
|
|
mkdir -p "$MEMSTICK_MNT"
|
|
mount_memstick_rootfs "$MEMSTICK_MNT"
|
|
|
|
(
|
|
cd "$MEMSTICK_MNT"
|
|
pax -rw -pe . "$MOUNT_POINT"
|
|
)
|
|
|
|
cleanup_memstick_rootfs "$MEMSTICK_MNT"
|
|
rm -rf "$MEMSTICK_MNT"
|
|
|
|
install_image_bootcode "$MD" "$MOUNT_POINT" "$ROOT_SLICE_INDEX"
|
|
if [ "$ROOT_SLICE_INDEX" = "2" ]; then
|
|
install_image_uefi_bootcode "$MD" "$MOUNT_POINT" "1"
|
|
fi
|
|
fi
|
|
|
|
# Store MD device for later cleanup
|
|
echo "$MD" > "${CACHE_DIR}/.md_device"
|
|
fi
|
|
|
|
# --- step 6: inject payload ---
|
|
echo "==> [6/7] Injecting payload..."
|
|
|
|
# Create share directory on USB
|
|
USB_SHARE="${MOUNT_POINT}/usr/local/share/clawdie-iso"
|
|
mkdir -p "$USB_SHARE"
|
|
|
|
# Step 1 switches the live boot entrypoint away from auto-running
|
|
# /etc/installerconfig. Keep the script in USB_SHARE for later explicit
|
|
# bsdinstall invocation, but remove any legacy boot-time auto script.
|
|
rm -f "${MOUNT_POINT}/etc/installerconfig"
|
|
|
|
install_live_runtime_packages
|
|
configure_live_operator_session
|
|
install_colibri_service
|
|
install_zot_agent
|
|
install_nvidia_universal_repo
|
|
|
|
# Copy payload
|
|
# Rebuild payload paths from scratch inside the reusable work image. A failed
|
|
# prior build can leave a partial package tree behind, and overlaying a new
|
|
# hashed pkg repo onto that stale path produces "Not a directory" copy errors.
|
|
rm -rf "${USB_SHARE}/firstboot" "${USB_SHARE}/packages" "${USB_SHARE}/npm-globals"
|
|
rm -f "${USB_SHARE}/installerconfig" \
|
|
"${USB_SHARE}/clawdie-ai.tar.gz" \
|
|
"${USB_SHARE}/build.cfg" \
|
|
"${USB_SHARE}/build-manifest.json"
|
|
|
|
cp "${SCRIPT_DIR}/installerconfig" "${USB_SHARE}/installerconfig"
|
|
mkdir -p "${USB_SHARE}/firstboot"
|
|
(
|
|
cd "${SCRIPT_DIR}/firstboot"
|
|
pax -rw -pe . "${USB_SHARE}/firstboot"
|
|
)
|
|
mkdir -p "${USB_SHARE}/packages"
|
|
(
|
|
cd "${PKG_REPO_DIR}"
|
|
pax -rw -pe . "${USB_SHARE}/packages"
|
|
)
|
|
if [ -d "$NPM_GLOBALS_DIR" ]; then
|
|
mkdir -p "${USB_SHARE}/npm-globals"
|
|
(
|
|
cd "${NPM_GLOBALS_DIR}"
|
|
pax -rw -pe . "${USB_SHARE}/npm-globals"
|
|
)
|
|
NPM_GLOBAL_TARBALL_COUNT=$(find "${NPM_GLOBALS_DIR}" -maxdepth 1 -type f -name '*.tgz' | wc -l | tr -d ' ')
|
|
echo " Bundled npm-globals: ${NPM_GLOBAL_TARBALL_COUNT} tarballs"
|
|
fi
|
|
install_live_npm_globals
|
|
cp "${CLAWDIE_TARBALL_ISO}" "${USB_SHARE}/clawdie-ai.tar.gz"
|
|
cp "${SCRIPT_DIR}/build.cfg" "${USB_SHARE}/"
|
|
write_build_manifest "${USB_SHARE}/build-manifest.json"
|
|
|
|
# Bake runtime vars so firstboot reads the right target config
|
|
{
|
|
echo "ISO_VERSION=\"${ISO_VERSION}\""
|
|
echo "BUILD_CHANNEL=\"${BUILD_CHANNEL}\""
|
|
echo "CLAWDIE_VERSION=\"${CLAWDIE_VERSION}\""
|
|
echo "CLAWDIE_REF=\"${CLAWDIE_REF}\""
|
|
echo "CLAWDIE_AI_COMMIT=\"${CLAWDIE_AI_COMMIT}\""
|
|
echo "TARGET=\"${TARGET:-baremetal}\""
|
|
[ -n "${GPU_DRIVER:-}" ] && echo "GPU_DRIVER=\"${GPU_DRIVER}\""
|
|
[ -n "${ASSISTANT_NAME:-}" ] && echo "ASSISTANT_NAME=\"${ASSISTANT_NAME}\""
|
|
[ -n "${AGENT_GENDER:-}" ] && echo "AGENT_GENDER=\"${AGENT_GENDER}\""
|
|
[ -n "${AGENT_DOMAIN:-}" ] && echo "AGENT_DOMAIN=\"${AGENT_DOMAIN}\""
|
|
[ -n "${TZ:-}" ] && echo "TZ=\"${TZ}\""
|
|
[ -n "${PI_TUI_PROVIDER:-}" ] && echo "PI_TUI_PROVIDER=\"${PI_TUI_PROVIDER}\""
|
|
[ -n "${PI_TUI_MODEL:-}" ] && echo "PI_TUI_MODEL=\"${PI_TUI_MODEL}\""
|
|
[ -n "${ZAI_API_KEY:-}" ] && echo "ZAI_API_KEY=\"${ZAI_API_KEY}\""
|
|
[ -n "${OPENROUTER_API_KEY:-}" ] && echo "OPENROUTER_API_KEY=\"${OPENROUTER_API_KEY}\""
|
|
[ -n "${ANTHROPIC_API_KEY:-}" ] && echo "ANTHROPIC_API_KEY=\"${ANTHROPIC_API_KEY}\""
|
|
[ -n "${EMBED_BASE_URL:-}" ] && echo "EMBED_BASE_URL=\"${EMBED_BASE_URL}\""
|
|
[ -n "${EMBED_MODEL:-}" ] && echo "EMBED_MODEL=\"${EMBED_MODEL}\""
|
|
[ -n "${TELEGRAM_BOT_TOKEN:-}" ] && echo "TELEGRAM_BOT_TOKEN=\"${TELEGRAM_BOT_TOKEN}\""
|
|
[ -n "${TELEGRAM_ADMIN_IDS:-}" ] && echo "TELEGRAM_ADMIN_IDS=\"${TELEGRAM_ADMIN_IDS}\""
|
|
[ -n "${FEATURE_TELEGRAM:-}" ] && echo "FEATURE_TELEGRAM=\"${FEATURE_TELEGRAM}\""
|
|
[ -n "${FEATURE_COLIBRI:-}" ] && echo "FEATURE_COLIBRI=\"${FEATURE_COLIBRI}\""
|
|
[ -n "${COLIBRI_STAGE_TEST_AGENT:-}" ] && echo "COLIBRI_STAGE_TEST_AGENT=\"${COLIBRI_STAGE_TEST_AGENT}\""
|
|
[ -n "${COLIBRI_DAEMON_ENABLE:-}" ] && echo "COLIBRI_DAEMON_ENABLE=\"${COLIBRI_DAEMON_ENABLE}\""
|
|
[ -n "${COLIBRI_COST_MODE:-}" ] && echo "COLIBRI_COST_MODE=\"${COLIBRI_COST_MODE}\""
|
|
[ -n "${SSH_PUBLIC_KEY:-}" ] && echo "SSH_PUBLIC_KEY=\"${SSH_PUBLIC_KEY}\""
|
|
[ -n "${ROOT_PASSWORD:-}" ] && echo "ROOT_PASSWORD=\"${ROOT_PASSWORD}\""
|
|
[ -n "${CLAWDIE_USER_PASSWORD:-}" ] && echo "CLAWDIE_USER_PASSWORD=\"${CLAWDIE_USER_PASSWORD}\""
|
|
} >> "${USB_SHARE}/build.cfg"
|
|
|
|
echo " Payload injected."
|
|
|
|
# Image-size headroom guard. The live filesystem is fixed at IMAGE_SIZE; rust + go
|
|
# + offline repo staging + the package cache can fill it. Report usage and fail
|
|
# *before* shipping an image that booted with no room to breathe, rather than
|
|
# discovering it on the stick. Override the floor with IMAGE_MIN_FREE_MB.
|
|
echo "==> Image space report (mounted live filesystem):"
|
|
df -h "${MOUNT_POINT}" || true
|
|
echo " Largest staged trees:"
|
|
du -sh "${MOUNT_POINT}/usr/local" "${MOUNT_POINT}/home" "${MOUNT_POINT}/var" 2>/dev/null || true
|
|
_img_free_kb=$(df -k "${MOUNT_POINT}" | awk 'NR==2 {print $4}')
|
|
_img_min_free_mb="${IMAGE_MIN_FREE_MB:-1024}"
|
|
if [ -n "${_img_free_kb:-}" ] && [ "${_img_free_kb}" -lt $((_img_min_free_mb * 1024)) ]; then
|
|
echo "ERROR: only $((_img_free_kb / 1024)) MB free on the live filesystem (< ${_img_min_free_mb} MB floor)."
|
|
echo " Raise IMAGE_SIZE in build.cfg (e.g. for a larger USB) or trim staged content,"
|
|
echo " or lower the floor with IMAGE_MIN_FREE_MB if you know it fits."
|
|
exit 1
|
|
fi
|
|
echo " Free space OK ($((_img_free_kb / 1024)) MB, floor ${_img_min_free_mb} MB)."
|
|
|
|
# Unmount and detach
|
|
umount "$MOUNT_POINT"
|
|
if [ -f "${CACHE_DIR}/.md_device" ]; then
|
|
MD=$(cat "${CACHE_DIR}/.md_device")
|
|
mdconfig -d -u "$MD"
|
|
rm "${CACHE_DIR}/.md_device"
|
|
fi
|
|
|
|
# --- step 7: write output ---
|
|
echo "==> [7/7] Writing output image..."
|
|
mkdir -p "$OUTPUT_DIR"
|
|
cp "$WORK_IMG" "${OUTPUT_DIR}/${IMAGE_NAME}"
|
|
sync
|
|
echo ""
|
|
OUTPUT_IMAGE="${OUTPUT_DIR}/${IMAGE_NAME}"
|
|
IMAGE_LOGICAL_SIZE=$(ls -lh "$OUTPUT_IMAGE" | awk '{print $5}')
|
|
IMAGE_ALLOCATED_SIZE=$(du -sh "$OUTPUT_IMAGE" | awk '{print $1}')
|
|
echo " Done : ${OUTPUT_IMAGE}"
|
|
echo " Image size : ${IMAGE_LOGICAL_SIZE}"
|
|
echo " Allocated : ${IMAGE_ALLOCATED_SIZE} (sparse on build host)"
|
|
echo ""
|
|
echo " Write to USB:"
|
|
echo " dd if=${OUTPUT_IMAGE} of=/dev/daX bs=1M status=progress"
|