Автоматизированная сборка Delphi-приложения

  • Tutorial

Я довольно часто сталкивался с тем, что разработчики на Delphi (можно сказать традиционно) компилируют свои приложения "ручками", что далеко не production-решение, а со стороны выглядит кустарщиной и "делаем на-коленке", хотя продукты бывают весьма серьёзными и продаваемыми. Вероятно, это пошло ещё с тех пор, когда для автоматизации нужно было придумывать свои батнички, которые запускали компилятор командной строки dcc32 с нужными параметрами. Некоторые даже сделали свой "Публикатор" — Delphi-expert, который делает работу сервера сборок: компилирует (правда, открытый в IDE) проект, выставляя ему взятый из какой-то БД инкрементированный номер версии, записывает некий changelog и копирует это куда-то в сетевой каталог.


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


Файл проекта современной версии Delphi — это .dproj-файл (здесь и далее я буду ориентироваться на Delphi 10 Rio, но с небольшими отличиями это верно для всех более ранних версий Delphi, начиная с 2007). В нём хранятся все настройки проекта, которые обычно изменяют в IDE (меню Project - Options (Ctrl+Shift+F11)). В рамках данной статьи я сконцентрируюсь на "основных", которые понадобятся для демонстрации общих принципов: это Config — конфигурация, Platform — платформа, OutputDirectory — путь выходного файла и ConditionalDefines (директивы условной компиляции). Остальные настройки, если таковые нужно менять при сборке, я предлагаю выявить самостоятельно. Этот же .dproj-файл, если в него заглянуть обычным текстовым редактором, является ничем иным как скриптом сборки MSBuild (давайте создадим простое консольное приложение и назовём его DelphiAutomatedBuild):


<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <ProjectGuid>{6880AD8E-6CB3-47B9-B8E3-7304CF6E9735}</ProjectGuid>
        <ProjectVersion>18.1</ProjectVersion>
        <FrameworkType>None</FrameworkType>
        <MainSource>DelphiAutomatedBuild.dpr</MainSource>
        <Base>True</Base>
        <Config Condition="'$(Config)'==''">Debug</Config>
        <Platform Condition="'$(Platform)'==''">Win32</Platform>
        <TargetedPlatforms>1</TargetedPlatforms>
        <AppType>Console</AppType>
    </PropertyGroup>
    ...

Скрипты сборки MSBuild также используются для описания проектов, например, Visual Studio. Я коснусь некоторых деталей MSBuild, но я предлагаю читателю самостоятельно освоить его азы. Что нам это даёт? Это позволяет нам выполнить сборку Delphi-проекта из командной строки одной строчкой (что, в свою очередь, позволяет автоматизировать сборку проекта)


msbuild /t:build DelphiAutomatedBuild.dproj

Где взять MSBuild? Если установлена Delphi, то MSBuild уже тоже есть, и Delphi его использует. Скорее всего, это каталог %WINDIR%\Microsoft.Net\Framework\v3.5, либо найти в каталоге .Net 4.0/4.5/4.6. Но можно и скачать отдельным приложением с сайта Microsoft. Далее нам понадобится MSBuild минимум 4.0, но пока хватит и того, что по умолчанию


Если же читатель откроет командную строку в каталоге с проектом (hint: это можно быстро сделать, щёлкнув правой кнопкой мыши (ПКМ) на проекте в IDE — Show in Explorer, затем в Проводнике ПКМ — Открыть окно команд), то вышеприведённая команда не сработает:


...>msbuild /t:build DelphiAutomatedBuild.dproj
"msbuild" не является внутренней или внешней
командой, исполняемой программой или пакетным файлом.

т.к по умолчанию, пути к MSBuild-у в PATH нет. Так что добавим его туда:


set PATH=%WINDIR%\Microsoft.Net\Framework\v3.5;%PATH%

Теперь повторим:


...>msbuild /t:build DelphiAutomatedBuild.dproj
Microsoft (R) Build Engine версии 12.0.21005.1
[Microsoft .NET Framework версии 4.0.30319.42000]
(C) Корпорация Майкрософт (Microsoft Corporation). Все права защищены.

Сборка начата 24.11.2018 0:12:14.
Проект "Z:\habr\delphi-automate-build\DelphiAutomatedBuild.dproj" в узле 1 (целевые объекты build).
Z:\habr\delphi-automate-build\DelphiAutomatedBuild.dproj : error MSB4057: в проекте нет целевого объекта "build".
Сборка проекта "Z:\habr\delphi-automate-build\DelphiAutomatedBuild.dproj" завершена (целевые объекты build) с ошибкой.

Ошибка сборки.

"Z:\habr\delphi-automate-build\DelphiAutomatedBuild.dproj" (целевой объект build ) (1) ->
  Z:\habr\delphi-automate-build\DelphiAutomatedBuild.dproj : error MSB4057: в проекте нет целевого объекта "build".

    Предупреждений: 0
    Ошибок: 1

Затраченное время: 00:00:00.04

Сборка запустилась, но завершилась с ошибкой. В чём же дело? Почему нет задачи build?


Тут мы заглянем в .dproj-файл, там мы найдём следующее:


...
    <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
...

И если мы откроем файл в каталоге Delphi
c:\Program Files\Embarcadero\Studio\20.0\Bin\CodeGear.Delphi.Targets, то мы увидим там ещё один MSBuild-скрипт, в котором объявлена задача Build:


<Target Name="Build"...

Т.е. нужно задать переменную окружения BDS ($(VAR) в MSBuild разыменовывает как свойство (Property) VAR, заданное в скрипте, так и одноимённую переменную окружения), указать в ней путь к той версии Delphi, которая будет компилировать проект (да-да, один и тот же проект можно компилировать разными версиями Delphi, лишь заменив значение переменной окружения BDS). Тогда скрипт проекта разыменует $(BDS), найдёт общий .Targets файл из каталога Delphi и запустит задачу Build.
Сделаем это:


