Сообщений 29    Оценка 375 [+1/-1]         Оценить  
Система Orphus

Структура проектов на C++ с использованием Subversion и Mxx_ru

Автор: Евгений Охотников
Интервэйл

Источник: RSDN Magazine #1-2005
Опубликовано: 22.05.2005
Исправлено: 10.12.2016
Версия текста: 1.0
1. Введение
1.1. Что такое Mxx_ru
1.2. Что такое Subversion
2. Основные понятия
2.1. Проект, подпроект, модуль
2.2. Версия: поколение, ветвь, релиз
2.3. Репозитории
3. Управление проектом
3.1. Файловая структура проекта
3.2. Включение подпроектов в проект
4. Роль Mxx_ru
4.1. Расположение объектных файлов
4.2. Общие настройки всех подпроектов через build.rb
5. Заключение

1. Введение

Данная статья описывает предложения по организации файловой структуры проектов на C++ и компиляции проектов с помощью Mxx_ru, а также показывает, как использовать систему контроля версий Subversion не только в качестве инструмента для управления версиями исходных текстов, но и для отслеживания зависимостей между проектами.

Автор занимается экспериментами по выбору рациональной организации файловой структуры C++-проектов с 1995 года. За это время были опробованы несколько вариантов, каждый из которых был улучшением предыдущего. К 2003 году основная файловая структура была определена и с тех пор не изменялась. Представляется, что выбранная структура оказалась достаточно удачной. Даже переход на использование системы контроля версий Subversion не потребовал ее изменения. Более того, возможности Subversion упростили процедуру подключения в проект подпроектов.

Удобство разработки C++ проектов зависит не только от файловой структуры, но и от применяемых средств управления компиляцией. В этой области так же проводились эксперименты, результатами которых на данный момент является инструмент Mxx_ru. Важно, что развитие файловой структуры влияло на развитие инструментария для компиляции. И наоборот, возможности инструментария позволяли совершенствовать структуру каталогов проекта. Поэтому данная статья посвящена сразу двум эти аспектам, хотя, например, идею структуры каталогов проектов можно рассматривать отдельно не только от Mxx_ru, но и от языка C++.

1.1. Что такое Mxx_ru

Mxx_ru — это инструмент для управления компиляцией проекта, в чем-то аналогичный утилите make. Mxx_ru позволяет использовать один проектный файл для компиляции проекта на разных платформах разными компиляторами.

Основная идея Mxx_ru состоит в том, что проектный файл представляет собой небольшую программу на интерпретируемом языке программирования. В этой программе необходимо выбрать один из определенных в Mxx_ru сценариев (шаблонов) и уточнить частные детали (имена исходных файлов, имена результирующих файлов, расположение результирующих файлов и др.). Компиляция проекта выполняется в результате интерпретации проектного файла. За каждым из сценариев внутри Mxx_ru стоит управляющий код для конкретной платформы и конкретного компилятора. Интерпретация проектного файла приводит к выполнению этого кода и, соответственно, к построению необходимых результирующих файлов.

В Mxx_ru в качестве языка программирования используется язык Ruby.

1.2. Что такое Subversion

Subversion — это система контроля версий, аналогичная CVS, но лишенная некоторых недостатков CVS. Например, Subversion поддерживает атомарную операцию commit.

Subversion хранит все находящиеся под ее контролем файлы в централизованном хранилище — репозитории. Чтобы внести изменения в какой-либо файл, необходимо выполнить операцию check out в рабочуюкопию (рабочая копия — это каталог, в котором происходит работа с находящимися под управлением Subversion файлами на машине конкретного разработчика). Чтобы внесенные изменения попали в репозиторий, необходимо выполнить операцию check in (эта же операция называется commit) рабочей копии.

В отличие от ряда систем контроля версий, Subversion поддерживает идеологию copy-modify-merge. Взятые в рабочую копию операцией check out файлы не блокируются в репозитории. Другой разработчик также может выполнить check out для этих файлов. Subversion берет на себя задачу синхронизации независимых изменений при выполнении операции check in (commit).

Важной особенностью Subversion является поддержка метаинформации для находящихся под контролем версий файлов и каталогов. Эта информация называется в Subversion свойствами (properties). Некоторые свойства имеют зарезервированные имена и обрабатываются Subversion специальным образом. Описываемый далее способ управления C++ проектами базируется на использовании одного из таких специальных свойств — svn:externals.

