fold/meson.build
trvon fcf474e4f0
Some checks failed
build / build-linux (pull_request) Failing after 7m32s
build / build-macos (pull_request) Has been cancelled
build / release (pull_request) Has been cancelled
refactor(cli): one TU path normalizer for coverage, fan-out and quarantine
Problem: normalizeClangTuPath, normalizeQuarantinePath and fan-out's
normalizePath were three copies of the same absolute + weakly_canonical
routine. Coverage, worker records and the quarantine file must agree on a
TU's spelling, and three copies can drift apart.

Fix: move the routine to fold/cli/tu_path.hpp as normalizeTuPath and use it
at every site. No behaviour change.

Test: existing test_scan_coverage (path spellings), test_tu_quarantine and
test_subprocess_fanout, updated to the new name. Full suite: 140/140.
2026-09-24 21:46:12 -06:00

394 lines
15 KiB
Meson

project('fold', ['cpp'],
version: '0.1.0',
default_options: [
'buildtype=debugoptimized',
'b_ndebug=if-release',
'warning_level=2',
'cpp_std=c++23',
'cpp_args=-march=native',
],
)
cpp = meson.get_compiler('cpp')
fold_git_revision = 'unknown'
fold_git_dirty = 0
git_program = find_program('git', required: false)
if git_program.found()
revision_result = run_command(
git_program, 'rev-parse', '--short=12', 'HEAD',
check: false,
)
if revision_result.returncode() == 0
fold_git_revision = revision_result.stdout().strip()
endif
status_result = run_command(
git_program, 'status', '--porcelain', '--untracked-files=normal',
check: false,
)
if status_result.returncode() == 0 and status_result.stdout().strip() != ''
fold_git_dirty = 1
endif
endif
add_project_arguments(
'-DFOLD_PROJECT_VERSION="' + meson.project_version() + '"',
'-DFOLD_GIT_REVISION="' + fold_git_revision + '"',
'-DFOLD_GIT_DIRTY=' + fold_git_dirty.to_string(),
language: 'cpp',
)
_fold_include_dir = join_paths(meson.current_source_dir(), 'include')
add_project_arguments('-I' + _fold_include_dir, language: 'cpp')
if get_option('build-tests') or get_option('enable-dcheck')
add_project_arguments('-DFOLD_ENABLE_DCHECK=1', language: 'cpp')
message('Fold debug checks: enabled')
endif
if host_machine.system() == 'darwin'
add_global_link_arguments('-Wl,-no_warn_duplicate_libraries', language: 'cpp')
endif
# --- lightweight deps via Conan-generated pkg-config ---
spdlog_dep = dependency('spdlog')
cli11_dep = dependency('CLI11')
json_dep = dependency('nlohmann_json')
sqlite3_dep = dependency('sqlite3')
# --- xxhash (Conan pkg-config name: libxxhash) ---
# Activates the XXH3 path in src/ir/normalize.cpp (FOLD_HAVE_XXHASH).
# Used header-only via XXH_INLINE_ALL, so we only need the include path,
# but link the dep anyway so future non-inline usage keeps working.
xxhash_dep = dependency('libxxhash', required: false)
if not xxhash_dep.found()
xxhash_dep = dependency('xxhash', required: false)
endif
if xxhash_dep.found()
add_project_arguments('-DFOLD_HAVE_XXHASH=1', language: 'cpp')
message('xxhash: XXH3 hashing enabled')
else
# normalize.cpp falls back to scalar FNV-1a.
warning('xxhash not found - falling back to scalar FNV-1a hashing')
endif
# --- rapidfuzz-cpp ---
# Header-only bit-parallel Levenshtein implementation used by clone scoring.
rapidfuzz_dep = dependency('rapidfuzz')
# --- Z3 via pkg-config (Homebrew, distro packages, or Conan) ---
# Users may need to extend PKG_CONFIG_PATH on macOS:
# export PKG_CONFIG_PATH="$(brew --prefix z3)/lib/pkgconfig:$PKG_CONFIG_PATH"
z3_dep = dependency('z3', required: true, include_type: 'system')
# --- radare2 binary backend ---
# Prefer linking against libr_core when available. R2Backend still has a CLI
# fallback so binary scans remain usable when building without radare2 dev libs.
radare2_dep = dependency('r_core', required: get_option('radare2'))
if radare2_dep.found()
add_project_arguments('-DFOLD_HAVE_R2_CORE=1', language: 'cpp')
message('radare2 binary backend: using libr_core')
else
message('radare2 binary backend: using r2 CLI fallback')
endif
# --- Capstone: exhaustive overlapping ROP gadget discovery ---
# When enabled, the gadget-discovery router (gadget_source.cpp) does its own
# overlapping sweep; otherwise it falls back to the r2 /R search, so this is
# fully optional. Capstone is CMake-only (not on wrapdb), so we try the
# system/pkg-config copy first, then build the vendored git wrap
# (subprojects/capstone.wrap) as a CMake subproject.
capstone_dep = dependency('', required: false) # not-found placeholder for dep lists
if not get_option('capstone').disabled()
capstone_dep = dependency('capstone', required: false) # system / pkg-config first
if not capstone_dep.found()
cmake = import('cmake')
capstone_opts = cmake.subproject_options()
capstone_opts.add_cmake_defines({
'CAPSTONE_BUILD_SHARED_LIBS': false,
'CAPSTONE_BUILD_CSTOOL': false,
'CAPSTONE_INSTALL': false,
})
capstone_sub = cmake.subproject('capstone', options: capstone_opts,
required: get_option('capstone'))
if capstone_sub.found()
capstone_dep = capstone_sub.dependency('capstone')
endif
endif
endif
if capstone_dep.found()
add_project_arguments('-DFOLD_HAVE_CAPSTONE=1', language: 'cpp')
message('capstone gadget discovery: enabled')
else
message('capstone gadget discovery: disabled (r2 /R fallback)')
endif
# --- Optional analysis-library lanes ---
# Only Ghidra is wired today. Souffle remains as the one near-term reserved
# fact-engine lane; other research candidates stay docs-only until a prototype
# justifies a build surface.
if get_option('souffle').enabled()
warning('souffle lane requested, but only a vendor manifest exists; no build target is wired yet')
endif
# --- Ghidra decompiler (vendored SSA P-Code binary frontend) ---
# Optional; gated by -Dghidra. When enabled, third_party/ghidra-decompiler
# defines ghidra_decompiler_dep + the per-arch .sla + pcode_dump. fold_core does
# not link it yet (the lifter/encoder glue lands in later phases).
ghidra_enabled = false
# .allowed() is true for both `auto` (the default) and `enabled`; only an explicit
# -Dghidra=disabled skips it. The decompiler is vendored (third_party), so there is
# no external dependency to probe — auto means "build unless told not to".
if get_option('ghidra').allowed()
# add_project_arguments must precede any target (the subdir declares some).
add_project_arguments('-DFOLD_HAVE_GHIDRA=1', language: 'cpp')
# Absolute path to the checked-in vendored SLEIGH specs (.ldefs/.pspec/.cspec
# plus source-tree .sla copies), so the lifter can locate the complete spec
# set at runtime in a dev build. The Meson-generated build-tree .sla targets
# are validation artifacts until an install/datadir copy rule exists.
add_project_arguments(
'-DFOLD_GHIDRA_SPECS_DIR="' + meson.project_source_root() / 'third_party' / 'ghidra-decompiler' / 'specs' + '"',
language: 'cpp')
ghidra_enabled = true
message('ghidra decompiler: enabled (vendored)')
else
message('ghidra decompiler: disabled (r2-disasm fallback)')
endif
# Optional Rust/Swift frontend defines — must precede any build target.
if get_option('rust-frontend')
add_project_arguments('-DFOLD_HAVE_RUST=1', language: 'cpp')
endif
if get_option('swift-frontend')
add_project_arguments('-DFOLD_HAVE_SWIFT=1', language: 'cpp')
endif
# --- LLVM / Clang via llvm-config ---
# The user can override the discovered binary with -Dllvm-config-path=...
# or by exporting LLVM_CONFIG before invoking meson.
llvm_config_opt = get_option('llvm-config-path')
if llvm_config_opt != ''
llvm_config = find_program(llvm_config_opt, required: true)
else
llvm_config = find_program(
'llvm-config',
'llvm-config-22', 'llvm-config-21', 'llvm-config-20',
'llvm-config-19', 'llvm-config-18', 'llvm-config-17',
'llvm-config-16', 'llvm-config-15', 'llvm-config-14',
required: true,
)
endif
llvm_version = run_command(llvm_config, '--version', check: true).stdout().strip()
llvm_incdir = run_command(llvm_config, '--includedir', check: true).stdout().strip()
llvm_libdir = run_command(llvm_config, '--libdir', check: true).stdout().strip()
llvm_bindir = run_command(llvm_config, '--bindir', check: true).stdout().strip()
llvm_cxxflags = run_command(llvm_config, '--cxxflags', check: true).stdout().strip().split()
message('LLVM @0@ from @1@'.format(llvm_version, llvm_libdir))
# Find llvm-ar so meson/ninja can archive static libraries without
# requiring the user to put the LLVM bin directory on PATH.
llvm_ar = find_program(llvm_bindir / 'llvm-ar', llvm_bindir / 'llvm-ar-@0@'.format(llvm_version), required: false)
if not llvm_ar.found()
warning('llvm-ar not found in @0@ — static library archiving may fail on macOS'.format(llvm_bindir))
endif
# Pull only the include-related and necessary flags from llvm-config; strip
# warning flags and macro definitions that conflict with our own warning
# level. We keep -I, -D for system requirements like -DNDEBUG, and -std= if
# present (LLVM headers sometimes require C++17+).
_keep_prefixes = ['-I', '-D', '-isystem']
_clean_cxxflags = []
foreach f : llvm_cxxflags
foreach p : _keep_prefixes
if f.startswith(p)
_clean_cxxflags += f
break
endif
endforeach
endforeach
add_project_arguments(_clean_cxxflags, language: 'cpp')
# Clang ships in the same prefix as LLVM. Use -idirafter so the path comes
# after the SDK and clang's resource-dir / libc++ search dirs — preventing
# Homebrew clang's <cstddef> from finding the SDK <stddef.h> via -isystem
# and crashing the whole build. The cost is that LLVM headers fire on our
# warning level; we suppress -Wunused-parameter etc. just below.
add_project_arguments('-idirafter', llvm_incdir, language: 'cpp')
add_project_arguments(
'-Wno-unused-parameter',
'-Wno-unknown-warning-option',
language: 'cpp',
)
if get_option('strict_warnings').enabled()
add_project_arguments(
'-Wpedantic', '-Wshadow',
language: 'cpp'
)
endif
# Link against the umbrella Clang shared library (covers Tooling, AST,
# Frontend, Basic, etc.) and LLVM. This avoids enumerating ~40 static libs
# and matches the recommended way to link tools against Clang.
libclang_cpp_dep = cpp.find_library(
'clang-cpp',
dirs: [llvm_libdir],
has_headers: ['clang/Tooling/Tooling.h'],
header_args: ['-I' + llvm_incdir],
required: true,
)
# Prefer the umbrella libLLVM shared lib; fall back to enumerated static libs
# if the distro only shipped those.
libllvm_dep = cpp.find_library('LLVM', dirs: [llvm_libdir], required: false)
if not libllvm_dep.found()
libllvm_dep = cpp.find_library(
'LLVM-' + llvm_version.split('.')[0],
dirs: [llvm_libdir],
required: true,
)
endif
clang_deps = [libclang_cpp_dep]
llvm_dep = libllvm_dep
# --- core static library: everything except cli/main ---
fold_core_sources = []
# Vendored Ghidra decompiler targets (declared after all add_project_arguments,
# before fold_core). Builds the lib + sleigh + .sla + pcode_dump; not yet linked
# into fold_core (lifter/encoder glue is a later phase).
if ghidra_enabled
subdir('third_party/ghidra-decompiler')
endif
subdir('src/binary')
subdir('src/frontend')
subdir('src/ir')
fold_core_sources += files(
'src/analysis/production_roots.cpp',
'src/analysis/production_reachability.cpp',
'src/config/configuration_profile.cpp',
)
subdir('src/symbolic')
subdir('src/detect')
subdir('src/index')
subdir('src/report')
subdir('src/confirm')
subdir('src/witness')
subdir('src/fuzz')
subdir('src/mcp')
subdir('src/lsp')
# cli/scan_runner.cpp and cli/subprocess_fanout.cpp live in src/cli/ but are
# part of fold_core (the executable links them too via the dep). Pull them in
# before the static_library is finalized.
fold_core_sources += files(
'src/cli/clang_shard_tu.cpp',
'src/cli/backend_shard_runner.cpp',
'src/cli/configuration_callsite_guard.cpp',
'src/cli/incremental_fingerprint.cpp',
'src/cli/project_graph_orchestrator.cpp',
'src/cli/production_reachability_findings.cpp',
'src/cli/source_call_graph.cpp',
'src/cli/source_call_graph_contribution.cpp',
'src/cli/translation_unit_fingerprint.cpp',
'src/cli/source_summary_pipeline.cpp',
'src/cli/source_tu_runner.cpp',
'src/cli/tu_quarantine.cpp',
'src/cli/tu_path.cpp',
'src/cli/detector_plan.cpp',
'src/cli/detector_profile_accum.cpp',
'src/cli/finding_evidence.cpp',
'src/cli/detector_registry.cpp',
'src/cli/error_format.cpp',
'src/cli/executable_path.cpp',
'src/cli/fuzz_prep_runner.cpp',
'src/cli/fuzz_run_runner.cpp',
'src/cli/scan_option_catalog.cpp',
'src/cli/scan_preset.cpp',
'src/cli/scan_router.cpp',
'src/cli/scan_runner.cpp',
'src/cli/signal_isolation.cpp',
'src/cli/tu_security_prefilter.cpp',
'src/cli/subprocess_fanout.cpp',
)
fold_core = static_library(
'fold-core',
fold_core_sources,
dependencies: [
spdlog_dep, json_dep, sqlite3_dep, xxhash_dep, rapidfuzz_dep, z3_dep, radare2_dep, capstone_dep, llvm_dep,
] + clang_deps,
include_directories: include_directories('include', '.'),
install: false,
)
fold_core_dep = declare_dependency(
link_with: fold_core,
include_directories: include_directories('include', '.'),
dependencies: [spdlog_dep, json_dep, sqlite3_dep, xxhash_dep, rapidfuzz_dep, z3_dep, radare2_dep, capstone_dep, llvm_dep] + clang_deps,
)
# --- Ghidra lifter + dual-stream router (gated) ---
# Links fold_core (encoder + observers) with the vendored decompiler. Built only
# when -Dghidra=enabled; the Ghidra-header TU (ghidra_lifter.cpp) is compiled at
# fold's std with warnings suppressed. Defined after fold_core so fold_core_dep
# and ghidra_decompiler_dep (from the third_party subdir) are both in scope.
if ghidra_enabled
fold_ghidra = static_library(
'fold-ghidra',
files('src/binary/ghidra_lifter.cpp', 'src/binary/pcode_router.cpp',
'src/binary/binary_pcode_scan.cpp'),
cpp_args: ['-w'],
include_directories: [include_directories('include'), ghidra_inc],
dependencies: [fold_core_dep, ghidra_decompiler_dep],
install: false,
)
fold_ghidra_dep = declare_dependency(
link_with: fold_ghidra,
include_directories: include_directories('include'),
dependencies: [fold_core_dep, ghidra_decompiler_dep],
)
# Phase-3.5 smoke driver: one function → router → merged findings.
pcode_scan_exe = executable(
'pcode_scan', files('tools/pcode_scan.cpp'),
dependencies: [fold_ghidra_dep], install: false,
)
endif
# --- CLI binary ---
subdir('src/cli')
# --- Swift SIL frontend ---
if get_option('swift-frontend')
subdir('tools/fold-swift')
endif
# --- tests ---
if get_option('build-tests')
# Use the system Catch2 (Homebrew on macOS / distro pkg elsewhere) so the
# header and library stay in lock-step. Mixing a Conan-built lib with a
# Homebrew header leaks `makeExceptionHappenedString` references that the
# Conan lib never built. PKG_CONFIG_PATH must point at the system .pc.
catch2_dep = dependency('catch2-with-main', required: true)
subdir('tests')
endif
# --- fold-rust (Rust frontend, requires nightly rustc) ---
_fold_rust_manifest = join_paths(meson.project_source_root(), 'tools', 'fold-rust', 'Cargo.toml')
_fold_rust_src_bin = join_paths(meson.project_source_root(), 'tools', 'fold-rust', 'target', 'release', 'fold-rust')
# custom_target with build_by_default+install so a single `meson compile`
# builds all 3 binaries and `meson install` copies them to prefix/bin.
if get_option('rust-frontend')
_fold_rust = custom_target('fold-rust',
output: 'fold-rust',
command: ['sh', '-c',
'cd "' + join_paths(meson.project_source_root(), 'tools', 'fold-rust') + '" && ' +
'cargo build --release --manifest-path "' + _fold_rust_manifest + '" && ' +
'cp "' + _fold_rust_src_bin + '" "@OUTPUT@"'],
install: true,
install_dir: get_option('bindir'),
build_by_default: true,
build_always_stale: true,
)
endif