Использование bpg-файлов для пакетной компиляции

© 2005, Иван Равин

При работе с большими проектами появляется потребность в их разбиении на более мелкие специализированные модули. Например, нередко "релиз" программы включает в себя исполняемый файл, а также набор библиотек. Это могут быть динамически загружаемые библиотеки (dll), ActiveX библиотеки, пакеты (bpl).

Delphi предоставляет механизм группировки проектов, собирая их в Project Group – файлы (.bpg и .bdsgroup). Открыв такой файл, можно собрать большой проект, выполнив команду Project -> Build All Projects. При должной настройке каталогов в проектах (см. например статью на DelphiPlus.org) мы в результате сборки получаем готовый релиз, из которого дальше можно готовить дистрибутив.

Казалось бы, что еще надо? А надо исключить "ручной" этап подготовки дистрибутива. Сборка файлов, генерация баз могут быть выполнены с помощью bat – файлов и скриптов. Средства дополнительной обработки модулей (например, установка защиты, архивирование, сборка инсталлятора) также могут работать из командной строки. А вот выполнение команды Build All Projects требует открытия Delphi, да еще, чтобы у текущего пользователя были установлены все необходимые компоненты (даже если они есть на компьютере, это еще не значит, что они есть на палитре у конкретного пользователя). Плюс, возможно, необходима ручная установка флагов компиляции для того или иного вида дистрибутива. Все это можно автоматизировать, написав пакетный файл для компиляции нашей группы проектов с помощью компилятора командной строки dcc32.exe.

