Fix typos
[quassel.git] / cmake / QuasselMacros.cmake
1 # This file contains various functions and macros useful for building Quassel.
2 #
3 # (C) 2014-2022 by the Quassel Project <devel@quassel-irc.org>
4 #
5 # Redistribution and use is allowed according to the terms of the BSD license.
6 # For details see the accompanying COPYING-CMAKE-SCRIPTS file.
7 ###################################################################################################
8
9 include(CMakeParseArguments)
10 include(GenerateExportHeader)
11 include(QuasselCompileFeatures)
12
13 ###################################################################################################
14 # Adds a library target for a Quassel module.
15 #
16 # quassel_add_module(Module [STATIC] [EXPORT])
17 #
18 # The function expects the (CamelCased) module name as a parameter, and derives various
19 # strings from it. For example, quassel_add_module(Client) produces
20 #  - a library target named quassel_client with output name (lib)quassel-client(.so)
21 #  - an alias target named Quassel::Client in global scope
22 #
23 # If the optional argument STATIC is given, or the ENABLE_SHARED option is OFF,
24 # a static library is built; otherwise a shared library is created. For shared
25 # libraries, an install rule is also added.
26 #
27 # To generate an export header for the library, specify EXPORT. The header will be named
28 # ${module}-export.h (where ${module} is the lower-case name of the module).
29 #
30 # The function exports the TARGET variable which can be used in the current scope
31 # for setting source files, properties, link dependencies and so on.
32 # To refer to the target outside of the current scope, e.g. for linking, use
33 # the alias name.
34 #
35 function(quassel_add_module _module)
36     set(options EXPORT STATIC NOINSTALL)
37     set(oneValueArgs )
38     set(multiValueArgs )
39     cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
40
41     # Derive target, alias target, output name from the given module name
42     set(alias "Quassel::${_module}")
43     set(target ${alias})
44     string(TOLOWER ${target} target)
45     string(REPLACE "::" "_" target ${target})
46     string(REPLACE "_" "-" output_name ${target})
47
48     if (ARG_STATIC OR NOT ENABLE_SHARED)
49         set(buildmode STATIC)
50     else()
51         set(buildmode SHARED)
52     endif()
53
54     add_library(${target} ${buildmode} "")
55     add_library(${alias} ALIAS ${target})
56
57     target_link_libraries(${target} PRIVATE Qt5::Core)
58     target_include_directories(${target}
59         PUBLIC  ${CMAKE_CURRENT_SOURCE_DIR}
60         PRIVATE ${CMAKE_CURRENT_BINARY_DIR} # for generated files
61     )
62     target_compile_features(${target} PUBLIC ${QUASSEL_COMPILE_FEATURES})
63
64     set_target_properties(${target} PROPERTIES
65         OUTPUT_NAME ${output_name}
66         VERSION ${QUASSEL_MAJOR}.${QUASSEL_MINOR}.${QUASSEL_PATCH}
67     )
68
69     if (buildmode STREQUAL "SHARED" AND NOT ${ARG_NOINSTALL})
70         install(TARGETS ${target}
71             RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
72             LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
73             ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
74         )
75     endif()
76
77     if (ARG_EXPORT)
78         string(TOLOWER ${_module} lower_module)
79         string(TOUPPER ${_module} upper_module)
80         string(REPLACE "::" "-" header_base ${lower_module})
81         string(REPLACE "::" "_" macro_base ${upper_module})
82         generate_export_header(${target}
83             BASE_NAME ${macro_base}
84             EXPORT_FILE_NAME ${CMAKE_BINARY_DIR}/export/${header_base}-export.h
85         )
86         target_include_directories(${target} PUBLIC ${CMAKE_BINARY_DIR}/export)
87     endif()
88
89     # Export the target name for further use
90     set(TARGET ${target} PARENT_SCOPE)
91 endfunction()
92
93 ###################################################################################################
94 # Adds an executable target for Quassel.
95 #
96 # quassel_add_executable(<target> COMPONENT <Core|Client|Mono> [SOURCES src1 src2...] [LIBRARIES lib1 lib2...])
97 #
98 # This function supports the creation of either of the three hardcoded executable targets: Core, Client, and Mono.
99 # Given sources and libraries are added to the target.
100 #
101 # On macOS, the creation of bundles and corresponding DMG files is supported and can be enabled by setting the
102 # BUNDLE option to ON.
103 #
104 function(quassel_add_executable _target)
105     set(options)
106     set(oneValueArgs COMPONENT)
107     set(multiValueArgs SOURCES LIBRARIES)
108     cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
109
110     # Set up some hard-coded data based on the component to be built
111     if(ARG_COMPONENT STREQUAL "Core")
112         set(DEFINE BUILD_CORE)
113         set(WIN32 FALSE)
114         set(BUNDLE_NAME "Quassel Core")
115     elseif(ARG_COMPONENT STREQUAL "Client")
116         set(DEFINE BUILD_QTUI)
117         set(WIN32 TRUE)
118         set(BUNDLE_NAME "Quassel Client")
119     elseif(ARG_COMPONENT STREQUAL "Mono")
120         set(DEFINE BUILD_MONO)
121         set(WIN32 TRUE)
122         set(BUNDLE_NAME "Quassel")
123     else()
124         message(FATAL_ERROR "quassel_executable requires a COMPONENT argument with one of the values 'Core', 'Client' or 'Mono'")
125     endif()
126
127     add_executable(${_target} ${ARG_SOURCES})
128     set_property(TARGET ${_target} APPEND PROPERTY COMPILE_DEFINITIONS ${DEFINE})
129     set_target_properties(${_target} PROPERTIES
130         RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
131         WIN32_EXECUTABLE ${WIN32}  # Ignored on non-Windows platforms
132     )
133     target_link_libraries(${_target} PUBLIC ${ARG_LIBRARIES})  # Link publicly, so plugin detection for bundles work
134
135     # Prepare bundle creation on macOS
136     if(APPLE AND BUNDLE)
137         set(BUNDLE_PATH "${CMAKE_INSTALL_PREFIX}/${BUNDLE_NAME}.app")
138         set(DMG_PATH "${CMAKE_INSTALL_PREFIX}/Quassel${ARG_COMPONENT}_MacOSX-x86_64_${QUASSEL_VERSION_STRING}.dmg")
139
140         # Generate an appropriate Info.plist
141         set(BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info_${ARG_COMPONENT}.plist")
142         configure_file(${CMAKE_SOURCE_DIR}/cmake/MacOSXBundleInfo.plist.in ${BUNDLE_INFO_PLIST} @ONLY)
143
144         # Set some bundle-specific properties
145         set_target_properties(${_target} PROPERTIES
146             MACOSX_BUNDLE TRUE
147             MACOSX_BUNDLE_INFO_PLIST "${BUNDLE_INFO_PLIST}"
148             OUTPUT_NAME "${BUNDLE_NAME}"
149         )
150     endif()
151
152     # Install main target; this will also create an initial bundle skeleton if appropriate
153     install(TARGETS ${_target}
154         RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
155         BUNDLE DESTINATION ${CMAKE_INSTALL_PREFIX}  # Ignored when not creating a bundle
156         COMPONENT ${ARG_COMPONENT}
157     )
158
159     # Once the bundle skeleton has been created and the main executable installed, finalize bundle creation and build DMGs
160     if(APPLE AND BUNDLE)
161         # We cannot rely on Qt's macdeployqt for deploying plugins, because it will unconditionally install a bunch of unneeded ones,
162         # dragging in unwanted dependencies.
163         # Instead, transitively determine all Qt modules that the Quassel executable links against, and deploy only the plugins belonging
164         # to those modules.
165         # macdeployqt will take care of fixing up dependencies afterwards.
166         function(find_transitive_link_deps target var)
167             if(TARGET ${target})
168                 get_target_property(libs ${target} LINK_LIBRARIES)
169                 if(libs)
170                     foreach(lib IN LISTS libs)
171                         if(NOT lib IN_LIST ${var})
172                             list(APPEND ${var} ${lib})
173                             find_transitive_link_deps(${lib} ${var})
174                         endif()
175                     endforeach()
176                 endif()
177                 set(${var} ${${var}} PARENT_SCOPE)
178             endif()
179         endfunction()
180
181         find_transitive_link_deps(${_target} link_deps)
182         # TODO CMake 3.6: use list(FILTER...)
183         foreach(dep IN LISTS link_deps)
184             if(${dep} MATCHES "^Qt5::.*")
185                 list(APPEND qt_deps ${dep})
186             endif()
187         endforeach()
188
189         foreach(module IN LISTS qt_deps)
190             string(REPLACE "::" "" module ${module})
191             foreach(plugin ${${module}_PLUGINS})
192                 install(
193                     FILES $<TARGET_PROPERTY:${plugin},LOCATION>
194                     DESTINATION ${BUNDLE_PATH}/Contents/PlugIns/$<TARGET_PROPERTY:${plugin},QT_PLUGIN_TYPE>
195                     COMPONENT ${ARG_COMPONENT}
196                 )
197             endforeach()
198         endforeach()
199
200         # Generate iconset and deploy it as well as a qt.conf enabling plugins
201         add_dependencies(${_target} MacOsIcons)
202         install(
203             FILES ${CMAKE_SOURCE_DIR}/data/qt.conf ${CMAKE_BINARY_DIR}/pics/quassel.icns
204             DESTINATION ${BUNDLE_PATH}/Contents/Resources
205             COMPONENT ${ARG_COMPONENT}
206         )
207
208         # Determine the location of macdeployqt. Not available directly via CMake, so look for it in qmake's bindir...
209         get_target_property(QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION)
210         get_filename_component(qt_bin_dir ${QMAKE_EXECUTABLE} DIRECTORY)
211         find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS ${qt_bin_dir} REQUIRED)
212
213         # Generate and invoke post-install script, finalizing the bundle and creating a DMG image
214         #set(BUNDLE_PATH $ENV{DESTDIR}/${BUNDLE_PATH})
215         configure_file(${CMAKE_SOURCE_DIR}/cmake/FinalizeBundle.cmake.in ${CMAKE_BINARY_DIR}/FinalizeBundle_${ARG_COMPONENT}.cmake @ONLY)
216         install(CODE "
217                 execute_process(
218                     COMMAND ${CMAKE_COMMAND} -P ${CMAKE_BINARY_DIR}/FinalizeBundle_${ARG_COMPONENT}.cmake
219                     RESULT_VARIABLE result
220                 )
221                 if(NOT result EQUAL 0)
222                     message(FATAL_ERROR \"Finalizing bundle failed.\")
223                 endif()
224             "
225             COMPONENT ${ARG_COMPONENT}
226         )
227     endif()
228 endfunction()
229
230 ###################################################################################################
231 # Provides a library that contains data files as a Qt resource (.qrc).
232 #
233 # quassel_add_resource(QrcName
234 #                      [BASEDIR basedir]
235 #                      [PREFIX prefix]
236 #                      PATTERNS pattern1 pattern2...
237 #                      [DEPENDS dep1 dep2...]
238 # )
239 #
240 # The first parameter is the CamelCased name of the resource; the library target will be called
241 # "Quassel::Resource::QrcName". The library provides a Qt resource named "qrcname" (lowercased QrcName)
242 # containing the files matching PATTERNS relative to BASEDIR (by default, the current source dir).
243 # The resource prefix can be set by giving the PREFIX argument.
244 # Additional target dependencies can be specified with DEPENDS.
245 #
246 function(quassel_add_resource _name)
247     set(options )
248     set(oneValueArgs BASEDIR PREFIX)
249     set(multiValueArgs DEPENDS PATTERNS)
250     cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
251
252     if (NOT ARG_BASEDIR)
253         set(ARG_BASEDIR ${CMAKE_CURRENT_SOURCE_DIR})
254     endif()
255     get_filename_component(basedir ${ARG_BASEDIR} REALPATH)
256
257     string(TOLOWER ${_name} lower_name)
258
259     set(qrc_target   quassel-qrc-${lower_name})
260     set(qrc_file     ${lower_name}.qrc)
261     set(qrc_src      qrc_${lower_name}.cpp)
262     set(qrc_filepath ${CMAKE_CURRENT_BINARY_DIR}/${qrc_file})
263     set(qrc_srcpath  ${CMAKE_CURRENT_BINARY_DIR}/${qrc_src})
264
265     # This target will always be built, so the qrc file will always be freshly generated.
266     # That way, changes to the glob result are always taken into account.
267     add_custom_target(${qrc_target} VERBATIM
268         COMMENT "Generating ${qrc_file}"
269         COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake/GenerateQrc.cmake "${qrc_filepath}" "${ARG_PREFIX}" "${ARG_PATTERNS}"
270         DEPENDS ${ARG_DEPENDS}
271         BYPRODUCTS ${qrc_filepath}
272         WORKING_DIRECTORY ${basedir}
273     )
274     set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES ${qrc_filepath})
275
276     # RCC sucks and expects the data files relative to the qrc file, with no way to configure it differently.
277     # Only when reading from stdin ("-") it takes the working directory as a base, so we have to use this if
278     # we want to use generated qrc files (which obviously cannot be placed in the source directory).
279     # Since neither autorcc nor qt5_add_resources() support this, we have to invoke rcc manually :(
280     #
281     # On Windows, input redirection apparently doesn't work, however piping does. Use this for all platforms for
282     # consistency, accommodating for the fact that the 'cat' equivalent on Windows is 'type'.
283     if (WIN32 AND NOT MSYS)
284         set(cat_cmd type)
285     else()
286         set(cat_cmd cat)
287     endif()
288     add_custom_command(VERBATIM
289         COMMENT "Generating ${qrc_src}"
290         COMMAND ${cat_cmd} "$<SHELL_PATH:${qrc_filepath}>"
291                 | "$<SHELL_PATH:$<TARGET_FILE:Qt5::rcc>>" --name "${lower_name}" --output "$<SHELL_PATH:${qrc_srcpath}>" -
292         DEPENDS ${qrc_target}
293         MAIN_DEPENDENCY ${qrc_filepath}
294         OUTPUT ${qrc_srcpath}
295         WORKING_DIRECTORY ${basedir}
296     )
297
298     # Generate library target that can be referenced elsewhere. Force static, because
299     # we can't easily export symbols from the generated sources.
300     quassel_add_module(Resource::${_name} STATIC)
301     target_sources(${TARGET} PRIVATE ${qrc_srcpath})
302     set_target_properties(${TARGET} PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
303
304     # Set variable for referencing the target from outside
305     set(RESOURCE_TARGET ${TARGET} PARENT_SCOPE)
306 endfunction()
307
308 ###################################################################################################
309 # Adds a unit test case
310 #
311 # quassel_add_test(TestName
312 #                  [LIBRARIES lib1 lib2...]
313 # )
314 #
315 # The test name is given in CamelCase as first and mandatory parameter. The corresponding source file
316 # is expected the lower-cased test name plus the .cpp extension.
317 # The test case is automatically linked against Qt5::Test, GMock, Quassel::Common and
318 # Quassel::Test::Main, which contains the main function. This main function also instantiates a
319 # QCoreApplication, so the event loop can be used in test cases.
320 #
321 # Additional libraries can be given using the LIBRARIES argument.
322 #
323 # Test cases should include testglobal.h, which transitively includes the GTest/GMock headers and
324 # exports the main function.
325 #
326 # The compiled test case binary is located in the unit/ directory in the build directory.
327 #
328 function(quassel_add_test _target)
329     set(options )
330     set(oneValueArgs )
331     set(multiValueArgs LIBRARIES)
332     cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
333
334     string(TOLOWER ${_target} lower_target)
335     set(srcfile ${lower_target}.cpp)
336
337     list(APPEND ARG_LIBRARIES
338         Qt5::Test
339         Quassel::Common
340         Quassel::Test::Global
341         Quassel::Test::Main
342     )
343
344     if (WIN32)
345         # On Windows, tests need to be built in the same directory that contains the libraries
346         set(output_dir "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
347     else()
348         # On other platforms, separate the test cases out
349         set(output_dir "${CMAKE_BINARY_DIR}/unit")
350     endif()
351
352     add_executable(${_target} ${srcfile})
353     set_target_properties(${_target} PROPERTIES
354         OUTPUT_NAME ${_target}
355         RUNTIME_OUTPUT_DIRECTORY "${output_dir}"
356     )
357     target_link_libraries(${_target} PUBLIC ${ARG_LIBRARIES})
358
359     add_test(
360         NAME ${_target}
361         COMMAND $<TARGET_FILE:${_target}>
362         WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
363     )
364 endfunction()
365
366 ###################################################################################################
367 # target_link_if_exists(Target
368 #                       [PUBLIC dep1 dep2...]
369 #                       [PRIVATE dep3 dep4...]
370 # )
371 #
372 # Convenience function to add dependencies to a target only if they exist. This is useful when
373 # handling targets that are conditionally created, e.g. resource libraries depending on -DEMBED_DATA.
374 #
375 # NOTE: In order to link a given target, it must already have been created, i.e its subdirectory
376 #       must already have been added. This is also true for globally visible ALIAS targets that
377 #       can otherwise be linked to regardless of creation order; "if (TARGET...)" does not
378 #       support handling this case correctly.
379 #
380 function(target_link_if_exists _target)
381     set(options )
382     set(oneValueArgs )
383     set(multiValueArgs PUBLIC PRIVATE)
384     cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
385
386     if (ARG_PUBLIC)
387         foreach(dep ${ARG_PUBLIC})
388             if (TARGET ${dep})
389                 target_link_libraries(${_target} PUBLIC ${dep})
390             endif()
391         endforeach()
392     endif()
393
394     if (ARG_PRIVATE)
395         foreach(dep ${ARG_PRIVATE})
396             if (TARGET ${dep})
397                 target_link_libraries(${_target} PRIVATE ${dep})
398             endif()
399         endforeach()
400     endif()
401 endfunction()
402
403 ###################################################################################################
404 # process_cmake_cxx_flags()
405 #
406 # Append the options declared CMAKE_CXX_FLAGS and CMAKE_CXX_FLAGS_<BUILD_TYPE> to the global
407 # compile options.
408 # Unset the variables afterwards to avoid duplication.
409 #
410 function(process_cmake_cxx_flags)
411     string(TOUPPER ${CMAKE_BUILD_TYPE} upper_build_type)
412     set(cxx_flags "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${upper_build_type}}")
413     separate_arguments(sep_cxx_flags UNIX_COMMAND ${cxx_flags})
414     add_compile_options(${sep_cxx_flags})
415     set(CMAKE_CXX_FLAGS "" PARENT_SCOPE)
416     set(CMAKE_CXX_FLAGS_${upper_build_type} "" PARENT_SCOPE)
417 endfunction()