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[Configure: CMake]
B --> C[Build dir: 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 и ваша психика начинают жить дружнее.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