JavaRush /Курси /C++ SELF /Навіщо потрібен CMake: кросплатформне збирання

Навіщо потрібен CMake: кросплатформне збирання

C++ SELF
Рівень 31 , Лекція 0
Відкрита

1. Збирання стає окремим завданням

Коли ви пишете перші програми, здається, що збирання — це магія кнопки Run. Натиснули — і «воно працює». Але щойно проєкт виростає бодай трохи й перестає вміщатися в один файл, ця магія починає пахнути… трохи підгорілим пластиком. Зʼявляються заголовки, кілька .cpp, папки include/ і src/, і раптом ви витрачаєте час уже не на логіку програми, а на запитання: «Чому це не збирається на моєму ноутбуці, а в друга — збирається?»

Скажімо чесно: C++ не «ламається» — ламається наша наївна модель «проєкт = один файл». Насправді проєкт — це набір вихідних файлів, залежностей і правил збирання. І ці правила десь мають бути зафіксовані.

Уявіть, що ви печете хліб. Код — це інгредієнти. Компілятор — духовка. Але без рецепта — скільки борошна, скільки води, яка температура і скільки часу — результат дуже залежатиме і від настрою пекаря, і від умов довкола. CMake — саме такий рецепт: його можна прочитати і на кухні Windows, і на кухні Linux, і на кухні macOS.

У цей момент інколи виникає спокуса: «Гаразд, я просто запускатиму компілятор якось… вручну». Для одного файла це цілком реально. Але в багатофайловому проєкті дуже швидко стає виснажливо: доводиться памʼятати, які .cpp входять до програми, які include-шляхи потрібні, які прапорці використовувати і в якому порядку все це звʼязувати.

Якщо не заглиблюватися в командний рядок і конкретні ключі, збирання концептуально виглядає так:

main.cpp  -> main.o
task.cpp  -> task.o
io.cpp    -> io.o
(main.o + task.o + io.o) -> app.exe

Проблема не лише в тому, що це складно. Така схема ще й дуже крихка: додали parser.cpp, але забули включити його до збирання, — отримали «undefined reference»; перейменували папку include/, десь не оновили шляхи, — отримали «file not found»; змінили IDE — і все зламалося, бо налаштування були заховані в галочках.

CMake потрібен, щоб правила були не у вас у голові й не в «священних налаштуваннях IDE», а в репозиторії поруч із кодом.

2. Система збирання: компілятор, лінкер і «диригент»

Якщо компілятор і лінкер — це «музиканти», то системі збирання потрібен диригент: хтось має визначити, хто і коли грає, які партії потрібні та що робити, якщо в проєкті зʼявився ще один «скрипаль» — новий .cpp.

Тут важливо чітко розрізняти ці поняття.

Компілятор (наприклад, GCC/Clang/MSVC) уміє компілювати код C++. Лінкер — поєднувати обʼєктні файли й бібліотеки. Але жоден із них не зобовʼязаний розуміти структуру проєкту як цілісність: де лежать вихідні файли, які є залежності, які include-шляхи потрібні, які прапорці використовувати та які бібліотеки підʼєднувати.

Щоб усе це можна було відтворити, потрібен опис збирання.

Невелика схема, щоб зафіксувати ідею:

flowchart TD
    A["Ваш код: .cpp/.hpp"] --> B["Опис збирання: правила проєкту"]
    B --> C["Генератор збирання / проєкт IDE"]
    C --> D["Компілятор компілює .cpp"]
    D --> E["Лінкер звʼязує обʼєктні файли"]
    E --> F["Готовий виконуваний файл"]

Тут CMake займає місце «опису збирання» і частково «генератора»: він читає ваш рецепт і генерує те, з чим уміє працювати конкретне середовище.

Коли щось не збирається, новачкові часто хочеться узагальнити: «Компʼютер мене ненавидить». Але збирання — це конвеєр. Якщо він ламається, важливо зрозуміти, на якій саме ділянці.

Проблема в описі збирання

Це ситуація, коли правила збирання неправильні або неповні. Наприклад: «Ми забули сказати, що існує src/task.cpp». Компілятор може навіть не побачити цей файл, бо його просто не включили до збирання.

Помилка компіляції

