無論你是開發 Console 主控台應用程式,或是 ASP․NET Core 網頁應用程式,最終都會需要發行部署,而一個簡單的 dotnet publish
卻潛藏著許多不為人知的用法。今天這篇文章我打算來梳理一下我過往曾經用過的發行技巧,做個詳細的用法整理。
準備範例專案
dotnet new console -n ConsoleApp1
cd ConsoleApp1
基本發行方法
-
基本用法
基本上透過以下命令就可以發行一個可執行檔:
dotnet publish
從輸出訊息可以看出,你的預設 Configuration 為 Debug
組態,而目標框架為 net6.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\net6.0\ConsoleApp1.dll
ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net6.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\net6.0\ConsoleApp1.dll
ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net6.0\publish\
-
不要建置專案
如果你的專案已經先透過 dotnet build
建置過 (例如 CI Pipelines 已經先跑過),你可以透過 --no-build
參數,跳過建置作業,加快發行速度!
dotnet publish --nologo --no-build
輸出訊息:
ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Debug\net6.0\publish\
-
顯示詳細建置記錄
我們在執行 dotnet publish
的時候,你知道你是發行 Solution 還是 Project 嗎?想看到建置的細節,就要加上 -v
或 --verbosity
參數。其參數值總共有 5 種,分別是:
-
q
或 quiet
完全不輸出訊息
-
m
或 minimal
輸出最小訊息 (預設值)
-
n
或 normal
輸出基本訊息
-
d
或 detailed
輸出詳細的訊息
-
diag
或 diagnostic
輸出超級詳細的訊息
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\net6.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
套用組態設定
-
套用 Release
或 Debug
組態
你可以透過 -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\net6.0\ConsoleApp1.dll
ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net6.0\publish\
-
套用自訂的組態名稱
如果你的專案有 *.sln
方案檔,裡面會定義方案可用的組態設定,你可以用以下命令建立方案檔:
dotnet new sln
dotnet sln ConsoleApp1.sln add ConsoleApp1.csproj
建立方案檔之後,裡面就可以看到一個 GlobalSection
全域區段的 SolutionConfigurationPlatforms
設定,裡面預設只有定義 Debug
與 Release
組態名稱:
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\net6.0\ConsoleApp1.dll
ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Staging\net6.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 還有區分兩種:
-
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 有 win10-x64
, linux-x64
, osx-x64
這三個,完整的 RID 清單請見 .NET RID Catalog
-
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 win10-x64
輸出訊息:
Determining projects to restore...
All projects are up-to-date for restore.
ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net6.0\win10-x64\ConsoleApp1.dll
ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net6.0\win10-x64\publish\
常見的 RID 有 win10-x64
, linux-x64
, osx-x64
這三個,完整的 RID 清單請見 .NET RID Catalog
這樣的部署方式會產生大量的檔案在輸出目錄,而且總檔案大小會較大!
-
將自封式部署發行成單一檔案
加上 -p:PublishSingleFile=true
參數,就可以把所有相依的 *.dll
或 *.json
檔案全部打包進 *.exe
主要執行檔中:
dotnet publish -c Release --nologo --self-contained -r win10-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\net6.0\win10-x64\ConsoleApp1.dll
ConsoleApp1 -> G:\Projects\ConsoleApp1\bin\Release\net6.0\win10-x64\publish\
我們列出 bin\Release\net6.0\win10-x64\publish\
資料夾的內容,你會發現檔案只剩下兩個:
Directory of G:\Projects\ConsoleApp1\bin\Release\net6.0\win10-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 win10-x64 -p:PublishSingleFile=true -p:DebugType=embedded
如果你不想輸出 *.pdb
也不想內嵌 *.pdb
的話,可以這樣下命令:
dotnet publish -c Release --nologo --self-contained -r win10-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 win10-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 win10-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\net6.0\win10-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 win10-x64 -p:PublishSingleFile=true -p:DebugType=embedded -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true -p:EnableCompressionInSingleFile=true
壓縮之後的執行檔,會在第一次執行時自動解壓縮到 %TEMP%\.net
目錄下,所以啟動速度會稍微慢一點。
相關連結