Use generic hook for pre-build
[invirt/packages/invirt-dev.git] / invirtibuilder
index 15b4e05..b6d4eb8 100755 (executable)
@@ -29,19 +29,39 @@ principal is the Kerberos principal that requested the build.
 from __future__ import with_statement
 
 import contextlib
 from __future__ import with_statement
 
 import contextlib
+import glob
 import os
 import re
 import shutil
 import subprocess
 import os
 import re
 import shutil
 import subprocess
+import tempfile
+import traceback
 
 import pyinotify
 
 
 import pyinotify
 
+from debian_bundle import deb822
+
 import invirt.builder as b
 import invirt.builder as b
+import invirt.common as c
 from invirt import database
 from invirt import database
+from invirt.config import structs as config
 
 
 DISTRIBUTION = 'hardy'
 
 
 DISTRIBUTION = 'hardy'
-
+logfile = None
+
+def logAndRun(cmd, *args, **kwargs):
+    # Always grab stdout, even if the caller doesn't need it.
+    # TODO: don't slurp it all into memory in that case.
+    if 'stdout' in kwargs and kwargs['stdout'] is None:
+        del kwargs['stdout']
+    kwargs['stderr'] = logfile
+    logfile.write('---> Ran %s\n' % (cmd, ))
+    logfile.write('STDERR:\n')
+    output = c.captureOutput(cmd, *args, **kwargs)
+    logfile.write('STDOUT:\n')
+    logfile.write(output)
+    return output
 
 def getControl(package, ref):
     """Get the parsed debian/control file for a given package.
 
 def getControl(package, ref):
     """Get the parsed debian/control file for a given package.
@@ -51,7 +71,7 @@ def getControl(package, ref):
     acts roughly like a dict.
     """
     return deb822.Deb822.iter_paragraphs(
     acts roughly like a dict.
     """
     return deb822.Deb822.iter_paragraphs(
-        getGitFile(package, ref, 'debian/control').split('\n'))
+        b.getGitFile(package, ref, 'debian/control').split('\n'))
 
 
 def getBinaries(package, ref):
 
 
 def getBinaries(package, ref):
@@ -71,7 +91,7 @@ def getArches(package, ref):
 
 def getDscName(package, ref):
     """Return the .dsc file that will be generated for this package."""
 
 def getDscName(package, ref):
     """Return the .dsc file that will be generated for this package."""
-    v = getVersion(package, ref)
+    v = b.getVersion(package, ref)
     if v.debian_version:
         v_str = '%s-%s' % (v.upstream_version,
                            v.debian_version)
     if v.debian_version:
         v_str = '%s-%s' % (v.upstream_version,
                            v.debian_version)
@@ -86,31 +106,31 @@ def sanitizeVersion(version):
     """Sanitize a Debian package version for use as a git tag.
 
     This function strips the epoch from the version number and
     """Sanitize a Debian package version for use as a git tag.
 
     This function strips the epoch from the version number and
-    replaces any tildes with periods."""
-    if v.debian_version:
+    replaces any tildes with underscores."""
+    if version.debian_version:
         v = '%s-%s' % (version.upstream_version,
                        version.debian_version)
     else:
         v = version.upstream_version
         v = '%s-%s' % (version.upstream_version,
                        version.debian_version)
     else:
         v = version.upstream_version
-    return v.replace('~', '.')
+    return v.replace('~', '_')
 
 
 
 
-def aptCopy(packages, dst_pocket, src_pocket):
+def aptCopy(package, commit, dst_pocket, src_pocket):
     """Copy a package from one pocket to another."""
     binaries = getBinaries(package, commit)
     """Copy a package from one pocket to another."""
     binaries = getBinaries(package, commit)
-    cpatureOutput(['reprepro-env', 'copy',
-                   b.pocketToApt(dst_pocket),
-                   b.pocketToApt(src_pocket),
-                   package] + binaries)
+    logAndRun(['reprepro-env', 'copy',
+               b.pocketToApt(dst_pocket),
+               b.pocketToApt(src_pocket),
+               package] + binaries)
 
 
 def sbuild(package, ref, arch, workdir, arch_all=False):
     """Build a package for a particular architecture."""
 
 
 def sbuild(package, ref, arch, workdir, arch_all=False):
     """Build a package for a particular architecture."""
-    args = ['sbuild', '-d', DISTRIBUTION, '--arch', arch]
+    args = ['sbuild', '-v', '-d', DISTRIBUTION, '--arch', arch]
     if arch_all:
         args.append('-A')
     args.append(getDscName(package, ref))
     if arch_all:
         args.append('-A')
     args.append(getDscName(package, ref))
