將 .NET 執行檔跟所參照 DLL 合併成單一 EXE 檔的做法之前介紹過(Visual Studio編譯小技巧:工具程式一檔搞定 ),在專案用 NuGet 安裝 MSBuild.ILMerge.Task 就能輕鬆搞定。之前在 Console Application 用得挺順利,今天用在 WPF 卻卡在一個錯誤無法編譯:

The item "X:\TFS\RptBatchPrint\packages\Hardcodet.NotifyIcon.Wpf.1.0.8\lib\net451\Hardcodet.Wpf.TaskbarNotification.dll" in item list "ReferencePath" does not define a value for metadata "CopyLocal".  In order to use this metadata, either qualify it by specifying %(ReferencePath.CopyLocal), or ensure that all items in this list define a value for this metadata.  

用關鍵字爬文竟連回自己的文章,網友 agrozyme 留言提到相似問題,在 WPF 專案遇過一模一樣的錯誤稍後還留言分享了解決方法,但不幸地文件連結年久毁損,解法失傳…(搥牆)再爬文找到一兩則相似問題回報,但無人提出解決方案。

最後,換了關鍵字幸運找到另一種解法:Combining multiple assemblies into a single EXE for a WPF application – DigitallyCreated

原理是在 csproj 加入一段 AfterResolveReference 編譯作業指令(位置可放在 Microsoft.CSharp.targets 下方)

 

  <Target Name="AfterResolveReferences">
    <ItemGroup>
      <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
        <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
      </EmbeddedResource>
    </ItemGroup>
  </Target>

這段設定會在編譯時將專案參照到的 DLL 轉為 EmbeddedResource,之就可發現 EXE 變肥許多,用 JustDecompile 反組譯可看到專案參照的 Nancy.Hosting.Self、Json.NET、NLog、Hardcodet.Wpf.TaskbarNotification 等 DLL 已被轉成 EXE 內嵌資源:

接著在專案新増一個 Program.cs 當作啟動物件,先註冊自訂 AppDomain.CurrentDomain.AssemblyResolve 事件再呼叫原本的 WPF Application 啟動方法(App.Main())。在 AssemblyResolve 事件中,依組件名稱從 EmbeddedResource 中取出組件 DLL,再以 Reflection 方式載入傳回:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
 
namespace RptBatchPrintAgent
{
    public class Program
    {
        [STAThreadAttribute]
        public static void Main()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
            App.Main();
        }
 
        private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
        {
            Assembly executingAssembly = Assembly.GetExecutingAssembly();
            AssemblyName assemblyName = new AssemblyName(args.Name);
 
            string path = assemblyName.Name + ".dll";
            if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
            {
                path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);
            }
 
            using (Stream stream = executingAssembly.GetManifestResourceStream(path))
            {
                if (stream == null)
                    return null;
 
                byte[] assemblyRawBytes = new byte[stream.Length];
                stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
                return Assembly.Load(assemblyRawBytes);
            }
        }
    }
}

最後修改 Startup Object 指向 Program,大功告成!

就這樣,在 ILMerge 之外又學會一招  DLL 合併技巧~

註:原始文章下方有一些讀者回應提及這個做法在某些情境可能遇到的問題,如遇狀況可供參考。


Comments

Be the first to post a comment

Post a comment