Voltar para todos os artigos
Dominando a Complexidade: Bibliotecas, Dependências e Compilação Cruzada com `cmake`

Dominando a Complexidade: Bibliotecas, Dependências e Compilação Cruzada com `cmake`

Um guia prático sobre como usar Modern CMake para gerenciar projetos com múltiplas bibliotecas, encontrar dependências externas e configurar compilação...

Pesquisa técnica projetada por humanos, sintetizada com assistência de personas de IA.
8 min de leitura

TL;DR / Sumário Executivo

Um guia prático sobre como usar Modern CMake para gerenciar projetos com múltiplas bibliotecas, encontrar dependências externas e configurar compilação...

Por Athena, Senior Software Enginner

💡 TL;DR (Resumo)

Este artigo avança do "Hello, World" para um projeto C realista com cmake, demonstrando como gerenciar múltiplos diretórios, criar e vincular bibliotecas e encontrar dependências externas (como a zlib). A chave é a filosofia do "Modern CMake" focada em targets. Em vez de manipular flags de compilador manualmente, você define targets (executáveis ou bibliotecas) e anexa propriedades a eles (ex: diretórios de include) com target_* commands. find_package localiza dependências externas de forma portável. Finalmente, mostramos como o cmake resolve elegantemente a compilação cruzada (cross-compiling) através de um arquivo toolchain, permitindo compilar para diferentes arquiteturas (como ARM) sem alterar uma única linha do seu CMakeLists.txt principal.


Na nossa última conversa, estabelecemos uma verdade fundamental: cmake nos convida a descrever o que é nosso projeto, em vez de ditar obsessivamente como ele deve ser construído. Deixamos para trás a filosofia imperativa do make para abraçar a clareza de um sistema declarativo.

Agora, vamos sair do "Hello, World" e entrar no mundo real. O mundo real é feito de componentes, de bibliotecas que interagem, de dependências externas e, para muitos de nós no universo embarcado e de sistemas, de múltiplas arquiteturas de hardware. É aqui, no gerenciamento dessa complexidade, que o cmake não apenas brilha, mas se torna indispensável.

Vamos modelar um projeto simples, porém comum: um pequeno sistema de monitoramento de energia. Ele terá um executável principal (app) que utiliza uma biblioteca (lib) para ler dados de um sensor.

Nossa estrutura de diretórios será:

power_monitor/
├── CMakeLists.txt         # O arquivo principal
├── app/
│   ├── CMakeLists.txt     # Para o executável
│   └── monitor.c
└── lib/
    ├── CMakeLists.txt     # Para a biblioteca
    ├── sensor.c
    └── sensor.h

No mundo do make, isso geralmente significa um Makefile complexo na raiz ou múltiplos Makefiles interligados com regras e variáveis confusas. Com cmake, a lógica é mais limpa. O CMakeLists.txt da raiz orquestra o projeto, e cada subdiretório descreve o que ele contém.

O CMakeLists.txt da raiz é simples:

cmake
# power_monitor/CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(PowerMonitor C) # Adiciona os subdiretórios ao build. # CMake irá procurar e processar os CMakeLists.txt dentro deles. add_subdirectory(lib) add_subdirectory(app)

Targets: A Moeda Corrente do "Modern CMake"

Este é o conceito mais importante que você precisa internalizar: no "Modern CMake", tudo gira em torno de targets. Um target é uma entidade lógica — seja um executável ou uma biblioteca. Você cria um target e depois anexa propriedades a ele.

Vamos definir nossa biblioteca de sensor.

cmake
# lib/CMakeLists.txt # Cria um target de biblioteca chamado "sensor" a partir dos seus fontes. # Pode ser STATIC (padrão) ou SHARED. add_library(sensor sensor.c)

Com uma única linha, criamos um target chamado sensor. O cmake cuidará de todas as regras para compilar sensor.c e criar o arquivo de biblioteca (libsensor.a ou sensor.so).

Agora, vamos definir nosso executável.

cmake
# app/CMakeLists.txt # Cria um target executável chamado "monitor" a partir do seu fonte. add_executable(monitor monitor.c) # Aqui está a mágica: vinculamos o executável ao target da biblioteca. target_link_libraries(monitor PRIVATE sensor)

A linha target_link_libraries é o coração da questão. Veja o que ela faz, que em um Makefile exigiria várias linhas e regras manuais:

  1. Garante a Ordem de Build: Sabe que o target sensor deve ser construído antes do target monitor.
  2. Automatiza a Vinculação: Adiciona automaticamente as flags corretas ao linker, como -L/caminho/para/a/lib -lsensor. Você nunca mais precisa escrever isso.
  3. Passa Propriedades (Veremos a seguir): Transfere informações, como diretórios de include, da biblioteca para o executável.

Gerenciando Propriedades: A Família target_*

Nossa biblioteca sensor tem um cabeçalho público, sensor.h. O executável monitor precisa incluí-lo. Como fazemos isso sem caminhos relativos frágeis como -I../lib? Atribuindo a propriedade de diretório de include ao target da biblioteca.

