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 uses dynamic_cast 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 pin label, baked into the binary so analysis.json's provenance can
# report it. 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 ()
message(STATUS "stig-code-analyzer: PhASAR version = ${STIG_PHASAR_VERSION}")

# ─────────────────────────────────────────────────────────────────────────
# Dependencies
#
# LLVM 18 + PhASAR 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}")

# 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
)

target_include_directories(stig_code_analyzer_lib PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/src
)

target_include_directories(stig_code_analyzer_lib SYSTEM PUBLIC
        ${LLVM_INCLUDE_DIRS}
)

target_compile_options(stig_code_analyzer_lib PRIVATE
        -Wall
        -Wextra
        -Wpedantic
        -Werror
        # LLVM 18 reaches GCC 16's removed <ciso646> compatibility header,
        # which deliberately emits #warning under C++20. Keep that dependency
        # diagnostic visible without reclassifying it as a STIG source error.
        -Wno-error=#warnings
)

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
        ${LLVM_LIBS}
)

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

target_compile_options(stig-code-analyzer PRIVATE
        -Wall
        -Wextra
        -Wpedantic
        -Werror
        -Wno-error=#warnings
)

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

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

# Hand the binary the same RPATH the PhASAR install uses so it can find
# libphasar.so at runtime without LD_LIBRARY_PATH gymnastics. The actual
# install prefix comes 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.)
# ─────────────────────────────────────────────────────────────────────────
