C++ проект, который годами собирается у одного разработчика, часто рассыпается на ноутбуке нового коллеги в первый рабочий день. Исходник тот же, компилятор вроде бы тот же, а заголовки OpenSSL не видны, линковщик ругается на чужой рантайм, и счёт ошибок уходит за сотню. Виноват не язык и не редактор. Виноват порядок, при котором часть библиотек лежит в системных путях, часть скопирована в third_party, и никто в команде точно не ответит, какие версии нужны проекту прямо сейчас. Ниже разбор того, почему в C++ так исторически сложилось, чем связка CMake и Conan закрывает вопрос воспроизводимости, что фиксирует замок conan.lock и в какой момент усилия на инструмент начинают окупаться.
Как сторонние библиотеки оседали в проекте и почему сборка ломалась годами
У C++ никогда не было встроенного способа тянуть внешние библиотеки. Хочешь zlib, иди в систему: apt install zlib1g-dev на Ubuntu, brew install openssl на macOS, на Windows своя экосистема с ручной сборкой. README рос по мере боли и обрастал инструкцией из десятка команд, которая устаревала с первым обновлением дистрибутива.
Второй путь выглядел честнее: скопировать исходники зависимости в папку third_party рядом со своим кодом. Всё нужное лежит в репозитории, никаких сюрпризов. На практике папка становилась музеем: fmt из 2019 года, spdlog из 2020-го, json, обновлённый однажды копированием поверх старой версии. Сколько времени уходит на сборку такой папки, тоже вопрос, у реальных проектов third_party собирался дольше самого приложения.
Боль умножается транзитивными зависимостями. Boost тащит zlib, bzip2, icu и ещё десяток опций, protobuf собирается вместе с абсейлом, а OpenSSL на macOS после перехода Homebrew на путь /opt/homebrew для ARM переехал с привычного /usr/local. Скрипты, в которых пути вбиты руками, начинают жить своей жизнью, и чинить их приходится каждому по отдельности.
Windows добавляет рантайм MSVC. Библиотека, собранная с /MD, отказывается дружить с приложением на /MT, ошибка LNK2038 прямо называет несовпадение RuntimeLibrary, Release-бинарник, прилинкованный к Debug-приложению, валит его на первом же std::string, а предупреждение C4251 при экспорте шаблонов из DLL знает каждый, кто пробовал. Разные версии Visual Studio и разные наборы символов добавляют ещё измерений.
Итогом становилась формула "собирается у меня". Обновления откладывались, потому что каждое могло уронить соседний проект, CI либо отсутствовал, либо собирал окружение той же ручной цепочкой команд, а новый сотрудник тратил первую неделю на изучение порядков из старых инструкций. Проблема не в лени, а в отсутствии механизма, который фиксирует состав окружения так же надёжно, как система версий фиксирует исходный код.
Что фиксирует conan.lock в Conan 2 сверх номеров версий
Conan 2.0 вышел в феврале 2023 года и переписал архитектуру первой версии: новые профили, новый граф зависимостей, новые генераторы. К осени 2026 года ветка доросла до версии 2.33, релизы идут ровным ритмом, рецепты централизованного индекса закрывают большинство ходовых библиотек. Идея инструмента проста: зависимости описываются декларативно в conanfile, а фактический состав вычисленного графа фиксируется в conan.lock, который хранится в репозитории наравне с кодом.
Замок содержит больше, чем пары из имени и номера версии. У каждого рецепта есть ревизия, контрольная сумма его собственного содержимого. Если кто-то выложит в индекс заново ту же версию zlib/1.3.1, но с изменённым рецептом, команда со старым замком продолжит получать прежнюю ревизию и не заметит подмены, пока сама не решит обновиться. У собранных бинарников есть собственные ревизии, то есть содержимое кэша на диске тоже под контролем.
Базовый цикл работы с замком выглядит так:
conan lock add --requires="zlib/1.3.1" --lockfile-out=conan.lock
conan install . --lockfile=conan.lock
Первая команда фиксирует требование в замке, вторая собирает окружение строго по нему и падает с понятной ошибкой, если граф расходится с описанием. Внутри замок хранит требования вместе с ревизиями:
{
"version": "2.0.0",
"requires": [
"zlib/1.3.1#<ревизия рецепта>",
"fmt/11.1.4#<ревизия рецепта>"
]
}
Формат не предназначен для чтения за обедом, это рабочий журнал машины. Человек читает conanfile и видит намерение, машина читает замок и видит факт, конфликт между ними обнаруживается на первой же команде установки, а не в ночной сборке перед релизом.
Профили Conan и разделение настроек для Linux macOS и Windows
Воспроизводимость в C++ упирается не только в версии библиотек, но и в настройки сборки. Один и тот же код на Linux с GCC, на macOS с Clang и на Windows с MSVC собирается по-разному, и эти различия нужно где-то честно описать. В Conan для этого служат профили. Команда conan profile detect создаёт стартовый профиль с настройками, определёнными по текущей машине, а дальше команда правит файлы руками:
[settings]
os=Linux
arch=x86_64
compiler=gcc
compiler.version=13
compiler.libcxx=libstdc++11
build_type=Release
Профили хранятся в репозитории проекта, по одному на платформу: linux-gcc13, macos-clang17, windows-msvc193. Каждый участник собирает своим профилем, состав входа одинаков, различия лежат на поверхности и видны при ревью.
Вторая половина механизма, идентификатор пакета. Для каждой комбинации настроек Conan вычисляет package_id, и бинарники с разными комбинациями не смешиваются в кэше. Сборка под MSVC и под GCC получает разные бинарники одной и той же библиотеки, кэш переиспользуется между проектами на одной машине, а флаг --build=missing заставляет собирать из исходников только то, чего нет в готовом виде. Это практический ответ на историческую боль с ручным сбором: одна и та же команда работает на всех платформах, а различия сведены в короткий файл профиля.
Профили же держат спорные настройки вроде рантайма MSVC:
[conf]
tools.build:compiler_runtime=MD
Строка фиксирует динамический рантайм для всех зависимостей сразу, и конфликт /MD против /MT перестаёт быть чьей-то личной проблемой в свойствах одного проекта.
Интеграция Conan с CMake через генераторы и переносимые цели библиотек
Описанные зависимости сами по себе ещё не дают собрать проект. Их нужно доставить до компилятора и линковщика так, чтобы CMakeLists.txt не оброс сотней строк с платформенными путями. В Conan 2 этим занимаются два генератора, CMakeToolchain и CMakeDeps. Первый строит toolchain-файл с флагами, путями и настройками рантайма, второй генерирует для каждой зависимости конфигурационные файлы, которые находит find_package.
Описанию зависимостей место в conanfile.txt:
[requires]
fmt/11.1.4
zlib/1.3.1
[generators]
CMakeToolchain
CMakeDeps
[layout]
cmake_layout
Сборка сводится к трём командам:
conan install . --build=missing
cmake --preset conan-release
cmake --build --preset conan-release
А в самом CMakeLists.txt остаются обычные декларации, знакомые по любому туториалу:
cmake_minimum_required(VERSION 3.23)
project(mailer CXX)
find_package(fmt REQUIRED)
find_package(ZLIB REQUIRED)
add_executable(mailer src/main.cpp)
target_link_libraries(mailer PRIVATE fmt::fmt ZLIB::ZLIB)
Никаких -I и -L с платформенными путями, никаких веток if (WIN32) ради выбора между библиотечными файлами. Цель fmt::fmt одинакова на трёх платформах, CMakeDeps транслирует её в нужные флаги конкретного компилятора. Слой между find_package и кэшем Conan снимает с автора проекта работу, которую раньше делали руками и часто переделывали для каждой новой машины.
Пресеты CMake как общий вход для всех машин команды
У CMake своя часть работы над предсказуемостью. Ещё недавно конфигурация проекта выглядела как стена флагов в командах сборки, и каждый разработчик держал свою стену, чуть-чуть отличавшуюся от соседней. В версии 3.19, вышедшей в ноябре 2020 года, появились пресеты, файл CMakePresets.json, который хранит настройки конфигурации прямо в репозитории:
{
"version": 3,
"configurePresets": [
{
"name": "release",
"binaryDir": "build",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_STANDARD": "20"
}
}
]
}
После этого сборка на любой машине выглядит одинаково: cmake --preset release. Пресеты поддерживают наследование и условные секции, личные настройки прячутся в CMakeUserPresets.json, а Conan при разметке cmake_layout дописывает свои пресеты туда автоматически. Свежая ветка CMake 4.4 содержит очередные уточнения формата, схема выросла через несколько редакций, и старые пресеты продолжают работать.
Дисциплина версий у CMake жёсткая, и это полезно знать заранее. Версия 4.0, вышедшая весной 2025 года, перестала поддерживать совместимость с проектами, объявляющими требования ниже 3.5. Легаси с cmake_minimum_required(VERSION 2.8), годами спокойно конфигурировавшееся, получило отказ с внятной диагностикой и требованием обновить строку. Боль разовая, но она напомнила сообществу, что у инфраструктуры сборки тоже есть срок жизни, и его надо планировать, а не пережидать.
Когда vcpkg и FetchContent справляются лучше Conan
Conan не единственный ответ на вопрос зависимостей, и честный обзор обязан это признать. vcpkg, менеджер из экосистемы Visual Studio, работает по манифесту:
{
"name": "mailer",
"version": "1.2.0",
"dependencies": [
"fmt",
"zlib"
]
}
Версии в нём фиксируются baseline, привязкой к коммиту репозитория с наборами портов. Схема работоспособна, особенно для Windows: интеграция с решениями Visual Studio из коробки, понятные триплеты вроде x64-windows, бинарный кэш. Слабые места известны: в классическом режиме пакеты ставились в глобальное окружение и расползались между проектами, смешение триплетов со статическим и динамическим рантаймом даёт те же ошибки линковки, а выход за пределы Windows требует больше ручной настройки.
Для мелких зависимостей у CMake есть собственный механизм, FetchContent:
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY <адрес репозитория fmt>
GIT_TAG 11.1.4
)
FetchContent_MakeAvailable(fmt)
Механизм удобен для header-only библиотек и одиночных небольших зависимостей, всё скачивается и собирается в дереве сборки без внешних инструментов. Но общего бинарного кэша между проектами у него нет, каждая сборка тянет исходники заново, а указание GIT_TAG именем ветки превращает воспроизводимость в удачу, метка релиза или коммит обязательны.
Выбор между инструментами складывается в простую картину. Windows-проект с одним стеком может спокойно жить на vcpkg, небольшой проект с парой header-only библиотек обойдётся FetchContent, а Conan окупается там, где есть несколько платформ, смешанные компиляторы и команда больше двух человек.
Пошаговый план перехода команды с ручной сборки на Conan
Перевод живого проекта с исторического хаоса на воспроизводимую схему занимает пару вечеров, если держаться проверенного порядка:
- снять снимок текущего состава библиотек, версии системных пакетов, содержимое third_party и команды из старых инструкций;
- выписать зависимости, которые приложение подключает напрямую, остальное подтянется транзитивно по графу;
- описать их в conanfile и сгенерировать conan.lock, зафиксировав ревизии рецептов;
- завести профили для всех платформ команды и прогнать сборку каждой из них на чистой машине;
- встроить в CI установку строго по замку с запретом пересчёта и записью версии Conan в артефакт сборки;
- удалить папку third_party и устаревшие инструкции, оставив команде один источник правды.
Для CI достаточно одной строки в скрипте:
conan install . --lockfile=conan.lock --build=missing
Отказ замка пересчитываться на месте и есть та защита, ради которой всё затевалось. Исторические проблемы платформ не исчезают, у macOS остаются свои пути, у MSVC свой рантайм, но они переезжают в профили и toolchain, где каждому различию отведено своё место. Воспроизводимость в C++ складывается из трёх привычек: зависимости описаны декларативно, фактический состав зафиксирован замком, вход в сборку один для всех машин. Как только привычки закрепились, формула "собирается у меня" теряет смысл, потому что собираться начинает у всех.