我一年大概都會幫幾家企業導入 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 架構提供了兩個版本:
-
MSBuild x86
(預設值)
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe
-
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.2 的 ASP.NET Web Forms 專案,另一個是 .NET Core 2.1 的 ASP.NET Core 專案。
因為兩個不同架構的東西,我就先嘗試分別建置專案檔(*.csproj
),結果遇到以下問題:
-
.NET Core 2.1 的 ASP.NET Core 專案
MSBuild MyProject\MyProject.csproj /p:platform="any cpu" /p:configuration="release"
建置成功!
-
.NET Framework 4.7.2 的 ASP.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
屬性的地方,有個 PropertyGroup
的 Condition
是 '$(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)
變數值才能解決。所以我的第一版解決方法是:
-
.NET Core 2.1 的 ASP.NET Core 專案
MSBuild MyProject\MyProject.csproj /p:platform="anycpu" /p:configuration="release"
建置成功!
-
.NET Framework 4.7.2 的 ASP.NET Web Forms 專案
MSBuild MyProject.BackEnd\MyProject.BackEnd.csproj /p:platform="anycpu" /p:configuration="release"
建置成功!
比較漂亮的解決方法
雖然上述的作法是可以成功建置的,但我無法接受,一定有個更合理的解決方法才對。
進一步研究後發現,原來用 MSBuild 建置 *.sln
方案檔的時候,事實上會在背後偷偷幫你產生一個暫時的 *.csproj
專案檔 (MSBuild File),並且幫你建置專案。所以上述的問題,我發現有個更漂亮的解決方式,那就是直接建置 *.sln
方案檔,但指定特定專案進行建置!
-
.NET Core 2.1 的 ASP.NET Core 專案
MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject
建置成功!
-
.NET Framework 4.7.2 的 ASP.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"
相關連結