#!/usr/bin/env python """ The main top-level ``ape`` script to manage software packages. Please consult the :ref:`user-manual` on how to use ``ape``. """ import os import os.path # @UnusedImport (pydev bug) import sys import optparse import textwrap import ApePackages # @UnusedImport (init packages) from ApeTools import Config from ApeTools import Fetch from ApeTools import Build from ApeTools import InstallError from ApeTools import Environment from ApeTools.Version import fullVersion class UsageError(RuntimeError): """ Class to allow commands to report errors. This error will be caught to produce help and/or a message. """ pass def abort(error, parser=None, printHelp=False): """Abort the processing in the case of an error. This function can optionally print usage information for the script. :param error: The error message to report. :param parser: An :class:`optparse.OptionParser` instance. If present, use it to provide usage information or help. :param printHelp: Flag to indicate if to print the full help information or just a sort usage information. """ if parser is not None: if printHelp: parser.print_help(sys.stderr) print >>sys.stderr else: parser.print_usage(sys.stderr) print >>sys.stderr, error sys.exit(1) def expandDependencies(packages, expand=True): """Expands a list of packages to include dependencies if requested. If *expand* is ``False``, return the original list unchanged. """ if expand: return Build.DependencyTree(packages).buildList() else: return packages def listInstalled(): """Return a list of all packages which are currenly installed. """ return [p for p in Build.getPackages() if p.isInstalled()] def packageEnv(packages=None): """Return an :class:`ApeTools.Environment.Environment`` instance with is populated with the environment variables provided by all the packages listed in *packages*. If *packages* is not given, build environment for currently installed. Helper to emit instructions for setting the [c]sh environment. """ if not packages: raise UsageError("Package name(s) required to emit environment.") packages = expandDependencies(packages) installedPackages = listInstalled() ghosts = [p for p in packages if p not in installedPackages and p.env()] if ghosts: raise InstallError(args=["Unsatisfied dependencies", "Cannot emit environment"], pack=ghosts) env = Environment.Environment(baseEnv=os.environ) map(env.update, [p.env() for p in packages]) return env def haveToInstall(pack, options, listed): """Determine if a package has to be installed or if its installation can be safely skipped. A package has to be installed if one of the following conditions holds: - The package is not yet installed. - The option :option:`--force-all` has be specified. - The option :option:`--force` has be specified and the package is listed in *listed*. This way, the option :option:`--force` only acts on packages explicitly requested but not on their dependencies. - The package attribute :attr:`alwaysInstall` is set to ``True``. """ return options.forceAll or \ (options.force and pack in listed) or \ not pack.isInstalled() or \ pack.alwaysInstall class DescriptionFormatter(optparse.IndentedHelpFormatter): """A formatter for :class:`optparse.OptionParse` object. """ def format_description(self, description): """If *description* empty, return "", else return description with "\\\\n". """ if description: return description + "\n" else: return "" class Dispatch: """Dispatch handler for ``ape``\ 's actions. """ def __init__(self): self.actions = {} self.descriptions = [] class DispatchError(UsageError): """Exception, so we can catch dispatch errors selectively. """ pass def addAction(self, name, action, description=""): """Add callable *action* to use when requesting *name*. Provides *description* for help. """ self.actions[name] = action self.descriptions.append((name, description)) def call(self, name, *args): """Call action *name* with arguments \*\ *args*. :raises: :exc:`Dispatch.DispatchError` if *name* not registered. """ try: self.actions[name](*args) except KeyError: raise Dispatch.DispatchError("Unknown action requested: %s" % name) def getDescriptions(self): """Generate a short explanation from the descriptions of all actions. The descriptions will be listed in the order in which the actions where added. The names are listed on the left. The space for the names is determined by the longest name in the list. The description for each action is wrapped to fit into the remaining space. The result is inteded to be passed as a description to an :class:`optparse.OptionParser` instance which uses an instance of :class:`DescriptionFormatter` as its formatter. """ result = ["The verb selects the action performed by the script. " "Valid actions are:"] indent = max([len(d[0]) for d in self.descriptions]) extraSpace = 4 leader = " " * (indent + extraSpace) for d in self.descriptions: fullDescription = " %-*s %s" % ((indent,) + d) result.extend(textwrap.wrap(fullDescription, 75, subsequent_indent=leader)) return '\n'.join(result) class Ape: """This class bundles methods implementing the individual actions of the ``ape`` script and a few shared attributed. """ def __init__(self): self.downloadManager = None def getDownloadManager(self): """Provide access to the :class:`ApeTools.Fetch.DownloadManager`` instance we use. Create it if necessary. """ if self.downloadManager is None: self.downloadManager = Fetch.DownloadManager() return self.downloadManager def install(self, packages): """Install the *packages* and their dependencies. Download if needed. If a package is already installed, don't install again, unless :option:`--force`\ ed. """ if not packages: abort("Ape: install needs at least one package name. None given.") installPackages = expandDependencies(packages) print "The following packages are requested:" for pack in installPackages: if pack.isDummy(): continue print pack.name if self.options.verbose: print " Version:", pack.version print " Prefix:", pack.prefix print "... Looking for sources" for pack in installPackages: if haveToInstall(pack, self.options, packages) and \ not pack.isDummy(): print " ... Sources for", pack.name pack.fetch(self.getDownloadManager()) for pack in installPackages: # reset the environment (and add environment from dependencies) env = Environment.Environment(baseEnv=os.environ) for dep in Build.DependencyTree([pack]).buildList()[:-1]: env.update(dep.env()) if pack.isDummy(): continue # find if not haveToInstall(pack, self.options, packages): print "... Package already installed: %s" % pack.name continue else: print "... Building", pack.name print " ... Unpacking", pack.name pack.unpack() # build and install print " ... Installing", pack.name pack.build(env) def fetch(self, packages): """Download the *packages*. Unless disabled, also fetches the dependencies. """ myPackages = expandDependencies(packages, expand=self.options.dependencies) for p in myPackages: print " ... Fetching", p.name p.fetch(self.getDownloadManager()) def unpack(self, packages): """Unpack the *packages*. Unless disabled, also unpacks the dependencies. """ myPackages = expandDependencies(packages, expand=self.options.dependencies) for p in myPackages: if not p.isDummy(): print "... Unpacking", p.name p.unpack() def clean(self, packages): """Clean up build directories of *packages*. If *package* is empty, cleans up build directories of all installed packages. """ if not packages: packages = listInstalled() print "... Cleaning" for p in packages: print" ... Considering", p.name p.clean() def packages(self, packages): """Provide a list of packages known to ``ape``. If *packages* is empty, list all packages known. Otherwise, provide information only on those listed in *packages*. """ if packages: print "Packages:" else: print "Available packages:" packages = Build.getPackages() for p in packages: print " ", p.name, if self.options.verbose: print "(%s)" % p.version else: print if self.options.dependencies: fullList = expandDependencies([p]) if len(fullList) > 1: print " -->", " ".join([p.name for p in fullList]) def installed(self, _packages): """List the packages which are currently installed. """ print "Installed packages:" for p in listInstalled(): print " ", p.name, if self.options.verbose: print "(%s)" % p.version else: print def packageConfig(self, packages, outFile=sys.stdout): """Provide information about the configuration of packages to *outFile*. If the list *packages* is empty, provide information about all known packages. Otherwise, provide information only on those in *packages*. """ if not packages: packages = Build.getPackages() for p in packages: print >>outFile, "\n".join(p.info()) def envSh(self, packages): """Print a list of instructions which can be :command:`source`\ ed to provide the environment variables defined by the currently installed packages to the user. This version is for the *Bourne-shell* family of shells. """ print packageEnv(packages).sh() def envCsh(self, packages): """Print a list of instructions which can be :command:`source`\ ed to provide the environment variables defined by the currently installed packages to the user. This version is for the *C-shell* family of shells. """ print packageEnv(packages).csh() def mirrors(self, _packages): """Print a list of known download-mirrors. """ print "Available mirror sites:" for m in self.getDownloadManager().mirrorList: print "* Tag:", m.tag print " ", m.location print " ", m.url def apeConfig(self, _packages, outFile=sys.stdout): """Provide information about the configuration of ``ape`` to *outFile*. """ for v in "home user base build logs distfiles mirrors jobs preDependencies".split(): print >>outFile, "%s: %s" % (v, Config.get("ape", v)) specials = Build.packageCreators.keys() print >>outFile, "Packages with special code (%i): %s" % \ (len(specials), ", ".join(specials)) def dumpConfig(self, _packages): """Write the configuration information of ``ape`` and of the *packages* to logfile. """ name = os.path.join(Config.get("ape", "logs"), "ape-dump.apelog") print "Dumping configuration data to", name log = open(name, "w") self.apeConfig([], log) self.packageConfig([], log) #: Dictionary mapping verbs to methods implementing # the corresponding action. dispatch = Dispatch() dispatch.addAction("install", install, """Install one or several packages. Install the package dependencies first.""") dispatch.addAction("fetch", fetch, """Download one or several packages. Also fetches all dependencies.""") dispatch.addAction("unpack", unpack, "Unpack one or several packages " \ "and their dependencies.") dispatch.addAction("clean", clean, "Clean up build directories.") dispatch.addAction("installed", installed, "List of installed packages.") dispatch.addAction("mirrors", mirrors, "List data mirror sites.") dispatch.addAction("packages", packages, "List packages, with dependencies.") dispatch.addAction("sh", envSh, "Provide shell instructions to set environment.") dispatch.addAction("csh", envCsh, "Provide c-shell instructions to set environment.") dispatch.addAction("package-config", packageConfig, "Dump package configuration (for debug).") dispatch.addAction("ape-config", apeConfig, "Dump ape configuration.") dispatch.addAction("dump-config", dumpConfig, "Write package and ape configuration to log file.") def main(self): """The main routine for ``ape``. It handles command-line parsing, validation, and dispatch according to the requested action. """ aparser = optparse.OptionParser( usage="%prog [options] verb [package...]", version=("Auger Package Environment (APE) " + fullVersion), formatter=DescriptionFormatter(), description=self.dispatch.getDescriptions() ) aparser.add_option("-f", "--force", action="store_true", default=False, help="Force action for packages listed " \ "on the command line.") aparser.add_option("--force-all", action="store_true", dest="forceAll", default=False, help="Force action for all packages.") aparser.add_option("-x", "--no-dependencies", dest="dependencies", action="store_false", default=True, help="Do not process the dependencies " \ "of the named packages") Config.addOptions(aparser) (self.options, args) = aparser.parse_args() try: Config.init(self.options) except RuntimeError, e: abort("Initialization error: %s" % str(e), aparser, printHelp=True) try: jobs = Config.getint("ape", "jobs") except ValueError: abort("Invalid specification for number of jobs.") if jobs < 0: abort("Number of jobs has to be positive, found %i." % jobs) try: verb = args[0] except IndexError: abort("Missing verb to specify action.", aparser, printHelp=True) Build.getPackages() packageNames = args[1:] if packageNames: try: packages = Build.getPackages(*packageNames) except InstallError, e: abort(str(e) + "\nUnknown package: %s" % e.pack, aparser) else: packages = [] if not os.path.exists(Config.get("ape", "logs")): os.makedirs(Config.get("ape", "logs")) try: self.dispatch.call(verb, self, packages) except InstallError, error: log = open(os.path.join(Config.get("ape", "logs"), "ape-failure.apelog"), "w") print >>log, str(error) log.close() abort(str(error)) except UsageError, error: abort(str(error), aparser, True) except KeyboardInterrupt: abort("\nApe: interrupted by user") if __name__ == "__main__": Ape().main()