使用 Nuke 建置專案時碰到的微妙狀況

記錄一下我在使用 Nuke 來建置專案時碰到一個奇妙狀況,以及跟 Nuke 開發者討論此問題的過程和一些心得。

內容綱要
  1. 問題描述
  2. 調查過程
  3. Lessons Learned
(寫得挺囉嗦,可直接看結論,畢竟我碰到的是比較特殊的狀況。)

問題描述

當我看到 Nuke 0.24.7 發布時,便將我的一個 .NET 專案裡面使用的 Nuke 套件更新到 0.24.7 版。更新之後,專案建置失敗,錯誤訊息是:
Package 'GitVersion.Tool' is referenced with multiple versions. Use NuGetPackageResolver and SetToolPath.
如下圖:


根據錯誤訊息來研判,似乎是我的 build project 參考了多個版本的 GitVersion.Tool 套件。然而,我的 build project 完全沒有參考 GitVersion.Tool。

奇怪的是,如果我重新建立一個 Nuke 建置腳本專案,而且用一個簡單的 Console App 專案來測試建置腳本,卻不會發生上述錯誤。

經由比對兩個建置腳本專案,我發現只要在那個無法順利執行的 build 專案的 .csproj 檔案裡面加入一行 <PackageDownload Include="GitVersion.Tool" Version="[5.1.1]" /> 就解決了,像這樣:

看來問題已經解決?也許,但不確定這是否為正解,畢竟跟錯誤訊息的意思兜不攏。

錯誤訊息明明說的是「多重參考了 GitVersion.Tool 套件」,可是解決問題的方法卻告訴我們相反的事:其實是遺漏參考了 GitVersion.Tool,導致建置過程找不到那個套件。

對此現象,我想 Nuke 作者 Matthias Koch 也許能提供一些線索或建議,於是到 Gitter 上面發問(後來移至 Slack 討論),對方也花了不少時間協助排除問題。

調查過程

我先提供了能夠重現問題的程式碼,放在 GitHub 上面讓對方測試(現在已經從 GitHub 上移除)。結果在他的環境上執行 Nuke build 時,出現的錯誤訊息卻是「找不到 GitVersion.Tool。」

同樣的程式碼,在不同電腦上面執行,出現了不同的錯誤訊息。這表示很可能是環境或某個組態設定的差異所致。此時仍猜不出到底是哪裡的差異,於是 Koch 問我能否除錯看看。

除錯過程中發現一個有趣的地方:如果我用「加入專案參考」的方式把 Nuke.Common 專案加入我的 solution,打算以此方式來設定中斷點和單步除錯 Nuke 原始碼,在執行建置腳本時,卻不會出現 GitVersion.Tool 多重參考的錯誤訊息,反倒是出現跟 Koch 那邊一樣的錯誤:「找不到 GitVersion.Tool。」

我對此現象很好奇,但此時仍猜不出原因。(後來找到原因後,這現象便有了合理解釋)

既然「加入專案參考」的方式無法重現問題,Koch 建議我用 JetBrains 的 Rider 來除錯,因為 Rider 在 Step Into 某個函式時,若沒有原始碼,就會自動下載原始碼。

感謝 Koch 先生的建議,透過 Rider 強大的除錯功能,我似乎找到問題的原因:exception 是由 NuGetPackageResolver GetGlobalInstalledPackage 方法所拋出來。具體來說,問題出在那個方法當中的某個 SingleOrDefault 呼叫,如下圖:


然而 Koch 先生並不同意我的看法。後來,我描述了單步追蹤過程,搭配畫面截圖,甚至錄製了一個影片來呈現 exception 的「發源地」,可以肯定就在程式碼較深處的「那一行」SingleOrDefault 呼叫,但是他依然認為這些都無法解釋「GitVersion.Tool 重複參考」的錯誤訊息。也許我真的弄錯方向、太快下結論了。

由於對方的環境無法重現問題,我只好再努力一下,提供更具體的線索:那個較為底層的 SingleOrDefault 呼叫所取得的結果有兩筆以上的資料(因而導致 exception),而那兩筆資料是存在本機的 NuGet 套件:System.Dynamic.Runtime。如下圖:


也就是說,Nuke 預期某個套件只能有一個,可是執行時卻找到了兩個。於是我去看本機的 NuGet 全域套件的所在目錄,果然:



當我把這兩個長得很像的目錄刪除其中一個,再次除錯時,便發現程式依然會出錯,只是錯誤的原因換成了另一個套件名稱,而且該套件一樣是位在上圖的目錄下。

於是,根據我目前對 Nuke 原始碼的理解,再加上前面的幾個現象,我得出結論:Nuke 建置時,會從當前的 build project 的 .csproj 取出參考的套件,然後又會針對這些套件逐一取得它們各自參考的套件,且反覆這個程序,直到找出所有的相依套件清單。就是在這個過程當中,由於我的 NuGet 全域套件資料夾底下有許多「可疑的重複套件」的資料夾,導致 Nuke 程式發生錯誤。

後來,我又錄製了一個影片。這次不錄程式碼的單步追蹤過程,而只是凸顯一個事實:如果我先前的研判正確,那麼只要我把整個 NuGet 全域套件資料夾清空,再去執行 Nuke 建置腳本,應該就不會再出現「GitVersion.Tool 重複參考」的錯誤。影片如下:


就如影片最後顯示的,把 NuGet 全域套件資料夾清空之後,再跑一次建置腳本,錯誤訊息便從「多重參考 GitVersion.Tool」變成了「找不到 GitVersion.Tool」了。這才是正確的錯誤訊息!

現在,前面的謎團和現象都可以兜得攏、說得通了。

解決方法也如開頭講的,在 build 專案的 .csproj 檔案中加入這行就行了:
 <PackageDownload Include="GitVersion.Tool" Version="[5.1.1]" />

Lessons Learned

  • 溝通不容易,跟別人討論他寫的程式碼更不容易,而跟別人用英文來討論他寫的程式碼而且對方環境無法重現問題...... 😵
  • 小心層層傳遞的 IEnumerable<T> 和委派,它們讓程式碼看起來簡潔優雅(甚至高端),但是有可能增加日後除錯和維護的麻煩。
  • Rider 在除錯方面的功能很好用(其他功能我還沒特別去了解,故不敢說,不過這個 IDE 的口碑是很好的)。
  • 正常來說,NuGet 的全域套件資料夾底下,所有的套件子目錄應該都是全部小寫來命名。像本文提到的現象,那些「System.Dynamic.Runtime.4.0.11」之類的資料夾,我也無從追查究竟是何時、由哪個應用程式產生的了。
另外,我越來越不喜歡在建置腳本中使用 GitVersion 來自動設定應用程式的版本編號了。還是用一個簡單的文字檔案來手動指定版本編號最單純、直觀。

延伸閱讀

技術提供:Blogger.