Но и тут возникают проблемы: надо будет отслеживать соответствие между нашей группой проектов и этим bat-файлом. Лучше всего исключить эту зависимость, написав универсальный bat-файл, который будет использовать информацию из файла группы проектов. Взглянем на такой файл (дальше речь пойдет об использовании групп проектов до Delphi7 включительно. Начиная с Delphi8 формат файлов изменился, но несложно написать программу для преобразования xml в bpg файл. Возможно, стоит попробовать применить результаты данной статьи с использованием XML средств сборки, например, Ant.

Итак, откроем в текстовом редакторе простейший файл bpg:

#------------------------------------------------------------------------------
VERSION = BWS.01
#------------------------------------------------------------------------------
!ifndef ROOT
ROOT = $(MAKEDIR)\..
!endif
#------------------------------------------------------------------------------
MAKE = $(ROOT)\bin\make.exe -$(MAKEFLAGS) -f$**
DCC = $(ROOT)\bin\dcc32.exe $**
BRCC = $(ROOT)\bin\brcc32.exe $**
#------------------------------------------------------------------------------
PROJECTS = Project1.exe
#------------------------------------------------------------------------------
default: $(PROJECTS)
#------------------------------------------------------------------------------

Project1.exe: Project1.dpr
  $(DCC)

Кто хоть раз работал с утилитой MAKE, сразу скажет, что это make-скрипт. Что мы тут видим:

  1. Несколько макроопределений:
  2. Действие, которое необходимо выполнить, когда MAKE запущена без конкретного задания (default) – в данном случае выполняется сборка всех проектов, указанных в макроопределении PROJECTS.
  3. Явные правила для сборки каждого из проектов: указывается целевой файл, базовый файл и действия, для получения результата из базового файла. Как видно, происходит запуск компилятора dcc32.exe с параметром, передаваемым через специальную макрокоманду $**, обозначающую базовый файл правила. Например, для проекта Project1.dpr вызывается компилятор dcc32 с параметром Project1.dpr.

Вот и решение: нужно выполнить команду "make -f ProjectGroup1.bpg" и группа скомпилируется.

Но не тут-то было. На первом же проекте вы получите ошибку, что компилятор не может найти тот или иной файл. Скорее всего, это VCLx0 или файлы от компонент, которые применяются в вашем проекте. Почему же все компилируется в Delphi? А потому, что Delphi знает, где искать нужные файлы: в путях из переменной среды Library Path, значение которой записано в реестре.

Средства командной строки не позволяют читать реестр Windows, поэтому создадим make-файл Compsearch.mki, куда скопируем значение ключа "HKCU\Software\Borland\Delphi\x.0\Library\Search Path".

Можно этот файл структурировать, например так:

< Compsearch.mki >
BROOT = c:\borland\delphi5
BORLAND = $(BROOT)\Objrepos;$(BROOT)\Projects\Bpl
ICS = C:\VCL\ics\DELPHI\D5
MIDWARE = C:\VCL\midware\DELPHI\D5
JVCL = C:\VCL\jvcl\lib\D5;C:\VCL\jcl\lib\D5;C:\VCL\jvcl\common

LIBPATH = $(JVCL);$(ICS);$(MIDWARE);$(BORLAND)
RESPATH = C:\VCL\JVCL\Resources

Вернемся к нашей группе проектов. Добавим несколько строк. Вот кусочек текста:

...
#------------------------------------------------------------------------------
MAKE = $(ROOT)\bin\make.exe -$(MAKEFLAGS) -f$**
DCC = $(ROOT)\bin\dcc32.exe $**
# файл с путями к библиотекам
!include CompSearch.mki
# Корень проекта
ROOTDIR = c:\work
# каталог с этим файлом и файлом compile.bat
SRCROOTDIR = $(ROOTDIR)\source
# Опции компилятора
COMPILER = 5
DCP_DIR = $(ROOTDIR)\DCU
DCU_DIR = $(ROOTDIR)\DCU\$(COMPILER)
DCCOPT = -LN$(DCP_DIR) -i$(LIBPATH) -n$(DCU_DIR) -u$(DCU_DIR);$(LIBPATH) \
      -r$(RESPATH) \
      -w- -h- -b -q
# Переопределение стандартного макроса DCC
DCC = $(DCC) $(DCCOPT)
BRCC = $(ROOT)\bin\brcc32.exe $**
#------------------------------------------------------------------------------
...
  1. Мы добавили макроопределения путей к библиотекам: !include CompSearch.mki;
  2. Определили ряд дополнительных рабочих каталогов ROOTDIR, DCP_DIR, DCU_DIR – в данном случае DCP-файлы попадают в c:\work\DCU, а DCU-файлы в c:\work\DCU\5;
  3. Дополнили определение макроса DCC опциями компилятора (я включил флаги компиляции -w- -h- -b –q, чтобы подавить лишний вывод – будем считать, что отладка проектов была произведена в интерактивном режиме, при сборке релиза нас уже не интересуют предупреждения компилятора)

Если вы создали заранее все каталоги (DCP_DIR, DCU_DIR, а также те, что указаны в Output Directory проектов), то, возможно, этого будет достаточно для компиляции вашей группы проектов. При этом Delphi по-прежнему сможет работать с вашим bpg-файлом, не изменяя введенной вами информации. Но, если проекты лежат в отдельных каталогах, компиляция не будет выполнена.

Посмотрим на правило из более сложного файла bpg:

Doc5.bpl: ..\Common\Document\Doc5.dpk
  $(DCC)
Посмотрим также на сам файл проекта Doc5.dpk. Оказывается, что файлы записаны в проекте с относительными путями, поэтому, чтобы сработал макрос DCC, необходимо перейти в каталог проекта, выполнить компиляцию и вернуться обратно, например, так:
Doc5.bpl: ..\Common\Document\Doc5.dpk
  CD Common\Document
  $(DCC)
  CD ..\..\

Можно ли переопределить макрос DCC так, чтобы не писать эти строки? Все макросы представляют собой "строчные" команды. В то же время выполнение 3 DOS-команд в одной строке невозможно. Поэтому мы не сможем нужным образом переопределить макроподстановку DCC, и нам придется добавлять строчки с командой CD в каждое make-правило. При большом количестве проектов это неудобно, да и неизвестно, сможет ли Delphi работать с таким файлом.

Есть ли выход?

В Project Jedi VCL была создана специальная утилита конвертации Bpg2make, но, как оказывается, можно обойтись и без нее, более того, bpg файл можно использовать и для пост-обработки проектов, например, установки защиты на скомпилированные модули. Как окажется далее, достаточно будет добавить только 1 строчку в bpg-файл.

Идея заключается в подмене макроса DCC таким образом, чтобы в нем вызывался bat-файл, который:

  1. перейдет в нужный каталог
  2. выполнит компиляцию
  3. вернется в прежний каталог

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

DCC = compile.bat $** $(SRCROOTDIR)
В результате, в файл compile.bat с помощью параметра №1 будет передан базовый файл правила. Например, в приведенном выше примере при сборке Doc5.bpl make "развернет" этот вызов в следующий:
compile.bat ..\Common\Document\Doc5.dpk c:\work\Source
Остается в этом bat-файле вычислить каталог, перейти в него и выполнить компиляцию проекта. Обратный переход был бы нужен, если бы вызов выглядел так:
DCC = call compile.bat ..\Common\Document\Doc5.dpk
В данном же случае командный интерпретатор избавляет нас от необходимости обратного перехода. Однако средствами командной строки сложно из строки выделить путь. Оказывается, это может сделать утилита Make с помощью модификаторов макроса $**:

Как ими воспользоваться? Предлагается из пакетного файла Compile.bat снова вызвать make.exe, передав через параметр имя универсального make-файла, умеющего собирать любой проект. Итак, bat-файл оказался несложным (тут нам понадобится параметр №2 – каталог с файлом compile.mak):

<compile.bat>
make.exe -f %2\compile.mak %1dummy
@if not errorlevel 1 goto exit
exit 1
:exit
(файл содержит средства прерывания при ошибках – exit 1; расширение базового файла дописывается символами "dummy").

Осталось подготовить универсальный скрипт compile.mak, который содержит правила сборки проектов, заканчивающихся расширением dprdummy и dpkdummy:

<compile.mak>
# неявное правило для всех файлов с расширением dprdummy
.dpr.dprdummy:
  cd $(**D)
  $(ROOT)\bin\dcc32.exe $(**F) $(DCCOPT)

# неявное правило для всех файлов с расширением dpkdummy
.dpk.dpkdummy:
  cd $(**D)
  $(ROOT)\bin\dcc32.exe $(**F) $(DCCOPT)

Итак, что же мы получим при сборке нашего примера?

  1. В ProjectGroup1.bpg правило для Doc5.bpl развернется в вызов
    Compile.bat ..\Common\Document\Doc5.dpk c:\work\source
  2. В Compile.bat команда будет развернута следующим образом:
    make.exe -f c:\work\source\compile.mak ..\Common\Document\Doc5.dpkdummy
  3. В файле compile.mak правило для dpkdummy будет развернуто следующим образом:
    cd ..\Common\Document
    $(ROOT)\bin\dcc32.exe Doc5.dpk $(DCCOPT)
(я не стал разворачивать макроопределения ROOT и DCCOPT)

Согласно неявному правилу .dpk.dpkdummy, для целевых файлов с расширением dpkdummy ищутся базовые файлы с тем же именем, но расширением dpk, поэтому макроопределение $(**F) развернулось в нужную нам строку Doc5.dpk. Задача выполнена.

В этих же неявных правилах можно описать обработку флагов компиляции и пост-обработку откомпилированных файлов.

Остается одна хитрость. В начале данной статьи было показано, как в bpg файл добавить нужные каталоги. Пока этот файл один, проблем нет. Когда их много, почему бы не сделать все это один раз в каком-нибудь include-файле? Например, в уже известном нам compile.mak. Заодно можно в нем же переопределить макрос DCC. Вот что получается:

<compile.mak>

#------------------------------------------------------------------------------
# Вставить в .bpg после объявления DCC, и до описания правил:
# !include c:\work\source\compile.mak
#------------------------------------------------------------------------------
ROOTDIR = c:\work
# каталог с этим файлом и файлом compile.bat
SRCROOTDIR = $(ROOTDIR)\source
# файл с путями к библиотекам
!include $(SRCROOTDIR)\CompSearch.mki
# Переопределение стандартного макроса DCC
DCC = $(SRCROOTDIR)\compile.bat $** $(SRCROOTDIR)
#------------------------------------------------------------------------------
# Опции компилятора
COMPILER = 5
DCP_DIR = $(ROOTDIR)\DCU
DCU_DIR = $(ROOTDIR)\DCU\$(COMPILER)
DCCOPT = -LN$(DCP_DIR) -i$(LIBPATH) -n$(DCU_DIR) -u$(DCP_DIR);$(LIBPATH) \
      -r$(RESPATH) \
      -w- -h- -b -q
#------------------------------------------------------------------------------
# неявное правило для всех файлов с расширением dprdummy
.dpr.dprdummy:
cd $(**D)
$(ROOT)\bin\dcc32.exe $(**F) $(DCCOPT)
# неявное правило для всех файлов с расширением dpkdummy
.dpk.dpkdummy:
cd $(**D)
$(ROOT)\bin\dcc32.exe $(**F) $(DCCOPT)

Сюда же можно включить содержимое файла Compsearch.mki, но я предпочитаю держать его отдельно, поскольку пути установки библиотек на разных компьютерах могут различаться. Структура же рабочих каталогов при использовании репозитория будет на всех компьютерах одинаковой (ROOTDIR, SRCROOTDIR и т.д).

Итак, один раз готовим файлы Compsearch.mki, Compile.bat и Compile.mak (все в каталоге ROOTDIR) – и проблема автоматической компиляции решена. Остается добавить в ваш bpg-файл единственную строчку (где-то после определения DCC):

#------------------------------------------------------------------------------
VERSION = BWS.01
#------------------------------------------------------------------------------
!ifndef ROOT
ROOT = $(MAKEDIR)\..
!endif
#------------------------------------------------------------------------------
MAKE = $(ROOT)\bin\make.exe -$(MAKEFLAGS) -f$**
DCC = $(ROOT)\bin\dcc32.exe $**
BRCC = $(ROOT)\bin\brcc32.exe $**
!include c:\work\source\compile.mak
#------------------------------------------------------------------------------
PROJECTS = Project1.exe
#------------------------------------------------------------------------------
default: $(PROJECTS)
#------------------------------------------------------------------------------

Project1.exe: Project1.dpr
  $(DCC)

Теперь перейдите в каталог с bpg-файлом, выполните команду "make -f ProjectGroup1.bpg" и все ваши проекты будут собраны. Для сборки конкретного проекта добавьте его имя: make.exe -f ProjectGroup1.bpg Project1.exe. Возможно, этот способ не заработает, если где-то в путях есть пробелы. Я стараюсь избегать пробелов в рабочих каталогах, и таких тестов не проводил. Но уверен, что расстановка кавычек (") в нужных местах решит проблемы.

Возможно, вам захочется провести дополнительную обработку откомпилированных файлов (strip relocation, шифрование, сжатие). Просто дополните неявные правила .dprdummy и .dpkdummy нужными командами. Для их вызова очень помогут макросы:

Смотрите также описание MAKE. Следует помнить, что Make в Borland Delphi несколько отличается и обладает меньшими возможностями.

Замечания по структуре каталогов

В упоминавшемся ранее ресурсе достаточно подробно расписана структура каталогов проекта. Я бы добавил к приведенной структуре каталоги DCU, и, если необходимо, OBJ. Кроме того, если вы используете несколько компиляторов, то каталог DCU следует разделить на подкаталоги: DCU\5, DCU\7, DCU\9. Все откомпилированные модули должны попадать в эти каталоги (настройка Unit Output Directory или –n$(DCU_DIR) для dcc32). Это позволяет упростить настройку путей поиска в ваших проектах – в Search Path достаточно добавить директорию DCU\x вместо перечисления путей к исходникам проектов, от которых зависит текущий проект. Отказ от следования этому правилу приводит к тому, что проект становится "непереносимым" - он может быть собран только на компьютере разработчика, поскольку сильно зависит от настроек среды. Попробуйте, например, откомпилировать исходники проекта Гедемин.

Если ваш проект содержит пакеты, то стоит выделить отдельный каталог для DCP-файлов – настройте DCP output directory всех проектов на него (-LN$(DCP_DIR)). Как правило, версия Delphi должна входить в название пакета (например, VCL50), поэтому нет необходимости разделять каталоги DCP файлов по версиям компилятора, как это сделано для файлов DCU. DCP - файл содержит заголовки входящих в него модулей, а также конкатенацию входящих в пакет модулей (DCU). Поэтому, если ваш проект является пакетом или использует пакеты, то нет необходимости при компиляции искать DCU-файлы. Наоборот, Search Path таких проектов должен указывать на каталог с DCP – файлами.

Вот возможная структура каталогов:

|--$(ROOT_DIR)
      |--DCU – тут находятся файлы DCP (DCP_DIR)
            |--5
            |--7 - тут DCU файлы (DCU_DIR)
            |--9
      |--Source – тут compile.mak, compile.bat и compsearch.mki. (SRCROOTDIR)
            |--Projects
                  |-- Project1
...
Именно она использована в универсальном файле compile.mak. Я использую пакеты, поэтому в опциях компилятора указал путь к DCP – файлам:
-u$(DCP_DIR);$(LIBPATH)
Без пакетов надо было написать:
-u$(DCU_DIR);$(LIBPATH)

Итак, вот они правила:

  1. DCU – файлы должны собираться в одном каталоге
    То же относится к DCP файлам
  2. Если вы используете модули, не входящие в текущий проект, то путь поиска должен указывать не на их исходники, а на откомпилированные версии (DCU или DCP – в зависимости от свойств проекта)
    При выполнении пункта 1 переменная Search Path сильно упрощается
  3. Если возможно, используйте относительные пути – это упростит переносимость проектов.
    Например, для проекта Project1 из приведенной структуры Search path = ..\..\..\DCU\x (..\..\..\DCU для пакетов и проектов с пакетами)

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

Copyright© 2005, Иван Равин  Специально для Delphi Plus


Пожалуйста, оцените статью
Отлично
Хорошо
Средне
Плохо
Очень плохо