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

Ben Dunbobbin via llvm-commits llvm-commits at lists.llvm.org
Wed Jan 21 18:04:23 PST 2026


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

>From 5a804b492865fa561f363f8dad2db0be5856b302 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/5] [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.
Additionally, a minimal test has been added to show the COFF
behaviour.

SIE Internal tracker: TOOLCHAIN-21022
---
 cross-project-tests/dtlto/link-savetemps.test |  44 +++++
 cross-project-tests/dtlto/savetemps.test      | 154 ++++++++++++++++++
 cross-project-tests/dtlto/signal.test         |  23 ++-
 lld/COFF/LTO.cpp                              |   6 +-
 lld/ELF/LTO.cpp                               |   7 +-
 llvm/include/llvm/DTLTO/DTLTO.h               |  17 +-
 llvm/lib/DTLTO/DTLTO.cpp                      |  24 ++-
 7 files changed, 254 insertions(+), 21 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/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 666964dd94e06..7abab23dbea34 100644
--- a/cross-project-tests/dtlto/signal.test
+++ b/cross-project-tests/dtlto/signal.test
@@ -1,17 +1,21 @@
 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
 
+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 t1.o t2.o -o %{tdir}/t.elf -shared \
+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
 
@@ -25,7 +29,7 @@ EMPTY-NOT: {{.}}
 # 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
+RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,TEMPS,INDEX,OTHER
 
 # Check that --thinlto-emit-index-files preserves the index files if the process
 # is aborted.
@@ -36,11 +40,14 @@ RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,INDEX
 # No files are expected before.
 BOOKEND-NOT: {{.}}
 TEMPS: {{^}}t.[[#PID:]].dist-file.json{{$}}
-TEMPS: {{^}}t.elf.{{.+$}}
-TEMPS: {{^}}t1.1.[[#PID]].native.o{{$}}
-INDEX: {{^}}t1.1.[[#PID:]].native.o.thinlto.bc{{$}}
-TEMPS: {{^}}t2.2.[[#PID]].native.o{{$}}
-INDEX: {{^}}t2.2.[[#PID]].native.o.thinlto.bc{{$}}
+# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o.
+TEMPS: {{^}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[#%X,HEXPID:]].1.[[#PID]].native.o{{$}}
+INDEX: {{^}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[#%X,HEXPID:]].1.[[#PID:]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.a(t1.o at [[#T1_OFFSET]]).1.[[#%X,HEXPID]].o{{$}}
+TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[#%X,HEXPID]].2.[[#PID]].native.o{{$}}
+INDEX: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[#%X,HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET]]).2.[[#%X,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 cd795d39209541cbcfaf36e0ee76bf00845c8eb7 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/5] 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.
---
 .../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 -
 5 files changed, 130 insertions(+), 103 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/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 719a5245d7e1eee170adb7071478271294adf4ec 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/5] 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()

>From 11b8af053dca4e3abbefff395dfdbe073cf324a9 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Thu, 22 Jan 2026 01:56:05 +0000
Subject: [PATCH 4/5] Use test_temps.py+local_codegen_and_wait.py in signal.py

Also:
- Improve top level script comments.
- Regularise some FileCheck match expressions I missed earlier.
---
 .../dtlto/local_codegen_and_wait.py           | 12 ++++-
 cross-project-tests/dtlto/savetemps-lock.test |  2 +-
 cross-project-tests/dtlto/savetemps.test      | 19 ++++---
 cross-project-tests/dtlto/signal.test         | 49 ++-----------------
 cross-project-tests/dtlto/test_temps.py       | 48 +++++++++++++++---
 5 files changed, 67 insertions(+), 63 deletions(-)

diff --git a/cross-project-tests/dtlto/local_codegen_and_wait.py b/cross-project-tests/dtlto/local_codegen_and_wait.py
index 72b72419a3329..a211153f33b77 100644
--- a/cross-project-tests/dtlto/local_codegen_and_wait.py
+++ b/cross-project-tests/dtlto/local_codegen_and_wait.py
@@ -1,5 +1,13 @@
-"""Perform codegen locally, create "send-signal1" file and wait
-for "send-signal2" file to exist before exiting."""
+"""
+This simple distributor performs code generation locally, creates the
+"send-signal1" file, and then waits for the "send-signal2" file to appear
+before exiting. It is intended to be used in tandem with test_temps.py.
+
+By coordinating via the "send-signal*" files, the scripts ensure that the
+requested actions are performed after all DTLTO backend compilations have
+completed but before DTLTO itself finishes. At this point, DTLTO temporary
+files have not yet been cleaned up.
+"""
 
 import json, subprocess, sys, time, os, pathlib
 
diff --git a/cross-project-tests/dtlto/savetemps-lock.test b/cross-project-tests/dtlto/savetemps-lock.test
index 9daeee078d4ac..5a85f08583514 100644
--- a/cross-project-tests/dtlto/savetemps-lock.test
+++ b/cross-project-tests/dtlto/savetemps-lock.test
@@ -20,7 +20,7 @@ RUN: llvm-ar rcs t.a t1.o t2.o
 # 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: %python %S/test_temps.py locked lock \
 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 \
diff --git a/cross-project-tests/dtlto/savetemps.test b/cross-project-tests/dtlto/savetemps.test
index c7cae822f45d6..35ecefef3e3ab 100644
--- a/cross-project-tests/dtlto/savetemps.test
+++ b/cross-project-tests/dtlto/savetemps.test
@@ -11,9 +11,10 @@ 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: %{action} = dummy-to-make-lit-work
 DEFINE: %{test-temps-dtlto} = \
 DEFINE:   rm -f send-signal1 && rm -f send-signal2 && mkdir %{tdir} && \
-DEFINE:   %python %S/test_temps.py %{tdir} \
+DEFINE:   %python %S/test_temps.py %{tdir} %{action} \
 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 \
@@ -21,16 +22,19 @@ DEFINE:       -Xthinlto-distributor=%S/local_codegen_and_wait.py
 
 # Check that all temporary files are removed in normal operation.
 REDEFINE: %{tdir} = empty
+REDEFINE: %{action} = none
 RUN: %{test-temps-dtlto}
 RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,ELF
 
 # Check that --save-temps preserves temporary files.
 REDEFINE: %{tdir} = savetemps
+REDEFINE: %{action} = none
 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
+REDEFINE: %{action} = none
 RUN: %{test-temps-dtlto} -Wl,--thinlto-emit-index-files
 RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,INDEX,ELF
 
@@ -38,12 +42,12 @@ RUN: ls %{tdir} | sort | FileCheck %s --check-prefixes=BOOKEND,INDEX,ELF
 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{{$}}
+TEMPS: {{^}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[#%X,HEXPID:]].1.[[#PID]].native.o{{$}}
+INDEX: {{^}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[#%X,HEXPID:]].1.[[#PID:]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.a(t1.o at [[#T1_OFFSET]]).1.[[#%X,HEXPID]].o{{$}}
+TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[#%X,HEXPID]].2.[[#PID]].native.o{{$}}
+INDEX: {{^}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[#%X,HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
+TEMPS: {{^}}t.a(t2.o at [[#T2_OFFSET]]).2.[[#%X,HEXPID]].o{{$}}
 ELF:   {{^}}t.elf{{$}}
 TEMPS: {{^}}t.elf.resolution.txt{{$}}
 # No files are expected after.
@@ -53,6 +57,7 @@ BOOKEND-NOT: {{.}}
 # the use of the name "removed" for the output directory triggers special
 # behaviour in test_temps.py.
 REDEFINE: %{tdir} = removed
+REDEFINE: %{action} = remove
 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.
diff --git a/cross-project-tests/dtlto/signal.test b/cross-project-tests/dtlto/signal.test
index 7abab23dbea34..4378ca0c34b39 100644
--- a/cross-project-tests/dtlto/signal.test
+++ b/cross-project-tests/dtlto/signal.test
@@ -12,12 +12,13 @@ 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: %{kill-dtlto} = rm -f send-signal && mkdir %{tdir} && \
-DEFINE:   %python killer.py \
+DEFINE: %{kill-dtlto} = \
+DEFINE:   rm -f send-signal1 && rm -f send-signal2 && mkdir %{tdir} && \
+DEFINE:   %python %S/test_temps.py %{tdir} kill \
 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 if the process is aborted.
 REDEFINE: %{tdir} = empty
@@ -56,45 +57,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-signal" file and wait."""
-from pathlib import Path
-import json, subprocess, sys, time
-
-# 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-signal").touch()
-while True:
-    time.sleep(1)
-
-#--- killer.py
-"""Run command, wait for "send-signal" file to exist, and then send a
-termination signal."""
-import os, sys, time, signal, subprocess
-
-if os.name == "nt":
-    # CREATE_NEW_PROCESS_GROUP is used so that p.send_signal(CTRL_BREAK_EVENT)
-    # does not get sent to the LIT processes that are running the test.
-    kwargs = {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}
-else:
-    # Makes the child a process-group leader so os.killpg(p.pid, SIGINT) works.
-    kwargs = {"start_new_session": True}
-
-p = subprocess.Popen(sys.argv[1:], **kwargs)
-
-while not os.path.exists("send-signal"):
-    time.sleep(0.05)
-
-if os.name == "nt":
-    # Note that CTRL_C_EVENT does not appear to work for clang.
-    p.send_signal(signal.CTRL_BREAK_EVENT)
-else:
-    os.killpg(p.pid, signal.SIGINT)
-p.wait()
-
-sys.exit(0 if p.returncode != 0 else 1)
diff --git a/cross-project-tests/dtlto/test_temps.py b/cross-project-tests/dtlto/test_temps.py
index cdd0842e109d0..e2e38cd605560 100644
--- a/cross-project-tests/dtlto/test_temps.py
+++ b/cross-project-tests/dtlto/test_temps.py
@@ -1,8 +1,16 @@
-"""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."""
+"""
+Run a command, wait for the "send-signal1" file to appear, then perform an
+action on the DTLTO files in the output directory (for example, locking
+them). The output directory is specified by sys.argv[1], and the action to
+perform is specified by sys.argv[2]. Finally, create the "send-signal2" file.
 
-import ctypes, os, subprocess, sys, time
+This script works in tandem with local_codegen_and_wait.py. By coordinating
+via the "send-signal*" files, the scripts ensure that the action is performed
+after all DTLTO backend compilations have completed but before DTLTO itself
+finishes. At this stage, DTLTO temporary files have not yet been cleaned up.
+"""
+
+import ctypes, os, subprocess, sys, time, signal
 from ctypes import wintypes
 from pathlib import Path
 
@@ -37,20 +45,39 @@ def lock_no_delete_share(path):
 
 
 output_dir = sys.argv[1]
-p = subprocess.Popen(sys.argv[2:])
+action = sys.argv[2]
+
+kwargs = {}
+if action == "kill":
+    if os.name == "nt":
+        # CREATE_NEW_PROCESS_GROUP is used so that p.send_signal(CTRL_BREAK_EVENT)
+        # does not get sent to the LIT processes that are running the test.
+        kwargs = {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}
+    else:
+        # Makes the child a process-group leader so os.killpg(p.pid, SIGINT) works.
+        kwargs = {"start_new_session": True}
+
+p = subprocess.Popen(sys.argv[3:], **kwargs)
 
 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":
+if action == "kill":
+    if os.name == "nt":
+        # Note that CTRL_C_EVENT does not appear to work for clang.
+        p.send_signal(signal.CTRL_BREAK_EVENT)
+    else:
+        os.killpg(p.pid, signal.SIGINT)
+
+if action == "lock":
     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":
+if action == "remove":
     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"):
@@ -58,4 +85,9 @@ def lock_no_delete_share(path):
 
 Path("send-signal2").touch()
 
-sys.exit(p.wait())
+if action == "kill":
+    p.wait()
+    sys.exit(0 if p.returncode != 0 else 1)
+else:
+    sys.exit(p.wait())
+

>From ac312c678417e6a5159b51d254f13c7853481b73 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Thu, 22 Jan 2026 02:04:09 +0000
Subject: [PATCH 5/5] Applied black python formatter

---
 cross-project-tests/dtlto/test_temps.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/cross-project-tests/dtlto/test_temps.py b/cross-project-tests/dtlto/test_temps.py
index e2e38cd605560..8afbc61d6bc44 100644
--- a/cross-project-tests/dtlto/test_temps.py
+++ b/cross-project-tests/dtlto/test_temps.py
@@ -90,4 +90,3 @@ def lock_no_delete_share(path):
     sys.exit(0 if p.returncode != 0 else 1)
 else:
     sys.exit(p.wait())
-



More information about the llvm-commits mailing list