#!/usr/bin/env python """\ @file install.py @author Phoenix @date 2008-01-27 @brief Install files into an indra checkout. Install files as specified by: https://wiki.lindenlab.com/wiki/User:Phoenix/Library_Installation $LicenseInfo:firstyear=2007&license=mit$ Copyright (c) 2007, Linden Research, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. $/LicenseInfo$ """ import copy import md5 import optparse import os import pprint import sys import tarfile import urllib import urlparse from sets import Set as set, ImmutableSet as frozenset # Locate -our- python library relative to our install location. from os.path import realpath, dirname, join # Walk back to checkout base directory base_dir = dirname(dirname(realpath(__file__))) # Walk in to libraries directory lib_dir = join(join(join(base_dir, 'indra'), 'lib'), 'python') if lib_dir not in sys.path: sys.path.insert(0, lib_dir) from indra.base import llsd from indra.util import helpformatter class InstallFile(object): "This is just a handy way to throw around details on a file in memory." def __init__(self, pkgname, url, md5sum, cache_dir, platform_path): self.pkgname = pkgname self.url = url self.md5sum = md5sum filename = urlparse.urlparse(url)[2].split('/')[-1] self.filename = os.path.join(cache_dir, filename) self.platform_path = platform_path def __str__(self): return "ifile{%s:%s}" % (self.pkgname, self.url) def _is_md5sum_match(self): hasher = md5.new(file(self.filename, 'rb').read()) if hasher.hexdigest() == self.md5sum: return True return False def is_match(self, platform): """@brief Test to see if this ifile is part of platform @param platform The target platform. Eg, windows or linux/i686/gcc/3.3 @return Returns True if the ifile is in the platform. """ if self.platform_path[0] == 'common': return True req_platform_path = platform.split('/') #print "platform:",req_platform_path #print "path:",self.platform_path # to match, every path part much match match_count = min(len(req_platform_path), len(self.platform_path)) for ii in range(0, match_count): if req_platform_path[ii] != self.platform_path[ii]: return False #print "match!" return True def fetch_local(self): #print "Looking for:",self.filename if not os.path.exists(self.filename): pass elif self.md5sum and not self._is_md5sum_match(): print "md5 mismatch:", self.filename os.remove(self.filename) else: print "Found matching package:", self.filename return print "Downloading",self.url,"to local file",self.filename urllib.urlretrieve(self.url, self.filename) if self.md5sum and not self._is_md5sum_match(): raise RuntimeError("Error matching md5 for %s" % self.url) class LicenseDefinition(object): def __init__(self, definition): #probably looks like: # { text : ..., # url : ... # blessed : ... # } self._definition = definition class InstallableDefinition(object): def __init__(self, definition): #probably looks like: # { packages : {platform...}, # copyright : ... # license : ... # description: ... # } self._definition = definition def _ifiles_from(self, tree, pkgname, cache_dir): return self._ifiles_from_path(tree, pkgname, cache_dir, []) def _ifiles_from_path(self, tree, pkgname, cache_dir, path): ifiles = [] if 'url' in tree: ifiles.append(InstallFile( pkgname, tree['url'], tree.get('md5sum', None), cache_dir, path)) else: for key in tree: platform_path = copy.copy(path) platform_path.append(key) ifiles.extend( self._ifiles_from_path( tree[key], pkgname, cache_dir, platform_path)) return ifiles def ifiles(self, pkgname, platform, cache_dir): """@brief return a list of appropriate InstallFile instances to install @param pkgname The name of the package to be installed, eg 'tut' @param platform The target platform. Eg, windows or linux/i686/gcc/3.3 @param cache_dir The directory to cache downloads. @return Returns a list of InstallFiles which are part of this install """ if 'packages' not in self._definition: return [] all_ifiles = self._ifiles_from( self._definition['packages'], pkgname, cache_dir) if platform == 'all': return all_ifiles #print "Considering", len(all_ifiles), "packages for", pkgname # split into 2 lines because pychecker thinks it might return none. files = [ifile for ifile in all_ifiles if ifile.is_match(platform)] return files class InstalledPackage(object): def __init__(self, definition): # looks like: # { url1 : { files: [file1,file2,...], md5sum:... }, # url2 : { files: [file1,file2,...], md5sum:... },... # } self._installed = {} for url in definition: self._installed[url] = definition[url] def urls(self): return self._installed.keys() def files_in(self, url): return self._installed[url].get('files', []) def get_md5sum(self, url): return self._installed[url].get('md5sum', None) def remove(self, url): self._installed.pop(url) def add_files(self, url, files): if url not in self._installed: self._installed[url] = {} self._installed[url]['files'] = files def set_md5sum(self, url, md5sum): if url not in self._installed: self._installed[url] = {} self._installed[url]['md5sum'] = md5sum class Installer(object): def __init__(self, install_filename, installed_filename, dryrun): self._install_filename = install_filename self._install_changed = False self._installed_filename = installed_filename self._installed_changed = False self._dryrun = dryrun self._installables = {} self._licenses = {} self._installed = {} self.load() def load(self): if os.path.exists(self._install_filename): install = llsd.parse(file(self._install_filename, 'rb').read()) try: for name in install['installables']: self._installables[name] = InstallableDefinition( install['installables'][name]) except KeyError: pass try: for name in install['licenses']: self._licenses[name] = LicenseDefinition(install['licenses'][name]) except KeyError: pass if os.path.exists(self._installed_filename): installed = llsd.parse(file(self._installed_filename, 'rb').read()) try: bins = installed['installables'] for name in bins: self._installed[name] = InstalledPackage(bins[name]) except KeyError: pass def _write(self, filename, state): print "Writing state to",filename if not self._dryrun: file(filename, 'wb').write(llsd.format_pretty_xml(state)) def save(self): if self._install_changed: state = {} state['licenses'] = {} for name in self._licenses: state['licenses'][name] = self._licenses[name]._definition #print "self._installables:",self._installables state['installables'] = {} for name in self._installables: state['installables'][name] = \ self._installables[name]._definition self._write(self._install_filename, state) if self._installed_changed: state = {} state['installables'] = {} bin = state['installables'] for name in self._installed: #print "installed:",name,self._installed[name]._installed bin[name] = self._installed[name]._installed self._write(self._installed_filename, state) def is_valid_license(self, bin): "@brief retrun true if we have valid license info for installable." installable = self._installables[bin]._definition if 'license' not in installable: print >>sys.stderr, "No license info found for", bin print >>sys.stderr, 'Please add the license with the', print >>sys.stderr, '--add-installable option. See', \ sys.argv[0], '--help' return False if installable['license'] not in self._licenses: lic = installable['license'] print >>sys.stderr, "Missing license info for '" + lic + "'.", print >>sys.stderr, 'Please add the license with the', print >>sys.stderr, '--add-license option. See', sys.argv[0], print >>sys.stderr, '--help' return False return True def list_installables(self): "Return a list of all known installables." return self._installables.keys() def detail_installable(self, name): "Return a installable definition detail" return self._installables[name]._definition def list_licenses(self): "Return a list of all known licenses." return self._licenses.keys() def detail_license(self, name): "Return a license definition detail" return self._licenses[name]._definition def list_installed(self): "Return a list of installed packages." return self._installed.keys() def _update_field(self, description, field, value, multiline=False): """Given a block and a field name, add or update it. @param description a dict containing all the details of a description. @param field the name of the field to update. @param value the value of the field to update; if omitted, interview will ask for value. @param multiline boolean specifying whether field is multiline or not. """ if value: description[field] = value else: if field in description: print "Update value for '" + field + "'" print "(Leave blank to keep current value)" print "Current Value: '" + description[field] + "'" else: print "Specify value for '" + field + "'" if not multiline: new_value = raw_input("Enter New Value: ") else: print "Please enter " + field + ". End input with EOF (^D)." new_value = sys.stdin.read() if field in description and not new_value: pass elif new_value: description[field] = new_value self._install_changed = True return True def _update_installable(self, name, platform, url, md5sum): """Update installable entry with specific package information. @param installable[in,out] a dict containing installable details. @param platform Platform info, i.e. linux/i686, windows/i686 etc. @param url URL of tar file @param md5sum md5sum of tar file """ installable = self._installables[name]._definition path = platform.split('/') if 'packages' not in installable: installable['packages'] = {} update = installable['packages'] for child in path: if child not in update: update[child] = {} parent = update update = update[child] parent[child]['url'] = llsd.uri(url) parent[child]['md5sum'] = md5sum self._install_changed = True return True def add_installable_package(self, name, **kwargs): """Add an url for a platform path to the installable. @param installable[in,out] a dict containing installable details. """ platform_help_str = """\ Please enter a new package location and url. Some examples: common -- specify a package for all platforms linux -- specify a package for all arch and compilers on linux darwin/universal -- specify a mac os x universal windows/i686/vs/2003 -- specify a windows visual studio 2003 package""" if name not in self._installables: print "Error: must add library with --add-installable or " \ +"--add-installable-metadata before using " \ +"--add-installable-package option" return False else: print "Updating installable '" + name + "'." for arg in ('platform', 'url', 'md5sum'): if not kwargs[arg]: if arg == 'platform': print platform_help_str kwargs[arg] = raw_input("Package "+arg+":") path = kwargs['platform'].split('/') return self._update_installable(name, kwargs['platform'], kwargs['url'], kwargs['md5sum']) def add_installable_metadata(self, name, **kwargs): """Interactively add (only) library metadata into install, w/o adding installable""" if name not in self._installables: print "Adding installable '" + name + "'." self._installables[name] = InstallableDefinition({}) else: print "Updating installable '" + name + "'." installable = self._installables[name]._definition for field in ('copyright', 'license', 'description'): self._update_field(installable, field, kwargs[field]) print "Added installable '" + name + "':" pprint.pprint(self._installables[name]) return True def add_installable(self, name, **kwargs): "Interactively pull a new installable into the install" ret_a = self.add_installable_metadata(name, **kwargs) ret_b = self.add_installable_package(name, **kwargs) return (ret_a and ret_b) def remove_installable(self, name): self._installables.pop(name) self._install_changed = True def add_license(self, name, **kwargs): if name not in self._licenses: print "Adding license '" + name + "'." self._licenses[name] = LicenseDefinition({}) else: print "Updating license '" + name + "'." license = self._licenses[name]._definition for field in ('url', 'text'): multiline = False if field == 'text': multiline = True self._update_field(license, field, kwargs[field], multiline) self._install_changed = True return True def remove_license(self, name): self._licenses.pop(name) self._install_changed = True def _uninstall(self, installables): """@brief Do the actual removal of files work. *NOTE: This method is not transactionally safe -- ie, if it raises an exception, internal state may be inconsistent. How should we address this? @param installables The package names to remove """ remove_file_list = [] for pkgname in installables: for url in self._installed[pkgname].urls(): remove_file_list.extend( self._installed[pkgname].files_in(url)) self._installed[pkgname].remove(url) if not self._dryrun: self._installed_changed = True if not self._dryrun: self._installed.pop(pkgname) remove_dir_set = set() for filename in remove_file_list: print "rm",filename if not self._dryrun: if os.path.exists(filename): remove_dir_set.add(os.path.dirname(filename)) os.remove(filename) for dirname in remove_dir_set: try: os.removedirs(dirname) except OSError: # This is just for cleanup, so we don't care about # normal failures. pass def uninstall(self, installables, install_dir): """@brief Remove the packages specified. @param installables The package names to remove @param install_dir The directory to work from """ print "uninstall",installables,"from",install_dir cwd = os.getcwdu() os.chdir(install_dir) try: self._uninstall(installables) finally: os.chdir(cwd) def _build_ifiles(self, platform, cache_dir): """@brief determine what files to install @param platform The target platform. Eg, windows or linux/i686/gcc/3.3 @param cache_dir The directory to cache downloads. @return Returns the ifiles to install """ ifiles = [] for bin in self._installables: ifiles.extend(self._installables[bin].ifiles(bin, platform, cache_dir)) to_install = [] #print "self._installed",self._installed for ifile in ifiles: if ifile.pkgname not in self._installed: to_install.append(ifile) elif ifile.url not in self._installed[ifile.pkgname].urls(): to_install.append(ifile) elif ifile.md5sum != \ self._installed[ifile.pkgname].get_md5sum(ifile.url): # *TODO: We may want to uninstall the old version too # when we detect it is installed, but the md5 sum is # different. to_install.append(ifile) else: #print "Installation up to date:", # ifile.pkgname,ifile.platform_path pass #print "to_install",to_install return to_install def _install(self, to_install, install_dir): for ifile in to_install: tar = tarfile.open(ifile.filename, 'r') print "Extracting",ifile.filename,"to destination",install_dir if not self._dryrun: # *NOTE: try to call extractall, which first appears # in python 2.5. Phoenix 2008-01-28 try: tar.extractall(path=install_dir) except AttributeError: _extractall(tar, path=install_dir) if ifile.pkgname in self._installed: self._installed[ifile.pkgname].add_files( ifile.url, tar.getnames()) self._installed[ifile.pkgname].set_md5sum( ifile.url, ifile.md5sum) else: # *HACK: this understands the installed package syntax. definition = { ifile.url : {'files': tar.getnames(), 'md5sum' : ifile.md5sum } } self._installed[ifile.pkgname] = InstalledPackage(definition) self._installed_changed = True def install(self, installables, platform, install_dir, cache_dir): """@brief Do the installation for for the platform. @param installables The requested installables to install. @param platform The target platform. Eg, windows or linux/i686/gcc/3.3 @param install_dir The root directory to install into. Created if missing. @param cache_dir The directory to cache downloads. Created if missing. """ # The ordering of steps in the method is to help reduce the # likelihood that we break something. _mkdir(install_dir) _mkdir(cache_dir) to_install = self._build_ifiles(platform, cache_dir) # Filter for files which we actually requested to install. to_install = [ifl for ifl in to_install if ifl.pkgname in installables] for ifile in to_install: ifile.fetch_local() self._install(to_install, install_dir) # # *NOTE: PULLED FROM PYTHON 2.5 tarfile.py Phoenix 2008-01-28 # def _extractall(tar, path=".", members=None): """Extract all members from the archive to the current working directory and set owner, modification time and permissions on directories afterwards. `path' specifies a different directory to extract to. `members' is optional and must be a subset of the list returned by getmembers(). """ directories = [] if members is None: members = tar for tarinfo in members: if tarinfo.isdir(): # Extract directory with a safe mode, so that # all files below can be extracted as well. try: os.makedirs(os.path.join(path, tarinfo.name), 0777) except EnvironmentError: pass directories.append(tarinfo) else: tar.extract(tarinfo, path) # Reverse sort directories. directories.sort(lambda a, b: cmp(a.name, b.name)) directories.reverse() # Set correct owner, mtime and filemode on directories. for tarinfo in directories: path = os.path.join(path, tarinfo.name) try: tar.chown(tarinfo, path) tar.utime(tarinfo, path) tar.chmod(tarinfo, path) except tarfile.ExtractError, e: if tar.errorlevel > 1: raise else: tar._dbg(1, "tarfile: %s" % e) def _mkdir(directory): "Safe, repeatable way to make a directory." if not os.path.exists(directory): os.makedirs(directory) def _get_platform(): "Return appropriate platform packages for the environment." platform_map = { 'darwin': 'darwin', 'linux2': 'linux', 'win32' : 'windows', 'cygwin' : 'windows', 'solaris' : 'solaris' } return platform_map[sys.platform] def _getuser(): "Get the user" try: # Unix-only. import getpass return getpass.getuser() except ImportError: import win32api return win32api.GetUserName() def _default_installable_cache(): """In general, the installable files do not change much, so find a host/user specific location to cache files.""" user = _getuser() cache_dir = "/var/tmp/%s/install.cache" % user if _get_platform() == 'windows': import tempfile cache_dir = os.path.join(tempfile.gettempdir(), \ 'install.cache.%s' % user) return cache_dir def parse_args(): parser = optparse.OptionParser( usage="usage: %prog [options] [installable1 [installable2...]]", formatter = helpformatter.Formatter(), description="""This script fetches and installs installable packages. It also handles uninstalling those packages and manages the mapping between packages and their license. The process is to open and read an install manifest file which specifies what files should be installed. For each installable to be installed. * make sure it has a license * check the installed version ** if not installed and needs to be, download and install ** if installed version differs, download & install If no installables are specified on the command line, then the defaut behavior is to install all known installables appropriate for the platform specified or uninstall all installables if --uninstall is set. You can specify more than one installable on the command line. When specifying a platform, you can specify 'all' to install all packages, or any platform of the form: OS[/arch[/compiler[/compiler_version]]] Where the supported values for each are: OS: darwin, linux, windows, solaris arch: i686, x86_64, ppc, universal compiler: vs, gcc compiler_version: 2003, 2005, 2008, 3.3, 3.4, 4.0, etc. No checks are made to ensure a valid combination of platform parts. Some exmples of valid platforms: windows windows/i686/vs/2005 linux/x86_64/gcc/3.3 linux/x86_64/gcc/4.0 darwin/universal/gcc/4.0 """) parser.add_option( '--dry-run', action='store_true', default=False, dest='dryrun', help='Do not actually install files. Downloads will still happen.') parser.add_option( '--install-manifest', type='string', default=join(base_dir, 'install.xml'), dest='install_filename', help='The file used to describe what should be installed.') parser.add_option( '--installed-manifest', type='string', default=join(base_dir, 'installed.xml'), dest='installed_filename', help='The file used to record what is installed.') parser.add_option( '--export-manifest', action='store_true', default=False, dest='export_manifest', help="Print the install manifest to stdout and exit.") parser.add_option( '-p', '--platform', type='string', default=_get_platform(), dest='platform', help="""Override the automatically determined platform. \ You can specify 'all' to do a installation of installables for all platforms.""") parser.add_option( '--cache-dir', type='string', default=_default_installable_cache(), dest='cache_dir', help='Where to download files. Default: %s'% \ (_default_installable_cache())) parser.add_option( '--install-dir', type='string', default=base_dir, dest='install_dir', help='Where to unpack the installed files.') parser.add_option( '--list-installed', action='store_true', default=False, dest='list_installed', help="List the installed package names and exit.") parser.add_option( '--skip-license-check', action='store_false', default=True, dest='check_license', help="Do not perform the license check.") parser.add_option( '--list-licenses', action='store_true', default=False, dest='list_licenses', help="List known licenses and exit.") parser.add_option( '--detail-license', type='string', default=None, dest='detail_license', help="Get detailed information on specified license and exit.") parser.add_option( '--add-license', type='string', default=None, dest='new_license', help="""Add a license to the install file. Argument is the name of \ license. Specify --license-url if the license is remote or specify \ --license-text, otherwse the license text will be read from standard \ input.""") parser.add_option( '--license-url', type='string', default=None, dest='license_url', help="""Put the specified url into an added license. \ Ignored if --add-license is not specified.""") parser.add_option( '--license-text', type='string', default=None, dest='license_text', help="""Put the text into an added license. \ Ignored if --add-license is not specified.""") parser.add_option( '--remove-license', type='string', default=None, dest='remove_license', help="Remove a named license.") parser.add_option( '--remove-installable', type='string', default=None, dest='remove_installable', help="Remove a installable from the install file.") parser.add_option( '--add-installable', type='string', default=None, dest='add_installable', help="""Add a installable into the install file. Argument is \ the name of the installable to add.""") parser.add_option( '--add-installable-metadata', type='string', default=None, dest='add_installable_metadata', help="""Add package for library into the install file. Argument is \ the name of the library to add.""") parser.add_option( '--installable-copyright', type='string', default=None, dest='installable_copyright', help="""Copyright for specified new package. Ignored if \ --add-installable is not specified.""") parser.add_option( '--installable-license', type='string', default=None, dest='installable_license', help="""Name of license for specified new package. Ignored if \ --add-installable is not specified.""") parser.add_option( '--installable-description', type='string', default=None, dest='installable_description', help="""Description for specified new package. Ignored if \ --add-installable is not specified.""") parser.add_option( '--add-installable-package', type='string', default=None, dest='add_installable_package', help="""Add package for library into the install file. Argument is \ the name of the library to add.""") parser.add_option( '--package-platform', type='string', default=None, dest='package_platform', help="""Platform for specified new package. \ Ignored if --add-installable or --add-installable-package is not specified.""") parser.add_option( '--package-url', type='string', default=None, dest='package_url', help="""URL for specified package. \ Ignored if --add-installable or --add-installable-package is not specified.""") parser.add_option( '--package-md5', type='string', default=None, dest='package_md5', help="""md5sum for new package. \ Ignored if --add-installable or --add-installable-package is not specified.""") parser.add_option( '--list', action='store_true', default=False, dest='list_installables', help="List the installables in the install manifest and exit.") parser.add_option( '--detail', type='string', default=None, dest='detail_installable', help="Get detailed information on specified installable and exit.") parser.add_option( '--uninstall', action='store_true', default=False, dest='uninstall', help="""Remove the installables specified in the arguments. Just like \ during installation, if no installables are listed then all installed \ installables are removed.""") return parser.parse_args() def main(): options, args = parse_args() installer = Installer( options.install_filename, options.installed_filename, options.dryrun) # # Handle the queries for information # if options.list_installed: print "installed list:", installer.list_installed() return 0 if options.list_installables: print "installable list:", installer.list_installables() return 0 if options.detail_installable: try: detail = installer.detail_installable(options.detail_installable) print "Detail on installable",options.detail_installable+":" pprint.pprint(detail) except KeyError: print "Binary '"+options.detail_installable+"' not found in", print "install file." return 0 if options.list_licenses: print "license list:", installer.list_licenses() return 0 if options.detail_license: try: detail = installer.detail_license(options.detail_license) print "Detail on license",options.detail_license+":" pprint.pprint(detail) except KeyError: print "License '"+options.detail_license+"' not defined in", print "install file." return 0 if options.export_manifest: # *HACK: just re-parse the install manifest and pretty print # it. easier than looking at the datastructure designed for # actually determining what to install install = llsd.parse(file(options.install_filename, 'rb').read()) pprint.pprint(install) return 0 # # Handle updates -- can only do one of these # *TODO: should this change the command line syntax? # if options.new_license: if not installer.add_license( options.new_license, text=options.license_text, url=options.license_url): return 1 elif options.remove_license: installer.remove_license(options.remove_license) elif options.remove_installable: installer.remove_installable(options.remove_installable) elif options.add_installable: if not installer.add_installable( options.add_installable, copyright=options.installable_copyright, license=options.installable_license, description=options.installable_description, platform=options.package_platform, url=options.package_url, md5sum=options.package_md5): return 1 elif options.add_installable_metadata: if not installer.add_installable_metadata( options.add_installable_metadata, copyright=options.installable_copyright, license=options.installable_license, description=options.installable_description): return 1 elif options.add_installable_package: if not installer.add_installable_package( options.add_installable_package, platform=options.package_platform, url=options.package_url, md5sum=options.package_md5): return 1 elif options.uninstall: # Do not bother to check license if we're uninstalling. all_installed = installer.list_installed() if not len(args): uninstall_installables = all_installed else: # passed in on the command line. We'll need to verify we # know about them here. uninstall_installables = args for installable in uninstall_installables: if installable not in all_installed: raise RuntimeError('Binary not installed: %s' % (installable,)) installer.uninstall(uninstall_installables, options.install_dir) else: # Determine what installables should be installed. If they were # passed in on the command line, use them, otherwise install # all known installables. all_installables = installer.list_installables() if not len(args): install_installables = all_installables else: # passed in on the command line. We'll need to verify we # know about them here. install_installables = args for installable in install_installables: if installable not in all_installables: raise RuntimeError('Unknown installable: %s' % (installable,)) if options.check_license: # *TODO: check against a list of 'known good' licenses. # *TODO: check for urls which conflict -- will lead to # problems. for installable in install_installables: if not installer.is_valid_license(installable): return 1 # Do the work of installing the requested installables. installer.install( install_installables, options.platform, options.install_dir, options.cache_dir) # save out any changes installer.save() return 0 if __name__ == '__main__': #print sys.argv sys.exit(main())