├── .gitignore ├── .travis.yml ├── .travis ├── Dockerfile ├── after-success.sh ├── install.sh └── script.sh ├── LICENSE ├── README.md ├── appimage ├── build-plugin.sh ├── build-python.sh ├── recipes │ ├── python2.7.17.sh │ ├── python3.7.6.sh │ ├── python3.8.2.sh │ ├── scipy.sh │ └── xonsh.sh ├── resources │ ├── apprun.sh │ ├── plugin.desktop │ ├── python.desktop │ └── python.png └── update-version.sh ├── linuxdeploy-plugin-python.sh ├── share ├── python-wrapper.sh └── sitecustomize.py └── tests ├── __init__.py ├── __main__.py └── test_plugin.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | appimage/*.AppImage 5 | share/excludelist 6 | wiki 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - docker 3 | 4 | cache: docker 5 | 6 | branches: 7 | only: 8 | - master 9 | except: 10 | - # Do not build tags that we create when we upload to GitHub Releases 11 | - /^(?i:continuous)$/ 12 | 13 | matrix: 14 | include: 15 | - env: ARCH=x86_64 16 | 17 | install: 18 | - # Setup the build env 19 | - docker build -t ${DOCKER_USERNAME}/linuxdeploy-plugin-python:${ARCH} --build-arg Arch=${ARCH} .travis 20 | 21 | script: 22 | - # Build the AppImages 23 | - docker run --cap-add SYS_ADMIN --device /dev/fuse --mount src=$PWD,dst=/work,type=bind ${DOCKER_USERNAME}/linuxdeploy-plugin-python:${ARCH} 24 | - # Test the Python AppImages 25 | - mkdir -p home/test/.local/lib/python2.7/site-packages 26 | - mkdir -p home/test/.local/lib/python3.7/site-packages 27 | - mkdir -p home/test/.local/lib/python3.8/site-packages 28 | - HOME=$PWD/home/test USER=test PATH="$PWD/home/test/.local/bin:$PATH" python3 -m tests 29 | 30 | after_success: 31 | - # Upload the AppImages 32 | - ./.travis/after-success.sh 33 | -------------------------------------------------------------------------------- /.travis/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/pypa/manylinux2010_x86_64 2 | LABEL maintainer="https://github.com/niess" 3 | 4 | ARG Arch=x86_64 5 | ENV ARCH="${Arch}" \ 6 | OPENSSL="1.1.1c" 7 | 8 | RUN yum -y install bzip2-devel fuse-sshfs gdbm-devel ncurses-devel \ 9 | readline-devel sqlite-devel openssl-devel symlinks tk-devel \ 10 | xz-devel wget 11 | 12 | COPY . /work/.travis 13 | WORKDIR /work 14 | SHELL ["/bin/bash", "-c"] 15 | 16 | ENV OPENSSL_DIR="${HOME}/openssl/${OPENSSL}" 17 | RUN ./.travis/install.sh 18 | 19 | ENV PATH="${OPENSSL_DIR}/bin:${PATH}" \ 20 | CFLAGS="${CFLAGS} -I${OPENSSL_DIR}/include" \ 21 | LDFLAGS="-L${OPENSSL_DIR}/lib" \ 22 | LD_LIBRARY_PATH="${OPENSSL_DIR}/lib:${LD_LIBRARY_PATH}" 23 | 24 | CMD ["./.travis/script.sh"] 25 | -------------------------------------------------------------------------------- /.travis/after-success.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Upload the AppImage 4 | ls -lh appimage/*.AppImage 5 | wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh 6 | source upload.sh appimage/*.AppImage 7 | 8 | 9 | # Upload the Docker container 10 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 11 | docker push $DOCKER_USERNAME/linuxdeploy-plugin-python:${ARCH} 12 | -------------------------------------------------------------------------------- /.travis/install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -ex 4 | 5 | # Download, compile, and install openssl 6 | wget --no-check-certificate -q "https://www.openssl.org/source/openssl-${OPENSSL}.tar.gz" 7 | tar zxf "openssl-${OPENSSL}.tar.gz" 8 | pushd "openssl-${OPENSSL}" 9 | ./config no-ssl2 no-ssl3 shared -fPIC --prefix="$OPENSSL_DIR" 10 | make depend 11 | make -j"$(nproc)" 12 | if [[ "${OPENSSL}" =~ 1.0.1 ]]; then 13 | # OpenSSL 1.0.1 doesn't support installing without the docs. 14 | make install 15 | else 16 | # Avoid installing the docs 17 | make install_sw install_ssldirs 18 | fi 19 | popd 20 | -------------------------------------------------------------------------------- /.travis/script.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -ex 4 | 5 | 6 | wrapped_run() { 7 | local script="./appimage/build-$1.sh" 8 | local arg="$2" 9 | local log 10 | if [ -z "$2" ]; then 11 | log="plugin.log" 12 | else 13 | log="${2}.log" 14 | fi 15 | 16 | ("${script}" "${arg}" >& "${log}" ; tail -300 "${log}") 17 | } 18 | 19 | wrapped_run plugin 20 | wrapped_run python python2.7.17 21 | wrapped_run python python3.7.6 22 | wrapped_run python python3.8.2 23 | wrapped_run python scipy 24 | wrapped_run python xonsh 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Université Clermont Auvergne, CNRS/IN2P3, LPC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This project is no longer being updated. You can now find ready-to-use 3 | > Manylinux Python AppImages on the 4 | > [python-appimage](https://github.com/niess/python-appimage) project. The 5 | > latter project also makes it easier to build basic Python apps. 6 | 7 | --- 8 | 9 | # A Python plugin for linuxdeploy [![Build Status](https://travis-ci.com/niess/linuxdeploy-plugin-python.svg?branch=master)](https://travis-ci.com/niess/linuxdeploy-plugin-python) 10 | 11 | 12 | ## [For developers][WIKI_DEVS] 13 | 14 | This is a plugin for [linuxdeploy][LINUXDEPLOY]. It helps building _lightweight_ 15 | [Python][PYTHON] based [AppImage][APPIMAGE] applications by bundling a 16 | _minimalist_ source distribution of [Python][PYTHON] inside an [AppDir][APPDIR]. 17 | Extra site specific packages can be bundled as well using `pip`, E.g. binaries 18 | from [PyPi][PYPI]. Specific instructions for building and configuring the image 19 | are located on the [wiki][WIKI_DEVS]. 20 | 21 | 22 | ## [For users][WIKI_USERS] 23 | 24 | Ready to use [AppImage][APPIMAGE] distributions of [Python][PYTHON] are provided 25 | in the [downloads](#downloads) sections below or in the [release][RELEASE] 26 | area. A one liner example is: 27 | ``` 28 | wget -cq https://github.com/niess/linuxdeploy-plugin-python/releases/download/continuous/python3.8.2-x86_64.AppImage && chmod u+x python3.8.2-x86_64.AppImage && ./python3.8.2-x86_64.AppImage 29 | ``` 30 | which will install and run a [Python][PYTHON] instance. See the instructions on 31 | the [wiki][WIKI_USERS] for more detailed usage. 32 | 33 | ## Projects using [linuxdeploy-plugin-python][PYTHON_PLUGIN] 34 | * [grand/python](https://github.com/grand-mother/python) - Contained, portable 35 | and modern python for [GRAND][GRAND] running from an AppImage 36 | * [xxh](https://github.com/xxh/xxh) - Bring your favorite shell wherever you go 37 | through the ssh 38 | 39 | ## Downloads 40 | 41 | [![Python 2.7](https://img.shields.io/badge/python2.7-x86_64-blue.svg)](https://github.com/niess/linuxdeploy-plugin-python/releases/download/continuous/python2.7.17-x86_64.AppImage) 42 | [![Python 3.7](https://img.shields.io/badge/python3.7-x86_64-blue.svg)](https://github.com/niess/linuxdeploy-plugin-python/releases/download/continuous/python3.7.6-x86_64.AppImage) 43 | [![Python 3.8](https://img.shields.io/badge/python3.8-x86_64-blue.svg)](https://github.com/niess/linuxdeploy-plugin-python/releases/download/continuous/python3.8.2-x86_64.AppImage) 44 | [![Scipy](https://img.shields.io/badge/scipy-x86_64-blue.svg)](https://github.com/niess/linuxdeploy-plugin-python/releases/download/continuous/scipy-x86_64.AppImage) 45 | [![Xonsh](https://img.shields.io/badge/xonsh-x86_64-blue.svg)](https://github.com/niess/linuxdeploy-plugin-python/releases/download/continuous/xonsh-x86_64.AppImage) 46 | [![Plugin](https://img.shields.io/badge/plugin-x86_64-blue.svg)](https://github.com/niess/linuxdeploy-plugin-python/releases/download/continuous/linuxdeploy-plugin-python-x86_64.AppImage) 47 | 48 | 49 | [APPIMAGE]: https://appimage.org 50 | [APPDIR]: https://docs.appimage.org/reference/appdir.html 51 | [GRAND]: http://grand.cnrs.fr 52 | [LINUXDEPLOY]: https://github.com/linuxdeploy/linuxdeploy 53 | [PYPI]: https://pypi.org 54 | [PYTHON]: https://www.python.org 55 | [PYTHON_PLUGIN]: https://github.com/niess/linuxdeploy-plugin-python 56 | [RELEASE]: https://github.com/niess/linuxdeploy-plugin-python/releases 57 | [WIKI_DEVS]: ../../wiki/Developers 58 | [WIKI_USERS]: ../../wiki/Users 59 | 60 | -------------------------------------------------------------------------------- /appimage/build-plugin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$DEBUG" ] || [ "$DEBUG" -eq "0" ]; then 4 | set -e 5 | else 6 | set -ex 7 | fi 8 | 9 | 10 | EXEC_NAME="${EXEC_NAME:-linuxdeploy-plugin-python}" 11 | ARCH="${ARCH:-$(arch)}" 12 | PATCHELF_VERSION="0.10" 13 | 14 | REPO_ROOT=$(readlink -f $(dirname "$0")"/..") 15 | 16 | 17 | # Setup a temporary work space 18 | if [ -z "${BUILD_DIR}" ]; then 19 | BUILD_DIR=$(mktemp -d) 20 | 21 | _cleanup() { 22 | rm -rf "${BUILD_DIR}" 23 | } 24 | 25 | trap _cleanup EXIT 26 | else 27 | BUILD_DIR=$(readlink -m "${BUILD_DIR}") 28 | mkdir -p "${BUILD_DIR}" 29 | fi 30 | 31 | pushd $BUILD_DIR 32 | 33 | 34 | # Install the ELF patcher 35 | prefix="${PWD}/AppDir/usr" 36 | mkdir -p "${prefix}" 37 | wget -cq --no-check-certificate "https://github.com/NixOS/patchelf/archive/${PATCHELF_VERSION}.tar.gz" 38 | tar -xzf "${PATCHELF_VERSION}.tar.gz" 39 | pushd "patchelf-${PATCHELF_VERSION}" 40 | ./bootstrap.sh 41 | if [ "$ARCH" == "i386" ]; then 42 | ./configure --prefix="${prefix}" --build=i686-pc-linux-gnu CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32 43 | elif [ "$ARCH" == "x86_64" ]; then 44 | ./configure --prefix="${prefix}" 45 | fi 46 | make -j$(nproc) 47 | make install 48 | popd 49 | rm -rf "AppDir/usr/share" 50 | strip "AppDir/usr/bin/patchelf" 51 | 52 | 53 | # Package the exclusion list 54 | mkdir -p "AppDir/share" 55 | pushd "AppDir/share" 56 | wget -cq --no-check-certificate "https://raw.githubusercontent.com/probonopd/AppImages/master/excludelist" 57 | popd 58 | 59 | 60 | # Build the AppImage 61 | appimagetool="appimagetool-${ARCH}.AppImage" 62 | 63 | if [ ! -f "${appimagetool}" ]; then 64 | url="https://github.com/AppImage/AppImageKit/releases/download/continuous" 65 | wget --no-check-certificate -q "${url}/${appimagetool}" 66 | chmod u+x "${appimagetool}" 67 | fi 68 | 69 | cp "${REPO_ROOT}/${EXEC_NAME}.sh" "AppDir/AppRun" 70 | chmod +x "AppDir/AppRun" 71 | cp -r "${REPO_ROOT}/share" "AppDir" 72 | 73 | pushd "AppDir" 74 | cp "${REPO_ROOT}/${EXEC_NAME}.sh" "." 75 | cp "${REPO_ROOT}/appimage/resources/python.png" "${EXEC_NAME}.png" 76 | cp "${REPO_ROOT}/appimage/resources/plugin.desktop" "${EXEC_NAME}.desktop" 77 | ln -s "${EXEC_NAME}.png" ".DirIcon" 78 | popd 79 | 80 | ARCH="${ARCH}" ./"${appimagetool}" AppDir 81 | popd 82 | mv -f "${BUILD_DIR}/${EXEC_NAME}-${ARCH}.AppImage" "${REPO_ROOT}/appimage" 83 | -------------------------------------------------------------------------------- /appimage/build-python.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$DEBUG" ] || [ "$DEBUG" -eq "0" ]; then 4 | set -e 5 | else 6 | set -ex 7 | fi 8 | 9 | 10 | REPO_ROOT=$(readlink -f $(dirname "$0")"/..") 11 | 12 | 13 | if [ -z "$1" ]; then 14 | echo "no recipe specified. Aborting..." 15 | exit 1 16 | fi 17 | if [ -f "$1" ]; then 18 | source "$1" 19 | filename=$(basename "$1") 20 | name="${filename%.*}" 21 | else 22 | source "${REPO_ROOT}/appimage/recipes/${1}.sh" 23 | name="$1" 24 | fi 25 | 26 | export PYTHON_SOURCE="${PYTHON_SOURCE:-https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz}" 27 | export PIP_REQUIREMENTS 28 | 29 | 30 | ARCH="${ARCH:-$(arch)}" 31 | export ARCH="${ARCH}" 32 | 33 | 34 | # Setup a temporary work space 35 | if [ -z "${BUILD_DIR}" ]; then 36 | BUILD_DIR=$(mktemp -d) 37 | 38 | _cleanup() { 39 | rm -rf "${BUILD_DIR}" 40 | } 41 | 42 | trap _cleanup EXIT 43 | else 44 | BUILD_DIR=$(readlink -m "${BUILD_DIR}") 45 | mkdir -p "${BUILD_DIR}" 46 | fi 47 | 48 | pushd $BUILD_DIR 49 | 50 | 51 | # Build the AppImage 52 | linuxdeploy="linuxdeploy-${ARCH}.AppImage" 53 | plugin="linuxdeploy-plugin-python-${ARCH}.AppImage" 54 | 55 | if [ ! -f "${linuxdeploy}" ]; then 56 | url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous" 57 | wget --no-check-certificate -q "${url}/${linuxdeploy}" 58 | chmod u+x "${linuxdeploy}" 59 | fi 60 | 61 | if [ ! -f "${REPO_ROOT}/appimage/${plugin}" ]; then 62 | ${REPO_ROOT}/appimage/build-plugin.sh 63 | fi 64 | cp "${REPO_ROOT}/appimage/${plugin}" "." 65 | 66 | exe="python${PYTHON_VERSION::3}" 67 | 68 | cp "${REPO_ROOT}/appimage/resources/python.desktop" "${name}.desktop" 69 | sed -i "s|[{][{]exe[}][}]|${exe}|g" "${name}.desktop" 70 | sed -i "s|[{][{]name[}][}]|${name}|g" "${name}.desktop" 71 | 72 | cp "${REPO_ROOT}/appimage/resources/apprun.sh" "AppRun" 73 | sed -i "s|[{][{]exe[}][}]|${exe}|g" "AppRun" 74 | sed -i "s|[{][{]entrypoint[}][}]|${APPRUN_ENTRYPOINT}|g" "AppRun" 75 | 76 | ./"${linuxdeploy}" --appdir AppDir \ 77 | --plugin python \ 78 | -i "${REPO_ROOT}/appimage/resources/python.png" \ 79 | -d "${name}.desktop" \ 80 | --custom-apprun "AppRun" \ 81 | --output "appimage" 82 | 83 | popd 84 | mv -f "${BUILD_DIR}/${name}-${ARCH}.AppImage" "${REPO_ROOT}/appimage" 85 | -------------------------------------------------------------------------------- /appimage/recipes/python2.7.17.sh: -------------------------------------------------------------------------------- 1 | export PYTHON_VERSION="2.7.17" 2 | export PIP_REQUIREMENTS= 3 | -------------------------------------------------------------------------------- /appimage/recipes/python3.7.6.sh: -------------------------------------------------------------------------------- 1 | export PYTHON_VERSION="3.7.6" 2 | export PIP_REQUIREMENTS= 3 | -------------------------------------------------------------------------------- /appimage/recipes/python3.8.2.sh: -------------------------------------------------------------------------------- 1 | export PYTHON_VERSION="3.8.2" 2 | export PIP_REQUIREMENTS= 3 | -------------------------------------------------------------------------------- /appimage/recipes/scipy.sh: -------------------------------------------------------------------------------- 1 | export PYTHON_VERSION="3.8.1" 2 | export PIP_REQUIREMENTS="numpy scipy matplotlib sympy pandas" 3 | -------------------------------------------------------------------------------- /appimage/recipes/xonsh.sh: -------------------------------------------------------------------------------- 1 | export PYTHON_VERSION="3.8.1" 2 | export PIP_REQUIREMENTS="xonsh prompt_toolkit gnureadline Pygments" 3 | export APPRUN_ENTRYPOINT='"-u" "-c" "from xonsh.main import main; main()"' 4 | -------------------------------------------------------------------------------- /appimage/resources/apprun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT=`readlink -f -- $0` 4 | SCRIPTPATH=`dirname $SCRIPT` 5 | APPDIR="${APPDIR:-$SCRIPTPATH}" 6 | 7 | ${APPDIR}/usr/bin/{{exe}} {{entrypoint}} "$@" 8 | -------------------------------------------------------------------------------- /appimage/resources/plugin.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Categories=Utility; 3 | Type=Application 4 | Icon=linuxdeploy-plugin-python 5 | Exec=linuxdeploy-plugin-python 6 | Name=linuxdeploy-plugin-python 7 | Terminal=true 8 | -------------------------------------------------------------------------------- /appimage/resources/python.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Categories=Development; 3 | Type=Application 4 | Icon=python 5 | Exec={{exe}} 6 | Name={{name}} 7 | Terminal=true 8 | -------------------------------------------------------------------------------- /appimage/resources/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niess/linuxdeploy-plugin-python/d22f05d8eb2dd019f993e639f64a65a75e373564/appimage/resources/python.png -------------------------------------------------------------------------------- /appimage/update-version.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | 6 | # Parse the CLI 7 | show_usage () { 8 | echo "Usage: $(basename $0) " 9 | echo 10 | echo "Update a Python version tag." 11 | echo "" 12 | } 13 | 14 | 15 | while [ ! -z "$1" ]; do 16 | case "$1" in 17 | -h) 18 | show_usage 19 | exit 0 20 | ;; 21 | -*) 22 | echo "Invalid option: $1." 23 | echo 24 | show_usage 25 | exit 1 26 | ;; 27 | *) 28 | break 29 | esac 30 | done 31 | 32 | if [ "$#" -ne 2 ]; then 33 | echo "Invalid number of arguments. Got $#, expected 2." 34 | echo 35 | show_usage 36 | exit 1 37 | fi 38 | 39 | 40 | # Process the version tag 41 | match_and_replace() { 42 | local prefix="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" 43 | if [ ! -f "${prefix}/appimage/recipes/python$1.sh" ]; then 44 | echo "Error: invalid version tag: $1. Aborting." 45 | exit 1 46 | fi 47 | 48 | local files=".travis/script.sh README.md appimage/recipes/python$1.sh tests/test_plugin.py" 49 | 50 | local file 51 | for file in ${files}; do 52 | echo "updating tag in ${file}" 53 | sed -i -- "s/$1/$2/g" "${prefix}/${file}" 54 | done 55 | 56 | git mv "${prefix}/appimage/recipes/python$1.sh" \ 57 | "${prefix}/appimage/recipes/python$2.sh" 58 | } 59 | 60 | match_and_replace "$@" 61 | -------------------------------------------------------------------------------- /linuxdeploy-plugin-python.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "$DEBUG" ]] || [[ "$DEBUG" -eq "0" ]]; then 4 | set -e 5 | else 6 | set -ex 7 | fi 8 | 9 | 10 | # Configuration variables 11 | NPROC="${NPROC:-$(nproc)}" 12 | PIP_OPTIONS="${PIP_OPTIONS:---upgrade}" 13 | PIP_REQUIREMENTS="${PIP_REQUIREMENTS:-}" 14 | PYTHON_BUILD_DIR="${PYTHON_BUILD_DIR:-}" 15 | PYTHON_CONFIG="${PYTHON_CONFIG:-}" 16 | version="3.8.2" 17 | PYTHON_SOURCE="${PYTHON_SOURCE:-https://www.python.org/ftp/python/${version}/Python-${version}.tgz}" 18 | 19 | script=$(readlink -f $0) 20 | exe_name="$(basename ${APPIMAGE:-$script})" 21 | BASEDIR="${APPDIR:-$(readlink -m $(dirname $script))}" 22 | 23 | prefix="usr/python" 24 | 25 | 26 | # Parse the CLI 27 | show_usage () { 28 | echo "Usage: ${exe_name} --appdir " 29 | echo 30 | echo "Bundle Python into an AppDir under \${APPDIR}/${prefix}. In addition" 31 | echo "extra packages can be bundled as well using \$PIP_REQUIREMENTS." 32 | echo "Note that if an existing install of Python already exists then no new" 33 | echo "install of Python is done, i.e. \${PYTHON_SOURCE} and its related" 34 | echo "arguments are ignored. Yet, extra \$PIP_REQUIREMENTS are installed." 35 | echo 36 | echo "Variables:" 37 | echo " NPROC=\"${NPROC}\"" 38 | echo " The number of processors to use for building Python from a" 39 | echo " source distribution" 40 | echo 41 | echo " PIP_OPTIONS=\"${PIP_OPTIONS}\"" 42 | echo " Options for pip when bundling extra site-packages" 43 | echo 44 | echo " PIP_REQUIREMENTS=\"${PIP_REQUIREMENTS}\"" 45 | echo " Specify extra site-packages to embed in the AppImage. Those are" 46 | echo " installed with pip as requirements" 47 | echo 48 | echo " PYTHON_BUILD_DIR=\"\"" 49 | echo " Set the build directory for Python. A temporary one will be" 50 | echo " created otherwise" 51 | echo 52 | echo " PYTHON_CONFIG=\"${PYTHON_CONFIG}\"" 53 | echo " Provide extra configuration flags for the Python build. Note" 54 | echo " that the install prefix will be overwritten" 55 | echo 56 | echo " PYTHON_SOURCE=\"${PYTHON_SOURCE}\"" 57 | echo " The source to use for Python. Can be a directory, an url or/and" 58 | echo " an archive" 59 | echo 60 | } 61 | 62 | APPDIR= 63 | 64 | while [[ ! -z "$1" ]]; do 65 | case "$1" in 66 | --plugin-api-version) 67 | echo "0" 68 | exit 0 69 | ;; 70 | --appdir) 71 | APPDIR="$2" 72 | shift 73 | shift 74 | ;; 75 | --help) 76 | show_usage 77 | exit 0 78 | ;; 79 | *) 80 | echo "Invalid argument: $1" 81 | echo 82 | show_usage 83 | exit 1 84 | ;; 85 | esac 86 | done 87 | 88 | if [[ -z "$APPDIR" ]]; then 89 | show_usage 90 | exit 1 91 | else 92 | APPDIR=$(readlink -m "$APPDIR") 93 | mkdir -p "$APPDIR" 94 | fi 95 | 96 | 97 | # Setup a temporary work space 98 | if [[ -z "${PYTHON_BUILD_DIR}" ]]; then 99 | PYTHON_BUILD_DIR=$(mktemp -d) 100 | 101 | atexit() { 102 | rm -rf "${PYTHON_BUILD_DIR}" 103 | } 104 | 105 | trap atexit EXIT 106 | else 107 | PYTHON_BUILD_DIR=$(readlink -m "${PYTHON_BUILD_DIR}") 108 | mkdir -p "${PYTHON_BUILD_DIR}" 109 | fi 110 | 111 | 112 | # Install Python from source, if not already in the AppDir 113 | set +e 114 | python=$(ls "${APPDIR}/${prefix}/bin/python"?"."?) 115 | set -e 116 | 117 | if [[ -x "${python}" ]]; then 118 | echo "Found existing install under ${APPDIR}/${prefix}. Skipping Python build." 119 | else 120 | # Check if the given sources are a local file or directory; if yes, 121 | # "save" the full path. It might've been given relative to the current 122 | # directory. 123 | if [[ -e "${PYTHON_SOURCE}" ]]; then 124 | PYTHON_SOURCE=$(readlink -f "${PYTHON_SOURCE}") 125 | fi 126 | 127 | cd "${PYTHON_BUILD_DIR}" 128 | source_file=$(basename "${PYTHON_SOURCE}") 129 | if [[ "${PYTHON_SOURCE}" == http* ]] || [[ "${PYTHON_SOURCE}" == ftp* ]]; then 130 | wget -c --no-check-certificate "${PYTHON_SOURCE}" 131 | else 132 | cp -r "${PYTHON_SOURCE}" "." 133 | fi 134 | if [[ "${source_file}" == *.tgz ]] || [[ "${source_file}" == *.tar.gz ]]; then 135 | if [[ "${source_file}" == *.tgz ]]; then 136 | dirname="${source_file%.*}" 137 | else 138 | dirname="${source_file%.*.*}" 139 | fi 140 | [[ -f $dirname ]] || tar -xzf "${source_file}" 141 | source_dir="$dirname" 142 | else 143 | source_dir="$source_file" 144 | fi 145 | 146 | cd "${source_dir}" 147 | ./configure ${PYTHON_CONFIG} "--with-ensurepip=install" "--prefix=/${prefix}" LDFLAGS="${LDFLAGS} -Wl,-rpath='"'$$ORIGIN'"/../../lib'" 148 | HOME="${PYTHON_BUILD_DIR}" make -j"$NPROC" DESTDIR="$APPDIR" install 149 | fi 150 | 151 | cd "${APPDIR}/${prefix}/bin" 152 | PYTHON_X_Y=$(ls "python"?"."?) 153 | 154 | 155 | # Install any extra requirements with pip 156 | if [[ ! -z "${PIP_REQUIREMENTS}" ]]; then 157 | HOME="${PYTHON_BUILD_DIR}" PYTHONHOME=$(readlink -f ${PWD}/..) ./${PYTHON_X_Y} -m pip install ${PIP_OPTIONS} --upgrade pip 158 | HOME="${PYTHON_BUILD_DIR}" PYTHONHOME=$(readlink -f ${PWD}/..) ./${PYTHON_X_Y} -m pip install ${PIP_OPTIONS} ${PIP_REQUIREMENTS} 159 | fi 160 | 161 | 162 | # Prune the install 163 | cd "$APPDIR/${prefix}" 164 | rm -rf "bin/python"*"-config" "bin/idle"* "lib/pkgconfig" \ 165 | "share/doc" "share/man" "lib/libpython"*".a" "lib/python"*"/test" \ 166 | "lib/python"*"/config-"*"-x86_64-linux-gnu" 167 | 168 | 169 | # Wrap the Python executables 170 | cd "$APPDIR/${prefix}/bin" 171 | set +e 172 | pythons=$(ls "python" "python"? "python"?"."? "python"?"."?"m" 2>/dev/null) 173 | set -e 174 | mkdir -p "$APPDIR/usr/bin" 175 | cd "$APPDIR/usr/bin" 176 | for python in $pythons 177 | do 178 | if [[ ! -L "$python" ]]; then 179 | strip "$APPDIR/${prefix}/bin/${python}" 180 | cp "${BASEDIR}/share/python-wrapper.sh" "$python" 181 | sed -i "s|[{][{]PYTHON[}][}]|$python|g" "$python" 182 | sed -i "s|[{][{]PREFIX[}][}]|$prefix|g" "$python" 183 | fi 184 | done 185 | 186 | 187 | # Sanitize the shebangs of local Python scripts 188 | cd "$APPDIR/${prefix}/bin" 189 | for exe in $(ls "${APPDIR}/${prefix}/bin"*) 190 | do 191 | if [[ -x "$exe" ]] && [[ ! -d "$exe" ]]; then 192 | sed -i '1s|^#!.*\(python[0-9.]*\).*|#!/bin/sh\n"exec" "$(dirname $(readlink -f $\{0\}))/../../bin/\1" "$0" "$@"|' "$exe" 193 | fi 194 | done 195 | 196 | 197 | # Set a hook in Python for cleaning the path detection 198 | cp "$BASEDIR/share/sitecustomize.py" "$APPDIR"/${prefix}/lib/python*/site-packages 199 | 200 | 201 | # Patch binaries and install dependencies 202 | excludelist="${BASEDIR}/share/excludelist" 203 | if [[ ! -f "${excludelist}" ]]; then 204 | pushd "${BASEDIR}" 205 | wget -cq --no-check-certificate "https://raw.githubusercontent.com/probonopd/AppImages/master/excludelist" 206 | excludelist="$(pwd)/excludelist" 207 | popd 208 | fi 209 | excludelist=$(cat "${excludelist}" | sed 's|#.*||g' | sed -r '/^\s*$/d') 210 | 211 | is_excluded () { 212 | local e 213 | for e in ${excludelist}; do 214 | [[ "$e" == "$1" ]] && echo "true" && return 0 215 | done 216 | return 0 217 | } 218 | 219 | set +e 220 | patchelf=$(command -v patchelf) 221 | set -e 222 | patchelf="${patchelf:-${BASEDIR}/usr/bin/patchelf}" 223 | if [[ ! -x "${patchelf}" ]]; then 224 | ARCH="${ARCH:-x86_64}" 225 | pushd "${BASEDIR}" 226 | wget -cq https://github.com/niess/patchelf.appimage/releases/download/${ARCH}/patchelf-${ARCH}.AppImage 227 | patchelf="$(pwd)/patchelf-${ARCH}.AppImage" 228 | chmod u+x "${patchelf}" 229 | popd 230 | fi 231 | 232 | patch_binary() { 233 | local name="$(basename $1)" 234 | 235 | if [[ "${name::3}" == "lib" ]]; then 236 | if [[ ! -f "${APPDIR}/usr/lib/${name}" ]] && [[ ! -L "${APPDIR}/usr/lib/${name}" ]]; then 237 | echo "Patching dependency ${name}" 238 | "${patchelf}" --set-rpath '$ORIGIN' "$1" 239 | ln -s "$2"/"$1" "${APPDIR}/usr/lib/${name}" 240 | fi 241 | else 242 | echo "Patching C-extension module ${name}" 243 | local rpath="$(${patchelf} --print-rpath $1)" 244 | local rel="$(dirname $(readlink -f $1))" 245 | rel=${rel#${APPDIR}/usr} 246 | rel=$(echo $rel | sed 's|/[_a-zA-Z0-9.-]*|/..|g') 247 | if grep -qv '$ORIGIN'"${rel}/lib" <<< "${rpath}" ; then 248 | [[ ! -z "${rpath}" ]] && rpath="${rpath}:" 249 | "${patchelf}" --set-rpath "${rpath}"'$ORIGIN'"${rel}/lib" "$1" 250 | fi 251 | fi 252 | 253 | local deps 254 | for deps in $(ldd $1); do 255 | if [[ "${deps::1}" == "/" ]] && [[ "${deps}" != "${APPDIR}"* ]]; then 256 | local lib="$(basename ${deps})" 257 | if [[ ! -f "${APPDIR}/usr/lib/${lib}" ]]; then 258 | if [[ ! "$(is_excluded ${lib})" ]]; then 259 | echo "Installing dependency ${lib}" 260 | cp "${deps}" "${APPDIR}/usr/lib" 261 | "${patchelf}" --set-rpath '$ORIGIN' "${APPDIR}/usr/lib/${lib}" 262 | fi 263 | fi 264 | fi 265 | done 266 | return 0 267 | } 268 | 269 | cd "$APPDIR/${prefix}/bin" 270 | [[ -f python3 ]] && ln -fs python3 python 271 | mkdir -p "${APPDIR}/usr/lib" 272 | cd "${APPDIR}/${prefix}/lib/${PYTHON_X_Y}" 273 | relpath="../../${prefix}/lib/${PYTHON_X_Y}" 274 | find "lib-dynload" -name '*.so' -type f | while read file; do patch_binary "${file}" "${relpath}"; done 275 | 276 | 277 | # Copy any TCl/Tk shared data 278 | if [[ ! -d "${APPDIR}/${prefix}/share/tcltk" ]]; then 279 | if [[ -d "/usr/share/tcltk" ]]; then 280 | mkdir -p "${APPDIR}/${prefix}/share" 281 | cp -r "/usr/share/tcltk" "${APPDIR}/${prefix}/share" 282 | else 283 | mkdir -p "${APPDIR}/${prefix}/share/tcltk" 284 | tclpath="$(ls -d /usr/share/tcl* | tail -1)" 285 | tkpath="$(ls -d /usr/share/tk* | tail -1)" 286 | for path in "${tclpath}" "${tkpath}"; do 287 | cp -r "${path}" "${APPDIR}/${prefix}/share/tcltk" 288 | done 289 | fi 290 | fi 291 | -------------------------------------------------------------------------------- /share/python-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT=`readlink -f -- $0` 4 | SCRIPTPATH=`dirname $SCRIPT` 5 | APPDIR="${APPDIR:-$SCRIPTPATH/../..}" 6 | 7 | # Configure the environment 8 | prefix="{{PREFIX}}" 9 | export TCL_LIBRARY="$(ls -d ${APPDIR}/${prefix}/share/tcltk/tcl* | tail -1)" 10 | export TK_LIBRARY="$(ls -d ${APPDIR}/${prefix}/share/tcltk/tk* | tail -1)" 11 | export TKPATH="${TK_LIBRARY}" 12 | 13 | # Resolve symlinks within the image 14 | nickname="{{PYTHON}}" 15 | executable="${APPDIR}/${prefix}/bin/${nickname}" 16 | if [ -L "${executable}" ]; then 17 | nickname="$(basename $(readlink -f ${executable}))" 18 | fi 19 | 20 | for opt in "$@" 21 | do 22 | [ "${opt:0:1}" != "-" ] && break 23 | if [[ "${opt}" =~ "I" ]] || [[ "${opt}" =~ "E" ]]; then 24 | # Environment variables are disabled ($PYTHONHOME). Let's run in a safe 25 | # mode from the raw Python binary inside the AppImage 26 | "$APPDIR/${prefix}/bin/${nickname}" "$@" 27 | exit "$?" 28 | fi 29 | done 30 | 31 | # But don't resolve symlinks from outside! 32 | if [[ "${ARGV0}" =~ "/" ]]; then 33 | executable="$(cd $(dirname ${ARGV0}) && pwd)/$(basename ${ARGV0})" 34 | elif [[ "${ARGV0}" != "" ]]; then 35 | executable=$(which "${ARGV0}") 36 | fi 37 | 38 | # Wrap the call to Python in order to mimic a call from the source 39 | # executable ($ARGV0), but potentially located outside of the Python 40 | # install ($PYTHONHOME) 41 | (PYTHONHOME="${APPDIR}/${prefix}" exec -a "${executable}" "$APPDIR/${prefix}/bin/${nickname}" "$@") 42 | exit "$?" 43 | -------------------------------------------------------------------------------- /share/sitecustomize.py: -------------------------------------------------------------------------------- 1 | """Hook for cleaning the paths detected by Python 2 | """ 3 | import os 4 | import sys 5 | 6 | 7 | def clean_path(): 8 | site_packages = "/usr/local/lib/python{:}.{:}/site-packages".format( 9 | *sys.version_info[:2]) 10 | binaries_path = "/usr/local/bin" 11 | env_path = os.getenv("PYTHONPATH") 12 | if env_path is None: 13 | env_path = [] 14 | else: 15 | env_path = [os.path.realpath(path) for path in env_path.split(":")] 16 | 17 | if ((os.path.dirname(sys.executable) != binaries_path) and 18 | (site_packages not in env_path)): 19 | # Remove the builtin site-packages from the path 20 | try: 21 | sys.path.remove(site_packages) 22 | except ValueError: 23 | pass 24 | 25 | 26 | clean_path() 27 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the python plugin""" 2 | 3 | from .test_plugin import PluginTest 4 | 5 | __all__ = ["PluginTest"] 6 | 7 | -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run all unit tests for the python plugin 3 | """ 4 | import os 5 | import unittest 6 | import sys 7 | 8 | def suite(): 9 | # Load the unit tests 10 | test_loader = unittest.TestLoader() 11 | path = os.path.dirname(__file__) 12 | suite = test_loader.discover(path, pattern="test_*.py") 13 | 14 | return suite 15 | 16 | 17 | if __name__ == "__main__": 18 | runner = unittest.TextTestRunner(verbosity=2, failfast=True) 19 | r = not runner.run(suite()).wasSuccessful() 20 | sys.exit(r) 21 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import subprocess 5 | import unittest 6 | import sys 7 | 8 | 9 | TAGS = ("python2.7.17", "python3.7.6", "python3.8.2") 10 | 11 | TESTDIR = "/tmp/test-linuxdeploy-plugin-python" 12 | ROOTDIR = os.path.realpath(os.path.dirname(__file__) + "/..").strip() 13 | 14 | _is_python2 = sys.version_info[0] == 2 15 | 16 | 17 | def get_version(recipe): 18 | path = os.path.join(ROOTDIR, "appimage", "recipes", recipe + ".sh") 19 | with open(path) as f: 20 | for line in f: 21 | if line.startswith("export PYTHON_VERSION="): 22 | version = line.split("=")[-1] 23 | return version.strip().replace('"', "").replace("'", "") 24 | else: 25 | raise ValueError("version not found") 26 | 27 | 28 | def system(command, env=None): 29 | """Wrap system calls 30 | """ 31 | p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, 32 | stderr=subprocess.STDOUT, env=env) 33 | out, _ = p.communicate() 34 | if not _is_python2: 35 | out = out.decode() 36 | if p.returncode != 0: 37 | raise RuntimeError(os.linesep.join( 38 | ("", "COMMAND:", command, "OUTPUT:", out))) 39 | return out 40 | 41 | 42 | class PluginTest(unittest.TestCase): 43 | """Unit tests for the python plugin 44 | """ 45 | 46 | def __init__(self, *args, **kwargs): 47 | if _is_python2: 48 | super(PluginTest, self).__init__(*args, **kwargs) 49 | else: 50 | super().__init__(*args, **kwargs) 51 | 52 | # Configure the test environment 53 | if not "ARCH" in os.environ: 54 | os.environ["ARCH"] = system("arch").strip() 55 | 56 | user = os.getenv("USER") 57 | if (user is None) or (user == "root"): 58 | user = "beta" 59 | home = "/tmp/home/" + user 60 | os.environ["USER"] = user 61 | os.environ["HOME"] = home 62 | if not os.path.exists(home): 63 | for tag in TAGS: 64 | version = get_version(tag) 65 | os.makedirs(os.path.join(home, ".local", "lib", 66 | "python" + version[:3], "site-packages")) 67 | bindir = os.path.join(home, ".local", "bin") 68 | os.environ["PATH"] = ":".join((bindir, os.environ["PATH"])) 69 | 70 | if not os.path.exists(TESTDIR): 71 | os.makedirs(TESTDIR) 72 | os.chdir(TESTDIR) 73 | 74 | for tag in TAGS: 75 | appimage = "{:}-{:}.AppImage".format(tag, os.environ["ARCH"]) 76 | shutil.copy( 77 | os.path.join(ROOTDIR, "appimage", appimage), 78 | os.path.join(TESTDIR, appimage)) 79 | 80 | 81 | def test_python38_base(self): 82 | """Test the base functionalities of a Python 3 AppImage 83 | """ 84 | self.check_base("python3.8.2") 85 | 86 | 87 | def test_python38_modules(self): 88 | """Test the modules availability of a Python 3 AppImage 89 | """ 90 | self.check_modules("python3.8.2") 91 | 92 | 93 | def test_python38_venv(self): 94 | """Test venv from a Python 3 AppImage 95 | """ 96 | self.check_venv("python3.8.2") 97 | 98 | 99 | def test_python37_base(self): 100 | """Test the base functionalities of a Python 3 AppImage 101 | """ 102 | self.check_base("python3.7.6") 103 | 104 | 105 | def test_python37_modules(self): 106 | """Test the modules availability of a Python 3 AppImage 107 | """ 108 | self.check_modules("python3.7.6") 109 | 110 | 111 | def test_python37_venv(self): 112 | """Test venv from a Python 3 AppImage 113 | """ 114 | self.check_venv("python3.7.6") 115 | 116 | 117 | def test_python2_base(self): 118 | """Test the base functionalities of a Python 2 AppImage 119 | """ 120 | self.check_base("python2.7.17") 121 | 122 | 123 | def check_base(self, tag): 124 | """Check the base functionalities of a Python AppImage 125 | """ 126 | version = get_version(tag) 127 | appimage = "python{:}-{:}.AppImage".format( 128 | version, os.getenv("ARCH")) 129 | 130 | # Check the Python system configuration 131 | python = os.path.join(TESTDIR, appimage) 132 | cfg = self.get_python_config(python) 133 | 134 | v = [int(vi) for vi in version.split(".")] 135 | self.assertEqual(cfg["version"][:3], v) 136 | self.assertEqual(cfg["executable"], python) 137 | self.assertEqual(cfg["prefix"], os.path.join(cfg["appdir"], "usr", 138 | "python")) 139 | site_packages = os.path.join("lib", 140 | "python{:}.{:}".format(*cfg["version"][:2]), "site-packages") 141 | self.assertEqual(cfg["path"][-1], os.path.join(cfg["appdir"], 142 | "usr", "python", site_packages)) 143 | user_packages = os.path.join(cfg["home"], ".local", site_packages) 144 | self.assertTrue(user_packages in cfg["path"]) 145 | 146 | # Check pip install 147 | system("./{:} -m pip uninstall test-pip-install -y || exit 0".format( 148 | appimage)) 149 | r = system("./{:} -m pip install --user test-pip-install".format( 150 | appimage)) 151 | r = system("test-pip-install").strip() 152 | self.assertEqual(r, "running Python {:} from {:}".format( 153 | version, os.path.join(TESTDIR, appimage))) 154 | 155 | 156 | def check_venv(self, tag): 157 | """Check venv from a Python AppImage 158 | """ 159 | version = get_version(tag) 160 | appimage = "python{:}-{:}.AppImage".format( 161 | version, os.getenv("ARCH")) 162 | 163 | # Generate a virtual environment 164 | if os.path.exists("ENV"): 165 | shutil.rmtree("ENV") 166 | 167 | system("./{:} -m venv ENV".format(appimage)) 168 | envdir = TESTDIR + "/ENV" 169 | python = envdir + "/bin/python" 170 | self.assertTrue(os.path.exists(python)) 171 | 172 | # Bootstrap pip 173 | def bash(cmd): 174 | return system("/bin/bash -c '. ENV/bin/activate; {:}'".format(cmd)) 175 | 176 | bash("python -m ensurepip") 177 | pip = "pip" + version[0] 178 | self.assertTrue(os.path.exists("ENV/bin/" + pip)) 179 | 180 | # Check the Python system configuration 181 | cfg = self.get_python_config("python", setup="ENV/bin/activate") 182 | 183 | v = [int(vi) for vi in version.split(".")] 184 | self.assertEqual(cfg["version"][:3], v) 185 | self.assertEqual(cfg["executable"], str(python)) 186 | self.assertEqual(cfg["prefix"], str(envdir)) 187 | site_packages = os.path.join("lib", 188 | "python{:}.{:}".format(*cfg["version"][:2]), "site-packages") 189 | self.assertEqual(cfg["path"][-1], str(os.path.join(envdir, 190 | site_packages))) 191 | self.assertTrue(os.path.join(cfg["home"], ".local", 192 | site_packages) not in cfg["path"]) 193 | 194 | # Check pip install 195 | system("{:} uninstall test-pip-install -y || exit 0".format(pip)) 196 | bash("{:} uninstall test-pip-install -y".format(pip)) 197 | bash("{:} install test-pip-install".format(pip)) 198 | r = bash("test-pip-install").strip() 199 | bash("{:} uninstall test-pip-install -y".format(pip)) 200 | self.assertEqual(r, "running Python {:} from {:}".format( 201 | version, str(python))) 202 | 203 | 204 | def check_modules(self, tag): 205 | """Check the modules availability of a Python AppImage 206 | """ 207 | version = get_version(tag) 208 | appimage = "python{:}-{:}.AppImage".format( 209 | version, os.getenv("ARCH")) 210 | 211 | def import_(module): 212 | system("./{:} -c 'import {:}'".format(appimage, module)) 213 | 214 | modules = { 215 | "a": ["abc", "aifc", "argparse", "array", "ast", "asynchat", 216 | "asyncio", "asyncore", "atexit", "audioop"], 217 | "b": ["base64", "bdb", "binascii", "binhex", "bisect", "builtins", 218 | "bz2"], 219 | "c": ["calendar", "cgi", "cgitb", "chunk", "cmath", "cmd", "code", 220 | "codecs", "codeop", "collections", "colorsys", "compileall", 221 | "concurrent", "configparser", "contextlib", "contextvars", 222 | "copy", "copyreg", "cProfile", "crypt", "csv", "ctypes", 223 | "curses"], 224 | "d": ["dataclasses", "datetime", "dbm", "decimal", "difflib", 225 | "dis", "distutils", "doctest"], 226 | "e": ["email", "encodings", "ensurepip", "enum", "errno"], 227 | "f": ["faulthandler", "fcntl", "filecmp", "fileinput", "fnmatch", 228 | "fractions", "ftplib", "functools"], 229 | "g": ["gc", "getopt", "getpass", "gettext", "glob", "grp", "gzip"], 230 | "h": ["hashlib", "heapq", "hmac", "html", "http"], 231 | "i": ["imaplib", "imghdr", "importlib", "inspect", "io", 232 | "ipaddress", "itertools"], 233 | "j": ["json"], 234 | "k": ["keyword"], 235 | "l": ["lib2to3", "linecache", "locale", "logging", "lzma"], 236 | "m": ["mailbox", "mailcap", "marshal", "math", "mimetypes", "mmap", 237 | "modulefinder", "multiprocessing"], 238 | "n": ["netrc", "nis", "nntplib", "numbers"], 239 | "t": ["tkinter"] 240 | } 241 | 242 | for sublist in modules.values(): 243 | for module in sublist: 244 | import_(module) 245 | 246 | 247 | def get_python_config(self, python, setup=None): 248 | """Get the config loaded by the given Python instance 249 | """ 250 | 251 | cfg_file = "cfg.json" 252 | if os.path.exists(cfg_file): 253 | os.remove(cfg_file) 254 | 255 | script = """\ 256 | import json 257 | import os 258 | import sys 259 | 260 | with open("{:}", "w+") as f: 261 | json.dump({{"path": sys.path, "executable": sys.executable, 262 | "prefix": sys.prefix, "user": os.getenv("USER"), 263 | "home": os.getenv("HOME"), "version": tuple(sys.version_info), 264 | "appdir": os.getenv("APPDIR")}}, f) 265 | """.format(cfg_file) 266 | 267 | script_file = "script.py" 268 | with open(script_file, "w") as f: 269 | f.write(script) 270 | 271 | if setup: 272 | system("/bin/bash -c '. {:}; {:} {:}'".format( 273 | setup, python, script_file)) 274 | else: 275 | system("{:} {:}".format(python, script_file)) 276 | 277 | with open(cfg_file) as f: 278 | return json.load(f) 279 | 280 | 281 | if __name__ == "__main__": 282 | unittest.main(failfast=True) 283 | --------------------------------------------------------------------------------