on master help sign in
[enh] utils/lib.sh - commands pyenv, pyenv.drop pyenv.(un)install

Implement a boilerplate to manage performance optimized virtualenv builds. Shell scripts can use (e.g.) 'pyenv.cmd' to execute command in the virtualenv without having to worry about whether and how the environment is provided. :: pyenv.cmd which python ..../local/py3/bin/python pyenv.cmd which pip ..../local/py3/bin/pip If pyenv.cmd released multiple times the installation will only rebuild if the function 'pyenv.OK' fails. Function 'pyenv.OK' make some test to validate that the virtualenv exists and works as expected. The check also fails if requirements listed requirements-dev.txt and requirements.txt has been edited. Among these tests 'pyenv.OK' calls 'pyenv.check' which implements a python script that validate the python installation. Here is an example how a 'pyenv.check' implementation could look like:: pyenv.check() { cat <<EOF import yaml print('import yaml --> OK') EOF } Signed-off-by: Markus Heiser <markus@darmarit.de>

commitce9312ddeeff59b664b84d43774871cc405cc7ae authorMarkus Heiser <markus@darmarit.de> dateMon 22 Feb 2021, 19:38 (6 years ago) parent6c01145
1 file changed+211−4
utils/lib.sh+211−4
utils/lib.sh+211−4
@@ -86,7 +86,7 @@ set_terminal_colors() {
8686 _Red='\e[0;31m'
8787 _Green='\e[0;32m'
8888 _Yellow='\e[0;33m'
89− _Blue='\e[0;34m'
89+ _Blue='\e[0;94m'
9090 _Violet='\e[0;35m'
9191 _Cyan='\e[0;36m'
9292
@@ -95,12 +95,12 @@ set_terminal_colors() {
9595 _BRed='\e[1;31m'
9696 _BGreen='\e[1;32m'
9797 _BYellow='\e[1;33m'
98− _BBlue='\e[1;34m'
98+ _BBlue='\e[1;94m'
9999 _BPurple='\e[1;35m'
100100 _BCyan='\e[1;36m'
101101}
102102
103−if [ ! -p /dev/stdout ]; then
103+if [ ! -p /dev/stdout ] && [ ! "$TERM" = 'dumb' ] && [ ! "$TERM" = 'unknown' ]; then
104104 set_terminal_colors
105105fi
106106
@@ -152,6 +152,12 @@ err_msg() { echo -e "${_BRed}ERROR:${_creset} $*" >&2; }
152152warn_msg() { echo -e "${_BBlue}WARN:${_creset} $*" >&2; }
153153info_msg() { echo -e "${_BYellow}INFO:${_creset} $*" >&2; }
154154
155+build_msg() {
156+ local tag="$1 "
157+ shift
158+ echo -e "${_Blue}${tag:0:10}${_creset}$*"
159+}
160+
155161clean_stdin() {
156162 if [[ $(uname -s) != 'Darwin' ]]; then
157163 while read -r -n1 -t 0.1; do : ; done
@@ -496,6 +502,203 @@ service_is_available() {
496502 return "$exit_val"
497503}
498504
505+# python
506+# ------
507+
508+PY="${PY:=3}"
509+PYTHON="${PYTHON:=python$PY}"
510+PY_ENV="${PY_ENV:=local/py${PY}}"
511+PY_ENV_BIN="${PY_ENV}/bin"
512+PY_ENV_REQ="${PY_ENV_REQ:=${REPO_ROOT}/requirements*.txt}"
513+
514+# List of python packages (folders) or modules (files) installed by command:
515+# pyenv.install
516+PYOBJECTS="${PYOBJECTS:=.}"
517+
518+# folder where the python distribution takes place
519+PYDIST="${PYDIST:=dist}"
520+
521+# folder where the intermediate build files take place
522+PYBUILD="${PYBUILD:=build/py${PY}}"
523+
524+# https://www.python.org/dev/peps/pep-0508/#extras
525+#PY_SETUP_EXTRAS='[develop,test]'
526+PY_SETUP_EXTRAS="${PY_SETUP_EXTRAS:=[develop,test]}"
527+
528+PIP_BOILERPLATE=( pip wheel setuptools )
529+
530+# shellcheck disable=SC2120
531+pyenv() {
532+
533+ # usage: pyenv [vtenv_opts ...]
534+ #
535+ # vtenv_opts: see 'pip install --help'
536+ #
537+ # Builds virtualenv with 'requirements*.txt' (PY_ENV_REQ) installed. The
538+ # virtualenv will be reused by validating sha256sum of the requirement
539+ # files.
540+
541+ required_commands \
542+ sha256sum "${PYTHON}" \
543+ || exit
544+
545+ local pip_req=()
546+
547+ if ! pyenv.OK > /dev/null; then
548+ rm -f "${PY_ENV}/${PY_ENV_REQ}.sha256"
549+ pyenv.drop > /dev/null
550+ build_msg PYENV "[virtualenv] installing ${PY_ENV_REQ} into ${PY_ENV}"
551+
552+ "${PYTHON}" -m venv "$@" "${PY_ENV}"
553+ "${PY_ENV_BIN}/python" -m pip install -U "${PIP_BOILERPLATE[@]}"
554+
555+ for i in ${PY_ENV_REQ}; do
556+ pip_req=( "${pip_req[@]}" "-r" "$i" )
557+ done
558+
559+ (
560+ [ "$VERBOSE" = "1" ] && set -x
561+ # shellcheck disable=SC2086
562+ "${PY_ENV_BIN}/python" -m pip install "${pip_req[@]}" \
563+ && sha256sum ${PY_ENV_REQ} > "${PY_ENV}/requirements.sha256"
564+ )
565+ fi
566+ pyenv.OK
567+}
568+
569+_pyenv_OK=''
570+pyenv.OK() {
571+
572+ # probes if pyenv exists and runs the script from pyenv.check
573+
574+ [ "$_pyenv_OK" == "OK" ] && return 0
575+
576+ if [ ! -f "${PY_ENV_BIN}/python" ]; then
577+ build_msg PYENV "[virtualenv] missing ${PY_ENV_BIN}/python"
578+ return 1
579+ fi
580+
581+ if [ ! -f "${PY_ENV}/requirements.sha256" ] \
582+ || ! sha256sum --check --status <"${PY_ENV}/requirements.sha256" 2>/dev/null; then
583+ build_msg PYENV "[virtualenv] requirements.sha256 failed"
584+ sed 's/^/ [virtualenv] - /' <"${PY_ENV}/requirements.sha256"
585+ return 1
586+ fi
587+
588+ pyenv.check \
589+ | "${PY_ENV_BIN}/python" 2>&1 \
590+ | prefix_stdout "${_Blue}PYENV ${_creset}[check] "
591+
592+ local err=${PIPESTATUS[1]}
593+ if [ "$err" -ne "0" ]; then
594+ build_msg PYENV "[check] python test failed"
595+ return "$err"
596+ fi
597+
598+ build_msg PYENV "OK"
599+ _pyenv_OK="OK"
600+ return 0
601+}
602+
603+pyenv.drop() {
604+
605+ build_msg PYENV "[virtualenv] drop ${PY_ENV}"
606+ rm -rf "${PY_ENV}"
607+ _pyenv_OK=''
608+
609+}
610+
611+pyenv.check() {
612+
613+ # Prompts a python script with additional checks. Used by pyenv.OK to check
614+ # if virtualenv is ready to install python objects. This function should be
615+ # overwritten by the application script.
616+
617+ local imp=""
618+
619+ for i in "${PIP_BOILERPLATE[@]}"; do
620+ imp="$imp, $i"
621+ done
622+
623+ cat <<EOF
624+import ${imp#,*}
625+
626+EOF
627+}
628+
629+pyenv.install() {
630+
631+ if ! pyenv.OK; then
632+ py.clean > /dev/null
633+ fi
634+ if ! pyenv.install.OK > /dev/null; then
635+ build_msg PYENV "[install] ${PYOBJECTS}"
636+ if ! pyenv.OK >/dev/null; then
637+ pyenv
638+ fi
639+ for i in ${PYOBJECTS}; do
640+ build_msg PYENV "[install] pip install -e '$i${PY_SETUP_EXTRAS}'"
641+ "${PY_ENV_BIN}/python" -m pip install -e "$i${PY_SETUP_EXTRAS}"
642+ done
643+ fi
644+ pyenv.install.OK
645+}
646+
647+_pyenv_install_OK=''
648+pyenv.install.OK() {
649+
650+ [ "$_pyenv_install_OK" == "OK" ] && return 0
651+
652+ local imp=""
653+ local err=""
654+
655+ if [ "." = "${PYOBJECTS}" ]; then
656+ imp="import $(basename "$(pwd)")"
657+ else
658+ # shellcheck disable=SC2086
659+ for i in ${PYOBJECTS}; do imp="$imp, $i"; done
660+ imp="import ${imp#,*} "
661+ fi
662+ (
663+ [ "$VERBOSE" = "1" ] && set -x
664+ "${PY_ENV_BIN}/python" -c "import sys; sys.path.pop(0); $imp;" 2>/dev/null
665+ )
666+
667+ err=$?
668+ if [ "$err" -ne "0" ]; then
669+ build_msg PYENV "[install] python installation test failed"
670+ return "$err"
671+ fi
672+
673+ build_msg PYENV "[install] OK"
674+ _pyenv_install_OK="OK"
675+ return 0
676+}
677+
678+pyenv.uninstall() {
679+
680+ build_msg PYENV "[uninstall] ${PYOBJECTS}"
681+
682+ if [ "." = "${PYOBJECTS}" ]; then
683+ pyenv.cmd python setup.py develop --uninstall 2>&1 \
684+ | prefix_stdout "${_Blue}PYENV ${_creset}[pyenv.uninstall] "
685+ else
686+ pyenv.cmd python -m pip uninstall --yes ${PYOBJECTS} 2>&1 \
687+ | prefix_stdout "${_Blue}PYENV ${_creset}[pyenv.uninstall] "
688+ fi
689+}
690+
691+
692+pyenv.cmd() {
693+ pyenv.install
694+ ( set -e
695+ # shellcheck source=/dev/null
696+ source "${PY_ENV_BIN}/activate"
697+ [ "$VERBOSE" = "1" ] && set -x
698+ "$@"
699+ )
700+}
701+
499702# golang
500703# ------
501704
@@ -1250,7 +1453,7 @@ pkg_install() {
12501453 centos)
12511454 # shellcheck disable=SC2068
12521455 yum install -y $@
1253− ;;
1456+ ;;
12541457 esac
12551458}
12561459
@@ -1382,6 +1585,10 @@ LXC_ENV_FOLDER=
13821585if in_container; then
13831586 # shellcheck disable=SC2034
13841587 LXC_ENV_FOLDER="lxc-env/$(hostname)/"
1588+ PY_ENV="${LXC_ENV_FOLDER}${PY_ENV}"
1589+ PY_ENV_BIN="${LXC_ENV_FOLDER}${PY_ENV_BIN}"
1590+ PYDIST="${LXC_ENV_FOLDER}${PYDIST}"
1591+ PYBUILD="${LXC_ENV_FOLDER}${PYBUILD}"
13851592fi
13861593
13871594lxc_init_container_env() {