aboutsummaryrefslogtreecommitdiffstats
path: root/setuptools/command/easy_install.py
diff options
context:
space:
mode:
Diffstat (limited to 'setuptools/command/easy_install.py')
-rwxr-xr-xsetuptools/command/easy_install.py820
1 files changed, 820 insertions, 0 deletions
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
new file mode 100755
index 00000000..145b7395
--- /dev/null
+++ b/setuptools/command/easy_install.py
@@ -0,0 +1,820 @@
+#!python
+"""\
+
+Easy Install
+------------
+
+A tool for doing automatic download/extract/build of distutils-based Python
+packages. For detailed documentation, see the accompanying EasyInstall.txt
+file, or visit the `EasyInstall home page`__.
+
+__ http://peak.telecommunity.com/DevCenter/EasyInstall
+"""
+
+import sys, os.path, zipimport, shutil, tempfile, zipfile
+
+from setuptools import Command
+from setuptools.sandbox import run_setup
+from distutils import log, dir_util
+from distutils.sysconfig import get_python_lib
+from distutils.errors import DistutilsArgError, DistutilsOptionError
+from setuptools.archive_util import unpack_archive
+from setuptools.package_index import PackageIndex, parse_bdist_wininst
+from setuptools.package_index import URL_SCHEME
+from setuptools.command import bdist_egg
+from pkg_resources import *
+
+__all__ = [
+ 'samefile', 'easy_install', 'PthDistributions', 'extract_wininst_cfg',
+ 'main', 'get_exe_prefixes',
+]
+
+def samefile(p1,p2):
+ if hasattr(os.path,'samefile') and (
+ os.path.exists(p1) and os.path.exists(p2)
+ ):
+ return os.path.samefile(p1,p2)
+ return (
+ os.path.normpath(os.path.normcase(p1)) ==
+ os.path.normpath(os.path.normcase(p2))
+ )
+
+class easy_install(Command):
+ """Manage a download/build/install process"""
+
+ description = "Find/get/install Python packages"
+ command_consumes_arguments = True
+ user_options = [
+ ("zip-ok", "z", "install package as a zipfile"),
+ ("multi-version", "m", "make apps have to require() a version"),
+ ("upgrade", "U", "force upgrade (searches PyPI for latest versions)"),
+ ("install-dir=", "d", "install package to DIR"),
+ ("script-dir=", "s", "install scripts to DIR"),
+ ("exclude-scripts", "x", "Don't install scripts"),
+ ("always-copy", "a", "Copy all needed packages to install dir"),
+ ("index-url=", "i", "base URL of Python Package Index"),
+ ("find-links=", "f", "additional URL(s) to search for packages"),
+ ("build-directory=", "b",
+ "download/extract/build in DIR; keep the results"),
+ ('optimize=', 'O',
+ "also compile with optimization: -O1 for \"python -O\", "
+ "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
+ ('record=', None,
+ "filename in which to record list of installed files"),
+ ]
+ boolean_options = [
+ 'zip-ok', 'multi-version', 'exclude-scripts', 'upgrade', 'always-copy'
+ ]
+ create_index = PackageIndex
+
+ def initialize_options(self):
+ self.zip_ok = None
+ self.install_dir = self.script_dir = self.exclude_scripts = None
+ self.index_url = None
+ self.find_links = None
+ self.build_directory = None
+ self.args = None
+ self.optimize = self.record = None
+ self.upgrade = self.always_copy = self.multi_version = None
+ # Options not specifiable via command line
+ self.package_index = None
+ self.pth_file = None
+
+ def finalize_options(self):
+ # If a non-default installation directory was specified, default the
+ # script directory to match it.
+ if self.script_dir is None:
+ self.script_dir = self.install_dir
+
+ # Let install_dir get set by install_lib command, which in turn
+ # gets its info from the install command, and takes into account
+ # --prefix and --home and all that other crud.
+ self.set_undefined_options('install_lib',
+ ('install_dir','install_dir')
+ )
+ # Likewise, set default script_dir from 'install_scripts.install_dir'
+ self.set_undefined_options('install_scripts',
+ ('install_dir', 'script_dir')
+ )
+ # default --record from the install command
+ self.set_undefined_options('install', ('record', 'record'))
+
+ site_packages = get_python_lib()
+ instdir = self.install_dir
+
+ if instdir is None or samefile(site_packages,instdir):
+ instdir = site_packages
+ if self.pth_file is None:
+ self.pth_file = PthDistributions(
+ os.path.join(instdir,'easy-install.pth')
+ )
+ self.install_dir = instdir
+
+ elif self.multi_version is None:
+ self.multi_version = True
+
+ elif not self.multi_version:
+ # explicit false set from Python code; raise an error
+ raise DistutilsArgError(
+ "Can't do single-version installs outside site-packages"
+ )
+
+ self.index_url = self.index_url or "http://www.python.org/pypi"
+
+ self.shadow_path = sys.path[:]
+
+ for path_item in self.install_dir, self.script_dir:
+ if path_item not in self.shadow_path:
+ self.shadow_path.insert(0, self.install_dir)
+
+ if self.package_index is None:
+ self.package_index = self.create_index(
+ self.index_url, search_path = self.shadow_path
+ )
+
+ self.local_index = AvailableDistributions(self.shadow_path)
+
+ if self.find_links is not None:
+ if isinstance(self.find_links, basestring):
+ self.find_links = self.find_links.split()
+ else:
+ self.find_links = []
+
+ self.set_undefined_options('install_lib', ('optimize','optimize'))
+
+ if not isinstance(self.optimize,int):
+ try:
+ self.optimize = int(self.optimize)
+ if not (0 <= self.optimize <= 2): raise ValueError
+ except ValueError:
+ raise DistutilsOptionError("--optimize must be 0, 1, or 2")
+
+ if not self.args:
+ raise DistutilsArgError(
+ "No urls, filenames, or requirements specified (see --help)")
+
+ elif len(self.args)>1 and self.build_directory is not None:
+ raise DistutilsArgError(
+ "Build directory can only be set when using one URL"
+ )
+
+ self.outputs = []
+
+
+
+ def alloc_tmp(self):
+ if self.build_directory is None:
+ return tempfile.mkdtemp(prefix="easy_install-")
+ tmpdir = os.path.realpath(self.build_directory)
+ if not os.path.isdir(tmpdir):
+ os.makedirs(tmpdir)
+ return tmpdir
+
+
+ def run(self):
+ if self.verbose<>self.distribution.verbose:
+ log.set_verbosity(self.verbose)
+ try:
+ for link in self.find_links:
+ self.package_index.scan_url(link)
+ for spec in self.args:
+ self.easy_install(spec)
+ if self.record:
+ from distutils import file_util
+ self.execute(
+ file_util.write_file, (self.record, self.outputs),
+ "writing list of installed files to '%s'" %
+ self.record
+ )
+ finally:
+ log.set_verbosity(self.distribution.verbose)
+
+
+ def add_output(self, path):
+ if os.path.isdir(path):
+ for base, dirs, files in os.walk(path):
+ for filename in files:
+ self.outputs.append(os.path.join(base,filename))
+ else:
+ self.outputs.append(path)
+
+
+
+
+
+
+ def easy_install(self, spec):
+ tmpdir = self.alloc_tmp()
+ download = None
+
+ try:
+ if not isinstance(spec,Requirement):
+ if URL_SCHEME(spec):
+ # It's a url, download it to tmpdir and process
+ download = self.package_index.download(spec, tmpdir)
+ return self.install_item(None, download, tmpdir, True)
+
+ elif os.path.exists(spec):
+ # Existing file or directory, just process it directly
+ return self.install_item(None, spec, tmpdir, True)
+ else:
+ try:
+ spec = Requirement.parse(spec)
+ except ValueError:
+ raise RuntimeError(
+ "Not a URL, existing file, or requirement spec: %r"
+ % (spec,)
+ )
+
+ if isinstance(spec, Requirement):
+ download = self.package_index.fetch(spec, tmpdir, self.upgrade)
+ else:
+ spec = None
+
+ if download is None:
+ raise RuntimeError(
+ "Could not find distribution for %r" % spec
+ )
+
+ return self.install_item(spec, download, tmpdir)
+
+ finally:
+ if self.build_directory is None:
+ shutil.rmtree(tmpdir)
+
+
+
+ def install_item(self, spec, download, tmpdir, install_needed=False):
+ # Installation is also needed if file in tmpdir or is not an egg
+ install_needed = install_needed or os.path.dirname(download) == tmpdir
+ install_needed = install_needed or not download.endswith('.egg')
+ log.info("Processing %s", os.path.basename(download))
+ if install_needed or self.always_copy:
+ dists = self.install_eggs(download, self.zip_ok, tmpdir)
+ for dist in dists:
+ self.process_distribution(spec, dist)
+ else:
+ dists = [self.egg_distribution(download)]
+ self.process_distribution(spec, dists[0], "Using")
+ if spec is not None:
+ for dist in dists:
+ if dist in spec:
+ return dist
+
+ def process_distribution(self, requirement, dist, *info):
+ self.update_pth(dist)
+ self.package_index.add(dist)
+ self.local_index.add(dist)
+ self.install_egg_scripts(dist)
+ log.warn(self.installation_report(dist, *info))
+ if requirement is None:
+ requirement = Requirement.parse('%s==%s'%(dist.name,dist.version))
+ if dist in requirement:
+ log.info("Processing dependencies for %s", requirement)
+ try:
+ self.local_index.resolve(
+ [requirement], self.shadow_path, self.easy_install
+ )
+ except DistributionNotFound, e:
+ raise RuntimeError(
+ "Could not find required distribution %s" % e.args
+ )
+ except VersionConflict, e:
+ raise RuntimeError(
+ "Installed distribution %s conflicts with requirement %s"
+ % e.args
+ )
+
+ def install_egg_scripts(self, dist):
+ metadata = dist.metadata
+ if self.exclude_scripts or not metadata.metadata_isdir('scripts'):
+ return
+
+ from distutils.command.build_scripts import first_line_re
+
+ for script_name in metadata.metadata_listdir('scripts'):
+ target = os.path.join(self.script_dir, script_name)
+
+ log.info("Installing %s script to %s", script_name,self.script_dir)
+
+ script_text = metadata.get_metadata('scripts/'+script_name)
+ script_text = script_text.replace('\r','\n')
+ first, rest = script_text.split('\n',1)
+
+ match = first_line_re.match(first)
+ options = ''
+ if match:
+ options = match.group(1) or ''
+ if options:
+ options = ' '+options
+
+ spec = '%s==%s' % (dist.name,dist.version)
+
+ script_text = '\n'.join([
+ "#!%s%s" % (os.path.normpath(sys.executable),options),
+ "# EASY-INSTALL-SCRIPT: %r,%r" % (spec, script_name),
+ "import pkg_resources",
+ "pkg_resources.run_main(%r, %r)" % (spec, script_name)
+ ])
+ if not self.dry_run:
+ f = open(target,"w")
+ f.write(script_text)
+ f.close()
+ try:
+ os.chmod(target,0755)
+ except (AttributeError, os.error):
+ pass
+
+
+ def install_eggs(self, dist_filename, zip_ok, tmpdir):
+ # .egg dirs or files are already built, so just return them
+ if dist_filename.lower().endswith('.egg'):
+ return [self.install_egg(dist_filename, True, tmpdir)]
+
+ if dist_filename.lower().endswith('.exe'):
+ return [self.install_exe(dist_filename, tmpdir)]
+
+ # Anything else, try to extract and build
+ if os.path.isfile(dist_filename):
+ unpack_archive(dist_filename, tmpdir, self.unpack_progress)
+
+ # Find the setup.py file
+ from glob import glob
+ setup_script = os.path.join(tmpdir, 'setup.py')
+ if not os.path.exists(setup_script):
+ setups = glob(os.path.join(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.build_egg(tmpdir, setup_script)
+ dist_dir = os.path.join(os.path.dirname(setup_script),'dist')
+
+ eggs = []
+ for egg in glob(os.path.join(dist_dir,'*.egg')):
+ eggs.append(self.install_egg(egg, zip_ok, tmpdir))
+
+ if not eggs and not self.dry_run:
+ log.warn("No eggs found in %s (setup script problem?)", dist_dir)
+
+ return eggs
+
+
+
+ def egg_distribution(self, egg_path):
+ if os.path.isdir(egg_path):
+ metadata = PathMetadata(egg_path,os.path.join(egg_path,'EGG-INFO'))
+ else:
+ metadata = EggMetadata(zipimport.zipimporter(egg_path))
+ return Distribution.from_filename(egg_path,metadata=metadata)
+
+ def install_egg(self, egg_path, zip_ok, tmpdir):
+ destination = os.path.join(self.install_dir,os.path.basename(egg_path))
+ destination = os.path.abspath(destination)
+
+ if not self.dry_run:
+ ensure_directory(destination)
+
+ if not samefile(egg_path, destination):
+ if os.path.isdir(destination):
+ dir_util.remove_tree(destination, dry_run=self.dry_run)
+ elif os.path.isfile(destination):
+ self.execute(os.unlink,(destination,),"Removing "+destination)
+
+ if os.path.isdir(egg_path):
+ if egg_path.startswith(tmpdir):
+ f,m = shutil.move, "Moving"
+ else:
+ f,m = shutil.copytree, "Copying"
+ elif zip_ok:
+ if egg_path.startswith(tmpdir):
+ f,m = shutil.move, "Moving"
+ else:
+ f,m = shutil.copy2, "Copying"
+ else:
+ self.mkpath(destination)
+ f,m = self.unpack_and_compile, "Extracting"
+
+ self.execute(f, (egg_path, destination),
+ (m+" %s to %s") %
+ (os.path.basename(egg_path),os.path.dirname(destination)))
+
+ self.add_output(destination)
+ return self.egg_distribution(destination)
+
+ def install_exe(self, dist_filename, tmpdir):
+ # See if it's valid, get data
+ cfg = extract_wininst_cfg(dist_filename)
+ if cfg is None:
+ raise RuntimeError(
+ "%s is not a valid distutils Windows .exe" % dist_filename
+ )
+
+ # Create a dummy distribution object until we build the real distro
+ dist = Distribution(None,
+ name=cfg.get('metadata','name'),
+ version=cfg.get('metadata','version'),
+ platform="win32"
+ )
+
+ # Convert the .exe to an unpacked egg
+ egg_path = dist.path = os.path.join(tmpdir, dist.egg_name()+'.egg')
+ egg_tmp = egg_path+'.tmp'
+ self.exe_to_egg(dist_filename, egg_tmp)
+
+ # Write EGG-INFO/PKG-INFO
+ pkg_inf = os.path.join(egg_tmp, 'EGG-INFO', 'PKG-INFO')
+ f = open(pkg_inf,'w')
+ f.write('Metadata-Version: 1.0\n')
+ for k,v in cfg.items('metadata'):
+ if k<>'target_version':
+ f.write('%s: %s\n' % (k.replace('_','-').title(), v))
+ f.close()
+
+ # Build .egg file from tmpdir
+ bdist_egg.make_zipfile(
+ egg_path, egg_tmp,
+ verbose=self.verbose, dry_run=self.dry_run
+ )
+
+ # install the .egg
+ return self.install_egg(egg_path, self.zip_ok, tmpdir)
+
+
+
+
+ def exe_to_egg(self, dist_filename, egg_tmp):
+ """Extract a bdist_wininst to the directories an egg would use"""
+
+ # Check for .pth file and set up prefix translations
+ prefixes = get_exe_prefixes(dist_filename)
+ to_compile = []
+ native_libs = []
+
+ def process(src,dst):
+ for old,new in prefixes:
+ if src.startswith(old):
+ src = new+src[len(old):]
+ dst = os.path.join(egg_tmp, *src.split('/'))
+ dl = dst.lower()
+ if dl.endswith('.pyd') or dl.endswith('.dll'):
+ native_libs.append(src)
+ elif dl.endswith('.py') and old!='SCRIPTS/':
+ to_compile.append(dst)
+ return dst
+ if not src.endswith('.pth'):
+ log.warn("WARNING: can't process %s", src)
+ return None
+
+ # extract, tracking .pyd/.dll->native_libs and .py -> to_compile
+ unpack_archive(dist_filename, egg_tmp, process)
+
+ for res in native_libs:
+ if res.lower().endswith('.pyd'): # create stubs for .pyd's
+ parts = res.split('/')
+ resource, parts[-1] = parts[-1], parts[-1][:-1]
+ pyfile = os.path.join(egg_tmp, *parts)
+ to_compile.append(pyfile)
+ bdist_egg.write_stub(resource, pyfile)
+
+ self.byte_compile(to_compile) # compile .py's
+
+ if native_libs: # write EGG-INFO/native_libs.txt
+ nl_txt = os.path.join(egg_tmp, 'EGG-INFO', 'native_libs.txt')
+ ensure_directory(nl_txt)
+ open(nl_txt,'w').write('\n'.join(native_libs)+'\n')
+
+ def installation_report(self, dist, what="Installed"):
+ """Helpful installation message for display to package users"""
+
+ msg = "\n%(what)s %(eggloc)s"
+ if self.multi_version:
+ msg += """
+
+Because this distribution was installed --multi-version or --install-dir,
+before you can import modules from this package in an application, you
+will need to 'import pkg_resources' and then use a 'require()' call
+similar to one of these examples, in order to select the desired version:
+
+ pkg_resources.require("%(name)s") # latest installed version
+ pkg_resources.require("%(name)s==%(version)s") # this exact version
+ pkg_resources.require("%(name)s>=%(version)s") # this version or higher
+"""
+ if not samefile(get_python_lib(),self.install_dir):
+ msg += """
+
+Note also that the installation directory must be on sys.path at runtime for
+this to work. (e.g. by being the application's script directory, by being on
+PYTHONPATH, or by being added to sys.path by your code.)
+"""
+ eggloc = dist.path
+ name = dist.name
+ version = dist.version
+ return msg % locals()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ def build_egg(self, tmpdir, setup_script):
+ sys.modules.setdefault('distutils.command.bdist_egg', bdist_egg)
+
+ args = ['bdist_egg']
+ if self.verbose>2:
+ v = 'v' * self.verbose - 1
+ args.insert(0,'-'+v)
+ elif self.verbose<2:
+ args.insert(0,'-q')
+ if self.dry_run:
+ args.insert(0,'-n')
+
+ log.info("Running %s %s", setup_script[len(tmpdir)+1:], ' '.join(args))
+ try:
+ try:
+ run_setup(setup_script, args)
+ except SystemExit, v:
+ raise RuntimeError(
+ "Setup script exited with %s" % (v.args[0],)
+ )
+ finally:
+ log.set_verbosity(self.verbose) # restore our log verbosity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ def update_pth(self,dist):
+ if self.pth_file is None:
+ return
+
+ for d in self.pth_file.get(dist.key,()): # drop old entries
+ if self.multi_version or d.path != dist.path:
+ log.info("Removing %s from easy-install.pth file", d)
+ self.pth_file.remove(d)
+ if d.path in self.shadow_path:
+ self.shadow_path.remove(d.path)
+
+ if not self.multi_version:
+ if dist.path in self.pth_file.paths:
+ log.info(
+ "%s is already the active version in easy-install.pth",
+ dist
+ )
+ else:
+ log.info("Adding %s to easy-install.pth file", dist)
+ self.pth_file.add(dist) # add new entry
+ if dist.path not in self.shadow_path:
+ self.shadow_path.append(dist.path)
+
+ self.pth_file.save()
+
+ if dist.name=='setuptools':
+ # Ensure that setuptools itself never becomes unavailable!
+ f = open(os.path.join(self.install_dir,'setuptools.pth'), 'w')
+ f.write(dist.path+'\n')
+ f.close()
+
+
+ def unpack_progress(self, src, dst):
+ # Progress filter for unpacking
+ log.debug("Unpacking %s to %s", src, dst)
+ return dst # only unpack-and-compile skips files for dry run
+
+
+
+
+
+ def unpack_and_compile(self, egg_path, destination):
+ to_compile = []
+
+ def pf(src,dst):
+ if dst.endswith('.py'):
+ to_compile.append(dst)
+ self.unpack_progress(src,dst)
+ return not self.dry_run and dst or None
+
+ unpack_archive(egg_path, destination, pf)
+ self.byte_compile(to_compile)
+
+
+ def byte_compile(self, to_compile):
+ from distutils.util import byte_compile
+ try:
+ # try to make the byte compile messages quieter
+ log.set_verbosity(self.verbose - 1)
+
+ byte_compile(to_compile, optimize=0, force=1, dry_run=self.dry_run)
+ if self.optimize:
+ byte_compile(
+ to_compile, optimize=self.optimize, force=1,
+ dry_run=self.dry_run
+ )
+ finally:
+ log.set_verbosity(self.verbose) # restore original verbosity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def extract_wininst_cfg(dist_filename):
+ """Extract configuration data from a bdist_wininst .exe
+
+ Returns a ConfigParser.RawConfigParser, or None
+ """
+ f = open(dist_filename,'rb')
+ try:
+ endrec = zipfile._EndRecData(f)
+ if endrec is None:
+ return None
+
+ prepended = (endrec[9] - endrec[5]) - endrec[6]
+ if prepended < 12: # no wininst data here
+ return None
+ f.seek(prepended-12)
+
+ import struct, StringIO, ConfigParser
+ tag, cfglen, bmlen = struct.unpack("<iii",f.read(12))
+ if tag<>0x1234567A:
+ return None # not a valid tag
+
+ f.seek(prepended-(12+cfglen+bmlen))
+ cfg = ConfigParser.RawConfigParser({'version':'','target_version':''})
+ try:
+ cfg.readfp(StringIO.StringIO(f.read(cfglen)))
+ except ConfigParser.Error:
+ return None
+ if not cfg.has_section('metadata') or not cfg.has_section('Setup'):
+ return None
+ return cfg
+
+ finally:
+ f.close()
+
+
+
+
+
+
+
+
+def get_exe_prefixes(exe_filename):
+ """Get exe->egg path translations for a given .exe file"""
+
+ prefixes = [
+ ('PURELIB/', ''),
+ ('PLATLIB/', ''),
+ ('SCRIPTS/', 'EGG-INFO/scripts/')
+ ]
+ z = zipfile.ZipFile(exe_filename)
+ try:
+ for info in z.infolist():
+ name = info.filename
+ if not name.endswith('.pth'):
+ continue
+ parts = name.split('/')
+ if len(parts)<>2:
+ continue
+ if parts[0] in ('PURELIB','PLATLIB'):
+ pth = z.read(name).strip()
+ prefixes[0] = ('PURELIB/%s/' % pth), ''
+ prefixes[1] = ('PLATLIB/%s/' % pth), ''
+ break
+ finally:
+ z.close()
+
+ return prefixes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+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)
+
+
+
+
+def main(argv, **kw):
+ from setuptools import setup
+ try:
+ setup(script_args = ['-q','easy_install', '-v']+argv, **kw)
+ except RuntimeError, v:
+ print >>sys.stderr,"error:",v
+ sys.exit(1)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+