Перейти к содержанию

dotnet publish single-file

Финальный артефакт - один .exe файл, ~250 МБ. Включает в себя .NET runtime, WPF, WebView2 loader, нативные DLL (RageLib native bits), весь React UI после npm run build.

Команда сборки

Пользовательская сборка - только с флагами флейвора

По умолчанию AdminBuild=true и VITE_ADMIN не задан - это админская сборка (на ней живёт DEV владельца). Для релиза игрокам обязательны VITE_ADMIN=0 при сборке UI и -p:AdminBuild=false у dotnet publish.

После publish csproj кладёт в publish/ маркер build-flavor.txt (user или admin). scripts/zip-payload.cjs, scripts/gen-delta-manifest.mjs и installer-user.iss проверяют его (плюс косвенные признаки: SharpCompress.dll, additionals/admin, ui/assets/adminEntry-*.js) и без маркера или с admin отказываются собирать пользовательский релиз (аудит 25.10.2026).

scripts/publish.ps1
cd MiamiGraphics.Shell

# Step 1: build React UI (пользовательский флейвор)
Push-Location ui
npm install
$env:VITE_ADMIN = '0'
npm run build
Remove-Item Env:VITE_ADMIN
Pop-Location

# Step 2: dotnet publish (пользовательский флейвор)
dotnet publish MiamiGraphics.Shell.csproj `
    -c Release `
    -r win-x64 `
    --self-contained true `
    -p:AdminBuild=false `
    -p:PublishSingleFile=true `
    -p:IncludeNativeLibrariesForSelfExtract=true `
    -p:EnableCompressionInSingleFile=true `
    -p:DebugType=None `
    -p:DebugSymbols=false `
    -o ../publish

Результат - publish/MiamiGraphics.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
AdminBuild=false Пользовательский флейвор: без #if ADMIN, без SharpCompress и additionals/admin; маркер build-flavor.txt = user

Это даёт .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 есть:

MiamiGraphics.Shell.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, breaks XDocument.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 полностью

scripts/publish.ps1 (full)
param(
    [string]$Version = "1.0.0",
    [string]$Configuration = "Release"
)

$ErrorActionPreference = "Stop"

# Bump version in csproj
$csproj = "MiamiGraphics.Shell/MiamiGraphics.Shell.csproj"
(Get-Content $csproj) -replace '<Version>.*</Version>', "<Version>$Version</Version>" |
    Set-Content $csproj

# Build UI (пользовательский флейвор: без VITE_ADMIN=0 собирается админка)
Push-Location MiamiGraphics.Shell/ui
npm install --silent
$env:VITE_ADMIN = '0'
npm run build
Remove-Item Env:VITE_ADMIN
Pop-Location

# Publish .NET (пользовательский флейвор: без AdminBuild=false собирается админка)
dotnet publish MiamiGraphics.Shell/MiamiGraphics.Shell.csproj `
    -c $Configuration `
    -r win-x64 `
    --self-contained true `
    -p:AdminBuild=false `
    -p:PublishSingleFile=true `
    -p:IncludeNativeLibrariesForSelfExtract=true `
    -p:EnableCompressionInSingleFile=true `
    -p:Version=$Version `
    -p:DebugType=None `
    -p:DebugSymbols=false `
    -o publish

# Флейвор: маркер пишет csproj (таргет WriteBuildFlavorMarkerToPublish)
if ((Get-Content publish/build-flavor.txt -ErrorAction Stop).Trim() -ne 'user') {
    throw "publish/ - не пользовательская сборка (build-flavor.txt)"
}

# Rename output
Move-Item publish/MiamiGraphics.Shell.exe "publish/MiamiGraphics_$Version.exe" -Force

Write-Host "Published: publish/MiamiGraphics_$Version.exe"

Дальше: Inno installer →