Merge lp:~larryprice/libertine/confinement-fixups into lp:libertine
- confinement-fixups
- Merge into devel
Proposed by
Larry Price
Status: | Merged |
---|---|
Approved by: | Christopher Townsend |
Approved revision: | 396 |
Merged at revision: | 405 |
Proposed branch: | lp:~larryprice/libertine/confinement-fixups |
Merge into: | lp:libertine |
Diff against target: |
471 lines (+146/-95) 11 files modified
debian/python3-libertine.install (+1/-0) python/libertine/Client.py (+61/-0) python/libertine/LxcContainer.py (+14/-27) python/libertine/LxdContainer.py (+13/-30) python/libertine/service/manager.py (+3/-3) python/libertine/utils.py (+17/-13) setup/gui/libertine-manager-app.desktop (+0/-12) snap/gui/libertine-manager-app.desktop (+12/-0) snap/snap-runner.wrapper (+1/-1) snapcraft.yaml (+20/-8) tools/libertined (+4/-1) |
To merge this branch: | bzr merge lp:~larryprice/libertine/confinement-fixups |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher Townsend | Approve | ||
Libertine CI Bot | continuous-integration | Approve | |
Review via email: mp+317391@code.launchpad.net |
Commit message
Various fixes for getting the confined snap working with libertined.
Description of the change
Various fixes for getting the confined snap working with libertined. Will require manually starting d-bus service and manually connecting the interfaces. If the interface is unavailable, this should fall back to not using libertined.
To post a comment you must log in.
- 396. By Larry Price
-
Client, not LibertineClient
Revision history for this message
Libertine CI Bot (libertine-ci-bot) wrote : | # |
review:
Approve
(continuous-integration)
Revision history for this message
Christopher Townsend (townsend) wrote : | # |
LGTM, +1 and thanks!
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'debian/python3-libertine.install' |
2 | --- debian/python3-libertine.install 2017-02-07 12:35:48 +0000 |
3 | +++ debian/python3-libertine.install 2017-02-15 21:25:37 +0000 |
4 | @@ -1,3 +1,4 @@ |
5 | +usr/lib/python*/*/libertine/Client.py |
6 | usr/lib/python*/*/libertine/ContainersConfig.py |
7 | usr/lib/python*/*/libertine/HostInfo.py |
8 | usr/lib/python*/*/libertine/Libertine.py |
9 | |
10 | === added file 'python/libertine/Client.py' |
11 | --- python/libertine/Client.py 1970-01-01 00:00:00 +0000 |
12 | +++ python/libertine/Client.py 2017-02-15 21:25:37 +0000 |
13 | @@ -0,0 +1,61 @@ |
14 | +# Copyright 2017 Canonical Ltd. |
15 | +# |
16 | +# This program is free software: you can redistribute it and/or modify |
17 | +# it under the terms of the GNU General Public License as published by |
18 | +# the Free Software Foundation; version 3 of the License. |
19 | +# |
20 | +# This program is distributed in the hope that it will be useful, |
21 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
22 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
23 | +# GNU General Public License for more details. |
24 | +# |
25 | +# You should have received a copy of the GNU General Public License |
26 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
27 | + |
28 | + |
29 | +import dbus |
30 | + |
31 | +from . import utils |
32 | + |
33 | + |
34 | +class Client(object): |
35 | + def __init__(self): |
36 | + self._manager = None |
37 | + |
38 | + try: |
39 | + from .service.manager import LIBERTINE_MANAGER_NAME, LIBERTINE_MANAGER_OBJECT |
40 | + if utils.set_session_dbus_env_var(): |
41 | + bus = dbus.SessionBus() |
42 | + self._manager = bus.get_object(LIBERTINE_MANAGER_NAME, LIBERTINE_MANAGER_OBJECT) |
43 | + self._interface = LIBERTINE_MANAGER_NAME |
44 | + except ImportError as e: |
45 | + utils.get_logger().warning("Libertine service libraries not installed.") |
46 | + except dbus.exceptions.DBusException as e: |
47 | + utils.get_logger().warning("Exception raised while discovering d-bus service: {}".format(str(e))) |
48 | + |
49 | + @property |
50 | + def valid(self): |
51 | + return self._manager is not None |
52 | + |
53 | + def container_operation_start(self, id): |
54 | + if not self.valid: |
55 | + return False |
56 | + |
57 | + retries = 0 |
58 | + while not self._manager.get_dbus_method('container_operation_start', self._interface)(id): |
59 | + retries += 1 |
60 | + if retries > 5: |
61 | + return False |
62 | + time.sleep(.5) |
63 | + |
64 | + return True |
65 | + |
66 | + def container_operation_finished(self, id): |
67 | + return self.valid and self._manager.get_dbus_method("container_operation_finished", self._interface)(id) |
68 | + |
69 | + def container_stopped(self, id): |
70 | + if not self.valid: |
71 | + return False |
72 | + |
73 | + self._manager.get_dbus_method('container_stopped', self._interface)(id) |
74 | + return True |
75 | |
76 | === modified file 'python/libertine/LxcContainer.py' |
77 | --- python/libertine/LxcContainer.py 2017-02-08 16:44:02 +0000 |
78 | +++ python/libertine/LxcContainer.py 2017-02-15 21:25:37 +0000 |
79 | @@ -25,8 +25,7 @@ |
80 | import time |
81 | |
82 | from .Libertine import BaseContainer |
83 | -from .service.manager import LIBERTINE_MANAGER_NAME, LIBERTINE_STORE_PATH |
84 | -from . import utils, HostInfo |
85 | +from . import utils, HostInfo, Client |
86 | |
87 | |
88 | home_path = os.environ['HOME'] |
89 | @@ -163,16 +162,10 @@ |
90 | def __init__(self, container_id, config): |
91 | super().__init__(container_id, 'lxc', config) |
92 | self.container = lxc_container(container_id) |
93 | - self.lxc_manager_interface = None |
94 | self.host_info = HostInfo.HostInfo() |
95 | self._freeze_on_stop = config.get_freeze_on_stop(self.container_id) |
96 | |
97 | - if utils.set_session_dbus_env_var(): |
98 | - try: |
99 | - bus = dbus.SessionBus() |
100 | - self.lxc_manager_interface = bus.get_object(LIBERTINE_MANAGER_NAME, LIBERTINE_STORE_PATH) |
101 | - except dbus.exceptions.DBusException: |
102 | - pass |
103 | + self._manager = Client.Client() |
104 | |
105 | def _setup_pulse(self): |
106 | pulse_socket_path = os.path.join(utils.get_libertine_runtime_dir(), 'pulse_socket') |
107 | @@ -222,14 +215,8 @@ |
108 | return fd.read().strip('\n') != self.host_info.get_host_timezone() |
109 | |
110 | def start_container(self): |
111 | - if self.lxc_manager_interface: |
112 | - retries = 0 |
113 | - while not self.lxc_manager_interface.container_operation_start(self.container_id): |
114 | - retries += 1 |
115 | - if retries > 5: |
116 | - return False |
117 | - time.sleep(.5) |
118 | - |
119 | + self._manager.container_operation_start(self.container_id) |
120 | + |
121 | if self.container.state == 'RUNNING': |
122 | return True |
123 | |
124 | @@ -244,14 +231,14 @@ |
125 | return True |
126 | |
127 | def stop_container(self): |
128 | - if self.lxc_manager_interface: |
129 | - if self.lxc_manager_interface.container_operation_finished(self.container_id): |
130 | - if not lxc_stop(self.container, self._freeze_on_stop): |
131 | - return False |
132 | - self.lxc_manager_interface.container_stopped(self.container_id) |
133 | - return True |
134 | - else: |
135 | + if self._manager.valid: |
136 | + if not self._manager.container_operation_finished(self.container_id): |
137 | return False |
138 | + |
139 | + if not lxc_stop(self.container, self._freeze_on_stop): |
140 | + return False |
141 | + |
142 | + return self._manager.container_stopped(self.container_id) |
143 | else: |
144 | return lxc_stop(self.container, self._freeze_on_stop) |
145 | |
146 | @@ -264,7 +251,7 @@ |
147 | self._freeze_on_stop = False |
148 | |
149 | # We never want to use the manager when restarting. |
150 | - self.lxc_manager_interface = None |
151 | + self._manager = None |
152 | |
153 | if not (self.stop_container() and self.start_container()): |
154 | return False |
155 | @@ -402,7 +389,7 @@ |
156 | self.container.save_config() |
157 | |
158 | def start_application(self, app_exec_line, environ): |
159 | - if self.lxc_manager_interface == None: |
160 | + if not self._manager.valid: |
161 | utils.get_logger().error("No interface to libertine-lxc-manager. Failing application launch.") |
162 | return |
163 | |
164 | @@ -410,7 +397,7 @@ |
165 | os.environ.update(environ) |
166 | |
167 | if not self.start_container(): |
168 | - self.lxc_manager_interface.container_stopped(self.container_id) |
169 | + self._manager.container_stopped(self.container_id) |
170 | return |
171 | |
172 | app_launch_cmd = "sudo -E -u " + os.environ['USER'] + " env PATH=" + os.environ['PATH'] |
173 | |
174 | === modified file 'python/libertine/LxdContainer.py' |
175 | --- python/libertine/LxdContainer.py 2017-02-07 16:32:02 +0000 |
176 | +++ python/libertine/LxdContainer.py 2017-02-15 21:25:37 +0000 |
177 | @@ -22,8 +22,7 @@ |
178 | import subprocess |
179 | import time |
180 | |
181 | -from libertine import Libertine, utils, HostInfo |
182 | -from .service.manager import LIBERTINE_MANAGER_NAME, LIBERTINE_STORE_PATH |
183 | +from . import Libertine, utils, HostInfo, Client |
184 | |
185 | |
186 | def _get_devices_map(): |
187 | @@ -295,22 +294,13 @@ |
188 | super().__init__(name, 'lxd', config) |
189 | self._host_info = HostInfo.HostInfo() |
190 | self._container = None |
191 | - self._manager = None |
192 | self._freeze_on_stop = config.get_freeze_on_stop(self.container_id) |
193 | |
194 | if not _setup_lxd(): |
195 | raise Exception("Failed to setup lxd.") |
196 | |
197 | self._client = pylxd.Client() |
198 | - |
199 | - try: |
200 | - if utils.set_session_dbus_env_var(): |
201 | - bus = dbus.SessionBus() |
202 | - self._manager = bus.get_object(LIBERTINE_MANAGER_NAME, LIBERTINE_STORE_PATH) |
203 | - except PermissionError as e: |
204 | - utils.get_logger().warning("Failed to set dbus session env var") |
205 | - except dbus.exceptions.DBusException: |
206 | - utils.get_logger().warning("D-Bus Service not found.") |
207 | + self._manager = Client.Client() |
208 | |
209 | def create_libertine_container(self, password=None, multiarch=False): |
210 | if self._try_get_container(): |
211 | @@ -409,13 +399,7 @@ |
212 | if not self._try_get_container(): |
213 | return False |
214 | |
215 | - if self._manager: |
216 | - retries = 0 |
217 | - while not self._manager.container_operation_start(self.container_id): |
218 | - retries += 1 |
219 | - if retries > 5: |
220 | - return False |
221 | - time.sleep(.5) |
222 | + self._manager.container_operation_start(self.container_id) |
223 | |
224 | if self._container.status == 'Running': |
225 | return True |
226 | @@ -427,8 +411,7 @@ |
227 | update_bind_mounts(self._container, self._config, home) |
228 | |
229 | if not lxd_start(self._container): |
230 | - if self._manager: |
231 | - self._manager.container_stopped(self.container_id) |
232 | + self._manager.container_stopped() |
233 | return False |
234 | |
235 | if not _wait_for_network(self._container): |
236 | @@ -443,14 +426,14 @@ |
237 | if not self._try_get_container(): |
238 | return False |
239 | |
240 | - if self._manager: |
241 | - if self._manager.container_operation_finished(self.container_id): |
242 | - if not lxd_stop(self._container, freeze_on_stop=self._freeze_on_stop): |
243 | - return False |
244 | - self._manager.container_stopped(self.container_id) |
245 | - return True |
246 | - else: |
247 | - return False |
248 | + if self._manager.valid: |
249 | + if not self._manager.container_operation_finished(self.container_id): |
250 | + return False |
251 | + |
252 | + if not lxd_stop(self._container, freeze_on_stop=self._freeze_on_stop): |
253 | + return False |
254 | + |
255 | + return self._manager.container_stopped(self.container_id) |
256 | else: |
257 | return lxd_stop(self._container, freeze_on_stop=self._freeze_on_stop) |
258 | |
259 | @@ -473,7 +456,7 @@ |
260 | |
261 | self._freeze_on_stop = orig_freeze |
262 | return self.stop_container(wait=True) |
263 | - |
264 | + |
265 | def start_application(self, app_exec_line, environ): |
266 | if not self._try_get_container(): |
267 | utils.get_logger().error("Could not get container '{}'".format(self.container_id)) |
268 | |
269 | === modified file 'python/libertine/service/manager.py' |
270 | --- python/libertine/service/manager.py 2017-02-07 12:35:48 +0000 |
271 | +++ python/libertine/service/manager.py 2017-02-15 21:25:37 +0000 |
272 | @@ -21,9 +21,9 @@ |
273 | from libertine import utils |
274 | |
275 | |
276 | -LIBERTINE_MANAGER_NAME = "com.canonical.libertine.Service" |
277 | +LIBERTINE_MANAGER_NAME = "com.canonical.libertine.Service" |
278 | LIBERTINE_MANAGER_INTERFACE = LIBERTINE_MANAGER_NAME |
279 | -LIBERTINE_STORE_PATH = "/Manager" |
280 | +LIBERTINE_MANAGER_OBJECT = "/Manager" |
281 | |
282 | |
283 | class Manager(dbus.service.Object): |
284 | @@ -39,7 +39,7 @@ |
285 | utils.get_logger().warning("service is already running") |
286 | raise |
287 | |
288 | - super().__init__(bus_name, LIBERTINE_STORE_PATH) |
289 | + super().__init__(bus_name, LIBERTINE_MANAGER_OBJECT) |
290 | |
291 | self._dispatcher = libertine.service.task_dispatcher.TaskDispatcher(self.connection) |
292 | |
293 | |
294 | === modified file 'python/libertine/utils.py' |
295 | --- python/libertine/utils.py 2017-02-07 16:32:02 +0000 |
296 | +++ python/libertine/utils.py 2017-02-15 21:25:37 +0000 |
297 | @@ -159,19 +159,23 @@ |
298 | def set_session_dbus_env_var(): |
299 | dbus_session_set = 'SESSION' in os.environ |
300 | |
301 | - if not dbus_session_set: |
302 | - for p in psutil.process_iter(): |
303 | - try: |
304 | - if p.name() == 'unity8' or p.name() == 'compiz': |
305 | - p_environ = subprocess.check_output(["cat", "/proc/{}/environ".format(p.pid)]) |
306 | - for line in p_environ.decode().split('\0'): |
307 | - if line.startswith('DBUS_SESSION_BUS_ADDRESS'): |
308 | - os.environ['DBUS_SESSION_BUS_ADDRESS'] = line.partition('DBUS_SESSION_BUS_ADDRESS=')[2].rstrip('\n') |
309 | - dbus_session_set = True |
310 | - break |
311 | - break |
312 | - except psutil.NoSuchProcess as e: |
313 | - get_logger().warning(str(e)) |
314 | + try: |
315 | + if not dbus_session_set: |
316 | + for p in psutil.process_iter(): |
317 | + try: |
318 | + if p.name() == 'unity8' or p.name() == 'compiz': |
319 | + p_environ = subprocess.check_output(["cat", "/proc/{}/environ".format(p.pid)]) |
320 | + for line in p_environ.decode().split('\0'): |
321 | + if line.startswith('DBUS_SESSION_BUS_ADDRESS'): |
322 | + os.environ['DBUS_SESSION_BUS_ADDRESS'] = line.partition('DBUS_SESSION_BUS_ADDRESS=')[2].rstrip('\n') |
323 | + dbus_session_set = True |
324 | + break |
325 | + break |
326 | + except psutil.NoSuchProcess as e: |
327 | + get_logger().warning(str(e)) |
328 | + except subprocess.CalledProcessError as e: |
329 | + get_logger().warning("Exception caught while setting session dbus address: {}".format(str(e))) |
330 | + dbus_session_set = 'DBUS_SESSION_BUS_ADDRESS' in os.environ |
331 | |
332 | return dbus_session_set |
333 | |
334 | |
335 | === removed directory 'setup' |
336 | === removed directory 'setup/gui' |
337 | === removed file 'setup/gui/libertine-manager-app.desktop' |
338 | --- setup/gui/libertine-manager-app.desktop 2017-01-23 15:21:46 +0000 |
339 | +++ setup/gui/libertine-manager-app.desktop 1970-01-01 00:00:00 +0000 |
340 | @@ -1,12 +0,0 @@ |
341 | -[Desktop Entry] |
342 | -Version=1.0 |
343 | -Name=Libertine Manager |
344 | -Comment=Legacy Application Sandbox |
345 | -Exec=libertine.manager-app %U |
346 | -Terminal=false |
347 | -Type=Application |
348 | -Icon=${SNAP}/usr/share/libertine/libertine_64.png |
349 | -Categories= |
350 | -GenericName=Application Sandbox |
351 | -X-Ubuntu-Touch=true |
352 | - |
353 | |
354 | === added directory 'snap/gui' |
355 | === added file 'snap/gui/libertine-manager-app.desktop' |
356 | --- snap/gui/libertine-manager-app.desktop 1970-01-01 00:00:00 +0000 |
357 | +++ snap/gui/libertine-manager-app.desktop 2017-02-15 21:25:37 +0000 |
358 | @@ -0,0 +1,12 @@ |
359 | +[Desktop Entry] |
360 | +Version=1.0 |
361 | +Name=Libertine Manager |
362 | +Comment=Legacy Application Sandbox |
363 | +Exec=libertine.manager-app %U |
364 | +Terminal=false |
365 | +Type=Application |
366 | +Icon=${SNAP}/usr/share/libertine/libertine_64.png |
367 | +Categories= |
368 | +GenericName=Application Sandbox |
369 | +X-Ubuntu-Touch=true |
370 | + |
371 | |
372 | === modified file 'snap/snap-runner.wrapper' |
373 | --- snap/snap-runner.wrapper 2017-02-14 19:17:30 +0000 |
374 | +++ snap/snap-runner.wrapper 2017-02-15 21:25:37 +0000 |
375 | @@ -7,6 +7,6 @@ |
376 | |
377 | # Useful debug variables |
378 | # export FAKECHROOT_DEBUG=1 |
379 | -# export LIBERTINE_DEBUG=1 |
380 | +# export LIBERTINE_DEBUG=2 |
381 | |
382 | exec $SNAP/bin/desktop-launch "$@" |
383 | |
384 | === modified file 'snapcraft.yaml' |
385 | --- snapcraft.yaml 2017-02-14 19:17:30 +0000 |
386 | +++ snapcraft.yaml 2017-02-15 21:25:37 +0000 |
387 | @@ -3,7 +3,7 @@ |
388 | summary: Libertine suite |
389 | description: | |
390 | Suite for maintaining deb-based applications in a non-deb environment |
391 | -confinement: devmode # TODO: update to 'strict' |
392 | +confinement: strict |
393 | grade: devel |
394 | |
395 | slots: |
396 | @@ -12,7 +12,22 @@ |
397 | name: com.canonical.libertine.Service |
398 | bus: session |
399 | |
400 | +plugs: |
401 | + libertined-client: |
402 | + interface: dbus |
403 | + name: com.canonical.libertine.Service |
404 | + bus: session |
405 | + |
406 | apps: |
407 | + libertined: |
408 | + command: usr/bin/snap-runner.wrapper libertined |
409 | + plugs: |
410 | + - lxd |
411 | + - network |
412 | + - network-bind |
413 | + slots: |
414 | + - libertined |
415 | + # daemon: simple # Waiting on LP:1613420 |
416 | launch: |
417 | command: usr/bin/snap-runner.wrapper libertine-launch |
418 | aliases: |
419 | @@ -22,18 +37,12 @@ |
420 | - network |
421 | - network-bind |
422 | - home |
423 | - - mir |
424 | - x11 |
425 | - unity7 |
426 | - opengl |
427 | - pulseaudio |
428 | - alsa |
429 | - libertined: |
430 | - command: usr/bin/snap-runner.wrapper libertined |
431 | - plugs: |
432 | - - lxd |
433 | - - network-bind |
434 | - # daemon: simple # Waiting on LP:1613420 |
435 | + - libertined-client |
436 | container-manager: |
437 | command: usr/bin/snap-runner.wrapper libertine-container-manager |
438 | aliases: |
439 | @@ -41,6 +50,7 @@ |
440 | plugs: |
441 | - lxd |
442 | - network |
443 | + - libertined-client |
444 | manager-app: |
445 | command: usr/bin/snap-runner.wrapper libertine-manager-app |
446 | # desktop command currently being labeled unknown by app store, restore with snapcraft 2.26 release |
447 | @@ -51,6 +61,8 @@ |
448 | # For debugging purposes |
449 | bash: |
450 | command: usr/bin/snap-runner.wrapper bash |
451 | + plugs: |
452 | + - libertined-client |
453 | |
454 | parts: |
455 | libertine-source: |
456 | |
457 | === modified file 'tools/libertined' |
458 | --- tools/libertined 2016-12-07 21:24:16 +0000 |
459 | +++ tools/libertined 2017-02-15 21:25:37 +0000 |
460 | @@ -27,7 +27,10 @@ |
461 | class OutputRedirector(object): |
462 | def __init__(self, config): |
463 | self.config = config |
464 | - self.cache_path = '%s/.cache/libertined' % os.environ['HOME'] |
465 | + if utils.is_snap_environment(): |
466 | + self.cache_path = '%s/.cache/libertined' % os.environ['SNAP_USER_COMMON'] |
467 | + else: |
468 | + self.cache_path = '%s/.cache/libertined' % os.environ['HOME'] |
469 | self.cache_file = '%s/libertined.log' % self.cache_path |
470 | self.output_file = None |
471 | self.copied_stdout = None |
PASSED: Continuous integration, rev:396 /jenkins. canonical. com/libertine/ job/lp- libertine- ci/384/ /jenkins. canonical. com/libertine/ job/build/ 735 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=amd64, release= xenial+ overlay, testname= default/ 605 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=amd64, release= zesty,testname= default/ 605 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=i386, release= xenial+ overlay, testname= default/ 605 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=i386, release= zesty,testname= default/ 605 /jenkins. canonical. com/libertine/ job/build- 0-fetch/ 745 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= xenial+ overlay/ 726 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= xenial+ overlay/ 726/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= zesty/726 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= zesty/726/ artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= xenial+ overlay/ 726 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= xenial+ overlay/ 726/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= zesty/726 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= zesty/726/ artifact/ output/ *zip*/output. zip
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/libertine/ job/lp- libertine- ci/384/ rebuild
https:/