Projetos Maiores com CMake

[414] Projetos Maiores com CMake

Com o Make você escreve como compilar; com o CMake, descreve o que o projeto é e deixa a ferramenta gerar o build de cada plataforma. Daí saem o CMakeLists.txt de seis linhas, o diretório build separado que se apaga inteiro sem dó, e o critério para saber quando essa camada extra compensa.
Linguagem C

10 min de leitura

Na aula anterior, o Make automatizou nossa compilação com elegância. Mas há uma fronteira onde o próprio Make começa a doer: projetos grandes, com dezenas de arquivos, bibliotecas externas, e a necessidade de compilar em Windows, Linux e macOS ao mesmo tempo. Escrever Makefiles à mão para todos esses cenários vira um trabalho complexo e específico de cada plataforma. Hoje conhecemos o CMake, a ferramenta que resolve isso operando num nível mais alto: em vez de escrever como compilar, você descreve o que seu projeto é, e o CMake gera o sistema de build apropriado para cada plataforma. É o padrão moderno dominante em projetos C e C++ de porte sério.

A mudança de paradigma: gerar o build, não escrevê-lo

Aqui está a ideia central, e ela é diferente do Make. Com o Make, você escrevia diretamente as regras de compilação. Com o CMake, você escreve uma descrição do projeto num arquivo chamado CMakeLists.txt, e o CMake usa essa descrição para gerar os arquivos de build concretos — um Makefile no Linux, um projeto do Visual Studio no Windows, e assim por diante. Você descreve seu projeto uma vez, de forma portável, e o CMake se encarrega de traduzir isso para o sistema de build de cada plataforma. Essa indireção é o que dá ao CMake seu superpoder: portabilidade sem reescrever nada.

Um CMakeLists.txt mínimo

Vamos ver como fica a descrição do nosso projeto de exemplo (o main.c e mymath.c da aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos). O arquivo CMakeLists.txt:

# versão mínima do CMake exigida
cmake_minimum_required(VERSION 3.10)

# nome do projeto e linguagem
project(MeuProjeto C)

# cria um executável chamado 'programa' a partir dos fontes
add_executable(programa main.c mymath.c)

Compare com o Makefile da aula anterior. Não há regras de compilação, não há comandos gcc, não há gerenciamento de arquivos .o. Você apenas declara três coisas: a versão do CMake necessária, o nome e a linguagem do projeto, e que quer um executável chamado programa construído a partir de main.c e mymath.c. O CMake descobre sozinho como compilar cada arquivo, gerar os objetos e ligá-los — em qualquer plataforma. Essa concisão é resultado de operar num nível de abstração mais alto: você diz o "o quê", e o CMake resolve o "como".

O fluxo de trabalho: configurar e construir

Usar o CMake envolve dois passos, e uma convenção importante — o build fora da árvore de fontes (out-of-source build), em que os arquivos gerados ficam num diretório separado, mantendo seu código-fonte limpo:

# 1. cria um diretório separado para o build e entra nele
mkdir build
cd build

# 2. CONFIGURAR: o CMake lê o CMakeLists.txt e gera o sistema de build
cmake ..

# 3. CONSTRUIR: compila o projeto usando o build gerado
cmake --build .

O primeiro comando, cmake .., é a fase de configuração: o CMake lê o CMakeLists.txt (no diretório pai, ..), detecta seu compilador e sistema, e gera os arquivos de build (no Linux, tipicamente um Makefile) dentro do diretório build. O segundo, cmake --build ., é a fase de construção: ele invoca o sistema de build gerado para de fato compilar o programa. A vantagem do diretório build separado é que todos os arquivos gerados — objetos, executáveis, caches do CMake — ficam ali, longe do seu código; para recomeçar do zero, basta apagar a pasta build inteira, sem tocar nos fontes. Essa separação é uma prática que mantém projetos organizados.

Configurando flags e padrões

Assim como fizemos variáveis no Make, no CMake configuramos aspectos do build com comandos próprios. Podemos definir o padrão da linguagem C, adicionar flags de compilação, e mais:

cmake_minimum_required(VERSION 3.10)
project(MeuProjeto C)

# define o padrão C (por exemplo, C11) e o exige
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# adiciona flags de aviso a todas as compilações
add_compile_options(-Wall -Wextra)

