3 """Process the Invirt build queue.
5 The Invirtibuilder handles package builds and uploads. On demand, it
6 attempts to build a particular package.
8 If the build succeeds, the new version of the package is uploaded to
9 the apt repository, tagged in its git repository, and the Invirt
10 superproject is updated to point at the new version.
12 If the build fails, the Invirtibuilder sends mail with the build log.
14 The build queue is tracked via files in /var/lib/invirt-dev/queue. In
15 order to maintain ordering, all filenames in that directory are the
16 timestamp of their creation time.
18 Each queue file contains a file of the form
20 pocket package hash principal
22 where pocket is one of the pockets globally configured in
23 build.pockets. For instance, the pockets in XVM are "prod" and "dev".
25 principal is the Kerberos principal that requested the build.
29 from __future__ import with_statement
39 import invirt.builder as b
40 from invirt import database
43 DISTRIBUTION = 'hardy'
46 def getControl(package, ref):
47 """Get the parsed debian/control file for a given package.
49 This returns a list of debian_bundle.deb822.Deb822 objects, one
50 for each section of the debian/control file. Each Deb822 object
51 acts roughly like a dict.
53 return deb822.Deb822.iter_paragraphs(
54 getGitFile(package, ref, 'debian/control').split('\n'))
57 def getBinaries(package, ref):
58 """Get a list of binary packages in a package at a given ref."""
59 return [p['Package'] for p in getControl(package, ref)
63 def getArches(package, ref):
64 """Get the set of all architectures in any binary package."""
66 for section in getControl(package, ref):
67 if 'Architecture' in section:
68 arches.update(section['Architecture'].split())
72 def getDscName(package, ref):
73 """Return the .dsc file that will be generated for this package."""
74 v = getVersion(package, ref)
76 v_str = '%s-%s' % (v.upstream_version,
79 v_str = v.upstream_version
80 return '%s_%s.dsc' % (
85 def sanitizeVersion(version):
86 """Sanitize a Debian package version for use as a git tag.
88 This function strips the epoch from the version number and
89 replaces any tildes with periods."""
91 v = '%s-%s' % (version.upstream_version,
92 version.debian_version)
94 v = version.upstream_version
95 return v.replace('~', '.')
98 def aptCopy(packages, dst_pocket, src_pocket):
99 """Copy a package from one pocket to another."""
100 binaries = getBinaries(package, commit)
101 cpatureOutput(['reprepro-env', 'copy',
102 b.pocketToApt(dst_pocket),
103 b.pocketToApt(src_pocket),
107 def sbuild(package, ref, arch, workdir, arch_all=False):
108 """Build a package for a particular architecture."""
109 args = ['sbuild', '-d', DISTRIBUTION, '--arch', arch]
112 args.append(getDscName(package, ref))
113 c.captureOutput(args, cwd=workdir, stdout=None)
116 def sbuildAll(package, ref, workdir):
117 """Build a package for all architectures it supports."""
118 arches = getArches(package, ref)
119 if 'all' in arches or 'any' in arches or 'amd64' in arches:
120 sbuild(package, ref, 'amd64', workdir, arch_all=True)
121 if 'any' in arches or 'i386' in arches:
122 sbuild(package, ref, 'i386', workdir)
125 def tagSubmodule(pocket, package, ref, principal):
126 """Tag a new version of a submodule.
128 If this pocket does not allow_backtracking, then this will create
129 a new tag of the version at ref.
131 This function doesn't need to care about lock
132 contention. git-receive-pack updates one ref at a time, and only
133 takes out a lock for that ref after it's passed the update
134 hook. Because we reject pushes to tags in the update hook, no push
135 can ever take out a lock on any tags.
137 I'm sure that long description gives you great confidence in the
138 legitimacy of my reasoning.
140 if not config.build.pockets[pocket].get('allow_backtracking', False):
141 env = dict(os.environ)
142 branch = b.pocketToGit(pocket)
143 version = b.getVersion(package, ref)
145 env['GIT_COMMITTER_NAME'] = config.build.tagger.name
146 env['GIT_COMMITTER_EMAIL'] = config.build.tagger.email
147 tag_msg = ('Tag %s of %s\n\n'
148 'Requested by %s' % (version.full_version,
153 ['git', 'tag', '-m', tag_msg, commit],
158 def updateSubmoduleBranch(pocket, package, ref):
159 """Update the appropriately named branch in the submodule."""
160 branch = b.pocketToGit(pocket)
162 ['git', 'update-ref', 'refs/heads/%s' % branch, ref])
165 def uploadBuild(pocket, workdir):
166 """Upload all build products in the work directory."""
167 apt = b.pocketToApt(pocket)
168 for changes in glob.glob(os.path.join(workdir, '*.changes')):
169 c.captureOutput(['reprepro-env',
171 '--ignore=wrongdistribution',
176 def updateSuperproject(pocket, package, commit, principal):
177 """Update the superproject.
179 This will create a new commit on the branch for the given pocket
180 that sets the commit for the package submodule to commit.
182 Note that there's no locking issue here, because we disallow all
183 pushes to the superproject.
185 superproject = os.path.join(b._REPO_DIR, 'invirt/packages.git')
186 branch = b.pocketToGit(pocket)
187 tree = c.captureOutput(['git', 'ls-tree', branch],
190 new_tree = re.compile(
191 r'^(160000 commit )[0-9a-f]*(\t%s)$' % package, re.M).sub(
195 new_tree_id = c.captureOutput(['git', 'mktree'],
199 commit_msg = ('Update %s to version %s\n\n'
200 'Requested by %s' % (package,
201 version.full_version,
203 new_commit = c.captureOutput(
204 ['git', 'commit-tree', new_tree_hash, '-p', branch],
207 stdin_str=commit_msg)
210 ['git', 'update-ref', 'refs/heads/%s' % branch, new_commit],
214 @contextlib.contextmanager
215 def packageWorkdir(package, commit):
216 """Checkout the package in a temporary working directory.
218 This context manager returns that working directory. The requested
219 package is checked out into a subdirectory of the working
220 directory with the same name as the package.
222 When the context wrapped with this context manager is exited, the
223 working directory is automatically deleted.
225 workdir = tempfile.mkdtemp()
227 p_archive = subprocess.Popen(
229 '--remote=file://%s' % b.getRepo(package),
230 '--prefix=%s' % package,
233 stdout=subprocess.PIPE,
235 p_tar = subprocess.Popen(
237 stdin=p_archive.stdout,
245 shutil.rmtree(workdir)
248 def reportBuild(build):
249 """Run hooks to report the results of a build attempt."""
251 c.captureOutput(['run-parts',
252 '--arg=%s' % build.build_id,
258 """Deal with items in the build queue.
260 When triggered, iterate over build queue items one at a time,
261 until there are no more pending build jobs.
264 stage = 'processing incoming job'
265 queue = os.listdir(b._QUEUE_DIR)
270 job = open(os.path.join(b._QUEUE_DIR, build)).read().strip()
271 pocket, package, commit, principal = job.split()
273 database.session.begin()
274 db = database.Build()
278 db.principal = principal
279 database.session.save_or_update(db)
285 db.failed_stage = 'validating job'
286 src = validateBuild(pocket, package, commit)
288 db.version = str(b.getVersion(package, commit))
290 # If validateBuild returns something other than True, then
291 # it means we should copy from that pocket to our pocket.
293 # (If the validation failed, validateBuild would have
294 # raised an exception)
296 db.failed_stage = 'copying package from another pocket'
297 aptCopy(packages, pocket, src)
298 # If we can't copy the package from somewhere, but
299 # validateBuild didn't raise an exception, then we need to
300 # do the build ourselves
302 db.failed_stage = 'checking out package source'
303 with packageWorkdir(package, commit) as workdir:
304 db.failed_stage = 'preparing source package'
305 packagedir = os.path.join(workdir, package)
307 # We should be more clever about dealing with
308 # things like non-Debian-native packages than we
311 # If we were, we could use debuild and get nice
312 # environment scrubbing. Since we're not, debuild
313 # complains about not having an orig.tar.gz
314 c.captureOutput(['dpkg-buildpackage', '-us', '-uc', '-S'],
319 db.failed_stage = 'building binary packages'
320 sbuildAll(package, commit, workdir)
322 logdir = os.path.join(b._LOG_DIR, db.build_id)
323 if not os.path.exists(logdir):
326 for log in glob.glob(os.path.join(workdir, '*.build')):
327 os.copy2(log, logdir)
328 db.failed_stage = 'tagging submodule'
329 tagSubmodule(pocket, package, commit, principal)
330 db.failed_stage = 'updating submodule branches'
331 updateSubmoduleBranch(pocket, package, commit)
332 db.failed_stage = 'updating superproject'
333 updateSuperproject(pocket, package, commit, principal)
334 db.failed_stage = 'uploading packages to apt repo'
335 uploadBuild(pocket, workdir)
337 db.failed_stage = 'cleaning up'
339 # Finally, now that everything is done, remove the
341 os.unlink(os.path.join(b._QUEUE_DIR, build))
343 db.traceback = traceback.format_exc()
346 db.failed_stage = None
348 database.session.save_or_update(db)
349 database.session.commit()
354 class Invirtibuilder(pyinotify.ProcessEvent):
355 """Process inotify triggers to build new packages."""
356 def process_default(self, event):
357 """Handle an inotify event.
359 When an inotify event comes in, trigger the builder.
365 """Initialize the inotifications and start the main loop."""
368 watch_manager = pyinotify.WatchManager()
369 invirtibuilder = Invirtibuilder()
370 notifier = pyinotify.Notifier(watch_manager, invirtibuilder)
371 watch_manager.add_watch(b._QUEUE_DIR,
372 pyinotify.EventsCodes.ALL_FLAGS['IN_CREATE'] |
373 pyinotify.EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
375 # Before inotifying, run any pending builds; otherwise we won't
376 # get notified for them.
380 notifier.process_events()
381 if notifier.check_events():
382 notifier.read_events()
385 if __name__ == '__main__':