281 lines
12 KiB
Bash
Executable File
281 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Configure (if needed) and build the project, preferably with Ninja.
|
|
#
|
|
# Usage (from the project root):
|
|
# scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
|
|
# scripts/build.sh --list-presets
|
|
# scripts/build.sh --help
|
|
#
|
|
# --preset <name> Use a CMakePresets.json preset. A preset selects that
|
|
# target's toolchain file (cmake/toolchains/<name>.cmake),
|
|
# which picks the compiler and declares the DIST_TARGET
|
|
# together, instead of you having to pass a matching
|
|
# pair by hand:
|
|
# scripts/build.sh --preset windows-x64-mingw64
|
|
# Each preset builds in its own directory,
|
|
# out/build/<name>, so switching between presets never
|
|
# invalidates -- or deletes -- another one's build.
|
|
# Run --list-presets to see every preset for your OS.
|
|
# Must come first if given at all; --preset=<name> also
|
|
# works. Given with no value, or a value that isn't one
|
|
# of --list-presets' own, this fails fast with that
|
|
# same preset list shown right there, rather than
|
|
# letting a typo fall through to a less obvious `cmake`
|
|
# error further down.
|
|
# BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
|
|
# (default: Configuration.cmake's DEFAULT_BUILD_TYPE)
|
|
# CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.:
|
|
# scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
|
|
# scripts/build.sh -DBUILD_TESTS=ON
|
|
# (BUILD_TYPE omitted -- recognized by not
|
|
# starting with "-")
|
|
#
|
|
# Picking a DIST_TARGET: three ways; the first two set the compiler and the
|
|
# label together, and this script rejects mixing --preset with a
|
|
# -DDIST_TARGET rather than leaving it ambiguous which one wins.
|
|
# 1. --preset <name> The recommended way; see --list-presets.
|
|
# 2. -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<name>.cmake
|
|
# (no --preset) What a preset does, by hand, as one of
|
|
# CMAKE_ARGS above.
|
|
# 3. -DDIST_TARGET=<value> Sets the label alone, as one of CMAKE_ARGS
|
|
# (no --preset) above. Without also passing a matching
|
|
# compiler (or, better, the toolchain file
|
|
# above), DIST_TARGET is auto-detected from
|
|
# whatever compiler CMake finds on its own; if
|
|
# the two disagree, Configuration.cmake
|
|
# FATAL_ERRORs rather than silently building
|
|
# the wrong toolchain and mislabeling the
|
|
# result -- see that file and the README.
|
|
# Passing --preset together with an explicit -DDIST_TARGET=... in
|
|
# CMAKE_ARGS is rejected by this script before cmake even runs: the
|
|
# preset's toolchain already declares DIST_TARGET, so a second value
|
|
# alongside it is always either redundant (same value) or contradictory
|
|
# (different value) -- never useful.
|
|
#
|
|
# Build directory: out/build/<preset> for --preset <preset>; without a
|
|
# preset, out/build/default (the compiler is auto-detected there, so keep
|
|
# to one compiler per directory or use presets). Set BUILD_DIR to use an
|
|
# exact directory of your own instead. scripts/run.sh, install.sh and
|
|
# docs.sh use the directory built last, or --preset <name> to pick another.
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
|
|
scripts/build.sh --list-presets
|
|
scripts/build.sh --help
|
|
|
|
--preset <name> Use a CMakePresets.json preset: it selects the target's
|
|
toolchain file (compiler and DIST_TARGET together) and
|
|
builds in its own directory, out/build/<name>. Must come
|
|
first if given at all; --preset=<name> also works. Run
|
|
--list-presets to see every preset for your OS.
|
|
BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
|
|
(default: Configuration.cmake's DEFAULT_BUILD_TYPE)
|
|
CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.
|
|
-DDIST_TARGET=<value> or -DBUILD_TESTS=ON.
|
|
|
|
Don't combine --preset with an explicit -DDIST_TARGET=... in CMAKE_ARGS --
|
|
the preset's toolchain already declares one; see the comment at the top of
|
|
this script (scripts/build.sh) and Configuration.cmake's "Target selection"
|
|
section for why picking a DIST_TARGET is one or the other, not both.
|
|
|
|
Examples:
|
|
scripts/build.sh --preset linux-x64-clang
|
|
scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
|
|
scripts/build.sh -DBUILD_TESTS=ON
|
|
EOF
|
|
}
|
|
|
|
case "${1:-}" in
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$SCRIPT_DIR/.."
|
|
|
|
command -v cmake >/dev/null 2>&1 || { echo "error: cmake not found on PATH." >&2; exit 1; }
|
|
|
|
if [ "${1:-}" = "--list-presets" ]; then
|
|
exec cmake --list-presets
|
|
fi
|
|
|
|
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir.
|
|
source "$SCRIPT_DIR/_common.sh"
|
|
|
|
# Prints CMake's own preset listing (already filtered to this OS by each
|
|
# preset's "condition" in CMakePresets.json) to stderr -- used to show the
|
|
# actual options right alongside a preset-related error below, instead of
|
|
# just telling the user to go run --list-presets separately.
|
|
_pmn_show_presets() {
|
|
echo "" >&2
|
|
cmake --list-presets >&2
|
|
}
|
|
|
|
# Membership test against that same listing -- $1 matches a real preset
|
|
# name for this OS only if it appears there as a quoted entry (anchored on
|
|
# both quote characters so e.g. "windows-x64-mingw32" can't false-match a
|
|
# check for "windows-x64-mingw3").
|
|
_pmn_known_preset() {
|
|
cmake --list-presets 2>/dev/null | grep -qF "\"$1\""
|
|
}
|
|
|
|
_pmn_has_explicit_dist_target() {
|
|
local arg
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-DDIST_TARGET=*) return 0 ;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# --preset must come first if given at all -- see Configuration.cmake's
|
|
# and CMakePresets.json's own comments for why this is the recommended
|
|
# way to pick a non-default DIST_TARGET: its toolchain file sets the
|
|
# compiler and the label together, instead of you having to pass both by
|
|
# hand.
|
|
PRESET=""
|
|
case "${1:-}" in
|
|
--preset)
|
|
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
|
|
echo "error: --preset requires a value, e.g. --preset windows-x64-mingw64" >&2
|
|
_pmn_show_presets
|
|
exit 1
|
|
fi
|
|
PRESET="$2"
|
|
shift 2
|
|
;;
|
|
--preset=*)
|
|
PRESET="${1#--preset=}"
|
|
shift
|
|
if [ -z "$PRESET" ]; then
|
|
echo "error: --preset= requires a value, e.g. --preset=windows-x64-mingw64" >&2
|
|
_pmn_show_presets
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
if [ -n "$PRESET" ] && ! _pmn_known_preset "$PRESET"; then
|
|
echo "error: '${PRESET}' is not a preset --list-presets knows about for this OS." >&2
|
|
_pmn_show_presets
|
|
exit 1
|
|
fi
|
|
|
|
# One build directory per preset (out/build/<preset>; "default" when no
|
|
# preset was given), so switching presets never has to reuse -- or wipe -- a
|
|
# directory configured for another compiler. cmake/BuildDir.cmake owns the
|
|
# layout (Configuration.cmake's BUILD_OUTPUT_DIR); an explicit BUILD_DIR
|
|
# from the environment beats it, and is then also not recorded as "the one
|
|
# built last" (run/install/docs would otherwise be sent to a directory the
|
|
# layout doesn't know about).
|
|
_pmn_record_build=1
|
|
if [ -n "${BUILD_DIR:-}" ]; then
|
|
_pmn_record_build=0
|
|
else
|
|
BUILD_DIR="$(_pmn_resolve_build_dir "${PRESET:-default}")"
|
|
fi
|
|
|
|
# If the first remaining argument doesn't start with "-", it's BUILD_TYPE
|
|
# and everything after it is CMAKE_ARGS; otherwise there's no BUILD_TYPE
|
|
# override and every argument is CMAKE_ARGS. Plain bash "$@"/shift, no
|
|
# special-casing needed: unlike Windows cmd.exe (see build.bat), bash
|
|
# never splits a "-DFOO=BAR"-style argument on its own "=".
|
|
if [ "$#" -gt 0 ] && [[ "$1" != -* ]]; then
|
|
BUILD_TYPE="$1"
|
|
shift
|
|
else
|
|
BUILD_TYPE="$(_pmn_read_config DEFAULT_BUILD_TYPE)"
|
|
fi
|
|
CMAKE_ARGS=("$@")
|
|
|
|
# ${arr[@]+"${arr[@]}"} rather than a plain "${arr[@]}" wherever an array can be
|
|
# empty: under `set -u`, bash before 4.4 -- which includes the 3.2 that macOS
|
|
# still ships as /bin/bash -- treats expanding an EMPTY array as an unset
|
|
# variable and aborts with "unbound variable".
|
|
if [ -n "$PRESET" ] && _pmn_has_explicit_dist_target ${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}; then
|
|
echo "error: both --preset ${PRESET} and an explicit -DDIST_TARGET=... were given." >&2
|
|
echo " A preset's toolchain already declares DIST_TARGET (and picks the compiler) -- pick one:" >&2
|
|
echo " scripts/build.sh --preset ${PRESET}" >&2
|
|
echo " scripts/build.sh -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<target>.cmake" >&2
|
|
echo " See the comment at the top of this script, or Configuration.cmake's" >&2
|
|
echo " \"Target selection\" section, for why these two aren't combined." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Prefer a pinned ninja (fetched by scripts/setup.sh -- see
|
|
# cmake/FetchNinja.cmake) over a system one, same "always the pinned copy"
|
|
# preference as Doxygen. Needed whether or not --preset is used: every
|
|
# preset sets its generator to Ninja but doesn't hardcode which ninja
|
|
# binary to use (that would just be TOOLCACHE_DIR's default duplicated a
|
|
# third time -- see Configuration.cmake) -- CMAKE_MAKE_PROGRAM supplies it
|
|
# either way; only -G Ninja itself is skipped below when a preset already
|
|
# sets the generator.
|
|
TOOLCACHE_DIR="${TOOLCACHE_DIR:-$(_pmn_read_config TOOLCACHE_DIR)}"
|
|
NINJA_BIN=""
|
|
for _candidate in "${TOOLCACHE_DIR}"/ninja-*/ninja; do
|
|
if [ -x "$_candidate" ]; then
|
|
# Must be absolute: CMake invokes CMAKE_MAKE_PROGRAM from several
|
|
# different working directories during its own internal
|
|
# try-compile steps, where a relative path doesn't resolve.
|
|
NINJA_BIN="$(cd "$(dirname "$_candidate")" && pwd)/$(basename "$_candidate")"
|
|
break
|
|
fi
|
|
done
|
|
|
|
GENERATOR_ARGS=()
|
|
if [ -n "$NINJA_BIN" ]; then
|
|
echo "==> Using pinned ninja: ${NINJA_BIN}"
|
|
if [ -z "$PRESET" ]; then
|
|
GENERATOR_ARGS=(-G Ninja "-DCMAKE_MAKE_PROGRAM=${NINJA_BIN}")
|
|
else
|
|
GENERATOR_ARGS=("-DCMAKE_MAKE_PROGRAM=${NINJA_BIN}")
|
|
fi
|
|
elif command -v ninja >/dev/null 2>&1; then
|
|
echo "==> Using system ninja: $(command -v ninja)"
|
|
if [ -z "$PRESET" ]; then
|
|
GENERATOR_ARGS=(-G Ninja)
|
|
fi
|
|
else
|
|
if [ -n "$PRESET" ]; then
|
|
echo "warning: ninja not found (no pinned copy, none on PATH) -- the '${PRESET}'" >&2
|
|
echo " preset requires it; configure will likely fail. Run" >&2
|
|
echo " scripts/setup.sh to fetch a pinned ninja, or install ninja" >&2
|
|
echo " yourself." >&2
|
|
else
|
|
echo "warning: ninja not found (no pinned copy, none on PATH) -- falling back to" >&2
|
|
echo " CMake's default generator for this platform. Run scripts/setup.sh" >&2
|
|
echo " to fetch a pinned ninja, or install ninja yourself, for a faster" >&2
|
|
echo " and more consistent build. Continuing without it." >&2
|
|
fi
|
|
fi
|
|
|
|
CONFIGURE_ARGS=(-S . -B "${BUILD_DIR}")
|
|
if [ -n "$PRESET" ]; then
|
|
CONFIGURE_ARGS=(--preset "${PRESET}" "${CONFIGURE_ARGS[@]}")
|
|
fi
|
|
|
|
echo "==> Configuring (${BUILD_TYPE}) into ${BUILD_DIR}/"
|
|
if [ -n "$PRESET" ]; then
|
|
echo " Preset: ${PRESET}"
|
|
fi
|
|
if [ "${#CMAKE_ARGS[@]}" -gt 0 ]; then
|
|
echo " Extra CMake arguments: ${CMAKE_ARGS[*]}"
|
|
fi
|
|
cmake "${CONFIGURE_ARGS[@]}" ${GENERATOR_ARGS[@]+"${GENERATOR_ARGS[@]}"} -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" ${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}
|
|
|
|
# Configure succeeded: this is now the build the other scripts default to.
|
|
if [ "$_pmn_record_build" = 1 ]; then
|
|
_pmn_record_build_dir "${PRESET:-default}"
|
|
fi
|
|
|
|
echo "==> Building"
|
|
cmake --build "${BUILD_DIR}"
|
|
|
|
echo "==> Done. Run a module with: scripts/run.sh <target-name>"
|