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
- CMake Documentation — a referência oficial — https://cmake.org/cmake/help/latest/
- Professional CMake: A Practical Guide, Craig Scott — o livro de referência sobre CMake moderno
- Modern CMake — guia comunitário de boas práticas — https://cliutils.gitlab.io/modern-cmake/
- CMake Tutorial oficial — passo a passo introdutório — https://cmake.org/cmake/help/latest/guide/tutorial/
- 21st Century C, Ben Klemens — panorama de ferramentas de build modernas
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.