# Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc.  See LICENSE.
cmake_minimum_required(VERSION 3.0...3.23)


PROJECT(lsquic C CXX)

SET(LIBS "-lstdc++")

SET(LSQUIC_LIBSSL "" CACHE STRING "Specify SSL library used, can be BORINGSSL or OPENSSL")
OPTION(LSQUIC_FIU "Use Fault Injection in Userspace (FIU)" OFF)
OPTION(LSQUIC_BIN "Compile example binaries that use the library" ON)
OPTION(LSQUIC_TESTS "Compile library unit tests" ON)
OPTION(LSQUIC_SHARED_LIB "Compile as shared librarry" OFF)
OPTION(LSQUIC_DEVEL "Compile in development mode" OFF)
OPTION(LSQUIC_WEBTRANSPORT "Enable WebTransport support" OFF)

INCLUDE(GNUInstallDirs)
INCLUDE(CheckSymbolExists)

MESSAGE(STATUS "CMake v${CMAKE_VERSION}")
IF (CMAKE_SYSTEM_NAME STREQUAL "Linux")
    # If using older glibc, need to link with -lrt.  See clock_getres(2).
    check_symbol_exists(clock_getres "time.h" HAS_clock_getres_WITHOUT_LIBRT)

    if(NOT HAS_clock_getres_WITHOUT_LIBRT)
        find_library(RT_LIBRARY rt)
        set(NEED_LIBRT_FOR_clock_getres ON)
    endif()
ELSEIF (CMAKE_SYSTEM_NAME STREQUAL "Android")
    # for android-ndk >= r19b
    set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY "BOTH")
    set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE "BOTH")
    set(CMAKE_FIND_ROOT_PATH_MODE_PATH "BOTH")
ENDIF()

IF("${CMAKE_BUILD_TYPE}" STREQUAL "")
    SET(CMAKE_BUILD_TYPE Debug)
ENDIF()

MESSAGE(STATUS "Build type: ${CMAKE_BUILD_TYPE}")

IF (NOT "$ENV{EXTRA_CFLAGS}" MATCHES "-DLSQUIC_DEBUG_NEXT_ADV_TICK")
    SET(MY_CMAKE_FLAGS "-DLSQUIC_DEBUG_NEXT_ADV_TICK=1")
ENDIF()

IF (NOT "$ENV{EXTRA_CFLAGS}" MATCHES "-DLSQUIC_CONN_STATS=")
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_CONN_STATS=1")
ENDIF()

IF (NOT MSVC)

SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -Wall -Wextra -Wno-unused-parameter")
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -fno-omit-frame-pointer")

IF(CMAKE_COMPILER_IS_GNUCC AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9.3)
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -Wno-missing-field-initializers")
ENDIF()

IF(LSQUIC_FIU)
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DFIU_ENABLE=1")
    SET(LIBS ${LIBS} fiu)
ENDIF()

IF(CMAKE_BUILD_TYPE STREQUAL "Debug")
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -O0 -g3")
    IF(CMAKE_C_COMPILER MATCHES "clang" AND
                        NOT CMAKE_SYSTEM_NAME STREQUAL "Android" AND
                        NOT "$ENV{TRAVIS}" MATCHES "^true$" AND
                        NOT "$ENV{EXTRA_CFLAGS}" MATCHES "-fsanitize")
        SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -fsanitize=address")
        SET(LIBS ${LIBS} -fsanitize=address)
    ENDIF()
    # Uncomment to enable cleartext protocol mode (no crypto):
    #SET (MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_ENABLE_HANDSHAKE_DISABLE=1")
ELSE()
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -O3 -g0")
    # Comment out the following line to compile out debug messages:
    #SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_LOWEST_LOG_LEVEL=LSQ_LOG_INFO")
ENDIF()

IF (LSQUIC_DEVEL)
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_DEVEL=1")
ENDIF()

IF (LSQUIC_WEBTRANSPORT)
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_WEBTRANSPORT_SERVER_SUPPORT=1")
ENDIF()

IF(LSQUIC_PROFILE EQUAL 1)
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -g -pg")
ENDIF()

IF(LSQUIC_COVERAGE EQUAL 1)
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -fprofile-arcs -ftest-coverage")
ENDIF()

