@@ -265,24 +265,32 @@ static ELF_ALLOWED_LIBRARIES_BY_TRIPLE: Lazy<HashMap<&'static str, Vec<&'static
265265 . collect ( )
266266 } ) ;
267267
268- static ELF_ALLOWED_LIBRARIES_BY_MODULE : Lazy < HashMap < & ' static str , Vec < & ' static str > > > =
269- Lazy :: new ( || {
270- [
271- (
272- // libcrypt is provided by the system, but only on older distros.
273- "_crypt" ,
274- vec ! [ "libcrypt.so.1" ] ,
275- ) ,
276- (
277- // libtcl and libtk are shipped in our distribution.
278- "_tkinter" ,
279- vec ! [ "libtcl8.6.so" , "libtk8.6.so" ] ,
280- ) ,
281- ]
282- . iter ( )
283- . cloned ( )
284- . collect ( )
285- } ) ;
268+ #[ derive( Copy , Clone , PartialEq ) ]
269+ enum DepSource {
270+ SystemRequired ,
271+ SystemOptional ,
272+ Vendored ,
273+ }
274+ use DepSource :: * ;
275+
276+ static ELF_ALLOWED_LIBRARIES_BY_MODULE : Lazy <
277+ HashMap < & ' static str , Vec < ( & ' static str , DepSource ) > > ,
278+ > = Lazy :: new ( || {
279+ [
280+ (
281+ // libcrypt is provided by the system, but only on older distros.
282+ "_crypt" ,
283+ vec ! [ ( "libcrypt.so.1" , SystemOptional ) ] ,
284+ ) ,
285+ (
286+ "_tkinter" ,
287+ vec ! [ ( "libtcl8.6.so" , Vendored ) , ( "libtk8.6.so" , Vendored ) ] ,
288+ ) ,
289+ ]
290+ . iter ( )
291+ . cloned ( )
292+ . collect ( )
293+ } ) ;
286294
287295static DARWIN_ALLOWED_DYLIBS : Lazy < Vec < MachOAllowedDylib > > = Lazy :: new ( || {
288296 [
@@ -1022,7 +1030,7 @@ fn validate_elf<Elf: FileHeader<Endian = Endianness>>(
10221030 if let Some ( filename) = path. file_name ( ) {
10231031 if let Some ( ( module, _) ) = filename. to_string_lossy ( ) . split_once ( ".cpython-" ) {
10241032 if let Some ( extra) = ELF_ALLOWED_LIBRARIES_BY_MODULE . get ( module) {
1025- allowed_libraries. extend ( extra. iter ( ) . map ( |x| x. to_string ( ) ) ) ;
1033+ allowed_libraries. extend ( extra. iter ( ) . map ( |x| x. 0 . to_string ( ) ) ) ;
10261034 }
10271035 }
10281036 }
@@ -2186,6 +2194,85 @@ fn verify_distribution_behavior(dist_path: &Path) -> Result<Vec<String>> {
21862194 errors. push ( "errors running interpreter tests" . to_string ( ) ) ;
21872195 }
21882196
2197+ // Explicitly test ldd directly on the extension modules, which PyInstaller
2198+ // relies on. This is not strictly needed for a working distribution (e.g.
2199+ // you can set an rpath on just python+libpython), so we test here for
2200+ // compatibility with tools that run ldd.
2201+ // that fails this check (e.g. by setting an rpath on just python+libpython).
2202+ // https://github.com/pyinstaller/pyinstaller/issues/9204#issuecomment-3171050891
2203+ // TODO(geofft): musl doesn't do lazy binding for the argument to
2204+ // ldd, so we will get complaints about missing Py_* symbols. Need
2205+ // to handle this somehow, skip testing for now.
2206+ if cfg ! ( target_os = "linux" ) && !python_json. target_triple . contains ( "-musl" ) {
2207+ // musl's ldd is packaged in the "musl-tools" Debian package.
2208+ let ldd = if python_json. target_triple . contains ( "-musl" ) && cfg ! ( not( target_env = "musl" ) ) {
2209+ "musl-ldd"
2210+ } else {
2211+ "ldd"
2212+ } ;
2213+ for ( name, variants) in python_json. build_info . extensions . iter ( ) {
2214+ for ext in variants {
2215+ let Some ( shared_lib) = & ext. shared_lib else {
2216+ continue ;
2217+ } ;
2218+ let shared_lib_path = temp_dir. path ( ) . join ( "python" ) . join ( shared_lib) ;
2219+ let output = duct:: cmd ( ldd, [ shared_lib_path] )
2220+ . unchecked ( )
2221+ . stdout_capture ( )
2222+ . run ( )
2223+ . context ( format ! ( "Failed to run `{ldd} {shared_lib}`" ) ) ?;
2224+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
2225+ // Format of ldd output, for both glibc and musl:
2226+ // - Everything starts with a tab.
2227+ // - Most things are "libxyz.so.1 => /usr/lib/libxyz.so.1 (0xabcde000)".
2228+ // - The ELF interpreter is displayed as just "/lib/ld.so (0xabcde000)".
2229+ // - glibc, but not musl, shows the vDSO as "linux-vdso.so.1 (0xfffff000)".
2230+ // - If a library is listed in DT_NEEDED with an absolute path, or (currently only
2231+ // supported on glibc) with an $ORIGIN-relative path, it displays as just
2232+ // "/path/to/libxyz.so (0xabcde000)".
2233+ // - On glibc, if a library cannot be found ldd returns zero and shows "=> not
2234+ // found" as the resolution (even if it wouldn't use the => form if found).
2235+ // - On musl, if a library cannot be found, ldd returns nonzero and shows "Error
2236+ // loading shared library ...:" on stderr.
2237+ if !output. status . success ( ) {
2238+ // TODO: If we ever have any optional dependencies besides libcrypt (which is
2239+ // glibc-only), we will need to capture musl ldd's stderr and parse it.
2240+ errors. push ( format ! (
2241+ "`{ldd} {shared_lib}` exited with {}:\n {stdout}" ,
2242+ output. status
2243+ ) ) ;
2244+ } else {
2245+ let mut ldd_errors = vec ! [ ] ;
2246+ let deps = ELF_ALLOWED_LIBRARIES_BY_MODULE . get ( & name[ ..] ) ;
2247+ let temp_dir_lossy = temp_dir. path ( ) . to_string_lossy ( ) . into_owned ( ) ;
2248+ for line in stdout. lines ( ) {
2249+ let Some ( ( needed, resolution) ) = line. trim ( ) . split_once ( " => " ) else {
2250+ continue ;
2251+ } ;
2252+ let dep_source = deps
2253+ . and_then ( |deps| {
2254+ deps. iter ( ) . find ( |dep| dep. 0 == needed) . map ( |dep| dep. 1 )
2255+ } )
2256+ . unwrap_or ( SystemRequired ) ;
2257+ if resolution. starts_with ( "not found" ) && dep_source != SystemOptional {
2258+ ldd_errors. push ( format ! ( "{needed} was expected to be found" ) ) ;
2259+ } else if !resolution. contains ( & temp_dir_lossy) && dep_source == Vendored {
2260+ ldd_errors. push ( format ! (
2261+ "{needed} should not come from the OS (missing rpath/$ORIGIN?)"
2262+ ) ) ;
2263+ }
2264+ }
2265+ if !ldd_errors. is_empty ( ) {
2266+ errors. push ( format ! (
2267+ "In `{ldd} {shared_lib}`:\n - {}\n {stdout}" ,
2268+ ldd_errors. join( "\n - " )
2269+ ) ) ;
2270+ }
2271+ }
2272+ }
2273+ }
2274+ }
2275+
21892276 Ok ( errors)
21902277}
21912278
0 commit comments