要對 .csproj 專案檔進行偵錯,其實是可行的(參考這篇很久以前的(官方文章)[https://devblogs.microsoft.com/visualstudio/debugging-msbuild-script-with-visual-studio/]),但是有些時候,我們只是想輸出變數的值,在建置過程中稍微檢查一下專案檔的設定或變數值,這篇來看一下怎麼做。

專案檔基本概念

我們常見到的 .csproj 專案檔,就是交給 MSBuild.exe (Microsoft Build Engine) 這隻程式讀取並執行建置的。而這個以 XML 為基礎所撰寫出來的專案檔,有一些基本結構要先認識一下,主要的元素結構會長的像下面這樣:

Project
├─ PropertyGroup
|   └─ Property
├─ ItemGroup
|   └─ Item
|       └─ ItemMetadata
└─ Target
    ├─ Task
    ├─ PropertyGroup
    |   └─ Property
    └─ ItemGroup
        └─ Item
            └─ ItemMetadata

其中特別要知道的元素用途如下:

  • Property 用來管理建置過程中會使用到的變數
  • Item 用來識別建置過程中的輸入值,例如程式碼檔案名稱、路徑等等
  • TargetTask 用來提供給 MSBuild 要如何執行建置程序

專案檔的內容還有很多細節,詳請參考瞭解專案檔這份官方文件。

關於輸出訊息

知道專案檔的結構後,不難發現要在哪裡處理輸出訊息,關鍵就是在 TargetTask 這兩個元素中。Target 可以看成較大的執行區塊,而 Task 則是某一個執行動作。

因此我們可以在專案檔中寫一個 Target 標籤,然後從MSBuild 工作參考這份官方文件種,找到預設所有可以使用的 Task,例如 Message

Message 這個 Task 使用方式相當簡單 <Message Text="$(TestProperty)" Importance="high"/>,其中 Importance 是在指定訊息的重要性,可以是 highnormallow,預設值是 normal。在一些輸出視窗中,會使用這個屬性來調整輸出的顏色。

所以如果要在 MSBuild 執行的過程中,將專案檔內的資訊輸出,則可以參考以下寫法:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TestProperty>Property Value</TestProperty>
    </PropertyGroup>

    <Target Name="OutputMessage" AfterTargets="Build" >
        <Message Text="$(TestProperty)" Importance="high"/>
    </Target>
</Project>

輸出結果如下:

Property Value

這樣當碰到一些建置過程中用到的預設變數時,例如 $(Configuration)$(BaseOutputPath)$(VisualStudioVersion) 等專案屬性的值,就可以藉此輸出出來,方便確定建置過程的正確性。

如果想知道 MSBuild 預設會有那些變數和專案屬性,可以參考這份一般 MSBuild 專案屬性官方文件。

本篇完整範例程式碼請參考 poychang/output-project-variable-from-msbuild

後記

關於 Target 還有一件很重要的設定,就是甚麼時候執行

由於 Target 只會被 MSBuidl 執行一次,在過去 .NET Framework 的時代,我們可以直接把 Target Name 設定成預設的名字,例如 BuildCompile 等(更多 Target 預設名字請參考MSBuild 目標),來覆蓋原本的行為(因為後者覆蓋前者)。

到了 .NET (.NET Core)時代,專案檔已經改用 SDK 風格的寫法,因此你無法直接覆蓋,而是建議你使用 AfterTargetsBeforeTargets 兩個屬性來將你的 Target 加到指定執行順序中,例如上面的範例就是在 Build 之後執行。

如果你真的要覆蓋某一個 Target 的時候,可以參考以下寫法:

<Project>
    <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
    <!-- 專案會用到的設定,例如專案屬性、套件位置等內容 -->

    <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
    <Target Name="Build">
        <!-- 將你要覆蓋的行為寫在這裡,這會覆蓋 Build 預設的行為 -->
    </Target>
</Project>

之所以這樣會可行,是因為你寫的 Target 會在 Sdk.targets 之後執行,所以可以覆蓋掉預設的行為。


參考資料:


Poy Chang

Trial and Error