The Will Will Web

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

分享幾個發行 .NET 應用程式的奇技淫巧

無論你是開發 Console 主控台應用程式,或是 ASP․NET Core 網頁應用程式,最終都會需要發行部署,而一個簡單的 dotnet publish 卻潛藏著許多不為人知的用法。今天這篇文章我打算來梳理一下我過往曾經用過的發行技巧,做個詳細的用法整理。

publish

準備範例專案

dotnet new console -n ConsoleApp1
cd ConsoleApp1

基本發行方法

  • 基本用法

    基本上透過以下命令就可以發行一個可執行檔:

    dotnet publish
    

    從輸出訊息可以看出,你的預設 Configuration 為 Debug 組態,而目標框架為 net8.0

    Microsoft (R) Build Engine version 17.2.2+038f9bae9 for .NET
    Copyright (C) Microsoft Corporation. All rights reserved.
    
      Determining projects to restore...
      All projects are up-to-date for restore.
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.dll
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net8.0\publish\
    

    注意: 發行過程同時也包含了還原套件與專案建置過程。

  • 不要顯示 Logo 資訊

    你可以透過 --nologo 參數,將 Microsoft 的產品版本宣告從輸出中移除:

    dotnet publish --nologo
    

    輸出訊息:

      Determining projects to restore...
      All projects are up-to-date for restore.
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.dll
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net8.0\publish\
    
  • 不要建置專案

    如果你的專案已經先透過 dotnet build 建置過 (例如 CI Pipelines 已經先跑過),你可以透過 --no-build 參數,跳過建置作業,加快發行速度!

    dotnet publish --nologo --no-build
    

    輸出訊息:

      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net8.0\publish\
    
  • 顯示詳細建置記錄

    我們在執行 dotnet publish 的時候,你知道你是發行 Solution 還是 Project 嗎?想看到建置的細節,就要加上 -v--verbosity 參數。其參數值總共有 5 種,分別是:

    • qquiet 完全不輸出訊息

    • mminimal 輸出最小訊息 (預設值)

    • nnormal 輸出基本訊息

    • ddetailed 輸出詳細的訊息

    • diagdiagnostic 輸出超級詳細的訊息

      dotnet publish --nologo --no-build -v n
      

      輸出訊息:

      Build started 2023/8/24 下午 01:11:31.
          1>Project "G:\Projects\ConsoleApp1\ConsoleApp1.csproj" on node 1 (Publish target(s)).
          1>_CopyResolvedFilesToPublishPreserveNewest:
            Skipping target "_CopyResolvedFilesToPublishPreserveNewest" because all output files are up-to-date
            with respect to the input files.
            Publish:
              ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net8.0\publish\
          1>Done Building Project "G:\Projects\ConsoleApp1\ConsoleApp1.csproj" (Publish target(s)).
      
      Build succeeded.
          0 Warning(s)
          0 Error(s)
      
      Time Elapsed 00:00:01.00
      