IF(MY_CMAKE_FLAGS MATCHES "fsanitize=address")
    MESSAGE(STATUS "AddressSanitizer is ON")
ELSE()
    MESSAGE(STATUS "AddressSanitizer is OFF")
ENDIF()

#MSVC
ELSE()
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4100")	# unreferenced formal parameter
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4115")	# unnamed type definition in parentheses
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4116")	# named type definition in parentheses
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4146")	# unary minus operator applied to unsigned type, result still unsigned
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4132")	# const initialization
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4200")	# zero-sized array
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4204")	# non-constant aggregate initializer
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4244")	# integer conversion
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4245")	# conversion from 'int' to 'unsigned int', signed/unsigned mismatch
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4267")	# integer conversion
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4214")	# nonstandard extension used: bit field types other than int
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4295")	# array is too small to include a terminating null character
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4324")	# structure was padded due to alignment specifier
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4334")	# result of 32-bit shift implicitly converted to 64 bits (was 64-bit shift intended?)
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4456")	# hide previous local declaration
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4459")	# hide global declaration
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4706")	# assignment within conditional expression
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4090")	# different 'const' qualifier (TODO: debug ls-sfparser.c)
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4305")	# truncation from double to float
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} /wd4201") # Allow nameless structs
SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -W4 -WX -Zi -DWIN32_LEAN_AND_MEAN -DNOMINMAX -D_CRT_SECURE_NO_WARNINGS -I${CMAKE_CURRENT_SOURCE_DIR}/wincompat")
IF(LSQUIC_SHARED_LIB)
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_SHARED_LIB")
ENDIF()
IF(CMAKE_BUILD_TYPE STREQUAL "Debug")
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -Od")
    #SET (MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DFIU_ENABLE=1")
    #SET(LIBS ${LIBS} fiu)
ELSE()
    SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -Ox")
    # Comment out the following line to compile out debug messages:
    #SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_LOWEST_LOG_LEVEL=LSQ_LOG_INFO")
ENDIF()

ENDIF() #MSVC

find_package(Perl)
IF(NOT PERL_FOUND)
    MESSAGE(FATAL_ERROR "Perl not found -- need it to generate source code")
ENDIF()

IF (MSVC)
    IF(LSQUIC_SHARED_LIB)
        set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS YES CACHE BOOL "Export all symbols")
        SET(LIB_SUFFIX .dll)
    ELSE()
        SET(LIB_SUFFIX .lib)
    ENDIF()
ELSE()
    IF(LSQUIC_SHARED_LIB)
        SET(LIB_SUFFIX .so)
    ELSE()
        SET(LIB_SUFFIX .a)
    ENDIF()
ENDIF()

IF(DEFINED BORINGSSL_DIR AND NOT DEFINED LIBSSL_DIR)
    SET(LIBSSL_DIR ${BORINGSSL_DIR})
    SET(LSQUIC_LIBSSL "BORINGSSL")
ENDIF()

IF(DEFINED BORINGSSL_LIB AND NOT DEFINED LIBSSL_LIB)
    SET(LIBSSL_LIB ${BORINGSSL_LIB})
    SET(LSQUIC_LIBSSL "BORINGSSL")
ENDIF()

IF(DEFINED BORINGSSL_LIB_ssl AND NOT DEFINED LIBSSL_LIB_ssl)
    SET(LIBSSL_LIB_ssl ${BORINGSSL_LIB_ssl})
    SET(LSQUIC_LIBSSL "BORINGSSL")
ENDIF()

IF(DEFINED BORINGSSL_LIB_crypto AND NOT DEFINED LIBSSL_LIB_crypto)
    SET(LIBSSL_LIB_crypto ${BORINGSSL_LIB_crypto})
    SET(LSQUIC_LIBSSL "BORINGSSL")
ENDIF()

IF (DEFINED BORINGSSL_INCLUDE AND NOT DEFINED SSLLIB_INCLUDE)
    SET(SSLLIB_INCLUDE ${BORINGSSL_INCLUDE})
    SET(LSQUIC_LIBSSL "BORINGSSL")
ENDIF()

