gh-159819: Fix python-config.sh for installation paths containing spaces (#159820)

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
This commit is contained in:
Itamar Oren authored and GitHub committed 2026-10-21 21:29:37 -07:00
1 parent d4750a7751
commit 3e13407763
3 files changed
+115 -23

No files matched your search

+70 -1
View File
@@ -1,3 +1,4 @@
import itertools
import platform
import re
import unittest
@@ -21,7 +22,8 @@ from test.support import (
)
from test.support.import_helper import import_module
from test.support.os_helper import (TESTFN, unlink, skip_unless_symlink,
change_cwd, EnvironmentVarGuard)
change_cwd, temp_dir,
EnvironmentVarGuard)
from test.support.venv import VirtualEnvironmentMixin
import sysconfig
@@ -767,6 +769,73 @@ class TestSysConfig(unittest.TestCase, VirtualEnvironmentMixin):
self.assertEqual(config_vars['exec_prefix'], sys.exec_prefix)
self.assertEqual(config_vars['platbase'], sys.exec_prefix)
@unittest.skipUnless(os.name == 'posix', 'python-config.sh is POSIX only')
@unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds")
@unittest.skipIf(is_android or is_apple_mobile,
'Android and iOS do not ship python-config')
@requires_subprocess()
def test_python_config_sh_prefix_special_characters(self):
# gh-159819: the shell variant of python-config must report the
# directory it was installed to even if that path contains spaces or
# characters special to the shell, sed or echo.
if is_python_build():
script = os.path.join(_PROJECT_BASE, 'python-config')
else:
ldversion = get_config_var('LDVERSION')
script = os.path.join(os.path.dirname(sys.executable),
f'python{ldversion}-config')
if not os.path.isfile(script):
self.skipTest(f'{script!r} does not exist')
with open(script, encoding='utf-8') as f:
if f.readline().rstrip() != '#!/bin/sh':
self.skipTest('python-config is not the shell script variant')
with open(script, encoding='utf-8') as f:
script_text = f.read()
makefile_vars = _parse_makefile(sysconfig.get_makefile_filename())
configured_prefix = makefile_vars['prefix']
def check(script_copy, option, expected):
proc = subprocess.run(['/bin/sh', script_copy, option],
capture_output=True, text=True)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(proc.stdout.rstrip('\n'), expected)
self.assertEqual(proc.stderr, '')
# '#', '&' and '\' were special to the sed replacement, and '\c' makes
# some echo implementations stop printing.
names = ['path with spaces', 'path#with&amp', r'back\slash\cdir']
# The configured prefix is also matched literally. Simulate a build
# configured with special characters in its prefix by substituting
# them into a copy of the script.
fake_prefixes = [None]
if configured_prefix != '/':
fake_prefixes.append('/conf#with&special\\chars')
for name, fake_prefix in itertools.product(names, fake_prefixes):
with (self.subTest(name=name, configured_prefix=fake_prefix),
temp_dir() as tmpdir):
prefix = os.path.realpath(os.path.join(tmpdir, name))
bindir = os.path.join(prefix, 'bin')
os.mkdir(prefix)
os.mkdir(bindir)
script_copy = os.path.join(bindir, 'python-config')
text = script_text
if fake_prefix is not None:
text = text.replace(configured_prefix, fake_prefix)
with open(script_copy, 'w', encoding='utf-8') as f:
f.write(text)
check(script_copy, '--prefix', prefix)
check(script_copy, '--exec-prefix', prefix)
# The script rewrites the configured include directory by
# replacing the configured prefix with the actual one.
includedir = makefile_vars['INCLUDEDIR'].replace(
configured_prefix, prefix, 1)
incdir = os.path.join(includedir, 'python{VERSION}{ABIFLAGS}'
.format_map(makefile_vars))
check(script_copy, '--includes', f'-I{incdir} -I{incdir}')
def test_parse_config_h(self):
config = textwrap.dedent('''
#ifndef Py_PYCONFIG_H
@@ -0,0 +1,6 @@
Fix the shell version of :program:`python3-config` (used on all POSIX
platforms except macOS) when the installation prefix contains spaces or
other characters special to the shell or :program:`sed`: it printed an
empty ``--prefix`` and wrong ``--includes``/``--ldflags`` paths while
exiting successfully. The script now also exits with status 1 if it cannot
determine its installation prefix.
+39 -22
View File
@@ -7,9 +7,9 @@ exit_with_usage ()
local usage
usage="Usage: $0 --prefix|--exec-prefix|--includes|--libs|--cflags|--ldflags|--extension-suffix|--help|--abiflags|--configdir|--embed"
if [ "$1" -eq 0 ]; then
echo "$usage"
printf '%s\n' "$usage"
else
echo "$usage" >&2
printf '%s\n' "$usage" >&2
fi
exit $1
}
@@ -19,29 +19,46 @@ if [ "$1" = "" ] ; then
fi
# Returns the actual prefix where this script was installed to.
# Fails if the directory containing this script cannot be resolved.
installed_prefix ()
{
RESULT=$(dirname $(cd $(dirname "$1") && pwd -P))
RESULT=$(cd "$(dirname "$1")" && pwd -P) || return 1
RESULT=$(dirname "$RESULT")
if which readlink >/dev/null 2>&1 ; then
if readlink -f "$RESULT" >/dev/null 2>&1; then
RESULT=$(readlink -f "$RESULT")
fi
fi
echo $RESULT
printf '%s\n' "$RESULT"
}
prefix_real=$(installed_prefix "$0")
prefix_real=$(installed_prefix "$0") || {
printf '%s\n' "$0: cannot determine the installation prefix" >&2
exit 1
}
# Use sed to fix paths from their built-to locations to their installed-to
# locations. Keep prefix & exec_prefix using their original values in case
# Fix paths from their built-to locations to their installed-to locations
# by replacing the first occurrence of the configured prefix with the actual
# one. Keep prefix & exec_prefix using their original values in case
# they are referenced in other configure variables, to prevent double
# substitution, issue #22140.
prefix="@prefix@"
exec_prefix="@exec_prefix@"
exec_prefix_real=${prefix_real}
includedir=$(echo "@includedir@" | sed "s#$prefix#$prefix_real#")
libdir=$(echo "@libdir@" | sed "s#$prefix#$prefix_real#")
CFLAGS=$(echo "@CFLAGS@" | sed "s#$prefix#$prefix_real#")
relocate ()
{
case "$1" in
*"$prefix"*)
printf '%s\n' "${1%%"$prefix"*}${prefix_real}${1#*"$prefix"}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
includedir=$(relocate "@includedir@")
libdir=$(relocate "@libdir@")
CFLAGS=$(relocate "@CFLAGS@")
VERSION="@VERSION@"
LIBM="@LIBM@"
LIBC="@LIBC@"
@@ -55,7 +72,7 @@ OPT="@OPT@"
PY_ENABLE_SHARED="@PY_ENABLE_SHARED@"
LDVERSION="@LDVERSION@"
LIBDEST=${prefix_real}/lib/python${VERSION}
LIBPL=$(echo "@LIBPL@" | sed "s#$prefix#$prefix_real#")
LIBPL=$(relocate "@LIBPL@")
SO="@EXT_SUFFIX@"
PYTHONFRAMEWORK="@PYTHONFRAMEWORK@"
INCDIR="-I$includedir/python${VERSION}${ABIFLAGS}"
@@ -63,9 +80,9 @@ PLATINCDIR="-I$includedir/python${VERSION}${ABIFLAGS}"
PY_EMBED=0
# Scan for --help or unknown argument.
for ARG in $*
for ARG in "$@"
do
case $ARG in
case "$ARG" in
--help)
exit_with_usage 0
;;
@@ -88,35 +105,35 @@ for ARG in "$@"
do
case "$ARG" in
--prefix)
echo "$prefix_real"
printf '%s\n' "$prefix_real"
;;
--exec-prefix)
echo "$exec_prefix_real"
printf '%s\n' "$exec_prefix_real"
;;
--includes)
echo "$INCDIR $PLATINCDIR"
printf '%s\n' "$INCDIR $PLATINCDIR"
;;
--cflags)
echo "$INCDIR $PLATINCDIR $BASECFLAGS $CFLAGS $OPT"
printf '%s\n' "$INCDIR $PLATINCDIR $BASECFLAGS $CFLAGS $OPT"
;;
--libs)
echo "$LIBS"
printf '%s\n' "$LIBS"
;;
--ldflags)
LIBPLUSED=
if [ "$PY_ENABLE_SHARED" = "0" ] ; then
LIBPLUSED="-L$LIBPL"
fi
echo "$LIBPLUSED -L$libdir $LIBS"
printf '%s\n' "$LIBPLUSED -L$libdir $LIBS"
;;
--extension-suffix)
echo "$SO"
printf '%s\n' "$SO"
;;
--abiflags)
echo "$ABIFLAGS"
printf '%s\n' "$ABIFLAGS"
;;
--configdir)
echo "$LIBPL"
printf '%s\n' "$LIBPL"
;;
esac
done