使用 Visual Studio 發佈( Publish )一個使用 System.Data.SQLite 的 ASP.NET MVC 專案,部署到 IIS 後發生錯誤:

Unable to load DLL 'SQLite.Interop.dll': 
The specified module could not be found. (Exception from HRESULT: 0x8007007E)

檢查原因為 Publish 產生 bin 目錄有 System.Data.SQLite.dll 但缺了 x86 跟 x64 資料夾。System.Data.SQLite.dll 是所謂的 Mixed-Mode Assembly,需要搭配 x86\SQLite.Interop.dll 及 x64\SQLite.Interop.dll Native 程式庫才能運作。參考

Visual Studio 編譯( Build )時會依據開發環境的作業系統建立 bin/x86、x64 並放入 32/64 位元版 SQLite.Interop.dll,但發佈( Publish )則不會附上 SQLite.Interop.dll,預期由部署人員依伺服器作業系統補上 Native 程式庫, 猜想是 Publish 輸出結果的 bin 目錄只有 System.Data.SQLite.dll 但沒有 x86/x64 資料夾及 SQLite.Interop.dll 的原因。

部署前手動補上是種解法,但確定伺服器為 Windows,我決定把複製檔案的步驟整合進 Publish 程序,感覺更完美。要做到這點,可以從修改 .csproj 下手。

.csproj 可以加入額外的 Target,較為人知的是 BeforeBuild、AfterBuild,在其中放入 Task 即能在編譯前後執行自訂邏輯。 用文字編譯器檢視 .csproj,裡面就有段註解內容提示我們可以自訂 BeforeBuild 及 AfterBuild:

  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->

若不想花太多腦筋,安排個 Task 呼叫 DOS 批次指令將 SQLite.Interop.dll 複製到 Publish 目的資料夾問題就解了。 但這次我想依循 MSBuild 的正統方式完成工作,在 Target 放入 Task 建立 x86/x64 資料夾,並從編譯結果複製 SQLite.Interop.dll 過去。

實測 Publish 不像 Build 有 AfterBuild Target 可指定在編譯後執行,寫 <Target Name="AfterPublish"> 沒用。 但 Target 有個好用 Attribute 叫 AfterTargets,可以指定在某個 Target 執行完畢後觸發。

所以找出將檔案複製到 Publish 目的資料夾的 Target 名稱,緊接它後面執行就好了。

怎麼找呢? MSBuild Log 有線索,開啟 Visual Studio 選項,找到 Project and Solutions / Build and Run / MSBuild projectx build output verbosity,調成 Diagnotics:

之後再執行一次 Publish,Output 視窗的 Log 資訊變多,從中找到下列線索:

2>        Task "CallTarget"
2>        Task Parameter:RunEachTargetSeparately=False
2>        Done executing task "CallTarget".
2>        Done building target "CopyAllFilesToSingleFolderForPackage" in project "MyProj.csproj".
2>    Target "GatherAllFilesToPublish" in file "C:\Program Files (x86)\Microsoft Visual Studio\
2019\Enterprise\MSBuild\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.targets" 
from project "X:\Git\MyProj\MyProj.csproj" (entry point):
2>        Done building target "GatherAllFilesToPublish" in project "MyProj.csproj".

Publish 程序最後一個 Target 的名稱是 GatherAllFilesToPublish,所以我們新增一個<Target Name="CopySQLiteInteropAfterPublish" AfterTargets="GatherAllFilesToPublish">便能在 Publish 複製完檔案後被觸發。

下一個麻煩問題是,我要怎麼知道 Publish 輸出目錄在哪悝? MSBuild 過程有些巨集變數如 $(ProjectDir)、$(PublishDir) 可供利用,若不熟悉, Visual Studio 專案屬性頁的 Build Events / Edit Post-build... 對話框是很好的查詢來源:

不過在裡面我查不到本案例需要的 Publish 輸出資料夾位置(沒辦法,它不是 Build 會用到的項目),於是再回到 MSBuild Output 找線索:

1>PublishProfileName = FolderProfile
1>PublishProfileRootFolder = X:\GIT\MyProj\Properties\PublishProfiles
1>PublishProvider = FileSystem
1>publishUrl = D:\PublishOutput
1>RebuildDependsOn = 
1>      BeforeRebuild;
1>      Clean;
1>      Build;
1>      AfterRebuild;

Bingo! 我找到一個參數 publishUrl 指向 Publish 輸出目錄。

再來是組裝 Task,MSBuild 有各式 Task 可供利用,本次的案例算單純,我用到這兩個:

註:若不想花時間研究 MSBuild 的 Task,如先前所提,可用 Exec Task 呼叫用 Batch 或外部程式解決是條省事的路。(但也喪失了解 MSBuild 的機會)

依據範例,Copy Task 的 SourceFiles 習慣會先用<ItemGroup><Include>定義要複製的檔案項目。

經過一番研究,我寫出笨拙但可以跑的第一版,加在 .csproj 的最後方:

  <!-- ...略
  <Target Name="AfterBuild">
  </Target>
  -->
  <Target Name="CopySQLiteInteropAfterPublish" AfterTargets="GatherAllFilesToPublish">
	<ItemGroup>
		<X86 Include="$(TargetDir)x86\*.*" />
		<X64 Include="$(TargetDir)x64\*.*" />
	</ItemGroup>
	<MakeDir Directories="$(PublishUrl)\bin\x86" Condition="!Exists('$(PublishUrl)\bin\x86')" />
	<Copy SourceFiles="@(X86)" DestinationFolder="$(PublishUrl)\bin\x86" SkipUnchangedFiles="true" />
	<MakeDir Directories="$(PublishUrl)\bin\x64" Condition="!Exists('$(PublishUrl)\bin\x64')" />
	<Copy SourceFiles="@(X64)" DestinationFolder="$(PublishUrl)\bin\x64" SkipUnchangedFiles="true" />
  </Target>
</Project>

做了功課,再找出一些可進化的地方:

  1. Copy Task 會自己建立資料夾,故 MakeDir 是多餘的。
  2. <ItemGroup> Include 檔案時,可使用 ** 萬用字元,之後 Copy 時搭配 %(RecursiveDir) Metadata 可在目的地建立相同的資料夾結構。

改良版精簡多了,功能相同:

  <Target Name="CopySQLiteInteropAfterPublish" AfterTargets="GatherAllFilesToPublish">
	<ItemGroup>
		<SQLiteFiles Include="$(TargetDir)**\SQLite.Interop.dll" />
	</ItemGroup>
	<Copy SourceFiles="@(SQLiteFiles)" DestinationFolder="$(PublishUrl)\bin\%(RecursiveDir)" 
	      SkipUnchangedFiles="true" />
  </Target>

整理我們在這個案例學到的技巧:

  1. .csproj 可以加上 Target 搭配 Task 在編譯或發佈時加入自訂步驟(例如:複製檔案、建立資料夾... 等)
  2. 開啟 VS Project and Solutions / Build and Run / MSBuild projectx build output verbosity 選項,可從 Output 視窗觀察 MSBuild 過程找到線索
  3. 若要在 Publish 後執行特定工作,可使用 <Target Name="CopySQLiteInteropAfterPublish" AfterTargets="GatherAllFilesToPublish">
  4. Copy Task 的使用方式,以及 Item Include ** 配合 %(RecursiveDir) 的實用技巧

When using System.Data.SQLite, the native SQLite.Interop.dll are not included in publish output folder. This article introduce how to add a custom Target in csproj to finish this job.


Comments

Be the first to post a comment

Post a comment


23 + 12 =