dotnet publish single-file¶
Финальный артефакт — один .exe файл, ~250 МБ. Включает в себя .NET runtime, WPF, WebView2 loader, нативные DLL (RageLib native bits), весь React UI после npm run build.
Команда сборки¶
cd HunterGraphics.Shell
# Step 1: build React UI
Push-Location ui
npm install
npm run build
Pop-Location
# Step 2: dotnet publish
dotnet publish HunterGraphics.Shell.csproj `
-c Release `
-r win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:EnableCompressionInSingleFile=true `
-p:DebugType=None `
-p:DebugSymbols=false `
-o ../publish
Результат — publish/HunterGraphics.Shell.exe. Это standalone бинарь, не требует .NET установленного у юзера.
Ключевые свойства MSBuild¶
| Property | Что делает |
|---|---|
PublishSingleFile=true |
Упаковать всё в один .exe |
IncludeNativeLibrariesForSelfExtract=true |
Включить нативные DLL (без этого они рядом останутся) |
EnableCompressionInSingleFile=true |
LZ4 сжатие (250 МБ → 80 МБ) |
DebugType=None, DebugSymbols=false |
Не включать PDB |
SelfContained=true |
Включить .NET runtime |
Это даёт .exe ~80 МБ после сжатия. Без сжатия был бы ~250 МБ. Запуск чуть медленнее (приходится распаковывать в %TEMP%), но размер критичнее.
Распаковка single-file в runtime¶
При первом запуске .NET runtime распаковывает содержимое в %TEMP%\.net\<HostId>\<version>\. Это ~250 МБ файлов на диске. Запуск занимает ~5 секунд только в первый раз. Последующие запуски используют extracted cache (если timestamp не изменился) — миллисекунды.
Это проблема для нашего auto-update'а: после update у нового .exe новый timestamp, кеш distrust'ится, юзер опять ждёт 5 секунд. Это причина почему мы скачиваем заранее — пока юзер в лаунчере, мы экстрагируем новый билд, чтобы при перезапуске был быстрый старт.
React UI как embedded resource¶
ui/dist/ после npm run build — это index.html + JS-чанки + CSS. Чтобы они оказались внутри .exe, в .csproj есть:
<ItemGroup>
<Content Include="ui\dist\**\*" CopyToOutputDirectory="PreserveNewest">
<Link>ui\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Content>
</ItemGroup>
При publish все файлы из ui/dist/ попадают в publish/ui/. WebView2 потом грузит file:///<path-to-self-extract>/ui/index.html.
installed_version.txt¶
Critical файл для auto-update — см. installed-version-marker. Эта файла нет в publish output по дефолту. Inno installer его пишет рядом с .exe после установки. Лаунчер при старте читает.
Это решение, потому что прямое чтение из MSBuild <Version> через Assembly.GetExecutingAssembly().GetName().Version не работает в single-file bundle (reflection возвращает 1.0.0.0 всегда).
Размер vs Performance trade-offs¶
Альтернативы которые отвергнуты:
PublishTrimmed=true— обрезает unused code. Экономит ~30 МБ. Но ломает reflection в JSON serialization, breaks Supabase client, breaksXDocument.Load. Слишком хрупко для нашего размера codebase.- ReadyToRun (R2R) — pre-compile в native code. Ускорит startup, но добавит ~50 МБ.
- NativeAOT — полностью native compile. Не поддерживается WPF.
Выбран trade-off: single-file + compression, без trimming/R2R. Acceptable startup, простая поддержка.
Build script полностью¶
param(
[string]$Version = "1.0.0",
[string]$Configuration = "Release"
)
$ErrorActionPreference = "Stop"
# Bump version in csproj
$csproj = "HunterGraphics.Shell/HunterGraphics.Shell.csproj"
(Get-Content $csproj) -replace '<Version>.*</Version>', "<Version>$Version</Version>" |
Set-Content $csproj
# Build UI
Push-Location HunterGraphics.Shell/ui
npm install --silent
npm run build
Pop-Location
# Publish .NET
dotnet publish HunterGraphics.Shell/HunterGraphics.Shell.csproj `
-c $Configuration `
-r win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:EnableCompressionInSingleFile=true `
-p:Version=$Version `
-p:DebugType=None `
-p:DebugSymbols=false `
-o publish
# Rename output
Move-Item publish/HunterGraphics.Shell.exe "publish/MiamiGraphics_$Version.exe" -Force
Write-Host "Published: publish/MiamiGraphics_$Version.exe"