cmake_minimum_required(VERSION 3.20)
project(ngscopeclient VERSION 0.2)
include_directories(cmake) #WORKAROUND needed for find_package(Vulkan) for Vulkan SDK
enable_language(CXX)

# Warn if platform is not 64-bit
if(NOT (CMAKE_SIZEOF_VOID_P EQUAL 8))
	message(WARNING "You are attempting to build on a 32-bit platform. While this may work and we may accept fixes "
		              "upstream, it will most likely fail and is unsupported.")
endif()

# Check for Vulkan SDK env var and prepend it to CMAKE_PREFIX_PATH if set to prefer Vulkan SDK packages if available
if(DEFINED ENV{VULKAN_SDK})
	#WORKAROUND: Vulkan SDK is incompatible with MinGW due to C++ linkage
	if(WIN32 AND (NOT CMAKE_GENERATOR MATCHES "Visual Studio"))
		message(WARNING "Detected VULKAN_SDK at $ENV{VULKAN_SDK}.")
		message(WARNING "Compiling and linking against the Vulkan SDK is not supported with MSYS. The build system "
			      "will ignore the SDK and you will need to have the Vulkan packages installed from the MSYS repositories.")
	else()
		message("Detected and using VULKAN_SDK at $ENV{VULKAN_SDK}")
		cmake_path(CONVERT "$ENV{VULKAN_SDK}" TO_CMAKE_PATH_LIST VULKAN_SDK_PATH NORMALIZE)
		list(PREPEND CMAKE_PREFIX_PATH ${VULKAN_SDK_PATH})
	endif()
endif()

# Needed for vk::PhysicalDevicePortabilitySubsetFeaturesKHR
add_compile_definitions(VK_ENABLE_BETA_EXTENSIONS=1)

# Git is used for git-describe based version generation if we have it
find_package(Git)

if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/release-info.cmake")
	message("Release-info file is present")
	include("${CMAKE_CURRENT_SOURCE_DIR}/release-info.cmake")
endif ()

