Search code examples
c++qtcmakemingw-w64

Export custom library which uses Qt via CMake for use in another CMake project (Windows, Mingw-w64)


A bit of background on what I am trying to achieve: I already have a project that I have developed in CMake (it is collection of CMake projects: basically a state machine with a few helper libraries). Now, I want to develop a GUI in Qt that this state machine can control (and make it do stuff). However, for the life of me, I cannot figure out how to properly export the library which has the Qt classes and functions to another CMake package's executable. I am on Windows, and using the Mingw-w64 compiler and make program.

I was experimenting with 2 simple CMake projects, one which has a simple Qt form (which I just copied from and modified this repository and the corresponding youtube video given in the README), and the other which has the executable. I was trying to link the first project with the second project, however, whenever I build the autogenerated makefile, it fails in the end, saying that symbols are undefined (For example undefined reference to run_stuff::run_stuff()'). I apologize if the files are dirty, I have been trying out various stuff to no avail.

Things I have tried:

  • I have followed the answer here very carefully.

  • I have tried setting CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS to ON, as suggested by many sites (like this one) for running DLLs

  • I have tried making the library static (hence doing away with the need for DLLs)

  • I have tried setting the target_include_dirs from private to public and also commenting out that line. (not sure what that does, but okay)

However, no matter what I do, I always end with linking errors in the end, and my executable in the second project is not able to access any functions from the Qt project. Any suggestions on what I should try next? Would be really helpful if you could point out errors I am making in my CMakeLists.txt files (once again apologies for the messy code and CMakeLists)

My project CMake file looks like this:

cmake_minimum_required(VERSION 3.14)

# if (WIN32)
#   set(CMAKE_SHARED_LIBRARY_PREFIX "")

# endif ()

set(CMAKE_MAKE_PROGRAM $ENV{MAKE})
set(CMAKE_CONFIGURATION_TYPES "Release;RelWithDebInfo" CACHE STRING "" FORCE)
set(CMAKE_SUPPORT_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(WINDOWS_EXPORT_ALL_SYMBOLS ON)

add_subdirectory(QT6CMake)
add_subdirectory(Exec)

The library CMake file:

cmake_minimum_required(VERSION 3.14)

if (WIN32)
    project(MY_PROJECT LANGUAGES CXX)
elseif(UNIX)
    project(MY_PROJECT)
endif()

set(CMAKE_CONFIGURATION_TYPES "Release;RelWithDebInfo" CACHE STRING "" FORCE)

#======================= INCLUSION OF Qt =======================#
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_PREFIX_PATH $ENV{QTDIR})
find_package(Qt6Core REQUIRED)
find_package(Qt6Widgets REQUIRED)

#=================== INCLUSION OF Project Files ====================#
set(FORMS_DIR "${CMAKE_SOURCE_DIR}/forms")
set(INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include")
set(SOURCE_DIR "${CMAKE_SOURCE_DIR}/src")

include_directories(${FORMS_DIR})
include_directories(${INCLUDE_DIR})
include_directories(${SOURCE_DIR})

file(GLOB_RECURSE SOURCES
    "${FORMS_DIR}/*.ui"
    "${FORMS_DIR}/*.qrc"
    "${INCLUDE_DIR}/*.h"
    "${SOURCE_DIR}/*.cpp"
)

#=================== SETUP EXECTUABLE ====================#
# Enable debug logging on RELWITHDEBINFO configuration
set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS
    $<$<CONFIG:RELWITHDEBINFO>:QT_MESSAGELOGCONTEXT>
)

# Add the forms directory to the AUTOUIC search paths
set(CMAKE_AUTOUIC_SEARCH_PATHS ${CMAKE_AUTOUIC_SEARCH_PATHS} ${FORMS_DIR})

# Add the executable
if (WIN32) 
    add_executable(MY_PROJECT WIN32 ${SOURCES})
elseif(UNIX)
    add_executable(MY_PROJECT ${SOURCES})
endif()

# Add the target includes for MY_PROJECT 
target_include_directories(MY_PROJECT PRIVATE ${FORMS_DIR})
target_include_directories(MY_PROJECT PRIVATE ${INCLUDE_DIR})
target_include_directories(MY_PROJECT PRIVATE ${SOURCE_DIR})

#===================== LINKING LIBRARIES =======================#
target_link_libraries(MY_PROJECT Qt6::Widgets)

The Exec CMake file:

cmake_minimum_required(VERSION 3.14)
project(somexec)

add_definitions(${MY_PROJECT_DEFINITIONS})
include_directories(${MY_PROJECT_INCLUDE_DIRS})

if (WIN32) 
    add_executable(${PROJECT_NAME} WIN32 main.cpp)
elseif(UNIX)
    add_executable(${PROJECT_NAME} main.cpp)
endif()

target_link_libraries(${PROJECT_NAME} MY_PROJECT)

Here is the codebase.

EDIT: Finally the code seems to be working, courtesy of Guillaume Racicot's multiple edits and fixes. I am going to leave this repository public in case anyone wants to see the codebase. Also, right now, I don't understand all the CMake commands that he has used, will try to understand those as well


Solution

  • The CMake code from the tutorial uses very old fashioned and outdated CMake practices. It works when creating a simple project, but won't work when doing libraries. To share files between projects, you need to export the CMake targets. You can do that by creating this file first:

    cmake/yourlibrary-config.cmake

    include(CMakeFindDependencyMacro)
    
    # write it like you find_package of your cmake scripts
    find_dependency(Qt6 COMPONENTS Core Widgets REQUIRED)
    
    include("${CMAKE_CURRENT_LIST_DIR}/yourlibrary-targets.cmake")
    

    Then, add this to your main project cmake files. You should have a CMake file that looks like this:

    cmake_minimum_required(VERSION 3.21)
    project(yourlibrary CXX)
    
    # First, create your library. List all the files one by one. Don't use globs
    add_library(yourlibrary src/mainwindow.cpp)
    
    # Then add an alias of your library to enable target syntax
    add_library(yourlibrary::yourlibrary ALIAS yourlibrary)
    
    # Automatically generate Qt ui and moc
    set_target_properties(yourlibrary PROPERTIES
        AUTOUIC ON
        AUTOMOC ON
        AUTOUIC_SEARCH_PATHS forms
    )
    
    # Then, include gnuinstalldir to get the platform's standard directories:
    include(GNUInstallDirs)
    
    # Then, carefully add your include directories. All of your `target_include_directories` must look like this
    target_include_directories(yourlibrary PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> # include directory in your build tree
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>       # include directory when installed
    )
    
    # Then, include Qt and link qt
    find_package(Qt6 COMPONENTS Core Widgets REQUIRED)
    target_link_libraries(yourlibrary PUBLIC Qt6::Core Qt6::Widgets)
    
    # Now, create the install script. We install all header files under `include/yourlibrary` to install them in `<prefix>/include/yourlibrary`:
    install(
        DIRECTORY include/yourlibrary
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h"
    )
    
    # We add `yourlibrary` target into the export set.
    # The export set will contain all targets to be imported by the other project.
    # It also installs the library to the install script so they are installed:
    install(TARGETS yourlibrary EXPORT yourlibrary-targets
        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
        ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    )
    
    # Now, we install the export set. This will generate a CMake file exporting all the target for other projects to use:
    install(EXPORT yourlibrary-targets
        DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/yourlibrary"
        NAMESPACE yourlibrary::
    )
    
    # Now, we also export the current buildtree. Other project will be able to import the project directly from a build dir:
    configure_file(cmake/yourlibrary-config.cmake yourlibrary-config.cmake COPYONLY)
    export(
        EXPORT yourlibrary-targets
        NAMESPACE yourlibrary::
        FILE "${PROJECT_BINARY_DIR}/yourlibrary-targets.cmake"
    )
    
    # The file we created earlier:
    install(
        FILES cmake/yourlibrary-config.cmake
        DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/yourlibrary"
    )
    

    I omitted the installation of the generated headers since they are usually considered private for the project.

    For the library, have a source tree looking like this:

    yourlibrary
    ├── cmake
    │   └── yourlibrary-config.cmake
    ├── forms
    │   └── mainwindow.ui
    ├── include
    │   └── yourlibrary
    │       └── mainwindow.h
    ├── src
    │   └── mainwindow.cpp
    └── CMakeLists.txt
    

    To use the library, you have two choices.

    1. Either you embed it in your project using add_subdirectory(yourlibrary)
    2. Either you build it separately and use find_package(yourlibrary REQUIRED)

    You can't do both.

    I usually prefer using find_package, but it require a package manager to avoid manually building all dependencies.


    If you use add_subdirectory

    Then remove find_package(yourlibrary REQUIRED) from your project. Simply add the directory and have your main project file look like this:

    cmake_minimum_required(VERSION 3.21)
    project(exec LANGUAGES CXX)
    
    # Embed the project
    add_subdirectory(yourlibrary)
    
    add_executable(myexec main.cpp)
    target_link_libraries(myexec PUBLIC yourlibrary::yourlibrary)
    

    I assume a project tree like this:

    SampleProject
    ├── yourlibrary
    │   └── ...
    ├── CMakeLists.txt
    └── main.cpp
    

    If you build yourlibrary separately

    First, build the library and (optionally) install it in a known location.

    Now, you can build your library that uses Qt. You will need a CMakeLists.txt that looks like this:

    cmake_minimum_required(VERSION 3.21)
    project(myexec LANGUAGES CXX)
    
    # also finds Qt and all
    find_package(yourlibrary REQUIRED)
    
    add_executable(myexec main.cpp)
    target_link_libraries(myexec PUBLIC yourlibrary::yourlibrary)
    

    If you install the yourlibrary library, it will simply work. If you want to use it from its build tree, simply run the CMake command with -DCMAKE_PREFIX_PATH=/path/to/yourlibrary/build. It should point to the build directory of the library, or the installation prefix if you installed it in a custom location.