Spec-Zone.ru › Node.js

Однофайловые исполняемые приложения

Исходный код: src/node_sea.ccДобавлено в: v19.7.0, v18.16.0
История
Версия Изменения
v25.5.0

Добавлена встроенная генерация однофайловых исполняемых приложений с помощью флага CLI --build-sea.

v20.6.0

Добавлена поддержка "useSnapshot".

v20.6.0

Добавлена поддержка "useCodeCache".

Стабильность: 1.1 — Активная разработка

Эта функция позволяет удобно распространять приложение Node.js в системе, где Node.js не установлен.

Node.js поддерживает создание однофайловых исполняемых приложений, позволяя внедрить подготовленный Node.js блоб, который может содержать объединённый скрипт, в двоичный файл node. При запуске программа проверяет, был ли что-либо внедрено. Если блоб найден, программа выполняет скрипт из блоба. В противном случае Node.js работает в обычном режиме.

Функция однофайлового исполняемого приложения поддерживает выполнение одного встроенного скрипта с использованием модульной системы CommonJS или модулей ECMAScript.

Пользователи могут создать однофайловое исполняемое приложение из объединённого скрипта с помощью самого двоичного файла node и любого инструмента, который может внедрять ресурсы в двоичный файл.

  1. Создайте файл JavaScript:

    echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js
    bashcopy
  2. Создайте файл конфигурации для построения блоба, который можно внедрить в однофайловое исполняемое приложение (подробности см. в разделе Создание блобов подготовки однофайлового исполняемого приложения):

    • В системах, отличных от Windows:
    echo '{ "main": "hello.js", "output": "sea" }' > sea-config.json
    bashcopy
    • В Windows:
    echo '{ "main": "hello.js", "output": "sea.exe" }' > sea-config.json
    bashcopy

    Необходимо расширение .exe.

  3. Создайте целевой исполняемый файл:

    node --build-sea sea-config.json
    bashcopy
  4. Подпишите двоичный файл (только macOS и Windows):

    • В macOS:
    codesign --sign - hello
    bashcopy
    • В Windows (необязательно):

    Для этого должен быть доступен сертификат. Однако неподписанный двоичный файл всё равно можно будет запустить.

    signtool sign /fd SHA256 hello.exe
    powershellcopy
  5. Запустите двоичный файл:

    • В системах, отличных от 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(), чтобы настроить код, который должен запускаться при запуске итогового исполняемого файла пользователями.

Типичный шаблон использования снимка в однофайловом исполняемом приложении выглядит так:

  1. Во время сборки, на машине сборки, запускается основной скрипт, который инициализирует кучу до состояния, готового к получению пользовательского ввода. Скрипт также должен настроить основную функцию с помощью v8.startupSnapshot.setDeserializeMainFunction(). Эта функция будет скомпилирована и сериализована в снимок, но не будет вызвана во время сборки.
  2. Во время выполнения основная функция запускается поверх десериализованной кучи на машине пользователя для обработки пользовательского ввода и создания вывода.

Общие ограничения для скриптов снимка при запуске также применяются к основному скрипту, используемому для создания снимка однофайлового исполняемого приложения. Основной скрипт может использовать 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()

Добавлено в: v21.7.0, v20.12.0
  • Возвращает: <boolean> значение, указывающее, выполняется ли этот скрипт внутри однофайлового исполняемого приложения.

sea.getAsset(key[, encoding])

Добавлено в: v21.7.0, v20.12.0

Этот метод можно использовать для получения ресурсов, настроенных для включения в однофайловое исполняемое приложение во время сборки. Если соответствующий ресурс не найден, выбрасывается ошибка.

  • key <string> ключ ресурса в словаре, указанном в поле assets конфигурации однофайлового исполняемого приложения.
  • encoding <string> Если задано, ресурс будет декодирован как строка. Допускается любая кодировка, поддерживаемая TextDecoder. Если значение не задано, вместо этого будет возвращён ArrayBuffer, содержащий копию ресурса.
  • Возвращает: <string> | <ArrayBuffer>

sea.getAssetAsBlob(key[, options])

Добавлено в: v21.7.0, v20.12.0

Подобно sea.getAsset(), но возвращает результат в виде <Blob>. Если соответствующий ресурс не найден, выбрасывается ошибка.

  • key <string> ключ ресурса в словаре, указанном в поле assets конфигурации однофайлового исполняемого приложения.
  • options <Object>
    • type <string> Необязательный MIME-тип блоба.
  • Возвращает: <Blob>

sea.getRawAsset(key)

Добавлено в: v21.7.0, v20.12.0

Этот метод можно использовать для получения ресурсов, настроенных для включения в однофайловое исполняемое приложение во время сборки. Если соответствующий ресурс не найден, выбрасывается ошибка.

В отличие от sea.getAsset() или sea.getAssetAsBlob(), этот метод не возвращает копию. Вместо этого он возвращает исходный ресурс, встроенный в исполняемый файл.

Пока что пользователям следует избегать записи в возвращаемый буфер массива. Если внедрённая секция не помечена как доступная для записи или неправильно выровнена, запись в возвращаемый буфер массива, вероятно, приведёт к аварийному завершению.

  • key <string> ключ ресурса в словаре, указанном в поле assets конфигурации однофайлового исполняемого приложения.
  • Возвращает: <ArrayBuffer>

sea.getAssetKeys()

Добавлено в: v24.8.0, v22.20.0
  • Возвращает <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"
}
jsoncopy
2. Внедрение блоба подготовки в двоичный файл 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:

  1. Создайте копию исполняемого файла node и назовите её по своему усмотрению:

    • В системах, отличных от Windows:
    cp $(command -v node) hello
    bashcopy
    • В Windows:
    node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')"
    textcopy

    Необходимо расширение .exe.

  2. Удалите подпись двоичного файла (только macOS и Windows):

    • В macOS:
    codesign --remove-signature hello
    bashcopy
    • В Windows (необязательно):

    signtool можно использовать из установленного Windows SDK. Если этот шаг пропущен, игнорируйте предупреждения postject, связанные с подписью.

    signtool remove /s hello.exe
    powershellcopy
  3. Внедрите блоб в скопированный двоичный файл, запустив 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API