- Add --ssh-key flag for providing SSH public key at build time - Add --root-password and --clawdie-password flags for custom system passwords - Update build.cfg with SSH_PUBLIC_KEY, ROOT_PASSWORD, CLAWDIE_USER_PASSWORD - Bake all security-related vars into ISO for firstboot access Enables secure passwordless SSH auth and custom password provisioning. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
316 lines
11 KiB
Bash
Executable file
316 lines
11 KiB
Bash
Executable file
#!/bin/sh
|
|
# clawdie-iso build script
|
|
# Produces a bootable FreeBSD memstick image with Clawdie-AI pre-bundled.
|
|
# All packages are fetched and bundled for fully offline installation.
|
|
#
|
|
# 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 0.9.0 # pin Clawdie-AI version
|
|
#
|
|
# Requirements (run on FreeBSD host):
|
|
# pkg install curl # for fetching
|
|
# pkg install (root) # for step 5-6 (mdconfig, mount)
|
|
#
|
|
# The 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")")"
|
|
PKG_DIR="${SCRIPT_DIR}/packages"
|
|
CACHE_DIR="${SCRIPT_DIR}/cache"
|
|
|
|
. "${SCRIPT_DIR}/build.cfg"
|
|
|
|
# --- argument parsing ---
|
|
SKIP_FETCH=0
|
|
FETCH_ONLY=0
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--clawdie-version) CLAWDIE_VERSION="$2"; shift 2 ;;
|
|
--skip-fetch) SKIP_FETCH=1; shift ;;
|
|
--fetch-only) FETCH_ONLY=1; shift ;;
|
|
--gpu-driver) GPU_DRIVER="$2"; shift 2 ;;
|
|
--target) TARGET="$2"; shift 2 ;;
|
|
--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
|
|
|
|
# Validate GPU_DRIVER
|
|
case "${GPU_DRIVER:-}" in
|
|
intel|amd|nvidia-590|nvidia-470|nvidia-390|vesa|"") ;;
|
|
*) echo "ERROR: Unknown --gpu-driver: $GPU_DRIVER"; exit 1 ;;
|
|
esac
|
|
|
|
# Validate TARGET
|
|
case "${TARGET:-baremetal}" in
|
|
cloud|baremetal) ;;
|
|
*) echo "ERROR: Unknown --target: $TARGET"; exit 1 ;;
|
|
esac
|
|
|
|
# Cloud target requires pre-bake vars
|
|
if [ "${TARGET:-baremetal}" = "cloud" ]; then
|
|
[ -z "${ASSISTANT_NAME:-}" ] && echo "ERROR: --target cloud requires --assistant-name" && exit 1
|
|
[ -z "${AGENT_DOMAIN:-}" ] && echo "ERROR: --target cloud requires --domain" && exit 1
|
|
[ -z "${TZ:-}" ] && echo "ERROR: --target cloud requires --tz" && exit 1
|
|
fi
|
|
|
|
echo "==> clawdie-iso build"
|
|
echo " FreeBSD : ${FREEBSD_VERSION} ${FREEBSD_ARCH}"
|
|
echo " Clawdie : v${CLAWDIE_VERSION}"
|
|
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 (cloud or GPU_DRIVER or baremetal) (after arg parsing, before it's used)
|
|
case "${TARGET:-baremetal}" in
|
|
cloud) VARIANT="cloud" ;;
|
|
*) VARIANT="${GPU_DRIVER:-baremetal}" ;;
|
|
esac
|
|
IMAGE_NAME="clawdie-iso-${VARIANT}-$(date +%d.%b.%Y | tr 'A-Z' 'a-z').img"
|
|
|
|
# --- helper: read package lists into a single deduplicated list ---
|
|
pkg_list_all() {
|
|
# Cloud: headless, no desktop, no GPU drivers
|
|
if [ "${TARGET:-baremetal}" = "cloud" ]; then
|
|
cat \
|
|
"${PKG_DIR}/pkg-list-host.txt" \
|
|
"${PKG_DIR}/pkg-list-jails.txt" \
|
|
| grep -v '^#' | grep -v '^$' | sort -u
|
|
return 0
|
|
fi
|
|
|
|
# Baremetal: desktop + optional GPU packages
|
|
case "${GPU_DRIVER:-}" in
|
|
nvidia-590) GPU_PKG="${PKG_DIR}/pkg-list-nvidia-590.txt" ;;
|
|
nvidia-470) GPU_PKG="${PKG_DIR}/pkg-list-nvidia-470.txt" ;;
|
|
nvidia-390) GPU_PKG="${PKG_DIR}/pkg-list-nvidia-390.txt" ;;
|
|
*) GPU_PKG="" ;;
|
|
esac
|
|
|
|
cat \
|
|
"${PKG_DIR}/pkg-list-host.txt" \
|
|
"${PKG_DIR}/pkg-list-jails.txt" \
|
|
"${PKG_DIR}/pkg-list-desktop-base.txt" \
|
|
"${PKG_DIR}/pkg-list-xfce.txt" \
|
|
${GPU_PKG:+"$GPU_PKG"} \
|
|
| grep -v '^#' | grep -v '^$' | sort -u
|
|
}
|
|
|
|
# --- 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"
|
|
sha256 -c "${MEMSTICK}.SHA256" || { echo "ERROR: checksum mismatch on memstick"; exit 1; }
|
|
else
|
|
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 packages/..."
|
|
mkdir -p "$PKG_DIR"
|
|
|
|
# Set pkg repo to configured branch before fetching
|
|
# Use temporary user-level config to avoid requiring root
|
|
PKG_CONFIG_DIR=$(mktemp -d)
|
|
trap "rm -rf $PKG_CONFIG_DIR" EXIT
|
|
ABI=$(pkg config abi 2>/dev/null || echo "FreeBSD:15:amd64")
|
|
mkdir -p "$PKG_CONFIG_DIR"
|
|
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 packages/
|
|
# Use PKG_REPOS_DIR to override system repos with our temporary config
|
|
export PKG_REPOS_DIR="$PKG_CONFIG_DIR"
|
|
echo "$PKGS" | xargs pkg fetch --yes --dependencies --output "$PKG_DIR"
|
|
|
|
echo " Fetch complete."
|
|
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_DIR/All" ]; then
|
|
pkg repo "$PKG_DIR"
|
|
echo " Repo metadata written to packages/"
|
|
else
|
|
echo " WARN: packages/All/ not found — run without --skip-fetch first"
|
|
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 4: fetch Clawdie-AI tarball ---
|
|
# Resolve "latest" to the most recent Codeberg tag
|
|
if [ "$CLAWDIE_VERSION" = "latest" ] || [ -z "$CLAWDIE_VERSION" ]; 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//')
|
|
echo " Resolved: v${CLAWDIE_VERSION}"
|
|
fi
|
|
|
|
CLAWDIE_TARBALL="${CACHE_DIR}/clawdie-ai-v${CLAWDIE_VERSION}.tar.gz"
|
|
if [ "$SKIP_FETCH" -eq 0 ] || [ ! -f "$CLAWDIE_TARBALL" ]; then
|
|
echo "==> [4/7] Fetching Clawdie-AI v${CLAWDIE_VERSION}..."
|
|
mkdir -p "$CACHE_DIR"
|
|
curl -L --progress-bar -o "$CLAWDIE_TARBALL" \
|
|
"https://codeberg.org/Clawdie/Clawdie-AI/archive/v${CLAWDIE_VERSION}.tar.gz"
|
|
else
|
|
echo "==> [4/7] Clawdie-AI v${CLAWDIE_VERSION} cached."
|
|
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 MBR partition table
|
|
gpart create -s MBR /dev/${MD}
|
|
gpart add -t freebsd /dev/${MD}
|
|
|
|
# Create BSD label (partition a) in the FreeBSD slice
|
|
gpart create -s BSD /dev/${MD}s1
|
|
gpart add -t freebsd-ufs /dev/${MD}s1
|
|
|
|
# Create UFS filesystem on partition a
|
|
newfs -U /dev/${MD}s1a
|
|
|
|
# Mount the new filesystem
|
|
MOUNT_POINT="${CACHE_DIR}/mnt"
|
|
mkdir -p "$MOUNT_POINT"
|
|
mount /dev/${MD}s1a "$MOUNT_POINT"
|
|
echo " Mounted /dev/${MD}s1a at ${MOUNT_POINT}"
|
|
|
|
# Mount memstick read-only to extract base system
|
|
MEMSTICK_MNT="${CACHE_DIR}/memstick-src"
|
|
mkdir -p "$MEMSTICK_MNT"
|
|
MD_SRC=$(mdconfig -a -t vnode -f "$MEMSTICK")
|
|
mount -r -t ufs /dev/${MD_SRC}s2a "$MEMSTICK_MNT"
|
|
echo " Copying base system from memstick..."
|
|
|
|
# Copy all files from memstick to new image (excluding package cache)
|
|
tar -C "$MEMSTICK_MNT" -cf - . | tar -C "$MOUNT_POINT" -xf -
|
|
|
|
# Cleanup memstick mount
|
|
umount "$MEMSTICK_MNT"
|
|
mdconfig -d -u ${MD_SRC}
|
|
rm -rf "$MEMSTICK_MNT"
|
|
|
|
# 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"
|
|
mount /dev/${MD}s1a "$MOUNT_POINT"
|
|
echo " Mounted /dev/${MD}s1a 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"
|
|
MD_SRC=$(mdconfig -a -t vnode -f "$MEMSTICK")
|
|
mount -r -t ufs /dev/${MD_SRC}s2a "$MEMSTICK_MNT"
|
|
|
|
tar -C "$MEMSTICK_MNT" -cf - . | tar -C "$MOUNT_POINT" -xf -
|
|
|
|
umount "$MEMSTICK_MNT"
|
|
mdconfig -d -u ${MD_SRC}
|
|
rm -rf "$MEMSTICK_MNT"
|
|
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"
|
|
|
|
# Copy payload
|
|
cp "${SCRIPT_DIR}/installerconfig" "${MOUNT_POINT}/etc/installerconfig"
|
|
cp -r "${SCRIPT_DIR}/firstboot" "${USB_SHARE}/"
|
|
cp -r "${PKG_DIR}" "${USB_SHARE}/"
|
|
cp "${CACHE_DIR}/clawdie-ai-v${CLAWDIE_VERSION}.tar.gz" "${USB_SHARE}/clawdie-ai.tar.gz"
|
|
cp "${SCRIPT_DIR}/build.cfg" "${USB_SHARE}/"
|
|
|
|
# Bake runtime vars so firstboot reads the right target config
|
|
{
|
|
echo "TARGET=\"${TARGET:-baremetal}\""
|
|
[ -n "${GPU_DRIVER:-}" ] && echo "GPU_DRIVER=\"${GPU_DRIVER}\""
|
|
[ -n "${ASSISTANT_NAME:-}" ] && echo "ASSISTANT_NAME=\"${ASSISTANT_NAME}\""
|
|
[ -n "${AGENT_DOMAIN:-}" ] && echo "AGENT_DOMAIN=\"${AGENT_DOMAIN}\""
|
|
[ -n "${TZ:-}" ] && echo "TZ=\"${TZ}\""
|
|
[ -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..."
|
|
cp "$WORK_IMG" "${SCRIPT_DIR}/${IMAGE_NAME}"
|
|
echo ""
|
|
echo " Done : ${SCRIPT_DIR}/${IMAGE_NAME}"
|
|
echo " Size : $(du -sh "${SCRIPT_DIR}/${IMAGE_NAME}" | cut -f1)"
|
|
echo ""
|
|
echo " Write to USB:"
|
|
echo " dd if=${IMAGE_NAME} of=/dev/daX bs=1M status=progress"
|