之前尝试xrepo的时候了解到conda也可以管理C++项目1,现在针对常用的库验证下。conda安装过程不细说,建议参考官方文档2。由于conda提供的库多以动态链接形式发布,为了确保程序打包输出能正常运行,尝试通过cmake自动打包输出依赖的库文件。

1. 编译CGAL+qt项目

1.1 建立虚拟环境

首先要建立并激活虚拟环境。conda自带的默认虚拟环境为base,命名上注意不要和默认虚拟环境重名。这里通过虚拟环境安装依赖,避免不同项目之间的依赖产生冲突。

conda create -n cgal_env # 建立虚拟环境,-n 指定虚拟环境名称
conda activate cgal_env # 激活虚拟环境

虚拟环境激活之后会在shell前面显示(cgal_env)。如果是安装conda之后第一次建立虚拟环境,激活的时候可能会报错,一般重启shell即可解决。

激活虚拟环境之后,在虚拟环境中安装cgal包及其依赖。这里碰到一个问题,conda的托管的cgal不会自动安装依赖的qt6,得手动安装。

conda install cgal -c conda-forge	# -c 指定通道 conda-forge
conda install qt6-main -c conda-forge

1.2 编写工程文件

以官方教程3为例,建立cmake项目文件夹surface_mesh_viewer。编写C++源文件main.cpp如下所示。

// main.cpp

/***
 * reference: https://doc.cgal.org/latest/Manual/devman_create_and_use_a_cmakelist.html
 ***/

#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/draw_surface_mesh.h>
#include <fstream>
#include <QApplication>
#include <QFileDialog>

typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point;
typedef CGAL::Surface_mesh<Point> Mesh;

int main(int argc, char *argv[])
{
  QApplication app(argc, argv);
  const std::string filename = (argc > 1) ? argv[1] : QFileDialog::getOpenFileName(nullptr, "Open a mesh file", "", "Supported formats (*.off *.stl *.obj *.ply);;OFF format (*.off);;STL format (*.stl);;OBJ format (*.obj);;PLY format (*.ply)").toStdString();
  if (filename.empty())
    return EXIT_FAILURE;

  Mesh sm;
  if (!CGAL::IO::read_polygon_mesh(filename, sm))
  {
    if (filename.substr(filename.find_last_of(".") + 1) == "stl")
      std::cerr << "Invalid STL file: " << filename << std::endl;
    else if (filename.substr(filename.find_last_of(".") + 1) == "obj")
      std::cerr << "Invalid OBJ file: " << filename << std::endl;
    else if (filename.substr(filename.find_last_of(".") + 1) == "ply")
      std::cerr << "Invalid PLY file: " << filename << std::endl;
    else if (filename.substr(filename.find_last_of(".") + 1) == "off")
      std::cerr << "Invalid OFF file: " << filename << std::endl;
    else
      std::cerr << "Invalid file: " << filename << "(Unknown file format.)" << std::endl;
    return EXIT_FAILURE;
  }

  // Internal color property maps are used if they exist and are called
  // "v:color", "e:color" and "f:color".
  auto vcm =
      sm.add_property_map<Mesh::Vertex_index, CGAL::IO::Color>("v:color").first;
  auto ecm =
      sm.add_property_map<Mesh::Edge_index, CGAL::IO::Color>("e:color").first;
  auto fcm = sm.add_property_map<Mesh::Face_index>(
                   "f:color", CGAL::IO::white() /*default*/)
                 .first;

  for (auto v : vertices(sm))
  {
    if (v.idx() % 2)
    {
      put(vcm, v, CGAL::IO::black());
    }
    else
    {
      put(vcm, v, CGAL::IO::blue());
    }
  }

  for (auto e : edges(sm))
  {
    put(ecm, e, CGAL::IO::gray());
  }

  put(fcm, *(sm.faces().begin()), CGAL::IO::red());

  // Draw!
  CGAL::draw(sm);

  return EXIT_SUCCESS;
}