#Set up versioning (with a dummy string for now if Git isn't present)
if(Git_FOUND OR NGSCOPECLIENT_PACKAGE_VERSION)
	if(NOT NGSCOPECLIENT_PACKAGE_VERSION)
		execute_process(
			COMMAND ${GIT_EXECUTABLE} describe --always --tags
			WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
			OUTPUT_VARIABLE NGSCOPECLIENT_VERSION
			OUTPUT_STRIP_TRAILING_WHITESPACE)
		message("Git reports scopehal-apps version ${NGSCOPECLIENT_VERSION}")
	else ()
		set(NGSCOPECLIENT_VERSION "${NGSCOPECLIENT_PACKAGE_VERSION}")
		message("External release version is ${NGSCOPECLIENT_VERSION}")
	endif ()

	if(NOT NGSCOPECLIENT_PACKAGE_VERSION_LONG)
		execute_process(
			COMMAND ${GIT_EXECUTABLE} describe --always --tags --long
			WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
			OUTPUT_VARIABLE NGSCOPECLIENT_VERSION_LONG
			OUTPUT_STRIP_TRAILING_WHITESPACE)
	else()
		set(NGSCOPECLIENT_VERSION_LONG "${NGSCOPECLIENT_PACKAGE_VERSION_LONG}")
		message("External long release version is ${NGSCOPECLIENT_VERSION_LONG}")
	endif ()

	# For point releases make MSI version 10x+9. This limits us to 9 release candidates which should be ok??
	# ex: 0.1.2-rc2 is 0.1.22
	# ex: 0.1.2 is 0.1.29

	# Ugly string parsing to make windows build happy
	# First path: release candidate tags for primary releases
	if(NGSCOPECLIENT_VERSION_LONG MATCHES "v([0-9]*).([0-9]*)-rc([0-9])-([0-9]*)")
		set(MSI_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}.${CMAKE_MATCH_4}")

	# Release candidates for point releases
	elseif(NGSCOPECLIENT_VERSION_LONG MATCHES "v([0-9]*).([0-9]*).([0-9]*)-rc([0-9])-([0-9]*)")
		set(MSI_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}${CMAKE_MATCH_4}.${CMAKE_MATCH_5}")

	# Release tags
	elseif(NGSCOPECLIENT_VERSION_LONG MATCHES "v([0-9]*).([0-9]*)-([0-9]*)")
		set(MSI_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.9.${CMAKE_MATCH_3}")

	# Release tags for point releases
	elseif(NGSCOPECLIENT_VERSION_LONG MATCHES "v([0-9]*).([0-9]*).([0-9]*)-([0-9]*)")
		set(MSI_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}9.${CMAKE_MATCH_4}")

	# No release matched
	else()
		message(FATAL_ERROR "Unrecognized version tag ${NGSCOPECLIENT_VERSION} / ${NGSCOPECLIENT_VERSION_LONG}, can't create a VERSIONINFO from it. Must be of format v1.2, v1.2.3, v1.2-rc3, v1.2.3-rc4")
	endif()

else()
	set(NGSCOPECLIENT_VERSION "unknown")
	set(MSI_VERSION "0.2")
	message("Git not detected, scopehal-apps version unknown")
endif()

set(PROJECT_VERSION "${NGSCOPECLIENT_VERSION}")

include(CTest)

# Configuration settings
set(BUILD_DOCS CACHE BOOL "Build end user documentation")
set(BUILD_DEVDOCS CACHE BOOL "Build developer documentation")
set(ANALYZE CACHE BOOL "Run static analysis on the code, requires cppcheck and clang-analyzer to be installed")
set(DISABLE_PCH CACHE BOOL "Disable precompiled headers as this may break certain configurations")
set(BUILD_TESTING CACHE BOOL "Build unit tests")

# Build with C++ 17 on Linux for maximum compatibility with older system
# but for Windows, enable C++ 20 to bypass a deprecation warning
# (workaround for https://github.com/KhronosGroup/Vulkan-Hpp/issues/2034)
# even though we do not yet use any C++ 20 features in our codebase
if(WIN32)
	set(CMAKE_CXX_STANDARD 20)

	if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
		add_compile_options(-Wno-error=c2y-extensions)
	endif()
else()
	set(CMAKE_CXX_STANDARD 17)
endif()

set(CMAKE_CXX_STANDARD_REQUIRED ON) # error if compiler doesn't support c++17
set(CMAKE_CXX_EXTENSIONS OFF) # use c++17 instead of gnu++17

# PCH needs to be disabled before targets are created/added/PCH is added to them
if(DISABLE_PCH)
	set(CMAKE_DISABLE_PRECOMPILE_HEADERS ON)
endif()

# Compiler warnings for GCC/Clang
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
	add_compile_options(-Wall -Wextra -Wuninitialized)
	add_compile_options(-Wshadow -Wpedantic -Wcast-align -Wwrite-strings)
	add_compile_options(-Wmissing-declarations -Wvla)
	add_compile_options(-Werror -Wno-error=deprecated-declarations -Wno-error=unused-parameter -Wno-error=unused-variable -Wno-error=unused-result -Wno-error=shadow)

	# 2026-05-01 mingw g++ 16.1.0 seems to be giving false positives around uninitialized values inside std::string
	# 2026-05-03 arch has picked up the same gcc issue
	add_compile_options(-Wno-error=uninitialized)
	add_compile_options(-Wno-error=maybe-uninitialized)
endif()

# Compiler warnings specific to GCC or Clang
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 13)
	add_compile_options($<$<COMPILE_LANG_AND_ID:CXX,GNU>:-Woverloaded-virtual=1>)
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Woverloaded-virtual -Wpointer-sign -Wno-gnu-zero-variadic-macro-arguments -Wno-unknown-warning-option)
endif()
add_compile_options(-mtune=native)
add_compile_options("$<$<CONFIG:RELEASE>:-O3>")
add_compile_options("$<$<CONFIG:RELWITHDEBINFO>:-O3;-g>")
add_compile_options("$<$<CONFIG:DEBUG>:-Og;-D_DEBUG;-g>")
add_compile_options("$<$<CONFIG:DEBUGNOOPT>:-O0;-D_DEBUG;-g>")

# Needed for %zu/%zd on MSYS2, must be added here to avoid redefinition
if(WIN32 AND (NOT MSVC))
    add_compile_definitions(__USE_MINGW_ANSI_STDIO=1)
endif()

# Default build type
if(NOT CMAKE_BUILD_TYPE)
	set(CMAKE_BUILD_TYPE "Release")
endif()

