Когда много лет пишешь на C++ и регулярно работаешь с множеством небольших проектов, рано или поздно устаешь от рутины. Каждый раз — новый make-файл, новые зависимости и ошибки в длинных строках компилятора. В какой-то момент я поймал себя на мысли: «А почему бы не сделать сборку проще?».
Меня зовут Сергей Струков, я старший инженер-программист в YADRO. В телекоме я уже больше тридцати лет, а мой основной язык программирования — C++. В этой статье я расскажу о моем пет-проекте — build-системе LightForge. Если вы делаете небольшие библиотеки, утилиты или учебные проекты, то, возможно, LightForge станет для вас удобным инструментом.
Зачем вообще нужны build-системы
Когда я только начинал работать с C++, большинство проектов собирались вручную с помощью простых скриптов или команд в терминале. Тогда это казалось естественным: исходников немного, зависимости понятны, компилятор выдает результат быстро. Но с ростом проектов и их сложности ручная сборка перестала быть жизнеспособной.
Как собирается проект на C++
Если вы хоть раз пытались собрать крупное приложение на C++, вы знаете, насколько это многослойный процесс. Есть множество исходных файлов — обычно с расширением .cpp. Каждый из них, как правило, включает в себя заголовочные файлы (.h или .hpp).
Далее мы запускаем компилятор, который из каждого исходного файла создает объектный файл (.o или .obj). После компиляции исходников мы получаем набор таких объектных файлов.

Чтобы получить готовое приложение, используется специальная программа — линкер, или редактор связей. Линкер объединяет объектные файлы и формирует исполняемый файл (.exe, .out и т. п.).

Иногда применяется более сложная схема: из объектных файлов предварительно собирают библиотеки. Затем при линковке конечного приложения линкер получает набор библиотек и объектных файлов, объединяя только модули, на которые есть ссылки. Это позволяет уменьшить размер итогового файла и ускорить сборку.
На этом уровне становится понятна ценность модульности: один проект может зависеть от другого, а библиотеки могут быть как внутренними, так и внешними.

Каждый из исполняемых файлов нужно скомпилировать, получить объектный файл, а затем связать в один исполняемый модуль или библиотеку. Казалось бы, дело техники, но есть нюанс:
командные строки компиляции для каждого файла длинные, и в них легко ошибиться;
зависимости между исходниками и заголовками сложно отследить вручную;
повторная сборка проекта «с нуля» после любого изменения занимает слишком много времени;
линковка и работа с внешними библиотеками требуют точности в настройках.
Пример командной строки:
g++ -c -std=c++20 -fwrapv -fconcepts-diagnostics-depth=3 -O3 -march=native -Wall -Wextra -Wno-non-virtual-dtor -Wno-switch -Wno-type-limits -Wno-enum-compare -Wno-missing-field-initializers -Wno-delete-non-virtual-dtor -Wno-misleading-indentation -Wno-inaccessible-base -Wno-array-bounds -I/usr/include/freetype2 @CC-opt.txt ../../../../../C++/CCore-5-xx/Target/../Simple/CCore/src/Crc.cpp -o obj/Crc.o
Поэтому на каком-то этапе становится очевидно: без системы, которая умеет автоматически понимать, что нужно пересобрать, и сама выстраивает цепочку действий, далеко не уедешь. Именно для этого существуют build-системы.
Самая первая и по-своему легендарная — Make
Эта build-система появилась еще в мире Unix и до сих пор используется повсеместно. Make была и остается простым и эффективным инструментом, который выполняет одну главную задачу: следит за зависимостями между файлами. Если вы изменили один исходник, Make пересоберет только его и зависимости. Это экономит часы.
Однако у Make есть и серьезные недостатки. Ее язык описания сборки — это, по сути, смесь примитивных конструкций и макросов, в которых легко запутаться.
Пример make-файла:
. . .$(OBJ_PATH)/CmdInput.o :../../../../../C++/CCore-5-xx/Target/../Applied/CCore/src/CmdInput.cpp CC-opt.txt $(CC) $(CCOPT) @CC-opt.txt $< -o $@$(ASM_PATH)/CmdInput.s : ../../../../../C++/CCore-5-xx/Target/../Applied/CCore/src/CmdInput.cpp CC-opt.txt $(CC) -S $(CCOPT) @CC-opt.txt $< -o $@. . .
Настоящие make-файлы, особенно в реальных проектах, достигают сотен и тысяч строк. Добавьте сюда отсутствие модульности и поддержки современных практик — и вы получите инструмент, который прост только на бумаге. Тем не менее, Make до сих пор жив и входит в арсенал почти каждого разработчика C++.
Продвинутые решения — CMake и Bazel
CMake стал своего рода промышленным стандартом: он умеет генерировать проекты для разных IDE и платформ, работать с внешними зависимостями и библиотеками. Но с опытом я убедился, что его язык описания сборки — слишком своеобразный. Он вроде бы функциональный, но при этом неуклюжий. Когда в проекте несколько сотен модулей, отладка CMake-скриптов становится мучением.
Bazel, напротив, я считаю действительно мощной системой. Она создана для больших корпоративных проектов, где важно масштабирование, параллельная сборка и централизованные build-серверы. Bazel стабилен, гибок, у него хороший внутренний язык и богатая экосистема. Но для небольших команд и учебных задач это слишком тяжелый инструмент — его настройка требует времени и инфраструктуры.
В этом контексте и возникла идея LightForge. Я захотел разработать систему, которая бы заняла место между Make и Bazel: простую, легкую, но достаточно умную, чтобы брать на себя всю рутину сборки. Не требующую сложных конфигураций, но при этом поддерживающую модульность и возможность генерации кода.
Мне хотелось получить инструмент, который понимает потребности обычного разработчика на C++ и не заставляет его писать километры скриптов. И если Make — это минимальный уровень автоматизации, а Bazel — корпоративный уровень, то LightForge — это инструмент разработчика, которому важно писать код, а не возиться со сборкой.
Задача build-системы — не только автоматизировать компиляцию, но и управлять зависимостями, отслеживать изменения и минимизировать лишнюю работу. Когда build-система хорошо настроена, она компилирует только то, что действительно изменилось, а не весь проект целиком. Это экономит часы, а иногда и дни.
Пример реального проекта
Рассмотрим пример достаточно крупного проекта, включающего несколько тысяч файлов. Для сборки такого проекта с помощью обычной утилиты make требуется огромный Makefile. Вот лишь небольшой его фрагмент — в нем около 7 000 строк описания проекта:

