cmake_minimum_required(VERSION 3.20)
# C is enabled (not just CXX) because LLVM's `find_package(LLVM)` invokes
# `check_c_source_compiles` via FindFFI/FindLibEdit/FindTerminfo. Without
# C, the LLVM config aborts the configure step.
project(stig_code_analyzer LANGUAGES C CXX)

# ─────────────────────────────────────────────────────────────────────────
# Toolchain
#
# C++20 — required by PhASAR. PhASAR's `phasar_macros.cmake` puts
# `target_compile_features(... PUBLIC cxx_std_20)` on every library
# target we link against (phasar::llvm, phasar::llvm_ifdside,
# phasar::taintconfig), so consumers inherit the c++20 requirement.
# A previous version of this file said `17` "to match PhASAR's surface"
# — that was a misread of phasar_macros; CMake was silently escalating
# our compile to 20 anyway because of PhASAR's PUBLIC requirement.
# Made explicit here so the actual standard matches what the build does.
# Issue 19 task #32 audit revealed the discrepancy.
#
# RTTI must be ON because PhASAR + SVF both use dynamic_cast extensively
# across plugin boundaries.
#
# Stack-protector + function/data sections: PhASAR uses these on ITS OWN
# build (-fstack-protector-strong, -ffunction-sections, -fdata-sections in
# phasar/CMakeLists.txt:75) but doesn't propagate to consumers. We don't
# propagate to the analyzer either — it's a single-user dev tool, not a
# multi-tenant service. Worth knowing exists; not worth a difference today.
# ─────────────────────────────────────────────────────────────────────────
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif ()

# Export compile_commands.json for clangd / IDE integration.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# ─────────────────────────────────────────────────────────────────────────
# Source SHA — binary identity.
#
# A SHA-256 (hex, 64 chars) of the C++ source tree this binary was built
# from. Computed at JAR build time by `stig-code-analyzer/build.gradle.kts`
# and propagated through scripts/setup.sh → cmake → this define. Used:
#   - by `stig-code-analyzer --version` (binary self-reports identity);
#   - by Provision.kt (cache dir keyed on first 12 chars);
#   - by the Kotlin runner (verifies the cached binary matches the
#     expected SHA from the JAR manifest at every task action).
#
# When invoked via plain `cmake -S ... -B ...` outside the Gradle/Provision
# pipeline (developer working directly in src/main/cpp/), the variable is
# unset and we fall back to "dev-untracked" so the binary still builds but
# self-identifies as outside the cache-keyed flow.
# ─────────────────────────────────────────────────────────────────────────
if (NOT DEFINED STIG_VERSION OR STIG_VERSION STREQUAL "")
    set(STIG_VERSION "dev-untracked")
endif ()
message(STATUS "stig-code-analyzer: version = ${STIG_VERSION}")

# PhASAR + SVF pin labels, baked into the binary so analysis.json's
# provenance can report them. Values originate in scripts/pins.env;
# scripts/setup.sh propagates them as cmake -D defines. Fall back to
# "unknown" when unset (developer running plain `cmake -B ...` without
# Provision / Gradle); the binary still builds but its provenance loses
# this signal.
if (NOT DEFINED STIG_PHASAR_VERSION OR STIG_PHASAR_VERSION STREQUAL "")
    set(STIG_PHASAR_VERSION "unknown")
endif ()
if (NOT DEFINED STIG_SVF_VERSION OR STIG_SVF_VERSION STREQUAL "")
    set(STIG_SVF_VERSION "unknown")
endif ()
message(STATUS "stig-code-analyzer: PhASAR version = ${STIG_PHASAR_VERSION}")
message(STATUS "stig-code-analyzer: SVF version    = ${STIG_SVF_VERSION}")

# ─────────────────────────────────────────────────────────────────────────
# Dependencies
#
# LLVM 18 + PhASAR + SVF come from the host (installed by scripts/setup.sh
# into known prefixes — see scripts/pins.env). Header-only deps (CLI11,
# nlohmann/json, GoogleTest later) are vendored via FetchContent at pinned
# tags so the same versions land on every machine.
# ─────────────────────────────────────────────────────────────────────────
include(cmake/dependencies.cmake)