if(SANITIZE)
	message(STATUS "SANITIZE set, enabling sanitizers")
	add_compile_options(-fsanitize=address -fsanitize=undefined)
	add_link_options(-fsanitize=address -fsanitize=undefined)
endif()

if(WIN32)
	add_compile_options(-D_USE_MATH_DEFINES -D_POSIX_THREAD_SAFE_FUNCTIONS)
endif()

# Package detection
find_package(PkgConfig MODULE REQUIRED)
find_package(ZLIB REQUIRED)

# yaml-cpp is used in scopehal and ngscopeclient
find_package(yaml-cpp REQUIRED)
#WORKAROUND Needed for Debian Bullseye, which does not provide a yaml-cpp::yaml-cpp target
if(NOT TARGET yaml-cpp::yaml-cpp)
	find_library(YAML_CPP_LIBRARIES_FILES NAMES ${YAML_CPP_LIBRARIES})
	if(YAML_CPP_LIBRARIES_FILES MATCHES ".so$")
		add_library(yaml-cpp::yaml-cpp SHARED IMPORTED)
	elseif(YAML_CPP_LIBRARIES_FILES MATCHES ".a$")
		add_library(yaml-cpp::yaml-cpp STATIC IMPORTED)
	else()
		message(FATAL_ERROR "Unexpected partially-installed yaml-cpp, is it installed correctly?")
	endif()
	set_property(TARGET yaml-cpp::yaml-cpp PROPERTY IMPORTED_LOCATION ${YAML_CPP_LIBRARIES_FILES})
	#WORKAROUND The cmake file for yaml-cpp on debian bullseye is broken and provides a wrong YAML_CPP_INCLUDE_DIR
	find_path(YAML_CPP_INCLUDEFILES_DIR yaml-cpp/yaml.h REQUIRED)
	cmake_path(GET YAML_CPP_INCLUDEFILES_DIR PARENT_PATH YAML_CPP_INCLUDE_DIR)
	set_property(TARGET yaml-cpp::yaml-cpp PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${YAML_CPP_INCLUDEFILES_DIR})
endif()

pkg_check_modules(SIGCXX QUIET IMPORTED_TARGET sigc++-3.0) # look for latest version first
if(NOT SIGCXX_FOUND)
	pkg_check_modules(SIGCXX QUIET IMPORTED_TARGET sigc++-2.0) # look for older version too
endif()
if(NOT SIGCXX_FOUND)
	message(FATAL_ERROR "Unable to find any version of sigc++; this is required to build ngscopeclient.")
endif()

# Check if the user has provided a path for nvtx3 (not autodetected currently)
include(CheckIncludeFileCXX)
if(NVTX_PATH)
	check_include_file_cxx("${NVTX_PATH}/include/nvtx3/nvtx3.hpp" NVTX_FOUND)
	if(NVTX_FOUND)
		message("-- Found NVIDIA NVTX3: ${NVTX_PATH}")
		include_directories("${NVTX_PATH}/include")
		add_compile_definitions(HAVE_NVTX=1)
	endif()
endif()

# We still use gtk on Linux for the file browser dialog in "native" mode)
if(LINUX)
	pkg_check_modules(GTK QUIET IMPORTED_TARGET REQUIRED gtk+-3.0)
	pkg_search_module(WAYLAND_CLIENT wayland-client IMPORTED_TARGET GLOBAL REQUIRED)
endif()

# Required for glslang on multiple old platforms: not just Fedora 38 as the original comment indicated,
# but also Debian Bookworm. Can't remove this until we've cut support for all of those
# (likely when Forky comes out in 2027)
find_package(Threads MODULE REQUIRED)

# Configure and enable OpenMP
find_package(OpenMP MODULE)
if(NOT OpenMP_CXX_FOUND)
	message(FATAL_ERROR "ngscopeclient requires OpenMP but your C++ compiler does not appear to support it.")
endif()

# GLFW is required
find_package(glfw3 3.2 REQUIRED)

# Find Vulkan-related packages
#WORKAROUND glslang does not look for SPIRV-Tools-opt but needs it on macOS, harmless elsewhere
find_package(SPIRV-Tools-opt REQUIRED)
if(DEFINED VULKAN_SDK_PATH)
	#WORKAROUND The Vulkan SDK ships incomplete cmake config files.
	# Work around this, which does require FindVulkan.cmake.
	# FindVulkan does not find glslang correctly on macOS
	find_package(Vulkan REQUIRED COMPONENTS glslc shaderc_combined SPIRV-Tools)
	find_package(glslang REQUIRED)
	add_library(Vulkan::Loader ALIAS Vulkan::Vulkan)
