""" Sub-module :mod:`Build` ======================= This module provides the general tools for package building, dependency analysis, and a registry of known packages. """ import sys import os import os.path # @UnusedImport (pydev bug) import re import tarfile import shutil import shlex import operator import ConfigParser import subprocess import errno import types from string import Template from ApeTools import Config, InstallError patchRe = re.compile(r".*\.patch") def call(args, cwd=None, output=None, env=None, append=False, package=None, stage=None, useException=True): """Wrapper to execute command in sub-process. We always tie ``stdout`` and ``stderr`` together for logging, so we have only one output argument. :param args: Command to execute :type args: list or string :param cwd: Directory to execute the command in :param output: Name of output file. Write to `sys.stdout` if not set. :param env: Enviroment to execute command in :param append: Flag to determine if to overwrite or append to output file :param package: Name of package being worked on. Leave empty for global processing. :param stage: Processing stage :param useException: Flag to determine if to report failure by throwing a :exc:`ApeTools.InstallError` or by returning a non-zero return code. :raises: :exc:`ApeTools.InstallError` is used to report the case when the return code of the command is not 0. This can be suppressed setting *useException* to `False`. :returns: The return code of the command executed. A non-zero result will only be returned when *useException* is set to `False`. """ if not cwd: cwd = os.getcwd() if output: if append: mode = "a" else: mode = "w" outputFile = open(output, mode) else: outputFile = sys.stdout if not env: env = os.environ # needed for older version of python, at least 2.4 if type(env) != dict: env = dict(env) if type(args) == str: args = shlex.split(args) args = [Template(a).safe_substitute(env) for a in args] hostname = os.uname()[1] outputFile.write("ape >>> on host %s\n" % hostname) outputFile.write("ape >>> %s \n" % " ".join(args)) outputFile.flush() if Config.getboolean("ape", "verbose"): print " Executing:", " ".join(args) print " in directory", cwd if Config.getboolean("ape", "dryRun"): return 0 try: rc = subprocess.call(args, cwd=cwd, stdout=outputFile, stderr=subprocess.STDOUT, env=env) except OSError, e: if e.errno == errno.ENOENT: rc = 1 # for compatibility with popen2; shell returns 127 else: raise if output: outputFile.close() if rc and useException: raise InstallError(cwd=cwd, pack=package, stage=stage, log=output, cmd=args) return rc def untar(arName, outDir): """Untar an archive by wrapping system calls to tar (for old pythons) :param arName: name of the tarball to be extracted :param outDir: location of the extracted output """ arName = arName.strip() bzp = re.compile(".*bz2$") gzp = re.compile(".*gz$") cmd = ["tar"] if bzp.match(arName): cmd.append("-xjf") elif gzp.match(arName): cmd.append("-xzf") else: raise Exception("untar: archive must be bzip2 or gzip compressed") cmd.append(arName) cmd.append("-C") cmd.append(outDir) return call(cmd, output="/dev/null") packageCreators = {} packageInstances = {} def registerPackage(name, creator): """Register a callable object :param name: Package name :param creator: Callable object to create *name* """ packageCreators[name] = creator def getPackage(name): """Get an instance of :class:`Package` or a sub-class. The class is meant to handle the building of *name*. This function returns an existing instance if possible. Otherwise, it tries to create a new one. The instance is created calling a callable object registered with, :func:`registerPackage`, typically a sub-class of :class:`Package`. The name used to locate the creator is given by the *builder* configuration variable for the package *name*. If *name* is not a string, we assume it is already a package and return it as is. :raises: :exc:`ApeTools.InstallError` if the package name or the associated builder are unkown. """ if not isinstance(name, types.StringTypes): return name try: return packageInstances[name] except KeyError: try: creatorName = Config.get("package %s" % name, "builder", ["package"]) except ConfigParser.NoSectionError: raise InstallError(["Unknown package requested."], pack=name, stage="Startup") try: creator = packageCreators[creatorName] except KeyError: raise InstallError(["Cannot find code to handle package."], pack=name, stage="Startup") instance = creator(name) packageInstances[name] = instance return instance def getPackageNames(): """Returns the list of all package names configured, except hidden packages. """ sectionPairs = [(s, s.split()[1]) for s in Config.sections() if s.startswith("package ")] return [s[1] for s in sectionPairs if not Config.getboolean(s[0], "isHidden", ["package"])] def getPackages(*args): """Returns instances for all packages requested. If no argument is passed, return instances of all configured packages. :param args: The list of packages requested as individual arguments. """ if args: names = args else: names = getPackageNames() return [getPackage(n) for n in names] class CircularDependencyError(InstallError): """Exception thrown when a circular dependency is detected. """ def __init__(self, name): InstallError.__init__(self, args=["Circular dependency"], pack=name) class DependencyNode: """A node in the dependency tree of packages. We separate *strong* and *weak* dependencies. *Weak* dependencies are only for ordering the build process, but do not indicate an obligatory dependency. :param package: An instance of :class:`ApeTools.Build.Package` :param strong: A flag indicating if this node marks a strong or weak dependency """ def __init__(self, package, strong): self.children = [] self.strong = strong self.package = package for p in package.dependencies: self.add(p, strong) for p in package.weakDependencies: self.add(p, False) def add(self, package, strong): """Add *package* to the dependencies of this node. :param package: Package this node depends on :param strong: Flag indicating if the dependency is strong or weak """ if not package.isHidden: try: package.setDependencyActive() self.children.append(DependencyNode(package, strong)) finally: package.setDependencyInactive() def isStrong(self): """Returns flag if this node indicates a strong or weak dependency. """ return self.strong def flatten(self, flatTree): """Flatten the sub-tree rooted at this node. Append itself after the child nodes. :param flatTree: The flattened tree to append to. """ for c in self.children: c.flatten(flatTree) flatTree.append(self) def getPackage(self): """Return the package represented by this node. """ return self.package class DependencyTree(DependencyNode): """A specialization of :class:`DependencyNode`, representing the root of the dependency tree. Packages listed in the configuration variable *preDependencies* are automatically prepended. This allows the injection of packages at the head of the dependency list. :param packages: The initial list of packages requested. These are automatically strong dependencies. This ensures that they and all their dependencies will be built. """ def __init__(self, packages): self.children = [] preDependencies = Config.getlist("ape", "preDependencies") if preDependencies: preDependencies = getPackages(*preDependencies) for p in preDependencies + packages: self.add(p, True) def buildList(self): """Get list of packages to build. Flattens the list and returns a list of packages marked as strong somewhere in the tree. """ flatTree = [] for c in self.children: c.flatten(flatTree) toBuild = [p.getPackage() for p in flatTree if p.isStrong()] result = [] for p in [x.getPackage() for x in flatTree]: if p in toBuild and p not in result: result.append(p) return result class package_meta(type): """Metaclass for packages. Takes care of the registration of the package builder. """ def __new__(cls, name, bases, attrs): try: package_name = attrs["name"] except KeyError: package_name = name.lower() c = super(package_meta, cls).__new__(cls, name, bases, attrs) registerPackage(package_name, c) return c class Package: """Base class to provide default implementations for all steps of the fetching and building process of a package. For simple packages, the process can be customized using configuration files. For packages requiring a more complicated build process, you can derive your installer from this class and re-implement some methods. The dependencies of a package are listed and have to be build before its dependents. Additionally, there are *weak* dependencies, which are only built if requested. But if requested, they ought to be build before packages depending weakly on them. Full dependencies activate packages. Both types of dependencies are used to determine the order in which to build packages. :param name: Used to set the attribute :attr:`name` .. attribute:: name The name of the package. This is used to refer to the package on the command line and to retrieve configuration information. """ __metaclass__ = package_meta def __init__(self, name): strings = \ """builder version prefix buildDirectory sourceDirectory patchCmd configureCmd makeCmd installCmd """ bools = "parallelBuild alwaysInstall alwaysKeep isHidden" lists = \ """tarballs extraFiles dependencies weakDependencies patches patchArgs configureArgs makeArgs installArgs environment infoVariables """ self.name = name self.section = "package " + self.name self.fallback = ["package"] try: self.setAttributes(strings) self.setAttributes(bools, Config.getboolean) self.setAttributes(lists, Config.getlist) except ConfigParser.NoSectionError: raise InstallError(args=["No configuration information."], pack=name, stage="Package constructor") if self.dependencies: self.dependencies = getPackages(*self.dependencies) if self.weakDependencies: self.weakDependencies = getPackages(*self.weakDependencies) self._dependants = set() for p in self.dependencies + self.weakDependencies: p._dependants.add(self) self.dependenciesActive = False self._env = {} for var in self.environment: self._env[var] = Config.get(self.section, "env." + var, self.fallback) def __repr__(self): return "Package(%s)" % self.name def env(self): """The additions to the environment for this package. The environment can be set using configuration variables or by overriding this function. """ return self._env def setAttributes(self, names, getter=Config.get): """Set instance attributes from configuration variables. :param names: List of attribute names :type names: list or string :param getter: One of the ``get...`` functions from the :mod:`ApeTools.Config` modules. Defaults to the string getter. """ if type(names) == str: names = names.split() for name in names: if not hasattr(self, name): setattr(self, name, getter(self.section, name, self.fallback)) def setDependencyActive(self): """Mark package as visited already during dependency checking. :raise: :exc:`CircularDependencyError` exception if the package is already in the current branch of the dependency tree. """ if self.dependenciesActive: raise CircularDependencyError(self.name) self.dependenciesActive = True def setDependencyInactive(self): """Set package to be not active during dependency checking. """ self.dependenciesActive = False def dependants(self): return self._dependants def isDummy(self): """Is this package a dummy package? Dummy packages do not require any files. They are typically used to collect dependencies in one place. """ return not (self.tarballs or self.extraFiles) def isInstalled(self): """Evaluates if this package is already installed. A regular package is installed if the top-level installation directory :attr:`prefix` exists. A dummy package has no files to install. It is installed if all dependencies are installed. """ if self.isDummy(): return reduce(operator.and_, [p.isInstalled() for p in self.dependencies], True) else: return os.path.exists(self.prefix) def info(self): """Returns a list of variables and their values for this package. """ result = [] for var in self.infoVariables: result.append("%s.%s: %s" % (self.name, var, getattr(self, var))) if Config.getboolean("ape", "verbose") and self.isInstalled(): result.append("%s.%s: %s" % (self.name, "env()", self.env())) return result def logFile(self, stageNumber, stageName): """Determine the name of the logfile to use. :param stageNumber: The number of the stage we are currently processing :param stageName: The name of the current processing stage """ return os.path.join(Config.get("ape", "logs"), "%s-%1u-%s.apelog" % (self.name, stageNumber, stageName)) def removePrefixDir(self): """Remove installation directory for this package. - Use when building or installing fail - Only removes directories in ape's install area (protection against configuration errors) """ if self.prefix.startswith(Config.get("ape", "base")) and os.path.exists(self.prefix): shutil.rmtree(self.prefix) def fetch(self, manager): """This function tries fetch all files specified in :attr:`tarballs` and :attr:`extraFiles`, using services from :class:`ApeTools.Fetch.DownloadManager`. """ for src in self.tarballs + self.extraFiles: distfiles = Config.get("ape", "distfiles") print " ... Fetching %s" % src if Config.getboolean("ape", "verbose"): print " into", distfiles if Config.getboolean("ape", "dryRun"): continue if not os.path.exists(distfiles): os.makedirs(distfiles) manager.fetchFile(src, distfiles) def unpack(self, parentDir=None): """This function assumes the tarballs listed in :attr:`tarballs` are located in the distfile Directory and unpacks them. """ if parentDir is None: parentDir = os.path.dirname(self.sourceDirectory) if not os.path.exists(parentDir) and \ not Config.getboolean("ape", "dryRun"): os.makedirs(parentDir) if Config.getboolean("ape", "verbose"): print " Extracting to", parentDir if Config.getboolean("ape", "dryRun"): return for tarball in self.tarballs: filename = os.path.join(Config.get("ape", "distfiles"), tarball) try: if sys.version_info[1] < 5: untar(filename, parentDir) else: tar = tarfile.open(filename, 'r') try: try: tar.extractall(parentDir) except AttributeError: untar(filename, parentDir) #for member in tar: # tar.extract(member, parentDir) finally: tar.close() except Exception, e: raise InstallError(args=[str(e)], pack=self.name, stage='unpack') def patch(self, logFile): """Apply the patches for this package. """ patchDir = os.path.join(Config.get("ape internal", "patches"), self.name) append = False for patch in self.patches: print " ... Patching with", patch cmd = [self.patchCmd] + self.patchArgs + \ [os.path.join(patchDir, patch)] call(cmd, cwd=self.sourceDirectory, output=logFile, append=append, package=self.name, stage="patch") append = True def configure(self, logFile, env): """Configure the current package. This will typically call a :program:`./configure` script or use :command:`cmake`. """ if self.configureCmd: print " ... Configuring" configureCmd = [self.configureCmd] + self.configureArgs if not os.path.exists(self.buildDirectory): os.makedirs(self.buildDirectory) call(configureCmd, cwd=self.buildDirectory, output=logFile, package=self.name, stage="configure", env=env) def make(self, logFile, env): """Build the package. This is typically done invoking :command:`make`. """ if self.makeCmd: print " ... Building" makeCmd = [self.makeCmd] + self.makeArgs jobs = Config.getint("ape", "jobs") if self.parallelBuild and jobs > 1: makeCmd += ["-j%i" % jobs] try: call(makeCmd, cwd=self.buildDirectory, output=logFile, package=self.name, stage="make", env=env) except: self.removePrefixDir() raise def install(self, logFile, env): """Install the package. This is typically done invoking :command:`make` with the argument ``install``. """ if self.installCmd: print " ... Installing" installCmd = [self.installCmd] + self.installArgs try: call(installCmd, cwd=self.buildDirectory, output=logFile, package=self.name, stage="install", env=env) except: self.removePrefixDir() raise def clean(self): """This method removes the source and build directories, unless one of the following conditions is met: - The package was built outside the general build area. The condition is important to avoid removing packages which do an in-place build directly in the install area. - The configuration variable *keep* is set in section ``[ape]``. - The option :option:`--keep` was given on the command line. This works by setting the configuration variable *keep* is set in section ``[ape]``. - The package configuration variable *alwaysKeep* is set for this package. This is set in the configuration section for the current package. .. code-block:: ini [package myPackageName] alwaysKeep = yes """ toConsider = [self.sourceDirectory, self.buildDirectory] if (not Config.getboolean("ape", "keep") and not self.alwaysKeep and not self.isDummy()): toRemove = [d for d in toConsider if os.path.exists(d) and d.startswith(Config.get("ape", "build"))] else: toRemove = [] if toRemove: print " ... Removing", if Config.getboolean("ape", "verbose"): print ", ".join(toRemove) else: print "source and build areas" if (not Config.getboolean("ape", "dryRun")): try: for d in toRemove: if os.path.exists(d): shutil.rmtree(d) except RuntimeError, e: raise InstallError(args=["Cleanup error", str(e)], pack=self.name, stage="cleanup") def build(self, env): """Build the package by invoking the individual stages. 1. patch 2. configure 3. make 4. install 5. clean The process can be customized by setting configuration variables or by replacing this method or the methods implementing one of the stages. """ if not os.path.exists(self.sourceDirectory) and \ not Config.getboolean("ape", "dryRun"): os.makedirs(self.sourceDirectory) self.patch(self.logFile(1, "patch")) self.configure(self.logFile(2, "configure"), env) self.make(self.logFile(3, "make"), env) self.install(self.logFile(4, "install"), env) self.clean() registerPackage("package", Package)