1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978
|
# -*- coding: utf-8 -*-
# This program is free software; you can redistribute it and/or modify it under
# the terms of the (LGPL) GNU Lesser General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# written by: Jurko Gospodnetić ( jurko.gospodnetic@pke.hr )
"""
Sets up base Python development environments used by this project.
These are the Python environments from which multiple virtual environments can
then be spawned as needed.
The environments should have the following Python packages installed:
* setuptools (for installing pip)
* pip (for installing everything except itself)
* pytest (for running the project's test suite)
* virtualenv (for creating virtual Python environments)
plus certain specific Python versions may require additional backward
compatibility support packages.
"""
#TODO: Python 3.4.0 comes with setuptools & pip preinstalled but they can be
# installed and/or upgraded manually if needed so we choose not to use their
# built-in installations, i.e. the built-in ensurepip module. Consider using
# the built-in ensurepip module if regular setuptools/pip installation fails
# for some reason or has been configured to run locally.
#TODO: logging
#TODO: command-line option support
#TODO: support for additional configuration files, e.g. ones that are developer
# or development environment specific.
#TODO: warn if multiple environments use the same executable
#TODO: make the script importable
#TODO: report when the installed package version is newer than the last tested
# one
#TODO: hold a list of last tested package versions and report a warning if a
# newer one is encountered
# > # Where no Python package version has been explicitly specified, the
# > # following 'currently latest available' package release has been
# > # successfully used:
# > "last tested package version": {
# > "argparse": "1.2.1",
# > "backports.ssl_match_hostname": "3.4.0.2",
# > "colorama": "0.3.1",
# > "pip": "1.5.5",
# > "py": "1.4.20",
# > "pytest": "2.5.2",
# > "setuptools": "3.6",
# > "virtualenv": "1.11.5"},
#TODO: automated checking for new used package versions, e.g. using PyPI XRC
# API:
# > import xmlrpc.client as xrc
# > client = xrc.ServerProxy("http://pypi.python.org/pypi")
# > client.package_releases("six") # just the latest release
# > client.package_releases("six", True) # all releases
#TODO: option to break on bad environments
#TODO: verbose option to report bad environment detection output
#TODO: verbose option to report all environment detection output
#TODO: verbose option to report environment details
#TODO: Consider running the environment scanner script from an empty temporary
# folder to avoid importing random same-named modules from the current working
# folder. An alternative would be to play around with sys.path in the scanner
# script, e.g. remove its first element. Also, we might want to clear out any
# globally set Python environment variables such as PYTHONPATH.
#TODO: collect stdout, stderr & log outputs for each easy_install/pip run
#TODO: configurable - avoid downloads if a suitable locally downloaded source
# is available (ez_setup, setuptools, pip)
#TODO: 244 downloads must come before 243 installation
#TODO: support configuring what latest suitable version we want to use if
# available in the following layers:
# - already installed
# - local installation cache (can not find out the latest available content,
# can only download to it or install from it)
# - pypi
# related configuration options:
# - allow already installed,
# - allow locally downloaded,
# - allow pypi
#TODO: if you want better local-cache support - use devpi:
# - better caching support and version detection
# - devpi & pypi usage transparent
# - we'll need a script for installing & setting up the devpi server
#TODO: parallelization
#TODO: concurrency support - file locking required for:
# installation cache folder:
# downloading (write)
# zipping eggs (write)
# installing from local folder (read)
# Python environment installation area:
# installing new packages (write)
# running Python code (read)
#TODO: test whether we can upgrade pip in-place
#TODO: test how we can make pip safe to use when there are multiple pip based
# installations being run at the same time by the same user - specify some
# global folders, like the temp build folder, explicitly
#TODO: Detect most recent packages on PyPI, but do that at most once in a
# single script run, or with a separate script, or use devpi. Currently, if a
# suitable package is found locally, a more suitable one will not be checked
# for on PyPI.
#TODO: Recheck error handling to make sure all failed commands are correctly
# detected. Some might not set a non-0 exit code on error and so their output
# must be used as a success/failure indicator.
import itertools
import os
import os.path
import re
import sys
import tempfile
from suds_devel.configuration import BadConfiguration, Config, configparser
from suds_devel.egg import zip_eggs_in_folder
from suds_devel.environment import BadEnvironment, Environment
from suds_devel.exception import EnvironmentSetupError
from suds_devel.parse_version import parse_version
from suds_devel.requirements import pytest_requirements, virtualenv_requirements
import suds_devel.utility as utility
# -------------
# Configuration
# -------------
class MyConfig(Config):
# Section names.
SECTION__ACTIONS = "setup base environments - actions"
SECTION__FOLDERS = "setup base environments - folders"
SECTION__REUSE_PREINSTALLED_SETUPTOOLS = \
"setup base environments - reuse pre-installed setuptools"
def __init__(self, script, project_folder, ini_file):
"""
Initialize new script configuration.
External configuration parameters may be specified relative to the
following folders:
* script - relative to the current working folder
* project_folder - relative to the script folder
* ini_file - relative to the project folder
"""
super(MyConfig, self).__init__(script, project_folder, ini_file)
self.__cached_paths = {}
try:
self.__read_configuration()
except configparser.Error:
raise BadConfiguration(sys.exc_info()[1].message)
def ez_setup_folder(self):
return self.__get_cached_path("ez_setup folder")
def installation_cache_folder(self):
return self.__get_cached_path("installation cache")
def pip_download_cache_folder(self):
return self.__get_cached_path("pip download cache")
def __get_cached_path(self, option):
try:
return self.__cached_paths[option]
except KeyError:
x = self.__cached_paths[option] = self.__get_path(option)
return x
def __get_path(self, option):
try:
folder = self._reader.get(self.SECTION__FOLDERS, option)
except (configparser.NoOptionError, configparser.NoSectionError):
return
except configparser.Error:
raise BadConfiguration("Error reading configuration option "
"'%s.%s' - %s" % (self.SECTION__FOLDERS, option,
sys.exc_info()[1]))
base_paths = {
"project-folder": self.project_folder,
"script-folder": self.script_folder,
"ini-folder": os.path.dirname(self.ini_file)}
folder_parts = re.split("[\\/]{2}", folder, maxsplit=1)
base_path = None
if len(folder_parts) == 2:
base_path = base_paths.get(folder_parts[0].lower())
if base_path is not None:
folder = folder_parts[1]
if not folder:
raise BadConfiguration("Configuration option '%s.%s' invalid. A "
"valid relative path must not be empty. Use '.' to represent "
"the base folder." % (section, option))
if base_path is None:
base_path = base_paths.get("ini-folder")
return os.path.normpath(os.path.join(base_path, folder))
def __read_configuration(self):
self._read_environment_configuration()
section = self.SECTION__REUSE_PREINSTALLED_SETUPTOOLS
self.reuse_old_setuptools = self._get_bool(section, "old")
self.reuse_best_setuptools = self._get_bool(section, "best")
self.reuse_future_setuptools = self._get_bool(section, "future")
section = self.SECTION__ACTIONS
self.report_environment_configuration = (
self._get_bool(section, "report environment configuration"))
self.report_raw_environment_scan_results = (
self._get_bool(section, "report raw environment scan results"))
self.setup_setuptools = (
self._get_tribool(section, "setup setuptools"))
self.download_installations = (
self._get_tribool(section, "download installations"))
self.install_environments = (
self._get_tribool(section, "install environments"))
def _prepare_configuration():
# We know we are a regular stand-alone script file and not an imported
# module (either frozen, imported from disk, zip-file, external database or
# any other source). That means we can safely assume we have the __file__
# attribute available.
global config
config = MyConfig(__file__, "..", "setup.cfg")
# --------------------
# Environment scanning
# --------------------
def report_environment_configuration(env):
if not (env and env.initial_scan_completed):
return
print(" ctypes version: %s" % (env.ctypes_version,))
print(" pip version: %s" % (env.pip_version,))
print(" pytest version: %s" % (env.pytest_version,))
print(" python version: %s" % (env.python_version,))
print(" setuptools version: %s" % (env.setuptools_version,))
if env.setuptools_zipped_egg is not None:
print(" setuptools zipped egg: %s" % (env.setuptools_zipped_egg,))
print(" virtualenv version: %s" % (env.virtualenv_version,))
def report_raw_environment_scan_results(out, err, exit_code):
if out is None and err is None and exit_code is None:
return
print("-----------------------------------")
print("--- RAW SCAN RESULTS --------------")
print("-----------------------------------")
if exit_code is not None:
print("*** EXIT CODE: %d" % (exit_code,))
for name, value in (("STDOUT", out), ("STDERR", err)):
if value:
print("*** %s:" % (name,))
sys.stdout.write(value)
if value[-1] != "\n":
sys.stdout.write("\n")
print("-----------------------------------")
class ScanProgressReporter:
"""
Reports scanning progress to the user.
Takes care of all the gory progress output formatting details so they do
not pollute the actual scanning logic implementation.
A ScanProgressReporter's output formatting logic assumes that the reporter
is the one with full output control between calls to a its report_start() &
report_finish() methods. Therefore, user code must not do any custom output
during that time or it risks messing up the reporter's output formatting.
"""
def __init__(self, environments):
self.__max_name_length = max(len(x.name()) for x in environments)
self.__count = len(environments)
self.__count_width = len(str(self.__count))
self.__current = 0
self.__reporting = False
print("Scanning Python environments...")
def report_start(self, name):
assert len(name) <= self.__max_name_length
assert self.__current <= self.__count
assert not self.__reporting
self.__reporting = True
self.__current += 1
name_padding = " " * (self.__max_name_length - len(name))
sys.stdout.write("[%*d/%d] Scanning '%s'%s - " % (self.__count_width,
self.__current, self.__count, name, name_padding))
sys.stdout.flush()
def report_finish(self, report):
assert self.__reporting
self.__reporting = False
print(report)
class ScannedEnvironmentTracker:
"""Helps track scanned Python environments and report duplicates."""
def __init__(self):
self.__names = set()
self.__last_name = None
self.__environments = []
def environments(self):
return self.__environments
def track_environment(self, env):
assert env not in self.__environments
assert env.name() == self.__last_name
self.__environments.append(env)
def track_name(self, name):
if name in self.__names:
raise BadConfiguration("Python environment '%s' configured "
"multiple times." % (name,))
self.__names.add(name)
self.__last_name = name
def scan_python_environment(env, progress_reporter, environment_tracker):
environment_tracker.track_name(env.name())
# N.B. No custom output allowed between calls to our progress_reporter's
# report_start() & report_finish() methods or we risk messing up its output
# formatting.
progress_reporter.report_start(env.name())
try:
try:
out, err, exit_code = env.run_initial_scan()
except:
progress_reporter.report_finish("----- %s" % (_exc_str(),))
raise
except BadEnvironment:
out, err, exit_code = sys.exc_info()[1].raw_scan_results()
else:
progress_reporter.report_finish(env.description())
environment_tracker.track_environment(env)
if config.report_raw_environment_scan_results:
report_raw_environment_scan_results(out, err, exit_code)
if config.report_environment_configuration:
report_environment_configuration(env)
def scan_python_environments():
environments = config.python_environments
if not environments:
raise BadConfiguration("No Python environments configured.")
progress_reporter = ScanProgressReporter(environments)
environment_tracker = ScannedEnvironmentTracker()
for env in environments:
scan_python_environment(env, progress_reporter, environment_tracker)
return environment_tracker.environments()
# ------------------------------------------
# Generic functionality local to this module
# ------------------------------------------
def _create_installation_cache_folder_if_needed():
assert config.installation_cache_folder() is not None
if not os.path.isdir(config.installation_cache_folder()):
print("Creating installation cache folder...")
# os.path.abspath() to avoid ".." entries in the path that would
# otherwise confuse os.makedirs().
os.makedirs(os.path.abspath(config.installation_cache_folder()))
def _exc_str():
exc_type, exc = sys.exc_info()[:2]
type_desc = []
if exc_type.__module__ and exc_type.__module__ != "__main__":
type_desc.append(exc_type.__module__)
type_desc.append(exc_type.__name__)
desc = ".".join(type_desc), str(exc)
return ": ".join(x for x in desc if x)
def _report_configuration():
folder = config.installation_cache_folder()
if folder is not None:
print("Installation cache folder: '%s'" % (folder,))
folder = config.pip_download_cache_folder()
if folder is not None:
print("PIP download cache folder: '%s'" % (folder,))
def _report_startup_information():
print("Running in folder: '%s'" % (os.getcwd(),))
# ----------------------------------
# Processing setuptools installation
# ----------------------------------
def process_setuptools(env, actions):
if "setup setuptools" not in actions:
return
installer = _ez_setup_script(env)
if _reuse_pre_installed_setuptools(env, installer):
return
_avoid_setuptools_zipped_egg_upgrade_issue(env, installer)
try:
# 'ez_setup' script will download its setuptools installation to the
# 'current working folder'. If we are using an installation cache
# folder, we run the script from there to get the downloaded setuptools
# installation stored together with all of the other used
# installations. If we are not, then just have it downloaded to the
# current folder.
if config.installation_cache_folder() is not None:
_create_installation_cache_folder_if_needed()
installer.execute(cwd=config.installation_cache_folder())
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
raise EnvironmentSetupError("setuptools installation failed - %s" % (
_exc_str(),))
class _ez_setup_script:
"""setuptools project's ez_setup installer script."""
def __init__(self, env):
self.__env = env
if not config.ez_setup_folder():
self.__error("ez_setup folder not configured")
self.__ez_setup_folder = config.ez_setup_folder()
self.__cached_script_path = None
self.__cached_setuptools_version = None
if not os.path.isfile(self.script_path()):
self.__error("installation script '%s' not found" % (
self.script_path(),))
def execute(self, cwd=None):
script_path = self.script_path()
kwargs = {}
if cwd:
kwargs["cwd"] = cwd
script_path = os.path.abspath(script_path)
self.__env.execute([script_path], **kwargs)
def script_path(self):
if self.__cached_script_path is None:
self.__cached_script_path = self.__script_path()
return self.__cached_script_path
def setuptools_version(self):
if self.__cached_setuptools_version is None:
self.__cached_setuptools_version = self.__setuptools_version()
return self.__cached_setuptools_version
def __error(self, msg):
raise EnvironmentSetupError("Can not install setuptools - %s." % (
msg,))
def __script_path(self):
import suds_devel.ez_setup_versioned as ez
script_name = ez.script_name(self.__env.sys_version_info)
return os.path.join(self.__ez_setup_folder, script_name)
def __setuptools_version(self):
"""Read setuptools version from the underlying ez_setup script."""
# Read the script directly as a file instead of importing it as a
# Python module and reading the value from the loaded module's global
# DEFAULT_VERSION variable. Not all ez_setup scripts are compatible
# with all Python environments and so importing them would require
# doing so using a separate process run in the target Python
# environment instead of the current one.
f = open(self.script_path(), "r")
try:
matcher = re.compile(r'\s*DEFAULT_VERSION\s*=\s*"([^"]*)"\s*$')
for i, line in enumerate(f):
if i > 50:
break
match = matcher.match(line)
if match:
return match.group(1)
finally:
f.close()
self.__error("error parsing setuptools installation script '%s'" % (
self.script_path(),))
def _avoid_setuptools_zipped_egg_upgrade_issue(env, ez_setup):
"""
Avoid the setuptools self-upgrade issue.
setuptools versions prior to version 3.5.2 have a bug that can cause their
upgrade installations to fail when installing a new zipped egg distribution
over an existing zipped egg setuptools distribution with the same name.
The following Python versions are not affected by this issue:
Python 2.4 - use setuptools 1.4.2 - installs itself as a non-zipped egg
Python 2.6+ - use setuptools versions not affected by this issue
That just leaves Python versions 2.5.x to worry about.
This problem occurs because of an internal stale cache issue causing the
upgrade to read data from the new zip archive at a location calculated
based on the original zip archive's content, effectively causing such read
operations to either succeed (if read content had not changed its
location), fail with a 'bad local header' exception or even fail silently
and return incorrect data.
To avoid the issue, we explicitly uninstall the previously installed
setuptools distribution before installing its new version.
"""
if env.sys_version_info[:2] != (2, 5):
return # only Python 2.5.x affected by this
if not env.setuptools_zipped_egg:
return # setuptools not pre-installed as a zipped egg
pv_new = parse_version(ez_setup.setuptools_version())
if pv_new != parse_version(env.setuptools_version):
return # issue avoided since zipped egg archive names will not match
fixed_version = utility.lowest_version_string_with_prefix("3.5.2")
if pv_new >= parse_version(fixed_version):
return # issue fixed in setuptools
# We could check for pip and use it for a cleaner setuptools uninstall if
# available, but YAGNI since only Python 2.5.x environments are affected by
# the zipped egg upgrade issue.
os.remove(env.setuptools_zipped_egg)
def _reuse_pre_installed_setuptools(env, installer):
"""
Return whether a pre-installed setuptools distribution should be reused.
"""
if not env.setuptools_version:
return # no prior setuptools ==> no reuse
reuse_old = config.reuse_old_setuptools
reuse_best = config.reuse_best_setuptools
reuse_future = config.reuse_future_setuptools
reuse_comment = None
if reuse_old or reuse_best or reuse_future:
pv_old = parse_version(env.setuptools_version)
pv_new = parse_version(installer.setuptools_version())
if pv_old < pv_new:
if reuse_old:
reuse_comment = "%s+ recommended" % (
installer.setuptools_version(),)
elif pv_old > pv_new:
if reuse_future:
reuse_comment = "%s+ required" % (
installer.setuptools_version(),)
elif reuse_best:
reuse_comment = ""
if reuse_comment is None:
return # reuse not allowed by configuration
if reuse_comment:
reuse_comment = " (%s)" % (reuse_comment,)
print("Reusing pre-installed setuptools %s distribution%s." % (
env.setuptools_version, reuse_comment))
return True # reusing pre-installed setuptools
# ---------------------------
# Processing pip installation
# ---------------------------
def calculate_pip_requirements(env_version_info):
# pip releases supported on older Python versions:
# * Python 2.4.x - pip 1.1.
# * Python 2.5.x - pip 1.3.1.
pip_version = None
if env_version_info < (2, 5):
pip_version = "1.1"
elif env_version_info < (2, 6):
pip_version = "1.3.1"
requirement_spec = utility.requirement_spec
requirements = [requirement_spec("pip", pip_version)]
# Although pip claims to be compatible with Python 3.0 & 3.1 it does not
# seem to work correctly from within such clean Python environments.
# * Tested using pip 1.5.4 & Python 3.1.3.
# * pip can be installed using Python 3.1.3 ('py313 -m easy_install pip')
# but attempting to use it or even just import its pip Python module
# fails.
# * The problem is caused by a bug in pip's backward compatibility
# support implementation, but can be worked around by installing the
# backports.ssl_match_hostname package from PyPI.
if (3,) <= env_version_info < (3, 2):
requirements.append(requirement_spec("backports.ssl_match_hostname"))
return requirements
def download_pip(env, requirements):
"""Download pip and its requirements using setuptools."""
if config.installation_cache_folder() is None:
raise EnvironmentSetupError("Local installation cache folder not "
"defined but required for downloading a pip installation.")
# Installation cache folder needs to be explicitly created for setuptools
# to be able to copy its downloaded installation files into it. Seen using
# Python 2.4.4 & setuptools 1.4.
_create_installation_cache_folder_if_needed()
try:
env.execute(["-m", "easy_install", "--zip-ok", "--multi-version",
"--always-copy", "--exclude-scripts", "--install-dir",
config.installation_cache_folder()] + requirements)
zip_eggs_in_folder(config.installation_cache_folder())
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
raise EnvironmentSetupError("pip download failed.")
def setuptools_install_options(local_storage_folder):
"""
Return options to make setuptools use installations from the given folder.
No other installation source is allowed.
"""
if local_storage_folder is None:
return []
# setuptools expects its find-links parameter to contain a list of link
# sources (either local paths, file: URLs pointing to folders or URLs
# pointing to a file containing HTML links) separated by spaces. That means
# that, when specifying such items, whether local paths or URLs, they must
# not contain spaces. The problem can be worked around by using a local
# file URL, since URLs can contain space characters encoded as '%20' (for
# more detailed information see below).
#
# Any URL referencing a folder needs to be specified with a trailing '/'
# character in order for setuptools to correctly recognize it as a folder.
#
# All this has been tested using Python 2.4.3/2.4.4 & setuptools 1.4/1.4.2
# as well as Python 3.4 & setuptools 3.3.
#
# Supporting paths with spaces - method 1:
# ----------------------------------------
# One way would be to prepare a link file and pass an URL referring to that
# link file. The link file needs to contain a list of HTML link tags
# (<a href="..."/>), one for every item stored inside the local storage
# folder. If a link file references a folder whose name matches the desired
# requirement name, it will be searched recursively (as described in method
# 2 below).
#
# Note that in order for setuptools to recognize a local link file URL
# correctly, the file needs to be named with the '.html' extension. That
# will cause the underlying urllib2.open() operation to return the link
# file's content type as 'text/html' which is required for setuptools to
# recognize a valid link file.
#
# Supporting paths with spaces - method 2:
# ----------------------------------------
# Another possible way is to use an URL referring to the local storage
# folder directly. This will cause setuptools to prepare and use a link
# file internally - with its content read from a 'index.html' file located
# in the given local storage folder, if it exists, or constructed so it
# contains HTML links to all top-level local storage folder items, as
# described for method 1 above.
if " " in local_storage_folder:
find_links_param = utility.path_to_URL(local_storage_folder)
if find_links_param[-1] != "/":
find_links_param += "/"
else:
find_links_param = local_storage_folder
return ["-f", find_links_param, "--allow-hosts=None"]
def install_pip(env, requirements):
"""Install pip and its requirements using setuptools."""
try:
installation_source_folder = config.installation_cache_folder()
options = setuptools_install_options(installation_source_folder)
if installation_source_folder is not None:
zip_eggs_in_folder(installation_source_folder)
env.execute(["-m", "easy_install"] + options + requirements)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
raise EnvironmentSetupError("pip installation failed.")
def process_pip(env, actions):
download = "download pip installation" in actions
install = "run pip installation" in actions
if not download and not install:
return
requirements = calculate_pip_requirements(env.sys_version_info)
if download:
download_pip(env, requirements)
if install:
install_pip(env, requirements)
# ----------------------------------
# Processing pip based installations
# ----------------------------------
def pip_invocation_arguments(env_version_info):
"""
Returns Python arguments for invoking pip with a specific Python version.
Running pip based installations on Python prior to 2.7.
* pip based installations may be run using:
python -c "import pip;pip.main()" install <package-name-to-install>
in addition to the regular command:
python -m pip install <package-name-to-install>
* The '-m' option can not be used with certain Python versions prior to
Python 2.7.
* Whether this is so also depends on the specific pip version used.
* Seems to not work with Python 2.4 and pip 1.1.
* Seems to work fine with Python 2.5.4 and pip 1.3.1.
* Seems to not work with Python 2.6.6 and pip 1.5.4.
"""
if (env_version_info < (2, 5)) or ((2, 6) <= env_version_info < (2, 7)):
return ["-c", "import pip;pip.main()"]
return ["-m", "pip"]
def pip_requirements_file(requirements):
janitor = None
try:
os_handle, file_path = tempfile.mkstemp(suffix=".pip-requirements",
text=True)
requirements_file = os.fdopen(os_handle, "w")
try:
janitor = utility.FileJanitor(file_path)
for line in requirements:
requirements_file.write(line)
requirements_file.write("\n")
finally:
requirements_file.close()
return file_path, janitor
except:
if janitor:
janitor.clean()
raise
def prepare_pip_requirements_file_if_needed(requirements):
"""
Make requirements be passed to pip via a requirements file if needed.
We must be careful about how we pass shell operator characters (e.g. '<',
'>', '|' or '^') included in our command-line arguments or they might cause
problems if run through an intermediate shell interpreter. If our pip
requirement specifications contain such characters, we pass them using a
separate requirements file.
This problem has been encountered on Windows 7 SP1 x64 using Python 2.4.3,
2.4.4 & 2.5.4.
"""
if utility.any_contains_any(requirements, "<>|()&^"):
file_path, janitor = pip_requirements_file(requirements)
requirements[:] = ["-r", file_path]
return janitor
def prepare_pip_requirements(env):
requirements = list(itertools.chain(
pytest_requirements(env.sys_version_info, env.ctypes_version),
virtualenv_requirements(env.sys_version_info)))
janitor = prepare_pip_requirements_file_if_needed(requirements)
return requirements, janitor
def pip_download_cache_options(download_cache_folder):
if download_cache_folder is None:
return []
return ["--download-cache=" + download_cache_folder]
def download_pip_based_installations(env, pip_invocation, requirements,
download_cache_folder):
"""Download requirements for pip based installation."""
if config.installation_cache_folder() is None:
raise EnvironmentSetupError("Local installation cache folder not "
"defined but required for downloading pip based installations.")
# Installation cache folder needs to be explicitly created for pip to be
# able to copy its downloaded installation files into it. The same does not
# hold for pip's download cache folder which gets created by pip on-demand.
# Seen using Python 3.4.0 & pip 1.5.4.
_create_installation_cache_folder_if_needed()
try:
pip_options = ["install", "-d", config.installation_cache_folder(),
"--exists-action=i"]
pip_options.extend(pip_download_cache_options(download_cache_folder))
# Running pip based installations on Python 2.5.
# * Python 2.5 does not come with SSL support enabled by default and
# so pip can not use SSL certified downloads from PyPI.
# * To work around this either install the
# https://pypi.python.org/pypi/ssl package or run pip using the
# '--insecure' command-line options.
# * Installing the ssl package seems ridden with problems on
# Python 2.5 so this workaround has not been tested.
if (2, 5) <= env.sys_version_info < (2, 6):
# There are some potential cases where we do not need to use
# "--insecure", e.g. if the target Python environment already has
# the 'ssl' module installed. However, detecting whether this is so
# does not seem to be worth the effort. The only way to detect
# whether secure download is supported would be to scan the target
# environment for this information, e.g. setuptools has this
# information in its pip.backwardcompat.ssl variable - if it is
# None, the necessary SSL support is not available. But then we
# would have to be careful:
# - not to run the scan if we already know this information from
# some previous scan
# - to track all actions that could have invalidated our previous
# scan results, etc.
# It just does not seem to be worth the hassle so for now - YAGNI.
pip_options.append("--insecure")
env.execute(pip_invocation + pip_options + requirements)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
raise EnvironmentSetupError("pip based download failed.")
def run_pip_based_installations(env, pip_invocation, requirements,
download_cache_folder):
# 'pip' download caching system usage notes:
# 1. When not installing from our own local installation storage folder, we
# can still use pip's internal download caching system.
# 2. We must not enable pip's internal download caching system when
# installing from our own local installation storage folder. In that
# case, pip attempts to populate its cache from our local installation
# folder, but that logic fails when our folder contains a wheel (.whl)
# distribution. More precisely, it fails attempting to store the wheel
# distribution file's content type information. Tested using Python
# 3.4.0 & pip 1.5.4.
try:
pip_options = ["install"]
if config.installation_cache_folder() is None:
pip_options.extend(pip_download_cache_options(
download_cache_folder))
else:
# pip allows us to identify a local folder containing predownloaded
# installation packages using its '-f' command-line option taking
# an URL parameter. However, it does not require the URL to be
# URL-quoted and it does not even seem to recognize URLs containing
# %xx escaped characters. Tested using an installation cache folder
# path containing spaces with Python 3.4.0 & pip 1.5.4.
installation_cache_folder_URL = utility.path_to_URL(
config.installation_cache_folder(), escape=False)
pip_options.extend(["-f", installation_cache_folder_URL,
"--no-index"])
env.execute(pip_invocation + pip_options + requirements)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
raise EnvironmentSetupError("pip based installation failed.")
def post_pip_based_installation_fixups(env):
"""Apply simple post-installation fixes for pip installed packages."""
if env.sys_version_info[:2] == (3, 1):
from suds_devel.patch_pytest_on_python_31 import patch
patch(env)
def process_pip_based_installations(env, actions, download_cache_folder):
download = "download pip based installations" in actions
install = "run pip based installations" in actions
if not download and not install:
return
pip_invocation = pip_invocation_arguments(env.sys_version_info)
janitor = None
try:
requirements, janitor = prepare_pip_requirements(env)
if download:
download_pip_based_installations(env, pip_invocation, requirements,
download_cache_folder)
if install:
run_pip_based_installations(env, pip_invocation, requirements,
download_cache_folder)
post_pip_based_installation_fixups(env)
finally:
if janitor:
janitor.clean()
# ------------------------------
# Processing Python environments
# ------------------------------
def enabled_actions_for_env(env):
"""Returns actions to perform when processing the given environment."""
def enabled(config_value, required):
if config_value is Config.TriBool.No:
return False
if config_value is Config.TriBool.Yes:
return True
assert config_value is Config.TriBool.IfNeeded
return bool(required)
# Some old Python versions do not support HTTPS downloads and therefore can
# not download installation packages from PyPI. To run setuptools or pip
# based installations on such Python versions, all the required
# installation packages need to be downloaded locally first using a
# compatible Python version (e.g. Python 2.4.4 for Python 2.4.3) and then
# installed locally.
download_supported = not ((2, 4, 3) <= env.sys_version_info < (2, 4, 4))
local_install = config.installation_cache_folder() is not None
actions = set()
pip_required = False
run_pip_based_installations = enabled(config.install_environments, True)
if run_pip_based_installations:
actions.add("run pip based installations")
pip_required = True
if download_supported and enabled(config.download_installations,
local_install and run_pip_based_installations):
actions.add("download pip based installations")
pip_required = True
setuptools_required = False
run_pip_installation = enabled(config.install_environments, pip_required)
if run_pip_installation:
actions.add("run pip installation")
setuptools_required = True
if download_supported and enabled(config.download_installations,
local_install and run_pip_installation):
actions.add("download pip installation")
setuptools_required = True
if enabled(config.setup_setuptools, setuptools_required):
actions.add("setup setuptools")
return actions
def print_environment_processing_title(env):
title_length = 73
print("-" * title_length)
title = "--- %s - Python %s " % (env.name(), env.python_version)
title += "-" * max(0, title_length - len(title))
print(title)
print("-" * title_length)
def process_Python_environment(env):
actions = enabled_actions_for_env(env)
if not actions:
return
print_environment_processing_title(env)
process_setuptools(env, actions)
process_pip(env, actions)
process_pip_based_installations(env, actions,
config.pip_download_cache_folder())
def process_python_environments(python_environments):
for env in python_environments:
try:
process_Python_environment(env)
except EnvironmentSetupError:
utility.report_error(sys.exc_info()[1])
def main():
try:
_report_startup_information()
_prepare_configuration()
_report_configuration()
python_environments = scan_python_environments()
except BadConfiguration:
utility.report_error(sys.exc_info()[1])
return -2
process_python_environments(python_environments)
return 0
if __name__ == "__main__":
sys.exit(main())
|