cmake
# lib/CMakeLists.txt add_library(sensor sensor.c) # Anexa o diretório de include ao target "sensor". target_include_directories(sensor PUBLIC # Variável que aponta para o diretório do CMakeLists.txt atual ${CMAKE_CURRENT_SOURCE_DIR} )

A palavra-chave PUBLIC é crucial. Ela significa:

  • O diretório de include é necessário para compilar a própria biblioteca sensor (PRIVATE).
  • O diretório de include também é necessário para qualquer coisa que se vincule a sensor (INTERFACE).

Como monitor se vincula a sensor via target_link_libraries, o cmake automaticamente propaga essa propriedade. O monitor agora sabe onde encontrar sensor.h sem que seu CMakeLists.txt precise ter qualquer conhecimento da estrutura de diretórios da biblioteca. Isso é encapsulamento de build. É poderoso e reduz drasticamente a complexidade em projetos grandes.

O Fim da Caça ao Tesouro: find_package()

Nossos projetos raramente vivem em uma ilha. Digamos que nosso monitor precise usar uma biblioteca externa popular, como a zlib para compressão de dados.

Com make, você estaria procurando onde a zlib está instalada, adicionando -I/usr/include e -lz ao seu Makefile, torcendo para que funcione em todos os sistemas.

Com cmake, nós pedimos:

cmake
# app/CMakeLists.txt add_executable(monitor monitor.c) # Encontre o pacote ZLIB. Se não encontrar, o CMake falhará com um erro. find_package(ZLIB REQUIRED) # Vinculamos tanto à nossa biblioteca interna 'sensor' quanto à externa 'ZLIB'. target_link_libraries(monitor PRIVATE sensor ZLIB::ZLIB)

O comando find_package(ZLIB REQUIRED) procura por arquivos de configuração que a zlib (e a maioria das bibliotecas bem-comportadas) instala no sistema. Esses arquivos informam ao cmake onde encontrar os cabeçalhos e as bibliotecas. O resultado é um "target importado" chamado ZLIB::ZLIB, que podemos usar para vincular de forma limpa e portável. A caça ao tesouro acabou.

A Solução Definitiva para Compilação Cruzada

Agora, a joia da coroa para nós, engenheiros de sistemas. Precisamos compilar nosso power_monitor para um Raspberry Pi (arquitetura arm-linux-gnueabihf).

No mundo do make, isso seria um pesadelo de variáveis e condicionais. Com cmake, nós descrevemos nosso toolchain (o conjunto de ferramentas do compilador cruzado) em um arquivo separado.

Crie um arquivo chamado raspberrypi.cmake:

cmake
# raspberrypi.cmake - Arquivo de Toolchain # O sistema de destino set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) # O caminho para o seu compilador cruzado set(TOOLCHAIN_PREFIX /path/to/your/toolchain/bin/arm-linux-gnueabihf) # Os compiladores set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++) # Onde procurar por bibliotecas e cabeçalhos do sistema de destino set(CMAKE_FIND_ROOT_PATH /path/to/your/target/sysroot) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Agora, para construir para o Raspberry Pi, o processo é o seguinte. Note que não mudamos uma única linha nos nossos arquivos CMakeLists.txt:

bash
# Crie um diretório de build separado para o alvo ARM mkdir build-rpi && cd build-rpi # Invoque o cmake, dizendo a ele para usar nosso arquivo de toolchain cmake .. -DCMAKE_TOOLCHAIN_FILE=../raspberrypi.cmake # Construa o projeto cmake --build .

Dentro do diretório build-rpi, você encontrará um executável monitor compilado para ARM. Para compilar para seu host x86 novamente, basta ir para o diretório de build original e executar o build. A definição do seu projeto permanece pura e agnóstica à arquitetura. O cmake abstrai completamente a complexidade do toolchain.

Conclusão da Segunda Parte

Hoje, saímos da teoria e resolvemos problemas do mundo real. Construímos um projeto com múltiplos diretórios, criamos e vinculamos uma biblioteca interna, gerenciamos suas propriedades de forma encapsulada, encontramos e vinculamos uma dependência externa de forma portável e, finalmente, compilamos o mesmo código para uma arquitetura completamente diferente sem alterar nossa lógica de build.

Cada uma dessas tarefas representa um ponto de fragilidade e complexidade em um Makefile. Com cmake, elas se tornam parte de um fluxo de trabalho declarativo e robusto, focado em targets e suas relações.

Na parte final de nossa série, iremos além. Exploraremos o ecossistema que o cmake oferece para automatizar todo o ciclo de vida do software: testes com CTest, empacotamento com CPack e a integração de ferramentas customizadas, solidificando seu papel como a espinha dorsal de qualquer projeto de engenharia de sistemas moderno.


Receba novos artigos

Cadastre-se para receber notificações sobre novos artigos direto no seu email

Não enviaremos spam. Você pode cancelar a inscrição a qualquer momento.