Marcel Telka
2022-10-02 0536849c3ea4c2c3f8248cba04cebacd909b2c3d
Add python-integrate-project - tool for automatic integration, update, and rebuild of python projects

2 files added
4 files modified
476 ■■■■■ changed files
make-rules/ips.mk 4 ●●● patch | view | raw | blame | history
make-rules/setup.py-defaults.mk 29 ●●●●● patch | view | raw | blame | history
make-rules/setup.py.mk 16 ●●●●● patch | view | raw | blame | history
make-rules/shared-macros.mk 10 ●●●●● patch | view | raw | blame | history
tools/python-integrate-project 408 ●●●●● patch | view | raw | blame | history
transforms/generate-cleanup 9 ●●●●● patch | view | raw | blame | history
make-rules/ips.mk
@@ -226,7 +226,6 @@
PYV_MANIFESTS = $(foreach v,$(PYV_VALUES),$(shell echo $(PY_MANIFESTS) | sed -e 's/-PYVER.p5m/-$(v).p5m/g'))
PYNV_MANIFESTS = $(shell echo $(PY_MANIFESTS) | sed -e 's/-PYVER//')
MKGENERIC_SCRIPTS += $(BUILD_DIR)/mkgeneric-python
GENERATE_GENERIC_TRANSFORMS+=$(foreach v,$(PYTHON_VERSIONS), -e 's/$(subst .,\.,$(v))/\$$\(PYVER\)/g')
else
NOPY_MANIFESTS = $(UNVERSIONED_MANIFESTS)
endif
@@ -315,8 +314,7 @@
    $(PKGSEND) generate $(PKG_HARDLINKS:%=--target %) $(PROTO_DIR) | \
    $(PKGMOGRIFY) $(PKG_OPTIONS) /dev/fd/0 $(GENERATE_TRANSFORMS) | \
        sed -e '/^$$/d' -e '/^#.*$$/d' \
        -e '/\.la$$/d' -e '/\.pyo$$/d' -e '/usr\/lib\/python[23]\..*\.pyc$$/d' \
        -e '/usr\/lib\/python3\..*\/__pycache__\/.*/d'  | \
        -e '/\.la$$/d' | \
        $(PKGFMT) | \
        uniq | \
        cat $(METADATA_TEMPLATE) - $(GENERATE_EXTRA_CMD) | \
make-rules/setup.py-defaults.mk
New file
@@ -0,0 +1,29 @@
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source.  A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#
#
# Copyright 2022 Marcel Telka
#
# Component defaults
COMPONENT_CLASSIFICATION ?=    Development/Python
COMPONENT_SRC ?=        $(COMPONENT_NAME)-$(COMPONENT_VERSION)
COMPONENT_ARCHIVE ?=        $(COMPONENT_SRC).tar.gz
COMPONENT_FMRI ?=        library/python/$(COMPONENT_NAME)
COMPONENT_PROJECT_URL ?=    https://pypi.org/project/$(COMPONENT_NAME)/
COMPONENT_ARCHIVE_URL ?=    $(call pypi_url)
# Enable ASLR by default.  Component could disable ASLR by setting
# COMPONENT_ASLR to 'no'.
ifeq ($(strip $(COMPONENT_ASLR)),no)
ASLR_MODE = $(ASLR_DISABLE)
else
ASLR_MODE = $(ASLR_ENABLE)
endif
make-rules/setup.py.mk
@@ -112,6 +112,16 @@
    $(COMPONENT_POST_INSTALL_ACTION)
    $(TOUCH) $@
# Rename binaries in /usr/bin to contain version number
COMPONENT_POST_INSTALL_ACTION += \
    for f in $(PROTOUSRBINDIR)/* ; do \
        [[ -f $$f ]] || continue ; \
        for v in $(PYTHON_VERSIONS) ; do \
            [[ "$$f" == "$${f%%-$$v}" ]] || continue 2 ; \
        done ; \
        $(MV) $$f $$f-$(PYTHON_VERSION) ; \
    done ;
# Define bit specific and Python version specific filenames.
ifeq ($(strip $(USE_COMMON_TEST_MASTER)),no)
COMPONENT_TEST_MASTER =    $(COMPONENT_TEST_RESULTS_DIR)/results-$(PYTHON_VERSION)-$(BITS).master
@@ -164,6 +174,12 @@
    $(COMPONENT_TEST_CLEANUP)
    $(TOUCH) $@
# We need to add -$(PYV) to package fmri
GENERATE_EXTRA_CMD += | \
    $(GSED) -e 's/^\(set name=pkg.fmri [^@]*\)\(.*\)$$/\1-$$(PYV)\2/' \
clean::
    $(RM) -r $(SOURCE_DIR) $(BUILD_DIR)
make-rules/shared-macros.mk
@@ -172,6 +172,16 @@
# 64-bit only.
PYTHON_64_ONLY_VERSIONS = $(PYTHON_VERSIONS)
# List of python versions we are currently obsoleting.  We no longer build any
# packages for these python versions, but there still might be hanging some not
# obsoleted yet versioned packages built for PYTHON_VERSIONS_OBSOLETING python
# versions.  Or there is just the versioned runtime/python package still
# available.
#
# This list should be usually empty.  Intersection of
# PYTHON_VERSIONS_OBSOLETING and PYTHON_VERSIONS lists MUST be always empty.
PYTHON_VERSIONS_OBSOLETING = 2.7 3.5
# PYTHON3_SOABI variable defines the naming scheme
# of python3 extension libraries: cpython or abi3.
# Currently, most of the components use cpython naming scheme by default,
tools/python-integrate-project
New file
@@ -0,0 +1,408 @@
#! /usr/bin/ksh
#
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source.  A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#
#
# Copyright 2022 Marcel Telka
#
THIS="python-integrate-project"
CONF="$THIS.conf"
SNIPPET="$THIS.snippet"
APIURL=https://pypi.org/pypi
CURL="/usr/bin/curl -s"
function usage
{
    [[ -n "$1" ]] && printf "ERROR: %s\n\n" "$1" >&2
    printf "Usage: %s [-l VERSION] [-o OBSOLETE].. [-u] PROJECT\n" "$THIS" >&2
    [[ -n "$1" ]] && exit 1
    exit 0
}
VERSION=
OBSOLETE=
UPGRADE_ONLY=0
while getopts ":hl:o:u" OPT ; do
    case "$OPT" in
    "?"|"h")    usage ;;
    "l")        VERSION="$OPTARG" ;;
    "o")        OBSOLETE="$OBSOLETE $OPTARG" ;;
    "u")        UPGRADE_ONLY=1 ;;
    esac
done
shift $((OPTIND - 1))
(($# == 0)) && usage
(($# > 1)) && usage "Too many arguments"
PROJECT="$1"
WS_TOP=$(git rev-parse --show-toplevel 2>/dev/null)
[[ -z "$WS_TOP" ]] && usage "The script must be run in git repo"
DIR="$WS_TOP/components/python"
[[ -d "$DIR" ]] || usage "Directory $DIR not found"
# Get data from pypi
PYPI_PROJECT=$($CURL "$APIURL/$PROJECT/json")
if (($? != 0)) || [[ -z "$PYPI_PROJECT" ]] ; then
    printf "FATAL: Failed to get data from pypi\n" >&2
    exit 1
fi
# Get project homepage
HOMEPAGE=$(printf "%s" "$PYPI_PROJECT" | /usr/bin/jq -r '.info.home_page')
if (($? != 0)) || [[ -z "$HOMEPAGE" || "$HOMEPAGE" == "null" ]] ; then
    printf "WARNING: Failed to get homepage for project %s from pypi\n" "$PROJECT" >&2
    HOMEPAGE="TODO"
fi
# Find the latest version if not provided by user
if [[ -z "$VERSION" ]] ; then
    VERSION=$(printf "%s" "$PYPI_PROJECT" | /usr/bin/jq -r '.info.version')
    if (($? != 0)) || [[ -z "$VERSION" || "$VERSION" == "null" ]] ; then
        printf "FATAL: Failed to get version for project %s from pypi\n" "$PROJECT" >&2
        exit 1
    fi
fi
# Get release data from pypi
PYPI_PROJECT_RELEASE=$($CURL "$APIURL/$PROJECT/$VERSION/json")
if (($? != 0)) || [[ -z "$PYPI_PROJECT_RELEASE" ]] ; then
    printf "FATAL: Failed to get data for version %s from pypi\n" "$VERSION" >&2
    exit 1
fi
# Get download url
DOWNLOAD_URL=$(printf "%s" "$PYPI_PROJECT_RELEASE" | /usr/bin/jq -r '.urls[]|select(.packagetype=="sdist")|.url')
if (($? != 0)) || [[ -z "$DOWNLOAD_URL" || "$DOWNLOAD_URL" == "null" ]] ; then
    printf "WARNING: Failed to get download url for project %s, version %s from pypi\n" "$PROJECT" "$VERSION" >&2
    DOWNLOAD_URL="TODO"
fi
# Prepare the directory
DIR="$DIR/$PROJECT"
mkdir -p "$DIR"
cd "$DIR"
git restore --staged . > /dev/null 2>&1
git checkout . > /dev/null 2>&1
# Is this new project, or just a rebuild?
NEW=1
REBUILD=0
PREV_VER=
PREV_HVER=
PREV_REV=0
if git ls-files --error-unmatch Makefile > /dev/null 2>&1 ; then
    NEW=0
    REBUILD=1
    PREV_VER=$(gmake print-value-COMPONENT_VERSION 2>/dev/null)
    PREV_REV=$(gmake print-value-COMPONENT_REVISION 2>/dev/null)
    # If we were asked to do version upgrade, but we do not have new
    # version, then we are done.
    PREV_HVER=$(gmake print-value-HUMAN_VERSION 2>/dev/null)
    ((UPGRADE_ONLY)) && [[ "$PREV_HVER" == "$VERSION" ]] && exit 0
    gmake clobber > /dev/null 2>&1
fi
# Remove everything from git (except known patches, history, and $CONF)
touch "$CONF"
grep "^%patch%" "$CONF" | while read TAG PATCH ; do rm -f "patches/$PATCH" ; done
rm -f history "$CONF"
find . -type f | while read f ; do git rm "$f" > /dev/null 2>&1 ; done
rm -rf "$DIR" 2>/dev/null
git checkout history > /dev/null 2>&1
git checkout "$CONF" > /dev/null 2>&1
touch "$CONF"
grep "^%patch%" "$CONF" | while read TAG PATCH ; do
    git checkout "patches/$PATCH" > /dev/null 2>&1
    [[ -f "patches/$PATCH" ]] || printf "WARNING: Patch %s not found\n" "$PATCH" >&2
done
# Makefile template
(
cat $WS_TOP/transforms/copyright-template | sed -e '/^$/,$d'
cat <<EOF
#
# This file was automatically generated using the following command:
#   \$WS_TOOLS/$THIS $PROJECT
#
BUILD_STYLE = setup.py
EOF
gsed -e '0,/^%include-1%/d' -e '/^%/,$d' < "$CONF"
cat <<EOF
include ../../../make-rules/shared-macros.mk
COMPONENT_NAME =        $PROJECT
COMPONENT_VERSION =        $VERSION
COMPONENT_REVISION =        $((PREV_REV + 1))
COMPONENT_SUMMARY =        $PROJECT - TODO
COMPONENT_PROJECT_URL =        $HOMEPAGE
COMPONENT_ARCHIVE_URL =        \\
    $DOWNLOAD_URL
COMPONENT_ARCHIVE_HASH =    \\
    sha256:TODO
COMPONENT_LICENSE =        license:TODO
COMPONENT_LICENSE_FILE =    licfile:TODO
TEST_TARGET = \$(NO_TESTS)
EOF
cat "$CONF" | gsed -e '0,/^%include-2%/d' -e '/^%/,$d' | gsed -e '1s/^./\n&/'
printf "\ninclude \$(WS_MAKE_RULES)/common.mk\n"
cat "$CONF" | gsed -e '0,/^%include-3%/d' -e '/^%/,$d' | gsed -e '1s/^./\n&/'
printf "\n"
) > Makefile
# Remove COMPONENT_REVISION if not needed
COMPONENT_VERSION=$(gmake print-value-COMPONENT_VERSION)
[[ "$PREV_VER" != "$COMPONENT_VERSION" ]] && REBUILD=0 && sed -i -e '/^COMPONENT_REVISION/d' Makefile
# Calculate sham256 sum for source package
gmake fetch > /dev/null 2>&1
USERLAND_ARCHIVES=$(gmake print-value-USERLAND_ARCHIVES)
COMPONENT_ARCHIVE=$(gmake print-value-COMPONENT_ARCHIVE)
SHA256=$(digest -a sha256 "$USERLAND_ARCHIVES/$COMPONENT_ARCHIVE")
sed -i -e 's/sha256:TODO/sha256:'"$SHA256"'/g' Makefile
git add Makefile
# Unpack sources
! gmake prep > /dev/null 2>&1 && printf "FATAL: 'gmake prep' failed!\n" >&2 && exit 1
SOURCE_DIR=$(gmake print-value-SOURCE_DIR)
# Get summary
SUMMARY=$(printf "%s" "$PYPI_PROJECT" | /usr/bin/jq -r '.info.summary')
if (($? != 0)) || [[ -z "$SUMMARY" || "$SUMMARY" == "null" ]] ; then
    printf "WARNING: Failed to get summary for project %s from pypi\n" "$PROJECT" >&2
    SUMMARY="TODO"
fi
# Summary needs to be sanitized
SUMMARY="${SUMMARY//\`/\\\\\`}"
SUMMARY="${SUMMARY//\"/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"}"
SUMMARY="${SUMMARY//\//\/}"
SUMMARY="${SUMMARY//\$/\\\\\$\$}"
sed -i -e 's/\(COMPONENT_SUMMARY.*\)TODO$/\1'"$SUMMARY"'/g' Makefile
# Try to detect license type(s)
function detect_license
{
    typeset -n L="$1"
    typeset F="$2"
    typeset D
    D=$("$WS_TOP/tools/license-detector" "$F")
    [[ -n "$L" ]] && L="$L OR " ; L="$L$D"
}
LICENSE=
LICFILE=
for f in LICENSE LICENSE.txt LICENSE.rst ; do
    [[ -f "$SOURCE_DIR/$f" ]] || continue
    LICFILE="$f"
    detect_license LICENSE "$SOURCE_DIR/$LICFILE"
    [[ -n "$LICENSE" ]] && break
    printf "WARNING: Failed to detect license type in %s file\n" "$f" >&2
done
if [[ -z "$LICFILE" ]] ; then
    printf "WARNING: No license file found\n" >&2
else
    sed -i -e 's|licfile:TODO|'"$LICFILE"'|g' Makefile
fi
if [[ -z "$LICENSE" ]] ; then
    # Execute hook-no-license snippet
    gsed -e '0,/^%hook-no-license%/d' -e '/^%/,$d' < "$CONF" > "$SNIPPET"
    . "./$SNIPPET"
    rm -f "$SNIPPET"
    if [[ -f "$PROJECT.license" ]] ; then
        sed -i -e '/^COMPONENT_LICENSE_FILE/d' Makefile
        git add "$PROJECT.license"
        [[ -z "$LICENSE" ]] && detect_license LICENSE "$PROJECT.license"
    fi
    [[ -z "$LICENSE" ]] && LICENSE="TODO"
fi
# Store the detected license into the Makefile
sed -i -e 's/license:TODO/'"$LICENSE"'/g' Makefile
# Create manifests
if ! gmake sample-manifest > /dev/null 2>&1 ; then
    printf "ERROR: 'gmake sample-manifest' failed!\n" >&2
else
    cat manifests/sample-manifest.p5m \
        | sed -e 's/^#.*Copyright.*<contributor>.*$/# This file was automatically generated using '$THIS'/g' \
        > "$PROJECT-PYVER.p5m"
    # Execute hook-manifest snippet
    gsed -e '0,/^%hook-manifest%/d' -e '/^%/,$d' < "$CONF" > "$SNIPPET"
    . "./$SNIPPET"
    rm -f "$SNIPPET"
    git add manifests/sample-manifest.p5m "$PROJECT-PYVER.p5m"
fi
# $CONF is no longer needed
rm -f "$CONF"
git checkout "$CONF" > /dev/null 2>&1
# Detect support for tests
TESTS=0
if [[ -f "$SOURCE_DIR/runtests.py" ]] ; then
    TESTS=1
    sed -i -e '/^TEST_TARGET/,+1d' Makefile
fi
# Generate REQUIRED_PACKAGES
gmake REQUIRED_PACKAGES > /dev/null 2>&1 || printf "ERROR: 'gmake REQUIRED_PACKAGES' failed!\n" >&2
git add Makefile
# Check for Makefile completeness
grep -q "TODO" Makefile && printf "ERROR: Makefile is not complete (TODO found)\n" >&2
# Make sure the build environment is setup properly and we do have all
# requirements installed.  Otherwise we cannot continue.
! gmake env-check > /dev/null 2>&1 && printf "FATAL: 'gmake env-check' failed!\n" >&2 && exit 1
PYTHON_VERSIONS=$(gmake print-value-PYTHON_VERSIONS)
if ((TESTS == 0)) ; then
    printf "WARNING: Testing not supported\n"
else
    # Run tests to make sure they pass
    for v in $PYTHON_VERSIONS ; do
        gmake PYTHON_VERSIONS=$v test > /dev/null 2>&1 || printf "ERROR: Testing failed for %s!\n" "$v" >&2
    done
    # Run tests again and create test results master file(s)
    COMPONENT_TEST_MASTER_PREV=
    for v in $PYTHON_VERSIONS ; do
        COMPONENT_TEST_MASTER=$(gmake PYTHON_VERSION=$v print-value-COMPONENT_TEST_MASTER)
        COMPONENT_TEST_SNAPSHOT=$(gmake PYTHON_VERSION=$v print-value-COMPONENT_TEST_SNAPSHOT)
        mkdir -p $(dirname "$COMPONENT_TEST_MASTER")
        touch "$COMPONENT_TEST_MASTER"
        gmake PYTHON_VERSIONS=$v test > /dev/null 2>&1
        TEST_RET=$?
        # If there is no snapshot produced the component likely does not support tests
        if [[ ! -f "$COMPONENT_TEST_SNAPSHOT" ]] ; then
            printf "WARNING: test unsupported for %s\n" "$v" >&2
            rm -f "$COMPONENT_TEST_MASTER"
            rmdir $(dirname "$COMPONENT_TEST_MASTER")
            continue
        fi
        if [[ "$COMPONENT_TEST_MASTER_PREV" != "$COMPONENT_TEST_MASTER" ]] ; then
            [[ -s "$COMPONENT_TEST_SNAPSHOT" ]] || printf "WARNING: 'gmake test' produced empty results for %s\n" "$v" >&2
            cp -p "$COMPONENT_TEST_SNAPSHOT" "$COMPONENT_TEST_MASTER"
            git add "$COMPONENT_TEST_MASTER"
            COMPONENT_TEST_MASTER_PREV="$COMPONENT_TEST_MASTER"
            gmake PYTHON_VERSIONS=$v test > /dev/null 2>&1
            TEST_RET=$?
        fi
        ((TEST_RET != 0)) && printf "ERROR: 'gmake test' results differ for %s!\n" "$v" >&2
    done
fi
# Publish packages and create pkg5 file
if ! gmake publish > /dev/null 2>&1 ; then
    printf "ERROR: 'gmake publish' failed!\n" >&2
else
    git add pkg5
fi
# Handle history
COMPONENT_FMRI=$(gmake print-value-COMPONENT_FMRI)
PYTHON_VERSIONS_OBSOLETING=$(gmake print-value-PYTHON_VERSIONS_OBSOLETING)
OV=
OV_PLURAL=
for o in $(echo $OBSOLETE $PYTHON_VERSIONS_OBSOLETING | LC_ALL=C sort -u) ; do
    PYV=${o//.}
    FMRI=$(pkg list -avH "$COMPONENT_FMRI-$PYV" 2>/dev/null | egrep -v '(o|r)$' | sed -e 's|^.*\('"$COMPONENT_FMRI"'\)|\1|g' -e 's/:[^:]*$//g' -e 's/\(-[^-]*\)$/,5.11\1/g')
    [[ -n "$FMRI" ]] || continue
    FMRI_H=${FMRI%.*}
    FMRI_T=${FMRI##*.}
    if [[ "$FMRI_H" == "$FMRI" ]] ; then
        printf "WARNING: Wrong fmri format: %s\n" "$FMRI" >&2
        continue
    fi
    FMRI_T=$((FMRI_T + 1))
    printf "%s.%s noincorporate\n" "$FMRI_H" "$FMRI_T" >> history
    [[ -n "$OV" ]] && OV="$OV and " && OV_PLURAL="s"
    OV="$OV$o"
done
if [[ -f history ]] ; then
    LC_ALL=C sort -u history > history.new
    mv history.new history
    git add history
fi
# Construct the commit message
MSG=
if ((NEW)) ; then
    MSG="Add $PROJECT python project"
else
    if ((REBUILD == 0)) ; then
        [[ "$PREV_HVER" != "$VERSION" ]] && MSG="update to $VERSION" || MSG="change version format"
    fi
    NV=
    for v in $PYTHON_VERSIONS ; do
        PYV=${v//.}
        pkg list -avH "$COMPONENT_FMRI-$PYV" 2>/dev/null | egrep -q -v '(o|r)$' && continue
        [[ -n "$NV" ]] && NV="$NV and "
        NV="$NV$v"
    done
    REBUILDMSG=
    [[ -n "$NV" ]] && REBUILDMSG="rebuild for python $NV"
    if [[ -n "$OV" ]] ; then
        [[ -n "$REBUILDMSG" ]] && REBUILDMSG="$REBUILDMSG and" || REBUILDMSG="rebuild"
        REBUILDMSG="$REBUILDMSG to get package$OV_PLURAL for python $OV obsoleted"
    fi
    if [[ -n "$REBUILDMSG" ]] ; then
        [[ -n "$MSG" ]] && MSG="$MSG; "
        MSG="$MSG$REBUILDMSG"
    fi
    [[ -z "$MSG" ]] && MSG="rebuild"
    MSG="python/$PROJECT: $MSG"
fi
# Commit the results
! git commit -m "$MSG" > /dev/null 2>&1 && printf "FATAL: 'git commit' failed!\n" >&2 && exit 1
transforms/generate-cleanup
@@ -67,6 +67,15 @@
<transform dir file link hardlink \
    path=usr/perl5/.*/\.packlist$ -> drop>
# python support
<transform dir file link hardlink path=usr\/lib\/python3\.\d+\/.*\/__pycache__\/ -> drop>
<transform dir file link hardlink -> \
    edit path "^(usr/bin/[^/]+-)3\.\d+$" "\1$!(PYVER)">
<transform dir file link hardlink -> \
    edit path "^(usr/lib/python3\.\d+/vendor-packages/[^/]+-py)3\.\d+(\.egg-info/)" "\1$!(PYVER)\2">
<transform dir file link hardlink -> \
    edit path "^(usr/lib/python)3\.\d+(/vendor-packages/)" "\1$!(PYVER)\2">
<transform dir file link hardlink -> \
    edit target "/(sparcv9|amd64)$" "/$!(MACH64)">
<transform dir file link hardlink -> \