Приложения в виде единого исполняемого файла
Исходный код: 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
"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 } = require('node:sea');
// 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().
Поддержка снимков состояния при запуске
Поле 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.
Внедрённый основной скрипт
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(), этот метод не возвращает копию. Вместо этого он возвращает исходный ресурс, встроенный в исполняемый файл.
Пока что пользователям следует избегать записи в возвращаемый буфер массива. Если внедрённая секция не помечена как доступная для записи или не выровнена должным образом, запись в возвращаемый буфер массива, скорее всего, приведёт к аварийному завершению.
-
key<string> ключ ресурса в словаре, указанном в полеassetsконфигурации приложения в виде единого исполняемого файла. - Возвращает: <ArrayBuffer>
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-v20.x/docs/api/single-executable-applications.html