Це ситуація, коли компілятор обробляє .cpp і не може його скомпілювати. Типові причини: синтаксична помилка, не знайдено заголовок, не збігаються типи або забуто ;.

Класичний приклад: include-шлях не налаштовано, і компілятор не знаходить наш заголовок:

#include "todo/task.hpp" // помилка: файл не знайдено

Помилка лінкування

Це ситуація, коли кожен .cpp окремо скомпілювався, але лінкер не зміг знайти визначення. Те саме «undefined reference», через яке новачкам іноді хочеться змінити професію й піти вирощувати полуницю. Хоча полуниця теж уміє вередувати.

Наприклад, якщо print_task оголосили, але її реалізацію не додали до збирання, лінкування завершиться помилкою.

І саме тут CMake стає важливим: він допомагає зробити так, щоб проєкт збирався однаково, а список файлів і правил існував не лише у вас у голові.

3. Кросплатформне збирання та переваги CMake

Слово «кросплатформний» інколи звучить як маркетинговий штамп, але для C++ це майже побутова необхідність. Люди працюють у різних операційних системах і з різними інструментами.

На Windows часто використовують Visual Studio і компілятор MSVC; на Linux — GCC/Clang і збирання через Make/Ninja; на macOS — Xcode і Clang. Навіть якщо код однаковий, спосіб його зібрати буде різним: інші формати проєктів, інші ключі, інші розширення, інші очікування.

Щоб відчути це на практиці, ось табличка — спрощена, але чесна за змістом:

Що ви хочете отримати Типовий інструмент Що реально «розуміє» інструмент
Visual Studio Solution Visual Studio / MSBuild
.sln, .vcxproj
Makefile‑збирання
make
Makefile
Ninja‑збирання
ninja
build.ninja
Xcode‑проєкт Xcode
.xcodeproj

І саме тут зʼявляється CMake як перекладач: ви один раз описуєте проєкт, а далі CMake вміє згенерувати потрібний формат для поточного середовища.

Ключова думка: CMake — не «ще один компілятор». Це інструмент, який робить збирання переносним і відтворюваним.

Важливо підсумувати це, не перетворюючи розмову на довідник команд: синтаксис буде далі. CMake дає три великі переваги, які відчутні навіть у невеликих проєктах.

Перше — єдине джерело правди про збирання. Проєкт описано в тексті: його можна читати, перевіряти під час ревʼю, змінювати й зберігати в Git. Це різко зменшує кількість «магічних збирань», які працюють лише на компʼютері автора.

Друге — кросплатформність. Ви описуєте проєкт один раз, а потім можете зібрати його в різних середовищах, бо CMake вміє «перекласти» цей опис у формат, який розуміє конкретна система збирання або IDE.

Третє — масштабованість. Коли проєкт зростає, ви додаєте нові частини — наприклад, бібліотеку з логікою, окремий застосунок, залежності та налаштування. Якщо від самого початку мислити проєктом як набором «цілей збирання» (targets), хаосу й болю буде значно менше.

4. CMake як опис проєкту на прикладі TodoApp

Зараз хочеться сказати: «Ну, CMake просто збирає». Але це хибна ментальна модель. Згодом саме вона заважає нормально налагоджувати проблеми зі збиранням.

Правильніше сказати так: CMake описує проєкт.

Тобто ви фіксуєте, з яких частин складається проєкт — програми, бібліотеки, — які вихідні файли до них входять, які include-шляхи їм потрібні, які макроси й прапорці компіляції застосовуються, які залежності потрібні під час лінкування.

І головне: цей опис має бути таким, щоб інша людина — або навіть ваш власний компʼютер за місяць — могла відтворити збирання без шаманства в дусі «постав галочку в IDE, потім ще одну, а потім перезапусти й помолися».

Щоб це не звучало надто абстрактно, привʼяжімо цю ідею до нашого навчального застосунку.

Уявімо, що ми поступово розвиваємо простий консольний застосунок: TODO‑список. Нічого «космічного»: додати завдання, вивести список. Ми беремо цей приклад не тому, що мріємо конкурувати з Notion, а тому, що такий проєкт природно стає багатофайловим.

Нехай структура проєкту така:

