https://github.com/c-rhodes updated https://github.com/llvm/llvm-project/pull/178035
>From a777e13923c000953aa9f987395f6543e89883d0 Mon Sep 17 00:00:00 2001 From: Ben Dunbobbin <[email protected]> Date: Thu, 22 Jan 2026 00:48:31 +0000 Subject: [PATCH 1/2] [DTLTO][NFC] Minor cleanups and improvements to DTLTO tests (#177282) This change makes small, non-functional improvements to the DTLTO test suite, including: - Tightening+Regularizing FileCheck match expressions across tests - Simplifying `signal.test` using more lit macro features (cherry picked from commit 00fecbcf00f96e816608dcf8e82562e6aa272d21) --- cross-project-tests/dtlto/archive.test | 31 +++++++------ .../dtlto/archives-mixed-lto-modes-test.test | 8 ++-- .../dtlto/archives-same-module-id.test | 16 +++---- .../dtlto/link-archive-thin.test | 18 ++++---- cross-project-tests/dtlto/signal.test | 44 ++++++++++--------- 5 files changed, 61 insertions(+), 56 deletions(-) diff --git a/cross-project-tests/dtlto/archive.test b/cross-project-tests/dtlto/archive.test index 1c01ae7a5691a..cbf4b00bdffa0 100644 --- a/cross-project-tests/dtlto/archive.test +++ b/cross-project-tests/dtlto/archive.test @@ -23,32 +23,32 @@ 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{{$}} +# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o. +OUTPUTS-DAG: {{^}}archive.a(boo.o at [[#BOO_OFFSET:]]).2.[[#%X,HEXPID:]].2.[[#PID]].native.o{{$}} +OUTPUTS-DAG: {{^}}archive.a(boo.o at [[#BOO_OFFSET]]).2.[[#%X,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.[[#%X,HEXPID]].3.[[#PID]].native.o{{$}} +OUTPUTS-DAG: {{^}}archive.a(foo.o at [[#FOO_OFFSET]]).3.[[#%X,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.[[#%X,HEXPID]].4.[[#PID]].native.o{{$}} +OUTPUTS-DAG: {{^}}archive.a(moo.o at [[#MOO_OFFSET]]).4.[[#%X,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{{$}} +OUTPUTS-DAG: {{^}}main.elf{{$}} #--- foo.c volatile int foo_int; @@ -77,4 +77,3 @@ extern int voo(int x); __attribute__((retain)) int main(int argc, char** argv) { return boo(argc) + moo() + voo(argc + 3); } - 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..cd640367a403a 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.[[#%X,HEXPID:]].o +CHECK: archive.a(two.o at [[#TWO_OFFSET:]]).2.[[#%X,HEXPID]].o +CHECK: archive.a(three.o at [[#THREE_OFFSET:]]).3.[[#%X,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..dbff8061a39b5 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{{$}} +# Filename composition: <archive>(<member> at <offset>).<task>.<pid>.<task>.<pid>.native.o. +OUTPUTS-DAG: {{^}}archive.a(t3.o at [[#T3_OFFSET:]]).2.[[#%X,HEXPID:]].2.[[#PID]].native.o{{$}} +OUTPUTS-DAG: {{^}}archive.a(t3.o at [[#T3_OFFSET]]).2.[[#%X,HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}} +OUTPUTS-DAG: {{^}}archive.a(t1.o at [[#T1_OFFSET:]]).3.[[#%X,HEXPID]].3.[[#PID]].native.o{{$}} +OUTPUTS-DAG: {{^}}archive.a(t1.o at [[#T1_OFFSET]]).3.[[#%X,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/signal.test b/cross-project-tests/dtlto/signal.test index 95b6bf368b577..666964dd94e06 100644 --- a/cross-project-tests/dtlto/signal.test +++ b/cross-project-tests/dtlto/signal.test @@ -7,36 +7,40 @@ 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 +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 # 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 -# 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.[[#PID:]].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: {{^}}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{{$}} # No files are expected after. BOOKEND-NOT: {{.}} >From 63300b7e0d128741cea7a24a90e939e842b172b9 Mon Sep 17 00:00:00 2001 From: Ben Dunbobbin <[email protected]> Date: Mon, 26 Jan 2026 17:55:49 +0000 Subject: [PATCH 2/2] [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 (cherry picked from commit 1d74db7e8d6a211fe421a02275eae1756c8d40fe) --- 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 0000000000000..8bf0bd6b08ae7 --- /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.[[#%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 0000000000000..ead5ff03b6b61 --- /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 0000000000000..822744b1dfc4f --- /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: <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.[[#%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 0000000000000..f8615a00ad664 --- /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: <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{{$}} +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 666964dd94e06..ba08b2bc1066e 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: <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: {{.}} @@ -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 0000000000000..ecf1a3882b2b3 --- /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 <output_dir> <action> <command...> + +Run <command>, 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 <action>, and then creates the +"send-signal2" file to allow the link to continue. + +Actions: + kill Send an interrupt to cause <command> to terminate after the + ThinLTO backend compilations complete. + lock (Windows only) Hold open handles to DTLTO temporary files in + <output_dir> 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 <output_dir> 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 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..4d8f8ba0fc4ac 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,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(); } _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
