Merge lp:~cjwatson/launchpad-buildd/avoid-pbuilder into lp:launchpad-buildd

Proposed by Colin Watson on 2015-06-15
Status: Merged
Merged at revision: 152
Proposed branch: lp:~cjwatson/launchpad-buildd/avoid-pbuilder
Merge into: lp:launchpad-buildd
Diff against target: 338 lines (+257/-8)
5 files modified
Makefile (+1/-0)
buildrecipe (+105/-7)
debian/changelog (+8/-0)
debian/control (+1/-1)
lpbuildd/tests/test_buildrecipe.py (+142/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad-buildd/avoid-pbuilder
Reviewer Review Type Date Requested Status
William Grant code 2015-06-15 Approve on 2015-06-24
Review via email: mp+261989@code.launchpad.net

Commit Message

Reimplement build-dependency installation for recipes by hand using sbuild-like logic, allowing us to drop use of pbuilder (LP: #728494) and support :native in recipe build-dependencies (LP: #1322294).

Description of the Change

Reimplement build-dependency installation for recipes by hand using sbuild-like logic, allowing us to drop use of pbuilder and support :native in recipe build-dependencies.

I've had most of this lying around for a while, and finally managed to polish this up and get it working today. Full testing is fiddly, but I got it working against a local Launchpad instance with a slightly older version of lp:unity-scopes-api that still had a :native build-dependency.

To post a comment you must log in.
153. By Colin Watson on 2015-06-17

Avoiding pbuilder fixes LP: #728494 as well.

William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2013-11-04 11:22:02 +0000
3+++ Makefile 2015-06-17 12:39:57 +0000
4@@ -29,6 +29,7 @@
5 PYTHONPATH=$(PWD):$(PYTHONPATH) $(PYTHON) -m testtools.run -v \
6 lpbuildd.tests.test_binarypackage \
7 lpbuildd.tests.test_buildd_slave \
8+ lpbuildd.tests.test_buildrecipe \
9 lpbuildd.tests.test_check_implicit_pointer_functions \
10 lpbuildd.tests.test_harness \
11 lpbuildd.tests.test_livefs \
12
13=== modified file 'buildrecipe'
14--- buildrecipe 2014-08-06 07:07:26 +0000
15+++ buildrecipe 2015-06-17 12:39:57 +0000
16@@ -17,6 +17,9 @@
17 check_call,
18 )
19 import sys
20+from textwrap import dedent
21+
22+from debian.deb822 import Deb822
23
24
25 RETCODE_SUCCESS = 0
26@@ -66,17 +69,18 @@
27 self.work_dir_relative[1:])
28
29 self.tree_path = os.path.join(self.work_dir, 'tree')
30+ self.apt_dir_relative = os.path.join(self.work_dir_relative, 'apt')
31+ self.apt_dir = os.path.join(self.work_dir, 'apt')
32 self.username = pwd.getpwuid(os.getuid())[0]
33+ self.apt_sources_list_dir = os.path.join(
34+ self.chroot_path, "etc/apt/sources.list.d")
35
36 def install(self):
37 """Install all the requirements for building recipes.
38
39 :return: A retcode from apt.
40 """
41- # XXX: AaronBentley 2010-07-07 bug=602463: pbuilder uses aptitude but
42- # does not depend on it.
43- return self.chroot([
44- 'apt-get', 'install', '-y', 'pbuilder', 'aptitude', 'lsb-release'])
45+ return self.chroot(['apt-get', 'install', '-y', 'lsb-release'])
46
47 def buildTree(self):
48 """Build the recipe into a source tree.
49@@ -137,6 +141,101 @@
50 changelog = os.path.join(source_dir, 'debian/changelog')
51 return open(changelog, 'r').readline().split(' ')[0]
52
53+ def getSourceControl(self):
54+ """Return the parsed source control stanza from the source tree."""
55+ source_dir = os.path.join(
56+ self.chroot_path, self.source_dir_relative.lstrip('/'))
57+ with open(os.path.join(source_dir, 'debian/control')) as control_file:
58+ # Don't let Deb822.iter_paragraphs use apt_pkg.TagFile
59+ # internally, since that only handles real tag files and not the
60+ # slightly more permissive syntax of debian/control which also
61+ # allows comments.
62+ return Deb822.iter_paragraphs(
63+ control_file, use_apt_pkg=False).next()
64+
65+ def makeDummyDsc(self, package):
66+ control = self.getSourceControl()
67+ with open(os.path.join(
68+ self.apt_dir, "%s.dsc" % package), "w") as dummy_dsc:
69+ print >>dummy_dsc, dedent("""\
70+ Format: 1.0
71+ Source: %(package)s
72+ Architecture: any
73+ Version: 99:0
74+ Maintainer: invalid@example.org""") % {"package": package}
75+ for field in (
76+ "Build-Depends", "Build-Depends-Indep",
77+ "Build-Conflicts", "Build-Conflicts-Indep",
78+ ):
79+ if field in control:
80+ print >>dummy_dsc, "%s: %s" % (field, control[field])
81+ print >>dummy_dsc
82+
83+ def runAptFtparchive(self):
84+ conf_path = os.path.join(self.apt_dir, "ftparchive.conf")
85+ with open(conf_path, "w") as conf:
86+ print >>conf, dedent("""\
87+ Dir::ArchiveDir "%(apt_dir)s";
88+ Default::Sources::Compress ". bzip2";
89+ BinDirectory "%(apt_dir)s" { Sources "Sources"; };
90+ APT::FTPArchive::Release {
91+ Origin "buildrecipe-archive";
92+ Label "buildrecipe-archive";
93+ Suite "invalid";
94+ Codename "invalid";
95+ Description "buildrecipe temporary archive";
96+ };""") % {"apt_dir": self.apt_dir}
97+ ftparchive_env = dict(os.environ)
98+ ftparchive_env.pop("APT_CONFIG", None)
99+ ret = call(
100+ ["apt-ftparchive", "-q=2", "generate", conf_path],
101+ env=ftparchive_env)
102+ if ret != 0:
103+ return ret
104+
105+ with open(os.path.join(self.apt_dir, "Release"), "w") as release:
106+ return call(
107+ ["apt-ftparchive", "-q=2", "-c", conf_path, "release",
108+ self.apt_dir],
109+ stdout=release, env=ftparchive_env)
110+
111+ def enableAptArchive(self):
112+ """Enable the dummy apt archive.
113+
114+ We run "apt-get update" with a temporary sources.list and some
115+ careful use of APT::Get::List-Cleanup=false, so that we don't have
116+ to update all sources (and potentially need to mimic the care taken
117+ by update-debian-chroot, etc.).
118+ """
119+ tmp_list_path = os.path.join(self.apt_dir, "buildrecipe-archive.list")
120+ tmp_list_path_relative = os.path.join(
121+ self.apt_dir_relative, "buildrecipe-archive.list")
122+ with open(tmp_list_path, "w") as tmp_list:
123+ print >>tmp_list, "deb-src file://%s ./" % self.apt_dir_relative
124+ ret = self.chroot([
125+ 'apt-get',
126+ '-o', 'Dir::Etc::sourcelist=%s' % tmp_list_path_relative,
127+ '-o', 'APT::Get::List-Cleanup=false',
128+ 'update',
129+ ])
130+ if ret == 0:
131+ list_path = os.path.join(
132+ self.apt_sources_list_dir, "buildrecipe-archive.list")
133+ return call(['sudo', 'mv', tmp_list_path, list_path])
134+ return ret
135+
136+ def setUpAptArchive(self, package):
137+ """Generate a dummy apt archive with appropriate build-dependencies.
138+
139+ Based on Sbuild::ResolverBase.
140+ """
141+ os.makedirs(self.apt_dir)
142+ self.makeDummyDsc(package)
143+ ret = self.runAptFtparchive()
144+ if ret != 0:
145+ return ret
146+ return self.enableAptArchive()
147+
148 def installBuildDeps(self):
149 """Install the build-depends of the source tree."""
150 package = self.getPackageName()
151@@ -152,9 +251,8 @@
152 currently_building = open(currently_building_path, 'w')
153 currently_building.write(currently_building_contents)
154 currently_building.close()
155- return self.chroot(['sh', '-c', 'cd %s &&'
156- '/usr/lib/pbuilder/pbuilder-satisfydepends'
157- % self.source_dir_relative])
158+ self.setUpAptArchive(package)
159+ return self.chroot(['apt-get', 'build-dep', '-y', package])
160
161 def chroot(self, args, echo=False):
162 """Run a command in the chroot.
163
164=== modified file 'debian/changelog'
165--- debian/changelog 2015-06-04 10:43:23 +0000
166+++ debian/changelog 2015-06-17 12:39:57 +0000
167@@ -1,3 +1,11 @@
168+launchpad-buildd (130) UNRELEASED; urgency=medium
169+
170+ * Reimplement build-dependency installation for recipes by hand using
171+ sbuild-like logic, allowing us to drop use of pbuilder (LP: #728494) and
172+ support :native in recipe build-dependencies (LP: #1322294).
173+
174+ -- Colin Watson <cjwatson@ubuntu.com> Mon, 15 Jun 2015 12:27:56 +0100
175+
176 launchpad-buildd (129) trusty; urgency=low
177
178 [ William Grant ]
179
180=== modified file 'debian/control'
181--- debian/control 2015-05-13 01:11:43 +0000
182+++ debian/control 2015-06-17 12:39:57 +0000
183@@ -22,7 +22,7 @@
184 Package: python-lpbuildd
185 Section: python
186 Architecture: all
187-Depends: python, python-twisted-core, python-twisted-web, python-zope.interface, python-apt, ${misc:Depends}
188+Depends: python, python-twisted-core, python-twisted-web, python-zope.interface, python-apt, python-debian, apt-utils, ${misc:Depends}
189 Breaks: launchpad-buildd (<< 88)
190 Replaces: launchpad-buildd (<< 88)
191 Description: Python libraries for a Launchpad buildd slave
192
193=== added file 'lpbuildd/tests/test_buildrecipe.py'
194--- lpbuildd/tests/test_buildrecipe.py 1970-01-01 00:00:00 +0000
195+++ lpbuildd/tests/test_buildrecipe.py 2015-06-17 12:39:57 +0000
196@@ -0,0 +1,142 @@
197+# Copyright 2014 Canonical Ltd. This software is licensed under the
198+# GNU Affero General Public License version 3 (see the file LICENSE).
199+
200+__metaclass__ = type
201+
202+from contextlib import contextmanager
203+import imp
204+import os
205+import shutil
206+import sys
207+import tempfile
208+from textwrap import dedent
209+
210+from testtools import TestCase
211+
212+
213+@contextmanager
214+def disable_bytecode():
215+ original = sys.dont_write_bytecode
216+ sys.dont_write_bytecode = True
217+ yield
218+ sys.dont_write_bytecode = original
219+
220+
221+# By-hand import to avoid having to put .py suffixes on slave binaries.
222+with disable_bytecode():
223+ RecipeBuilder = imp.load_source("buildrecipe", "buildrecipe").RecipeBuilder
224+
225+
226+class TestRecipeBuilder(TestCase):
227+ def setUp(self):
228+ super(TestRecipeBuilder, self).setUp()
229+ self.save_env = dict(os.environ)
230+ self.home_dir = tempfile.mkdtemp()
231+ self.addCleanup(lambda: shutil.rmtree(self.home_dir))
232+ os.environ["HOME"] = self.home_dir
233+ self.build_id = "1"
234+ self.builder = RecipeBuilder(
235+ self.build_id, "Recipe Builder", "builder@example.org>", "grumpy",
236+ "grumpy", "main", "PPA")
237+ os.makedirs(self.builder.work_dir)
238+
239+ def resetEnvironment(self):
240+ for key in set(os.environ.keys()) - set(self.save_env.keys()):
241+ del os.environ[key]
242+ for key, value in os.environ.items():
243+ if value != self.save_env[key]:
244+ os.environ[key] = self.save_env[key]
245+ for key in set(self.save_env.keys()) - set(os.environ.keys()):
246+ os.environ[key] = self.save_env[key]
247+
248+ def tearDown(self):
249+ self.resetEnvironment()
250+ super(TestRecipeBuilder, self).tearDown()
251+
252+ def test_makeDummyDsc(self):
253+ self.builder.source_dir_relative = os.path.join(
254+ self.builder.work_dir_relative, "tree", "foo")
255+ control_path = os.path.join(
256+ self.builder.work_dir, "tree", "foo", "debian", "control")
257+ os.makedirs(os.path.dirname(control_path))
258+ os.makedirs(self.builder.apt_dir)
259+ with open(control_path, "w") as control:
260+ print >>control, dedent("""\
261+ Source: foo
262+ Build-Depends: debhelper (>= 9~), libfoo-dev
263+
264+ Package: foo
265+ Depends: ${shlibs:Depends}""")
266+ self.builder.makeDummyDsc("foo")
267+ with open(os.path.join(self.builder.apt_dir, "foo.dsc")) as dsc:
268+ self.assertEqual(
269+ dedent("""\
270+ Format: 1.0
271+ Source: foo
272+ Architecture: any
273+ Version: 99:0
274+ Maintainer: invalid@example.org
275+ Build-Depends: debhelper (>= 9~), libfoo-dev
276+
277+ """),
278+ dsc.read())
279+
280+ def test_makeDummyDsc_comments(self):
281+ # apt_pkg.TagFile doesn't support comments, but python-debian's own
282+ # parser does. Make sure we're using the right one.
283+ self.builder.source_dir_relative = os.path.join(
284+ self.builder.work_dir_relative, "tree", "foo")
285+ control_path = os.path.join(
286+ self.builder.work_dir, "tree", "foo", "debian", "control")
287+ os.makedirs(os.path.dirname(control_path))
288+ os.makedirs(self.builder.apt_dir)
289+ with open(control_path, "w") as control:
290+ print >>control, dedent("""\
291+ Source: foo
292+ Build-Depends: debhelper (>= 9~),
293+ libfoo-dev,
294+ # comment line
295+ pkg-config
296+
297+ Package: foo
298+ Depends: ${shlibs:Depends}""")
299+ self.builder.makeDummyDsc("foo")
300+ with open(os.path.join(self.builder.apt_dir, "foo.dsc")) as dsc:
301+ self.assertEqual(
302+ dedent("""\
303+ Format: 1.0
304+ Source: foo
305+ Architecture: any
306+ Version: 99:0
307+ Maintainer: invalid@example.org
308+ Build-Depends: debhelper (>= 9~),
309+ libfoo-dev,
310+ pkg-config
311+
312+ """),
313+ dsc.read())
314+
315+ def test_runAptFtparchive(self):
316+ os.makedirs(self.builder.apt_dir)
317+ with open(os.path.join(self.builder.apt_dir, "foo.dsc"), "w") as dsc:
318+ print >>dsc, dedent("""\
319+ Format: 1.0
320+ Source: foo
321+ Architecture: any
322+ Version: 99:0
323+ Maintainer: invalid@example.org
324+ Build-Depends: debhelper (>= 9~), libfoo-dev""")
325+ self.assertEqual(0, self.builder.runAptFtparchive())
326+ self.assertEqual(
327+ ["Release", "Sources", "Sources.bz2", "foo.dsc",
328+ "ftparchive.conf"],
329+ sorted(os.listdir(self.builder.apt_dir)))
330+ with open(os.path.join(self.builder.apt_dir, "Sources")) as sources:
331+ sources_text = sources.read()
332+ self.assertIn("Package: foo\n", sources_text)
333+ self.assertIn(
334+ "Build-Depends: debhelper (>= 9~), libfoo-dev\n", sources_text)
335+
336+ # XXX cjwatson 2015-06-15: We should unit-test enableAptArchive and
337+ # installBuildDeps too, but it involves a lot of mocks. For now,
338+ # integration testing is probably more useful.

Subscribers

People subscribed via source and target branches

to all changes: