Use a bash array for MESON_NATIVE_FILES instead of a space-joined string (safe word-splitting/quoting), and set core.hooksPath to .githooks so the tracked lint/pre-push hooks are active after setup.
267 lines
9.5 KiB
Bash
Executable file
267 lines
9.5 KiB
Bash
Executable file
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
git -C "$ROOT" config core.hooksPath .githooks
|
|
BUILD_TYPE="release"
|
|
if [ "$#" -gt 0 ]; then
|
|
BUILD_TYPE="$1"
|
|
shift
|
|
fi
|
|
if [ "$BUILD_TYPE" != "release" ] && [ "$BUILD_TYPE" != "debug" ]; then
|
|
echo "ERROR: build type must be 'release' or 'debug' (got '$BUILD_TYPE')." >&2
|
|
exit 2
|
|
fi
|
|
BUILD_DIR="$ROOT/build/${BUILD_TYPE}"
|
|
OS="$(uname -s)"
|
|
|
|
# --- find LLVM ---
|
|
# Order: env override → macOS Homebrew Cellar (versioned dirs) → macOS
|
|
# brew --prefix llvm@N → Linux apt /usr/lib/llvm-N → Linux unversioned
|
|
# llvm-config on PATH. Sets LLVM_BIN (directory containing llvm-config)
|
|
# and LLVM_CONFIG (full path to the binary).
|
|
find_llvm_darwin() {
|
|
for ver in 22 21 20 19 18; do
|
|
local candidate="/opt/homebrew/Cellar/llvm/${ver}.${ver}.0/bin"
|
|
if [ -d "$candidate" ]; then
|
|
echo "$candidate"
|
|
return
|
|
fi
|
|
done
|
|
for ver in 22 21 20 19 18; do
|
|
local found
|
|
found=$(find /opt/homebrew/Cellar/llvm -maxdepth 1 -name "${ver}.*" -type d 2>/dev/null | head -1)
|
|
if [ -n "$found" ]; then
|
|
echo "$found/bin"
|
|
return
|
|
fi
|
|
done
|
|
# brew --prefix llvm@N covers Intel macOS + custom prefixes
|
|
for ver in 22 21 20 19 18; do
|
|
if command -v brew >/dev/null 2>&1; then
|
|
local prefix
|
|
prefix=$(brew --prefix "llvm@${ver}" 2>/dev/null || true)
|
|
if [ -n "$prefix" ] && [ -d "$prefix/bin" ]; then
|
|
echo "$prefix/bin"
|
|
return
|
|
fi
|
|
fi
|
|
done
|
|
echo ""
|
|
}
|
|
|
|
find_llvm_linux() {
|
|
# Apt-installed clang lives at /usr/lib/llvm-N/bin/ with
|
|
# llvm-config-N on PATH. Probe newest-first.
|
|
for ver in 22 21 20 19 18; do
|
|
if [ -x "/usr/lib/llvm-${ver}/bin/llvm-config" ]; then
|
|
echo "/usr/lib/llvm-${ver}/bin"
|
|
return
|
|
fi
|
|
if command -v "llvm-config-${ver}" >/dev/null 2>&1; then
|
|
dirname "$(command -v "llvm-config-${ver}")"
|
|
return
|
|
fi
|
|
done
|
|
# Last-ditch: bare llvm-config (some distros symlink it).
|
|
if command -v llvm-config >/dev/null 2>&1; then
|
|
dirname "$(command -v llvm-config)"
|
|
return
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
if [ -n "${LLVM_BIN:-}" ] && [ -d "$LLVM_BIN" ]; then
|
|
: # honor caller-provided LLVM_BIN
|
|
elif [ "$OS" = "Darwin" ]; then
|
|
LLVM_BIN=$(find_llvm_darwin)
|
|
elif [ "$OS" = "Linux" ]; then
|
|
LLVM_BIN=$(find_llvm_linux)
|
|
else
|
|
LLVM_BIN=""
|
|
fi
|
|
|
|
if [ -z "$LLVM_BIN" ]; then
|
|
if [ "$OS" = "Linux" ]; then
|
|
echo "ERROR: No LLVM installation found." >&2
|
|
echo " Install with: sudo apt-get install clang-19 llvm-19-dev libclang-19-dev libclang-cpp19-dev" >&2
|
|
echo " Or set LLVM_BIN=/path/to/llvm/bin to override." >&2
|
|
else
|
|
echo "ERROR: No Homebrew LLVM installation found." >&2
|
|
echo " Install with: brew install llvm" >&2
|
|
echo " Or set LLVM_BIN=/path/to/llvm/bin to override." >&2
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
LLVM_CONFIG="$LLVM_BIN/llvm-config"
|
|
if [ ! -x "$LLVM_CONFIG" ]; then
|
|
# Versioned binary (llvm-config-19) — pick it up.
|
|
LLVM_CONFIG=$(find "$LLVM_BIN" -maxdepth 1 -name 'llvm-config*' -type f -perm -u+x 2>/dev/null | head -1)
|
|
fi
|
|
if [ -z "$LLVM_CONFIG" ] || [ ! -x "$LLVM_CONFIG" ]; then
|
|
echo "ERROR: Found LLVM_BIN=$LLVM_BIN but no executable llvm-config inside it." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Find llvm-ar and create a native file so meson/ninja can archive
|
|
# static libraries without the LLVM bin dir on PATH.
|
|
MESON_NATIVE_FILES=()
|
|
LLVM_AR="$LLVM_BIN/llvm-ar"
|
|
if [ ! -x "$LLVM_AR" ]; then
|
|
# Versioned fallback: llvm-ar-22, llvm-ar-21, etc.
|
|
LLVM_VER=$(echo "$LLVM_VERSION" | cut -d. -f1)
|
|
LLVM_AR="$LLVM_BIN/llvm-ar-$LLVM_VER"
|
|
fi
|
|
if [ -x "$LLVM_AR" ]; then
|
|
# Ensure the build dir exists before writing the native file into it.
|
|
# `conan install -of "$BUILD_DIR"` (below) also creates it, but that runs
|
|
# later — on a fresh checkout this write would otherwise fail.
|
|
mkdir -p "$BUILD_DIR"
|
|
cat >"$BUILD_DIR/llvm_ar.ini" <<EOF
|
|
[binaries]
|
|
ar = '$LLVM_AR'
|
|
EOF
|
|
echo " Using llvm-ar: $LLVM_AR"
|
|
MESON_NATIVE_FILES+=(--native-file "$BUILD_DIR/llvm_ar.ini")
|
|
fi
|
|
|
|
echo "Using LLVM: $LLVM_BIN (llvm-config=$LLVM_CONFIG)"
|
|
|
|
export PATH="$LLVM_BIN:$PATH"
|
|
export LLVM_CONFIG
|
|
|
|
# Default CC/CXX to clang from the discovered LLVM_BIN when unset; this
|
|
# lets Conan and meson pick up matching binaries on Linux apt installs
|
|
# where the unversioned `clang` may not exist.
|
|
if [ -z "${CC:-}" ]; then
|
|
if [ -x "$LLVM_BIN/clang" ]; then
|
|
export CC="$LLVM_BIN/clang"
|
|
else
|
|
CC_CANDIDATE=$(find "$LLVM_BIN" -maxdepth 1 -name 'clang-[0-9]*' -type f -perm -u+x 2>/dev/null | head -1)
|
|
[ -n "$CC_CANDIDATE" ] && export CC="$CC_CANDIDATE"
|
|
fi
|
|
fi
|
|
if [ -z "${CXX:-}" ]; then
|
|
if [ -x "$LLVM_BIN/clang++" ]; then
|
|
export CXX="$LLVM_BIN/clang++"
|
|
else
|
|
CXX_CANDIDATE=$(find "$LLVM_BIN" -maxdepth 1 -name 'clang++-[0-9]*' -type f -perm -u+x 2>/dev/null | head -1)
|
|
[ -n "$CXX_CANDIDATE" ] && export CXX="$CXX_CANDIDATE"
|
|
fi
|
|
fi
|
|
|
|
# --- Conan (vendors spdlog, CLI11, nlohmann_json, sqlite3, Z3) ---
|
|
echo "Installing Conan dependencies..."
|
|
conan install "$ROOT" \
|
|
-of "$BUILD_DIR" \
|
|
--build=missing \
|
|
-s build_type="$(if [ "$BUILD_TYPE" = "debug" ]; then echo Debug; else echo Release; fi)" \
|
|
-s compiler.cppstd=23 \
|
|
-o "build_tests=True" ||
|
|
echo "Conan install had warnings (non-fatal)"
|
|
|
|
# --- configure ---
|
|
# Darwin: z3 and radare2 come from Homebrew/source, not Conan. Symlink
|
|
# their .pc files into the build dir because MesonToolchain's
|
|
# conan_meson_native.ini sets a scoped pkg_config_path that overrides
|
|
# the PKG_CONFIG_PATH env var.
|
|
if [ "$OS" = "Darwin" ] && command -v brew >/dev/null 2>&1; then
|
|
Z3_PC="$(brew --prefix z3 2>/dev/null)/lib/pkgconfig"
|
|
if [ -f "$Z3_PC/z3.pc" ]; then
|
|
ln -sf "$Z3_PC/z3.pc" "$BUILD_DIR/z3.pc"
|
|
echo " Z3 via pkg-config: $Z3_PC/z3.pc (symlinked to $BUILD_DIR/z3.pc)"
|
|
fi
|
|
# radare2 may be installed at /usr/local (not Homebrew). The r_core
|
|
# package requires its sub-packages (r_config, r_cons, etc.) so symlink
|
|
# all r_*.pc files.
|
|
if ls /usr/local/lib/pkgconfig/r_*.pc >/dev/null 2>&1; then
|
|
ln -sf /usr/local/lib/pkgconfig/r_*.pc "$BUILD_DIR/"
|
|
echo " radare2 via pkg-config: /usr/local/lib/pkgconfig/r_*.pc (symlinked)"
|
|
fi
|
|
fi
|
|
|
|
# Linux: meson can't find z3.pc by default because libz3-dev installs it
|
|
# under /usr/lib/<arch>-linux-gnu/pkgconfig/. Symlink into the conan-
|
|
# generated build dir so meson's pkg-config search picks it up. Arch is
|
|
# detected at runtime (x86_64-linux-gnu on amd64, aarch64-linux-gnu on
|
|
# arm64).
|
|
if [ "$OS" = "Linux" ]; then
|
|
for d in "/usr/lib/$(uname -m)-linux-gnu" /usr/lib/x86_64-linux-gnu \
|
|
/usr/lib/aarch64-linux-gnu /usr/lib; do
|
|
if [ -f "$d/pkgconfig/z3.pc" ]; then
|
|
ln -sf "$d/pkgconfig/z3.pc" "$BUILD_DIR/z3.pc" || true
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Meson + Ninja resolution.
|
|
#
|
|
# Stamp the build dir with the *system* meson the user will invoke later, so a
|
|
# bare `meson install` matches it. Capture meson's absolute path BEFORE touching
|
|
# the Conan env, then call it explicitly below — Conan ships its own (often
|
|
# older) meson via conanbuild.sh, and letting that shadow the system meson is
|
|
# exactly what stamps the dir with a version a newer system meson then refuses.
|
|
#
|
|
# Ninja: prefer a system ninja; only when none exists do we source Conan's env
|
|
# to borrow its pinned ninja (still using the system meson by absolute path).
|
|
# Guarded against set -u (Conan's env scripts reference vars that may be unset).
|
|
MESON_BIN="$(command -v meson || true)"
|
|
SOURCED_CONAN_ENV=0
|
|
if ! command -v ninja >/dev/null 2>&1 && [ -f "$BUILD_DIR/conanbuild.sh" ]; then
|
|
set +u
|
|
# shellcheck disable=SC1091
|
|
. "$BUILD_DIR/conanbuild.sh"
|
|
set -u
|
|
SOURCED_CONAN_ENV=1
|
|
echo " No system ninja found; borrowing Conan's ninja ($(command -v ninja 2>/dev/null || echo 'not found'))"
|
|
fi
|
|
# Fall back to whatever meson is now on PATH only if there was no system meson.
|
|
[ -z "$MESON_BIN" ] && MESON_BIN="$(command -v meson || echo meson)"
|
|
echo " Using meson: $MESON_BIN ($("$MESON_BIN" --version 2>/dev/null || echo '?'))"
|
|
|
|
echo "Configuring Meson (cpp_std=c++23, buildtype=${BUILD_TYPE})..."
|
|
# Capstone-backed ROP gadget discovery: -Dcapstone=auto + --wrap-mode=default.
|
|
# auto tries system pkg-config, then builds the vendored CMake wrap
|
|
# (subprojects/capstone.wrap); if neither is possible (offline / no cmake) it
|
|
# degrades to the r2 /R search with no build failure. --wrap-mode=default is
|
|
# required to override the wrap_mode=nofallback that Conan's native file sets,
|
|
# which would otherwise block the wrap. Meson prints the resolved status as
|
|
# "capstone gadget discovery: enabled|disabled".
|
|
"$MESON_BIN" setup "$BUILD_DIR" \
|
|
--reconfigure \
|
|
--native-file "$BUILD_DIR/conan_meson_native.ini" \
|
|
"${MESON_NATIVE_FILES[@]}" \
|
|
--wrap-mode=default \
|
|
-Dcpp_std=c++23 \
|
|
-Dbuildtype="${BUILD_TYPE}" \
|
|
-Dbuild-tests=true \
|
|
-Dghidra=auto \
|
|
-Dcapstone=auto \
|
|
-Dllvm-config-path="$LLVM_CONFIG"
|
|
|
|
# --- build ---
|
|
ninja -C "$BUILD_DIR" "$@"
|
|
|
|
echo ""
|
|
echo "Build complete. Binary at $BUILD_DIR/src/cli/fold"
|
|
# Surface the resolved optional-backend status (the meson messages above scroll
|
|
# past during a long build).
|
|
if grep -q 'FOLD_HAVE_GHIDRA' "$BUILD_DIR/build.ninja" 2>/dev/null; then
|
|
echo "Ghidra P-Code binary backend: ENABLED (vendored)"
|
|
else
|
|
echo "Ghidra P-Code binary backend: disabled (r2-disasm fallback)"
|
|
fi
|
|
if grep -q 'FOLD_HAVE_CAPSTONE' "$BUILD_DIR/build.ninja" 2>/dev/null; then
|
|
echo "Capstone ROP gadget discovery: ENABLED"
|
|
else
|
|
echo "Capstone ROP gadget discovery: disabled (r2 /R fallback) — needs cmake + network to build the wrap, or 'brew install capstone'"
|
|
fi
|
|
# The dir is stamped with the system meson ($MESON_BIN), so a plain install
|
|
# matches it. --no-rebuild avoids needing ninja (sudo's PATH usually lacks it).
|
|
echo "Install: sudo $MESON_BIN install -C $BUILD_DIR --no-rebuild"
|
|
if [ "$SOURCED_CONAN_ENV" = "1" ]; then
|
|
echo " (no system ninja was found — Conan's was borrowed for this build."
|
|
echo " For 'meson test' / 'ninja -C $BUILD_DIR' later, install one: brew install ninja)"
|
|
fi
|