Fixes to invirtibuilder for case of missing package in super-repo, missing
[invirt/packages/invirt-dev.git] / invirtibuilder
index aa70b53..eac57f3 100755 (executable)
@@ -47,8 +47,23 @@ from invirt import database
 from invirt.config import structs as config
 
 
 from invirt.config import structs as config
 
 
-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, ))
+    if 'stdin_str' in kwargs:
+        logfile.write('STDIN:\n')
+        logfile.write(kwargs['stdin_str'])
+    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.
@@ -93,43 +108,53 @@ 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(package, commit, dst_pocket, src_pocket):
     """Copy a package from one pocket to another."""
     binaries = getBinaries(package, commit)
 
 
 def aptCopy(package, commit, dst_pocket, src_pocket):
     """Copy a package from one pocket to another."""
     binaries = getBinaries(package, commit)
-    c.captureOutput(['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."""
-    args = ['sbuild', '-v', '-d', DISTRIBUTION, '--arch', arch]
+    logAndRun(['reprepro-env', 'copy',
+               b.pocketToApt(dst_pocket),
+               b.pocketToApt(src_pocket),
+               package] + binaries)
+
+
+def sbuild(package, ref, distro, arch, workdir, arch_all=False):
+    """Build a package for a particular architecture and distro."""
+    # We append a suffix like ~ubuntu8.04 to differentiate the same
+    # version built for multiple distros
+    nmutag = b.distroToSuffix(distro)
+    env = os.environ.copy()
+    env['NMUTAG'] = nmutag
+
+    # Run sbuild with a hack in place to append arbitrary versions
+    args = ['perl', '-I/usr/share/invirt-dev', '-MSbuildHack',
+            '/usr/bin/sbuild',
+            '--binNMU=171717', '--make-binNMU=Build with sbuild',
+            '-v', '-d', distro, '--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)
+    logAndRun(args, cwd=workdir, env=env)
 
 
 
 
-def sbuildAll(package, ref, workdir):
+def sbuildAll(package, ref, distro, workdir):
     """Build a package for all architectures it supports."""
     arches = getArches(package, ref)
     if 'all' in arches or 'any' in arches or 'amd64' in arches:
     """Build a package for all architectures it supports."""
     arches = getArches(package, ref)
     if 'all' in arches or 'any' in arches or 'amd64' in arches:
-        sbuild(package, ref, 'amd64', workdir, arch_all=True)
+        sbuild(package, ref, distro, 'amd64', workdir, arch_all=True)
     if 'any' in arches or 'i386' in arches:
     if 'any' in arches or 'i386' in arches:
-        sbuild(package, ref, 'i386', workdir)
+        sbuild(package, ref, distro, 'i386', workdir)
 
 
 
 
-def tagSubmodule(pocket, package, principal, version, env):
+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
@@ -151,28 +176,38 @@ def tagSubmodule(pocket, package, principal, version, env):
                                         package,
                                         principal))
 
                                         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], cwd=b.getRepo(package))
+    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',
-                       '--ignore=wrongdistribution',
-                       'include',
-                       apt,
-                       changes])
+        upload = ['reprepro-env', '--ignore=wrongdistribution',
+                  'include', apt, changes]
+        try:
+            logAndRun(upload)
+        except subprocess.CalledProcessError, e:
+            if not force:
+                raise
+            changelog = deb822.Changes(open(changes).read())
+            packages = set(changelog['Binary'].split())
+            packages.add(changelog['Source'])
+            for package in packages:
+                logAndRun(['reprepro-env', 'remove', apt, package])
+            logAndRun(upload)
 
 
 def updateSuperproject(pocket, package, commit, principal, version, env):
 
 
 def updateSuperproject(pocket, package, commit, principal, version, env):
@@ -185,30 +220,76 @@ def updateSuperproject(pocket, package, commit, principal, version, env):
     pushes to the superproject.
     """
     superproject = os.path.join(b._REPO_DIR, 'invirt/packages.git')
     pushes to the superproject.
     """
     superproject = os.path.join(b._REPO_DIR, 'invirt/packages.git')
+
     branch = b.pocketToGit(pocket)
     branch = b.pocketToGit(pocket)
