Skip to content

[DTLTO] Normalize Windows 8.3 short paths to long form #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: dtlto_elf_lld
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1680a58
[DTLTO][LLD][ELF] Add support for Integrated Distributed ThinLTO
bd1976bris Jun 3, 2025
d24e531
[DTLTO] Normalize Windows 8.3 short paths to long form
bd1976bris Jun 17, 2025
2ca44d9
Update cross-project-tests/dtlto/path.test
bd1976bris Jun 17, 2025
c2a3334
Update lld/ELF/InputFiles.cpp
bd1976bris Jun 17, 2025
f90d4fd
Update llvm/lib/Support/Windows/Path.inc
bd1976bris Jun 17, 2025
b96acc2
Update llvm/unittests/Support/Path.cpp
bd1976bris Jun 17, 2025
9d4d009
Update cross-project-tests/dtlto/path.test
bd1976bris Jun 17, 2025
0207d9f
Update cross-project-tests/dtlto/path.test
bd1976bris Jun 17, 2025
0a7560d
Mark LIT test as UNSUPPORTED where short paths are not available
bd1976bris Jun 17, 2025
f858814
Add LLVM_ABI and make the naming of function parameters more consiste…
bd1976bris Jun 17, 2025
ccd3e92
Just use path::root_path and drop Win32 API wrapper functions we were…
bd1976bris Jun 18, 2025
649e0fa
Simplify python in lit config by calling PathLib.anchor rather than G…
bd1976bris Jun 18, 2025
d102386
Remove archive test-case from LIT test.
bd1976bris Jun 18, 2025
826a250
Improve variable naming in unit test
bd1976bris Jun 18, 2025
fa8c95f
Fix removal of test directories.
bd1976bris Jun 18, 2025
0026ef6
Improve LIT test. Use more meaningful names and call GetShortPathName…
bd1976bris Jun 18, 2025
7227c20
Do some manual testing to ensure that UNC paths are handled as expect…
bd1976bris Jun 18, 2025
2bf19f1
Revert "Do some manual testing to ensure that UNC paths are handled a…
bd1976bris Jun 18, 2025
f3394d0
Reduce the size of the unit test by using a helper function to check …
bd1976bris Jun 18, 2025
057b7c3
Use a simple filesystem test rather than AreShortNamesEnabled
bd1976bris Jun 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions cross-project-tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ set(CROSS_PROJECT_TEST_DEPS
FileCheck
check-gdb-llvm-support
count
llvm-dwarfdump
llvm-ar
llvm-config
llvm-dwarfdump
llvm-objdump
split-file
not
split-file
)

if ("clang" IN_LIST LLVM_ENABLE_PROJECTS)
Expand Down Expand Up @@ -94,6 +95,13 @@ add_lit_testsuite(check-cross-amdgpu "Running AMDGPU cross-project tests"
DEPENDS clang
)

# DTLTO tests.
add_lit_testsuite(check-cross-dtlto "Running DTLTO cross-project tests"
${CMAKE_CURRENT_BINARY_DIR}/dtlto
EXCLUDE_FROM_CHECK_ALL
DEPENDS ${CROSS_PROJECT_TEST_DEPS}
)

