~canonical-launchpad-branches/launchpad-buildd/trunk

« back to all changes in this revision

Viewing changes to buildrecipe

  • Committer: Martin Pool
  • Date: 2011-11-09 07:50:56 UTC
  • Revision ID: mbp@canonical.com-20111109075056-mj9ofht4vmu7lto1
Add snapshot of buildd source extracted from the Launchpad tree

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/env python -u
 
2
# Copyright 2010, 2011 Canonical Ltd.  This software is licensed under the
 
3
# GNU Affero General Public License version 3 (see the file LICENSE).
 
4
 
 
5
"""A script that builds a package from a recipe and a chroot."""
 
6
 
 
7
__metaclass__ = type
 
8
 
 
9
 
 
10
import os
 
11
import os.path
 
12
import pwd
 
13
from resource import RLIMIT_AS, setrlimit
 
14
import socket
 
15
from subprocess import (
 
16
    Popen,
 
17
    call,
 
18
    )
 
19
import sys
 
20
 
 
21
 
 
22
RETCODE_SUCCESS = 0
 
23
RETCODE_FAILURE_INSTALL = 200
 
24
RETCODE_FAILURE_BUILD_TREE = 201
 
25
RETCODE_FAILURE_INSTALL_BUILD_DEPS = 202
 
26
RETCODE_FAILURE_BUILD_SOURCE_PACKAGE = 203
 
27
 
 
28
 
 
29
class NotVirtualized(Exception):
 
30
    """Exception raised when not running in a virtualized environment."""
 
31
 
 
32
    def __init__(self):
 
33
        Exception.__init__(self, 'Not running under Xen.')
 
34
 
 
35
 
 
36
def call_report_rusage(args):
 
37
    """Run a subprocess.
 
38
 
 
39
    Report that it was run, and the resources used, and complain if it fails.
 
40
 
 
41
    :return: The process wait status.
 
42
    """
 
43
    print 'RUN %r' % args
 
44
    proc = Popen(args)
 
45
    pid, status, rusage = os.wait4(proc.pid, 0)
 
46
    print(rusage)
 
47
    return status
 
48
 
 
49
 
 
50
class RecipeBuilder:
 
51
    """Builds a package from a recipe."""
 
52
 
 
53
    def __init__(self, build_id, author_name, author_email,
 
54
                 suite, distroseries_name, component, archive_purpose):
 
55
        """Constructor.
 
56
 
 
57
        :param build_id: The id of the build (a str).
 
58
        :param author_name: The name of the author (a str).
 
59
        :param author_email: The email address of the author (a str).
 
60
        :param suite: The suite the package should be built for (a str).
 
61
        """
 
62
        self.build_id = build_id
 
63
        self.author_name = author_name.decode('utf-8')
 
64
        self.author_email = author_email
 
65
        self.archive_purpose = archive_purpose
 
66
        self.component = component
 
67
        self.distroseries_name = distroseries_name
 
68
        self.suite = suite
 
69
        self.base_branch = None
 
70
        self.chroot_path = get_build_path(build_id, 'chroot-autobuild')
 
71
        self.work_dir_relative = os.environ['HOME'] + '/work'
 
72
        self.work_dir = os.path.join(self.chroot_path,
 
73
                                     self.work_dir_relative[1:])
 
74
        self.tree_path = os.path.join(self.work_dir, 'tree')
 
75
        self.username = pwd.getpwuid(os.getuid())[0]
 
76
 
 
77
    def install(self):
 
78
        """Install all the requirements for building recipes.
 
79
 
 
80
        :return: A retcode from apt.
 
81
        """
 
82
        # XXX: AaronBentley 2010-07-07 bug=602463: pbuilder uses aptitude but
 
83
        # does not depend on it.
 
84
        return self.chroot([
 
85
            'apt-get', 'install', '-y', 'pbuilder', 'aptitude'])
 
86
 
 
87
    def buildTree(self):
 
88
        """Build the recipe into a source tree.
 
89
 
 
90
        As a side-effect, sets self.source_dir_relative.
 
91
        :return: a retcode from `bzr dailydeb`.
 
92
        """
 
93
        try:
 
94
            ensure_virtualized()
 
95
        except NotVirtualized, e:
 
96
            sys.stderr.write('Aborting on failed virtualization check:\n')
 
97
            sys.stderr.write(str(e))
 
98
            return 1
 
99
        assert not os.path.exists(self.tree_path)
 
100
        recipe_path = os.path.join(self.work_dir, 'recipe')
 
101
        manifest_path = os.path.join(self.tree_path, 'manifest')
 
102
        recipe_file = open(recipe_path, 'rb')
 
103
        try:
 
104
            recipe = recipe_file.read()
 
105
        finally:
 
106
            recipe_file.close()
 
107
        # As of bzr 2.2, a defined identity is needed.  In this case, we're
 
108
        # using buildd@<hostname>.
 
109
        hostname = socket.gethostname()
 
110
        bzr_email = 'buildd@%s' % hostname
 
111
 
 
112
        print 'Bazaar versions:'
 
113
        check_call(['bzr', 'version'])
 
114
        check_call(['bzr', 'plugins'])
 
115
 
 
116
        print 'Building recipe:'
 
117
        print recipe
 
118
        sys.stdout.flush()
 
119
        env = {
 
120
            'DEBEMAIL': self.author_email,
 
121
            'DEBFULLNAME': self.author_name.encode('utf-8'),
 
122
            'BZR_EMAIL': bzr_email}
 
123
        retcode = call_report_rusage([
 
124
            'bzr', 'dailydeb', '--safe', '--no-build', recipe_path,
 
125
            self.tree_path, '--manifest', manifest_path,
 
126
            '--allow-fallback-to-native', '--append-version',
 
127
            '~%s1' % self.distroseries_name], env=env)
 
128
        if retcode != 0:
 
129
            return retcode
 
130
        (source,) = [name for name in os.listdir(self.tree_path)
 
131
                     if name != 'manifest']
 
132
        self.source_dir_relative = os.path.join(
 
133
            self.work_dir_relative, 'tree', source)
 
134
        return retcode
 
135
 
 
136
    def getPackageName(self):
 
137
        source_dir = os.path.join(
 
138
            self.chroot_path, self.source_dir_relative.lstrip('/'))
 
139
        changelog = os.path.join(source_dir, 'debian/changelog')
 
140
        return open(changelog, 'r').readline().split(' ')[0]
 
141
 
 
142
    def installBuildDeps(self):
 
143
        """Install the build-depends of the source tree."""
 
144
        package = self.getPackageName()
 
145
        currently_building_path = os.path.join(
 
146
            self.chroot_path, 'CurrentlyBuilding')
 
147
        currently_building_contents = (
 
148
            'Package: %s\n'
 
149
            'Suite: %s\n'
 
150
            'Component: %s\n'
 
151
            'Purpose: %s\n'
 
152
            'Build-Debug-Symbols: no\n' %
 
153
            (package, self.suite, self.component, self.archive_purpose))
 
154
        currently_building = open(currently_building_path, 'w')
 
155
        currently_building.write(currently_building_contents)
 
156
        currently_building.close()
 
157
        return self.chroot(['sh', '-c', 'cd %s &&'
 
158
                     '/usr/lib/pbuilder/pbuilder-satisfydepends'
 
159
                     % self.source_dir_relative])
 
160
 
 
161
    def chroot(self, args, echo=False):
 
162
        """Run a command in the chroot.
 
163
 
 
164
        :param args: the command and arguments to run.
 
165
        :return: the status code.
 
166
        """
 
167
        if echo:
 
168
            print "Running in chroot: %s" % ' '.join(
 
169
                "'%s'" % arg for arg in args)
 
170
            sys.stdout.flush()
 
171
        return call([
 
172
            '/usr/bin/sudo', '/usr/sbin/chroot', self.chroot_path] + args)
 
173
 
 
174
    def buildSourcePackage(self):
 
175
        """Build the source package.
 
176
 
 
177
        :return: a retcode from dpkg-buildpackage.
 
178
        """
 
179
        retcode = self.chroot([
 
180
            'su', '-c', 'cd %s && /usr/bin/dpkg-buildpackage -i -I -us -uc -S'
 
181
            % self.source_dir_relative, self.username])
 
182
        for filename in os.listdir(self.tree_path):
 
183
            path = os.path.join(self.tree_path, filename)
 
184
            if os.path.isfile(path):
 
185
                os.rename(path, get_build_path(self.build_id, filename))
 
186
        return retcode
 
187
 
 
188
 
 
189
def get_build_path(build_id, *extra):
 
190
    """Generate a path within the build directory.
 
191
 
 
192
    :param build_id: the build id to use.
 
193
    :param extra: the extra path segments within the build directory.
 
194
    :return: the generated path.
 
195
    """
 
196
    return os.path.join(
 
197
        os.environ["HOME"], "build-" + build_id, *extra)
 
198
 
 
199
 
 
200
def ensure_virtualized():
 
201
    """Raise an exception if not running in a virtualized environment.
 
202
 
 
203
    Raises if not running under Xen.
 
204
    """
 
205
    if not os.path.isdir('/proc/xen') or os.path.exists('/proc/xen/xsd_kva'):
 
206
        raise NotVirtualized()
 
207
 
 
208
 
 
209
if __name__ == '__main__':
 
210
    setrlimit(RLIMIT_AS, (1000000000, -1))
 
211
    builder = RecipeBuilder(*sys.argv[1:])
 
212
    if builder.buildTree() != 0:
 
213
        sys.exit(RETCODE_FAILURE_BUILD_TREE)
 
214
    if builder.install() != 0:
 
215
        sys.exit(RETCODE_FAILURE_INSTALL)
 
216
    if builder.installBuildDeps() != 0:
 
217
        sys.exit(RETCODE_FAILURE_INSTALL_BUILD_DEPS)
 
218
    if builder.buildSourcePackage() != 0:
 
219
        sys.exit(RETCODE_FAILURE_BUILD_SOURCE_PACKAGE)
 
220
    sys.exit(RETCODE_SUCCESS)