#!/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" </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/-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