2. Основные понятия

2.1. Проект, подпроект, модуль

Проект — это базовое понятие. Под проектом подразумевается совокупность исходных файлов, распределенных каким-то образом по подкаталогам проекта. Проект может содержать один или более проектный файл для построения из исходных текстов чего-либо, что может считаться целью проекта. Например, для C++ это может быть статическая библиотека, динамическая библиотека, исполнимый файл.

ПРИМЕЧАНИЕ

Проект может вообще не содержать проектных файлов, если целью проекта является предоставление исходных файлов другим проектам. Для С++ примерами таких проектов являются cpp_util_2, auto_ptr_3, большая часть библиотек boost и т.д., в которых код сосредоточен в inline-функциях и не требует отдельной компиляции.

Проект может нуждаться в подпроектах. Подпроект — это исходные тексты (и не только) другого проекта, импортированные в структуру каталогов проекта. Например, проект so_4 нуждается в исходных текстах проектов auto_ptr_3, cpp_util_2, memcheck_2, oess_1, threads_1 и др. Эти проекты объявляются подпроектами проекта so_4. При помощи специальной команды их исходные тексты копируются в каталог проекта so_4 и используются для компиляции so_4.

Проект состоит из одного или нескольких модулей. Каждый модуль представляет некую самостоятельную сущность, которая может выступать в качестве подпроекта в другом проекте. Например, проект oess_1 состоит из модулей: oess_1/defs, oess_1/io, oess_1/file, oess_1/stdsn, oess_1/db, oess_1/tlv и т.д. Включение всех модулей подпроекта oess_1, например, в проект so_4 может быть невыгодно, если проект so_4 использует функциональность только модуля oess_1/stdsn. В этом случае в качестве подпроекта so_4 использует модуль oess_1/stdsn.

2.2. Версия: поколение, ветвь, релиз

Версия — это некоторый идентификатор, который определяет функциональность проекта. Предлагается использовать идентификатор из трех составляющих: поколение (generation), ветвь (branch), релиз (release). Например, запись oess версии 1.1.4 говорит о релизе 4 ветви “2” первого поколения проекта oess.

Поколение и ветвь определяют степень несовместимости различных версий проектов. Версии абсолютно не совместимы, если не совпадает значение поколения. Т.е., если проект so использовал oess первого поколения, то для перехода на второе поколение oess потребуется серьезная переделка проекта so. Значение поколения следует изменять, если в проекте происходят фундаментальные изменения. Например, меняется идеология работы с проектом.

Поколение проекта является настолько важным показателем, что номер поколения следует включать в имя проекта. Так, название oess_1 сразу говорит о проекте “oess” первого поколения, название so_4 — о проекте “so” четвертого поколения и т.д.

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

Если в качестве имен ветвей использовать числовые значения, например, 4.0.3, 4.1.5, 4.2.7, то увеличение номера ветви говорит о создании новой версии, не гарантирующей 100% совместимости “сверху-вниз”.

СОВЕТ

В качестве имени ветви может быть удобно использовать символьные идентификаторы, например, собственное имя, данное этой ветви в процессе разработки. Так, при выпуске ветви проекта SObjectizerпод названием “Домбай” имени ветви можно присвоить значение “dombai”.

Релиз задается числовым значением. Номер релиза увеличивается при внесении очередных “значимых” изменений в проект. При этом, если версии проекта различаются только номером релиза, то должно гарантироваться, что версия с большим номером релиза обеспечивает 100% совместимость “сверху-вниз”. Т.е. переход, например, с версии 4.1.5 на 4.1.18 должен пройти без проблем. Должно быть достаточно импортировать новую версию подпроекта и перекомпилировать проект.

ПРИМЕЧАНИЕ

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

ПРЕДУПРЕЖДЕНИЕ

Версия с большим номером релиза не гарантирует обратной совместимости. Т.е., в общем случае, нельзя после использования, например, версии 4.1.18 перейти на использование версии 4.1.5.

2.2.1. Имя проекта и номер версии

