Merge ~twom/launchpad-buildd:add-charm-build into launchpad-buildd:master
- Git
- lp:~twom/launchpad-buildd
- add-charm-build
- Merge into master
Proposed by
Tom Wardill
Status: | Merged |
---|---|
Approved by: | Tom Wardill |
Approved revision: | 5ab0ec8cab110953120da8e090273dbe1a37c783 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~twom/launchpad-buildd:add-charm-build |
Merge into: | launchpad-buildd:master |
Diff against target: |
1098 lines (+849/-46) 15 files modified
debian/changelog (+6/-0) lpbuildd/buildd-slave.tac (+2/-0) lpbuildd/charm.py (+94/-0) lpbuildd/oci.py (+1/-1) lpbuildd/target/backend.py (+13/-0) lpbuildd/target/build_charm.py (+128/-0) lpbuildd/target/build_oci.py (+4/-15) lpbuildd/target/build_snap.py (+1/-28) lpbuildd/target/cli.py (+2/-0) lpbuildd/target/operation.py (+2/-0) lpbuildd/target/tests/test_build_charm.py (+427/-0) lpbuildd/target/tests/test_build_oci.py (+1/-1) lpbuildd/target/vcs.py (+28/-0) lpbuildd/tests/test_charm.py (+139/-0) lpbuildd/tests/test_oci.py (+1/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+403811@code.launchpad.net |
Commit message
Add charm building
Description of the change
Add building for charms using charmcraft with local network support.
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
Tom Wardill (twom) : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/debian/changelog b/debian/changelog |
2 | index 26c21ed..6543629 100644 |
3 | --- a/debian/changelog |
4 | +++ b/debian/changelog |
5 | @@ -1,3 +1,9 @@ |
6 | +launchpad-buildd (197) UNRELEASED; urgency=medium |
7 | + |
8 | + * Add charm building support |
9 | + |
10 | + -- Tom Wardill <tom.wardill@canonical.com> Mon, 07 Jun 2021 10:22:50 +0100 |
11 | + |
12 | launchpad-buildd (196) bionic; urgency=medium |
13 | |
14 | * Handle symlinks in OCI image files |
15 | diff --git a/lpbuildd/buildd-slave.tac b/lpbuildd/buildd-slave.tac |
16 | index 3299b0d..7c1947e 100644 |
17 | --- a/lpbuildd/buildd-slave.tac |
18 | +++ b/lpbuildd/buildd-slave.tac |
19 | @@ -23,6 +23,7 @@ from twisted.web import ( |
20 | |
21 | from lpbuildd.binarypackage import BinaryPackageBuildManager |
22 | from lpbuildd.builder import XMLRPCBuilder |
23 | +from lpbuildd.charm import CharmBuildManager |
24 | from lpbuildd.oci import OCIBuildManager |
25 | from lpbuildd.livefs import LiveFilesystemBuildManager |
26 | from lpbuildd.log import RotatableFileLogObserver |
27 | @@ -47,6 +48,7 @@ builder.registerManager( |
28 | builder.registerManager(LiveFilesystemBuildManager, "livefs") |
29 | builder.registerManager(SnapBuildManager, "snap") |
30 | builder.registerManager(OCIBuildManager, "oci") |
31 | +builder.registerManager(CharmBuildManager, "charm") |
32 | |
33 | application = service.Application('Builder') |
34 | application.addComponent( |
35 | diff --git a/lpbuildd/charm.py b/lpbuildd/charm.py |
36 | new file mode 100644 |
37 | index 0000000..e5c5fe9 |
38 | --- /dev/null |
39 | +++ b/lpbuildd/charm.py |
40 | @@ -0,0 +1,94 @@ |
41 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
42 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
43 | + |
44 | +from __future__ import print_function |
45 | + |
46 | +__metaclass__ = type |
47 | + |
48 | +import os |
49 | + |
50 | +from lpbuildd.debian import ( |
51 | + DebianBuildState, |
52 | + DebianBuildManager, |
53 | + ) |
54 | + |
55 | + |
56 | +RETCODE_SUCCESS = 0 |
57 | +RETCODE_FAILURE_INSTALL = 200 |
58 | +RETCODE_FAILURE_BUILD = 201 |
59 | + |
60 | + |
61 | +class CharmBuildState(DebianBuildState): |
62 | + BUILD_CHARM = "BUILD_CHARM" |
63 | + |
64 | + |
65 | +class CharmBuildManager(DebianBuildManager): |
66 | + """Build a charm.""" |
67 | + |
68 | + backend_name = "lxd" |
69 | + initial_build_state = CharmBuildState.BUILD_CHARM |
70 | + |
71 | + @property |
72 | + def needs_sanitized_logs(self): |
73 | + return True |
74 | + |
75 | + def initiate(self, files, chroot, extra_args): |
76 | + """Initiate a build with a given set of files and chroot.""" |
77 | + self.name = extra_args["name"] |
78 | + self.branch = extra_args.get("branch") |
79 | + self.git_repository = extra_args.get("git_repository") |
80 | + self.git_path = extra_args.get("git_path") |
81 | + self.build_path = extra_args.get("build_path") |
82 | + self.channels = extra_args.get("channels", {}) |
83 | + |
84 | + super(CharmBuildManager, self).initiate(files, chroot, extra_args) |
85 | + |
86 | + def doRunBuild(self): |
87 | + """Run the process to build the charm.""" |
88 | + args = [] |
89 | + for snap, channel in sorted(self.channels.items()): |
90 | + args.extend(["--channel", "%s=%s" % (snap, channel)]) |
91 | + if self.branch is not None: |
92 | + args.extend(["--branch", self.branch]) |
93 | + if self.git_repository is not None: |
94 | + args.extend(["--git-repository", self.git_repository]) |
95 | + if self.git_path is not None: |
96 | + args.extend(["--git-path", self.git_path]) |
97 | + if self.build_path is not None: |
98 | + args.extend(["--build-path", self.build_path]) |
99 | + args.append(self.name) |
100 | + self.runTargetSubProcess("build-charm", *args) |
101 | + |
102 | + def iterate_BUILD_CHARM(self, retcode): |
103 | + """Finished building the charm.""" |
104 | + if retcode == RETCODE_SUCCESS: |
105 | + print("Returning build status: OK") |
106 | + return self.deferGatherResults() |
107 | + elif (retcode >= RETCODE_FAILURE_INSTALL and |
108 | + retcode <= RETCODE_FAILURE_BUILD): |
109 | + if not self.alreadyfailed: |
110 | + self._builder.buildFail() |
111 | + print("Returning build status: Build failed.") |
112 | + self.alreadyfailed = True |
113 | + else: |
114 | + if not self.alreadyfailed: |
115 | + self._builder.builderFail() |
116 | + print("Returning build status: Builder failed.") |
117 | + self.alreadyfailed = True |
118 | + self.doReapProcesses(self._state) |
119 | + |
120 | + def iterateReap_BUILD_CHARM(self, retcode): |
121 | + """Finished reaping after building the charm.""" |
122 | + self._state = DebianBuildState.UMOUNT |
123 | + self.doUnmounting() |
124 | + |
125 | + def gatherResults(self): |
126 | + """Gather the results of the build and add them to the file cache.""" |
127 | + output_path = os.path.join("/home/buildd", self.name) |
128 | + if self.backend.path_exists(output_path): |
129 | + for entry in sorted(self.backend.listdir(output_path)): |
130 | + path = os.path.join(output_path, entry) |
131 | + if self.backend.islink(path): |
132 | + continue |
133 | + if entry.endswith(".charm") or entry.endswith(".manifest"): |
134 | + self.addWaitingFileFromBackend(path) |
135 | diff --git a/lpbuildd/oci.py b/lpbuildd/oci.py |
136 | index 7a17939..53198c8 100644 |
137 | --- a/lpbuildd/oci.py |
138 | +++ b/lpbuildd/oci.py |
139 | @@ -59,7 +59,7 @@ class OCIBuildManager(SnapBuildProxyMixin, DebianBuildManager): |
140 | super(OCIBuildManager, self).initiate(files, chroot, extra_args) |
141 | |
142 | def doRunBuild(self): |
143 | - """Run the process to build the snap.""" |
144 | + """Run the process to build the OCI image.""" |
145 | args = [] |
146 | args.extend(self.startProxy()) |
147 | if self.revocation_endpoint: |
148 | diff --git a/lpbuildd/target/backend.py b/lpbuildd/target/backend.py |
149 | index 8356542..a7e778e 100644 |
150 | --- a/lpbuildd/target/backend.py |
151 | +++ b/lpbuildd/target/backend.py |
152 | @@ -13,6 +13,19 @@ class BackendException(Exception): |
153 | pass |
154 | |
155 | |
156 | +class InvalidBuildFilePath(Exception): |
157 | + pass |
158 | + |
159 | + |
160 | +def check_path_escape(buildd_path, path_to_check): |
161 | + """Check the build file path doesn't escape the build directory.""" |
162 | + build_file_path = os.path.realpath( |
163 | + os.path.join(buildd_path, path_to_check)) |
164 | + common_path = os.path.commonprefix((build_file_path, buildd_path)) |
165 | + if common_path != buildd_path: |
166 | + raise InvalidBuildFilePath("Invalid build file path.") |
167 | + |
168 | + |
169 | class Backend: |
170 | """A backend implementation for the environment where we run builds.""" |
171 | |
172 | diff --git a/lpbuildd/target/build_charm.py b/lpbuildd/target/build_charm.py |
173 | new file mode 100644 |
174 | index 0000000..6bd29cc |
175 | --- /dev/null |
176 | +++ b/lpbuildd/target/build_charm.py |
177 | @@ -0,0 +1,128 @@ |
178 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
179 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
180 | + |
181 | +from __future__ import print_function |
182 | +import functools |
183 | + |
184 | +__metaclass__ = type |
185 | + |
186 | +from collections import OrderedDict |
187 | +import logging |
188 | +import os |
189 | +import sys |
190 | + |
191 | +from lpbuildd.target.backend import check_path_escape |
192 | +from lpbuildd.target.build_snap import SnapChannelsAction |
193 | +from lpbuildd.target.operation import Operation |
194 | +from lpbuildd.target.snapstore import SnapStoreOperationMixin |
195 | +from lpbuildd.target.vcs import VCSOperationMixin |
196 | + |
197 | + |
198 | +RETCODE_FAILURE_INSTALL = 200 |
199 | +RETCODE_FAILURE_BUILD = 201 |
200 | + |
201 | + |
202 | +logger = logging.getLogger(__name__) |
203 | + |
204 | + |
205 | +class BuildCharm(VCSOperationMixin, SnapStoreOperationMixin, Operation): |
206 | + |
207 | + description = "Build a charm." |
208 | + |
209 | + core_snap_names = ["core", "core16", "core18", "core20"] |
210 | + |
211 | + @classmethod |
212 | + def add_arguments(cls, parser): |
213 | + super(BuildCharm, cls).add_arguments(parser) |
214 | + parser.add_argument( |
215 | + "--channel", action=SnapChannelsAction, metavar="SNAP=CHANNEL", |
216 | + dest="channels", default={}, help=( |
217 | + "install SNAP from CHANNEL " |
218 | + "(supported snaps: {}, charmcraft)".format( |
219 | + ", ".join(cls.core_snap_names)))) |
220 | + parser.add_argument( |
221 | + "--build-path", default=".", |
222 | + help="location of charm to build.") |
223 | + parser.add_argument("name", help="name of charm to build") |
224 | + |
225 | + def __init__(self, args, parser): |
226 | + super(BuildCharm, self).__init__(args, parser) |
227 | + self.bin = os.path.dirname(sys.argv[0]) |
228 | + self.buildd_path = os.path.join("/home/buildd", self.args.name) |
229 | + |
230 | + def run_build_command(self, args, env=None, build_path=None, **kwargs): |
231 | + """Run a build command in the target. |
232 | + |
233 | + :param args: the command and arguments to run. |
234 | + :param env: dictionary of additional environment variables to set. |
235 | + :param kwargs: any other keyword arguments to pass to Backend.run. |
236 | + """ |
237 | + full_env = OrderedDict() |
238 | + full_env["LANG"] = "C.UTF-8" |
239 | + full_env["SHELL"] = "/bin/sh" |
240 | + if env: |
241 | + full_env.update(env) |
242 | + cwd = kwargs.pop('cwd', self.buildd_path) |
243 | + return self.backend.run( |
244 | + args, cwd=cwd, env=full_env, **kwargs) |
245 | + |
246 | + def install(self): |
247 | + logger.info("Running install phase") |
248 | + deps = [] |
249 | + if self.args.backend == "lxd": |
250 | + # udev is installed explicitly to work around |
251 | + # https://bugs.launchpad.net/snapd/+bug/1731519. |
252 | + for dep in "snapd", "fuse", "squashfuse", "udev": |
253 | + if self.backend.is_package_available(dep): |
254 | + deps.append(dep) |
255 | + deps.extend(self.vcs_deps) |
256 | + self.backend.run(["apt-get", "-y", "install"] + deps) |
257 | + if self.args.backend in ("lxd", "fake"): |
258 | + self.snap_store_set_proxy() |
259 | + for snap_name in self.core_snap_names: |
260 | + if snap_name in self.args.channels: |
261 | + self.backend.run( |
262 | + ["snap", "install", |
263 | + "--channel=%s" % self.args.channels[snap_name], |
264 | + snap_name]) |
265 | + if "charmcraft" in self.args.channels: |
266 | + self.backend.run( |
267 | + ["snap", "install", |
268 | + "--channel=%s" % self.args.channels["charmcraft"], |
269 | + "charmcraft"]) |
270 | + else: |
271 | + self.backend.run(["snap", "install", "charmcraft"]) |
272 | + # The charmcraft snap can't see /build, so we have to do our work under |
273 | + # /home/buildd instead. Make sure it exists. |
274 | + self.backend.run(["mkdir", "-p", "/home/buildd"]) |
275 | + |
276 | + def repo(self): |
277 | + """Collect git or bzr branch.""" |
278 | + logger.info("Running repo phase...") |
279 | + self.vcs_fetch(self.args.name, cwd="/home/buildd") |
280 | + self.save_status(self.buildd_path) |
281 | + |
282 | + def build(self): |
283 | + logger.info("Running build phase...") |
284 | + build_context_path = os.path.join( |
285 | + "/home/buildd", |
286 | + self.args.name, |
287 | + self.args.build_path) |
288 | + check_path_escape(self.buildd_path, build_context_path) |
289 | + args = ["charmcraft", "build", "-f", build_context_path] |
290 | + self.run_build_command(args) |
291 | + |
292 | + def run(self): |
293 | + try: |
294 | + self.install() |
295 | + except Exception: |
296 | + logger.exception('Install failed') |
297 | + return RETCODE_FAILURE_INSTALL |
298 | + try: |
299 | + self.repo() |
300 | + self.build() |
301 | + except Exception: |
302 | + logger.exception('Build failed') |
303 | + return RETCODE_FAILURE_BUILD |
304 | + return 0 |
305 | + |
306 | diff --git a/lpbuildd/target/build_oci.py b/lpbuildd/target/build_oci.py |
307 | index af56671..5615325 100644 |
308 | --- a/lpbuildd/target/build_oci.py |
309 | +++ b/lpbuildd/target/build_oci.py |
310 | @@ -12,6 +12,7 @@ import sys |
311 | import tempfile |
312 | from textwrap import dedent |
313 | |
314 | +from lpbuildd.target.backend import check_path_escape |
315 | from lpbuildd.target.operation import Operation |
316 | from lpbuildd.target.snapbuildproxy import SnapBuildProxyOperationMixin |
317 | from lpbuildd.target.snapstore import SnapStoreOperationMixin |
318 | @@ -25,10 +26,6 @@ RETCODE_FAILURE_BUILD = 201 |
319 | logger = logging.getLogger(__name__) |
320 | |
321 | |
322 | -class InvalidBuildFilePath(Exception): |
323 | - pass |
324 | - |
325 | - |
326 | class BuildOCI(SnapBuildProxyOperationMixin, VCSOperationMixin, |
327 | SnapStoreOperationMixin, Operation): |
328 | |
329 | @@ -47,7 +44,7 @@ class BuildOCI(SnapBuildProxyOperationMixin, VCSOperationMixin, |
330 | help="A docker build ARG in the format of key=value. " |
331 | "This option can be repeated many times. For example: " |
332 | "--build-arg VAR1=A --build-arg VAR2=B") |
333 | - parser.add_argument("name", help="name of snap to build") |
334 | + parser.add_argument("name", help="name of image to build") |
335 | |
336 | def __init__(self, args, parser): |
337 | super(BuildOCI, self).__init__(args, parser) |
338 | @@ -72,14 +69,6 @@ class BuildOCI(SnapBuildProxyOperationMixin, VCSOperationMixin, |
339 | systemd_file.flush() |
340 | self.backend.copy_in(systemd_file.name, file_path) |
341 | |
342 | - def _check_path_escape(self, path_to_check): |
343 | - """Check the build file path doesn't escape the build directory.""" |
344 | - build_file_path = os.path.realpath( |
345 | - os.path.join(self.buildd_path, path_to_check)) |
346 | - common_path = os.path.commonprefix((build_file_path, self.buildd_path)) |
347 | - if common_path != self.buildd_path: |
348 | - raise InvalidBuildFilePath("Invalid build file path.") |
349 | - |
350 | def run_build_command(self, args, env=None, **kwargs): |
351 | """Run a build command in the target. |
352 | |
353 | @@ -130,7 +119,7 @@ class BuildOCI(SnapBuildProxyOperationMixin, VCSOperationMixin, |
354 | if self.args.build_file is not None: |
355 | build_file_path = os.path.join( |
356 | self.args.build_path, self.args.build_file) |
357 | - self._check_path_escape(build_file_path) |
358 | + check_path_escape(self.buildd_path, build_file_path) |
359 | args.extend(["--file", build_file_path]) |
360 | |
361 | # Keep this at the end, so we give the user a chance to override any |
362 | @@ -140,7 +129,7 @@ class BuildOCI(SnapBuildProxyOperationMixin, VCSOperationMixin, |
363 | |
364 | build_context_path = os.path.join( |
365 | self.buildd_path, self.args.build_path) |
366 | - self._check_path_escape(build_context_path) |
367 | + check_path_escape(self.buildd_path, build_context_path) |
368 | args.append(build_context_path) |
369 | self.run_build_command(args) |
370 | |
371 | diff --git a/lpbuildd/target/build_snap.py b/lpbuildd/target/build_snap.py |
372 | index 50fab7a..8b08cfa 100644 |
373 | --- a/lpbuildd/target/build_snap.py |
374 | +++ b/lpbuildd/target/build_snap.py |
375 | @@ -99,17 +99,6 @@ class BuildSnap(SnapBuildProxyOperationMixin, VCSOperationMixin, |
376 | full_env.update(env) |
377 | return self.backend.run(args, env=full_env, **kwargs) |
378 | |
379 | - def save_status(self, status): |
380 | - """Save a dictionary of status information about this build. |
381 | - |
382 | - This will be picked up by the build manager and included in XML-RPC |
383 | - status responses. |
384 | - """ |
385 | - status_path = os.path.join(self.backend.build_path, "status") |
386 | - with open("%s.tmp" % status_path, "w") as status_file: |
387 | - json.dump(status, status_file) |
388 | - os.rename("%s.tmp" % status_path, status_path) |
389 | - |
390 | def install_svn_servers(self): |
391 | proxy = urlparse(self.args.proxy_url) |
392 | svn_servers = dedent("""\ |
393 | @@ -173,23 +162,7 @@ class BuildSnap(SnapBuildProxyOperationMixin, VCSOperationMixin, |
394 | logger.info("Running repo phase...") |
395 | env = self.build_proxy_environment(proxy_url=self.args.proxy_url) |
396 | self.vcs_fetch(self.args.name, cwd="/build", env=env) |
397 | - status = {} |
398 | - if self.args.branch is not None: |
399 | - status["revision_id"] = self.run_build_command( |
400 | - ["bzr", "revno"], |
401 | - cwd=os.path.join("/build", self.args.name), |
402 | - get_output=True, universal_newlines=True).rstrip("\n") |
403 | - else: |
404 | - rev = ( |
405 | - self.args.git_path |
406 | - if self.args.git_path is not None else "HEAD") |
407 | - status["revision_id"] = self.run_build_command( |
408 | - # The ^{} suffix copes with tags: we want to peel them |
409 | - # recursively until we get an actual commit. |
410 | - ["git", "rev-parse", rev + "^{}"], |
411 | - cwd=os.path.join("/build", self.args.name), |
412 | - get_output=True, universal_newlines=True).rstrip("\n") |
413 | - self.save_status(status) |
414 | + self.save_status(os.path.join("/build", self.args.name)) |
415 | |
416 | @property |
417 | def image_info(self): |
418 | diff --git a/lpbuildd/target/cli.py b/lpbuildd/target/cli.py |
419 | index 94b291b..85e5b26 100644 |
420 | --- a/lpbuildd/target/cli.py |
421 | +++ b/lpbuildd/target/cli.py |
422 | @@ -14,6 +14,7 @@ from lpbuildd.target.apt import ( |
423 | OverrideSourcesList, |
424 | Update, |
425 | ) |
426 | +from lpbuildd.target.build_charm import BuildCharm |
427 | from lpbuildd.target.build_oci import BuildOCI |
428 | from lpbuildd.target.build_livefs import BuildLiveFS |
429 | from lpbuildd.target.build_snap import BuildSnap |
430 | @@ -51,6 +52,7 @@ def configure_logging(): |
431 | operations = { |
432 | "add-trusted-keys": AddTrustedKeys, |
433 | "build-oci": BuildOCI, |
434 | + "build-charm": BuildCharm, |
435 | "buildlivefs": BuildLiveFS, |
436 | "buildsnap": BuildSnap, |
437 | "generate-translation-templates": GenerateTranslationTemplates, |
438 | diff --git a/lpbuildd/target/operation.py b/lpbuildd/target/operation.py |
439 | index 2f9fe64..7020786 100644 |
440 | --- a/lpbuildd/target/operation.py |
441 | +++ b/lpbuildd/target/operation.py |
442 | @@ -5,6 +5,8 @@ from __future__ import print_function |
443 | |
444 | __metaclass__ = type |
445 | |
446 | +import os |
447 | + |
448 | from lpbuildd.target.backend import make_backend |
449 | |
450 | |
451 | diff --git a/lpbuildd/target/tests/test_build_charm.py b/lpbuildd/target/tests/test_build_charm.py |
452 | new file mode 100644 |
453 | index 0000000..2900879 |
454 | --- /dev/null |
455 | +++ b/lpbuildd/target/tests/test_build_charm.py |
456 | @@ -0,0 +1,427 @@ |
457 | +# Copyright 2019 Canonical Ltd. This software is licensed under the |
458 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
459 | + |
460 | +__metaclass__ = type |
461 | + |
462 | +import json |
463 | +import os |
464 | +import subprocess |
465 | +from textwrap import dedent |
466 | + |
467 | +from fixtures import ( |
468 | + FakeLogger, |
469 | + TempDir, |
470 | + ) |
471 | +import responses |
472 | +from testtools.matchers import ( |
473 | + AnyMatch, |
474 | + Equals, |
475 | + Is, |
476 | + MatchesAll, |
477 | + MatchesDict, |
478 | + MatchesListwise, |
479 | + ) |
480 | +from testtools.testcase import TestCase |
481 | + |
482 | +from lpbuildd.target.backend import InvalidBuildFilePath |
483 | +from lpbuildd.target.build_charm import ( |
484 | + RETCODE_FAILURE_BUILD, |
485 | + RETCODE_FAILURE_INSTALL, |
486 | + ) |
487 | +from lpbuildd.tests.fakebuilder import FakeMethod |
488 | +from lpbuildd.target.tests.test_build_snap import ( |
489 | + FakeRevisionID, |
490 | + RanSnap, |
491 | + ) |
492 | +from lpbuildd.target.cli import parse_args |
493 | + |
494 | + |
495 | +class RanCommand(MatchesListwise): |
496 | + |
497 | + def __init__(self, args, echo=None, cwd=None, input_text=None, |
498 | + get_output=None, universal_newlines=None, **env): |
499 | + kwargs_matcher = {} |
500 | + if echo is not None: |
501 | + kwargs_matcher["echo"] = Is(echo) |
502 | + if cwd: |
503 | + kwargs_matcher["cwd"] = Equals(cwd) |
504 | + if input_text: |
505 | + kwargs_matcher["input_text"] = Equals(input_text) |
506 | + if get_output is not None: |
507 | + kwargs_matcher["get_output"] = Is(get_output) |
508 | + if universal_newlines is not None: |
509 | + kwargs_matcher["universal_newlines"] = Is(universal_newlines) |
510 | + if env: |
511 | + kwargs_matcher["env"] = MatchesDict( |
512 | + {key: Equals(value) for key, value in env.items()}) |
513 | + super(RanCommand, self).__init__( |
514 | + [Equals((args,)), MatchesDict(kwargs_matcher)]) |
515 | + |
516 | + |
517 | +class RanAptGet(RanCommand): |
518 | + |
519 | + def __init__(self, *args): |
520 | + super(RanAptGet, self).__init__(["apt-get", "-y"] + list(args)) |
521 | + |
522 | + |
523 | +class RanBuildCommand(RanCommand): |
524 | + |
525 | + def __init__(self, args, **kwargs): |
526 | + kwargs.setdefault("LANG", "C.UTF-8") |
527 | + kwargs.setdefault("SHELL", "/bin/sh") |
528 | + super(RanBuildCommand, self).__init__(args, **kwargs) |
529 | + |
530 | + |
531 | +class TestBuildCharm(TestCase): |
532 | + |
533 | + def test_run_build_command_no_env(self): |
534 | + args = [ |
535 | + "build-charm", |
536 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
537 | + "--branch", "lp:foo", "test-image", |
538 | + ] |
539 | + build_charm = parse_args(args=args).operation |
540 | + build_charm.run_build_command(["echo", "hello world"]) |
541 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
542 | + RanBuildCommand( |
543 | + ["echo", "hello world"], |
544 | + cwd="/home/buildd/test-image"), |
545 | + ])) |
546 | + |
547 | + def test_run_build_command_env(self): |
548 | + args = [ |
549 | + "build-charm", |
550 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
551 | + "--branch", "lp:foo", "test-image", |
552 | + ] |
553 | + build_charm = parse_args(args=args).operation |
554 | + build_charm.run_build_command( |
555 | + ["echo", "hello world"], env={"FOO": "bar baz"}) |
556 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
557 | + RanBuildCommand( |
558 | + ["echo", "hello world"], |
559 | + FOO="bar baz", |
560 | + cwd="/home/buildd/test-image") |
561 | + ])) |
562 | + |
563 | + def test_install_channels(self): |
564 | + args = [ |
565 | + "build-charm", |
566 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
567 | + "--channel=core=candidate", "--channel=core18=beta", |
568 | + "--channel=charmcraft=edge", |
569 | + "--branch", "lp:foo", "test-snap", |
570 | + ] |
571 | + build_snap = parse_args(args=args).operation |
572 | + build_snap.install() |
573 | + self.assertThat(build_snap.backend.run.calls, MatchesListwise([ |
574 | + RanAptGet("install", "bzr"), |
575 | + RanSnap("install", "--channel=candidate", "core"), |
576 | + RanSnap("install", "--channel=beta", "core18"), |
577 | + RanSnap("install", "--channel=edge", "charmcraft"), |
578 | + RanCommand(["mkdir", "-p", "/home/buildd"]), |
579 | + ])) |
580 | + |
581 | + def test_install_bzr(self): |
582 | + args = [ |
583 | + "build-charm", |
584 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
585 | + "--branch", "lp:foo", "test-image" |
586 | + ] |
587 | + build_charm = parse_args(args=args).operation |
588 | + build_charm.install() |
589 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
590 | + RanAptGet("install", "bzr"), |
591 | + RanCommand(["snap", "install", "charmcraft"]), |
592 | + RanCommand(["mkdir", "-p", "/home/buildd"]), |
593 | + ])) |
594 | + |
595 | + def test_install_git(self): |
596 | + args = [ |
597 | + "build-charm", |
598 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
599 | + "--git-repository", "lp:foo", "test-image" |
600 | + ] |
601 | + build_charm = parse_args(args=args).operation |
602 | + build_charm.install() |
603 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
604 | + RanAptGet("install", "git"), |
605 | + RanCommand(["snap", "install", "charmcraft"]), |
606 | + RanCommand(["mkdir", "-p", "/home/buildd"]), |
607 | + ])) |
608 | + |
609 | + @responses.activate |
610 | + def test_install_snap_store_proxy(self): |
611 | + store_assertion = dedent("""\ |
612 | + type: store |
613 | + store: store-id |
614 | + url: http://snap-store-proxy.example |
615 | + |
616 | + body |
617 | + """) |
618 | + |
619 | + def respond(request): |
620 | + return 200, {"X-Assertion-Store-Id": "store-id"}, store_assertion |
621 | + |
622 | + responses.add_callback( |
623 | + "GET", "http://snap-store-proxy.example/v2/auth/store/assertions", |
624 | + callback=respond) |
625 | + args = [ |
626 | + "build-charm", |
627 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
628 | + "--git-repository", "lp:foo", |
629 | + "--snap-store-proxy-url", "http://snap-store-proxy.example/", |
630 | + "test-snap", |
631 | + ] |
632 | + build_snap = parse_args(args=args).operation |
633 | + build_snap.install() |
634 | + self.assertThat(build_snap.backend.run.calls, MatchesListwise([ |
635 | + RanAptGet("install", "git"), |
636 | + RanCommand( |
637 | + ["snap", "ack", "/dev/stdin"], input_text=store_assertion), |
638 | + RanCommand(["snap", "set", "core", "proxy.store=store-id"]), |
639 | + RanSnap("install", "charmcraft"), |
640 | + RanCommand(["mkdir", "-p", "/home/buildd"]), |
641 | + ])) |
642 | + |
643 | + def test_repo_bzr(self): |
644 | + args = [ |
645 | + "build-charm", |
646 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
647 | + "--branch", "lp:foo", "test-image", |
648 | + ] |
649 | + build_charm = parse_args(args=args).operation |
650 | + build_charm.backend.build_path = self.useFixture(TempDir()).path |
651 | + build_charm.backend.run = FakeRevisionID("42") |
652 | + build_charm.repo() |
653 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
654 | + RanBuildCommand( |
655 | + ["bzr", "branch", "lp:foo", "test-image"], cwd="/home/buildd"), |
656 | + RanBuildCommand( |
657 | + ["bzr", "revno"], |
658 | + cwd="/home/buildd/test-image", get_output=True, |
659 | + universal_newlines=True), |
660 | + ])) |
661 | + status_path = os.path.join(build_charm.backend.build_path, "status") |
662 | + with open(status_path) as status: |
663 | + self.assertEqual({"revision_id": "42"}, json.load(status)) |
664 | + |
665 | + def test_repo_git(self): |
666 | + args = [ |
667 | + "build-charm", |
668 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
669 | + "--git-repository", "lp:foo", "test-image", |
670 | + ] |
671 | + build_charm = parse_args(args=args).operation |
672 | + build_charm.backend.build_path = self.useFixture(TempDir()).path |
673 | + build_charm.backend.run = FakeRevisionID("0" * 40) |
674 | + build_charm.repo() |
675 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
676 | + RanBuildCommand( |
677 | + ["git", "clone", "lp:foo", "test-image"], cwd="/home/buildd"), |
678 | + RanBuildCommand( |
679 | + ["git", "submodule", "update", "--init", "--recursive"], |
680 | + cwd="/home/buildd/test-image"), |
681 | + RanBuildCommand( |
682 | + ["git", "rev-parse", "HEAD^{}"], |
683 | + cwd="/home/buildd/test-image", |
684 | + get_output=True, universal_newlines=True), |
685 | + ])) |
686 | + status_path = os.path.join(build_charm.backend.build_path, "status") |
687 | + with open(status_path) as status: |
688 | + self.assertEqual({"revision_id": "0" * 40}, json.load(status)) |
689 | + |
690 | + def test_repo_git_with_path(self): |
691 | + args = [ |
692 | + "build-charm", |
693 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
694 | + "--git-repository", "lp:foo", "--git-path", "next", "test-image", |
695 | + ] |
696 | + build_charm = parse_args(args=args).operation |
697 | + build_charm.backend.build_path = self.useFixture(TempDir()).path |
698 | + build_charm.backend.run = FakeRevisionID("0" * 40) |
699 | + build_charm.repo() |
700 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
701 | + RanBuildCommand( |
702 | + ["git", "clone", "-b", "next", "lp:foo", "test-image"], |
703 | + cwd="/home/buildd"), |
704 | + RanBuildCommand( |
705 | + ["git", "submodule", "update", "--init", "--recursive"], |
706 | + cwd="/home/buildd/test-image"), |
707 | + RanBuildCommand( |
708 | + ["git", "rev-parse", "next^{}"], |
709 | + cwd="/home/buildd/test-image", get_output=True, |
710 | + universal_newlines=True), |
711 | + ])) |
712 | + status_path = os.path.join(build_charm.backend.build_path, "status") |
713 | + with open(status_path) as status: |
714 | + self.assertEqual({"revision_id": "0" * 40}, json.load(status)) |
715 | + |
716 | + def test_repo_git_with_tag_path(self): |
717 | + args = [ |
718 | + "build-charm", |
719 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
720 | + "--git-repository", "lp:foo", "--git-path", "refs/tags/1.0", |
721 | + "test-image", |
722 | + ] |
723 | + build_charm = parse_args(args=args).operation |
724 | + build_charm.backend.build_path = self.useFixture(TempDir()).path |
725 | + build_charm.backend.run = FakeRevisionID("0" * 40) |
726 | + build_charm.repo() |
727 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
728 | + RanBuildCommand( |
729 | + ["git", "clone", "-b", "1.0", "lp:foo", "test-image"], |
730 | + cwd="/home/buildd"), |
731 | + RanBuildCommand( |
732 | + ["git", "submodule", "update", "--init", "--recursive"], |
733 | + cwd="/home/buildd/test-image"), |
734 | + RanBuildCommand( |
735 | + ["git", "rev-parse", "refs/tags/1.0^{}"], |
736 | + cwd="/home/buildd/test-image", get_output=True, |
737 | + universal_newlines=True), |
738 | + ])) |
739 | + status_path = os.path.join(build_charm.backend.build_path, "status") |
740 | + with open(status_path) as status: |
741 | + self.assertEqual({"revision_id": "0" * 40}, json.load(status)) |
742 | + |
743 | + def test_build(self): |
744 | + args = [ |
745 | + "build-charm", |
746 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
747 | + "--branch", "lp:foo", "test-image", |
748 | + ] |
749 | + build_charm = parse_args(args=args).operation |
750 | + build_charm.backend.add_dir('/build/test-directory') |
751 | + build_charm.build() |
752 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
753 | + RanBuildCommand( |
754 | + ["charmcraft", "build", "-f", |
755 | + "/home/buildd/test-image/."], |
756 | + cwd="/home/buildd/test-image"), |
757 | + ])) |
758 | + |
759 | + def test_build_with_path(self): |
760 | + args = [ |
761 | + "build-charm", |
762 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
763 | + "--branch", "lp:foo", "--build-path", "build-aux/", |
764 | + "test-image", |
765 | + ] |
766 | + build_charm = parse_args(args=args).operation |
767 | + build_charm.backend.add_dir('/build/test-directory') |
768 | + build_charm.build() |
769 | + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ |
770 | + RanBuildCommand( |
771 | + ["charmcraft", "build", "-f", |
772 | + "/home/buildd/test-image/build-aux/"], |
773 | + cwd="/home/buildd/test-image"), |
774 | + ])) |
775 | + |
776 | + def test_run_succeeds(self): |
777 | + args = [ |
778 | + "build-charm", |
779 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
780 | + "--branch", "lp:foo", "test-image", |
781 | + ] |
782 | + build_charm = parse_args(args=args).operation |
783 | + build_charm.backend.build_path = self.useFixture(TempDir()).path |
784 | + build_charm.backend.run = FakeRevisionID("42") |
785 | + self.assertEqual(0, build_charm.run()) |
786 | + self.assertThat(build_charm.backend.run.calls, MatchesAll( |
787 | + AnyMatch(RanAptGet("install", "bzr"),), |
788 | + AnyMatch(RanBuildCommand( |
789 | + ["bzr", "branch", "lp:foo", "test-image"], |
790 | + cwd="/home/buildd")), |
791 | + AnyMatch(RanBuildCommand( |
792 | + ["charmcraft", "build", "-f", |
793 | + "/home/buildd/test-image/."], |
794 | + cwd="/home/buildd/test-image")), |
795 | + )) |
796 | + |
797 | + def test_run_install_fails(self): |
798 | + class FailInstall(FakeMethod): |
799 | + def __call__(self, run_args, *args, **kwargs): |
800 | + super(FailInstall, self).__call__(run_args, *args, **kwargs) |
801 | + if run_args[0] == "apt-get": |
802 | + raise subprocess.CalledProcessError(1, run_args) |
803 | + |
804 | + self.useFixture(FakeLogger()) |
805 | + args = [ |
806 | + "build-charm", |
807 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
808 | + "--branch", "lp:foo", "test-image", |
809 | + ] |
810 | + build_charm = parse_args(args=args).operation |
811 | + build_charm.backend.run = FailInstall() |
812 | + self.assertEqual(RETCODE_FAILURE_INSTALL, build_charm.run()) |
813 | + |
814 | + def test_run_repo_fails(self): |
815 | + class FailRepo(FakeMethod): |
816 | + def __call__(self, run_args, *args, **kwargs): |
817 | + super(FailRepo, self).__call__(run_args, *args, **kwargs) |
818 | + if run_args[:2] == ["bzr", "branch"]: |
819 | + raise subprocess.CalledProcessError(1, run_args) |
820 | + |
821 | + self.useFixture(FakeLogger()) |
822 | + args = [ |
823 | + "build-charm", |
824 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
825 | + "--branch", "lp:foo", "test-image", |
826 | + ] |
827 | + build_charm = parse_args(args=args).operation |
828 | + build_charm.backend.run = FailRepo() |
829 | + self.assertEqual(RETCODE_FAILURE_BUILD, build_charm.run()) |
830 | + |
831 | + def test_run_build_fails(self): |
832 | + class FailBuild(FakeMethod): |
833 | + def __call__(self, run_args, *args, **kwargs): |
834 | + super(FailBuild, self).__call__(run_args, *args, **kwargs) |
835 | + if run_args[0] == "charmcraft": |
836 | + raise subprocess.CalledProcessError(1, run_args) |
837 | + |
838 | + self.useFixture(FakeLogger()) |
839 | + args = [ |
840 | + "build-charm", |
841 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
842 | + "--branch", "lp:foo", "test-image", |
843 | + ] |
844 | + build_charm = parse_args(args=args).operation |
845 | + build_charm.backend.build_path = self.useFixture(TempDir()).path |
846 | + build_charm.backend.run = FailBuild() |
847 | + self.assertEqual(RETCODE_FAILURE_BUILD, build_charm.run()) |
848 | + |
849 | + def test_build_with_invalid_build_path_parent(self): |
850 | + args = [ |
851 | + "build-charm", |
852 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
853 | + "--branch", "lp:foo", "--build-path", "../", |
854 | + "test-image", |
855 | + ] |
856 | + build_charm = parse_args(args=args).operation |
857 | + build_charm.backend.add_dir('/build/test-directory') |
858 | + self.assertRaises(InvalidBuildFilePath, build_charm.build) |
859 | + |
860 | + def test_build_with_invalid_build_path_absolute(self): |
861 | + args = [ |
862 | + "build-charm", |
863 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
864 | + "--branch", "lp:foo", "--build-path", "/etc", |
865 | + "test-image", |
866 | + ] |
867 | + build_charm = parse_args(args=args).operation |
868 | + build_charm.backend.add_dir('/build/test-directory') |
869 | + self.assertRaises(InvalidBuildFilePath, build_charm.build) |
870 | + |
871 | + def test_build_with_invalid_build_path_symlink(self): |
872 | + args = [ |
873 | + "build-charm", |
874 | + "--backend=fake", "--series=xenial", "--arch=amd64", "1", |
875 | + "--branch", "lp:foo", "--build-path", "build/", |
876 | + "test-image", |
877 | + ] |
878 | + build_charm = parse_args(args=args).operation |
879 | + build_charm.buildd_path = self.useFixture(TempDir()).path |
880 | + os.symlink( |
881 | + '/etc/hosts', |
882 | + os.path.join(build_charm.buildd_path, 'build')) |
883 | + self.assertRaises(InvalidBuildFilePath, build_charm.build) |
884 | diff --git a/lpbuildd/target/tests/test_build_oci.py b/lpbuildd/target/tests/test_build_oci.py |
885 | index e58344f..3b9966a 100644 |
886 | --- a/lpbuildd/target/tests/test_build_oci.py |
887 | +++ b/lpbuildd/target/tests/test_build_oci.py |
888 | @@ -24,8 +24,8 @@ from testtools.matchers import ( |
889 | MatchesListwise, |
890 | ) |
891 | |
892 | +from lpbuildd.target.backend import InvalidBuildFilePath |
893 | from lpbuildd.target.build_oci import ( |
894 | - InvalidBuildFilePath, |
895 | RETCODE_FAILURE_BUILD, |
896 | RETCODE_FAILURE_INSTALL, |
897 | ) |
898 | diff --git a/lpbuildd/target/vcs.py b/lpbuildd/target/vcs.py |
899 | index cb9875d..d7c80c3 100644 |
900 | --- a/lpbuildd/target/vcs.py |
901 | +++ b/lpbuildd/target/vcs.py |
902 | @@ -6,6 +6,7 @@ from __future__ import print_function |
903 | __metaclass__ = type |
904 | |
905 | from collections import OrderedDict |
906 | +import json |
907 | import logging |
908 | import os.path |
909 | import subprocess |
910 | @@ -101,3 +102,30 @@ class VCSOperationMixin: |
911 | logger.error( |
912 | "'git submodule update --init --recursive failed with " |
913 | "exit code %s (build may fail later)" % e.returncode) |
914 | + |
915 | + def save_status(self, cwd): |
916 | + """Save a dictionary of status information about this build. |
917 | + |
918 | + This will be picked up by the build manager and included in XML-RPC |
919 | + status responses. |
920 | + """ |
921 | + status = {} |
922 | + if self.args.branch is not None: |
923 | + status["revision_id"] = self.run_build_command( |
924 | + ["bzr", "revno"], |
925 | + cwd=cwd, |
926 | + get_output=True, universal_newlines=True).rstrip("\n") |
927 | + else: |
928 | + rev = ( |
929 | + self.args.git_path |
930 | + if self.args.git_path is not None else "HEAD") |
931 | + status["revision_id"] = self.run_build_command( |
932 | + # The ^{} suffix copes with tags: we want to peel them |
933 | + # recursively until we get an actual commit. |
934 | + ["git", "rev-parse", rev + "^{}"], |
935 | + cwd=cwd, |
936 | + get_output=True, universal_newlines=True).rstrip("\n") |
937 | + status_path = os.path.join(self.backend.build_path, "status") |
938 | + with open("%s.tmp" % status_path, "w") as status_file: |
939 | + json.dump(status, status_file) |
940 | + os.rename("%s.tmp" % status_path, status_path) |
941 | diff --git a/lpbuildd/tests/test_charm.py b/lpbuildd/tests/test_charm.py |
942 | new file mode 100644 |
943 | index 0000000..407f732 |
944 | --- /dev/null |
945 | +++ b/lpbuildd/tests/test_charm.py |
946 | @@ -0,0 +1,139 @@ |
947 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
948 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
949 | + |
950 | +__metaclass__ = type |
951 | + |
952 | +import os |
953 | + |
954 | +from fixtures import ( |
955 | + EnvironmentVariable, |
956 | + TempDir, |
957 | + ) |
958 | +from testtools import TestCase |
959 | +from testtools.deferredruntest import AsynchronousDeferredRunTest |
960 | +from twisted.internet import defer |
961 | + |
962 | +from lpbuildd.charm import CharmBuildManager, CharmBuildState |
963 | +from lpbuildd.tests.fakebuilder import FakeBuilder |
964 | +from lpbuildd.tests.matchers import HasWaitingFiles |
965 | + |
966 | + |
967 | +class MockBuildManager(CharmBuildManager): |
968 | + def __init__(self, *args, **kwargs): |
969 | + super(MockBuildManager, self).__init__(*args, **kwargs) |
970 | + self.commands = [] |
971 | + self.iterators = [] |
972 | + |
973 | + def runSubProcess(self, path, command, iterate=None, env=None): |
974 | + self.commands.append([path] + command) |
975 | + if iterate is None: |
976 | + iterate = self.iterate |
977 | + self.iterators.append(iterate) |
978 | + return 0 |
979 | + |
980 | + |
981 | +class TestCharmBuildManagerIteration(TestCase): |
982 | + """Run CharmBuildManager through its iteration steps.""" |
983 | + |
984 | + run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5) |
985 | + |
986 | + def setUp(self): |
987 | + super(TestCharmBuildManagerIteration, self).setUp() |
988 | + self.working_dir = self.useFixture(TempDir()).path |
989 | + builder_dir = os.path.join(self.working_dir, "builder") |
990 | + home_dir = os.path.join(self.working_dir, "home") |
991 | + for dir in (builder_dir, home_dir): |
992 | + os.mkdir(dir) |
993 | + self.useFixture(EnvironmentVariable("HOME", home_dir)) |
994 | + self.builder = FakeBuilder(builder_dir) |
995 | + self.buildid = "123" |
996 | + self.buildmanager = MockBuildManager(self.builder, self.buildid) |
997 | + self.buildmanager._cachepath = self.builder._cachepath |
998 | + |
999 | + def getState(self): |
1000 | + """Retrieve build manager's state.""" |
1001 | + return self.buildmanager._state |
1002 | + |
1003 | + @defer.inlineCallbacks |
1004 | + def startBuild(self, args=None, options=None): |
1005 | + # The build manager's iterate() kicks off the consecutive states |
1006 | + # after INIT. |
1007 | + extra_args = { |
1008 | + "series": "xenial", |
1009 | + "arch_tag": "i386", |
1010 | + "name": "test-charm", |
1011 | + } |
1012 | + if args is not None: |
1013 | + extra_args.update(args) |
1014 | + original_backend_name = self.buildmanager.backend_name |
1015 | + self.buildmanager.backend_name = "fake" |
1016 | + self.buildmanager.initiate({}, "chroot.tar.gz", extra_args) |
1017 | + self.buildmanager.backend_name = original_backend_name |
1018 | + |
1019 | + # Skip states that are done in DebianBuildManager to the state |
1020 | + # directly before BUILD_CHARM. |
1021 | + self.buildmanager._state = CharmBuildState.UPDATE |
1022 | + |
1023 | + # BUILD_CHARM: Run the builder's payload to build the charm. |
1024 | + yield self.buildmanager.iterate(0) |
1025 | + self.assertEqual(CharmBuildState.BUILD_CHARM, self.getState()) |
1026 | + expected_command = [ |
1027 | + "sharepath/bin/in-target", "in-target", "build-charm", |
1028 | + "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid, |
1029 | + ] |
1030 | + if options is not None: |
1031 | + expected_command.extend(options) |
1032 | + expected_command.append("test-charm") |
1033 | + self.assertEqual(expected_command, self.buildmanager.commands[-1]) |
1034 | + self.assertEqual( |
1035 | + self.buildmanager.iterate, self.buildmanager.iterators[-1]) |
1036 | + self.assertFalse(self.builder.wasCalled("chrootFail")) |
1037 | + |
1038 | + @defer.inlineCallbacks |
1039 | + def test_iterate(self): |
1040 | + # The build manager iterates a normal build from start to finish. |
1041 | + args = { |
1042 | + "git_repository": "https://git.launchpad.dev/~example/+git/charm", |
1043 | + "git_path": "master", |
1044 | + } |
1045 | + expected_options = [ |
1046 | + "--git-repository", "https://git.launchpad.dev/~example/+git/charm", |
1047 | + "--git-path", "master", |
1048 | + ] |
1049 | + yield self.startBuild(args, expected_options) |
1050 | + |
1051 | + log_path = os.path.join(self.buildmanager._cachepath, "buildlog") |
1052 | + with open(log_path, "w") as log: |
1053 | + log.write("I am a build log.") |
1054 | + |
1055 | + self.buildmanager.backend.add_file( |
1056 | + "/home/buildd/test-charm/test-charm_0_all.charm", |
1057 | + b"I am charming.") |
1058 | + |
1059 | + # After building the package, reap processes. |
1060 | + yield self.buildmanager.iterate(0) |
1061 | + expected_command = [ |
1062 | + "sharepath/bin/in-target", "in-target", "scan-for-processes", |
1063 | + "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid, |
1064 | + ] |
1065 | + |
1066 | + self.assertEqual(CharmBuildState.BUILD_CHARM, self.getState()) |
1067 | + self.assertEqual(expected_command, self.buildmanager.commands[-1]) |
1068 | + self.assertNotEqual( |
1069 | + self.buildmanager.iterate, self.buildmanager.iterators[-1]) |
1070 | + self.assertFalse(self.builder.wasCalled("buildFail")) |
1071 | + self.assertThat(self.builder, HasWaitingFiles.byEquality({ |
1072 | + "test-charm_0_all.charm": b"I am charming.", |
1073 | + })) |
1074 | + |
1075 | + # Control returns to the DebianBuildManager in the UMOUNT state. |
1076 | + self.buildmanager.iterateReap(self.getState(), 0) |
1077 | + expected_command = [ |
1078 | + "sharepath/bin/in-target", "in-target", "umount-chroot", |
1079 | + "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid, |
1080 | + ] |
1081 | + self.assertEqual(CharmBuildState.UMOUNT, self.getState()) |
1082 | + self.assertEqual(expected_command, self.buildmanager.commands[-1]) |
1083 | + self.assertEqual( |
1084 | + self.buildmanager.iterate, self.buildmanager.iterators[-1]) |
1085 | + self.assertFalse(self.builder.wasCalled("buildFail")) |
1086 | diff --git a/lpbuildd/tests/test_oci.py b/lpbuildd/tests/test_oci.py |
1087 | index 78b0dbb..7fc0b3a 100644 |
1088 | --- a/lpbuildd/tests/test_oci.py |
1089 | +++ b/lpbuildd/tests/test_oci.py |
1090 | @@ -89,7 +89,7 @@ class TestOCIBuildManagerIteration(TestCase): |
1091 | # directly before BUILD_OCI. |
1092 | self.buildmanager._state = OCIBuildState.UPDATE |
1093 | |
1094 | - # BUILD_OCI: Run the builder's payload to build the snap package. |
1095 | + # BUILD_OCI: Run the builder's payload to build the OCI image. |
1096 | yield self.buildmanager.iterate(0) |
1097 | self.assertEqual(OCIBuildState.BUILD_OCI, self.getState()) |
1098 | expected_command = [ |