# -----------------------------------------------------------------------------------------------------------
# Copyright (c) 2025 Huawei Technologies Co., Ltd.
# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
# CANN Open Software License Agreement Version 2.0 (the "License").
# Please refer to the License for details. You may not use this file except in compliance with the License.
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
# See LICENSE in the root of the software repository for the full text of the License.
# -----------------------------------------------------------------------------------------------------------


########################################################################################################################
# 环境检查
########################################################################################################################

# Python3 分析
# 1. Python3_EXECUTABLE 用以标识 Python3 可执行文件路径, -D 指定的优先级高于环境变量;
# 2. 刷新 CMake 内部变量 Python3_ROOT_DIR 值, 以便后续 find python 相关 package 使用(主要是 Development 组件);
if (DEFINED Python3_EXECUTABLE)
    set(Python3_EXECUTABLE "${Python3_EXECUTABLE}")  # 不可直接获取绝对路径, 避免不同 executable 权限差异导致的 pip 包查找问题
elseif (DEFINED ENV{Python3_EXECUTABLE})
    set(Python3_EXECUTABLE "$$ENV{Python3_EXECUTABLE}")  # 不可直接获取绝对路径, 避免不同 executable 权限差异导致的 pip 包查找问题
else ()
    # 当外部未指定 Python3 时, 一般是从 CMake 为入口触发的编译, 此时直接 find.
    find_package(Python3 COMPONENTS Interpreter Development)
    if ("${Python3_EXECUTABLE}x" STREQUAL "x")
        message(FATAL_ERROR "Can't find python3 Interpreter.")
    endif ()
endif ()
message(STATUS "Python3_EXECUTABLE=${Python3_EXECUTABLE}")
if (NOT (DEFINED Python3_ROOT_DIR OR DEFINED ENV{Python3_ROOT_DIR}))
    get_filename_component(_Python3__EXECUTABLE "${Python3_EXECUTABLE}"     REALPATH)
    get_filename_component(_Python3_ROOT_DIR    "${_Python3__EXECUTABLE}"   DIRECTORY)
    get_filename_component(_Python3_ROOT_DIR    "${_Python3_ROOT_DIR}/../"  REALPATH)
    set(Python3_ROOT_DIR "${_Python3_ROOT_DIR}" CACHE INTERNAL "Python3 root path" FORCE)
    message(STATUS "Python3_ROOT_DIR=${Python3_ROOT_DIR}")
endif ()

get_filename_component(_Py3CMakeFile "${PTO_FWK_BIN_ROOT}/_pypto_py3_env.cmake" REALPATH)
PTO_Fwk_AnalysisPython3Environ(OUTPUT_FILE ${_Py3CMakeFile})
include(${_Py3CMakeFile})

if (ENABLE_FEATURE_PYTHON_FRONT_END)
    find_package(Python3 ${PYTHON3_VERSION_ID} EXACT COMPONENTS Development)
    if (NOT Python3_Development_FOUND)
        message(FATAL_ERROR "Can't get python3-dev, Python Frontend can't build.")
    endif ()
    if ("${PY3_MOD_PYBIND11_CMAKE_DIR}x" STREQUAL "x")
        message(FATAL_ERROR "Can't get pybind11 cmake dir, Python Frontend can't build.")
    endif ()
    find_package(pybind11 CONFIG REQUIRED PATHS ${PY3_MOD_PYBIND11_CMAKE_DIR} NO_DEFAULT_PATH)
endif ()


# 获取 CANN 路径
set(ASCEND_CANN_PACKAGE_PATH)
if (BUILD_WITH_CANN)
    if (CUSTOM_ASCEND_CANN_PACKAGE_PATH)
        get_filename_component(ASCEND_CANN_PACKAGE_PATH "${CUSTOM_ASCEND_CANN_PACKAGE_PATH}" REALPATH)
    elseif (DEFINED ENV{ASCEND_HOME_PATH})
        get_filename_component(ASCEND_CANN_PACKAGE_PATH "$ENV{ASCEND_HOME_PATH}" REALPATH)
    else ()
        set(ASCEND_CANN_PACKAGE_PATH)
    endif ()
    if ("${ASCEND_CANN_PACKAGE_PATH}x" STREQUAL "x" OR NOT EXISTS "${ASCEND_CANN_PACKAGE_PATH}")
        set(BUILD_WITH_CANN OFF)
        message(STATUS "ASCEND_CANN_PACKAGE_PATH=${ASCEND_CANN_PACKAGE_PATH} is empty or not exist, auto turn off BUILD_WITH_CANN")
    endif ()
endif ()
message(STATUS "ASCEND_CANN_PACKAGE_PATH=${ASCEND_CANN_PACKAGE_PATH}")
message(STATUS "BUILD_WITH_CANN=${BUILD_WITH_CANN}")

# 获取 3rd Path
if (PYPTO_THIRD_PARTY_PATH)
    get_filename_component(PYPTO_THIRD_PARTY_PATH "${PYPTO_THIRD_PARTY_PATH}" REALPATH)
elseif (DEFINED ENV{PYPTO_THIRD_PARTY_PATH})
    get_filename_component(PYPTO_THIRD_PARTY_PATH "$ENV{PYPTO_THIRD_PARTY_PATH}" REALPATH)
else ()
    get_filename_component(PYPTO_THIRD_PARTY_PATH "${PTO_FWK_SRC_ROOT}/third_party_path" REALPATH)
    set(_Msg
            "PYPTO_THIRD_PARTY_PATH is not specified, ${PYPTO_THIRD_PARTY_PATH} will be used as its default value. "
            "It is necessary to confirm that the relevant software already exists in this path or that the network "
            "can be accessed normally so that CMake can automatically download the corresponding software."
    )
    string(REPLACE ";" "" _Msg "${_Msg}")
    message(WARNING "${_Msg}")
endif ()
message(STATUS "PYPTO_THIRD_PARTY_PATH=${PYPTO_THIRD_PARTY_PATH}")


########################################################################################################################
# CMake 选项, 缺省参数设置
#   按 CMake 构建过程对 CMake 选项, CMake 缺省参数进行配置
#   CMake 构建过程: 1) 配置阶段(Configure); 2) 构建阶段(Build); 3) 安装阶段(Install);
########################################################################################################################

# 构建阶段(Build)
#   构建类型
#       CMake中的Generator(生成器)是用于生成本地/本机构建系统的工具. 一般分为两种:
#       1. 单配置生成器(Single-configuration generator):
#          在配置(Configuration)阶段, 仅允许指定一种构建类型, 通过变量 CMAKE_BUILD_TYPE 指定;
#          在构建阶段(Build)无法更改构建类型, 仅允许使用配置(Configuration)阶段通过变量 CMAKE_BUILD_TYPE 指定的构建类型;
#          常见的此类型生成器有: Ninja, Unix Makefiles
#       2. 多配置生成器(Multi-configuration generator) :
#          在配置(Configuration)阶段, 仅指定构建阶段(Build)可用的构建类型列表, 通过变量 CMAKE_CONFIGURATION_TYPES 指定;
#          在构建阶段(Build)通过 "--config" 参数, 指定构建阶段具体的构建类型;
#          常见的此类型生成器有: Xcode, Visual Studio
#       所以:
#           1. 单配置生成器(Single-configuration generator)场景下, 如果构建类型(CMAKE_BUILD_TYPE)未指定, 则默认为 Debug ;
#           2. 多配置生成器(Multi-configuration generator)场景下, 如果构建阶段可选的构建类型(CMAKE_CONFIGURATION_TYPES)未指定,
#              则默认将其指定为CMake允许的构建类型全集 [Debug;Release;MinSizeRel;RelWithDebInfo]
message(STATUS "CMAKE_GENERATOR=${CMAKE_GENERATOR}")
get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if (GENERATOR_IS_MULTI_CONFIG)
    if (NOT CMAKE_CONFIGURATION_TYPES)
        set(CMAKE_CONFIGURATION_TYPES "Debug;Release;MinSizeRel;RelWithDebInfo" CACHE STRING "Configuration Build type" FORCE)
    endif ()
else ()
    if (NOT CMAKE_BUILD_TYPE)
        set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type(default Release)" FORCE)
    endif ()
    message(STATUS "CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}")
endif ()


# 构建阶段(Build)
#   可执行文件运行时库文件搜索路径 RPATH
#       在 UTest 及 STest 场景不略去 RPATH
string(REPLACE "," ":" ENABLE_UTEST "${ENABLE_UTEST}")
string(REPLACE "," ":" ENABLE_STEST "${ENABLE_STEST}")
string(REPLACE "," ":" ENABLE_STEST_DISTRIBUTED "${ENABLE_STEST_DISTRIBUTED}")
string(REPLACE "," ":" ENABLE_STEST_GROUP "${ENABLE_STEST_GROUP}")
if (NOT BUILD_WITH_CANN)
    if (ENABLE_STEST)
        set(ENABLE_STEST OFF)
        message(STATUS "Build without CANN, auto turn off ENABLE_STEST")
    endif ()
    if (ENABLE_STEST_DISTRIBUTED)
        set(ENABLE_STEST_DISTRIBUTED OFF)
        message(STATUS "Build without CANN, auto turn off ENABLE_STEST_DISTRIBUTED")
    endif ()
endif ()
if (ENABLE_UTEST OR ENABLE_STEST OR ENABLE_STEST_DISTRIBUTED)
    set(ENABLE_TESTS ON)
else ()
    set(ENABLE_TESTS OFF)
endif ()

if (ENABLE_TESTS)
    set(CMAKE_SKIP_RPATH FALSE)
else ()
    set(CMAKE_SKIP_RPATH TRUE)
endif ()

# 构建阶段(Build)
#   语言标准
set(CMAKE_C_STANDARD 11)  # 指定 C 语言使用 ISO C11 标准
set(CMAKE_C_STANDARD_REQUIRED ON)  # 要求严格支持 C11 标准
set(CMAKE_C_EXTENSIONS OFF)  # 禁用 C 编译器扩展, 使用纯 ISO C 标准
set(CMAKE_CXX_STANDARD 17)  # 指定 C++ 语言使用 ISO C++17 标准
set(CMAKE_CXX_STANDARD_REQUIRED ON)  # 要求严格支持 C++17 标准
set(CMAKE_CXX_EXTENSIONS OFF)  # 禁用 C 编译器扩展, 使用纯 ISO C++17 标准


# 构建阶段(Build)
#   CCACHE 配置
find_program(CCACHE_PROGRAM ccache)
if (CCACHE_PROGRAM)
    set(CMAKE_C_COMPILER_LAUNCHER   ${CCACHE_PROGRAM} CACHE PATH "C cache Compiler")
    set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM} CACHE PATH "CXX cache Compiler")
    message(STATUS "Use ccache, CCACHE_BASEDIR=$ENV{CCACHE_BASEDIR}")
else ()
    message(STATUS "ccache not found.")
endif ()

# 构建阶段(Build)
#   输出路径
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PTO_FWK_BIN_OUTPUT_ROOT}/bin)     # 设置可执行文件输出目录
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PTO_FWK_BIN_OUTPUT_ROOT}/lib)     # 设置动态库输出目录
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PTO_FWK_BIN_OUTPUT_ROOT}/lib)     # 设置静态库输出目录


# 安装阶段(Install)
#   安装路径
#       未显示设置 CMAKE_INSTALL_PREFIX (即 CMAKE_INSTALL_PREFIX 取缺省值)时, 设置与构建树根目录 CMAKE_CURRENT_BINARY_DIR 平级
if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
    get_filename_component(_Install_Path_Prefix "${CMAKE_CURRENT_BINARY_DIR}/../output" REALPATH)
    set(CMAKE_INSTALL_PREFIX    "${_Install_Path_Prefix}"  CACHE STRING "Install path" FORCE)
