When I previously tried xrepo, I learned that conda can also manage C++ projects1. Now, I will verify this with some commonly used libraries. The conda installation process will not be detailed here; it is recommended to refer to the official documentation2. Since the libraries provided by conda are often released in dynamically linked form, in order to ensure that the packaged output of the program can run properly, I will attempt to use CMake to automatically package the dependent library files.

1. Compiling a CGAL and Qt project

1.1 Creating a Virtual Environment

First, you need to create and activate a virtual environment. Conda’s default virtual environment is called base, so be careful not to name your environment the same as the default. Installing dependencies within a virtual environment helps prevent conflicts between dependencies of different projects.

conda create -n cgal_env # Create a virtual environment, using -n to specify the name of the virtual environment
conda activate cgal_env # Activate the virtual environment

After the virtual environment is activated, (cgal_env) will appear at the beginning of the shell prompt. If it is the first time creating a virtual environment after installing Conda, an error may occur during activation, which can usually be resolved by restarting the shell.

Once the virtual environment is activated, install the CGAL package and its dependencies within the virtual environment. Here, there is a potential issue: the Conda-hosted CGAL package does not automatically install the required Qt6 dependency, which must be installed manually.

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

1.2 Prepare engineering documentation

Taking the official tutorial3 as an example, create a CMake project folder named surface_mesh_viewer. Write the cpp source file main.cpp as shown below.

// 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 is used here as the build tool, and a CMakeLists.txt file is written.

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()

Configure CMake and compile it into an executable using the following command.

cmake -B build .	# Configure CMake and specify the build directory.
cmake --build build --config release	# Compile as a release version

1.3 Manually Publishing a Qt Application

On the Windows platform, it is important to note that the generated exe file cannot be run directly at this stage, as an error indicating missing DLLs will appear. This is because the Qt6 installed by default via Conda is a dynamic library version. Under normal circumstances, you need to use the windeployqt6 deployment tool to copy the Qt DLL files required for running the program to the installation directory of the exe file according to the specified paths.

windeployqt6 build\Release\surface_mesh_viewer.exe

However, running the windeployqt6 tool here results in an error indicating that qtpaths cannot be launched. This is likely due to an improper setup during the packaging of Qt6. The issue can be resolved by copying qtpaths6.exe from the directory where windeployqt6.exe is located and renaming it to qtpaths.exe.

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

The program can be run from the terminal via the command line, but double-clicking the executable file will prompt a message indicating that a DLL file is missing.

.\build\Release\surface_mesh_viewer.exe

229d9adad4bd23116ce24eeb6b029c37.png

1.4 Automatically deploying Qt applications via CMake

Add the following content to the end of the CMakeLists.txt file in section 1.2 to invoke the official CMake deployment script4 for automatically deploying and installing the executable generated by Qt. In addition to using qt_generate_deploy_app_script() to deploy Qt’s DLLs, it is also necessary to copy other runtime-dependent DLL files to the bin directory. At this point, CMake’s file(GET_RUNTIME_DEPENDENCIES) needs to be called to parse the dependencies of the target files5.

...

# 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()
]])

Configure CMake and compile the installation, specifying the installation path with --prefix. Note that the installation path must be an absolute path; otherwise, an error will occur.

cmake -B build .
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/deploy # Install the Qt program generated from compilation; the installation location is the 'deploy' folder under the current directory.

It can be observed that, in addition to the DLL files that come with Qt, other dependencies have also been copied. At this point, double-clicking the executable file in the installation path allows it to run properly.

...
-- 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. Compiling the wxWidgets Project

2.1 Creating a Virtual Environment

Create a new virtual environment for installing wxWidgets and its related dependencies.

conda create -n wx_env
conda activate wx_env

Install wxWidgets and the related libraries.

conda install wxwidgets -c conda-forge

2.2 Prepare engineering documentation

Write the cpp source file with reference to the official documentation6.

// 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!");
}

Write the CMakeLists.txt file.

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 Compilation and Release

The following command will compile the program into a release version and deploy it.

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

The generated executable program and the dependent DLLs have all been copied to the installation directory.

-- 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. Compile the GTK project

3.1 Creating a Virtual Environment

Create a new virtual environment for installing GTK and related dependencies.

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

Here, GTK4 is used. Conda’s support for GTK4 dependencies is not very robust, so it is necessary to use the pkg-config command line to verify completeness7. Any missing packages will need to be installed manually.

pkg-config --cflags gtk4

3.2 Prepare engineering documentation

Write the C source file 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;
}

When writing the CMakeLists.txt file, CMake needs to call pkgconfig to locate the installation paths of GTK4 and its related dependencies. When releasing the program, in addition to copying DLL dependencies to the /bin directory, it is also necessary to copy resource files89 from the /etc, /lib, and /share directories to the corresponding directories in the installation path.

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()
]])

Here, we directly search for folders that match the name and then copy them to the installation path. Some invalid dependencies may exist and need to be manually cleaned up. Some of the GTK and GLib program resource files released officially by Conda are missing, but fortunately, it doesn’t affect operation for now; it can be addressed later when there is time.

3.3 Compile and Release

The following command will compile the program into a release version and deploy it.

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

13c5792f4c810a690130e715e8c44376.png

The folder structure of the final released program is shown in the figure below.

e6241bf0454fb419177b6b4921b1e95b.png

4. Summary

The C/C++ libraries provided officially by conda are still fairly comprehensive, and the version updates are quite timely. In the Windows environment, the binary packages provided by conda are basically all based on MSVC, requiring the use of other build tools like CMake or Meson to call them. This is similar to using MSYS2 with GCC and can make up for the shortcomings of Windows package management. The most important point is that you don’t have to wait through a long compilation process like with vcpkg.

On the downside, conda does not provide statically compiled packages, so you need to write your own release scripts for compiled programs to avoid missing runtime issues. Some libraries, like GTK4, have unresolved dependencies, requiring you to manually download the dependent libraries. Additionally, some packaged libraries are missing certain resource files, though fortunately this doesn’t affect compilation or runtime for now.

Possibly influenced by Python, conda requires you to first create a virtual environment and then switch to it to carry out compilation and release operations. Since I’m used to Python, I personally find this quite helpful, as it can effectively avoid dependency conflicts.

Conda initialization tends to “pollute” the system shell; every time you open the terminal, the virtual environment prompt appears at the front, which is always somewhat annoying.

0f75cfd077d5fad57c8b017c4a086298.png

Regarding the issue of code highlighting in VSCode, if you are only invoking CMake through the command line, IntelliSense will not be able to recognize header file paths and dependencies. It is best to switch to the designated virtual environment in the terminal, and then open VSCode from the command line. At this point, VSCode will load the environment variables specified by the virtual environment. After this, configuring CMake through the CMake Tools extension will allow IntelliSense highlighting to be activated.