Skip to content

Commit 20eacbc

Browse files
committed
Give _tkinter $ORIGIN-relative dependencies on glibc and an rpath on musl
Partially addresses #742 and makes it consistent with what we're doing on macOS. There's an argument in the comments above that we should not set an rpath on libpython (except on musl where it's needed), and I need to see if I still believe that. In the meantime I'm following that pattern and setting $ORIGIN-relative NEEDED on glibc and rpath on musl only. This also adds a specific ldd regression test but not the additional tests listed in #742.
1 parent 51d355f commit 20eacbc

File tree

2 files changed

+125
-19
lines changed

2 files changed

+125
-19
lines changed

cpython-unix/build-cpython.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,25 @@ if [ "${PYBUILD_SHARED}" = "1" ]; then
821821
${ROOT}/out/python/install/lib/libpython3.so
822822
fi
823823
fi
824+
825+
# PyInstaller would like to see `ldd` work on modules.
826+
# https://github.com/pyinstaller/pyinstaller/issues/9204#issuecomment-3171583553
827+
# Also this probably helps programs linking libpython avoid having to set an rpath.
828+
patchelf_args=()
829+
if [ "${CC}" == "musl-clang" ]; then
830+
patchelf_args+=(--set-rpath '${ORIGIN}/../..')
831+
else
832+
for lib in ${ROOT}/out/python/install/lib/*; do
833+
basename=${lib##*/}
834+
patchelf_args+=(--replace-needed "$basename" '${ORIGIN}/../../'"$basename")
835+
done
836+
fi
837+
# At the moment, python3 and libpython don't have shared-library
838+
# dependencies, but at some point we will want to run this for
839+
# them too.
840+
for module in ${ROOT}/out/python/install/lib/python*/lib-dynload/*.so; do
841+
patchelf "${patchelf_args[@]}" "$module"
842+
done
824843
fi
825844
fi
826845

src/validation.rs

Lines changed: 106 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

287295
static 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

Comments
 (0)