这里用到了cmake作为编译工具,编写CMakeLists.txt文件。

cmake_minimum_required(VERSION 3.21)
project(surface_mesh_viewer LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)

# check if conda environment is activated
if(NOT $ENV{CONDA_PREFIX} STREQUAL "")
  message(STATUS "CURRENT CONDA PREFIX: $ENV{CONDA_PREFIX}")
  include_directories($ENV{CONDA_PREFIX}/include)
  link_directories($ENV{CONDA_PREFIX}/lib)
endif()

# CGAL_Qt6 is needed for the drawing.
find_package(CGAL REQUIRED OPTIONAL_COMPONENTS Qt6)
add_executable(${PROJECT_NAME} main.cpp)

if(CGAL_Qt6_FOUND)
  # link it with the required CGAL libraries
  target_link_libraries(${PROJECT_NAME} PUBLIC CGAL::CGAL_Basic_viewer)
endif()

通过以下命令配置cmake并编译成可执行文件。

cmake -B build .	# 配置 cmake,指定编译目录 build
cmake --build build --config release	# 编译为 release 版本

1.3 手动发布qt程序

windows平台上要注意,这时候还不能直接运行生成的exe文件,会提示缺少dll。因为conda默认安装的qt6是动态链接库版本,正常情况下需要使用windeployqt6发布工具将程序运行所依赖的qt的dll文件按规定的路径复制到exe文件的安装目录中。

windeployqt6 build\Release\surface_mesh_viewer.exe

但是这里运行windeployqt6工具出现报错,提示qtpaths无法启动,应该是qt6发布打包的时候没有处理好,将windeployqt6.exe所在目录的qtpaths6.exe复制并重命名为qtpaths.exe即可解决。

Unable to query qtpaths: Error running binary qtpaths: Process failed to start: 

可以在终端里通过命令行运行程序,但是双击驱动可执行文件会提示缺少dll文件。

.\build\Release\surface_mesh_viewer.exe

229d9adad4bd23116ce24eeb6b029c37.png

1.4 通过cmake自动发布qt程序

将1.2中的CMakeLists.txt文件末尾增加以下内容,调用cmake官方的发布脚本4自动发布安装qt生成的可执行程序。除了调用qt_generate_deploy_app_script()部署qt的dll之外,还需要将其他运行时依赖的dll文件复制到bin目录中,这时候就需要调用cmake的file(GET_RUNTIME_DEPENDENCIES)解析目标文件的依赖5

...

# set installation directories
install(TARGETS ${PROJECT_NAME}
  BUNDLE DESTINATION bin
  RUNTIME DESTINATION bin
)

# generate deploy app script
qt_generate_deploy_app_script(
  TARGET ${PROJECT_NAME}
  OUTPUT_SCRIPT deploy_script
  NO_UNSUPPORTED_PLATFORM_ERROR
)

# install deploy script
install(SCRIPT ${deploy_script})

