Skip to content

Commit d80ca35

Browse files
committed
unix: build _crypt extension module as shared
Modern Linux distributions are starting to remove `libcrypt.so.1` from the base disto. See #173 and #113 before it for more context. The `_crypt` extension module depends on `libcrypt.so.1` and our static linking of this extension is causing the full Python distribution to depend on `libcrypt.so.1`, causing our binaries to not load/run on these distributions. This commit adds support for building extension modules as shared libraries (the way CPython does things by default). We update our YAML config to build `_crypt` as a shared library.
1 parent 31e1732 commit d80ca35

8 files changed

+96
-64
lines changed

cpython-unix/build-cpython.sh

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,10 @@ fi
174174
# invoke the host Python on our own.
175175
patch -p1 -i ${ROOT}/patch-write-python-for-build.patch
176176

177-
# We build all extensions statically. So remove the auto-generated make
178-
# rules that produce shared libraries for them.
179-
if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_11}" ]; then
180-
patch -p1 -i ${ROOT}/patch-remove-extension-module-shared-libraries.patch
181-
else
182-
patch -p1 -i ${ROOT}/patch-remove-extension-module-shared-libraries-legacy.patch
177+
# Object files can get listed multiple times leading to duplicate symbols
178+
# when linking. Prevent this.
179+
if [ -n "${PYTHON_MEETS_MAXIMUM_VERSION_3_10}" ]; then
180+
patch -p1 -i ${ROOT}/patch-makesetup-deduplicate-objs.patch
183181
fi
184182

185183
# The default build rule for the macOS dylib doesn't pick up libraries

cpython-unix/build.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,11 @@ def python_build_info(
582582
"variant": d["variant"],
583583
}
584584

585+
if info.get("build-mode") == "shared":
586+
shared_dir = extra_metadata["python_config_vars"]["DESTSHARED"].strip("/")
587+
extension_suffix = extra_metadata["python_config_vars"]["EXT_SUFFIX"]
588+
entry["shared_lib"] = "%s/%s%s" % (shared_dir, extension, extension_suffix)
589+
585590
add_licenses_to_extension_entry(entry)
586591

587592
bi["extensions"].setdefault(extension, []).append(entry)

cpython-unix/extension-modules.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ _contextvars:
6767
- _contextvarsmodule.c
6868

6969
_crypt:
70+
build-mode: shared
7071
sources:
7172
- _cryptmodule.c
7273
links-conditional:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
diff --git a/Modules/makesetup b/Modules/makesetup
2+
index 1a767838c9..9f6d2f4396 100755
3+
--- a/Modules/makesetup
4+
+++ b/Modules/makesetup
5+
@@ -253,6 +253,9 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' |
6+
done
7+
done
8+
9+
+ # Deduplicate OBJS.
10+
+ OBJS=$(echo $OBJS | tr ' ' '\n' | sort -u | xargs)
11+
+
12+
case $SHAREDMODS in
13+
'') ;;
14+
*) DEFS="SHAREDMODS=$SHAREDMODS$NL$DEFS";;

cpython-unix/patch-remove-extension-module-shared-libraries-legacy.patch

Lines changed: 0 additions & 25 deletions
This file was deleted.

cpython-unix/patch-remove-extension-module-shared-libraries.patch

Lines changed: 0 additions & 24 deletions
This file was deleted.

pythonbuild/cpython.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
EXTENSION_MODULE_SCHEMA = {
1515
"type": "object",
1616
"properties": {
17+
"build-mode": {"type": "string"},
1718
"config-c-only": {"type": "boolean"},
1819
"defines": {"type": "array", "items": {"type": "string"}},
1920
"defines-conditional": {
@@ -228,6 +229,9 @@ def derive_setup_local(
228229
python_version, info.get("maximum-python-version", "100.0")
229230
)
230231

232+
if info.get("build-mode") not in (None, "shared", "static"):
233+
raise Exception("unsupported build-mode for extension module %s" % name)
234+
231235
if not (python_min_match and python_max_match):
232236
log(f"ignoring extension module {name} because Python version incompatible")
233237
ignored.add(name)
@@ -387,6 +391,7 @@ def derive_setup_local(
387391

388392
section_lines = {
389393
"disabled": [],
394+
"shared": [],
390395
"static": [],
391396
}
392397

@@ -427,7 +432,13 @@ def derive_setup_local(
427432
enabled_extensions[name]["setup_line"] = name.encode("ascii")
428433
continue
429434

430-
section = "static"
435+
# musl is static only. Ignore build-mode override.
436+
if "musl" in target_triple:
437+
section = "static"
438+
else:
439+
section = info.get("build-mode", "static")
440+
441+
enabled_extensions[name]["build-mode"] = section
431442

432443
# Presumably this means the extension comes from the distribution's
433444
# Setup. Lack of sources means we don't need to derive a Setup.local
@@ -549,6 +560,9 @@ def derive_setup_local(
549560
dest_lines = []
550561

551562
for section, lines in sorted(section_lines.items()):
563+
if not lines:
564+
continue
565+
552566
dest_lines.append(b"\n*%s*\n" % section.encode("ascii"))
553567
dest_lines.extend(lines)
554568

src/validation.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,9 @@ const GLOBAL_EXTENSIONS_WINDOWS: &[&str] = &[
707707
/// Extension modules not present in Windows static builds.
708708
const GLOBAL_EXTENSIONS_WINDOWS_NO_STATIC: &[&str] = &["_testinternalcapi", "_tkinter"];
709709

710+
/// Extension modules that should be built as shared libraries.
711+
const SHARED_LIBRARY_EXTENSIONS: &[&str] = &["_crypt"];
712+
710713
const PYTHON_VERIFICATIONS: &str = include_str!("verify_distribution.py");
711714

712715
fn allowed_dylibs_for_triple(triple: &str) -> Vec<MachOAllowedDylib> {
@@ -1742,24 +1745,70 @@ fn validate_distribution(
17421745
}
17431746
}
17441747

1745-
// Validate extension module initialization functions are present.
1746-
//
1747-
// Note that we export PyInit_* functions from libpython on POSIX whereas these
1748-
// aren't exported from official Python builds. We may want to consider changing
1749-
// this.
1748+
// Validate extension module metadata.
17501749
for (name, variants) in json.as_ref().unwrap().build_info.extensions.iter() {
17511750
for ext in variants {
1751+
if let Some(shared) = &ext.shared_lib {
1752+
if !seen_paths.contains(&PathBuf::from("python").join(shared)) {
1753+
context.errors.push(format!(
1754+
"extension module {} references missing shared library path {}",
1755+
name, shared
1756+
));
1757+
}
1758+
}
1759+
1760+
// Static builds never have shared library extension modules.
1761+
let want_shared = if is_static {
1762+
false
1763+
// Extension modules in libpython core are never shared libraries.
1764+
} else if ext.in_core {
1765+
false
1766+
// All remaining extensions are shared on Windows.
1767+
} else if triple.contains("windows") {
1768+
true
1769+
// On POSIX platforms we maintain a list.
1770+
} else {
1771+
SHARED_LIBRARY_EXTENSIONS.contains(&name.as_str())
1772+
};
1773+
1774+
if want_shared && ext.shared_lib.is_none() {
1775+
context.errors.push(format!(
1776+
"extension module {} does not have a shared library",
1777+
name
1778+
));
1779+
} else if !want_shared && ext.shared_lib.is_some() {
1780+
context.errors.push(format!(
1781+
"extension module {} contains a shared library unexpectedly",
1782+
name
1783+
));
1784+
}
1785+
1786+
// Ensure initialization functions are exported.
1787+
1788+
// Note that we export PyInit_* functions from libpython on POSIX whereas these
1789+
// aren't exported from official Python builds. We may want to consider changing
1790+
// this.
17521791
if ext.init_fn == "NULL" {
17531792
continue;
17541793
}
17551794

17561795
let exported = context.libpython_exported_symbols.contains(&ext.init_fn);
17571796

17581797
// Static distributions never export symbols.
1798+
let wanted = if is_static {
1799+
false
1800+
// For some strange reason _PyWarnings_Init is exported as part of the ABI.
1801+
} else if name == "_warnings" {
1802+
true
17591803
// Windows dynamic doesn't export extension module init functions.
1760-
// And for some strange reason _PyWarnings_Init is exported as part of the ABI.
1761-
let wanted =
1762-
!(is_static || triple.contains("-windows-")) || (!is_static && name == "_warnings");
1804+
} else if triple.contains("-windows-") {
1805+
false
1806+
// Presence of a shared library extension implies no export.
1807+
} else if ext.shared_lib.is_some() {
1808+
false
1809+
} else {
1810+
true
1811+
};
17631812

17641813
if exported != wanted {
17651814
context.errors.push(format!(

0 commit comments

Comments
 (0)