Одноисполняемые приложения¶
Стабильность: 1 – Экспериментальная
Функция находится в стадии активной разработки и может меняться.
Эта возможность позволяет удобно распространять приложение Node.js на систему, где Node.js не установлен.
Node.js поддерживает создание одноисполняемых приложений, внедряя подготовленный Node.js блоб (в нём может быть упакованный скрипт) в двоичный файл node. При запуске проверяется, было ли что-то внедрено. Если блоб найден, выполняется скрипт из блоба. Иначе Node.js ведёт себя как обычно.
Одноисполняемое приложение может запускать один встроенный скрипт в системе модулей CommonJS или ECMAScript Modules.
Создать одноисполняемое приложение из упакованного скрипта можно с помощью самого бинарника node и любого инструмента, умеющего внедрять ресурсы в исполняемый файл.
-
Создайте файл JavaScript:
1echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js -
Создайте конфигурационный файл, задающий блоб для внедрения в одноисполняемое приложение (подробности — в разделе Генерация подготовительных блобов SEA):
-
На системах, кроме Windows:
1 | |
- На Windows:
1 | |
Расширение .exe обязательно.
-
Соберите целевой исполняемый файл:
1node --build-sea sea-config.json -
Подпишите бинарник (только macOS и Windows):
-
На macOS:
1 | |
- На Windows (по желанию):
Для подписи нужен сертификат; без подписи бинарник всё равно обычно запускается.
1 | |
-
Запустите бинарник:
-
На системах, кроме Windows:
1 2 | |
- На Windows:
1 2 | |
Генерация одноисполняемых приложений с --build-sea¶
Чтобы сразу собрать одноисполняемое приложение, используйте флаг --build-sea. Он принимает путь к JSON-конфигурации. Если путь не абсолютный, Node.js берёт его относительно текущего рабочего каталога.
Сейчас на верхнем уровне конфигурации читаются такие поля:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Если пути не абсолютные, Node.js интерпретирует их относительно текущего рабочего каталога. Версия бинарника Node.js, которым собирается блоб, должна совпадать с той, в который блоб будет внедрён.
Примечание: при кросс-платформенной сборке SEA (например, linux-x64 на darwin-arm64) поля useCodeCache и useSnapshot нужно установить в false, чтобы не получить несовместимые исполняемые файлы. Кэш кода и снимки можно загрузить только на той же платформе, где они собраны; иначе при старте возможен сбой при загрузке кэша или снимка с другой платформы.
Ресурсы¶
Ресурсы задаются словарём «ключ — путь» в поле assets. На этапе сборки Node.js читает файлы по указанным путям и включает их в подготовительный блоб. В собранном исполняемом файле ресурсы доступны через API sea.getAsset() и sea.getAssetAsBlob().
1 2 3 4 5 6 7 8 | |
Доступ к ресурсам из одноисполняемого приложения:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Подробнее см. sea.getAsset(), sea.getAssetAsBlob(), sea.getRawAsset() и sea.getAssetKeys().
Поддержка снимка запуска (startup snapshot)¶
Поле useSnapshot включает поддержку снимка при запуске. Тогда скрипт main не выполняется при запуске итогового исполняемого файла. Он выполняется при генерации подготовительного блоба на машине сборки. В блоб попадает снимок состояния, инициализированного скриптом main. Итоговый исполняемый файл с внедрённым блобом десериализует снимок во время работы.
Если useSnapshot равен true, основной скрипт должен вызвать v8.startupSnapshot.setDeserializeMainFunction(), чтобы задать код, который выполнится при запуске итогового исполняемого файла пользователем.
Типичная схема:
- На этапе сборки скрипт
mainвыполняется, чтобы подготовить кучу к приёму ввода пользователя, и настраивает главную функцию черезv8.startupSnapshot.setDeserializeMainFunction(). Эта функция компилируется и сериализуется в снимок, но на этапе сборки не вызывается. - Во время работы главная функция выполняется поверх десериализованной кучи на машине пользователя.
На основной скрипт при сборке снимка распространяются общие ограничения сценариев startup snapshot; можно использовать v8.startupSnapshot API. См. документацию по startup snapshot в Node.js.
Поддержка кэша кода V8¶
Если в конфигурации useCodeCache равен true, при генерации подготовительного блоба Node.js компилирует скрипт main и формирует кэш кода V8. Кэш входит в блоб и внедряется в итоговый исполняемый файл. При запуске вместо полной компиляции main с нуля используется кэш, что ускоряет запуск.
Примечание: при useCodeCache: true не работает import().
Аргументы выполнения¶
Поле execArgv задаёт специфичные для Node.js аргументы, которые автоматически применяются при старте одноисполняемого приложения. Так разработчики могут задать параметры среды выполнения без необходимости передавать флаги конечным пользователям.
Пример конфигурации:
1 2 3 4 5 | |
SEA будет запущен с флагами --no-warnings и --max-old-space-size=2048. Во встроенном скрипте они доступны через process.execArgv:
1 2 3 4 5 | |
Пользовательские аргументы — в process.argv, начиная с индекса 2, как при запуске:
1 | |
Расширение аргументов выполнения¶
Поле execArgvExtension задаёт, как можно дополнять аргументы сверх указанных в execArgv. Допустимы три строковых значения:
"none": расширение запрещено. Используются только аргументы изexecArgv, переменная окруженияNODE_OPTIONSигнорируется."env": (по умолчанию) переменнаяNODE_OPTIONSможет дополнять аргументы выполнения. Так сохраняется обратная совместимость."cli": исполняемый файл можно запускать с--node-options="--flag1 --flag2"; эти флаги разбираются как аргументы Node.js, а не передаются пользовательскому скрипту. Так можно использовать флаги, не поддерживаемые черезNODE_OPTIONS.
Пример с "execArgvExtension": "cli":
1 2 3 4 5 6 | |
Запуск:
1 | |
Эквивалентно:
1 | |
API одноисполняемого приложения¶
Встроенный модуль node:sea позволяет работать с одноисполняемым приложением из основного JavaScript-скрипта, встроенного в исполняемый файл.
sea.isSea()¶
- Возвращает:
<boolean>Выполняется ли этот скрипт внутри одноисполняемого приложения.
sea.getAsset(key[, encoding])¶
Метод возвращает ресурсы, заданные для включения в одноисполняемое приложение на этапе сборки. Если ресурс не найден, выбрасывается ошибка.
key<string>ключ в словаре поляassetsконфигурации одноисполняемого приложения.encoding<string>Если указано, ресурс декодируется в строку. Допустима любая кодировка, поддерживаемаяTextDecoder. Если не указано, возвращаетсяArrayBufferс копией данных.- Возвращает:
<string>|<ArrayBuffer>
sea.getAssetAsBlob(key[, options])¶
Аналогично sea.getAsset(), но результат — Blob. Если ресурс не найден, выбрасывается ошибка.
key<string>ключ в словаре поляassetsконфигурации одноисполняемого приложения.options<Object>type<string>необязательный MIME-тип для blob.- Возвращает:
<Blob>
sea.getRawAsset(key)¶
Возвращает ресурсы, заданные для включения на этапе сборки. Если ресурс не найден, выбрасывается ошибка.
В отличие от sea.getAsset() и sea.getAssetAsBlob(), метод не возвращает копию: возвращается «сырой» ресурс, встроенный в исполняемый файл.
Пока не следует записывать в возвращённый ArrayBuffer. Если внедрённая секция не помечена как доступная для записи или выравнивание неверное, запись может привести к падению.
key<string>ключ в словаре поляassetsконфигурации одноисполняемого приложения.- Возвращает:
<ArrayBuffer>
sea.getAssetKeys()¶
- Возвращает:
<string[]>Массив ключей всех встроенных ресурсов. Если ресурсов нет — пустой массив.
Возвращает список ключей ресурсов, встроенных в исполняемый файл. Вне одноисполняемого приложения вызов приводит к ошибке.
Во встроенном основном скрипте¶
Формат модуля встроенного основного скрипта¶
Интерпретацию встроенного основного скрипта задаёт поле mainFormat в конфигурации одноисполняемого приложения. Допустимые значения:
"commonjs": скрипт трактуется как модуль CommonJS."module": скрипт трактуется как ECMAScript-модуль.
Если mainFormat не указан, по умолчанию "commonjs".
Сейчас "mainFormat": "module" нельзя сочетать с "useSnapshot".
Загрузка модулей во встроенном основном скрипте¶
Во встроенном основном скрипте загрузка модулей не читает файловую систему. По умолчанию и require(), и import могут подгружать только встроенные модули. Попытка загрузить модуль только из файловой системы приведёт к ошибке.
Приложение можно собрать в один автономный JavaScript-файл для внедрения — так проще получить предсказуемый граф зависимостей.
Чтобы загружать модули с диска, создайте функцию require через module.createRequire(). Пример для точки входа CommonJS:
1 2 | |
require() во встроенном основном скрипте¶
require() здесь не совпадает с require() у обычных не встроенных модулей. Сейчас у него нет свойств не встроенного require(), кроме require.main.
__filename и module.filename во встроенном основном скрипте¶
Значения __filename и module.filename равны process.execPath.
__dirname во встроенном основном скрипте¶
__dirname равен каталогу process.execPath.
import.meta во встроенном основном скрипте¶
При "mainFormat": "module" во встроенном скрипте доступен import.meta со свойствами:
import.meta.url:file:URL, соответствующийprocess.execPath.import.meta.filename: равенprocess.execPath.import.meta.dirname: каталогprocess.execPath.import.meta.main:true.
import.meta.resolve пока не поддерживается.
import() во встроенном основном скрипте¶
При "mainFormat": "module" import() может динамически загружать встроенные модули. Загрузка модулей с файловой системы через import() приведёт к ошибке.
Нативные аддоны во встроенном основном скрипте¶
Нативные аддоны можно включить как ресурсы в поле assets конфигурации, из которой собирается подготовительный блоб одноисполняемого приложения. Аддон затем можно записать во временный файл и загрузить через process.dlopen().
1 2 3 4 5 6 7 | |
1 2 3 4 5 6 7 8 9 10 11 | |
Известное ограничение: если одноисполняемое приложение собрано через postject в контейнере Linux arm64, ELF-файл может иметь некорректную хэш-таблицу для загрузки аддонов, и process.dlopen() упадёт. Собирайте на других платформах или хотя бы вне контейнера Linux arm64.
Примечания¶
Процесс создания одноисполняемого приложения¶
Описанный здесь процесс может измениться.
1. Генерация подготовительных блобов SEA¶
Чтобы собрать одноисполняемое приложение, Node.js сначала генерирует блоб со всей информацией для запуска упакованного скрипта. При использовании --build-sea этот шаг выполняется вместе с внедрением.
Сохранение подготовительного блоба на диск¶
До появления --build-sea использовался сценарий записи подготовительного блоба на диск для внешних инструментов внедрения. Его ещё можно использовать для проверки.
Чтобы выгрузить блоб на диск, используйте --experimental-sea-config. Записывается файл, который можно внедрить в бинарник Node.js инструментами вроде postject.
Конфигурация похожа на --build-sea, но поле output задаёт путь к файлу блоба, а не к итоговому исполняемому файлу.
1 2 3 4 5 | |
2. Внедрение подготовительного блоба в бинарник node¶
Чтобы завершить сборку одноисполняемого приложения, сгенерированный блоб нужно внедрить в копию бинарника node, как описано ниже.
При --build-sea этот шаг выполняется вместе с генерацией блоба.
- Если бинарник
node— PE, блоб внедряется как ресурс с именемNODE_SEA_BLOB. - Если это Mach-O, блоб внедряется как секция
NODE_SEA_BLOBв сегментеNODE_SEA. - Если это ELF, блоб внедряется как нота
NODE_SEA_BLOB.
Затем процесс сборки SEA ищет в бинарнике строку fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 и переводит последний символ в 1, чтобы отметить внедрение ресурса.
Ручное внедрение подготовительного блоба¶
До --build-sea использовался сценарий с внешними инструментами.
Например, с postject:
-
Скопируйте исполняемый файл
nodeпод нужным именем: -
На системах, кроме Windows:
1 | |
- На Windows:
1 | |
Нужно расширение .exe.
-
Снимите подпись бинарника (только macOS и Windows):
-
На macOS:
1 | |
- На Windows (по желанию):
signtool из Windows SDK. Если шаг пропущен, игнорируйте предупреждения postject о подписи.
1 | |
-
Внедрите блоб в скопированный бинарник через
postjectс опциями: -
hello/hello.exe— имя копииnodeс шага 1. NODE_SEA_BLOB— имя ресурса / ноты / секции, где хранится блоб.sea-prep.blob— файл блоба с шага 1.--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2— fuse, которым Node.js определяет внедрение.--macho-segment-name NODE_SEA(только macOS) — сегмент, где хранится блоб.
Команды по платформам:
-
Linux:
1 2
npx postject hello NODE_SEA_BLOB sea-prep.blob \ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 -
Windows — PowerShell:
1 2
npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ` --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 -
Windows — Command Prompt:
1 2
npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ^ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 -
macOS:
1 2 3
npx postject hello NODE_SEA_BLOB sea-prep.blob \ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ --macho-segment-name NODE_SEA
Поддержка платформ¶
Одноисполняемые приложения регулярно тестируются в CI только на:
- Windows
- macOS
- Linux (все дистрибутивы, поддерживаемые Node.js, кроме Alpine, и все архитектуры, поддерживаемые Node.js, кроме s390x)
Из-за нехватки инструментов генерации одноисполняемых файлов для других платформ.
Предложения по другим инструментам и сценариям внедрения приветствуются: обсуждения — на https://github.com/nodejs/single-executable/discussions.