view treepkg/packager.py @ 9:16689c948bbc

Rename the instance variable plant to pkg_line in some classes
author Bernhard Herzog <bh@intevation.de>
date Fri, 09 Mar 2007 15:02:41 +0100
parents 574506a022f6
children 59d6055493f5
line wrap: on
line source
# Copyright (C) 2007 by Intevation GmbH
# Authors:
# Bernhard Herzog <bh@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with the software for details.

"""Classes to automatically build debian packages from subversion checkouts"""

import os
import time
import re
import logging
import shutil
import traceback

import util
import subversion
import run


def _filenameproperty(relative_dir):
    def get(self):
        return os.path.join(self.base_dir, relative_dir)
    return property(get)


class SourcePackager(object):

    # Derived classes must supply the package basename
    pkg_basename = None

    def __init__(self, pkg_line, status, work_dir, src_dir, revision):
        self.pkg_line = pkg_line
        self.status = status
        self.work_dir = work_dir
        self.src_dir = src_dir
        self.revision = revision
        assert(self.pkg_basename)

    def determine_package_version(self, directory):
        """Returns the version number of the new package as a string

        The directory parameter is the name of the directory containing
        the newly exported sources.  The sources were exported with the
        export_sources method.

        The default implementation simply returns the revision converted
        to a string.
        """
        return str(self.revision)

    def export_sources(self):
        """Export the sources from the subversion working directory

        This method first exports the sources to a temporary directory
        and then renames the directory.  The new name is of the form

          <pkg_basename>-<version>

        Where pkg_basename is the value of self.pkg_basename and version
        is the return value of the determine_package_version() method.
        """
        temp_dir = os.path.join(self.work_dir, "temp")
        self.pkg_line.export_sources(temp_dir)

        pkgbaseversion = self.determine_package_version(temp_dir)
        pkgbasedir = os.path.join(self.work_dir,
                                  self.pkg_basename + "-" + pkgbaseversion)

        os.rename(temp_dir, pkgbasedir)
        return pkgbaseversion, pkgbasedir


    def update_version_numbers(self, pkgbasedir):
        """Updates the version numbers in the code in pkgbasedir.

        The default implementation does nothing.  Derived classes should
        override this method if necessary.
        """

    def create_tarball(self, tarballname, workdir, basedir):
        """Creates a new tarball.

        Parameters:

          tarballname -- the filename of the new tarball
          workdir -- The directory into which to change before running tar.
                     (actually this is done with GNUI tar's -C option)
          basedir -- The basedirectory of the files that are packaged
                     into the tarfile.  This should be a relative
                     filename directly in workdir.
        """
        logging.info("Creating tarball %r", tarballname)
        run.call(["tar", "czf", tarballname, "-C", workdir, basedir])

    def copy_debian_directory(self, pkgbasedir, pkgbaseversion, changemsg):
        """Copies the debian directory and updates the copy's changelog

        Parameter:
          pkgbasedir -- The directory holding the unpacked source package
          pkgbaseversion -- The version to update the changelog to
          changemsg -- The message for the changelog

        When determining the actual version for the new package, this
        function looks at the previous version in the changelog.  If it
        has a prefix separated from the version number by a colon this
        prefix is prepended to the pkgbaseversion parameter.  Debian
        uses such prefixes for the kde packages.
        """
        debian_dir = os.path.join(pkgbasedir, "debian")
        changelog = os.path.join(debian_dir, "changelog")

        self.pkg_line.copy_debian_directory(debian_dir)

        logging.info("Updating %r", changelog)
        oldversion = util.debian_changelog_version(changelog)
        if ":" in oldversion:
            oldversionprefix = oldversion.split(":")[0] + ":"
        else:
            oldversionprefix = ""
        run.call(["debchange", "-c", changelog,
                  "-v", oldversionprefix + pkgbaseversion + "-kk1",
                  changemsg],
                 env=self.pkg_line.debian_environment())


    def create_source_package(self, pkgbasedir, origtargz):
        """Creates a new source package from pkgbasedir and origtargz"""
        logging.info("Creating new source package")
        run.call(["dpkg-source", "-b", os.path.basename(pkgbasedir),
                  os.path.basename(origtargz)],
                 cwd=os.path.dirname(pkgbasedir),
                 suppress_output=True,
                 env=self.pkg_line.debian_environment())

    def move_source_package(self, pkgbasename):
        """Moves the new source package from the work_dir to the src_dir"""
        logging.info("Moving source package to %r", self.src_dir)
        util.ensure_directory(self.src_dir)
        for filename in [filename for filename in os.listdir(self.work_dir)
                                  if filename.startswith(pkgbasename)]:
            os.rename(os.path.join(self.work_dir, filename),
                      os.path.join(self.src_dir, filename))

    def package(self):
        """Creates a source package from a subversion checkout.

        After setting up the working directory, this method calls the
        do_package method to do the actual packaging.  Afterwards the
        work directory is removed.
        """
        util.ensure_directory(self.work_dir)
        try:
            self.status.set("creating_source_package")
            self.do_package()
            self.status.set("source_package_created")
        finally:
            logging.info("Removing workdir %r", self.work_dir)
            shutil.rmtree(self.work_dir)

    def do_package(self):
        """Does the work of creating a source package
        This method must be overriden by derived classes.

        The method should do the work in self.work_dir.  When the
        package is done, the source package files should be in
        self.src_dir.
        """
        raise NotImplementedError