TodoApp/
  include/
    todo/
      task.hpp
      io.hpp
  src/
    main.cpp
    task.cpp
    io.cpp

У нас є модель Task і функції виведення.

Приклад заголовка моделі:

// include/todo/task.hpp
#pragma once
#include <string>

namespace todo {
struct Task {
    int id = 0;
    std::string text;
    bool done = false;
};
} // namespace todo

І, наприклад, проста функція виведення завдання (реалізація — у .cpp):

// src/task.cpp
#include "todo/task.hpp"
#include <iostream>

namespace todo {
void print_task(const Task& t) {
    std::cout << t.id << ": " << t.text << '\n'; // 1: Купити молоко
}
} // namespace todo

А main.cpp викликає це:

// src/main.cpp
#include "todo/task.hpp"
#include <vector>

int main() {
    std::vector<todo::Task> tasks = { {1, "Купити молоко", false} };
    // todo::print_task(tasks[0]);  // (поки не підʼєднали оголошення)
}

Навіть на цьому етапі багато новачків уже вловлюють першу важливу думку: щоб викликати print_task, треба, по‑перше, мати оголошення в заголовку, а по‑друге — реалізацію в .cpp у складі збирання.

І саме на цьому кроці стає видно, чому «опис проєкту» такий важливий.

Target як «одиниця збирання»

Ми ще не заглиблюємося в команди CMake — це наступний крок. Але одну ідею дуже корисно зафіксувати вже сьогодні.

У CMake центральне поняття — target, тобто ціль збирання. По суті, це те, що ми хочемо отримати: застосунок або бібліотеку.

Target — це не просто назва майбутнього результату збирання. Це ще й набір властивостей, які визначають, з яких вихідних файлів збирати, де шукати заголовки, які вимоги до стандарту мови має проєкт, які залежності потрібні для лінкування тощо.

Якщо сказати простіше, target — це як «страва в меню», у якої є рецепт та інгредієнти. А CMake‑проєкт — це ціла кухня, де може бути кілька страв.

5. Типові помилки

Помилка № 1: думати, що CMake «компілює код».
Це дуже поширена й цілком зрозуміла помилка: в IDE ви натиснули кнопку — і все ніби сталося само собою. Але правильна картина така: CMake описує, як збирати проєкт, а компілятор і лінкер виконують реальну роботу. Якщо плутати ці рівні, ви почнете лагодити не те: наприклад, намагатиметеся виправити C++‑код, хоча насправді просто не додали .cpp до збирання.

Помилка № 2: сподіватися, що система збирання «сама знайде всі файли».
Новачки часто думають: «Ну, раз файл лежить у папці src/, він має збиратися». Ні. За замовчуванням будь-яка система збирання працює лише з тим, що їй явно вказали. Якщо новий .cpp не включено в правила збирання, він просто не братиме участі в процесі, а ви отримаєте помилки лінкування або здивовано питатимете себе: «Чому мій код не виконується?»

Помилка № 3: плутати «підʼєднав заголовок» і «додав реалізацію».
#include "todo/task.hpp" лише робить оголошення видимим для компілятора. Але лінкеру все одно потрібні визначення. Якщо реалізація функції лежить у task.cpp, цей файл має брати участь у збиранні — безпосередньо або через бібліотеку. Це різні рівні, і CMake допомагає тримати їх окремо, щоб не плутати.

Помилка № 4: намагатися лагодити збирання галочками в IDE, не фіксуючи правила в проєкті.
Так, інколи така «галочка» рятує просто зараз. Але за тиждень ви забудете, яка саме галочка була важливою, колега не зможе це повторити, і проєкт працюватиме «лише на одному компʼютері». Саме тому CMake цінний: правила збирання описані явно й придатні до перенесення.

Помилка № 5: звалювати все в одну купу, не думаючи про структуру.
Коли проєкт зростає, дуже хочеться «швидше зробити, аби збиралося», і тоді зʼявляються рішення на кшталт «додаймо в include-шляхи весь диск C:». На короткій дистанції це може зменшити кількість помилок «file not found», але в довгій перспективі перетворює проєкт на загадку. Ідея CMake як «опису проєкту» якраз про протилежне: збирання має бути прозорим і мінімально достатнім, а не магічним і надмірним.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