From ad3b4f9e12e6eb43b7c8245a0309f9e999e130ad Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 27 Oct 2026 12:22:03 -0500 Subject: [PATCH] Record a linker job again when a path it names changes A Linux build that links with lld records the driver's ld.lld job with the identities of the files and directories it names, and later links run that job directly while those identities match. When one of them changed, for example when a system update replaced libc or the linker, the link went back to the driver, but the entry stayed: record() kept the existing entry, so every later link of that shape went through the driver until the entry was pruned. An entry whose job names a changed path now counts as missing. A listed link records the job again with the driver's -v -### and runs it; a traced link through the driver records it from its -v output. Entries without a job, for links that cannot run as one ld.lld job, are kept as before. --- .../compiler/src/native/linker-job.test.ts | 38 +++++++++++++++++-- packages/compiler/src/native/linker-job.ts | 21 +++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/native/linker-job.test.ts b/packages/compiler/src/native/linker-job.test.ts index b9d718c03..96d186baf 100644 --- a/packages/compiler/src/native/linker-job.test.ts +++ b/packages/compiler/src/native/linker-job.test.ts @@ -212,7 +212,7 @@ test.skipIf(process.platform === "win32")( ); test.skipIf(process.platform === "win32")( - "a changed startup object, library directory or linker sends the link to the driver", + "a changed startup object, library directory or linker records the job again", () => { for (const change of ["crt", "lib", "ld"] as const) { const f = fixture(); @@ -230,12 +230,44 @@ test.skipIf(process.platform === "win32")( } const second = f.stage(); const count = f.calls().length; - expect(open().run(second.args, second.paths, second.directory, true)).toBeNull(); - expect(f.calls().length).toBe(count); + expect(open().run(second.args, second.paths, second.directory, true)).not.toBeNull(); + expect(f.calls().slice(count)).toEqual([ + `driver ${second.args.join(" ")} -v -###`, + `ld.lld -pie ${f.crt} -L${f.lib} -pthread ${second.paths[0]} -lm -o ${second.output}`, + ]); } }, ); +test.skipIf(process.platform === "win32")( + "a traced link whose recorded job names a changed path records it again", + () => { + const f = fixture(); + const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!; + const traced = (s: ReturnType) => [...s.args, "-v", "-Wl,-t"]; + const linkThroughDriver = (s: ReturnType) => { + const link = open().link(s.paths, s.directory); + expect(link.run(traced(s), false)).toBeNull(); + const linked = spawnSync(f.driver, traced(s), { encoding: "utf8" }); + expect(linked.status).toBe(0); + link.record(traced(s), linked.stdout + "\n" + linked.stderr); + }; + linkThroughDriver(f.stage()); + const before = statSync(f.crt); + writeFileSync(f.crt, "new startup object"); + utimesSync(f.crt, before.atime, new Date(before.mtimeMs + 1000)); + // The recorded job names the old startup object: the link runs through the driver once, + // and its job is recorded again. + linkThroughDriver(f.stage()); + const third = f.stage(); + const count = f.calls().length; + expect(open().link(third.paths, third.directory).run(traced(third), false)).not.toBeNull(); + expect(f.calls().slice(count)).toEqual([ + `ld.lld -pie ${f.crt} -L${f.lib} -pthread ${third.paths[0]} -lm -o ${third.output} -t`, + ]); + }, +); + test.skipIf(process.platform === "win32")("a changed driver records the job again", () => { const f = fixture(); const first = f.stage(); diff --git a/packages/compiler/src/native/linker-job.ts b/packages/compiler/src/native/linker-job.ts index e1195e4c4..f006cb233 100644 --- a/packages/compiler/src/native/linker-job.ts +++ b/packages/compiler/src/native/linker-job.ts @@ -164,10 +164,11 @@ export class LinkerJobs { * Runs the link that the driver arguments `args` describe as its recorded * job. `paths` are the link's own inputs and output among `args`, which may * differ from build to build; `stage` is the directory they are in. Without - * an entry, `list` records the job first with the driver's `-v -###`; + * an entry, or with one whose job names a path that changed since it was + * recorded, `list` records the job first with the driver's `-v -###`; * otherwise the caller links through the driver and passes its `-v` output - * to record(). Null when the link has to run through the driver: no job is - * recorded, a recorded path changed, or the job failed. + * to record(), which records it again. Null when the link has to run + * through the driver: no job is recorded, or the job failed. */ run( args: readonly string[], @@ -180,7 +181,7 @@ export class LinkerJobs { const roots = [resolve(stage), realpathSync(stage)]; const key = this.key(args, paths, roots); if (key === null) return null; - let entry = this.read(key, paths.length); + let entry = this.fresh(this.read(key, paths.length)); if (entry === null && list) { const result = spawnSync(this.driver, [...args, "-v", "-###"], { encoding: "utf8", @@ -190,7 +191,7 @@ export class LinkerJobs { return null; entry = this.store(key, result.stdout + "\n" + result.stderr, paths, roots); } - if (entry === null || entry.job === null || !stillMatches(entry.inputs)) return null; + if (entry === null || entry.job === null) return null; const job = entry.job.map((token) => (typeof token === "number" ? paths[token]! : token)); const result = spawnSync(job[0]!, job.slice(1), { encoding: "utf8", @@ -206,6 +207,14 @@ export class LinkerJobs { } } + /** `entry`, or null when a path its job names changed since it was + * recorded: the next link through the driver records the job again. An + * entry without a job, a link the driver cannot hand to one ld.lld job, + * stays. */ + private fresh(entry: LinkerJobEntry | null): LinkerJobEntry | null { + return entry !== null && entry.job !== null && !stillMatches(entry.inputs) ? null : entry; + } + private read(key: string, paths: number): LinkerJobEntry | null { const bytes = this.cache.read("linker-job", key, MAX_ENTRY_BYTES); if (bytes === null) return null; @@ -231,7 +240,7 @@ export class LinkerJobs { try { const roots = [resolve(stage), realpathSync(stage)]; const key = this.key(args, paths, roots); - if (key !== null && this.read(key, paths.length) === null) + if (key !== null && this.fresh(this.read(key, paths.length)) === null) this.store(key, output, paths, roots, driverInputs); } catch { /* The next link records it. */