class BinaryPackager(object):

    def __init__(self, pkg_line, status, binary_dir, dsc_file, logfile):
        self.pkg_line = pkg_line
        self.status = status
        self.binary_dir = binary_dir
        self.dsc_file = dsc_file
        self.logfile = logfile

    def package(self):
        self.status.set("creating_binary_package")
        util.ensure_directory(self.binary_dir)
        logging.info("Building binary package; loging to %r", self.logfile)
        cmd = ["/usr/sbin/pbuilder", "build", "--logfile", self.logfile,
               "--buildresult", self.binary_dir, self.dsc_file]
        run.call(self.pkg_line.root_cmd + cmd, suppress_output=True)
        self.status.set("binary_package_created")


class RevisionPackager(object):

    source_packager_cls = SourcePackager
    binary_packager_cls = BinaryPackager

    def __init__(self, pkg_line, revision):
        self.pkg_line = pkg_line
        self.revision = revision
        self.base_dir = self.pkg_line.pkg_dir_for_revision(self.revision, 1)
        self.status = util.StatusFile(os.path.join(self.base_dir, "status"))

    work_dir = _filenameproperty("work")
    binary_dir = _filenameproperty("binary")
    src_dir = _filenameproperty("src")

    def find_dsc_file(self):
        for filename in os.listdir(self.src_dir):
            if filename.endswith(".dsc"):
                return os.path.join(self.src_dir, filename)
        return None

    def package(self):
        try:
            src_packager = self.source_packager_cls(self.pkg_line, self.status,
                                                    self.work_dir, self.src_dir,
                                                    self.revision)
            src_packager.package()

            dsc_file = self.find_dsc_file()
            if dsc_file is None:
                raise RuntimeError("Cannot find dsc File in %r" % self.src_dir)

            bin_packager = self.binary_packager_cls(self.pkg_line, self.status,
                                                    self.binary_dir, dsc_file,
                                                    os.path.join(self.base_dir,
                                                                 "build.log"))
            bin_packager.package()
        except:
            self.status.set("failed", traceback.format_exc())
            raise

    def remove_package_dir(self):
        logging.info("Removing pkgdir %r", self.base_dir)
        shutil.rmtree(self.base_dir)


