When CMake Meets Python

CMake is not unfamiliar to many who are familiar with C and C++. Today, we will look at a somewhat unusual application scenario: using CMake to manage and run Python applications. This approach can be effective in certain situations, such as when code generation is done in C++, and tool scripts are written in Python, while the entire project is integrated through CMake, allowing it to ignore the differences in dependency virtual environments.First, let’s look at a simple example, with the project organized as follows:

project/├── app1/              # Python Application 1│   ├── CMakeLists.txt # Module build script│   ├── main.py        # Main program│   ├── requirements.txt # Dependency list├── app2/              # Python Application 2│   ├── CMakeLists.txt │   ├── main.py │   ├── requirements.txt ├── app3/              # C++ Application│   ├── CMakeLists.txt │   ├── main.cpp       # C++ source code└── build/             # Build output directory    ├── CMakeLists.txt # Top-level build script

Analysis of the top-level CMakeLists.txt

# Specify the minimum CMake versioncmake_minimum_required(VERSION 3.28)# Set the project nameproject(ExamplePythonSetup)# Find the Python interpreterfind_package(Python3 REQUIRED COMPONENTS Interpreter)# Define a function to create a Python virtual environmentfunction(create_venv venv_dir requirements_path)    # Check if the virtual environment already exists    if(EXISTS ${venv_dir})        message(STATUS "The virtual environment already exists at ${venv_dir}, skipping creation.")        return()    endif()    # Ensure the dependency file exists    if(NOT EXISTS ${requirements_path})        message(FATAL_ERROR "Dependency file not found: ${requirements_path}")    endif()    # Create the virtual environment    execute_process(        COMMAND ${Python3_EXECUTABLE} -m venv ${venv_dir}        RESULT_VARIABLE venv_creation_ret_code    )    # Check the creation result    if(venv_creation_ret_code)        message(FATAL_ERROR "Failed to create virtual environment at ${venv_dir}!")    endif()    # Install dependencies    execute_process(        COMMAND ${venv_dir}/bin/pip install -r ${requirements_path}        RESULT_VARIABLE pip_install_ret_code    )    # Check the installation result    if(pip_install_ret_code)        message(FATAL_ERROR "Failed to install dependencies from ${requirements_path}!")    endif()    # Output success message    message(STATUS "The virtual environment has been configured at ${venv_dir}, dependencies have been installed from ${requirements_path}")endfunction()# Include all subdirectories for buildingadd_subdirectory(app1)add_subdirectory(app2)add_subdirectory(app3)

It does a few things: 1. Finds the system Python interpreter; 2. Defines the create_venv function to implement the creation of the virtual environment and installation of dependencies; 3. Unifies the build process of each submodule.For the Python application module app1, the CMakeLists.txt is as follows:

# Specify the app1 virtual environment pathset(APP1_VENV ${CMAKE_BINARY_DIR}/app1_venv)# Create app1's virtual environmentcreate_venv(${APP1_VENV} ${CMAKE_SOURCE_DIR}/app1/requirements.txt)# Add a custom target to run app1add_custom_target(run_app1    COMMAND ${APP1_VENV}/bin/python ${CMAKE_SOURCE_DIR}/app1/main.py    DEPENDS ${APP1_VENV})

Another app2’s CMakeLists.txt is as follows:

# Specify the app2 virtual environment pathset(APP2_VENV ${CMAKE_BINARY_DIR}/app2_venv)# Create app2's virtual environmentcreate_venv(${APP2_VENV} ${CMAKE_SOURCE_DIR}/app2/requirements.txt)# Add a custom target to run app2add_custom_target(run_app2    COMMAND ${APP2_VENV}/bin/python ${CMAKE_SOURCE_DIR}/app2/main.py    DEPENDS ${APP2_VENV})

For the C++ module app3, it is as follows:

# Compile C++ code into an executableadd_executable(main main.cpp)# Add a custom target to run app3add_custom_target(run_app3    COMMAND ${CMAKE_BINARY_DIR}/app3/main    DEPENDS main)

Initializing the build environmentEnter the build directory and execute

cmake ..

At this point, CMake will automatically trigger the process of creating virtual environments, generating independent virtual environments for the two Python modules, and installing dependencies based on requirements.txt.

# Build all targetscmake --build .# Run applications (both Python and C++)make run_app1  # Run the first Python applicationmake run_app2  # Run the second Python applicationmake run_app3  # Run the C++ application

Additionally, in VS Code, through the CMake extension, you can directly see these three run targets in the bottom status bar of the compiler, and click the button to execute them without needing to remember command line arguments.If you prefer the efficient build of Ninja, you can also operate like this:

# Generate Ninja configurationcmake .. -G Ninja# Execute buildcmake --build .# Run applications with Ninja ninja run_app1ninja run_app2ninja run_app3

In summary, with the above method, CMake is no longer just a tool exclusive to C++, but a good choice for cross-language projects. So, when encountering such situations, consider trying this approach.

Leave a Comment