История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619758)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Распространение Windows-приложения одним файлом — single binary и пределы зависимости от ОС. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619758 https://comcomponent.com/ru/blog/2026/03/19/003-windows-single-binary-and-os-dependencies/
- DOI (последняя версия)
- 10.5281/zenodo.21619758
- DOI (эта версия)
- 10.5281/zenodo.21619759
Эта статья началась с разговора о том, «до какой степени в Windows ещё можно говорить single binary». Пост, с которого всё пошло, ниже. Этот абзац можно пропустить: дальше текст читается самостоятельно.
В Windows пожелание «хотелось бы распространять по возможности одним файлом» встречается сплошь и рядом. Для внутренних инструментов, средств интеграции с оборудованием, терминалов мониторинга, офлайн-сред и площадок, которые стремятся по максимуму избегать установщиков, переход на single binary выглядит очень привлекательно.
Однако если не разделить этот разговор на составляющие с самого начала, обсуждение обычно перестаёт складываться где-то на середине. Дело в том, что за словами «хотим single binary» в Windows нередко скрываются сразу четыре разных запроса.
- Хотим, чтобы файл поставки был один
- Хотим, чтобы не требовалась предварительная установка среды выполнения .NET или Visual C++
- Хотим, чтобы приложение заработало, если его просто положить, без установщика и без прав администратора
- Не хотим зависеть от различий между версиями целевой Windows
Эти четыре вещи не одно и то же. На практике меньше всего путаницы, если рассуждать так:
Свести файлы поставки к одному EXE — вполне реально. А вот свести к нулю зависимость от целевой Windows — нет.
В этой статье мы разбираем эту границу применительно к практике разработки Windows-приложений.
flowchart TB
accTitle: Один EXE ещё не убирает зависимость от ОС
accDescr: Рисунок показывает, что в желании получить single binary легко смешиваются разговор о поставке и разговор о зависимостях: свести поставку к одному EXE вполне реально, а зависимость от целевой Windows к нулю свести нельзя.
a0["«Хотим single binary»"] --> a1["Поставка: один файл, просто положить"]
a0 --> a2["Зависимости: без рантайма, без привязки к ОС"]
a1 --> a3["Свести к одному EXE вполне реально"]
a2 --> a4["Зависимость от ОС к нулю не сводится"]
Рис. 1: В «хотим один файл» смешаны и возможное, и невозможное.
1. Сначала вывод
Если сразу сформулировать вывод, он звучит так.
- Для обычного настольного EXE упаковку в single binary можно довести довольно далеко
- Но возможность собрать один EXE и отсутствие зависимости от целевой Windows — разные вещи
- Для расширений оболочки, служб Windows, драйверов, WebView2 и части приложений на WinUI 3 главный вопрос обычно не в числе файлов, а в том, что регистрируется в ОС и что при этом предполагается по умолчанию
- Важнее всего на практике — заранее разделить, чего именно вы хотите: single binary, отказа от установщика или снижения зависимости от ОС
Другими словами, границы в Windows проходят так.
- Свести поставку к одному файлу: вполне реально
- Включить дополнительные среды выполнения в поставку: вполне реально
- Приблизиться к развёртыванию методом xcopy: зависит от типа приложения
- Убрать зависимость от самой целевой Windows: невозможно
flowchart TB
accTitle: Границы в Windows
accDescr: Рисунок показывает границу: свести поставку к одному файлу и включить дополнительную среду выполнения в поставку вполне реально, приблизиться к xcopy зависит от типа приложения, убрать зависимость со стороны целевой Windows невозможно.
b1["Свести поставку к одному файлу"] --> b2["Вполне реально"]
b3["Включить дополнительный рантайм в поставку"] --> b2
b4["Приблизиться к развёртыванию xcopy"] --> b5["Зависит от типа приложения"]
b6["Убрать зависимость со стороны ОС"] --> b7["Невозможно"]
Рис. 2: Граница между тем, что можно, что зависит от типа и что нельзя.
1.1 Термины, которыми пользуется эта статья
Сначала сведём слова, которые дальше будут встречаться снова и снова.
| Термин | Как читать и раскрывать | Смысл в этой статье |
|---|---|---|
| UCRT | Universal C Runtime | Часть стандартной библиотеки C, которая появилась, когда в Visual Studio 2015 среду выполнения C разделили. Начиная с Windows 10 входит в состав ОС |
| Распространяемый пакет VC++ | Visual C++ Redistributable | Установщик, который кладёт на целевую машину DLL среды выполнения Visual C++, кроме UCRT |
| framework-dependent | развёртывание с зависимостью от .NET в целевой среде | Форма развёртывания, которая исходит из того, что на целевой машине уже есть среда выполнения .NET |
| self-contained | среда выполнения .NET в поставке | Форма, в которой приложение несёт с собой полный набор среды выполнения .NET |
| single-file | публикация одним файлом | Параметр публикации .NET, который собирает поставку в один EXE |
| Native AOT | Native Ahead-Of-Time | Форма развёртывания, при которой IL заранее компилируется в машинный код. Во время выполнения JIT не используется |
| развёртывание app-local | DLL рядом с приложением | Способ поставки, при котором DLL лежат в той же папке, что и EXE, и ими пользуется только это приложение |
| развёртывание xcopy | распространение простым копированием | Способ поставки, при котором не нужны ни установщик, ни права администратора: достаточно положить папку целиком |
| SCM | Service Control Manager, диспетчер управления службами | Механизм ОС, который регистрирует, запускает и останавливает службы Windows |
| UAC | User Account Control, контроль учётных записей | Механизм безопасности Windows, который управляет повышением до прав администратора |
| расширение оболочки | shell extension | COM-компонент, который загружается в процесс вроде Проводника и работает внутри него |
| Evergreen | постоянно обновляемый канал | Режим распространения, в котором WebView2 Runtime оставляют одной общей автоматически обновляемой копией |
| Fixed Version | фиксированная версия | Режим распространения, в котором конкретную версию WebView2 Runtime включают в поставку своего приложения |
| arch | architecture, архитектура CPU | x86 / x64 / Arm64 |
Карта знаний этой статьи
Windows-приложение довольно часто можно свести к одному EXE: self-contained, single-file и Native AOT в .NET, статическая компоновка (/MT) в C/C++. Но зависимость от версии ОС, архитектуры, системных DLL и модели безопасности остаётся — зависимость от целевой Windows до нуля не свести. self-contained и framework-dependent — взаимоисключающие варианты; Native AOT по сути работает как single-file, но взамен нельзя использовать, например, built-in COM. Для WebView2 нужно выбрать, как поставлять runtime, — Evergreen или Fixed Version; у WinUI 3 доступность PublishSingleFile зависит от packaged (MSIX) или unpackaged. У расширений оболочки и драйверов основная задача — регистрация и подпись, поэтому сопровождать чаще проще распространение app-local, чем насильно сжимать всё в один EXE.
flowchart LR
accTitle: Карта знаний: один двоичный файл Windows-приложения
accDescr: Схема показывает, что свести дистрибутив к одному файлу и убрать зависимость от ОС — разные оси; какие средства single-file есть в .NET и в C/C++; и как интеграция с хостом вроде WebView2 или WinUI 3 сама ограничивает способ распространения
single_binary_packaging["упаковка в один двоичный файл"]
windows_os_dependency["зависимость от целевой Windows"]
self_contained_deployment["self-contained публикация"]
framework_dependent_deployment["framework-dependent публикация (.NET)"]
native_aot["Native AOT"]
builtin_com_interop["built-in COM interop"]
single_file_publish["single-file публикация (.NET)"]
static_linking_crt["статическая линковка CRT (/MT)"]
dynamic_linking_crt["динамическая линковка CRT (/MD)"]
vc_redistributable["Visual C++ Redistributable"]
ucrt["UCRT (Universal C Runtime)"]
msix_packaging["упаковка MSIX (packaged)"]
winui3["WinUI 3 / Windows App SDK"]
webview2["Microsoft Edge WebView2"]
webview2_runtime["WebView2 Runtime"]
webview2_evergreen["режим распространения Evergreen (WebView2)"]
webview2_fixed_version["рантайм Fixed Version"]
shell_extension["расширение оболочки (Explorer)"]
com["COM (Component Object Model)"]
driver_signing["подпись драйвера"]
driver_package["пакет драйвера"]
app_local_deployment["app-local развёртывание"]
single_binary_packaging -->|"требует"| windows_os_dependency
self_contained_deployment -->|"несовместимо с"| framework_dependent_deployment
native_aot -->|"несовместимо с"| builtin_com_interop
single_file_publish -.->|"требует"| windows_os_dependency
native_aot -.->|"требует"| windows_os_dependency
static_linking_crt -->|"несовместимо с"| dynamic_linking_crt
dynamic_linking_crt -->|"требует"| vc_redistributable
static_linking_crt -->|"использует"| ucrt
dynamic_linking_crt -->|"использует"| ucrt
msix_packaging -.->|"несовместимо с"| single_file_publish
winui3 -.->|"использует"| msix_packaging
webview2 -->|"требует"| webview2_runtime
webview2_runtime -->|"настраивается"| webview2_evergreen
webview2_runtime -->|"настраивается"| webview2_fixed_version
webview2_evergreen -->|"несовместимо с"| webview2_fixed_version
shell_extension -->|"использует"| com
driver_signing -->|"требует"| driver_package
driver_package -->|"не рекомендуется"| single_binary_packaging
app_local_deployment -->|"рекомендуется для"| single_binary_packaging
self_contained_deployment -.->|"требует"| windows_os_dependency
single_binary_packaging -.->|"использует"| static_linking_crt
webview2 -.->|"требует"| windows_os_dependency
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 22, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. «Single binary» стоит рассматривать как четыре уровня
Если сначала показать уровни схемой, получится так. Снизу вверх становится труднее, до самого верхнего в Windows не добраться.
flowchart BT
A["уровень A: один файл поставки<br/>вполне реально"]
B["уровень B: языковая среда выполнения не ставится заранее<br/>вполне реально"]
C["уровень C: не нужны установка и регистрация<br/>зависит от типа приложения"]
D["уровень D: нет зависимости от целевой Windows<br/>в Windows недостижимо"]
A --> B
B --> C
C --> D
Рис. 3: Четыре уровня single binary. Снизу вверх становится труднее, до уровня D не добраться.
От того, какой из этих четырёх уровней имеется в виду под «хотим single binary», нужная работа меняется полностью.
2.1 Уровень A: один файл поставки
Это самый поверхностный уровень.
- Можно отправить один файл по почте
- Достаточно положить один файл на USB-накопитель
- В месте развёртывания находится только
app.exe
Речь идёт о видимой единице поставки. Даже если приложение при запуске временно распаковывает какие-то файлы или зависит от системных DLL, одно это условие может выполняться.
2.2 Уровень B: не требуется предварительная установка языковой среды выполнения
Следующий уровень — состояние, при котором приложение работает без предварительной установки на целевой машине среды выполнения .NET или распространяемого пакета VC++.
- статическая линковка в C/C++
- self-contained в .NET
- single-file в .NET
- Native AOT в .NET
На этом уровне ощущение «это можно унести с собой как самостоятельную единицу» становится заметно сильнее.
2.3 Уровень C: не требуется установка или регистрация
Начиная отсюда, сложность резко возрастает.
Обычный EXE иногда способен работать сразу после того, как его просто положили в нужное место. Но следующее — уже другая история.
- расширения оболочки
- службы Windows
- пользовательские URL-схемы и файловые ассоциации
- драйверы
- компоненты, загружаемые в другие процессы, такие как Проводник или Office
Для этой области недостаточно просто разместить файл. Требуется регистрация на стороне ОС или привязка к хост-процессу.
flowchart TB
accTitle: Область, где просто положить файл недостаточно
accDescr: Рисунок показывает, что расширения оболочки, службы Windows, файловые ассоциации, драйверы и компоненты, загружаемые в чужой процесс, нельзя просто положить файлом: нужна регистрация на стороне ОС или привязка к хосту.
c1["Расширения оболочки и службы"] --> c4["Просто положить недостаточно"]
c2["Ассоциации и драйверы"] --> c4
c3["То, что загружается в чужой процесс"] --> c4
c4 --> c5["Нужны регистрация в ОС и привязка к хосту"]
Рис. 4: На уровне C резко становится труднее, потому что появляется область, где нужна регистрация в ОС.
2.4 Уровень D: отсутствие зависимости от целевой Windows
Это в Windows невозможно.
Windows-приложение в конечном счёте выполняется поверх API Windows, загрузчика, модели безопасности и стека устройств. Упаковка в single binary покрывает лишь зону ответственности самого приложения. Саму операционную систему с собой не унести.
3. Область, где приложение довольно легко свести к одному EXE
Даже в Windows встречаются приложения, которые относительно легко сводятся к одному EXE.
- настольные инструменты, запускаемые как самостоятельный процесс
- бизнес-приложения, где интерфейс и обработка сосредоточены в самом EXE
- инструменты для связи, обработки файлов, сбора логов, мониторинга, управления оборудованием
- то, что не требует интеграции с хост-процессами вроде Проводника или Office
- интерфейсы, не предполагающие веб-среду выполнения
Для такого типа приложений многое легко включить в само приложение.
- собственный код
- ресурсы
- манифест
- настройки по умолчанию
- шаблонные данные
- часть сторонних библиотек
- саму языковую среду выполнения
Более того, даже без полного встраивания DLL внутрь EXE, развёртывание app-local, при котором DLL размещаются рядом с EXE, в Windows остаётся вполне жизнеспособным и распространённым вариантом. На практике это часто выглядит так:
- один
app.exe - либо
app.exeплюс несколько соседних DLL - при этом установщик не нужен, права администратора не нужны, распространение возможно методом xcopy
Такая форма нередко оказывается проще в сопровождении, чем попытка насильно сжать всё в один EXE.
flowchart TB
accTitle: Практичный вариант — развёртывание app-local
accDescr: Рисунок показывает, что даже без полного встраивания DLL в EXE форма, где DLL лежат рядом с EXE, не нужен установщик и права администратора и можно распространять методом xcopy, нередко проще в сопровождении, чем попытка насильно сжать всё в один EXE.
d1["app.exe плюс несколько соседних DLL"] --> d2["Без установщика и без прав администратора"]
d2 --> d3["Можно распространять методом xcopy"]
d3 -.-> d4["Иногда проще в сопровождении, чем насильственное сжатие в один EXE"]
Рис. 5: Даже без настойчивости на одном EXE цель часто закрывается развёртыванием app-local.
4. Зависимости от Windows, которые остаются даже при одном EXE
Если решить, что «раз EXE один, значит, зависимости от целевой Windows нет», именно здесь и происходит сбой. На деле зависимости остаются даже при одном EXE.
4.1 Зависимость от версии ОС
У каждого API Windows есть своя минимально поддерживаемая версия ОС. Есть и различия между x64 и Arm64. То есть даже при одном EXE заранее нужно зафиксировать:
- работает ли приложение вплоть до Windows 10;
- предполагается ли Windows 11 как базовая версия;
- должно ли приложение работать и на Windows Server;
- какая архитектура является целевой — x86, x64 или Arm64.
4.2 Зависимость от системных DLL
Даже если вам кажется, что EXE один, во время выполнения приложение, разумеется, использует компоненты, предоставляемые ОС.
kernel32.dlluser32.dlladvapi32.dll- инфраструктура COM
- инфраструктура управления службами
Всё это — зона ответственности самой Windows.
4.3 Зависимость от модели безопасности
- UAC
- ACL файловой системы
- диспетчер управления службами
- реестр
- политика подписи драйверов
Всё это приложение не может взять на себя в одиночку.
flowchart TB
accTitle: Зависимости, которые остаются и при одном EXE
accDescr: Рисунок показывает, что даже при одном EXE остаются зависимости от минимально поддерживаемой версии Windows API и архитектуры, от системных DLL вроде kernel32.dll и инфраструктуры COM, а также от модели безопасности вроде UAC, ACL и политики подписи драйверов.
e0["Приложение из одного EXE"] --> e1["Версия ОС и arch"]
e0 --> e2["Системные DLL и инфраструктура COM"]
e0 --> e3["Модель безопасности"]
e1 --> e4["Ничего из этого приложение не может взять на себя"]
e2 --> e4
e3 --> e4
Рис. 6: Даже если файл поставки один, зависимость от зоны ответственности Windows остаётся.
4.4 Зависимость от хоста и среды выполнения
Если архитектура — не самостоятельный EXE, а нечто, работающее поверх хоста, зависимости сразу резко увеличиваются.
- используется WebView2 — требуется WebView2 Runtime;
- используется WinUI 3 / Windows App SDK — требуется продумать режим развёртывания;
- создаётся расширение оболочки — требуется регистрация на стороне Проводника.
Иными словами, выбор интерфейса или интеграции часто напрямую превращается в сложность распространения.
flowchart TB
accTitle: Выбор UI и интеграции становится сложностью распространения
accDescr: Рисунок показывает, что WebView2 требует WebView2 Runtime, WinUI 3 требует продумать режим развёртывания, расширение оболочки требует регистрации в Проводнике, то есть выбор хоста и среды выполнения сразу становится сложностью распространения.
f1["Используем WebView2"] --> f2["Нужен Runtime"]
f3["Используем WinUI 3"] --> f4["Нужно продумать режим развёртывания"]
f5["Делаем расширение оболочки"] --> f6["Нужна регистрация в Проводнике"]
Рис. 7: Чем дальше от самостоятельно запускаемого EXE, тем резче растут зависимости.
5. Реалистичные компромиссы по технологиям
5.1 Нативный C/C++
Нативный C/C++ обладает высокой степенью свободы в упаковке в single binary. Доступна статическая линковка, и самостоятельный EXE можно свести к очень компактной форме.
Тем не менее на практике важнее не «утрамбовать всё в один файл», а решить:
- что делать с UCRT и средой выполнения VC++;
- размещать ли сторонние DLL по схеме app-local;
- насколько узко ограничивать целевые CPU и ОС.
Отправная точка: разница между /MT и /MD
В MSVC вопрос «нести среду выполнения с собой или нет» по сути решает этот параметр компилятора.
| Параметр | Что компонуется | Что нужно на целевой машине |
|---|---|---|
/MD |
библиотеки импорта DLL ucrt.lib и vcruntime.lib. Во время выполнения используются ucrtbase.dll и vcruntime<версия>.dll |
UCRT и среда выполнения VC++ |
/MT |
статическая компоновка libucrt.lib, libvcruntime.lib, libcmt.lib |
дополнительная среда выполнения не нужна |
Минимальный способ попробовать — вот эта команда в Developer Command Prompt.
cl /std:c++17 /EHsc /O2 /MT app.cpp /Fe:app.exe
Здесь стоит запомнить три момента.
- UCRT стал составляющей Windows, когда в Visual Studio 2015 среду выполнения C разделили, и начиная с Windows 10 входит в ОС. Если целевая система старше, нужна повторная поставка через
vcredist. - Собирать DLL с
/MTне рекомендуется. Состояние статически слинкованной CRT замыкается внутри этой DLL, поэтому выделение памяти, локаль и действие_set_se_translatorначинают расходиться между EXE и DLL. Если небрежно выровнять «EXE на/MT, прилагаемые DLL тоже на/MT», авария случается на выделении и освобождении через границу модуля. /clrи/MTвместе использовать нельзя. Если в смеси есть C++/CLI, остаётесь на стороне/MD.
flowchart TB
accTitle: Три оговорки статической линковки
accDescr: Рисунок показывает, что UCRT начиная с Windows 10 входит в ОС, а на более старых системах нужна повторная поставка через vcredist; собирать DLL с /MT не рекомендуется, потому что состояние CRT замыкается внутри DLL и выделение с освобождением через границу модуля ломается; /clr и /MT вместе использовать нельзя.
g0["Статическая линковка /MT"] --> g1["На старой Windows нужен vcredist"]
g0 --> g2["/MT на стороне DLL не рекомендуется"]
g0 --> g3["С /clr вместе нельзя"]
g2 -.-> g4["Авария на выделении и освобождении через границу модуля"]
Рис. 8: /MT силён, но нужны три оговорки: предпосылки UCRT, граница DLL и C++/CLI.
5.2 .NET
В .NET есть single-file, self-contained и Native AOT, поэтому видимую единицу поставки можно сделать весьма компактной.
Однако различия важно понимать чётко.
- framework-dependent — зависит от .NET, установленного в целевой среде
- self-contained — несёт среду выполнения .NET вместе с собой
- single-file — объединяет поставку в один артефакт
- Native AOT — дополнительно снижает зависимости на этапе запуска, но накладывает функциональные ограничения
«Раз это single-file, значит, зависимостей от ОС меньше» — неверное утверждение. Уменьшается в первую очередь разрозненность файлов поставки приложения.
flowchart TB
accTitle: Single-file уменьшает разрозненность поставки, а не зависимость от ОС
accDescr: Рисунок показывает, что framework-dependent зависит от .NET в целевой среде, self-contained несёт рантайм с собой, single-file только собирает поставку в один файл, и из того, что это single-file, снижение зависимости от ОС не следует.
h1["framework-dependent"] --> h2["Зависит от .NET в целевой среде"]
h3["self-contained"] --> h4["Несёт среду выполнения .NET с собой"]
h5["single-file"] --> h6["Собирает поставку в один файл"]
h6 -.-> h7["Зависимость от ОС от этого не уменьшается"]
Рис. 9: У каждой формы публикации .NET своё, что уменьшается и что остаётся.
Отправная точка: три варианта dotnet publish
Чтобы сначала запустить и увидеть разницу, этих трёх команд достаточно.
rem 1. framework-dependent + single-file: на целевой машине нужна среда выполнения .NET
dotnet publish -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true
rem 2. self-contained + single-file: среда выполнения .NET идёт с собой
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
rem 3. Native AOT: публиковать после того, как в csproj записан PublishAot
dotnet publish -c Release -r win-x64
PublishSingleFile и PublishAot официально рекомендуется писать не в командной строке, а в csproj. Тогда во время сборки включается анализ совместимости, и меньше случаев, когда об инциденте узнаёшь уже после публикации.
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot>
</PropertyGroup>
Заметьте: если указать RuntimeIdentifier, SelfContained по умолчанию становится true. Если нужно framework-dependent, явно пишите --self-contained false. Чтобы публиковать Native AOT на Windows, нужна рабочая нагрузка Visual Studio Разработка классических приложений на C++.
Компромиссы: «сделать один файл» не бесплатно
Если здесь написать только «можно», будет авария, поэтому рядом положим и цену.
Распаковка single-file при запуске
- По умолчанию в пакет попадают только управляемые DLL. При запуске они читаются в память и на диск в папку не распаковываются. Собственные машинные двоичные файлы среды выполнения остаются отдельными файлами.
- Чтобы включить в один файл и машинные двоичные файлы, используют
IncludeNativeLibrariesForSelfExtract; чтобы перед запуском распаковать всё —IncludeAllContentForSelfExtract. Оба варианта работают как «сначала распаковать, потом запустить», и в Windows распаковка идёт в%TEMP%\.net. Место можно сменить черезDOTNET_BUNDLE_EXTRACT_BASE_DIR, но не ставьте каталог, в который могут писать пользователи с другими правами или службы. - Если включить
EnableCompressionInSingleFile, EXE заметно худеет, но при запуске его ещё нужно разжать в памяти, и старт замедляется. В официальной документации прямо сказано: «прежде чем включать, измерьте и размер, и стоимость запуска». Влияние сильно зависит от приложения.
Несовместимость API у single-file
Когда поставка становится одним файлом, код, который исходит из пути к файлу, тихо ломается.
| API | Поведение в single-file |
|---|---|
Assembly.Location |
возвращает пустую строку |
Assembly.CodeBase |
PlatformNotSupportedException |
Assembly.GetFile |
IOException |
Module.Name |
возвращает строку <Unknown> |
Если нужно трогать файлы рядом с EXE, опирайтесь на AppContext.BaseDirectory; если нужен путь к исполняемому файлу — на Environment.ProcessPath. Сторонние библиотеки иногда используют эти API внутри себя, поэтому после перехода на single-file хотя бы раз нужно проверить запуск на реальной машине.
flowchart TB
accTitle: Код, завязанный на путь, тихо ломается
accDescr: Рисунок показывает, что когда поставка становится одним файлом, поведение API, завязанных на путь к файлу, меняется — например Assembly.Location возвращает пустую строку, — поэтому соседние файлы берут через AppContext.BaseDirectory, путь к исполняемому файлу через Environment.ProcessPath, и запуск хотя бы раз проверяют на реальной машине.
i1["Переход на single-file"] --> i2["Меняется поведение API, завязанных на путь"]
i2 --> i3["Соседние файлы — AppContext.BaseDirectory"]
i2 --> i4["Исполняемый файл — Environment.ProcessPath"]
i3 --> i5["Хотя бы раз проверить запуск на реальной машине"]
i4 --> i5
Рис. 10: После перехода на single-file способ получения пути меняют и проверяют на реальной машине.
Функциональные ограничения Native AOT
Цена «быстрый запуск, среда выполнения не нужна» в том, что следующее становится недоступно.
- динамическая загрузка вроде
Assembly.LoadFile - генерация кода во время выполнения вроде
System.Reflection.Emit - C++/CLI
- на Windows — встроенный COM
- обрезка (trimming) обязательна, поэтому её ограничения тоже наследуются
- по сути это single-file, поэтому несовместимость API выше тоже наследуется
System.Linq.Expressionsвсегда работает интерпретатором и медленнее кода, сгенерированного во время выполнения
Целевая платформа тоже задаётся жёстко. В .NET 8 для Windows это x64 и Arm64, начиная с .NET 9 добавился x86. Идея «один файл под AnyCPU» с самого начала не складывается.
flowchart TB
accTitle: Цена Native AOT
accDescr: Рисунок показывает, что Native AOT снижает зависимости на запуске ценой того, что недоступны динамическая загрузка и генерация кода во время выполнения, C++/CLI и встроенный COM на Windows, плюс наследуются ограничения trimming и single-file, а целевая платформа задаётся жёстко.
j0["Native AOT"] --> j1["Зависимости на запуске можно снизить"]
j0 --> j2["Динамическая загрузка и генерация во время выполнения недоступны"]
j0 --> j3["C++/CLI и встроенный COM недоступны"]
j2 -.-> j4["Наследуются ограничения trimming и single-file"]
j3 -.-> j5["Целевые ОС и arch задаются жёстко"]
Рис. 11: Плюсы Native AOT смотрят в комплекте с тем, что становится недоступно.
5.3 WebView2
Использование WebView2 полностью меняет сложность упаковки в single binary. Здесь реальный вопрос не в количестве EXE, а в том, как обращаться с WebView2 Runtime.
Прежде чем спрашивать «можно ли сделать один EXE», нужно ответить на другие вопросы.
- Предполагается ли, что Runtime уже есть в среде?
- Использовать ли канал Evergreen?
- Включать ли в поставку Fixed Version?
- В какой мере брать на себя ответственность за офлайн-распространение?
Разница между Evergreen и Fixed Version в общих чертах такая.
| Evergreen | Fixed Version | |
|---|---|---|
| Кто обновляет | Microsoft. Обновляется автоматически | Вы. Меняете вместе с обновлением приложения |
| Экземпляр на машине | одна общая копия на все приложения | свой экземпляр в поставке каждого приложения |
| Размер поставки | почти не растёт | превышает 250 MB |
| Как ставить | Bootstrapper около 2 MB скачивает нужное. Для офлайна в поставку кладут Standalone Installer | вместе с приложением разносят весь развёрнутый набор двоичных файлов |
| Существующие ограничения | нужно проверить, есть ли Runtime на машине, до запуска | нельзя запускать с сетевого пути или UNC |
Evergreen Runtime на Windows 11 предустановлен как часть ОС. На стороне Windows 10 машины без него ещё остаются, поэтому Microsoft сама пишет в духе «даже если выбрали Evergreen, Runtime лучше всё же распространять». То есть процедура «проверить наличие на стороне приложения и поставить, если не хватает» нужна в любом случае. Fixed Version даёт «момент обновления держим сами» ценой того, что поставка тяжелеет на сотни мегабайт. Это нужно решить раньше и отдельно от разговора про один EXE.
flowchart TB
accTitle: У WebView2 сначала решают способ распространения
accDescr: Рисунок показывает, что Evergreen — одна общая автоматически обновляемая Microsoft копия, которой на машине может не быть, а Fixed Version даёт контроль над обновлением ценой сотен мегабайт в поставке, поэтому в любом варианте приложению нужно проверить наличие и поставить Runtime, если его нет.
k0["Принимаем WebView2"] --> k1["Evergreen: одна общая копия, автообновление"]
k0 --> k2["Fixed Version: сами кладём в поставку и обновляем"]
k1 --> k3["Проверить наличие и поставить, если не хватает"]
k2 -.-> k4["Поставка тяжелеет на сотни мегабайт"]
Рис. 12: Главный вопрос WebView2 не в числе EXE, а в том, как обращаться с Runtime.
5.4 WinUI 3 / Windows App SDK
WinUI 3 тоже меняет требования к развёртыванию сразу в момент выбора этой технологии. Выбор UI-технологии, по сути, и есть выбор способа распространения.
Конкретно решать нужно по двум осям.
- packaging: packaged (MSIX), packaged с указанием внешнего расположения или unpackaged
- runtime: framework-dependent или self-contained
А с точки зрения single binary срабатывает вот это ограничение комбинаций.
PublishSingleFileдоступен только для приложений WinUI 3 в конфигурации unpackaged и self-contained, и нужен Windows App SDK 1.5 или новее.- В packaged-приложениях и в packaged-приложениях с указанием внешнего расположения
PublishSingleFileиспользовать нельзя. - В unpackaged теряется package identity, поэтому функции Windows, которые исходят из package identity, — уведомления, фоновые задачи, файловые ассоциации, расширения контекстного меню — становятся недоступны.
То есть в WinUI 3 «сделать один EXE» и «пользоваться расширениями Windows» сталкиваются лоб в лоб. Если single binary — приоритет номер один, часто быстрее сначала пересмотреть саму предпосылку выбора UI-технологии.
flowchart TB
accTitle: Столкновение одного EXE и package identity
accDescr: Рисунок показывает, что в WinUI 3 PublishSingleFile доступен только в конфигурации unpackaged и self-contained, unpackaged лишает package identity, уведомления и ассоциации становятся недоступны, поэтому один EXE и расширения Windows сталкиваются лоб в лоб.
m1["Хотим PublishSingleFile"] --> m2["Только unpackaged + self-contained"]
m2 --> m3["Теряется package identity"]
m3 --> m4["Уведомления, ассоциации и прочее недоступны"]
m4 -.-> m5["Один EXE и расширения сталкиваются лоб в лоб"]
Рис. 13: В WinUI 3 выбор одного EXE означает отказ от package identity.
6. Область, где регистрация и зависимости неизбежны по своей природе
6.1 Расширения оболочки
Расширение оболочки, загружаемое в Проводник, — это совсем не то же самое, что «EXE, который просто кладут в папку». Здесь реальный вопрос не в количестве файлов, а в том, как зарегистрировать расширение в Проводнике.
6.2 Службы Windows
Даже если сам исполняемый файл службы можно свести к одному файлу, распространение — отдельная проблема.
- регистрация в SCM;
- права доступа;
- учётная запись для запуска;
- настройки восстановления после сбоя.
Всё это нужно продумать. Иными словами, для служб важнее не «как сделать один EXE», а «как организовать установку».
6.3 Драйверы
С драйверами всё ещё более однозначно. Они складываются в цельный пакет только вместе с INF-файлом, подписью и процедурой установки, поэтому изначально плохо вписываются в парадигму single binary.
flowchart TB
accTitle: Область, где главный вопрос — регистрация и подпись
accDescr: Рисунок показывает, что расширению оболочки нужна регистрация в Проводнике, службе Windows — регистрация в SCM, права и учётная запись запуска, драйверу — INF и подпись, поэтому главный вопрос не в числе файлов, а в проектировании регистрации и зависимостей.
n1["Расширение оболочки"] --> n2["Регистрация в Проводнике"]
n3["Служба"] --> n4["Регистрация в SCM, права, учётная запись"]
n5["Драйвер"] --> n6["INF и подпись"]
n4 -.-> n7["Главный вопрос — проектирование регистрации, а не число файлов"]
Рис. 14: В этих трёх областях прорабатывают не «как сделать один EXE», а «как регистрировать».
7. Таблица решений для практики
Для грубой оценки удобна такая таблица.
| Что вы хотите создать | Реалистичность одного EXE | Что продумать в первую очередь |
|---|---|---|
| Самостоятельный инструмент на Win32 / C++ | Высокая | Статическая линковка, целевая ОС / архитектура |
| Самостоятельный инструмент на WinForms / WPF | Высокая | Применимость self-contained, single-file, Native AOT |
| Приложение на WinUI 3 / Windows App SDK | Средняя | Режим развёртывания, дополнительные зависимости |
| Настольный интерфейс на базе WebView2 | От низкой до средней | Способ распространения Runtime |
| Расширение контекстного меню Проводника или обработчик предпросмотра | Низкая | Регистрация через COM / реестр |
| Служба Windows | Средняя | Регистрация в SCM, права, процедура обновления |
| Приложение с драйвером в комплекте | Низкая | INF, подпись, установка |
Главный вывод из этой таблицы — понимание того, что «количество двоичных файлов» и «зона ответственности за распространение» — разные вещи.
8. Что нужно решить заранее при проектировании развёртывания
Если вы хотите, чтобы упаковка в single binary действительно удалась, есть вещи, которые стоит решить ещё до реализации.
8.1 Определите, что именно должно быть «одним»
- Хотите ли вы один файл поставки?
- Хотите ли вы избавиться от предварительной установки среды выполнения?
- Хотите ли вы отказаться от установщика?
- Хотите ли вы упростить офлайн-обновления?
От ответа зависит, какую технологию выбрать.
8.2 Заранее зафиксируйте минимально поддерживаемую версию Windows и архитектуру
И single-file, и Native AOT по своей природе привязаны к конкретной ОС и архитектуре. Если оставить это неопределённым и просто двигаться в духе «главное — один файл», в итоге можно упереться в нехватку API или несовпадение версий среды выполнения.
8.3 Явно зафиксируйте, что включается в поставку, а что остаётся на стороне Windows
На практике достаточно просто выписать такую таблицу, чтобы избежать значительной части проблем.
- что включается в поставку приложения
- основной exe-файл;
- собственные DLL;
- шаблоны настроек;
- self-contained-среда выполнения.
- что остаётся на стороне Windows
- системные DLL;
- API ОС;
- SCM / реестр / Проводник;
- инфраструктура драйверов.
- что закладывается как отдельное внешнее условие
- WebView2 Runtime;
- VC++ Redistributable;
- Office / Excel;
- специализированные драйверы.
flowchart TB
accTitle: Письменно зафиксировать три зоны ответственности
accDescr: Рисунок показывает, что значительную часть инцидентов распространения снимает уже то, что вы заранее выписываете три класса: что кладёте в поставку приложения, что оставляете Windows и что принимаете как отдельную внешнюю предпосылку.
p0["Зона ответственности поставки"] --> p1["Что кладём в поставку приложения"]
p0 --> p2["Что оставляем Windows"]
p0 --> p3["Что принимаем как отдельную предпосылку"]
p3 -.-> p4["Уже запись этого снижает число инцидентов"]
Рис. 15: Три класса — в поставке, на стороне ОС, отдельная предпосылка — фиксируют письменно заранее.
8.4 Если single binary в приоритете — снижайте интеграцию с хостом
Это работает очень хорошо.
- отказаться от расширения оболочки в пользу обычного EXE;
- не превращать приложение в службу, а использовать Планировщик заданий или явный запуск;
- использовать нативный интерфейс вместо WebView2;
- держать COM в пределах собственного процесса.
Иначе говоря, чем меньше в архитектуре решений, где ОС что-то «загружает» или «регистрирует», тем ближе приложение к single binary.
flowchart TB
accTitle: Чем меньше интеграции с хостом, тем ближе
accDescr: Рисунок показывает, что отказ от расширения оболочки в пользу обычного EXE, отказ от службы в пользу Планировщика заданий или явного запуска, нативный UI вместо WebView2 — то есть сокращение решений, где ОС что-то загружает и регистрирует, — приближают к single binary.
q1["Отказаться от расширения оболочки в пользу обычного EXE"] --> q4["Сократить решения, где ОС регистрирует"]
q2["Не делать службу, запускать явно"] --> q4
q3["Нативный UI вместо WebView2"] --> q4
q4 --> q5["Ближе к single binary"]
Рис. 16: Если single binary в приоритете, сокращают саму интеграцию с хостом.
9. Итог
Упаковка в single binary в Windows возможна в значительной степени. Но в конечном счёте всё сводится к одной формулировке:
Приложение можно сделать одним EXE. Но Windows, от которой это приложение зависит, одним EXE сделать нельзя.
Особенно стоит запомнить пять пунктов.
- Для обычного самостоятельного EXE распространение одним файлом можно довести весьма далеко
- Статическая линковка в C/C++, single-file в .NET и Native AOT — сильные варианты
- Но зависимость от версии ОС, архитектуры, системных DLL и модели безопасности никуда не исчезает
- Для расширений оболочки, служб, драйверов, WebView2 и части приложений на WinUI 3 главной темой становится регистрация в ОС и дополнительные среды выполнения
- Успех упаковки в single binary определяется тем, разделили ли вы заранее, что именно должно быть «одним»
Если single binary — сильный приоритет, гораздо больше шансов на успех даёт проектирование, при котором ещё на этапе выбора технологии закладывается снижение связанности с ОС.
flowchart TB
accTitle: Ключ успеха — степень связанности с ОС
accDescr: Рисунок показывает, что приложение можно сделать одним EXE, а Windows, от которой оно зависит, одним EXE сделать нельзя, поэтому если single binary в сильном приоритете, успешнее проектировать со снижением связанности с ОС уже на этапе выбора технологии.
r1["Сделать приложение одним EXE"] --> r2["Можно"]
r3["Сделать одним EXE и Windows, от которой оно зависит"] --> r4["Нельзя"]
r4 -.-> r5["Поэтому на выборе технологии снижают связанность с ОС"]
Рис. 17: Успех сведения к одному EXE решается ещё на стартовом проектировании связанности с ОС.
10. Источники
- Microsoft Learn, Create a single file for application deployment
- Microsoft Learn, Native AOT deployment overview
- Microsoft Learn, C runtime (CRT) and C++ standard library (STL) lib files
- Microsoft Learn, Dynamic-link library search order
- Microsoft Learn, Targeting your application for Windows
- Microsoft Learn, Creating Registration-Free COM Objects
- Microsoft Learn, Registering Shell Extension Handlers
- Microsoft Learn, CreateServiceW function
- Microsoft Learn, Overview of INF Files
- Microsoft Learn, Windows driver signing tutorial
- Microsoft Learn, Distribute your app and the WebView2 Runtime
- Microsoft Learn, Windows App SDK deployment guide for self-contained apps
- Microsoft Learn, Package and deploy Windows apps overview
- Microsoft Learn, Packaging overview - Windows apps
- Microsoft Learn, Evergreen vs. fixed version of the WebView2 Runtime
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
После режима IE — подойдёт ли WebView2? Ограничение ActiveX и реалистичный план миграции
С точки зрения внутренних корпоративных систем разбираем устройство WebView2, стратегии распространения Evergreen и Fixed Version, типичн...
Что продумать перед заказом разработки Windows-приложения
Перед заказом разработки Windows-приложения разберём, что стоит прояснить: доработка существующего ПО, интеграция с оборудованием, COM/Ac...
Чек-лист безопасной работы с дочерними процессами в Windows-приложении
Чтобы безопасно работать с дочерними процессами в Windows-приложении, важнее не API запуска, а владение деревом процессов и процедура зав...
Пул потоков Win32 — параллелизм через CreateThreadpoolWork без своих потоков
Не плодите ли вы CreateThread по всему нативному коду? Разбираем API пула потоков Win32, переработанный в Vista: четыре объекта work, tim...
Именованные каналы на практике — от проектирования до безопасности IPC в Windows
Практический разбор именованных каналов — стандартного IPC в Windows. По первоисточникам: выбор байтового режима и режима сообщений, серв...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
При распространении Windows-приложений меньше приходится переделывать, если сразу заложить в проект переход на single-file, включение среды выполнения в поставку, решение об использовании WebView2 или WinUI и целесообразность реализации в виде службы.
Технические консультации и ревью дизайна
Запрос «хотим один EXE» становится заметно проще для решения, если заранее разделить единицу распространения, зависимости от ОС, необходимость регистрации и ответственность за обновления.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Можно ли распространять Windows-приложение единственным EXE-файлом?
- Для настольного инструмента, который запускается сам по себе, это можно довести довольно далеко. Статическая линковка C/C++, self-contained и single-file в .NET, Native AOT позволяют свести поставку к одному EXE. Но возможность собрать один EXE и отсутствие зависимости от целевой Windows — разные вещи: зависимости от версии ОС, архитектуры, системных DLL и модели безопасности никуда не исчезают.
- Если сделать .NET single-file, зависимость от ОС исчезнет?
- Нет. Single-file в первую очередь уменьшает разрозненность файлов поставки приложения, а не зависимость от ОС. Развёртывание framework-dependent зависит от .NET в целевой среде, self-contained несёт среду выполнения .NET с собой, Native AOT ещё сильнее снижает зависимости на запуске, но накладывает ограничения на функции. И single-file, и Native AOT по сути привязаны к конкретной ОС и архитектуре, поэтому минимально поддерживаемую Windows и arch нужно зафиксировать с самого начала.
- Какие приложения трудно свести к одному EXE?
- Расширения оболочки, службы Windows, драйверы, интерфейс на базе WebView2 и часть приложений на WinUI 3. Здесь главный вопрос не в числе файлов, а в регистрации в ОС и в том, как обращаться с дополнительной средой выполнения. Расширению оболочки нужна регистрация в Проводнике, службе — регистрация в SCM, права и учётная запись запуска, драйверу — INF и подпись, поэтому распространение «просто положил файлы» не складывается. Для WebView2 сначала нужно решить, как распространять WebView2 Runtime.
- Есть ли способ распространения лучше, чем насильно встраивать всё в один EXE?
- Сильный вариант — развёртывание app-local, когда DLL лежат рядом с EXE. Форма `app.exe` плюс несколько соседних DLL, если при этом не нужен установщик, не нужны права администратора и можно распространять методом xcopy, нередко проще в сопровождении, чем попытка насильно сжать всё в один EXE. Главное — с самого начала разделить, хотите ли вы один файл поставки, хотите ли убрать предварительную установку среды выполнения или хотите обойтись без установщика.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.