# Add check-cross-project-* targets.
add_lit_testsuites(CROSS_PROJECT ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${CROSS_PROJECT_TEST_DEPS}
Expand Down
3 changes: 3 additions & 0 deletions cross-project-tests/dtlto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Tests for DTLTO (Integrated Distributed ThinLTO) functionality.

These are integration tests as DTLTO invokes `clang` for code-generation.
65 changes: 65 additions & 0 deletions cross-project-tests/dtlto/archive-thin.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# REQUIRES: x86-registered-target,ld.lld,llvm-ar

# Test that a DTLTO link succeeds and outputs the expected set of files
# correctly when thin archives are present.

RUN: rm -rf %t && split-file %s %t && cd %t
RUN: %clang --target=x86_64-linux-gnu -flto=thin -c \
RUN: foo.c bar.c dog.c cat.c _start.c

RUN: llvm-ar rcs foo.a foo.o --thin
# Create this bitcode thin archive in a subdirectory to test the expansion of
# the path to a bitcode file that is referenced using "..", e.g., in this case
# "../bar.o".
RUN: mkdir lib
RUN: llvm-ar rcs lib/bar.a bar.o --thin
# Create this bitcode thin archive with an absolute path entry containing "..".
RUN: llvm-ar rcs dog.a %t/lib/../dog.o --thin
RUN: llvm-ar rcs cat.a cat.o --thin
RUN: llvm-ar rcs _start.a _start.o --thin

RUN: mkdir %t/out && cd %t/out

RUN: ld.lld %t/foo.a %t/lib/bar.a ../_start.a %t/cat.a \
RUN: --whole-archive ../dog.a \
RUN: --thinlto-distributor=%python \
RUN: --thinlto-distributor-arg=%llvm_src_root/utils/dtlto/local.py \
RUN: --thinlto-remote-compiler=%clang \
RUN: --save-temps

# Check that the required output files have been created.
RUN: ls | FileCheck %s --check-prefix=OUTPUTS \
RUN: --implicit-check-not=cat --implicit-check-not=foo

# JSON jobs description.
OUTPUTS-DAG: a.[[PID:[a-zA-Z0-9_]+]].dist-file.json

# Individual summary index files.
OUTPUTS-DAG: dog.1.[[PID]].native.o.thinlto.bc{{$}}
OUTPUTS-DAG: _start.2.[[PID]].native.o.thinlto.bc{{$}}
OUTPUTS-DAG: bar.3.[[PID]].native.o.thinlto.bc{{$}}

# Native output object files.
OUTPUTS-DAG: dog.1.[[PID]].native.o{{$}}
OUTPUTS-DAG: _start.2.[[PID]].native.o{{$}}
OUTPUTS-DAG: bar.3.[[PID]].native.o{{$}}

#--- foo.c
__attribute__((retain)) void foo() {}

#--- bar.c
extern void foo();
__attribute__((retain)) void bar() { foo(); }

#--- dog.c
__attribute__((retain)) void dog() {}

#--- cat.c
__attribute__((retain)) void cat() {}

#--- _start.c
extern void bar();
__attribute__((retain)) void _start() {
bar();
}

35 changes: 35 additions & 0 deletions cross-project-tests/dtlto/dtlto.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// REQUIRES: x86-registered-target,ld.lld

/// Simple test that DTLTO works with a single input bitcode file and that
/// --save-temps can be applied to the remote compilation.
// RUN: rm -rf %t && mkdir %t && cd %t

// RUN: %clang --target=x86_64-linux-gnu -c -flto=thin %s

// RUN: ld.lld dtlto.o \
// RUN: --thinlto-distributor=%python \
// RUN: --thinlto-distributor-arg=%llvm_src_root/utils/dtlto/local.py \
// RUN: --thinlto-remote-compiler=%clang \
// RUN: --thinlto-remote-compiler-arg=--save-temps

/// Check that the required output files have been created.
// RUN: ls | count 10
// RUN: ls | FileCheck %s

/// Produced by the bitcode compilation.
// CHECK-DAG: {{^}}dtlto.o{{$}}

/// Linked ELF.
// CHECK-DAG: {{^}}a.out{{$}}

/// --save-temps output for the backend compilation.
// CHECK-DAG: {{^}}dtlto.s{{$}}
// CHECK-DAG: {{^}}dtlto.s.0.preopt.bc{{$}}
// CHECK-DAG: {{^}}dtlto.s.1.promote.bc{{$}}
// CHECK-DAG: {{^}}dtlto.s.2.internalize.bc{{$}}
// CHECK-DAG: {{^}}dtlto.s.3.import.bc{{$}}
// CHECK-DAG: {{^}}dtlto.s.4.opt.bc{{$}}
// CHECK-DAG: {{^}}dtlto.s.5.precodegen.bc{{$}}
// CHECK-DAG: {{^}}dtlto.s.resolution.txt{{$}}

int _start() { return 0; }
2 changes: 2 additions & 0 deletions cross-project-tests/dtlto/lit.local.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
if "clang" not in config.available_features:
config.unsupported = True
80 changes: 80 additions & 0 deletions cross-project-tests/dtlto/path.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# REQUIRES: x86-registered-target,ld.lld,llvm-ar,system-windows,tempshortpaths

# Check that any shortened "8.3" paths containing '~' characters are expanded,
# and their path seperators changed to Windows style ones, prior to being passed
# to the distributor.

RUN: rm -rf %t && split-file %s %t && cd %t

RUN: prospero-clang --target=x86_64-linux-gnu -c start.c f.c -flto=thin -O2

RUN: %python in-83-dir.py \
RUN: ld.lld start.o f.o -o start.elf \
RUN: --thinlto-distributor=%python \
RUN: --thinlto-distributor-arg=%llvm_src_root/utils/dtlto/local.py \
RUN: --thinlto-remote-compiler=%clang \
RUN: --save-temps

# Ensure that cross-module importing occurred. Cross-module importing is somewhat
# fragile - e.g. it requires amenable code and compilation with -O2 or above.
# It's important that importing happens for this test case, as the paths to
# imported-from modules are recorded in the ThinLTO metadata.
RUN: prospero-llvm-objdump -d start.elf | FileCheck %s --check-prefix=IMPORT
IMPORT: <_start>:
IMPORT: jmp 0x{{[0-9A-F]+}} <_start>
IMPORT: <f>:
IMPORT-NEXT: jmp 0x{{[0-9A-F]+}} <f>

RUN: FileCheck --input-file start.*.dist-file.json %s --check-prefix=TILDE
TILDE-NOT: ~

#--- f.c
int _start();
int f() { return _start(); }

#--- start.c
int f();
int _start() { return f(); }

#--- in-83-dir.py
import os, shutil, sys, uuid, subprocess
from pathlib import Path
import ctypes
from ctypes import wintypes


def get_short_path(p):
g = ctypes.windll.kernel32.GetShortPathNameW
g.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]
g.restype = wintypes.DWORD
n = g(os.path.abspath(p), None, 0) # First call gets required buffer size.
b = ctypes.create_unicode_buffer(n)
if g(os.path.abspath(p), b, n): # Second call fills buffer.
return b.value
raise ctypes.WinError()


temp = Path(os.environ["TEMP"])
assert temp.is_dir()

# Copy the CWD to a unique directory inside TEMP and ensure that one of the path
# components is long enough to have a distinct 8.3 form.
# TEMP is likely to be on a drive that supports 8.3 form paths.
d = (temp / str(uuid.uuid4()) / "veryverylong").resolve()
d.parent.mkdir(parents=True)
try:
shutil.copytree(Path.cwd(), d)

# Replace the arguments of the command that name files with equivalents in
# our temp directory, prefixed with the 8.3 form.
cmd = [
a if Path(a).is_absolute() or not (d / a).is_file() else get_short_path(d / a)
for a in sys.argv[1:]
]

print(cmd)

sys.exit(subprocess.run(cmd).returncode)

finally:
shutil.rmtree(d.parent)
4 changes: 3 additions & 1 deletion cross-project-tests/lit.cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
config.test_format = lit.formats.ShTest(not llvm_config.use_lit_shell)

# suffixes: A list of file extensions to treat as test files.
config.suffixes = [".c", ".cl", ".cpp", ".m"]
config.suffixes = [".c", ".cl", ".cpp", ".m", ".test"]

# excludes: A list of directories to exclude from the testsuite. The 'Inputs'
# subdirectories contain auxiliary inputs for various tests in their parent
Expand Down Expand Up @@ -107,6 +107,8 @@ def get_required_attr(config, attr_name):
if lldb_path is not None:
config.available_features.add("lldb")

