Skip to content

[LinkerWrapper] Add support for --no-canonical-prefixes#184160

Merged
boomanaiden154 merged 5 commits intomainfrom
users/boomanaiden154/linkerwrapper-add-support-for-no-canonical-prefixes
Mar 5, 2026
Merged

[LinkerWrapper] Add support for --no-canonical-prefixes#184160
boomanaiden154 merged 5 commits intomainfrom
users/boomanaiden154/linkerwrapper-add-support-for-no-canonical-prefixes

Conversation

@boomanaiden154
Copy link
Contributor

This is necessary to support build environments where the
compiler/associated tools are actually just symlinks into a CAS. Without
this, we try and resolve binaries relative to the real path of
clang-linker-wrapper, which is usually in a directory prefixed with the
first couple characters of a SHA digest and named with a SHA digest. We
also need to ensure that we propagate --no-canonical-prefixes to sub
clang invocations so that clang is able to resolve lld in such
environments.

Created using spr 1.3.7
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' labels Mar 2, 2026
@llvmbot
Copy link
Member

llvmbot commented Mar 2, 2026

@llvm/pr-subscribers-clang-driver

@llvm/pr-subscribers-clang

Author: Aiden Grossman (boomanaiden154)

Changes

This is necessary to support build environments where the
compiler/associated tools are actually just symlinks into a CAS. Without
this, we try and resolve binaries relative to the real path of
clang-linker-wrapper, which is usually in a directory prefixed with the
first couple characters of a SHA digest and named with a SHA digest. We
also need to ensure that we propagate --no-canonical-prefixes to sub
clang invocations so that clang is able to resolve lld in such
environments.


Full diff: https://github.com/llvm/llvm-project/pull/184160.diff

3 Files Affected:

  • (added) clang/test/Driver/linker-wrapper-canonical-prefixes.c (+25)
  • (modified) clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp (+9)
  • (modified) clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td (+4)
diff --git a/clang/test/Driver/linker-wrapper-canonical-prefixes.c b/clang/test/Driver/linker-wrapper-canonical-prefixes.c
new file mode 100644
index 0000000000000..79152cc69c078
--- /dev/null
+++ b/clang/test/Driver/linker-wrapper-canonical-prefixes.c
@@ -0,0 +1,25 @@
+// UNSUPPORTED: system-windows
+// REQUIRES: x86-registered-target
+// REQUIRES: amdgpu-registered-target
+
+// An externally visible variable so static libraries extract.
+__attribute__((visibility("protected"), used)) int x;
+
+// RUN: rm -rf %t.test_dir && mkdir -p %t.test_dir
+// RUN: touch %t.test_dir/clang
+// RUN: chmod +x %t.test_dir/clang
+// RUN: ln -s clang-linker-wrapper %t.test_dir/clang-linker-wrapper
+
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t.elf.o
+// RUN: %clang -cc1 %s -triple amdgcn-amd-amdhsa -emit-llvm-bc -o %t.amdgpu.bc
+
+// RUN: llvm-offload-binary -o %t.out \
+// RUN:   --image=file=%t.elf.o,kind=openmp,triple=amdgcn-amd-amdhsa,arch=gfx908 \
+// RUN:   --image=file=%t.elf.o,kind=openmp,triple=amdgcn-amd-amdhsa,arch=gfx908
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t.o -fembed-offload-object=%t.out
+// RUN: %t.test_dir/clang-linker-wrapper --host-triple=x86_64-unknown-linux-gnu --dry-run \
+// RUN:   --linker-path=/usr/bin/ld %t.o -o a.out --no-canonical-prefixes 2>&1 | FileCheck %s
+
+// Check that we resolve clang to the symlink rather than the bin/ directory
+// and that the sub-clang invocation was passed -no-canonical-prefixes.
+// CHECK: test_dir/clang" {{.*}} -no-canonical-prefixes
diff --git a/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp b/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
index c49ce44432e5a..b2b6910cd7e0b 100644
--- a/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
+++ b/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
@@ -140,6 +140,9 @@ static std::list<SmallString<128>> TempFiles;
 /// Codegen flags for LTO backend.
 static codegen::RegisterCodeGenFlags CodeGenFlags;
 
