Andrew Moa Blog Site

CPack打包程序依赖问题

之前用CPack打包QML程序的时候发现,CPack打包程序安装的文件和使用cmake install命令安装的文件存在明显的不同,下面解释并解决这个问题。

1. 问题代码

还是以之前编写的QML程序为例,为了方便改进代码将CMakeLists.txt改写如下,依赖安装和打包模块分别写入cmake/dependencies.cmakecmake/packaging.cmake

cmake_minimum_required(VERSION 3.21)
set(PROJECT_NAME sample)
project(${PROJECT_NAME} VERSION 0.1 LANGUAGES CXX)
set(EXECUTABLE_NAME ${PROJECT_NAME}_app)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(NOT $ENV{VCPKG_TARGET_TRIPLET} STREQUAL "")
  set(VCPKG_TARGET_TRIPLET $ENV{VCPKG_TARGET_TRIPLET})
  message(STATUS "VCPKG_TARGET_TRIPLET: ${VCPKG_TARGET_TRIPLET}")
  link_libraries("$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/lib")
  message(STATUS "Link libraries from: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/$<$<CONFIG:Debug>:Debug>/lib")
  include_directories("$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/include")
  message(STATUS "Include directories from: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/include")
  if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
    set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6/qt.toolchain.cmake")
    message(STATUS "Using static Qt6 toolchain file: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6/qt.toolchain.cmake")
    set(Qt6_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6")
    message(STATUS "Using static Qt6_DIR: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6")
  endif()
endif()

find_package(Qt6 6.5 REQUIRED COMPONENTS Quick)

if(Qt6_FOUND)
    message(STATUS "Qt6 found: ${Qt6_DIR}")
else()
    message(FATAL_ERROR "Qt6 not found!")
endif()

qt_standard_project_setup(REQUIRES 6.5)
qt_add_executable(${EXECUTABLE_NAME}
    main.cpp
)
qt_add_qml_module(${EXECUTABLE_NAME}
    URI ${PROJECT_NAME}
    VERSION 1.0
    QML_FILES
    Main.qml
)

# set static linking for MSVC
if(MSVC AND (${VCPKG_TARGET_TRIPLET} MATCHES "static"))
    message(STATUS "Configuring for MSVC static linking")
    set_property(TARGET ${EXECUTABLE_NAME} PROPERTY
        MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

set_target_properties(${EXECUTABLE_NAME} PROPERTIES

    # MACOSX_BUNDLE_GUI_IDENTIFIER com.example.${EXECUTABLE_NAME}
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

target_link_libraries(${EXECUTABLE_NAME}
    PRIVATE Qt6::Quick
)

include(GNUInstallDirs)
install(TARGETS ${EXECUTABLE_NAME}
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

if(MSVC AND (${VCPKG_TARGET_TRIPLET} MATCHES "static"))
    message(STATUS "Linking static MSVC runtime, skipping install of MSVC runtime DLLs")
else()
    message(STATUS "Configuring install of runtime dependencies")
    include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/dependencies.cmake)
endif()

include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/packaging.cmake)

cmake/dependencies.cmake模块定义了如何从qt安装路径中复制依赖带安装路径。

阅读时长4分钟
Andrew Moa

使用CPack打包生成安装程序

事实上,CMake除了构建管理之外,还可以进行测试和打包安装程序。其中打包功能由CPack模块1支持,可以针对不同平台打包生成适用的安装程序。

1. Qt程序CMake配置

以之前编写的QML程序为例,编写CMakeLists.txt配置文件如下。末尾引入的packaging.cmake文件定义了打包配置相关信息。

cmake_minimum_required(VERSION 3.16)
set(PROJECT_NAME sample)
project(${PROJECT_NAME} VERSION 0.1 LANGUAGES CXX)
set(EXECUTABLE_NAME ${PROJECT_NAME}_app)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 REQUIRED COMPONENTS Quick)

if(Qt6_FOUND)
    message(STATUS "Qt6 found: ${Qt6_DIR}")
else()
    message(FATAL_ERROR "Qt6 not found!")
endif()

qt_standard_project_setup(REQUIRES 6.8)
qt_add_executable(${EXECUTABLE_NAME}
    main.cpp
)
qt_add_qml_module(${EXECUTABLE_NAME}
    URI ${PROJECT_NAME}
    VERSION 1.0
    QML_FILES
    Main.qml
)

# set static linking for MSVC
if(MSVC AND (${VCPKG_TARGET_TRIPLET} MATCHES "static"))
    message(STATUS "Configuring for MSVC static linking")
    set_property(TARGET ${EXECUTABLE_NAME} PROPERTY
        MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

set_target_properties(${EXECUTABLE_NAME} PROPERTIES

    # MACOSX_BUNDLE_GUI_IDENTIFIER com.example.${EXECUTABLE_NAME}
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

target_link_libraries(${EXECUTABLE_NAME}
    PRIVATE Qt6::Quick
)

include(GNUInstallDirs)
install(TARGETS ${EXECUTABLE_NAME}
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

if(MSVC AND (${VCPKG_TARGET_TRIPLET} MATCHES "static"))
    message(STATUS "Linking static MSVC runtime, skipping install of MSVC runtime DLLs")
else()
    message(STATUS "Configuring install of runtime dependencies")
    set(deploy_tool_options_arg "")

    if(APPLE)
        set(deploy_tool_options_arg --hardened-runtime)
    elseif(WIN32)
        set(deploy_tool_options_arg --no-compiler-runtime)
    endif()

    qt_generate_deploy_qml_app_script(
        TARGET ${EXECUTABLE_NAME}
        OUTPUT_SCRIPT deploy_script
        MACOS_BUNDLE_POST_BUILD
        NO_UNSUPPORTED_PLATFORM_ERROR
        DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM
        DEPLOY_TOOL_OPTIONS ${deploy_tool_options_arg}
    )

    install(SCRIPT ${deploy_script})

    install(CODE [[
    if("$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
      find_program(QMAKE qmake6 HINTS $ENV{PATH})
      if(NOT QMAKE)
        message(FATAL_ERROR "qmake not found in PATH")
      else()
        message(STATUS "qmake found: ${QMAKE}")
        cmake_path(GET QMAKE PARENT_PATH QT_BIN_DIR)
        message(STATUS "qmake bin dir: ${QT_BIN_DIR}")
      endif()
    else()
      set(QT_BIN_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/bin")
      if(NOT EXISTS ${QT_BIN_DIR})
        message(FATAL_ERROR "qmake bin dir not found: ${QT_BIN_DIR}")
      endif()
    endif()
    file(GET_RUNTIME_DEPENDENCIES
        RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
        UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
        # path to the executable files
        EXECUTABLES $<TARGET_FILE:sample_app>
        # directories to search for library files
        DIRECTORIES ${QT_BIN_DIR}
        PRE_EXCLUDE_REGEXES "system32"
        PRE_EXCLUDE_REGEXES "api-ms-*"
        POST_EXCLUDE_REGEXES "system32"
    )
    foreach(DEP_LIB ${RESOLVED_DEPS})
        file(INSTALL ${DEP_LIB} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
    endforeach()
    ]])
endif()

include(${CMAKE_CURRENT_SOURCE_DIR}/packaging.cmake)

install定义了要安装什么内容之后,在cmake配置文件末尾增加打包相关内容2

阅读时长3分钟
Andrew Moa

Lambda表达式处理事件回调函数

C++11开始支持Lambda表达式,Lambda表达式除了可以作为匿名函数使用,还可以通过捕获列表访问环境中的变量。在UI编程中使用Lambda表达式替代传统的函数指针处理回调函数,不仅可以简化代码提高可读性,还可以减少参数传递提高灵活性。下面针对常用的C++ UI库尝试实现相关操作。

1. wxWidgets事件绑定

1.1 wxWidgets事件机制

wxWidgets有两套事件机制,一套是静态的事件表1,另一套是动态事件机制2。动态事件提供了两套API,分别是Connect()Bind()3,后者较新,前者已经淘汰。

实际上Bind()是原生支持Lambda表达式的,我们可以将官方文档的示例代码4改写如下。

// --- main.cpp ---

#include <wx/wx.h>

enum
{
    ID_Hello = 1
};

class MyFrame : public wxFrame
{
public:
    MyFrame()
        : wxFrame(nullptr, wxID_ANY, "Hello World")
    {
        wxMenu *menuFile = new wxMenu;
        menuFile->Append(ID_Hello, "&Hello...\tCtrl-H",
                         "Help string shown in status bar for this menu item");
        menuFile->AppendSeparator();
        menuFile->Append(wxID_EXIT);

        wxMenu *menuHelp = new wxMenu;
        menuHelp->Append(wxID_ABOUT);

        wxMenuBar *menuBar = new wxMenuBar;
        menuBar->Append(menuFile, "&File");
        menuBar->Append(menuHelp, "&Help");

        SetMenuBar(menuBar);

        CreateStatusBar();
        SetStatusText("Welcome to wxWidgets!");

        Bind(wxEVT_MENU, [](wxCommandEvent &event)
             { wxLogMessage("Hello world from wxWidgets!"); }, ID_Hello);
        Bind(wxEVT_MENU, [](wxCommandEvent &event)
             { wxMessageBox("This is a wxWidgets Hello World example",
                            "About Hello World", wxOK | wxICON_INFORMATION); }, wxID_ABOUT);
        Bind(wxEVT_MENU, [this](wxCommandEvent &event)
             { this->Close(true); }, wxID_EXIT);
    }
};

class MyApp : public wxApp
{
public:
    bool OnInit() override
    {
        MyFrame *frame = new MyFrame();
        frame->Show(true);
        return true;
    }
};

wxIMPLEMENT_APP(MyApp);

注意Bind()函数传入的参数个数,这里不再需要传入this指针。使用Lambda表达式改写之后窗口类的成员函数基本不需要定义,需要调用对象自身的方法执行特定操作时(比如执行关闭窗口操作),可以通过捕获列表获取对象的this指针。

阅读时长11分钟
Andrew Moa