You've already forked fruix-bootstrap
bootstrap: add builder preparation workflow
This commit is contained in:
@@ -38,6 +38,27 @@ Override the checkout location with:
|
||||
|
||||
- `FRUIX_CHANNEL_DIR=/path/to/fruix`
|
||||
|
||||
## Builder preparation
|
||||
|
||||
The main bootstrap entry point is:
|
||||
|
||||
- `./bootstrap/prepare-builder`
|
||||
|
||||
It prepares a reusable per-user builder environment under:
|
||||
|
||||
- `~/.local/opt/fruix-builder`
|
||||
|
||||
including:
|
||||
|
||||
- a local fixed Guile build
|
||||
- local Guile extension modules
|
||||
- a local Shepherd install
|
||||
- a Guix source checkout used by the current Fruix code
|
||||
|
||||
After that, `./bin/fruix` will automatically prefer that builder root if present.
|
||||
|
||||
On the current foreign-FreeBSD host path, preparing the builder is mostly a user-space action, but materializing full Fruix system artifacts may still require `sudo` because some host inputs are root-readable only and some system-oriented build steps still assume elevated access.
|
||||
|
||||
## Design docs
|
||||
|
||||
- `docs/bootstrap.md` — boundary definition
|
||||
|
||||
@@ -9,10 +9,37 @@ fi
|
||||
|
||||
fruix_channel_dir=${FRUIX_CHANNEL_DIR:-$default_channel_dir}
|
||||
fruix_channel_url=${FRUIX_CHANNEL_URL:-https://git.teralink.net/self/fruix.git}
|
||||
guix_source_dir=${GUIX_SOURCE_DIR:-"$HOME/repos/guix"}
|
||||
guile_bin=${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile}
|
||||
guile_extra_prefix=${GUILE_EXTRA_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install}
|
||||
shepherd_prefix=${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}
|
||||
builder_root=${FRUIX_BUILDER_ROOT:-$HOME/.local/opt/fruix-builder}
|
||||
legacy_guile_bin=/tmp/guile-freebsd-validate-install/bin/guile
|
||||
legacy_guile_extra_prefix=/tmp/guile-gnutls-freebsd-validate-install
|
||||
legacy_shepherd_prefix=/tmp/shepherd-freebsd-validate-install
|
||||
legacy_guix_source_dir=$HOME/repos/guix
|
||||
|
||||
if [ -x "$builder_root/guile/bin/guile" ]; then
|
||||
default_guile_bin=$builder_root/guile/bin/guile
|
||||
else
|
||||
default_guile_bin=$legacy_guile_bin
|
||||
fi
|
||||
if [ -d "$builder_root/guile-extra" ]; then
|
||||
default_guile_extra_prefix=$builder_root/guile-extra
|
||||
else
|
||||
default_guile_extra_prefix=$legacy_guile_extra_prefix
|
||||
fi
|
||||
if [ -d "$builder_root/shepherd" ]; then
|
||||
default_shepherd_prefix=$builder_root/shepherd
|
||||
else
|
||||
default_shepherd_prefix=$legacy_shepherd_prefix
|
||||
fi
|
||||
if [ -d "$builder_root/src/guix/guix" ]; then
|
||||
default_guix_source_dir=$builder_root/src/guix
|
||||
else
|
||||
default_guix_source_dir=$legacy_guix_source_dir
|
||||
fi
|
||||
|
||||
guix_source_dir=${GUIX_SOURCE_DIR:-$default_guix_source_dir}
|
||||
guile_bin=${GUILE_BIN:-$default_guile_bin}
|
||||
guile_extra_prefix=${GUILE_EXTRA_PREFIX:-$default_guile_extra_prefix}
|
||||
shepherd_prefix=${SHEPHERD_PREFIX:-$default_shepherd_prefix}
|
||||
script=$fruix_channel_dir/scripts/fruix.scm
|
||||
modules_dir=$fruix_channel_dir/modules
|
||||
|
||||
@@ -30,27 +57,29 @@ fi
|
||||
|
||||
if [ ! -x "$guile_bin" ]; then
|
||||
echo "Guile binary is not executable: $guile_bin" >&2
|
||||
echo "Run $bootstrap_root/bootstrap/prepare-builder.sh first, or set GUILE_BIN explicitly." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
guile_prefix=$(CDPATH= cd -- "$(dirname "$guile_bin")/.." && pwd)
|
||||
|
||||
ensure_built() {
|
||||
if [ ! -d "$guile_extra_prefix/share/guile/site" ] || \
|
||||
! GUILE_LOAD_PATH="$guile_extra_prefix/share/guile/site/3.0${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" \
|
||||
GUILE_LOAD_COMPILED_PATH="$guile_extra_prefix/lib/guile/3.0/site-ccache${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" \
|
||||
GUILE_EXTENSIONS_PATH="$guile_extra_prefix/lib/guile/3.0/extensions${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" \
|
||||
LD_LIBRARY_PATH="$guile_extra_prefix/lib:/tmp/guile-freebsd-validate-install/lib:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
|
||||
LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_prefix/lib:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
|
||||
"$guile_bin" -c '(catch #t (lambda () (use-modules (fibers)) (display "ok") (newline)) (lambda _ (display "missing") (newline)))' | grep -qx ok; then
|
||||
METADATA_OUT= ENV_OUT= "$bootstrap_root/tests/shepherd/build-local-guile-fibers.sh"
|
||||
METADATA_OUT= ENV_OUT= GUILE_BIN="$guile_bin" INSTALL_PREFIX="$guile_extra_prefix" "$bootstrap_root/tests/shepherd/build-local-guile-fibers.sh"
|
||||
fi
|
||||
|
||||
if [ ! -x "$shepherd_prefix/bin/shepherd" ] || [ ! -x "$shepherd_prefix/bin/herd" ]; then
|
||||
METADATA_OUT= ENV_OUT= GUILE_EXTRA_PREFIX="$guile_extra_prefix" "$bootstrap_root/tests/shepherd/build-local-shepherd.sh"
|
||||
METADATA_OUT= ENV_OUT= GUILE_BIN="$guile_bin" GUILE_EXTRA_PREFIX="$guile_extra_prefix" INSTALL_PREFIX="$shepherd_prefix" GUIX_SOURCE_DIR="$guix_source_dir" "$bootstrap_root/tests/shepherd/build-local-shepherd.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_built
|
||||
|
||||
guile_prefix=$(CDPATH= cd -- "$(dirname "$guile_bin")/.." && pwd)
|
||||
guile_lib_dir=$guile_prefix/lib
|
||||
|
||||
if [ -n "${GUILE_LOAD_PATH:-}" ]; then
|
||||
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
bootstrap_root=$(CDPATH= cd -- "$script_dir/.." && pwd)
|
||||
|
||||
guile_repo=${FRUIX_BOOTSTRAP_GUILE_REPO:-https://codeberg.org/guile/guile.git}
|
||||
guile_commit=${FRUIX_BOOTSTRAP_GUILE_COMMIT:-bbf2baa10f6cc8dfdd9e4ea14b503d748287a03d}
|
||||
install_prefix=${INSTALL_PREFIX:-$HOME/.local/opt/fruix-builder/guile}
|
||||
make_bin=${MAKE_BIN:-gmake}
|
||||
metadata_target=${METADATA_OUT:-}
|
||||
env_target=${ENV_OUT:-}
|
||||
jobs=${JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 4)}
|
||||
|
||||
for tool in git gm4 autoconf automake libtoolize autoreconf pkg-config "$make_bin" cc; do
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
echo "Required tool not found: $tool" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
cleanup=0
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-build-local-guile.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
cleanup_workdir() {
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap cleanup_workdir EXIT INT TERM
|
||||
|
||||
src_dir=$workdir/guile-src
|
||||
build_dir=$workdir/guile-build
|
||||
metadata_file=$workdir/local-guile-build-metadata.txt
|
||||
env_file=$workdir/local-guile-env.sh
|
||||
autogen_log=$workdir/autogen.log
|
||||
configure_log=$workdir/configure.log
|
||||
build_log=$workdir/build.log
|
||||
install_log=$workdir/install.log
|
||||
validation_log=$workdir/validation.log
|
||||
|
||||
rm -rf "$src_dir" "$build_dir"
|
||||
mkdir -p "$build_dir" "$install_prefix"
|
||||
|
||||
printf 'Cloning Guile from: %s\n' "$guile_repo"
|
||||
printf 'Checking out commit: %s\n' "$guile_commit"
|
||||
printf 'Install prefix: %s\n' "$install_prefix"
|
||||
printf 'Work directory: %s\n' "$workdir"
|
||||
|
||||
git clone "$guile_repo" "$src_dir" >"$workdir/clone.log" 2>&1
|
||||
(
|
||||
cd "$src_dir"
|
||||
git checkout "$guile_commit"
|
||||
) >"$workdir/checkout.log" 2>&1
|
||||
resolved_commit=$(git -C "$src_dir" rev-parse HEAD)
|
||||
|
||||
(
|
||||
cd "$src_dir"
|
||||
M4=gm4 ./autogen.sh
|
||||
) >"$autogen_log" 2>&1
|
||||
|
||||
(
|
||||
cd "$build_dir"
|
||||
env \
|
||||
M4=gm4 \
|
||||
MAKE="$make_bin" \
|
||||
PKG_CONFIG=pkg-config \
|
||||
PKG_CONFIG_PATH=/usr/local/libdata/pkgconfig:/usr/local/lib/pkgconfig \
|
||||
CPPFLAGS='-I/usr/local/include' \
|
||||
LDFLAGS="-L/usr/local/lib -Wl,-rpath,/usr/local/lib -Wl,-rpath,$install_prefix/lib" \
|
||||
"$src_dir/configure" \
|
||||
--prefix="$install_prefix" \
|
||||
--with-bdw-gc=bdw-gc-threaded \
|
||||
--with-libgmp-prefix=/usr/local \
|
||||
--with-libunistring-prefix=/usr/local \
|
||||
--with-libiconv-prefix=/usr/local \
|
||||
--with-libintl-prefix=/usr/local
|
||||
) >"$configure_log" 2>&1
|
||||
|
||||
(
|
||||
cd "$build_dir"
|
||||
"$make_bin" -j"$jobs"
|
||||
) >"$build_log" 2>&1
|
||||
|
||||
(
|
||||
cd "$build_dir"
|
||||
"$make_bin" install
|
||||
) >"$install_log" 2>&1
|
||||
|
||||
guile_bin=$install_prefix/bin/guile
|
||||
[ -x "$guile_bin" ] || {
|
||||
echo "Built Guile binary missing: $guile_bin" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
guile_version=$(LD_LIBRARY_PATH="$install_prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$guile_bin" --version | head -n 1)
|
||||
(
|
||||
EXPECT_GUILE_SUBPROCESS_CRASH=0 \
|
||||
GUILE_BIN="$guile_bin" \
|
||||
"$bootstrap_root/tests/guile/run-subprocess-diagnostics.sh"
|
||||
) >"$validation_log" 2>&1
|
||||
|
||||
cat >"$env_file" <<EOF
|
||||
export GUILE_BIN='$guile_bin'
|
||||
export GUILE_PREFIX='$install_prefix'
|
||||
export LD_LIBRARY_PATH='$install_prefix/lib:/usr/local/lib'
|
||||
EOF
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
guile_repo=$guile_repo
|
||||
requested_commit=$guile_commit
|
||||
resolved_commit=$resolved_commit
|
||||
install_prefix=$install_prefix
|
||||
guile_bin=$guile_bin
|
||||
guile_version=$guile_version
|
||||
jobs=$jobs
|
||||
autogen_log=$autogen_log
|
||||
configure_log=$configure_log
|
||||
build_log=$build_log
|
||||
install_log=$install_log
|
||||
validation_log=$validation_log
|
||||
env_file=$env_file
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
if [ -n "$env_target" ]; then
|
||||
mkdir -p "$(dirname "$env_target")"
|
||||
cp "$env_file" "$env_target"
|
||||
fi
|
||||
|
||||
printf 'PASS local-guile-build\n'
|
||||
printf 'Environment file: %s\n' "$env_file"
|
||||
printf 'Metadata file: %s\n' "$metadata_file"
|
||||
printf '%s\n' '--- metadata ---'
|
||||
cat "$metadata_file"
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
pkg_cmd=${PKG_CMD:-pkg}
|
||||
sudo_cmd=${SUDO_CMD:-sudo}
|
||||
packages=${FRUIX_BOOTSTRAP_PACKAGES:-"autoconf automake libtool gettext-tools texinfo help2man gperf pkgconf m4 git gmake bash boehm-gc-threaded libffi gmp libunistring libiconv sqlite3 libgcrypt lzlib libgit2 gnutls ca_root_nss gsed argp-standalone"}
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
install_cmd="$pkg_cmd install -y"
|
||||
else
|
||||
command -v "$sudo_cmd" >/dev/null 2>&1 || {
|
||||
echo "sudo is required to install host packages as a non-root user" >&2
|
||||
exit 1
|
||||
}
|
||||
install_cmd="$sudo_cmd $pkg_cmd install -y"
|
||||
fi
|
||||
|
||||
printf 'Installing Fruix bootstrap host packages:\n'
|
||||
for package in $packages; do
|
||||
printf ' %s\n' "$package"
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
$install_cmd $packages
|
||||
|
||||
printf 'PASS install-host-deps\n'
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
exec "$script_dir/prepare-builder.sh" "$@"
|
||||
Executable
+255
@@ -0,0 +1,255 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
bootstrap_root=$(CDPATH= cd -- "$script_dir/.." && pwd)
|
||||
|
||||
builder_root_default=$HOME/.local/opt/fruix-builder
|
||||
builder_root=${FRUIX_BUILDER_ROOT:-$builder_root_default}
|
||||
guile_prefix=${FRUIX_BUILDER_GUILE_PREFIX:-$builder_root/guile}
|
||||
guile_extra_prefix=${FRUIX_BUILDER_GUILE_EXTRA_PREFIX:-$builder_root/guile-extra}
|
||||
shepherd_prefix=${FRUIX_BUILDER_SHEPHERD_PREFIX:-$builder_root/shepherd}
|
||||
guix_source_dir=${GUIX_SOURCE_DIR:-$builder_root/src/guix}
|
||||
guix_source_url=${GUIX_SOURCE_URL:-https://git.teralink.net/tribes/guix.git}
|
||||
guile_repo=${FRUIX_BOOTSTRAP_GUILE_REPO:-https://codeberg.org/guile/guile.git}
|
||||
guile_commit=${FRUIX_BOOTSTRAP_GUILE_COMMIT:-bbf2baa10f6cc8dfdd9e4ea14b503d748287a03d}
|
||||
fruix_channel_url=${FRUIX_CHANNEL_URL:-https://git.teralink.net/self/fruix.git}
|
||||
install_host_deps=1
|
||||
jobs=${JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 4)}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: prepare-builder.sh [OPTIONS]
|
||||
|
||||
Options:
|
||||
--builder-root DIR Builder root (default: $builder_root_default)
|
||||
--guix-source-dir DIR Guix source checkout location
|
||||
--guix-source-url URL Guix source repository URL
|
||||
--guile-repo URL Guile git repository URL
|
||||
--guile-commit COMMIT Guile git commit to build
|
||||
--skip-host-deps Do not run pkg installation
|
||||
--help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--builder-root)
|
||||
builder_root=$2
|
||||
shift 2
|
||||
;;
|
||||
--guix-source-dir)
|
||||
guix_source_dir=$2
|
||||
shift 2
|
||||
;;
|
||||
--guix-source-url)
|
||||
guix_source_url=$2
|
||||
shift 2
|
||||
;;
|
||||
--guile-repo)
|
||||
guile_repo=$2
|
||||
shift 2
|
||||
;;
|
||||
--guile-commit)
|
||||
guile_commit=$2
|
||||
shift 2
|
||||
;;
|
||||
--skip-host-deps)
|
||||
install_host_deps=0
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
guile_prefix=$builder_root/guile
|
||||
guile_extra_prefix=$builder_root/guile-extra
|
||||
shepherd_prefix=$builder_root/shepherd
|
||||
|
||||
state_dir=$builder_root/state
|
||||
logs_dir=$builder_root/logs
|
||||
mkdir -p "$builder_root" "$state_dir" "$logs_dir"
|
||||
metadata_file=$builder_root/prepare-builder-metadata.txt
|
||||
env_file=$builder_root/env.sh
|
||||
|
||||
have_working_guile() {
|
||||
[ -x "$guile_prefix/bin/guile" ] || return 1
|
||||
EXPECT_GUILE_SUBPROCESS_CRASH=0 \
|
||||
GUILE_BIN="$guile_prefix/bin/guile" \
|
||||
"$bootstrap_root/tests/guile/run-subprocess-diagnostics.sh" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
guile_module_probe() {
|
||||
module_code=$1
|
||||
[ -x "$guile_prefix/bin/guile" ] || return 1
|
||||
guile_bin=$guile_prefix/bin/guile
|
||||
guile_version=$(LD_LIBRARY_PATH="$guile_prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$guile_bin" -c '(display (effective-version))')
|
||||
site_dir=$guile_extra_prefix/share/guile/site/$guile_version
|
||||
site_ccache_dir=$guile_extra_prefix/lib/guile/$guile_version/site-ccache
|
||||
extensions_dir=$guile_extra_prefix/lib/guile/$guile_version/extensions
|
||||
gui_load_path=${site_dir}
|
||||
gui_load_compiled_path=${site_ccache_dir}
|
||||
gui_extensions_path=${extensions_dir}
|
||||
env \
|
||||
LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_prefix/lib:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
|
||||
GUILE_LOAD_PATH="$gui_load_path" \
|
||||
GUILE_LOAD_COMPILED_PATH="$gui_load_compiled_path" \
|
||||
GUILE_EXTENSIONS_PATH="$gui_extensions_path" \
|
||||
"$guile_bin" -c "$module_code" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
have_shepherd() {
|
||||
[ -x "$shepherd_prefix/bin/shepherd" ] || return 1
|
||||
[ -x "$shepherd_prefix/bin/herd" ] || return 1
|
||||
guile_module_probe '(use-modules (fibers) (shepherd service)) (display "ok") (newline)'
|
||||
}
|
||||
|
||||
run_step() {
|
||||
name=$1
|
||||
shift
|
||||
printf '\n== %s ==\n' "$name"
|
||||
"$@"
|
||||
}
|
||||
|
||||
if [ "$install_host_deps" -eq 1 ]; then
|
||||
run_step install-host-deps "$script_dir/install-host-deps.sh"
|
||||
fi
|
||||
|
||||
if have_working_guile; then
|
||||
printf '\nUsing existing working Guile at %s\n' "$guile_prefix/bin/guile"
|
||||
else
|
||||
run_step build-local-guile env \
|
||||
FRUIX_BOOTSTRAP_GUILE_REPO="$guile_repo" \
|
||||
FRUIX_BOOTSTRAP_GUILE_COMMIT="$guile_commit" \
|
||||
INSTALL_PREFIX="$guile_prefix" \
|
||||
JOBS="$jobs" \
|
||||
METADATA_OUT="$logs_dir/local-guile-build-metadata.txt" \
|
||||
ENV_OUT="$logs_dir/local-guile-env.sh" \
|
||||
"$script_dir/build-local-guile.sh"
|
||||
fi
|
||||
|
||||
run_step setup-guix-source env \
|
||||
GUIX_SOURCE_URL="$guix_source_url" \
|
||||
GUIX_SOURCE_DIR="$guix_source_dir" \
|
||||
METADATA_OUT="$logs_dir/guix-source-metadata.txt" \
|
||||
"$script_dir/setup-guix-source.sh"
|
||||
|
||||
if guile_module_probe '(use-modules (gnutls)) (display "ok") (newline)'; then
|
||||
printf '\nGuile (gnutls) already present in %s\n' "$guile_extra_prefix"
|
||||
else
|
||||
run_step build-local-guile-gnutls env \
|
||||
GUILE_BIN="$guile_prefix/bin/guile" \
|
||||
INSTALL_PREFIX="$guile_extra_prefix" \
|
||||
GUIX_SOURCE_DIR="$guix_source_dir" \
|
||||
JOBS="$jobs" \
|
||||
"$bootstrap_root/tests/guix/build-local-guile-gnutls.sh"
|
||||
fi
|
||||
|
||||
if guile_module_probe '(use-modules (bytestructures guile) (git)) (display "ok") (newline)'; then
|
||||
printf '\nGuile-Git stack already present in %s\n' "$guile_extra_prefix"
|
||||
else
|
||||
run_step build-local-guile-git env \
|
||||
GUILE_BIN="$guile_prefix/bin/guile" \
|
||||
INSTALL_PREFIX="$guile_extra_prefix" \
|
||||
JOBS="$jobs" \
|
||||
"$bootstrap_root/tests/guix/build-local-guile-git.sh"
|
||||
fi
|
||||
|
||||
if guile_module_probe '(use-modules (json)) (display "ok") (newline)'; then
|
||||
printf '\nGuile-JSON already present in %s\n' "$guile_extra_prefix"
|
||||
else
|
||||
run_step build-local-guile-json env \
|
||||
GUILE_BIN="$guile_prefix/bin/guile" \
|
||||
INSTALL_PREFIX="$guile_extra_prefix" \
|
||||
GUIX_SOURCE_DIR="$guix_source_dir" \
|
||||
JOBS="$jobs" \
|
||||
"$bootstrap_root/tests/guix/build-local-guile-json.sh"
|
||||
fi
|
||||
|
||||
if guile_module_probe '(use-modules (sqlite3) (gcrypt hash) (lzlib) (semver)) (display "ok") (newline)'; then
|
||||
printf '\nGuile configure-time dependency modules already present in %s\n' "$guile_extra_prefix"
|
||||
else
|
||||
run_step build-local-guile-configure-deps env \
|
||||
GUILE_BIN="$guile_prefix/bin/guile" \
|
||||
INSTALL_PREFIX="$guile_extra_prefix" \
|
||||
GUIX_SOURCE_DIR="$guix_source_dir" \
|
||||
JOBS="$jobs" \
|
||||
"$bootstrap_root/tests/guix/build-local-guile-configure-deps.sh"
|
||||
fi
|
||||
|
||||
if guile_module_probe '(use-modules (fibers)) (display "ok") (newline)'; then
|
||||
printf '\nGuile Fibers already present in %s\n' "$guile_extra_prefix"
|
||||
else
|
||||
run_step build-local-guile-fibers env \
|
||||
GUILE_BIN="$guile_prefix/bin/guile" \
|
||||
INSTALL_PREFIX="$guile_extra_prefix" \
|
||||
JOBS="$jobs" \
|
||||
"$bootstrap_root/tests/shepherd/build-local-guile-fibers.sh"
|
||||
fi
|
||||
|
||||
if have_shepherd; then
|
||||
printf '\nShepherd already present in %s\n' "$shepherd_prefix"
|
||||
else
|
||||
run_step build-local-shepherd env \
|
||||
GUILE_BIN="$guile_prefix/bin/guile" \
|
||||
GUILE_EXTRA_PREFIX="$guile_extra_prefix" \
|
||||
INSTALL_PREFIX="$shepherd_prefix" \
|
||||
GUIX_SOURCE_DIR="$guix_source_dir" \
|
||||
JOBS="$jobs" \
|
||||
"$bootstrap_root/tests/shepherd/build-local-shepherd.sh"
|
||||
fi
|
||||
|
||||
guile_bin=$guile_prefix/bin/guile
|
||||
guile_version=$(LD_LIBRARY_PATH="$guile_prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$guile_bin" -c '(display (effective-version))')
|
||||
site_dir=$guile_extra_prefix/share/guile/site/$guile_version
|
||||
site_ccache_dir=$guile_extra_prefix/lib/guile/$guile_version/site-ccache
|
||||
extensions_dir=$guile_extra_prefix/lib/guile/$guile_version/extensions
|
||||
guix_commit=$(git -C "$guix_source_dir" rev-parse HEAD)
|
||||
|
||||
cat >"$env_file" <<EOF
|
||||
export FRUIX_BUILDER_ROOT='$builder_root'
|
||||
export GUILE_BIN='$guile_bin'
|
||||
export GUILE_PREFIX='$guile_prefix'
|
||||
export GUILE_EXTRA_PREFIX='$guile_extra_prefix'
|
||||
export SHEPHERD_PREFIX='$shepherd_prefix'
|
||||
export GUIX_SOURCE_DIR='$guix_source_dir'
|
||||
export FRUIX_CHANNEL_URL='$fruix_channel_url'
|
||||
export GUILE_AUTO_COMPILE=0
|
||||
export LD_LIBRARY_PATH='$guile_extra_prefix/lib:$guile_prefix/lib:/usr/local/lib'
|
||||
export GUILE_LOAD_PATH='$site_dir'
|
||||
export GUILE_LOAD_COMPILED_PATH='$site_ccache_dir'
|
||||
export GUILE_EXTENSIONS_PATH='$extensions_dir'
|
||||
EOF
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
builder_root=$builder_root
|
||||
guile_repo=$guile_repo
|
||||
guile_commit=$guile_commit
|
||||
guile_bin=$guile_bin
|
||||
guile_prefix=$guile_prefix
|
||||
guile_extra_prefix=$guile_extra_prefix
|
||||
shepherd_prefix=$shepherd_prefix
|
||||
guile_version=$guile_version
|
||||
guix_source_url=$guix_source_url
|
||||
guix_source_dir=$guix_source_dir
|
||||
guix_source_commit=$guix_commit
|
||||
env_file=$env_file
|
||||
logs_dir=$logs_dir
|
||||
EOF
|
||||
|
||||
printf '\nPASS prepare-builder\n'
|
||||
printf 'Environment file: %s\n' "$env_file"
|
||||
printf 'Metadata file: %s\n' "$metadata_file"
|
||||
printf '\nTo use Fruix from bootstrap or the canonical checkout, you can run:\n'
|
||||
printf ' . %s\n' "$env_file"
|
||||
printf '\nOr rely on the default builder root detection in ./bin/fruix.\n'
|
||||
printf '%s\n' '--- metadata ---'
|
||||
cat "$metadata_file"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
guix_source_url=${GUIX_SOURCE_URL:-https://git.teralink.net/tribes/guix.git}
|
||||
guix_source_dir=${GUIX_SOURCE_DIR:-$HOME/.local/opt/fruix-builder/src/guix}
|
||||
metadata_target=${METADATA_OUT:-}
|
||||
|
||||
command -v git >/dev/null 2>&1 || {
|
||||
echo "git is required" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdir -p "$(dirname "$guix_source_dir")"
|
||||
|
||||
if [ -d "$guix_source_dir/guix" ]; then
|
||||
printf 'Using existing Guix source tree: %s\n' "$guix_source_dir"
|
||||
else
|
||||
rm -rf "$guix_source_dir"
|
||||
printf 'Cloning Guix source from: %s\n' "$guix_source_url"
|
||||
git clone "$guix_source_url" "$guix_source_dir" >"$guix_source_dir.clone.log" 2>&1
|
||||
fi
|
||||
|
||||
resolved_commit=$(git -C "$guix_source_dir" rev-parse HEAD)
|
||||
metadata_file=$guix_source_dir/.fruix-bootstrap-guix-source.txt
|
||||
cat >"$metadata_file" <<EOF
|
||||
guix_source_url=$guix_source_url
|
||||
guix_source_dir=$guix_source_dir
|
||||
guix_source_commit=$resolved_commit
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS setup-guix-source\n'
|
||||
printf '%s\n' '--- metadata ---'
|
||||
cat "$metadata_file"
|
||||
@@ -0,0 +1,167 @@
|
||||
# Bootstrap builder preparation and fresh-user validation on FreeBSD
|
||||
|
||||
Date: 2026-04-07
|
||||
|
||||
## Goal
|
||||
|
||||
Turn `fruix-bootstrap` into a real **Fruix builder** entry point on a plain FreeBSD host:
|
||||
|
||||
- install required host packages
|
||||
- build a fixed local Guile into a reusable per-user prefix
|
||||
- build the required local Guile extension stack
|
||||
- build Shepherd into a reusable per-user prefix
|
||||
- prepare the Guix source checkout required by the current Fruix code
|
||||
- then validate the whole path from a fresh non-privileged user account to a built Fruix installer ISO from the canonical `fruix` checkout
|
||||
|
||||
## What was added
|
||||
|
||||
Bootstrap entrypoints:
|
||||
|
||||
- `bootstrap/prepare-builder`
|
||||
- `bootstrap/prepare-builder.sh`
|
||||
- `bootstrap/install-host-deps.sh`
|
||||
- `bootstrap/build-local-guile.sh`
|
||||
- `bootstrap/setup-guix-source.sh`
|
||||
|
||||
Validation harness:
|
||||
|
||||
- `tests/bootstrap/run-prepare-builder-fresh-user.sh`
|
||||
|
||||
Wrapper behavior updates:
|
||||
|
||||
- `bin/fruix` in `fruix-bootstrap` now prefers a prepared builder root at:
|
||||
- `~/.local/opt/fruix-builder`
|
||||
- `bin/fruix` in the canonical `fruix` checkout now prefers the same prepared builder root
|
||||
- bootstrap still defaults to the sibling canonical checkout:
|
||||
- `../fruix`
|
||||
- bootstrap still records the original Fruix channel URL as:
|
||||
- `https://git.teralink.net/self/fruix.git`
|
||||
|
||||
## Builder layout
|
||||
|
||||
The new default reusable per-user builder root is:
|
||||
|
||||
- `~/.local/opt/fruix-builder`
|
||||
|
||||
Current prepared content includes:
|
||||
|
||||
- `guile`
|
||||
- `guile-extra`
|
||||
- `shepherd`
|
||||
- `src/guix`
|
||||
- `env.sh`
|
||||
- `prepare-builder-metadata.txt`
|
||||
- `logs/...`
|
||||
|
||||
## Current bootstrap assumptions
|
||||
|
||||
`prepare-builder` currently installs host packages with `pkg`, then prepares the local builder stack.
|
||||
|
||||
Representative package set includes:
|
||||
|
||||
- autotools and related tools
|
||||
- `git`
|
||||
- `gmake`
|
||||
- `pkgconf`
|
||||
- `m4`
|
||||
- `boehm-gc-threaded`
|
||||
- `libffi`
|
||||
- `gmp`
|
||||
- `libunistring`
|
||||
- `libiconv`
|
||||
- `sqlite3`
|
||||
- `libgcrypt`
|
||||
- `lzlib`
|
||||
- `libgit2`
|
||||
- `gnutls`
|
||||
- `gsed`
|
||||
- `argp-standalone`
|
||||
|
||||
The fixed local Guile build is currently sourced from:
|
||||
|
||||
- repo: `https://codeberg.org/guile/guile.git`
|
||||
- commit: `bbf2baa10f6cc8dfdd9e4ea14b503d748287a03d`
|
||||
|
||||
The Guix source checkout currently defaults to:
|
||||
|
||||
- `https://git.teralink.net/tribes/guix.git`
|
||||
|
||||
## Validation performed
|
||||
|
||||
A fresh local user account was created on the current FreeBSD host:
|
||||
|
||||
- user: `fruixbuilder`
|
||||
|
||||
The harness:
|
||||
|
||||
1. created the fresh user
|
||||
2. copied in sibling working checkouts:
|
||||
- `fruix-bootstrap`
|
||||
- `fruix`
|
||||
3. ran as that user:
|
||||
- `./bootstrap/prepare-builder`
|
||||
4. verified bootstrap `./bin/fruix --help`
|
||||
5. ran as that user, using `sudo` for the host-facing system build step:
|
||||
- `./bin/fruix system installer-iso tests/system/phase7-minimal-operating-system.scm ...`
|
||||
6. verified the resulting installer ISO metadata and output files
|
||||
|
||||
## Why the final build used sudo
|
||||
|
||||
On the current foreign-FreeBSD host path, a pure unprivileged system build still trips over root-readable host inputs such as:
|
||||
|
||||
- `/etc/defaults/devfs.rules`
|
||||
|
||||
So the builder preparation itself is mostly user-space, but the final host-oriented system artifact materialization still currently uses `sudo`.
|
||||
|
||||
That matches the present bootstrap reality and does not change the main boundary:
|
||||
|
||||
- bootstrap prepares the builder
|
||||
- canonical Fruix defines the system artifact logic
|
||||
|
||||
## Result
|
||||
|
||||
Passing validation:
|
||||
|
||||
- `PASS prepare-builder-fresh-user`
|
||||
|
||||
Representative metadata:
|
||||
|
||||
```text
|
||||
workdir=/tmp/current-prepare-builder-fresh-user
|
||||
test_user=fruixbuilder
|
||||
test_home=/home/fruixbuilder
|
||||
builder_root=/home/fruixbuilder/.local/opt/fruix-builder
|
||||
bootstrap_checkout=/home/fruixbuilder/fruix-bootstrap
|
||||
fruix_checkout=/home/fruixbuilder/fruix
|
||||
store_dir=/home/fruixbuilder/frx/store
|
||||
iso_store_path=/home/fruixbuilder/frx/store/90b5c64eb948e6f333aa375c07e102556ebf53fc-fruix-installer-iso-fruix-freebsd-installer
|
||||
iso_image=/home/fruixbuilder/frx/store/90b5c64eb948e6f333aa375c07e102556ebf53fc-fruix-installer-iso-fruix-freebsd-installer/installer.iso
|
||||
installer_closure_path=/home/fruixbuilder/frx/store/82e1a0d2b2083ef66fd0c84683d9c07c27465e66-fruix-system-fruix-freebsd-installer
|
||||
target_closure_path=/home/fruixbuilder/frx/store/dad9f6bd7428725cefc039f2466691627c77522f-fruix-system-fruix-freebsd
|
||||
```
|
||||
|
||||
## Meaning
|
||||
|
||||
This is the first real validation of the new split direction:
|
||||
|
||||
- `fruix-bootstrap` can now act as a real builder-preparation layer
|
||||
- the canonical `fruix` checkout is the logic actually used to materialize the Fruix artifact
|
||||
- a fresh user on a normal FreeBSD host can prepare the builder and produce a Fruix installer ISO from the canonical checkout
|
||||
|
||||
That is the right shape for the new boundary:
|
||||
|
||||
- plain FreeBSD
|
||||
- `fruix-bootstrap`
|
||||
- Fruix builder
|
||||
- canonical `fruix`
|
||||
- built Fruix artifact
|
||||
|
||||
## Remaining limitations
|
||||
|
||||
Current limitations remain explicit:
|
||||
|
||||
- the current Fruix code still depends on a Guix source checkout during host-side evaluation
|
||||
- host-facing system artifact builds still currently use `sudo` on foreign FreeBSD
|
||||
- this is not yet the final polished installer UX or TUI installer
|
||||
|
||||
But the builder split itself is now real and validated.
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
bootstrap_src=$(CDPATH= cd -- "$script_dir/../.." && pwd)
|
||||
fruix_src=${FRUIX_SRC:-$bootstrap_src/../fruix}
|
||||
test_user=${TEST_USER:-fruixbuilder}
|
||||
test_home=${TEST_HOME:-/home/$test_user}
|
||||
builder_root=${FRUIX_BUILDER_ROOT:-$test_home/.local/opt/fruix-builder}
|
||||
store_dir=${STORE_DIR:-$test_home/frx/store}
|
||||
install_target_device=${INSTALL_TARGET_DEVICE:-/dev/vtbd0}
|
||||
root_size=${ROOT_SIZE:-6g}
|
||||
keep_user=${KEEP_USER:-0}
|
||||
|
||||
[ -d "$fruix_src" ] || {
|
||||
echo "Canonical fruix checkout not found: $fruix_src" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v sudo >/dev/null 2>&1 || {
|
||||
echo "sudo is required" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup=0
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
sudo mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-bootstrap-fresh-user.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
sudoers_dir=/usr/local/etc/sudoers.d
|
||||
sudoers_file=$sudoers_dir/fruix-bootstrap-$test_user
|
||||
prepare_log=$workdir/prepare-builder.log
|
||||
help_log=$workdir/fruix-help.txt
|
||||
installer_log=$workdir/installer-iso.txt
|
||||
metadata_file=$workdir/fresh-user-metadata.txt
|
||||
|
||||
cleanup_all() {
|
||||
if [ "$keep_user" -ne 1 ]; then
|
||||
sudo rm -f "$sudoers_file" >/dev/null 2>&1 || true
|
||||
if id "$test_user" >/dev/null 2>&1; then
|
||||
sudo pkill -u "$test_user" >/dev/null 2>&1 || true
|
||||
sudo pw userdel "$test_user" -r >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
sudo rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap cleanup_all EXIT INT TERM
|
||||
|
||||
run_as_test_user() {
|
||||
sudo -H -u "$test_user" env HOME="$test_home" "$@"
|
||||
}
|
||||
|
||||
copy_tree() {
|
||||
src=$1
|
||||
dst=$2
|
||||
sudo rm -rf "$dst"
|
||||
sudo mkdir -p "$dst"
|
||||
(cd "$src" && tar -cf - .) | sudo tar -C "$dst" -xf -
|
||||
sudo chown -R "$test_user":"$test_user" "$dst"
|
||||
}
|
||||
|
||||
if id "$test_user" >/dev/null 2>&1; then
|
||||
sudo pkill -u "$test_user" >/dev/null 2>&1 || true
|
||||
sudo pw userdel "$test_user" -r >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
sudo pw useradd "$test_user" -m -s /bin/sh -G wheel
|
||||
sudo mkdir -p "$sudoers_dir"
|
||||
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$test_user" | sudo tee "$sudoers_file" >/dev/null
|
||||
sudo chmod 440 "$sudoers_file"
|
||||
sudo chown -R "$test_user":"$test_user" "$workdir"
|
||||
|
||||
copy_tree "$bootstrap_src" "$test_home/fruix-bootstrap"
|
||||
copy_tree "$fruix_src" "$test_home/fruix"
|
||||
run_as_test_user mkdir -p "$store_dir"
|
||||
|
||||
run_as_test_user sh -lc "cd '$test_home/fruix-bootstrap' && ./bootstrap/prepare-builder --builder-root '$builder_root' >'$prepare_log' 2>&1"
|
||||
run_as_test_user sh -lc "cd '$test_home/fruix-bootstrap' && ./bin/fruix --help >'$help_log' 2>&1"
|
||||
run_as_test_user sh -lc "cd '$test_home/fruix-bootstrap' && sudo env HOME='$test_home' FRUIX_BUILDER_ROOT='$builder_root' FRUIX_FREEBSD_BUILD_JOBS='${FRUIX_FREEBSD_BUILD_JOBS:-8}' ./bin/fruix system installer-iso tests/system/phase7-minimal-operating-system.scm --system phase7-operating-system --store '$store_dir' --root-size '$root_size' --install-target-device '$install_target_device' >'$installer_log' 2>&1"
|
||||
|
||||
field() {
|
||||
sed -n "s/^$1=//p" "$installer_log" | tail -n 1
|
||||
}
|
||||
|
||||
iso_store_path=$(field iso_store_path)
|
||||
iso_image=$(field iso_image)
|
||||
installer_closure_path=$(field installer_closure_path)
|
||||
target_closure_path=$(field target_closure_path)
|
||||
store_dir_out=$(field store_dir)
|
||||
|
||||
[ -d "$iso_store_path" ] || {
|
||||
echo "missing iso_store_path: $iso_store_path" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$iso_image" ] || {
|
||||
echo "missing iso_image: $iso_image" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -n "$installer_closure_path" ] || {
|
||||
echo "missing installer_closure_path" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -n "$target_closure_path" ] || {
|
||||
echo "missing target_closure_path" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$store_dir_out" = "$store_dir" ] || {
|
||||
echo "unexpected store_dir in output: $store_dir_out" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
sudo chown -R "$(id -un)":"$(id -gn)" "$workdir"
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
test_user=$test_user
|
||||
test_home=$test_home
|
||||
builder_root=$builder_root
|
||||
bootstrap_checkout=$test_home/fruix-bootstrap
|
||||
fruix_checkout=$test_home/fruix
|
||||
store_dir=$store_dir
|
||||
prepare_log=$prepare_log
|
||||
help_log=$help_log
|
||||
installer_log=$installer_log
|
||||
iso_store_path=$iso_store_path
|
||||
iso_image=$iso_image
|
||||
installer_closure_path=$installer_closure_path
|
||||
target_closure_path=$target_closure_path
|
||||
EOF
|
||||
|
||||
printf 'PASS prepare-builder-fresh-user\n'
|
||||
printf 'Metadata file: %s\n' "$metadata_file"
|
||||
printf '%s\n' '--- metadata ---'
|
||||
cat "$metadata_file"
|
||||
Reference in New Issue
Block a user