# LLVM major pinned in scripts/pins.env (`LLVM_MAJOR=18`). We do NOT pass
# a version number to `find_package` here because LLVM's installed
# `LLVMConfigVersion.cmake` requires callers to specify major.minor with
# patch ≥ installed-patch — too brittle (any Fedora/Ubuntu point-release
# bump would break it). Instead we point CMake at the right install via
# -DLLVM_DIR=... (set by build.gradle.kts's autodetect or by the
# CMakePresets dev preset) and validate the major version ourselves.
# Keep this STIG_REQUIRED_LLVM_MAJOR in sync with pins.env.
set(STIG_REQUIRED_LLVM_MAJOR 18)
find_package(LLVM REQUIRED CONFIG)
if (NOT LLVM_VERSION_MAJOR STREQUAL "${STIG_REQUIRED_LLVM_MAJOR}")
    message(FATAL_ERROR
            "stig-code-analyzer requires LLVM ${STIG_REQUIRED_LLVM_MAJOR}.x; "
            "found LLVM ${LLVM_PACKAGE_VERSION} at ${LLVM_DIR}. "
            "Point CMake at the right install via -DLLVM_DIR=/path/to/llvm${STIG_REQUIRED_LLVM_MAJOR}/lib/cmake/llvm "
            "or update scripts/pins.env if the pin should move."
    )
endif ()
message(STATUS "stig-code-analyzer: found LLVM ${LLVM_PACKAGE_VERSION} at ${LLVM_DIR}")

# Phase 2: PhASAR is required. Installed by scripts/setup.sh into
# ${STIG_PHASAR_PREFIX} (default ~/.local/stig-code-analyzer/install/phasar).
# Components we link directly; transitive deps (utils, config, controlflow,
# llvm_utils, llvm_pointer, llvm_controlflow, etc.) come in via INTERFACE_LINK_LIBRARIES
# on phasar::llvm_ifdside.
if (NOT DEFINED phasar_DIR AND DEFINED STIG_PHASAR_PREFIX)
    set(phasar_DIR "${STIG_PHASAR_PREFIX}/lib64/cmake/phasar")
endif ()
find_package(phasar REQUIRED COMPONENTS llvm_ifdside taintconfig llvm)
message(STATUS "stig-code-analyzer: found PhASAR ${PHASAR_VERSION} at ${PHASAR_INCLUDE_DIR}")

# SVF — required directly under issue 31 Phase B for the `--backend=svf`
# taint pipeline (SvfTaintChecker.cpp subclasses SVF::SrcSnkDDA, calls
# SVF::LLVMModuleSet::buildSVFModule + SVF::SVFIRBuilder::build). SVF_DIR
# is set by scripts/setup.sh:922 from the same SVF install PhASAR uses
# (so PhASAR's transitive svf-dda/svf-vfs alias backends and our direct
# SVF use share one SVF install — no version drift).
find_package(SVF REQUIRED CONFIG)
message(STATUS "stig-code-analyzer: found SVF ${SVF_VERSION} at ${SVF_INSTALL_PREFIX}")

# PhASAR's static archives ship as LLVM IR bitcode (built with -flto under
# the hood by its release toolchain). BFD ld rejects bitcode .a members
# with "file format not recognized"; lld handles them via the LLVMgold
# plugin path. Force lld globally — system clang/gcc both honour the
# -fuse-ld= flag.
add_link_options(-fuse-ld=lld)

# ─────────────────────────────────────────────────────────────────────────
# Targets
#
# The analyzer's analysis logic — Pipeline, EmitFacts, AnalysisSelect,
# AliasSelect — lives in a static library so the CLI binary AND the test
# binaries (Phase 3+) can link against it without source-set duplication.
# `main.cpp` is the only TU exclusive to the CLI binary.
# ─────────────────────────────────────────────────────────────────────────
llvm_map_components_to_libnames(LLVM_LIBS Support Core IRReader BitReader Demangle)

