Skip to content
Open
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
4 changes: 4 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3936,6 +3936,10 @@ from anywhere else, such as the real file system, needs nothing added.
the command line's decision, and the environment must not be able to redirect
it.

A [single executable application][] built with `"vfsArchive"` mounts that
archive at the mount point reserved for `--vfs-load`, so `--vfs-load` cannot be
used in it.

```console
$ node --experimental-vfs --vfs-load=app.zip
```
Expand Down
22 changes: 22 additions & 0 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,27 @@ executable only embeds the archive; read the files through the file system
APIs instead. `"vfsArchive"` requires `"useVfs": true` and cannot be
combined with `"assets"`.

The archive is mounted at the mount point reserved for [`--vfs-load`][], which
is the same in every thread, and worker threads mount it as well. A path into
the archive, such as the main script's `__filename`, can therefore be loaded
in a worker:

```cjs
const { Worker, isMainThread } = require('node:worker_threads');

if (isMainThread) {
new Worker(__filename);
} else {
// The worker runs the same script, from the same archive.
console.log(require('./lib/math.js').add(2, 3));
}
```

Since that mount point is taken by the archive, `--vfs-load` cannot be used
with `"vfsArchive"`: `--build-sea` rejects it in `"execArgv"`, and the
executable rejects it at startup when it comes from `--node-options` or from
the `execArgv` of a worker.

#### Snapshot and code caching limitations

`"useVfs": true` cannot be used together with `"useSnapshot": true` or
Expand Down Expand Up @@ -786,6 +807,7 @@ to help us document them.
[Using native addons in the injected main script]: #using-native-addons-in-the-injected-main-script
[VFS documentation]: vfs.md
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
[`--vfs-load`]: cli.md#--vfs-loadsource
[`node:zlib`]: zlib.md
[`process.execPath`]: process.md#processexecpath
[`require()`]: modules.md#requireid
Expand Down
2 changes: 1 addition & 1 deletion doc/api/vfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ const fs = require('node:fs');
const myVfs = vfs.create();
myVfs.writeFileSync('/data.txt', 'Hello');
const mountPoint = myVfs.mount();
// e.g. '/dev/null/vfs/0'
// e.g. '/dev/null/vfs/1'

fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello'
```
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,9 @@ from anywhere else, such as the real file system, needs nothing added.
\fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is
the command line's decision, and the environment must not be able to redirect
it.
A single executable application built with \fB"vfsArchive"\fR mounts that
archive at the mount point reserved for \fB--vfs-load\fR, so \fB--vfs-load\fR cannot be
used in it.
.Bd -literal
$ node --experimental-vfs --vfs-load=app.zip
.Ed
Expand Down
8 changes: 3 additions & 5 deletions lib/internal/main/embedding.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const {
isExperimentalSeaWarningNeeded,
isSea,
isVfsEnabled,
mainCodePath: seaMainCodePath,
} = internalBinding('sea');
const { emitExperimentalWarning } = require('internal/util');
const { emitWarningSync } = require('internal/process/warning');
Expand Down Expand Up @@ -134,13 +133,12 @@ function embedderRunESM(content, filename) {
* @returns {string|null} The VFS path of the main script
*/
function setUpSeaVfs(content) {
const mainName = path.basename(seaMainCodePath || process.execPath);
const { initSeaVfs } = require('internal/vfs/sea');
const seaVfs = initSeaVfs({ extraFiles: { [mainName]: content } });
const { initSeaVfs, getSeaMainName } = require('internal/vfs/sea');
const seaVfs = initSeaVfs(content);
if (seaVfs === null) {
return null;
}
return path.join(seaVfs.mountPoint, mainName);
return path.join(seaVfs.mountPoint, getSeaMainName());
}
/* c8 ignore stop */

Expand Down
3 changes: 3 additions & 0 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ port.on('message', (message) => {
if (getOptionValue('--import').length === 0) {
finishVfsMounts();
}
// Unlike the --vfs-load source, a SEA archive needs no provider that a
// preload could register, so its mount is never deferred.
require('internal/vfs/sea').initSeaVfsInWorker();
}

if (!hasStdin)
Expand Down
45 changes: 36 additions & 9 deletions lib/internal/vfs/sea.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ const {
isVfsEnabled,
isVfsArchiveEnabled,
getAsset,
getMainCode,
mainCodePath,
} = internalBinding('sea');
const { kEmptyObject } = require('internal/util');
const {
codes: {
ERR_INVALID_STATE,
Expand All @@ -29,14 +30,12 @@ let initialized = false;
* SEA main script is placed at the mount point root so bundled code can
* reach the assets through `__dirname`-relative paths and relative
* `require()` calls.
* @param {object} [options] Configuration options
* @param {Record<string, string|Buffer>} [options.extraFiles] Additional
* files to serve alongside the assets (used for the SEA main script)
* @param {string|Buffer} mainCode The source of the SEA main script
* @returns {VirtualFileSystem|null} The mounted VFS, or null if not running
* as a SEA or VFS is not enabled in the SEA configuration
* @throws {ERR_INVALID_STATE} If already initialized
*/
function initSeaVfs(options = kEmptyObject) {
function initSeaVfs(mainCode) {
if (initialized) {
throw new ERR_INVALID_STATE('SEA VFS is already initialized');
}
Expand All @@ -46,25 +45,51 @@ function initSeaVfs(options = kEmptyObject) {
return null;
}

const { VirtualFileSystem } = require('internal/vfs/file_system');
const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system');

const extraFiles = { __proto__: null, [getSeaMainName()]: mainCode };
const isArchive = isVfsArchiveEnabled();
let provider;
if (isVfsArchiveEnabled()) {
provider = createZipProvider(options.extraFiles);
if (isArchive) {
provider = createZipProvider(extraFiles);
} else {
const { SEAProvider } = require('internal/vfs/providers/sea');
provider = new SEAProvider({ extraFiles: options.extraFiles });
provider = new SEAProvider({ extraFiles });
}
// The SEA warning already covers the feature; don't emit the
// VirtualFileSystem experimental warning for the implicit SEA mount.
// An archive is mounted where --vfs-load mounts its source, which is the
// same mount point in every thread, so a worker can reach the code in it.
const vfs = new VirtualFileSystem(provider, {
__proto__: null,
emitExperimentalWarning: false,
[kLoadLayer]: isArchive,
});
vfs.mount();

return vfs;
}

/**
* Mounts the SEA virtual file system in a worker, when it is backed by a
* `"vfsArchive"`, so that a path into the mount in the main thread, such as
* the main script's `__filename`, is also valid in the worker.
*/
function initSeaVfsInWorker() {
if (isVfsArchiveEnabled()) {
initSeaVfs(getMainCode());
}
}

/**
* The name under which the SEA main script is placed at the mount point root.
* @returns {string}
*/
function getSeaMainName() {
const { basename } = require('path');
return basename(mainCodePath || process.execPath);
}

// 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';
Expand Down Expand Up @@ -105,5 +130,7 @@ function createZipProvider(extraFiles) {
/* c8 ignore stop */

module.exports = {
getSeaMainName,
initSeaVfs,
initSeaVfsInWorker,
};
10 changes: 10 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,16 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
errors->push_back("either --check or --eval can be used, not both");
}

#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
// The archive takes the mount point reserved for the --vfs-load source.
if (!vfs_load_source.empty() && sea::IsSingleExecutable() &&
sea::FindSingleExecutableResource().use_vfs_archive()) {
errors->push_back(
"--vfs-load cannot be used in a single executable application "
"built with \"vfsArchive\"");
}
#endif // DISABLE_SINGLE_EXECUTABLE_APPLICATION

for (const std::string& pattern : permission::ParseEnvAllowList(allow_env)) {
if (!permission::IsValidEnvAllowPattern(pattern)) {
errors->push_back("--allow-env must be '*', a variable name, or a "
Expand Down
37 changes: 31 additions & 6 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ bool SeaResource::use_code_cache() const {
return static_cast<bool>(flags & SeaFlags::kUseCodeCache);
}

bool SeaResource::use_vfs_archive() const {
return static_cast<bool>(flags & SeaFlags::kVfsArchive);
}

const SeaResource& FindSingleExecutableResource() {
static const SeaResource sea_resource = []() -> SeaResource {
std::string_view blob = FindSingleExecutableBlob();
Expand Down Expand Up @@ -277,12 +281,8 @@ void IsVfsEnabled(const FunctionCallbackInfo<Value>& args) {
}

void IsVfsArchiveEnabled(const FunctionCallbackInfo<Value>& args) {
bool enabled = false;
if (IsSingleExecutable()) {
const SeaResource& sea_resource = FindSingleExecutableResource();
enabled = static_cast<bool>(sea_resource.flags & SeaFlags::kVfsArchive);
}
args.GetReturnValue().Set(enabled);
args.GetReturnValue().Set(IsSingleExecutable() &&
FindSingleExecutableResource().use_vfs_archive());
}

void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
Expand Down Expand Up @@ -636,6 +636,15 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
config_path);
return std::nullopt;
}
// The archive takes the mount point reserved for the --vfs-load source.
for (const std::string& arg : result.exec_argv) {
if (arg == "--vfs-load" || arg.starts_with("--vfs-load=")) {
FPrintF(stderr,
"\"vfsArchive\" cannot be used together with --vfs-load in "
"\"execArgv\"\n");
return std::nullopt;
}
}
// The archive is embedded as a single reserved asset.
result.flags |= SeaFlags::kIncludeAssets;
}
Expand Down Expand Up @@ -966,6 +975,20 @@ void GetAssetKeys(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result);
}

// A worker has no main script handed to it, but places the SEA main script in
// its virtual file system just as the main thread does.
void GetMainCode(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 0);
const SeaResource& sea_resource = FindSingleExecutableResource();
CHECK(!sea_resource.use_snapshot());
Local<Value> code;
if (ToV8Value(args.GetIsolate()->GetCurrentContext(),
sea_resource.main_code_or_snapshot)
.ToLocal(&code)) {
args.GetReturnValue().Set(code);
}
}

MaybeLocal<Value> LoadSingleExecutableApplication(
const StartExecutionCallbackInfoWithModule& info) {
// Here we are currently relying on the fact that in NodeMainInstance::Run(),
Expand Down Expand Up @@ -1048,6 +1071,7 @@ void Initialize(Local<Object> target,
IsExperimentalSeaWarningNeeded);
SetMethod(context, target, "getAsset", GetAsset);
SetMethod(context, target, "getAssetKeys", GetAssetKeys);
SetMethod(context, target, "getMainCode", GetMainCode);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
Expand All @@ -1057,6 +1081,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsExperimentalSeaWarningNeeded);
registry->Register(GetAsset);
registry->Register(GetAssetKeys);
registry->Register(GetMainCode);
}

} // namespace sea
Expand Down
1 change: 1 addition & 0 deletions src/node_sea.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ struct SeaResource {

bool use_snapshot() const;
bool use_code_cache() const;
bool use_vfs_archive() const;

static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags) +
sizeof(SeaExecArgvExtension) +
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/sea/vfs-zip/sea-config-vfs-load-rejected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"main": "sea.js",
"output": "sea-vfs-load-rejected",
"useVfs": true,
"vfsArchive": "assets.zip",
"execArgvExtension": "cli"
}
31 changes: 30 additions & 1 deletion test/fixtures/sea/vfs-zip/sea.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const os = require('os');
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (!isMainThread) {
// A worker mounts the archive too, at the same mount point, so the main
// script and the code next to it can be loaded from there.
parentPort.postMessage({
dirname: __dirname,
greeting: fs.readFileSync(path.join(__dirname, 'data', 'greeting.txt'), 'utf8'),
sum: require('./modules/math.js').add(2, 3),
});
return;
}

// With "useVfsZip", the bundled assets are stored as a single ZIP archive
// and mounted through the ZipProvider. Everything is reachable through
Expand All @@ -13,6 +26,9 @@ assert.strictEqual(path.basename(__filename), 'sea.js');
assert.strictEqual(require.main, module);
console.log('main script runs from', __filename);

// The archive is mounted at the mount point reserved for --vfs-load.
assert.strictEqual(__dirname, path.join(os.devNull, 'vfs', '0'));

// The main script itself is readable through fs.
assert.strictEqual(fs.existsSync(__filename), true);

Expand Down Expand Up @@ -58,4 +74,17 @@ assert.notStrictEqual(a, b);
a[0] = 0;
assert.strictEqual(b.toString('utf8'), 'Hello from SEA VFS!');

console.log('All SEA VFS zip tests passed!');
// The mount point is taken by the archive, so a worker cannot --vfs-load.
assert.throws(() => new Worker(__filename, {
execArgv: ['--experimental-vfs', `--vfs-load=${__dirname}`],
}), { code: 'ERR_WORKER_INVALID_EXEC_ARGV' });

const worker = new Worker(__filename);
worker.once('message', (message) => {
assert.deepStrictEqual(message, {
dirname: __dirname,
greeting: 'Hello from SEA VFS!',
sum: 5,
});
console.log('All SEA VFS zip tests passed!');
});
21 changes: 21 additions & 0 deletions test/sea/test-build-sea-vfs-incompatible-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,24 @@ skipIfBuildSEAIsNotSupported();
stderr: /"vfsArchive" field of .*vfs-archive-not-string\.json is not a string/,
});
}

// Test: "vfsArchive" combined with --vfs-load in "execArgv"
for (const arg of ['--vfs-load=assets.zip', '--vfs-load']) {
tmpdir.refresh();
const config = tmpdir.resolve('vfs-archive-vfs-load.json');
writeFileSync(config, JSON.stringify({
main: 'bundle.js',
output: 'sea',
useVfs: true,
vfsArchive: 'assets.zip',
execArgv: ['--experimental-vfs', arg],
}), 'utf8');
spawnSyncAndAssert(
process.execPath,
['--build-sea', config], {
cwd: tmpdir.path,
}, {
status: 1,
stderr: /"vfsArchive" cannot be used together with --vfs-load in "execArgv"/,
});
}
Loading
Loading