Use packages/npm-globals.txt as the source of truth for offline npm CLI tarballs, update Pi to 0.75.5, and keep Claude Code out of the XFCE USB path. --- Build: not run — ISO build not requested Tests: pass — sh -n fetch-npm-globals and shell-npm-globals; pinned npm pack smoke passed
869 lines
33 KiB
Bash
Executable file
869 lines
33 KiB
Bash
Executable file
#!/bin/sh
|
|
# clawdie-iso build script (UNIFIED)
|
|
# Produces a bootable FreeBSD memstick image with Clawdie-AI pre-bundled.
|
|
# All packages are fetched and bundled for fully offline installation.
|
|
#
|
|
# Unified ISO: Single image works on VPS, baremetal, and cloud
|
|
# 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 --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 qt6-base qt6-declarative qt6-buildtools # to build the live QML installer
|
|
# 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
|
|
TMP_DIR="${SCRIPT_DIR}/tmp"
|
|
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/installer-session"
|
|
|
|
. "${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 and QML stages cannot drop to a non-root build user."
|
|
fi
|
|
|
|
# --- argument parsing ---
|
|
SKIP_FETCH=0
|
|
FETCH_ONLY=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 ;;
|
|
--fetch-only) FETCH_ONLY=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 ;;
|
|
--root-password) ROOT_PASSWORD="$2"; shift 2 ;;
|
|
--clawdie-password) CLAWDIE_USER_PASSWORD="$2"; shift 2 ;;
|
|
*) echo "Unknown arg: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [ "${BUILD_CHANNEL}" = "release" ]; then
|
|
case "${CLAWDIE_REF}" in
|
|
v[0-9]*.[0-9]*.[0-9]*) ;;
|
|
*)
|
|
echo "ERROR: release builds must pin a Clawdie-AI tag with --clawdie-version X.Y.Z"
|
|
echo " Current Clawdie ref: ${CLAWDIE_REF}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Tailscale warning (recommended but optional)
|
|
if [ "${FEATURE_TAILSCALE:-YES}" = "YES" ] && [ -z "${TAILSCALE_AUTHKEY:-}" ]; then
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "⚠️ WARNING: Tailscale auth key not set"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
echo "Tailscale provides secure remote access without exposing SSH."
|
|
echo "Without it, your instance will have public SSH on port 22."
|
|
echo ""
|
|
echo "Recommended: Generate a key and rebuild"
|
|
echo " https://login.tailscale.com/admin/settings/keys"
|
|
echo " export TAILSCALE_AUTHKEY='tskey-auth-...'"
|
|
echo " ./build.sh"
|
|
echo ""
|
|
echo "Continuing without Tailscale (SSH will be publicly accessible)..."
|
|
echo ""
|
|
export FEATURE_TAILSCALE="NO"
|
|
fi
|
|
|
|
echo "==> clawdie-iso build"
|
|
echo " ISO : ${ISO_VERSION}-${BUILD_CHANNEL}"
|
|
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 " Target : ${TARGET:-baremetal}"
|
|
echo ""
|
|
|
|
# Set IMAGE_NAME with VARIANT (vps or GPU_DRIVER or baremetal) (after arg parsing, before it's used)
|
|
IMAGE_NAME="clawdie-iso-unified-$(LC_TIME=C date +%d.%b.%Y | tr 'A-Z' 'a-z').img"
|
|
mkdir -p "$TMP_DIR"
|
|
|
|
# --- helper: read package lists into a single deduplicated list ---
|
|
pkg_list_all() {
|
|
# Unified ISO: include all packages for maximum flexibility
|
|
# No target-specific logic - runtime detection handles configuration
|
|
|
|
echo "==> Unified ISO: including all packages (host + jails + desktop + GPU drivers)" >&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-lumina.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-live-installer.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-lumina.txt" \
|
|
"${PKG_LIST_DIR}/pkg-list-live-installer.txt" \
|
|
| grep -v '^#' \
|
|
| grep -v '^$' \
|
|
| grep -v -E '^(hal|lumina-filemanager|lumina-open)$' \
|
|
| 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
|
|
}
|
|
|
|
pkg_archive_for() {
|
|
_pkg_name="$1"
|
|
find "${PKG_REPO_DIR}/All" -type f -name "${_pkg_name}-*.pkg" | sort | tail -1
|
|
}
|
|
|
|
json_escape() {
|
|
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
}
|
|
|
|
resolve_clawdie_commit() {
|
|
_ref="$1"
|
|
_repo="https://codeberg.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
|
|
}
|
|
|
|
is_pinned_clawdie_ref() {
|
|
_ref="$1"
|
|
printf '%s' "$_ref" | grep -Eq '^[0-9a-fA-F]{40}$|^v[0-9]+\.[0-9]+\.[0-9]+$'
|
|
}
|
|
|
|
write_build_manifest() {
|
|
_manifest_path="$1"
|
|
_iso_repo_commit="unknown"
|
|
_iso_repo_dirty="null"
|
|
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 git -C "$SCRIPT_DIR" diff --quiet 2>/dev/null && git -C "$SCRIPT_DIR" diff --cached --quiet 2>/dev/null; then
|
|
_iso_repo_dirty="false"
|
|
else
|
|
_iso_repo_dirty="true"
|
|
fi
|
|
fi
|
|
mkdir -p "$(dirname "$_manifest_path")"
|
|
cat > "$_manifest_path" <<EOF
|
|
{
|
|
"iso_version": "$(json_escape "${ISO_VERSION}")",
|
|
"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}")",
|
|
"iso_repo_commit": "$(json_escape "${_iso_repo_commit}")",
|
|
"iso_repo_dirty": ${_iso_repo_dirty},
|
|
"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=$(
|
|
fdisk "/dev/${MD_SRC}" \
|
|
| awk '
|
|
/sysid 165 / { want = 1; next }
|
|
want && $1 == "start" {
|
|
gsub(",", "", $2)
|
|
gsub(",", "", $4)
|
|
print $2 " " $4
|
|
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
|
|
}
|
|
|
|
build_live_qml_installer() {
|
|
QML_BUILD_DIR="${CACHE_DIR}/qml-installer-build"
|
|
QML_SOURCE_DIR="${SCRIPT_DIR}/firstboot/gui/qml-installer"
|
|
QML_INSTALLER_BIN="${CACHE_DIR}/clawdie-qml-installer"
|
|
|
|
if ! command -v qmake6 >/dev/null 2>&1; then
|
|
echo "ERROR: qmake6 not found on build host."
|
|
echo "Install Qt6 build tools, then rerun build.sh."
|
|
echo "Example (FreeBSD): sudo pkg install -y qt6-base qt6-declarative qt6-buildtools"
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> [5b/7] Building live QML installer..."
|
|
rm -rf "$QML_BUILD_DIR"
|
|
mkdir -p "$QML_BUILD_DIR"
|
|
|
|
_qml_build_script="${CACHE_DIR}/run-qml-build.sh"
|
|
cat > "$_qml_build_script" <<'EOF'
|
|
#!/bin/sh
|
|
set -eu
|
|
cd "$BUILD_QML_DIR"
|
|
qmake6 "$BUILD_QML_PRO"
|
|
make
|
|
EOF
|
|
chmod 700 "$_qml_build_script"
|
|
[ -n "${BUILD_HOST_USER}" ] && chown "$BUILD_HOST_USER":"$BUILD_HOST_USER" "$QML_BUILD_DIR" "$_qml_build_script" 2>/dev/null || true
|
|
|
|
if [ -n "${BUILD_HOST_USER}" ]; then
|
|
su -m "$BUILD_HOST_USER" -c "env HOME='${BUILD_HOST_HOME}' PATH='${PATH}' BUILD_QML_DIR='${QML_BUILD_DIR}' BUILD_QML_PRO='${QML_SOURCE_DIR}/qml-installer.pro' sh '${_qml_build_script}'"
|
|
else
|
|
env BUILD_QML_DIR="${QML_BUILD_DIR}" BUILD_QML_PRO="${QML_SOURCE_DIR}/qml-installer.pro" sh "$_qml_build_script"
|
|
fi
|
|
|
|
cp "${QML_BUILD_DIR}/clawdie-qml-installer" "$QML_INSTALLER_BIN"
|
|
chmod +x "$QML_INSTALLER_BIN"
|
|
}
|
|
|
|
install_live_runtime_packages() {
|
|
echo " Installing live GUI 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
|
|
|
|
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
|
|
|
|
[ "$_mounted_procfs" -eq 1 ] && umount "${MOUNT_POINT}/proc" 2>/dev/null || true
|
|
[ "$_mounted_devfs" -eq 1 ] && umount "${MOUNT_POINT}/dev" 2>/dev/null || true
|
|
}
|
|
|
|
configure_live_installer_session() {
|
|
echo " Configuring live installer session..."
|
|
|
|
mkdir -p "${MOUNT_POINT}/usr/local/bin"
|
|
install -m 0755 "${CACHE_DIR}/clawdie-qml-installer" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-qml-installer"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-installer-launch.sh" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-live-installer-launch.sh"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-commit.sh" \
|
|
"${MOUNT_POINT}/usr/local/bin/clawdie-live-commit.sh"
|
|
|
|
# The stock FreeBSD memstick starts bsdinstall from /etc/rc.local before
|
|
# our graphical live session can own the install flow. 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
|
|
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/lightdm/lightdm.conf.d"
|
|
install -m 0644 "${LIVE_SESSION_DIR}/lightdm-live.conf" \
|
|
"${MOUNT_POINT}/usr/local/etc/lightdm/lightdm.conf.d/50-clawdie-live.conf"
|
|
|
|
if ! /usr/sbin/pw -R "$MOUNT_POINT" usershow clawdie-installer >/dev/null 2>&1; then
|
|
/usr/sbin/pw -R "$MOUNT_POINT" useradd clawdie-installer \
|
|
-m \
|
|
-s /bin/sh \
|
|
-c "Clawdie Live Installer"
|
|
fi
|
|
|
|
mkdir -p "${MOUNT_POINT}/home/clawdie-installer"
|
|
install -m 0755 "${LIVE_SESSION_DIR}/xprofile" \
|
|
"${MOUNT_POINT}/home/clawdie-installer/.xprofile"
|
|
cat > "${MOUNT_POINT}/home/clawdie-installer/.dmrc" <<'EOF'
|
|
[Desktop]
|
|
Session=lumina
|
|
EOF
|
|
chroot "$MOUNT_POINT" chown -R clawdie-installer:clawdie-installer /home/clawdie-installer
|
|
|
|
mkdir -p "${MOUNT_POINT}/usr/local/etc/sudoers.d"
|
|
cat > "${MOUNT_POINT}/usr/local/etc/sudoers.d/clawdie-live-installer" <<'EOF'
|
|
# Allow the live autologin user to run only the installer commit helper and
|
|
# reboot from the success screen, without a password prompt.
|
|
Defaults:clawdie-installer !requiretty
|
|
clawdie-installer ALL=(root) NOPASSWD: /usr/local/bin/clawdie-live-commit.sh, /sbin/reboot
|
|
EOF
|
|
chmod 0440 "${MOUNT_POINT}/usr/local/etc/sudoers.d/clawdie-live-installer"
|
|
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'dbus_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'seatd_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'lightdm_enable="YES"'
|
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'display_manager="lightdm"'
|
|
}
|
|
|
|
# --- step 1: fetch FreeBSD memstick ---
|
|
MEMSTICK="${CACHE_DIR}/FreeBSD-${FREEBSD_VERSION}-${FREEBSD_ARCH}-memstick.img"
|
|
if [ "$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
|
|
if ! echo "$PKGS" | xargs pkg fetch --yes --dependencies --output "$PKG_REPO_DIR" 2>/tmp/pkg-fetch-error.log; then
|
|
# If privilege error, offer to re-run with sudo
|
|
if grep -q "Insufficient privileges" /tmp/pkg-fetch-error.log 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 /tmp/pkg-fetch-error.log
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo " Fetch complete."
|
|
|
|
# Fetch pinned npm-global CLI tarballs (pi, gemini-cli)
|
|
echo "==> [2b/7] Fetching pinned 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
|
|
|
|
# --- 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
|
|
if ! pkg repo "$PKG_REPO_DIR" 2>/tmp/pkg-repo-error.log; then
|
|
if grep -q "Insufficient privileges" /tmp/pkg-repo-error.log 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 /tmp/pkg-repo-error.log
|
|
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" to the most recent Codeberg tag.
|
|
if [ "${CLAWDIE_REF:-${CLAWDIE_VERSION:-}}" = "latest" ] || [ -z "${CLAWDIE_REF:-}" ]; then
|
|
echo "==> [4/7] Resolving latest Clawdie-AI version..."
|
|
CLAWDIE_VERSION=$(curl -s "https://codeberg.org/api/v1/repos/Clawdie/Clawdie-AI/releases?limit=1" \
|
|
| grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4 | sed 's/^v//')
|
|
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://codeberg.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"
|
|
|
|
# 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
|
|
gpart create -s MBR /dev/${MD}
|
|
gpart add -t efi -s 64M /dev/${MD}
|
|
gpart add -t freebsd /dev/${MD}
|
|
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
|
|
|
|
# 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..."
|
|
|
|
build_live_qml_installer
|
|
|
|
# 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_installer_session
|
|
|
|
# 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"
|
|
cp -r "${SCRIPT_DIR}/firstboot" "${USB_SHARE}/firstboot"
|
|
mkdir -p "${USB_SHARE}/packages"
|
|
cp -R "${PKG_REPO_DIR}/." "${USB_SHARE}/packages/"
|
|
if [ -d "$NPM_GLOBALS_DIR" ]; then
|
|
mkdir -p "${USB_SHARE}/npm-globals"
|
|
cp -R "${NPM_GLOBALS_DIR}/." "${USB_SHARE}/npm-globals/"
|
|
echo " Bundled npm-globals: $(ls "${NPM_GLOBALS_DIR}"/*.tgz 2>/dev/null | wc -l | tr -d ' ') tarballs"
|
|
fi
|
|
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 "${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."
|
|
|
|
# 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=${IMAGE_NAME} of=/dev/daX bs=1M status=progress"
|