четверг, 27 октября 2011 г.

Что-то мне в последнее время не нравится make...

Что-то мне начал надоедать make. А так хочется быть гибким, хочется применять всякие новостные методики, но make мешается.

В принципе конечно все проблемы разрешаются, но что-то мне не хочется их решать. Да пусть этих Makefile вообще не будет. В каждом каталоге лежат, чтоб им пусто было. Лучшая система сборки та, которой нету.

Да, я, как всегда, занимаюсь велосипедостроением, но давайте обо всем по порядку.

Очень хочется применять TDD. Так, чтобы про настоящему, с максимальным покрытием кода. Состояния полного просветления я еще не достиг, и иногда начинаю просто писать код. Но вскоре понимаю, что действую неправильно. Если, ради того, чтобы сошелся тест надо написать еще один классик, то для нового класса тоже нужны тесты. Причем до того, как я начну его реализовывать. Хобби проект очень хорошее место для того, чтобы отточить навыки в TDD и освоить на практике новый стандарт c++.

Ради тестирования структура проекта выстраивается так, чтобы функция main была бы отдельно и не мешалась, а остальной код можно было бы использовать как с упомянутой выше main, так и в рамках тестового модуля. Классики TDD об этом почему-то не пишут. Может быть потому, что они все работают на java. Но в c ++ иначе никак не получится.

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

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

Короче, нужна максимально быстрая сборка. И make с этим более-менее справляется, хотя к его скорости и есть серьёзные претензии у сообщества.

Кроме того нужна минимальная пересборка. И с этим make справляется, если прикрутить к нему файлы с зависимостями.

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

И тут я подумал, что вообще не хочу беспокоится о зависимостях. Даже более того, я готов терпеть чуть более долгий процесс сборки, ради того, чтобы сборка при любых изменениях собиралась корректно и, конечно, в минимальном объеме.

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

Его использование дает несколько больших плюсов:
  • Мне больше не нужны Makefile в подкаталогах. От корневого Makefile отказываться, конечно, не стоит. Но на его плечи ложиться сравнительно небольшая нагрузка, как то свести все воедино, и подчистить за собой.
  • Зависимости я определяю на лету, думаю что препроцессор отрабатывает сравнительно быстро, и мне нет смысла с этим заморачиваться пока у меня проект не перерос какие-то разумные размеры.
  • Мне больше нет необходимости наблюдать вывод сборки. Я могу сделать дружественные сообщения, вплоть до прогресса. Это можно было сделать и в Makefile, но там это не так гибко.

Сам про себе скрипт, конечно не универсален, и имеет минусы:
  • Все исходные тексты, для полноты картины даже main.cpp, необходимо раскладывать по отдельным папочкам. Ведь не для того все это затевалось, чтобы потом некоторые файлы собирать чем-то еще?
  • Пока он никак не многопоточен. Это большой минус и его можно решить по разному. Можно, например, запускать несколько каталогов паралельно - самое простое. Или можно встроить в скрипт пул компиляторов, но тогда это сильно утяжелит его, думаю в этом нет особой необходимости.

Сейчас он решает мою конкретную задачу, и этого довольно. Если идея кому-то покажется интересной, я всегда открыт для общения.

25 коммент.:

Unknown комментирует...

Может я не понял сути проблемы, но scons не подходит, как система сборки?

Andrey Valyaev комментирует...

Не хочу ставить еще одну систему сборки.. много их.

make есть почти везде. А scons - нет.

Хотя с другой стороны я завязался на python, будем рассчитывать, что питон тоже есть везде. :)

Qehgt комментирует...

Вы переизобретаете заново SCons/CMake.

Andrey Valyaev комментирует...

Может быть, но только я написал всего 80 строк кода...

И мне не надо ставить никаких дополнительных инструментов - это плюс. :)

Unknown комментирует...

CMake ftw. И TDD изкоробки умеет, и файлы спецификаций нужны по одному на проект.

Andrey Valyaev комментирует...

Не понимаю какое отношение CMake имеет к системе сборки? Давайте называть вещи своими именами. CMake - это система конфигурирования.

И чего я больше всего не люблю - это когда кто-то гадит в тех каталогах, где лежат мои исходники!

Вообще идею autoconf я считаю ущербной. и CMake идет туда же, лесом. Мне не нужно проверять наличие в моей системе функций типа strcmp, и меня бесит когда каждый собираемый пакет тупо пытается это делать...

Dmitriy T. комментирует...

"И чего я больше всего не люблю - это когда кто-то гадит в тех каталогах, где лежат мои исходники!"

