From e83c5a1156ed62df807ea442539f05cda0d076bc Mon Sep 17 00:00:00 2001 From: Clint Branham Date: Wed, 29 Jul 2026 20:05:00 -0500 Subject: [PATCH] Canonicalize the path returned by current_binary_dir() current_binary_dir() returns path(dli_fname).parent_path() unresolved, and the two JIT include-path lookups then take parent_path() of that value again: include_path_args() in cuda/jit_module.cpp and get_preamble() in cpu/jit_compiler.cpp both compute root_dir = current_binary_dir().parent_path() and search root_dir/"include". parent_path() is lexical -- it strips the last component of the spelling, not of the directory -- so two spellings of the same directory produce different include roots: dli_fname current_binary_dir() root_dir include root /p/lib/libmlx.so /p/lib /p /p/include /p/lib/./libmlx.so /p/lib/. /p/lib /p/lib/include ./tests . "" resolved against CWD On Linux glibc reports dli_fname for a main executable as argv[0] verbatim, so which row applies depends on how the process was started. A single-component path has an empty parent, which drops the include root onto the current working directory. This is what made a CUDA build fail to JIT when run from its build tree: mlx/backend/cuda/device/indexing.cuh(3): catastrophic error: cannot open source file "cuda/std/tuple" with the CCCL headers present and readable at the path the canonicalized lookup computes. Holding the invocation fixed and varying only this commit on a GB10 (sm_121, CUDA 13.0.88): the base FAILs with the error above, the base plus this commit PASSes. weakly_canonical() resolves ".", ".." and symlinks and roots a CWD-relative path. Two guards around it: - A dli_fname with no directory component (glibc gives a bare argv[0] for a PATH-launched executable) carries no location to resolve, so it returns the empty parent unchanged. Without this, libc++ and libstdc++ disagree: libc++ resolves a bare name against the current working directory and invents an unrelated absolute answer, where libstdc++ leaves it alone. - The error_code overload, because the throwing one is reachable. The value is computed lazily in a function-local static, so a relative dli_fname whose CWD has since been removed makes weakly_canonical throw where the old lexical code could not fail. That matters most in delayload.cpp, which runs inside a Windows delay-load hook where an escaping exception would terminate rather than fail the load. Resolving symlinks is a deliberate part of this: for an install reached through a symlinked lib directory, the sibling include/ tree lives next to the real path, not next to the link. The declaration gains MLX_API. The library is built with hidden visibility, so without it the new test does not link in a shared build. This is a no-op on macOS, where dyld reports a fully resolved dli_fname in every one of those cases. --- mlx/backend/common/utils.cpp | 14 +++++++++++++- mlx/backend/common/utils.h | 7 +++++-- tests/utils_tests.cpp | 12 ++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/mlx/backend/common/utils.cpp b/mlx/backend/common/utils.cpp index ae169e35e2..5598076eb4 100644 --- a/mlx/backend/common/utils.cpp +++ b/mlx/backend/common/utils.cpp @@ -12,7 +12,19 @@ std::filesystem::path current_binary_dir() { if (!dladdr(reinterpret_cast(¤t_binary_dir), &info)) { throw std::runtime_error("Unable to get current binary dir."); } - return std::filesystem::path(info.dli_fname).parent_path(); + // glibc reports dli_fname for a main executable as argv[0] verbatim, so it + // may be relative or contain "."; resolve it so that parent_path() of the + // result names the real parent directory. + std::filesystem::path path(info.dli_fname); + if (!path.has_parent_path()) { + // A bare argv[0] carries no location, and the two standard libraries + // disagree about it: libc++ would resolve it against the current working + // directory, inventing an unrelated answer. + return path.parent_path(); + } + std::error_code ec; + auto resolved = std::filesystem::weakly_canonical(path, ec); + return ec ? path.parent_path() : resolved.parent_path(); }(); return binary_dir; } diff --git a/mlx/backend/common/utils.h b/mlx/backend/common/utils.h index c6d7820619..b1873493b7 100644 --- a/mlx/backend/common/utils.h +++ b/mlx/backend/common/utils.h @@ -6,12 +6,15 @@ #include #include +#include "mlx/api.h" #include "mlx/array.h" namespace mlx::core { -// Return the directory that contains current shared library. -std::filesystem::path current_binary_dir(); +// Return the resolved directory that contains the current binary, with ".", +// ".." and symlinks removed. Empty when the loader reports no location for +// it, which glibc does for a PATH-launched executable. +MLX_API std::filesystem::path current_binary_dir(); inline int64_t elem_to_loc(int elem, const Shape& shape, const Strides& strides) { diff --git a/tests/utils_tests.cpp b/tests/utils_tests.cpp index 88c3e7b378..38a18bb3fd 100644 --- a/tests/utils_tests.cpp +++ b/tests/utils_tests.cpp @@ -2,10 +2,22 @@ #include "doctest/doctest.h" +#include "mlx/backend/common/utils.h" #include "mlx/mlx.h" using namespace mlx::core; +TEST_CASE("test current binary dir is resolved") { + auto dir = current_binary_dir(); + // Empty is a legal result: a PATH-launched executable gets a bare argv[0], + // which carries no location to resolve. + if (dir.empty()) { + return; + } + CHECK(dir.is_absolute()); + CHECK_EQ(dir, dir.lexically_normal()); +} + TEST_CASE("test type promotion") { for (auto t : {bool_, uint32, int32, int64, float32}) { auto a = array(0, t);