套用組態設定

  • 套用 ReleaseDebug 組態

    你可以透過 -c 參數加入組態名稱(Configuration Name):

    dotnet publish -c Release --nologo
    

    從輸出訊息可以看出,你指定了 Release 組態:

      Determining projects to restore...
      All projects are up-to-date for restore.
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net8.0\ConsoleApp1.dll
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net8.0\publish\
    
  • 套用自訂的組態名稱

    如果你的專案有 *.sln 方案檔,裡面會定義方案可用的組態設定,你可以用以下命令建立方案檔:

    dotnet new sln
    dotnet sln ConsoleApp1.sln add ConsoleApp1.csproj
    

    建立方案檔之後,裡面就可以看到一個 GlobalSection 全域區段的 SolutionConfigurationPlatforms 設定,裡面預設只有定義 DebugRelease 組態名稱:

    Microsoft Visual Studio Solution File, Format Version 12.00
    # Visual Studio Version 17
    VisualStudioVersion = 17.0.31903.59
    MinimumVisualStudioVersion = 10.0.40219.1
    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1.csproj", "{F2538AF2-D25E-4FA2-9D3C-DC6EF10D2BE6}"
    EndProject
    Global
      GlobalSection(SolutionConfigurationPlatforms) = preSolution
        Debug|Any CPU = Debug|Any CPU
        Release|Any CPU = Release|Any CPU
      EndGlobalSection
      GlobalSection(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
      EndGlobalSection
      GlobalSection(ProjectConfigurationPlatforms) = postSolution
        {F2538AF2-D25E-4FA2-9D3C-DC6EF10D2BE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {F2538AF2-D25E-4FA2-9D3C-DC6EF10D2BE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {F2538AF2-D25E-4FA2-9D3C-DC6EF10D2BE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {F2538AF2-D25E-4FA2-9D3C-DC6EF10D2BE6}.Release|Any CPU.Build.0 = Release|Any CPU
      EndGlobalSection
    EndGlobal
    

    如果你建置或發行的對象是 *.sln 方案檔的話,使用了定義以外的組態名稱就會出現錯誤,我以 Staging 這個名稱為例:

    dotnet publish -c Staging --nologo
    

    從輸出的錯誤訊息就可以看出有個 MSB4126 的建置錯誤:

    G:\Projects\ConsoleApp1\ConsoleApp1.sln.metaproj : error MSB4126: The specified solution configuration "Staging|Any CPU" is invalid. Please specify a valid solution configuration using the Configuration and Platform properties (e.g. MSBuild.exe Solution.sln /p:Configuration=Debug /p:Platform="Any CPU") or leave those properties blank to use the default solution configuration. [G:\Projects\ConsoleApp1\ConsoleApp1.sln]
    

    如果建置發行的目錄沒有 *.sln 或是直接建置發行 *.csproj 的話,就沒有這個限制,例如:

    dotnet publish ConsoleApp1.csproj -c Staging --nologo
    

    輸出訊息:

      Determining projects to restore...
      All projects are up-to-date for restore.
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Staging\net8.0\ConsoleApp1.dll
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Staging\net8.0\publish\
    
  • 組態的用法

    如果你有一段 C# 程式碼只需要在特定組態下執行,那麼你可以透過條件式編譯語法來定義,如下範例:

    #if DEBUG
        Console.WriteLine("Debug build");
    #elif STAGING
        Console.WriteLine("Staging build");
    #else
        Console.WriteLine("Release build");
    #endif
    

    請注意:程式碼中的組態名稱「一定」要寫成「大寫英文」喔!

框架相依部署 (Framework-dependent Deployment)

框架相依部署 (FDD) 意味著你要在部署的目標電腦先安裝 .NET Runtime 才能執行你的應用程式。

事實上 FDD 還有區分兩種:

  1. FDE (Framework-dependent Executable)

    含有可執行檔的應用程式,如果你在 Windows 平台執行 dotnet publish 命令,預設就是採用這種方式進行部署,預設會產生一個 *.exe 可執行檔。以本文的範例來說,就會出現一個 ConsoleApp1.exe 執行檔。

    你可以透過以下命令來發行指定 Linux 平台的可執行檔:

    dotnet publish -c Release -r linux-x64 --no-self-contained
    

    其中的 linux-x64 我們稱為 RID (Runtime Identifier),常見的 RID 有 win-x64, linux-x64, osx-x64 這三個,完整的 RID 清單請見 .NET RID Catalog

  2. FDD (Framework-dependent Deployment)

    沒有可執行檔的應用程式,部署的檔案主要以 *.dll 為主,需要透過 .NET Runtime 的 AppHost (也就是 dotnet.exe 程式) 來啟動應用程式,例如:

    dotnet ConsoleApp1.dll
    

    事實上,這個 ConsoleApp1.exe 執行檔只是一個「啟動器」,它會去呼叫 dotnet.exe 來執行 ConsoleApp1.dll 檔案,因此對容器化應用程式來說,這個 ConsoleApp1.exe 著實有點雞肋,所以我們在部署的時候可以加入 -p:UseAppHost=false 避免產生這個可執行檔。

    dotnet publish -c Release -p:UseAppHost=false
    

自封式部署 (Self-contained Deployment)

自封式部署 (SCD) 意味著你的應用程式會包含 .NET Runtime,所以不需要在部署的目標電腦安裝 .NET Runtime 就可以執行,但是會導致輸出的執行檔較大。

  • 發行一個內含 .NET Runtime 的部署

    dotnet publish -c Release --nologo --self-contained -r win-x64
    

    輸出訊息:

      Determining projects to restore...
      All projects are up-to-date for restore.
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net8.0\win-x64\ConsoleApp1.dll
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net8.0\win-x64\publish\
    

    常見的 RID 有 win-x64, linux-x64, osx-x64 這三個,完整的 RID 清單請見 .NET RID Catalog

    這樣的部署方式會產生大量的檔案在輸出目錄,而且總檔案大小會較大!

  • 將自封式部署發行成單一檔案

    加上 -p:PublishSingleFile=true 參數,就可以把所有相依的 *.dll*.json 檔案全部打包進 *.exe 主要執行檔中:

    dotnet publish -c Release --nologo --self-contained -r win-x64 -p:PublishSingleFile=true
    

    自行在 *.csproj 加入一個 PublishSingleFile 屬性並將屬性值設定為 true 也一樣有相同效果,請見 Sample project file 範例!

    輸出訊息:

      Determining projects to restore...
      All projects are up-to-date for restore.
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net8.0\win-x64\ConsoleApp1.dll
      ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net8.0\win-x64\publish\
    

    我們列出 bin\Release\net8.0\win-x64\publish\ 資料夾的內容,你會發現檔案只剩下兩個:

     Directory of G:\Projects\ConsoleApp1\bin\Release\net8.0\win-x64\publish
    
    2023/08/24  下午 02:15    <DIR>          .
    2023/08/24  下午 02:15    <DIR>          ..
    2023/08/24  下午 02:15        63,704,638 ConsoleApp1.exe
    2023/08/24  下午 02:15            10,256 ConsoleApp1.pdb
    
  • 將自封式部署發行成「真正的」單一檔案

    如果你連 *.pdb 都不想看到的話,也可以考慮將所有的 *.pdb 偵錯符號檔內嵌到 *.dll*.exe 之中,只要加入 -p:DebugType=embedded 參數即可!範例如下:

    dotnet publish -c Release --nologo --self-contained -r win-x64 -p:PublishSingleFile=true -p:DebugType=embedded
    

    如果你不想輸出 *.pdb 也不想內嵌 *.pdb 的話,可以這樣下命令:

    dotnet publish -c Release --nologo --self-contained -r win-x64 -p:PublishSingleFile=true -p:DebugType=none
    

    相關設定可參考 C# Compiler Options that control code generation 文件。

    注意:在 .NET Core 3.1 之前有個 -p:IncludeSymbolsInSingleFile=true 參數已經失效,請用本文介紹的方式取代。

    另外,如果你希望特定 *.dll 檔案不要封裝到單一檔案中的話,必須在 *.csproj 專案檔加入以下設定 (參考 Exclude files from being embedded 文件):

    <ItemGroup>
      <Content Update="Plugin.dll">
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      </Content>
    </ItemGroup>
    
  • 將自封式部署發行成「真真真正的」單一檔案

    你在發行的時候所加入的 --self-contained 參數,只會將 .NET 的 Managed DLL 合併在一起,若建置過程包含 Native libraries (原生函式庫) 檔案的話,這些檔案就會跟輸出的執行檔放在一起,那就不是真正的「單一檔案」發行了!

    為了要將這些原生檔案加入到單一執行檔中,你可以額外加入 -p:IncludeNativeLibrariesForSelfExtract=true 參數:

    dotnet publish -c Release --nologo --self-contained -r win-x64 -p:PublishSingleFile=true -p:DebugType=embedded -p:IncludeNativeLibrariesForSelfExtract=true
    

    另外還有個 -p:IncludeAllContentForSelfExtract=true 參數可以將所有發行的「內容」檔案都加入到執行檔中,執行時所有檔案都會被解壓縮出來,整個應用程式會被解壓縮到一個暫存目錄中執行。這個暫存目錄 Windows 的預設路徑為 %TEMP%/.net,而 Linux 與 macOS 的預設路徑為 $HOME/.net,你也可以定義 DOTNET_BUNDLE_EXTRACT_BASE_DIR 環境變數來指定解壓縮的資料夾路徑。

    如果你的應用程式會部署到 Linux 的 systemd 執行成背景服務的話,請記得務必要設定 DOTNET_BUNDLE_EXTRACT_BASE_DIR 環境變數,否則應用程式會找不到 $HOME/.net 路徑來解壓縮。設定範例如下:

    [Service]
    Environment="DOTNET_BUNDLE_EXTRACT_BASE_DIR=%h/.net"
    

    這兩個參數會改變 AppContext.BaseDirectory 的路徑,會變成是「解壓縮後」的目錄喔,這點要注意!

  • 將自封式部署發行成「真真真正的」單一檔案,順便刪減沒用到的 IL 指令碼

    你可以透過 -p:PublishTrimmed=true 將沒用到的 IL 指令碼給刪減掉,大幅降低最後輸出的執行檔大小:

    dotnet publish -c Release --nologo --self-contained -r win-x64 -p:PublishSingleFile=true -p:DebugType=embedded -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true
    

    結果的執行檔真的蠻小的:

     Volume in drive G is Temporary
     Volume Serial Number is FA93-F4AA
    
     Directory of G:\Projects\ConsoleApp1\bin\Release\net8.0\win-x64\publish
    
    2023/08/24  下午 02:42    <DIR>          .
    2023/08/24  下午 02:42    <DIR>          ..
    2023/08/24  下午 02:59        11,435,338 ConsoleApp1.exe
    

    刪減 IL 指令碼可能會導致部分用到 Reflection 機制的程式碼失效,使用這個參數前請務必測試過才能放心使用。另外,使用了 -p:PublishTrimmed=true 參數後,就不能同時指定 --no-build 參數。

  • 將自封式部署發行成「真真真正的」單一檔案,刪減沒用到的 IL 指令碼,還順便壓縮執行檔

    由於自封式部署的輸出大小較大,為了要能降低執行檔的大小,你還可以加上 -p:EnableCompressionInSingleFile=true 參數:

    dotnet publish -c Release --nologo --self-contained -r win-x64 -p:PublishSingleFile=true -p:DebugType=embedded -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true -p:EnableCompressionInSingleFile=true
    

    壓縮之後的執行檔,會在第一次執行時自動解壓縮到 %TEMP%\.net 目錄下,所以啟動速度會稍微慢一點。

相關連結

留言評論