python-utils-r1.eclass: Make python_fix_shebang force full path

Change the behavior of python_fix_shebang to always output full path
to the Python interpreter (i.e. ${PYTHON}) instead of mangling
the original path.  Most importantly, this ensures that:

1. EPREFIX is included in the final path

2. /usr/bin/env is replaced by the absolute path to avoid issues
   when calling system executables from inside a venv

Note that this implies that a few unlikely corner cases may stop
working, notably:

a. "weird" shebangs such as "/usr/bin/foo python" will no longer work

b. the mangled scripts will escape temporary venv e.g. created
   in distutils-r1 PEP517 mode (python_fix_shebang is not used in such
   a way in ::gentoo)

Signed-off-by: Michał Górny <mgorny@gentoo.org>
This commit is contained in:
Michał Górny
2022-03-31 23:09:30 +02:00
parent 6c3fdd7708
commit 85820b0b54
2 changed files with 100 additions and 108 deletions

View File

@@ -1001,25 +1001,30 @@ _python_wrapper_setup() {
# @FUNCTION: python_fix_shebang
# @USAGE: [-f|--force] [-q|--quiet] <path>...
# @DESCRIPTION:
# Replace the shebang in Python scripts with the current Python
# implementation (EPYTHON). If a directory is passed, works recursively
# on all Python scripts.
# Replace the shebang in Python scripts with the full path
# to the current Python implementation (PYTHON, including EPREFIX).
# If a directory is passed, works recursively on all Python scripts
# found inside the directory tree.
#
# Only files having a 'python*' shebang will be modified. Files with
# other shebang will either be skipped when working recursively
# on a directory or treated as error when specified explicitly.
# Only files having a Python shebang (a path to any known Python
# interpreter, optionally preceded by env(1) invocation) will
# be processed. Files with any other shebang will either be skipped
# silently when a directory was passed, or an error will be reported
# for any files without Python shebangs specified explicitly.
#
# Shebangs matching explicitly current Python version will be left
# unmodified. Shebangs requesting another Python version will be treated
# as fatal error, unless --force is given.
# Shebangs that are compatible with the current Python version will be
# mangled unconditionally. Incompatible shebangs will cause a fatal
# error, unless --force is specified.
#
# --force causes the function to replace even shebangs that require
# incompatible Python version. --quiet causes the function not to list
# modified files verbosely.
# --force causes the function to replace shebangs with incompatible
# Python version (but not non-Python shebangs). --quiet causes
# the function not to list modified files verbosely.
python_fix_shebang() {
debug-print-function ${FUNCNAME} "${@}"
[[ ${EPYTHON} ]] || die "${FUNCNAME}: EPYTHON unset (pkg_setup not called?)"
local PYTHON
_python_export "${EPYTHON}" PYTHON
local force quiet
while [[ ${@} ]]; do
@@ -1035,13 +1040,13 @@ python_fix_shebang() {
local path f
for path; do
local any_correct any_fixed is_recursive
local any_fixed is_recursive
[[ -d ${path} ]] && is_recursive=1
while IFS= read -r -d '' f; do
local shebang i
local error= from=
local error= match=
# note: we can't ||die here since read will fail if file
# has no newline characters
@@ -1050,64 +1055,36 @@ python_fix_shebang() {
# First, check if it's shebang at all...
if [[ ${shebang} == '#!'* ]]; then
local split_shebang=()
read -r -a split_shebang <<<${shebang} || die
read -r -a split_shebang <<<${shebang#"#!"} || die
# Match left-to-right in a loop, to avoid matching random
# repetitions like 'python2.7 python2'.
for i in "${split_shebang[@]}"; do
case "${i}" in
*"${EPYTHON}")
debug-print "${FUNCNAME}: in file ${f#${D%/}}"
debug-print "${FUNCNAME}: shebang matches EPYTHON: ${shebang}"
local in_path=${split_shebang[0]}
local from='^#! *[^ ]*'
# if the first component is env(1), skip it
if [[ ${in_path} == */env ]]; then
in_path=${split_shebang[1]}
from+=' *[^ ]*'
fi
# Nothing to do, move along.
any_correct=1
from=${EPYTHON}
break
;;
*python|*python[23])
debug-print "${FUNCNAME}: in file ${f#${D%/}}"
debug-print "${FUNCNAME}: rewriting shebang: ${shebang}"
if [[ ${i} == *python2 ]]; then
from=python2
if [[ ! ${force} ]]; then
error=1
fi
elif [[ ${i} == *python3 ]]; then
from=python3
else
from=python
fi
break
;;
*python[23].[0-9]|*python3.[1-9][0-9]|*pypy|*pypy3|*jython[23].[0-9])
# Explicit mismatch.
if [[ ! ${force} ]]; then
error=1
else
case "${i}" in
*python[23].[0-9])
from="python[23].[0-9]";;
*python3.[1-9][0-9])
from="python3.[1-9][0-9]";;
*pypy)
from="pypy";;
*pypy3)
from="pypy3";;
*jython[23].[0-9])
from="jython[23].[0-9]";;
*)
die "${FUNCNAME}: internal error in 2nd pattern match";;
esac
fi
break
;;
esac
done
case ${in_path##*/} in
"${EPYTHON}")
match=1
;;
python|python[23])
match=1
[[ ${in_path##*/} == python2 ]] && error=1
;;
python[23].[0-9]|python3.[1-9][0-9]|pypy|pypy3|jython[23].[0-9])
# Explicit mismatch.
match=1
error=1
;;
esac
fi
if [[ ! ${error} && ! ${from} ]]; then
# disregard mismatches in force mode
[[ ${force} ]] && error=
if [[ ! ${match} ]]; then
# Non-Python shebang. Allowed in recursive mode,
# disallowed when specifying file explicitly.
[[ ${is_recursive} ]] && continue
@@ -1119,13 +1096,9 @@ python_fix_shebang() {
fi
if [[ ! ${error} ]]; then
# We either want to match ${from} followed by space
# or at end-of-string.
if [[ ${shebang} == *${from}" "* ]]; then
sed -i -e "1s:${from} :${EPYTHON} :" "${f}" || die
else
sed -i -e "1s:${from}$:${EPYTHON}:" "${f}" || die
fi
debug-print "${FUNCNAME}: in file ${f#${D%/}}"
debug-print "${FUNCNAME}: rewriting shebang: ${shebang}"
sed -i -e "1s@${from}@#!${PYTHON}@" "${f}" || die
any_fixed=1
else
eerror "The file has incompatible shebang:"
@@ -1138,12 +1111,7 @@ python_fix_shebang() {
if [[ ! ${any_fixed} ]]; then
eerror "QA error: ${FUNCNAME}, ${path#${D%/}} did not match any fixable files."
if [[ ${any_correct} ]]; then
eerror "All files have ${EPYTHON} shebang already."
else
eerror "There are no Python files in specified directory."
fi
eerror "There are no Python files in specified directory."
die "${FUNCNAME} did not match any fixable files"
fi
done

View File

@@ -41,7 +41,7 @@ test_fix_shebang() {
local expect=${3}
local args=( "${@:4}" )
tbegin "python_fix_shebang${args[@]+ ${args[*]}} from ${from} to ${to} (exp: ${expect})"
tbegin "python_fix_shebang${args[@]+ ${args[*]}} from ${from@Q} to ${to@Q} (exp: ${expect@Q})"
echo "${from}" > "${tmpfile}"
output=$( EPYTHON=${to} python_fix_shebang "${args[@]}" -q "${tmpfile}" 2>&1 )
@@ -156,36 +156,60 @@ fi
test_var PYTHON_PKG_DEP pypy3 '*dev-python/pypy3*:0='
test_var PYTHON_SCRIPTDIR pypy3 /usr/lib/python-exec/pypy3
# generic shebangs
test_fix_shebang '#!/usr/bin/python' python3.6 '#!/usr/bin/python3.6'
test_fix_shebang '#!/usr/bin/python' pypy3 '#!/usr/bin/pypy3'
for EPREFIX in '' /foo; do
einfo "with EPREFIX=${EPREFIX@Q}"
eindent
# generic shebangs
test_fix_shebang '#!/usr/bin/python' python3.6 \
"#!${EPREFIX}/usr/bin/python3.6"
test_fix_shebang '#!/usr/bin/python' pypy3 \
"#!${EPREFIX}/usr/bin/pypy3"
# python2/python3 matching
test_fix_shebang '#!/usr/bin/python3' python3.6 '#!/usr/bin/python3.6'
test_fix_shebang '#!/usr/bin/python2' python3.6 FAIL
test_fix_shebang '#!/usr/bin/python2' python3.6 '#!/usr/bin/python3.6' --force
# python2/python3 matching
test_fix_shebang '#!/usr/bin/python3' python3.6 \
"#!${EPREFIX}/usr/bin/python3.6"
test_fix_shebang '#!/usr/bin/python2' python3.6 FAIL
test_fix_shebang '#!/usr/bin/python2' python3.6 \
"#!${EPREFIX}/usr/bin/python3.6" --force
# pythonX.Y matching (those mostly test the patterns)
test_fix_shebang '#!/usr/bin/python2.7' python3.2 FAIL
test_fix_shebang '#!/usr/bin/python2.7' python3.2 '#!/usr/bin/python3.2' --force
test_fix_shebang '#!/usr/bin/python3.2' python3.2 '#!/usr/bin/python3.2'
# pythonX.Y matching (those mostly test the patterns)
test_fix_shebang '#!/usr/bin/python2.7' python3.2 FAIL
test_fix_shebang '#!/usr/bin/python2.7' python3.2 \
"#!${EPREFIX}/usr/bin/python3.2" --force
test_fix_shebang '#!/usr/bin/python3.2' python3.2 \
"#!${EPREFIX}/usr/bin/python3.2"
# fancy path handling
test_fix_shebang '#!/mnt/python2/usr/bin/python' python3.6 \
'#!/mnt/python2/usr/bin/python3.6'
test_fix_shebang '#!/mnt/python2/usr/bin/python3' python3.8 \
'#!/mnt/python2/usr/bin/python3.8'
test_fix_shebang '#!/mnt/python2/usr/bin/env python' python3.8 \
'#!/mnt/python2/usr/bin/env python3.8'
test_fix_shebang '#!/mnt/python2/usr/bin/python3 python3' python3.8 \
'#!/mnt/python2/usr/bin/python3.8 python3'
test_fix_shebang '#!/mnt/python2/usr/bin/python2 python3' python3.8 FAIL
test_fix_shebang '#!/mnt/python2/usr/bin/python2 python3' python3.8 \
'#!/mnt/python2/usr/bin/python3.8 python3' --force
test_fix_shebang '#!/usr/bin/foo' python3.8 FAIL
# fancy path handling
test_fix_shebang '#!/mnt/python2/usr/bin/python' python3.6 \
"#!${EPREFIX}/usr/bin/python3.6"
test_fix_shebang '#!/mnt/python2/usr/bin/python3' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8"
test_fix_shebang '#!/mnt/python2/usr/bin/env python' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8"
test_fix_shebang '#!/mnt/python2/usr/bin/python3 python3' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8 python3"
test_fix_shebang '#!/mnt/python2/usr/bin/python2 python3' python3.8 FAIL
test_fix_shebang '#!/mnt/python2/usr/bin/python2 python3' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8 python3" --force
test_fix_shebang '#!/usr/bin/foo' python3.8 FAIL
# regression test for bug #522080
test_fix_shebang '#!/usr/bin/python ' python3.8 '#!/usr/bin/python3.8 '
# regression test for bug #522080
test_fix_shebang '#!/usr/bin/python ' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8 "
# test random whitespace in shebang
test_fix_shebang '#! /usr/bin/python' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8"
test_fix_shebang '#! /usr/bin/python' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8"
test_fix_shebang '#! /usr/bin/env python' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8"
# test preserving options
test_fix_shebang '#! /usr/bin/python -b' python3.8 \
"#!${EPREFIX}/usr/bin/python3.8 -b"
eoutdent
done
# check _python_impl_matches behavior
test_is "_python_impl_matches python3_6 -3" 0