-    c.captureOutput(args, cwd=workdir, stdout=None)
+    logAndRun(args, cwd=workdir)
 
 
 def sbuildAll(package, ref, workdir):
 
 
 def sbuildAll(package, ref, workdir):
@@ -122,7 +142,7 @@ def sbuildAll(package, ref, workdir):
         sbuild(package, ref, 'i386', workdir)
 
 
         sbuild(package, ref, 'i386', workdir)
 
 
-def tagSubmodule(pocket, package, ref, principal):
+def tagSubmodule(pocket, package, commit, principal, version, env):
     """Tag a new version of a submodule.
 
     If this pocket does not allow_backtracking, then this will create
     """Tag a new version of a submodule.
 
     If this pocket does not allow_backtracking, then this will create
@@ -137,43 +157,45 @@ def tagSubmodule(pocket, package, ref, principal):
     I'm sure that long description gives you great confidence in the
     legitimacy of my reasoning.
     """
     I'm sure that long description gives you great confidence in the
     legitimacy of my reasoning.
     """
-    if config.build.pockets[pocket].get('allow_backtracking', False):
-        env = dict(os.environ)
+    if not config.build.pockets[pocket].get('allow_backtracking', False):
         branch = b.pocketToGit(pocket)
         branch = b.pocketToGit(pocket)
-        version = b.getVersion(package, ref)
-
-        env['GIT_COMMITTER_NAME'] = config.build.tagger.name
-        env['GIT_COMMITTER_EMAIL'] = config.build.tagger.email
         tag_msg = ('Tag %s of %s\n\n'
                    'Requested by %s' % (version.full_version,
                                         package,
                                         principal))
 
         tag_msg = ('Tag %s of %s\n\n'
                    'Requested by %s' % (version.full_version,
                                         package,
                                         principal))
 
-        c.captureOutput(
-            ['git', 'tag', '-m', tag_msg, commit],
-            stdout=None,
-            env=env)
+        logAndRun(
+            ['git', 'tag', '-m', tag_msg, '--', sanitizeVersion(version),
+             commit],
+            env=env,
+            cwd=b.getRepo(package))
 
 
 
 
-def updateSubmoduleBranch(pocket, package, ref):
+def updateSubmoduleBranch(pocket, package, commit):
     """Update the appropriately named branch in the submodule."""
     branch = b.pocketToGit(pocket)
     """Update the appropriately named branch in the submodule."""
     branch = b.pocketToGit(pocket)
-    c.captureOutput(
-        ['git', 'update-ref', 'refs/heads/%s' % branch, ref])
+    logAndRun(
+        ['git', 'update-ref', 'refs/heads/%s' % branch, commit], cwd=b.getRepo(package))
 
 
 def uploadBuild(pocket, workdir):
     """Upload all build products in the work directory."""
 
 
 def uploadBuild(pocket, workdir):
     """Upload all build products in the work directory."""
+    force = config.build.pockets[pocket].get('allow_backtracking', False)
     apt = b.pocketToApt(pocket)
     for changes in glob.glob(os.path.join(workdir, '*.changes')):
     apt = b.pocketToApt(pocket)
     for changes in glob.glob(os.path.join(workdir, '*.changes')):
-        c.captureOutput(['reprepro-env',
-                       'include',
-                       '--ignore=wrongdistribution',
-                       apt,
-                       changes])
+        upload = ['reprepro-env', '--ignore=wrongdistribution',
+                  'include', apt, changes]
+        try:
+            logAndRun(upload)
+        except subprocess.CalledProcessError, e:
+            if not force:
+                raise
+            package = deb822.Changes(open(changes).read())['Binary']
+            logAndRun(['reprepro-env', 'remove', apt, package])
+            logAndRun(upload)
 
 
 
 
-def updateSuperproject(pocket, package, commit, principal):
+def updateSuperproject(pocket, package, commit, principal, version, env):
     """Update the superproject.
 
     This will create a new commit on the branch for the given pocket
     """Update the superproject.
 
     This will create a new commit on the branch for the given pocket
@@ -184,33 +206,36 @@ def updateSuperproject(pocket, package, commit, principal):
     """
     superproject = os.path.join(b._REPO_DIR, 'invirt/packages.git')
     branch = b.pocketToGit(pocket)
     """
     superproject = os.path.join(b._REPO_DIR, 'invirt/packages.git')
     branch = b.pocketToGit(pocket)
-    tree = c.captureOutput(['git', 'ls-tree', branch],
-                         cwd=superproject)
+    tree = logAndRun(['git', 'ls-tree', branch],
+                     cwd=superproject).strip()
 
     new_tree = re.compile(
         r'^(160000 commit )[0-9a-f]*(\t%s)$' % package, re.M).sub(
 
     new_tree = re.compile(
         r'^(160000 commit )[0-9a-f]*(\t%s)$' % package, re.M).sub(
-        r'\1%s\2' % commit,
+        r'\g<1>%s\g<2>' % commit,
         tree)
 
         tree)
 
