2012/05/24: Just any old python2... -- depending in the FreeBSD ports collection

In a recent update of the uzbl port I had to specify that a script is to be executed by some 2.x version of python (it would break with python 3). Any old python2 would be good enough. At upstream the assumption was that python2 in the search path would be the preferred 2.x version of python. In the FreeBSD ports collection, this works slightly different.

By stating


USE_PYTHON= 2.4-2.7

I specify the range of python version suitable for this port. I can then use the variable ${PYTHON_VERSION} as the name of a binary in the search path that is guaranteed to be a suitable python version (regardless of other versions already installed, or installed later). E.g., I can have in my post-patch target the lines
post-patch:
    # [...]
    @${REINPLACE_CMD} -e "s|/usr/bin/env python2|/usr/bin/env ${PYTHON_VERSION}|" \
        ${WRKSRC}/bin/uzbl-event-manager

to replace python2 by the name of the correct python version. Needless to say, that the dependency on the correct package is registered. Software installation the way I like it.

The whole magic happens in ${PORTSDIR}/bsd.python.mk which is .include'd if you USE_PYTHON. Looking at it is quite interesting. It is one of those typical ports-Makefiles that do a lot of business logic. I quote a few of the relevant passages. (Copyright The FreeBSD Project, BSD-License.)

It starts by specifying the FreeBSD-wide defaults.


_PYTHON_PORTBRANCH=     2.7
_PYTHON_ALLBRANCHES=    2.7 2.6 2.5 2.4 3.2 3.1 # preferred first

Default python version is 2.7 and if you have special version constraints prefer 2.7 over 2.6 over ... over 3.2 over 3.1.

However, the actual default version is either what is explicity specified (in the port or /etc/make.conf) or, in absence of that, what is currently installed as ${LOCALBASE}/bin/python. Only if neither gives a clue, we use the FreeBSD-wide default.


# Determine version number of Python to use
.if !defined(PYTHON_DEFAULT_VERSION)
. if exists(${LOCALBASE}/bin/python)
_PYTHON_DEFAULT_VERSION!=   (${LOCALBASE}/bin/python -c \
                            'import sys; print sys.version[:3]' 2> /dev/null \
                            || ${ECHO_CMD} ${_PYTHON_PORTBRANCH}) | ${TAIL} -1
. else
_PYTHON_DEFAULT_VERSION=    ${_PYTHON_PORTBRANCH}
. endif
PYTHON_DEFAULT_VERSION=     python${_PYTHON_DEFAULT_VERSION}
.endif

.if defined(PYTHON_VERSION)
_PYTHON_VERSION:=   ${PYTHON_VERSION:S/^python//}
_PYTHON_CMD=        ${LOCALBASE}/bin/${PYTHON_VERSION}
.else
_PYTHON_VERSION:=   ${PYTHON_DEFAULT_VERSION:S/^python//}
_PYTHON_CMD=        ${LOCALBASE}/bin/${PYTHON_DEFAULT_VERSION}
.endif


This version is compared against the constraints specified, if any. A simple YES does not match the shape of a range specification.


# Validate Python version whether it meets USE_PYTHON version restriction.
_PYTHON_VERSION_CHECK:=         ${USE_PYTHON:C/^([1-9]\.[0-9])$/\1-\1/}
_PYTHON_VERSION_MINIMUM_TMP:=   ${_PYTHON_VERSION_CHECK:C/([1-9]\.[0-9])[-+].*/\1/}
_PYTHON_VERSION_MINIMUM:=       ${_PYTHON_VERSION_MINIMUM_TMP:M[1-9].[0-9]}
_PYTHON_VERSION_MAXIMUM_TMP:=   ${_PYTHON_VERSION_CHECK:C/.*-([1-9]\.[0-9])/\1/}
_PYTHON_VERSION_MAXIMUM:=       ${_PYTHON_VERSION_MAXIMUM_TMP:M[1-9].[0-9]}

.if !empty(_PYTHON_VERSION_MINIMUM) && ( \
        ${_PYTHON_VERSION} < ${_PYTHON_VERSION_MINIMUM})
_PYTHON_VERSION_NONSUPPORTED=   ${_PYTHON_VERSION_MINIMUM} at least
.elif !empty(_PYTHON_VERSION_MAXIMUM) && ( \
        ${_PYTHON_VERSION} > ${_PYTHON_VERSION_MAXIMUM})
_PYTHON_VERSION_NONSUPPORTED=   ${_PYTHON_VERSION_MAXIMUM} at most
.endif


If the constrains are not fulfilled, look through the supported python versions, in order of preference for the first version matching. This is the one to take.
# If we have an unsupported version of Python, try another.
.if defined(_PYTHON_VERSION_NONSUPPORTED)
.if defined(PYTHON_VERSION) || defined(PYTHON_CMD)
IGNORE=             needs Python ${_PYTHON_VERSION_NONSUPPORTED}.\
                    But you specified ${_PYTHON_VERSION}
.else
.undef _PYTHON_VERSION
.for ver in ${_PYTHON_ALLBRANCHES}
__VER=      ${ver}
.if !defined(_PYTHON_VERSION) && \
    !(!empty(_PYTHON_VERSION_MINIMUM) && ( \
        ${__VER} < ${_PYTHON_VERSION_MINIMUM})) && \
    !(!empty(_PYTHON_VERSION_MAXIMUM) && ( \
        ${__VER} > ${_PYTHON_VERSION_MAXIMUM}))
_PYTHON_VERSION=    ${ver}
_PYTHON_CMD=        ${LOCALBASE}/bin/python${ver}
.endif
.endfor
.if !defined(_PYTHON_VERSION)
IGNORE=             needs an unsupported version of Python
_PYTHON_VERSION=    ${_PYTHON_PORTBRANCH} # just to avoid version sanity checking.
.endif
.endif  # defined(PYTHON_VERSION) || defined(PYTHON_CMD)
.endif  # defined(_PYTHON_VERSION_NONSUPPORTED)

PYTHON_VERSION?=    python${_PYTHON_VERSION}
PYTHON_CMD?=        ${_PYTHON_CMD}