Под именем проекта можно понимать имя некоторого глобального направления работ, в рамках которых будет создано несколько поколений и множество версий этого проекта. Например, имя oess может привести к существованию версий 1.0.* (звездочка в качестве номера релиза указывает, что номер релиза не имеет значения), 1.1.*, 1.2.*, 2.0.* и т.д. Поэтому, когда используется имя oess, следует указывать, о какой конкретно версии или поколении идет речь. Например, oess v.1.1.4 или oess v.2.*.*.

Поскольку номер поколения нужно включать в имя проекта, то имя oess_1 уже говорит о всех версиях проекта oess первого поколения. Для уточнения версии после имени проекта можно указывать имя ветви и номер ревизии. Например, oess_1.1.4 — это то же самое, что oess v.1.1.4. Или so_4.dombai.4 — то же самое, что so v.4.dombai.4.

2.2.2. Имена пространств имен и номера версий

Если включать номер поколения в имена пространств имен, например, oess_1::defs, so_4::rt и т.д., то можно получить интересный эффект: два разных поколения одного проекта могут совместно использоваться в третьем проекте. Например, пусть проект message_board_1 использует подпроект parallel_vm_1, которому необходим oess_1. Так же проект message_board_1 использует проект reliable_messaging_4, который нуждается в oess_2. Если исходные тексты проектов oess_1 и oess_2 находятся в разных каталогах (скажем, message_board_1/dev/oess_1 и message_board_1/dev/oess_2), то нет проблем с директивами #include:

          #include <oess_1/defs/h/defs.hpp> 
#include <oess_2/defs/h/defs.hpp> 

Если же при этом в каждом проекте будет всеобъемлющее пространство имен самого верхнего уровня, и в имя этого пространства имен входит номер поколения, то нет проблем и с совместным использованием символов из разных пространств имен:

          try 
{ 
    std::auto_ptr< oess_1::db::cln::db_t > old_db( 
      open_old_db( old_db_name ) ); 

  std::auto_ptr< oess_2::db::cln::db_t > new_db( 
    create_new_db( new_db_name ) ); 
    ... 
} 
catchconst oess_1::ex_t & x ) 
{ ... } 
catchconst oess_2::defs::ex_t & x ) 
{ ... } 

2.3. Репозитории

Система контроля версий Subversion хранит все содержимое проекта в репозитории — собственном централизованном хранилище. Репозиторий в Subversion может содержать один или несколько проектов. Выбор того, сколько проектов будет находиться в одном репозитории, зависит от ряда объективных и субъективных факторов:

Далее предполагается, что каждый проект находится в отдельном репозитории.

Доступ разработчика к разным репозиториям может быть ограничен. Например, разработчик может иметь read-write доступ к репозиторию, в котором он ведет собственный проект, и read-only доступ к репозиториям, из которых он может только импортировать необходимые ему подпроекты.

2.3.1. Включение в имя репозитория номера версии

