1# This program is free software; you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Portions Copyright Buildbot Team Members
15# Portions Copyright Marius Rieder <marius.rieder@durchmesser.ch>
16"""
17Steps and objects related to pbuilder
18"""
19
20
21import re
22import stat
23import time
24
25from twisted.internet import defer
26from twisted.python import log
27
28from buildbot import config
29from buildbot.process import logobserver
30from buildbot.process import remotecommand
31from buildbot.process import results
32from buildbot.steps.shell import WarningCountingShellCommand
33
34
35class DebPbuilder(WarningCountingShellCommand):
36
37    """Build a debian package with pbuilder inside of a chroot."""
38    name = "pbuilder"
39
40    haltOnFailure = 1
41    flunkOnFailure = 1
42    description = ["building"]
43    descriptionDone = ["built"]
44
45    warningPattern = r".*(warning[: ]|\sW: ).*"
46
47    architecture = None
48    distribution = 'stable'
49    basetgz = None
50    _default_basetgz = "/var/cache/pbuilder/{distribution}-{architecture}-buildbot.tgz"
51    mirror = "http://cdn.debian.net/debian/"
52    othermirror = ""
53    extrapackages = []
54    keyring = None
55    components = None
56
57    maxAge = 60 * 60 * 24 * 7
58    pbuilder = '/usr/sbin/pbuilder'
59    baseOption = '--basetgz'
60
61    renderables = ['architecture', 'distribution', 'basetgz', 'mirror', 'othermirror',
62            'extrapackages', 'keyring', 'components']
63
64    def __init__(self,
65                 architecture=None,
66                 distribution=None,
67                 basetgz=None,
68                 mirror=None,
69                 othermirror=None,
70                 extrapackages=None,
71                 keyring=None,
72                 components=None,
73                 **kwargs):
74        super().__init__(**kwargs)
75
76        if architecture:
77            self.architecture = architecture
78        if distribution:
79            self.distribution = distribution
80        if mirror:
81            self.mirror = mirror
82        if othermirror:
83            self.othermirror = "|".join(othermirror)
84        if extrapackages:
85            self.extrapackages = extrapackages
86        if keyring:
87            self.keyring = keyring
88        if components:
89            self.components = components
90        if basetgz:
91            self.basetgz = basetgz
92
93        if not self.distribution:
94            config.error("You must specify a distribution.")
95
96        self.suppressions.append(
97            (None, re.compile(r"\.pbuilderrc does not exist"), None, None))
98
99        self.addLogObserver(
100            'stdio', logobserver.LineConsumerLogObserver(self.logConsumer))
101
102    @defer.inlineCallbacks
103    def run(self):
104        if self.basetgz is None:
105            self.basetgz = self._default_basetgz
106            kwargs = {}
107            if self.architecture:
108                kwargs['architecture'] = self.architecture
109            else:
110                kwargs['architecture'] = 'local'
111            kwargs['distribution'] = self.distribution
112            self.basetgz = self.basetgz.format(**kwargs)
113
114        self.command = ['pdebuild', '--buildresult', '.', '--pbuilder', self.pbuilder]
115        if self.architecture:
116            self.command += ['--architecture', self.architecture]
117        self.command += ['--', '--buildresult', '.', self.baseOption, self.basetgz]
118        if self.extrapackages:
119            self.command += ['--extrapackages', " ".join(self.extrapackages)]
120
121        res = yield self.checkBasetgz()
122        if res != results.SUCCESS:
123            return res
124
125        res = yield super().run()
126        return res
127
128    @defer.inlineCallbacks
129    def checkBasetgz(self):
130        cmd = remotecommand.RemoteCommand('stat', {'file': self.basetgz})
131        yield self.runCommand(cmd)
132
133        if cmd.rc != 0:
134            log.msg("basetgz not found, initializing it.")
135
136            command = ['sudo', self.pbuilder, '--create', self.baseOption,
137                       self.basetgz, '--distribution', self.distribution,
138                       '--mirror', self.mirror]
139            if self.othermirror:
140                command += ['--othermirror', self.othermirror]
141            if self.architecture:
142                command += ['--architecture', self.architecture]
143            if self.extrapackages:
144                command += ['--extrapackages', " ".join(self.extrapackages)]
145            if self.keyring:
146                command += ['--debootstrapopts', "--keyring={}".format(self.keyring)]
147            if self.components:
148                command += ['--components', self.components]
149
150            cmd = remotecommand.RemoteShellCommand(self.workdir, command)
151
152            stdio_log = yield self.addLog("pbuilder")
153            cmd.useLog(stdio_log, True, "stdio")
154
155            self.description = ["PBuilder", "create."]
156            yield self.updateSummary()
157
158            yield self.runCommand(cmd)
159            if cmd.rc != 0:
160                log.msg("Failure when running {}.".format(cmd))
161                return results.FAILURE
162            return results.SUCCESS
163
164        s = cmd.updates["stat"][-1]
165        # basetgz will be a file when running in pbuilder
166        # and a directory in case of cowbuilder
167        if stat.S_ISREG(s[stat.ST_MODE]) or stat.S_ISDIR(s[stat.ST_MODE]):
168            log.msg("{} found.".format(self.basetgz))
169            age = time.time() - s[stat.ST_MTIME]
170            if age >= self.maxAge:
171                log.msg("basetgz outdated, updating")
172                command = ['sudo', self.pbuilder, '--update',
173                           self.baseOption, self.basetgz]
174
175                cmd = remotecommand.RemoteShellCommand(self.workdir, command)
176                stdio_log = yield self.addLog("pbuilder")
177                cmd.useLog(stdio_log, True, "stdio")
178
179                yield self.runCommand(cmd)
180                if cmd.rc != 0:
181                    log.msg("Failure when running {}.".format(cmd))
182                    return results.FAILURE
183            return results.SUCCESS
184
185        log.msg("{} is not a file or a directory.".format(self.basetgz))
186        return results.FAILURE
187
188    def logConsumer(self):
189        r = re.compile(r"dpkg-genchanges  >\.\./(.+\.changes)")
190        while True:
191            stream, line = yield
192            mo = r.search(line)
193            if mo:
194                self.setProperty("deb-changes", mo.group(1), "DebPbuilder")
195
196
197class DebCowbuilder(DebPbuilder):
198
199    """Build a debian package with cowbuilder inside of a chroot."""
200    name = "cowbuilder"
201
202    _default_basetgz = "/var/cache/pbuilder/{distribution}-{architecture}-buildbot.cow/"
203
204    pbuilder = '/usr/sbin/cowbuilder'
205    baseOption = '--basepath'
206
207
208class UbuPbuilder(DebPbuilder):
209
210    """Build a Ubuntu package with pbuilder inside of a chroot."""
211    distribution = None
212    mirror = "http://archive.ubuntu.com/ubuntu/"
213
214    components = "main universe"
215
216
217class UbuCowbuilder(DebCowbuilder):
218
219    """Build a Ubuntu package with cowbuilder inside of a chroot."""
220    distribution = None
221    mirror = "http://archive.ubuntu.com/ubuntu/"
222
223    components = "main universe"
224