Гм. CMake гадит в той директории откуда вы его запускаете. Лично у меня эта директория build одного уровня с директорией сорцов, туда же гадит и gtest (как-то к нему привык, а не к ctest) в случае облома.

А по сути, как только задача слегка усложнится или потребуется интеграция куда-нибудь - уверен на свой велосипедный скрипт придётся положить болт.

--------

Я не фанат CMake, более того я на c++ программирую редко. Из-за лени и "по совету друзей" выбрал CMake. Хотя периодически душа просит autotools, но лень и направление тенденций в сторону CMake/SCons помогает перебороть эти нездоровые позывы.

Andrey Valyaev комментирует...

Лаврентий Павлович:

туда же гадит и gtest в случае облома.

Это Google test чтоль? то-то он мне не нравится, хотя не помню чтобы он гадил, отчеты наверное у меня были выключены. boost test не гадит.

Сейчас у меня вообще никто никуда не гадит.

А по сути, как только задача слегка усложнится или потребуется интеграция куда-нибудь - уверен на свой велосипедный скрипт придётся положить болт.

Возможно, но с другой стороны он может собрать очень много. :) Он простой и прямолинейный.

А что касается CMake - то он как раз таки очень жестко относится к переименованиям файлов... переименовал - перезапускай cmake.

Может быть это и не правильно, но я не люблю в мейкфайлах писать, какие файлы нужно включить в сборку, предпочитаю не держать лишних файлов.

eao197 комментирует...

Да, знакомое чувство. Правда make я выбросил когда-то давным-давно не из-за TDD (коего тогда в наших краях и не было), а из-за необходимости таскать проект между Win, Linux и OS/2. Да еще и разными компиляторами под Win и OS/2.

В конце-концов сделал собственную систему сборки. Чем сильно доволен. А если бы ее еще развивал кто-нибудь вместо меня -- так вообще была бы лепота ;)

BTW. Имхо, любая система сборки C/C++ проектов должна уметь самостоятельно разруливать зависимости. Хотя бы на примитивном уровне.

Andrey Valyaev комментирует...

Спасибо, Евгений! хоть кто-то меня поддерживает :)

Есть несколько сторон нескольких медалей...

Если программа внутренняя - для нее можно найти любой, самый нетипичный и никому не известный тул и успешно его юзать. Но если программа открытая - то надо предполагать что люди захотят ее собрать. Зачем навязывать им дополнительные сложности? Нужно действовать максимально типичным способом.

Другая медаль про переносимость что-то хотел сказать, забыл... Переносимость зло! минимальные требования к окружению - компилятор с поддержкой C++11 и буст. :) Поэтому древние, бородатые юниксы, в которых нет strcmp, идут лесом.

Еще одна медаль - про разрастание проектов... Нам не дано предугадать будущее. Рассчитывать надо на сегодня. :) Возникнут трудности - мы будем их решать.

Честно говоря мне совсем не хочется тратить время на изучение еще одного синтаксиса еще одной системы сборки, я и мейк то до конца не постиг.. :) там много всяких возможностей. Усложнять себе жизнь? Зачем? Программы должны быть простые, без сложных описаний.

Вот оно - почти все описание:
build.py module.o module

Ну и плюс еще несколько небольших ньюансов. :)

eao197 комментирует...

@Андрей Валяев:

>Нужно действовать максимально типичным способом.

Так в том-то и дело, что для C++ типичных способов как собак -- от обычных Makefile с autoconf под *nix-ами до MS Build и MS VS Project files, с промежуточными вариантами вроде SCons, Boost.Build, CMake, MPS и даже Ant.

>Переносимость зло! минимальные требования к окружению - компилятор с поддержкой C++11 и буст. :)

Эх если бы хватало только C++ и, хрен уж с ним, буста... :)

Dmitriy T. комментирует...

"Это Google test чтоль?"

Он самый.

"Может быть это и не правильно, но я не люблю в мейкфайлах писать, какие файлы нужно включить в сборку, предпочитаю не держать лишних файлов."

В случае CMake это вобщем неправильно(если конечно это не маленький проектик под собственные нужды, который никто кроме тебя не увидит). Например, если не захордкодить в CMakeLists.txt список сорцов (а загружать его через например AUX_SOURCE_DIRECTORY), то придётся при добавлении нового сорца форсировать вызов cmake руками, тк make его не вызовет в этом случае. Аналогичные проблемы могут быть в различных связках для continuous integration. Так что проще захордкодить список сорцов и не париться что кто-то добавит/переименует файл, а автоматическая сборка на сервере не пройдёт пока кто-то не форсирует на нём запуск cmake руками (из dashboard или консоли - не важно). Вобщем лишний повод наступить на грабли взамен сэкономленных копеек времени.