-    new_tree_id = c.captureOutput(['git', 'mktree'],
-                                cwd=superproject,
-                                stdin_str=new_tree)
+    new_tree_id = logAndRun(['git', 'mktree', '--missing'],
+                            cwd=superproject,
+                            stdin_str=new_tree).strip()
 
     commit_msg = ('Update %s to version %s\n\n'
                   'Requested by %s' % (package,
                                        version.full_version,
                                        principal))
 
     commit_msg = ('Update %s to version %s\n\n'
                   'Requested by %s' % (package,
                                        version.full_version,
                                        principal))
-    new_commit = c.captureOutput(
-        ['git', 'commit-tree', new_tree_hash, '-p', branch],
+    new_commit = logAndRun(
+        ['git', 'commit-tree', new_tree_id, '-p', branch],
         cwd=superproject,
         env=env,
         cwd=superproject,
         env=env,
-        stdin_str=commit_msg)
+        stdin_str=commit_msg).strip()
 
 
-    c.captureOutput(
+    logAndRun(
         ['git', 'update-ref', 'refs/heads/%s' % branch, new_commit],
         cwd=superproject)
 
 
         ['git', 'update-ref', 'refs/heads/%s' % branch, new_commit],
         cwd=superproject)
 
 
+def makeReadable(workdir):
+    os.chmod(workdir, 0755)
+
 @contextlib.contextmanager
 def packageWorkdir(package, commit):
     """Checkout the package in a temporary working directory.
 @contextlib.contextmanager
 def packageWorkdir(package, commit):
     """Checkout the package in a temporary working directory.
