Однофайловые исполняемые приложения
Стабильность: 1.1 — Активная разработка
Эта функция позволяет удобно распространять приложение Node.js в системе, где Node.js не установлен.
Node.js поддерживает создание однофайловых исполняемых приложений, позволяя внедрить подготовленный Node.js блоб, который может содержать объединённый скрипт, в двоичный файл node. При запуске программа проверяет, был ли что-либо внедрено. Если блоб найден, программа выполняет скрипт из блоба. В противном случае Node.js работает в обычном режиме.
Функция однофайлового исполняемого приложения поддерживает выполнение одного встроенного скрипта с использованием модульной системы CommonJS или модулей ECMAScript.
Пользователи могут создать однофайловое исполняемое приложение из объединённого скрипта с помощью самого двоичного файла node и любого инструмента, который может внедрять ресурсы в двоичный файл.
-
Создайте файл JavaScript:
echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js bashcopy -
Создайте файл конфигурации для построения блоба, который можно внедрить в однофайловое исполняемое приложение (подробности см. в разделе Создание блобов подготовки однофайлового исполняемого приложения):
- В системах, отличных от Windows:
echo '{ "main": "hello.js", "output": "sea" }' > sea-config.json bashcopy- В Windows:
echo '{ "main": "hello.js", "output": "sea.exe" }' > sea-config.json bashcopyНеобходимо расширение
.exe. -
Создайте целевой исполняемый файл:
node --build-sea sea-config.json bashcopy
-
Подпишите двоичный файл (только macOS и Windows):
- В macOS:
codesign --sign - hello bashcopy
- В Windows (необязательно):
Для этого должен быть доступен сертификат. Однако неподписанный двоичный файл всё равно можно будет запустить.
signtool sign /fd SHA256 hello.exe powershellcopy
-
Запустите двоичный файл:
- В системах, отличных от Windows
$ ./hello world Hello, world! consolecopy
- В Windows
$ .\hello.exe world Hello, world! consolecopy
Создание однофайловых исполняемых приложений с помощью --build-sea
Чтобы создать однофайловое исполняемое приложение напрямую, можно использовать флаг --build-sea. Он принимает путь к файлу конфигурации в формате JSON. Если переданный путь не абсолютный, Node.js будет использовать путь относительно текущего рабочего каталога.
В настоящее время конфигурация считывает следующие поля верхнего уровня:
{
"main": "/path/to/bundled/script.js",
"mainFormat": "commonjs", // Default: "commonjs", options: "commonjs", "module"
"executable": "/path/to/node/binary", // Optional, if not specified, uses the current Node.js binary
"output": "/path/to/write/the/generated/executable",
"disableExperimentalSEAWarning": true, // Default: false
"useSnapshot": false, // Default: false
"useCodeCache": true, // Default: false
"execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional
"execArgvExtension": "env", // Default: "env", options: "none", "env", "cli"
"assets": { // Optional
"a.dat": "/path/to/a.dat",
"b.txt": "/path/to/b.txt"
}
}
jsoncopy
Если пути не абсолютные, Node.js будет использовать пути относительно текущего рабочего каталога. Версия двоичного файла Node.js, используемого для создания блоба, должна совпадать с версией, в которую будет внедрён блоб.
Примечание: при создании кроссплатформенных однофайловых исполняемых приложений (например, при создании SEA для linux-x64 на darwin-arm64) для избежания генерации несовместимых исполняемых файлов необходимо установить useCodeCache и useSnapshot в false. Поскольку кэш кода и снимки можно загружать только на той же платформе, где они были скомпилированы, созданный исполняемый файл может аварийно завершиться при запуске, если попытается загрузить кэш кода или снимки, созданные на другой платформе.
Ресурсы
Пользователи могут включать ресурсы, добавив в конфигурацию словарь ключей и путей в качестве поля assets. Во время сборки Node.js считывает ресурсы по указанным путям и объединяет их в блоб подготовки. В созданном исполняемом файле пользователи могут получить ресурсы с помощью API sea.getAsset() и sea.getAssetAsBlob().
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/executable",
"assets": {
"a.jpg": "/path/to/a.jpg",
"b.txt": "/path/to/b.txt"
}
}
jsoncopy
Однофайловое исполняемое приложение может обращаться к ресурсам следующим образом:
const { getAsset, getAssetAsBlob, getRawAsset, getAssetKeys } = require('node:sea');
// Get all asset keys.
const keys = getAssetKeys();
console.log(keys); // ['a.jpg', 'b.txt']
// Returns a copy of the data in an ArrayBuffer.
const image = getAsset('a.jpg');
// Returns a string decoded from the asset as UTF8.
const text = getAsset('b.txt', 'utf8');
// Returns a Blob containing the asset.
const blob = getAssetAsBlob('a.jpg');
// Returns an ArrayBuffer containing the raw asset without copying.
const raw = getRawAsset('a.jpg');
cjscopy
Дополнительную информацию см. в документации по API sea.getAsset(), sea.getAssetAsBlob(), sea.getRawAsset() и sea.getAssetKeys().
Поддержка снимка при запуске
Для включения поддержки снимка при запуске можно использовать поле useSnapshot. В этом случае скрипт main не будет выполняться при запуске итогового исполняемого файла. Вместо этого он будет запущен при создании блоба подготовки однофайлового исполняемого приложения на машине сборки. Созданный блоб подготовки будет содержать снимок, фиксирующий состояния, инициализированные скриптом main. Итоговый исполняемый файл со внедрённым блобом десериализует снимок во время выполнения.
Если useSnapshot имеет значение true, основной скрипт должен вызвать API v8.startupSnapshot.setDeserializeMainFunction(), чтобы настроить код, который должен запускаться при запуске итогового исполняемого файла пользователями.
Типичный шаблон использования снимка в однофайловом исполняемом приложении выглядит так:
- Во время сборки, на машине сборки, запускается основной скрипт, который инициализирует кучу до состояния, готового к получению пользовательского ввода. Скрипт также должен настроить основную функцию с помощью
v8.startupSnapshot.setDeserializeMainFunction(). Эта функция будет скомпилирована и сериализована в снимок, но не будет вызвана во время сборки. - Во время выполнения основная функция запускается поверх десериализованной кучи на машине пользователя для обработки пользовательского ввода и создания вывода.
Общие ограничения для скриптов снимка при запуске также применяются к основному скрипту, используемому для создания снимка однофайлового исполняемого приложения. Основной скрипт может использовать API v8.startupSnapshot, чтобы учитывать эти ограничения. См. документацию о поддержке снимка при запуске в Node.js.
Поддержка кэша кода V8
Если в конфигурации для useCodeCache установлено значение true, во время создания блоба подготовки однофайлового исполняемого приложения Node.js скомпилирует скрипт main для создания кэша кода V8. Созданный кэш кода станет частью блоба подготовки и будет внедрён в итоговый исполняемый файл. При запуске однофайлового исполняемого приложения Node.js вместо компиляции скрипта main с нуля воспользуется кэшем кода, чтобы ускорить компиляцию, а затем выполнит скрипт, что повысит производительность запуска.
Примечание: import() не работает, если useCodeCache имеет значение true.
Аргументы выполнения
Поле execArgv можно использовать для указания аргументов Node.js, которые будут автоматически применяться при запуске однофайлового исполняемого приложения. Это позволяет разработчикам приложений настраивать параметры среды выполнения Node.js, не требуя от конечных пользователей знания этих флагов.
Например, следующая конфигурация:
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/executable",
"execArgv": ["--no-warnings", "--max-old-space-size=2048"]
}
jsoncopy
укажет запускать SEA с флагами --no-warnings и --max-old-space-size=2048. В скриптах, встроенных в исполняемый файл, к этим флагам можно обратиться с помощью свойства process.execArgv:
// If the executable is launched with `sea user-arg1 user-arg2` console.log(process.execArgv); // Prints: ['--no-warnings', '--max-old-space-size=2048'] console.log(process.argv); // Prints ['/path/to/sea', 'path/to/sea', 'user-arg1', 'user-arg2'] jscopy
Аргументы, переданные пользователем, находятся в массиве process.argv, начиная с индекса 2, как и при запуске приложения следующим образом:
node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2 consolecopy
Расширение аргументов выполнения
Поле execArgvExtension определяет способ передачи дополнительных аргументов выполнения помимо указанных в поле execArgv. Оно принимает одно из трёх строковых значений:
-
"none": расширение не допускается. Будут использоваться только аргументы, указанные вexecArgv, а переменная средыNODE_OPTIONSбудет проигнорирована. -
"env": (по умолчанию) переменная средыNODE_OPTIONSможет расширять список аргументов выполнения. Это поведение используется по умолчанию для обратной совместимости. -
"cli": исполняемый файл можно запустить с--node-options="--flag1 --flag2", и эти флаги будут обработаны как аргументы выполнения Node.js, а не переданы пользовательскому скрипту. Это позволяет использовать аргументы, не поддерживаемые переменной средыNODE_OPTIONS.
Например, при "execArgvExtension": "cli":
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/executable",
"execArgv": ["--no-warnings"],
"execArgvExtension": "cli"
}
jsoncopy
Исполняемый файл можно запустить так:
./my-sea --node-options="--trace-exit" user-arg1 user-arg2 consolecopy
Это будет эквивалентно выполнению:
node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2 consolecopy
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>. Если соответствующий ресурс не найден, выбрасывается ошибка.
sea.getRawAsset(key)
Этот метод можно использовать для получения ресурсов, настроенных для включения в однофайловое исполняемое приложение во время сборки. Если соответствующий ресурс не найден, выбрасывается ошибка.
В отличие от sea.getAsset() или sea.getAssetAsBlob(), этот метод не возвращает копию. Вместо этого он возвращает исходный ресурс, встроенный в исполняемый файл.
Пока что пользователям следует избегать записи в возвращаемый буфер массива. Если внедрённая секция не помечена как доступная для записи или неправильно выровнена, запись в возвращаемый буфер массива, вероятно, приведёт к аварийному завершению.
-
key<string>ключ ресурса в словаре, указанном в полеassetsконфигурации однофайлового исполняемого приложения. - Возвращает:
<ArrayBuffer>
sea.getAssetKeys()
- Возвращает
<string[]>массив, содержащий все ключи ресурсов, встроенных в исполняемый файл. Если ресурсы не встроены, возвращает пустой массив.
Этот метод можно использовать для получения массива всех ключей ресурсов, встроенных в однофайловое исполняемое приложение. Если метод вызван вне однофайлового исполняемого приложения, выбрасывается ошибка.
Внедрённый основной скрипт
Формат модуля внедрённого основного скрипта
Чтобы указать, как Node.js должен интерпретировать внедрённый основной скрипт, используйте поле mainFormat в конфигурации однофайлового исполняемого приложения. Допустимые значения:
-
"commonjs": внедрённый основной скрипт обрабатывается как модуль CommonJS. -
"module": внедрённый основной скрипт обрабатывается как модуль ECMAScript.
Если поле mainFormat не указано, по умолчанию используется "commonjs".
В настоящее время "mainFormat": "module" нельзя использовать вместе с "useSnapshot".
Загрузка модулей во внедрённом основном скрипте
Во внедрённом основном скрипте загрузка модулей не обращается к файловой системе. По умолчанию операторы require() и import могут загружать только встроенные модули. При попытке загрузить модуль, который можно найти только в файловой системе, будет выброшена ошибка.
Пользователи могут объединить приложение в автономный файл JavaScript для внедрения в исполняемый файл. Это также обеспечивает более детерминированный граф зависимостей.
Чтобы загружать модули из файловой системы во внедрённом основном скрипте, пользователи могут создать функцию require, которая загружает их из файловой системы с помощью module.createRequire(). Например, в точке входа CommonJS:
const { createRequire } = require('node:module');
require = createRequire(__filename);
jscopy
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: URL типаfile:, соответствующий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().
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/executable",
"assets": {
"myaddon.node": "/path/to/myaddon/build/Release/myaddon.node"
}
}
jsoncopy
// script.js
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { getRawAsset } = require('node:sea');
const addonPath = path.join(os.tmpdir(), 'myaddon.node');
fs.writeFileSync(addonPath, new Uint8Array(getRawAsset('myaddon.node')));
const myaddon = { exports: {} };
process.dlopen(myaddon, addonPath);
console.log(myaddon.exports);
fs.rmSync(addonPath);
jscopy
Известное ограничение: если однофайловое исполняемое приложение создано с помощью postject в контейнере Docker Linux arm64, созданный двоичный файл ELF не содержит правильную хеш-таблицу для загрузки дополнений и аварийно завершится при вызове process.dlopen(). Чтобы обойти эту проблему, создавайте однофайловое исполняемое приложение на других платформах или хотя бы в среде Linux arm64 вне контейнера.
Примечания
Процесс создания однофайлового исполняемого приложения
Описанный здесь процесс может измениться.
1. Создание блобов подготовки однофайлового исполняемого приложения
Для сборки однофайлового исполняемого приложения Node.js сначала создаёт блоб, содержащий всю необходимую информацию для запуска объединённого скрипта. При использовании --build-sea этот этап выполняется автоматически вместе с внедрением.
Сохранение блоба подготовки на диск
До появления --build-sea был введён старый рабочий процесс для записи блоба подготовки на диск с целью его внедрения внешними инструментами. Его всё ещё можно использовать для проверки.
Чтобы сохранить блоб подготовки на диск для проверки, используйте --experimental-sea-config. Эта команда создаёт файл, который можно внедрить в двоичный файл Node.js с помощью таких инструментов, как postject.
Конфигурация аналогична конфигурации --build-sea, за исключением того, что поле output указывает путь для записи созданного файла блоба, а не итогового исполняемого файла.
{
"main": "/path/to/bundled/script.js",
// Instead of the final executable, this is the path to write the blob.
"output": "/path/to/write/the/generated/blob.blob"
}
jsoncopy2. Внедрение блоба подготовки в двоичный файл node
Чтобы завершить создание однофайлового исполняемого приложения, созданный блоб необходимо внедрить в копию двоичного файла node, как описано ниже.
При использовании --build-sea этот этап выполняется автоматически вместе с созданием блоба.
- Если двоичный файл
nodeявляется файлом PE, блоб следует внедрить как ресурс с именемNODE_SEA_BLOB. - Если двоичный файл
nodeявляется файлом Mach-O, блоб следует внедрить как секцию с именемNODE_SEA_BLOBв сегментеNODE_SEA. - Если двоичный файл
nodeявляется файлом ELF, блоб следует внедрить как примечание с именемNODE_SEA_BLOB.
Затем процесс сборки SEA ищет в двоичном файле строку-предохранитель NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 fuse и меняет её последний символ на 1, чтобы указать, что ресурс был внедрён.
Внедрение блоба подготовки вручную
До появления --build-sea был введён старый рабочий процесс, позволяющий внешним инструментам внедрять созданный блоб в копию двоичного файла node.
Например, с помощью postject:
-
Создайте копию исполняемого файла
nodeи назовите её по своему усмотрению:- В системах, отличных от Windows:
cp $(command -v node) hello bashcopy
- В Windows:
node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')" textcopyНеобходимо расширение
.exe. -
Удалите подпись двоичного файла (только macOS и Windows):
- В macOS:
codesign --remove-signature hello bashcopy
- В Windows (необязательно):
signtool можно использовать из установленного Windows SDK. Если этот шаг пропущен, игнорируйте предупреждения postject, связанные с подписью.
signtool remove /s hello.exe powershellcopy
-
Внедрите блоб в скопированный двоичный файл, запустив
postjectсо следующими параметрами:-
hello/hello.exe— имя копии исполняемого файлаnode, созданной на шаге 4. -
NODE_SEA_BLOB— имя ресурса / примечания / секции двоичного файла, в которой будет храниться содержимое блоба. -
sea-prep.blob— имя блоба, созданного на шаге 1. -
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2— предохранитель, используемый проектом Node.js для определения факта внедрения файла. -
--macho-segment-name NODE_SEA(требуется только в macOS) — имя сегмента двоичного файла, в котором будет храниться содержимое блоба.
Итоговые команды для каждой платформы:
-
В Linux:
npx postject hello NODE_SEA_BLOB sea-prep.blob \ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 bashcopy -
В Windows — PowerShell:
npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ` --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 powershellcopy -
В Windows — командная строка:
npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ^ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 textcopy -
В macOS:
npx postject hello NODE_SEA_BLOB sea-prep.blob \ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ --macho-segment-name NODE_SEA bashcopy
-
Поддержка платформ
Поддержка однофайловых исполняемых приложений регулярно проверяется в CI только на следующих платформах:
- Windows
- macOS (только arm64; x64 в настоящее время не поддерживается и исключён из тестов)
- Linux (все дистрибутивы, поддерживаемые Node.js, кроме Alpine, и все архитектуры, поддерживаемые Node.js, кроме s390x)
Это связано с отсутствием более совершенных инструментов для создания однофайловых исполняемых файлов, которые можно было бы использовать для тестирования этой функции на других платформах.
Мы приветствуем предложения по другим инструментам и рабочим процессам внедрения ресурсов. Чтобы помочь нам задокументировать их, начните обсуждение по адресу https://github.com/nodejs/single-executable/discussions.
© Joyent, Inc. and other Node contributors
Licensed under the MIT License.
Node.js is a trademark of Joyent, Inc. and is used with its permission.
We are not endorsed by or affiliated with Joyent.
https://nodejs.org/api/single-executable-applications.html