# install runtime dependencies
install(CODE [[
file(GET_RUNTIME_DEPENDENCIES
    RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
    UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
    # 要解析的目标可执行文件路径
    EXECUTABLES $<TARGET_FILE:surface_mesh_viewer>
    # 库文件的搜索路径
    DIRECTORIES "$ENV{CONDA_PREFIX}/Library/bin"
    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()
]])

配置cmake并编译安装,可以通过--prefix指定安装路径。注意安装路径必须为绝对路径,否则会报错。

cmake -B build .
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/deploy # 安装编译生成的 Qt 程序, 安装位置为当前目录下的 deploy 文件夹

可以看到除了qt自带的dll文件,还复制了其他的依赖,这时候双击安装路径下的可执行文件可以正常运行了。

...
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/msvcp140.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/msvcp140_1.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/msvcp140_2.dll
-- Up-to-date: D:\example\conda\surface_mesh_viewer/deploy/bin/Qt6Core.dll
-- Up-to-date: D:\example\conda\surface_mesh_viewer/deploy/bin/Qt6Gui.dll
-- Up-to-date: D:\example\conda\surface_mesh_viewer/deploy/bin/Qt6OpenGL.dll
-- Up-to-date: D:\example\conda\surface_mesh_viewer/deploy/bin/Qt6OpenGLWidgets.dll
-- Up-to-date: D:\example\conda\surface_mesh_viewer/deploy/bin/Qt6Svg.dll
-- Up-to-date: D:\example\conda\surface_mesh_viewer/deploy/bin/Qt6Widgets.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/vcruntime140.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/vcruntime140_1.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/double-conversion.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/freetype.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/libgmp-10.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/libpng16.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/pcre2-16.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/zlib.dll
-- Installing: D:\example\conda\surface_mesh_viewer/deploy/bin/zstd.dll

2. 编译wxWidgets项目

2.1 建立虚拟环境

建立一个新的虚拟环境用于安装wxWidgets及相关依赖。

conda create -n wx_env
conda activate wx_env

安装wxWidgets及相关库。

conda install wxwidgets -c conda-forge

2.2 编写工程文件

参考官方文档6编写c++源文件。

// main.cpp

#include <wx/wx.h>

class MyApp : public wxApp
{
public:
    bool OnInit() override;
};

wxIMPLEMENT_APP(MyApp);

class MyFrame : public wxFrame
{
public:
    MyFrame();

private:
    void OnHello(wxCommandEvent &event);
    void OnExit(wxCommandEvent &event);
    void OnAbout(wxCommandEvent &event);
};

enum
{
    ID_Hello = 1
};

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

MyFrame::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, &MyFrame::OnHello, this, ID_Hello);
    Bind(wxEVT_MENU, &MyFrame::OnAbout, this, wxID_ABOUT);
    Bind(wxEVT_MENU, &MyFrame::OnExit, this, wxID_EXIT);
}

void MyFrame::OnExit(wxCommandEvent &event)
{
    Close(true);
}

void MyFrame::OnAbout(wxCommandEvent &event)
{
    wxMessageBox("This is a wxWidgets Hello World example",
                 "About Hello World", wxOK | wxICON_INFORMATION);
}

void MyFrame::OnHello(wxCommandEvent &event)
{
    wxLogMessage("Hello world from wxWidgets!");
}

编写CMakeLists.txt文件。

cmake_minimum_required(VERSION 3.21)
project(wx_sample LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)

# check if conda environment is activated
if(NOT $ENV{CONDA_PREFIX} STREQUAL "")
  message(STATUS "CURRENT CONDA PREFIX: $ENV{CONDA_PREFIX}")
  include_directories($ENV{CONDA_PREFIX}/include)
  link_directories($ENV{CONDA_PREFIX}/lib)
endif()

# find wxWidgets
find_package(wxWidgets REQUIRED COMPONENTS core base)

if(wxWidgets_USE_FILE) 
  include(${wxWidgets_USE_FILE})
endif()

add_executable(${PROJECT_NAME} main.cpp)

if(MSVC)
  set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS")
endif()

target_link_libraries(${PROJECT_NAME} ${wxWidgets_LIBRARIES})

# set installation directories
install(TARGETS ${PROJECT_NAME}
  BUNDLE DESTINATION bin
  RUNTIME DESTINATION bin
)