@@ -227,7 +252,7 @@ def packageWorkdir(package, commit):
         p_archive = subprocess.Popen(
             ['git', 'archive',
              '--remote=file://%s' % b.getRepo(package),
         p_archive = subprocess.Popen(
             ['git', 'archive',
              '--remote=file://%s' % b.getRepo(package),
-             '--prefix=%s' % package,
+             '--prefix=%s/' % package,
              commit,
              ],
             stdout=subprocess.PIPE,
              commit,
              ],
             stdout=subprocess.PIPE,
@@ -244,22 +269,14 @@ def packageWorkdir(package, commit):
     finally:
         shutil.rmtree(workdir)
 
     finally:
         shutil.rmtree(workdir)
 
-
-def reportBuild(build):
-    """Run hooks to report the results of a build attempt."""
-
-    c.captureOutput(['run-parts',
-                   '--arg=%s' % build.build_id,
-                   '--',
-                   b._HOOKS_DIR])
-
-
 def build():
     """Deal with items in the build queue.
 
     When triggered, iterate over build queue items one at a time,
     until there are no more pending build jobs.
     """
 def build():
     """Deal with items in the build queue.
 
     When triggered, iterate over build queue items one at a time,
     until there are no more pending build jobs.
     """
+    global logfile
+
     while True:
         stage = 'processing incoming job'
         queue = os.listdir(b._QUEUE_DIR)
     while True:
         stage = 'processing incoming job'
         queue = os.listdir(b._QUEUE_DIR)
@@ -277,15 +294,31 @@ def build():
         db.commit = commit
         db.principal = principal
         database.session.save_or_update(db)
         db.commit = commit
         db.principal = principal
         database.session.save_or_update(db)
-        database.commit()
+        database.session.commit()
+
+        database.session.begin()
 
 
-        database.begin()
+        logdir = os.path.join(b._LOG_DIR, str(db.build_id))
+        if not os.path.exists(logdir):
+            os.makedirs(logdir)
 
         try:
             db.failed_stage = 'validating job'
 
         try:
             db.failed_stage = 'validating job'
-            src = validateBuild(pocket, package, commit)
+            # Don't expand the commit in the DB until we're sure the user
+            # isn't trying to be tricky.
+            b.ensureValidPackage(package)
+
+            logfile = open(os.path.join(logdir, '%s.log' % db.package), 'w')
+
+            db.commit = commit = b.canonicalize_commit(package, commit)
+            src = b.validateBuild(pocket, package, commit)
+            version = b.getVersion(package, commit)
+            db.version = str(version)
+            b.runHook('pre-build', [str(db.build_id)])
 
 
-            db.version = str(b.getVersion(package, commit))
+            env = dict(os.environ)
+            env['GIT_COMMITTER_NAME'] = config.build.tagger.name
+            env['GIT_COMMITTER_EMAIL'] = config.build.tagger.email
 
             # If validateBuild returns something other than True, then
             # it means we should copy from that pocket to our pocket.
 
             # If validateBuild returns something other than True, then
             # it means we should copy from that pocket to our pocket.
@@ -293,8 +326,16 @@ def build():
             # (If the validation failed, validateBuild would have
             # raised an exception)
             if src != True:
             # (If the validation failed, validateBuild would have
             # raised an exception)
             if src != True:
+                # TODO: cut out this code duplication
+                db.failed_stage = 'tagging submodule before copying package'
+                tagSubmodule(pocket, package, commit, principal, version, env)
+                db.failed_stage = 'updating submodule branches before copying package'
+                updateSubmoduleBranch(pocket, package, commit)
+                db.failed_stage = 'updating superproject before copying package'
+                updateSuperproject(pocket, package, commit, principal, version, env)
                 db.failed_stage = 'copying package from another pocket'
                 db.failed_stage = 'copying package from another pocket'
-                aptCopy(packages, pocket, src)
+                aptCopy(package, commit, pocket, src)
+                
             # If we can't copy the package from somewhere, but
             # validateBuild didn't raise an exception, then we need to
             # do the build ourselves
             # If we can't copy the package from somewhere, but
             # validateBuild didn't raise an exception, then we need to
             # do the build ourselves
@@ -311,45 +352,43 @@ def build():
                     # If we were, we could use debuild and get nice
                     # environment scrubbing. Since we're not, debuild
                     # complains about not having an orig.tar.gz
                     # If we were, we could use debuild and get nice
                     # environment scrubbing. Since we're not, debuild
                     # complains about not having an orig.tar.gz
-                    c.captureOutput(['dpkg-buildpackage', '-us', '-uc', '-S'],
-                                  cwd=packagedir,
-                                  stdout=None)
-
-                    try:
-                        db.failed_stage = 'building binary packages'
-                        sbuildAll(package, commit, workdir)
-                    finally:
-                        logdir = os.path.join(b._LOG_DIR, db.build_id)
-                        if not os.path.exists(logdir):
-                            os.makedirs(logdir)
-
-                        for log in glob.glob(os.path.join(workdir, '*.build')):
-                            os.copy2(log, logdir)
+                    logAndRun(['dpkg-buildpackage', '-us', '-uc', '-S'],
+                              cwd=packagedir)
+
+                    db.failed_stage = 'building binary packages'
+                    sbuildAll(package, commit, workdir)
                     db.failed_stage = 'tagging submodule'
                     db.failed_stage = 'tagging submodule'
-                    tagSubmodule(pocket, package, commit, principal)
+                    tagSubmodule(pocket, package, commit, principal, version, env)
                     db.failed_stage = 'updating submodule branches'
                     updateSubmoduleBranch(pocket, package, commit)
                     db.failed_stage = 'updating superproject'
                     db.failed_stage = 'updating submodule branches'
                     updateSubmoduleBranch(pocket, package, commit)
                     db.failed_stage = 'updating superproject'
-                    updateSuperproject(pocket, package, commit, principal)
+                    updateSuperproject(pocket, package, commit, principal, version, env)
+                    db.failed_stage = 'relaxing permissions on workdir'
+                    makeReadable(workdir)
                     db.failed_stage = 'uploading packages to apt repo'
                     uploadBuild(pocket, workdir)
 
                     db.failed_stage = 'cleaning up'
                     db.failed_stage = 'uploading packages to apt repo'
                     uploadBuild(pocket, workdir)
 
                     db.failed_stage = 'cleaning up'
-
-                # Finally, now that everything is done, remove the
-                # build queue item
-                os.unlink(os.path.join(b._QUEUE_DIR, build))
         except:
             db.traceback = traceback.format_exc()
         else:
             db.succeeded = True
             db.failed_stage = None
         finally:
         except:
             db.traceback = traceback.format_exc()
         else:
             db.succeeded = True
             db.failed_stage = None
         finally:
+            if logfile is not None:
+                logfile.close()
+
             database.session.save_or_update(db)
             database.session.commit()
 
             database.session.save_or_update(db)
             database.session.commit()
 
-            reportBuild(db)
+            # Finally, now that everything is done, remove the
+            # build queue item
+            os.unlink(os.path.join(b._QUEUE_DIR, build))
 
 
+            if db.succeeded:
+                b.runHook('post-build', [str(db.build_id)])
+            else:
+                b.runHook('failed-build', [str(db.build_id)])
 
 class Invirtibuilder(pyinotify.ProcessEvent):
     """Process inotify triggers to build new packages."""
 
 class Invirtibuilder(pyinotify.ProcessEvent):
     """Process inotify triggers to build new packages."""