[lld] [llvm] [DTLTO] Make temporary file handling consistent (PR #176807)

Ben Dunbobbin via llvm-commits llvm-commits at lists.llvm.org
Tue Jan 20 09:10:25 PST 2026


https://github.com/bd1976bris updated https://github.com/llvm/llvm-project/pull/176807

>From a4c53d660503e0a9d2c596bfab833d37e1ecfe30 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Tue, 13 Jan 2026 11:04:35 +0000
Subject: [PATCH 1/3] [DTLTO] Make temporary file handling consistent

DTLTO emits temporary files to allow distribution of archive
member inputs.

It also emits temporary files from the ThinLTO backend, such as
the index files needed for each distributed ThinLTO backend
compilation.

This change brings archive member temporary files into line with
those produced by the ThinLTO backend. They are now emitted in
the same location, warnings are emitted if they cannot be
deleted, and they are cleaned up on abnormal exit (e.g. Ctrl-C).
All temporary files are preserved when --save-temps is specified.

The existing signal-handling test has been extended to cover the
full set of DTLTO temporary files, and a new test has been added
to exercise temporary file handling in normal operation. The two
tests have been regularised as much as possible. Additionally, a
minimal test has been added to show the COFF behaviour and the
FileCheck match expressions have been corrected/regularised
throughout the tests.

SIE Internal tracker: TOOLCHAIN-21022
---
 cross-project-tests/dtlto/archive.test        |  26 +--
 .../dtlto/archives-mixed-lto-modes-test.test  |   8 +-
 .../dtlto/archives-same-module-id.test        |  14 +-
 .../dtlto/link-archive-thin.test              |  18 +-
 cross-project-tests/dtlto/link-savetemps.test |  44 +++++
 cross-project-tests/dtlto/savetemps.test      | 154 ++++++++++++++++++
 cross-project-tests/dtlto/signal.test         |  54 +++---
 lld/COFF/LTO.cpp                              |   6 +-
 lld/ELF/LTO.cpp                               |   7 +-
 llvm/include/llvm/DTLTO/DTLTO.h               |  17 +-
 llvm/lib/DTLTO/DTLTO.cpp                      |  24 ++-
 11 files changed, 305 insertions(+), 67 deletions(-)
 create mode 100644 cross-project-tests/dtlto/link-savetemps.test
 create mode 100644 cross-project-tests/dtlto/savetemps.test

diff --git a/cross-project-tests/dtlto/archive.test b/cross-project-tests/dtlto/archive.test
index 1c01ae7a5691a..e59850672bcef 100644
--- a/cross-project-tests/dtlto/archive.test
+++ b/cross-project-tests/dtlto/archive.test
@@ -23,29 +23,29 @@ RUN:   -Wl,--save-temps
 RUN: ls | FileCheck %s --check-prefix=OUTPUTS
 
 # JSON jobs description.
-OUTPUTS-DAG:    {{^}}main.[[PID:[0-9]+]].dist-file.json
+OUTPUTS-DAG:    {{^}}main.[[#PID:]].dist-file.json
 
 # Main source.
-OUTPUTS-DAG:    {{^}}main.{{[0-9]+}}.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}main.{{[0-9]+}}.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:    {{^}}main.1.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:    {{^}}main.1.[[#PID]].native.o.thinlto.bc{{$}}
 
 # Regular archive members.
 # Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o[.thinlto.bc].
-OUTPUTS-DAG:    {{^}}archive.a(boo.o at {{[0-9]+}}).2.[[HEXPID:[a-fA-F0-9]+]].2.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}archive.a(boo.o at {{[0-9]+}}).2.[[HEXPID]].2.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:    {{^}}archive.a(boo.o at [[#BOO_OFFSET:]]).2.[[HEXPID:[A-F0-9]+]].2.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:    {{^}}archive.a(boo.o at [[#BOO_OFFSET]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
 
-OUTPUTS-DAG:    {{^}}archive.a(foo.o at {{[0-9]+}}).3.[[HEXPID]].3.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}archive.a(foo.o at {{[0-9]+}}).3.[[HEXPID]].3.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:    {{^}}archive.a(foo.o at [[#FOO_OFFSET:]]).3.[[HEXPID]].3.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:    {{^}}archive.a(foo.o at [[#FOO_OFFSET]]).3.[[HEXPID]].3.[[#PID]].native.o.thinlto.bc{{$}}
 
-OUTPUTS-DAG:    {{^}}archive.a(moo.o at {{[0-9]+}}).4.[[HEXPID]].4.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}archive.a(moo.o at {{[0-9]+}}).4.[[HEXPID]].4.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:    {{^}}archive.a(moo.o at [[#MOO_OFFSET:]]).4.[[HEXPID]].4.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:    {{^}}archive.a(moo.o at [[#MOO_OFFSET]]).4.[[HEXPID]].4.[[#PID]].native.o.thinlto.bc{{$}}
 
 # Thin archive members.
-OUTPUTS-DAG:    {{^}}voo.{{[0-9]+}}.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}voo.{{[0-9]+}}.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:    {{^}}voo.5.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:    {{^}}voo.5.[[#PID]].native.o.thinlto.bc{{$}}
 
-OUTPUTS-DAG:    {{^}}loo.{{[0-9]+}}.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}loo.{{[0-9]+}}.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:    {{^}}loo.6.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:    {{^}}loo.6.[[#PID]].native.o.thinlto.bc{{$}}
 
 # Executable file.
 OUTPUTS-DAG:    {{^}}main.elf{{$}}
diff --git a/cross-project-tests/dtlto/archives-mixed-lto-modes-test.test b/cross-project-tests/dtlto/archives-mixed-lto-modes-test.test
index 74f146028b4b6..70a93c1755ac3 100644
--- a/cross-project-tests/dtlto/archives-mixed-lto-modes-test.test
+++ b/cross-project-tests/dtlto/archives-mixed-lto-modes-test.test
@@ -21,9 +21,11 @@ RUN:   -Wl,--save-temps,--lto-partitions=2
 # Show that the FullLTO modules have been prepared for distribution, this is
 # not optimal but has no functional impact.
 RUN: FileCheck %s --input-file=a.out.resolution.txt
-CHECK: archive.a(one.o at {{.*}}).1.[[PID:[a-zA-Z0-9_]+]].o
-CHECK: archive.a(two.o at {{.*}}).2.[[PID]].o
-CHECK: archive.a(three.o at {{.*}}).3.[[PID]].o
+
+# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.o.
+CHECK: archive.a(one.o at [[#ONE_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].o
+CHECK: archive.a(two.o at [[#TWO_OFFSET:]]).2.[[HEXPID]].o
+CHECK: archive.a(three.o at [[#THREE_OFFSET:]]).3.[[HEXPID]].o
 
 #--- one.c
 __attribute__((retain)) void one() {}
diff --git a/cross-project-tests/dtlto/archives-same-module-id.test b/cross-project-tests/dtlto/archives-same-module-id.test
index 09d5f7492bfa5..b750d9452885f 100644
--- a/cross-project-tests/dtlto/archives-same-module-id.test
+++ b/cross-project-tests/dtlto/archives-same-module-id.test
@@ -32,18 +32,18 @@ RUN:   -Wl,--thinlto-remote-compiler=%clang
 RUN: ls | FileCheck %s --check-prefix=OUTPUTS
 
 # JSON jobs description.
-OUTPUTS-DAG:    {{^}}main.[[PID:[0-9]+]].dist-file.json
+OUTPUTS-DAG: {{^}}main.[[#PID:]].dist-file.json
 
 # Sources.
-OUTPUTS-DAG:    {{^}}start.{{[0-9]+}}.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}start.{{[0-9]+}}.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG: {{^}}start.1.[[#PID]].native.o{{$}}
+OUTPUTS-DAG: {{^}}start.1.[[#PID]].native.o.thinlto.bc{{$}}
 
 # Archive members.
 # Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o[.thinlto.bc].
-OUTPUTS-DAG:    {{^}}archive.a(t3.o at {{[0-9]+}}).2.[[HEXPID:[a-fA-F0-9]+]].2.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}archive.a(t3.o at {{[0-9]+}}).2.[[HEXPID]].2.[[PID]].native.o.thinlto.bc{{$}}
-OUTPUTS-DAG:    {{^}}archive.a(t1.o at {{[0-9]+}}).3.[[HEXPID]].3.[[PID]].native.o{{$}}
-OUTPUTS-DAG:    {{^}}archive.a(t1.o at {{[0-9]+}}).3.[[HEXPID]].3.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG: {{^}}archive.a(t3.o at [[#T3_OFFSET:]]).2.[[HEXPID:[A-F0-9]+]].2.[[#PID]].native.o{{$}}
+OUTPUTS-DAG: {{^}}archive.a(t3.o at [[#T3_OFFSET]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG: {{^}}archive.a(t1.o at [[#T1_OFFSET:]]).3.[[HEXPID]].3.[[#PID]].native.o{{$}}
+OUTPUTS-DAG: {{^}}archive.a(t1.o at [[#T1_OFFSET]]).3.[[HEXPID]].3.[[#PID]].native.o.thinlto.bc{{$}}
 
 #--- t1.c
 __attribute__((retain)) void t1() { }
diff --git a/cross-project-tests/dtlto/link-archive-thin.test b/cross-project-tests/dtlto/link-archive-thin.test
index fbd8fd67300cf..e6bbb517cb32f 100644
--- a/cross-project-tests/dtlto/link-archive-thin.test
+++ b/cross-project-tests/dtlto/link-archive-thin.test
@@ -36,19 +36,19 @@ RUN:   /lldsavetemps
 RUN: ls | FileCheck %s --check-prefix=OUTPUTS --implicit-check-not=cat
 
 ## JSON jobs description.
-OUTPUTS-DAG: my.[[PID:[a-zA-Z0-9_]+]].dist-file.json
+OUTPUTS-DAG: my.[[#PID:]].dist-file.json
 
 ## Individual summary index files.
-OUTPUTS-DAG: start.1.[[PID]].native.o.thinlto.bc{{$}}
-OUTPUTS-DAG:   dog.2.[[PID]].native.o.thinlto.bc{{$}}
-OUTPUTS-DAG:   foo.3.[[PID]].native.o.thinlto.bc{{$}}
-OUTPUTS-DAG:   bar.4.[[PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG: start.1.[[#PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:   dog.2.[[#PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:   foo.3.[[#PID]].native.o.thinlto.bc{{$}}
+OUTPUTS-DAG:   bar.4.[[#PID]].native.o.thinlto.bc{{$}}
 
 ## Native output object files.
-OUTPUTS-DAG: start.1.[[PID]].native.o{{$}}
-OUTPUTS-DAG:   dog.2.[[PID]].native.o{{$}}
-OUTPUTS-DAG:   foo.3.[[PID]].native.o{{$}}
-OUTPUTS-DAG:   bar.4.[[PID]].native.o{{$}}
+OUTPUTS-DAG: start.1.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:   dog.2.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:   foo.3.[[#PID]].native.o{{$}}
+OUTPUTS-DAG:   bar.4.[[#PID]].native.o{{$}}
 
 
 ## It is important that cross-module inlining occurs for this test to show that Clang can
diff --git a/cross-project-tests/dtlto/link-savetemps.test b/cross-project-tests/dtlto/link-savetemps.test
new file mode 100644
index 0000000000000..00a8960ed79c1
--- /dev/null
+++ b/cross-project-tests/dtlto/link-savetemps.test
@@ -0,0 +1,44 @@
+REQUIRES: lld-link
+
+# Test that DTLTO temporary files are "best-effort" cleaned up unless
+# --save-temps is specified. We use archives in this test as the handling for
+# archives requires a superset of the temporary files used for object inputs.
+
+RUN: rm -rf %t && split-file %s %t && cd %t
+
+RUN: %clang --target=x86_64-pc-windows-msvc -O2 t1.c -flto=thin -c
+
+RUN: lld-link /lib /out:t.lib t1.o
+
+DEFINE: %{tdir} = dummy-to-make-lit-work
+DEFINE: %{dtlto} = mkdir %{tdir} && \
+DEFINE:   lld-link /subsystem:console /machine:x64 /out:%{tdir}/my.exe  \
+DEFINE:     /wholearchive:t.lib \
+DEFINE:     -thinlto-distributor:%python \
+DEFINE:     -thinlto-distributor-arg:%llvm_src_root/utils/dtlto/local.py \
+DEFINE:     -thinlto-remote-compiler:%clang
+
+# Check that temporary files are removed normally.
+REDEFINE: %{tdir} = empty
+RUN: %{dtlto}
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,ELF
+
+# Check that --save-temps preserves temporary files.
+REDEFINE: %{tdir} = savetemps
+RUN: %{dtlto} /lldsavetemps
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,ELF,OTHER
+
+# No files are expected before.
+BOOKEND-NOT: {{.}}
+TEMPS: {{^}}my.[[#PID:]].dist-file.json{{$}}
+ELF:   {{^}}my.exe{{$}}
+OTHER: {{^}}my.exe.resolution.txt{{$}}
+# Filename composition: <archive><member><member offset>.<task>.<pid>.<task>.<pid>.native.o.
+TEMPS: {{^}}t.libt1.o[[#T1_OFFSET:]].1.[[HEXPID:[A-Z0-9]+]].1.[[#PID]].native.o{{$}}
+TEMPS: {{^}}t.libt1.o[[#T1_OFFSET]].1.[[HEXPID]].1.[[#PID]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.libt1.o[[#T1_OFFSET]].1.[[HEXPID]].o{{$}}
+# No files are expected after.
+BOOKEND-NOT: {{.}}
+
+#--- t1.c
+__attribute__((retain)) int mainCRTStartup() { return 0; }
diff --git a/cross-project-tests/dtlto/savetemps.test b/cross-project-tests/dtlto/savetemps.test
new file mode 100644
index 0000000000000..df50fd9c5b680
--- /dev/null
+++ b/cross-project-tests/dtlto/savetemps.test
@@ -0,0 +1,154 @@
+# This test relies on locking files which is difficult to do in a robust test
+# on Linux.
+REQUIRES: ld.lld,system-windows
+
+# Test that DTLTO temporary files are "best-effort" cleaned up unless
+# --save-temps is specified. We use archives in this test as the handling for
+# archives requires a superset of the temporary files used for object inputs.
+
+RUN: rm -rf %t && split-file %s %t && cd %t
+
+RUN: %clang --target=x86_64-linux-gnu -O2 t1.c t2.c -flto=thin -c
+
+RUN: llvm-ar rcs t.a t1.o t2.o
+
+DEFINE: %{tdir} = dummy-to-make-lit-work
+DEFINE: %{locktemps-dtlto} = \
+DEFINE:   rm -f send-signal1 && rm -f send-signal2 && mkdir %{tdir} && \
+DEFINE:   %python locktemps.py %{tdir} \
+DEFINE:     %clang --target=x86_64-linux-gnu -nostdlib -O2 -flto=thin \
+DEFINE:       -fuse-ld=lld -Wl,--whole-archive t.a -o %{tdir}/t.elf -shared \
+DEFINE:       -fthinlto-distributor=%python \
+DEFINE:       -Xthinlto-distributor=local_codegen_and_wait.py
+
+# Check that all temporary files are removed in normal operation.
+REDEFINE: %{tdir} = empty
+RUN: %{locktemps-dtlto}
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,ELF
+
+# Check that --save-temps preserves temporary files.
+REDEFINE: %{tdir} = savetemps
+RUN: mkdir %{tdir}
+RUN: %{locktemps-dtlto} -Wl,--save-temps
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,INDEX,ELF,OTHER
+
+# Check that --thinlto-emit-index-files preserves the index files.
+REDEFINE: %{tdir} = index
+RUN: %{locktemps-dtlto} -Wl,--thinlto-emit-index-files
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,INDEX,ELF
+
+# No files are expected before.
+BOOKEND-NOT: {{.}}
+TEMPS: {{^}}t.[[#PID:]].dist-file.json{{$}}
+# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
+TEMPS: {{^}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].1.[[#PID:]].native.o{{$}}
+INDEX: {{^}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].1.[[#PID:]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.a(t1.o at [[#T1_OFFSET]]).1.[[HEXPID]].o{{$}}
+TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o{{$}}
+INDEX: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].o{{$}}
+ELF:   {{^}}t.elf{{$}}
+OTHER: {{^}}t.elf.resolution.txt{{$}}
+# No files are expected after.
+BOOKEND-NOT: {{.}}
+
+# Check that a warning is reported for each file that can't be removed.
+REDEFINE: %{tdir} = locked
+RUN: %{locktemps-dtlto} 2>&1 \
+RUN:   | FileCheck %s --check-prefixes=WARN --implicit-check-not=warning
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,INDEX,ELF
+
+WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.[[#PID:]].dist-file.json': {{.*}}
+# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
+WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].1.[[#PID]].native.o': {{.*}}
+WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET]]).1.[[HEXPID]].1.[[#PID]].native.o.thinlto.bc': {{.*}}
+WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o': {{.*}}
+WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc': {{.*}}
+WARN-DAG: warning: could not remove temporary DTLTO input file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET]]).1.[[HEXPID]].o': {{.*}}
+WARN-DAG: warning: could not remove temporary DTLTO input file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].o': {{.*}}
+
+# Check that no warnings are produced if temporary files are missing.
+REDEFINE: %{tdir} = removed
+RUN: %{locktemps-dtlto} 2>&1 | FileCheck %s --check-prefix=NOWARN --allow-empty
+RUN: ls %{tdir} | sort |  FileCheck %s --check-prefixes=BOOKEND,ELF
+NOWARN-NOT: warning
+
+#--- t1.c
+__attribute__((retain)) int t1(int x) { return x; }
+
+#--- t2.c
+__attribute__((retain)) int t2(int x) { return x; }
+
+#--- local_codegen_and_wait.py
+"""Perform codegen locally, create "send-signal1" file and wait
+for "send-signal2" file to exist before exiting."""
+from pathlib import Path
+import json, subprocess, sys, time, os
+
+# Load the DTLTO information from the input JSON file.
+data = json.loads(Path(sys.argv[-1]).read_bytes())
+
+# Iterate over the jobs and execute the codegen tool.
+for job in data["jobs"]:
+    subprocess.check_call(data["common"]["args"] + job["args"])
+Path("send-signal1").touch()
+while not os.path.exists("send-signal2"):
+    time.sleep(0.05)
+
+#--- locktemps.py
+"""Run command, wait for "send-signal1" file to exist, lock the DTLTO
+temporary files then create "send-signal2" file."""
+import ctypes, os, subprocess, sys, time
+from ctypes import wintypes
+from pathlib import Path
+
+CreateFileW = ctypes.WinDLL("kernel32", use_last_error=True).CreateFileW
+CreateFileW.argtypes = [
+    wintypes.LPCWSTR, # lpFileName
+    wintypes.DWORD,   # dwDesiredAccess
+    wintypes.DWORD,   # dwShareMode
+    wintypes.LPVOID,  # lpSecurityAttributes
+    wintypes.DWORD,   # dwCreationDisposition
+    wintypes.DWORD,   # dwFlagsAndAttributes
+    wintypes.HANDLE,  # hTemplateFile
+]
+CreateFileW.restype = wintypes.HANDLE
+
+def lock_no_delete_share(path):
+    h = CreateFileW(
+        path,
+        0x80000000, # GENERIC_READ
+        0x00000003, # FILE_SHARE_READ/WRITE (no FILE_SHARE_DELETE)
+        None,       # lpSecurityAttributes
+        3,          # OPEN_EXISTING
+        0,          # dwFlagsAndAttributes
+        None,       # hTemplateFile
+    )
+    if h == wintypes.HANDLE(-1).value:
+        err = ctypes.get_last_error()
+        raise OSError(err, f"CreateFileW failed ({err}) for: {path}")
+    return h
+
+output_dir = sys.argv[1]
+p = subprocess.Popen(sys.argv[2:])
+
+while not os.path.exists("send-signal1") and p.poll() is None:
+    time.sleep(0.05)
+if p.poll() is not None:
+    sys.exit(1)
+
+if output_dir == "locked":
+    # lock any files in the output directory.
+    for f in Path(output_dir).iterdir():
+        if f.is_file():
+            lock_no_delete_share(str(f))
+
+if output_dir == "removed":
+    # remove non-essential files in the output directory.
+    for f in Path(output_dir).iterdir():
+        if f.is_file() and not f.name.endswith("native.o"):
+            f.unlink()
+
+Path("send-signal2").touch()
+
+sys.exit(p.wait())
diff --git a/cross-project-tests/dtlto/signal.test b/cross-project-tests/dtlto/signal.test
index 95b6bf368b577..3d4cd13258938 100644
--- a/cross-project-tests/dtlto/signal.test
+++ b/cross-project-tests/dtlto/signal.test
@@ -1,42 +1,52 @@
 REQUIRES: ld.lld
 
 # Test that if a link is terminated by a signal (or the equivalent on
-# Windows), e.g. CTRL-C, DTLTO temporary files are cleaned up.
+# Windows), e.g. CTRL-C, DTLTO temporary files are cleaned up. We use
+# archives in this test as the handling for archives requires a superset
+# of the temporary files used for object inputs.
 
 RUN: rm -rf %t && split-file %s %t && cd %t
 
 RUN: %clang --target=x86_64-linux-gnu -O2 t1.c t2.c -flto=thin -c
 
-DEFINE: %{kill-dtlto} = %python killer.py %clang --target=x86_64-linux-gnu \
-DEFINE:   -nostdlib -O2 -flto=thin -fuse-ld=lld t1.o t2.o \
-DEFINE:   -fthinlto-distributor=%python \
-DEFINE:   -Xthinlto-distributor=local_codegen_and_wait.py
+RUN: llvm-ar rcs t.a t1.o t2.o
+
+DEFINE: %{tdir} = dummy-to-make-lit-work
+DEFINE: %{kill-dtlto} = rm -f send-signal && mkdir %{tdir} && \
+DEFINE:   %python killer.py \
+DEFINE:     %clang --target=x86_64-linux-gnu -nostdlib -O2 -flto=thin \
+DEFINE:       -fuse-ld=lld -Wl,--whole-archive t.a -o %{tdir}/t.elf -shared \
+DEFINE:       -fthinlto-distributor=%python \
+DEFINE:       -Xthinlto-distributor=local_codegen_and_wait.py
 
 # Check that all temporary files are removed if the process is aborted.
-RUN: mkdir none
-RUN: %{kill-dtlto} -o none/t.elf
-RUN: ls none | FileCheck %s --allow-empty --check-prefix=EMPTY
+REDEFINE: %{tdir} = empty
+RUN: %{kill-dtlto}
+RUN: ls %{tdir} | FileCheck %s --allow-empty --check-prefix=EMPTY
 
 EMPTY-NOT: {{.}}
 
-# Check that --save-temps preserves all the files.
-RUN: rm send-signal && mkdir all
-RUN: %{kill-dtlto} -Wl,--save-temps -o all/t.elf
-RUN: ls all | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,INDEX
+# Check that --save-temps preserves temporary files if the process is aborted.
+REDEFINE: %{tdir} = savetemps
+RUN: %{kill-dtlto} -Wl,--save-temps
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,INDEX,OTHER
 
-# Check that --thinlto-emit-index-files preserves the index files.
-RUN: rm send-signal && mkdir index
-RUN: %{kill-dtlto} -Wl,--thinlto-emit-index-files -o index/t.elf
-RUN: ls index | sort | FileCheck %s --check-prefixes=BOOKEND,INDEX
+# Check that --thinlto-emit-index-files preserves the index files if the process is aborted.
+REDEFINE: %{tdir} = index
+RUN: %{kill-dtlto} -Wl,--thinlto-emit-index-files
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,INDEX
 
 # No files are expected before.
 BOOKEND-NOT: {{.}}
-TEMPS: {{^}}t.[[#]].dist-file.json{{$}}
-TEMPS: {{^}}t.elf.{{.+$}}
-TEMPS: {{^}}t1.1.[[#]].native.o{{$}}
-INDEX: {{^}}t1.1.[[#]].native.o.thinlto.bc{{$}}
-TEMPS: {{^}}t2.2.[[#]].native.o{{$}}
-INDEX: {{^}}t2.2.[[#]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.[[#PID:]].dist-file.json{{$}}
+# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
+TEMPS: {{^}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].1.[[#PID]].native.o{{$}}
+INDEX: {{^}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].1.[[#PID:]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.a(t1.o at [[#T1_OFFSET]]).1.[[HEXPID]].o{{$}}
+TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o{{$}}
+INDEX: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].o{{$}}
+OTHER: {{^}}t.elf.resolution.txt{{$}}
 # No files are expected after.
 BOOKEND-NOT: {{.}}
 
diff --git a/lld/COFF/LTO.cpp b/lld/COFF/LTO.cpp
index e621bb263d2c9..d55f95493a85f 100644
--- a/lld/COFF/LTO.cpp
+++ b/lld/COFF/LTO.cpp
@@ -142,8 +142,10 @@ BitcodeCompiler::BitcodeCompiler(COFFLinkerContext &c) : ctx(c) {
     ltoObj = std::make_unique<lto::LTO>(createConfig(), backend,
                                         ctx.config.ltoPartitions);
   else
-    ltoObj = std::make_unique<lto::DTLTO>(createConfig(), backend,
-                                          ctx.config.ltoPartitions);
+    ltoObj = std::make_unique<lto::DTLTO>(
+        createConfig(), backend, ctx.config.ltoPartitions,
+        llvm::lto::LTO::LTOKind::LTOK_Default, ctx.config.outputFile,
+        !ctx.config.saveTempsArgs.empty());
 }
 
 BitcodeCompiler::~BitcodeCompiler() = default;
diff --git a/lld/ELF/LTO.cpp b/lld/ELF/LTO.cpp
index 44a679498ed1d..a43d7b321a02f 100644
--- a/lld/ELF/LTO.cpp
+++ b/lld/ELF/LTO.cpp
@@ -205,9 +205,10 @@ BitcodeCompiler::BitcodeCompiler(Ctx &ctx) : ctx(ctx) {
                                         ctx.arg.ltoPartitions,
                                         ltoModes[ctx.arg.ltoKind]);
   else
-    ltoObj = std::make_unique<lto::DTLTO>(createConfig(ctx), backend,
-                                          ctx.arg.ltoPartitions,
-                                          ltoModes[ctx.arg.ltoKind]);
+    ltoObj = std::make_unique<lto::DTLTO>(
+        createConfig(ctx), backend, ctx.arg.ltoPartitions,
+        ltoModes[ctx.arg.ltoKind], ctx.arg.outputFile,
+        !ctx.arg.saveTempsArgs.empty());
   // Initialize usedStartStop.
   if (ctx.bitcodeFiles.empty())
     return;
diff --git a/llvm/include/llvm/DTLTO/DTLTO.h b/llvm/include/llvm/DTLTO/DTLTO.h
index 14f1f5fd00e30..02b098a68aec5 100644
--- a/llvm/include/llvm/DTLTO/DTLTO.h
+++ b/llvm/include/llvm/DTLTO/DTLTO.h
@@ -19,9 +19,14 @@ class DTLTO : public LTO {
   using Base = LTO;
 
 public:
-  // Inherit constructors.
-  using Base::Base;
-  ~DTLTO() override = default;
+  LLVM_ABI DTLTO(Config Conf, ThinBackend Backend,
+                 unsigned ParallelCodeGenParallelismLevel, LTOKind LTOMode,
+                 StringRef LinkerOutputFile, bool SaveTemps)
+      : Base(std::move(Conf), Backend, ParallelCodeGenParallelismLevel,
+             LTOMode),
+        LinkerOutputFile(LinkerOutputFile), SaveTemps(SaveTemps) {
+    assert(!LinkerOutputFile.empty() && "expected a valid linker output file");
+  }
 
   // Add an input file and prepare it for distribution.
   LLVM_ABI Expected<std::shared_ptr<InputFile>>
@@ -37,6 +42,12 @@ class DTLTO : public LTO {
   BumpPtrAllocator PtrAlloc;
   StringSaver Saver{PtrAlloc};
 
+  /// The output file to which this LTO invocation will contribute.
+  StringRef LinkerOutputFile;
+
+  /// Controls preservation of any created temporary files.
+  bool SaveTemps;
+
   // Determines if a file at the given path is a thin archive file.
   Expected<bool> isThinArchive(const StringRef ArchivePath);
 
diff --git a/llvm/lib/DTLTO/DTLTO.cpp b/llvm/lib/DTLTO/DTLTO.cpp
index b9a5cd3a062e2..a94f597cab914 100644
--- a/llvm/lib/DTLTO/DTLTO.cpp
+++ b/llvm/lib/DTLTO/DTLTO.cpp
@@ -25,6 +25,7 @@
 #include "llvm/Support/MemoryBufferRef.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/Process.h"
+#include "llvm/Support/Signals.h"
 #include "llvm/Support/TimeProfiler.h"
 #include "llvm/Support/raw_ostream.h"
 
@@ -158,7 +159,9 @@ lto::DTLTO::addInput(std::unique_ptr<lto::InputFile> InputPtr) {
     std::string PID = utohexstr(sys::Process::getProcessId());
     std::string Seq = std::to_string(InputFiles.size());
 
-    NewModuleId = {sys::path::filename(ModuleId), ".", Seq, ".", PID, ".o"};
+    NewModuleId = sys::path::parent_path(LinkerOutputFile);
+    sys::path::append(NewModuleId, sys::path::filename(ModuleId) + "." + Seq +
+                                       "." + PID + ".o");
   }
 
   // Update the module identifier and save it.
@@ -174,6 +177,9 @@ Error lto::DTLTO::saveInputArchiveMember(lto::InputFile *Input) {
   StringRef ModuleId = Input->getName();
   if (Input->isMemberOfArchive()) {
     TimeTraceScope TimeScope("Save input archive member for DTLTO", ModuleId);
+    // Cleanup this file on abnormal process exit.
+    if (!SaveTemps)
+      llvm::sys::RemoveFileOnSignal(ModuleId);
     MemoryBufferRef MemoryBufferRef = Input->getFileBuffer();
     if (Error EC = saveBuffer(MemoryBufferRef.getBuffer(), ModuleId))
       return EC;
@@ -207,11 +213,19 @@ llvm::Error lto::DTLTO::handleArchiveInputs() {
 
 // Remove temporary archive member files created to enable distribution.
 void lto::DTLTO::cleanup() {
-  {
+  if (!SaveTemps) {
     TimeTraceScope TimeScope("Remove temporary inputs for DTLTO");
-    for (auto &Input : InputFiles)
-      if (Input->isMemberOfArchive())
-        sys::fs::remove(Input->getName(), /*IgnoreNonExisting=*/true);
+    for (auto &Input : InputFiles) {
+      if (!Input->isMemberOfArchive())
+        continue;
+
+      std::error_code EC =
+          sys::fs::remove(Input->getName(), /*IgnoreNonExisting=*/true);
+      if (EC &&
+          EC != std::make_error_code(std::errc::no_such_file_or_directory))
+        errs() << "warning: could not remove temporary DTLTO input file '"
+               << Input->getName() << "': " << EC.message() << "\n";
+    }
   }
   Base::cleanup();
 }

>From 5f4be2936624c96b297d2f260ad041ff2abdb0f9 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Tue, 20 Jan 2026 15:24:56 +0000
Subject: [PATCH 2/3] Run more test-cases on Linux

Split out the locking files test-case from savetemps.test to
savetemps-lock.test so that the remaining test-cases can be
run on Linux.
---
 cross-project-tests/dtlto/archive.test        |   2 +-
 .../dtlto/archives-same-module-id.test        |   2 +-
 .../dtlto/local_codegen_and_wait.py           |  16 +++
 cross-project-tests/dtlto/savetemps-lock.test |  43 +++++++
 cross-project-tests/dtlto/savetemps.test      | 112 ++----------------
 cross-project-tests/dtlto/test_temps.py       |  61 ++++++++++
 llvm/lib/DTLTO/DTLTO.cpp                      |   1 -
 7 files changed, 132 insertions(+), 105 deletions(-)
 create mode 100644 cross-project-tests/dtlto/local_codegen_and_wait.py
 create mode 100644 cross-project-tests/dtlto/savetemps-lock.test
 create mode 100644 cross-project-tests/dtlto/test_temps.py

diff --git a/cross-project-tests/dtlto/archive.test b/cross-project-tests/dtlto/archive.test
index e59850672bcef..30398ee7ebad7 100644
--- a/cross-project-tests/dtlto/archive.test
+++ b/cross-project-tests/dtlto/archive.test
@@ -30,7 +30,7 @@ OUTPUTS-DAG:    {{^}}main.1.[[#PID]].native.o{{$}}
 OUTPUTS-DAG:    {{^}}main.1.[[#PID]].native.o.thinlto.bc{{$}}
 
 # Regular archive members.
-# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o[.thinlto.bc].
+# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
 OUTPUTS-DAG:    {{^}}archive.a(boo.o at [[#BOO_OFFSET:]]).2.[[HEXPID:[A-F0-9]+]].2.[[#PID]].native.o{{$}}
 OUTPUTS-DAG:    {{^}}archive.a(boo.o at [[#BOO_OFFSET]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
 
diff --git a/cross-project-tests/dtlto/archives-same-module-id.test b/cross-project-tests/dtlto/archives-same-module-id.test
index b750d9452885f..3f94c68914798 100644
--- a/cross-project-tests/dtlto/archives-same-module-id.test
+++ b/cross-project-tests/dtlto/archives-same-module-id.test
@@ -39,7 +39,7 @@ OUTPUTS-DAG: {{^}}start.1.[[#PID]].native.o{{$}}
 OUTPUTS-DAG: {{^}}start.1.[[#PID]].native.o.thinlto.bc{{$}}
 
 # Archive members.
-# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o[.thinlto.bc].
+# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
 OUTPUTS-DAG: {{^}}archive.a(t3.o at [[#T3_OFFSET:]]).2.[[HEXPID:[A-F0-9]+]].2.[[#PID]].native.o{{$}}
 OUTPUTS-DAG: {{^}}archive.a(t3.o at [[#T3_OFFSET]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
 OUTPUTS-DAG: {{^}}archive.a(t1.o at [[#T1_OFFSET:]]).3.[[HEXPID]].3.[[#PID]].native.o{{$}}
diff --git a/cross-project-tests/dtlto/local_codegen_and_wait.py b/cross-project-tests/dtlto/local_codegen_and_wait.py
new file mode 100644
index 0000000000000..72b72419a3329
--- /dev/null
+++ b/cross-project-tests/dtlto/local_codegen_and_wait.py
@@ -0,0 +1,16 @@
+"""Perform codegen locally, create "send-signal1" file and wait
+for "send-signal2" file to exist before exiting."""
+
+import json, subprocess, sys, time, os, pathlib
+
+# Load the DTLTO information from the input JSON file.
+data = json.loads(pathlib.Path(sys.argv[-1]).read_bytes())
+
+# Iterate over the jobs and execute the codegen tool.
+for job in data["jobs"]:
+    subprocess.check_call(data["common"]["args"] + job["args"])
+
+pathlib.Path("send-signal1").touch()
+
+while not os.path.exists("send-signal2"):
+    time.sleep(0.05)
diff --git a/cross-project-tests/dtlto/savetemps-lock.test b/cross-project-tests/dtlto/savetemps-lock.test
new file mode 100644
index 0000000000000..d93c6cf0767e1
--- /dev/null
+++ b/cross-project-tests/dtlto/savetemps-lock.test
@@ -0,0 +1,43 @@
+# This test relies on locking files which is difficult to do in a way the keeps
+# a test robust on Linux, so it is restricted to Windows.
+REQUIRES: ld.lld,system-windows
+
+# Test that a warning is emitted for each DTLTO temporary file that cannot be
+# removed. This test uses archives because archive handling exercises a superset
+# of the temporary files used for object inputs.
+#
+# This scenario is logically related to the cases in savetemps.test; however, it
+# is placed here to maintain coverage, as this behavior can only be tested
+# effectively on Windows.
+
+RUN: rm -rf %t && split-file %s %t && cd %t
+
+RUN: %clang --target=x86_64-linux-gnu -O2 t1.c t2.c -flto=thin -c
+
+RUN: llvm-ar rcs t.a t1.o t2.o
+
+# Check that a warning is reported for each temporary file that cannot be
+# removed. Note that the use of the name "locked" for the output directory
+# triggers special behaviour in test_temps.py.
+RUN: rm -f send-signal1 && rm -f send-signal2 && mkdir locked
+RUN: %python %S/test_temps.py locked \
+RUN:     %clang --target=x86_64-linux-gnu -nostdlib -O2 -flto=thin \
+RUN:       -fuse-ld=lld -Wl,--whole-archive t.a -o locked/t.elf -shared \
+RUN:       -fthinlto-distributor=%python \
+RUN:       -Xthinlto-distributor=%S/local_codegen_and_wait.py 2>&1 \
+RUN:         | FileCheck %s --implicit-check-not=warning
+
+CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.[[#PID:]].dist-file.json': {{.*}}
+# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
+CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].1.[[#PID]].native.o': {{.*}}
+CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET]]).1.[[HEXPID]].1.[[#PID]].native.o.thinlto.bc': {{.*}}
+CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o': {{.*}}
+CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc': {{.*}}
+CHECK-DAG: warning: could not remove temporary DTLTO input file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET]]).1.[[HEXPID]].o': {{.*}}
+CHECK-DAG: warning: could not remove temporary DTLTO input file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].o': {{.*}}
+
+#--- t1.c
+__attribute__((retain)) int t1(int x) { return x; }
+
+#--- t2.c
+__attribute__((retain)) int t2(int x) { return x; }
diff --git a/cross-project-tests/dtlto/savetemps.test b/cross-project-tests/dtlto/savetemps.test
index df50fd9c5b680..0d9a64e1b0e30 100644
--- a/cross-project-tests/dtlto/savetemps.test
+++ b/cross-project-tests/dtlto/savetemps.test
@@ -1,6 +1,4 @@
-# This test relies on locking files which is difficult to do in a robust test
-# on Linux.
-REQUIRES: ld.lld,system-windows
+REQUIRES: ld.lld
 
 # Test that DTLTO temporary files are "best-effort" cleaned up unless
 # --save-temps is specified. We use archives in this test as the handling for
@@ -13,28 +11,27 @@ RUN: %clang --target=x86_64-linux-gnu -O2 t1.c t2.c -flto=thin -c
 RUN: llvm-ar rcs t.a t1.o t2.o
 
 DEFINE: %{tdir} = dummy-to-make-lit-work
-DEFINE: %{locktemps-dtlto} = \
+DEFINE: %{test-temps-dtlto} = \
 DEFINE:   rm -f send-signal1 && rm -f send-signal2 && mkdir %{tdir} && \
-DEFINE:   %python locktemps.py %{tdir} \
+DEFINE:   %python %S/test_temps.py %{tdir} \
 DEFINE:     %clang --target=x86_64-linux-gnu -nostdlib -O2 -flto=thin \
 DEFINE:       -fuse-ld=lld -Wl,--whole-archive t.a -o %{tdir}/t.elf -shared \
 DEFINE:       -fthinlto-distributor=%python \
-DEFINE:       -Xthinlto-distributor=local_codegen_and_wait.py
+DEFINE:       -Xthinlto-distributor=%S/local_codegen_and_wait.py
 
 # Check that all temporary files are removed in normal operation.
 REDEFINE: %{tdir} = empty
-RUN: %{locktemps-dtlto}
+RUN: %{test-temps-dtlto}
 RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,ELF
 
 # Check that --save-temps preserves temporary files.
 REDEFINE: %{tdir} = savetemps
-RUN: mkdir %{tdir}
-RUN: %{locktemps-dtlto} -Wl,--save-temps
-RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,INDEX,ELF,OTHER
+RUN: %{test-temps-dtlto} -Wl,--save-temps
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,INDEX,ELF
 
 # Check that --thinlto-emit-index-files preserves the index files.
 REDEFINE: %{tdir} = index
-RUN: %{locktemps-dtlto} -Wl,--thinlto-emit-index-files
+RUN: %{test-temps-dtlto} -Wl,--thinlto-emit-index-files
 RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,INDEX,ELF
 
 # No files are expected before.
@@ -48,28 +45,13 @@ TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o{{$}}
 INDEX: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
 TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].o{{$}}
 ELF:   {{^}}t.elf{{$}}
-OTHER: {{^}}t.elf.resolution.txt{{$}}
+TEMPS: {{^}}t.elf.resolution.txt{{$}}
 # No files are expected after.
 BOOKEND-NOT: {{.}}
 
-# Check that a warning is reported for each file that can't be removed.
-REDEFINE: %{tdir} = locked
-RUN: %{locktemps-dtlto} 2>&1 \
-RUN:   | FileCheck %s --check-prefixes=WARN --implicit-check-not=warning
-RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,INDEX,ELF
-
-WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.[[#PID:]].dist-file.json': {{.*}}
-# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
-WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].1.[[#PID]].native.o': {{.*}}
-WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET]]).1.[[HEXPID]].1.[[#PID]].native.o.thinlto.bc': {{.*}}
-WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[HEXPID]].2.[[#PID]].native.o': {{.*}}
-WARN-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].2.[[#PID]].native.o.thinlto.bc': {{.*}}
-WARN-DAG: warning: could not remove temporary DTLTO input file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET]]).1.[[HEXPID]].o': {{.*}}
-WARN-DAG: warning: could not remove temporary DTLTO input file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET]]).2.[[HEXPID]].o': {{.*}}
-
 # Check that no warnings are produced if temporary files are missing.
 REDEFINE: %{tdir} = removed
-RUN: %{locktemps-dtlto} 2>&1 | FileCheck %s --check-prefix=NOWARN --allow-empty
+RUN: %{test-temps-dtlto} 2>&1 | FileCheck %s --check-prefix=NOWARN --allow-empty
 RUN: ls %{tdir} | sort |  FileCheck %s --check-prefixes=BOOKEND,ELF
 NOWARN-NOT: warning
 
@@ -78,77 +60,3 @@ __attribute__((retain)) int t1(int x) { return x; }
 
 #--- t2.c
 __attribute__((retain)) int t2(int x) { return x; }
-
-#--- local_codegen_and_wait.py
-"""Perform codegen locally, create "send-signal1" file and wait
-for "send-signal2" file to exist before exiting."""
-from pathlib import Path
-import json, subprocess, sys, time, os
-
-# Load the DTLTO information from the input JSON file.
-data = json.loads(Path(sys.argv[-1]).read_bytes())
-
-# Iterate over the jobs and execute the codegen tool.
-for job in data["jobs"]:
-    subprocess.check_call(data["common"]["args"] + job["args"])
-Path("send-signal1").touch()
-while not os.path.exists("send-signal2"):
-    time.sleep(0.05)
-
-#--- locktemps.py
-"""Run command, wait for "send-signal1" file to exist, lock the DTLTO
-temporary files then create "send-signal2" file."""
-import ctypes, os, subprocess, sys, time
-from ctypes import wintypes
-from pathlib import Path
-
-CreateFileW = ctypes.WinDLL("kernel32", use_last_error=True).CreateFileW
-CreateFileW.argtypes = [
-    wintypes.LPCWSTR, # lpFileName
-    wintypes.DWORD,   # dwDesiredAccess
-    wintypes.DWORD,   # dwShareMode
-    wintypes.LPVOID,  # lpSecurityAttributes
-    wintypes.DWORD,   # dwCreationDisposition
-    wintypes.DWORD,   # dwFlagsAndAttributes
-    wintypes.HANDLE,  # hTemplateFile
-]
-CreateFileW.restype = wintypes.HANDLE
-
-def lock_no_delete_share(path):
-    h = CreateFileW(
-        path,
-        0x80000000, # GENERIC_READ
-        0x00000003, # FILE_SHARE_READ/WRITE (no FILE_SHARE_DELETE)
-        None,       # lpSecurityAttributes
-        3,          # OPEN_EXISTING
-        0,          # dwFlagsAndAttributes
-        None,       # hTemplateFile
-    )
-    if h == wintypes.HANDLE(-1).value:
-        err = ctypes.get_last_error()
-        raise OSError(err, f"CreateFileW failed ({err}) for: {path}")
-    return h
-
-output_dir = sys.argv[1]
-p = subprocess.Popen(sys.argv[2:])
-
-while not os.path.exists("send-signal1") and p.poll() is None:
-    time.sleep(0.05)
-if p.poll() is not None:
-    sys.exit(1)
-
-if output_dir == "locked":
-    # lock any files in the output directory.
-    for f in Path(output_dir).iterdir():
-        if f.is_file():
-            lock_no_delete_share(str(f))
-
-if output_dir == "removed":
-    # remove non-essential files in the output directory.
-    for f in Path(output_dir).iterdir():
-        if f.is_file() and not f.name.endswith("native.o"):
-            f.unlink()
-
-Path("send-signal2").touch()
-
-sys.exit(p.wait())
diff --git a/cross-project-tests/dtlto/test_temps.py b/cross-project-tests/dtlto/test_temps.py
new file mode 100644
index 0000000000000..b4b1f196dd6b1
--- /dev/null
+++ b/cross-project-tests/dtlto/test_temps.py
@@ -0,0 +1,61 @@
+"""Run command, wait for "send-signal1" file to exist, perform some action on
+the DTLTO files (such as locking them) the action performed is based on the test
+directory name, then create "send-signal2" file."""
+
+import ctypes, os, subprocess, sys, time
+from ctypes import wintypes
+from pathlib import Path
+
+if os.name == "nt":
+    CreateFileW = ctypes.WinDLL("kernel32", use_last_error=True).CreateFileW
+    CreateFileW.argtypes = [
+        wintypes.LPCWSTR,  # lpFileName
+        wintypes.DWORD,  # dwDesiredAccess
+        wintypes.DWORD,  # dwShareMode
+        wintypes.LPVOID,  # lpSecurityAttributes
+        wintypes.DWORD,  # dwCreationDisposition
+        wintypes.DWORD,  # dwFlagsAndAttributes
+        wintypes.HANDLE,  # hTemplateFile
+    ]
+    CreateFileW.restype = wintypes.HANDLE
+
+
+def lock_no_delete_share(path):
+    h = CreateFileW(
+        path,
+        0x80000000,  # GENERIC_READ
+        0x00000003,  # FILE_SHARE_READ/WRITE (no FILE_SHARE_DELETE)
+        None,  # lpSecurityAttributes
+        3,  # OPEN_EXISTING
+        0,  # dwFlagsAndAttributes
+        None,  # hTemplateFile
+    )
+    if h == wintypes.HANDLE(-1).value:
+        err = ctypes.get_last_error()
+        raise OSError(err, f"CreateFileW failed ({err}) for: {path}")
+    return h
+
+
+output_dir = sys.argv[1]
+p = subprocess.Popen(sys.argv[2:])
+
+while not os.path.exists("send-signal1") and p.poll() is None:
+    time.sleep(0.05)
+if p.poll() is not None:
+    sys.exit(1)
+
+if output_dir == "locked":
+    # lock any files in the output directory.
+    for f in Path(output_dir).iterdir():
+        if f.is_file():
+            lock_no_delete_share(str(f))
+
+if output_dir == "removed":
+    # remove non-essential files in the output directory.
+    for f in Path(output_dir).iterdir():
+        if f.is_file() and not f.name.endswith("native.o"):
+            f.unlink()
+
+Path("send-signal2").touch()
+
+sys.exit(p.wait())
diff --git a/llvm/lib/DTLTO/DTLTO.cpp b/llvm/lib/DTLTO/DTLTO.cpp
index a94f597cab914..4d8f8ba0fc4ac 100644
--- a/llvm/lib/DTLTO/DTLTO.cpp
+++ b/llvm/lib/DTLTO/DTLTO.cpp
@@ -218,7 +218,6 @@ void lto::DTLTO::cleanup() {
     for (auto &Input : InputFiles) {
       if (!Input->isMemberOfArchive())
         continue;
-
       std::error_code EC =
           sys::fs::remove(Input->getName(), /*IgnoreNonExisting=*/true);
       if (EC &&

>From cfacb60a5773e7f69cbc4db185f3a8ba425aea14 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Tue, 20 Jan 2026 17:07:08 +0000
Subject: [PATCH 3/3] Improve the use of test_temps.py

Add sanity checks and comments to confirm and explain test_temps.py
behaviour for specially named output directories.
---
 cross-project-tests/dtlto/savetemps-lock.test | 2 ++
 cross-project-tests/dtlto/savetemps.test      | 6 +++++-
 cross-project-tests/dtlto/test_temps.py       | 4 ++--
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/cross-project-tests/dtlto/savetemps-lock.test b/cross-project-tests/dtlto/savetemps-lock.test
index d93c6cf0767e1..9daeee078d4ac 100644
--- a/cross-project-tests/dtlto/savetemps-lock.test
+++ b/cross-project-tests/dtlto/savetemps-lock.test
@@ -27,6 +27,8 @@ RUN:       -fthinlto-distributor=%python \
 RUN:       -Xthinlto-distributor=%S/local_codegen_and_wait.py 2>&1 \
 RUN:         | FileCheck %s --implicit-check-not=warning
 
+# Sanity check for the expected test_temps.py behaviour.
+CHECK-DAG: Lock any files in the output directory.
 CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.[[#PID:]].dist-file.json': {{.*}}
 # Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
 CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[HEXPID:[A-Z0-9]+]].1.[[#PID]].native.o': {{.*}}
diff --git a/cross-project-tests/dtlto/savetemps.test b/cross-project-tests/dtlto/savetemps.test
index 0d9a64e1b0e30..c7cae822f45d6 100644
--- a/cross-project-tests/dtlto/savetemps.test
+++ b/cross-project-tests/dtlto/savetemps.test
@@ -49,10 +49,14 @@ TEMPS: {{^}}t.elf.resolution.txt{{$}}
 # No files are expected after.
 BOOKEND-NOT: {{.}}
 
-# Check that no warnings are produced if temporary files are missing.
+# Check that no warnings are produced if temporary files are missing. Note that
+# the use of the name "removed" for the output directory triggers special
+# behaviour in test_temps.py.
 REDEFINE: %{tdir} = removed
 RUN: %{test-temps-dtlto} 2>&1 | FileCheck %s --check-prefix=NOWARN --allow-empty
 RUN: ls %{tdir} | sort |  FileCheck %s --check-prefixes=BOOKEND,ELF
+# Sanity check for the expected test_temps.py behaviour.
+NOWARN: Remove non-essential files in the output directory.
 NOWARN-NOT: warning
 
 #--- t1.c
diff --git a/cross-project-tests/dtlto/test_temps.py b/cross-project-tests/dtlto/test_temps.py
index b4b1f196dd6b1..cdd0842e109d0 100644
--- a/cross-project-tests/dtlto/test_temps.py
+++ b/cross-project-tests/dtlto/test_temps.py
@@ -45,13 +45,13 @@ def lock_no_delete_share(path):
     sys.exit(1)
 
 if output_dir == "locked":
-    # lock any files in the output directory.
+    print("Lock any files in the output directory.")
     for f in Path(output_dir).iterdir():
         if f.is_file():
             lock_no_delete_share(str(f))
 
 if output_dir == "removed":
-    # remove non-essential files in the output directory.
+    print("Remove non-essential files in the output directory.")
     for f in Path(output_dir).iterdir():
         if f.is_file() and not f.name.endswith("native.o"):
             f.unlink()



More information about the llvm-commits mailing list