diff options
Diffstat (limited to 'scripts/install.py')
-rwxr-xr-x | scripts/install.py | 968 |
1 files changed, 715 insertions, 253 deletions
diff --git a/scripts/install.py b/scripts/install.py index 7421d697f6..7368af0b37 100755 --- a/scripts/install.py +++ b/scripts/install.py @@ -11,7 +11,7 @@ https://wiki.lindenlab.com/wiki/User:Phoenix/Library_Installation $LicenseInfo:firstyear=2007&license=mit$ -Copyright (c) 2007, Linden Research, Inc. +Copyright (c) 2007-2009, 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 @@ -33,33 +33,69 @@ THE SOFTWARE. $/LicenseInfo$ """ +import sys +import os.path + +# Look for indra/lib/python in all possible parent directories ... +# This is an improvement over the setup-path.py method used previously: +# * the script may blocated anywhere inside the source tree +# * it doesn't depend on the current directory +# * it doesn't depend on another file being present. + +def add_indra_lib_path(): + root = os.path.realpath(__file__) + # always insert the directory of the script in the search path + dir = os.path.dirname(root) + if dir not in sys.path: + sys.path.insert(0, dir) + + # Now go look for indra/lib/python in the parent dies + while root != os.path.sep: + root = os.path.dirname(root) + dir = os.path.join(root, 'indra', 'lib', 'python') + if os.path.isdir(dir): + if dir not in sys.path: + sys.path.insert(0, dir) + return root + else: + print >>sys.stderr, "This script is not inside a valid installation." + sys.exit(1) + +base_dir = add_indra_lib_path() + import copy -import errno -import md5 import optparse import os +import platform import pprint -import sys +import shutil import tarfile -import urllib +import tempfile +import urllib2 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) +try: + # Python 2.6 + from hashlib import md5 +except ImportError: + # Python 2.5 and earlier + from md5 import new as md5 from indra.base import llsd from indra.util import helpformatter +# *HACK: Necessary for python 2.3. Consider removing this code wart +# after etch has deployed everywhere. 2008-12-23 Phoenix +try: + sorted = sorted +except NameError: + def sorted(in_list): + "Return a list which is a sorted copy of in_list." + # Copy the source to be more functional and side-effect free. + out_list = copy.copy(in_list) + out_list.sort() + return out_list + 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): @@ -73,18 +109,18 @@ class InstallFile(object): def __str__(self): return "ifile{%s:%s}" % (self.pkgname, self.url) - def _is_md5_match(self): - hasher = md5.new(file(self.filename).read()) + def _is_md5sum_match(self): + hasher = md5(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, win32 or linux/i686/gcc/3.3 -@return Returns True if the ifile is in the 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 == 'common': + if self.platform_path[0] == 'common': return True req_platform_path = platform.split('/') #print "platform:",req_platform_path @@ -96,21 +132,20 @@ class InstallFile(object): return False #print "match!" return True - def fetch_local(self): - print "Looking for:",self.filename + #print "Looking for:",self.filename if not os.path.exists(self.filename): - print "not there -- fetching" - elif self.md5sum and not self._is_md5_match(): - print "Found, but md5 does not match." + pass + elif self.md5sum and not self._is_md5sum_match(): + print "md5 mismatch:", self.filename os.remove(self.filename) else: - print "Found matching package" + 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_md5_match(): + file(self.filename, 'wb').write(urllib2.urlopen(self.url).read()) + if self.md5sum and not self._is_md5sum_match(): raise RuntimeError("Error matching md5 for %s" % self.url) class LicenseDefinition(object): @@ -121,9 +156,9 @@ class LicenseDefinition(object): # blessed : ... # } self._definition = definition - -class BinaryDefinition(object): + +class InstallableDefinition(object): def __init__(self, definition): #probably looks like: # { packages : {platform...}, @@ -138,7 +173,7 @@ class BinaryDefinition(object): def _ifiles_from_path(self, tree, pkgname, cache_dir, path): ifiles = [] - if tree.has_key('url'): + if 'url' in tree: ifiles.append(InstallFile( pkgname, tree['url'], @@ -159,10 +194,10 @@ class BinaryDefinition(object): 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, win32 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 + @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 [] @@ -172,13 +207,16 @@ class BinaryDefinition(object): cache_dir) if platform == 'all': return all_ifiles - return [ifile for ifile in all_ifiles if ifile.is_match(platform)] + #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 : [file1,file2,...], - # url2 : [file1,file2,...],... + # { url1 : { files: [file1,file2,...], md5sum:... }, + # url2 : { files: [file1,file2,...], md5sum:... },... # } self._installed = {} for url in definition: @@ -188,13 +226,23 @@ class InstalledPackage(object): return self._installed.keys() def files_in(self, url): - return self._installed[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): - self._installed[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): @@ -203,18 +251,18 @@ class Installer(object): self._installed_filename = installed_filename self._installed_changed = False self._dryrun = dryrun - self._binaries = {} + 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).read()) + install = llsd.parse(file(self._install_filename, 'rb').read()) try: - for name in install['binaries']: - self._binaries[name] = BinaryDefinition( - install['binaries'][name]) + for name in install['installables']: + self._installables[name] = InstallableDefinition( + install['installables'][name]) except KeyError: pass try: @@ -223,9 +271,9 @@ class Installer(object): except KeyError: pass if os.path.exists(self._installed_filename): - installed = llsd.parse(file(self._installed_filename).read()) + installed = llsd.parse(file(self._installed_filename, 'rb').read()) try: - bins = installed['binaries'] + bins = installed['installables'] for name in bins: self._installed[name] = InstalledPackage(bins[name]) except KeyError: @@ -234,7 +282,7 @@ class Installer(object): def _write(self, filename, state): print "Writing state to",filename if not self._dryrun: - file(filename, 'w').write(llsd.format_xml(state)) + file(filename, 'wb').write(llsd.format_pretty_xml(state)) def save(self): if self._install_changed: @@ -242,115 +290,186 @@ class Installer(object): state['licenses'] = {} for name in self._licenses: state['licenses'][name] = self._licenses[name]._definition - #print "self._binaries:",self._binaries - state['binaries'] = {} - for name in self._binaries: - state['binaries'][name] = self._binaries[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['binaries'] = {} - bin = state['binaries'] + 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_license_info_valid(self): - valid = True - for bin in self._binaries: - binary = self._binaries[bin]._definition - if not binary.has_key('license'): - valid = False - print >>sys.stderr, "No license info for binary", bin + '.' - continue - if binary['license'] not in self._licenses: - valid = False - lic = binary['license'] - print >>sys.stderr, "Missing license info for '" + lic + "'", - print >>sys.stderr, 'in binary', bin + '.' - return valid - - def detail_binary(self, name): - "Return a binary definition detail" - try: - detail = self._binaries[name]._definition - return detail - except KeyError: - return None + 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 sorted(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 sorted(self._licenses.keys()) + + def detail_license(self, name): + "Return a license definition detail" + return self._licenses[name]._definition - def _update_field(self, binary, field): + def list_installed(self): + "Return a list of installed packages." + return sorted(self._installed.keys()) + + def detail_installed(self, name): + "Return file list for specific installed package." + filelist = [] + for url in self._installed[name]._installed.keys(): + filelist.extend(self._installed[name].files_in(url)) + return filelist + + def _update_field(self, description, field, value, multiline=False): """Given a block and a field name, add or update it. - @param binary[in,out] a dict containing all the details about a binary. + @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 binary.has_key(field): - print "Update value for '" + field + "'" - print "(Leave blank to keep current value)" - print "Current Value: '" + binary[field] + "'" + if value: + description[field] = value else: - print "Specify value for '" + field + "'" - value = raw_input("Enter New Value: ") - if binary.has_key(field) and not value: - pass - elif value: - binary[field] = value + 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() - def _add_package(self, binary): - """Add an url for a platform path to the binary. - @param binary[in,out] a dict containing all the details about a binary.""" - print """\ -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 -win32/i686/vs/2003 -- specify a windows visual studio 2003 package""" - target = raw_input("Package path: ") - url = raw_input("Package URL: ") - md5sum = raw_input("Package md5: ") - path = target.split('/') - if not binary.has_key('packages'): - binary['packages'] = {} - update = binary['packages'] + 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 not update.has_key(child): + if child not in update: update[child] = {} parent = update update = update[child] parent[child]['url'] = llsd.uri(url) parent[child]['md5sum'] = md5sum - def adopt_binary(self, name): - "Interactively pull a new binary into the install" - if not self._binaries.has_key(name): - print "Adding binary '" + name + "'." - self._binaries[name] = BinaryDefinition({}) + 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 binary '" + name + "'." - binary = self._binaries[name]._definition + print "Updating installable '" + name + "'." + installable = self._installables[name]._definition for field in ('copyright', 'license', 'description'): - self._update_field(binary, field) - self._add_package(binary) - print "Adopted binary '" + name + "':" - pprint.pprint(self._binaries[name]) - self._install_changed = True + self._update_field(installable, field, kwargs[field]) + print "Added installable '" + name + "':" + pprint.pprint(self._installables[name]) + return True - def orphan_binary(self, name): - self._binaries.pop(name) + 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, text, url): - if self._licenses.has_key(name): - print "License '" + name + "' being overwritten." - definition = {} - if url: - definition['url'] = url - if not url and text is None: - print "Please enter license text. End input with EOF (^D)." - text = sys.stdin.read() - definition['text'] = text - self._licenses[name] = LicenseDefinition(definition) + 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 + "'." + the_license = self._licenses[name]._definition + for field in ('url', 'text'): + multiline = False + if field == 'text': + multiline = True + self._update_field(the_license, field, kwargs[field], multiline) self._install_changed = True return True @@ -358,52 +477,91 @@ win32/i686/vs/2003 -- specify a windows visual studio 2003 package""" self._licenses.pop(name) self._install_changed = True - def _determine_install_set(self, ifiles): - """@brief determine what to install -@param ifiles A list of InstallFile instances which are necessary for this install -@return Returns the tuple (ifiles to install, ifiles to remove)""" - installed_list = [] - for package in self._installed: - installed_list.extend(self._installed[package].urls()) - installed_set = set(installed_list) - #print "installed_set:",installed_set - install_list = [ifile.url for ifile in ifiles] - install_set = set(install_list) - #print "install_set:",install_set - remove_set = installed_set.difference(install_set) - to_remove = [ifile for ifile in ifiles if ifile.url in remove_set] - #print "to_remove:",to_remove - install_set = install_set.difference(installed_set) - to_install = [ifile for ifile in ifiles if ifile.url in install_set] - #print "to_install:",to_install - return to_install, to_remove - - def _build_ifiles(self, platform, cache_dir): - """@brief determine what files to install and remove -@param platform The target platform. Eg, win32 or linux/i686/gcc/3.3 -@param cache_dir The directory to cache downloads. -@return Returns the tuple (ifiles to install, ifiles to remove)""" - ifiles = [] - for bin in self._binaries: - ifiles.extend(self._binaries[bin].ifiles(bin, platform, cache_dir)) - return self._determine_install_set(ifiles) - - def _remove(self, to_remove): + 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 ifile in to_remove: - remove_file_list.extend( - self._installed[ifile.pkgname].files_in(ifile.url)) - self._installed[ifile.pkgname].remove(ifile.url) - self._installed_changed = True + 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: - os.remove(filename) + if os.path.exists(filename): + remove_dir_set.add(os.path.dirname(filename)) + try: + os.remove(filename) + except OSError: + # This is just for cleanup, so we don't care + # about normal failures. + pass + 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 + print "Extracting",ifile.filename,"to",install_dir if not self._dryrun: # *NOTE: try to call extractall, which first appears # in python 2.5. Phoenix 2008-01-28 @@ -411,32 +569,154 @@ win32/i686/vs/2003 -- specify a windows visual studio 2003 package""" tar.extractall(path=install_dir) except AttributeError: _extractall(tar, path=install_dir) - if self._installed.has_key(ifile.pkgname): - self._installed[ifile.pkgname].add_files(ifile.url, tar.getnames()) + 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 : tar.getnames() } + definition = { ifile.url : + {'files': tar.getnames(), + 'md5sum' : ifile.md5sum } } self._installed[ifile.pkgname] = InstalledPackage(definition) self._installed_changed = True - def do_install(self, platform, install_dir, cache_dir): + def install(self, installables, platform, install_dir, cache_dir): """@brief Do the installation for for the platform. -@param platform The target platform. Eg, win32 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.""" - if not self._binaries: - raise RuntimeError("No binaries to install. Please add them.") + @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. + install_dir = os.path.realpath(install_dir) + cache_dir = os.path.realpath(cache_dir) _mkdir(install_dir) _mkdir(cache_dir) - to_install, to_remove = self._build_ifiles(platform, cache_dir) + to_install = self._build_ifiles(platform, cache_dir) - # we do this in multiple steps reduce the likelyhood to have a - # bad install. + # 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._remove(to_remove) self._install(to_install, install_dir) + def do_install(self, installables, platform, install_dir, cache_dir=None, + check_license=True, scp=None): + """Determine what installables should be installed. If they were + passed in on the command line, use them, otherwise install + all known installables. + """ + if not cache_dir: + cache_dir = _default_installable_cache() + all_installables = self.list_installables() + if not len(installables): + install_installables = all_installables + else: + # passed in on the command line. We'll need to verify we + # know about them here. + install_installables = installables + for installable in install_installables: + if installable not in all_installables: + raise RuntimeError('Unknown installable: %s' % + (installable,)) + if 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 self.is_valid_license(installable): + return 1 + + # Set up the 'scp' handler + opener = urllib2.build_opener() + scp_or_http = SCPOrHTTPHandler(scp) + opener.add_handler(scp_or_http) + urllib2.install_opener(opener) + + # Do the work of installing the requested installables. + self.install( + install_installables, + platform, + install_dir, + cache_dir) + scp_or_http.cleanup() + + def do_uninstall(self, installables, install_dir): + # Do not bother to check license if we're uninstalling. + all_installed = self.list_installed() + if not len(installables): + uninstall_installables = all_installed + else: + # passed in on the command line. We'll need to verify we + # know about them here. + uninstall_installables = installables + for installable in uninstall_installables: + if installable not in all_installed: + raise RuntimeError('Installable not installed: %s' % + (installable,)) + self.uninstall(uninstall_installables, install_dir) + +class SCPOrHTTPHandler(urllib2.BaseHandler): + """Evil hack to allow both the build system and developers consume + proprietary binaries. + To use http, export the environment variable: + INSTALL_USE_HTTP_FOR_SCP=true + """ + def __init__(self, scp_binary): + self._scp = scp_binary + self._dir = None + + def scp_open(self, request): + #scp:codex.lindenlab.com:/local/share/install_pkgs/package.tar.bz2 + remote = request.get_full_url()[4:] + if os.getenv('INSTALL_USE_HTTP_FOR_SCP', None) == 'true': + return self.do_http(remote) + try: + return self.do_scp(remote) + except: + self.cleanup() + raise + + def do_http(self, remote): + url = remote.split(':',1) + if not url[1].startswith('/'): + # in case it's in a homedir or something + url.insert(1, '/') + url.insert(0, "http://") + url = ''.join(url) + print "Using HTTP:",url + return urllib2.urlopen(url) + + def do_scp(self, remote): + if not self._dir: + self._dir = tempfile.mkdtemp() + local = os.path.join(self._dir, remote.split('/')[-1:][0]) + command = [] + for part in (self._scp, remote, local): + if ' ' in part: + # I hate shell escaping. + part.replace('\\', '\\\\') + part.replace('"', '\\"') + command.append('"%s"' % part) + else: + command.append(part) + #print "forking:", command + rv = os.system(' '.join(command)) + if rv != 0: + raise RuntimeError("Cannot fetch: %s" % remote) + return file(local, 'rb') + + def cleanup(self): + if self._dir: + shutil.rmtree(self._dir) + # # *NOTE: PULLED FROM PYTHON 2.5 tarfile.py Phoenix 2008-01-28 @@ -485,43 +765,79 @@ def _extractall(tar, path=".", members=None): def _mkdir(directory): "Safe, repeatable way to make a directory." - try: + if not os.path.exists(directory): os.makedirs(directory) - except OSError, e: - if e[0] != errno.EEXIST: - raise def _get_platform(): "Return appropriate platform packages for the environment." platform_map = { 'darwin': 'darwin', 'linux2': 'linux', - 'win32' : 'win32', - 'cygwin' : 'win32', + 'win32' : 'windows', + 'cygwin' : 'windows', 'solaris' : 'solaris' } - return platform_map[sys.platform] - -def main(): + this_platform = platform_map[sys.platform] + if this_platform == 'linux': + if platform.architecture()[0] == '64bit': + # TODO -- someday when install.py accepts a platform of the form + # os/arch/compiler/compiler_version then we can replace the + # 'linux64' platform with 'linux/x86_64/gcc/4.1' + this_platform = 'linux' + return this_platform + +def _getuser(): + "Get the user" + try: + # Unix-only. + import getpass + return getpass.getuser() + except ImportError: + import ctypes + MAX_PATH = 260 # according to a recent WinDef.h + name = ctypes.create_unicode_buffer(MAX_PATH) + namelen = ctypes.c_int(len(name)) # len in chars, NOT bytes + if not ctypes.windll.advapi32.GetUserNameW(name, ctypes.byref(namelen)): + raise ctypes.WinError() + return name.value + +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': + cache_dir = os.path.join(tempfile.gettempdir(), \ + 'install.cache.%s' % user) + return cache_dir + +def parse_args(): parser = optparse.OptionParser( - usage="usage: %prog [options]", + usage="usage: %prog [options] [installable1 [installable2...]]", formatter = helpformatter.Formatter(), - description="""This script fetches and installs binary packages. + 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 file in the manifest: +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, win32, solaris +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. @@ -529,8 +845,8 @@ 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: -win32 -win32/i686/vs/2005 +windows +windows/i686/vs/2005 linux/x86_64/gcc/3.3 linux/x86_64/gcc/4.0 darwin/universal/gcc/4.0 @@ -544,28 +860,35 @@ darwin/universal/gcc/4.0 parser.add_option( '--install-manifest', type='string', - default=join(base_dir, 'install.xml'), + default=os.path.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'), + default=os.path.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 complete installation of all binaries.""") +You can specify 'all' to do a installation of installables for all platforms.""") parser.add_option( '--cache-dir', type='string', - default=join(base_dir, '.install.cache'), + default=_default_installable_cache(), dest='cache_dir', - help='Where to download files.') + help='Where to download files. Default: %s'% \ + (_default_installable_cache())) parser.add_option( '--install-dir', type='string', @@ -573,12 +896,30 @@ You can specify 'all' to do a complete installation of all binaries.""") 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, @@ -588,12 +929,6 @@ 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( - '--remove-license', - type='string', - default=None, - dest='remove_license', - help="Remove a named license.") - parser.add_option( '--license-url', type='string', default=None, @@ -608,33 +943,117 @@ Ignored if --add-license is not specified.""") help="""Put the text into an added license. \ Ignored if --add-license is not specified.""") parser.add_option( - '--orphan', + '--remove-license', + type='string', + default=None, + dest='remove_license', + help="Remove a named license.") + parser.add_option( + '--remove-installable', type='string', default=None, - dest='orphan', - help="Remove a binary from the install file.") + dest='remove_installable', + help="Remove a installable from the install file.") parser.add_option( - '--adopt', + '--add-installable', type='string', default=None, - dest='adopt', - help="""Add a binary into the install file. Argument is the name of \ -the binary to add.""") + 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_binaries', - help="List the binaries in the install manifest") + 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( - '--details', + '--detail-installed', type='string', default=None, - dest='detail_binary', - help="Get detailed information on specified binary.") - options, args = parser.parse_args() + dest='detail_installed', + help="Get list of files for specified installed 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.""") + parser.add_option( + '--scp', + type='string', + default='scp', + dest='scp', + help="Specify the path to your scp program.") + return parser.parse_args() +def main(): + options, args = parse_args() installer = Installer( options.install_filename, options.installed_filename, @@ -643,18 +1062,51 @@ the binary to add.""") # # Handle the queries for information # - if options.list_binaries: - print "binary list:",installer._binaries.keys() + if options.list_installed: + print "installed list:", installer.list_installed() return 0 - if options.detail_binary: - detail = installer.detail_binary(options.detail_binary) - if detail: - print "Detail on binary",options.detail_binary+":" + 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) - else: - print "Bianry '"+options.detail_binary+"' not found in", + except KeyError: + print "Installable '"+options.detail_installable+"' not found in", + print "install file." + return 0 + if options.detail_installed: + try: + detail = installer.detail_installed(options.detail_installed) + #print "Detail on installed",options.detail_installed+":" + for line in detail: + print line + except: + raise + print "Installable '"+options.detail_installed+"' 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 @@ -663,38 +1115,48 @@ the binary to add.""") if options.new_license: if not installer.add_license( options.new_license, - options.license_text, - options.license_url): + text=options.license_text, + url=options.license_url): return 1 elif options.remove_license: installer.remove_license(options.remove_license) - elif options.orphan: - installer.orphan_binary(options.orphan) - elif options.adopt: - if not installer.adopt_binary(options.adopt): + 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: + installer.do_uninstall(args, options.install_dir) else: - if options.check_license: - if not installer.is_license_info_valid(): - print >>sys.stderr, 'Please add or correct the license', - print >>sys.stderr, 'information in', - print >>sys.stderr, options.install_filename + '.' - print >>sys.stderr, "You can also use the --add-license", - print >>sys.stderr, "option. See", sys.argv[0], "--help" - return 1 - - # *TODO: check against a list of 'known good' licenses. - # *TODO: check for urls which conflict -- will lead to - # problems. - - installer.do_install( - options.platform, - options.install_dir, - options.cache_dir) + installer.do_install(args, options.platform, options.install_dir, + options.cache_dir, options.check_license, + options.scp) # save out any changes installer.save() return 0 if __name__ == '__main__': + #print sys.argv sys.exit(main()) |