Skip to content

Commit d03bf27

Browse files
committed
src: add an option to make compile cache portable
Adds an option (NODE_COMPILE_CACHE_PORTABLE) for the built-in compile cache to encode the hashes with relative file paths. On enabling the option, the source directory along with cache directory can be bundled and moved, and the cache continues to work. When enabled, paths encoded in hash are relative to compile cache directory.
1 parent 9bcc5a8 commit d03bf27

16 files changed

+449
-19
lines changed

doc/api/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3284,6 +3284,10 @@ added: v22.1.0
32843284
Enable the [module compile cache][] for the Node.js instance. See the documentation of
32853285
[module compile cache][] for details.
32863286

3287+
### `NODE_COMPILE_CACHE_PORTABLE=1`
3288+
3289+
When set to 1, the path for [module compile cache][] is considered relative.
3290+
32873291
### `NODE_DEBUG=module[,…]`
32883292

32893293
<!-- YAML

doc/api/module.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,28 @@ the [`NODE_COMPILE_CACHE=dir`][] environment variable if it's set, or defaults
399399
to `path.join(os.tmpdir(), 'node-compile-cache')` otherwise. To locate the compile cache
400400
directory used by a running Node.js instance, use [`module.getCompileCacheDir()`][].
401401
402+
By default, caches are invalidated when the absolute paths of the modules being
403+
cached are changed. To keep the cache working after moving the
404+
project directory, enable portable compile cache. This allows previously compiled
405+
modules to be reused across different directory locations as long as the layout relative
406+
to the cache directory remains the same. This would be done on a best-effort basis. If
407+
Node.js cannot compute the location of a module relative to the cache directory, the module
408+
will not be cached.
409+
410+
There are two ways to enable the portable mode:
411+
412+
1. Using the portable option in module.enableCompileCache():
413+
414+
```js
415+
// Absolute paths (default): cache breaks if project is moved
416+
module.enableCompileCache({ path: '.cache' });
417+
418+
// Relative paths (portable): cache works after moving project
419+
module.enableCompileCache({ path: '.cache', portable: true });
420+
```
421+
422+
2. Setting the environment variable: [`NODE_COMPILE_CACHE_PORTABLE=1`][]
423+
402424
Currently when using the compile cache with [V8 JavaScript code coverage][], the
403425
coverage being collected by V8 may be less precise in functions that are
404426
deserialized from the code cache. It's recommended to turn this off when
@@ -1789,6 +1811,7 @@ returned object contains the following keys:
17891811
[`--import`]: cli.md#--importmodule
17901812
[`--require`]: cli.md#-r---require-module
17911813
[`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir
1814+
[`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1
17921815
[`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1
17931816
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
17941817
[`SourceMap`]: #class-modulesourcemap

doc/node.1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,13 @@ Enable the
718718
.Sy module compile cache
719719
for the Node.js instance.
720720
.
721+
.It Ev NODE_COMPILE_CACHE_PORTABLE
722+
When set to '1' or 'true', the
723+
.Sy module compile cache
724+
will be hit as long as the location of the modules relative to the cache directory remain
725+
consistent. This can be used in conjunction with .Ev NODE_COMPILE_CACHE
726+
to enable portable on-disk caching.
727+
.
721728
.It Ev NODE_DEBUG Ar modules...
722729
Comma-separated list of core modules that should print debug information.
723730
.

lib/internal/modules/helpers.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,18 +383,31 @@ function stringify(body) {
383383
}
384384

385385
/**
386-
* Enable on-disk compiled cache for all user modules being complied in the current Node.js instance
386+
* Enable on-disk compiled cache for all user modules being compiled in the current Node.js instance
387387
* after this method is called.
388-
* If cacheDir is undefined, defaults to the NODE_MODULE_CACHE environment variable.
389-
* If NODE_MODULE_CACHE isn't set, default to path.join(os.tmpdir(), 'node-compile-cache').
390-
* @param {string|undefined} cacheDir
388+
* This method accepts either:
389+
* - A string `cacheDir`: the path to the cache directory.
390+
* - An options object `{path?: string, portable?: boolean}`:
391+
* - `path`: A string path to the cache directory.
392+
* - `portable`: If `portable` is true, the cache directory will be considered relative. Defaults to false.
393+
* If cache path is undefined, it defaults to the NODE_MODULE_CACHE environment variable.
394+
* If `NODE_MODULE_CACHE` isn't set, it defaults to `path.join(os.tmpdir(), 'node-compile-cache')`.
395+
* @param {string | { path?: string, portable?: boolean } | undefined} options
391396
* @returns {{status: number, message?: string, directory?: string}}
392397
*/
393-
function enableCompileCache(cacheDir) {
398+
function enableCompileCache(options) {
399+
let cacheDir;
400+
let portable = false;
401+
402+
if (typeof options === 'object' && options !== null) {
403+
({ path: cacheDir, portable = false } = options);
404+
} else {
405+
cacheDir = options;
406+
}
394407
if (cacheDir === undefined) {
395408
cacheDir = join(lazyTmpdir(), 'node-compile-cache');
396409
}
397-
const nativeResult = _enableCompileCache(cacheDir);
410+
const nativeResult = _enableCompileCache(cacheDir, portable);
398411
const result = { status: nativeResult[0] };
399412
if (nativeResult[1]) {
400413
result.message = nativeResult[1];

src/compile_cache.cc

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
#include <unistd.h> // getuid
1414
#endif
1515

16+
#ifdef _WIN32
17+
#include <windows.h>
18+
#endif
1619
namespace node {
1720

1821
using v8::Function;
@@ -223,13 +226,48 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
223226
Debug(" success, size=%d\n", total_read);
224227
}
225228

229+
static std::string GetRelativePath(std::string_view path,
230+
std::string_view base) {
231+
// On Windows, the native encoding is UTF-16, so we need to convert
232+
// the paths to wide strings before using std::filesystem::path.
233+
// On other platforms, std::filesystem::path can handle UTF-8 directly.
234+
#ifdef _WIN32
235+
std::filesystem::path module_path(
236+
ConvertToWideString(std::string(path), CP_UTF8));
237+
std::filesystem::path base_path(
238+
ConvertToWideString(std::string(base), CP_UTF8));
239+
#else
240+
std::filesystem::path module_path(path);
241+
std::filesystem::path base_path(base);
242+
#endif
243+
std::filesystem::path relative = module_path.lexically_relative(base_path);
244+
auto u8str = relative.u8string();
245+
return std::string(u8str.begin(), u8str.end());
246+
}
247+
226248
CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
227249
Local<String> filename,
228250
CachedCodeType type) {
229251
DCHECK(!compile_cache_dir_.empty());
230252

253+
Environment* env = Environment::GetCurrent(isolate_->GetCurrentContext());
231254
Utf8Value filename_utf8(isolate_, filename);
232-
uint32_t key = GetCacheKey(filename_utf8.ToStringView(), type);
255+
std::string file_path = filename_utf8.ToString();
256+
// If the relative path is enabled, we try to use a relative path
257+
// from the compile cache directory to the file path
258+
if (portable_ && IsAbsoluteFilePath(file_path)) {
259+
// Normalize the path to ensure it is consistent.
260+
std::string normalized_file_path = NormalizeFileURLOrPath(env, file_path);
261+
std::string relative_path =
262+
GetRelativePath(normalized_file_path, normalized_compile_cache_dir_);
263+
if (!relative_path.empty()) {
264+
file_path = relative_path;
265+
Debug("[compile cache] using relative path %s from %s\n",
266+
file_path.c_str(),
267+
absolute_compile_cache_dir_.c_str());
268+
}
269+
}
270+
uint32_t key = GetCacheKey(file_path, type);
233271

234272
// TODO(joyeecheung): don't encode this again into UTF8. If we read the
235273
// UTF8 content on disk as raw buffer (from the JS layer, while watching out
@@ -500,11 +538,15 @@ CompileCacheHandler::CompileCacheHandler(Environment* env)
500538
// - $NODE_VERSION-$ARCH-$CACHE_DATA_VERSION_TAG-$UID
501539
// - $FILENAME_AND_MODULE_TYPE_HASH.cache: a hash of filename + module type
502540
CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
503-
const std::string& dir) {
541+
const std::string& dir,
542+
bool portable) {
504543
std::string cache_tag = GetCacheVersionTag();
505-
std::string absolute_cache_dir_base = PathResolve(env, {dir});
506-
std::string cache_dir_with_tag =
507-
absolute_cache_dir_base + kPathSeparator + cache_tag;
544+
std::string base_dir = dir;
545+
if (!portable) {
546+
base_dir = PathResolve(env, {dir});
547+
}
548+
549+
std::string cache_dir_with_tag = base_dir + kPathSeparator + cache_tag;
508550
CompileCacheEnableResult result;
509551
Debug("[compile cache] resolved path %s + %s -> %s\n",
510552
dir,
@@ -546,8 +588,11 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
546588
return result;
547589
}
548590

549-
result.cache_directory = absolute_cache_dir_base;
591+
result.cache_directory = base_dir;
550592
compile_cache_dir_ = cache_dir_with_tag;
593+
absolute_compile_cache_dir_ = PathResolve(env, {compile_cache_dir_});
594+
portable_ = portable;
595+
normalized_compile_cache_dir_ = NormalizeFileURLOrPath(env, absolute_compile_cache_dir_);
551596
result.status = CompileCacheEnableStatus::ENABLED;
552597
return result;
553598
}

src/compile_cache.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ struct CompileCacheEnableResult {
6565
class CompileCacheHandler {
6666
public:
6767
explicit CompileCacheHandler(Environment* env);
68-
CompileCacheEnableResult Enable(Environment* env, const std::string& dir);
68+
CompileCacheEnableResult Enable(Environment* env,
69+
const std::string& dir,
70+
bool portable);
6971

7072
void Persist();
7173

@@ -103,6 +105,9 @@ class CompileCacheHandler {
103105
bool is_debug_ = false;
104106

105107
std::string compile_cache_dir_;
108+
std::string absolute_compile_cache_dir_;
109+
std::string normalized_compile_cache_dir_;
110+
bool portable_ = false;
106111
std::unordered_map<uint32_t, std::unique_ptr<CompileCacheEntry>>
107112
compiler_cache_store_;
108113
};

src/env.cc

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,11 +1122,21 @@ void Environment::InitializeCompileCache() {
11221122
dir_from_env.empty()) {
11231123
return;
11241124
}
1125-
EnableCompileCache(dir_from_env);
1125+
std::string portable_env;
1126+
bool portable = credentials::SafeGetenv(
1127+
"NODE_COMPILE_CACHE_PORTABLE", &portable_env, this) &&
1128+
!portable_env.empty() &&
1129+
(portable_env == "1" || portable_env == "true");
1130+
if (portable) {
1131+
Debug(this,
1132+
DebugCategory::COMPILE_CACHE,
1133+
"[compile cache] using relative path\n");
1134+
}
1135+
EnableCompileCache(dir_from_env, portable);
11261136
}
11271137

11281138
CompileCacheEnableResult Environment::EnableCompileCache(
1129-
const std::string& cache_dir) {
1139+
const std::string& cache_dir, bool portable) {
11301140
CompileCacheEnableResult result;
11311141
std::string disable_env;
11321142
if (credentials::SafeGetenv(
@@ -1143,7 +1153,7 @@ CompileCacheEnableResult Environment::EnableCompileCache(
11431153
if (!compile_cache_handler_) {
11441154
std::unique_ptr<CompileCacheHandler> handler =
11451155
std::make_unique<CompileCacheHandler>(this);
1146-
result = handler->Enable(this, cache_dir);
1156+
result = handler->Enable(this, cache_dir, portable);
11471157
if (result.status == CompileCacheEnableStatus::ENABLED) {
11481158
compile_cache_handler_ = std::move(handler);
11491159
AtExit(

src/env.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1022,7 +1022,8 @@ class Environment final : public MemoryRetainer {
10221022
void InitializeCompileCache();
10231023
// Enable built-in compile cache if it has not yet been enabled.
10241024
// The cache will be persisted to disk on exit.
1025-
CompileCacheEnableResult EnableCompileCache(const std::string& cache_dir);
1025+
CompileCacheEnableResult EnableCompileCache(const std::string& cache_dir,
1026+
bool portable);
10261027
void FlushCompileCache();
10271028

10281029
void RunAndClearNativeImmediates(bool only_refed = false);

src/node_file.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,8 @@ int SyncCallAndThrowOnError(Environment* env,
531531
FSReqWrapSync* req_wrap,
532532
Func fn,
533533
Args... args);
534+
535+
std::string ConvertWideToUTF8(const std::wstring& wstr);
534536
} // namespace fs
535537

536538
} // namespace node

src/node_modules.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,14 @@ void EnableCompileCache(const FunctionCallbackInfo<Value>& args) {
513513
THROW_ERR_INVALID_ARG_TYPE(env, "cacheDir should be a string");
514514
return;
515515
}
516+
517+
bool portable = false;
518+
if (args.Length() > 1 && args[1]->IsTrue()) {
519+
portable = true;
520+
}
521+
516522
Utf8Value value(isolate, args[0]);
517-
CompileCacheEnableResult result = env->EnableCompileCache(*value);
523+
CompileCacheEnableResult result = env->EnableCompileCache(*value, portable);
518524
Local<Value> values[3];
519525
values[0] = v8::Integer::New(isolate, static_cast<uint8_t>(result.status));
520526
if (ToV8Value(context, result.message).ToLocal(&values[1]) &&

0 commit comments

Comments
 (0)