Skip to content

Commit de78d1a

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 0fd1ecd commit de78d1a

File tree

13 files changed

+388
-19
lines changed

13 files changed

+388
-19
lines changed

doc/api/cli.md

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

3256+
### `NODE_COMPILE_CACHE_PORTABLE=1`
3257+
3258+
When set to 1, the path for [module compile cache][] is considered relative.
3259+
32563260
### `NODE_DEBUG=module[,…]`
32573261

32583262
<!-- YAML

doc/api/module.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,31 @@ 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, cache 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+
424+
If a module's absolute path cannot be made relative to the cache directory,
425+
Node.js will fall back to using the absolute path.
426+
402427
Currently when using the compile cache with [V8 JavaScript code coverage][], the
403428
coverage being collected by V8 may be less precise in functions that are
404429
deserialized from the code cache. It's recommended to turn this off when
@@ -1789,6 +1814,7 @@ returned object contains the following keys:
17891814
[`--import`]: cli.md#--importmodule
17901815
[`--require`]: cli.md#-r---require-module
17911816
[`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir
1817+
[`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1
17921818
[`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1
17931819
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
17941820
[`SourceMap`]: #class-modulesourcemap

doc/node.1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,13 @@ Enable the
701701
.Sy module compile cache
702702
for the Node.js instance.
703703
.
704+
.It Ev NODE_COMPILE_CACHE_PORTABLE
705+
When set to '1' or 'true', the
706+
.Sy module compile cache
707+
will be hit as long as the location of the modules relative to the cache directory remain
708+
consistent. This can be used in conjunction with .Ev NODE_COMPILE_CACHE
709+
to enable portable on-disk caching.
710+
.
704711
.It Ev NODE_DEBUG Ar modules...
705712
Comma-separated list of core modules that should print debug information.
706713
.

lib/internal/modules/helpers.js

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

373373
/**
374-
* Enable on-disk compiled cache for all user modules being complied in the current Node.js instance
374+
* Enable on-disk compiled cache for all user modules being compiled in the current Node.js instance
375375
* after this method is called.
376-
* If cacheDir is undefined, defaults to the NODE_MODULE_CACHE environment variable.
377-
* If NODE_MODULE_CACHE isn't set, default to path.join(os.tmpdir(), 'node-compile-cache').
378-
* @param {string|undefined} cacheDir
376+
* This method accepts either:
377+
* - A string `cacheDir`: the path to the cache directory.
378+
* - An options object `{path?: string, portable?: boolean}`:
379+
* - `path`: A string path to the cache directory.
380+
* - `portable`: If `portable` is true, the cache directory will be considered relative. Defaults to false.
381+
* If cache path is undefined, it defaults to the NODE_MODULE_CACHE environment variable.
382+
* If `NODE_MODULE_CACHE` isn't set, it defaults to `path.join(os.tmpdir(), 'node-compile-cache')`.
383+
* @param {string | { path?: string, portable?: boolean } | undefined} options
379384
* @returns {{status: number, message?: string, directory?: string}}
380385
*/
381-
function enableCompileCache(cacheDir) {
386+
function enableCompileCache(options) {
387+
let cacheDir;
388+
let portable = false;
389+
390+
if (typeof options === 'object' && options !== null) {
391+
({ path: cacheDir, portable = false } = options);
392+
} else {
393+
cacheDir = options;
394+
}
382395
if (cacheDir === undefined) {
383396
cacheDir = join(lazyTmpdir(), 'node-compile-cache');
384397
}
385-
const nativeResult = _enableCompileCache(cacheDir);
398+
const nativeResult = _enableCompileCache(cacheDir, portable);
386399
const result = { status: nativeResult[0] };
387400
if (nativeResult[1]) {
388401
result.message = nativeResult[1];

src/compile_cache.cc

Lines changed: 96 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,93 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
223226
Debug(" success, size=%d\n", total_read);
224227
}
225228

229+
#ifdef _WIN32
230+
constexpr bool IsWindowsDeviceRoot(const char c) noexcept {
231+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
232+
}
233+
#endif
234+
235+
static std::string NormalisePath(std::string_view path) {
236+
std::string normalised_string(path);
237+
constexpr std::string_view file_scheme = "file://";
238+
if (normalised_string.rfind(file_scheme, 0) == 0) {
239+
normalised_string.erase(0, file_scheme.size());
240+
}
241+
242+
#ifdef _WIN32
243+
if (normalised_string.size() > 2 &&
244+
IsWindowsDeviceRoot(normalised_string[0]) &&
245+
normalised_string[1] == ':' &&
246+
(normalised_string[2] == '/' || normalised_string[2] == '\\')) {
247+
normalised_string[0] = ToLower(normalised_string[0]);
248+
}
249+
#endif
250+
for (char& c : normalised_string) {
251+
if (c == '\\') {
252+
c = '/';
253+
}
254+
}
255+
256+
normalised_string = NormalizeString(normalised_string, false, "/");
257+
return normalised_string;
258+
}
259+
260+
// Check if a path looks like an absolute path or file URL.
261+
static bool IsAbsoluteFilePath(std::string_view path) {
262+
if (path.rfind("file://", 0) == 0) {
263+
return true;
264+
}
265+
#ifdef _WIN32
266+
if (path.size() > 2 && IsWindowsDeviceRoot(path[0]) &&
267+
(path[1] == ':' && (path[2] == '/' || path[2] == '\\')))
268+
return true;
269+
if (path.size() > 1 && path[0] == '\\' && path[1] == '\\') return true;
270+
#endif
271+
if (path.size() > 0 && path[0] == '/') return true;
272+
return false;
273+
}
274+
275+
static std::string GetRelativePath(std::string_view path,
276+
std::string_view base) {
277+
// On Windows, the native encoding is UTF-16, so we need to convert
278+
// the paths to wide strings before using std::filesystem::path.
279+
// On other platforms, std::filesystem::path can handle UTF-8 directly.
280+
#ifdef _WIN32
281+
std::filesystem::path module_path(
282+
ConvertToWideString(std::string(path), CP_UTF8));
283+
std::filesystem::path base_path(
284+
ConvertToWideString(std::string(base), CP_UTF8));
285+
#else
286+
std::filesystem::path module_path(path);
287+
std::filesystem::path base_path(base);
288+
#endif
289+
std::filesystem::path relative = module_path.lexically_relative(base_path);
290+
auto u8str = relative.u8string();
291+
return std::string(u8str.begin(), u8str.end());
292+
}
293+
226294
CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
227295
Local<String> filename,
228296
CachedCodeType type) {
229297
DCHECK(!compile_cache_dir_.empty());
230298

231299
Utf8Value filename_utf8(isolate_, filename);
232-
uint32_t key = GetCacheKey(filename_utf8.ToStringView(), type);
300+
std::string file_path = filename_utf8.ToString();
301+
// If the relative path is enabled, we try to use a relative path
302+
// from the compile cache directory to the file path
303+
if (portable_ && IsAbsoluteFilePath(file_path)) {
304+
// Normalise the path to ensure it is consistent.
305+
std::string normalised_file_path = NormalisePath(file_path);
306+
std::string relative_path =
307+
GetRelativePath(normalised_file_path, normalised_compile_cache_dir_);
308+
if (!relative_path.empty()) {
309+
file_path = relative_path;
310+
Debug("[compile cache] using relative path %s from %s\n",
311+
file_path.c_str(),
312+
absolute_compile_cache_dir_.c_str());
313+
}
314+
}
315+
uint32_t key = GetCacheKey(file_path, type);
233316

234317
// TODO(joyeecheung): don't encode this again into UTF8. If we read the
235318
// UTF8 content on disk as raw buffer (from the JS layer, while watching out
@@ -500,11 +583,15 @@ CompileCacheHandler::CompileCacheHandler(Environment* env)
500583
// - $NODE_VERSION-$ARCH-$CACHE_DATA_VERSION_TAG-$UID
501584
// - $FILENAME_AND_MODULE_TYPE_HASH.cache: a hash of filename + module type
502585
CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
503-
const std::string& dir) {
586+
const std::string& dir,
587+
bool portable) {
504588
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;
589+
std::string base_dir = dir;
590+
if (!portable) {
591+
base_dir = PathResolve(env, {dir});
592+
}
593+
594+
std::string cache_dir_with_tag = base_dir + kPathSeparator + cache_tag;
508595
CompileCacheEnableResult result;
509596
Debug("[compile cache] resolved path %s + %s -> %s\n",
510597
dir,
@@ -546,8 +633,11 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
546633
return result;
547634
}
548635

549-
result.cache_directory = absolute_cache_dir_base;
636+
result.cache_directory = base_dir;
550637
compile_cache_dir_ = cache_dir_with_tag;
638+
absolute_compile_cache_dir_ = PathResolve(env, {compile_cache_dir_});
639+
portable_ = portable;
640+
normalised_compile_cache_dir_ = NormalisePath(absolute_compile_cache_dir_);
551641
result.status = CompileCacheEnableStatus::ENABLED;
552642
return result;
553643
}

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 normalised_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)