+/// Whether or not to look through symlinks when resolving binaries.
+static bool CanonicalPrefixes = true;
+
 using OffloadingImage = OffloadBinary::OffloadingImage;
 
 namespace llvm {
@@ -219,6 +222,8 @@ void printCommands(ArrayRef<StringRef> CmdArgs) {
 }
 
 std::string getMainExecutable(const char *Name) {
+  if (!CanonicalPrefixes)
+    return sys::path::parent_path(LinkerExecutable).str();
   void *Ptr = (void *)(intptr_t)&getMainExecutable;
   auto COWPath = sys::fs::getMainExecutable(Name, Ptr);
   return sys::path::parent_path(COWPath).str();
@@ -539,6 +544,9 @@ Expected<StringRef> clang(ArrayRef<StringRef> InputFiles, const ArgList &Args,
       Args.MakeArgString("--target=" + Triple.getTriple()),
   };
 
+  if (!CanonicalPrefixes)
+    CmdArgs.push_back("-no-canonical-prefixes");
+
   if (!Arch.empty())
     Triple.isAMDGPU() ? CmdArgs.push_back(Args.MakeArgString("-mcpu=" + Arch))
                       : CmdArgs.push_back(Args.MakeArgString("-march=" + Arch));
@@ -1350,6 +1358,7 @@ int main(int Argc, char **Argv) {
   DryRun = Args.hasArg(OPT_dry_run);
   SaveTemps = Args.hasArg(OPT_save_temps);
   CudaBinaryPath = Args.getLastArgValue(OPT_cuda_path_EQ).str();
+  CanonicalPrefixes = !Args.hasArg(OPT_no_canonical_prefixes);
 
   llvm::Triple Triple(
       Args.getLastArgValue(OPT_host_triple_EQ, sys::getDefaultTargetTriple()));
diff --git a/clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td b/clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td
index ef3a16b2f58bb..973e5bb51a507 100644
--- a/clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td
+++ b/clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td
@@ -69,6 +69,10 @@ def emit_fatbin_only
       Flags<[WrapperOnlyOption]>,
       HelpText<"Emit fat binary directly without wrapping or host linking">;
 
+def no_canonical_prefixes : Flag<["--"], "no-canonical-prefixes">,
+  Flags<[WrapperOnlyOption]>,
+  HelpText<"Do not resolve symbolic links, turn relative paths into absolute ones, or do anything else to identify the executable">;
+
 // Flags passed to the device linker.
 def arch_EQ : Joined<["--"], "arch=">,
   Flags<[DeviceOnlyOption, HelpHidden]>, MetaVarName<"<arch>">,

@boomanaiden154 boomanaiden154 requested review from Artem-B, jhuber6 and sarnex and removed request for jhuber6 March 2, 2026 15:52
// RUN: %clang -cc1 %s -triple amdgcn-amd-amdhsa -emit-llvm-bc -o %t.amdgpu.bc

// RUN: llvm-offload-binary -o %t.out \
// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgcn-amd-amdhsa,arch=gfx908 \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is having the same --image line twice intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. Just me copying and pasting from other tests. Removed.

Flags<[WrapperOnlyOption]>,
HelpText<"Emit fat binary directly without wrapping or host linking">;

def no_canonical_prefixes : Flag<["--"], "no-canonical-prefixes">,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something other tools handle? Is that handling uniform?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only tools that need to resolve binary paths/invoke them. So probably not most tools, but definitely this one and the clang driver (which already supports it). In my limited understanding, support is relatively uniform across the standard toolchain binaries.

Created using spr 1.3.7
@boomanaiden154 boomanaiden154 requested review from jhuber6 and sarnex March 2, 2026 16:27
};

if (!CanonicalPrefixes)
CmdArgs.push_back("-no-canonical-prefixes");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguments like this that are meant to be propagated are better handled via the list at the top of when we make the clang-linker-wrapper job. The onyl change here seems to be for getting the directory of the current executable, but I'm not sure why that needs to be changed. If there's another way to do this then it's just a one line change to add that to the list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List at the top as in the definition of CmdArgs right above?

We don't want to propagate this unconditionally because it could change the behavior and would also make this inconsistent with the default behavior of the clang driver.

We need to change the way we get the directory of the current executable because otherwise we cannot resolve the path to clang if clang-linker-wrapper is a symlink in a directory that contains clang to a file in a directory that does not contain a binary named clang.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so the part that forwards it to the embedded job should just be handled by the list. The part that changes the relative search path for the main executable. If there's no way to detect if we should do that without the option then it should probably be a separate thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no way to detect if we should do that without the option then it should probably be a separate thing.

What exactly do you mean here by "a separate thing"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just wondering if it would be easier to rely on the forwarding behavior, but since we also need the option for the linker wrapper itself I suppose it's easier to have a single option.

Can we use the args list here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Required making clang-linker-wrapper accept -no-canonical-prefixes rather than --no-canonical-prefixes which I guess is inconsistent with the rest of the clang-linker-wrapper options, but makes it more consistent with the clang driver.

@jhuber6
Copy link
Contributor

jhuber6 commented Mar 2, 2026

Having two PRs for this is confusing, can you just close the other one and put that change here?

Created using spr 1.3.7
OPT_save_temps_EQ,
OPT_mcode_object_version_EQ,
OPT_load,
OPT_no_canonical_prefixes,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will automatically forward it to the internal clang, but not the internal uses of the tools. I'm wondering if we shouldn't just pass some directory from clang in this option is set. I think we do something similar with the CUDA path and this is basically the same thing IIRC.

Does that sound reasonable? We'd have clang pass in a path and that would be used instead of looking it up from the main executable if we assume the clang driver to be the arbiter of truth here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in LinkerWrapper::ConstructJob. It forwards specifically to clang-linker-wrapper as far as I can tell.

We could pass the directory to clang down into clang-linker-wrapper, but then you run into the same problem with any other tool that clang-linker-wrapper could invoke like llvm-objcopy. I don't think passing in flags for all the tools makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clang driver computes the paths for these tools, I was more referring to passing forward the root of the 'clang-toolchain' since this config basically assumes they're disparate as far as I'm understanding. Not dissimilar to finding the GCC prefix or CUDA root?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that would work.

I don't see the advantage of that approach over supporting this flag though. If we do what you're proposing, we end up with the following:

  1. Propagation of -no-canonical-prefixes through --device-compiler to sub-clang invocations as currently.
  2. Replacement of -no-canonical-prefixes passed to clang-linker-wrapper with the clang driver passing something like -toolchain-dir=<resolved toolchain dir>. Instead of updating getExecutableDir, we need to update findProgram to take the new flag into account.

I think the complexity ends up being about the same at the cost of divergence. Instead of having one way that both the driver and clang-linker-wrapper can specify this behavior, now we have two different ways of achieving the same thing.

Created using spr 1.3.7
Created using spr 1.3.7
@boomanaiden154
Copy link
Contributor Author

@jhuber6 What do I need to do so that we can move forward with this? Now that the new offload driver is the default in more configurations (from #123359), this issue is blocking our toolchain release.

Copy link
Contributor

@jhuber6 jhuber6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine from what I understand

@boomanaiden154 boomanaiden154 merged commit 8ef1224 into main Mar 5, 2026
10 checks passed
@boomanaiden154 boomanaiden154 deleted the users/boomanaiden154/linkerwrapper-add-support-for-no-canonical-prefixes branch March 5, 2026 14:47
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Mar 5, 2026
This is necessary to support build environments where the
compiler/associated tools are actually just symlinks into a CAS. Without
this, we try and resolve binaries relative to the real path of
clang-linker-wrapper, which is usually in a directory prefixed with the
first couple characters of a SHA digest and named with a SHA digest. We
also need to ensure that we propagate --no-canonical-prefixes to sub
clang invocations so that clang is able to resolve lld in such
environments.

Reviewers: jhuber6, Artem-B, sarnex

Pull Request: llvm/llvm-project#184160
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants