├── LICENSE ├── README.rst └── multipy /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2014 Petri Lehtinen 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.rst: -------------------------------------------------------------------------------- 1 | *************************************************** 2 | multipy -- Install multiple Python versions locally 3 | *************************************************** 4 | 5 | *multipy* is a shell utility that helps you install and manage 6 | multiple local Python installations. It downloads source tarballs for 7 | the newest version of any Python X.Y, compiles the source, and 8 | installs everything under a single directory hierarchy. By default, 9 | the install location is ``~/multipy``. 10 | 11 | setuptools_ is also installed along with each Python version, as well 12 | as an activate script in the spirit of virtualenv_, for easier shell 13 | integration. 14 | 15 | *multipy* is a single shell script. It requires a POSIX compliant 16 | shell, ``wget``, ``tar`` and ``gzip`` to download and extract source 17 | tarballs, and a compiler, development headers and libraries to compile 18 | Python. No existing Python installation is required. *multipy* should 19 | work on any Unix-like system that Python can be compiled on. 20 | 21 | Python versions 2.4 and up can be installed (including all 3.x 22 | releases). 23 | 24 | 25 | Usage 26 | ===== 27 | 28 | Install Python 2.7.6 and 3.3.4:: 29 | 30 | $ multipy install 2.7.6 3.3.4 31 | 32 | Install the latest release of Python 2.6: 33 | 34 | $ multipy install 2.6 35 | 36 | Install all latest releases of all supported Python versions (2.4 and 37 | up):: 38 | 39 | $ multipy install all 40 | 41 | List installed Python versions:: 42 | 43 | $ multipy list 44 | 45 | Remove Python 2.7.6:: 46 | 47 | $ multipy remove 2.7.6 48 | 49 | Use a custom installation directory:: 50 | 51 | $ multipy -b /path/to/somewhere install 3.2 52 | 53 | Tweak ``PATH`` to "activate" the local Python 2.5.6:: 54 | 55 | $ . $(multipy activate 2.5.6) 56 | 57 | After this, e.g. ``python`` and ``easy_install`` can be used without 58 | an absolute path. To leave this mode, use ``deactivate``. 59 | 60 | Show the directory where Python 3.1.5 has been installed:: 61 | 62 | $ multipy path 3.1.5 63 | /home/you/multipy/pythons/3.1.5 64 | 65 | Show help:: 66 | 67 | $ multipy -h 68 | 69 | Here's a list of supported command line options:: 70 | 71 | -b BASEDIR The base directory [default: ~/multipy] 72 | -k Keep temporary files and logs after installation 73 | -n Don't install setuptools 74 | -j N Compile with N jobs in parallel [default: number of CPUs] 75 | 76 | Upon startup, multipy tries to source ``~/.multipyrc`` and 77 | ``~/.config/multipyrc``. The following variables can be assigned in 78 | these files:: 79 | 80 | Variable: Command-line option: 81 | 82 | basedir=BASEDIR -b BASEDIR 83 | keep_tmp=1 -k 84 | no_setuptools=1 -n 85 | no_distribute=1 -n (for backwards compatibility) 86 | jobs=2 -j 2 87 | 88 | 89 | Under the hood 90 | ============== 91 | 92 | By default, the top directory of the multipy is 93 | ``basedir=$HOME/multipy``. This can be changed with the ``-b`` option 94 | or in the config files discussed in the last section. 95 | 96 | When Python X.Y[.Z] is installed, the following things happen: 97 | 98 | * If Z is not specified the newest released X.Y is figured out. For 99 | example, when writing this, the newest version of Python 2.7 is 100 | 2.7.6, so Z is set to 6. 101 | 102 | * The source tarball of Python X.Y.Z is downloaded to 103 | ``$basedir/sources``. 104 | 105 | * The source is then extracted to a temporary directory under 106 | ``$basedir/tmp`` and compiled. The result is installed to 107 | ``$basedir/pythons/X.Y.Z/``. This is the standard ``configure``, 108 | ``make``, ``make install`` procedure. 109 | 110 | * The newest release of setuptools_ is downloaded to 111 | ``$basedir/sources`` (if not already there). It's extracted to a 112 | directory under ``$basedir/tmp`` and ``python setup.py install`` is 113 | run with the Python version that was installed in the previous step. 114 | (This step can be skipped using the ``-n`` option.) 115 | 116 | * An ``activate`` script is installed to the ``bin/`` directory of the 117 | Python installation. 118 | 119 | * Finally, ``$basedir/tmp`` is removed (this can be disabled with the 120 | ``-k`` option). 121 | 122 | If anything goes wrong, ``$basedir/tmp`` is left in place, and logs of 123 | each step are available as ``$basedir/tmp/*.log``. 124 | 125 | The source tarballs are left in the ``$basedir/sources`` directory for 126 | future use, but you can safely remove them if you want to free up some 127 | disk space. 128 | 129 | 130 | Copyright 131 | ========= 132 | 133 | Copyright (C) 2011-2014 Petri Lehtinen. Licensed under the MIT license. 134 | 135 | 136 | .. _setuptools: http://pypi.python.org/pypi/setuptools 137 | .. _virtualenv: http://pypi.python.org/pypi/virtualenv 138 | 139 | 140 | -------------------------------------------------------------------------------- /multipy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # "multipy" is free software; you can redistribute it and/or modify 4 | # it under the terms of the MIT license. See LICENSE for details. 5 | # 6 | 7 | PYTHON_VERSIONS="2.4 2.5 2.6 2.7 3.0 3.1 3.2 3.3 3.4" 8 | PYTHON_URL_TEMPLATE="http://www.python.org/ftp/python/%s/Python-%s.tgz" 9 | SETUPTOOLS_BASEURL="http://pypi.python.org/packages/source/s/setuptools/" 10 | 11 | # From virtualenv, with slight modifications 12 | ACTIVATE_SCRIPT=$(cat <&2 75 | } 76 | 77 | die() { 78 | [ -n "$1" ] && error "$1" 79 | [ -n "$2" ] && echo "For logs, see $tempdir/*.log" >&2 80 | exit 1 81 | } 82 | 83 | # Usage: append VAR STRING 84 | append() { 85 | eval local tmp=\$$1 86 | if [ -n "$tmp" ]; then 87 | tmp="$tmp $2" 88 | else 89 | tmp=$2 90 | fi 91 | eval $1=\$tmp 92 | } 93 | 94 | # http://rubinium.org/blog/archives/2010/04/05/shell-script-version-compare-vercmp/ 95 | version_cmp() { 96 | expr '(' "$1" : '\([^.]*\)' ')' '-' '(' "$2" : '\([^.]*\)' ')' '|' \ 97 | '(' "$1.0" : '[^.]*[.]\([^.]*\)' ')' '-' '(' "$2.0" : '[^.]*[.]\([^.]*\)' ')' '|' \ 98 | '(' "$1.0.0" : '[^.]*[.][^.]*[.]\([^.]*\)' ')' '-' '(' "$2.0.0" : '[^.]*[.][^.]*[.]\([^.]*\)' ')' '|' \ 99 | '(' "$1.0.0.0" : '[^.]*[.][^.]*[.][^.]*[.]\([^.]*\)' ')' '-' '(' "$2.0.0.0" : '[^.]*[.][^.]*[.][^.]*[.]\([^.]*\)' ')' 100 | } 101 | 102 | # X.Y 103 | is_minor_version() { 104 | echo $1 | grep -E -q '^[0-9]+.[0-9]+$' 105 | } 106 | 107 | # X.Y.Z 108 | is_micro_version() { 109 | echo $1 | grep -E -q '^[0-9]+.[0-9]+.[0-9]+$' 110 | } 111 | 112 | get_minor_version() { 113 | echo "$1" | sed -r -e 's/^([0-9]+\.[0-9]+)(\.[0-9]+)?$/\1/' 114 | } 115 | 116 | is_supported_version() { 117 | local minor_version v 118 | minor_version=$(get_minor_version $1) 119 | for v in $PYTHON_VERSIONS; do 120 | [ "$minor_version" = "$v" ] && return 121 | done 122 | 123 | # Not found 124 | return 1 125 | } 126 | 127 | # Assigns variables: versions 128 | get_version_args() { 129 | local version invalid 130 | 131 | if [ -z "$1" ]; then 132 | error "No Python versions specified!" 133 | return 1 134 | fi 135 | 136 | if [ "$*" = "all" ]; then 137 | versions=$PYTHON_VERSIONS 138 | else 139 | versions=$* 140 | 141 | for version in $versions; do 142 | is_supported_version $version || append invalid "$version" 143 | done 144 | 145 | if [ -n "$invalid" ]; then 146 | error "Invalid/unsupported Python version(s): $invalid" 147 | return 1 148 | fi 149 | fi 150 | } 151 | 152 | # Assigns variables: version 153 | get_version_arg() { 154 | local v 155 | 156 | if [ -z "$1" ]; then 157 | error "No Python version specified!" 158 | return 1 159 | fi 160 | 161 | for v in $*; do 162 | if [ -n "$version" ]; then 163 | error "Single version expected" 164 | return 1 165 | fi 166 | if ! is_supported_version $v; then 167 | error "Invalid/unsupported Python version: $v" 168 | return 1 169 | fi 170 | version=$v 171 | done 172 | } 173 | 174 | try_cmd() { 175 | local result 176 | 177 | result=$(eval "$1" 2>/dev/null) 178 | if [ $? -eq 0 -a -n "$result" ]; then 179 | echo $result 180 | else 181 | return 1 182 | fi 183 | } 184 | 185 | # Prints the number of CPUs, or 1 if unable to find out 186 | cpu_count() { 187 | try_cmd "nproc" && return 188 | try_cmd "[ -e /proc/cpuinfo ] && grep ^processor /proc/cpuinfo | wc -l" && return 189 | try_cmd "sysctl -n hw.ncpu" && return 190 | echo 1 191 | } 192 | 193 | is_installed() { 194 | [ -e $pythondir/$1 ] 195 | } 196 | 197 | # Assigns variables: latest_python_version 198 | find_latest_python() { 199 | echo "Checking for latest version of Python $1..." 200 | for micro in $(seq 9 -1 -1); do 201 | [ "$micro" != "-1" ] && v=$1.$micro || v=$1 202 | url=$(printf "$PYTHON_URL_TEMPLATE" $v $v) 203 | 204 | if wget -q --spider "$url"; then 205 | echo "Found latest version: $v" 206 | latest_python_version=$v 207 | return 0 208 | fi 209 | done 210 | 211 | return 1 212 | } 213 | 214 | # Assigns variables: tarball 215 | download_python() { 216 | local url 217 | mkdir -p $sourcedir 218 | 219 | url=$(printf "$PYTHON_URL_TEMPLATE" $1 $1) 220 | tarball=$sourcedir/Python-$1.tgz 221 | if [ -e $tarball ]; then 222 | echo " Python-$1.tgz already downloaded" 223 | else 224 | echo " Downloading..." 225 | wget -q -P $sourcedir "$url" || return 1 226 | fi 227 | } 228 | 229 | # Assigns variables: latest_setuptools 230 | install_setuptools() { 231 | local pyversion=$1 pyminor 232 | local version versions tarball srcdir python 233 | 234 | pyminor=$(get_minor_version $pyversion) 235 | 236 | # Setuptools doesn't support 2.4 or 2.5 237 | if [ "$pyminor" = "2.4" -o "$pyminor" = "2.5" ]; then 238 | echo " Setuptools does not support Python $pyminor, skip" 239 | return 240 | fi 241 | 242 | if [ -z "$latest_setuptools" ]; then 243 | echo " Checking for latest version of setuptools..." 244 | 245 | wget -q -O $tempdir/index.html $SETUPTOOLS_BASEURL || return 1 246 | 247 | versions=$(grep -E '>setuptools-[0-9.]+.tar.gz' $tempdir/index.html | sed -r -e 's%.*>setuptools-([0-9.]+).tar.gz.*%\1%') 248 | 249 | latest_setuptools=0 250 | for version in $versions; do 251 | if [ "$(version_cmp $version $latest_setuptools)" -gt 0 ]; then 252 | latest_setuptools=$version 253 | fi 254 | done 255 | 256 | echo " Found latest version: $latest_setuptools" 257 | 258 | tarball=setuptools-$latest_setuptools.tar.gz 259 | if [ ! -e "$tarball" ]; then 260 | echo " Downloading setuptools..." 261 | wget -q -P $sourcedir "$SETUPTOOLS_BASEURL/$tarball" || return 1 262 | else 263 | echo " $tarball already downloaded" 264 | fi 265 | 266 | echo " Unpacking setuptools..." 267 | rm -rf $tempdir/$tarball 268 | gunzip -c $sourcedir/$tarball | tar -C $tempdir -xf - || return 1 269 | fi 270 | 271 | echo " Installing setuptools..." 272 | srcdir=$tempdir/setuptools-$latest_setuptools 273 | python=$pythondir/$pyversion/bin/python$pyminor 274 | (cd $srcdir && $python setup.py install) \ 275 | > $tempdir/install-setuptools.$pyversion.log 2>&1 \ 276 | || return 1 277 | } 278 | 279 | 280 | ### commands ### 281 | 282 | install() { 283 | local versions version latest_python_version installed=0 284 | get_version_args "$@" || die # -> versions 285 | 286 | mkdir -p $tempdir 287 | 288 | for version in $versions; do 289 | if is_minor_version "$version"; then 290 | if find_latest_python "$version"; then # -> latest_python_version 291 | version=$latest_python_version 292 | else 293 | die "Nothing found" 294 | fi 295 | fi 296 | 297 | if is_installed $version; then 298 | echo "Python $version already installed, skipping" 299 | continue 300 | fi 301 | 302 | echo "Installing Python $version..." 303 | 304 | download_python $version || die "Unable to download Python $version" 305 | srcdir=$tempdir/${tarball##*/} 306 | srcdir=${srcdir%.tgz} 307 | 308 | echo " Unpacking..." 309 | rm -rf $srcdir 310 | gunzip -c $tarball | tar -C $tempdir -xf - || die "Unpack failed" 311 | 312 | echo " Configuring..." 313 | (cd $srcdir && ./configure --prefix=$pythondir/$version) \ 314 | >$tempdir/configure.$version.log 2>&1 \ 315 | || die "Configure failed" 1 316 | 317 | echo " Compiling..." 318 | (cd $srcdir && make -j "$jobs") \ 319 | >$tempdir/compile.$version.log 2>&1 \ 320 | || die "Compile failed" 1 321 | 322 | sed -n '/necessary bits/,/detect_modules/p' \ 323 | < $tempdir/compile.$version.log 324 | 325 | echo " Installing..." 326 | (cd $srcdir && make install) \ 327 | >$tempdir/install.$version.log 2>&1 \ 328 | || die "Install failed" 1 329 | 330 | if [ $no_setuptools -eq 0 ]; then 331 | install_setuptools $version || die "Unable to install setuptools" 1 332 | fi 333 | 334 | # Write the activate script 335 | printf "$ACTIVATE_SCRIPT" "$pythondir/$version" \ 336 | >$pythondir/$version/bin/activate 337 | 338 | installed=1 339 | done 340 | 341 | if [ $installed -eq 1 ]; then 342 | # Remove temporary files (disable with -k) 343 | if [ $keep_tmp -eq 0 ]; then 344 | echo "Cleaning up..." 345 | rm -rf $tempdir/* 346 | fi 347 | echo "All done." 348 | fi 349 | } 350 | 351 | remove() { 352 | local version 353 | for version in $(list); do 354 | if [ "$1" = "all" -o "$1" = "$version" ]; then 355 | echo "Removing Python $version..." 356 | rm -rf $pythondir/$version 357 | fi 358 | done 359 | 360 | echo "All done." 361 | } 362 | 363 | list() { 364 | local version 365 | for version in $pythondir/*; do 366 | version=$(basename $version) 367 | is_micro_version $version && echo $version 368 | 369 | # Backwards compatibility 370 | is_minor_version $version && echo $version 371 | done 372 | 373 | return 0 374 | } 375 | 376 | path() { 377 | local version 378 | get_version_arg "$@" || die # -> version 379 | 380 | is_installed $version || die "Python $version is not installed" 381 | echo $pythondir/$version 382 | } 383 | 384 | activate() { 385 | local version 386 | get_version_arg "$@" || die # -> version 387 | 388 | is_installed $version || die "Python $version is not installed" 389 | echo $pythondir/$version/bin/activate 390 | } 391 | 392 | ### main ### 393 | 394 | usage() { 395 | cat >&2 <&2 477 | usage 478 | ;; 479 | esac 480 | done 481 | 482 | [ -z "$jobs" ] && jobs=$jobs_default 483 | 484 | # Make basedir absolute 485 | [ "${basedir#/}" = "$basedir" ] && basedir=$(pwd)/$basedir 486 | 487 | pythondir=$basedir/pythons 488 | sourcedir=$basedir/sources 489 | tempdir=$basedir/tmp 490 | 491 | # No more arguments 492 | [ "$opt" = "--" ] && usage 493 | 494 | shift $(expr $OPTIND - 1) 495 | 496 | cmd=$1 497 | [ -z "$1" ] && usage 498 | shift 499 | 500 | case $cmd in 501 | install|remove|list|path|activate) 502 | ;; 503 | *) 504 | usage 505 | ;; 506 | esac 507 | 508 | $cmd "$@" 509 | --------------------------------------------------------------------------------