Commit 5d59de845c1 for nodejs
commit 5d59de845c196112f03538b5077936f56de5bad6
Author: Matteo Collina <hello@matteocollina.com>
Date: Mon Nov 9 07:27:20 2026 +0100
sea: generate a compile cache for the VFS modules
"useCodeCache" was rejected together with "useVfs" because the SEA code
cache is only consumed by the embedder compile paths, while a main
script in the VFS runs through the regular loaders and a main-only cache
would not cover the modules it depends on anyway.
Compile every JavaScript (and, with --strip-types, TypeScript) module in
the VFS when the preparation blob is generated, without executing them,
through a portable CompileCacheHandler keyed by the path relative to the
mount point. The entries are serialized in the cache file layout and
embedded as reserved node:sea:compile-cache/<key> assets, which the SEA
provider hides from the mount. Modules that fail to compile are skipped.
CompileCacheHandler gains a key root for portable caches and in-memory
serialization of its entries, shared with Persist(). The build runs in a
Node.js instance whose environment ignores NODE_COMPILE_CACHE.
Reading the embedded entries at startup is left to a follow-up.
Tests skip up front when building a single executable application on
Windows without signtool, so the async VFS code cache test is skipped
cleanly instead of failing on an unmatched mustCall.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Assisted-by: pi
PR-URL: https://github.com/nodejs/node/pull/66768
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md
index 6f35f28d4b9..dcef69a7ee9 100644
--- a/doc/api/single-executable-applications.md
+++ b/doc/api/single-executable-applications.md
@@ -7,6 +7,9 @@ added:
- v19.7.0
- v18.16.0
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/66768
+ description: Allowed "useCodeCache" together with "useVfs".
- version: v25.5.0
pr-url: https://github.com/nodejs/node/pull/61167
description: Added built-in single executable application generation via the CLI flag `--build-sea`.
@@ -317,13 +320,25 @@ 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
+#### Code cache for the modules in the VFS
-`"useVfs": true` cannot be used together with `"useSnapshot": true` or
-`"useCodeCache": true`. The code cache limitation is due to incomplete
-implementation, not a technical impossibility. Consider bundling the
-application if startup performance matters and do not rely on module loading
-from the VFS in that case.
+When `"useCodeCache": true` is combined with `"useVfs": true`, the code cache
+is not limited to the main script: `--build-sea` compiles every JavaScript
+module in the virtual file system (and every TypeScript module, when type
+stripping is enabled), including the main script and the modules under
+`node_modules`, and embeds their V8 code cache into the preparation blob.
+The modules are compiled without being run, so the build has no side
+effects. A module that fails to compile is left out of the cache and fails
+the same way at run time if the application loads it.
+
+The cache is keyed by the path of each module inside the mount and verified
+against its source, so it is used by the executable whenever the modules are
+loaded from the VFS, and ignored if they changed. The cache is
+platform-specific, like the code cache of the main script.
+
+#### Snapshot limitations
+
+`"useVfs": true` cannot be used together with `"useSnapshot": true`.
#### Native addon limitations
@@ -373,6 +388,9 @@ executable application is launched, instead of compiling the `main` script from
scratch, Node.js would use the code cache to speed up the compilation, then
execute the script, which would improve the startup performance.
+When `useVfs` is also set to `true`, the code cache covers every module in the
+virtual file system, see [Code cache for the modules in the VFS][].
+
### Execution arguments
The `execArgv` field can be used to specify Node.js-specific
@@ -798,6 +816,7 @@ Suggestions for other resource injection tools/workflows are welcomed. Please
start a discussion at <https://github.com/nodejs/single-executable/discussions>
to help us document them.
+[Code cache for the modules in the VFS]: #code-cache-for-the-modules-in-the-vfs
[CommonJS]: modules.md#modules-commonjs-modules
[ECMAScript Modules]: esm.md#modules-ecmascript-modules
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
diff --git a/doc/api/vfs.md b/doc/api/vfs.md
index 726c12c33d9..b4677aa4cfc 100644
--- a/doc/api/vfs.md
+++ b/doc/api/vfs.md
@@ -574,8 +574,10 @@ ESM entry points (`"mainFormat": "module"`) are supported: the main module
is loaded from inside the mount through the ESM loader, and
`import.meta.dirname` points at the mount root.
-`"useVfs"` cannot be used together with `"useSnapshot"` or `"useCodeCache"`.
-The SEA configuration parser will error if either combination is detected.
+`"useVfs"` cannot be used together with `"useSnapshot"`; the SEA
+configuration parser will error if the combination is detected. With
+`"useCodeCache"`, every module in the mount is compiled at build time and its
+code cache is embedded into the executable.
Instead of listing individual `"assets"`, the SEA configuration can point
`"vfsArchive"` at a prebuilt ZIP archive; the mount is then backed by a
diff --git a/lib/internal/vfs/providers/sea.js b/lib/internal/vfs/providers/sea.js
index 4a03246c271..fa8df1b8f79 100644
--- a/lib/internal/vfs/providers/sea.js
+++ b/lib/internal/vfs/providers/sea.js
@@ -44,6 +44,11 @@ const {
},
} = internalBinding('constants');
+// Assets under this prefix are reserved by --build-sea (the compile cache of
+// the modules in the virtual file system) and are not files of the
+// application. Must match src/node_sea.cc.
+const kReservedAssetPrefix = 'node:sea:';
+
// Private symbols
const kAssets = Symbol('kAssets');
const kExtraFiles = Symbol('kExtraFiles');
@@ -91,6 +96,7 @@ class SEAProvider extends VirtualProvider {
const keys = getAssetKeys() || [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
+ if (StringPrototypeStartsWith(key, kReservedAssetPrefix)) continue;
this[kAssets].set(this.#normalizePath(key), key);
}
const extraFiles = options.extraFiles;
diff --git a/lib/internal/vfs/sea.js b/lib/internal/vfs/sea.js
index 4bf2273c8bb..c6aa1931453 100644
--- a/lib/internal/vfs/sea.js
+++ b/lib/internal/vfs/sea.js
@@ -83,11 +83,13 @@ function initSeaVfsInWorker() {
/**
* The name under which the SEA main script is placed at the mount point root.
+ * @param {string} [mainPath] The "main" of the SEA configuration; defaults
+ * to the one recorded in the running executable
* @returns {string}
*/
-function getSeaMainName() {
+function getSeaMainName(mainPath = mainCodePath || process.execPath) {
const { basename } = require('path');
- return basename(mainCodePath || process.execPath);
+ return basename(mainPath);
}
// The reserved asset key under which --build-sea stores the ZIP archive
diff --git a/lib/internal/vfs/sea_compile_cache.js b/lib/internal/vfs/sea_compile_cache.js
new file mode 100644
index 00000000000..1d77e54f285
--- /dev/null
+++ b/lib/internal/vfs/sea_compile_cache.js
@@ -0,0 +1,234 @@
+'use strict';
+
+// Build-time counterpart of the SEA virtual file system: run by --build-sea
+// (src/node_sea.cc) in a regular Node.js instance to compile every module of
+// the virtual file system and collect their compile cache, which the
+// executable then carries as assets. The assets are mounted the way the
+// executable mounts them at startup, so the modules are compiled from the
+// same paths and sources that the loaders compile them from in the
+// executable, and the cache is keyed and hashed exactly as the loaders look
+// it up there.
+
+const {
+ ArrayPrototypeIncludes,
+ StringPrototypeEndsWith,
+ StringPrototypeReplaceAll,
+} = primordials;
+
+const { Buffer } = require('buffer');
+const fs = require('fs');
+const path = require('path');
+const { pathToFileURL } = require('internal/url');
+const { getOptionValue } = require('internal/options');
+const {
+ enableCompileCache: enableNativeCompileCache,
+} = internalBinding('modules');
+const {
+ constants: { compileCacheStatus },
+ stringify,
+} = require('internal/modules/helpers');
+const {
+ codes: {
+ ERR_INVALID_STATE,
+ },
+} = require('internal/errors');
+
+let debug = require('internal/util/debuglog').debuglog('sea', (fn) => {
+ debug = fn;
+});
+
+// Only files the module loaders compile are cached. Everything else in the
+// mount is data.
+const kScriptExtensions = ['.js', '.cjs', '.mjs'];
+const kTypeScriptExtensions = ['.ts', '.cts', '.mts'];
+
+/**
+ * Mounts the assets of the SEA configuration the way the executable mounts
+ * them: the main script at the mount point root, at the layer reserved for
+ * --vfs-load when they come from an archive (see lib/internal/vfs/sea.js).
+ * @param {object} options The SEA configuration, see buildSeaCompileCache()
+ * @param {string} mainName The name of the main script at the mount root
+ * @returns {string} The mount point
+ */
+function mountAssets(options, mainName) {
+ const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system');
+ const isArchive = options.vfsArchive !== undefined;
+
+ let provider;
+ if (isArchive) {
+ const { ZipBuffer } = require('internal/zip');
+ const { ZipProvider } = require('internal/vfs/providers/ziparchive');
+ const zip = new ZipBuffer(fs.readFileSync(options.vfsArchive));
+ zip.addSync(mainName, Buffer.from(options.mainSource),
+ { __proto__: null, method: 'store' });
+ provider = new ZipProvider(zip);
+ } else {
+ const { MemoryProvider } = require('internal/vfs/providers/memory');
+ provider = new MemoryProvider();
+ }
+
+ const vfs = new VirtualFileSystem(provider, {
+ __proto__: null,
+ emitExperimentalWarning: false,
+ [kLoadLayer]: isArchive,
+ });
+
+ if (!isArchive) {
+ // Every asset is a file at its key, as the SEAProvider serves them.
+ const { assetKeys, assetPaths } = options;
+ for (let i = 0; i < assetKeys.length; i++) {
+ const target = path.posix.normalize(
+ '/' + StringPrototypeReplaceAll(assetKeys[i], '\\', '/'));
+ const dir = path.posix.dirname(target);
+ if (dir !== '/') {
+ vfs.mkdirSync(dir, { __proto__: null, recursive: true });
+ }
+ vfs.writeFileSync(target, fs.readFileSync(assetPaths[i]));
+ }
+ vfs.writeFileSync('/' + mainName, options.mainSource);
+ provider.setReadOnly();
+ }
+
+ return vfs.mount();
+}
+
+/**
+ * Compiles a module without running it, the way the loaders compile it when
+ * it is loaded from the mount, so that its compile cache entry is created.
+ * @param {string} filename The path of the module inside the mount
+ * @returns {boolean} Whether the file is a module the loaders compile
+ */
+function compileModule(filename) {
+ const { Module, wrapSafe } = require('internal/modules/cjs/loader');
+ const {
+ defaultGetFormatWithoutErrors,
+ } = require('internal/modules/esm/get_format');
+ const {
+ compileSourceTextModule,
+ SourceTextModuleTypes: { kUser },
+ } = require('internal/modules/esm/utils');
+
+ const url = pathToFileURL(filename);
+ const buffer = fs.readFileSync(filename);
+ let format = defaultGetFormatWithoutErrors(url, {
+ __proto__: null,
+ source: buffer,
+ });
+
+ let source;
+ switch (format) {
+ case 'module':
+ // The ESM loader decodes the source it loads.
+ source = stringify(buffer);
+ break;
+ case 'commonjs':
+ case 'commonjs-typescript':
+ case 'module-typescript':
+ case 'typescript':
+ // The CommonJS loader reads the source as a UTF-8 string.
+ source = buffer.toString('utf8');
+ break;
+ default:
+ return false;
+ }
+
+ if (StringPrototypeEndsWith(format, 'typescript')) {
+ const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
+ source = stripTypeScriptModuleTypes(source, filename, url.href);
+ switch (format) {
+ case 'commonjs-typescript':
+ format = 'commonjs';
+ break;
+ case 'module-typescript':
+ format = 'module';
+ break;
+ default:
+ // Detected from the type-stripped source by wrapSafe(), like
+ // Module.prototype._compile() does.
+ format = undefined;
+ break;
+ }
+ }
+
+ if (format !== 'module') {
+ const result = wrapSafe(filename, source, new Module(filename, null), format);
+ if (!result.canParseAsESM) {
+ return true;
+ }
+ }
+ compileSourceTextModule(url.href, source, kUser);
+ return true;
+}
+
+/**
+ * Compiles every module under a directory of the mount.
+ * @param {string} dir The directory
+ * @param {boolean} stripTypes Whether TypeScript files are modules too
+ * @returns {number} The number of modules compiled
+ */
+function compileTree(dir, stripTypes) {
+ let compiled = 0;
+ const dirents = fs.readdirSync(dir, { __proto__: null, withFileTypes: true });
+ for (let i = 0; i < dirents.length; i++) {
+ const dirent = dirents[i];
+ const filename = path.join(dir, dirent.name);
+ if (dirent.isDirectory()) {
+ compiled += compileTree(filename, stripTypes);
+ continue;
+ }
+ if (!dirent.isFile()) continue;
+ const ext = path.extname(filename);
+ if (!ArrayPrototypeIncludes(kScriptExtensions, ext) &&
+ !(stripTypes && ArrayPrototypeIncludes(kTypeScriptExtensions, ext))) {
+ continue;
+ }
+ // A module that does not compile is left out of the cache: it fails the
+ // same way at run time if the application ever loads it.
+ try {
+ if (compileModule(filename)) compiled++;
+ } catch (err) {
+ debug('not caching %s: %s', filename, err?.message);
+ }
+ }
+ return compiled;
+}
+
+/**
+ * Compiles the modules of the SEA virtual file system into the compile cache
+ * of this Node.js instance, from which --build-sea collects them. The cache
+ * is keyed relative to the mount point, which is the same in the executable.
+ * @param {object} options
+ * @param {string} options.main The "main" of the SEA configuration
+ * @param {string} options.mainSource The source of the main script
+ * @param {string} [options.vfsArchive] The "vfsArchive" of the configuration
+ * @param {string[]} [options.assetKeys] The keys of the "assets"
+ * @param {string[]} [options.assetPaths] The paths of the "assets"
+ * @returns {number} The number of modules compiled
+ */
+function buildSeaCompileCache(options) {
+ const { getSeaMainName } = require('internal/vfs/sea');
+ const mountPoint = mountAssets(options, getSeaMainName(options.main));
+
+ // Enabling the compile cache takes a directory, but nothing is persisted
+ // to it: the entries are collected from memory by --build-sea.
+ const cacheDir = fs.mkdtempSync(
+ path.join(require('os').tmpdir(), 'node-sea-compile-cache-'));
+ try {
+ const { 0: status, 1: message } = enableNativeCompileCache(
+ cacheDir, true /* portable */, false /* readOnly */, mountPoint);
+ if (status !== compileCacheStatus.ENABLED) {
+ throw new ERR_INVALID_STATE(
+ `cannot enable the compile cache for ${mountPoint}: ${message}`);
+ }
+ const compiled = compileTree(mountPoint, getOptionValue('--strip-types'));
+ debug('compiled %d modules in %s for the compile cache',
+ compiled, mountPoint);
+ return compiled;
+ } finally {
+ fs.rmSync(cacheDir, { __proto__: null, recursive: true, force: true });
+ }
+}
+
+module.exports = {
+ buildSeaCompileCache,
+};
diff --git a/src/compile_cache.cc b/src/compile_cache.cc
index bab24256e06..90ed63f448a 100644
--- a/src/compile_cache.cc
+++ b/src/compile_cache.cc
@@ -282,12 +282,12 @@ CompileCacheEntry* CompileCacheHandler::GetOrInsert(const uint8_t* code,
return nullptr;
}
std::string relative_path =
- GetRelativePath(normalized_file_path, normalized_compile_cache_dir_);
+ GetRelativePath(normalized_file_path, normalized_key_root_);
if (!relative_path.empty()) {
file_path = relative_path;
Debug("[compile cache] using relative path %s from %s\n",
file_path,
- compile_cache_dir_);
+ normalized_key_root_);
}
}
uint32_t key = GetCacheKey(file_path, type);
@@ -473,20 +473,8 @@ void CompileCacheHandler::Persist() {
continue;
}
- DCHECK_EQ(entry->cache->buffer_policy,
- ScriptCompiler::CachedData::BufferOwned);
- char* cache_ptr =
- reinterpret_cast<char*>(const_cast<uint8_t*>(entry->cache->data));
- uint32_t cache_size = static_cast<uint32_t>(entry->cache->length);
- uint32_t cache_hash = GetHash(cache_ptr, cache_size);
-
- // Generating headers.
- std::vector<uint32_t> headers(kHeaderCount);
- headers[kMagicNumberOffset] = kCacheMagicNumber;
- headers[kCodeSizeOffset] = entry->code_size;
- headers[kCacheSizeOffset] = cache_size;
- headers[kCodeHashOffset] = entry->code_hash;
- headers[kCacheHashOffset] = cache_hash;
+ std::vector<char> data = SerializeEntry(entry);
+ const uint32_t* headers = reinterpret_cast<const uint32_t*>(data.data());
// Generate the temporary filename.
// The temporary file should be placed in a location like:
@@ -527,16 +515,13 @@ void CompileCacheHandler::Persist() {
headers[kCacheHashOffset]);
// Write to the temporary file.
- uv_buf_t headers_buf = uv_buf_init(reinterpret_cast<char*>(headers.data()),
- headers.size() * sizeof(uint32_t));
- uv_buf_t data_buf = uv_buf_init(cache_ptr, entry->cache->length);
- uv_buf_t bufs[] = {headers_buf, data_buf};
+ uv_buf_t buf = uv_buf_init(data.data(), data.size());
uv_fs_t write_req;
auto cleanup_write =
OnScopeLeave([&write_req]() { uv_fs_req_cleanup(&write_req); });
err = uv_fs_write(
- nullptr, &write_req, mkstemp_req.result, bufs, 2, 0, nullptr);
+ nullptr, &write_req, mkstemp_req.result, &buf, 1, 0, nullptr);
if (err < 0) {
Debug("failed: %s\n", uv_strerror(err));
continue;
@@ -581,6 +566,47 @@ void CompileCacheHandler::Persist() {
compiler_cache_store_.clear();
}
+// Serializes an entry in the layout of a cache file, see Persist().
+std::vector<char> CompileCacheHandler::SerializeEntry(
+ const CompileCacheEntry* entry) {
+ DCHECK_NOT_NULL(entry->cache);
+ DCHECK_EQ(entry->cache->buffer_policy,
+ ScriptCompiler::CachedData::BufferOwned);
+ const char* cache_ptr = reinterpret_cast<const char*>(entry->cache->data);
+ uint32_t cache_size = static_cast<uint32_t>(entry->cache->length);
+
+ uint32_t headers[kHeaderCount];
+ headers[kMagicNumberOffset] = kCacheMagicNumber;
+ headers[kCodeSizeOffset] = entry->code_size;
+ headers[kCacheSizeOffset] = cache_size;
+ headers[kCodeHashOffset] = entry->code_hash;
+ headers[kCacheHashOffset] = GetHash(cache_ptr, cache_size);
+
+ const char* headers_ptr = reinterpret_cast<const char*>(headers);
+ std::vector<char> data;
+ data.reserve(sizeof(headers) + cache_size);
+ data.insert(data.end(), headers_ptr, headers_ptr + sizeof(headers));
+ data.insert(data.end(), cache_ptr, cache_ptr + cache_size);
+ return data;
+}
+
+std::vector<std::pair<std::string, std::vector<char>>>
+CompileCacheHandler::SerializeEntries() const {
+ std::vector<std::pair<std::string, std::vector<char>>> result;
+ for (const auto& pair : compiler_cache_store_) {
+ const CompileCacheEntry* entry = pair.second.get();
+ if (entry->cache == nullptr) {
+ Debug("[compile cache] skip serializing %s %s because the cache was "
+ "not initialized\n",
+ entry->type_name(),
+ entry->source_filename);
+ continue;
+ }
+ result.emplace_back(Uint32ToHex(entry->cache_key), SerializeEntry(entry));
+ }
+ return result;
+}
+
CompileCacheHandler::CompileCacheHandler(Environment* env)
: isolate_(env->isolate()),
is_debug_(
@@ -591,9 +617,11 @@ CompileCacheHandler::CompileCacheHandler(Environment* env)
// - $NODE_VERSION-$ARCH-$CACHE_DATA_VERSION_TAG-$UID
// ($UID is omitted for a portable cache)
// - $FILENAME_AND_MODULE_TYPE_HASH.cache: a hash of filename + module type
-CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
- const std::string& dir,
- EnableOption option) {
+CompileCacheEnableResult CompileCacheHandler::Enable(
+ Environment* env,
+ const std::string& dir,
+ EnableOption option,
+ const std::string& key_root) {
std::string cache_tag = GetCacheVersionTag(option);
std::string absolute_cache_dir_base = PathResolve(env, {dir});
std::string cache_dir_with_tag =
@@ -665,8 +693,8 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
portable_ = option;
read_only_ = read_only;
if (HasOption(option, EnableOption::PORTABLE)) {
- normalized_compile_cache_dir_ =
- NormalizeFileURLOrPath(env, compile_cache_dir_);
+ normalized_key_root_ = NormalizeFileURLOrPath(
+ env, key_root.empty() ? compile_cache_dir_ : key_root);
}
result.status = CompileCacheEnableStatus::ENABLED;
return result;
diff --git a/src/compile_cache.h b/src/compile_cache.h
index 527f72f33d4..ec405cf55b8 100644
--- a/src/compile_cache.h
+++ b/src/compile_cache.h
@@ -8,6 +8,8 @@
#include <string>
#include <string_view>
#include <unordered_map>
+#include <utility>
+#include <vector>
#include "v8.h"
namespace node {
@@ -81,11 +83,18 @@ inline constexpr bool HasOption(EnableOption value, EnableOption flag) {
class CompileCacheHandler {
public:
explicit CompileCacheHandler(Environment* env);
+ // With PORTABLE, the cache keys are computed from paths relative to
+ // key_root, or to the cache directory when key_root is empty.
CompileCacheEnableResult Enable(Environment* env,
const std::string& dir,
- EnableOption option = EnableOption::DEFAULT);
+ EnableOption option = EnableOption::DEFAULT,
+ const std::string& key_root = std::string());
void Persist();
+ // Every entry holding a cache in the layout of a cache file, paired with
+ // the name of the file Persist() would write it to.
+ std::vector<std::pair<std::string, std::vector<char>>> SerializeEntries()
+ const;
CompileCacheEntry* GetOrInsert(v8::Local<v8::String> code,
v8::Local<v8::String> filename,
@@ -109,6 +118,7 @@ class CompileCacheHandler {
private:
void ReadCacheFile(CompileCacheEntry* entry);
+ static std::vector<char> SerializeEntry(const CompileCacheEntry* entry);
template <typename T>
void MaybeSaveImpl(CompileCacheEntry* entry,
@@ -129,7 +139,7 @@ class CompileCacheHandler {
bool is_debug_ = false;
std::string compile_cache_dir_;
- std::string normalized_compile_cache_dir_;
+ std::string normalized_key_root_;
EnableOption portable_ = EnableOption::DEFAULT;
bool read_only_ = false;
std::unordered_map<uint32_t, std::unique_ptr<CompileCacheEntry>>
diff --git a/src/env.cc b/src/env.cc
index c30b6e2f504..f14500da9d6 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -1393,7 +1393,9 @@ void Environment::InitializeCompileCache() {
}
CompileCacheEnableResult Environment::EnableCompileCache(
- const std::string& cache_dir, EnableOption option) {
+ const std::string& cache_dir,
+ EnableOption option,
+ const std::string& key_root) {
CompileCacheEnableResult result;
std::string disable_env;
if (credentials::SafeGetenv(
@@ -1410,7 +1412,7 @@ CompileCacheEnableResult Environment::EnableCompileCache(
if (!compile_cache_handler_) {
std::unique_ptr<CompileCacheHandler> handler =
std::make_unique<CompileCacheHandler>(this);
- result = handler->Enable(this, cache_dir, option);
+ result = handler->Enable(this, cache_dir, option, key_root);
if (result.status == CompileCacheEnableStatus::ENABLED) {
compile_cache_handler_ = std::move(handler);
AtExit(
diff --git a/src/env.h b/src/env.h
index 96b0cf52fb2..45867eab08e 100644
--- a/src/env.h
+++ b/src/env.h
@@ -1170,8 +1170,10 @@ class Environment final : public MemoryRetainer {
void InitializeCompileCache();
// Enable built-in compile cache if it has not yet been enabled.
// The cache will be persisted to disk on exit.
- CompileCacheEnableResult EnableCompileCache(const std::string& cache_dir,
- EnableOption option);
+ CompileCacheEnableResult EnableCompileCache(
+ const std::string& cache_dir,
+ EnableOption option,
+ const std::string& key_root = std::string());
void FlushCompileCache();
void RunAndClearNativeImmediates(bool only_refed = false);
diff --git a/src/node_modules.cc b/src/node_modules.cc
index 414e3a780f7..ee9be4557ac 100644
--- a/src/node_modules.cc
+++ b/src/node_modules.cc
@@ -685,9 +685,16 @@ void EnableCompileCache(const FunctionCallbackInfo<Value>& args) {
if (args.Length() > 2 && args[2]->IsTrue()) {
option = option | EnableOption::READ_ONLY;
}
+ // Internal: the directory the keys of a portable cache are relative to.
+ std::string key_root;
+ if (args.Length() > 3 && args[3]->IsString()) {
+ Utf8Value key_root_value(isolate, args[3]);
+ key_root = key_root_value.ToString();
+ }
Utf8Value value(isolate, args[0]);
- CompileCacheEnableResult result = env->EnableCompileCache(*value, option);
+ CompileCacheEnableResult result =
+ env->EnableCompileCache(*value, option, key_root);
Local<Value> values[3];
values[0] = v8::Integer::New(isolate, static_cast<uint8_t>(result.status));
if (ToV8Value(context, result.message).ToLocal(&values[1]) &&
diff --git a/src/node_sea.cc b/src/node_sea.cc
index 975df52d6ef..b25d0fcb93c 100644
--- a/src/node_sea.cc
+++ b/src/node_sea.cc
@@ -8,6 +8,7 @@
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_options.h"
+#include "node_realm-inl.h"
#include "node_snapshot_builder.h"
#include "node_union_bytes.h"
#include "node_v8_platform-inl.h"
@@ -47,6 +48,12 @@ namespace sea {
// The reserved asset key under which the ZIP archive named by "vfsArchive"
// is stored. Must match the name used by lib/internal/vfs/sea.js.
constexpr std::string_view kVfsArchiveAssetName = "node:sea:vfs.zip";
+// The prefix of the reserved asset keys holding the compile cache of the
+// modules in the virtual file system, one asset per module named by its
+// compile cache key. Must match the prefix skipped by
+// lib/internal/vfs/providers/sea.js.
+constexpr std::string_view kVfsCompileCacheAssetPrefix =
+ "node:sea:compile-cache/";
namespace {
@@ -613,11 +620,6 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
"\"useVfs\" is not supported when \"useSnapshot\" is true\n");
return std::nullopt;
}
- if (static_cast<bool>(result.flags & SeaFlags::kUseCodeCache)) {
- FPrintF(stderr,
- "\"useVfs\" is not supported when \"useCodeCache\" is true\n");
- return std::nullopt;
- }
}
if (static_cast<bool>(result.flags & SeaFlags::kVfsArchive)) {
@@ -792,6 +794,127 @@ std::optional<std::string> GenerateCodeCache(std::string_view main_path,
return code_cache;
}
+// Compiles every module in the virtual file system of the single executable
+// application and collects their compile cache, to be embedded as the assets
+// under kVfsCompileCacheAssetPrefix. The modules are compiled by a Node.js
+// instance that mounts the assets the way the executable does at startup
+// (see lib/internal/vfs/sea_compile_cache.js), so the cache is keyed and
+// hashed exactly as the module loaders look it up from the mount.
+ExitCode GenerateVfsCompileCache(
+ const SeaConfig& config,
+ const std::vector<std::string>& args,
+ const std::vector<std::string>& exec_args,
+ std::string_view main_script,
+ std::unordered_map<std::string, std::string>* assets) {
+ std::vector<std::string> errors;
+ std::vector<std::string> env_args = {args[0]};
+ std::unique_ptr<CommonEnvironmentSetup> setup =
+ CommonEnvironmentSetup::Create(
+ per_process::v8_platform.Platform(), &errors, env_args, exec_args);
+ if (!setup) {
+ for (const std::string& err : errors) {
+ FPrintF(stderr, "%s: %s\n", args[0], err);
+ }
+ return ExitCode::kBootstrapFailure;
+ }
+
+ Isolate* isolate = setup->isolate();
+ v8::Locker locker(isolate);
+ Isolate::Scope isolate_scope(isolate);
+ HandleScope handle_scope(isolate);
+ Local<Context> context = setup->context();
+ Context::Scope context_scope(context);
+ Environment* env = setup->env();
+
+ // The cache is collected from this instance, which must neither read nor
+ // write the compile cache the build process itself may be configured with.
+ env->set_env_vars(env->env_vars()->Clone(isolate));
+ for (const char* name : {"NODE_COMPILE_CACHE",
+ "NODE_COMPILE_CACHE_PORTABLE",
+ "NODE_COMPILE_CACHE_READONLY",
+ "NODE_DISABLE_COMPILE_CACHE"}) {
+ env->env_vars()->Delete(isolate, OneByteString(isolate, name));
+ }
+
+ errors::PrinterTryCatch bootstrapCatch(
+ isolate, errors::PrinterTryCatch::kPrintSourceLine);
+
+ Local<Object> options = Object::New(isolate);
+ auto set_option = [&](const char* key, auto&& value) {
+ Local<Value> v8_value;
+ return ToV8Value(context, value).ToLocal(&v8_value) &&
+ options->Set(context, OneByteString(isolate, key), v8_value)
+ .IsJust();
+ };
+ if (!set_option("main", config.main_path) ||
+ !set_option("mainSource", main_script)) {
+ return ExitCode::kGenericUserError;
+ }
+ if (static_cast<bool>(config.flags & SeaFlags::kVfsArchive)) {
+ if (!set_option("vfsArchive", config.vfs_archive_path)) {
+ return ExitCode::kGenericUserError;
+ }
+ } else {
+ std::vector<std::string> asset_keys;
+ std::vector<std::string> asset_paths;
+ for (auto const& [key, path] : config.assets) {
+ asset_keys.push_back(key);
+ asset_paths.push_back(path);
+ }
+ if (!set_option("assetKeys", asset_keys) ||
+ !set_option("assetPaths", asset_paths)) {
+ return ExitCode::kGenericUserError;
+ }
+ }
+
+ Local<Value> compiled;
+ bool loaded =
+ LoadEnvironment(
+ env,
+ [&](const StartExecutionCallbackInfo& info) -> MaybeLocal<Value> {
+ Local<Value> id = FIXED_ONE_BYTE_STRING(
+ isolate, "internal/vfs/sea_compile_cache");
+ Local<Value> builder;
+ if (!env->principal_realm()
+ ->builtin_module_require()
+ ->Call(context, Undefined(isolate), 1, &id)
+ .ToLocal(&builder)) {
+ return MaybeLocal<Value>();
+ }
+ Local<Value> fn;
+ if (!builder.As<Object>()
+ ->Get(
+ context,
+ FIXED_ONE_BYTE_STRING(isolate, "buildSeaCompileCache"))
+ .ToLocal(&fn)) {
+ return MaybeLocal<Value>();
+ }
+ Local<Value> argv[] = {options};
+ return fn.As<Function>()->Call(
+ context, Undefined(isolate), arraysize(argv), argv);
+ })
+ .ToLocal(&compiled);
+ if (!loaded) {
+ FPrintF(stderr,
+ "Cannot generate the compile cache of the virtual file system\n");
+ return ExitCode::kGenericUserError;
+ }
+
+ CHECK(env->use_compile_cache());
+ size_t entries = 0;
+ for (auto& [name, data] : env->compile_cache_handler()->SerializeEntries()) {
+ assets->emplace(std::string(kVfsCompileCacheAssetPrefix) + name,
+ std::string(data.begin(), data.end()));
+ entries++;
+ }
+ per_process::Debug(DebugCategory::SEA,
+ "Generated %zu compile cache entries for %d modules in "
+ "the virtual file system\n",
+ entries,
+ compiled.As<v8::Int32>()->Value());
+ return ExitCode::kNoFailure;
+}
+
int BuildAssets(const std::unordered_map<std::string, std::string>& config,
std::unordered_map<std::string, std::string>* assets) {
for (auto const& [key, path] : config) {
@@ -875,6 +998,17 @@ ExitCode GenerateSingleExecutableBlob(
}
assets.emplace(std::string(kVfsArchiveAssetName), std::move(archive));
}
+ SeaFlags flags = config.flags;
+ if (static_cast<bool>(flags & SeaFlags::kEnableVfs) &&
+ static_cast<bool>(flags & SeaFlags::kUseCodeCache)) {
+ ExitCode exit_code =
+ GenerateVfsCompileCache(config, args, exec_args, main_script, &assets);
+ if (exit_code != ExitCode::kNoFailure) {
+ return exit_code;
+ }
+ // The cache is embedded as assets even when the configuration has none.
+ flags |= SeaFlags::kIncludeAssets;
+ }
std::unordered_map<std::string_view, std::string_view> assets_view;
for (auto const& [key, content] : assets) {
assets_view.emplace(key, content);
@@ -884,7 +1018,7 @@ ExitCode GenerateSingleExecutableBlob(
exec_argv_view.emplace_back(arg);
}
SeaResource sea{
- config.flags,
+ flags,
config.exec_argv_extension,
config.main_path,
builds_snapshot_from_main
diff --git a/test/common/sea.js b/test/common/sea.js
index 13a9a4a615c..c9a5ea93c5b 100644
--- a/test/common/sea.js
+++ b/test/common/sea.js
@@ -240,8 +240,23 @@ function signSEA(targetExecutable, verifyWorkflow = false) {
}
}
+// On Windows, building a single executable application requires signtool to
+// be available so the executable can be signed. Check for it up front so that
+// tests that carry out asynchronous work (and therefore register a mustCall
+// before this point) are skipped cleanly instead of failing on the pending
+// mustCall when signSEA() discovers signtool is missing mid-flight.
+function skipIfSigningIsNotSupported() {
+ if (process.platform !== 'win32') return;
+ try {
+ spawnSyncAndExitWithoutError('where', [ 'signtool' ]);
+ } catch (e) {
+ common.skip(`Cannot find signtool: ${inspect(e)}`);
+ }
+}
+
module.exports = {
skipIfBuildSEAIsNotSupported,
+ skipIfSigningIsNotSupported,
skipIfSingleExecutableIsNotSupported,
generateSEA,
signSEA,
diff --git a/test/fixtures/sea/vfs-esm/sea-config-code-cache.json b/test/fixtures/sea/vfs-esm/sea-config-code-cache.json
new file mode 100644
index 00000000000..26bd6b0f6f4
--- /dev/null
+++ b/test/fixtures/sea/vfs-esm/sea-config-code-cache.json
@@ -0,0 +1,15 @@
+{
+ "main": "main.mjs",
+ "mainFormat": "module",
+ "output": "sea-prep.blob",
+ "useVfs": true,
+ "useCodeCache": true,
+ "assets": {
+ "config.json": "config.json",
+ "data/greeting.txt": "greeting.txt",
+ "modules/math.mjs": "math.mjs",
+ "modules/calculator.cjs": "calculator.cjs",
+ "node_modules/test-esm-pkg/package.json": "test-esm-pkg-package.json",
+ "node_modules/test-esm-pkg/index.mjs": "test-esm-pkg-index.mjs"
+ }
+}
diff --git a/test/fixtures/sea/vfs-zip/sea-config-code-cache.json b/test/fixtures/sea/vfs-zip/sea-config-code-cache.json
new file mode 100644
index 00000000000..3a8791cd4ae
--- /dev/null
+++ b/test/fixtures/sea/vfs-zip/sea-config-code-cache.json
@@ -0,0 +1,7 @@
+{
+ "main": "sea.js",
+ "output": "sea-prep.blob",
+ "useVfs": true,
+ "useCodeCache": true,
+ "vfsArchive": "assets.zip"
+}
diff --git a/test/fixtures/sea/vfs/sea-config-code-cache.json b/test/fixtures/sea/vfs/sea-config-code-cache.json
new file mode 100644
index 00000000000..7627849eb9d
--- /dev/null
+++ b/test/fixtures/sea/vfs/sea-config-code-cache.json
@@ -0,0 +1,16 @@
+{
+ "main": "sea.js",
+ "output": "sea-prep.blob",
+ "useVfs": true,
+ "useCodeCache": true,
+ "assets": {
+ "config.json": "config.json",
+ "data/greeting.txt": "greeting.txt",
+ "modules/math.js": "math.js",
+ "modules/calculator.js": "calculator.js",
+ "node_modules/test-pkg/package.json": "test-pkg-package.json",
+ "node_modules/test-pkg/index.js": "test-pkg-index.js",
+ "node_modules/test-exports-pkg/package.json": "test-exports-pkg-package.json",
+ "node_modules/test-exports-pkg/lib/entry.js": "test-exports-pkg-entry.js"
+ }
+}
diff --git a/test/sea/test-build-sea-vfs-code-cache-errors.js b/test/sea/test-build-sea-vfs-code-cache-errors.js
new file mode 100644
index 00000000000..b672d16587e
--- /dev/null
+++ b/test/sea/test-build-sea-vfs-code-cache-errors.js
@@ -0,0 +1,227 @@
+'use strict';
+
+// This tests the error conditions of generating the compile cache of the
+// virtual file system when "useCodeCache" is combined with "useVfs": a module
+// that does not compile is left out of the cache, while a broken archive, a
+// broken main script or a build environment without a usable temporary
+// directory fail the build.
+
+const common = require('../common');
+
+const {
+ buildSEA,
+ skipIfBuildSEAIsNotSupported,
+} = require('../common/sea');
+
+skipIfBuildSEAIsNotSupported();
+
+const assert = require('assert');
+const fs = require('fs');
+const tmpdir = require('../common/tmpdir');
+const fixtures = require('../common/fixtures');
+const { spawnSyncAndAssert } = require('../common/child_process');
+
+const kCompileCachePrefix = 'node:sea:compile-cache/';
+const kBlob = 'sea-prep.blob';
+
+const hasTypeScript = process.features.typescript === 'strip';
+
+function countCompileCacheEntries(blobPath) {
+ const blob = fs.readFileSync(blobPath).toString('latin1');
+ return blob.split(kCompileCachePrefix).length - 1;
+}
+
+function writeConfig(name, config) {
+ fs.writeFileSync(tmpdir.resolve(name), JSON.stringify(config));
+}
+
+// Generates the preparation blob of the configuration in the working
+// directory and returns the child process result.
+function generateBlob(configName, env, expectations) {
+ return spawnSyncAndAssert(
+ process.execPath,
+ ['--experimental-sea-config', configName],
+ {
+ cwd: tmpdir.path,
+ env: {
+ ...process.env,
+ NODE_DEBUG: 'sea',
+ NODE_DEBUG_NATIVE: 'SEA',
+ ...env,
+ },
+ },
+ expectations);
+}
+
+// Test: a module that does not compile is skipped, the rest is cached, and
+// the executable runs with the broken module failing when it is loaded.
+{
+ tmpdir.refresh();
+ fs.writeFileSync(tmpdir.resolve('main.js'), `
+ const path = require('path');
+ try {
+ require(path.join(__dirname, 'modules', 'broken.js'));
+ } catch (err) {
+ console.log('broken:', err.name);
+ }
+ console.log('good:', require(path.join(__dirname, 'modules', 'good.js')));
+ if (process.features.typescript === 'strip') {
+ console.log('ts:', require(path.join(__dirname, 'modules', 'good.ts')));
+ }
+ `);
+ fs.writeFileSync(tmpdir.resolve('good.js'), 'module.exports = 42;\n');
+ fs.writeFileSync(tmpdir.resolve('broken.js'), 'module.exports = {\n');
+ const assets = {
+ 'modules/good.js': 'good.js',
+ 'modules/broken.js': 'broken.js',
+ };
+ // main.js, good.js and, with type stripping, good.ts, which has two
+ // entries: the stripped source and the code cache compiled from it.
+ let modules = 2;
+ let entries = 2;
+ if (hasTypeScript) {
+ fs.writeFileSync(tmpdir.resolve('good.ts'),
+ 'const n: number = 42;\nmodule.exports = n;\n');
+ // Enums are not supported by type stripping.
+ fs.writeFileSync(tmpdir.resolve('enum.ts'), 'enum E { A }\n');
+ assets['modules/good.ts'] = 'good.ts';
+ assets['modules/enum.ts'] = 'enum.ts';
+ modules++;
+ entries += 2;
+ }
+ const config = {
+ main: 'main.js',
+ output: kBlob,
+ useVfs: true,
+ useCodeCache: true,
+ assets,
+ };
+ writeConfig('sea-config-blob.json', config);
+ writeConfig('sea-config.json', { ...config, output: 'sea' });
+
+ generateBlob('sea-config-blob.json', {}, {
+ stderr: common.mustCall((stderr) => {
+ assert.match(stderr, /not caching \S*broken\.js: /);
+ if (hasTypeScript) {
+ assert.match(stderr, /not caching \S*enum\.ts: /);
+ }
+ assert.match(stderr, new RegExp(
+ `Generated ${entries} compile cache entries for ${modules} modules`));
+ }),
+ });
+ assert.strictEqual(countCompileCacheEntries(tmpdir.resolve(kBlob)), entries);
+
+ const outputFile = buildSEA(tmpdir.path);
+ spawnSyncAndAssert(
+ outputFile,
+ {
+ env: {
+ ...process.env,
+ NODE_DEBUG: undefined,
+ NODE_DEBUG_NATIVE: undefined,
+ },
+ },
+ {
+ stdout: common.mustCall((stdout) => {
+ assert.match(stdout, /^broken: SyntaxError$/m);
+ assert.match(stdout, /^good: 42$/m);
+ if (hasTypeScript) {
+ assert.match(stdout, /^ts: 42$/m);
+ }
+ }),
+ stderr: common.mustCall((stderr) => {
+ assert.doesNotMatch(stderr, /Code cache data rejected/);
+ }),
+ });
+}
+
+// Test: an ES module that does not compile is skipped too.
+{
+ tmpdir.refresh();
+ fs.cpSync(fixtures.path('sea', 'vfs-esm'), tmpdir.path, { recursive: true });
+ fs.writeFileSync(tmpdir.resolve('broken.mjs'), 'export {\n');
+ const config = JSON.parse(
+ fs.readFileSync(tmpdir.resolve('sea-config-code-cache.json'), 'utf8'));
+ config.assets['modules/broken.mjs'] = 'broken.mjs';
+ writeConfig('sea-config-code-cache.json', config);
+
+ generateBlob('sea-config-code-cache.json', {}, {
+ stderr: common.mustCall((stderr) => {
+ assert.match(stderr, /not caching \S*broken\.mjs: /);
+ assert.match(stderr, /Generated 4 compile cache entries for 4 modules/);
+ }),
+ });
+ assert.strictEqual(countCompileCacheEntries(tmpdir.resolve(kBlob)), 4);
+}
+
+// Test: a "vfsArchive" that passes the signature check but cannot be parsed
+// fails the build when the compile cache is generated from it.
+{
+ tmpdir.refresh();
+ fs.writeFileSync(tmpdir.resolve('main.js'), 'console.log("hello");\n');
+ fs.writeFileSync(tmpdir.resolve('assets.zip'),
+ Buffer.concat([Buffer.from('PK\x03\x04'), Buffer.alloc(64)]));
+ writeConfig('sea-config.json', {
+ main: 'main.js',
+ output: kBlob,
+ useVfs: true,
+ useCodeCache: true,
+ vfsArchive: 'assets.zip',
+ });
+
+ generateBlob('sea-config.json', {}, {
+ status: 1,
+ stderr: common.mustCall((stderr) => {
+ assert.match(stderr, /ERR_ZIP_INVALID_ARCHIVE/);
+ assert.match(
+ stderr, /Cannot generate the compile cache of the virtual file system/);
+ }),
+ });
+ assert.strictEqual(fs.existsSync(tmpdir.resolve(kBlob)), false);
+}
+
+// Test: a main script that does not compile fails the build.
+{
+ tmpdir.refresh();
+ fs.writeFileSync(tmpdir.resolve('main.js'), 'console.log(\n');
+ writeConfig('sea-config.json', {
+ main: 'main.js',
+ output: kBlob,
+ useVfs: true,
+ useCodeCache: true,
+ });
+
+ generateBlob('sea-config.json', {}, {
+ status: 1,
+ stderr: /Cannot generate V8 code cache/,
+ });
+ assert.strictEqual(fs.existsSync(tmpdir.resolve(kBlob)), false);
+}
+
+// Test: the compile cache is collected through a temporary directory, so the
+// build fails without a usable one.
+{
+ tmpdir.refresh();
+ fs.writeFileSync(tmpdir.resolve('main.js'), 'console.log("hello");\n');
+ writeConfig('sea-config.json', {
+ main: 'main.js',
+ output: kBlob,
+ useVfs: true,
+ useCodeCache: true,
+ });
+
+ const missing = tmpdir.resolve('missing');
+ generateBlob('sea-config.json', {
+ TMPDIR: missing,
+ TMP: missing,
+ TEMP: missing,
+ }, {
+ status: 1,
+ stderr: common.mustCall((stderr) => {
+ assert.match(stderr, /ENOENT/);
+ assert.match(
+ stderr, /Cannot generate the compile cache of the virtual file system/);
+ }),
+ });
+ assert.strictEqual(fs.existsSync(tmpdir.resolve(kBlob)), false);
+}
diff --git a/test/sea/test-build-sea-vfs-incompatible-options.js b/test/sea/test-build-sea-vfs-incompatible-options.js
index bd51ee12c16..e2ef11a0145 100644
--- a/test/sea/test-build-sea-vfs-incompatible-options.js
+++ b/test/sea/test-build-sea-vfs-incompatible-options.js
@@ -54,28 +54,6 @@ skipIfBuildSEAIsNotSupported();
});
}
-// Test: "useVfs" with "useCodeCache"
-{
- tmpdir.refresh();
- const config = tmpdir.resolve('vfs-code-cache.json');
- writeFileSync(config, `
-{
- "main": "bundle.js",
- "output": "sea",
- "useVfs": true,
- "useCodeCache": true
-}
- `, 'utf8');
- spawnSyncAndAssert(
- process.execPath,
- ['--build-sea', config], {
- cwd: tmpdir.path,
- }, {
- status: 1,
- stderr: /"useVfs" is not supported when "useCodeCache" is true/,
- });
-}
-
// Test: "vfsArchive" without "useVfs"
{
tmpdir.refresh();
diff --git a/test/sea/test-single-executable-application-vfs-code-cache.js b/test/sea/test-single-executable-application-vfs-code-cache.js
new file mode 100644
index 00000000000..a1666473ce8
--- /dev/null
+++ b/test/sea/test-single-executable-application-vfs-code-cache.js
@@ -0,0 +1,133 @@
+'use strict';
+
+// This tests that "useCodeCache" combined with "useVfs" embeds a compile
+// cache entry for every module in the virtual file system into the
+// preparation blob, and that the resulting executable runs.
+
+const common = require('../common');
+
+const {
+ buildSEA,
+ skipIfBuildSEAIsNotSupported,
+ skipIfSigningIsNotSupported,
+} = require('../common/sea');
+
+skipIfBuildSEAIsNotSupported();
+// buildSEA() signs the executable with signtool on Windows. Skip before the
+// async main() below registers a mustCall, otherwise the test fails on the
+// unmatched mustCall when it is skipped because signtool is not available.
+skipIfSigningIsNotSupported();
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { createWriteStream } = require('fs');
+const { pipeline } = require('stream/promises');
+const { zipFiles } = require('zlib');
+const tmpdir = require('../common/tmpdir');
+const fixtures = require('../common/fixtures');
+const { spawnSyncAndAssert } = require('../common/child_process');
+
+// Every entry is stored as an asset under this reserved prefix, see
+// src/node_sea.cc.
+const kCompileCachePrefix = 'node:sea:compile-cache/';
+
+function countCompileCacheEntries(blobPath) {
+ const blob = fs.readFileSync(blobPath).toString('latin1');
+ return blob.split(kCompileCachePrefix).length - 1;
+}
+
+// Generates the preparation blob of a fixture with "useCodeCache" and returns
+// the number of compile cache entries it contains.
+async function generateBlob(fixtureDir, prepare) {
+ tmpdir.refresh();
+ fs.cpSync(fixtureDir, tmpdir.path, { recursive: true });
+ await prepare?.();
+ spawnSyncAndAssert(
+ process.execPath,
+ ['--experimental-sea-config', 'sea-config-code-cache.json'],
+ {
+ cwd: tmpdir.path,
+ env: {
+ ...process.env,
+ NODE_DEBUG_NATIVE: 'SEA',
+ },
+ },
+ {
+ stderr: /Generated \d+ compile cache entries for \d+ modules/,
+ });
+ return countCompileCacheEntries(tmpdir.resolve('sea-prep.blob'));
+}
+
+async function main() {
+ // CommonJS: sea.js, modules/math.js, modules/calculator.js and the two
+ // packages under node_modules; config.json and greeting.txt are data.
+ assert.strictEqual(await generateBlob(fixtures.path('sea', 'vfs')), 5);
+
+ // ESM: main.mjs, modules/math.mjs, modules/calculator.cjs and the package.
+ assert.strictEqual(await generateBlob(fixtures.path('sea', 'vfs-esm')), 4);
+
+ // From a ZIP archive: sea.js, modules/math.js and the package.
+ const zipFixture = (...args) => fixtures.path('sea', 'vfs-zip', ...args);
+ assert.strictEqual(await generateBlob(zipFixture(), () => pipeline(
+ zipFiles([
+ [zipFixture('config.json'), 'config.json'],
+ [zipFixture('greeting.txt'), 'data/greeting.txt'],
+ [zipFixture('math.js'), 'modules/math.js'],
+ [zipFixture('test-pkg-package.json'), 'node_modules/test-pkg/package.json'],
+ [zipFixture('test-pkg-index.js'), 'node_modules/test-pkg/index.js'],
+ ]),
+ createWriteStream(tmpdir.resolve('assets.zip')),
+ )), 3);
+
+ // The build is not affected by the compile cache configuration of the
+ // build process itself: the cache is generated although NODE_COMPILE_CACHE
+ // points at an unusable location and the compile cache is disabled.
+ tmpdir.refresh();
+ fs.cpSync(fixtures.path('sea', 'vfs'), tmpdir.path, { recursive: true });
+ spawnSyncAndAssert(
+ process.execPath,
+ ['--experimental-sea-config', 'sea-config-code-cache.json'],
+ {
+ cwd: tmpdir.path,
+ env: {
+ ...process.env,
+ NODE_COMPILE_CACHE: path.join(tmpdir.path, 'config.json', 'nope'),
+ NODE_DISABLE_COMPILE_CACHE: '1',
+ NODE_DEBUG_NATIVE: undefined,
+ },
+ },
+ {
+ stderr: /Wrote single executable preparation blob/,
+ });
+ assert.strictEqual(
+ countCompileCacheEntries(tmpdir.resolve('sea-prep.blob')), 5);
+
+ // The executable runs with the cache embedded, and the reserved assets
+ // are not visible as files of the virtual file system.
+ tmpdir.refresh();
+ const outputFile = buildSEA(fixtures.path('sea', 'vfs'), {
+ configPath: 'sea-config-code-cache.json',
+ });
+ spawnSyncAndAssert(
+ outputFile,
+ {
+ env: {
+ ...process.env,
+ NODE_DEBUG_NATIVE: undefined,
+ },
+ },
+ {
+ stdout(stdout) {
+ assert.match(stdout, /All SEA VFS tests passed!/);
+ assert.doesNotMatch(stdout, /node:sea:/);
+ },
+ stderr(stderr) {
+ assert.doesNotMatch(stderr, /Code cache data rejected/);
+ assert.doesNotMatch(stderr, /ExperimentalWarning: VirtualFileSystem/);
+ },
+ },
+ );
+}
+
+main().then(common.mustCall());