From 32ed073cb2b45d2dce7fef78fb4b14daee2b5e3e Mon Sep 17 00:00:00 2001 From: Marcus Holland-Moritz Date: Sat, 9 Aug 2025 08:16:17 +0200 Subject: [PATCH] chore: add CI for FreeBSD --- .docker/build-freebsd.sh | 33 +++++ .github/scripts/freebsd_jail_build.sh | 189 ++++++++++++++++++++++++++ .github/scripts/freebsd_setup_base.sh | 67 +++++++++ .github/workflows/build.yml | 31 +++++ 4 files changed, 320 insertions(+) create mode 100644 .docker/build-freebsd.sh create mode 100644 .github/scripts/freebsd_jail_build.sh create mode 100755 .github/scripts/freebsd_setup_base.sh diff --git a/.docker/build-freebsd.sh b/.docker/build-freebsd.sh new file mode 100644 index 00000000..9dcb956b --- /dev/null +++ b/.docker/build-freebsd.sh @@ -0,0 +1,33 @@ +#!/bin/sh +set -eux + +export HOME=/root +export PATH=/usr/local/bin:/usr/local/sbin:/bin:/usr/bin:/sbin:/usr/sbin +export CCACHE_DIR=/ccache +export RUNNER_TEMP=/runner_temp +export RUNNER_WORKSPACE=/work +export GITHUB_RUN_NUMBER="${GITHUB_RUN_NUMBER:-0}" +export BUILD_MODE="${BUILD_MODE:-Release}" +export CMAKE_ARGS="${CMAKE_ARGS:-}" + +# Source tarball path (NFS is nullfs-mounted from host) +SRC_TARBALL="/mnt/opensource/artifacts/dwarfs/cache/dwarfs-source-${GITHUB_RUN_NUMBER}.tar.zst" +test -r "${SRC_TARBALL}" + +cd "$HOME" +rm -rf dwarfs dwarfs-* +tar xf "${SRC_TARBALL}" +ln -sfn dwarfs-* dwarfs + +rm -rf "${RUNNER_TEMP}/build" +cmake --fresh \ + -B"${RUNNER_TEMP}/build" \ + -S"${HOME}/dwarfs" \ + -DCMAKE_BUILD_TYPE="${BUILD_MODE}" \ + -DWITH_TESTS=ON \ + -DWITH_PXATTR=ON \ + ${CMAKE_ARGS} + +cmake --build "${RUNNER_TEMP}/build" -j"$(sysctl -n hw.ncpu)" +ctest --test-dir "${RUNNER_TEMP}/build" --output-on-failure -j"$(sysctl -n hw.ncpu)" +cmake --build "${RUNNER_TEMP}/build" --target realclean diff --git a/.github/scripts/freebsd_jail_build.sh b/.github/scripts/freebsd_jail_build.sh new file mode 100644 index 00000000..3cafb724 --- /dev/null +++ b/.github/scripts/freebsd_jail_build.sh @@ -0,0 +1,189 @@ +#!/bin/sh + +set -eu + +# ---------------------------- +# Config / Inputs (env-driven) +# ---------------------------- +FREEBSD_VER_TAG="${FREEBSD_VERSION:-14_3}" +SNAP_LABEL="${FREEBSD_SNAPSHOT:-base-20250809}" +ZPOOL="${ZPOOL:-zroot}" +JAILS_DS="${JAILS_DS:-${ZPOOL}/z/jails}" +JAILS_MP="${JAILS_MP:-/z/jails}" + +GITHUB_RUN_ID="${GITHUB_RUN_ID:-localrun}" +GITHUB_RUN_ATTEMPT="${GITHUB_RUN_ATTEMPT:-1}" + +BUILD_MODE="${BUILD_MODE:-Release}" +CMAKE_ARGS="${CMAKE_ARGS:-}" + +BASE_DS="${JAILS_DS}/${FREEBSD_VER_TAG}" +RUN_DS="${JAILS_DS}/ci-${FREEBSD_VER_TAG}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" +RUN_MP="${JAILS_MP}/ci-${FREEBSD_VER_TAG}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" +JAIL="ci_${FREEBSD_VER_TAG}_${GITHUB_RUN_ID}_${GITHUB_RUN_ATTEMPT}" + +# ---------------------------- +# Helpers +# ---------------------------- +log() { printf '%s %s\n' "[freebsd-jail]" "$*" >&2; } + +# Stop a jail if it's running; kill any lingering processes by JID if needed +stop_stale_jail() { + jname="$1" + if jls -j "$jname" >/dev/null 2>&1; then + log "Stopping stale jail: $jname" + if ! sudo jail -r "$jname" 2>/dev/null; then + JID="$(jls -j "$jname" jid -h 2>/dev/null || true)" + if [ -n "$JID" ]; then + sudo killall -j "$JID" -TERM 2>/dev/null || true + sleep 1 + sudo killall -j "$JID" -KILL 2>/dev/null || true + fi + sudo jail -r "$jname" 2>/dev/null || true + fi + fi +} + +# Unmount everything under a mountpoint (deepest-first) +umount_tree() { + rootmp="$1" + # List mounted paths under $rootmp, deepest-first + # shellcheck disable=SC2016 + for m in $(mount | awk -v p="$rootmp" '$3 ~ ("^"p) { print length, $3 }' | sort -rn | cut -d" " -f2-); do + sudo umount -f "$m" 2>/dev/null || true + done +} + +# Destroy a cloned dataset (after umount) +destroy_dataset() { + ds="$1" + if sudo zfs list -H "$ds" >/dev/null 2>&1; then + mp="$(sudo zfs get -H -o value mountpoint "$ds" 2>/dev/null || echo "")" + if [ -n "$mp" ] && [ "$mp" != "-" ]; then + umount_tree "$mp" + fi + log "Destroying dataset: $ds" + sudo zfs destroy -r "$ds" + fi +} + +# Remove stale jail + dataset that match our run IDs +cleanup_stale_before_start() { + stop_stale_jail "$JAIL" + destroy_dataset "$RUN_DS" +} + +# Full cleanup for traps: stop jail, unmount, destroy dataset +cleanup_on_exit() { + # Be idempotent; ignore errors + stop_stale_jail "$JAIL" + destroy_dataset "$RUN_DS" +} + +# Generate an env file inside jail that exports safe GitHub env vars +write_gha_env() { + out="$1" + # Allow common CI vars; deny obvious secrets + # Adjust allow/deny as needed for your org. + ALLOW='GITHUB_* RUNNER_* CI ACTIONS_* INPUT_* MATRIX_* BUILD_MODE CMAKE_ARGS' + DENY='*TOKEN* *SECRET* *PASSWORD* *PASS* *KEY* *CERT* AWS_* AZURE_* GCP_*' + + # Function to test name against allow/deny (POSIX sh compatible inline) + is_allowed() { + name="$1" + case "$name" in + GITHUB_*|RUNNER_*|CI|ACTIONS_*|INPUT_*|MATRIX_*|BUILD_MODE|CMAKE_ARGS) : ;; + *) return 1 ;; + esac + case "$name" in + *TOKEN*|*SECRET*|*PASSWORD*|*PASS*|*KEY*|*CERT*|AWS_*|AZURE_*|GCP_* ) return 1 ;; + esac + return 0 + } + + tmp="$(mktemp)" + { + echo "# Autogenerated; sourced by /root/build.sh" + # Ensure our two common knobs are present even if not in env + printf "export BUILD_MODE='%s'\n" "$BUILD_MODE" + # Escape single quotes in CMAKE_ARGS + esc_ca=$(printf "%s" "$CMAKE_ARGS" | sed "s/'/'\"'\"'/g") + printf "export CMAKE_ARGS='%s'\n" "$esc_ca" + + env | while IFS='=' read -r name value; do + if is_allowed "$name"; then + esc=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g") + printf "export %s='%s'\n" "$name" "$esc" + fi + done + } > "$tmp" + sudo mkdir -p "$(dirname "$out")" + sudo cp "$tmp" "$out" + sudo chmod 0644 "$out" + rm -f "$tmp" +} + +# ---------------------------- +# Trap: ensure cleanup on any exit +# ---------------------------- +trap cleanup_on_exit EXIT INT TERM HUP + +# ---------------------------- +# Prep: nuke stale, then clone snapshot +# ---------------------------- +cleanup_stale_before_start + +log "Cloning ${BASE_DS}@${SNAP_LABEL} -> ${RUN_DS}" +sudo zfs clone "${BASE_DS}@${SNAP_LABEL}" "${RUN_DS}" + +# ---------------------------- +# Bootstrapping jail root: DNS + mounts +# ---------------------------- +# DNS for networking inside jail +sudo mkdir -p "${RUN_MP}/etc" +sudo cp /etc/resolv.conf "${RUN_MP}/etc/resolv.conf" +[ -f /etc/hosts ] && sudo cp /etc/hosts "${RUN_MP}/etc/hosts" +[ -f /etc/nsswitch.conf ] && sudo cp /etc/nsswitch.conf "${RUN_MP}/etc/nsswitch.conf" + +# Core mounts +sudo mount -t devfs devfs "${RUN_MP}/dev" +sudo mount -t fdescfs fdesc "${RUN_MP}/dev/fd" +sudo mount -t procfs proc "${RUN_MP}/proc" +sudo mount -t tmpfs tmpfs "${RUN_MP}/tmp" + +# Workspace / temp / ccache / NFS from host +RUNNER_WORKSPACE="${RUNNER_WORKSPACE:-$PWD}" +RUNNER_TEMP="${RUNNER_TEMP:-/tmp/runner_temp}" +mkdir -p "${RUNNER_TEMP}" "${HOME}/.ccache" + +sudo mkdir -p "${RUN_MP}/work" "${RUN_MP}/runner_temp" "${RUN_MP}/ccache" "${RUN_MP}/mnt/opensource" +sudo mount -t nullfs -o rw "${RUNNER_WORKSPACE}" "${RUN_MP}/work" +sudo mount -t nullfs -o rw "${RUNNER_TEMP}" "${RUN_MP}/runner_temp" +sudo mount -t nullfs -o rw "${HOME}/github-ccache" "${RUN_MP}/ccache" +sudo mount -t nullfs -o rw "/mnt/opensource" "${RUN_MP}/mnt/opensource" + +# ---------------------------- +# Start jail (FUSE enabled) +# ---------------------------- +log "Starting jail: ${JAIL}" +sudo jail -c name="${JAIL}" host.hostname="${JAIL}" \ + path="${RUN_MP}" persist \ + mount.devfs devfs_ruleset=5 enforce_statfs=1 \ + allow.mount allow.mount.fusefs \ + allow.raw_sockets \ + ip4=inherit ip6=inherit + +# ---------------------------- +# Inject env + build script, then run it +# ---------------------------- +write_gha_env "${RUN_MP}/root/gha_env.sh" + +sudo cp ".docker/build-freebsd.sh" "${RUN_MP}/root/build.sh" +sudo chmod +x "${RUN_MP}/root/build.sh" + +# Run inside the jail with env sourced +log "Executing build inside jail…" +sudo jexec "${JAIL}" /bin/sh -lc '. /root/gha_env.sh; exec /root/build.sh' + +log "Build finished successfully." +# Cleanup happens automatically via trap on EXIT. diff --git a/.github/scripts/freebsd_setup_base.sh b/.github/scripts/freebsd_setup_base.sh new file mode 100755 index 00000000..751bb8b5 --- /dev/null +++ b/.github/scripts/freebsd_setup_base.sh @@ -0,0 +1,67 @@ +#!/bin/sh +set -euo pipefail + +# ===== Config ===== +FREEBSD_REL="${FREEBSD_REL:-14.3-RELEASE}" +FREEBSD_VER_TAG="${FREEBSD_VER_TAG:-14_3}" + +SNAP_LABEL="${SNAP_LABEL:-base-$(date +%Y%m%d)}" + +ZPOOL="${ZPOOL:-zroot}" +JAILS_DS="${JAILS_DS:-${ZPOOL}/z/jails}" +JAILS_MP="${JAILS_MP:-/z/jails}" + +PKG_REPO_URL='pkg+http://pkg.FreeBSD.org/${ABI}/latest' + +PKGS="boost-all brotli ccache cmake date double-conversion flac fuse fusefs-libs fusefs-libs3 glog gnulibiberty libarchive liblz4 mold ninja nlohmann-json openssl parallel-hashmap pkgconf range-v3 utf8cpp xxhash zstd ca_root_nss git" + +# ===== Layout ===== +sudo zfs list -H "${ZPOOL}" >/dev/null 2>&1 || { echo "No pool ${ZPOOL}"; exit 1; } +sudo zfs create -o mountpoint=/z "${ZPOOL}/z" >/dev/null 2>&1 || true +sudo zfs create -o mountpoint=/z/jails "${JAILS_DS}" >/dev/null 2>&1 || true + +BASE_DS="${JAILS_DS}/${FREEBSD_VER_TAG}" +BASE_MP="${JAILS_MP}/${FREEBSD_VER_TAG}" + +if ! sudo zfs list -H "${BASE_DS}" >/dev/null 2>&1; then + sudo zfs create -o mountpoint="${BASE_MP}" "${BASE_DS}" +fi + +# ===== Fetch & extract base.txz ===== +if [ ! -f "${BASE_MP}/.seeded" ]; then + TMPDIR="$(mktemp -d)" + trap 'rm -rf "$TMPDIR"' EXIT + fetch -o "${TMPDIR}/base.txz" "https://download.freebsd.org/releases/amd64/${FREEBSD_REL}/base.txz" + sudo tar -xpf "${TMPDIR}/base.txz" -C "${BASE_MP}" + sudo touch "${BASE_MP}/.seeded" +fi + +# ===== Provide networking config to the chroot ===== +sudo mkdir -p "${BASE_MP}/etc" +# copy DNS + name service config from host so pkg can resolve names +sudo cp /etc/resolv.conf "${BASE_MP}/etc/resolv.conf" +[ -f /etc/hosts ] && sudo cp /etc/hosts "${BASE_MP}/etc/hosts" +[ -f /etc/nsswitch.conf ] && sudo cp /etc/nsswitch.conf "${BASE_MP}/etc/nsswitch.conf" + +# ===== Preinstall deps inside the tree ===== +sudo mount -t devfs devfs "${BASE_MP}/dev" +sudo mount -t fdescfs fdesc "${BASE_MP}/dev/fd" +sudo mount -t procfs proc "${BASE_MP}/proc" + +sudo chroot "${BASE_MP}" /bin/sh -lc " + set -e + env ASSUME_ALWAYS_YES=yes pkg bootstrap -f + mkdir -p /usr/local/etc/pkg/repos + echo 'FreeBSD: { url: \"${PKG_REPO_URL}\" }' > /usr/local/etc/pkg/repos/FreeBSD.conf + pkg update + pkg install -y ${PKGS} + mkdir -p /root/.ccache +" + +sudo umount -f "${BASE_MP}/proc" || true +sudo umount -f "${BASE_MP}/dev/fd" || true +sudo umount -f "${BASE_MP}/dev" || true + +# ===== Snapshot ===== +sudo zfs snapshot "${BASE_DS}@${SNAP_LABEL}" +echo "Snapshot created: ${BASE_DS}@${SNAP_LABEL}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 357942ea..30f0ddaf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -490,3 +490,34 @@ jobs: rm -rf dwarfs-*/ rm -f dwarfs-source-*.tar.zst rm -f dwarfs + +################################################################################ + + freebsd: + needs: package-source + + runs-on: + - self-hosted + - freebsd + - ${{ matrix.arch }} + + strategy: + matrix: + arch: + - amd64 + build_mode: + - Release + - Debug + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: 'false' + fetch-depth: '0' + ref: ${{ github.ref }} + + - name: Run Build + run: | + BUILD_MODE=${{ matrix.build_mode }} \ + sh .github/scripts/freebsd_jail_build.sh