diff --git a/hatch_cpp/tests/test_platform_specific.py b/hatch_cpp/tests/test_platform_specific.py index 34778bd..455d7e1 100644 --- a/hatch_cpp/tests/test_platform_specific.py +++ b/hatch_cpp/tests/test_platform_specific.py @@ -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.""" diff --git a/hatch_cpp/tests/test_structs.py b/hatch_cpp/tests/test_structs.py index afa0c25..fa22880 100644 --- a/hatch_cpp/tests/test_structs.py +++ b/hatch_cpp/tests/test_structs.py @@ -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: @@ -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 diff --git a/hatch_cpp/toolchains/common.py b/hatch_cpp/toolchains/common.py index 120ed4a..300f4e8 100644 --- a/hatch_cpp/toolchains/common.py +++ b/hatch_cpp/toolchains/common.py @@ -20,6 +20,7 @@ "PlatformDefaults", "HatchCppLibrary", "HatchCppPlatform", + "_normalize_rpath", ) @@ -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 @@ -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)