sea: add vfsArchive to serve the assets from a ZIP archive by mcollina · Pull Request #65810 · nodejs/node · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions doc/api/single-executable-applications.md
6 changes: 6 additions & 0 deletions doc/api/vfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ is loaded from inside the mount through the ESM loader, and
`"useVfs"` cannot be used together with `"useSnapshot"` or `"useCodeCache"`.
The SEA configuration parser will error if either combination is detected.

Instead of listing individual `"assets"`, the SEA configuration can point
`"vfsArchive"` at a prebuilt ZIP archive; the mount is then backed by a
[`ZipProvider`][] over the embedded archive, and each file is inflated when
it is read. See [Serving the assets from a ZIP archive][] for details.

See the [Single Executable Application][] documentation for more information
on creating SEA builds with assets.

Expand Down Expand Up @@ -628,6 +633,7 @@ fields use synthetic but stable values:
[CommonJS resolution algorithm]: modules.md#all-together
[ES modules resolution algorithm]: esm.md#resolution-algorithm
[Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management
[Serving the assets from a ZIP archive]: single-executable-applications.md#serving-the-assets-from-a-zip-archive-with-vfsarchive
[Single Executable Application]: single-executable-applications.md
[`MemoryProvider`]: #class-memoryprovider
[`RealFSProvider`]: #class-realfsprovider
Expand Down
57 changes: 54 additions & 3 deletions lib/internal/vfs/sea.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
'use strict';

const { isSea, isVfsEnabled } = internalBinding('sea');
const {
ObjectKeys,
} = primordials;

const {
isSea,
isVfsEnabled,
isVfsArchiveEnabled,
getAsset,
} = internalBinding('sea');
const { kEmptyObject } = require('internal/util');
const {
codes: {
Expand Down Expand Up @@ -38,9 +47,14 @@ function initSeaVfs(options = kEmptyObject) {
}

const { VirtualFileSystem } = require('internal/vfs/file_system');
const { SEAProvider } = require('internal/vfs/providers/sea');

const provider = new SEAProvider({ extraFiles: options.extraFiles });
let provider;
if (isVfsArchiveEnabled()) {
provider = createZipProvider(options.extraFiles);
} else {
const { SEAProvider } = require('internal/vfs/providers/sea');
provider = new SEAProvider({ extraFiles: options.extraFiles });
}
// The SEA warning already covers the feature; don't emit the
// VirtualFileSystem experimental warning for the implicit SEA mount.
const vfs = new VirtualFileSystem(provider, {
Expand All @@ -51,6 +65,43 @@ function initSeaVfs(options = kEmptyObject) {
return vfs;
}

// The reserved asset key under which --build-sea stores the ZIP archive
// named by "vfsArchive". Must match kVfsArchiveAssetName in src/node_sea.cc.
const kVfsArchiveAssetName = 'node:sea:vfs.zip';

/**
* Creates a ZipProvider over the ZIP archive embedded by `"vfsArchive"`.
* The archive bytes are used in place (a zero-copy view over the SEA
* blob); entries are inflated on demand when they are opened.
* The extra files (the SEA main script) are added to the in-memory archive
* index as stored entries, leaving the embedded bytes untouched.
* @param {Record<string, string|Buffer>} [extraFiles] Additional files to
* serve alongside the assets (used for the SEA main script)
* @returns {ZipProvider}
*/
function createZipProvider(extraFiles) {
const { Buffer } = require('buffer');
const { ZipBuffer } = require('internal/zip');
const { ZipProvider } = require('internal/vfs/providers/ziparchive');

// getAsset returns a zero-copy ArrayBuffer over the (possibly read-only)
// SEA blob; ZipBuffer only reads from it, and decompressed contents are
// fresh buffers, so the view can be used without copying the archive.
const archive = getAsset(kVfsArchiveAssetName);
const zip = new ZipBuffer(Buffer.from(archive));

if (extraFiles !== undefined) {
const names = ObjectKeys(extraFiles);
for (let i = 0; i < names.length; i++) {
const content = extraFiles[names[i]];
const data = typeof content === 'string' ? Buffer.from(content) : content;
zip.addSync(names[i], data, { __proto__: null, method: 'store' });
}
}

return new ZipProvider(zip);
}

/* c8 ignore stop */

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion src/module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
// For embedder ESM in a SEA, use the bundled code cache if available.
if (id_symbol == realm->isolate_data()->embedder_module_hdo() &&
sea::IsSingleExecutable()) {
sea::SeaResource sea = sea::FindSingleExecutableResource();
const sea::SeaResource& sea = sea::FindSingleExecutableResource();
if (sea.use_code_cache()) {
std::string_view data = sea.code_cache.value();
user_cached_data = new ScriptCompiler::CachedData(
Expand Down
6 changes: 3 additions & 3 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ MaybeLocal<Value> StartExecution(Environment* env,
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
// Snapshot in SEA is only loaded for the main thread.
if (sea::IsSingleExecutable() && env->is_main_thread()) {
sea::SeaResource sea = sea::FindSingleExecutableResource();
const sea::SeaResource& sea = sea::FindSingleExecutableResource();
// The SEA preparation blob building process should already enforce this,
// this check is just here to guard against the unlikely case where
// the SEA preparation blob has been manually modified by someone.
Expand Down Expand Up @@ -976,7 +976,7 @@ static ExitCode InitializeNodeWithArgsInternal(
!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv);
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
if (sea::IsSingleExecutable()) {
sea::SeaResource sea_resource = sea::FindSingleExecutableResource();
const sea::SeaResource& sea_resource = sea::FindSingleExecutableResource();
if (sea_resource.exec_argv_extension != sea::SeaExecArgvExtension::kEnv) {
should_parse_node_options = false;
}
Expand Down Expand Up @@ -1562,7 +1562,7 @@ bool LoadSnapshotData(const SnapshotData** snapshot_data_ptr) {
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
if (sea::IsSingleExecutable()) {
is_sea = true;
sea::SeaResource sea = sea::FindSingleExecutableResource();
const sea::SeaResource& sea = sea::FindSingleExecutableResource();
if (sea.use_snapshot()) {
std::unique_ptr<SnapshotData> read_data =
std::make_unique<SnapshotData>();
Expand Down
2 changes: 1 addition & 1 deletion src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1768,7 +1768,7 @@ static void CompileFunctionForCJSLoader(
ScriptCompiler::CachedData* cached_data = nullptr;
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
if (is_sea_main) {
sea::SeaResource sea = sea::FindSingleExecutableResource();
const sea::SeaResource& sea = sea::FindSingleExecutableResource();
// Use the "main" field in SEA config for the filename.
Local<Value> filename_from_sea;
if (!ToV8Value(context, sea.code_path).ToLocal(&filename_from_sea)) {
Expand Down
84 changes: 75 additions & 9 deletions src/node_sea.cc
Loading
Loading