else()
	find_package(glslang REQUIRED)
	find_package(VulkanHeaders REQUIRED)
	find_package(VulkanLoader QUIET)
	if(NOT VulkanLoader_FOUND) #WORKAROUND Alpine Linux has missing cmake files for VulkanLoader
		find_library(VulkanLoader_LIB libvulkan.so REQUIRED)
		cmake_path(GET VulkanLoader_LIB FILENAME VulkanLoader_LIB_NAME)
		add_library(Vulkan::Loader SHARED IMPORTED)
		set_property(TARGET Vulkan::Loader PROPERTY IMPORTED_LOCATION ${VulkanLoader_LIB})
	endif()
endif()
# The following should work regardless of Vulkan SDK or individual components
# shaderc does not provide a .cmake file, but does provide a pkgconfig file
# pkgconfig file is not provided by Vulkan SDK, however
pkg_check_modules(SHADERC shaderc QUIET IMPORTED_TARGET)
# This is needed due to shaderc not providing a programmatic way to derive the path of the glslc executable
if(SHADERC_FOUND)
	cmake_path(GET SHADERC_INCLUDE_DIRS PARENT_PATH SHADERC_PREFIX)
endif()
# This should find glslc from either shaderc or the Vulkan SDK
find_program(Vulkan_GLSLC_EXECUTABLE glslc HINTS SHADERC_PREFIX)

if(NOT Vulkan_GLSLC_EXECUTABLE)
	message(FATAL_ERROR "glslc not found. This is needed to compile shaders. Please install shaderc or load the Vulkan SDK.")
else()
	message("-- Found glslc: ${Vulkan_GLSLC_EXECUTABLE}")
endif()

# This is needed due to VkFFT not using a standard path for glslang_c_interface.h
get_target_property(glslang_INCLUDE_DIR glslang::glslang INTERFACE_INCLUDE_DIRECTORIES)
# Find MoltenVK on macOS, no need to link it later
if(APPLE)
	find_library(MOLTENVK_LIBRARIES NAMES MoltenVK libMoltenVK)
	if( NOT MOLTENVK_LIBRARIES )
		message( FATAL_ERROR "scopehal-apps requires MoltenVK to be installed in order to function on macOS.")
	endif()
endif()

if(NOT WIN32)
	include(GNUInstallDirs)
endif()

# Documentation
add_subdirectory("${PROJECT_SOURCE_DIR}/doc")
if(NOT BUILD_DOCS)
	set_property(TARGET doc PROPERTY EXCLUDE_FROM_ALL ON)
endif()