if llvm_config.use_llvm_tool("llvm-ar"):
config.available_features.add("llvm-ar")

def configure_dexter_substitutions():
"""Configure substitutions for host platform and return list of dependencies"""
Expand Down
17 changes: 16 additions & 1 deletion cross-project-tests/lit.site.cfg.py.in
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
@LIT_SITE_CFG_IN_HEADER@

import sys
import shutil, sys, os, uuid
import lit.util
from pathlib import Path

config.targets_to_build = "@TARGETS_TO_BUILD@".split()
config.llvm_src_root = "@LLVM_SOURCE_DIR@"
Expand All @@ -22,7 +23,21 @@ config.mlir_src_root = "@MLIR_SOURCE_DIR@"
config.llvm_use_sanitizer = "@LLVM_USE_SANITIZER@"

import lit.llvm

lit.llvm.initialize(lit_config, config)

# Let the main config do the real work.
lit_config.load_config(config, "@CROSS_PROJECT_TESTS_SOURCE_DIR@/lit.cfg.py")


def are_short_names_enabled(path):
d = Path(path) / str(uuid.uuid4()) / "verylongdir"
d.mkdir(parents=True)
try:
return (d.parent / "verylo~1").exists()
finally:
shutil.rmtree(d.parent)


if os.name == "nt" and are_short_names_enabled(config.environment.get("TEMP")):
config.available_features.add("tempshortpaths")
18 changes: 18 additions & 0 deletions lld/Common/Filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
#include <unistd.h>
#endif
#include <thread>
#if defined(_WIN32)
#include "llvm/Support/ConvertUTF.h"
#include "llvm/Support/Windows/WindowsSupport.h"
#include "llvm/Support/WindowsError.h"
#include <io.h>
#endif

using namespace llvm;
using namespace lld;
Expand Down Expand Up @@ -155,3 +161,15 @@ std::unique_ptr<raw_fd_ostream> lld::openLTOOutputFile(StringRef file) {
return fs;
return openFile(file);
}

std::error_code lld::dtltoNormalizePath(std::string &PathOut) {
#if defined(_WIN32)
llvm::SmallString<128> Expanded;
if (auto EC = llvm::sys::windows::makeLong(PathOut, Expanded))
return EC;

PathOut.assign(Expanded.begin(), Expanded.end());
#endif
(void)PathOut;
return std::error_code{};
}
4 changes: 4 additions & 0 deletions lld/ELF/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ struct Config {
llvm::SmallVector<llvm::StringRef, 0> searchPaths;
llvm::SmallVector<llvm::StringRef, 0> symbolOrderingFile;
llvm::SmallVector<llvm::StringRef, 0> thinLTOModulesToCompile;
llvm::StringRef dtltoDistributor;
llvm::SmallVector<llvm::StringRef, 0> dtltoDistributorArgs;
llvm::StringRef dtltoCompiler;
llvm::SmallVector<llvm::StringRef, 0> dtltoCompilerArgs;
llvm::SmallVector<llvm::StringRef, 0> undefined;
llvm::SmallVector<SymbolVersion, 0> dynamicList;
llvm::SmallVector<uint8_t, 0> buildIdVector;
Expand Down
5 changes: 5 additions & 0 deletions lld/ELF/Driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,11 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
args.hasFlag(OPT_dependent_libraries, OPT_no_dependent_libraries, true);
ctx.arg.disableVerify = args.hasArg(OPT_disable_verify);
ctx.arg.discard = getDiscard(args);
ctx.arg.dtltoDistributor = args.getLastArgValue(OPT_thinlto_distributor_eq);
ctx.arg.dtltoDistributorArgs =
args::getStrings(args, OPT_thinlto_distributor_arg);
ctx.arg.dtltoCompiler = args.getLastArgValue(OPT_thinlto_compiler_eq);
ctx.arg.dtltoCompilerArgs = args::getStrings(args, OPT_thinlto_compiler_arg);
ctx.arg.dwoDir = args.getLastArgValue(OPT_plugin_opt_dwo_dir_eq);
ctx.arg.dynamicLinker = getDynamicLinker(ctx, args);
ctx.arg.ehFrameHdr =
Expand Down
46 changes: 42 additions & 4 deletions lld/ELF/InputFiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
#include "SyntheticSections.h"
#include "Target.h"
#include "lld/Common/DWARF.h"
#include "lld/Common/Filesystem.h"
#include "llvm/ADT/CachedHashString.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/LTO/LTO.h"
#include "llvm/Object/Archive.h"
#include "llvm/Object/IRObjectFile.h"
#include "llvm/Support/ARMAttributeParser.h"
#include "llvm/Support/ARMBuildAttributes.h"
Expand Down Expand Up @@ -1739,6 +1741,36 @@ static uint8_t getOsAbi(const Triple &t) {
}
}

// For DTLTO, bitcode member names must be valid paths to files on disk.
// For thin archives, resolve `memberPath` relative to the archive's location.
// Returns true if adjusted; false otherwise. Non-thin archives are unsupported.
static bool dtltoAdjustMemberPathIfThinArchive(Ctx &ctx, StringRef archivePath,
std::string &memberPath) {
assert(!archivePath.empty() && !ctx.arg.dtltoDistributor.empty());

// Check if the archive file is a thin archive by reading its header.
auto bufferOrErr =
MemoryBuffer::getFileSlice(archivePath, sizeof(ThinArchiveMagic) - 1, 0);
if (std::error_code ec = bufferOrErr.getError()) {
ErrAlways(ctx) << "cannot open " << archivePath << ": " << ec.message();
return false;
}

if (!bufferOrErr->get()->getBuffer().starts_with(ThinArchiveMagic))
return false;

SmallString<64> resolvedPath;
if (path::is_relative(memberPath)) {
resolvedPath = path::parent_path(archivePath);
path::append(resolvedPath, memberPath);
memberPath = resolvedPath.str();
}

lld::dtltoNormalizePath(memberPath);

return true;
}

BitcodeFile::BitcodeFile(Ctx &ctx, MemoryBufferRef mb, StringRef archiveName,
uint64_t offsetInArchive, bool lazy)
: InputFile(ctx, BitcodeKind, mb) {
Expand All @@ -1749,17 +1781,23 @@ BitcodeFile::BitcodeFile(Ctx &ctx, MemoryBufferRef mb, StringRef archiveName,
if (ctx.arg.thinLTOIndexOnly)
path = replaceThinLTOSuffix(ctx, mb.getBufferIdentifier());

if (!ctx.arg.dtltoDistributor.empty())
lld::dtltoNormalizePath(path);

// ThinLTO assumes that all MemoryBufferRefs given to it have a unique
// name. If two archives define two members with the same name, this
// causes a collision which result in only one of the objects being taken
// into consideration at LTO time (which very likely causes undefined
// symbols later in the link stage). So we append file offset to make
// filename unique.
StringSaver &ss = ctx.saver;
StringRef name = archiveName.empty()
? ss.save(path)
: ss.save(archiveName + "(" + path::filename(path) +
" at " + utostr(offsetInArchive) + ")");
StringRef name =
(archiveName.empty() ||
(!ctx.arg.dtltoDistributor.empty() &&
dtltoAdjustMemberPathIfThinArchive(ctx, archiveName, path)))
? ss.save(path)
: ss.save(archiveName + "(" + path::filename(path) + " at " +
utostr(offsetInArchive) + ")");
MemoryBufferRef mbref(mb.getBuffer(), name);

obj = CHECK2(lto::InputFile::create(mbref), this);
Expand Down
Loading