-    tree = c.captureOutput(['git', 'ls-tree', branch],
-                           cwd=superproject).strip()
-
-    new_tree = re.compile(
-        r'^(160000 commit )[0-9a-f]*(\t%s)$' % package, re.M).sub(
-        r'\g<1>%s\g<2>' % commit,
-        tree)
-
-    new_tree_id = c.captureOutput(['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))
-    new_commit = c.captureOutput(
+
+    if not b.pocketExists(pocket, superproject):
+        gitmodules = "\n"
+        gitmodules_hash = logAndRun(['git', 'hash-object', '-w', '--stdin'],
+                                    cwd=superproject,
+                                    stdin_str=gitmodules).strip()
+        tree_items = {'.gitmodules': "100644 blob "+gitmodules_hash}
+        new_tree = "\n".join("%s\t%s" % (v, k) for (k, v) in tree_items.iteritems())
+        new_tree_id = logAndRun(['git', 'mktree', '--missing'],
+                                cwd=superproject,
+                                stdin_str=new_tree).strip()
+        env2 = dict(os.environ)
+        env2['GIT_AUTHOR_NAME'] = config.build.tagger.name
+        env2['GIT_AUTHOR_EMAIL'] = config.build.tagger.email
+        env2['GIT_COMMITTER_NAME'] = config.build.tagger.name
+        env2['GIT_COMMITTER_EMAIL'] = config.build.tagger.email
+        new_commit = logAndRun(['git', 'commit-tree', new_tree_id],
+                               cwd=superproject,
+                               env=env2,
+                               stdin_str="Create new pocket").strip()
+        logAndRun(['git', 'update-ref', 'refs/heads/%s' % branch, new_commit],
+                  cwd=superproject)
+
+    tree = logAndRun(['git', 'ls-tree', branch],
+                     cwd=superproject).strip()
+
+    tree_items = dict((k, v) for (v, k) in (x.split("\t") for x in tree.split("\n")))
+
+    created = not (package in tree_items)
+
+    tree_items[package] = "160000 commit "+commit
+
+    # If "created" is true, we need to check if the package is
+    # mentioned in .gitmodules, and add it if not.
+    if created:
+        gitmodules = logAndRun(['git', 'cat-file', 'blob', '%s:.gitmodules' % (branch)],
+                               cwd=superproject)
+        if ('[submodule "%s"]' % (package)) not in gitmodules.split("\n"):
+            gitmodules += """[submodule "%s"]
+\tpath = %s
+\turl = ../packages/%s.git
+""" % (package, package, package)
+            gitmodules_hash = logAndRun(['git', 'hash-object', '-w', '--stdin'],
+                                        cwd=superproject,
+                                        stdin_str=gitmodules).strip()
+            tree_items['.gitmodules'] = "100644 blob "+gitmodules_hash
+
+    new_tree = "\n".join("%s\t%s" % (v, k) for (k, v) in tree_items.iteritems())
+
+    new_tree_id = logAndRun(['git', 'mktree', '--missing'],
+                            cwd=superproject,
+                            stdin_str=new_tree).strip()
+
+    if created:
+        commit_msg = 'Add %s at version %s'
+    else:
+        commit_msg = 'Update %s to version %s'
+    commit_msg = ((commit_msg + '\n\n'
+                   'Requested by %s') % (package,
+                                         version.full_version,
+                                         principal))
+    new_commit = logAndRun(
         ['git', 'commit-tree', new_tree_id, '-p', branch],
         cwd=superproject,
         env=env,
         stdin_str=commit_msg).strip()
 
         ['git', 'commit-tree', new_tree_id, '-p', branch],
         cwd=superproject,
         env=env,
         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)
 
@@ -217,7 +298,7 @@ def makeReadable(workdir):
     os.chmod(workdir, 0755)
 
 @contextlib.contextmanager
     os.chmod(workdir, 0755)
 
 @contextlib.contextmanager
-def packageWorkdir(package, commit):
+def packageWorkdir(package, commit, build_id):
     """Checkout the package in a temporary working directory.
 
     This context manager returns that working directory. The requested
     """Checkout the package in a temporary working directory.
 
     This context manager returns that working directory. The requested
@@ -227,11 +308,11 @@ def packageWorkdir(package, commit):
     When the context wrapped with this context manager is exited, the
     working directory is automatically deleted.
     """
     When the context wrapped with this context manager is exited, the
     working directory is automatically deleted.
     """
-    workdir = tempfile.mkdtemp()
+    workdir = tempfile.mkdtemp(prefix=("b%d-" % build_id))
     try:
         p_archive = subprocess.Popen(
     try:
         p_archive = subprocess.Popen(
-            ['git', 'archive',
-             '--remote=file://%s' % b.getRepo(package),
+            ['git', '--git-dir=%s' % (b.getRepo(package),),
+             'archive',
              '--prefix=%s/' % package,
              commit,
              ],
              '--prefix=%s/' % package,
              commit,
              ],
@@ -249,22 +330,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)
@@ -286,15 +359,29 @@ def build():
 
         database.session.begin()
 
 
         database.session.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 = b.validateBuild(pocket, package, commit)
             # Don't expand the commit in the DB until we're sure the user
             # isn't trying to be tricky.
             # Don't expand the commit in the DB until we're sure the user
             # isn't trying to be tricky.
-            db.commit = commit = c.captureOutput(['git', 'rev-parse', commit],
-                                                 cwd=b.getRepo(package)).strip()
+            b.ensureValidPackage(package)
+
+            logfile = open(os.path.join(logdir, '%s.log' % db.package), 'w')
 
 
-            db.version = str(b.getVersion(package, commit))
+            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)])
+
+            env = dict(os.environ)
+            env['GIT_COMMITTER_NAME'] = config.build.tagger.name
+            env['GIT_COMMITTER_EMAIL'] = config.build.tagger.email
+            env['GIT_AUTHOR_NAME'] = principal.split('@')[0]
+            env['GIT_AUTHOR_EMAIL'] = principal
 
             # 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.