# Static analysis
if(ANALYZE)
	find_program(CPPCHECK_PATH cppcheck DOC "Path to cppcheck when ANALYZE is enabled")
	if(CPPCHECK_PATH)
		execute_process(COMMAND ${CPPCHECK_PATH} "--version" OUTPUT_VARIABLE CPPCHECK_VER_STR ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
		string(REPLACE "Cppcheck " "" CPPCHECK_VERSION ${CPPCHECK_VER_STR})
		if(CPPCHECK_VERSION VERSION_GREATER_EQUAL "2")
			set(CMAKE_CXX_CPPCHECK "${CPPCHECK_PATH};-DFT_USE_AUTOCONF_SIZEOF_TYPES;-D__GNUC__;--enable=warning,performance,portability;--suppress=*:*sigc*;--suppress=*:*glibmm*;--suppress=*:*gtkmm*;--suppress=*:*imgui*;--suppress=*:*nativefiledialog*;--suppress=*:*tests*/*;--suppress=*:*canvas_ity*/*;--suppress=*:*nativefiledialog-extended*/*--suppress=*:*ImGuiFileDialog*/*;--inline-suppr;-q;--std=c++17")
			message(STATUS "Found CPPCheck: ${CPPCHECK_PATH} (found version \"${CPPCHECK_VERSION}\")")
		else()
			message(STATUS "Found CPPCheck: ${CPPCHECK_PATH} but ignored it as it was ${CPPCHECK_VERSION} < 2")
		endif()
	else()
		message(FATAL_ERROR "CPPCheck not found, required for ANALYZE")
	endif()
endif()

# Main project code
add_subdirectory("${PROJECT_SOURCE_DIR}/lib/scopehal")
add_subdirectory("${PROJECT_SOURCE_DIR}/lib/scopeprotocols")
add_subdirectory("${PROJECT_SOURCE_DIR}/lib/xptools")
add_subdirectory("${PROJECT_SOURCE_DIR}/lib/log")
add_subdirectory("${PROJECT_SOURCE_DIR}/src/ngscopeclient")
add_subdirectory("${PROJECT_SOURCE_DIR}/src/nativefiledialog-extended")

add_subdirectory(devdoc)

# Unit tests
if(Git_FOUND AND BUILD_TESTING)
	find_package(Catch2 REQUIRED)
	include(Catch)
	#Catch2 v3.x.y have a breaking change:
	# Must include catch2/catch_all.hpp instead of catch2/catch.hpp
	# So we set a compile flag to let the code know.
	if(NOT Catch2_VERSION MATCHES "^[0-2]\\.")
		add_compile_options(-D_CATCH2_V3)
	endif()
	add_subdirectory("${PROJECT_SOURCE_DIR}/tests")
endif()

# Example code and other utilities, don't build on non-POSIX yet
if(NOT WIN32)
	add_subdirectory("${PROJECT_SOURCE_DIR}/src/examples/curvetrace")
	#add_subdirectory("${PROJECT_SOURCE_DIR}/src/examples/usbcsv")
endif()

# Make sure all of our shared libraries are built relocatable
set_property(TARGET scopehal PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET log PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET xptools PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET scopeprotocols PROPERTY POSITION_INDEPENDENT_CODE ON)

# CPack package generation
set(CPACK_PACKAGE_NAME "ngscopeclient")
set(CPACK_PACKAGE_VENDOR "ngscopeclient.org project")
string(REGEX MATCH "v([^ ]*)" "\\1" VERSION_OUT "${NGSCOPECLIENT_VERSION}")
set(CPACK_PACKAGE_VERSION "${CMAKE_MATCH_1}")
set(CPACK_PACKAGE_DESCRIPTION "Cross platform T&M remote control and signal analysis suite")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://www.ngscopeclient.org/")
set(CPACK_THREADS 0)
set(CPACK_STRIP_FILES TRUE)

# Figure out what distro version we're on
if(LINUX)
	find_program(LSB_RELEASE_EXEC lsb_release REQUIRED)
	execute_process(COMMAND ${LSB_RELEASE_EXEC} -is
		OUTPUT_VARIABLE DISTRO_NAME
		OUTPUT_STRIP_TRAILING_WHITESPACE
		COMMAND_ERROR_IS_FATAL ANY)
	execute_process(COMMAND ${LSB_RELEASE_EXEC} -rs
		OUTPUT_VARIABLE DISTRO_VER
		OUTPUT_STRIP_TRAILING_WHITESPACE
		COMMAND_ERROR_IS_FATAL ANY)

	message(STATUS "Linux distribution target for packaging: name ${DISTRO_NAME}, version ${DISTRO_VER}")

	# Debian specific packaging config
	set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Andrew Zonenberg <andrew.zonenberg@antikernel.net>")
	set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)

endif()

if(APPLE)
	set(CPACK_BUNDLE "ON")
	set(CPACK_GENERATOR "Bundle")
	set(CPACK_BUNDLE_PLIST "${PROJECT_SOURCE_DIR}/src/ngscopeclient/macos/Info.plist")
	# Must be generated with:
	# iconutil -c icns ngscopeclient.iconset
	set(CPACK_BUNDLE_ICON "${PROJECT_SOURCE_DIR}/src/ngscopeclient/icons/macos/ngscopeclient.icns")
	set(CPACK_PACKAGE_ICON "${PROJECT_SOURCE_DIR}/src/ngscopeclient/icons/macos/ngscopeclient.icns")
	set(CPACK_BUNDLE_NAME "ngscopeclient")
	set(CPACK_BUNDLE_STARTUP_COMMAND "${PROJECT_SOURCE_DIR}/src/ngscopeclient/macos/ngscopeclient.sh")
	set_target_properties(ngscopeclient PROPERTIES
		INSTALL_RPATH "@executable_path/../lib;/usr/local/lib;/opt/homebrew/lib")
endif()

# this must be at the very end *after* we've done all the config
include(CPack)