add_executable(programa main.c mymath.c)

Aqui pedimos o padrão C11 e adicionamos os avisos -Wall -Wextra que recomendamos durante todo o curso. Note uma vantagem de portabilidade: set(CMAKE_C_STANDARD 11) funciona independentemente do compilador — o CMake traduz isso para a flag correta de cada um (-std=c11 no GCC/Clang, ou o equivalente em outros). Você especifica a intenção ("quero C11"), e o CMake cuida da sintaxe específica de cada ferramenta. Essa é a diferença essencial em relação ao Make, onde você escreveria a flag exata, presa a um compilador.

Organizando projetos com bibliotecas

Onde o CMake realmente brilha é em projetos estruturados, com o código separado em bibliotecas. Suponha que mymath seja uma biblioteca reutilizável, separada do programa principal:

cmake_minimum_required(VERSION 3.10)
project(MeuProjeto C)

set(CMAKE_C_STANDARD 11)

# cria uma biblioteca a partir de mymath.c
add_library(mymath mymath.c)

# cria o executável a partir de main.c
add_executable(programa main.c)

# liga o executável à biblioteca
target_link_libraries(programa mymath)

Aqui declaramos que mymath.c forma uma biblioteca (add_library), que main.c forma um executável (add_executable), e que o executável usa a biblioteca (target_link_libraries). O CMake entende essas relações e organiza a compilação e a ligação corretamente. Em projetos reais, com muitas bibliotecas internas e dependências entre elas, essa forma declarativa de expressar a estrutura é muito mais gerenciável do que Makefiles manuais. É assim que projetos grandes se organizam: módulos como bibliotecas, ligados nos executáveis que os consomem.

Make ou CMake: quando usar cada um

Vale ser claro sobre a escolha, para você não achar que um substitui o outro em todos os casos. O Make é direto, transparente e perfeito para projetos pequenos e médios de uma única plataforma — você vê exatamente os comandos que rodam, e um Makefile simples resolve bem. O CMake compensa sua camada extra de abstração quando o projeto cresce: múltiplas plataformas, bibliotecas externas, muitos módulos, ou quando você quer que outras pessoas construam o projeto facilmente em seus próprios ambientes. Para um exercício de duas ou três fontes, o Make (ou até o gcc direto) basta; para uma aplicação real que você pretende distribuir ou manter por anos, o CMake é o investimento certo. Ambos são ferramentas legítimas — conhecer os dois e escolher conforme a escala é a postura profissional. Não por acaso, o CMake se tornou praticamente o padrão de fato para projetos C e C++ de código aberto de maior porte.

O que vem a seguir

Hoje subimos um degrau na automação de build com o CMake: em vez de escrever comandos de compilação, descrevemos o projeto num CMakeLists.txt portável, e o CMake gera o sistema de build de cada plataforma — com o fluxo configurar/construir, o build fora da árvore de fontes, e a organização em bibliotecas e executáveis. Com Make e CMake, encerramos as ferramentas de build. Na próxima aula, voltamos ao próprio código para encarar de frente um tema que sobrevoou o curso inteiro como um aviso recorrente: o comportamento indefinido (undefined behavior). Vamos reunir e entender sistematicamente essas armadilhas do C — o que são, por que existem, e como evitá-las —, consolidando a consciência de risco que a linguagem exige.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Crie um CMakeLists.txt mínimo para o projeto de exemplo (main.c e mymath.c), com cmake_minimum_required, project e add_executable. Configure e construa o projeto usando o fluxo mkdir build, cd build, cmake .., cmake --build ..

Ver resposta

✓ Resposta:

cmake_minimum_required(VERSION 3.10)
project(MeuProjeto C)
add_executable(programa main.c mymath.c)

Fluxo de uso:

mkdir build && cd build
cmake ..            # configura: gera o Makefile
cmake --build .     # constrói: compila o programa
./programa          # executa

O CMake detecta o compilador, gera o build e compila os dois fontes num único executável, sem que você escreva nenhum comando gcc.

Exercício 2

Ao seu CMakeLists.txt, adicione a definição do padrão C11 (set(CMAKE_C_STANDARD 11)) e as flags de aviso -Wall -Wextra (com add_compile_options). Reconstrua e confirme que compila sem erros.

