aboutsummaryrefslogtreecommitdiff
#
# InspIRCd -- Internet Relay Chat Daemon
#
#   Copyright (C) 2026 Sadie Powell <sadie@witchery.services>
#
# This file is part of InspIRCd.  InspIRCd is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#


################################################################################
# build_module: Builds a single module.
#
# MODULE (STRING): The name of the module. This will be used as the CMake target
#                  name and (with CMAKE_SHARED_LIBRARY_SUFFIX appended) as the
#                  module's filename.
#
# MODULE_SOURCE(LIST): One or more source files for the module.
function(build_module MODULE MODULE_SOURCE)
	if(NOT ${MODULE} MATCHES "^core_")
		set(MODULE "m_${MODULE}")
	endif()

	add_library(${MODULE} MODULE ${MODULE_SOURCE})
	target_compile_definitions(${MODULE} PRIVATE
		"MODNAME=\"${MODULE}\""
	)
	target_link_libraries(${MODULE} PRIVATE
		"inspircd"
	)
	set_target_properties(${MODULE} PROPERTIES
		APPLE_RESOLVE_SYMBOLS_DYNAMICALLY ON
		FOLDER "Modules"
		PREFIX ""
	)
	list(LENGTH MODULE_SOURCE MODULE_SOURCE_COUNT)
	if(MODULE_SOURCE_COUNT LESS_EQUAL 1)
		set_target_properties(${MODULE} PROPERTIES
			UNITY_BUILD OFF
		)
	endif()
	inline_cmake(${MODULE} ${MODULE_SOURCE})
	install_owned(
		TARGETS ${MODULE}
		DESTINATION "${MODULE_DIR}"
	)
endfunction()

################################################################################
# build_modules: Builds all modules in a directory and any subdirectories.
#
# If a subdirectory contains CMakeLists.txt, add_subdirectory will be called
# on it. If it contains a file called main.cpp or a .cpp file with the same
# name as its directory, build_module will be called on it. Otherwise,
# build_modules (i.e. this function) will be called on it.
#
# MODULE_SOURCE_DIR (STRING): The directory to build modules in.
function(build_modules MODULE_SOURCE_DIR)
	file(GLOB MODULES CONFIGURE_DEPENDS "${MODULE_SOURCE_DIR}/*")
	foreach(MODULE IN LISTS MODULES)
		cmake_path(GET MODULE STEM MODULE_NAME)
		if(IS_DIRECTORY ${MODULE} AND NOT MODULE_NAME IN_LIST ARGN)
			if(EXISTS "${MODULE}/CMakeLists.txt")
				add_subdirectory(${MODULE})
			elseif(EXISTS "${MODULE}/${MODULE_NAME}.cpp" OR EXISTS "${MODULE}/main.cpp")
				file(GLOB_RECURSE MODULE_SOURCE CONFIGURE_DEPENDS
					"${MODULE}/*.cpp"
					"${MODULE}/*.h"
				)
				build_module(${MODULE_NAME} "${MODULE_SOURCE}")
			else()
				build_modules(${MODULE})
			endif()
		elseif(MODULE MATCHES "\\.cpp$")
			build_module(${MODULE_NAME} ${MODULE})
		endif()
	endforeach()
endfunction()

################################################################################
# configure_path: Configures a path based on the path layout options passed to
# CMake.
#
# NAME (STRING): The name of the variable to store the path in. An absolute
#                version of the path is also stored in ABSOLUTE_${NAME}.
#
# SOURCE_DEF (PATH): The default path used on UNIX when neither PORTABLE nor
#                    SYSTEM are set.
#
# PORTABLE_DEF (PATH): The default path used when the PORTABLE flag is set or
#                      when building on Windows.
#
# SYSTEM_DEF (PATH): The default path used when the SYSTEM flag is set.
#
# DESCRIPTION (STRING): A description of the path for the cache entry.
function(configure_path)
	cmake_parse_arguments(PARSE_ARGV 0 "OPT" "" "NAME;SOURCE_DEF;PORTABLE_DEF;SYSTEM_DEF;DESCRIPTION" "")
	if(NOT DEFINED ${OPT_NAME})
		if(PORTABLE OR WIN32)
			set(${OPT_NAME} ${OPT_PORTABLE_DEF} CACHE PATH ${OPT_DESCRIPTION})
		elseif(SYSTEM)
			set(${OPT_NAME} ${OPT_SYSTEM_DEF} CACHE PATH ${OPT_DESCRIPTION})
		else()
			set(${OPT_NAME} ${OPT_SOURCE_DEF} CACHE PATH ${OPT_DESCRIPTION})
		endif()

		if(PORTABLE OR WIN32)
			set(ABSOLUTE_${OPT_NAME} ${${OPT_NAME}})
		else()
			cmake_path(
				APPEND CMAKE_INSTALL_PREFIX "${${OPT_NAME}}"
				OUTPUT_VARIABLE ABSOLUTE_${OPT_NAME}
			)
			cmake_path(NORMAL_PATH ABSOLUTE_${OPT_NAME})
		endif()
	endif()
	install_owned(
		DIRECTORY
		DESTINATION ${${OPT_NAME}}
	)
	return(PROPAGATE ABSOLUTE_${OPT_NAME} ${OPT_NAME})
