From ad8f8daccd5e193cb54fa239d07d9167a9f7671e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 20:56:10 -0700 Subject: [PATCH] Keep scr_lib.c's default object byte-identical for the fs.watchFile poll Adding scr_stats_of_path and scr_stats_zero to scr_lib.c moved every program's executable: scr_lib.o is linked everywhere, so its new symbols and strings shifted addresses even for programs that never watch a file. The two entry points now live in an SCR_FS_WATCH_FILE build of scr_lib.c, the matrix's variant for programs whose feature set has watchFile, and the poll unit calls the renamed scr_stats_poll_path. Programs that do not poll link the default object unchanged: compiled without the define, scr_lib.o is byte-identical to the base (verified by comparing the two objects). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/native/executable.ts | 2 ++ .../runtime-pack-common/runtime-pack-matrix.mjs | 11 +++++++++++ packages/runtime/src/scr_lib.c | 14 ++++++++------ packages/runtime/src/scr_lib.h | 6 ++++-- packages/runtime/src/scr_watch_file.c | 2 +- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 8ecac9923..8222bd9b7 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -426,6 +426,8 @@ const RUNTIME_DEFINES: readonly { { define: "SCR_NATIVE_REACTIONS", when: (opts) => opts.webStreams }, { define: "SCR_HANDLE_CLASSES", when: (opts) => opts.webStreams }, { define: "SCR_YIN", when: (opts) => opts.youngNursery }, + // scr_lib.c's path stat and zero Stats for the fs.watchFile poll. + { define: "SCR_FS_WATCH_FILE", when: (opts) => opts.watchFile }, { define: "SCR_NODE_ISLANDS", when: (opts) => opts.nodeIsland }, ]; diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6fff2b653..41eef2b23 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -274,6 +274,17 @@ export function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + // fs.watchFile's poll needs a path stat that reports the errno (its failed + // poll IS a reportable event). The Stats snapshot is private to scr_lib.c, + // so the two entry points live in a build of that unit only programs that + // poll link; every other program keeps the default object, byte for byte. + if (source === "scr_lib.c") { + variants.push({ + id: "fs-watch-file", + when: { watchFile: true }, + defines: ["SCR_FS_WATCH_FILE"], + }); + } // Programs that assign process.emitWarning link the replacement-aware // warning paths; every other program keeps the default object. if (source === "scr_async_dyn.c") { diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index c47339a68..f10b4f5fa 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -5584,7 +5584,7 @@ static bool scr_stats_link_target(HANDLE h, DWORD tag, double *size_out, ScrStr return true; } -static ScrStats *scr_stats_win_handle_stats(const ScrStr *path, const char *op, +static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op, bool no_follow) { WCHAR *wide = scr_fs_win_wide(path); if (!wide) return scr_stats_crt_fallback(path, op); @@ -5630,7 +5630,7 @@ static ScrStats *scr_stats_win_handle_stats(const ScrStr *path, const char *op, /* Reparse points are a general interception mechanism. Node/libuv * follows non-link tags even for lstat, so reopen the resolved target. */ CloseHandle(h); - return scr_stats_win_handle_stats(path, op, false); + return scr_stats_of_path(path, op, false); } if (!scr_stats_link_target(h, tagged.ReparseTag, &link_size, NULL)) { /* A mount-point tag may name a mounted volume rather than a junction. @@ -5638,7 +5638,7 @@ static ScrStats *scr_stats_win_handle_stats(const ScrStr *path, const char *op, * ordinary reparse points and retries with the final component * followed. Do not classify from the tag alone. */ CloseHandle(h); - return scr_stats_win_handle_stats(path, op, false); + return scr_stats_of_path(path, op, false); } is_link = true; } @@ -6081,7 +6081,7 @@ ScrBytes *scr_fs_readlink_bytes(ScrStr *path) { ScrStats *scr_fs_stat(ScrStr *path) { if (!scr_fs_path_ok(path, "path")) return NULL; #ifdef _WIN32 - return scr_stats_win_handle_stats(path, "stat", false); + return scr_stats_of_path(path, "stat", false); #else int error = 0; ScrStats *s = scr_stats_posix(path->data, -1, false, &error); /* follows symlinks */ @@ -6093,7 +6093,7 @@ ScrStats *scr_fs_stat(ScrStr *path) { ScrStats *scr_fs_lstat(ScrStr *path) { if (!scr_fs_path_ok(path, "path")) return NULL; #ifdef _WIN32 - return scr_stats_win_handle_stats(path, "lstat", true); + return scr_stats_of_path(path, "lstat", true); #else int error = 0; ScrStats *s = scr_stats_posix(path->data, -1, true, &error); /* no follow */ @@ -6135,10 +6135,11 @@ ScrStats *scr_fs_fstat(double fd) { return s; } +#ifdef SCR_FS_WATCH_FILE /* fs.watchFile's poll (scr_watch_file.c calls this): stat(2) following * symlinks, libuv's uv_fs_stat — NULL with *error = errno on failure, * never throwing, because a failed poll IS a reportable event. +1. */ -ScrStats *scr_stats_of_path(ScrStr *path, int *error) { +ScrStats *scr_stats_poll_path(ScrStr *path, int *error) { if (!scr_fs_path_ok(path, "path")) { *error = EINVAL; return NULL; @@ -6161,6 +6162,7 @@ ScrStats *scr_stats_of_path(ScrStr *path, int *error) { * field 0, so isFile()/isDirectory() and the size/date reads all answer * zero/false. +1. */ ScrStats *scr_stats_zero(void) { return scr_stats_new(); } +#endif /* SCR_FS_WATCH_FILE */ /* ── Stats objects ─────────────────────────────────────────────────── * Node's Stats and BigIntStats over a snapshot. The integers and the *Ms diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 775f04e33..69c470217 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -507,8 +507,10 @@ double scr_stats_big_birthtime(ScrStats *s); /* fstat without throwing: +1, or NULL with *error set to the errno. */ ScrStats *scr_stats_of_fd(int fd, int *error); /* stat(2) following symlinks without throwing (the fs.watchFile poll): - * +1, or NULL with *error set to the errno. */ -ScrStats *scr_stats_of_path(ScrStr *path, int *error); + * +1, or NULL with *error set to the errno. Both this and scr_stats_zero + * exist only in the SCR_FS_WATCH_FILE build of scr_lib.c, which programs + * that poll link; every other program links the default object. */ +ScrStats *scr_stats_poll_path(ScrStr *path, int *error); /* The all-zero snapshot a failed poll reports (Node's kZeroStats). +1. */ ScrStats *scr_stats_zero(void); /* Node's Stats (or BigIntStats) object for the snapshot: own fields in the diff --git a/packages/runtime/src/scr_watch_file.c b/packages/runtime/src/scr_watch_file.c index b1bee2826..325e10adb 100644 --- a/packages/runtime/src/scr_watch_file.c +++ b/packages/runtime/src/scr_watch_file.c @@ -171,7 +171,7 @@ static void scr_watch_file_poll(ScrClosure *cb) { if (p == NULL) return; p->timer = 0; /* the loop consumed this arm */ int err = 0; - ScrStats *cur = scr_stats_of_path(p->path, &err); + ScrStats *cur = scr_stats_poll_path(p->path, &err); if (cur == NULL) { if (p->state != -err) { scr_watch_file_emit(p, NULL, p->last, -err);