set BDS=c:\Program Files\Embarcadero\Studio\20.0

ещё раз
...>msbuild /t:build DelphiAutomatedBuild.dproj
Microsoft (R) Build Engine версии 12.0.21005.1
[Microsoft .NET Framework версии 4.0.30319.42000]
(C) Корпорация Майкрософт (Microsoft Corporation). Все права защищены.

Сборка начата 24.11.2018 0:20:40.
Проект "Z:\habr\delphi-automate-build\DelphiAutomatedBuild.dproj" в узле 1 (целевые объекты build).
CreateProjectDirectories:
  Создание каталога ".\Win32\Debug".
BuildVersionResource:
  C:\Program Files\Embarcadero\Studio\20.0\bin\cgrc.exe -c65001 DelphiAutomatedBuild.vrc -foDelphiAutomatedBuild.res 
  CodeGear Resource Compiler/Binder
  Version 1.2.2 Copyright (c) 2008-2012 Embarcadero Technologies Inc.

  Microsoft (R) Windows (R) Resource Compiler Version 6.0.5724.0

  Copyright (C) Microsoft Corporation.  All rights reserved.

  Файл "DelphiAutomatedBuild.vrc" удаляется.
_PasCoreCompile:
  C:\Program Files\Embarcadero\Studio\20.0\bin\dcc32.exe -$O- -$W+ --no-config -B -Q -TX.exe -AGenerics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;WinTypes=Winapi.Windows;WinProcs=Winapi.Windows;DbiTypes=BDE;DbiProcs=BDE;DbiErrs=BDE -DDEBUG -E.\Win32\Debug -I"c:\program files\embarcadero\studio\20.0\lib\Win32\debug";"c:\program files\embarcadero\studio\20.0\lib\Win32\release";C:\Users\USER\Documents\Embarcadero\Studio\20.0\Imports;"C:\Program Files\Embarcadero\Studio\20.0\Imports";"C:\Users\Public\Documents\RAD Studio\5.0\Dcp";"C:\Program Files\Embarcadero\Studio\20.0\include";C:\Users\USER\AppData\Local\Programs\TestInsight\Source -LE"C:\Users\Public\Documents\RAD Studio\5.0\Bpl" -LN"C:\Users\Public\Documents\RAD Studio\5.0\Dcp" -NU.\Win32\Debug -NSWinapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;System;Xml;Data;Datasnap;Web;Soap; -O"c:\program files\embarcadero\studio\20.0\lib\Win32\release";C:\Users\USER\Documents\Embarcadero\Studio\20.0\Imports;"C:\Program Files\Embarcadero\Studio\20.0\Imports";"C:\Users\Public\Documents\RAD Studio\5.0\Dcp";"C:\Program Files\Embarcadero\Studio\20.0\include";C:\Users\USER\AppData\Local\Programs\TestInsight\Source -R"c:\program files\embarcadero\studio\20.0\lib\Win32\release";C:\Users\USER\Documents\Embarcadero\Studio\20.0\Imports;"C:\Program Files\Embarcadero\Studio\20.0\Imports";"C:\Users\Public\Documents\RAD Studio\5.0\Dcp";"C:\Program Files\Embarcadero\Studio\20.0\include";C:\Users\USER\AppData\Local\Programs\TestInsight\Source -U"c:\program files\embarcadero\studio\20.0\lib\Win32\debug";"c:\program files\embarcadero\studio\20.0\lib\Win32\release";C:\Users\USER\Documents\Embarcadero\Studio\20.0\Imports;"C:\Program Files\Embarcadero\Studio\20.0\Imports";"C:\Users\Public\Documents\RAD Studio\5.0\Dcp";"C:\Program Files\Embarcadero\Studio\20.0\include";C:\Users\USER\AppData\Local\Programs\TestInsight\Source -CC -V -VN -NB"C:\Users\Public\Documents\RAD Studio\5.0\Dcp" -NH"C:\Users\Public\Documents\RAD Studio\5.0\hpp\Win32" -NO.\Win32\Debug   DelphiAutomatedBuild.dpr   
  Embarcadero Delphi for Win32 compiler version 30.0
  Copyright (c) 1983,2015 Embarcadero Technologies, Inc.
  19 lines, 0.27 seconds, 100748 bytes code, 26044 bytes data.
Сборка проекта "Z:\habr\delphi-automate-build\DelphiAutomatedBuild.dproj" завершена (целевые объекты build).

Сборка успешно завершена.
    Предупреждений: 0
    Ошибок: 0

Затраченное время: 00:00:01.32

Та-дам! Проект скомпилировался. В выходном каталоге Win32\Debug лежит наш DelphiAutomatedBuild.exe.


Но это отладочная сборка (по умолчанию, новый проект активируется в Debug-конфигурации), а мы хотим для выпуска релиза собирать Release-конфигурацию (подробнее про конфигурации). В IDE это сделать легко, но это ручная работа, и это то, чего мы хотим избежать, то ради чего мы читаем эту статью. Заглянем опять в .dproj-файл, и заметим в его начале такую строку


...
        <Config Condition="'$(Config)'==''">Debug</Config>
...

Мы ж программисты, и понимаем, что если свойство/переменная Config, не задана, то по умолчанию она принимается равной Debug. Это как раз то, что мы меняем в IDE (поменяйте в IDE текущую конфигурацию на Release и сохраните проект — строка сменится на


...
        <Config Condition="'$(Config)'==''">Release</Config>
...

в коде для контроля исполняемого файла добавим такое:


    {$IFDEF RELEASE}
    WriteLn('This is RELEASE build');
    {$ENDIF RELEASE}
    {$IFDEF DEBUG}
    WriteLn('This is DEBUG build');
    {$ENDIF DEBUG}

и убедимся, что conditional defines в настройках проекта для Release и Debug-конфигураций содержат RELEASE и DEBUG, соответственно


Так что нужно лишь задать свойство Config в нужное нам значение, и собираться будет нужная конфигурация:


...>msbuild /t:build DelphiAutomatedBuild.dproj /p:Config=Release
Microsoft (R) Build Engine версии 12.0.21005.1
[Microsoft .NET Framework версии 4.0.30319.42000]
(C) Корпорация Майкрософт (Microsoft Corporation). Все права защищены.

Сборка начата 24.11.2018 0:48:30.
Проект "Z:\habr\delphi-automate-build\DelphiAutomatedBuild.dproj" в узле 1 (целевые объекты build).
CreateProjectDirectories:
  Создание каталога ".\Win32\Release".
...

...>Win32\Debug\DelphiAutomatedBuild.exe
This is DEBUG build

...>Win32\Release\DelphiAutomatedBuild.exe
This is RELEASE build

Замечательно!


Часто разработчики указывают путь отладочной (а то и релизной) сборки в какой-то каталог на своём диске, но мы автоматизируем сборку и подразумеваем, что выполняться она будет на сервере сборок, а получать выходные файлы непонятно где в файловой системе сервера — как-то неправильно. Значит, мы должны уметь задавать этот выходной путь. Заглянем опять в .dproj:


...
        <DCC_ExeOutput>.\$(Platform)\$(Config)</DCC_ExeOutput>
...

но что это? тут нет условия (если не задано), и свойство задаётся всегда, сможем ли мы его переопределить? попробуем


...>msbuild /t:build DelphiAutomatedBuild.dproj /p:DCC_ExeOutput=binaries

Та-дам! Появился каталог binaries, в котором — наш DelphiAutomatedBuild.exe. Как же так? Тот, кто уже освоил MSBuild, знает, что свойства, заданные при запуске MSBuild-а, имеют высший приоритет, и уже не могут быть переопределены в скрипте. Сейчас нас это устраивает. Но с этим мы ещё столкнёмся...


Выходной каталог мы менять научились. Теперь нужно собирать сразу и релизную, и отладочную версии (надеюсь, не надо объяснять зачем такое надо). Конечно, можно запустить сначала с одним параметром ConfigDebug, затем — с другим — Release, но это потребует, во-первых, дублирования остальных параметров (например, DCC_ExeOutput и параметра версии сборки (об этом — ниже)), а во-вторых, это придётся учитывать и при конфигурировании сервера сборок, что влечёт дублирование и там (либо написание очередного батничка, что лишает встроенной поддержки MSBuild-а сервером сборок). Так что требуется выполнить всё ту же одну команду


...>msbuild /t:build DelphiAutomatedBuild.dproj /p:DCC_ExeOutput=binaries

но она бы выполнила сборку обеих конфигураций. Можно так? Конечно!
Напишем свою задачу Build. Поскольку есть нежелание менять что-то в файле, который меняет IDE (часто самым дурацким образом; кстати, есть три замечательных инструмента от автора эксперта MMX: DProjNormalizer, DProjSplitter и сумма их — ProjectMagician — для удобства отслеживания изменений .dproj-файлов), то сделаем отдельный файл проекта. Назовём его DAB.ciproj (CI-project, от CI — Continuous Integration):


<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="Build">
        <MSBuild Projects="DelphiAutomatedBuild.dproj"
            Targets="Build"
            Properties="Config=Debug"/>
        <MSBuild Projects="DelphiAutomatedBuild.dproj"
            Targets="Build"
            Properties="Config=Release"/>
    </Target>
</Project>

Запускаем


...>msbuild /t:build DAB.ciproj /p:DCC_ExeOutput=binaries

и… получаем один файл DelphiAutomatedBuild.exe в binaries, той конфигурации, что собралась последней:


...>binaries\DelphiAutomatedBuild.exe
This is RELEASE build

DCC_Exeoutput задался и для каждой задачи MSBuild — это хорошо, но каждая конфигурация скомпилировала файл в один и тот же каталог. Тогда зададим подкаталоги соответственно конфигурации:


<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="Build">
        <MSBuild Projects="DelphiAutomatedBuild.dproj"
            Targets="Build"
            Properties="Config=Debug;DCC_Exeoutput=$(DCC_ExeOutput)\Debug"/>
        <MSBuild Projects="DelphiAutomatedBuild.dproj"
            Targets="Build"
            Properties="Config=Release;DCC_Exeoutput=$(DCC_ExeOutput)\Release"/>
    </Target>
</Project>

Запускаем опять


...>msbuild /t:build DAB.ciproj /p:DCC_ExeOutput=binaries

и теперь на выходе мы имеем два файла


binaries\Debug\DelphiAutomatedBuild.exe и binaries\Release\DelphiAutomatedBuild.exe.


...>binaries\Debug\DelphiAutomatedBuild.exe
This is DEBUG build

...>binaries\Release\DelphiAutomatedBuild.exe
This is RELEASE build

Теперь представим, что у нас есть желание/необходимость временно задавать conditional define при сборке проекта (например, у нас есть демо-версия, в которой мы ограничиваем функциональность нашей программы, если задано переменная условной компиляции TRIAL )


В нашем демо-коде это выглядит так


    {$IFDEF TRIAL}
    WriteLn('This is TRIAL version');
    {$ENDIF TRIAL}

Добавим в Debug-конфигурацию conditional define TRIAL и посмотрим, куда оно прописывается в .dproj:


        <DCC_Define>DEBUG;TRIAL;$(DCC_Define)</DCC_Define>

Ага, т.е. если задать /p:DCC_Define=TRIAL,


...>msbuild /t:build DAB.ciproj /p:DCC_ExeOutput=binaries /p:DCC_Define=TRIAL

то


...>binaries\Debug\DelphiAutomatedBuild.exe
This is TRIAL version

...>binaries\Release\DelphiAutomatedBuild.exe
This is TRIAL version

Сработало, но как-то не так, куда-то делись DEBUG и RELEASE, а нам такого не надо, т.к. у нас там обычно куча полезных define-ов.
А дело в том, что свойства заданные через командную строку имеют высший приоритет, и переопределяют значения в скриптах. Но выход есть.
Определяем переменную окружения DCC_Define:


...>set DCC_Define=TRIAL
...>msbuild /t:build DAB.ciproj /p:DCC_ExeOutput=binaries
...

...>binaries\Debug\DelphiAutomatedBuild.exe
This is DEBUG build
This is TRIAL version
...>binaries\Release\DelphiAutomatedBuild.exe
This is RELEASE build
This is TRIAL version

С компиляцией разобрались, теперь можно настраивать сервер сборок, который бы после каждого изменения в центральном репозитории (я ориентируюсь на Git, но для того же SVN это тоже применимо) собирал нам проект, дабы мы ничего не забыли добавить в исходники, и прогонял тесты, буде таковые у нас есть, и мы всегда будем готовы выпустить релиз или отдать на тестирование уже готовую сборку.


Однако ж, при таких частых сборках может стать проблема нумерации версий. Какая? Каждая новая сборка будет иметь ровно ту версию, которая прописана в свойствах проекта, а менять её с каждым коммитом — как-то рутинно и не "по-нашенски", к тому же, зависит от разработчика человека (а что такое "человеческий фактор" — не мне вам рассказывать).


В рамках обычной для Windows/Delphi-проектов нумерации Major.Minor.Release.Build, нормальный сервер сборок, как правило, умеет увеличивать для каждой сборке число Release, и, естественно, передавать её в скрипты сборки. Однако ж, если мы посмотрим на то, как задаётся информация о версии в .dproj-файле


        <VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>

мы поймём, что передавать её некуда. И даже если мы додумаемся заменить FileVersion=0.0.0.0, например, так FileVersion=$(Version), где Version — переменная, которую мы бы передавали при сборке, то сборка из командной строки у нас получится, а вот уже изменять свойства проекта в IDE — нет, т.к. она будет "ругаться" на такое значение. Ну да где наша не пропадала. Видим, что это свойство, значение которого — CSV-список, одно из значений которого нам нужно заменить.
Долго не думая, перейдём сразу к делу. MSBuild 4.0 умеет понимать скрипты на C#, чем мы и воспользуемся (но напомню, что тогда в PATH надо прописать именно его). Накидаем такой файлик


Delphi.Version.Targets
<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="12.0">
  <UsingTask TaskName="__SetFileVersion" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
    <ParameterGroup>
      <VerInfoKeys ParameterType="System.String" Required="true" />
      <VerInfoProperties ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Out ParameterType="System.String" Output="true" />
    </ParameterGroup>
    <Task>
      <Code Type="Fragment" Language="cs"><![CDATA[
        // split values as CSV (by ";")
        String[] verInfoKeysList = VerInfoKeys.Split(';');
        Dictionary<String, String> d = new Dictionary<String, String>();
        foreach (String verInfoValue in verInfoKeysList) {
            // split values as "key=value"
            if (! String.IsNullOrEmpty(verInfoValue)) {
                String[] kv = verInfoValue.Split('=');
                d.Add(kv[0], kv[1]);
            }
        }
        if (VerInfoProperties.Length > 0) {
          foreach (ITaskItem item in VerInfoProperties) {
            String value = item.GetMetadata("Value");
            if (value.Length > 0) {
              Log.LogMessage("{0}: {1}", item.ItemSpec, value);
              d.Remove(item.ItemSpec);
              d.Add(item.ItemSpec, value);
            }
          }
        }

        List<String> L = new List<String>();
        foreach (KeyValuePair<String, String> kv in d) {
            L.Add(kv.Key + "=" + kv.Value);
        }
        _Out = String.Join(";", L.ToArray());
]]></Code>
    </Task>
  </UsingTask>

  <Target Name='_SetVersionCode_Name'>
    <Message Text="$(VerInfo_Keys)" />

    <ItemGroup>
      <VerInfoProperties Include="FileVersion">
        <Value>$(FileVersion)</Value>
      </VerInfoProperties>
    </ItemGroup>
    <__SetFileVersion VerInfoKeys="$(VerInfo_Keys)" VerInfoProperties="@(VerInfoProperties)">
      <Output PropertyName="VerInfo_Keys" TaskParameter="Out" />
    </__SetFileVersion>
    <Message Text="$(VerInfo_Keys)" />
  </Target>

  <Target Name='_SetFileVersion' BeforeTargets="_BuildRCFile"
      Condition="'$(FileVersion)' != ''">
     <CallTarget Targets='_SetVersionCode_Name'/>
  </Target>
</Project> 

Любознательный читатель наверняка уже догадывается как примерно такое использовать.
Добавим в наш DelphiAutomatedBuild.dproj


...
<Import Project="Delphi.Version.Targets" Condition="$(MSBuildToolsVersion) >= 4.0" />
...

(Условие "$(MSBuildToolsVersion) >= 4.0" необходимо для того, чтобы проект не падал с ошибкой при сборке в IDE, которая, как мы помним, использует MSBuild 3.5, который не поддерживает UsingTask)


Таким образом, мы импортируем задание _SetFileVersion, которое выполнится после задания _BuildRCFile (тут я немного срезал угол: это внутреннее задание, которое выполняется при сборке проекта — см. $(BDS)\bin\Codegear.Common.Targets), и только если задано свойство FileVersion. Это задание берёт тот самый CSV-список в переменной VerInfo_Keys, разбивает его на пары ключ-значение, заменяет некоторые значения на заданные, и собирает обратно в строку VerInfo_Keys.


Поставим в свойствах проекта "Include version information in project" и добавим вывод текущей версии (оставим это за скобками), и:


...>msbuild /t:build DAB.ciproj /p:DCC_ExeOutput=binaries /p:FileVersion=4.3.2.1

...>binaries\Debug\DelphiAutomatedBuild.exe
This is RELEASE build
This is TRIAL version
This file version is 4.3.2.1
...>binaries\Release\DelphiAutomatedBuild.exe
This is DEBUG build
This is TRIAL version
This file version is 4.3.2.1

Profit!


Заключение


Так мы научились автоматизированно собирать Delphi-приложения одной командой, что экономит нам время и нервы, в том числе, за счёт того, что позволяет переложить компиляцию и выпуск релизов на сервер сборок, и тем самым застраховаться от ситуаций, когда проект собирается только на машине разработчика. К тому же, позволяет автоматизировать простановку версии как каждой сборки (на каждый коммит), так и увеличение релизной версии при выпуске релиза.


В дальнейшем я ещё планирую рассказать


  1. как запускать статический анализ кода (на примере FixInsight, не реклама!) во время сборки
  2. как писать unit-тесты на Delphi (увы, некоторым приходится объяснять ))). И запускать их в пайплане сборки )
  3. как "прикрутить" сборку Delphi-проектов к GitLab CI
  4. а также, как можно использовать отладчик WinDbg, например, для поиска причин сбоя/падения приложений из-за библиотек, написанных на Delphi (ну, конечно же, как при этом интегрировать формирование необходимых для этого PDB-файлов в автосборку)

