Приложения в виде единого исполняемого файла
Исходный код: src/node_sea.cc
Эта функция позволяет удобно распространять приложение Node.js в системе, где Node.js не установлен.
Node.js поддерживает создание приложений в виде единого исполняемого файла, позволяя внедрить в двоичный файл node подготовленный Node.js блоб, который может содержать объединённый скрипт. При запуске программа проверяет, было ли что-либо внедрено. Если блоб найден, программа выполняет содержащийся в нём скрипт. В противном случае Node.js работает как обычно.
В настоящее время функция создания приложения в виде единого исполняемого файла поддерживает запуск только одного встроенного скрипта с использованием модульной системы CommonJS.
Пользователи могут создать приложение в виде единого исполняемого файла из своего объединённого скрипта, используя сам двоичный файл node и любой инструмент, способный внедрять ресурсы в двоичный файл.
Ниже приведены шаги по созданию приложения в виде единого исполняемого файла с помощью одного из таких инструментов — postject:
-
Создайте файл JavaScript:
echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js copy -
Создайте файл конфигурации для формирования блоба, который можно внедрить в приложение в виде единого исполняемого файла (подробности см. в разделе Создание подготовительных блобов для приложений в виде единого исполняемого файла):
echo '{ "main": "hello.js", "output": "sea-prep.blob" }' > sea-config.json copy -
Создайте блоб для внедрения:
node --experimental-sea-config sea-config.json copy
-
Создайте копию исполняемого файла
nodeи назовите её по своему усмотрению:- В системах, отличных от Windows:
cp $(command -v node) hello copy
- В Windows:
node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')" copyРасширение
.exeнеобходимо. -
Удалите подпись двоичного файла (только macOS и Windows):
- В macOS:
codesign --remove-signature hello copy
- В Windows (необязательно):
Можно использовать signtool из установленного Windows SDK. Если этот шаг пропущен, игнорируйте предупреждения postject, связанные с подписью.
signtool remove /s hello.exe copy
-
Внедрите блоб в скопированный двоичный файл, запустив
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 copy -
В Windows — PowerShell:
npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ` --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 copy -
В Windows — командная строка:
npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ^ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 copy -
В macOS:
npx postject hello NODE_SEA_BLOB sea-prep.blob \ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ --macho-segment-name NODE_SEA copy
-
-
Подпишите двоичный файл (только macOS и Windows):
- В macOS:
codesign --sign - hello copy
- В Windows (необязательно):
Для этого должен быть доступен сертификат. Однако неподписанный двоичный файл всё равно можно будет запустить.
signtool sign /fd SHA256 hello.exe copy
-
Запустите двоичный файл:
- В системах, отличных от Windows
$ ./hello world Hello, world! copy
- В Windows
$ .\hello.exe world Hello, world! copy
Создание подготовительных блобов для приложений в виде единого исполняемого файла
Подготовительные блобы для приложений в виде единого исполняемого файла, внедряемые в приложение, можно создать с помощью флага --experimental-sea-config двоичного файла Node.js, который будет использоваться для сборки приложения. Флаг принимает путь к файлу конфигурации в формате JSON. Если указанный путь не является абсолютным, Node.js будет использовать путь относительно текущего рабочего каталога.
В настоящее время конфигурация считывает следующие поля верхнего уровня:
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/blob.blob",
"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"
}
} copy Если пути не являются абсолютными, Node.js будет использовать пути относительно текущего рабочего каталога. Версия двоичного файла Node.js, используемого для создания блоба, должна совпадать с версией файла, в который будет внедрён блоб.
Примечание: при создании SEA для другой платформы (например, 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/blob.blob",
"assets": {
"a.jpg": "/path/to/a.jpg",
"b.txt": "/path/to/b.txt"
}
} copy Приложение в виде единого исполняемого файла может получить доступ к ресурсам следующим образом:
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'); copy Дополнительную информацию см. в документации по 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/blob.blob",
"execArgv": ["--no-warnings", "--max-old-space-size=2048"]
} copy укажет запускать 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'] copy
Аргументы, переданные пользователем, находятся в массиве process.argv начиная с индекса 2, как и при запуске приложения следующей командой:
node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2 copy
Расширение аргументов выполнения
Поле 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/blob.blob",
"execArgv": ["--no-warnings"],
"execArgvExtension": "cli"
} copy Исполняемый файл можно запустить так:
./my-sea --node-options="--trace-exit" user-arg1 user-arg2 copy
Это будет эквивалентно выполнению следующей команды:
node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2 copy
Во внедрённом основном скрипте
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[]> Массив, содержащий все ключи ресурсов, встроенных в исполняемый файл. Если ресурсы не встроены, возвращается пустой массив.
Этот метод можно использовать для получения массива всех ключей ресурсов, встроенных в приложение в виде единого исполняемого файла. Если метод вызван не внутри приложения в виде единого исполняемого файла, возникает ошибка.
require(id) во внедрённом основном скрипте не использует файловую систему
require() во внедрённом основном скрипте — это не то же самое, что require(), доступный модулям, которые не были внедрены. У него также нет свойств, которыми обладает невнедрённый require(), за исключением require.main. Его можно использовать только для загрузки встроенных модулей. Попытка загрузить модуль, доступный только в файловой системе, вызовет ошибку.
Вместо того чтобы полагаться на файловый require(), пользователи могут объединить приложение в отдельный файл JavaScript и внедрить его в исполняемый файл. Это также обеспечивает более детерминированный граф зависимостей.
Однако, если файловый require() всё же необходим, этого также можно добиться:
const { createRequire } = require('node:module');
require = createRequire(__filename); copy
__filename и module.filename во внедрённом основном скрипте
Значения __filename и module.filename во внедрённом основном скрипте равны process.execPath.
__dirname во внедрённом основном скрипте
Значение __dirname во внедрённом основном скрипте равно имени каталога для process.execPath.
Примечания
Процесс создания приложения в виде единого исполняемого файла
Инструмент, предназначенный для создания приложения Node.js в виде единого исполняемого файла, должен внедрить содержимое блоба, подготовленного с помощью --experimental-sea-config", в:
- ресурс с именем
NODE_SEA_BLOB, если двоичный файлnodeявляется файлом PE - секцию с именем
NODE_SEA_BLOBв сегментеNODE_SEA, если двоичный файлnodeявляется файлом Mach-O - заметку с именем
NODE_SEA_BLOB, если двоичный файлnodeявляется файлом ELF
Найдите в двоичном файле строку NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0 предохранителя и замените последний символ на 1, чтобы указать, что ресурс был внедрён.
Поддержка платформ
Поддержка приложений в виде единого исполняемого файла регулярно тестируется в CI только на следующих платформах:
- Windows
- macOS
- 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/dist/latest-v22.x/docs/api/single-executable-applications.html