IF (NOT DEFINED SSLLIB_INCLUDE AND DEFINED LIBSSL_DIR)
    FIND_PATH(SSLLIB_INCLUDE NAMES openssl/ssl.h
                PATHS ${LIBSSL_DIR}/include
                NO_DEFAULT_PATH)
ENDIF()
# This must be done before adding other include directories to take
# precedence over header files from other SSL installs.

IF (SSLLIB_INCLUDE)
    MESSAGE(STATUS "TLS library include directory ${SSLLIB_INCLUDE}")
    INCLUDE_DIRECTORIES(${SSLLIB_INCLUDE})
ELSE()
    MESSAGE(FATAL_ERROR "TLS library headers not found")
ENDIF()

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}  ${MY_CMAKE_FLAGS} $ENV{EXTRA_CFLAGS} -I${SSLLIB_INCLUDE}")

MESSAGE(STATUS "Compiler flags: ${CMAKE_C_FLAGS}")

IF (NOT DEFINED LIBSSL_LIB AND DEFINED LIBSSL_DIR)
    SET(LIBSSL_LIB ${LIBSSL_DIR})
    FOREACH(LIB_NAME ssl crypto)
        IF (CMAKE_SYSTEM_NAME STREQUAL Windows)
            FIND_LIBRARY(LIBSSL_LIB_${LIB_NAME}
                NAMES ${LIB_NAME}
                PATHS ${LIBSSL_LIB}/ ${LIBSSL_LIB}/lib/ ${LIBSSL_LIB}/ssl/ ${LIBSSL_LIB}/crypto/
                PATH_SUFFIXES Debug Release MinSizeRel RelWithDebInfo
                NO_DEFAULT_PATH)
        ELSE()
            FIND_LIBRARY(LIBSSL_LIB_${LIB_NAME}
                NAMES lib${LIB_NAME}${LIB_SUFFIX}
                PATHS ${LIBSSL_LIB} ${LIBSSL_LIB}/lib/ ${LIBSSL_LIB}/ssl/ ${LIBSSL_LIB}/crypto/
                NO_DEFAULT_PATH)
        ENDIF()
        IF(LIBSSL_LIB_${LIB_NAME})
            MESSAGE(STATUS "Found ${LIB_NAME} library: ${LIBSSL_LIB_${LIB_NAME}}")
        ELSE()
            MESSAGE(STATUS "${LIB_NAME} library not found")
        ENDIF()
    ENDFOREACH()

ELSE()


    FOREACH(LIB_NAME ssl crypto)
        # If LIBSSL_LIB is defined, try find each lib. Otherwise, user should define LIBSSL_LIB_ssl,
        # LIBSSL_LIB_crypto and so on explicitly. For example, including boringssl and lsquic both via
        # add_subdirectory:
        #   add_subdirectory(third_party/boringssl)
        #   set(LIBSSL_LIB_ssl ssl)
        #   set(LIBSSL_LIB_crypto crypto)
        #   add_subdirectory(third_party/lsquic)
        IF (DEFINED LIBSSL_LIB)
            IF (CMAKE_SYSTEM_NAME STREQUAL Windows)
                FIND_LIBRARY(LIBSSL_LIB_${LIB_NAME}
                    NAMES ${LIB_NAME}
                    PATHS ${LIBSSL_LIB}
                    PATH_SUFFIXES Debug Release MinSizeRel RelWithDebInfo
                    NO_DEFAULT_PATH)
            ELSE()
                FIND_LIBRARY(LIBSSL_LIB_${LIB_NAME}
                    NAMES lib${LIB_NAME}${LIB_SUFFIX}
                    PATHS ${LIBSSL_LIB} ${LIBSSL_LIB}/lib/ ${LIBSSL_LIB}/ssl ${LIBSSL_LIB}/crypto
                    PATH_SUFFIXES ${LIB_NAME}
                    NO_DEFAULT_PATH)
            ENDIF()
        ENDIF()
        IF(LIBSSL_LIB_${LIB_NAME})
            MESSAGE(STATUS "Found ${LIB_NAME} library: ${LIBSSL_LIB_${LIB_NAME}}")
        ELSE()
            MESSAGE(FATAL_ERROR "LIBSSL_LIB_${LIB_NAME} library not found")
        ENDIF()
    ENDFOREACH()

