The canonical Colibri FreeBSD port lives in the colibri repo (packaging/freebsd/port/sysutils/colibri), kept with the code it builds and guarded by a CARGO_CRATES drift check in colibri CI. The copy here was a divergent duplicate (wrong LICENSE=MIT vs AGPLv3, CARGO_BUILD=no skeleton that could not build, different binary set) that the drift check could not protect. - delete ports/sysutils/colibri/ (no longer maintained here) - build.sh release gate: fail if ports/sysutils/colibri/ reappears, pointing at the canonical location — keeps the cleanup structural, not just one-time - docs/POUDRIERE-BUILD-SERVER.md + scripts/poudriere/README.md: state colibri owns the port, copy it into the ports tree, this repo keeps no duplicate Validation: build.sh sh -n clean; release-gate self-test passes; guard fires on a reintroduced dir; markdown gate clean. Nothing references the deleted path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2477 lines
109 KiB
Bash
Executable file
2477 lines
109 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})"
|
|
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-test-agent 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
|
|
}
|
|
|
|
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.29} && \\"
|
|
echo " ZOT_BUILD_VERSION=\"\${ZOT_VERSION:-v0.2.29}\" && 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_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_clawdie_ai_repo
|
|
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 start a local provider-backed
|
|
Pi session from the live XFCE desktop, 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.
|
|
EOF
|
|
seed_live_ai_source_repo "${SCRIPT_DIR}" "clawdie-iso"
|
|
seed_live_ai_source_repo "${_resolved_clawdie_ai_repo}" "clawdie-ai"
|
|
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
|
|
|
|
# FreeBSD packages ship pythonX.Y (e.g. python3.12) but not a bare python3
|
|
# symlink. Scripts that use /usr/bin/env python3 break without this.
|
|
# Derive the installed minor version so a future bump (3.12 -> 3.13) needs
|
|
# only a package-list change, never an edit here.
|
|
if [ ! -e "${MOUNT_POINT}/usr/local/bin/python3" ]; then
|
|
py_bin=$(ls "${MOUNT_POINT}/usr/local/bin"/python3.* 2>/dev/null \
|
|
| sed 's@.*/@@' | grep -E '^python3\.[0-9]+$' | sort -V | tail -1)
|
|
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"
|
|
cat > "${MOUNT_POINT}/etc/profile.d/clawdie.sh" <<'EOF'
|
|
# Clawdie live operator environment.
|
|
_clawdie_npm_prefix="/opt/clawdie/npm-global"
|
|
_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 npm_config_prefix="${_clawdie_npm_prefix}"
|
|
export NPM_CONFIG_PREFIX="${_clawdie_npm_prefix}"
|
|
export NPM_CONFIG_UPDATE_NOTIFIER=false
|
|
export NO_UPDATE_NOTIFIER=1
|
|
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
|
|
_clawdie_path_has "${_clawdie_npm_prefix}/bin" || PATH="${_clawdie_npm_prefix}/bin:${PATH}"
|
|
export PATH
|
|
|
|
# Colibri daemon socket — needed for 'colibri' CLI without --socket
|
|
export COLIBRI_DAEMON_SOCKET="/var/run/colibri/colibri.sock"
|
|
|
|
unset _clawdie_npm_prefix
|
|
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"
|
|
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"
|
|
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_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"
|