Skip to content

Commit 313279e

Browse files
author
Chongkai Zhu
committed
Make the Unix build truly relocatable as CPython patch
1 parent 877d0fb commit 313279e

File tree

3 files changed

+145
-36
lines changed

3 files changed

+145
-36
lines changed

cpython-unix/build-cpython.sh

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ else
175175
patch -p1 -i ${ROOT}/patch-ctypes-callproc-legacy.patch
176176
fi
177177

178+
if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_13}" ]; then
179+
patch -p1 -i ${ROOT}/patch-cpython-relocatable-sysconfig-3.13.patch
180+
elif [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_10}" ]; then
181+
patch -p1 -i ${ROOT}/patch-cpython-relocatable-sysconfig-3.10.patch
182+
fi
183+
178184
# On Windows, CPython looks for the Tcl/Tk libraries relative to the base prefix,
179185
# which we want. But on Unix, it doesn't. This patch applies similar behavior on Unix,
180186
# thereby ensuring that the Tcl/Tk libraries are found in the correct location.
@@ -883,6 +889,7 @@ fi
883889
# that a) it works on as many machines as possible b) doesn't leak details
884890
# about the build environment, which is non-portable.
885891
cat > ${ROOT}/hack_sysconfig.py << EOF
892+
import ast
886893
import json
887894
import os
888895
import sys
@@ -894,7 +901,7 @@ FREETHREADED = sysconfig.get_config_var("Py_GIL_DISABLED")
894901
MAJMIN = ".".join([str(sys.version_info[0]), str(sys.version_info[1])])
895902
LIB_SUFFIX = "t" if FREETHREADED else ""
896903
PYTHON_CONFIG = os.path.join(ROOT, "install", "bin", "python%s-config" % MAJMIN)
897-
PLATFORM_CONFIG = os.path.join(ROOT, sysconfig.get_config_var("LIBPL").lstrip("/"))
904+
PLATFORM_CONFIG = sysconfig.get_config_var("LIBPL")
898905
MAKEFILE = os.path.join(PLATFORM_CONFIG, "Makefile")
899906
SYSCONFIGDATA = os.path.join(
900907
ROOT,
@@ -925,51 +932,56 @@ def replace_in_all(search, replace):
925932
replace_in_file(SYSCONFIGDATA, search, replace)
926933
927934
928-
def replace_in_sysconfigdata(search, replace, keys):
929-
"""Replace a string in the sysconfigdata file for select keys."""
930-
with open(SYSCONFIGDATA, "rb") as fh:
931-
data = fh.read()
935+
def _find_build_time_vars_assign(module):
936+
"""Return the Assign node for 'build_time_vars = {...}' or None."""
937+
for node in module.body:
938+
if isinstance(node, ast.Assign):
939+
for target in node.targets:
940+
if isinstance(target, ast.Name) and target.id == "build_time_vars":
941+
return node
942+
return None
932943
933-
globals_dict = {}
934-
locals_dict = {}
935-
exec(data, globals_dict, locals_dict)
936-
build_time_vars = locals_dict['build_time_vars']
937944
938-
for key in keys:
939-
if key in build_time_vars:
940-
build_time_vars[key] = build_time_vars[key].replace(search, replace)
945+
def _patch_build_time_vars(assign_node, keys, search, replace):
946+
"""Patch plain string values for the given keys inside the dict literal."""
947+
if not isinstance(assign_node.value, ast.Dict):
948+
return
941949
942-
with open(SYSCONFIGDATA, "wb") as fh:
943-
fh.write(b'# system configuration generated and used by the sysconfig module\n')
944-
fh.write(('build_time_vars = %s' % json.dumps(build_time_vars, indent=4, sort_keys=True)).encode("utf-8"))
945-
fh.close()
950+
d: ast.Dict = assign_node.value
951+
for key_node, val_node in zip(d.keys, d.values):
952+
k = key_node.value
953+
if k in keys and isinstance(val_node, ast.Constant) and isinstance(val_node.value, str):
954+
val_node.value = val_node.value.replace(search, replace)
946955
947956
948-
def format_sysconfigdata():
949-
"""Reformat the sysconfigdata file to avoid implicit string concatenations.
957+
def replace_in_sysconfigdata(search, replace, keys):
958+
"""Replace a string in the sysconfigdata file for select keys."""
959+
with open(SYSCONFIGDATA, "r", encoding="utf-8") as fh:
960+
source = fh.read()
950961
951-
In some Python versions, the sysconfigdata file contains implicit string
952-
concatenations that extend over multiple lines, which make string replacement
953-
much harder. This function reformats the file to avoid this issue.
962+
module = ast.parse(source)
963+
assign = _find_build_time_vars_assign(module)
964+
if assign is None:
965+
# Nothing to do if build_time_vars isn't present.
966+
return
954967
955-
See: https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Mac/BuildScript/build-installer.py#L1360C1-L1385C15.
956-
"""
957-
with open(SYSCONFIGDATA, "rb") as fh:
958-
data = fh.read()
968+
# Patch the dict
969+
_patch_build_time_vars(assign, keys, search, replace)
959970
960-
globals_dict = {}
961-
locals_dict = {}
962-
exec(data, globals_dict, locals_dict)
963-
build_time_vars = locals_dict['build_time_vars']
971+
# Compute the textual prefix up to (but not including) the start of build_time_vars.
972+
lines = source.splitlines()
973+
start_line = assign.lineno # 1-based
974+
head_text = "\n".join(lines[: start_line - 1])
964975
965-
with open(SYSCONFIGDATA, "wb") as fh:
966-
fh.write(b'# system configuration generated and used by the sysconfig module\n')
967-
fh.write(('build_time_vars = %s' % json.dumps(build_time_vars, indent=4, sort_keys=True)).encode("utf-8"))
968-
fh.close()
976+
# Unparse the patched assignment
977+
assign_src = ast.unparse(assign)
969978
979+
# Rewrite: preserved head + patched assignment + trailing newline
980+
new_source = head_text + ("\n" if head_text and not head_text.endswith("\n") else "")
981+
new_source += assign_src + "\n"
970982
971-
# Format sysconfig to ensure that string replacements take effect.
972-
format_sysconfigdata()
983+
with open(SYSCONFIGDATA, "w", encoding="utf-8") as fh:
984+
fh.write(new_source)
973985
974986
# Remove `-Werror=unguarded-availability-new` from `CFLAGS` and `CPPFLAGS`.
975987
# These flags are passed along when building extension modules. In that context,
@@ -1063,7 +1075,7 @@ metadata = {
10631075
"python_paths_abstract": sysconfig.get_paths(expand=False),
10641076
"python_exe": "install/bin/python%s%s" % (sysconfig.get_python_version(), sys.abiflags),
10651077
"python_major_minor_version": sysconfig.get_python_version(),
1066-
"python_stdlib_platform_config": sysconfig.get_config_var("LIBPL").lstrip("/"),
1078+
"python_stdlib_platform_config": sysconfig.get_config_var("LIBPL"),
10671079
"python_config_vars": {k: str(v) for k, v in sysconfig.get_config_vars().items()},
10681080
}
10691081
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
diff --git a/Lib/sysconfig.py b/Lib/sysconfig.py
2+
index daf9f000060..ed70b771e56 100644
3+
--- a/Lib/sysconfig.py
4+
+++ b/Lib/sysconfig.py
5+
@@ -1,6 +1,7 @@
6+
"""Access to Python's configuration information."""
7+
8+
import os
9+
+import re
10+
import sys
11+
from os.path import pardir, realpath
12+
13+
@@ -407,9 +408,29 @@ def _get_sysconfigdata_name():
14+
)
15+
16+
17+
+def _print_config_dict(d, stream):
18+
+ # Build a regex that matches the prefix at the start of the
19+
+ # string or following a whitespace character.
20+
+ prefix = d.get("prefix", "/install")
21+
+ install_prefix_pattern = re.compile(r"(^|\s)" + re.escape(prefix))
22+
+ # The replacement string. Use a backreference \1 to keep the
23+
+ # whitespace if it was present.
24+
+ relocatable_path = r"\1{installed_base}"
25+
+
26+
+ print ("{", file=stream)
27+
+ for k, v in sorted(d.items()):
28+
+ if isinstance(v, str):
29+
+ replaced, count = install_prefix_pattern.subn(relocatable_path, v)
30+
+ if count:
31+
+ value_literal = "f" + repr(replaced)
32+
+ print(f" {k!r}: {value_literal},", file=stream)
33+
+ continue
34+
+ print(f" {k!r}: {v!r},", file=stream)
35+
+ print ("}", file=stream)
36+
+
37+
+
38+
def _generate_posix_vars():
39+
"""Generate the Python module containing build-time variables."""
40+
- import pprint
41+
vars = {}
42+
# load the installed Makefile:
43+
makefile = get_makefile_filename()
44+
@@ -463,8 +484,10 @@ def _generate_posix_vars():
45+
with open(destfile, 'w', encoding='utf8') as f:
46+
f.write('# system configuration generated and used by'
47+
' the sysconfig module\n')
48+
+ f.write('from pathlib import Path\n')
49+
+ f.write('installed_base = str(Path(__file__).resolve().parent.parent.parent)\n')
50+
f.write('build_time_vars = ')
51+
- pprint.pprint(vars, stream=f)
52+
+ _print_config_dict(vars, stream=f)
53+
54+
# Create file used for sys.path fixup -- see Modules/getpath.c
55+
with open('pybuilddir.txt', 'w', encoding='utf8') as f:
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
diff --git a/Lib/sysconfig/__main__.py b/Lib/sysconfig/__main__.py
2+
index d7257b9d2d0..1209757b45b 100644
3+
--- a/Lib/sysconfig/__main__.py
4+
+++ b/Lib/sysconfig/__main__.py
5+
@@ -1,4 +1,5 @@
6+
import os
7+
+import re
8+
import sys
9+
from sysconfig import (
10+
_ALWAYS_STR,
11+
@@ -151,8 +152,22 @@ def _parse_makefile(filename, vars=None, keep_unresolved=True):
12+
13+
14+
def _print_config_dict(d, stream):
15+
+ # Build a regex that matches the prefix at the start of the
16+
+ # string or following a whitespace character.
17+
+ prefix = d.get("prefix", "/install")
18+
+ install_prefix_pattern = re.compile(r"(^|\s)" + re.escape(prefix))
19+
+ # The replacement string. Use a backreference \1 to keep the
20+
+ # whitespace if it was present.
21+
+ relocatable_path = r"\1{installed_base}"
22+
+
23+
print ("{", file=stream)
24+
for k, v in sorted(d.items()):
25+
+ if isinstance(v, str):
26+
+ replaced, count = install_prefix_pattern.subn(relocatable_path, v)
27+
+ if count:
28+
+ value_literal = "f" + repr(replaced)
29+
+ print(f" {k!r}: {value_literal},", file=stream)
30+
+ continue
31+
print(f" {k!r}: {v!r},", file=stream)
32+
print ("}", file=stream)
33+
34+
@@ -212,6 +227,8 @@ def _generate_posix_vars():
35+
with open(destfile, 'w', encoding='utf8') as f:
36+
f.write('# system configuration generated and used by'
37+
' the sysconfig module\n')
38+
+ f.write('from pathlib import Path\n')
39+
+ f.write('installed_base = str(Path(__file__).resolve().parent.parent.parent)\n')
40+
f.write('build_time_vars = ')
41+
_print_config_dict(vars, stream=f)
42+

0 commit comments

Comments
 (0)