之前尝试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
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
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
最后发布的程序,文件夹树如下图所示。
4. 总结
conda官方提供的C/C++库文件还是比较丰富的,版本更新也比较及时。windows环境下conda提供的二进制包基本都是基于msvc发布的,需要结合cmake或meson等其他编译工具来调用,相当于msys2搭配gcc,可以弥补windows包管理缺失的问题。最最重要的一点,不用像vcpkg那样等待漫长的编译过程。
美中不足的是,conda官方没有提供静态编译版本的包,编译的程序需要自己编写发布脚本,避免出现缺失运行时的问题。个别库如gtk4依赖关系没有完全解决,还要手动下载依赖的库。而且打包的库缺失了某些资源文件,好在暂时不影响编译和运行。
可能是受python的影响,conda需要先建立虚拟环境,然后再切换到虚拟环境中执行编译发布操作。由于用惯了python,个人觉得还是挺有帮助的,可以有效避免依赖冲突。
conda初始化会“污染”系统shell,每次打开终端前面都会带着虚拟环境提示,总令人感觉不舒服。
关于vscode代码提示的问题,如果只是通过命令行调用cmake,此时IntelliSense是没法识别头文件路径和依赖关系的。最好通过终端切换到指定的虚拟环境中,再通过命令行打开vscode,此时vscode会载入虚拟环境指定的环境变量。这个时候通过cmake tools拓展配置cmake之后就可以激活IntelliSense的代码提示了。