Skip to content

Commit b6eb597

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 b6eb597

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,18 @@ 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+
When portable mode is enabled, cache keys use paths relative to the compile cache directory.
403+
This allows the cache to be reused after moving the project across directories etc.
404+
405+
It can be enabled via:
406+
407+
```js
408+
module.enableCompileCache({ path: '...', portable: true });
409+
```
410+
411+
or [`NODE_COMPILE_CACHE_PORTABLE=1`][] environment variable.
412+
If a relative path can't be computed, Node.js falls back to using the absolute path.
413+
402414
Currently when using the compile cache with [V8 JavaScript code coverage][], the
403415
coverage being collected by V8 may be less precise in functions that are
404416
deserialized from the code cache. It's recommended to turn this off when
@@ -1789,6 +1801,7 @@ returned object contains the following keys:
17891801
[`--import`]: cli.md#--importmodule
17901802
[`--require`]: cli.md#-r---require-module
17911803
[`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir
1804+
[`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1
17921805
[`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1
17931806
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
17941807
[`SourceMap`]: #class-modulesourcemap

doc/node.1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,14 @@ 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+
uses relative paths when computing cache keys. This makes the cache
708+
portable across directories etc.
709+
This can be used in conjunction with .Ev NODE_COMPILE_CACHE
710+
to enable on-disk caching with relative path resolution.
711+
.
704712
.It Ev NODE_DEBUG Ar modules...
705713
Comma-separated list of core modules that should print debug information.
706714
.

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: 108 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,102 @@ 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+
#else
274+
if (path.size() > 0 && path[0] == '/') return true;
275+
#endif
276+
return false;
277+
}
278+
279+
static std::string GetRelativePath(std::string_view path,
280+
std::string_view base) {
281+
// On Windows, the native encoding is UTF-16, so we need to convert
282+
// the paths to wide strings before using std::filesystem::path.
283+
// On other platforms, std::filesystem::path can handle UTF-8 directly.
284+
#ifdef _WIN32
285+
std::wstring wpath = ConvertToWideString(std::string(path), CP_UTF8);
286+
std::wstring wbase = ConvertToWideString(std::string(base), CP_UTF8);
287+
std::filesystem::path relative =
288+
std::filesystem::path(wpath).lexically_relative(
289+
std::filesystem::path(wbase));
290+
if (relative.empty()) {
291+
return std::string();
292+
}
293+
std::string relative_path = ConvertWideToUTF8(relative.wstring());
294+
return relative_path;
295+
#else
296+
std::filesystem::path relative =
297+
std::filesystem::path(path).lexically_relative(
298+
std::filesystem::path(base));
299+
if (relative.empty()) {
300+
return std::string();
301+
}
302+
return relative.generic_string();
303+
#endif
304+
}
305+
226306
CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
227307
Local<String> filename,
228308
CachedCodeType type) {
229309
DCHECK(!compile_cache_dir_.empty());
230310

231311
Utf8Value filename_utf8(isolate_, filename);
232-
uint32_t key = GetCacheKey(filename_utf8.ToStringView(), type);
312+
std::string file_path = filename_utf8.ToString();
313+
// If the relative path is enabled, we try to use a relative path
314+
// from the compile cache directory to the file path
315+
if (portable_ && IsAbsoluteFilePath(file_path)) {
316+
// Normalise the path to ensure it is consistent.
317+
std::string normalised_file_path = NormalisePath(file_path);
318+
std::string relative_path =
319+
GetRelativePath(normalised_file_path, normalised_compile_cache_dir_);
320+
if (!relative_path.empty()) {
321+
file_path = relative_path;
322+
Debug("[compile cache] using relative path %s from %s\n",
323+
file_path.c_str(),
324+
absolute_compile_cache_dir_.c_str());
325+
}
326+
}
327+
uint32_t key = GetCacheKey(file_path, type);
233328

234329
// TODO(joyeecheung): don't encode this again into UTF8. If we read the
235330
// UTF8 content on disk as raw buffer (from the JS layer, while watching out
@@ -500,11 +595,15 @@ CompileCacheHandler::CompileCacheHandler(Environment* env)
500595
// - $NODE_VERSION-$ARCH-$CACHE_DATA_VERSION_TAG-$UID
501596
// - $FILENAME_AND_MODULE_TYPE_HASH.cache: a hash of filename + module type
502597
CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
503-
const std::string& dir) {
598+
const std::string& dir,
599+
bool portable) {
504600
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;
601+
std::string base_dir = dir;
602+
if (!portable) {
603+
base_dir = PathResolve(env, {dir});
604+
}
605+
606+
std::string cache_dir_with_tag = base_dir + kPathSeparator + cache_tag;
508607
CompileCacheEnableResult result;
509608
Debug("[compile cache] resolved path %s + %s -> %s\n",
510609
dir,
@@ -546,8 +645,11 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env,
546645
return result;
547646
}
548647

549-
result.cache_directory = absolute_cache_dir_base;
648+
result.cache_directory = base_dir;
550649
compile_cache_dir_ = cache_dir_with_tag;
650+
absolute_compile_cache_dir_ = PathResolve(env, {compile_cache_dir_});
651+
portable_ = portable;
652+
normalised_compile_cache_dir_ = NormalisePath(absolute_compile_cache_dir_);
551653
result.status = CompileCacheEnableStatus::ENABLED;
552654
return result;
553655
}

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)