4
4
5
5
from __future__ import annotations
6
6
7
+ from pathlib import Path
7
8
from typing import TYPE_CHECKING
8
9
9
- import auditwheel # noqa: F401
10
+ from auditwheel .elfutils import elf_read_rpaths
11
+ from auditwheel .patcher import Patchelf
10
12
13
+ from .._logging import logger
11
14
from . import WheelRepairer
12
15
13
16
if TYPE_CHECKING :
@@ -23,6 +26,133 @@ class LinuxWheelRepairer(WheelRepairer):
23
26
24
27
_platform = "Linux"
25
28
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
+
26
134
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