Andrey Valyaev комментирует...

Лаврентий Палыч: Дык это же собственно и не удобно, когда помимо просто переименования файла тебе нужно залезть еще куда-то и поменять его имя. С инклюдами и так хватает забот, поменять имена в тех местах, где они реально нужны.

В мейкфайле можно просто использовать wildcards, чтобы собрать все исходники в каталоге. Я, как бы, развил тему и собираю исходники на всю глубину указанной папки.

Но CMake - работает по другому. Я глянул мельком - он не использует обобщенные рули и все прописывает явно. Там мейкфайлы на сотни килобайт. :S Сотни килобайт мусора в папках с моими исходниками?!?

Джавистам то хорошо, у них кроме Ant и выбора наверное другого нету. :)

Dmitriy T. комментирует...

"Дык это же собственно и не удобно, когда помимо просто переименования файла тебе нужно залезть еще куда-то и поменять его имя."

Ну лично у меня эта операция занимает дай бог 0.5% от общего времени разработки(даже если представить что сидишь в консоли с vim - не так уж трудоемко вбить команду s/oldname/newname/g ). Понятно чем меньше и проще проект тем более затратна эта операция.

"Но CMake - работает по другому. Я глянул мельком - он не использует обобщенные рули и все прописывает явно. Там мейкфайлы на сотни килобайт. :S Сотни килобайт мусора в папках с моими исходниками?!?"

Откровенно говоря мне пофиг сколько там килобайт на выходе. Я выбрал CMake просто из-за лени, ну и потому что он вроде побыстрее SCons (вроде на сайте scons это и написано). Я лично для сборки относительно сложного проекта с кучей зависимостей и около 5-10 тестов написал CMakeLists.txt на 90 строк. И один кастомный FindXXX.cmake модуль строчек на 20 для кастомной зависимости. В папках с моими исходниками нету мусора вообще, я уже писал - весь мусор в build, который естественно не попадает в репозиторий.

"Джавистам то хорошо, у них кроме Ant и выбора наверное другого нету."

Выбор есть. Например самые популярные: maven, Ivy. Если нет боязни новизны и детских болезней свежих продуктов то можно ещё с полдесятка набрать наверное.

eao197 комментирует...

@Андрей Валяев:

На счет автоматического включения в проект файлов по маске (скажем *.cpp) вспомнился такой нюанс. Временами рядом с нормальными .cpp-файлами лежат файлы с таким же расширением, но которые не нужно включать в проект. Этому могут быть следующие причины:
1. Файл не должен компилироваться на данной платформе (скажем, в POCO есть файлы вида Mutex_WIN32.cpp, Mutex_POSIX.cpp).
2. Файл уже подключается куда-нибудь через #include. Например, файл содержит автоматически сгенерированный каким-то инструментом фрагмент. Можно было бы для таких файлов использовать другие расширения, вроде .inl, но это не всегда поддерживается.
3. Файл содержит какую-то промежуточную версию кода. Например, у меня есть файл some_stuff.cpp и я пытаюсь найти в нем поблему. Для чего перерабатываю его разными способами. Результат каждой переработки сохранятся в some_stuff_orig.cpp, some_stuff_v1.cpp, some_stuff_v2.cpp и т.д.

Как раз из-за таких вещей я предпочитаю сам указывать имена cpp-файлов в проекте.

Но, если проектная система реализована на скриптовом языке, то нет никаких проблем сделать выборку по маске. Скажем, у меня можно делать и так:

cpp_source "some_stuff.cpp"

и так:

cpp_sources Dir.glob( "*.cpp" )

Andrey Valyaev комментирует...

Евгений Охотников:

Файл не должен компилироваться на данной платформе.

Да, такое бывает. Но можно придумать альтернативные способы. Например вынести межпроцессное взаимодействие в отдельный модуль и собирать соответственно либо interprocess_linux, либо interprocess_win32 либо еще что-то...

Или все различия сконцентрировать в одном файле. То есть файл компилируется по любому, но не в зависимости от платформы имеет разный код.

Файл уже подключается куда-нибудь через #include.

Категорически не люблю inl. в любых проявлениях. Для этого есть линкер. А если кто-то один и тот же cpp файл включит в два разных исходника?

Хотя, может быть, я не все случаи могу себе представить. :)

Файл содержит какую-то промежуточную версию кода. Например, у меня есть файл some_stuff.cpp и я пытаюсь найти в нем поблему. Для чего перерабатываю его разными способами. Результат каждой переработки сохранятся в some_stuff_orig.cpp, some_stuff_v1.cpp, some_stuff_v2.cpp и т.д.