З.Ы. Буду рад ответить на любые вопросы, в том числе, в телеграме, как в личке, так и в чатах @Delphi_Lazarus и @DelphiCommunity

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 44

    +7
    А что так сложно-то? MSBuild всегда идет с .net framework и лежит он в определенном месте. Тут %systemroot%\microsoft.net\framework\v3.5\msbuild для framework 3.5. Я против добавления лишнего в PATH, если можно обойтись без этого. Внутри RAD студии есть файл rsvars.bat и там прописаны все переменные окружения нужные для сборки проектов delphi. Сам же каталог RAD Studio добавляется в PATH.
    Таким образом, чтобы собрать приложение делаем так:
    call rsvars.bat
    %systemroot%\microsoft.net\framework\v3.5\msbuild /t:Clean MyApp.dproj
    %systemroot%\microsoft.net\framework\v3.5\msbuild /t:Build /p:Config=Release MyApp.dproj
    Либо для пути msbuild писать %FrameworkDir%\msbuild. Переменная FrameworkDir описана в rsvars.bat
    Если нужно собрать несколько программ, то оформляем project group и собираем, заменяя MyApp.dproj на ProjGroup.groupproj
    Если нужны какие-то более сложные действия (условная сборка, подписывание сборок, сборка установщика, загрузка результата куда-то), то лучше сделать скрипт msbuild. Или powershell. Можно даже сделать bat файл, но это сложнее.
    Задаю выходные каталоги я прямо в настройках проекта. Он нормально переживает относительные пути.
    Номер версии лучше включать не через настройки проекта, а с помощью rc файла. Т.е. собирать как ресурс, а сами номера вынести в отдельный h-файл. Если интересно, то конкретный код могу показать вечером.
    Номер версии я предпочитаю устанавливать вручную, а вот установку номера релиза можно и автоматизировать.
    Таким образом получается проект, который можно собирать как в IDE, так и через командную строку без необходимости что-то где-то настраивать.
      0
      Плюсанул бы, если б мог)
      Точно так же сделал автодеплой через Jenkins.
        0
        Сложно только на первый взгляд, ведь всё достаточно понятно написано.

        Было бы интересно узнать, как ТС видит прикручивание сборки делфовских проектов к GitLab CI.
          0

          Спасибо )


          Было бы интересно узнать, как ТС видит прикручивание сборки делфовских проектов к GitLab CI.

          Stay tuned! )))))

            0
            Мы уже несколько лет собираем проекты под GitLab CI, для сборки используем скрипты Apache ANT. Там нет ничего сложного, по сути — GitLab CI Runner просто выкачивает исходники в темповую папку и запускает скрипт ant. Но это может быт и MSBuild при желании.
              0

              как у вас устроено версионирование при этом?
              есть ли "релизы"? ночные/тестовые сборки? как версионируются(нумеруются) они? как, например, потом отличить бинарник тестовой сборки от релизной?

                0
                У нас в корне проекта лежит файл version, в котором прописывается номер версии продукта, которую надо собрать.
                Файл version либо руками модифицируется, когда например стартуем новую ветку продукта, либо автоматом из батника.
                Процесс сборки начинается, если запушить тегированный коммит (можно руками, можно батником который инкрементирует версию и пушит на сервер коммит с тегом).

                Скрипты Ant поднимают версию из файла — таким образом Ant знает какую версию собирает и куда выкладывать результат сборки. Ant генерит rc файл со всеми нужными строками и подставленной версией (так она попадает в свойства EXE/DLL), дополнительно генерится inc файл где костантами прописывается версия, название приложения, компания и т.п. для целей потом использовать в самом приложении, например выводить в лог.

                GitLab крутится на одном серваке, а сборка идет на другом (под Windows).

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

                MSBuild не стали использовать потому что у нас были проекты под Delphi 7, а теперь еще и Golang, C++ и т.п.
                Apache Ant гибкая система, мы просто написали ряд макросов (запуск компилятора, подпись результата, сбор отладочных файлов (dbg, pdb, сборка инсталлятора и т.п.)
            0
            А что так сложно-то?

            мне кажется, "сложность" лишь из-за того, что я расписал детали того, как это устроено
            ничего сложного в запуске, например
            msbuild /t:build DAB.ciproj /p:DCC_ExeOutput=binaries /p:FileVersion=4.3.2.1
            мне не видится )


            Таким образом, чтобы собрать приложение делаем так:
            call rsvars.bat
            %systemroot%\microsoft.net\framework\v3.5\msbuild /t:Clean MyApp.dproj
            %systemroot%\microsoft.net\framework\v3.5\msbuild /t:Build /p:Config=Release MyApp.dproj

            это если запускать батник… сервер сборок, который я использовал (Quickbuild) из коробки поддерживает сборку MSBuild-ом, там негде вызывать call rsvars.bat для инициализации окружения… зато можно задать переменные окружения (ту же BDS) и свойства запуска (по сути, опции /property)


            , то лучше сделать скрипт msbuild. Или powershell. Можно даже сделать bat файл, но это сложнее.

            Можно, но зачем батники для сборки, если есть msbuild? )) ну, батник для запуска msbuild-скрипта — ок )) но "логику"? не думаю )


            Задаю выходные каталоги я прямо в настройках проекта. Он нормально переживает относительные пути.

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


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


            Номер версии лучше включать не через настройки проекта, а с помощью rc файла. Т.е. собирать как ресурс, а сами номера вынести в отдельный h-файл.

            Ну, в Delphi до версии 2007 я так и делал… Сейчас же мне нравится описанный способ. Кроме того, я в пример не стал включать, но там можно задавать ЛЮБЫЕ свойства, включаемые в Version Info — в частности, commit id (Git) /revision number(SVN), чтобы можно было достоверно установить из каких исходников собраны бинарники...


            Если интересно, то конкретный код могу показать вечером.

            Показывайте! может, кто-то захочет сделать так же )


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

            Мы предпочли выпуск релиза делать по нажатию кнопки )

            0
            1. Уже 20 лет как собираем через стандартный make (и dcc соответствующий — под нужную платформу). И даже Delphi ставить не нужно :), легко переключаемся между версиями библиотек

            2. res файл (а соответственно номер версии и все остальное), создается из rc-файла, И это возможность тоже была всегда — пользуйтесь. А так как в svn есть простая возмость проставить номер ревизии (с помощью утилиты SubWCRev), то всегда точно знаешь из каких исходников был собран файл.

              0
              1. Уже 20 лет как собираем через стандартный make

              Меня вот это и смущает обычно )), что на современных Delphi-проектах могут использоваться технологии такой давности )))


              1. res файл (а соответственно номер версии и все остальное), создается из rc-файла, И это возможность тоже была всегда — пользуйтесь.

              Спасибо )) только я не хочу шаблонизировать его руками )) я хочу подправить, например, Version Info только в файле проекта — и нигде больше ))


              А так как в svn есть простая возмость проставить номер ревизии (с помощью утилиты SubWCRev), то всегда точно знаешь из каких исходников был собран файл.

              Я уже 10 лет не использую SVN (в своих/рабочих проектах), заменил его Git-ом… выше я упомянул, что commit revision/id тоже можно включать в Version Info )

              0
              Считаю, статье с данной темой давно пора была появиться. Мне тема знакома не по наслышке, поэтому поделюсь своим опытом по теме статьи. Имею значительный перечень проектов, которые регулярно возникает потребность собирать. Использовать project groups среды разработки оказалось не удобным. Например, чтобы собрать 1,2 и 5й проeкт потребуется создание одной project group, для 2 и 3 и 5 – другой, и т.д. Группы, к тому же, нужно как-то называть.
              Для начала, я бы сформулировал задачи, которые ставит разработчик при компиляции ряда проэктов (РП).
              1. Происходит ли сборка РП без ошибок. Хорошо бы, например, понять, последние внесённые изменения в общие модули – не нарушили ли собираемость всех проeктов. А если я поменял ..unit8.pas и unit9.pas, хорошо бы знать, каких проeктов это каснулось.
              2. Возможность сборки при разных наборах условных директив
              3. Моментальное получение файлов результатов сборки (EXE, APK) в заданных папках
              4. Очистка/удаление результатов сборки, а также временных (debug) файлов
              5. Использование RAM-диска (в общем случае – отдельной папки) для временных файлов и результатов сборки, для ускорения компиляции. Также это позволит не засорять папки с исходниками
              6. Наглядность сборки. Хорошо бы видеть ряд проeктов и отметив галочками, выполнять действия по сборке, очистке и прочее, так сказать, визуально наблюдая за процессом. bat-файлы по сборке в командной строке наглядности не дадут.

              Поэтому после некоторого опыта создания bat-файлов с запуском консольного компиляторока Delphi DCC32.EXE было принято решение о написании приложения MultiCompiler, которое я уже пару лет успешно использую для сборки проeктов. Матрица директив в проeктах, матрица модулей в проeктах, быстрое включение/исключение модулей в проeктах просто поставив или убрав галочку, всё это существенно облегчает и упрощает жизнь. В данный момент поддерживаются компиляторы от версии 18.0 до 33.0, соответственно, от BDS 2006 до Embarcadero Delphi 10.3. В ближайшее время появится пакетная сборка APK.
              Статья оказалась интересной и детально осветившей тему, особенно описание dproj-файлов, за что автору выражаю своё уважение. Хотелось бы услышать от разработчиков, кто и с какими задачами, сложностями сталкивается в процессе сборки приложений и какое находят решение?
                0

                Спасибо за оценку )


                1. Происходит ли сборка РП без ошибок. Хорошо бы, например, понять, последние внесённые изменения в общие модули – не нарушили ли собираемость всех проeктов.
                  2,3,4,5,6...

                Я возлагаю все эти задачи на сервер сборок, который после каждого пуша исходников проводит тестовую сборку проекта (хоть это самостоятельный проект, хоть это библиотека… с библиотеками — даже должно быть строже, т.к. они используются во множестве проектов на разных версиях Delphi), его тестов и их запуск…
                При этом он: делает это заново в чистом каталоге (нет необходимости что-то чистить), задержка в одну-две минуты на накладные расходы по клонированию — приемлема.Она ничего не решает, по-моему )
                п. 6 — для меня спорный. Я отдаю всё на откуп автоматизации. Из ручных действий — максимум — нажать кнопку "выпустить релиз" ))


                В ближайшее время появится пакетная сборка APK.

                Мне хотелось поделиться мыслью о том, что всё, что нынче умеет Delphi из коробки (повторюсь, начиная с версии 2007), — можно автоматизировать запуском MSBuild-а с параметром YOUR_PROJECT.dproj.
                А собирать APK она умеет ))
                Т.е. запустив
                msbuild YOUR_FMX_PROJECT.dproj /p:Platform=Android /t:build;deploy
                на выходе получим APK )

                0
                Спасибо, Алексей. Жду статью про отладку и WinDbg, всё никак руки не дойдут разобраться самому, как на той картинке с квадратными колёсами.
                  0
                  В целом неплохо.
                  Действительно в современных Delphi используется msbuild и это уже хоть какой то сборочный стандарт, который позволяет для простых случаев отказаться от самописных сборочных скриптов.
                  К сожалению в приближенной к реальности ситуации всё равно не получится.
                  Рассмотрим тот же VersionInfo — .dproj не умеет multilang. Это большой шаг вперёд по сравнению с Delphi 7 которая вообще не умеет генерировать .res без IDE, но всё ещё не спасает от необходимости генерировать .res руками.
                  Возможность передать Defines в сборку — замечательно. Но вот сама Delphi уж очень ограничена в возможностях своих Define и вставить ими в исходник например дату сборки не получится. Всё равно нужна кодогенерация или патч исходников. И вот уже дополнительный шаг сборки.
                  Можно упомянуть про prebuild и postbuild (и вы скорее всего напишете про них в следующих статьях), но это тоже не решение всех проблем. Они имеют сильную завязку на окружение. И есть сценарии где шагами сборочного сервера решить задачу будет проще чем шагами проекта.
                  В целом хорошо что статьи появляются. Глобально есть очень много тёмных пятен. Например мало кто использует динамическую линковку компонентов, потому что возникают сложности с распространением нужных bpl со своим продуктом. (и это при том, что многие опенсорс компоненты по лицензии требуют именно динамическую линковку).
                  Мало кто понимает что такое bpl\dcu и прочие файлы генерируемые Delphi.
                  Не все различают файлы проекта и файлы IDE лежащие рядом с проектом и не знают чего добавить в гитигнор.
                  Так что буду ждать новых статей. Может увижу и для себя что-то полезное.
                    0
                    К сожалению в приближенной к реальности ситуации всё равно не получится.

                    я и не говорил, что этого достаточно, я лишь хотел обратить внимание, что кастомные сборки можно строить вокруг msbuild-скриптов, а не "батничков"


                    Можно упомянуть про prebuild и postbuild

                    признаться, не планировал ) на мой взгляд, это решение, которое провоцирует использовать IDE для сборки, что мне не нравится )


                    и вставить ими в исходник например дату сборки не получится

                    а зачем?
                    а если очень хочется, то можно вставить в Version Info, например… это чуть сложнее, чем просто использование hardcode-переменной "дата-сборки", но задачу может решить...


                    по сравнению с Delphi 7 которая вообще не умеет генерировать .res без IDE

                    кстати, даже D2007 не умеет )), но и это лечится custom-ным msbuild-скриптом ))


                    но всё ещё не спасает от необходимости генерировать .res руками.

                    для multilang version info?
                    Я правильно понимаю, что multilang version info — это когда в version info содержится несколько блоков с разными language code?
                    Мне кажется, и это можно кастомизировать скриптом, а не руками делать )

                      0
                      а зачем?

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

                      Я правильно понимаю, что multilang version info — это когда в version info содержится несколько блоков с разными language code?
                      Мне кажется, и это можно кастомизировать скриптом, а не руками делать )

                      Да, это когда в зависимости от локали ОС будет показано описание на соответствующем языке. Скриптами не получится, потому что rc генерирует расширение msbuild. Поставляемая на .net вместе с Delphi библиотека. И она просто физически не способна переварить формат отличный от того, который есть в .dproj. То есть кастомизировать скриптом будет означать всё равно генерацию rc руками и дальнейшую сборку :).

                      Идея использовать IDE для сборки уже почти никому не нравится. Но далеко не все вещи готовы к полноценной работе вне среды (речь не о Delphi, она это почти победила).
                    0
                    А у нас используется WANT
                    Он сам на Delphi написан, и если возможностей не будет хватать, всегда можно допилить.
                      +1

                      с какой версией Delphi? 7? :)
                      Про Want — знаю, сам его во времена D7 использовал и допиливал (кстати, самый starred репозиторий у меня )))) ...

                        0
                        В данный момент — RIO :). Want, естественно, пиленный.
                          0

                          ой, мамочки! )

                      +1
                      Всегда настраивал и производил компиляцию в среде. Познавательно. Спасибо.
                        0
                        Не пробовали упаковать RAD Studio в docker образ? Та еще боль ;( Хотелось бы найти решение как этот кусок го*** заставить работать.
                          0

                          нет, не было необходимости… но Эмба же пуликовала видосы у себя в канале

                            0

                            видел, но в свободном доступе есть только образы под линукс

                            0
                            Её даже на живой винде упаковать в учётную запись сборочного агента проблематично бывает.
                            При этом если общаться с техподдержкой то они заявляют что можно поставить на сборочный сервер чисто компилятор без IDE. Вот только нигде нет (не было лет несколько назад) как это сделать. А руками сидеть и вылавливать зависимости нет никакого желания.
                              +1

                              если про компилятор на сервере сборок без IDE (и без реестра), то я делал, и не раз, и для разных версий Delphi, и под разные целевые платформы (Win32, Win64, Android)
                              что, нужна описание как это работает? (я думал о такой статье, не знаю насколько она объёмной выйдет )) там не сильно много, на самом деле)

                                0
                                Всеми руками за. Потому что у меня сейчас более примитивное решение и с ним скоро начнутся проблемы.
                                Это можно сделать как раздел в контексте статьи про CI.
                            –1
                            DelphiAutomatedBuild.dpr (github)

                            {$IFDEF RELEASE}
                            WriteLn('This is RELEASE build');
                            {$ENDIF RELEASE}
                            {$IFDEF DEBUG}
                            WriteLn('This is DEBUG build');
                            {$ENDIF DEBUG}
                            {$IFDEF TRIAL}
                            WriteLn('This is TRIAL version');
                            {$ENDIF DEBUG} // {$ENDIF TRIAL} копипаст, будь он неладен
                            WriteLn('This file version is ', GetFileVersion);
                              0

                              спасибо, поправлю ))
                              хотя на работу не влияет

                              0

                              Для мобильных приложений на Delphi, подозреваю, всё сложнее

                                0

                                для Android — не прям уж )
                                как уже упоминал:


                                msbuild YOUR_FMX_PROJECT.dproj /p:Platform=Android /t:build;deploy

                                всё


                                для iOS (кроме симулятора) я не собирал, так что в деталях не подскажу, но, вроде, достаточно подключённой OSX

                                0
                                У нас команда использует свою собственную систему сборки с инкрементацией версии через rc. DUnit для юнит-тестов, Hudson для регулярных сборок-тестирования.

                                а также, как можно использовать отладчик WinDbg, например, для поиска причин сбоя/падения приложений из-за библиотек, написанных на Delphi (ну, конечно же, как при этом интегрировать формирование необходимых для этого PDB-файлов в автосборку)


                                Есть в планах на ближайшее полугодие статья про то, как я ловил пару неприятных ошибок при помощи WinDbg, но для D7.

                                MadHacker
                                Это большой шаг вперёд по сравнению с Delphi 7 которая вообще не умеет генерировать .res без IDE

                                Эммм в каком смысле?
                                brcc32 и пожалуйста — res без IDE.
                                  0
                                  Эммм в каком смысле?

                                  Исключительно в контексте сравнения группы проектов Delphi 7 — которая на самом деле make файл но включает в себя только компиляцию и то с особенностями, с msbuild сборочными проектами современных Delphi которые уже способны полностью провести сборку из исходников.
                                  Кстати brcc часто ломается на всякой фигне, лучше использовать rc из windows sdk.
                                  brcc ломался от .ico с большим количеством разрешений. Были и другие поломки (вроде все тоже с разными вариантами графики) но точно уже не вспомню.
                                    0

                                    присоединюсь против brcc (и за rc) ))
                                    у нас он ломался при "французских" кавычках в Version Info, например )

                                    0
                                    Есть в планах на ближайшее полугодие статья про то, как я ловил пару неприятных ошибок при помощи WinDbg, но для D7.

                                    как напишете, пульните, я бы хотел почитать )

                                    –1
                                    В группе «Delphi Community»: t.me/DelphiCommunity добавлен опрос на тему «Какие версии Delphi Вы используете?»: t.me/DelphiCommunity/833. Приглашаю в группу а также пройти опрос
                                      0
                                      Я просто делаю вручную билд с нужными опциями и прочим, потом копирую строку компиляции снизу из раскрывающегося окошка Messages в самой Дельфе, удаляю лишние переводы строк, и это дело в запихиваю в cmd-файл, который можно вызвать по телнету через спецмонитор. Теперь любой разработчик может записать свои изменения в версиях и в терминале вызвать билд. Можно и по расписанию делать.
                                        0

                                        а если define-ы, например, поменялись? всё повторить? ) (и какая версия Delphi?)


                                        по телнету? для чего?

                                          0
                                          А с чего у меня в проекте будут дефайны меняться? У нас не кружок начинающих «компьютерщиков», которые все проблемы лечат переустановкой Винды. Отладочные версии каждый сам себе делает, а выкладывается релизный билд. Всё уже устоялось, все дефайны прописаны годы назад. Версия 10.2
                                          По телнету мне проще всего оказалось сделать, чтоб чел в терминале же видел, как идёт билд.
                                            0
                                            А с чего у меня в проекте будут дефайны меняться?

                                            с того, например, что продукт, как бы, развивается? )


                                            Версия 10.2

                                            ну вот можно обойтись без копирования простыней ) и вставки их в телнет )...


                                            кроме того, dcc32 — это только ж компиляция, но выпуск ПО — это не только компиляция, это подготовка ресурсов до компиляции, после — подписывание цифровой подписью, запаковка в архив, деплой/копирование на общий ресурс...


                                            По телнету мне проще всего оказалось сделать, чтоб чел в терминале же видел, как идёт билд.

                                            а если связь порвётся в момент сборки (хоть она и быстрая обычно) — заново запускать?


                                            Телнет-то где? для чего? чтобы на сервере собирать, а не локально? сдаётся мне — это можно решить сервером сборок, который и логи ведёт и чтобы "чел видел, как идёт билд" (зачем?)

                                          0
                                          Ломается, как только надо перенести проект на другой компьютер. А то и просто на другой диск.
                                            0

                                            относительные пути помогли бы ЭТОЙ проблеме ) (но само такое решение — меня прям удивило ))

                                              0
                                              Относительные пути решают только часть этой проблемы.
                                              В другом месте может оказаться сама Delphi.
                                              Или из-за некорректно настроенного окружения первая оказавшаяся в PATH dcc может оказаться не той версии что тоже всё сломает.
                                              Это работает только в одном строго фиксированном окружении. Любое изменение (безусловно его может не быть если у человека ровно один проект) и всё надо начинать сначала.
                                              Но даже для такого сценария больше подошёл бы git с хуком на пуш, автозапуском скрипта и отправкой уведомления на почту по завершению.

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

                                                ну, проблему фиксированности окружения решает система управления конфигурациями — тот же Ansible (и да, он работает и на Windows)
                                                а запуск на таких окружениях должен делать сервер сборок… и никак не "ручками"…
                                                тогда есть и повторяемость, и логи, и мониторинг и отчётность и т.п...

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое