2019年4月28日 星期日

使用 Delphi 的指令進行自動建置,並排除執行時可能遇到的問題

前言、CI簡單介紹

在專業的軟體公司中,需求分析、系統分析、系統設計、Coding、版本控制、Build 版本、測試,是每一天不斷在進行的過程。從 Delphi 還在 Borland 旗下的時候,就不斷在這些流程當中尋求優化。

2005年到2007年之間,Borland出色的版本控制系統 - Star Team 是我當時服務的公司非常倚重的核心版本控制軟體,只可惜 Star Team 的價格很高,無法普及,隨著時間的流逝,版本控制系統一度流行走過了 SVN,現在則是全球流行用 Git 來做版本控制。

目前最流行的免費 Git 版本控制系統提供商,絕對是 GitHub 與 GitLab,這兩個提供者之間的差別,坊間有許多的比較資訊,透過 Command Line,我們可以很快速的把專案透過 GitHub/GitLab 同步,讓許多開發人員同時一起對一個相同的專案進行程式碼的修改與新增。

GitLab/GitHub 要如何跟 Delphi 搭配做到 CI (Continue Integration),Embarcadero在過去兩年內,也和很多家 CI 的軟體進行搭配,簡單的 Google 一下,絕對可以找的到相關文章,我在這裡就不獻醜了。

這一篇文章要跟大家分享的,是我們自己土炮製作出自動/手動建置系統時,透過 Delphi編譯指令時,一定會遇到的問題,截至 2019/4/28 下午4:41,我還沒有回報問題給 Embarcadero,但即使回報了,也不見得所有開發人員都會更新到最新版的 Delphi,所以,如何在現狀中找到求生方法,就是這一篇要跟大家分享的主題了。

如何透過指令來建置 Delphi 專案

Delphi 的開發人員,對於在 Delphi 的 IDE 當中要如何編譯、建置專案,應該都不用我多說,無論是 F9, Ctrl + F9, 或是 Shift + F9, 都可以簡單的把 EXE 檔案生出來。

但到了 DOS 視窗裡面,要怎麼面對 Delphi 的 DProj 與 DPR 檔案,恐怕連資深的開發人員也要稍微想上一會兒,不囉嗦,我們直接開講:

開啟一個 DOS 命令提示字元視窗 -> 快速鍵 (Win + R, Cmd, Enter)
開啟 DOS 視窗之後,我先把路徑切到專案所在的目錄 (D:\XE10.3-Rio\jsonDefTest),如上圖所示。

從 Delphi 2007 開始,Delphi 不做自己的組建系統,改以使用 MSBuild 來建置 Delphi 的專案,專案檔仍是使用從 Delphi 2005 開始就沿用的 dproj 檔案格式。一般來說,如果沒有特殊的需求,直接鍵入 "MSBuild 專案名.dproj",就能把專案建置出來了,我們這就來試試看:
居然出現有錯,說是找不到 msbuild 這個指令?!!!

是的,Delphi 的編譯還需要不少環境變數的設定,所以在安裝有 Delphi 的系統中,都會在 Delphi 的目錄裡加入一個名為 rsvars.bat 的批次檔案,這個路徑已經在安裝 Delphi 的時候自動被安裝程式加到了我們的登入設定中。

所以要執行 MSBuild 之前,請先執行 rsvars 這個指令,就可以成功了:






在 Embarcadero 的說明網站中,也有介紹許多 MSBuild 對於 Delphi 專案的參數,大家可以先看一下這些參數,了解一下 MSBuild 的操作方法,最少我們會需要知道如何建立 Debug/Release 這兩種不同設定組態的執行檔,以下的指令就是建置 Release 組態的 EXE檔案,相當於我們在 IDE 裡面按下 Shift+F9.

MSBuild "D:\XE10.3-Rio\jsonDefTest\jsonDefTest.dproj" /t:Build /p:Config="Release"


這樣的建置,會把專案裡面用到的所有檔案都重新編譯,不會用到之前就已經建置好的 dcu 檔案 (當然 RTL 除外啦),這作法也是大多數的開發朋友們最常用來建立 EXE 檔案的作法。

Pre-build, Post-build Event

在 Delphi 使用了 MSBuild 之後,也把 MSBuild 的好處一起帶進到 Delphi 的編譯環境中,例如建置前、建置後要執行哪些作業,就可以在專案裡面設定好,設定的畫面如下:


這個功能,只有在比較複雜的專案中,才能發覺它的重要性,舉幾個目前我遇到使用這個功能的專案需要它的原因:
  • 專案常常需要變更 Output Dir,因為開發環境跟安裝環境的路徑中,有些檔案可能不一致,所以編譯完成後,要把產出的檔案複製到固定目錄,讓安裝程式找到的都是最新版本的檔案。
  • 多個專案需要使用到共用的資源檔案,為了避免這些資源檔案有版本不一致的機會,在編譯 EXE 檔案前要先編譯資源檔 (*.RES),編譯好了之後,再把編譯出來的 RES檔案複製到各個專案目錄中。

介紹到這裡,相信大家都已經可以回頭在自己的系統中試試看怎麼用指令方式建置自己的系統了吧?如果要整合 GitLab/GitHub,相信也並不複雜,真的有問題,可以詢問 Embarcadero 的代理商,說不定我就會出現在貴單位,協助建置自動建置系統了。

問題一:版號如何維持最新?要用什麼方法更換版號與Debug/Release組態?

這是一個好問題,但如果你是比較積極的 Delphi 開發人員,應該不用問這個問題,可能你已經找到解決方法了。

這個方法就是:在 MSBuild 執行之前,先把 dproj 檔案裡面的 FileVersion 跟 ProductVersion 更換為我們想要的內容,我已經把這些邏輯寫成以下的程式碼,開放給所有需要的開發人員,可以免費商用,但請在產品的說明文件中提及作者與原始網址(就是本篇文章的部落格網址),程式碼在本篇文章的最後。

問題二:多個Pre-build/Post-build指令在 dproj 當中沒有問題,但為何用 MSBuild 指令建置的時候,會回報錯誤?這問題要怎麼解決?  

這問題就是本篇文章最開始的時候提到的,問題是因為在 dproj 存檔的時候,是以XML作為檔案格式,而多行指令存檔的時候,會以&作為換行的記號。

但問題來了,&符號是 XML 裡面用來標註特殊符號的保留字,但存檔的時候,XML會自行對它做 Encode,所以 & 符號就成了 sLineBreak + &&

這麼一來, MSBuild 執行的時候,遇到 & 符號就沒辦法處理(因為它應該是換行符號啊.......)。所以,我們得在執行MSBuild之前,把這個符號先換成一個 & 這樣一來,MSBuild 才能正確處理它。

MSBuild 使用方法很重要,使用時會遇到的兩個問題也為大家提供了解決方法,解決這兩個問題的程式碼如下,公開給大家免費使用,如果這個小工具有幫助,記得使用的時候發個 Email 給我,告訴我一聲,如果有遇到問題,也歡迎跟我聯繫一下:


program changeProjVer;

////////////////////////////////////////////////////////////////////////////////
/// Created by Dennies Chang dennies@ms4.hinet, dennies226@gmail.com
///
///   If you need to use this utility, please refer the original URL:
///   https://firemonkeylessons.blogspot.com/2019/04/delphiBuildCommandAndTools.html
///
///   And do not remve these lines.
///   The code is opened for all Delphi programmers, you can use it as
///   commercial/non-commercial usage, what you have to do, is to have a notice
///   for the original author.
///
///   And send an Email to dennies@ms4.hinet.net to me, thanks.

{$APPTYPE CONSOLE}
{$R *.res}

uses
   System.SysUtils, IdGlobal, Classes;

var
   currentFile, tmpStr, completeStr, tmpMajor, tmpMinor, tmpRelease,
       tmpBuild, configName: String;
   lineIdx: Integer;
   src: TStringList;
   bDebug : boolean;
begin
   try
      { TODO -oUser -cConsole Main : Insert code here }
      if ParamCount < 2 then begin
         writeln('Usage: changeProjVer.exe dprojFileFullPath versionNo [Debug|Release]');
         writeln('versionNo should be contain 3 dots, e.g.,: 107.1.108.321');
         writeln;
         Readln;
      end
      else begin
         currentFile := ParamStr(1);
         tmpBuild := ParamStr(2);

         bDebug := False;
         if ParamCount >= 3 then begin
            configName := ParamStr(3);
            bDebug := configName.ToLower = 'debug';
         end;

         tmpMajor := Trim(Fetch(tmpBuild, '.'));
         tmpMinor := Trim(Fetch(tmpBuild, '.'));
         tmpRelease := Trim(Fetch(tmpBuild, '.'));
         tmpBuild := Trim(Fetch(tmpBuild, '.'));

         if FileExists(currentFile) then begin
            src := TStringList.Create;
            try
               src.LoadFromFile(currentFile, TEncoding.UTF8);

               for lineIdx := 0 to src.Count - 1 do begin
                  completeStr := src.Strings[lineIdx];
                  tmpStr := '';

                  if Pos('<VerInfo_MajorVer>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_MajorVer>');
                     tmpStr := #9 + #9 + '<VerInfo_MajorVer>' + tmpMajor +
                         '</VerInfo_MajorVer>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_MinorVer>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_MinorVer>');
                     tmpStr := #9 + #9 + '<VerInfo_MinorVer>' + tmpMinor +
                         '</VerInfo_MinorVer>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_Release>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_Release>');
                     tmpStr := #9 + #9 + '<VerInfo_Release>' + tmpRelease +
                         '</VerInfo_Release>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_Build>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_Build>');
                     tmpStr := #9 + #9 + '<VerInfo_Build>' + tmpBuild +
                         '</VerInfo_Build>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('FileVersion=', completeStr) > 0 then begin
                     // FileVersion
                     completeStr := src.Strings[lineIdx];
                     tmpStr := '';
                     while Pos('FileVersion=', completeStr) > 0 do begin
                        tmpStr := Fetch(completeStr, 'FileVersion=');
                        tmpStr := tmpStr + 'FileVersion=' +
                            StringReplace(ParamStr(2), ' ', '',
                            [rfReplaceAll]) + ';';
                        Fetch(completeStr, ';');
                     end;

                     if Length(completeStr) > 0 then begin
                        tmpStr := tmpStr + completeStr;
                     end;
                  end;

                  // 這兩個會出現在同一行, 不要加 else
                  if Pos('ProductVersion=', completeStr) > 0 then begin
                     completeStr := tmpStr;
                     tmpStr := '';
                     // ProductVersion
                     while Pos('ProductVersion=', completeStr) > 0 do begin
                        tmpStr := Fetch(completeStr, 'ProductVersion=');
                        tmpStr := tmpStr + 'ProductVersion=' +
                            StringReplace(ParamStr(2), ' ', '',
                            [rfReplaceAll]) + ';';
                        Fetch(completeStr, ';');
                     end;

                     if Length(completeStr) > 0 then begin
                        tmpStr := tmpStr + completeStr;
                     end;
                  end;

                  if (tmpStr = '') and (tmpStr <> completeStr) then
                     tmpStr := completeStr;

                  src.Strings[lineIdx] := tmpStr;
               end;

               src.Text := StringReplace(src.Text, sLineBreak + '&amp;&amp;', '&amp;', [rfReplaceAll]);
               src.SaveToFile(currentFile, TEncoding.UTF8);
            finally
               src.Free;
            end;
         end;
      end;
   except
      on E: Exception do
         writeln(E.ClassName, ': ', E.Message);
   end;

end.