44
55from __future__ import annotations
66
7+ from pathlib import Path
78from typing import TYPE_CHECKING
89
9- import auditwheel # noqa: F401
10+ from auditwheel .elfutils import elf_read_rpaths
11+ from auditwheel .patcher import Patchelf
1012
13+ from .._logging import logger
1114from . import WheelRepairer
1215
1316if TYPE_CHECKING :
@@ -23,6 +26,133 @@ class LinuxWheelRepairer(WheelRepairer):
2326
2427 _platform = "Linux"
2528
29+ def patch_linux_library_rpath (self , artifact : Path , rpaths : list [str ]) -> None :
30+ """Patch the rpaths of a specific library."""
31+ # Flatten the current rpaths
32+ curr_rpaths = {
33+ path
34+ for dt_rpaths in elf_read_rpaths (artifact ).values ()
35+ for path in dt_rpaths
36+ }
37+ final_rpaths = set ()
38+ # Patch pre-existing rpaths
39+ for rpath_str in curr_rpaths :
40+ # If the rpath is already relative keep it
41+ if rpath_str .startswith ("$ORIGIN" ):
42+ final_rpaths .add (rpath_str )
43+ continue
44+ # Otherwise check if we need to patch it
45+ rpath_path = Path (rpath_str )
46+ if not self .path_is_in_site_packages (rpath_path ):
47+ # If it does not point to wheel install path, just keep it
48+ final_rpaths .add (rpath_str )
49+ continue
50+ # Otherwise change the RPATH to point use $ORIGIN
51+ new_rpath = self .path_relative_site_packages (rpath_path , artifact .parent )
52+ new_rpath_str = f"$ORIGIN/{ new_rpath } "
53+ final_rpaths .add (new_rpath_str )
54+ # Merge with all the rpaths we were given
55+ final_rpaths = final_rpaths .union (rpaths )
56+ patcher = Patchelf ()
57+ patcher .set_rpath (artifact , ":" .join (final_rpaths ))
58+
59+ def get_dependency_rpaths (self , target : Target , install_path : Path ) -> list [str ]:
60+ """Get the rpaths due to target link dependencies."""
61+ target_path = self .install_dir / install_path
62+ rpaths = []
63+ for dep_target in self .get_library_dependencies (target ):
64+ dep_install_paths = self .get_wheel_install_paths (dep_target )
65+ assert len (dep_install_paths ) == 1
66+ dep_install_path = self .install_dir / next (iter (dep_install_paths ))
67+ rpath = self .path_relative_site_packages (dep_install_path , target_path )
68+ new_rpath_str = f"$ORIGIN/{ rpath } "
69+ rpaths .append (new_rpath_str )
70+ return rpaths
71+
72+ def get_package_rpaths (self , target : Target , install_path : Path ) -> list [str ]:
73+ """
74+ Get the rpaths due to external package linkage.
75+
76+ Have to use the linker flags until the package targets are exposed.
77+ https://gitlab.kitware.com/cmake/cmake/-/issues/26755
78+ """
79+ if not target .link :
80+ return []
81+ rpaths = []
82+ for link_command in target .link .commandFragments :
83+ if link_command .role == "flags" :
84+ if not link_command .fragment :
85+ logger .debug (
86+ "Skipping {target} link-flags: {flags}" ,
87+ target = target .name ,
88+ flags = link_command .fragment ,
89+ )
90+ continue
91+ if link_command .role != "libraries" :
92+ logger .warning (
93+ "File-api link role {role} is not supported. "
94+ "Target={target}, command={command}" ,
95+ target = target .name ,
96+ role = link_command .role ,
97+ command = link_command .fragment ,
98+ )
99+ continue
100+ # Try to parse `-Wl,-rpath` flags
101+ if link_command .fragment .startswith ("-Wl,-rpath," ):
102+ # removeprefix(`-Wl,-rpath,`) but compatible with Python 3.9
103+ check_rpaths = link_command .fragment [len ("-Wl,-rpath," ) :]
104+ for rpath_str in check_rpaths .split (":" ):
105+ if not rpath_str :
106+ # Skip empty rpaths. Most likely will have on at the end
107+ continue
108+ rpath = Path (rpath_str )
109+ if not self .path_is_in_site_packages (rpath ):
110+ # Skip any paths that cannot be handled. We do not check for paths in
111+ # the build directory, it should be covered by `get_dependency_rpaths`
112+ continue
113+ rpath = self .path_relative_site_packages (rpath , install_path )
114+ new_rpath_str = f"$ORIGIN/{ rpath } "
115+ rpaths .append (new_rpath_str )
116+ # The remaining case should be a path
117+ try :
118+ # TODO: how to best catch if a string is a valid path?
119+ rpath = Path (link_command .fragment )
120+ if not rpath .is_absolute ():
121+ # Relative paths should be handled by `get_dependency_rpaths`
122+ continue
123+ rpath = self .path_relative_site_packages (rpath , install_path )
124+ new_rpath_str = f"$ORIGIN/{ rpath .parent } "
125+ rpaths .append (new_rpath_str )
126+ except Exception :
127+ logger .warning (
128+ "Could not parse link-library as a path: {fragment}" ,
129+ fragment = link_command .fragment ,
130+ )
131+ continue
132+ return rpaths
133+
26134 def patch_target (self , target : Target ) -> None :
27- # TODO: Implement patching
28- pass
135+ # Get the target install paths where the $ORIGIN is calculated from
136+ target_install_paths = self .get_wheel_install_paths (target )
137+ if not target_install_paths :
138+ logger .debug (
139+ "Skip patching {target} because all install paths are outside the wheel." ,
140+ target = target .name ,
141+ )
142+ return
143+ if len (set (target .artifacts )) != 1 :
144+ logger .warning (
145+ "Unexpected multiple artifacts for target {target}: {artifacts}" ,
146+ target = target .name ,
147+ artifacts = [item .path for item in target .artifacts ],
148+ )
149+ return
150+ artifact = target .artifacts [0 ]
151+ for install_path in target_install_paths :
152+ target_path = self .install_dir / install_path
153+ dependency_rpaths = self .get_dependency_rpaths (target , install_path )
154+ package_rpaths = self .get_package_rpaths (target , install_path )
155+ self .patch_linux_library_rpath (
156+ artifact = target_path / artifact .path ,
157+ rpaths = [* dependency_rpaths , * package_rpaths ],
158+ )
0 commit comments