The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

使用 MSBuild 建置方案檔(sln)與建置專案檔(csproj)的陷阱與注意事項

我一年大概都會幫幾家企業導入 Azure DevOps Server 平台,最近幫客戶導入的過程遇到了一個難題。一個方案檔中有 9 個專案,其中有 4 個 .NET Framework 4.7.2 類別庫專案、1 個 .NET Framework 4.7.2 的 ASP.NET Web Forms 專案、2 個 .NET Core 2.1 類別庫專案、1 個 .NET Standard 2.0 專案、2 個 .NET Core 2.1 類別庫專案、1 個 .NET Core 2.1 的 ASP.NET Core 專案。很少看到一個案子用這麼混搭的技術,而這個案子要做 CI/CD 確實也遇到了問題。這篇文章我將說明問題與解決方法!

問題說明 (1)

導入 CI/CD 的過程,首先要先確認透過 Visual Studio 可否順利建置,是可以的!

基本上只要可以順利透過 Visual Studio 2019 建置,那麼我們透過 MSBuild 一定也可以順利建置,只是這當中有很多不為人知的秘密,埋藏在一堆 Visual Studio 2019 才有的 *.targets 目標檔,不去分析 MSBuild 載入的檔案,很難抓到問題的根源。我這次導入這個方案的過程,花了我不少時間研究問題發生的原因,快速解決問題的方法不是沒有,但是不能釐清真相,就是讓人有那麼一絲絲的不太放心。

我習慣用 .NET CLI 來建置 .NET Core 專案,但是這個專案用 dotnet build 就是無法建置,因為在方案檔中用了一些 Visual Studio 2019 才有的目標檔 (*.targets),在 .NET SDK 中找不到,所以被迫要用 MSBuild 來建置專案,但過程並不順利。

首先,在 Azure DevOps Server 預設安裝完成後,設定 Pipelines 的時候 MSBuild 架構提供了兩個版本:

  1. MSBuild x86 (預設值)

    C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe
    
  2. MSBuild x64

    C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\amd64\msbuild.exe
    

基本上,這兩個 MSBuild 架構用來建置 .NET Framework 4.7.2 並沒有問題,但是用來建置 .NET Standard 2.0 類別庫專案的時候卻會失敗。要是用 .NET CLI 的 dotnet build 來建置,問題老早就解決了,可惜這個專案卻不能用,只能透過 MSBuild 才行。

如果我用預設的 MSBuild x86建置整個方案,就會遇到以下問題:

MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release"
##[error]MyProject.ViewModels\obj\Release\netstandard2.0\.NETStandard,Version=v2.0.AssemblyAttributes.cs(4,20): Error CS0400: 全域命名空間中找不到類型或命名空間名稱 'System' (是否遺漏了組件參考?)

嘗試了很久,最後發現有一個簡單的解決方法,那就是改用 MSBuild x64 就解決了,真是花惹發!

問題說明 (2)

好的,先解決建置整個方案的難題,問題就已經克服了一半,接著就是要分別建置方案中的兩個主要專案,一個是 .NET Framework 4.7.2ASP.NET Web Forms 專案,另一個是 .NET Core 2.1ASP.NET Core 專案。

因為兩個不同架構的東西,我就先嘗試分別建置專案檔(*.csproj),結果遇到以下問題:

  1. .NET Core 2.1ASP.NET Core 專案

    MSBuild MyProject\MyProject.csproj /p:platform="any cpu" /p:configuration="release"
    

    建置成功!

  2. .NET Framework 4.7.2ASP.NET Web Forms 專案

    MSBuild MyProject.BackEnd\MyProject.BackEnd.csproj /p:platform="any cpu" /p:configuration="release"
    

    建置失敗!錯誤訊息如下:

    ##[error]C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\amd64\Microsoft.Common.CurrentVersion.targets(820,5): Error : The BaseOutputPath/OutputPath property is not set for project 'MyProject.BackEnd.csproj'.  Please check to make sure that you have specified a valid combination of Configuration and Platform for this project.  Configuration='release'  Platform='any cpu'.  You may be seeing this message because you are trying to build a project without a solution file, and have specified a non-default Configuration or Platform that doesn't exist for this project.
    

