source: trunk/packages/invirt-dev/invirtibuilder @ 2543

Last change on this file since 2543 was 2543, checked in by broder, 14 years ago

Pull functions that are needed for the git remctl scripts out of the
invirtibuilder and into a common module.

  • Property svn:executable set to *
File size: 12.4 KB
Line 
1#!/usr/bin/python
2
3"""Process the Invirt build queue.
4
5The Invirtibuilder handles package builds and uploads. On demand, it
6attempts to build a particular package.
7
8If the build succeeds, the new version of the package is uploaded to
9the apt repository, tagged in its git repository, and the Invirt
10superrepo is updated to point at the new version.
11
12If the build fails, the Invirtibuilder sends mail with the build log.
13
14The build queue is tracked via files in /var/lib/invirt-dev/queue. In
15order to maintain ordering, all filenames in that directory are the
16timestamp of their creation time.
17
18Each queue file contains a file of the form
19
20    pocket package hash principal
21
22where pocket is one of the pockets globally configured in
23git.pockets. For instance, the pockets in XVM are "prod" and "dev".
24
25principal is the Kerberos principal that requested the build.
26"""
27
28
29import contextlib
30import os
31import re
32import shutil
33import subprocess
34
35import pyinotify
36
37import invirt.builder as b
38from invirt import database
39
40
41DISTRIBUTION = 'hardy'
42
43
44def getControl(package, ref):
45    """Get the parsed debian/control file for a given package.
46
47    This returns a list of debian_bundle.deb822.Deb822 objects, one
48    for each section of the debian/control file. Each Deb822 object
49    acts roughly like a dict.
50    """
51    return deb822.Deb822.iter_paragraphs(
52        getGitFile(package, ref, 'debian/control').split('\n'))
53
54
55def getBinaries(package, ref):
56    """Get a list of binary packages in a package at a given ref."""
57    return [p['Package'] for p in getControl(package, ref)
58            if 'Package' in p]
59
60
61def getArches(package, ref):
62    """Get the set of all architectures in any binary package."""
63    arches = set()
64    for section in getControl(package, ref):
65        if 'Architecture' in section:
66            arches.update(section['Architecture'].split())
67    return arches
68
69
70def getDscName(package, ref):
71    """Return the .dsc file that will be generated for this package."""
72    v = getVersion(package, ref)
73    return '%s_%s-%s.dsc' % (
74        package,
75        version.upstream_version,
76        version.debian_version)
77
78
79def sanitizeVersion(version):
80    """Sanitize a Debian package version for use as a git tag.
81
82    This function strips the epoch from the version number and
83    replaces any tildes with periods."""
84    v = '%s-%s' % (version.upstream_version,
85                   version.debian_version)
86    return v.replace('~', '.')
87
88
89def aptCopy(packages, dst_pocket, src_pocket):
90    """Copy a package from one pocket to another."""
91    binaries = []
92    for line in b.getGitFile(package, commit, 'debian/control').split('\n'):
93        m = re.match('Package: (.*)$')
94        if m:
95            binaries.append(m.group(1))
96
97    cpatureOutput(['reprepro-env', 'copy',
98                   b.pocketToApt(dst_pocket),
99                   b.pocketToApt(src_pocket),
100                   package] + binaries)
101
102
103def sbuild(package, ref, arch, workdir, arch_all=False):
104    """Build a package for a particular architecture."""
105    args = ['sbuild', '-d', DISTRIBUTION, '--arch', arch]
106    if arch_all:
107        args.append('-A')
108    args.append(getDscName(package, ref))
109    c.captureOutput(args, cwd=workdir, stdout=None)
110
111
112def sbuildAll(package, ref, workdir):
113    """Build a package for all architectures it supports."""
114    arches = getArches(package, ref)
115    if 'all' in arches or 'any' in arches or 'amd64' in arches:
116        sbuild(package, ref, 'amd64', workdir, arch_all=True)
117    if 'any' in arches or 'i386' in arches:
118        sbuild(package, ref, 'i386', workdir)
119
120
121def tagSubmodule(pocket, package, ref, principal):
122    """Tag a new version of a submodule.
123
124    If this pocket does not allow_backtracking, then this will create
125    a new tag of the version at ref.
126
127    This function doesn't need to care about lock
128    contention. git-receive-pack updates one ref at a time, and only
129    takes out a lock for that ref after it's passed the update
130    hook. Because we reject pushes to tags in the update hook, no push
131    can ever take out a lock on any tags.
132
133    I'm sure that long description gives you great confidence in teh
134    legitimacy of my reasoning.
135    """
136    if config.git.pockets[pocket].get('allow_backtracking', False):
137        env = dict(os.environ)
138        branch = b.pocketToGit(pocket)
139        version = b.getVersion(package, ref)
140
141        env['GIT_COMMITTER_NAME'] = config.git.tagger.name
142        env['GIT_COMMITTER_EMAIL'] = config.git.tagger.email
143        tag_msg = ('Tag %s of %s\n\n'
144                   'Requested by %s' % (version.full_version,
145                                        package,
146                                        principal))
147
148        c.captureOutput(
149            ['git', 'tag', '-m', tag_msg, commit],
150            stdout=None,
151            env=env)
152
153
154def updateSubmoduleBranch(pocket, package, ref):
155    """Update the appropriately named branch in the submodule."""
156    branch = b.pocketToGit(pocket)
157    c.captureOutput(
158        ['git', 'update-ref', 'refs/heads/%s' % branch, ref])
159
160
161def uploadBuild(pocket, workdir):
162    """Upload all build products in the work directory."""
163    apt = b.pocketToApt(pocket)
164    for changes in glob.glob(os.path.join(workdir, '*.changes')):
165        c.captureOutput(['reprepro-env',
166                       'include',
167                       '--ignore=wrongdistribution',
168                       apt,
169                       changes])
170
171
172def updateSuperrepo(pocket, package, commit, principal):
173    """Update the superrepo.
174
175    This will create a new commit on the branch for the given pocket
176    that sets the commit for the package submodule to commit.
177
178    Note that there's no locking issue here, because we disallow all
179    pushes to the superrepo.
180    """
181    superrepo = os.path.join(b._REPO_DIR, 'packages.git')
182    branch = b.pocketToGit(pocket)
183    tree = c.captureOutput(['git', 'ls-tree', branch],
184                         cwd=superrepo)
185
186    new_tree = re.compile(
187        r'^(160000 commit )[0-9a-f]*(\t%s)$' % package, re.M).sub(
188        r'\1%s\2' % commit,
189        tree)
190
191    new_tree_id = c.captureOutput(['git', 'mktree'],
192                                cwd=superrepo,
193                                stdin_str=new_tree)
194
195    commit_msg = ('Update %s to version %s\n\n'
196                  'Requested by %s' % (package,
197                                       version.full_version,
198                                       principal))
199    new_commit = c.captureOutput(
200        ['git', 'commit-tree', new_tree_hash, '-p', branch],
201        cwd=superrepo,
202        env=env,
203        stdin_str=commit_msg)
204
205    c.captureOutput(
206        ['git', 'update-ref', 'refs/heads/%s' % branch, new_commit],
207        cwd=superrepo)
208
209
210@contextlib.contextmanager
211def packageWorkdir(package):
212    """Checkout the package in a temporary working directory.
213
214    This context manager returns that working directory. The requested
215    package is checked out into a subdirectory of the working
216    directory with the same name as the package.
217
218    When the context wrapped with this context manager is exited, the
219    working directory is automatically deleted.
220    """
221    workdir = tempfile.mkdtemp()
222    try:
223        p_archive = subprocess.Popen(
224            ['git', 'archive',
225             '--remote=file://%s' % b.getRepo(package),
226             '--prefix=%s' % package,
227             commit,
228             ],
229            stdout=subprocess.PIPE,
230            )
231        p_tar = subprocess.Popen(
232            ['tar', '-x'],
233            stdin=p_archive.stdout,
234            cwd=workdir,
235            )
236        p_archive.wait()
237        p_tar.wait()
238
239        yield workdir
240    finally:
241        shutil.rmtree(workdir)
242
243
244def reportBuild(build):
245    """Run hooks to report the results of a build attempt."""
246
247    c.captureOutput(['run-parts',
248                   '--arg=%s' % build.build_id,
249                   '--',
250                   b._HOOKS_DIR])
251
252
253def build():
254    """Deal with items in the build queue.
255
256    When triggered, iterate over build queue items one at a time,
257    until there are no more pending build jobs.
258    """
259    while True:
260        stage = 'processing incoming job'
261        queue = os.listdir(b._QUEUE_DIR)
262        if not queue:
263            break
264
265        build = min(queue)
266        job = open(os.path.join(b._QUEUE_DIR, build)).read().strip()
267        pocket, package, commit, principal = job.split()
268
269        database.session.begin()
270        db = database.Build()
271        db.package = package
272        db.pocket = pocket
273        db.commit = commit
274        db.principal = principal
275        database.session.save_or_update(db)
276        database.commit()
277
278        database.begin()
279
280        try:
281            db.failed_stage = 'validating job'
282            src = validateBuild(pocket, package, commit)
283
284            db.version = str(b.getVersion(package, commit))
285
286            # If validateBuild returns something other than True, then
287            # it means we should copy from that pocket to our pocket.
288            #
289            # (If the validation failed, validateBuild would have
290            # raised an exception)
291            if src != True:
292                db.failed_stage = 'copying package from another pocket'
293                aptCopy(packages, pocket, src)
294            # If we can't copy the package from somewhere, but
295            # validateBuild didn't raise an exception, then we need to
296            # do the build ourselves
297            else:
298                db.failed_stage = 'checking out package source'
299                with packageWorkdir(package) as workdir:
300                    db.failed_stage = 'preparing source package'
301                    packagedir = os.path.join(workdir, package)
302
303                    # We should be more clever about dealing with
304                    # things like non-Debian-native packages than we
305                    # are.
306                    #
307                    # If we were, we could use debuild and get nice
308                    # environment scrubbing. Since we're not, debuild
309                    # complains about not having an orig.tar.gz
310                    c.captureOutput(['dpkg-buildpackage', '-us', '-uc', '-S'],
311                                  cwd=packagedir,
312                                  stdout=None)
313
314                    try:
315                        db.failed_stage = 'building binary packages'
316                        sbuildAll(package, commit, workdir)
317                    finally:
318                        logdir = os.path.join(b._LOG_DIR, db.build_id)
319                        if not os.path.exists(logdir):
320                            os.makedirs(logdir)
321
322                        for log in glob.glob(os.path.join(workdir, '*.build')):
323                            os.copy2(log, logdir)
324                    db.failed_stage = 'tagging submodule'
325                    tagSubmodule(pocket, package, commit, principal)
326                    db.failed_stage = 'updating submodule branches'
327                    updateSubmoduleBranch(pocket, package, commit)
328                    db.failed_stage = 'updating superrepo'
329                    updateSuperrepo(pocket, package, commit, principal)
330                    db.failed_stage = 'uploading packages to apt repo'
331                    uploadBuild(pocket, workdir)
332
333                    db.failed_stage = 'cleaning up'
334
335                # Finally, now that everything is done, remove the
336                # build queue item
337                os.unlink(os.path.join(b._QUEUE_DIR, build))
338        except:
339            db.traceback = traceback.format_exc()
340        else:
341            db.succeeded = True
342            db.failed_stage = None
343        finally:
344            database.session.save_or_update(db)
345            database.session.commit()
346
347            reportBuild(db)
348
349
350class Invirtibuilder(pyinotify.ProcessEvent):
351    """Process inotify triggers to build new packages."""
352    def process_IN_CREATE(self, event):
353        """Handle a created file or directory.
354
355        When an IN_CREATE event comes in, trigger the builder.
356        """
357        build()
358
359
360def main():
361    """Initialize the inotifications and start the main loop."""
362    database.connect()
363
364    watch_manager = pyinotify.WatchManager()
365    invirtibuilder = Invirtibuilder()
366    notifier = pyinotify.Notifier(watch_manager, invirtibuilder)
367    watch_manager.add_watch(b._QUEUE_DIR,
368                            pyinotify.EventsCodes.ALL_FLAGS['IN_CREATE'])
369
370    # Before inotifying, run any pending builds; otherwise we won't
371    # get notified for them.
372    build()
373
374    while True:
375        notifier.process_events()
376        if notifier.check_events():
377            notifier.read_events()
378
379
380if __name__ == '__main__':
381    main()
Note: See TracBrowser for help on using the repository browser.