mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
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:
1 parent
d4750a7751
commit
3e13407763
3 files changed
+115
-23
No files matched your search
@@ -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&', 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
@@ -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
|
||||
Reference in new issue
Block a user