вторник, 1 марта 2016 г.

[prog.c++] Файловая структура для кросс-платформенных C++ проекта

Очень долго пользовался придуманной много лет назад файловой структурой в своих C++ных проектах. Эта структура вырабатывалась долго, методом проб и ошибок. Довольно подробно я ее описывал почти одинадцать лет назад в RSDN Magazine. С тех пор она верой и правдой служила моей команде. Но времена меняются, имеет смысл посмотреть по сторонам.

Если говорить кратко, то мы использовали в проектах вот такую организацию каталогов:

prj/
`- dev/
   `- prj/
   `- samples/
   `- tests/
   `- ...
`- doxygen/
`- ...

В каталоге dev/prj были собраны как заголовочные, так и исходные файлы. Там же лежали rb-файлы для сборки prj из исходников (мы использовали Mxx_ru в качестве кросс-платформенной, заточенной под C++, билд-системы).

Такой подход в сочетании с Subversion (мега-фича svn:externals) позволял легко и просто подключать в свой проект мелкие подпроекты в качестве зависимостей. Так, если есть проект A, который использует в качестве зависимостей проекты B, C, D и E, то для каталога A/dev задавалось свойство svn:externals вида:

http://some.host/svn/B/tags/version/dev/B B
http://some.host/svn/C/tags/version/dev/C C
http://some.host/svn/D/tags/version/dev/D D
http://some.host/svn/E/tags/version/dev/E E

В результате, при чекауте исходников проекта A файловая структура принимала вид:

A/
`- dev/
   `- A/
   `- B/
   `- C/
   `- D/
   `- E/
   `- samples/
   `- tests/
   `- ...
`- doxygen/
`- ...

Где каталоги B, C, D и E создавались и наполнялись самим Subversion-ом.

Мы находили в этой схеме много достоинств. Так, если зависимости в svn:externals были зафиксированы правильно (т.е. ссылки на tags, да еще и с указанием ревизии), то можно было поднять из репозитория точную версию любого проекта со всеми его зависимостями. На любой платформе, где был svn. Внутри проекта можно было все подпроекты компилировать и перекомпилировать в разных режимах и с нужными ключиками компилятора (при этом в соседнем каталоге могла лежать другая версия этого же проекта, но собранная с другими настройками). Плюс через svn export можно было взять нужную версию исходников со всеми зависимостей, но без svn-овской служебной информации, и упаковать все это в обычный тарболл.

Правда приходилось сторонние проекты адаптировать под такую структуру. Если проект был относительно небольшой, то это оказывалось не так уж и сложно (например, мы подобным образом поступали с pcre, libcurl, crypto++, POCO и даже ACE). Но вот с очень большими проектами, вроде Qt или Boost-а этот подход уже не работал :(

Теперь же ситуация очень изменилась. Subversion уже не торт, Git наше фсё :) Хотя по удобству прописывания зависимостей для проекта Git-овские submodules и subtree, как и Mercurial-овский subrepos и в подметки не годятся svn:externals :(

Посему приходится задумываться, как же теперь жить дальше, раз уж Subversion так не любят. Да и вообще, нонешние разработчики офигели настолько, что ленятся даже простые тарболлы для своих проектов собирать и куда-нибудь выкладывать. Вот вам ссылка на github, а дальше трахайтесь с той хренью, которая в master-е лежит, как хотите.

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

Пока есть следующая идея. Раз уж модные и молодежные DVCS не позволяют вытянуть из репозитория только содержимое конкретного каталога на конкретной ревизии и тащить приходится все, то можно поступать следующим образом:

  • структура проекта приобретает вид:

    prj/
    `- .externals/
    `- dev/
       `- prj/
       `- ..
    `- doxygen/
    `- ...
    `- externals.rb
    
  • файлик prj/externals.rb содержит описание зависимостей для проекта и умеет эти зависимости подтаскивать. Например, для проекта A в A/externals.rb будет указано, что подпроекты B и C нужно брать с github-а, подпроект D -- в виде тарболла с такого-то URL, а подпроект E -- с bitbucket-а;
  • для подтягивания зависимостей запускается externals.rb, который извлекает зависимости из их месторасположения и укладывает в каталог prj/.externals. Укладывает именно в том виде, в котором исходники внешнего проекта удается достать. Так, если внешний проект B лежит на github.com/vasya-pupkin/B, то externals.rb запускает
    git clone --depth 1 https://github.com/vasya-pupkin/B.git .externals/B.

    В итоге в prj/.externals образуется свалка всех внешних подпроектов со всей их требухой. Что-то вроде:

    prj/
    `- .externals/
       `- B/
          `- .git/
          `- dev/
             `- B/
             `- samples/
             `- tests/
          `- ...
        `- C/
           `- .git/
           `- dev/
              `- C/
              `- ...
           `- ...
        `- ...
    

    Соответственно, следующим шагом externals.rb копирует из prj/.externals только те части подпроектов, которые нам нужны, и помещает их в prj/dev. Так, содержимое prj/.externals/B/dev/B копируется в prj/dev/B, содержимое prj/.externals/C/dev/C в prj/dev/C и т.д. В итоге получаем нужную нам картинку:

    prj/
    `- .externals/
    `- dev/
       `- prj/
       `- B/
       `- C/
       `- D/
       `- E/
       `- ..
    `- doxygen/
    `- ...
    `- externals.rb
    

В принципе, что-то подобное можно делать с помощью CMake и его ExternalProject. Но, блин, CMake сам по себе мозги выворачивает, нужно долго выкуривать разрозненные куски описаний со всего Интернета, чтобы разобраться что и куда CMake размещает. А с ExternalProject эта задачка еще больше усложняется. Кроме того, CMake лишь генерирует Makefiles, что не очень удобно, если даже на одной платформе с проектом нужно работать несколькими компиляторами (особенно, когда нужно несколько версий одного и того же компилятора задействовать).

Ну а теперь собственно суть поста: народ, а поделитесь, плиз, своими подходами к организации файловой структуры C++ проектов. Как у все каталоги организованы? Как вы зависимости подтягиваете? Может какими-то готовыми инструментами пользуетесь? Если да, то какими и как впечатления?

В общем, любая информация будет интересна. А то собирать свой собственный велосипед -- это, конечно, увлекательная задачка, но как-то староват я уже для этого :(

Комментариев нет: