diff options
Diffstat (limited to 'easy_install.py')
-rwxr-xr-x | easy_install.py | 536 |
1 files changed, 536 insertions, 0 deletions
diff --git a/easy_install.py b/easy_install.py new file mode 100755 index 00000000..0b12ef68 --- /dev/null +++ b/easy_install.py @@ -0,0 +1,536 @@ +#!python.exe +import site + +#!python +"""\ +Easy Install +============ + +Easy Install is a python module (easy_install) that lets you automatically +download, build, install, and manage Python packages. + +.. contents:: **Table of Contents** + + +Downloading and Installing a Package +------------------------------------ + +For basic use of ``easy_install``, you need only supply the filename or URL of +a source distribution or .egg file (Python Egg). + +**Example 1**. Download a source distribution, automatically building and +installing it:: + + easy_install http://example.com/path/to/MyPackage-1.2.3.tgz + +**Example 2**. Install an already-downloaded .egg file:: + + easy_install /my_downloads/OtherPackage-3.2.1-py2.3.egg + +Easy Install recognizes distutils *source* (not binary) distribution files with +extensions of .tgz, .tar, .tar.gz, .tar.bz2, or .zip. And of course it handles +already-built .egg distributions. + +By default, packages are installed to the running Python installation's +``site-packages`` directory, unless you provide the ``-d`` or ``--install-dir`` +option to specify an alternative directory. + +Packages installed to ``site-packages`` are added to an ``easy-install.pth`` +file, so that Python will be able to import the package by default. If you do +not want this to happen, you should use the ``-m`` or ``--multi`` option, which +allows multiple versions of the same package to be selected at runtime. + +Note that installing to a directory other than ``site-packages`` already +implies the ``-m`` option, so if you cannot install to ``site-packages``, +please see the `Options`_ section below (under ``--multi``) to find out how to +select packages at runtime. + + +Upgrading a Package +------------------- + +You don't need to do anything special to upgrade a package: just install the +new version. If you're using ``-m`` or ``--multi`` (or installing outside of +``site-packages``), the runtime system automatically selects the newest +available version of a package. If you're installing to ``site-packages`` and +not using ``-m``, installing a package automatically replaces its older version +in the ``easy-install.pth`` file, so that Python will import the latest version +by default. + +``easy_install`` never actually deletes packages (unless you're installing a +package with the same name and version number as an existing package), so if +you want to get rid of older versions of a package, please see `Uninstalling +Packages`_, below. + + +Changing the Active Version (``site-packages`` installs only) +------------------------------------------------------------- + +If you've upgraded a package, but need to revert to a previously-installed +version, you can do so like this:: + + easy_install PackageName==1.2.3 + +Where ``1.2.3`` is replaced by the exact version number you wish to switch to. +Note that the named package and version must already have been installed to +``site-packages``. + +If you'd like to switch to the latest version of ``PackageName``, you can do so +like this:: + + easy_install PackageName + +This will activate the latest installed version. + + +Uninstalling Packages +--------------------- + +If you have replaced a package with another version, then you can just delete +the package(s) you don't need by deleting the PackageName-versioninfo.egg file +or directory (found in the installation directory). + +If you want to delete the currently installed version of a package (or all +versions of a package), you should first run:: + + easy_install -m PackageName + +This will ensure that Python doesn't continue to search for a package you're +planning to remove. After you've done this, you can safely delete the .egg +files or directories. + + +Options +------- + +``--zip, -z`` + Enable installing the package as a zip file. This can significantly + increase Python's overall import performance if you're installing to + ``site-packages`` and not using the ``--multi`` option, because Python + process zipfile entries on ``sys.path`` much faster than it does + directories. So, if you don't use this option, and you install a lot of + packages, some of them may be slower to import. + + But, this option is disabled by default, unless you're installing from an + already-built binary zipfile (``.egg`` file). This is to avoid problems + when using packages that dosn't support running from a zip file. Such + packages usually access data files in their package directories using the + Python ``__file__`` or ``__path__`` attribute, instead of the + ``pkg_resources`` API. So, if you find that a package doesn't work properly + when used with this option, you may want to suggest to the author that they + switch to using the ``pkg_resources`` resource API, which will allow their + package to work whether it's installed as a zipfile or not. + + (Note: this option only affects the installation of newly-built packages + that are not already installed in the target directory; if you want to + convert an existing installed version from zipped to unzipped or vice + versa, you'll need to delete the existing version first.) + +``--multi-version, -m`` + "Multi-version" mode. Specifying this option prevents ``easy_install`` from + adding an ``easy-install.pth`` entry for the package being installed, and + if an entry for any version the package already exists, it will be removed + upon successful installation. In multi-version mode, no specific version of + the package is available for importing, unless you use + ``pkg_resources.require()`` to put it on ``sys.path``. This can be as + simple as:: + + from pkg_resources import require + require("SomePackage", "OtherPackage", "MyPackage") + + which will put the latest installed version of the specified packages on + ``sys.path`` for you. (For more advanced uses, like selecting specific + versions and enabling optional dependencies, see the ``pkg_resources`` API + doc.) Note that if you install to a directory other than ``site-packages``, + this option is automatically in effect, because ``.pth`` files can only be + used in ``site-packages`` (at least in Python 2.3 and 2.4). So, if you use + the ``--install-dir`` or ``-i`` options, you must also use ``require()`` to + enable packages at runtime + +``--install-dir=DIR, -d DIR`` + Set the installation directory. It is up to you to ensure that this + directory is on ``sys.path`` at runtime, and to use + ``pkg_resources.require()`` to enable the installed package(s) that you + need. +""" + +import sys, os.path, pkg_resources, re, zipimport +from pkg_resources import * + + + + + + + + + +class Installer: + """Manage a download/build/install process""" + + pth_file = None + + def __init__(self, instdir=None, zip_ok=False, multi=None, tmpdir=None): + from distutils.sysconfig import get_python_lib + site_packages = get_python_lib() + + if tmpdir is None: + from tempfile import mkdtemp + tmpdir = mkdtemp(prefix="easy_install-") + + self.tmpdir = tmpdir + + if instdir is None or self.samefile(site_packages,instdir): + instdir = site_packages + self.pth_file = PthDistributions( + os.path.join(instdir,'easy-install.pth') + ) + elif multi is None: + multi = True + + elif not multi: + # explicit false, raise an error + raise RuntimeError( + "Can't do single-version installs outside site-packages" + ) + + self.instdir = instdir + self.zip_ok = zip_ok + self.multi = multi + + def close(self): + if os.path.isdir(self.tmpdir): + from shutil import rmtree + rmtree(self.tmpdir,True) + + def __del__(self): + self.close() + + def samefile(self,p1,p2): + try: + os.path.samefile + except AttributeError: + return ( + os.path.normpath(os.path.normcase(p1)) == + os.path.normpath(os.path.normcase(p2)) + ) + else: + return os.path.samefile(p1,p2) + + def download(self, spec): + """Locate and/or download or `spec`, returning a local filename""" + if isinstance(spec,Requirement): + pass + else: + scheme = URL_SCHEME(spec) + if scheme: + # It's a url, download it to self.tmpdir + return self._download_url(scheme, spec) + + elif os.path.exists(spec): + # Existing file or directory, just return it + return spec + else: + try: + spec = Requirement.parse(spec) + except ValueError: + raise RuntimeError( + "Not a URL, existing file, or requirement spec: %r" % + (spec,) + ) + # process a Requirement + dist = AvailableDistributions().best_match(spec,[]) + if dist is not None and dist.path.endswith('.egg'): + return dist.path + + # TODO: search here for a distro to download + + raise DistributionNotFound(spec) + + def install_eggs(self, dist_filename): + # .egg dirs or files are already built, so just return them + if dist_filename.lower().endswith('.egg'): + return self.install_egg(dist_filename,True) + + # Anything else, try to extract and build + import zipfile, tarfile + if zipfile.is_zipfile(dist_filename): + self._extract_zip(dist_filename, self.tmpdir) + else: + import tarfile + try: + tar = tarfile.open(dist_filename) + except tarfile.TarError: + raise RuntimeError( + "Not a valid tar or zip archive: %s" % dist_filename + ) + else: + self._extract_tar(tar) + + # Find the setup.py file + from glob import glob + setup_script = os.path.join(self.tmpdir, 'setup.py') + if not os.path.exists(setup_script): + setups = glob(os.path.join(self.tmpdir, '*', 'setup.py')) + if not setups: + raise RuntimeError( + "Couldn't find a setup script in %s" % dist_filename + ) + if len(setups)>1: + raise RuntimeError( + "Multiple setup scripts in %s" % dist_filename + ) + setup_script = setups[0] + + self._run_setup(setups[0]) + for egg in glob( + os.path.join(os.path.dirname(setup_script),'dist','*.egg') + ): + self.install_egg(egg, self.zip_ok) + + def _extract_zip(self,zipname,extract_dir): + import zipfile + z = zipfile.ZipFile(zipname) + + try: + for info in z.infolist(): + name = info.filename + + # don't extract absolute paths or ones with .. in them + if name.startswith('/') or '..' in name: + continue + + target = os.path.join(extract_dir,name) + if name.endswith('/'): + # directory + ensure_directory(target) + else: + # file + ensure_directory(target) + data = z.read(info.filename) + f = open(target,'wb') + try: + f.write(data) + finally: + f.close() + del data + finally: + z.close() + + def _extract_tar(self,tarobj): + try: + tarobj.chown = lambda *args: None # don't do any chowning! + for member in tarobj: + if member.isfile() or member.isdir(): + name = member.name + # don't extract absolute paths or ones with .. in them + if not name.startswith('/') and '..' not in name: + tarobj.extract(member,self.tmpdir) + finally: + tarobj.close() + + def _run_setup(self, setup_script): + from setuptools.command import bdist_egg + sys.modules.setdefault('distutils.command.bdist_egg', bdist_egg) + old_dir = os.getcwd() + save_argv = sys.argv[:] + save_path = sys.path[:] + try: + os.chdir(os.path.dirname(setup_script)) + try: + sys.argv[:] = [setup_script, '-q', 'bdist_egg'] + sys.path.insert(0,os.getcwd()) + execfile(setup_script, {'__file__':setup_script}) + except SystemExit, v: + if v.args and v.args[0]: + raise RuntimeError( + "Setup script exited with %s" % (v.args[0],) + ) + finally: + os.chdir(old_dir) + sys.path[:] = save_path + sys.argv[:] = save_argv + + + + + + + + + + + + + + + + + + + + + def install_egg(self, egg_path, zip_ok): + import shutil + destination = os.path.join(self.instdir, os.path.basename(egg_path)) + ensure_directory(destination) + + if not self.samefile(egg_path, destination): + if os.path.isdir(destination): + shutil.rmtree(destination) + elif os.path.isfile(destination): + os.unlink(destination) + + if zip_ok: + if egg_path.startswith(self.tmpdir): + shutil.move(egg_path, destination) + else: + shutil.copy2(egg_path, destination) + + elif os.path.isdir(egg_path): + shutil.move(egg_path, destination) + + else: + os.mkdir(destination) + self._extract_zip(egg_path, destination) + + if self.pth_file is not None: + if os.path.isdir(destination): + dist = Distribution.from_filename( + destination, metadata=PathMetadata( + destination, os.path.join(destination,'EGG-INFO') + ) + ) + else: + metadata = EggMetadata(zipimport.zipimporter(destination)) + dist = Distribution.from_filename(destination,metadata=metadata) + + # remove old + map(self.pth_file.remove, self.pth_file.get(dist.key,())) + if not self.multi: + self.pth_file.add(dist) # add new + self.pth_file.save() + + def _download_url(self, scheme, url): + # Determine download filename + from urlparse import urlparse + name = filter(None,urlparse(url)[2].split('/'))[-1] + while '..' in name: + name = name.replace('..','.').replace('\\','_') + + # Download the file + from urllib import FancyURLopener, URLopener + class opener(FancyURLopener): + http_error_default = URLopener.http_error_default + try: + filename,headers = opener().retrieve( + url,os.path.join(self.tmpdir,name) + ) + except IOError,v: + if v.args and v.args[0]=='http error': + raise RuntimeError( + "Download error: %s %s" % v.args[1:3] + ) + else: + raise + # and return its filename + return filename + + + + + + + + + + + + + + + + + +class PthDistributions(AvailableDistributions): + """A .pth file with Distribution paths in it""" + + dirty = False + + def __init__(self, filename): + self.filename = filename; self._load() + AvailableDistributions.__init__( + self, list(yield_lines(self.paths)), None, None + ) + + def _load(self): + self.paths = [] + if os.path.isfile(self.filename): + self.paths = [line.rstrip() for line in open(self.filename,'rt')] + while self.paths and not self.paths[-1].strip(): self.paths.pop() + + def save(self): + """Write changed .pth file back to disk""" + if self.dirty: + data = '\n'.join(self.paths+['']) + f = open(self.filename,'wt') + f.write(data) + f.close() + self.dirty = False + + def add(self,dist): + """Add `dist` to the distribution map""" + if dist.path not in self.paths: + self.paths.append(dist.path); self.dirty = True + AvailableDistributions.add(self,dist) + + def remove(self,dist): + """Remove `dist` from the distribution map""" + while dist.path in self.paths: + self.paths.remove(dist.path); self.dirty = True + AvailableDistributions.remove(self,dist) + + + + +URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):',re.I).match + +def main(argv): + from optparse import OptionParser + + parser = OptionParser(usage = "usage: %prog [options] url [url...]") + parser.add_option("-d", "--install-dir", dest="instdir", default=None, + help="install package to DIR", metavar="DIR") + + parser.add_option("-z", "--zip", + action="store_true", dest="zip_ok", default=False, + help="install package as a zipfile") + + parser.add_option("-m", "--multi-version", + action="store_true", dest="multi", default=None, + help="make apps have to require() a version") + + (options, args) = parser.parse_args() + + try: + if not args: + raise RuntimeError("No urls, filenames, or requirements specified") + + for spec in args: + inst = Installer(options.instdir, options.zip_ok, options.multi) + try: + print "Downloading", spec + downloaded = inst.download(spec) + print "Installing", os.path.basename(downloaded) + inst.install_eggs(downloaded) + finally: + inst.close() + except RuntimeError, v: + parser.error(str(v)) + +if __name__ == '__main__': + main(sys.argv[1:]) + + + + |