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 superrepo 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 git.pockets. For instance, the pockets in XVM are "prod" and "dev".
25 principal is the Kerberos principal that requested the build.
37 import invirt.builder as b
38 from invirt import database
41 DISTRIBUTION = 'hardy'
44 def getControl(package, ref):
45 """Get the parsed debian/control file for a given package.
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.
51 return deb822.Deb822.iter_paragraphs(
52 getGitFile(package, ref, 'debian/control').split('\n'))
55 def 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)
61 def getArches(package, ref):
62 """Get the set of all architectures in any binary package."""
64 for section in getControl(package, ref):
65 if 'Architecture' in section:
66 arches.update(section['Architecture'].split())
70 def 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' % (
75 version.upstream_version,
76 version.debian_version)
79 def sanitizeVersion(version):
80 """Sanitize a Debian package version for use as a git tag.
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('~', '.')
89 def aptCopy(packages, dst_pocket, src_pocket):
90 """Copy a package from one pocket to another."""
92 for line in b.getGitFile(package, commit, 'debian/control').split('\n'):
93 m = re.match('Package: (.*)$')
95 binaries.append(m.group(1))
97 cpatureOutput(['reprepro-env', 'copy',
98 b.pocketToApt(dst_pocket),
99 b.pocketToApt(src_pocket),
103 def sbuild(package, ref, arch, workdir, arch_all=False):
104 """Build a package for a particular architecture."""
105 args = ['sbuild', '-d', DISTRIBUTION, '--arch', arch]
108 args.append(getDscName(package, ref))
109 c.captureOutput(args, cwd=workdir, stdout=None)
112 def 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)
121 def tagSubmodule(pocket, package, ref, principal):
122 """Tag a new version of a submodule.
124 If this pocket does not allow_backtracking, then this will create
125 a new tag of the version at ref.
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.
133 I'm sure that long description gives you great confidence in teh
134 legitimacy of my reasoning.
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)
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,
149 ['git', 'tag', '-m', tag_msg, commit],
154 def updateSubmoduleBranch(pocket, package, ref):
155 """Update the appropriately named branch in the submodule."""
156 branch = b.pocketToGit(pocket)
158 ['git', 'update-ref', 'refs/heads/%s' % branch, ref])
161 def 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',
167 '--ignore=wrongdistribution',
172 def updateSuperrepo(pocket, package, commit, principal):
173 """Update the superrepo.
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.
178 Note that there's no locking issue here, because we disallow all
179 pushes to the superrepo.
181 superrepo = os.path.join(b._REPO_DIR, 'packages.git')
182 branch = b.pocketToGit(pocket)
183 tree = c.captureOutput(['git', 'ls-tree', branch],
186 new_tree = re.compile(
187 r'^(160000 commit )[0-9a-f]*(\t%s)$' % package, re.M).sub(
191 new_tree_id = c.captureOutput(['git', 'mktree'],
195 commit_msg = ('Update %s to version %s\n\n'
196 'Requested by %s' % (package,
197 version.full_version,
199 new_commit = c.captureOutput(
200 ['git', 'commit-tree', new_tree_hash, '-p', branch],
203 stdin_str=commit_msg)
206 ['git', 'update-ref', 'refs/heads/%s' % branch, new_commit],
210 @contextlib.contextmanager
211 def packageWorkdir(package):
212 """Checkout the package in a temporary working directory.
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.
218 When the context wrapped with this context manager is exited, the
219 working directory is automatically deleted.
221 workdir = tempfile.mkdtemp()
223 p_archive = subprocess.Popen(
225 '--remote=file://%s' % b.getRepo(package),
226 '--prefix=%s' % package,
229 stdout=subprocess.PIPE,
231 p_tar = subprocess.Popen(
233 stdin=p_archive.stdout,
241 shutil.rmtree(workdir)
244 def reportBuild(build):
245 """Run hooks to report the results of a build attempt."""
247 c.captureOutput(['run-parts',
248 '--arg=%s' % build.build_id,
254 """Deal with items in the build queue.
256 When triggered, iterate over build queue items one at a time,
257 until there are no more pending build jobs.
260 stage = 'processing incoming job'
261 queue = os.listdir(b._QUEUE_DIR)
266 job = open(os.path.join(b._QUEUE_DIR, build)).read().strip()
267 pocket, package, commit, principal = job.split()
269 database.session.begin()
270 db = database.Build()
274 db.principal = principal
275 database.session.save_or_update(db)
281 db.failed_stage = 'validating job'
282 src = validateBuild(pocket, package, commit)
284 db.version = str(b.getVersion(package, commit))
286 # If validateBuild returns something other than True, then
287 # it means we should copy from that pocket to our pocket.
289 # (If the validation failed, validateBuild would have
290 # raised an exception)
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
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)
303 # We should be more clever about dealing with
304 # things like non-Debian-native packages than we
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'],
315 db.failed_stage = 'building binary packages'
316 sbuildAll(package, commit, workdir)
318 logdir = os.path.join(b._LOG_DIR, db.build_id)
319 if not os.path.exists(logdir):
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)
333 db.failed_stage = 'cleaning up'
335 # Finally, now that everything is done, remove the
337 os.unlink(os.path.join(b._QUEUE_DIR, build))
339 db.traceback = traceback.format_exc()
342 db.failed_stage = None
344 database.session.save_or_update(db)
345 database.session.commit()
350 class 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.
355 When an IN_CREATE event comes in, trigger the builder.
361 """Initialize the inotifications and start the main loop."""
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'])
370 # Before inotifying, run any pending builds; otherwise we won't
371 # get notified for them.
375 notifier.process_events()
376 if notifier.check_events():
377 notifier.read_events()
380 if __name__ == '__main__':