А вот это точно надо хранить в репозитории... Тебе не нужны две разных версии файла - тебе нужна ветка для экспериментов. :)

А то иногда бывает так, что файлы вроде бы есть, ты их внимательно изучаешь, ломаешь голову, а потом оказывается что эти файлы уже 5 лет не входят в сборку... Мертвый код должен умереть!

Или после таких экспериментов может получиться так, что вот два почти одинаковых файла, давно лежат в проекте... какой из них более правильный? Какой из них нужен?

Помоему этой проблемы не должно стоять изначально.

Еще такой момент. Эксперименты тоже лучше хранить в базе. Некоторые слишком увлекаются экспериментами, а результата (кода на сервере) нету неделями... Сама по себе необходимость выкладывать код - она сильно стимулирует к достижению результата. :)

А сам по себе проект - должен быть чистым.

eao197 комментирует...

@Андрей Валяев:

>А вот это точно надо хранить в репозитории... Тебе не нужны две разных версии файла - тебе нужна ветка для экспериментов. :)

Ну так в сложном случае я так и делаю -- беру отдельный бранч, делаю себя рабочую копию. А уже в ней крою все так, как мне удобно. Удобно заменить обращения к std::vector на прямые new/delete -- заменяю. Не помогло это -- выбрасываю. Делаю что-то другое.

Это моя каша. Я забочусь лишь о том, чтобы в репозиторий попал чистый вариант. Но то, что у меня творится в рабочей копии -- это моя проблема. И когда система сборки не дает мне разбрасывать мусор так, как мне вздумается, это напрягает.

Andrey Valyaev комментирует...

2Евгений Охотников: И когда система сборки не дает мне разбрасывать мусор так, как мне вздумается, это напрягает.

Это вопрос личной дисциплинированности. :)

С таким же успехом можно сказать, что компилятор, зараза, не дает мне писать код так, как я хочу... пишет предупреждения, ошибки... напрягает меня. :)

Я и сам часто грешу всякой безалаберностью, надо с этим бороться. :)

eao197 комментирует...

@Андрей Валяев:

>Это вопрос личной дисциплинированности. :)

Все-таки я думаю, что вопрос личной дисциплинированности -- это чистота и порядок в репозитории. А что делается в моей рабочей копии -- это дело привычки и личных предпочтений.

Andrey Valyaev комментирует...

2Евгений Охотников: чистота и порядок в репозитории. А что делается в моей рабочей копии -- это дело привычки и личных предпочтений.

Дык в том то и дело, что то, что находится в общем репозитории не должно содержать ничего лишнего. И если тулзы помогают поддерживать чистоту - это хорошо. :)

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

ЗЫ: только что наткнулись на проблему человеческой совместимости. У всех нормальных людей - табуляция 8 символов, а у одного - 4 символа... казалось бы - его личные червяки - пусть разбирается, но командное соглашение допускает строки до 100 символов в жесткой форме.

У него короткая строка, по его меркам, но командным соглашениям не соответствует. Конфликт. :)

И пробелами его не решить.

eao197 комментирует...

@Андрей Валяев:

>Хотя локально никто не мешает тебе иметь несколько версий одного и того же файла. Ты даже можешь наплевать на корпоративную систему сборки и собирать чем захочешь. Но это личные проблемы.

Вот об этом я и говорю. Если мне удобно иметь локальну у себя some_file_v1.cpp, some_file_v2.cpp и some_file_v3.cpp, а корпоративная система сборки будет наровить их компилировать всегда, то это неудобная система. Удобнее, когда есть выбор -- собирать ли все по маске или только то, что вручную указано.

Torsten комментирует...

Сейчас перевожу проект на Scons, с make. Вот это - хорошая вещь. Есть конечно свои минусы, некоторые мне удалось обойти, но плюсов все равно намного больше.

Mike комментирует...

Андрей, все нормальные люди используют 4 символа на табуляцию.

Andrey Valyaev комментирует...

8 символов на табуляцию рулят.

Не позволяют плодить монстров в виде 5-7 уровневых функций.

3 табуляции и баста... а дальше код перестает влезать в лимит на ширину строки - применяй рефакторинг.

Тем более, что в консоли табуляция тоже имеет длинну 8 символов.

Mike комментирует...

За монстры надо бить по рукам, но не позволять писать код, раскинувшийся от края экрана до края.

Да, кто много работает в консоли, используют 8 символов :-) Это знакомо ))

Кстати сказать, меня вообще удивляет интерфейсы различных прог в Linux. Много неиспользуемого места, большие кнопки, вокруг которых много пространства... вот под виндами все компактно.