Изменение номера поколения проекта в версии является очень серьезным шагом. Даже с технической точки зрения, увеличение номера поколения будет означать изменение имен подкаталогов, в которых находятся исходные тексты проекта, a так же изменение имен пространств имен этого проекта. А это будет означать, что произойдут изменения в очень большой части исходных текстов самого проекта (практика показывает, что практически во всех исходных файлах, т.к. везде используются директивы #include и определения пространств имен).

С точки зрения системы контроля версий важно понимать, что выделение нового поколения проекта из текущего поколения приведет к модификации практически всех файлов проекта. Т.е. если, скажем, oess_2 создается путем операции copy текущего состояния oess_1, то после копирования все файлы oess_2 будут отличаться от самих себя в oess_1. Это означает, что даже несмотря на то, что для Subversion файлы в oess_2 будут выведены из oess_1 (т.е. иметь общий корень в истории Subversion), выполнение последующих операций merge будет сопряжено с большими трудностями. Часть этих трудностей будет вызвана большими различиями в исходных текстах (например, изменятся все вызовы функций или объявления объектов из-за изменения имени самого верхнего пространства имен), другая часть — из-за изменения расположения файлов (ведь изменятся и имена каталогов).

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

3. Управление проектом

3.1. Файловая структура проекта

Для C++ проектов рекомендуется следующая файловая структура рабочего каталога:

  workspace_name/ 
  ‘--dev/ 
     |  *.exe, *.dll, *.rb (для компиляции и настройки всего проекта) 
     |--lib/ *.lib 
     |--project/ 
     |  |--module-1/ *.cpp, *.rc, *.rb 
     |  |  |--h/ *.hpp, *.h 
     |  |  ‘--o/ *.obj, *.res 
     |  |--module-2/ *.cpp, *.rc, *.rb 
     |  |  |--h/ *.hpp, *.h 
     |  |  ‘--o/ *.obj, *.res 
     |  |  ... 
     |  ‘--module-N/ *.cpp, *.rc, *.rb 
     |     |--h/ *.hpp, *.h 
     |     ‘--o/ *.obj, *.res 
     |--sub_project_1/ 
     |  |--module-1/ 
     |  |  |--h/ 
     |  |  ‘--o/ 
     |  |  ... 
     |  ‘--module-N/ 
     |     |--h/ 
     |     ‘--o/ 
     |  ... 
     ‘--sub_project_N/ 
        |--module-1/ 
        |  |--h/ 
        |  ‘--o/ 
        |  ... 
        ‘--module-N/ 
           |--h/ 
           ‘--o/ 

Каталог dev/, в котором располагаются подкаталоги с исходными текстами проектов и подпроектов, называется корневым каталогом проекта. Корневой каталог используется чаще всего, т.к. именно в нем инициируется компиляция проекта, в него помещаются исполнимые файлы и DLL (на платформе Windows).

3.1.1. Подробнее о корневом каталоге проекта

Работа с C++-проектами выглядит как исправление исходных текстов, компиляция проекта, запуск скомпилированных приложений (под отладчиком или без) и анализ результатов работы приложений проекта. После этого итерация повторяется. Если проект состоит только из одного приложения, размещение результирующего исполнимого (exe-) файла может не иметь значения. Например, при работе в Microsoft Visual Studio принято создавать подкаталоги Release (для release-режима компиляции) и Debug (для debug-режима компиляции) в том каталоге, который содержит проектный файл. И в эти каталоги помещаются результаты компиляции в том или ином режиме (включая объектные файлы, статические и динамические библиотеки, а так же exe-файлы). Такое размещение результатов компиляции в чем-то удобно — можно легко переключаться между debug- и release- режимами без ожидания перекомпиляции всего проекта. А если и запуск приложения выполняется из среды разработки, то расположение получившегося exe-файла вообще, казалось бы, не имеет значения.

Ситуация усложняется, если, например:

В результате получается, что удобно иметь один корневой каталог проекта, в который помещаются скомпилированные и слинкованные исполнимые файлы (exe- и dll- в терминологии Windows). В этом же каталоге находятся подкаталоги с исходными текстами, с конфигурационными файлами, подкаталоги для других результатов компиляции (например, статических библиотек и библиотек импорта), подкаталоги для приема результатов работы приложений (log/trace-файлов, вспомогательных баз данных, tmp-файлов и т.д.).

При работе в среде, где разработчику доступна только командная строка (большинство Unix-ов без X-Windows), такая организация проекта очень удобна. Находясь в одном и том же каталоге, разработчик может запустить компиляцию, увидеть ее результаты, править любой файл в текстовом редакторе, запустить скомпилированное приложение на выполнение, просмотреть результаты работы и т.д.

При необходимости работы над одним проектом сразу для нескольких платформ такой подход позволяет разработчику более комфортно себя чувствовать. Ведь практически на любой, даже экзотической платформе, можно создать одинаковое окружение, которое состоит в основном из shell-а (командного процессора) и любимого редактора (в идеале также кроссплатформенного, вроде vim или emacs). Это может выглядеть очень примитивно для многих Windows-программистов, которые привыкли к RAD-средствами типа Visual Studio или C++Builder. Но практика показывает, что при разработке не GUI-приложений такой подход ничуть не хуже, а может и лучше. Особенно при использовании мощных текстовых редакторов, в которых реализована такая функциональность, как автоматическая подсказка и автоматическое дополнение.

3.2. Включение подпроектов в проект

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

3.2.1. Включение подпроектов в виде исходных текстов

Существуют два принципиально разных подхода к включению подпроекта в проект. Первый подход, широко распространенный в Unix, состоит в том, что подпроект сначала инсталлируется как самостоятельный проект. А затем путь к его файлам либо прописывается через переменные среды INCLUDE и LIB или опции компилятора/линкера, либо файлы подпроекта после компиляции инсталлируются в специально отведенные для этого места (/usr/local/include, /usr/local/lib).

Например, пусть проект so_4 нуждается в проекте oess_1. Для включения oess_1 в so_4 придется:

Такой подход приемлем в большинстве Unix-систем, где определены специальные каталоги для заголовочных файлов, библиотек, динамических библиотек и исполняемых файлов, и где, как правило, существует всего один C++ компилятор.

Но даже под Unix ситуация усложняется, если, например:

На Windows-платформах ситуация еще серьезнее. Во-первых, под Windows не определено стандартных путей для размещения заголовочных файлов и библиотек. Поэтому каждый подпроект нужно будет устанавливать отдельно и прописывать все пути к нему в переменных среды INCLUDE, LIB или в опциях компилятора/линкера. Во-вторых, под Windows, как правило, нет единственного, штатного C++ компилятора. Несмотря на широкую распространенность Visual C++, на платформе Windows есть ряд серьезных конкурентов Visual C++, как коммерческих, так и open-source. Не так уж редки случаи, когда на одной машине установлены сразу несколько компиляторов или версий одного компилятора. В такой ситуации каждый проект со всеми своими подпроектами можно скомпилировать сначала одним компилятором, а затем, при необходимости, другим. Если проект и его подпроекты располагаются отдельно, то такая поочередная компиляция всего, что нужно проекту, может сильно осложнить жизнь программисту.

Поэтому предлагается включать в проект исходные тексты всех используемых подпроектов. Т.е., если so_4 нуждается в oess_1, то в so_4 входят не только исходные тексты oess_1, но и исходные тексты всех подпроектов, в которых нуждается oess_1 и т.д.

Такой подход, естественно, обладает своими недостатками:

Однако, как будет показано ниже, частично эти недостатки преодолеваются с помощью специального свойства svn:externals, которое особым образом обрабатывается Subversion.

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

3.2.2. Автоматическое подключение исходных текстов подпроектов средствами Subversion

Если подпроекты включаются в проект в виде исходных текстов, то возникают, например, такие вопросы:

Subversion уже содержит удобное решение для указанных вопросов. Это решение — использование свойства svn:externals. Свойство svn:externals задается для каталога и представляет собой множество строк. В каждой строке указываются имя подкаталога и URL svn-репозитория, из которого нужно взять содержимое подкаталога (более подробную информацию о свойстве svn:externals можно получить из Subversion Book. При выполнении операций check out и export Subversion специальным образом обрабатывает каталоги, для которых задано свойство svn:externals — Subversion создает указанные в свойстве подкаталоги и помещает в них содержимое, взятое по указанным URL.

Ранее было показано, что исходные тексты подпроектов размещаются в корневом каталоге проекта, и для каждого подпроекта создается каталог с именем этого подпроекта. Например, для oess_1.4.0-b1 структура каталогов выглядит следующим образом (показаны только подкаталоги, которые содержат исходные тексты подпроектов и самого проекта oess_1.4.0-b1):

  ‘--dev/ 
     |--ace/ 
     |--args_4/ 
     |--auto_ptr_3/ 
     |--cls_2/ 
     |--libpcre++/ 
     |--oess_1/ 
     |--pcre/ 
     |--smart_ref_3/ 
     ‘--threads_1/ 

Такая организация исходных текстов позволяет задавать свойство svn:externals для корневого каталога проекта — для каталога dev. Так, для dev/ из oess_1.4.0-b1 свойство svn:externals имеет значение:

args_4 http://svn.intervale.ru/svn/args_4/tags/4.4/dev/args_4 
auto_ptr_3 http://svn.intervale.ru/svn/auto_ptr_3/tags/3.2/dev/auto_ptr_3 
cls_2 http://svn.intervale.ru/svn/cls_2/tags/2.7/dev/cls_2 
cpp_util_2 http://svn.intervale.ru/svn/cpp_util_2/tags/2.2/dev/cpp_util_2 
smart_ref_3 http://svn.intervale.ru/svn/smart_ref_3/tags/3.1/dev/smart_ref_3 
threads_1 http://svn.intervale.ru/svn/threads_1/tags/1.4/dev/threads_1 
ace http://svn.intervale.ru/svn/ace/tags/5.4.3/dev/ace 
pcre http://svn.intervale.ru/svn/pcre/tags/4.5/dev/pcre 
libpcre++ http://svn.intervale.ru/svn/libpcre++/tags/0.9/dev/libpcre++ 

Такое значение свойства svn:externals предписывает Subversion взять, например, содержимое каталога ace из репозитория svn.intervale.ru/svn/ace, причем из ветви tags/5.4.3. При выполнении операций check out и export Subversion автоматически возьмет оттуда содержимое каталога ace.

Важно отметить, что при выполнении операции check out Subversion создает для подпроекта рабочую копию в подкаталоге, указанном в svn:externals. Так, после выполнения check out для oess_1.4.0-b1, каталог dev/ace будет представлять собой рабочую копию для ветви tags/5.4.3/dev/ace из репозитория svn.intervale.ru/svn/ace. Но при этом рабочая копия подпроекта ace располагается внутри рабочей копии проекта oess_1, как будто исходные тексты ACE были импортированы в репозиторий oess_1.

То, что для каждого подпроекта, подключенного через svn:externals, при выполнении check out создается рабочая копия, приводит к двум важным следствиям:

3.2.3. Использование проекта в качестве подпроекта

Если возникает задача использовать проект (который сам использует несколько подпроектов) в качестве подпроекта в другом проекте, то нужно решить два основных вопроса:

  1. Как указать проект в svn:externals другого проекта?
  2. Как отследить зависимости проекта при подключении в другой проект?

Первый вопрос разрешается просто. Как показано выше, исходные тексты проекта должны располагаться в подкаталоге с именем проекта в корневом каталоге проекта. Так, для проекта oess_1 его исходные тексты должны размещаться в dev/oess_1, для проекта ace – в dev/ace, для проекта smart_ref_3 – в каталоге dev/smart_ref_3, и т.д. Такая схема легко позволяет указывать в svn:externals нужные каталоги и URL репозиториев. Например, если в проекте so_4 требуется использовать oess_1.3, то это означает, что в корневом каталоге so_4 должен быть подкаталог oess_1. А содержимое этого подкаталога должно быть взято из ветви tags/1.3/dev/oess_1 репозитория проекта oess_1.

Ситуация со вторым вопросом гораздо сложнее. Проблема может быть в том, что подпроекты могут конфликтовать на уровне версий своих подпроектов. Например, oess_1 нуждается в threads_1.4, а проект so_4 – в threads_1.5. Проект threads_1.5 не обеспечивает полной совместимости с threads_1.4, поэтому он не может использоваться совместно с oess_1. В то же время so_4 нуждается в возможностях threads_1.5, которых еще нет в threads_1.4. Нужно отметить, что такой конфликт может быть автоматически диагностирован с помощью специальных инструментов, но не может быть автоматически разрешен – здесь потребуется участие программиста.

Еще одна сложность в том, что зависимости подпроекта не распространяются автоматически на проект, использующий этот продпроект. Все зависимости всех продпроектов, включая подпроекты подпроектов и т.д., нужно собрать в одно свойство svn:externals.

Хорошо было бы, чтобы эти вопросы автоматически разрешались с помощью некоторого набора правил и инструмента, который по этим правилам сможет построить значение svn:externals. Пока же такого инструмента нет, ответственность за корректное формирование svn:externals лежит на программисте.

3.2.4. Использование отдельных модулей проекта в качестве подпроекта

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

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

Достигается это также с помощью свойства svn:externals. Принцип практически тот же, что и в предыдущем разделе. Но нужно учитывать некоторые описанные ниже особенности.

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

Например, проект oess_1 состоит из следующих модулей: db, defs, file, io, scheme, stdsn, tlv. На уровне файловой системы эти модули организованы следующим образом:

  ‘--dev/ 
     ‘--oess_1/ 
        |--db/ 
        |--defs/ 
        |--file/ 
        |--io/ 
        |--scheme/ 
        |--stdsn/ 
        ‘--tlv/ 

т.е. все файлы каждого модуля собраны в подкаталоге с именем этого модуля.

Такая организация позволяет легко указывать пути к нужным модулям в свойствах svn:externals тех проектов, которые используют эти модули в качестве подпроектов. Например, если проекту so_4 необходим модуль oess_1/stdsn (который зависит от модулей oess_1/defs, oess_1/io) из oess_1.3, то в svn:externals будут указаны следующие URL:

  oess_1/defs  http://svn.intervale.ru/svn/oess_1/tags/1.3/dev/oess_1/defs 
  oess_1/io    http://svn.intervale.ru/svn/oess_1/tags/1.3/dev/oess_1/io 
  oess_1/stdsn http://svn.intervale.ru/svn/oess_1/tags/1.3/dev/oess_1/stdsn 

Основной проблемой при формировании svn:externals в проекте, использующем отдельные модули своих подпроектов, является необходимость отслеживания зависимостей между модулями и необходимыми им подпроектами. Так, в приведенном выше примере программист откуда-то должен был узнать, что модуль oess_1/stdsn зависит от модулей oess_1/defs и oess_1/io. Например, это должно было быть указано в документации по oess_1. Или выяснено экспериментальным путем. Также программист должен узнать, какие из подпроектов проекта oess_1 должны быть включены для модулей oess_1/defs, oess_1/io, oess_1/stdsn. Например, проект oess_1 нуждается в подпроектах ace, args_4, auto_ptr_3, cls_2, cpp_util_2, libpcre++, pcre, smart_ref_3, threads_1. Но это не означает, что все эти подпроекты нужны модулям oess_1/defs, oess_1/io, oess_1/stdsn. Т.е. программист как-то должен узнать, какие из этих подпроектов ему нужно включить в свойство svn:externals.

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

4. Роль Mxx_ru

Из описанных выше правил именования проектов, формирования номеров версий проектов и организации файловой структуры проектов не видно, какую же роль в этом играет Mxx_ru? Ответ прост: никакую.

Все, что было описано выше, может применяться к C++-проектам вне зависимости от инструментов, с помощью которых они разрабатываются. Более того, описанные правила могут относиться не только к C++ проектам, но и, например, к проектам на Perl, Python или Ruby.

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

4.1. Расположение объектных файлов

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

Можно рассмотреть несколько простейших стратегий расположения объектных файлов и их недостатки:

При разработке Mxx_ru большое внимание было уделено удобству управления расположением объектных файлов. В Mxx_ru даже выделено отдельное понятие obj_placement и предоставляются гибкие средства по настройке необходимого проекту способа расположения объектных файлов.

На данный момент в Mxx_ru реализовано два штатных способа размещения объектных файлов:

Например, если есть дерево каталогов:

  |-- engine 
  |-- interface 
  |   |-- high 
  |   ‘-- low 
  ‘-- monitor 

и используется Source_subdir_obj_placement, то после компиляции дерево каталогов примет вид:

  |-- engine 
  |   ‘-- o 
  |-- interface 
  |   |-- high 
  |   |   ‘-- o 
  |   |-- low 
  |   |   ‘-- o 
  |   ‘-- o 
  ‘-- monitor 
      ‘-- o 

Если же используется Runtime_subdir_obj_placement с подкаталогом output в качестве корневого и с режимом компиляции debug, то получится следующее дерево каталогов:

  |-- engine 
  |-- interface 
  |   |-- high 
  |   ‘-- low 
  |-- monitor 
  ‘-- output 
      ‘-- debug 
          |-- engine 
          |-- interface 
          |   |-- high 
          |   ‘-- low 
          ‘-- monitor 

Другие способы управления размещением объектных файлов могут быть легко получены путем создания Ruby-классов, производных либо от базового класса Mxx_ru::Cpp::Obj_placement, либо от уже существующих классов Mxx_ru::Cpp::Source_subdir_obj_placement, Mxx_ru::Cpp::Runtime_subdir_obj_placement.

4.2. Общие настройки всех подпроектов через build.rb

Одной из целей включения подпроектов в проект в виде исходных текстов является возможность общей компиляции всего проекта, со всеми его подпроектами. При этом для компиляции должны быть выбраны общие и, главное, согласованные настройки компиляции и линковки. Например, одинаковый тип компиляции (release или debug), одинаковый тип run-time библиотеки (статическая или динамическая), одинаковый режим поддержки многопоточности (многопоточное приложение или нет) и т.д. При этом типы параметров можно разделить на следующие категории:

Локальные, которые распространяются только на один подпроект и не оказывают влияния на другие подпроекты всего проекта. Например, такими параметрами являются define-символы для управления экспортом/импортом классов из DLL.

Глобальные, которые распространяются на весь проект. Например, таким параметром является режим компиляции (release или debug).

Глобальные параметры могут назначаться двумя способами:

Распространяемые, (upspread), которые подпроект распространяет на подпроекты, прямо или косвенно использующие его. Но эти параметры не оказывают влияния на остальные части проекта. Например, при использовании библиотеки поддержки регулярных выражений pcre можно определить define-символы PCRE_STATIC и SUPPORT_UTF8 при компиляции pcre. Эти же символы должны быть определены и при компиляции использующих pcre подпроектов, но не должны быть определены для других подпроектов, которые про pcre вообще ничего не знают. В этом случае определение PCRE_STATIC и SUPPORT_UTF8 производится в качестве распространяемых параметров подпроекта pcre.

Mxx_ru позволяет устанавливать в проектных файлах подпроектов (которые обычно называются prj.rb) локальные, глобальные и распространяемые параметры. Но указания значения параметров в локальном проектом файле мало. Нужно согласовать значения глобальных и распространяемых параметров для всех подпроектов в проекте. Для этого все используемые подпроекты должны быть перечислены в специальном, глобальном, проектном файле, который должен иметь особый Mxx_ru-псевдоним — Mxx_ru::BUILD_ROOT, и имя build.rb. Файл build.rb должен располагается в корневом каталоге проекта, и именно в этом каталоге должна осуществляться компиляция проекта.

Когда инициируется компиляция проекта или одного из подпроектов, Mxx_ru загружает и обрабатывает все проектные файлы для перечисленных в build.rb подпроектов. В процессе этой обработки формируются значения глобальных параметров для всего проекта, и проверяется их согласованность. Если установленные разными проектами значения одного глобального параметра противоречат друг другу, то компиляция прерывается. Если же нет рассогласования глобальных параметров, то выполняется компиляция либо всего проекта, либо указанного подпроекта.

5. Заключение

Описанные выше принципы именования проектов и их версий, организации структуры каталогов проектов, управления зависимостями проектов с помощью Subversion и компиляции проектов средствами Mxx_ru успешно применяются в компании Интервэйл для работы с более чем ста C++ проектами разного объема. Большую их часть составляют небольшие C++ библиотеки, которые используются в нескольких (порядка десяти) крупных проектах.

Практика показала, что описанный подход вполне жизнеспособен. Его основным недостатком на данный момент является необходимость ручного заполнения свойства svn:externals и ручного отслеживания зависимостей и совместимости между проектами и их версиями.

Однако для проектов малого и среднего объема этот недостаток не так серьезен, как может показаться на первый взгляд. Действительно, когда проект зависит не более чем от пяти-десяти подпроектов, формирование svn:externals не представляет проблемы. В крупных проектах, которые нуждаются более чем в десяти-пятнадцати подпроектах, это действительно сложно. Но и здесь влияние данного недостатка оказывается не таким серьезным, как могло бы быть. Во-первых, это связано со статичностью больших проектов — изменения их списков зависимостей происходят не часто. Поэтому суммарные временные затраты на заполнение svn:externals в общем объеме времени получаются очень небольшими. Во-вторых, во многих случаях все равно требуется вмешательство программиста для разрешения конфликтов между несовместимыми подпроектами и выбора их согласованных версий из числа доступных.

Тем не менее, данный недостаток требует принципиального разрешения, для чего необходим набор правил по описанию зависимостей проектов и автоматизированный инструмент для обработки этих описаний. А это означает, что эксперименты по совершенствованию структуры C++-проектов нужно продолжать. И что описанный в данном решении подход может быть значительно усовершенствован.


Эта статья опубликована в журнале RSDN Magazine #1-2005. Информацию о журнале можно найти здесь
    Сообщений 29    Оценка 375 [+1/-1]         Оценить