endfunction()

################################################################################
# find_id: Retrieves the id and name for a system group or user.
#
# ID (STRING): The name or numeric id to look up. This MUST already exist on the
#              system.
#
# FLAG (STRING): The type of entry to look up; either "g" for a group or "u" for
#                a user.
#
# ID_VAR (STRING): The variable to store the retrieved id in.
#
# NAME_VAR (STRING): The variable to store the retrieved name in.
function(find_id ID FLAG ID_VAR NAME_VAR)
	# `id` has existed since System V so this should always be available.
	find_program(ID_BINARY "id" REQUIRED)
	if(SYSTEM AND NOT ID)
		set(ID 0) # On system-wide installs we default to root ownership.
	endif()
	execute_process(
		COMMAND ${ID_BINARY} "-${FLAG}" ${ID}
		COMMAND_ERROR_IS_FATAL ANY
		OUTPUT_STRIP_TRAILING_WHITESPACE
		OUTPUT_VARIABLE ${ID_VAR}
	)
	execute_process(
		COMMAND ${ID_BINARY} "-${FLAG}n" ${ID}
		COMMAND_ERROR_IS_FATAL ANY
		OUTPUT_STRIP_TRAILING_WHITESPACE
		OUTPUT_VARIABLE ${NAME_VAR}
	)
	return(PROPAGATE ${ID_VAR} ${NAME_VAR})
endfunction()

################################################################################
# inline_cmake: Runs inline CMake code from within a source file.
#
# The inline CMake code is extracted from a triple-commented block beginning
# with "/// BEGIN CMAKE" and ending with "/// END CMAKE". This allows modules
# to have custom build configuration without needing a separate CMakeLists.txt.
#
# TARGET (STRING): The name of the CMake target. This is available to the
#                  target's CMake code, allowing it to call functions like
#                  target_link_libraries on itself.
#
# FILE (STRING): The path to the source file from which to parse inline CMake
#                code.
function(inline_cmake TARGET FILE)
	file(STRINGS ${FILE} SRC)
	set(CODE "")
	set(IN_CODE OFF)
	foreach(LINE IN LISTS SRC)
		if(IN_CODE)
			string(REGEX REPLACE "^/// " "" CLEAN_LINE ${LINE})
			if(CLEAN_LINE MATCHES "^END CMAKE$")
				cmake_language(EVAL CODE "${CODE}")
				set(CODE "")
				set(IN_CODE OFF)
			else()
				set(CODE "${CODE}\n${CLEAN_LINE}")
			endif()
		elseif(LINE MATCHES "^/// BEGIN CMAKE$")
			set(IN_CODE ON)
		endif()
	endforeach()
endfunction()