Каждая строка содержит информацию о том, как именно должен собираться каждый объектный файл и из каких исходников он формируется. Теоретически такой файл можно написать вручную, но на практике это почти невозможно: ошибки неизбежны, а объем работы — колоссальный.
Если заглянуть дальше, можно увидеть полный граф зависимостей между исходными и объектными файлами:

Он содержит порядка 46 000 строк. Поддерживать такую структуру вручную нереально. Поэтому подобные задачи решаются с помощью автоматизированных систем сборки, таких как LightForge, а также более тяжелых инструментов — CMake и Bazel, которые под капотом выполняют аналогичные операции.
Как работает LightForge
В основе LightForge лежит очень простая концепция. Есть проект — набор исходных файлов, библиотек и целей, которые нужно собрать. Есть цели (targets) — конкретные артефакты, которые система должна создать:
exe— исполняемые файлы;lib— библиотеки;pregen— вспомогательные программы для генерации кода.
Проекты могут зависеть друг от друга, а цели — одна от другой.

Например, если библиотека нужна для сборки приложения, то LightForge поймет, что ее нужно собрать первой. При этом циклические зависимости между проектами запрещены — как и должно быть, иначе вы просто не соберете систему.
Дисклеймер: LightForge не предназначен для сложных, масштабных корпоративных проектов. Система изначально разрабатывалась для небольших и средних решений и не обладает функциональностью, необходимой для крупных промышленных сборок. Кроме того, LightForge не поддерживает скриптинг, поэтому его возможности по автоматизации ограничены базовыми инструментами.
Структура проекта в LightForge
Рассмотрим, как проект описывается в файловой системе LightForge. Обычно в проекте есть корневая папка, в которой хранятся его основные файлы и несколько подпапок для отдельных модулей или компонентов.
В корневой папке размещается файл с именем PROJECT — он содержит общее описание проекта. Каждая подпапка, соответствующая отдельной цели (target), содержит файл TARGET. Таким образом, для каждой цели создается собственный файл TARGET, где задаются ее параметры и зависимости.

Регистрация проекта
Чтобы собрать проект с помощью LightForge, его необходимо зарегистрировать с помощью утилиты LightForge Manager.

