Files
cmake_common/mo2_utils.cmake
2025-05-29 10:54:03 +02:00

437 lines
14 KiB
CMake

# this file contains utility code that is not directly related to MO2
cmake_minimum_required(VERSION 3.16)
include(CMakeParseArguments)
if (DEFINED MO2_UTILS_DEFINED)
return()
endif()
#! mo2_set_if_not_defined : set a variable to the given value if not defined
#
# \param:NAME name of the variable
# \param:VALUE value of the variable to set (if not defined)
#
function (mo2_set_if_not_defined NAME VALUE)
if (NOT DEFINED ${NAME})
set(${NAME} ${VALUE} PARENT_SCOPE)
endif()
endfunction()
#! mo2_add_subdirectories : add all repositories matching the given list of patterns
#
# \param:FOLDER Folder (layout) to add the subdirectories to
# \param:GLOB List of glob patterns to find repositories
#
function (mo2_add_subdirectories)
cmake_parse_arguments(MO2 "" "FOLDER" "GLOB" ${ARGN})
if (NOT DEFINED MO2_FOLDER)
message(FATAL_ERROR "missing FOLDER in add_subdirectories")
endif()
if (NOT DEFINED MO2_GLOB)
message(FATAL_ERROR "missing GLOB in add_subdirectories")
endif()
file(GLOB directories RELATIVE ${CMAKE_CURRENT_LIST_DIR} LIST_DIRECTORIES TRUE ${MO2_GLOB})
set(CMAKE_FOLDER ${MO2_FOLDER})
foreach(directory ${directories})
add_subdirectory(${directory})
endforeach()
unset(CMAKE_FOLDER)
endfunction()
#! mo2_find_python_executable : find the full path to the Python executable
#
# \param:VARNAME name of the variable that will contain the path to Python
function(mo2_find_python_executable VARNAME)
if (NOT DEFINED Python_EXECUTABLE)
find_package(Python ${MO2_PYTHON_VERSION} COMPONENTS Interpreter REQUIRED)
endif()
set(${VARNAME} ${Python_EXECUTABLE} PARENT_SCOPE)
endfunction()
#! mo2_find_git_hash : find the git hash of HEAD on the current source project
#
# \param:VARNAME variable to store the git hash
function(mo2_find_git_hash VARNAME)
execute_process(
COMMAND git log -1 --format=%h
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(${VARNAME} ${GIT_COMMIT_HASH} PARENT_SCOPE)
endfunction()
#! mo2_find_qt_executable : find the path to the executable from Qt
#
function(mo2_find_qt_executable VARNAME EXECUTABLE)
# retrieve the absolute path to qmake and then use that path to find
# the windeployqt and macdeployqt binaries
get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION)
get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY)
# need to use a custom varname per executable to use the cache
find_program(QT_${EXECUTABLE} ${EXECUTABLE} HINTS "${_qt_bin_dir}")
if(WIN32 AND NOT QT_${EXECUTABLE})
message(FATAL_ERROR "${EXECUTABLE} not found")
endif()
set(${VARNAME} ${QT_${EXECUTABLE}} PARENT_SCOPE)
endfunction()
#! mo2_set_project_to_run_from_install : set a target to run from a given executable
#
# this function is only meaningful for VS generator
#
# \param:TARGET name of the target
# \param:EXECUTABLE full path to the executable
# \param:WORKDIR working directory (optional, default is the directory of the executable)
#
function(mo2_set_project_to_run_from_install TARGET)
cmake_parse_arguments(MO2 "" "EXECUTABLE;WORKDIR" "" ${ARGN})
# extract directory
if (NOT DEFINED MO2_WORKDIR)
get_filename_component(MO2_WORKDIR ${MO2_EXECUTABLE} DIRECTORY)
endif()
set_target_properties(${TARGET} PROPERTIES
VS_DEBUGGER_WORKING_DIRECTORY "${MO2_WORKDIR}"
VS_DEBUGGER_COMMAND "${MO2_EXECUTABLE}")
endfunction()
#! mo2_required_variable : check that a variable is defined, fails otherwise
#
# this function checks that a variable with the given NAME is defined, if it's not
# it fails with a FATAL_ERROR, otherwise it caches the variable
#
# \param:NAME name of the variable
# \param:TYPE type of the variable (optional)
# \param:DESC description of the variable (optional)
#
macro(mo2_required_variable)
cmake_parse_arguments(REQ_VAR "" "NAME;TYPE;DESC" "" ${ARGN})
if(NOT DEFINED REQ_VAR_DESC)
set(REQ_VAR_DESC "${name}")
endif()
if(NOT DEFINED ${REQ_VAR_NAME})
message(FATAL_ERROR "${REQ_VAR_NAME} is not defined")
endif()
set(${REQ_VAR_NAME} ${${REQ_VAR_NAME}} CACHE ${REQ_VAR_TYPE} "${REQ_VAR_DESC}")
endmacro()
#! mo2_add_filter : add source_group based on the given names
#
# \param:NAME name of the group
# \param:FILES files to add
# \param:GROUPS basename to add, e.g., "foo" will add "foo.cpp", "foo.h" and "foo.inc"
#
function(mo2_add_filter)
cmake_parse_arguments(PARSE_ARGV 0 add_filter "" "NAME" "FILES;GROUPS")
set(files ${add_filter_FILES})
foreach(f ${add_filter_GROUPS})
set(files ${files} ${f}.cpp ${f}.h ${f}.inc)
endforeach()
string(REPLACE "/" "\\" filter_name ${add_filter_NAME})
source_group(${filter_name} FILES ${files})
endfunction()
#! mo2_deploy_qt_for_tests : add comments to deploy Qt for tests
#
# unlike mo2_deploy_qt(), this function does not perform any cleaning
#
# \param:TARGET name of the target to deploy for
# \param:BINARIES names of the binaries to deploy from
#
function(mo2_deploy_qt_for_tests)
cmake_parse_arguments(DEPLOY "" "TARGET" "BINARIES" ${ARGN})
mo2_find_qt_executable(windeployqt windeployqt)
add_custom_command(TARGET "${DEPLOY_TARGET}"
POST_BUILD
COMMAND ${windeployqt}
ARGS
--dir $<TARGET_FILE_DIR:${DEPLOY_TARGET}>
--no-translations
--verbose 0
--no-compiler-runtime
"$<TARGET_FILE:${DEPLOY_TARGET}>"
"${DEPLOY_BINARIES}"
VERBATIM
COMMAND_EXPAND_LISTS
WORKING_DIRECTORY $<TARGET_FILE_DIR:${DEPLOY_TARGET}>
)
endfunction()
#! mo2_deploy_qt : add commands to deploy Qt from the given binaries
#
# this function attach install() entries that deploy Qt for the given binaries
#
# \param:NOPLUGINS do not deploy Qt plugins
# \param:DIRECTORY directory, relative to CMAKE_INSTALL_PREFIX, to deploy to, default
# to ${MO2_INSTALL_BIN}
# \param:BINARIES names of the binaries (in the install path) to deploy from
#
function(mo2_deploy_qt)
cmake_parse_arguments(DEPLOY "NOPLUGINS" "DIRECTORY" "BINARIES" ${ARGN})
mo2_set_if_not_defined(DEPLOY_DIRECTORY "${MO2_INSTALL_BIN}")
mo2_find_qt_executable(windeployqt windeployqt)
set(args
"--no-translations \
--verbose 0 \
--webenginewidgets \
--websockets \
--openglwidgets \
--libdir dlls \
--no-compiler-runtime")
if(${DEPLOY_NOPLUGINS})
set(args "${args} --no-plugins")
else()
set(args "${args} --plugindir qtplugins")
endif()
set(bin "${CMAKE_INSTALL_PREFIX}/${DEPLOY_DIRECTORY}")
set(deploys "")
foreach(binary ${DEPLOY_BINARIES})
set(deploys "${deploys}
EXECUTE_PROCESS(
COMMAND ${windeployqt} ${args} ${binary}
WORKING_DIRECTORY \"${bin}\")")
endforeach()
install(CODE "
${deploys}
file(REMOVE_RECURSE \"${bin}/platforms\")
file(REMOVE_RECURSE \"${bin}/styles\")
file(REMOVE_RECURSE \"${bin}/dlls/imageformats\")
file(REMOVE_RECURSE \"${bin}/dlls/tls\")
")
# === Begin Qt6 Fix ===
# until there is a cleaner way to do this with windqtdeploy?
# subfolder of QtQuick
set(qt6_qtquick_to_remove "")
list(APPEND qt6_qtquick_to_remove
Controls Dialogs Layouts LocalStorage NativeStyle Particles Pdf Scene2D
Scene3D Shapes Templates Timeline tooling VirtualKeyboard Window)
set(qt6_qtdlls_to_remove "")
list(APPEND qt6_qtdlls_to_remove
3DAnimation 3DCore 3DExtras 3DInput 3DLogic 3DQuickScene2D 3DRender
Pdf PdfQuick QmlLocalStorage QmlXmlListModel QuickControls2 QuickControls2Impl
QuickDialogs2 QuickDialogs2QuickImpl QuickDialogs2Utils QuickLayouts QuickParticles
QuickShapes QuickTemplates2 QuickTimeline Sql StateMachine StateMachineQml VirtualKeyboard)
set(removals "")
foreach (qt6_qtquick_removal ${qt6_qtquick_to_remove})
set(removals "${removals}
file(REMOVE_RECURSE \"${bin}/QtQuick/${qt6_qtquick_removal}\")")
endforeach()
foreach (qt6_dll_removal ${qt6_qtdlls_to_remove})
set(removals "${removals}
file(REMOVE \"${bin}/dlls/Qt6${qt6_dll_removal}.dll\")")
endforeach()
install(CODE "${removals}")
# === End Qt6 Fix ===
if(NOT ${DEPLOY_NOPLUGINS})
set(qtwebengine_process_exe $<IF:$<CONFIG:Debug>,QtWebEngineProcessd.exe,QtWebEngineProcess.exe>)
install(CODE "
file(RENAME \"${bin}/dlls/${qtwebengine_process_exe}\" \"${bin}/${qtwebengine_process_exe}\")
file(RENAME \"${bin}/qtplugins/platforms\" \"${bin}/platforms\")
file(RENAME \"${bin}/qtplugins/styles\" \"${bin}/styles\")
file(RENAME \"${bin}/qtplugins/imageformats\" \"${bin}/dlls/imageformats\")
file(RENAME \"${bin}/qtplugins/tls\" \"${bin}/dlls/tls\")
file(REMOVE_RECURSE \"${bin}/qtplugins\")
")
endif()
endfunction()
#! mo2_add_lupdate : generate .ts files from the given sources
#
# this function adds a ${TARGET}_lupdate target
#
# \param:TARGET target to generate lupdate for
# \param:TS_FILE .ts file to generate
# \param:SOURCES source folders to generate .ts file from
#
function(mo2_add_lupdate TARGET)
cmake_parse_arguments(MO2 "" "TS_FILE" "SOURCES" ${ARGN})
set(translation_files "")
set(ui_files "")
get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)
if ("CXX" IN_LIST languages)
set(is_cpp True)
else()
set(is_cpp False)
endif()
# we glob the source files for Python plugins to avoid duplicate string in .ui and
# .py, for C++, we will directly use MO2_SOURCES (using translation_files broke
# the modorganizer build, probably because there are too many sources?)
foreach (SOURCE ${MO2_SOURCES})
if (${is_cpp})
file(GLOB_RECURSE source_sources CONFIGURE_DEPENDS
${SOURCE}/*.cpp
${SOURCE}/*.h)
else()
file(GLOB_RECURSE source_sources CONFIGURE_DEPENDS
${SOURCE}/*.py)
endif()
# ui files
file(GLOB_RECURSE source_ui_files CONFIGURE_DEPENDS ${SOURCE}/*.ui)
list(APPEND ui_files ${source_ui_files})
list(APPEND translation_files ${source_sources} ${source_ui_files})
endforeach()
# for Python, we need to remove the .py generated because these can be generated
# in the source folder
list(TRANSFORM ui_files REPLACE "[.]ui$" ".py")
list(REMOVE_ITEM translation_files ${ui_files})
message(TRACE "TS_FILE: ${MO2_TS_FILE}, SOURCES: ${MO2_SOURCES}, FILES: ${translation_files}")
add_custom_target("${TARGET}_lupdate" DEPENDS ${MO2_TS_FILE})
if (${is_cpp})
mo2_find_qt_executable(lupdate lupdate)
set(lupdate_command ${lupdate} ${MO2_SOURCES} -ts ${MO2_TS_FILE})
else()
mo2_python_install_pyqt()
set(lupdate_command
${CMAKE_COMMAND}
-E env PYTHONPATH=${MO2_PYLIBS_DIR}
${MO2_PYLIBS_DIR}/bin/pylupdate${MO2_QT_VERSION_MAJOR}.exe
--ts "${MO2_TS_FILE}" ${translation_files})
add_dependencies("${TARGET}_lupdate" PyQt6)
endif()
add_custom_command(OUTPUT ${MO2_TS_FILE}
COMMAND ${lupdate_command}
DEPENDS ${translation_files}
VERBATIM)
# we need to set this property otherwise there is an issue with C# projects
# requiring nuget packages (e.g., installer_omod) that tries to resolve Nuget
# packages on these target but fails because there are obviously none
#
# we also "hide" the target by moving them to autogen
set_target_properties(${TARGET}_lupdate PROPERTIES
VS_GLOBAL_ResolveNugetPackages False
FOLDER autogen)
endfunction()
#! mo2_add_lrelease : generate .ts files from the given sources
#
# this function adds a ${TARGET}_lrelease target
#
# \param:TARGET target to generate releases for
# \param:INSTALL if set, QM files will be installed
# \param:DIRECTORY if INSTALL is set, path where translations should be installed,
# default to ${MO2_INSTALL_BIN}/translations
# \param:QM_FILE .qm file to generate
# \param:TS_FILES source ts
#
function(mo2_add_lrelease TARGET)
cmake_parse_arguments(MO2 "INSTALL" "DIRECTORY;QM_FILE" "TS_FILES" ${ARGN})
mo2_set_if_not_defined(MO2_DIRECTORY "${MO2_INSTALL_BIN}/translations")
mo2_find_qt_executable(lrelease_command lrelease)
add_custom_command(OUTPUT ${MO2_QM_FILE}
COMMAND ${lrelease_command}
ARGS ${MO2_TS_FILES} -qm ${MO2_QM_FILE}
DEPENDS "${MO2_TS_FILES}"
VERBATIM)
add_custom_target("${TARGET}_lrelease" DEPENDS ${MO2_QM_FILE})
# we need to set this property otherwise there is an issue with C# projects
# requiring nuget packages (e.g., installer_omod) that tries to resolve Nuget
# packages on these target but fails because there are obviously none
#
# we also "hide" the target by moving them to autogen
set_target_properties(${TARGET}_lrelease PROPERTIES
VS_GLOBAL_ResolveNugetPackages False
FOLDER autogen)
if (${MO2_INSTALL})
install(FILES ${MO2_QM_FILE} DESTINATION ${MO2_DIRECTORY})
endif()
endfunction()
#! mo2_add_translations : generate translation files
#
# this function is a wrapper around mo2_add_lupdate and mo2_add_lrelease
#
# \param:TARGET target to generate translations for
# \param:RELEASE if set, will use mo2_add_lrelease to generate .qm files
# \param:INSTALL_RELEASE if true, will install generated .qm files (force RELEASE)
# \param:INSTALL_DIRECTORY installation directory for .qm files, default to ${MO2_INSTALL_BIN}/translations
# \param:TS_FILE intermediate .ts file to generate
# \param:QM_FILE file .qm file to generate if RELEASE or INSTALL_RELEASE is set
# \param:SOURCES source directories to look for translations, send to mo2_add_lupdate
#
function(mo2_add_translations TARGET)
cmake_parse_arguments(MO2 "RELEASE;INSTALL_RELEASE" "TS_FILE;QM_FILE;INSTALL_DIRECTORY" "SOURCES" ${ARGN})
mo2_set_if_not_defined(MO2_TS_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}_en.ts)
mo2_set_if_not_defined(MO2_QM_FILE ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_en.qm)
mo2_set_if_not_defined(MO2_INSTALL_DIRECTORY "${MO2_INSTALL_BIN}/translations")
# force release with install
if (${MO2_INSTALL_RELEASE})
set(MO2_RELEASE True)
endif()
mo2_add_lupdate(${TARGET} TS_FILE ${MO2_TS_FILE} SOURCES ${MO2_SOURCES})
if (${MO2_RELEASE})
mo2_add_lrelease(${TARGET}
INSTALL ${MO2_INSTALL_RELEASE}
DIRECTORY ${MO2_INSTALL_DIRECTORY}
TS_FILES ${MO2_TS_FILE}
QM_FILE ${MO2_QM_FILE})
add_dependencies(${TARGET}_lrelease ${TARGET}_lupdate)
add_dependencies(${TARGET} ${TARGET}_lrelease)
else()
add_dependencies(${TARGET} ${TARGET}_lupdate)
endif()
endfunction()
set(MO2_UTILS_DEFINED TRUE)