xo-umbrella2/xo-cmake/bin/xo-build.in
Roland Conybeare 6dceb85132
Some checks failed
CI / smoke-test (push) Has been cancelled
cmake-docker / cmake-build (push) Has been cancelled
build: xo-build --utest auto re-runs w/ output on fail [UX] [CI]
2026-07-30 09:39:14 -04:00

584 lines
16 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
$0 [-u|--usage|-h|--help]
[--list] [-n|--dry-run] [--xoname=NAME]
[--repo
|--add-umbrella-remote
|--split-umbrella-remote
|--pull-umbrella-remote
|--push-umbrella-remote]
[--clone|--configure|--build|--install]
[--with-examples] [--with-utests] [--with-asm] [--debug-build]
[--with-opengl=GLFLAG] [--with-vulkan=VKFLAG]
[-S=SOURCEDIR] [-B=BUILDDIR] [-j N|--jobs=N]
[--all | [--with-deps] NAME ...]
EOF
}
help() {
usage
cat <<EOF
fetch and/or build xo component libraries
Options:
-u | --usage brief help message
-h | --help this help message
--list list known xo libraries
-n | --dry-run dry-run: don't actually invoke state-changing commands
--xoname=NAME operate on xo subsystem NAME
--all operate on every subsystem from --list, in dependency
order (bottom-up), excluding the xo-cmake bootstrap.
The chosen operations (e.g. --configure --build --utest
--install) run for each subsystem before moving to the
next, so a whole-umbrella cmake build is one command.
Multiple NAMEs may also be given explicitly instead.
--with-deps expand the named subsystem(s) to their transitive
dependency closure (using subsystem-edges) and operate
on the whole closure in bottom-up order. Use this to
build a subsystem from scratch (e.g. in a fresh docker
container) where its dependencies are not yet installed.
Ignored with --all.
--clone git clone xo library NAME in current directory
--configure run cmake for xo library NAME in immediate subdir
Will use NAME/.build as build directory
--build run cmake build for xo library NAME in immediate subdir
--build-docs build documentation for xo library NAME in immediate subdir
--utest run unit tests for xo library NAME
--install run cmake install for xo library NAME in immediate subdir
--realclean nuke build directory
-S=SOURCEDIR override path/to/source
-B=BUILDDIR override path/to/build
-j N | --jobs=N build with N parallel compile jobs (cmake --build -j N).
Default: min(nproc, RAM/~1.5GB), to avoid OOM-killing
the compiler on memory-limited machines. Takes
precedence over the XO_BUILD_JOBS environment variable.
--with-examples in configure step, set -DXO_ENABLE_EXAMPLES=1
--with-utests in configure step, set -DENABLE_TESTING=1
--with-asm in configure step, set -DXO_ENABLE_ASM=1
--debug-build in configure step, set -DCMAKE_BUILD_TYPE=Debug
--with-opengl=GLFLAG in configure step, set -DXO_ENABLE_OPENGL=GLFLAG [ON]
--with-vulkan=VKFLAG in configure step, set -DXO_ENABLE_VULKAN=VKFLAG [OFF]
--repo report github repo for NAME
--add-umbrella-remote (in umbrella sandbox) add remote for xo library NAME
--split-umbrella-remote (in umbrella sandbox) generate split branch for xo library NAME
--pull-umbrella-remote (in umbrella sandbox) pull satellite remote for xo library NAME
--push-umbrella-remote (in umbrella sandbox) push satellite remote for xo library NAME
EOF
}
cmd=
noop_flag=0
xoname=
clone_flag=0
configure_flag=0
build_flag=0
build_docs_flag=0
utest_flag=0
install_flag=0
realclean_flag=0
pathtosource=
pathtobuild=
jobs_arg=
with_examples=0
with_utests=0
with_asm=0
debug_build=0
with_opengl=
with_vulkan=
repo_flag=0
add_umbrella_remote_flag=0
split_umbrella_remote_flag=0
pull_umbrella_remote_flag=0
push_umbrella_remote_flag=0
all_flag=0
with_deps_flag=0
xonames=()
while [[ $# > 0 ]]; do
case "$1" in
-u | --usage)
cmd='usage'
;;
-h | --help)
cmd='help'
;;
-n | --dry-run)
noop_flag=1
;;
-S)
shift
pathtosource=$1
;;
-S=*)
pathtosource="${1#*=}"
;;
-B)
shift
pathtobuild=$1
;;
-B=*)
pathtobuild="${1#*=}"
;;
-j)
shift
jobs_arg=$1
;;
-j=*)
jobs_arg="${1#*=}"
;;
--jobs)
shift
jobs_arg=$1
;;
--jobs=*)
jobs_arg="${1#*=}"
;;
--list)
cmd='list'
;;
--all)
all_flag=1
;;
--with-deps)
with_deps_flag=1
;;
--xoname=*)
xoname="${1#*=}"
xonames+=("${1#*=}")
;;
--clone)
clone_flag=1
;;
--configure|--config)
configure_flag=1
;;
--build)
build_flag=1
;;
--build-docs)
build_docs_flag=1
;;
--utest)
utest_flag=1
;;
--install)
install_flag=1
;;
--realclean)
realclean_flag=1
;;
--with-examples)
with_examples=1
;;
--with-utests)
with_utests=1
;;
--with-asm)
with_asm=1
;;
--debug-build)
debug_build=1
;;
--with-opengl=*)
with_opengl="${1#*=}"
;;
--with-vulkan=*)
with_vulkan="${1#*=}"
;;
--repo)
repo_flag=1
;;
--add-umbrella-remote)
add_umbrella_remote_flag=1
;;
--split-umbrella-remote)
split_umbrella_remote_flag=1
;;
--pull-umbrella-remote)
pull_umbrella_remote_flag=1
;;
--push-umbrella-remote)
push_umbrella_remote_flag=1
;;
xo-*)
xoname="$1"
xonames+=("$1")
;;
*)
usage
exit 1
;;
esac
shift
done
#echo xoname=$xoname pathtosource=$pathtosource pathtobuild=$pathtobuild
# -S/-B overrides (if given) apply per-subsystem below; when empty the
# source/build dirs are derived from each subsystem name in the loop.
orig_pathtosource="$pathtosource"
orig_pathtobuild="$pathtobuild"
# compile parallelism (jobs passed to cmake --build -j). Precedence:
# 1. -j N / --jobs=N command-line argument
# 2. XO_BUILD_JOBS environment variable
# 3. memory-aware default (below)
#
# A bare "cmake --build -j" passes a no-count -j to make, which runs UNLIMITED
# parallel jobs. On a template-heavy subsystem (e.g. xo-expression2's ~49 facet
# translation units, ~300MB each) that OOM-kills cc1plus on a memory-limited
# runner (symptom: "g++: fatal error: Killed signal terminated program cc1plus").
# So the default caps the job count to what RAM allows (~1.5GB budget per job),
# bounded by cpu count.
if [[ -n "${jobs_arg}" ]]; then
build_jobs="${jobs_arg}"
elif [[ -n "${XO_BUILD_JOBS:-}" ]]; then
build_jobs="${XO_BUILD_JOBS}"
else
build_ncpu=$(nproc 2>/dev/null || echo 1)
build_memkb=$(awk '/^MemTotal:/ { print $2 }' /proc/meminfo 2>/dev/null || echo 0)
if [[ "${build_memkb}" -gt 0 ]]; then
build_memjobs=$(( build_memkb / 1500000 )) # ~1.5GB per compile job
[[ "${build_memjobs}" -lt 1 ]] && build_memjobs=1
if [[ "${build_memjobs}" -lt "${build_ncpu}" ]]; then
build_jobs="${build_memjobs}"
else
build_jobs="${build_ncpu}"
fi
else
build_jobs="${build_ncpu}"
fi
fi
SUBSYSTEMLIST_FILE=@CMAKE_INSTALL_FULL_DATADIR@/etc/xo/subsystem-list
SUBSYSTEMEDGES_FILE=@CMAKE_INSTALL_FULL_DATADIR@/etc/xo/subsystem-edges
subsystem_list() {
cat ${SUBSYSTEMLIST_FILE}
}
XO_REPO_STEM=
if [[ $(whoami) == "roland" ]]; then
XO_REPO_STEM="git@github.com:Rconybea"
else
XO_REPO_STEM="https://github.com/Rconybea"
fi
printrepo() {
xoname=$1
if grep -q $1 ${SUBSYSTEMLIST_FILE}; then
echo "${XO_REPO_STEM}/${1}.git"
else
>&2 echo "$0: unknown xo component [${xoname}]"
return 1
fi
return 0
}
if [[ $cmd == 'usage' ]]; then
echo -n "usage: "
usage
exit 0
elif [[ $cmd == 'help' ]]; then
echo -n "help; "
help
exit 0
fi
set -e
if [[ $cmd == 'list' ]]; then
subsystem_list
fi
# resolve the set of subsystems to operate on.
# --all -> every subsystem in dependency order (bottom-up),
# excluding the xo-cmake bootstrap itself
# one/more NAMEs -> exactly those, in the order given
if [[ $all_flag -eq 1 ]]; then
mapfile -t xonames < <(subsystem_list | grep -v '^xo-cmake$')
fi
# --with-deps: expand the named subsystems to their transitive dependency
# closure (via subsystem-edges), then order the closure bottom-up using
# subsystem-list (itself a valid topological order). No effect together with
# --all (already the whole graph) or when no subsystem is named.
if [[ $with_deps_flag -eq 1 && $all_flag -eq 0 && ${#xonames[@]} -gt 0 ]]; then
if [[ ! -r "${SUBSYSTEMEDGES_FILE}" ]]; then
>&2 echo "$0: --with-deps: cannot read edges file ${SUBSYSTEMEDGES_FILE}"
exit 1
fi
# deps_of[dependent] = space-separated direct dependencies.
# each edge line is "<dep> <dependent>".
declare -A deps_of=()
while read -r edge_dep edge_dependent; do
[[ -z "$edge_dep" || -z "$edge_dependent" ]] && continue
deps_of["$edge_dependent"]+=" $edge_dep"
done < "${SUBSYSTEMEDGES_FILE}"
# transitive closure of the requested subsystems (worklist to fixpoint)
declare -A in_closure=()
worklist=("${xonames[@]}")
while [[ ${#worklist[@]} -gt 0 ]]; do
cur="${worklist[0]}"
worklist=("${worklist[@]:1}")
[[ -n "${in_closure[$cur]:-}" ]] && continue
in_closure["$cur"]=1
for d in ${deps_of[$cur]:-}; do
[[ -z "${in_closure[$d]:-}" ]] && worklist+=("$d")
done
done
# rebuild xonames in bottom-up (subsystem-list) order, closure members only
xonames=()
while IFS= read -r sub; do
[[ -n "${in_closure[$sub]:-}" ]] && xonames+=("$sub")
done < <(subsystem_list)
fi
# preserve prior single-shot no-op behavior when no subsystem is named
if [[ ${#xonames[@]} -eq 0 ]]; then
xonames=("")
fi
# --- per-subsystem operations (loop body is intentionally left un-indented) ---
for xoname in "${xonames[@]}"; do
if [[ -n "$orig_pathtosource" ]]; then
pathtosource="$orig_pathtosource"
else
pathtosource="$xoname"
fi
if [[ -n "$orig_pathtobuild" ]]; then
pathtobuild="$orig_pathtobuild"
elif [[ -n "$xoname" ]]; then
pathtobuild="$xoname/.build"
else
pathtobuild=".build"
fi
if [[ $repo_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
printrepo $xoname
fi
fi
if [[ $add_umbrella_remote_flag -eq 1 ]]; then
if [[ $clone_flag -eq 1 || $configure_flag -eq 1 || $build_flag -eq 1 || $build_docs_flag -eq 1 || $install_flag -eq 1 || $realclean_flag -eq 1 ]]; then
2>&1 echo "error: xo-build: repo operation --add-umbrella-remote conflicts with"
2>&1 echo " build operation --clone|--configure|--build|--build-docs|--install|--realclean"
exit 1
fi
if [[ -n "$xoname" ]]; then
url=$(printrepo $xoname)
cmd="git remote add $xoname $url"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
$cmd
fi
fi
fi
if [[ $split_umbrella_remote_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
#branch=$(git branch --show-current)
demux_branch=_demux/$xoname
cmd="git subtree split --rejoin --prefix=$xoname -b ${demux_branch}"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
$cmd
fi
fi
fi
if [[ $pull_umbrella_remote_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
branch=$(git branch --show-current)
cmd="git subtree pull --rejoin --prefix=$xoname $xoname ${branch}"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
$cmd
fi
fi
fi
if [[ $push_umbrella_remote_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
branch=$(git branch --show-current)
demux_branch=_demux/$xoname
cmd1="git subtree split --rejoin --prefix=$xoname -b ${demux_branch}"
remote=${xoname}
remote_branch=${branch}
cmd2="git push ${remote} ${demux_branch}:${remote_branch}"
#cmd="git subtree push --rejoin --prefix=$xoname $xoname ${branch}"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd1
echo $cmd2
else
$cmd1
$cmd2
fi
fi
fi
if [[ $clone_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
url=$(printrepo $xoname)
cmd="git clone $url $xoname"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
$cmd
fi
fi
fi
if [[ $configure_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
testingarg=
if [[ $with_utests -eq 1 ]]; then
testingarg="-DENABLE_TESTING=1"
fi
asmarg=
if [[ $with_asm -eq 1 ]]; then
asmarg="-DENABLE_ASM=1"
fi
examplearg=
if [[ $with_examples -eq 1 ]]; then
examplearg="-DXO_ENABLE_EXAMPLES=1"
fi
cmakebuildtype=
if [[ $debug_build -eq 1 ]]; then
cmakebuildtype="-DCMAKE_BUILD_TYPE=Debug"
fi
openglarg=
if [[ -n ${with_opengl} ]]; then
openglarg="-DXO_ENABLE_OPENGL=${with_opengl}"
fi
vulkanarg=
if [[ -n ${with_vulkan} ]]; then
vulkanarg="-DXO_ENABLE_VULKAN=${with_vulkan}"
fi
cmd="cmake -DCMAKE_INSTALL_PREFIX=@CMAKE_INSTALL_PREFIX@ -DCMAKE_MODULE_PATH=@CMAKE_INSTALL_PREFIX@/share/cmake -S $pathtosource -B $pathtobuild ${testingarg} ${examplearg} ${asmarg} ${openglarg} ${vulkanarg} ${cmakebuildtype}"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
cwd=$(pwd)
# write script to re-run cmake later
rebootcmake=$pathtobuild/rebootcmake
mkdir -p $(dirname ${rebootcmake})
cat > ${rebootcmake} <<EOF
#!/usr/bin/env bash
cd ${cwd} && ${cmd}
EOF
chmod +rx ${rebootcmake}
$cmd
fi
fi
fi
if [[ $build_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
cmd="cmake --build $pathtobuild -j$build_jobs"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
$cmd
fi
fi
fi
if [[ $build_docs_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
cmd="cmake --build $pathtobuild -- docs"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
$cmd
fi
fi
fi
if [[ $utest_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
cmd="ctest --test-dir $pathtobuild"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
# On failure, re-run just the failed tests verbosely so CI logs show
# WHY they failed -- the first pass reports only pass/fail per test,
# whereas --output-on-failure prints each failed test's output (e.g.
# the Catch2 assertion detail). Preserve the failing exit status so
# the build still fails.
if ! $cmd; then
echo "xo-build: unit tests failed -- re-running failures with --rerun-failed --output-on-failure" 1>&2
ctest --test-dir "$pathtobuild" --rerun-failed --output-on-failure || true
exit 1
fi
fi
fi
fi
if [[ $install_flag -eq 1 ]]; then
if [[ -n "$xoname" ]]; then
cmd="cmake --install $pathtobuild"
if [[ $noop_flag -eq 1 ]]; then
echo $cmd
else
$cmd
fi
fi
fi
if [[ $realclean_flag -eq 1 ]]; then
if [[ $noop_flag -eq 1 ]]; then
echo "rm -rf $pathtobuild"
else
rm -rf "${pathtobuild}"
fi
fi
done