class PackageLine(object):

    revision_packager_cls = RevisionPackager

    svn_external_subdirs = []

    extra_config_desc = []

    def __init__(self, name, base_dir, svn_url, root_cmd, deb_email,
                 deb_fullname, packager_class="treepkg.packager"):
        self.name = name
        self.base_dir = base_dir
        self.svn_url = svn_url
        self.root_cmd = root_cmd
        self.deb_email = deb_email
        self.deb_fullname = deb_fullname
        self.pkg_dir_template = "%(revision)d-%(increment)d"
        self.pkg_dir_regex \
                   = re.compile(r"(?P<revision>[0-9]+)-(?P<increment>[0-9]+)$")

    checkout_dir = _filenameproperty("checkout")
    debian_dir = _filenameproperty("debian")
    pkg_dir = _filenameproperty("pkg")

    def pkg_dir_for_revision(self, revision, increment):
        return os.path.join(self.pkg_dir,
                            self.pkg_dir_template % locals())

    def last_changed_revision(self):
        revisions = []
        for directory in [self.checkout_dir] + self.svn_external_subdirs:
            directory = os.path.join(self.checkout_dir, directory)
            revisions.append(subversion.last_changed_revision(directory))
        return max(revisions)

    def last_packaged_revision(self):
        """Returns the revision number of the highest packaged revision.

        If the revision cannot be determined because no already packaged
        revisions can be found, the function returns -1.
        """
        revisions = [-1]
        if os.path.exists(self.pkg_dir):
            for filename in os.listdir(self.pkg_dir):
                match = self.pkg_dir_regex.match(filename)
                if match:
                    revisions.append(int(match.group("revision")))
        return max(revisions)

    def debian_source(self):
        return util.extract_value_for_key(open(os.path.join(self.debian_dir,
                                                            "control")),
                                          "Source:")

    def update_checkout(self):
        """Updates the working copy of self.svn_url in self.checkout_dir.

        If self.checkout_dir doesn't exist yet, self.svn_url is checked
        out into that directory.
        """
        localdir = self.checkout_dir
        if os.path.exists(localdir):
            logging.info("Updating the working copy in %r", localdir)
            subversion.update(localdir)
        else:
            logging.info("The working copy in %r doesn't exist yet."
                         "  Checking out fromo %r", localdir,
                         self.svn_url)
            subversion.checkout(self.svn_url, localdir)

    def export_sources(self, to_dir):
        logging.info("Exporting sources for tarball to %r", to_dir)
        subversion.export(self.checkout_dir, to_dir)
        # some versions of svn (notably version 1.4.2 shipped with etch)
        # do export externals such as the admin subdirectory.  We may
        # have to do that in an extra step.
        for subdir in self.svn_external_subdirs:
            absdir = os.path.join(to_dir, subdir)
            if not os.path.isdir(absdir):
                subversion.export(os.path.join(self.checkout_dir, subdir),
                                  absdir)

    def copy_debian_directory(self, to_dir):
        logging.info("Copying debian directory to %r", to_dir)
        shutil.copytree(self.debian_dir, to_dir)

    def debian_environment(self):
        """Returns the environment variables for the debian commands"""
        env = os.environ.copy()
        env["DEBFULLNAME"] = self.deb_fullname
        env["DEBEMAIL"] = self.deb_email
        return env

    def package_if_updated(self):
        """Checks if the checkout changed and returns a new packager if so"""
        self.update_checkout()
        current_revision = self.last_changed_revision()
        logging.info("New revision is %d", current_revision)
        previous_revision = self.last_packaged_revision()
        logging.info("Previously packaged revision was %d", previous_revision)
        if current_revision > previous_revision:
            logging.info("New revision is not packaged yet")
            return self.revision_packager_cls(self, current_revision)
        else:
            logging.info("New revision already packaged.")


def create_package_line(packager_class, **kw):
    module = util.import_dotted_name(packager_class)
    return module.PackageLine(**kw)


class PackagerGroup(object):

    def __init__(self, package_lines, check_interval):
        self.package_lines = package_lines
        self.check_interval = check_interval

    def run(self):
        """Runs the packager group indefinitely"""
        logging.info("Tree packager starts.  Will check every %d seconds",
                     self.check_interval)
        last_check = -1
        while 1:
            now = time.time()
            if now > last_check + self.check_interval:
                self.check_package_lines()
                last_check = now
                next_check = now + self.check_interval
                to_sleep = next_check - time.time()
                if to_sleep > 0:
                    logging.info("Next check at %s",
                                 time.strftime("%Y-%m-%d %H:%M:%S",
                                               time.localtime(next_check)))
                    time.sleep(to_sleep)
                else:
                    logging.info("Next check now")

    def check_package_lines(self):
        logging.info("Checking package lines")
        for line in self.package_lines:
            try:
                packager = line.package_if_updated()
                if packager:
                    packager.package()
            except:
                logging.exception("An error occurred while"
                                  " checking packager line %r", line.name)
        logging.info("Checked all packager lines")
This site is hosted by Intevation GmbH (Datenschutzerklärung und Impressum | Privacy Policy and Imprint)