@@ -302,14 +389,22 @@ 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'
                 aptCopy(package, commit, pocket, src)
                 db.failed_stage = 'copying package from another pocket'
                 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
             else:
                 db.failed_stage = 'checking out package source'
             # If we can't copy the package from somewhere, but
             # validateBuild didn't raise an exception, then we need to
             # do the build ourselves
             else:
                 db.failed_stage = 'checking out package source'
-                with packageWorkdir(package, commit) as workdir:
+                with packageWorkdir(package, commit, db.build_id) as workdir:
                     db.failed_stage = 'preparing source package'
                     packagedir = os.path.join(workdir, package)
 
                     db.failed_stage = 'preparing source package'
                     packagedir = os.path.join(workdir, package)
 
@@ -320,29 +415,15 @@ 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, str(db.build_id))
-                        if not os.path.exists(logdir):
-                            os.makedirs(logdir)
-
-                        for log in glob.glob(os.path.join(workdir, 'build-*.log')):
-                            os.copy(log, logdir)
-
-                    db.failed_stage = 'processing metadata'
-                    env = dict(os.environ)
-                    env['GIT_COMMITTER_NAME'] = config.build.tagger.name
-                    env['GIT_COMMITTER_EMAIL'] = config.build.tagger.email
-                    version = b.getVersion(package, commit)
+                    logAndRun(['schroot', '-c', 
+                               '%s-amd64-sbuild' % (b.pocketToDistro(pocket),), 
+                               '--', 'dpkg-buildpackage', '-us', '-uc', '-S'],
+                              cwd=packagedir)
 
 
+                    db.failed_stage = 'building binary packages'
+                    sbuildAll(package, commit, b.pocketToDistro(pocket), workdir)
                     db.failed_stage = 'tagging submodule'
                     db.failed_stage = 'tagging submodule'
-                    tagSubmodule(pocket, package, principal, version, env)
+                    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'
@@ -359,6 +440,9 @@ def build():
             db.succeeded = True
             db.failed_stage = None
         finally:
             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()
 
@@ -366,8 +450,10 @@ def build():
             # build queue item
             os.unlink(os.path.join(b._QUEUE_DIR, build))
 
             # build queue item
             os.unlink(os.path.join(b._QUEUE_DIR, build))
 
-            reportBuild(db)
-
+            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."""