# install runtime dependencies
install(CODE [[
file(GET_RUNTIME_DEPENDENCIES
    RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
    UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
    # path to the executable files
    EXECUTABLES $<TARGET_FILE:wx_sample>
    # directories to search for library files
    DIRECTORIES "$ENV{CONDA_PREFIX}/Library/bin"
    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()
]])

2.3 编译及发布

以下命令将程序编译成release版本并发布。

cmake -B build .
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/install

生成的可执行程序和依赖的dll都拷贝到安装目录中了。

-- Installing: D:\example\conda\wx_sample/install/bin/wx_sample.exe
-- Installing: D:\example\conda\wx_sample/install/bin/Lerc.dll
-- Installing: D:\example\conda\wx_sample/install/bin/msvcp140.dll
-- Installing: D:\example\conda\wx_sample/install/bin/vcruntime140.dll
-- Installing: D:\example\conda\wx_sample/install/bin/vcruntime140_1.dll
-- Installing: D:\example\conda\wx_sample/install/bin/deflate.dll
-- Installing: D:\example\conda\wx_sample/install/bin/jpeg8.dll
-- Installing: D:\example\conda\wx_sample/install/bin/liblzma.dll
-- Installing: D:\example\conda\wx_sample/install/bin/libpng16.dll
-- Installing: D:\example\conda\wx_sample/install/bin/pcre2-16.dll
-- Installing: D:\example\conda\wx_sample/install/bin/tiff.dll
-- Installing: D:\example\conda\wx_sample/install/bin/wxbase331u_vc_x64.dll
-- Installing: D:\example\conda\wx_sample/install/bin/wxmsw331u_core_vc_x64.dll
-- Installing: D:\example\conda\wx_sample/install/bin/zlib.dll
-- Installing: D:\example\conda\wx_sample/install/bin/zstd.dll

c52d1a67fa019fe23109fc9537141379.png

3. 编译gtk项目

3.1 建立虚拟环境

建立一个新的虚拟环境用于安装gtk及相关依赖。

conda create -n gtk_env
conda activate gtk_env
conda install  -c conda-forge
conda install pkgconfig glib gtk4 zlib -c conda-forge

这里用到的是gtk4,conda对gtk4的依赖支持不是很好,需要用pkg-config命令行验证是否完整7,缺失的包需要自己手动安装。

pkg-config --cflags gtk4

3.2 编写工程文件

编写c源文件main.c

// mian.c

#include <gtk/gtk.h>

static void
print_hello(GtkWidget *widget,
            gpointer data)
{
  GtkWidget *parent_window = gtk_widget_get_parent(widget);
  GtkAlertDialog *dialog = gtk_alert_dialog_new("Hello World!");
  gtk_alert_dialog_set_modal(dialog, TRUE);
  gtk_alert_dialog_show(dialog, GTK_WINDOW(parent_window));
  // g_print ("Hello World\n");
}

static void
activate(GtkApplication *app,
         gpointer user_data)
{
  GtkWidget *window;
  GtkWidget *button;
  GtkWidget *box;

  window = gtk_application_window_new(app);
  gtk_window_set_title(GTK_WINDOW(window), "Window");
  gtk_window_set_default_size(GTK_WINDOW(window), 200, 200);

  box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
  gtk_widget_set_valign(box, GTK_ALIGN_CENTER);

  gtk_window_set_child(GTK_WINDOW(window), box);

  button = gtk_button_new_with_label("Hello World");

  g_signal_connect(button, "clicked", G_CALLBACK(print_hello), NULL);
  // g_signal_connect_swapped(button, "clicked", G_CALLBACK(gtk_window_destroy), window);

  gtk_box_append(GTK_BOX(box), button);

  gtk_window_present(GTK_WINDOW(window));
}

int main(int argc,
         char **argv)
{
  GtkApplication *app;
  int status;

  app = gtk_application_new("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
  status = g_application_run(G_APPLICATION(app), argc, argv);
  g_object_unref(app);

  return status;
}

编写CMakeLists.txt文件,cmake需要调用pkgconfig来定位gtk4及相关依赖库的安装路径。最后程序发布除了要复制dll依赖到/bin目录之外,还要复制/etc/lib/share目录下的资源文件89到安装路径对应目录中。

cmake_minimum_required(VERSION 3.21)

project(gtk_sample LANGUAGES C)

set(CMAKE_C_STANDARD 11)

# check if conda environment is activated
if(NOT $ENV{CONDA_PREFIX} STREQUAL "")
    message(STATUS "CURRENT CONDA PREFIX: $ENV{CONDA_PREFIX}")
    include_directories($ENV{CONDA_PREFIX}/include)
    link_directories($ENV{CONDA_PREFIX}/lib)
endif()

set(SRC_LIST main.c)

if(MINGW)
    set(CMAKE_C_FLAGS "-mwindows")
endif()

find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED gtk4)
include_directories(${GTK_INCLUDE_DIRS})
link_directories(${GTK_LIBRARY_DIRS})
add_executable(${PROJECT_NAME} ${SRC_LIST})

if(MSVC)
    set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS /entry:mainCRTStartup")
endif()

target_link_libraries(${PROJECT_NAME} ${GTK_LIBRARIES})

include(GNUInstallDirs)
install(TARGETS ${PROJECT_NAME}
    BUNDLE DESTINATION bin
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

# install runtime dependencies
install(CODE [[
file(GET_RUNTIME_DEPENDENCIES
    RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
    UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
    # path to the executable files
    EXECUTABLES $<TARGET_FILE:gtk_sample>
    # directories to search for library files
    DIRECTORIES "$ENV{CONDA_PREFIX}/Library/bin"
    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()
set(GTK_SEARCH_DIRS "$ENV{CONDA_PREFIX}/Library")
set(GTK_SUB_DIRS "lib" "etc" "share")
set(SUB_DIR_PEN "gtk" "glib" "gdk" "pango" "themes" "icons" "locale")
foreach(SUB_DIR ${GTK_SUB_DIRS})
    foreach(PEN_DIR ${SUB_DIR_PEN})
        file(GLOB DIRECTORIES LIST_DIRECTORIES true RELATIVE "${GTK_SEARCH_DIRS}/${SUB_DIR}" "${GTK_SEARCH_DIRS}/${SUB_DIR}/*${PEN_DIR}*")
        foreach(DIR ${DIRECTORIES})
            if(IS_DIRECTORY "${GTK_SEARCH_DIRS}/${SUB_DIR}/${DIR}")
                file(INSTALL "${GTK_SEARCH_DIRS}/${SUB_DIR}/${DIR}" DESTINATION "${CMAKE_INSTALL_PREFIX}/${SUB_DIR}")
            endif()
        endforeach()
    endforeach()
endforeach()
]])

这里直接检索符合名称的文件夹然后复制到安装路径中,可能存在部分无效依赖,需要手工清理。这里用到的conda官方发布的gtk和glib程序资源文件有些缺失了,好在暂时不影响运行,以后有时间再来解决。

3.3 编译及发布

以下命令将程序编译成release版本并发布。

cmake -B build .
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/install

13c5792f4c810a690130e715e8c44376.png

最后发布的程序,文件夹树如下图所示。

e6241bf0454fb419177b6b4921b1e95b.png

4. 总结

conda官方提供的C/C++库文件还是比较丰富的,版本更新也比较及时。windows环境下conda提供的二进制包基本都是基于msvc发布的,需要结合cmake或meson等其他编译工具来调用,相当于msys2搭配gcc,可以弥补windows包管理缺失的问题。最最重要的一点,不用像vcpkg那样等待漫长的编译过程。

美中不足的是,conda官方没有提供静态编译版本的包,编译的程序需要自己编写发布脚本,避免出现缺失运行时的问题。个别库如gtk4依赖关系没有完全解决,还要手动下载依赖的库。而且打包的库缺失了某些资源文件,好在暂时不影响编译和运行。

可能是受python的影响,conda需要先建立虚拟环境,然后再切换到虚拟环境中执行编译发布操作。由于用惯了python,个人觉得还是挺有帮助的,可以有效避免依赖冲突。

conda初始化会“污染”系统shell,每次打开终端前面都会带着虚拟环境提示,总令人感觉不舒服。

0f75cfd077d5fad57c8b017c4a086298.png

关于vscode代码提示的问题,如果只是通过命令行调用cmake,此时IntelliSense是没法识别头文件路径和依赖关系的。最好通过终端切换到指定的虚拟环境中,再通过命令行打开vscode,此时vscode会载入虚拟环境指定的环境变量。这个时候通过cmake tools拓展配置cmake之后就可以激活IntelliSense的代码提示了。