Ver resposta

✓ Resposta:

cmake_minimum_required(VERSION 3.10)
project(MeuProjeto C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_compile_options(-Wall -Wextra)
add_executable(programa main.c mymath.c)

Reconstruindo (de dentro de build, rode cmake .. de novo e depois cmake --build .), o projeto compila com o padrão C11 e os avisos ativados. O CMake traduz o pedido de C11 para a flag apropriada do seu compilador automaticamente.

Exercício 3

Reorganize seu CMakeLists.txt para tratar mymath como uma biblioteca separada, usando add_library, add_executable (só com main.c) e target_link_libraries. Confirme que o programa ainda funciona.

Ver resposta

✓ Resposta:

cmake_minimum_required(VERSION 3.10)
project(MeuProjeto C)
set(CMAKE_C_STANDARD 11)

add_library(mymath mymath.c)          # mymath vira uma biblioteca
add_executable(programa main.c)        # o executável usa só main.c
target_link_libraries(programa mymath) # liga a biblioteca ao executável

O programa funciona igual: o CMake compila mymath.c como biblioteca, main.c como executável, e liga os dois. A diferença é organizacional — mymath agora é um módulo reutilizável e explicitamente separado.

Exercício 4

Explique a diferença fundamental de abordagem entre o Make e o CMake. O que significa dizer que "o CMake gera o sistema de build" em vez de "ser o sistema de build"?

Ver resposta

✓ Resposta: A diferença fundamental é o nível de abstração. O Make é o sistema de build: você escreve nele as regras concretas de compilação (os comandos gcc, as dependências, a ordem), e o Make as executa diretamente. O CMake opera um nível acima: você descreve o projeto de forma abstrata e portável (quais executáveis e bibliotecas existem, de quais fontes, com quais padrões), e o CMake gera os arquivos de um sistema de build concreto — um Makefile no Linux, um projeto do Visual Studio no Windows, arquivos do Ninja, etc. — que então fazem a compilação de fato. Dizer que "o CMake gera o sistema de build" significa que ele não compila diretamente: ele produz os arquivos que outra ferramenta (o Make, o Visual Studio, o Ninja) usará para compilar. Essa indireção é justamente o que dá portabilidade: a mesma descrição de projeto (CMakeLists.txt) gera o build apropriado para cada plataforma e ferramenta, sem que você reescreva nada.

Exercício 5

Explique a vantagem do "build fora da árvore de fontes" (o diretório build separado). O que você ganha ao manter os arquivos gerados separados do código-fonte, e como isso facilita "recomeçar do zero"?

Ver resposta

✓ Resposta: O "build fora da árvore de fontes" mantém todos os arquivos gerados pela compilação — objetos, executáveis, caches e arquivos internos do CMake, Makefiles gerados — dentro de um diretório separado (build), longe do seu código-fonte. A principal vantagem é a limpeza e organização: seu diretório de fontes contém apenas o que você escreveu (os .c, .h, CMakeLists.txt), sem se misturar com o "lixo" da compilação, o que facilita navegar o projeto, versioná-lo (basta ignorar a pasta build no controle de versão) e enxergar o que é código genuíno versus artefato gerado. E isso torna trivial recomeçar do zero: se algo no build deu errado ou você quer uma reconstrução completamente limpa, basta apagar a pasta build inteira (rm -rf build) e recriá-la — sem risco algum de tocar ou apagar acidentalmente um arquivo de código-fonte, já que fontes e artefatos estão fisicamente separados. Num build "dentro da árvore", os arquivos gerados ficariam espalhados junto aos fontes, tornando a limpeza trabalhosa e arriscada.

Comentários

Mais em Linguagem C

Quando o Programa Decide: if, else e switch
Quando o Programa Decide: if, else e switch

Trocar == por = dentro de um if compila, roda e faz a coisa errada em…

Variáveis, Tipos e a Memória por Trás Deles
Variáveis, Tipos e a Memória por Trás Deles

Um int não tem 4 bytes garantidos: o padrão fixa apenas mínimos e uma ordem, e…

Capstone: Um Projeto Real do Zero e a Retrospectiva da Jornada
Capstone: Um Projeto Real do Zero e a Retrospectiva da Jornada

Um gerenciador de tarefas de linha de comando, do zero: struct e lista…