Skip to content

Commit a4aadcb

Browse files
authored
[3.14] gh-159819: Fix python-config.sh for installation paths containing spaces (GH-159820) (#160240)
The shell `python-config` located its own prefix with unquoted command substitutions, so any whitespace in the installation path made it print an empty `--prefix` and `-I/include/...`-style paths while exiting 0. - Quote the substitutions and the argument loop (POSIX `sh`, no bashisms). - Exit 1 with a message when the script's directory cannot be resolved, instead of silently printing empty paths. - Replace `sed "s#$prefix#$prefix_real#"` with a `relocate` helper using parameter expansion, so `#`, `&`, `\` and regex metacharacters in either prefix are handled literally. Same first-occurrence semantics. - Add `test_python_config_sh_prefix_with_spaces`: runs the generated script from `…/path with spaces/bin/` and checks `--prefix`, `--exec-prefix`, `--includes` and empty stderr (cherry picked from commit 3e13407)
1 parent be9b776 commit a4aadcb

3 files changed

Lines changed: 114 additions & 23 deletions

File tree

‎Lib/test/test_sysconfig.py‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import itertools
12
import platform
23
import re
34
import unittest
@@ -20,7 +21,7 @@
2021
)
2122
from test.support.import_helper import import_module
2223
from test.support.os_helper import (TESTFN, unlink, skip_unless_symlink,
23-
change_cwd)
24+
change_cwd, temp_dir)
2425
from test.support.venv import VirtualEnvironmentMixin
2526

2627
import sysconfig
@@ -749,6 +750,73 @@ def test_sysconfig_config_vars_no_prefix_cache(self):
749750
self.assertEqual(config_vars['exec_prefix'], sys.exec_prefix)
750751
self.assertEqual(config_vars['platbase'], sys.exec_prefix)
751752