После регистрации внутри каталога LightForge создаются подпапки, соответствующие проекту и его целям (targets).
Концепция сборок (builds)
LightForge поддерживает концепцию сборок (builds). В корневом каталоге системы есть папка build, внутри которой может находиться одна или несколько сборок.
По умолчанию, после клонирования LightForge из репозитория, создается стандартная сборка с именем std. Сборка представляет собой набор инструментов, необходимых для компиляции, линковки и архивирования целей проекта. Каждая сборка определяет, какие компиляторы, флаги и утилиты используются при построении проекта.
Все инструменты, используемые внутри сборки, описываются в файле Makefile-tools. Это небольшой фрагмент Makefile, в котором задаются: определения компилятора, его параметры и флаги, а также другие настройки, применяемые единообразно ко всем зарегистрированным в сборке проектам.
При регистрации проекта в LightForge необходимо указать, в какой сборке он будет использоваться — в одной или нескольких. Например, если проект зарегистрирован в стандартной сборке std, то в каталоге build/std создается подпапка с именем проекта. Внутри нее автоматически генерируется Makefile. Для каждой цели (target) проекта также создаются отдельные подпапки, каждая со своим собственным Makefile. С их помощью можно выполнять сборку отдельных целей или всего проекта целиком.
Как работает LightForge
На экране, в правой панели, отображаются два проекта — AddLib и AddExe. Это простые примеры для демонстрации процесса установки и сборки проекта в системе LightForge.

В первом создается библиотека с простой функцией add, которая складывает три числа. Во втором — исполняемый файл, который вызывает эту функцию.
Рассмотрим сначала проект AddLib. В его корневом каталоге находится файл PROJECT — он описывает сам проект. В этом файле задаются имя проекта и список базовых проект��в (через двоеточие).
Также в корневой папке расположен Makefile. Он используется для регистрации проекта в LightForge. Чтобы это сделать, необходимо вызвать менеджер LightForge и выполнить команду add, указав путь к каталогу проекта.

После этого в структуре проекта создаются две подпапки: lib и test.
В папке lib находится файл TARGET, который описывает первую цель проекта.

Описание предельно простое: цель имеет тип lib, задается ее уникальное имя (в пределах проекта), при необходимости можно указать список базовых целей через двоеточие.
В файле также определены три обязательных параметра:
INC— список директорий с заголовочными файлами;SOURCE— пути к папкам с исходным кодом;OUT— имя выходной библиотеки, которая будет собрана.
В данном случае используется одна директория с одним файлом. В этом файле объявлена функция add(int a, int b, int c), которая складывает три аргумента, выводит аргументы и результат на консоль и возвращает вычисленное значение.
Вторая подпапка проекта — test. Здесь также расположен файл TARGET, который описывает вторую цель — сборку исполняемого файла.
Основные параметры:
тип:
exe(означает, что будет собран исполняемый файл);имя цели — уникально для проекта;
зависимости — указывают на библиотеку
AddLib, созданную ране��;inc— пустое значение, так как дополнительных заголовков нет;source— каталог с исходным кодом;out— имя итогового исполняемого файла.
Файл main.cpp внутри этой цели содержит функцию main(), которая вызывает функцию add() из библиотеки AddLib.
Сборка проекта AddLib
Перейдем в папку test. Здесь находится Makefile, с помощью которого можно собрать тестовый исполняемый файл.

Попробуем выполнить сборку командой make.
Далее используем команду make deep, она собирает не только текущую цель, но и необходимые для сборки зависимости.
После выполнения команды библиотека будет автоматически собрана, а затем создан итоговый исполняемый файл test.exe. Запустить его можно с помощью команды make run.
При запуске программа вызывает функцию add() с заданными параметрами и выводит результат на экран. Так выглядит пример базового проекта, собранного с помощью LightForge.

Поддержка генерации кода
Отдельная фишка LightForge — это поддержка построения с прегенерацией. Цели типа pregen позволяют автоматически генерировать файлы перед сборкой — например, заголовки с константами или шаблонные структуры. Это удобно, когда какие-то данные нужно вычислить заранее, а не хранить в коде.
Рассмотрим AddExe. Этот проект зависит от AddLib, о котором писали выше. У него определены две цели:
app— основное исполняемое приложение;pregen— цель для предварительной генерации кода.

