1. Out-of-source build: ідея та суть
Якщо ви лише нещодавно почали програмувати, легко подумати так: «У мене є main.cpp і CMakeLists.txt. Я натиснув Run — значить, десь зʼявиться програма. Яка різниця, де?» І тут починається магія реального світу: збирання — це процес, який породжує багато проміжних файлів. Їх теж треба десь зберігати, інакше проєкт перетвориться на смітник.
Out-of-source build — це підхід, за якого ви ніколи не генеруєте файли збирання в папці з вихідними кодами. Вихідні коди залишаються чистими: src/, include/, CMakeLists.txt — і все. А все, що створює система збирання, як-от тимчасові файли, обʼєктні файли, кеші, файли проєкту для конкретної IDE чи генератора, потрапляє в окрему папку — зазвичай build/ або до кількох папок на кшталт build-debug/, build-release/.
Щоб було простіше запамʼятати:
Вихідні коди — це текст, який ви пишете. Build‑папка — це «кухня», де з цього тексту отримують виконуваний файл.
На кухні неминуче буде безлад: борошно, ножі, посуд. Але це не привід зберігати все це у спальні.
2. Що живе в build‑папці і чому цьому не місце поруч із кодом
Якщо ви досі збирали проєкти лише у Web‑IDE, то могли взагалі не бачити «нутрощів» збирання. Локальна IDE і CMake значно чесніші: вони не приховують, що збирання — це цілий світ артефактів. І цей світ дуже швидко розростається.
У типовій build‑папці можна знайти обʼєктні файли, тобто результат компіляції окремих .cpp, файли системи збирання, наприклад для Ninja або Make, тимчасові каталоги CMake, різні службові конфігурації, кеш налаштувань і результати «пошуку середовища». Тобто інформацію про те, який компілятор використовується, які шляхи знайдено і які бібліотеки доступні. Навіть якщо ви про це не просили, системі це потрібно, щоб другий запуск збирання був швидшим і щоб не ставити одні й ті самі запитання знову й знову.
Є навіть кумедний факт зі світу «дуже серйозного C++»: навіть під час підготовки чернеток стандарту C++ окремо дякують тим, хто поліпшував build‑процес та інфраструктуру збирання. Настільки це важливо й нетривіально.
У межах нашого навчального міні‑проєкту, нехай він називається Tasker, — простенької консольної утиліти, яка поки лише друкує привітання, — вихідні коди можуть виглядати так:
tasker/
CMakeLists.txt
src/
main.cpp
include/
tasker_version.hpp
А build‑папка — приблизно ось так:
tasker/
build/
CMakeCache.txt
CMakeFiles/
build.ninja (або Makefile)
tasker (виконуваний файл)
...
Ключова думка: усе, що зʼявляється в build/, не є вихідним кодом. Це або проміжний результат, або «памʼять» системи збирання. Якщо тримати все це поруч із вихідними кодами, ви лише ускладнюєте собі життя: важче орієнтуватися, важче чистити проєкт, важче пояснювати його структуру іншій людині й підтримувати кілька конфігурацій.
3. Чому in-source build — майже завжди погана ідея
Початківці часто роблять так: створюють папку проєкту, відкривають термінал у корені, запускають CMake «як вийде», і раптом поруч із CMakeLists.txt зʼявляються якісь CMakeCache.txt, CMakeFiles/, а потім ще десятки файлів. Це і є in-source build.
Проблема не в тому, що «так не можна» — технічно можна. Проблема в тому, що це породжує буденні, але дуже неприємні труднощі. У проєкті стає складно відрізнити «мій код» від «сміття збирання». Коли ви робите рефакторинг, переміщуєте файли, пишете інструкції для команди або просто відкриваєте проєкт через рік, мозок витрачає час на наведення ладу замість роботи.
Ще одна неприємність — конфлікти імен і шляхів. Система збирання може генерувати файли з іменами, які випадково збігатимуться з вашими. Або ви можете ненароком додати до репозиторію те, чого там бути не повинно, наприклад кеші й артефакти. У підсумку у вас на GitHub лежить не проєкт, а археологічний шар зі «слідів збирання».
І найприкріше: in-source build робить «швидке скидання стану» дуже незручним. Коли збирання зламалося через дивний стан, найнадійніший спосіб — видалити build‑папку і створити її заново. Але якщо ви збирали в корені проєкту, «видалити build‑папку» означає «видалити половину файлів у корені й молитися, щоб не стерти вихідні коди».
4. Модель «вихідні коди окремо, збирання окремо»
На цьому етапі важливо не потонути в термінах і не перетворити просту ідею на «релігію CMake». Достатньо одного простого правила: у корені проєкту зберігати лише те, що ви редагуєте вручну, а все згенероване — виносити назовні.
Для нашого Tasker це означає ось що: папку tasker/ ми вважаємо «чистою зоною». Там лежать src/, include/, CMakeLists.txt. І все. А от build/ — це «брудна зона», де CMake і компілятор можуть робити що завгодно, і ми їх за це не засуджуємо.
Зазвичай out-of-source збирання зручно уявляти як дві паралельні «гілки»:
(вихідні коди) tasker/ (артефакти) tasker/build/
CMakeLists.txt CMakeCache.txt
src/main.cpp CMakeFiles/...
include/tasker_version.hpp tasker (exe)
Якщо ви любите схеми (я люблю, бо схеми не сперечаються), це можна намалювати так:
flowchart LR
A[Вихідні коди: src/, include/, CMakeLists.txt] --> B[Конфігурація: CMake]
B --> C[Каталог збирання: build/]
C --> D[Компіляція: .o/.obj]
D --> E[Лінкування]
E --> F[Виконуваний файл]
Суть схеми проста: етап configure «привʼязує» ваш проєкт до конкретної build‑папки, а далі все збирання живе там. Вихідні коди при цьому залишаються незмінними.
5. Практика на Tasker: чистий корінь і кілька build‑папок
Міні‑демонстрація: вихідні коди Tasker і чистий корінь
Зараз ми не намагаємося написати «суперзастосунок». Наша мета — щоб проєкт був достатньо реальним, аби збирання породжувало артефакти, але водночас достатньо простим, щоб ви не боялися відкрити будь-який файл.
Нехай src/main.cpp виглядає так:
#include <iostream>
#include "tasker_version.hpp"
int main() {
std::cout << "Tasker v" << TASKER_VERSION << '\n'; // Tasker v0.1
}
А заголовок include/tasker_version.hpp — такий:
#pragma once
#define TASKER_VERSION "0.1"
Так, це «майже нічого не робить». І це чудово: зараз ми тренуємо не алгоритми, а організацію збирання.
Тепер головний момент: хоч би як ви не збирали цей проєкт — в IDE, через Ninja чи Make, — якщо ви дотримуєтеся out-of-source підходу, корінь проєкту після збирання має залишатися таким само охайним:
tasker/
CMakeLists.txt
src/
include/
build/ (і от тут уже починається “внутрішня кухня”)
Так ви буквально купуєте собі зручність: завжди можете відкрити tasker/ і не бачити тисячі файлів, яких особисто не створювали.
Навіщо мати кілька build‑папок, навіть якщо проєкт маленький
Коли ви звикаєте тримати build окремо, майже одразу зʼявляється ще одна корисна звичка: різні сценарії збирання — різні build‑папки. І навіть якщо ви поки лише приблизно розумієте, чим Debug відрізняється від Release, сама ідея «дві різні збірки не мають наступати одна одній на ноги» дуже практична.
Уявіть, що ви хочете зібрати проєкт одним компілятором, а потім іншим. Або в одній IDE, а потім в іншій. Або з різними прапорцями. Будь-яка спроба «переконфігурувати» одну й ту саму build‑папку туди-сюди часто закінчується тим, що вона зберігає сліди старих налаштувань, і ви вже не впевнені, що саме зараз увімкнено.
Якщо ж ви створюєте дві папки, то отримуєте ясність уже на рівні файлової системи:
tasker/
build-debug/
build-release/
Тут навіть початківець, який нічого не знає про оптимізації, уже розуміє: «Це дві різні збірки». І якщо «зламалася release», ви не чіпаєте debug. Це знімає величезний клас випадкових проблем.
6. Гігієна build‑папки: стан, видалення і назви
Build‑папка — це стан, і її нормально видаляти
Є один психологічний барʼєр: початківцям страшно видаляти файли, бо «раптом видалю щось важливе». І це правильно: у папці з вихідними кодами видаляти страшно, і робити це «на автоматі» не можна.
Але build‑папка — це інше. Її спеціально задумано так, щоб ви будь-якої миті могли сказати: «Збирання стало дивним. Я хочу повернутися в чистий стан», видалити build‑каталог і створити його заново.
Чому це працює? Бо build‑папка — це похідна від вихідних кодів і налаштувань. Якщо вихідні коди у вас на місці, ви завжди зможете побудувати build‑папку заново. Це як папка bin/ або out/ в інших мовах: вона цінна як результат, але не як унікальний артефакт.
На практиці це перетворюється на дуже просте правило мислення:
Якщо ви не розумієте, чому збирання “поводиться дивно”, і підозрюєте, що проблема в накопиченому стані,
то створити build‑папку заново часто швидше, ніж лікувати її вручну.
Важливо: це не «милиця», а нормальний робочий інструмент. Системи збирання складні. У них є кеші. Іноді вони допомагають, іноді заважають. І out-of-source якраз дає вам кнопку «скинути весь стан» без ризику знести вихідні коди.
Де тримати build‑папку і як її називати
Коли ви робите out-of-source збирання, наступне практичне запитання звучить так: «Гаразд, папка окрема — а де вона має бути?» У навчальних проєктах найпростіше тримати build‑папку просто поруч із вихідними кодами, у корені проєкту: так її легко знайти й легко видалити.
Часто використовують імена build/, cmake-build-debug/ (деякі IDE так роблять автоматично), build-debug/, build-release/. Сенс не в назві, а в тому, щоб вона була очевидною та послідовною. Коли ви відкриваєте проєкт через місяць, маєте миттєво зрозуміти: «Ага, ось вихідні коди, а ось каталоги збирання».
Ще один важливий практичний момент: build‑папка зазвичай не має потрапляти до репозиторію. І не тому, що «так заведено», а тому, що build‑артефакти залежать від вашої машини, компілятора, ОС і налаштувань. Двоє людей, які зібрали один і той самий проєкт, отримають різні файли в build‑папці — і це нормально. Якщо додавати їх у коміти, репозиторій швидко перетвориться на поле битви.
7. Типові помилки під час out-of-source збирання
Зараз буде розділ, який заощадить більше часу, ніж будь-які «розумні поради». Бо більшість проблем із CMake у початківців — це не складні прапорці й не «магія лінкувальника», а банальна плутанина щодо того, де ви збираєте проєкт і яка папка містить актуальний стан збирання.
Помилка № 1: зібрати проєкт у корені, а потім дивуватися, «звідки взялися ці файли».
Коли ви запускаєте конфігурацію в папці вихідних кодів, CMake починає створювати там службові файли. Потім ви шукаєте main.cpp, а знаходите поруч CMakeCache.txt і папку CMakeFiles/. Вирішується це просто: домовитися із собою, що корінь проєкту — чиста зона, і збирання туди не пише.
Помилка № 2: вважати build‑папку частиною вихідних кодів і боятися її видалити.
Якщо ви ставитеся до build‑каталогу як до «крихкої речі, яку не можна чіпати», ви позбавляєте себе найкориснішого інструмента: скидання стану. З out-of-source ви якраз отримуєте безпечну можливість повністю видалити build‑каталог, коли збирання стало підозрілим, і зібрати все з нуля.
Помилка № 3: тримати одну build‑папку для «всього на світі» й постійно її перевикористовувати.
Сьогодні ви зібрали проєкт з одними налаштуваннями, завтра — з іншими, післязавтра IDE перемкнула генератор, і в підсумку build‑папка зберігає сліди всього. Симптоми зазвичай майже містичні: «я змінив файл, а воно не змінилося» або «у мене працює, у друга — ні». Значно спокійніше розводити різні сценарії збирання по різних build‑папках.
Помилка № 4: намагатися вручну «підчистити» build‑папку вибірково.
Іноді хочеться видалити «ось цей один дивний файл» і сподіватися, що все виправиться. Це як ремонтувати холодильник, трясучи його за ручку: іноді допомагає, але ви не розумієте чому. Якщо build‑стан зламався, найчесніший спосіб — видалити папку цілком. Out-of-source якраз робить це безпечним.
Помилка № 5: випадково додати build‑папку до репозиторію й потім воювати з дифами.
Навіть якщо ви працюєте самі, репозиторій із build‑артефактами починає «шуміти»: гігабайти сміття, незрозумілі зміни, конфлікти. У нормальному проєкті build‑каталоги живуть окремо й вважаються такими, які завжди можна створити заново. Щойно ви це приймаєте, Git і ваша психіка починають жити дружніше.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