753+
@unittest.skipUnless(os.name == 'posix', 'python-config.sh is POSIX only')
754+
@unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds")
755+
@unittest.skipIf(is_android or is_apple_mobile,
756+
'Android and iOS do not ship python-config')
757+
@requires_subprocess()
758+
def test_python_config_sh_prefix_special_characters(self):
759+
# gh-159819: the shell variant of python-config must report the
760+
# directory it was installed to even if that path contains spaces or
761+
# characters special to the shell, sed or echo.
762+
if is_python_build():
763+
script = os.path.join(_PROJECT_BASE, 'python-config')
764+
else:
765+
ldversion = get_config_var('LDVERSION')
766+
script = os.path.join(os.path.dirname(sys.executable),
767+
f'python{ldversion}-config')
768+
if not os.path.isfile(script):
769+
self.skipTest(f'{script!r} does not exist')
770+
with open(script, encoding='utf-8') as f:
771+
if f.readline().rstrip() != '#!/bin/sh':
772+
self.skipTest('python-config is not the shell script variant')
773+
774+
with open(script, encoding='utf-8') as f:
775+
script_text = f.read()
776+
makefile_vars = _parse_makefile(sysconfig.get_makefile_filename())
777+
configured_prefix = makefile_vars['prefix']
778+
779+
def check(script_copy, option, expected):
780+
proc = subprocess.run(['/bin/sh', script_copy, option],
781+
capture_output=True, text=True)
782+
self.assertEqual(proc.returncode, 0, proc.stderr)
783+
self.assertEqual(proc.stdout.rstrip('\n'), expected)
784+
self.assertEqual(proc.stderr, '')
785+
786+
# '#', '&' and '\' were special to the sed replacement, and '\c' makes
787+
# some echo implementations stop printing.
788+
names = ['path with spaces', 'path#with&amp', r'back\slash\cdir']
789+
# The configured prefix is also matched literally. Simulate a build
790+
# configured with special characters in its prefix by substituting
791+
# them into a copy of the script.
792+
fake_prefixes = [None]
793+
if configured_prefix != '/':
794+
fake_prefixes.append('/conf#with&special\\chars')
795+
for name, fake_prefix in itertools.product(names, fake_prefixes):
796+
with (self.subTest(name=name, configured_prefix=fake_prefix),
797+
temp_dir() as tmpdir):
798+
prefix = os.path.realpath(os.path.join(tmpdir, name))
799+
bindir = os.path.join(prefix, 'bin')
800+
os.mkdir(prefix)
801+
os.mkdir(bindir)
802+
script_copy = os.path.join(bindir, 'python-config')
803+
text = script_text
804+
if fake_prefix is not None:
805+
text = text.replace(configured_prefix, fake_prefix)
806+
with open(script_copy, 'w', encoding='utf-8') as f:
807+
f.write(text)
808+
809+
check(script_copy, '--prefix', prefix)
810+
check(script_copy, '--exec-prefix', prefix)
811+
812+
# The script rewrites the configured include directory by
813+
# replacing the configured prefix with the actual one.
814+
includedir = makefile_vars['INCLUDEDIR'].replace(
815+
configured_prefix, prefix, 1)
816+
incdir = os.path.join(includedir, 'python{VERSION}{ABIFLAGS}'
817+
.format_map(makefile_vars))
818+
check(script_copy, '--includes', f'-I{incdir} -I{incdir}')
819+
752820
def test_parse_config_h(self):
753821
config = textwrap.dedent('''
754822
#ifndef Py_PYCONFIG_H
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix the shell version of :program:`python3-config` (used on all POSIX
2+
platforms except macOS) when the installation prefix contains spaces or
3+
other characters special to the shell or :program:`sed`: it printed an
4+
empty ``--prefix`` and wrong ``--includes``/``--ldflags`` paths while
5+
exiting successfully. The script now also exits with status 1 if it cannot
6+
determine its installation prefix.

‎Misc/python-config.sh.in‎

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ exit_with_usage ()
77
local usage
88
usage="Usage: $0 --prefix|--exec-prefix|--includes|--libs|--cflags|--ldflags|--extension-suffix|--help|--abiflags|--configdir|--embed"
99
if [ "$1" -eq 0 ]; then
10-
echo "$usage"
10+
printf '%s\n' "$usage"
1111
else
12-
echo "$usage" >&2
12+
printf '%s\n' "$usage" >&2
1313
fi
1414
exit $1
1515
}
@@ -19,29 +19,46 @@ if [ "$1" = "" ] ; then
1919
fi
2020

2121
# Returns the actual prefix where this script was installed to.
22+
# Fails if the directory containing this script cannot be resolved.
2223
installed_prefix ()
2324
{
24-
RESULT=$(dirname $(cd $(dirname "$1") && pwd -P))
25+
RESULT=$(cd "$(dirname "$1")" && pwd -P) || return 1
26+
RESULT=$(dirname "$RESULT")
2527
if which readlink >/dev/null 2>&1 ; then
2628
if readlink -f "$RESULT" >/dev/null 2>&1; then
2729
RESULT=$(readlink -f "$RESULT")
2830
fi
2931
fi
30-
echo $RESULT
32+
printf '%s\n' "$RESULT"
3133
}
3234

33-
prefix_real=$(installed_prefix "$0")
35+
prefix_real=$(installed_prefix "$0") || {
36+
printf '%s\n' "$0: cannot determine the installation prefix" >&2
37+
exit 1
38+
}
3439

35-
# Use sed to fix paths from their built-to locations to their installed-to
36-
# locations. Keep prefix & exec_prefix using their original values in case
40+
# Fix paths from their built-to locations to their installed-to locations
41+
# by replacing the first occurrence of the configured prefix with the actual
42+
# one. Keep prefix & exec_prefix using their original values in case
3743
# they are referenced in other configure variables, to prevent double
3844
# substitution, issue #22140.
3945
prefix="@prefix@"
4046
exec_prefix="@exec_prefix@"
4147
exec_prefix_real=${prefix_real}
42-
includedir=$(echo "@includedir@" | sed "s#$prefix#$prefix_real#")
43-
libdir=$(echo "@libdir@" | sed "s#$prefix#$prefix_real#")
44-
CFLAGS=$(echo "@CFLAGS@" | sed "s#$prefix#$prefix_real#")
48+
relocate ()
49+
{
50+
case "$1" in
51+
*"$prefix"*)
52+
printf '%s\n' "${1%%"$prefix"*}${prefix_real}${1#*"$prefix"}"
53+
;;
54+
*)
55+
printf '%s\n' "$1"
56+
;;
57+
esac
58+
}
59+
includedir=$(relocate "@includedir@")
60+
libdir=$(relocate "@libdir@")
61+
CFLAGS=$(relocate "@CFLAGS@")
4562
VERSION="@VERSION@"
4663
LIBM="@LIBM@"
4764
LIBC="@LIBC@"
@@ -55,17 +72,17 @@ OPT="@OPT@"
5572
PY_ENABLE_SHARED="@PY_ENABLE_SHARED@"
5673
LDVERSION="@LDVERSION@"
5774
LIBDEST=${prefix_real}/lib/python${VERSION}
58-
LIBPL=$(echo "@LIBPL@" | sed "s#$prefix#$prefix_real#")
75+
LIBPL=$(relocate "@LIBPL@")
5976
SO="@EXT_SUFFIX@"
6077
PYTHONFRAMEWORK="@PYTHONFRAMEWORK@"
6178
INCDIR="-I$includedir/python${VERSION}${ABIFLAGS}"
6279
PLATINCDIR="-I$includedir/python${VERSION}${ABIFLAGS}"
6380
PY_EMBED=0
6481

6582
# Scan for --help or unknown argument.
66-
for ARG in $*
83+
for ARG in "$@"
6784
do
68-
case $ARG in
85+
case "$ARG" in
6986
--help)
7087
exit_with_usage 0
7188
;;
@@ -88,35 +105,35 @@ for ARG in "$@"
88105
do
89106
case "$ARG" in
90107
--prefix)
91-
echo "$prefix_real"
108+
printf '%s\n' "$prefix_real"
92109
;;
93110
--exec-prefix)
94-
echo "$exec_prefix_real"
111+
printf '%s\n' "$exec_prefix_real"
95112
;;
96113
--includes)
97-
echo "$INCDIR $PLATINCDIR"
114+
printf '%s\n' "$INCDIR $PLATINCDIR"
98115
;;
99116
--cflags)
100-
echo "$INCDIR $PLATINCDIR $BASECFLAGS $CFLAGS $OPT"
117+
printf '%s\n' "$INCDIR $PLATINCDIR $BASECFLAGS $CFLAGS $OPT"
101118
;;
102119
--libs)
103-
echo "$LIBS"
120+
printf '%s\n' "$LIBS"
104121
;;
105122
--ldflags)
106123
LIBPLUSED=
107124
if [ "$PY_ENABLE_SHARED" = "0" ] ; then
108125
LIBPLUSED="-L$LIBPL"
109126
fi
110-
echo "$LIBPLUSED -L$libdir $LIBS"
127+
printf '%s\n' "$LIBPLUSED -L$libdir $LIBS"
111128
;;
112129
--extension-suffix)
113-
echo "$SO"
130+
printf '%s\n' "$SO"
114131
;;
115132
--abiflags)
116-
echo "$ABIFLAGS"
133+
printf '%s\n' "$ABIFLAGS"
117134
;;
118135
--configdir)
119-
echo "$LIBPL"
136+
printf '%s\n' "$LIBPL"
120137
;;
121138
esac
122139
done

0 commit comments

Comments
 (0)