Цель app зависит сразу от двух компонентов: библиотеки AddLib и цели pregen, которая называется gen.const.
Файл main.cpp в проекте AddExe довольно простой: он вызывает функцию add() из библиотеки AddLib, используя параметры a, b и c. Однако этих параметров в коде нет — они должны быть объявлены в заголовочном файле, который отсутствует.
Этот файл создается автоматически при сборке. Его генерирует специальная цель типа pregen (pre-generation). Она описывается в файле TARGET примерно так же, как и другие цели: задаются параметры INC, SOURCE и OUT.
Разница в том, что параметр OUT указывает путь к папке, в которую прегенератор должен поместить результат своей работы — в данном случае сгенерированный заголовочный файл.
LightForge при этом заботится обо всем — передает правильные пути, запускает генератор, подставляет файлы, куда нужно. Вам не нужно следить, в какой папке выполняется код или где появится результат.
Один и тот же проект: CMake vs LightForge
Есть библиотека addlib и приложение app, которое ее использует.
Вариант на CMake
CMakeLists.txt:
add_library(addlib lib/source/add.cpp)target_include_directories(addlib PUBLIC lib/inc)add_executable(app app/source/main.cpp)target_link_libraries(app PRIVATE addlib)
Что вручную описано: какие файлы входят в библиотеку, include-директории, какие файлы в executable и что executable зависит от библиотеки.
Вариант на LightForge
PROJECT:
AddExample:
lib/TARGET:
type: libname: addlibinc: incsource: sourceout: libaddlib.a
app/TARGET:
type: exename: appdeps: addlibinc: incsource: sourceout: app
Что делает LightForge: сам генерирует make-файлы, автоматически связывает цели по deps, автоматически подставляет инклуды и собирает все через make или make deep.
Главные отличия:
Объем декларации. CMake: много явных команд (add_library, include_dirs, link_libraries). LightForge: только факты: type, source, inc, deps, out.
Зависимости. CMake: описываются вручную. LightForge: декларативно (deps:) + авто-генерация правил.
Codegen (pregen). CMake: через add_custom_command. LightForge: отдельный тип pregen (минимум строк, автоматические пути).
Сложность. CMake: мощно, но громоздко. LightForge: просто, локально, подходит для небольших и средних проектов.
Ниже — главное, что делает LightForge удобнее:
Не требует изучать отдельный язык, как CMake, все задается через простые декларации (TARGET, INC, SOURCE, OUT).
Устраняет необходимость писать огромные Makefile — LightForge генерирует их сам.
Четкая и понятная структура проекта: project → targets → сборка, без вложенных конфигурационных файлов.
Автоматическая работа с зависимостями.
Нативная поддержка генерации кода (pregen) — генераторы подключаются как обычные таргеты.
Минимальная сложность, низкий порог входа — идеально для студентов, исследовательских и индивидуальных проектов.
Прозрачность сборки — легко отлаживать, легко понять, что происходит, никаких скрытых слоев.
Меньше инфраструктурного шума, чем в CMake/Bazel — проще и быстрее стартует.
Быстрая установка (git clone + make), без зависимости от внешних генераторов, IDE-плагинов и версий CMake.
Вместо заключения
Если попытаться классифицировать build-системы, я бы сделал так:
Уровень |
Инструмент |
Для кого предназначен |
Характеристика |
Базовый класс |
Make |
Старые и простые проекты |
Минимализм, но устаревший синтаксис |
Средний класс |
LightForge |
Индивидуальные, учебные, исследовательские проекты |
Простота, понятность, автоматизация без лишней сложности |
Тяжелый класс |
CMake, Bazel |
Корпоративные проекты |
Гибкость и масштабируемость, но высокая сложность |
LightForge я часто называю инструментом для людей, которые хотят писать код, а не make-файлы. Он не требует установки сторонних пакетов, не нуждается в настройке IDE и при этом способен собирать даже довольно сложные проекты с библиотеками и зависимостями.
LightForge — это не конкурент Bazel. Это инструмент, который дает простоту Make, но с возможностями, которых у Make никогда не было. Он делает ровно то, что нужно большинству C++-разработчиков: компилирует, линкует, отслеживает зависимости, умеет генерировать код.
Развитие LightForge
Когда я впервые выложил LightForge в открытый доступ, я не думал, что эта идея кого-то особенно заинтересует. Этот инструмент я сделал исключительно для себя, чтобы не возиться с make-файлами каждый раз, когда нужно собрать очередной экспериментальный проект.
Но постепенно я начал получать отзывы — сначала от коллег, потом от студентов, потом от разработчиков, которым просто надоела рутина сборки. И я понял, что LightForge полезен не только мне.
Сейчас LightForge умеет все, что нужно для работы с проектами на C++ среднего размера:
поддерживает проекты с несколькими целями (
exe,lib,pregen);отслеживает зависимости между проектами и целями;
автоматически генерирует make-файлы;
поддерживает модульность и повторное использование библиотек;
упрощает работу с прегенерацией исходников;
работает в любом Unix-подобном окружении, включая Windows с Cygwin.
У меня есть несколько идей, как развивать LightForge.
Сделать плагин для Visual Studio Code. Мне хочется, чтобы можно было собирать проекты прямо из редактора, не выходя в консоль. Это сделает систему еще доступнее для начинающих. Скорее всего, плагин будет написан на TypeScript — благо, это относительно простой и приятный язык.
Расширить интеграционные возможности. Например, добавить поддержку внешних зависимостей или интеграцию с пакетными менеджерами вроде Conan. Это не первоочередная задача, но в будущем может сделать LightForge еще более универсальной.
Использовать LightForge в учебных целях. Я вижу, как многим студентам сложно разобраться, что такое сборка проекта, как работает компилятор, линковка, зависимости. LightForge идеально подходит для этого: у нее простая структура, понятные файлы и никаких «черных ящиков». Это отличный инструмент, чтобы наглядно показать весь процесс — от исходников до готового бинарника.
Для меня эта система — не просто утилита. Это отражение философии, которую я стараюсь придерживаться в работе:
если можно сделать проще — сделай проще;
если можно объяснить яснее — объясни яснее;
если можно автоматизировать рутину — автоматизируй и забудь о ней.
Где найти и как установить LightForge
Вся информация о проекте находится в открытом доступе. В репозитории проекта —
исходный код, документация, инструкции по установке, примеры использования.
Требования к окружению: для работы LightForge требуется Unix-подобная система с установленными инструментами g++ и make. Это может быть Linux, macOS или Cygwin под Windows.
Комментарии (2)

