Однофайловые исполняемые приложения
Исходный код: src/node_sea.cc
Эта возможность позволяет удобно распространять приложение Node.js в системе, где Node.js не установлен.
Node.js поддерживает создание однофайловых исполняемых приложений, позволяя внедрять blob, подготовленный Node.js и содержащий объединенный скрипт, в двоичный файл node. При запуске программа проверяет, было ли что-либо внедрено. Если blob найден, программа выполняет содержащийся в нем скрипт. В противном случае Node.js работает обычным образом.
В настоящее время возможность создания однофайловых исполняемых приложений поддерживает запуск только одного встроенного скрипта с использованием модульной системы CommonJS.
Пользователи могут создать однофайловое исполняемое приложение из объединенного скрипта с помощью самого двоичного файла node и любого инструмента, который может внедрять ресурсы в двоичный файл.
Ниже приведены шаги по созданию однофайлового исполняемого приложения с помощью такого инструмента, как postject:
-
Создайте файл JavaScript:
echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js copy -
Создайте файл конфигурации для сборки blob, который можно внедрить в однофайловое исполняемое приложение (подробности см. в разделе Создание подготовительных blob для однофайловых исполняемых приложений):
echo '{ "main": "hello.js", "output": "sea-prep.blob" }' > sea-config.json copy -
Создайте blob для внедрения:
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
-
Внедрите blob в скопированный двоичный файл, запустив
postjectсо следующими параметрами:-
hello/hello.exe— имя копии исполняемого файлаnode, созданной на шаге 4. -
NODE_SEA_BLOB— имя ресурса / заметки / секции двоичного файла, в которой будет храниться содержимое blob. -
sea-prep.blob— имя blob, созданного на шаге 1. -
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2— предохранитель, используемый проектом Node.js для определения факта внедрения файла. -
--macho-segment-name NODE_SEA(требуется только в macOS) — имя сегмента двоичного файла, в котором будет храниться содержимое blob.
Итак, необходимые команды для каждой платформы:
-
В 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
Создание подготовительных blob для однофайловых исполняемых приложений
Подготовительные blob для однофайловых исполняемых приложений, внедряемые в приложение, можно создать с помощью флага --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, используемого для создания blob, должна совпадать с версией файла, в который будет внедрен blob.
Примечание. При создании кроссплатформенных SEA (например, при создании SEA для linux-x64 на darwin-arm64) параметры useCodeCache и useSnapshot должны иметь значение false, чтобы избежать создания несовместимых исполняемых файлов. Поскольку кэш кода и снимки можно загружать только на той же платформе, на которой они были скомпилированы, созданный исполняемый файл может аварийно завершить работу при запуске, если попытается загрузить кэш кода или снимки, созданные на другой платформе.
Ресурсы
Пользователи могут включить ресурсы, добавив в конфигурацию словарь «ключ — путь» в поле assets. Во время сборки Node.js считывает ресурсы по указанным путям и объединяет их в подготовительный blob. В созданном исполняемом файле пользователи могут получить ресурсы с помощью 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 не будет выполняться при запуске итогового исполняемого файла. Вместо этого он будет выполнен при создании подготовительного blob однофайлового исполняемого приложения на машине сборки. Созданный подготовительный blob будет содержать снимок, фиксирующий состояния, инициализированные скриптом main. Итоговый исполняемый файл со внедренным подготовительным blob десериализует снимок во время выполнения.
Если useSnapshot имеет значение true, главный скрипт должен вызывать API v8.startupSnapshot.setDeserializeMainFunction(), чтобы настроить код, который должен выполняться при запуске итогового исполняемого файла пользователями.
Типичный шаблон использования снимка в однофайловом исполняемом приложении выглядит так:
- Во время сборки на машине сборки запускается главный скрипт, чтобы инициализировать кучу в состоянии, готовом к получению пользовательского ввода. Скрипт также должен настроить главную функцию с помощью
v8.startupSnapshot.setDeserializeMainFunction(). Эта функция будет скомпилирована и сериализована в снимок, но не будет вызвана во время сборки. - Во время выполнения главная функция будет запущена поверх десериализованной кучи на машине пользователя для обработки пользовательского ввода и формирования вывода.
Общие ограничения, накладываемые на скрипты снимков при запуске, также применяются к главному скрипту, когда он используется для создания снимка однофайлового исполняемого приложения. Главный скрипт может использовать API v8.startupSnapshot, чтобы адаптироваться к этим ограничениям. См. документацию о поддержке снимков при запуске в Node.js.
Поддержка кэша кода V8
Если в конфигурации для useCodeCache задано значение true, во время создания подготовительного blob однофайлового исполняемого приложения Node.js скомпилирует скрипт main для создания кэша кода V8. Созданный кэш кода станет частью подготовительного blob и будет внедрен в итоговый исполняемый файл. При запуске однофайлового исполняемого приложения 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(), этот метод не возвращает копию. Вместо этого он возвращает исходный ресурс, встроенный в исполняемый файл.
Пока что пользователям следует избегать записи в возвращаемый буфер ArrayBuffer. Если внедренная секция не помечена как доступная для записи или выровнена неправильно, запись в возвращаемый буфер ArrayBuffer, скорее всего, приведет к аварийному завершению работы.
-
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.
Использование нативных дополнений во внедренном главном скрипте
Нативные дополнения можно объединить в однофайловое исполняемое приложение как ресурсы, указав их в поле assets файла конфигурации, используемого для создания подготовительного blob однофайлового исполняемого приложения. Затем дополнение можно загрузить во внедренном главном скрипте, записав ресурс во временный файл и загрузив его с помощью process.dlopen().
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/blob.blob",
"assets": {
"myaddon.node": "/path/to/myaddon/build/Release/myaddon.node"
}
} copy // 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); copy Известная проблема: если однофайловое исполняемое приложение создано с помощью postject в контейнере Docker Linux arm64, созданный двоичный файл ELF не содержит правильную хеш-таблицу для загрузки дополнений и аварийно завершит работу при вызове process.dlopen(). Чтобы обойти эту проблему, создавайте однофайловое исполняемое приложение на других платформах или хотя бы в среде Linux arm64 вне контейнера.
Примечания
Процесс создания однофайлового исполняемого приложения
Инструмент, предназначенный для создания однофайлового исполняемого приложения Node.js, должен внедрить содержимое blob, подготовленного с помощью --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-v24.x/docs/api/single-executable-applications.html