Skip to content

Commit 5abcfc9

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 5abcfc9

File tree

13 files changed

+391
-19
lines changed

13 files changed

+391
-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: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@
1313
#include <unistd.h> // getuid
1414
#endif
1515

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

21+
#ifdef _WIN32
22+
using fs::ConvertWideToUTF8;
23+
#endif
1824
using v8::Function;
1925
using v8::Local;
2026
using v8::Module;
@@ -223,13 +229,93 @@ void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) {
223229
Debug(" success, size=%d\n", total_read);
224230
}
225231

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

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

234320
// TODO(joyeecheung): don't encode this again into UTF8. If we read the
235321
// UTF8 content on disk as raw buffer (from the JS layer, while watching out
@@ -500,11 +586,15 @@ CompileCacheHandler::CompileCacheHandler(Environment* env)
500586
// - $NODE_VERSION-$ARCH-$CACHE_DATA_VERSION_TAG-$UID
501587
// - $FILENAME_AND_MODULE_TYPE_HASH.cache: a hash of filename + module type
502588
CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
503-
const std::string& dir) {
589+
const std::string& dir,
590+
bool portable) {
504591
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;
592+
std::string base_dir = dir;
593+
if (!portable) {
594+
base_dir = PathResolve(env, {dir});
595+
}
596+
597+
std::string cache_dir_with_tag = base_dir + kPathSeparator + cache_tag;
508598
CompileCacheEnableResult result;
509599
Debug("[compile cache] resolved path %s + %s -> %s\n",
510600
dir,
@@ -546,8 +636,11 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
546636
return result;
547637
}
548638

549-
result.cache_directory = absolute_cache_dir_base;
639+
result.cache_directory = base_dir;
550640
compile_cache_dir_ = cache_dir_with_tag;
641+
absolute_compile_cache_dir_ = PathResolve(env, {compile_cache_dir_});
642+
portable_ = portable;
643+
normalised_compile_cache_dir_ = NormalisePath(absolute_compile_cache_dir_);
551644
result.status = CompileCacheEnableStatus::ENABLED;
552645
return result;
553646
}

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)