eao197
11.12.2025 11:05Позволю себе позанудствовать по поводу двух моментов.
Во-первых, очень похоже, что таки системой сборки у вас является make, а не ваш LightForge. LightForge генерирует Makefiles, а уже сам make занимается проверкой того, что нужно собрать, запуском компилятора/линкера и т.д. Так что и LightForge, и CMake более правильно было бы относить не к системам сборки (так как никто из вас собирать, собственно, и не умеет), а к системам генерации описаний для систем сборки.
Понятно, что здесь речь о терминах, но все таки было бы правильно отделять make/nmake/ninja и им подобное от паразитирующих над ними надстроек по типу CMake.
Во-вторых, то, что вы декларируете как достоинство, а именно "Не требует изучать отдельный язык, как CMake", для чего-то более более серьезного, чем HelloWorld, превращается в недостаток. Потому что даже для небольшого кросс-платформенного проекта в описании проекта очень быстро появляются if-ы для проверки разных условий (типа версия компилятора такая-то, версия сторонней библиотеки такая-то, дополнительные ключики для компиляции, которые нужны на время дополнительных проверок, такие-то и т.д., и т.п.). И тут уж какое-то подобие языка программирования неизбежно. Говорю вам как человек, который сам сделал несколько версий системы сборки для C++, и который в итоге пришел к выводу, что в мире C и C++, описывать проект таки нужно на каком-то ЯП. Хотелось бы не на таком ущербном, как в CMake, но все-таки.
А вообще по самой статье хочется сказать: написано много, какие-то скриншоты даже приведены, но нормального понимания того что же из себя представляет ваш велосипед и как им пользоваться, увы, не складывается :(
PS. И да, здесь очень уместна была бы ссылка на комикс про 15 конкурирующих стандартов.
Jijiki
вообще еще можно пойти другим путём, написать наверно плагин для какого-то компилятора(наверно кланг), скармливать ему только main - file(построение Tree проекта), суть концепции в том, что от мейн файла плагин(или интегрированная новая концепция сборки от 1 файла при помощи возможностей LLVM-clang базы) должен собрать сам екзешник) и точно так же с библиотеками, соответственно приходим к ужатию количества файлов, которые идут на вход компилятора
Проблематика - не все программисты хотят этим заниматься - типо итак кодить, а тут еще компилятор писать