Integrando VSCode, Mingw-w64 y CMake (Parte V)

Artículos de la serie:

Ejemplo de árbol de directorios gestionado por CMake


En esta continuación de la serie de artículos dedicada a la integración de VSCode, Mingw-w64 y CMake en MS Windows, analizaremos un nuevo ejemplo de construcción de proyectos mediante CMake moderno.

Como sabemos, CMake emplea ficheros de configuración CMakeLists.txt para producir archivos de construcción específicos que puedan ser utilizados por la herramienta de construcción nativa de nuestra plataforma (Make o Ninja, por ejemplo).

En un post anterior contemplábamos un caso de construcción extremadamente sencillo, consistente en la mera compilación de un único fichero fuente .cpp con dependencias a bibliotecas externas. Como segundo ejemplo, consideraremos ahora el diseño de un proyecto que involucre la codificación de varias bibliotecas internas y un ejecutable. Nuestra estructura de directorios tomará la forma siguiente (véase también la figura superior):

   +-- project
   |  +-- CMakeLists.txt
   |  +-- lib_1
   |  |  +-- CMakeLists.txt
   |  |  +-- inc
   |  |  |  +-- lib_1
   |  |  |  |  +-- *.hpp
   |  |  +-- src
   |  |  |  +-- *.cpp
   |  +-- lib_2
   |  |  +-- CMakeLists.txt
   |  |  +-- inc
   |  |  |  +-- lib_2
   |  |  |  |  +-- *.hpp
   |  |  +-- src
   |  |  |  +-- *.cpp
   | ...
   |  +-- app
   |  |  +-- CMakeLists.txt
   |  |  +-- app.cpp

En una primera aproximación al problema, asumiremos la inexistencia de interdependencias entre las propias bibliotecas, punto éste que discutiremos al final del artículo.


Ficheros CMakeLists.txt para las bibliotecas internas


La estructura típica del fichero de configuración CMakeLists.txt para la biblioteca i-ésima lib_i será el siguiente:

   # project/lib_i/CMakeLists.txt    add_library(lib_i       inc/lib_i/hdr_1.hpp       src/impl_1.cpp       # ...    )    target_include_directories(lib_i PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>)

El comando add_library especifica los ficheros de cabecera (.hpp) de la biblioteca y sus códigos de implementación asociados (.cpp), a incluir en los subdirectorios inc/lib_i/ y src/, respectivamente. Observemos la separación estricta entre ambos tipos de archivos.

En este caso, el tipo de la biblioteca será STATIC o SHARED en función de si el valor de la variable BUILD_SHARED_LIBS es OFF (por defecto) u ON, respectivamente.

El comando target_include_directories hace pública la dirección de ficheros de cabecera a los consumidores de la biblioteca. La variable CMAKE_CURRENT_SOURCE_DIR, en particular, contiene la path del directorio al que pertenece este fichero CMakeLists.txt.


Fichero CMakeLists.txt para el ejecutable


Por su parte, el fichero de configuración que situaremos en el directorio que contiene la aplicación app será:

   # project/app/CMakeLists.txt    add_executable(app_name app.cpp)    target_link_libraries(app_name PRIVATE       lib_1       lib_2       # ...    )

Aquí, el comando target_link_libraries enlaza al target con las bibliotecas internas de las que depende.


Fichero raíz CMakeLists.txt


El fichero de configuración del proyecto, localizado en la raíz de su árbol de directorios, se reducirá a una mera inclusión de subdirectorios:

   # project/CMakeLists.txt    cmake_minimum_required(VERSION 3.17)    project(project_name VERSION 0.1.0 LANGUAGES CXX)    set(CMAKE_CXX_STANDARD 17)    set(CMAKE_CXX_EXTENSIONS OFF)    add_subdirectory(lib_1)    add_subdirectory(lib_2)    # ...    add_subdirectory(app)

Observemos, eso sí, que con el fin de simplificar la gestión del proyecto hemos definido de forma global el estándar del lenguaje a emplear tanto por las bibliotecas como por la aplicación (en este caso, C++17).


Ejercicio práctico


A modo de ejemplo ilustrativo, consideremos un proyecto con una única biblioteca interna de nombre text que proporciona una función para la subdivisión (split) de un string en tokens de acuerdo a una cadena de delimitadores proporcionada por el usuario:

   // project/text/inc/text/split.hpp    #include <string>    #include <string_view>    #include <vector>    namespace text {       [[nodiscard]] auto split(std::string_view text,                                std::string_view delims) -> std::vector<std::string>;    }

   // project/text/src/split.cpp    #include <text/split.hpp>    namespace text {       [[nodiscard]] auto split(std::string_view text,                                std::string_view delims) -> std::vector<std::string>       {          using sz_t = std::string_view::size_type;          auto tokens = std::vector<std::string>{};          auto first = sz_t{};          while (first < text.length()) {             auto const last = text.find_first_of(delims, first);             if (first != last)                tokens.emplace_back(text.substr(first, last - first));             if (last == std::string::npos)                break;             first = last + 1;          }          return tokens;       }    }

Podemos comprobar la correcta configuración de nuestro proyecto con la siguiente función principal:

   // project/app/app.cpp    #include <iostream>    #include <text/split.hpp>    auto main() -> int    {       for (std::string_view token : text::split("I am Spartacus!"" !"))          std::cout << token << ',';    }

Ello produciría como ouput la secuencia: I,am,Spartacus,.

Resulta sencillo adaptar el esquema general explicado anteriormente con el fin de gestionar la construcción de este proyecto de ejemplo a partir de tres sencillos ficheros CMakeLists.txt, punto éste que dejaremos como ejercicio al lector.


Dependencias entre bibliotecas internas


En caso de que la biblioteca lib_i haga uso de una o más bibliotecas lib_j, bastará añadir dicha dependencia en el fichero CMakeLists.txt asociado a la primera de ellas con un comando similar al siguiente:

   target_link_libraries(lib_i PRIVATE lib_j)

En efecto, target_link_libraries agregará automáticamente una dependencia en el build system para asegurarse de que lib_j esté actualizada antes de ser enlazada con lib_i.


Referencias bibliográficas

  • CMake Documentation (latest release): https://cmake.org/cmake/help/latest/
  • How to CMake Good: https://youtu.be/_yFPO1ofyF0?list=PLK6MXr8gasrGmIiSuVQXpfFuE1uPT615s
  • Henry Schreiner - An Introduction to Modern CMake: https://cliutils.gitlab.io/modern-cmake/

No hay comentarios:

Publicar un comentario