################################################################################
# install_owned: Installs a target with the destination owned by the group and
# user specified in GID and UID.
#
# DESTINATION (STRING): The location that the targt will be installed to.
function(install_owned)
	install(${ARGN})

	if(DEFINED GROUP_ID OR DEFINED USER_ID)
		cmake_parse_arguments(PARSE_ARGV 0 "OPT" "" "DESTINATION" "")

		# `chown` has existed since Unix V1 so this should always be available.
		find_program(CHOWN_BINARY "chown" REQUIRED)

		# Build the owner for the installed file.
		set(OWNER "${USER_NAME}")
		if(GROUP_NAME)
			string(APPEND OWNER ":${GROUP_NAME}")
		endif()

		# Build the destination of the installed file. We need to make sure to
		# respect DESTDIR so changing the owner still works when packaging.
		cmake_path(
			ABSOLUTE_PATH OPT_DESTINATION
			BASE_DIRECTORY "${CMAKE_INSTALL_PREFIX}"
			OUTPUT_VARIABLE OPT_DESTINATION
		)
		string(PREPEND OPT_DESTINATION "\${DESTDIR}")

		install(CODE "
			execute_process(
				COMMAND ${CHOWN_BINARY} -R \"${OWNER}\" \"${OPT_DESTINATION}\"
				ERROR_STRIP_TRAILING_WHITESPACE
				ERROR_VARIABLE CHOWN_ERROR
				RESULT_VARIABLE CHOWN_RESULT
			)
			if(CHOWN_RESULT)
				message(FATAL_ERROR \"Unable to set owner of ${OPT_DESTINATION} to ${OWNER} - \${CHOWN_ERROR}\")
			endif()
		")
	endif()
endfunction()

################################################################################
# update_cache: Updates the value of a cache variable whilst keeping the same
# type and description.
#
# VAR_NAME (STRING): The the cache variable to update.
#
# VAR_VALUE (STRING): The new value for the cache variable.
function(update_cache VAR_NAME VAR_VALUE)
	get_property(OLD_DESC CACHE ${VAR_NAME} PROPERTY HELPSTRING)
	get_property(OLD_TYPE CACHE ${VAR_NAME} PROPERTY TYPE)
	if(NOT OLD_TYPE)
		set(OLD_TYPE "STRING") # Fall back to string
	endif()
	set(${VAR_NAME} "${VAR_VALUE}" CACHE ${OLD_TYPE} "${OLD_DESC}" FORCE)
endfunction()

################################################################################
# target_report_error: Enables a CMake target to report an error that is treated
# as non-fatal during a dry-run, but becomes fatal whilst trying to build the
# target.
#
# TARGET (STRING): The name of the CMake target to which the error refers. This
#                  is used for diagnostic reporting and does not need to exist
#                  yet.
#
# MESSAGE (STRING): The specific error message to report.
function(target_report_error TARGET MESSAGE)
	if(FIND_DEPENDENCY_DRY_RUN)
		update_cache(FIND_DEPENDENCY_FOUND NO)
		set(MESSAGE_TYPE "STATUS")
	else()
		set(MESSAGE_TYPE "FATAL_ERROR")
	endif()
	message(${MESSAGE_TYPE} "${TARGET}: ${MESSAGE}")
endfunction()

################################################################################
# target_require_package: Finds a package that a target depends on and links
# to it.
#
# On Windows, this uses find_package to try and find the package in the Conan
# cache. On other platforms, it uses pkg-config to try and find the package
# from the system package manager.
#
# PKGCONF_NAMES (STRING): A space-delimited list of pkg-config package names for
# use on non-Windows systems. The first available one is used.
#
# CMAKE_NAME (STRING): The CMake package name for use on Windows systems.
#
# LINK_TARGET (STRING): The name of the CMake import target to link against the
# module if the package is available. For pkg-config, this is an alias target to
# the pkg-config target; on Windows, this is the target provided by Conan.
function(target_require_package TARGET PKGCONF_NAMES CMAKE_NAME LINK_TARGET)
	if(NOT TARGET ${LINK_TARGET})
		if(WIN32)
			find_package(${CMAKE_NAME})
		elseif(PkgConfig_FOUND)
			string(REPLACE " " ";" PKGCONF_NAMES "${PKGCONF_NAMES}")
			foreach(PKGCONF_NAME IN LISTS PKGCONF_NAMES)
				pkg_check_modules(${PKGCONF_NAME} IMPORTED_TARGET ${PKGCONF_NAME})
				if(${PKGCONF_NAME}_FOUND)
					add_library(${LINK_TARGET} ALIAS PkgConfig::${PKGCONF_NAME})
					break()
				endif()
			endforeach()
		endif()
	endif()

	if(TARGET ${LINK_TARGET})
		update_cache(FIND_DEPENDENCY_FOUND YES)
		if(NOT FIND_DEPENDENCY_DRY_RUN)
			target_link_libraries(${TARGET} PRIVATE ${LINK_TARGET})
		endif()
	else()
		update_cache(FIND_DEPENDENCY_FOUND NO)
		if(WIN32)
			target_report_error(${TARGET} "Unable to find ${CMAKE_NAME} with find_package!")
		else()
			target_report_error(${TARGET} "Unable to find ${PKGCONF_NAMES} with pkg-config!")
		endif()
	endif()
endfunction()