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.
394 lines
15 KiB
Meson
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
|