add_library(stig_code_analyzer_lib STATIC
        src/AliasSelect.cpp
        src/AnalysisSelect.cpp
        src/Demangle.cpp
        src/Pipeline.cpp
        src/EmitAnalysisResults.cpp
        src/SvfTaintChecker.cpp
)

target_include_directories(stig_code_analyzer_lib PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/src
        ${LLVM_INCLUDE_DIRS}
)

target_compile_definitions(stig_code_analyzer_lib PUBLIC
        ${LLVM_DEFINITIONS}
)

target_link_libraries(stig_code_analyzer_lib PUBLIC
        phasar::llvm           # HelperAnalyses
        phasar::llvm_ifdside   # IDESolver, IDEExtendedTaintAnalysis
        phasar::taintconfig    # LLVMTaintConfig, parseTaintConfig
        phasar::deps::nlohmann_json
        # SVF (issue 31 Phase B) — INTERFACE target only.
        #
        # SvfFlags carries INTERFACE_INCLUDE_DIRECTORIES (SVF headers) +
        # the SVF_INSTALL_EXTAPI_BC compile define + the SVF lib-search
        # path. We link IT directly so SvfTaintChecker.{h,cpp} can
        # `#include "SABER/SrcSnkDDA.h"` etc.
        #
        # The actual SvfCore.a / SvfLLVM.a archives are PROVIDED
        # TRANSITIVELY by phasar's PhasarExports (`$<LINK_ONLY:SvfLLVM>;
        # $<LINK_ONLY:SvfCore>` in the phasar::llvm_pointer chain).
        # Linking them ALSO via explicit `SVF::SvfLLVM` / `SVF::SvfCore`
        # here would double-link the archives — and SvfLLVM's static
        # initializers (which embed parts of LLVM's PassBuilder) would
        # register cl::opt 'print-pipeline-passes' twice and abort the
        # binary at startup. INTERFACE-only is the safe path.
        SVF::SvfFlags
        ${LLVM_LIBS}
)

add_executable(stig-code-analyzer
        src/main.cpp
)

target_compile_definitions(stig-code-analyzer PRIVATE
        STIG_VERSION="${STIG_VERSION}"
        STIG_PHASAR_VERSION="${STIG_PHASAR_VERSION}"
        STIG_SVF_VERSION="${STIG_SVF_VERSION}"
)

target_link_libraries(stig-code-analyzer PRIVATE
        stig_code_analyzer_lib
        CLI11::CLI11
)

# Hand the binary the same RPATH the PhASAR/SVF install uses so it can
# find libphasar.so / libSvfLLVM.so at runtime without LD_LIBRARY_PATH
# gymnastics. The actual install prefixes come from scripts/setup.sh.
set_target_properties(stig-code-analyzer PROPERTIES
        INSTALL_RPATH_USE_LINK_PATH ON
        BUILD_RPATH_USE_ORIGIN ON
)

install(TARGETS stig-code-analyzer RUNTIME DESTINATION bin)

# ─────────────────────────────────────────────────────────────────────────
# Tests
#
# The analyzer's regression tests live in Kotlin/kotest under
# stig-code-analyzer/src/test/kotlin/. They run via `./gradlew
# :stig-code-analyzer:test`. Bitcode for the test fixtures is built by
# the Gradle `compileTestBitcode` task, not by CMake — keeping CMake
# focused on the production analyzer binary and out of test mechanics.
#
# (sanity_test.cpp, a tiny gtest covering selectAnalysis / selectAlias
# helpers, lives under src/test/cpp/ and is built by an opt-in Gradle
# task `compileSanityTest` that links against the analyzer's static
# library; it's not part of the default `gradlew test` path because the
# Kotlin tests already cover end-to-end behaviour.)
# ─────────────────────────────────────────────────────────────────────────