進一步研究 MyProject.BackEnd.csproj 後發現,Azure DevOps 上面的 $(BuildPlatform) 變數預設是 any cpu,但我從 MyProject.BackEnd.csproj 裡面含有 OutputPath 屬性的地方,有個 PropertyGroupCondition'$(Configuration)|$(Platform)' == 'Release|AnyCPU',這裡的 $(Platform) 應該是 AnyCPU 才對:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <DebugType>pdbonly</DebugType>
  <Optimize>true</Optimize>
  <OutputPath>bin\</OutputPath>
  <DefineConstants>TRACE</DefineConstants>
  <ErrorReport>prompt</ErrorReport>
  <WarningLevel>4</WarningLevel>
</PropertyGroup>

詭異的地方就在於,我明明在 Visual Studio 2019 中都可以順利建置,用 MSBuild 也可以順利建置 MyProject.sln 方案檔,但直接建置 MyProject.BackEnd.csproj 就不行呢?可是建置 .NET Core 的專案檔裡面,也有相關的 PropertyGroup,也有設定一樣的 Condition 就沒事呢?

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
  <DocumentationFile>bin\Release\netcoreapp2.1\MyProject.xml</DocumentationFile>
</PropertyGroup>

我進一步看了 MyProject.sln 方案檔的內容,發現在方案檔中的 Platform 定義,的確是 Any CPU 沒錯:

GlobalSection(SolutionConfigurationPlatforms) = preSolution
    Debug|Any CPU = Debug|Any CPU
    Release|Any CPU = Release|Any CPU
EndGlobalSection

目前到這裡,就是一堆無法理解的問題了,而且兩邊有點衝突,這個詭異的差異 (Any CPU v.s. AnyCPU),讓我在建置專案檔的時候,必須指定不同的 $(BuildPlatform) 變數值才能解決。所以我的第一版解決方法是:

  1. .NET Core 2.1ASP.NET Core 專案

    MSBuild MyProject\MyProject.csproj /p:platform="anycpu" /p:configuration="release"
    

    建置成功!

  2. .NET Framework 4.7.2ASP.NET Web Forms 專案

    MSBuild MyProject.BackEnd\MyProject.BackEnd.csproj /p:platform="anycpu" /p:configuration="release"
    

    建置成功!

比較漂亮的解決方法

雖然上述的作法是可以成功建置的,但我無法接受,一定有個更合理的解決方法才對。

進一步研究後發現,原來用 MSBuild 建置 *.sln 方案檔的時候,事實上會在背後偷偷幫你產生一個暫時的 *.csproj 專案檔 (MSBuild File),並且幫你建置專案。所以上述的問題,我發現有個更漂亮的解決方式,那就是直接建置 *.sln 方案檔,但指定特定專案進行建置!

  1. .NET Core 2.1ASP.NET Core 專案

    MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject
    

    建置成功!

  2. .NET Framework 4.7.2ASP.NET Web Forms 專案

    MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject.BackEnd
    

    建置失敗,因為方案檔的目標(/t:XXX)不能包含特殊符號,所有符號都要換成「底線」才能執行成功,所以正確的命令應該要改成這樣:

    MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject_BackEnd
    

事實上,上面這種解決方案才是透過 MSBuild 建置專案時的標準方法,尤其是在實作 CI/CD 的時候,就應該這麼做,才能盡可能的貼近你在 Visual Studio 2019 建置專案的行為。如果我們要使用 Visual Studio publish profiles (.pubxml) 來發行應用程式,我們可以不用針對專案檔進行操作,直接用這個技巧就可以,例如:

MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject_BackEnd /p:DeployOnBuild=true /p:PublishProfile="FolderProfile" /p:publishUrl="$(build.artifactstagingdirectory)\MyProject.BackEnd\\"

記得: /p:publishUrl="..." 的最後面如果是 \ 結尾,一定要改成兩個反斜線 (\\) 才是正確語法!

直接執行 MSBuild 命令的技巧

有時候我們想直接透過命令列直接執行 MSBuild 來建置或發行專案,但是偶爾會遇到一些環境變數的問題,這邊我分享一個很簡單的解決方案。

你可以在執行 MSBuild 命令之前先執行以下命令,就可以載入 Developer Command Prompt for VS 2019 所需要載入的所有設定,讓你在執行的時候減少很多潛在的問題!

  • Visual Studio 2019 Community Edition

    CALL "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat"
    
  • Visual Studio 2019 Enterprise Edition

    CALL "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat"
    

相關連結

留言評論