Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion hatch_cpp/tests/test_platform_specific.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def test_link_flags_include_platform_specific_link_args(self):

flags = platform.get_link_flags(library)
assert "-shared" in flags
assert "-Wl,-rpath,$ORIGIN/lib" in flags
assert r"-Wl,-rpath,\$ORIGIN/lib" in flags

def test_darwin_platform_uses_darwin_specific_fields(self):
"""Test that darwin platform uses darwin-specific fields."""
Expand Down
70 changes: 70 additions & 0 deletions hatch_cpp/tests/test_structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from toml import loads

from hatch_cpp import HatchCppBuildConfig, HatchCppBuildPlan, HatchCppLibrary, HatchCppPlatform
from hatch_cpp.toolchains.common import _normalize_rpath


class TestStructs:
Expand Down Expand Up @@ -168,3 +169,72 @@ def test_hatch_cpp_vcpkg_env_force_on(self):
with patch.dict(environ, {"HATCH_CPP_VCPKG": "1"}):
hatch_build_plan.generate()
assert "vcpkg" in hatch_build_plan._active_toolchains


class TestNormalizeRpath:
def test_origin_to_loader_path_on_darwin(self):
"""$ORIGIN should be translated to @loader_path on macOS."""
assert _normalize_rpath("-Wl,-rpath,$ORIGIN", "darwin") == "-Wl,-rpath,@loader_path"

def test_loader_path_to_origin_on_linux(self):
"""@loader_path should be translated to (escaped) $ORIGIN on Linux."""
result = _normalize_rpath("-Wl,-rpath,@loader_path", "linux")
assert result == r"-Wl,-rpath,\$ORIGIN"

def test_origin_escaped_on_linux(self):
"""$ORIGIN should be escaped as \\$ORIGIN on Linux for shell safety."""
result = _normalize_rpath("-Wl,-rpath,$ORIGIN", "linux")
assert result == r"-Wl,-rpath,\$ORIGIN"

def test_already_escaped_origin_on_darwin(self):
"""Already-escaped \\$ORIGIN should still translate to @loader_path on macOS."""
assert _normalize_rpath(r"-Wl,-rpath,\$ORIGIN", "darwin") == "-Wl,-rpath,@loader_path"

def test_no_rpath_unchanged(self):
"""Args without rpath values should pass through unchanged."""
assert _normalize_rpath("-lfoo", "linux") == "-lfoo"
assert _normalize_rpath("-lfoo", "darwin") == "-lfoo"

def test_win32_no_transform(self):
"""Windows should not transform rpath values."""
assert _normalize_rpath("$ORIGIN", "win32") == "$ORIGIN"
assert _normalize_rpath("@loader_path", "win32") == "@loader_path"

def test_link_flags_rpath_translation_darwin(self):
"""Full integration: extra_link_args with $ORIGIN produce @loader_path on macOS."""
library = HatchCppLibrary(
name="test",
sources=["test.cpp"],
binding="generic",
extra_link_args=["-Wl,-rpath,$ORIGIN"],
)
platform = HatchCppPlatform(
cc="clang",
cxx="clang++",
ld="ld",
platform="darwin",
toolchain="clang",
disable_ccache=True,
)
flags = platform.get_link_flags(library)
assert "@loader_path" in flags
assert "$ORIGIN" not in flags

def test_link_flags_rpath_escaped_linux(self):
"""Full integration: extra_link_args with $ORIGIN are shell-escaped on Linux."""
library = HatchCppLibrary(
name="test",
sources=["test.cpp"],
binding="generic",
extra_link_args=["-Wl,-rpath,$ORIGIN"],
)
platform = HatchCppPlatform(
cc="gcc",
cxx="g++",
ld="ld",
platform="linux",
toolchain="gcc",
disable_ccache=True,
)
flags = platform.get_link_flags(library)
assert r"\$ORIGIN" in flags
26 changes: 26 additions & 0 deletions hatch_cpp/toolchains/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"PlatformDefaults",
"HatchCppLibrary",
"HatchCppPlatform",
"_normalize_rpath",
)


Expand Down Expand Up @@ -207,6 +208,28 @@ def get_effective_undef_macros(self, platform: Platform) -> List[str]:
return macros


def _normalize_rpath(value: str, platform: Platform) -> str:
r"""Translate and escape rpath values for the target platform.

- On macOS (darwin): ``$ORIGIN`` is replaced with ``@loader_path``.
- On Linux: ``@loader_path`` is replaced with ``$ORIGIN``, and
``$ORIGIN`` is escaped as ``\$ORIGIN`` so that ``os.system()``
(which invokes a shell) passes it through literally.
- On Windows: no transformation is applied (Windows does not use
rpath).
"""
if platform == "darwin":
# Handle already-escaped \$ORIGIN first, then plain $ORIGIN
value = value.replace(r"\$ORIGIN", "@loader_path")
value = value.replace("$ORIGIN", "@loader_path")
elif platform == "linux":
# Translate macOS rpath to Linux equivalent
value = value.replace("@loader_path", "$ORIGIN")
# Escape $ORIGIN for shell safety (os.system runs through bash)
value = value.replace("$ORIGIN", r"\$ORIGIN")
return value


class HatchCppPlatform(BaseModel):
cc: str
cxx: str
Expand Down Expand Up @@ -336,6 +359,9 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele
effective_libraries = library.get_effective_libraries(self.platform)
effective_library_dirs = library.get_effective_library_dirs(self.platform)

# Normalize rpath values ($ORIGIN <-> @loader_path) and escape for shell
effective_link_args = [_normalize_rpath(arg, self.platform) for arg in effective_link_args]

if self.toolchain == "gcc":
flags += " -shared"
flags += " " + " ".join(effective_link_args)
Expand Down