ENDIF()

IF ("${LIBSSL_LIB_ssl}" MATCHES "\\.a" OR "${LIBSSL_LIB_ssl}" MATCHES "\\.lib")
    SET(STATIC_BUILD "yes")
ELSE()
    SET(STATIC_BUILD "no")
ENDIF()

if ("${LSQUIC_LIBSSL}" STREQUAL "BORINGSSL")

    SET(QUIC_TLS_LIB "BORINGSSL")
    MESSAGE(STATUS "Force BoringSSL, skip TLS library detection.")
    SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_BORINGSSL")

ELSEIF ("${LSQUIC_LIBSSL}" STREQUAL "OPENSSL")

    SET(QUIC_TLS_LIB "OPENSSL")
    MESSAGE(STATUS "Force OpenSSL, skip TLS library detection.")
    SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_OPENSSL")

ELSE()

    SET(CMAKE_REQUIRED_INCLUDES "${SSLLIB_INCLUDE}")
    SET(CMAKE_REQUIRED_LIBRARIES "${LIBSSL_LIB_ssl};${LIBSSL_LIB_crypto}")
    if ("${STATIC_BUILD}" STREQUAL "yes")
        LIST(APPEND CMAKE_REQUIRED_LIBRARIES "-pthread -lstdc++")
    ELSE()
        SET(CMAKE_REQUIRED_LINK_DIRECTORIES "${LIBSSL_DIR}")
        SET(CMAKE_EXE_LINKER_FLAGS "-Wl,-rpath=${LIBSSL_LIB} -L${LIBSSL_LIB} -lssl -lcrypto")
    ENDIF()
    check_symbol_exists(SSL_provide_quic_data openssl/ssl.h HAVE_SSL_PROVIDE_QUIC_DATA)
    IF(NOT HAVE_SSL_PROVIDE_QUIC_DATA)
        check_symbol_exists(SSL_set_quic_tls_cbs openssl/ssl.h HAVE_SSL_SET_QUIC_TLS_CBS)
        IF(NOT HAVE_SSL_SET_QUIC_TLS_CBS)
            MESSAGE(FATAL_ERROR "TLS library implementation contains no 3rd party quic api:")
        ELSE()
            SET(QUIC_TLS_LIB "OPENSSL")
            MESSAGE(STATUS "TLS library is canonical OpenSSL")
            SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_OPENSSL")
        ENDIF()
    ELSE()
        SET(QUIC_TLS_LIB "BORINGSSL")
        MESSAGE(STATUS "TLS library is BoringSSL")
        SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_BORINGSSL")
    ENDIF()
    UNSET(CMAKE_REQUIRED_INCLUDES)
    UNSET(CMAKE_EXE_LINKER_FLAGS)
    UNSET(CMAKE_REQUIRED_LIBRARIES)

ENDIF()

SET(CMAKE_INCLUDE_CURRENT_DIR ON)
INCLUDE_DIRECTORIES(include)
IF(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    # Find libevent on FreeBSD:
    include_directories( /usr/local/include )
    link_directories( /usr/local/lib )
ENDIF()

IF (CMAKE_SYSTEM_NAME STREQUAL Windows AND LSQUIC_TESTS AND LSQUIC_BIN)
    FIND_PATH(GETOPT_INCLUDE_DIR NAMES getopt.h)
    IF (GETOPT_INCLUDE_DIR)
        INCLUDE_DIRECTORIES(${GETOPT_INCLUDE_DIR})
    ELSE()
        MESSAGE(FATAL_ERROR "getopt.h was not found")
    ENDIF()
        FIND_LIBRARY(GETOPT_LIB getopt)
    IF(GETOPT_LIB)
        MESSAGE(STATUS "Found getopt: ${GETOPT_LIB}")
    ELSE()
        MESSAGE(STATUS "getopt not found")
    ENDIF()
ENDIF()

# Find zlib and libevent header files and library files
# TODO: libevent is not strictly necessary to build the library.
FIND_PATH(ZLIB_INCLUDE_DIR NAMES zlib.h)
IF (ZLIB_INCLUDE_DIR)
    INCLUDE_DIRECTORIES(${ZLIB_INCLUDE_DIR})
ELSE()
    MESSAGE(FATAL_ERROR "zlib.h was not found")
ENDIF()
IF (CMAKE_SYSTEM_NAME STREQUAL Windows)
    FIND_LIBRARY(ZLIB_LIB zlib)
ELSEIF(CMAKE_SYSTEM_NAME STREQUAL Darwin)
    # XXX somehow FIND_LIBRARY() does not find zlib on Travis?
    SET(ZLIB_LIB z)
ELSE()
    FIND_LIBRARY(ZLIB_LIB libz${LIB_SUFFIX})
ENDIF()
IF(ZLIB_LIB)
    MESSAGE(STATUS "Found zlib: ${ZLIB_LIB}")
ELSE()
    MESSAGE(STATUS "zlib not found")
ENDIF()

SET(LIBS lsquic ${LIBSSL_LIB_ssl} ${LIBSSL_LIB_crypto} ${ZLIB_LIB} ${LIBS})

IF (LSQUIC_BIN)
    FIND_PATH(EVENT_INCLUDE_DIR NAMES event2/event.h
              PATHS ${PROJECT_SOURCE_DIR}/../third-party/include)
    IF (EVENT_INCLUDE_DIR)
        INCLUDE_DIRECTORIES(${EVENT_INCLUDE_DIR})
    ELSE()
        MESSAGE(WARNING "event2/event.h was not found: binaries won't be built")
        SET(LSQUIC_BIN OFF)
    ENDIF()
ENDIF()

IF (LSQUIC_BIN)
    IF (CMAKE_SYSTEM_NAME STREQUAL Windows)
        FIND_LIBRARY(EVENT_LIB event)
    ELSE()
        FIND_LIBRARY(EVENT_LIB libevent${LIB_SUFFIX}
                    PATHS ${PROJECT_SOURCE_DIR}/../third-party/lib)
        IF(NOT EVENT_LIB)
            FIND_LIBRARY(EVENT_LIB libevent.so)
        ENDIF()
    ENDIF()
    IF(EVENT_LIB)
        MESSAGE(STATUS "Found event: ${EVENT_LIB}")
    ELSE()
        MESSAGE(WARNING "libevent not found: binaries won't be built")
        SET(LSQUIC_BIN OFF)
    ENDIF()
ENDIF()


IF (NOT MSVC)
    LIST(APPEND LIBS pthread m)
ELSE()
    LIST(APPEND LIBS ws2_32)
ENDIF()

IF (LSQUIC_BIN)
    ADD_SUBDIRECTORY(bin)
ENDIF()

add_subdirectory(src)

IF(LSQUIC_TESTS AND CMAKE_BUILD_TYPE STREQUAL "Debug")
    # Our test framework relies on assertions, only compile if assertions are
    # enabled.
    #
    enable_testing()

    # Enable parallel test execution with CTest
    include(ProcessorCount)
    ProcessorCount(N)
    if(NOT N EQUAL 0)
        message(STATUS "CTest will run tests in parallel with ${N} jobs by default")

        # Add a serial test target
        add_custom_target(test-serial
            COMMAND ${CMAKE_CTEST_COMMAND} --force-new-ctest-process
            COMMENT "Running tests serially"
            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
        )

        # Set the default test arguments to use parallel execution
        # This works by setting CTEST_TEST_ARGS which is used by the built-in test target
        set(CMAKE_CTEST_ARGUMENTS "-j${N};--force-new-ctest-process")
    endif()

    add_subdirectory(tests)
ENDIF()


FIND_PROGRAM(SPHINX NAMES sphinx-build)
IF(SPHINX)
    ADD_CUSTOM_TARGET(docs
        ${SPHINX} -b html
        docs
        docs/_build
    )
ELSE()
    MESSAGE(STATUS "sphinx-build not found: docs won't be made")
ENDIF()

INSTALL(FILES
    include/lsquic.h
    include/lsquic_types.h
    include/lsxpack_header.h
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/lsquic
)

if(WIN32)
    install(FILES
        wincompat/vc_compat.h
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/lsquic
    )
endif()