endif ()
message(STATUS "CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}")


########################################################################################################################
# 预处理
########################################################################################################################
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if (NOT "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU")
    if (ENABLE_GCOV)
        set(ENABLE_GCOV OFF)
        message(WARNING "GCov only supported in GNU Compiler, Current Compiler is ${CMAKE_C_COMPILER_ID}, Auto turn off it.")
    endif ()
    if (ENABLE_FEATURE_PYTHON_FRONT_END)
        message(FATAL_ERROR "Python frontend only supported GNU Compiler yet.")
    endif ()
endif ()

# ASAN / UBSAN 场景随编译执行用例场景下, 将相关检查在编译前执行, 避免出现编译完成后又无法执行的情况, 影响使用体验.
if ((ENABLE_ASAN OR ENABLE_UBSAN) AND (ENABLE_TESTS_EXECUTE OR ENABLE_FEATURE_PYTHON_FRONT_END))
    # 用于向外传递 XSAN 相关配置, 以保证 whl 包 ASan 场景的兼容性
    set(XSAN_CONFIG_FILE "${PTO_FWK_BIN_ROOT}/_pypto_xsan_config.txt")
    file(WRITE "${XSAN_CONFIG_FILE}" "")

    # LD_PRELOAD, 仅 GNU 编译器需要设置
    set(XSAN_LD_PRELOAD)
    if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        if (ENABLE_ASAN)
            # libasan.so
            execute_process(COMMAND ${CMAKE_C_COMPILER} --print-file-name=libasan.so
                    RESULT_VARIABLE _RST
                    OUTPUT_VARIABLE ASAN_SHARED_PATH)
            if (_RST)
                message(FATAL_ERROR "Can't get libasan.so path with ${CMAKE_C_COMPILER}")
            endif ()
            get_filename_component(ASAN_SHARED_PATH "${ASAN_SHARED_PATH}" DIRECTORY)
            get_filename_component(ASAN_SHARED_PATH "${ASAN_SHARED_PATH}/libasan.so" REALPATH)
            if (NOT EXISTS ${ASAN_SHARED_PATH})
                message(FATAL_ERROR
                        "ASAN_SHARED_PATH=${ASAN_SHARED_PATH} not exist. Please check the completeness of the compiler installation.")
            endif ()
            list(APPEND XSAN_LD_PRELOAD ${ASAN_SHARED_PATH})
        endif ()
        if (ENABLE_UBSAN)
            # libubsan.so
            execute_process(COMMAND ${CMAKE_C_COMPILER} --print-file-name=libubsan.so
                    RESULT_VARIABLE _RST
                    OUTPUT_VARIABLE UBSAN_SHARED_PATH)
            if (_RST)
                message(FATAL_ERROR "Can't get libubsan.so path with ${CMAKE_C_COMPILER}")
            endif ()
            get_filename_component(UBSAN_SHARED_PATH "${UBSAN_SHARED_PATH}" DIRECTORY)
            get_filename_component(UBSAN_SHARED_PATH "${UBSAN_SHARED_PATH}/libubsan.so" REALPATH)
            if (NOT EXISTS ${UBSAN_SHARED_PATH})
                message(FATAL_ERROR
                        "UBSAN_SHARED_PATH=${UBSAN_SHARED_PATH} not exist. Please check the completeness of the compiler installation.")
            endif ()
            list(APPEND XSAN_LD_PRELOAD ${UBSAN_SHARED_PATH})
        endif ()
        # libstdc++.so
        execute_process(COMMAND ${CMAKE_C_COMPILER} --print-file-name=libstdc++.so
                RESULT_VARIABLE _RST
                OUTPUT_VARIABLE STDC_SHARED_PATH)
        if (_RST)
            message(FATAL_ERROR "Can't get libstdc++.so path with ${CMAKE_C_COMPILER}")
        endif ()
        get_filename_component(STDC_SHARED_PATH "${STDC_SHARED_PATH}" DIRECTORY)
        get_filename_component(STDC_SHARED_PATH "${STDC_SHARED_PATH}/libstdc++.so" REALPATH)
        if (NOT EXISTS ${STDC_SHARED_PATH})
            message(FATAL_ERROR
                    "STDC_SHARED_PATH=${STDC_SHARED_PATH} not exist. Please check the completeness of the compiler installation.")
        endif ()
        list(APPEND XSAN_LD_PRELOAD ${STDC_SHARED_PATH})
        # 结果修正
        string(REPLACE ";" ":" XSAN_LD_PRELOAD "${XSAN_LD_PRELOAD}")
        set(XSAN_LD_PRELOAD "LD_PRELOAD=${XSAN_LD_PRELOAD}")
    endif ()
    if (XSAN_LD_PRELOAD)
        file(APPEND "${XSAN_CONFIG_FILE}" "${XSAN_LD_PRELOAD}\n")
    endif ()

    set(ASAN_OPTIONS)
    if (ENABLE_ASAN)
        # 谨慎修改 ASAN_OPTIONS 取值, 当前出现告警会使 GTest 失败.
        # halt_on_error=1, 出现告警时停止运行进而触发构建失败, 避免主进程或 fork 出的子进程出现错误无法发现的情况
        # detect_stack_use_after_return=1, 栈空间返回后使用检测
        # check_initialization_order, 尝试捕获初始化顺序问题
        # strict_init_order, 动态初始化器永远不能访问来自其他模块的全局变量, 及时或者已经初始化
        # strict_string_checks, 检查字符串参数是否正确以 null 终止
        # detect_leaks=1, 内存泄漏检测
        set(ASAN_OPTIONS "ASAN_OPTIONS=halt_on_error=1,detect_stack_use_after_return=1,check_initialization_order=1,strict_init_order=1,strict_string_checks=1,symbolize=1,detect_leaks=1")
        if (ENABLE_FEATURE_PYTHON_FRONT_END)
            set(LSAN_OPTIONS "LSAN_OPTIONS=suppressions=${PTO_FWK_SRC_ROOT}/cmake/asan_suppressions.txt")
            file(APPEND "${XSAN_CONFIG_FILE}" "${LSAN_OPTIONS}\n")
        endif ()
    endif ()
    if (ASAN_OPTIONS)
        file(APPEND "${XSAN_CONFIG_FILE}" "${ASAN_OPTIONS}\n")
    endif ()

    set(UBSAN_OPTIONS)
    if (ENABLE_UBSAN)
        # 谨慎修改 UBSAN_OPTIONS 取值, 当前出现告警会使 UT 失败.
        # halt_on_error=1, 出现告警时停止运行进而触发构建失败, 避免主进程或 fork 出的子进程出现错误无法发现的情况
        # print_stacktrace=1, 出错时打印调用栈
        set(UBSAN_OPTIONS "UBSAN_OPTIONS=halt_on_error=0,print_stacktrace=1")
    endif ()
    if (UBSAN_OPTIONS)
        file(APPEND "${XSAN_CONFIG_FILE}" "${UBSAN_OPTIONS}\n")
    endif ()
endif ()


########################################################################################################################
# 三方库
########################################################################################################################

# torch optional
set(ENABLE_TORCH_VERIFIER OFF)
if (ENABLE_TESTS)
    if ("${PY3_MOD_TORCH_VERSION}" STRGREATER_EQUAL "2.1.0")
        if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "9.4.0") OR
            (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "15.0.0"))
            set(ENABLE_TORCH_VERIFIER ON)
        endif()
    endif()
endif()
message(STATUS "ENABLE_TORCH_VERIFIER=${ENABLE_TORCH_VERIFIER}")