From 1d74db7e8d6a211fe421a02275eae1756c8d40fe Mon Sep 17 00:00:00 2001 From: Ben Dunbobbin Date: Mon, 26 Jan 2026 17:55:49 +0000 Subject: [PATCH] [DTLTO] Make temporary file handling consistent (#176807) 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 ++++++ .../dtlto/local_codegen_and_wait.py | 20 +++ cross-project-tests/dtlto/savetemps-lock.test | 44 ++++++ cross-project-tests/dtlto/savetemps.test | 70 ++++++++++ cross-project-tests/dtlto/signal.test | 74 +++------- cross-project-tests/dtlto/test_temps.py | 128 ++++++++++++++++++ lld/COFF/LTO.cpp | 6 +- lld/ELF/LTO.cpp | 7 +- llvm/include/llvm/DTLTO/DTLTO.h | 17 ++- llvm/lib/DTLTO/DTLTO.cpp | 23 +++- 10 files changed, 365 insertions(+), 68 deletions(-) create mode 100644 cross-project-tests/dtlto/link-savetemps.test 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/savetemps.test create mode 100644 cross-project-tests/dtlto/test_temps.py diff --git a/cross-project-tests/dtlto/link-savetemps.test b/cross-project-tests/dtlto/link-savetemps.test new file mode 100644 index 000000000000..8bf0bd6b08ae --- /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: .....native.o. +TEMPS: {{^}}t.libt1.o[[#T1_OFFSET:]].1.[[#%X,HEXPID:]].1.[[#PID]].native.o{{$}} +TEMPS: {{^}}t.libt1.o[[#T1_OFFSET]].1.[[#%X,HEXPID]].1.[[#PID]].native.o.thinlto.bc{{$}} +TEMPS: {{^}}t.libt1.o[[#T1_OFFSET]].1.[[#%X,HEXPID]].o{{$}} +# No files are expected after. +BOOKEND-NOT: {{.}} + +#--- t1.c +__attribute__((retain)) int mainCRTStartup() { return 0; } 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 000000000000..ead5ff03b6b6 --- /dev/null +++ b/cross-project-tests/dtlto/local_codegen_and_wait.py @@ -0,0 +1,20 @@ +""" +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. +Please see test_temps.py for more information. +""" + +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 000000000000..822744b1dfc4 --- /dev/null +++ b/cross-project-tests/dtlto/savetemps-lock.test @@ -0,0 +1,44 @@ +# 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: mkdir locked && %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 \ +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: ( at ).....native.o. +CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET:]]).1.[[#%X,HEXPID:]].1.[[#PID]].native.o': {{.*}} +CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t1.o at [[#T1_OFFSET]]).1.[[#%X,HEXPID]].1.[[#PID]].native.o.thinlto.bc': {{.*}} +CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET:]]).2.[[#%X,HEXPID]].2.[[#PID]].native.o': {{.*}} +CHECK-DAG: warning: could not remove the file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET]]).2.[[#%X,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.[[#%X,HEXPID]].o': {{.*}} +CHECK-DAG: warning: could not remove temporary DTLTO input file 'locked{{/|\\}}t.a(t2.o at [[#T2_OFFSET]]).2.[[#%X,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 new file mode 100644 index 000000000000..f8615a00ad66 --- /dev/null +++ b/cross-project-tests/dtlto/savetemps.test @@ -0,0 +1,70 @@ +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 +# 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: %{action} = dummy-to-make-lit-work +DEFINE: %{test-temps-dtlto} = \ +DEFINE: mkdir %{tdir} && %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 \ +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 + +# No files are expected before. +BOOKEND-NOT: {{.}} +TEMPS: {{^}}t.[[#PID:]].dist-file.json{{$}} +# Filename composition: ( at ).....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{{$}} +ELF: {{^}}t.elf{{$}} +TEMPS: {{^}}t.elf.resolution.txt{{$}} +# No files are expected after. +BOOKEND-NOT: {{.}} + +# 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 +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. +NOWARN: Remove non-essential files in the output directory. +NOWARN-NOT: warning + +#--- 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/signal.test b/cross-project-tests/dtlto/signal.test index 666964dd94e0..ba08b2bc1066 100644 --- a/cross-project-tests/dtlto/signal.test +++ b/cross-project-tests/dtlto/signal.test @@ -1,19 +1,22 @@ 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: -fthinlto-distributor=%python \ -DEFINE: -Xthinlto-distributor=local_codegen_and_wait.py +DEFINE: %{kill-dtlto} = mkdir %{tdir} && %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=%S/local_codegen_and_wait.py # Check that all temporary files are removed if the process is aborted. REDEFINE: %{tdir} = empty @@ -25,7 +28,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 +39,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: ( at ).....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: {{.}} @@ -49,45 +55,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 new file mode 100644 index 000000000000..ecf1a3882b2b --- /dev/null +++ b/cross-project-tests/dtlto/test_temps.py @@ -0,0 +1,128 @@ +""" +This script works in tandem with local_codegen_and_wait.py. By coordinating +via the "send-signal*" files, the scripts ensure that the requested action is +performed after all DTLTO backend compilations have completed but before +DTLTO itself finishes. At this point, DTLTO temporary files have been +created but have not yet been cleaned up. + +Usage: + %python test_temps.py + +Run , which must be a ThinLTO link invocation that uses +local_codegen_and_wait.py as the ThinLTO distributor. The script waits for +the "send-signal1" file to appear, performs , and then creates the +"send-signal2" file to allow the link to continue. + +Actions: + kill Send an interrupt to cause to terminate after the + ThinLTO backend compilations complete. + lock (Windows only) Hold open handles to DTLTO temporary files in + to prevent their deletion after the ThinLTO backend + compilations complete. This action is not supported on Linux, as + there is no reliable mechanism to prevent file deletion that is + guaranteed to be released when the script exits (AFAICT). + remove Delete non-essential files in after the ThinLTO + backend compilations complete. +""" + +import os +import signal +import subprocess +import sys +import time +from pathlib import Path + +IS_WIN = os.name == "nt" +SIGNAL1 = Path("send-signal1") +SIGNAL2 = Path("send-signal2") + +if IS_WIN: + import ctypes + from ctypes import wintypes + + 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): + # Windows-specific: deny FILE_SHARE_DELETE by omitting it from dwShareMode. + 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 = Path(sys.argv[1]) +action = sys.argv[2] + +# "lock" is Windows-only; fail early if invoked elsewhere. +if action == "lock" and not IS_WIN: + print("error: action 'lock' is only supported on Windows", file=sys.stderr) + sys.exit(1) + +# Remove any pre-existing signal files from previous script runs. +SIGNAL1.unlink(missing_ok=True) +SIGNAL2.unlink(missing_ok=True) + +kwargs = {} +if action == "kill": + if IS_WIN: + # 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 SIGNAL1.exists() and p.poll() is None: + time.sleep(0.05) +if p.poll() is not None: + sys.exit(1) + +if action == "kill": + if IS_WIN: + # 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 output_dir.iterdir(): + if f.is_file(): + lock_no_delete_share(str(f)) + +if action == "remove": + print("Remove non-essential files in the output directory.") + for f in output_dir.iterdir(): + if f.is_file() and not f.name.endswith("native.o"): + f.unlink() + +SIGNAL2.touch() + +if action == "kill": + # Expect termination: succeed if the child fails, fail if it exits cleanly. + p.wait() + sys.exit(0 if p.returncode != 0 else 1) + +sys.exit(p.wait()) diff --git a/lld/COFF/LTO.cpp b/lld/COFF/LTO.cpp index e621bb263d2c..d55f95493a85 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(createConfig(), backend, ctx.config.ltoPartitions); else - ltoObj = std::make_unique(createConfig(), backend, - ctx.config.ltoPartitions); + ltoObj = std::make_unique( + 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 fa39444408ee..6f916a501a26 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(createConfig(ctx), backend, - ctx.arg.ltoPartitions, - ltoModes[ctx.arg.ltoKind]); + ltoObj = std::make_unique( + 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 14f1f5fd00e3..02b098a68aec 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> @@ -37,6 +42,12 @@ private: 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 isThinArchive(const StringRef ArchivePath); diff --git a/llvm/lib/DTLTO/DTLTO.cpp b/llvm/lib/DTLTO/DTLTO.cpp index b9a5cd3a062e..4d8f8ba0fc4a 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 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,18 @@ 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(); }