Merge lp:~snappy-dev/click/default-apparmor into lp:click
- default-apparmor
- Merge into trunk
Proposed by
Barry Warsaw
Status: | Superseded |
---|---|
Proposed branch: | lp:~snappy-dev/click/default-apparmor |
Merge into: | lp:click |
Diff against target: |
2566 lines (+2056/-55) 24 files modified
acquire/http-udm (+28/-0) acquire/pycurl (+30/-0) click/acquire.py (+450/-0) click/build.py (+120/-8) click/commands/__init__.py (+3/-0) click/commands/info.py (+50/-5) click/commands/install.py (+41/-2) click/commands/login.py (+53/-0) click/commands/search.py (+46/-0) click/commands/update.py (+108/-0) click/install.py (+49/-13) click/network.py (+72/-0) click/oauth.py (+29/-0) click/paths.py.in (+3/-0) click/repository.py (+224/-0) click/tests/test_acquire.py (+174/-0) click/tests/test_build.py (+292/-8) click/tests/test_install.py (+20/-13) debian/changelog (+206/-0) debian/click.postinst (+2/-2) debian/control (+4/-3) debian/rules (+2/-1) lib/click/user.vala (+42/-0) setup.py.in (+8/-0) |
To merge this branch: | bzr merge lp:~snappy-dev/click/default-apparmor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jamie Strandboge | Pending | ||
Michael Vogt | Pending | ||
Review via email:
|
Commit message
Description of the change
snappy build needs to generate default apparmor profile automatically
To post a comment you must log in.
Unmerged revisions
- 574. By Barry Warsaw
-
Create default apparmor profiles if the yaml doesn't explicitly name
integration:<app>:apparmor{ ,-profile} With tests!
- 573. By Michael Vogt
-
merged lp:~mvo/click/yaml-manifest
- 572. By Michael Vogt
-
make dpkg output in install quiet unless "--verbose" is given
- 571. By Michael Vogt
-
merged lp:~snappy-dev/click/progressmeter (thanks to Barry Warsaw)
- 570. By Michael Vogt
-
add support for packages.yaml "binaries:"
- 569. By Michael Vogt
-
merge previous upload
- 568. By Michael Vogt
-
merged lp:~mvo/click/yaml-manifest
- 567. By Michael Vogt
-
more fixes
- 566. By Michael Vogt
-
fix test failure
- 565. By Michael Vogt
-
add support for "services" in packages.yaml
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'acquire' |
2 | === added symlink 'acquire/file' |
3 | === target is u'pycurl' |
4 | === added symlink 'acquire/ftp' |
5 | === target is u'pycurl' |
6 | === added symlink 'acquire/http' |
7 | === target is u'pycurl' |
8 | === added file 'acquire/http-udm' |
9 | --- acquire/http-udm 1970-01-01 00:00:00 +0000 |
10 | +++ acquire/http-udm 2014-12-03 23:19:34 +0000 |
11 | @@ -0,0 +1,28 @@ |
12 | +#!/usr/bin/python3 |
13 | + |
14 | +# Copyright (C) 2014 Canonical Ltd. |
15 | +# Author: Michael Vogt <michael.vogt@ubuntu.com> |
16 | + |
17 | +# This program is free software: you can redistribute it and/or modify |
18 | +# it under the terms of the GNU General Public License as published by |
19 | +# the Free Software Foundation; version 3 of the License. |
20 | +# |
21 | +# This program is distributed in the hope that it will be useful, |
22 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
23 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
24 | +# GNU General Public License for more details. |
25 | +# |
26 | +# You should have received a copy of the GNU General Public License |
27 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
28 | + |
29 | +"""Acquire method for ubuntu-download-manager.""" |
30 | + |
31 | +from click.acquire import ClickAcquireMethodUbuntuDownloadManager |
32 | + |
33 | +from dbus.mainloop.glib import DBusGMainLoop |
34 | +DBusGMainLoop(set_as_default=True) |
35 | + |
36 | + |
37 | +if __name__ == "__main__": |
38 | + m = ClickAcquireMethodUbuntuDownloadManager() |
39 | + m.run() |
40 | |
41 | === added symlink 'acquire/https' |
42 | === target is u'pycurl' |
43 | === added file 'acquire/pycurl' |
44 | --- acquire/pycurl 1970-01-01 00:00:00 +0000 |
45 | +++ acquire/pycurl 2014-12-03 23:19:34 +0000 |
46 | @@ -0,0 +1,30 @@ |
47 | +#!/usr/bin/python3 |
48 | + |
49 | +# Copyright (C) 2014 Canonical Ltd. |
50 | +# Author: Michael Vogt <michael.vogt@ubuntu.com> |
51 | + |
52 | +# This program is free software: you can redistribute it and/or modify |
53 | +# it under the terms of the GNU General Public License as published by |
54 | +# the Free Software Foundation; version 3 of the License. |
55 | +# |
56 | +# This program is distributed in the hope that it will be useful, |
57 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
58 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
59 | +# GNU General Public License for more details. |
60 | +# |
61 | +# You should have received a copy of the GNU General Public License |
62 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
63 | + |
64 | +"""Acquire pycurl method click.""" |
65 | + |
66 | +import signal |
67 | +import sys |
68 | + |
69 | +from click.acquire import ClickAcquireMethodPycurl |
70 | + |
71 | + |
72 | +if __name__ == "__main__": |
73 | + # apt will send a SIGINT to all methods that do not send "Need-Cleanup: 1" |
74 | + signal.signal(signal.SIGINT, lambda *args: sys.exit()) |
75 | + m = ClickAcquireMethodPycurl() |
76 | + m.run() |
77 | |
78 | === added file 'click/acquire.py' |
79 | --- click/acquire.py 1970-01-01 00:00:00 +0000 |
80 | +++ click/acquire.py 2014-12-03 23:19:34 +0000 |
81 | @@ -0,0 +1,450 @@ |
82 | +# Copyright (C) 2014 Canonical Ltd. |
83 | +# Author: Michael Vogt <michael.vogt@ubuntu.com> |
84 | + |
85 | +# This program is free software: you can redistribute it and/or modify |
86 | +# it under the terms of the GNU General Public License as published by |
87 | +# the Free Software Foundation; version 3 of the License. |
88 | +# |
89 | +# This program is distributed in the hope that it will be useful, |
90 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
91 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
92 | +# GNU General Public License for more details. |
93 | +# |
94 | +# You should have received a copy of the GNU General Public License |
95 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
96 | + |
97 | +"""Acquire of Click packages.""" |
98 | + |
99 | +from __future__ import print_function |
100 | + |
101 | +__metaclass__ = type |
102 | +__all__ = [ |
103 | + 'ClickAcquire', |
104 | + 'ClickAcquireError', |
105 | + 'ClickAcquireMethod', |
106 | + 'ClickAcquireMethodPycurl', |
107 | + 'ClickAcquireMethodUbuntuDownloadManager', |
108 | + 'ClickAcquireStatus', |
109 | + 'ClickAcquireStatusMeter', |
110 | + 'ClickAcquireStatusText', |
111 | + ] |
112 | + |
113 | +import os |
114 | +import math |
115 | +import select |
116 | +import subprocess |
117 | +import sys |
118 | +from textwrap import dedent |
119 | + |
120 | +from progressbar import ProgressBar, Bar, Widget |
121 | + |
122 | +import apt_pkg |
123 | +from debian.deb822 import Deb822 |
124 | +import dbus |
125 | +from gi.repository import GLib |
126 | +import pycurl |
127 | +from six.moves.urllib.parse import urlparse |
128 | + |
129 | +from click.paths import acquire_methods_dir |
130 | +from click.repository import get_repository |
131 | +from click.oauth import get_oauth_headers_for_uri |
132 | + |
133 | +class ClickAcquireError(Exception): |
134 | + """Error during acquire""" |
135 | + pass |
136 | + |
137 | + |
138 | +def extract_one_message(lines): |
139 | + msg = [] |
140 | + while True: |
141 | + try: |
142 | + line = lines.pop(0) |
143 | + except IndexError: |
144 | + break |
145 | + if os.environ.get("CLICK_DEBUG_ACQUIRE", ""): |
146 | + sys.stderr.write("[%s] raw_line: '%s'\n" % ( |
147 | + os.getpid(), line.replace("\n", "\\n"))) |
148 | + msg.append(line) |
149 | + if len(msg) > 0 and line == "": |
150 | + # we are done, collect all remaining "\n" and stop |
151 | + while len(lines) > 0 and lines[0] == "": |
152 | + lines.pop(0) |
153 | + break |
154 | + if len(msg) < 2: |
155 | + return -1 , "" |
156 | + if os.environ.get("CLICK_DEBUG_ACQUIRE", ""): |
157 | + sys.stderr.write("[%s] msg: '%s'\n" % (os.getpid(), str(msg))) |
158 | + number = int(msg[0].split()[0]) |
159 | + return number, "\n".join(msg) |
160 | + |
161 | + |
162 | +def _read_messages(input_file, timeout=None): |
163 | + infd = input_file.fileno() |
164 | + msgs = [] |
165 | + rl, wl, xl = select.select([infd], [], [], timeout) |
166 | + if rl: |
167 | + # FIXME: 16k message limit is arbitrary |
168 | + buf = os.read(infd, 16*1024).decode("utf-8") |
169 | + if os.environ.get("CLICK_DEBUG_ACQUIRE", ""): |
170 | + sys.stderr.write("[%s] read buf: '%s'\n" % ( |
171 | + os.getpid(), buf)) |
172 | + lines = buf.split("\n") |
173 | + while True: |
174 | + number, msg = extract_one_message(lines) |
175 | + if number < 0: |
176 | + break |
177 | + msgs.append( (number, msg) ) |
178 | + return msgs |
179 | + |
180 | + |
181 | +class ClickAcquireStatus: |
182 | + """Base class for the status reporting """ |
183 | + |
184 | + def __init__(self): |
185 | + self.fetched_bytes = 0 |
186 | + self.total_bytes = 1.0 |
187 | + self.uri = "" |
188 | + |
189 | + def pulse(self): |
190 | + pass |
191 | + |
192 | + def done(self): |
193 | + pass |
194 | + |
195 | + |
196 | +class ClickAcquireStatusText(ClickAcquireStatus): |
197 | + """Text based progress reporting for the acquire progress""" |
198 | + |
199 | + def pulse(self): |
200 | + sys.stdout.write("\r") |
201 | + sys.stdout.write("[%3.2f %%] Fetching %s" % ( |
202 | + (self.fetched_bytes/self.total_bytes)*100.0, |
203 | + os.path.basename(self.uri))) |
204 | + sys.stdout.flush() |
205 | + |
206 | + def done(self): |
207 | + self.pulse() |
208 | + sys.stdout.write("\n") |
209 | + sys.stdout.write("Done fetching %s\n" % self.uri) |
210 | + |
211 | + |
212 | +class FileSize(Widget): |
213 | + def update(self, pbar): |
214 | + value = pbar.maxval # or .currval? |
215 | + if value < 1000: |
216 | + return '{:3d} B '.format(int(value)) |
217 | + scale = int(math.log(value, 1000)) # or binary? e.g. MiB |
218 | + try: |
219 | + suffix = ' kMGTPEZY'[scale] |
220 | + except IndexError: |
221 | + # Should never happen, but still. |
222 | + suffix = '!' |
223 | + scaled_value = value // 1000 ** scale |
224 | + return '{:3d} {:1s}B '.format(scaled_value, suffix) |
225 | + |
226 | + |
227 | +class OK(Widget): |
228 | + def update(self, pbar): |
229 | + if pbar.currval >= pbar.maxval: |
230 | + return 'OK' |
231 | + else: |
232 | + return ' ' |
233 | + |
234 | +SPACER = ' ' |
235 | + |
236 | +class ClickAcquireStatusMeter(ClickAcquireStatus): |
237 | + """Progress meter reporting for the acquire progress""" |
238 | + |
239 | + # We have to defer creating the actual progress bar until its first use |
240 | + # because the constructor doesn't get called with essential information |
241 | + # (e.g. the uri and the total bytes). These get set after the fact but |
242 | + # before the first call to .pulse(), so initialize the object there. |
243 | + _progress = None |
244 | + |
245 | + def pulse(self): |
246 | + if self.total_bytes <= 1: |
247 | + return |
248 | + if self._progress is None: |
249 | + # Massage the uri. Use the part to the right of the .click |
250 | + # suffix, minus any version number. |
251 | + base = os.path.basename(self.uri) |
252 | + if base.startswith('com.ubuntu.snappy.'): |
253 | + base = base[18:] |
254 | + snap, _, extra = base.partition('_') |
255 | + self._progress = ProgressBar(widgets=[ |
256 | + snap, |
257 | + SPACER, |
258 | + # XXX in the examples, it's unclear whether the size field |
259 | + # should be the total size or the fetched size. Also, decimal |
260 | + # (MB) or binary (MiB)? For now, we go with total size in |
261 | + # decimal. |
262 | + FileSize(), |
263 | + SPACER, |
264 | + Bar(marker='=', left='[', right=']'), |
265 | + SPACER, |
266 | + OK(), |
267 | + SPACER, |
268 | + ], |
269 | + maxval=self.total_bytes) |
270 | + self._progress.start() |
271 | + self._progress.update(self.fetched_bytes) |
272 | + |
273 | + def done(self): |
274 | + self._progress.finish() |
275 | + |
276 | + |
277 | +# similar to Acquire/AcquireWorker |
278 | +class ClickAcquire: |
279 | + """Acquire from remote locations""" |
280 | + |
281 | + # default status reporting timeout |
282 | + TIMEOUT = 0.05 |
283 | + |
284 | + # acquire method status codecs |
285 | + M_CAPABILITIES = 100 |
286 | + M_STATUS = 102 |
287 | + M_REDIRECT = 103 |
288 | + URI_START = 200 |
289 | + URI_SUCCESS = 201 |
290 | + URI_FAILURE = 400 |
291 | + |
292 | + def __init__(self, status=ClickAcquireStatus()): |
293 | + self._fetch_queue = [] |
294 | + self._status = status |
295 | + |
296 | + def _uri_acquire(self, pipe, uri, destfile): |
297 | + cmd = dedent("""\ |
298 | + 600 URI Acquire |
299 | + URI: {uri} |
300 | + Filename: {destfile} |
301 | + |
302 | + """).format(uri=uri, destfile=destfile) |
303 | + pipe.write(cmd) |
304 | + pipe.flush() |
305 | + |
306 | + def _redirect(self, pipe, new_uri, destfile): |
307 | + self._uri_acquire(pipe, new_uri, destfile) |
308 | + |
309 | + def _run_acquire_method(self, uri, destfile): |
310 | + parsed_uri = urlparse(uri) |
311 | + cmd = os.path.join(acquire_methods_dir, parsed_uri.scheme) |
312 | + p = subprocess.Popen([cmd], |
313 | + stdout=subprocess.PIPE, stdin=subprocess.PIPE, |
314 | + universal_newlines=True) |
315 | + while True: |
316 | + for number, raw_message in _read_messages(p.stdout, self.TIMEOUT): |
317 | + message = Deb822(raw_message) |
318 | + if number == self.M_CAPABILITIES: |
319 | + self._uri_acquire(p.stdin, uri, destfile) |
320 | + elif number == self.M_STATUS: |
321 | + pass |
322 | + elif number == self.M_REDIRECT: |
323 | + self._redirect(p.stdin, message.get("New-URI"), destfile) |
324 | + elif number == self.URI_START: |
325 | + self._status.uri = message.get("URI", 0) |
326 | + self._status.total_bytes = int(message.get("Size", 0)) |
327 | + elif number == self.URI_SUCCESS: |
328 | + self._status.fetched_bytes = int(message.get("Size")) |
329 | + self._status.done() |
330 | + p.stdout.close() |
331 | + p.stdin.close() |
332 | + return True |
333 | + elif number == self.URI_FAILURE: |
334 | + p.stdout.close() |
335 | + p.stdin.close() |
336 | + raise ClickAcquireError("Uri failure for %s: %s" % ( |
337 | + message.get("uri"), message.get("Message"))) |
338 | + # update progress |
339 | + if os.path.exists(destfile): |
340 | + self._status.fetched_bytes = os.path.getsize(destfile) |
341 | + self._status.pulse() |
342 | + return False |
343 | + |
344 | + def fetch(self, uri, destfile): |
345 | + self._run_acquire_method(uri, destfile) |
346 | + |
347 | + |
348 | +# similar to the apt AcquireMethod |
349 | +class ClickAcquireMethod: |
350 | + |
351 | + M_CONFIGURATION = 601 |
352 | + M_FETCH = 600 |
353 | + |
354 | + VERSION = 0.1 |
355 | + |
356 | + def __init__(self): |
357 | + s = dedent("""\ |
358 | + 100 Capabilities |
359 | + Version: {version} |
360 | + Single-Instance: true |
361 | + |
362 | + """).format(version=self.VERSION) |
363 | + sys.stdout.write(s) |
364 | + sys.stdout.flush() |
365 | + |
366 | + def run(self): |
367 | + while True: |
368 | + msgs = _read_messages(sys.stdin) |
369 | + if not msgs: |
370 | + break |
371 | + for number, raw_message in msgs: |
372 | + message = Deb822(raw_message) |
373 | + if number == self.M_CONFIGURATION: |
374 | + pass |
375 | + elif number == self.M_FETCH: |
376 | + self.fetch(message.get("URI"), message.get("FileName")) |
377 | + |
378 | + def uri_start(self, uri, filename, size): |
379 | + # note that apt itself does not use "Filename" here because it |
380 | + # will set a filename and expects the method to use it, however |
381 | + # this will not work with UbuntuDownloadManager as it downloads |
382 | + # to its own location |
383 | + sys.stdout.write(dedent("""\ |
384 | + 200 URI Start |
385 | + URI: {uri} |
386 | + Filename: {filename} |
387 | + Size: {size} |
388 | + |
389 | + """).format(uri=uri, filename=filename, size=size)) |
390 | + sys.stdout.flush() |
391 | + |
392 | + def uri_done(self, uri, filename): |
393 | + # bug in python-apt |
394 | + hashes = apt_pkg.Hashes(filename.encode("utf-8")) |
395 | + sys.stdout.write(dedent("""\ |
396 | + 201 URI Done |
397 | + URI: {uri} |
398 | + Filename: {filename} |
399 | + Size: {size} |
400 | + Sha256-Hash: {sha256hash} |
401 | + |
402 | + """).format(uri=uri, filename=filename, size=os.path.getsize(filename), |
403 | + sha256hash=hashes.sha256)) |
404 | + sys.stdout.flush() |
405 | + |
406 | + def fail(self, uri, err="unknown error"): |
407 | + sys.stdout.write(dedent("""\ |
408 | + 400 URI Failure |
409 | + URI: {uri} |
410 | + Message: {err} |
411 | + |
412 | + """).format(uri=uri, err=err)) |
413 | + sys.stdout.flush() |
414 | + |
415 | + def fetch(self, uri, destfile): |
416 | + pass |
417 | + |
418 | + |
419 | +class ClickAcquireMethodPycurl(ClickAcquireMethod): |
420 | + |
421 | + def _write_to_file_callback(self, data): |
422 | + self._destfile_fp.write(data) |
423 | + return len(data) |
424 | + |
425 | + def _progress_info(self, dltotal, dlnow, ultotal, ulnow): |
426 | + if dltotal > 0 and not self._uri_start_reported: |
427 | + self.uri_start( |
428 | + self.uri, self._destfile_fp.name, int(dltotal)) |
429 | + self._uri_start_reported = True |
430 | + |
431 | + def fetch(self, uri, destfile): |
432 | + self._uri_start_reported = False |
433 | + self.uri = uri |
434 | + self._destfile_fp = open(destfile, "wb") |
435 | + curl = pycurl.Curl() |
436 | + curl.setopt(pycurl.URL, uri) |
437 | + curl.setopt(pycurl.WRITEFUNCTION, self._write_to_file_callback) |
438 | + curl.setopt(pycurl.NOPROGRESS, 0) |
439 | + curl.setopt(pycurl.PROGRESSFUNCTION, self._progress_info) |
440 | + curl.setopt(pycurl.FOLLOWLOCATION, 1) |
441 | + curl.setopt(pycurl.MAXREDIRS, 5) |
442 | + curl.setopt(pycurl.FAILONERROR, 1) |
443 | + # timeout 120s for conenction |
444 | + curl.setopt(pycurl.CONNECTTIMEOUT, 120) |
445 | + # timeout if the speed is 120s below 10 bytes/sec |
446 | + curl.setopt(pycurl.LOW_SPEED_LIMIT, 10); |
447 | + curl.setopt(pycurl.LOW_SPEED_TIME, 120); |
448 | + # ssl: no need to set any option here, |
449 | + # SSL_VERIFYPEER=1, SSL_VERIFYHOST=2, |
450 | + # CAINFO=/etc/ssl/certs/ca-certificates.crt |
451 | + # by default in libcurl3 these days |
452 | + |
453 | + # FIXME: duplicated code with network.py:http_request |
454 | + # add auth header |
455 | + repo = get_repository() |
456 | + if repo.credentials: |
457 | + headers = get_oauth_headers_for_uri(uri, repo.credentials) |
458 | + header_list = [] |
459 | + for k, v in headers.items(): |
460 | + header_list.append("%s: %s" % (k, v)) |
461 | + curl.setopt(pycurl.HTTPHEADER, header_list) |
462 | + # do it |
463 | + try: |
464 | + curl.perform() |
465 | + self._destfile_fp.close() |
466 | + self.uri_done(uri, destfile) |
467 | + except pycurl.error as e: |
468 | + self.fail(self.uri, e.args) |
469 | + self._destfile_fp.close() |
470 | + |
471 | + |
472 | +class ClickAcquireMethodUbuntuDownloadManager(ClickAcquireMethod): |
473 | + |
474 | + MANAGER_PATH = '/' |
475 | + MANAGER_IFACE = 'com.canonical.applications.DownloadManager' |
476 | + DOWNLOAD_IFACE = 'com.canonical.applications.Download' |
477 | + |
478 | + def __init__(self): |
479 | + super(ClickAcquireMethodUbuntuDownloadManager, self).__init__() |
480 | + self.bus = dbus.SessionBus() |
481 | + self.loop = GLib.MainLoop() |
482 | + |
483 | + def _created_callback(self, dbus_path): |
484 | + pass |
485 | + |
486 | + def _finished_callback(self, path, loop): |
487 | + self.down_path = path |
488 | + loop.quit() |
489 | + |
490 | + def _progress_callback(self, total, progress): |
491 | + #print('Progress is %s/%s' % (progress, total)) |
492 | + if not self._started: |
493 | + # FIXME: we need the tmpfile path from udm |
494 | + tmpfile = "meep" |
495 | + self.uri_start(self.uri, tmpfile, total) |
496 | + self._started = True |
497 | + |
498 | + def fetch(self, uri, destfile): |
499 | + self.uri = uri |
500 | + self._started = False |
501 | + manager = self.bus.get_object( |
502 | + 'com.canonical.applications.Downloader', self.MANAGER_PATH) |
503 | + manager_dev_iface = dbus.Interface( |
504 | + manager, dbus_interface=self.MANAGER_IFACE) |
505 | + manager_dev_iface.connect_to_signal( |
506 | + 'downloadCreated', self._created_callback) |
507 | + down_path = manager_dev_iface.createDownload( |
508 | + (uri, "", "", dbus.Dictionary({}, signature="sv"), |
509 | + dbus.Dictionary({}, signature="ss"))) |
510 | + download1 = self.bus.get_object('com.canonical.applications.Downloader', |
511 | + down_path) |
512 | + download_dev_iface1 = dbus.Interface( |
513 | + download1, dbus_interface=self.DOWNLOAD_IFACE) |
514 | + download_dev_iface1.connect_to_signal( |
515 | + 'progress', self._progress_callback) |
516 | + download_dev_iface1.connect_to_signal( |
517 | + 'finished', |
518 | + lambda path: self._finished_callback(path, self.loop)) |
519 | + download_dev_iface1.start() |
520 | + self.loop.run() |
521 | + # FIXME error handling, i.e. send self.fail() |
522 | + self.uri_done(uri, self.down_path) |
523 | + |
524 | + |
525 | +if __name__ == "__main__": |
526 | + log = ClickAcquireStatusText() |
527 | + acq = ClickAcquire(log) |
528 | + uri = sys.argv[1] |
529 | + if not acq.fetch(uri, os.path.basename(uri)): |
530 | + sys.exit(1) |
531 | + sys.exit(0) |
532 | |
533 | === modified file 'click/build.py' |
534 | --- click/build.py 2014-05-05 13:10:19 +0000 |
535 | +++ click/build.py 2014-12-03 23:19:34 +0000 |
536 | @@ -45,6 +45,12 @@ |
537 | # "click build" is required to work with only the Python standard library. |
538 | pass |
539 | |
540 | +try: |
541 | + import yaml |
542 | + HAVE_YAML=True |
543 | +except ImportError: |
544 | + HAVE_YAML=False |
545 | + |
546 | from click import osextras |
547 | from click.arfile import ArFile |
548 | from click.preinst import static_preinst |
549 | @@ -56,6 +62,18 @@ |
550 | ) |
551 | |
552 | |
553 | +APPARMOR_DEFAULT_TEMPLATE = """\ |
554 | +{ |
555 | + "template": "default", |
556 | + "policy_groups": [ |
557 | + "networking" |
558 | + ], |
559 | + "policy_vendor": "ubuntu-snappy", |
560 | + "policy_version": 1.3 |
561 | +} |
562 | +""" |
563 | + |
564 | + |
565 | @contextlib.contextmanager |
566 | def make_temp_dir(): |
567 | temp_dir = tempfile.mkdtemp(prefix="click") |
568 | @@ -83,24 +101,110 @@ |
569 | class ClickBuilderBase: |
570 | def __init__(self): |
571 | self.file_map = {} |
572 | + self.autogen_apparmor = [] |
573 | |
574 | def add_file(self, source_path, dest_path): |
575 | self.file_map[source_path] = dest_path |
576 | |
577 | def read_manifest(self, manifest_path): |
578 | + if manifest_path.endswith(".yaml"): |
579 | + self._read_manifest_snappy(manifest_path) |
580 | + elif manifest_path.endswith(".json"): |
581 | + self._read_manifest_json(manifest_path) |
582 | + self._remove_reserved_keys() |
583 | + |
584 | + def _read_manifest_snappy(self, manifest_path): |
585 | + """The "new" snappy yaml based format""" |
586 | + if not HAVE_YAML: |
587 | + raise Exception("Please install python3-yaml") |
588 | + with io.open(manifest_path, encoding="UTF-8") as manifest: |
589 | + try: |
590 | + self.manifest = yaml.safe_load(manifest) |
591 | + except Exception as e: |
592 | + raise ClickBuildError( |
593 | + "Error reading manifest from %s: %s" % (manifest_path, e)) |
594 | + # adjust differences |
595 | + self.manifest["version"] = str(self.manifest["version"]) |
596 | + readme = os.path.join(os.path.dirname(manifest_path), "readme.md") |
597 | + with io.open(readme) as f: |
598 | + self.manifest["title"] = f.readline().strip() |
599 | + # FIXME: read paragraph here instead of just the second line |
600 | + self.manifest["description"] = f.readline() |
601 | + # icon is mandatory |
602 | + if "icon" not in self.manifest: |
603 | + raise ClickBuildError( |
604 | + "Missing 'icon:' tag in '%s'" % manifest_path) |
605 | + # renamed fields in snappy |
606 | + if not "framework" in self.manifest: |
607 | + self.manifest["framework"] = "" |
608 | + for click_name, snappy_name in (("framework", "frameworks"), |
609 | + ("maintainer", "vendor"), |
610 | + ("hooks", "integration")): |
611 | + # FIXME: warn when the old names are used |
612 | + if snappy_name in self.manifest: |
613 | + self.manifest[click_name] = self.manifest[snappy_name] |
614 | + # its just a rename, no need to keep the old name around |
615 | + del self.manifest[snappy_name] |
616 | + |
617 | + # deal with new "services" field |
618 | + hooks = self.manifest.get("hooks", {}) |
619 | + for service in self.manifest.get("services", []): |
620 | + name = service["name"] |
621 | + current_hook = hooks.get(name, {}) |
622 | + snappy_systemd = "{}.snappy-systemd".format(name) |
623 | + description = service.get( |
624 | + "description", |
625 | + self.manifest.get("description", "no description")) |
626 | + systemd_path = os.path.join( |
627 | + os.path.dirname(manifest_path), snappy_systemd) |
628 | + with open(systemd_path, "w") as fp: |
629 | + yaml.dump({"description": description, |
630 | + "start": service["start"], |
631 | + }, fp) |
632 | + current_hook["snappy-systemd"] = "meta/"+snappy_systemd |
633 | + hooks[name] = current_hook |
634 | + # Is there an explicit apparmor hook? If so, use it otherwise |
635 | + # generate a default one. |
636 | + if ( 'apparmor' not in current_hook and |
637 | + 'apparmor-profile' not in current_hook): |
638 | + template_path = os.path.join( |
639 | + 'meta', '{}.apparmor'.format(name)) |
640 | + current_hook['apparmor'] = template_path |
641 | + self.autogen_apparmor.append(template_path) |
642 | + self.manifest["hooks"] = hooks |
643 | + # cleanup |
644 | + if "services" in self.manifest: |
645 | + del self.manifest["services"] |
646 | + |
647 | + # deal with the new "binaries" field and generate one app for each |
648 | + hooks = self.manifest.get("hooks", {}) |
649 | + for binary in self.manifest.get("binaries", []): |
650 | + base_name = os.path.basename(binary["name"]) |
651 | + current_hook = hooks.get(base_name, {}) |
652 | + current_hook["bin-path"] = binary["name"] |
653 | + hooks[base_name] = current_hook |
654 | + self.manifest["hooks"] = hooks |
655 | + # cleanup |
656 | + if "binaries" in self.manifest: |
657 | + del self.manifest["binaries"] |
658 | + |
659 | + def _read_manifest_json(self, manifest_path): |
660 | + """Compat with the "old" click json format""" |
661 | with io.open(manifest_path, encoding="UTF-8") as manifest: |
662 | try: |
663 | self.manifest = json.load(manifest) |
664 | except Exception as e: |
665 | raise ClickBuildError( |
666 | "Error reading manifest from %s: %s" % (manifest_path, e)) |
667 | - keys = sorted(self.manifest) |
668 | - for key in keys: |
669 | - if key.startswith("_"): |
670 | - print( |
671 | - "Ignoring reserved dynamic key '%s'." % key, |
672 | - file=sys.stderr) |
673 | - del self.manifest[key] |
674 | + |
675 | + def _remove_reserved_keys(self): |
676 | + keys = sorted(self.manifest) |
677 | + for key in keys: |
678 | + if key.startswith("_"): |
679 | + print( |
680 | + "Ignoring reserved dynamic key '%s'." % key, |
681 | + file=sys.stderr) |
682 | + del self.manifest[key] |
683 | |
684 | @property |
685 | def name(self): |
686 | @@ -135,6 +239,7 @@ |
687 | # TODO: This should be configurable, or at least extensible. |
688 | _ignore_patterns = [ |
689 | "*.click", |
690 | + "*.snap", |
691 | ".*.sw?", |
692 | "*~", |
693 | ",,*", |
694 | @@ -234,6 +339,11 @@ |
695 | if "framework" in self.manifest: |
696 | self._validate_framework(self.manifest["framework"]) |
697 | |
698 | + for template_path in self.autogen_apparmor: |
699 | + path = os.path.join(root_path, template_path) |
700 | + with io.open(path, 'w', encoding='utf-8') as fp: |
701 | + fp.write(APPARMOR_DEFAULT_TEMPLATE) |
702 | + |
703 | du_output = subprocess.check_output( |
704 | ["du", "-k", "-s", "--apparent-size", "."], |
705 | cwd=temp_dir, universal_newlines=True).rstrip("\n") |
706 | @@ -267,7 +377,9 @@ |
707 | self.manifest, ensure_ascii=False, sort_keys=True, |
708 | indent=4, separators=(",", ": ")), |
709 | file=manifest) |
710 | - os.unlink(full_manifest_path) |
711 | + # FIXME: ugly! |
712 | + if not full_manifest_path.endswith("package.yaml"): |
713 | + os.unlink(full_manifest_path) |
714 | os.chmod(real_manifest_path, 0o644) |
715 | |
716 | md5sums_path = os.path.join(control_dir, "md5sums") |
717 | |
718 | === modified file 'click/commands/__init__.py' |
719 | --- click/commands/__init__.py 2014-05-20 09:03:05 +0000 |
720 | +++ click/commands/__init__.py 2014-12-03 23:19:34 +0000 |
721 | @@ -29,8 +29,11 @@ |
722 | "info", |
723 | "install", |
724 | "list", |
725 | + "login", |
726 | "pkgdir", |
727 | "register", |
728 | + "search", |
729 | + "update", |
730 | "unregister", |
731 | "verify", |
732 | ) |
733 | |
734 | === modified file 'click/commands/info.py' |
735 | --- click/commands/info.py 2014-10-07 08:56:03 +0000 |
736 | +++ click/commands/info.py 2014-12-03 23:19:34 +0000 |
737 | @@ -28,6 +28,10 @@ |
738 | |
739 | from click.install import DebFile |
740 | from click.json_helpers import json_object_to_python |
741 | +from click.repository import ( |
742 | + get_repository, |
743 | + ClickRepositoryError, |
744 | +) |
745 | |
746 | |
747 | def _load_manifest(manifest_file): |
748 | @@ -73,14 +77,55 @@ |
749 | "--user", metavar="USER", |
750 | help="look up PACKAGE-NAME for USER (if you have permission; " |
751 | "default: current user)") |
752 | + parser.add_option( |
753 | + "--repository", default=False, action="store_true", |
754 | + help="Search in remote repository only") |
755 | + parser.add_option( |
756 | + "--no-repository", default=False, action="store_true", |
757 | + help="Never search in a remote repository ") |
758 | options, args = parser.parse_args(argv) |
759 | + pkgname = args[0] |
760 | if len(args) < 1: |
761 | parser.error("need file name") |
762 | - try: |
763 | - manifest = get_manifest(options, args[0]) |
764 | - except Exception as e: |
765 | - print(e, file=sys.stderr) |
766 | - return 1 |
767 | + |
768 | + # see if its available locally: pkgname, click-path, path-to-unpacked-click |
769 | + manifest = None |
770 | + if not options.repository: |
771 | + try: |
772 | + manifest = get_manifest(options, pkgname) |
773 | + except Exception as e: |
774 | + pass |
775 | + |
776 | + # now check remote |
777 | + if manifest is None and not options.no_repository: |
778 | + repo = get_repository() |
779 | + try: |
780 | + details = repo.details(pkgname) |
781 | + manifest = {k: details.get(k) |
782 | + # FIXME: this list is a big ugly |
783 | + for k in ('name', |
784 | + 'download_url', |
785 | + 'title', |
786 | + 'description', |
787 | + 'binary_filesize', |
788 | + 'publisher', |
789 | + 'framework', |
790 | + 'version', |
791 | + 'last_updated', |
792 | + 'architecture', |
793 | + 'department', |
794 | + 'changelog', |
795 | + 'ratings_average', |
796 | + 'price', |
797 | + 'keywords', |
798 | + 'license', |
799 | + )} |
800 | + manifest["_repository"] = repo.description |
801 | + except ClickRepositoryError as e: |
802 | + print( |
803 | + "Can not get details for %s:%s" % (pkgname, e), file=sys.stderr) |
804 | + return 1 |
805 | + |
806 | json.dump( |
807 | manifest, sys.stdout, ensure_ascii=False, sort_keys=True, indent=4, |
808 | separators=(",", ": ")) |
809 | |
810 | === modified file 'click/commands/install.py' |
811 | --- click/commands/install.py 2014-09-10 12:28:49 +0000 |
812 | +++ click/commands/install.py 2014-12-03 23:19:34 +0000 |
813 | @@ -18,12 +18,33 @@ |
814 | from __future__ import print_function |
815 | |
816 | from optparse import OptionParser |
817 | +import os |
818 | import sys |
819 | +import tempfile |
820 | from textwrap import dedent |
821 | |
822 | +from six.moves.urllib.parse import urlparse |
823 | + |
824 | from gi.repository import Click |
825 | |
826 | +from click.acquire import ( |
827 | + ClickAcquire, |
828 | + ClickAcquireStatusMeter, |
829 | +) |
830 | from click.install import ClickInstaller, ClickInstallerError |
831 | +from click.repository import ( |
832 | + get_repository, |
833 | +) |
834 | + |
835 | +def download_click(package_uri): |
836 | + t = tempfile.NamedTemporaryFile() |
837 | + package_path = t.name |
838 | + log = ClickAcquireStatusMeter() |
839 | + acq = ClickAcquire(log) |
840 | + acq.fetch(package_uri, package_path) |
841 | + # return the tempfile so that it does not get deleted when we |
842 | + # go out of scope here |
843 | + return t |
844 | |
845 | |
846 | def run(argv): |
847 | @@ -46,6 +67,9 @@ |
848 | parser.add_option( |
849 | "--allow-unauthenticated", default=False, action="store_true", |
850 | help="allow installing packages with no signatures") |
851 | + parser.add_option( |
852 | + "--verbose", default=False, action="store_true", |
853 | + help="be more verbose on install") |
854 | options, args = parser.parse_args(argv) |
855 | if len(args) < 1: |
856 | parser.error("need package file name") |
857 | @@ -53,13 +77,28 @@ |
858 | db.read(db_dir=None) |
859 | if options.root is not None: |
860 | db.add(options.root) |
861 | - package_path = args[0] |
862 | + package_uri = args[0] |
863 | installer = ClickInstaller( |
864 | db=db, force_missing_framework=options.force_missing_framework, |
865 | allow_unauthenticated=options.allow_unauthenticated) |
866 | + parsed_uri = urlparse(package_uri) |
867 | + if parsed_uri.scheme != "": |
868 | + t = download_click(package_uri) |
869 | + package_path = t.name |
870 | + elif os.path.exists(package_uri): |
871 | + package_path = package_uri |
872 | + else: |
873 | + repo = get_repository() |
874 | + details = repo.details(package_uri) |
875 | + if repo.credentials: |
876 | + t = download_click(details["download_url"]) |
877 | + else: |
878 | + t = download_click(details["anon_download_url"]) |
879 | + package_path = t.name |
880 | try: |
881 | installer.install( |
882 | - package_path, user=options.user, all_users=options.all_users) |
883 | + package_path, user=options.user, all_users=options.all_users, |
884 | + quiet=not options.verbose) |
885 | except ClickInstallerError as e: |
886 | print("Cannot install %s: %s" % (package_path, e), file=sys.stderr) |
887 | return 1 |
888 | |
889 | === added file 'click/commands/login.py' |
890 | --- click/commands/login.py 1970-01-01 00:00:00 +0000 |
891 | +++ click/commands/login.py 2014-12-03 23:19:34 +0000 |
892 | @@ -0,0 +1,53 @@ |
893 | +# Copyright (C) 2014 Canonical Ltd. |
894 | +# Author: Michael Vogt |
895 | + |
896 | +# This program is free software: you can redistribute it and/or modify |
897 | +# it under the terms of the GNU General Public License as published by |
898 | +# the Free Software Foundation; version 3 of the License. |
899 | +# |
900 | +# This program is distributed in the hope that it will be useful, |
901 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
902 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
903 | +# GNU General Public License for more details. |
904 | +# |
905 | +# You should have received a copy of the GNU General Public License |
906 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
907 | + |
908 | +"""Log into for Click repository""" |
909 | + |
910 | +from __future__ import print_function |
911 | + |
912 | +import getpass |
913 | +from optparse import OptionParser |
914 | +import sys |
915 | +from textwrap import dedent |
916 | + |
917 | +from click.repository import ( |
918 | + get_repository, |
919 | + ClickLoginTwoFactorRequired, |
920 | + ClickRepositoryError, |
921 | +) |
922 | + |
923 | + |
924 | +def run(argv): |
925 | + parser = OptionParser(dedent("""\ |
926 | + %prog login user-name |
927 | + |
928 | + """)) |
929 | + options, args = parser.parse_args(argv) |
930 | + if len(args) < 1: |
931 | + parser.error("need package file name") |
932 | + repo = get_repository() |
933 | + username = args[0] |
934 | + print("Please enter the password for '%s'" % username) |
935 | + password = getpass.getpass() |
936 | + try: |
937 | + repo.login(username, password, store_on_disk=True) |
938 | + except ClickLoginTwoFactorRequired: |
939 | + otp = getpass.getpass("2fa: ") |
940 | + repo.login(username, password, otp, store_on_disk=True) |
941 | + except ClickRepositoryError as e: |
942 | + print("login failed %s" % e, file=sys.stderr) |
943 | + return 1 |
944 | + |
945 | + return 0 |
946 | |
947 | === added file 'click/commands/search.py' |
948 | --- click/commands/search.py 1970-01-01 00:00:00 +0000 |
949 | +++ click/commands/search.py 2014-12-03 23:19:34 +0000 |
950 | @@ -0,0 +1,46 @@ |
951 | +# Copyright (C) 2014 Canonical Ltd. |
952 | +# Author: Michael Vogt |
953 | + |
954 | +# This program is free software: you can redistribute it and/or modify |
955 | +# it under the terms of the GNU General Public License as published by |
956 | +# the Free Software Foundation; version 3 of the License. |
957 | +# |
958 | +# This program is distributed in the hope that it will be useful, |
959 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
960 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
961 | +# GNU General Public License for more details. |
962 | +# |
963 | +# You should have received a copy of the GNU General Public License |
964 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
965 | + |
966 | +"""Search for Click packages""" |
967 | + |
968 | +from __future__ import print_function |
969 | + |
970 | +from optparse import OptionParser |
971 | +import sys |
972 | +from textwrap import dedent |
973 | + |
974 | +from click.repository import ( |
975 | + get_repository, |
976 | + ClickRepositoryError, |
977 | +) |
978 | + |
979 | + |
980 | +def run(argv): |
981 | + parser = OptionParser(dedent("""\ |
982 | + %prog search search-term(s) |
983 | + |
984 | + """)) |
985 | + options, args = parser.parse_args(argv) |
986 | + if len(args) < 1: |
987 | + parser.error("need package file name") |
988 | + repo = get_repository() |
989 | + try: |
990 | + result = repo.search(args[0]) |
991 | + except ClickRepositoryError as e: |
992 | + print("Search failed %s" % e, file=sys.stderr) |
993 | + return 1 |
994 | + for app in result: |
995 | + print("%s - %s" % (app["name"], app["title"])) |
996 | + return 0 |
997 | |
998 | === added file 'click/commands/update.py' |
999 | --- click/commands/update.py 1970-01-01 00:00:00 +0000 |
1000 | +++ click/commands/update.py 2014-12-03 23:19:34 +0000 |
1001 | @@ -0,0 +1,108 @@ |
1002 | +# Copyright (C) 2014 Canonical Ltd. |
1003 | +# Author: Michael Vogt |
1004 | + |
1005 | +# This program is free software: you can redistribute it and/or modify |
1006 | +# it under the terms of the GNU General Public License as published by |
1007 | +# the Free Software Foundation; version 3 of the License. |
1008 | +# |
1009 | +# This program is distributed in the hope that it will be useful, |
1010 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1011 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1012 | +# GNU General Public License for more details. |
1013 | +# |
1014 | +# You should have received a copy of the GNU General Public License |
1015 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1016 | + |
1017 | +"""Update Click packages""" |
1018 | + |
1019 | +from __future__ import print_function |
1020 | + |
1021 | +from optparse import OptionParser |
1022 | +import sys |
1023 | +import tempfile |
1024 | +from textwrap import dedent |
1025 | + |
1026 | +from prettytable import PrettyTable, PLAIN_COLUMNS |
1027 | + |
1028 | +from gi.repository import Click |
1029 | +from click.acquire import ( |
1030 | + ClickAcquire, |
1031 | + ClickAcquireStatusText, |
1032 | +) |
1033 | +from click.install import ClickInstaller |
1034 | + |
1035 | +from click.repository import ( |
1036 | + get_repository, |
1037 | + ClickRepositoryError, |
1038 | +) |
1039 | + |
1040 | + |
1041 | +def run(argv): |
1042 | + parser = OptionParser(dedent("""\ |
1043 | + %prog update |
1044 | + |
1045 | + """)) |
1046 | + parser.add_option( |
1047 | + "--list", default=False, action="store_true", |
1048 | + help="Just list the update") |
1049 | + parser.add_option( |
1050 | + "--machine-readable", default=False, action="store_true", |
1051 | + help="output in machine readable form") |
1052 | + parser.add_option( |
1053 | + "--user", metavar="USER", help="register package for USER") |
1054 | + parser.add_option( |
1055 | + "--all-users", default=False, action="store_true", |
1056 | + help="register package for all users") |
1057 | + options, args = parser.parse_args(argv) |
1058 | + repo = get_repository() |
1059 | + try: |
1060 | + all_updates = repo.get_upgradable() |
1061 | + except ClickRepositoryError as e: |
1062 | + print("Update failed: %s" % e, file=sys.stderr) |
1063 | + return 1 |
1064 | + if not all_updates: |
1065 | + return 0 |
1066 | + |
1067 | + if len(args) > 0: |
1068 | + updates = [up for up in all_updates if up[0]["name"] in args] |
1069 | + else: |
1070 | + updates = all_updates |
1071 | + |
1072 | + if options.machine_readable: |
1073 | + for (current, new) in updates: |
1074 | + print("%s|%s|%s" % ( |
1075 | + new["name"], current["version"], new["version"])) |
1076 | + elif options.list: |
1077 | + t = PrettyTable(["Part","Installed", "Available"]) |
1078 | + t.set_style(PLAIN_COLUMNS) |
1079 | + t.align["Part"] = "l" |
1080 | + for (current, new) in updates: |
1081 | + t.add_row((new["name"], current["version"], new["version"])) |
1082 | + print(t) |
1083 | + else: |
1084 | + db = Click.DB() |
1085 | + db.read(db_dir=None) |
1086 | + installer = ClickInstaller(db=db) |
1087 | + for (current, new) in updates: |
1088 | + if repo.credentials: |
1089 | + package_uri = new["download_url"] |
1090 | + else: |
1091 | + package_uri = new["anon_download_url"] |
1092 | + print(package_uri) |
1093 | + # FIXME: duplication from commands/install.py |
1094 | + t = tempfile.NamedTemporaryFile() |
1095 | + package_path = t.name |
1096 | + log = ClickAcquireStatusText() |
1097 | + acq = ClickAcquire(log) |
1098 | + acq.fetch(package_uri, package_path) |
1099 | + installer.install( |
1100 | + package_path, user=options.user, all_users=options.all_users) |
1101 | + # output what just happend |
1102 | + t = PrettyTable(["Part","Installed"]) |
1103 | + t.set_style(PLAIN_COLUMNS) |
1104 | + t.align["Part"] = "l" |
1105 | + for (current, new) in updates: |
1106 | + t.add_row((new["name"], new["version"])) |
1107 | + print(t) |
1108 | + |
1109 | + return 0 |
1110 | |
1111 | === modified file 'click/install.py' |
1112 | --- click/install.py 2014-09-10 11:50:18 +0000 |
1113 | +++ click/install.py 2014-12-03 23:19:34 +0000 |
1114 | @@ -16,6 +16,7 @@ |
1115 | """Installing Click packages.""" |
1116 | |
1117 | from __future__ import print_function |
1118 | +from hashlib import sha512 |
1119 | |
1120 | __metaclass__ = type |
1121 | __all__ = [ |
1122 | @@ -76,6 +77,12 @@ |
1123 | apt_pkg.init_system() |
1124 | |
1125 | |
1126 | +def _dpkg_architecture(): |
1127 | + return subprocess.check_output( |
1128 | + ["dpkg", "--print-architecture"], |
1129 | + universal_newlines=True).rstrip("\n") |
1130 | + |
1131 | + |
1132 | class DebsigVerifyError(Exception): |
1133 | pass |
1134 | |
1135 | @@ -145,11 +152,6 @@ |
1136 | return os.path.abspath(preload) |
1137 | return preload_path |
1138 | |
1139 | - def _dpkg_architecture(self): |
1140 | - return subprocess.check_output( |
1141 | - ["dpkg", "--print-architecture"], |
1142 | - universal_newlines=True).rstrip("\n") |
1143 | - |
1144 | def extract(self, path, target): |
1145 | command = ["dpkg-deb", "-R", path, target] |
1146 | with open(path, "rb") as fd: |
1147 | @@ -263,7 +265,7 @@ |
1148 | if check_arch: |
1149 | architecture = manifest.get("architecture", "all") |
1150 | if architecture != "all": |
1151 | - dpkg_architecture = self._dpkg_architecture() |
1152 | + dpkg_architecture = _dpkg_architecture() |
1153 | if isinstance(architecture, list): |
1154 | if dpkg_architecture not in architecture: |
1155 | raise ClickInstallerAuditError( |
1156 | @@ -346,8 +348,27 @@ |
1157 | os.mkdir(os.path.join(admin_dir, "info")) |
1158 | os.mkdir(os.path.join(admin_dir, "updates")) |
1159 | os.mkdir(os.path.join(admin_dir, "triggers")) |
1160 | - |
1161 | - def _unpack(self, path, user=None, all_users=False): |
1162 | + def _add_checksum_to_manifest(self, path, inst_dir, package_name): |
1163 | + with open(path, "rb") as fd: |
1164 | + checksum = sha512(fd.read()).hexdigest() |
1165 | + |
1166 | + admin_dir = os.path.join(inst_dir, ".click") |
1167 | + manifest_file = '{}.manifest'.format(package_name) |
1168 | + manifest_path = os.path.join(admin_dir, 'info', manifest_file) |
1169 | + |
1170 | + with open(manifest_path) as fd: |
1171 | + manifest = json.load(fd) |
1172 | + manifest['checksum-sha512'] = checksum |
1173 | + |
1174 | + tmp_file = '{}.tmp'.format(manifest_path) |
1175 | + with open(tmp_file, 'w') as manifest_fd: |
1176 | + # indent is for pretty-printing. |
1177 | + json.dump(manifest, fp=manifest_fd, indent=8) |
1178 | + |
1179 | + # overwrite |
1180 | + os.rename(tmp_file, manifest_path) |
1181 | + |
1182 | + def _unpack(self, path, user=None, all_users=False, quiet=True): |
1183 | package_name, package_version = self.audit(path, check_arch=True) |
1184 | |
1185 | # Is this package already unpacked in an underlay (non-topmost) |
1186 | @@ -401,9 +422,24 @@ |
1187 | kwargs = {} |
1188 | if sys.version >= "3.2": |
1189 | kwargs["pass_fds"] = (fd.fileno(),) |
1190 | - subprocess.check_call( |
1191 | - command, preexec_fn=partial(self._install_preexec, inst_dir), |
1192 | - env=env, **kwargs) |
1193 | + if quiet: |
1194 | + fn = subprocess.check_output |
1195 | + kwargs["stderr"] = subprocess.STDOUT |
1196 | + else: |
1197 | + fn = subprocess.check_call |
1198 | + try: |
1199 | + fn(command, |
1200 | + preexec_fn=partial(self._install_preexec, inst_dir), |
1201 | + env=env, universal_newlines=True, |
1202 | + **kwargs) |
1203 | + except subprocess.CalledProcessError as e: |
1204 | + logging.error("%s failed with exit_code %s:\n%s" % ( |
1205 | + command, e.returncode, e.output)) |
1206 | + raise |
1207 | + # Now that the package has been installed, add the .click's |
1208 | + # checksum to the manifest. |
1209 | + self._add_checksum_to_manifest(path, inst_dir, package_name) |
1210 | + # ensure the permissions |
1211 | for dirpath, dirnames, filenames in os.walk(inst_dir): |
1212 | for entry in dirnames + filenames: |
1213 | entry_path = os.path.join(dirpath, entry) |
1214 | @@ -441,9 +477,9 @@ |
1215 | |
1216 | return package_name, package_version, old_version |
1217 | |
1218 | - def install(self, path, user=None, all_users=False): |
1219 | + def install(self, path, user=None, all_users=False, quiet=True): |
1220 | package_name, package_version, old_version = self._unpack( |
1221 | - path, user=user, all_users=all_users) |
1222 | + path, user=user, all_users=all_users, quiet=quiet) |
1223 | |
1224 | if user is not None or all_users: |
1225 | if all_users: |
1226 | |
1227 | === added file 'click/network.py' |
1228 | --- click/network.py 1970-01-01 00:00:00 +0000 |
1229 | +++ click/network.py 2014-12-03 23:19:34 +0000 |
1230 | @@ -0,0 +1,72 @@ |
1231 | +# Copyright (C) 2014 Canonical Ltd. |
1232 | +# Author: Michael Vogt <mvo@ubuntu.com> |
1233 | + |
1234 | +# This program is free software: you can redistribute it and/or modify |
1235 | +# it under the terms of the GNU General Public License as published by |
1236 | +# the Free Software Foundation; version 3 of the License. |
1237 | +# |
1238 | +# This program is distributed in the hope that it will be useful, |
1239 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1240 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1241 | +# GNU General Public License for more details. |
1242 | +# |
1243 | +# You should have received a copy of the GNU General Public License |
1244 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1245 | + |
1246 | +import pycurl |
1247 | + |
1248 | +from io import BytesIO, StringIO |
1249 | +import os |
1250 | + |
1251 | + |
1252 | +class HttpResponse: |
1253 | + """Http Response with status and data members""" |
1254 | + |
1255 | + def __init__(self, status, data): |
1256 | + self.status = status |
1257 | + self.data = data |
1258 | + |
1259 | + def __unicode__(self): |
1260 | + return self.data |
1261 | + |
1262 | + |
1263 | +# similar to httplib2.Http().request() |
1264 | +def http_request(url, data=None, headers=None): |
1265 | + """Send a http request to the given url and return (response, data) |
1266 | + |
1267 | + param url The url to send the request to |
1268 | + param data (Optional) data to POST to the url |
1269 | + param headers (Optional) headers to include with the request |
1270 | + return A tuple of (response, body) |
1271 | + rtype (HttpResponse, bytes) |
1272 | + """ |
1273 | + def write_header(line): |
1274 | + header.write(line.decode("utf-8")) |
1275 | + return len(line) |
1276 | + def write_body(data): |
1277 | + body.write(data) |
1278 | + return len(data) |
1279 | + def post_data(num): |
1280 | + return data_stream.read(num) |
1281 | + header = StringIO() |
1282 | + body = BytesIO() |
1283 | + curl = pycurl.Curl() |
1284 | + curl.setopt(curl.URL, url) |
1285 | + if os.environ.get("CLICK_DEBUG_HTTP", ""): |
1286 | + curl.setopt(curl.VERBOSE, 1) |
1287 | + curl.setopt(pycurl.WRITEFUNCTION, write_body) |
1288 | + curl.setopt(pycurl.HEADERFUNCTION, write_header) |
1289 | + if data is not None: |
1290 | + data_stream = BytesIO(data) |
1291 | + curl.setopt(pycurl.READFUNCTION, post_data) |
1292 | + curl.setopt(pycurl.POSTFIELDSIZE, len(data)) |
1293 | + curl.setopt(pycurl.POST, 1) |
1294 | + if headers is not None: |
1295 | + header_list = [] |
1296 | + for k, v in headers.items(): |
1297 | + header_list.append("%s: %s" % (k, v)) |
1298 | + curl.setopt(pycurl.HTTPHEADER, header_list) |
1299 | + # ssl: no need to set extra options, pycurl has sensible defaults |
1300 | + curl.perform() |
1301 | + resp = HttpResponse(curl.getinfo(pycurl.HTTP_CODE), header.getvalue()) |
1302 | + return resp, body.getvalue() |
1303 | |
1304 | === added file 'click/oauth.py' |
1305 | --- click/oauth.py 1970-01-01 00:00:00 +0000 |
1306 | +++ click/oauth.py 2014-12-03 23:19:34 +0000 |
1307 | @@ -0,0 +1,29 @@ |
1308 | +# Copyright (C) 2014 Canonical Ltd. |
1309 | +# Author: Michael Vogt <mvo@ubuntu.com> |
1310 | + |
1311 | +# This program is free software: you can redistribute it and/or modify |
1312 | +# it under the terms of the GNU General Public License as published by |
1313 | +# the Free Software Foundation; version 3 of the License. |
1314 | +# |
1315 | +# This program is distributed in the hope that it will be useful, |
1316 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1317 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1318 | +# GNU General Public License for more details. |
1319 | +# |
1320 | +# You should have received a copy of the GNU General Public License |
1321 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1322 | + |
1323 | +import oauthlib.oauth1 |
1324 | + |
1325 | + |
1326 | +def get_oauth_headers_for_uri(uri, token): |
1327 | + client = oauthlib.oauth1.Client(token["consumer_key"], |
1328 | + token["consumer_secret"], |
1329 | + token["token_key"], |
1330 | + token["token_secret"]) |
1331 | + new_uri, headers, body = client.sign(uri) |
1332 | + if new_uri != uri: |
1333 | + raise Exception("uri change not supported") |
1334 | + if body is not None: |
1335 | + raise Exception("body != None not supported") |
1336 | + return headers |
1337 | |
1338 | === modified file 'click/paths.py.in' |
1339 | --- click/paths.py.in 2014-05-08 15:48:01 +0000 |
1340 | +++ click/paths.py.in 2014-12-03 23:19:34 +0000 |
1341 | @@ -14,6 +14,9 @@ |
1342 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
1343 | |
1344 | """Click paths.""" |
1345 | +import os |
1346 | |
1347 | preload_path = "@pkglibdir@/libclickpreload.so" |
1348 | frameworks_dir = "@pkgdatadir@/frameworks" |
1349 | +acquire_methods_dir = (os.environ.get("CLICK_ACQUIRE_METHODS_DIR", "") or |
1350 | + "@pkglibdir@/acquire") |
1351 | |
1352 | === added file 'click/repository.py' |
1353 | --- click/repository.py 1970-01-01 00:00:00 +0000 |
1354 | +++ click/repository.py 2014-12-03 23:19:34 +0000 |
1355 | @@ -0,0 +1,224 @@ |
1356 | +# Copyright (C) 2014 Canonical Ltd. |
1357 | +# Author: Michael Vogt |
1358 | + |
1359 | +# This program is free software: you can redistribute it and/or modify |
1360 | +# it under the terms of the GNU General Public License as published by |
1361 | +# the Free Software Foundation; version 3 of the License. |
1362 | +# |
1363 | +# This program is distributed in the hope that it will be useful, |
1364 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1365 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1366 | +# GNU General Public License for more details. |
1367 | +# |
1368 | +# You should have received a copy of the GNU General Public License |
1369 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1370 | + |
1371 | +"""Search Click packages.""" |
1372 | + |
1373 | +from __future__ import print_function |
1374 | + |
1375 | +__metaclass__ = type |
1376 | +__all__ = [ |
1377 | + 'ClickRepository', |
1378 | + 'ClickRepositoryError', |
1379 | + ] |
1380 | + |
1381 | +import json |
1382 | +import platform |
1383 | +import os |
1384 | + |
1385 | +import apt_pkg |
1386 | +from gi.repository import Click |
1387 | +from six.moves.urllib.parse import quote as url_quote |
1388 | + |
1389 | +from click.json_helpers import json_array_to_python |
1390 | + |
1391 | +import click.install |
1392 | +from click.network import http_request |
1393 | +from click.oauth import get_oauth_headers_for_uri |
1394 | + |
1395 | + |
1396 | +class ClickLoginTwoFactorRequired(Exception): |
1397 | + pass |
1398 | + |
1399 | + |
1400 | +class ClickRepositoryError(Exception): |
1401 | + pass |
1402 | + |
1403 | + |
1404 | +def get_repository(): |
1405 | + """Factory to get a click repository for the current distro""" |
1406 | + distro = platform.dist()[0] |
1407 | + if distro.startswith('Ubuntu'): |
1408 | + return ClickRepositoryAppsUbuntuCom() |
1409 | + raise ClickRepositoryError("No repository for distribution '%s'" % distro) |
1410 | + |
1411 | + |
1412 | +class ClickRepository: |
1413 | + |
1414 | + def search(self, search_string): |
1415 | + """Search the repository for the given search string |
1416 | + |
1417 | + Returns a list of click manifest from the repository |
1418 | + """ |
1419 | + raise NotImplementedError() |
1420 | + |
1421 | + def get_upgradable(self): |
1422 | + """Get all click packages where a newer version is available |
1423 | + |
1424 | + Returns a list of (old_manifest, new_manifest) pairs |
1425 | + """ |
1426 | + raise NotImplementedError() |
1427 | + |
1428 | + def details(self, name): |
1429 | + """Get the details for the given click name |
1430 | + |
1431 | + Returns a click manifest |
1432 | + """ |
1433 | + raise NotImplementedError() |
1434 | + |
1435 | + def login(self, username, password, otp=None, store_on_disk=True): |
1436 | + """Log into the given repository with username/password""" |
1437 | + raise NotImplementedError() |
1438 | + |
1439 | + @property |
1440 | + def description(self): |
1441 | + """The description of the repository as a URL""" |
1442 | + raise NotImplementedError() |
1443 | + |
1444 | + @property |
1445 | + def credentials(self): |
1446 | + """The credentials for the repository (or None)""" |
1447 | + return None |
1448 | + |
1449 | + |
1450 | +class ClickRepositoryAppsUbuntuCom(ClickRepository): |
1451 | + """Interface to click repository from apps.ubuntu.com""" |
1452 | + |
1453 | + SEARCH_URI = "https://search.apps.ubuntu.com/api/v1/search?q=%s" |
1454 | + DETAILS_URI = "https://search.apps.ubuntu.com/api/v1/package/%s" |
1455 | + BULK_APPS_URI = "https://myapps.developer.ubuntu.com/dev/api/"\ |
1456 | + "click-metadata/" |
1457 | + |
1458 | + TOKEN_FILE = os.path.expanduser("~/.config/click/ubuntu-sso.json") |
1459 | + |
1460 | + def _http_request(self, uri, *args, **kwargs): |
1461 | + """Send a oauth signed http request to the repo server""" |
1462 | + if self.credentials: |
1463 | + token = self.credentials |
1464 | + headers = kwargs.pop("headers", {}) |
1465 | + headers.update(get_oauth_headers_for_uri(uri, token)) |
1466 | + kwargs["headers"] = headers |
1467 | + return http_request(uri, *args, **kwargs) |
1468 | + |
1469 | + @property |
1470 | + def credentials(self): |
1471 | + if os.path.exists(self.TOKEN_FILE): |
1472 | + with open(self.TOKEN_FILE) as f: |
1473 | + return json.load(f) |
1474 | + return None |
1475 | + |
1476 | + @property |
1477 | + def description(self): |
1478 | + return "https://myapps.developer.ubuntu.com/dev/click-apps/" |
1479 | + |
1480 | + def search(self, search_term): |
1481 | + result = [] |
1482 | + url = self.SEARCH_URI % url_quote(search_term) |
1483 | + if not self.credentials: |
1484 | + url += ",allow_unauthenticated:True" |
1485 | + frameworks = ["%s-%s" % (f.get_base_name(), f.get_base_version()) |
1486 | + for f in Click.Framework.get_frameworks()] |
1487 | + headers = { |
1488 | + "Accept": "application/hal+json", |
1489 | + "X-Ubuntu-Frameworks": ",".join(frameworks), |
1490 | + "X-Ubuntu-Architecture": click.install._dpkg_architecture(), |
1491 | + } |
1492 | + resp, raw_content = http_request(url, headers=headers) |
1493 | + content = raw_content.decode("utf-8") |
1494 | + if resp.status != 200: |
1495 | + raise ClickRepositoryError(content) |
1496 | + data = json.loads(content) |
1497 | + if not "_embedded" in data: |
1498 | + return result |
1499 | + for item in data["_embedded"]["clickindex:package"]: |
1500 | + result.append(item) |
1501 | + return result |
1502 | + |
1503 | + def details(self, name): |
1504 | + resp, raw_content = self._http_request( |
1505 | + self.DETAILS_URI % url_quote(name)) |
1506 | + content = raw_content.decode("utf-8") |
1507 | + if resp.status != 200: |
1508 | + raise ClickRepositoryError(content) |
1509 | + return json.loads(content) |
1510 | + |
1511 | + def get_upgradable(self): |
1512 | + result = [] |
1513 | + db = Click.DB() |
1514 | + db.read(db_dir=None) |
1515 | + registry = Click.User.for_user(db, name=None) |
1516 | + # build the dict of installed apps for the POST |
1517 | + current_apps = {} |
1518 | + for app in json_array_to_python(registry.get_manifests()): |
1519 | + current_apps[app["name"]] = app |
1520 | + # if we have nothing installed, do not query the server |
1521 | + if not current_apps: |
1522 | + return result |
1523 | + json_post_data = json.dumps({ |
1524 | + "name": list(current_apps.keys()), |
1525 | + }).encode("utf-8") |
1526 | + # query the API |
1527 | + resp, raw_content = http_request( |
1528 | + self.BULK_APPS_URI, |
1529 | + json_post_data, |
1530 | + headers={"content-type": "application/json"}) |
1531 | + content = raw_content.decode("utf-8") |
1532 | + if resp.status != 200: |
1533 | + raise ClickRepositoryError( |
1534 | + "Failed to get upgradable packages: %s" % resp.content) |
1535 | + # return a list of (old, new) pairs to the caller |
1536 | + server_apps = json.loads(content) |
1537 | + result = [] |
1538 | + for new in server_apps: |
1539 | + current = current_apps[new["name"]] |
1540 | + if apt_pkg.version_compare(current["version"], new["version"]) < 0: |
1541 | + result.append( (current, new) ) |
1542 | + return result |
1543 | + |
1544 | + def login(self, username, password, otp=None, store_on_disk=True): |
1545 | + token_name = "click" |
1546 | + token = self._get_oauth_token(username, password, token_name, otp) |
1547 | + |
1548 | + if store_on_disk: |
1549 | + Click.ensuredir(os.path.dirname(self.TOKEN_FILE)) |
1550 | + with open(self.TOKEN_FILE, "w") as f: |
1551 | + os.chmod(f.name, 0o600) |
1552 | + f.write(json.dumps(token)) |
1553 | + |
1554 | + return token |
1555 | + |
1556 | + def _get_oauth_token(self, username, password, token_name, otp=None): |
1557 | + UBUNTUONE_API_BASE = "https://login.ubuntu.com/api/v2" |
1558 | + UBUNTUONE_OAUTH_API = UBUNTUONE_API_BASE + "/tokens/oauth" |
1559 | + |
1560 | + data = {"email": username, |
1561 | + "password": password, |
1562 | + "token_name": token_name, |
1563 | + } |
1564 | + if otp: |
1565 | + data["otp"] = otp |
1566 | + data = json.dumps(data).encode("utf-8") |
1567 | + resp, content = http_request( |
1568 | + UBUNTUONE_OAUTH_API, |
1569 | + data=data, |
1570 | + headers={"content-type": "application/json"}) |
1571 | + msg = json.loads(content.decode("utf-8")) |
1572 | + if resp.status not in (200, 201): |
1573 | + if msg["code"] == "TWOFACTOR_REQUIRED": |
1574 | + raise ClickLoginTwoFactorRequired() |
1575 | + else: |
1576 | + raise Exception( |
1577 | + "get_oauth_token failed with status %s" % resp.status) |
1578 | + token = msg |
1579 | + return token |
1580 | |
1581 | === added file 'click/tests/test_acquire.py' |
1582 | --- click/tests/test_acquire.py 1970-01-01 00:00:00 +0000 |
1583 | +++ click/tests/test_acquire.py 2014-12-03 23:19:34 +0000 |
1584 | @@ -0,0 +1,174 @@ |
1585 | +# Copyright (C) 2014 Canonical Ltd. |
1586 | +# Author: Michael Vogt <michael.vogt@ubuntu.com> |
1587 | + |
1588 | +# This program is free software: you can redistribute it and/or modify |
1589 | +# it under the terms of the GNU General Public License as published by |
1590 | +# the Free Software Foundation; version 3 of the License. |
1591 | +# |
1592 | +# This program is distributed in the hope that it will be useful, |
1593 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1594 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1595 | +# GNU General Public License for more details. |
1596 | +# |
1597 | +# You should have received a copy of the GNU General Public License |
1598 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1599 | + |
1600 | +"""Unit tests for click.acquire.""" |
1601 | + |
1602 | +from __future__ import print_function |
1603 | + |
1604 | +__metaclass__ = type |
1605 | +__all__ = [ |
1606 | + 'TestClickAcquire', |
1607 | + ] |
1608 | + |
1609 | +import multiprocessing |
1610 | +import os.path |
1611 | +import time |
1612 | + |
1613 | +from six.moves import ( |
1614 | + SimpleHTTPServer, |
1615 | + socketserver, |
1616 | +) |
1617 | + |
1618 | +from click.acquire import ( |
1619 | + ClickAcquire, |
1620 | + ClickAcquireError, |
1621 | + _read_messages, |
1622 | +) |
1623 | + |
1624 | +from click.tests.helpers import ( |
1625 | + TestCase, |
1626 | +) |
1627 | + |
1628 | +import click.acquire |
1629 | +click.acquire.acquire_methods_dir = os.path.join( |
1630 | + os.path.dirname(__file__), "..", "..", "acquire") |
1631 | +os.environ["PYTHONPATH"] = os.path.abspath( |
1632 | + os.path.join(os.path.dirname(__file__), "..", "..")) |
1633 | + |
1634 | +# local httpd |
1635 | +LOCALHOST = "localhost" |
1636 | +PORT = 8128 |
1637 | + |
1638 | + |
1639 | +class MyHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): |
1640 | + def log_message(self, format, *args): |
1641 | + pass |
1642 | + |
1643 | + |
1644 | +class MySocketServer(socketserver.TCPServer): |
1645 | + allow_reuse_address = True |
1646 | + |
1647 | + |
1648 | +class Httpd(multiprocessing.Process): |
1649 | + |
1650 | + def __init__(self, basedir): |
1651 | + super(Httpd, self).__init__() |
1652 | + self.basedir = basedir |
1653 | + |
1654 | + def run(self): |
1655 | + os.chdir(self.basedir) |
1656 | + server = MySocketServer((LOCALHOST, PORT), MyHandler) |
1657 | + while True: |
1658 | + server.handle_request() |
1659 | + server.shutdown() |
1660 | + |
1661 | + def stop(self): |
1662 | + self.terminate() |
1663 | + self.join() |
1664 | + |
1665 | + |
1666 | +class TestClickAcquire(TestCase): |
1667 | + |
1668 | + def setUp(self): |
1669 | + super(TestClickAcquire, self).setUp() |
1670 | + self.use_temp_dir() |
1671 | + self.httpd = Httpd(self.temp_dir) |
1672 | + self.httpd.start() |
1673 | + |
1674 | + def tearDown(self): |
1675 | + self.httpd.stop() |
1676 | + |
1677 | + def test_acquire_fail(self): |
1678 | + acq = ClickAcquire() |
1679 | + destfile = os.path.join(self.temp_dir, "meep") |
1680 | + with self.assertRaises(ClickAcquireError): |
1681 | + acq.fetch("http://%s:%s/not-here" % (LOCALHOST, PORT), destfile) |
1682 | + |
1683 | + def test_acquire_good(self): |
1684 | + acq = ClickAcquire() |
1685 | + destfile = os.path.join(self.temp_dir, "meep") |
1686 | + canary_str = "hello" |
1687 | + with open(os.path.join(self.temp_dir, "i-am-here"), "w") as f: |
1688 | + f.write(canary_str) |
1689 | + acq.fetch("http://%s:%s/i-am-here" % (LOCALHOST, PORT), destfile) |
1690 | + with open(destfile) as f: |
1691 | + data = f.read() |
1692 | + self.assertEqual(canary_str, data) |
1693 | + |
1694 | + |
1695 | +class TestClickAcquireReadMessages(TestCase): |
1696 | + |
1697 | + def test_forked_read_message(self): |
1698 | + read_end, write_end = os.pipe() |
1699 | + pid = os.fork() |
1700 | + if pid == 0: |
1701 | + os.close(write_end) |
1702 | + with os.fdopen(read_end) as f: |
1703 | + number, msg = _read_messages(f)[0] |
1704 | + self.assertEqual(msg, "102 Status\nFoo: Bar\n") |
1705 | + self.assertEqual(number, 102) |
1706 | + os._exit(0) |
1707 | + os.close(read_end) |
1708 | + os.write(write_end, "102 Status\nFoo: Bar\n\n".encode("utf-8")) |
1709 | + os.waitpid(pid, 0) |
1710 | + os.close(write_end) |
1711 | + |
1712 | + def test_forked_read_multiple_messages(self): |
1713 | + read_end, write_end = os.pipe() |
1714 | + pid = os.fork() |
1715 | + if pid == 0: |
1716 | + os.close(write_end) |
1717 | + with os.fdopen(read_end) as f: |
1718 | + msgs = _read_messages(f) |
1719 | + self.assertEqual(len(msgs), 2) |
1720 | + self.assertEqual(msgs[0][0], 100) |
1721 | + self.assertEqual(msgs[1][0], 200) |
1722 | + os._exit(0) |
1723 | + os.close(read_end) |
1724 | + os.write(write_end, "100 Status\n\n200 meep\n\n".encode("utf-8")) |
1725 | + os.waitpid(pid, 0) |
1726 | + os.close(write_end) |
1727 | + |
1728 | + def test_forked_read_message_broken_pipe(self): |
1729 | + read_end, write_end = os.pipe() |
1730 | + pid = os.fork() |
1731 | + if pid == 0: |
1732 | + os.close(write_end) |
1733 | + with os.fdopen(read_end) as f: |
1734 | + self.assertEqual(_read_messages(f), []) |
1735 | + os._exit(0) |
1736 | + os.close(read_end) |
1737 | + os.write(write_end, "102 close-before-msg".encode("utf-8")) |
1738 | + os.close(write_end) |
1739 | + os.waitpid(pid, 0) |
1740 | + |
1741 | + def test_forked_read_message_with_timeout(self): |
1742 | + read_end, write_end = os.pipe() |
1743 | + pid = os.fork() |
1744 | + if pid == 0: |
1745 | + os.close(write_end) |
1746 | + with os.fdopen(read_end) as f: |
1747 | + self.assertEqual(_read_messages(f, 0.1), []) |
1748 | + msgs = _read_messages(f) |
1749 | + self.assertEqual(len(msgs), 2) |
1750 | + self.assertEqual(msgs[0][1], "100 aaa\n") |
1751 | + self.assertEqual(msgs[1][1], "200 bbb\n") |
1752 | + os._exit(0) |
1753 | + os.close(read_end) |
1754 | + # simulate slow method |
1755 | + time.sleep(0.2) |
1756 | + os.write(write_end, "100 aaa\n\n".encode("utf-8")) |
1757 | + os.write(write_end, "200 bbb\n\n".encode("utf-8")) |
1758 | + os.waitpid(pid, 0) |
1759 | |
1760 | === modified file 'click/tests/test_build.py' |
1761 | --- click/tests/test_build.py 2014-09-04 13:41:30 +0000 |
1762 | +++ click/tests/test_build.py 2014-12-03 23:19:34 +0000 |
1763 | @@ -56,6 +56,12 @@ |
1764 | return decorator |
1765 | |
1766 | |
1767 | +def extract_field(path, name): |
1768 | + return subprocess.check_output( |
1769 | + ["dpkg-deb", "-f", path, name], |
1770 | + universal_newlines=True).rstrip("\n") |
1771 | + |
1772 | + |
1773 | class TestClickBuilderBaseMixin: |
1774 | def test_read_manifest(self): |
1775 | self.use_temp_dir() |
1776 | @@ -118,11 +124,6 @@ |
1777 | super(TestClickBuilder, self).setUp() |
1778 | self.builder = ClickBuilder() |
1779 | |
1780 | - def extract_field(self, path, name): |
1781 | - return subprocess.check_output( |
1782 | - ["dpkg-deb", "-f", path, name], |
1783 | - universal_newlines=True).rstrip("\n") |
1784 | - |
1785 | @disable_logging |
1786 | @umask(0o22) |
1787 | def test_build(self): |
1788 | @@ -157,9 +158,9 @@ |
1789 | ("Maintainer", "Foo Bar <foo@example.org>"), |
1790 | ("Description", "test title"), |
1791 | ): |
1792 | - self.assertEqual(value, self.extract_field(path, key)) |
1793 | + self.assertEqual(value, extract_field(path, key)) |
1794 | self.assertNotEqual( |
1795 | - "", self.extract_field(path, "Installed-Size")) |
1796 | + "", extract_field(path, "Installed-Size")) |
1797 | control_path = os.path.join(self.temp_dir, "control") |
1798 | subprocess.check_call(["dpkg-deb", "-e", path, control_path]) |
1799 | manifest_path = os.path.join(control_path, "manifest") |
1800 | @@ -242,7 +243,7 @@ |
1801 | path = os.path.join(self.temp_dir, "com.example.test_1.0_multi.click") |
1802 | self.assertEqual(path, self.builder.build(self.temp_dir)) |
1803 | self.assertTrue(os.path.exists(path)) |
1804 | - self.assertEqual("multi", self.extract_field(path, "Architecture")) |
1805 | + self.assertEqual("multi", extract_field(path, "Architecture")) |
1806 | control_path = os.path.join(self.temp_dir, "control") |
1807 | subprocess.check_call(["dpkg-deb", "-e", path, control_path]) |
1808 | manifest_path = os.path.join(control_path, "manifest") |
1809 | @@ -339,3 +340,286 @@ |
1810 | self.assertCountEqual( |
1811 | [".", "./bin", "./bin/foo", "./manifest.json"], tar.getnames()) |
1812 | self.assertTrue(tar.getmember("./bin/foo").isfile()) |
1813 | + |
1814 | + |
1815 | +SIMPLE_TEMPLATE = """\ |
1816 | +name: test-yaml |
1817 | +icon: data/icon.svg |
1818 | +version: 1.0 |
1819 | +maintainer: Foo Bar <foo@example.org> |
1820 | +architecture: all |
1821 | +framework: ubuntu-core-15.04 |
1822 | +integration: |
1823 | + app1: |
1824 | + hookname: hook-file-name |
1825 | +services: |
1826 | + - name: app1 |
1827 | + description: some description |
1828 | + start: start-something |
1829 | + stop: stop-something |
1830 | +binaries: |
1831 | + - name: path/to/cli1 |
1832 | +""" |
1833 | + |
1834 | +APPARMOR_1_TEMPLATE = """\ |
1835 | +name: test-yaml |
1836 | +icon: data/icon.svg |
1837 | +version: 1.0 |
1838 | +maintainer: Foo Bar <foo@example.org> |
1839 | +architecture: all |
1840 | +framework: ubuntu-core-15.04 |
1841 | +integration: |
1842 | + app1: |
1843 | + hookname: hook-file-name |
1844 | + apparmor: path/to/app1.apparmor |
1845 | +services: |
1846 | + - name: app1 |
1847 | + description: some description |
1848 | + start: start-something |
1849 | + stop: stop-something |
1850 | + - name: app2 |
1851 | + description: some description |
1852 | + start: start-something |
1853 | + stop: stop-something |
1854 | +binaries: |
1855 | + - name: path/to/cli1 |
1856 | +""" |
1857 | + |
1858 | +APPARMOR_2_TEMPLATE = """\ |
1859 | +name: test-yaml |
1860 | +icon: data/icon.svg |
1861 | +version: 1.0 |
1862 | +maintainer: Foo Bar <foo@example.org> |
1863 | +architecture: all |
1864 | +framework: ubuntu-core-15.04 |
1865 | +integration: |
1866 | + app1: |
1867 | + hookname: hook-file-name |
1868 | + apparmor-profile: path/to/app1.apparmor-profile |
1869 | +services: |
1870 | + - name: app1 |
1871 | + description: some description |
1872 | + start: start-something |
1873 | + stop: stop-something |
1874 | + - name: app2 |
1875 | + description: some description |
1876 | + start: start-something |
1877 | + stop: stop-something |
1878 | +binaries: |
1879 | + - name: path/to/cli1 |
1880 | +""" |
1881 | + |
1882 | + |
1883 | +class TestClickBuildYaml(TestCase): |
1884 | + def setUp(self): |
1885 | + super(TestClickBuildYaml, self).setUp() |
1886 | + self.builder = ClickBuilder() |
1887 | + |
1888 | + def _make_metadata(self, scratch, template=SIMPLE_TEMPLATE): |
1889 | + with mkfile(os.path.join(scratch, "meta", "package.yaml")) as f: |
1890 | + f.write(dedent(template)) |
1891 | + with mkfile(os.path.join(scratch, "meta", "readme.md")) as f: |
1892 | + f.write(dedent("""test title |
1893 | + |
1894 | + Long description that spawns |
1895 | + two lines. |
1896 | + |
1897 | + Some more random data. |
1898 | + """)) |
1899 | + |
1900 | + def test_build_valid_pkg(self): |
1901 | + self.use_temp_dir() |
1902 | + scratch = os.path.join(self.temp_dir, "scratch") |
1903 | + touch(os.path.join(scratch, "bin", "foo")) |
1904 | + touch(os.path.join(scratch, "data", "icon.svg")) |
1905 | + self._make_metadata(scratch) |
1906 | + self.builder.add_file(scratch, "./") |
1907 | + # FIXME: make it a .snap |
1908 | + expected_path = os.path.join(self.temp_dir, "test-yaml_1.0_all.click") |
1909 | + actual_path = self.builder.build(self.temp_dir, "meta/package.yaml") |
1910 | + self.assertEqual(expected_path, actual_path) |
1911 | + self.assertTrue(os.path.exists(actual_path)) |
1912 | + # check the fields |
1913 | + for key, value in ( |
1914 | + ("Package", "test-yaml"), |
1915 | + ("Version", "1.0"), |
1916 | + ("Click-Version", "0.4"), |
1917 | + ("Architecture", "all"), |
1918 | + ("Maintainer", "Foo Bar <foo@example.org>"), |
1919 | + ("Description", "test title"), |
1920 | + ): |
1921 | + self.assertEqual(value, extract_field(actual_path, key)) |
1922 | + click_contents = subprocess.check_output( |
1923 | + ["dpkg-deb", "-c", actual_path], universal_newlines=True) |
1924 | + for file_path in ("./data/icon.svg", "./meta/package.yaml", |
1925 | + "./meta/readme.md", "./bin/foo", |
1926 | + "./meta/app1.apparmor"): |
1927 | + self.assertIn(file_path, click_contents) |
1928 | + |
1929 | + def test_read_manifest(self): |
1930 | + self.use_temp_dir() |
1931 | + self._make_metadata(self.temp_dir) |
1932 | + self.builder.read_manifest(self.temp_dir+"/meta/package.yaml") |
1933 | + self.assertEqual("test-yaml", self.builder.name) |
1934 | + self.assertEqual("1.0", self.builder.version) |
1935 | + self.assertEqual("Foo Bar <foo@example.org>", self.builder.maintainer) |
1936 | + self.assertEqual("test title", self.builder.title) |
1937 | + self.assertEqual("all", self.builder.architecture) |
1938 | + self.assertEqual( |
1939 | + "meta/app1.snappy-systemd", |
1940 | + self.builder.manifest["hooks"]["app1"]["snappy-systemd"]) |
1941 | + self.assertEqual( |
1942 | + "hook-file-name", |
1943 | + self.builder.manifest["hooks"]["app1"]["hookname"]) |
1944 | + self.assertEqual( |
1945 | + "path/to/cli1", |
1946 | + self.builder.manifest["hooks"]["cli1"]["bin-path"]) |
1947 | + self.assertEqual( |
1948 | + "meta/app1.apparmor", |
1949 | + self.builder.manifest["hooks"]["app1"]["apparmor"]) |
1950 | + |
1951 | + def test_one_explicit_apparmor(self): |
1952 | + self.use_temp_dir() |
1953 | + self._make_metadata(self.temp_dir, APPARMOR_1_TEMPLATE) |
1954 | + self.builder.read_manifest(self.temp_dir + "/meta/package.yaml") |
1955 | + self.assertEqual( |
1956 | + "path/to/app1.apparmor", |
1957 | + self.builder.manifest["hooks"]["app1"]["apparmor"]) |
1958 | + self.assertEqual( |
1959 | + "meta/app2.apparmor", |
1960 | + self.builder.manifest["hooks"]["app2"]["apparmor"]) |
1961 | + |
1962 | + def test_one_explicit_apparmor_profile(self): |
1963 | + self.use_temp_dir() |
1964 | + self._make_metadata(self.temp_dir, APPARMOR_2_TEMPLATE) |
1965 | + self.builder.read_manifest(self.temp_dir + "/meta/package.yaml") |
1966 | + self.assertEqual( |
1967 | + "path/to/app1.apparmor-profile", |
1968 | + self.builder.manifest["hooks"]["app1"]["apparmor-profile"]) |
1969 | + self.assertIsNone( |
1970 | + self.builder.manifest["hooks"]["app1"].get('apparmor')) |
1971 | + self.assertEqual( |
1972 | + "meta/app2.apparmor", |
1973 | + self.builder.manifest["hooks"]["app2"]["apparmor"]) |
1974 | + |
1975 | + def test_one_explicit_apparmor_file(self): |
1976 | + self.use_temp_dir() |
1977 | + scratch = os.path.join(self.temp_dir, "scratch") |
1978 | + touch(os.path.join(scratch, "bin", "foo")) |
1979 | + touch(os.path.join(scratch, "data", "icon.svg")) |
1980 | + apparmor_path = os.path.join(scratch, 'path', 'to', 'app1.apparmor') |
1981 | + os.makedirs(os.path.dirname(apparmor_path)) |
1982 | + with open(apparmor_path, 'w', encoding='utf-8') as fp: |
1983 | + fp.write(dedent("""\ |
1984 | + { |
1985 | + "template": "default", |
1986 | + "policy_groups": [ |
1987 | + "networking" |
1988 | + ], |
1989 | + "policy_vendor": "ubuntu-snappy", |
1990 | + "policy_version": 99.99 |
1991 | + } |
1992 | + """)) |
1993 | + self._make_metadata(scratch, APPARMOR_1_TEMPLATE) |
1994 | + self.builder.add_file(scratch, "./") |
1995 | + # FIXME: make it a .snap |
1996 | + expected_path = os.path.join(self.temp_dir, "test-yaml_1.0_all.click") |
1997 | + actual_path = self.builder.build(self.temp_dir, "meta/package.yaml") |
1998 | + self.assertEqual(expected_path, actual_path) |
1999 | + self.assertTrue(os.path.exists(actual_path)) |
2000 | + contents = os.path.join(self.temp_dir, 'contents') |
2001 | + subprocess.check_output( |
2002 | + ["dpkg-deb", "-R", actual_path, contents], universal_newlines=True) |
2003 | + self.assertEqual(sorted(os.listdir(contents)), |
2004 | + ['DEBIAN', 'bin', 'data', 'meta', 'path']) |
2005 | + self.assertEqual( |
2006 | + sorted(os.listdir(os.path.join(contents, 'path', 'to'))), |
2007 | + ['app1.apparmor']) |
2008 | + with open(os.path.join(contents, 'path', 'to', 'app1.apparmor'), |
2009 | + encoding='utf-8') as fp: |
2010 | + data = fp.read() |
2011 | + self.assertEqual(data, dedent("""\ |
2012 | + { |
2013 | + "template": "default", |
2014 | + "policy_groups": [ |
2015 | + "networking" |
2016 | + ], |
2017 | + "policy_vendor": "ubuntu-snappy", |
2018 | + "policy_version": 99.99 |
2019 | + } |
2020 | + """)) |
2021 | + with open(os.path.join(contents, 'meta', 'app2.apparmor'), |
2022 | + encoding='utf-8') as fp: |
2023 | + data = fp.read() |
2024 | + self.assertEqual(data, dedent("""\ |
2025 | + { |
2026 | + "template": "default", |
2027 | + "policy_groups": [ |
2028 | + "networking" |
2029 | + ], |
2030 | + "policy_vendor": "ubuntu-snappy", |
2031 | + "policy_version": 1.3 |
2032 | + } |
2033 | + """)) |
2034 | + |
2035 | + def test_one_explicit_apparmor_profile_file(self): |
2036 | + self.use_temp_dir() |
2037 | + scratch = os.path.join(self.temp_dir, "scratch") |
2038 | + touch(os.path.join(scratch, "bin", "foo")) |
2039 | + touch(os.path.join(scratch, "data", "icon.svg")) |
2040 | + apparmor_path = os.path.join( |
2041 | + scratch, 'path', 'to', 'app1.apparmor-profile') |
2042 | + os.makedirs(os.path.dirname(apparmor_path)) |
2043 | + with open(apparmor_path, 'w', encoding='utf-8') as fp: |
2044 | + fp.write(dedent("""\ |
2045 | + { |
2046 | + "template": "default", |
2047 | + "policy_groups": [ |
2048 | + "networking" |
2049 | + ], |
2050 | + "policy_vendor": "ubuntu-snappy", |
2051 | + "policy_version": 88.88 |
2052 | + } |
2053 | + """)) |
2054 | + self._make_metadata(scratch, APPARMOR_2_TEMPLATE) |
2055 | + self.builder.add_file(scratch, "./") |
2056 | + # FIXME: make it a .snap |
2057 | + expected_path = os.path.join(self.temp_dir, "test-yaml_1.0_all.click") |
2058 | + actual_path = self.builder.build(self.temp_dir, "meta/package.yaml") |
2059 | + self.assertEqual(expected_path, actual_path) |
2060 | + self.assertTrue(os.path.exists(actual_path)) |
2061 | + contents = os.path.join(self.temp_dir, 'contents') |
2062 | + subprocess.check_output( |
2063 | + ["dpkg-deb", "-R", actual_path, contents], universal_newlines=True) |
2064 | + self.assertEqual(sorted(os.listdir(contents)), |
2065 | + ['DEBIAN', 'bin', 'data', 'meta', 'path']) |
2066 | + self.assertEqual( |
2067 | + sorted(os.listdir(os.path.join(contents, 'path', 'to'))), |
2068 | + ['app1.apparmor-profile']) |
2069 | + with open( |
2070 | + os.path.join(contents, 'path', 'to', 'app1.apparmor-profile'), |
2071 | + encoding='utf-8') as fp: |
2072 | + data = fp.read() |
2073 | + self.assertEqual(data, dedent("""\ |
2074 | + { |
2075 | + "template": "default", |
2076 | + "policy_groups": [ |
2077 | + "networking" |
2078 | + ], |
2079 | + "policy_vendor": "ubuntu-snappy", |
2080 | + "policy_version": 88.88 |
2081 | + } |
2082 | + """)) |
2083 | + with open(os.path.join(contents, 'meta', 'app2.apparmor'), |
2084 | + encoding='utf-8') as fp: |
2085 | + data = fp.read() |
2086 | + self.assertEqual(data, dedent("""\ |
2087 | + { |
2088 | + "template": "default", |
2089 | + "policy_groups": [ |
2090 | + "networking" |
2091 | + ], |
2092 | + "policy_vendor": "ubuntu-snappy", |
2093 | + "policy_version": 1.3 |
2094 | + } |
2095 | + """)) |
2096 | |
2097 | === modified file 'click/tests/test_install.py' |
2098 | --- click/tests/test_install.py 2014-08-19 06:32:16 +0000 |
2099 | +++ click/tests/test_install.py 2014-12-03 23:19:34 +0000 |
2100 | @@ -32,6 +32,7 @@ |
2101 | import subprocess |
2102 | |
2103 | from unittest import skipUnless |
2104 | +from hashlib import sha512 |
2105 | |
2106 | from debian.deb822 import Deb822 |
2107 | from gi.repository import Click |
2108 | @@ -446,6 +447,18 @@ |
2109 | "Description": "test", |
2110 | "Click-Version": "0.2", |
2111 | }, status[0]) |
2112 | + manifest_file = "test-package.manifest" |
2113 | + manifest_path = os.path.join(inst_dir, |
2114 | + ".click", "info", |
2115 | + manifest_file) |
2116 | + self.assertTrue(os.path.exists(manifest_path)) |
2117 | + with open(manifest_path) as fd: |
2118 | + manifest = json.load(fd) |
2119 | + self.assertTrue(manifest) |
2120 | + self.assertTrue("checksum-sha512" in manifest) |
2121 | + with open(path, "rb") as fd: |
2122 | + checksum = sha512(fd.read()).hexdigest() |
2123 | + self.assertEqual(checksum, manifest["checksum-sha512"]) |
2124 | mock_package_install_hooks.assert_called_once_with( |
2125 | db, "test-package", None, "1.0", user_name=None) |
2126 | |
2127 | @@ -456,18 +469,12 @@ |
2128 | with self.run_in_subprocess( |
2129 | "click_get_frameworks_dir") as (enter, preloads): |
2130 | enter() |
2131 | - original_call = subprocess.call |
2132 | + original_call = subprocess.check_output |
2133 | |
2134 | def call_side_effect(*args, **kwargs): |
2135 | - if "TEST_VERBOSE" in os.environ: |
2136 | - return original_call( |
2137 | - ["touch", os.path.join(self.temp_dir, "sentinel")], |
2138 | - **kwargs) |
2139 | - else: |
2140 | - with open("/dev/null", "w") as devnull: |
2141 | - return original_call( |
2142 | - ["touch", os.path.join(self.temp_dir, "sentinel")], |
2143 | - stdout=devnull, stderr=devnull, **kwargs) |
2144 | + return original_call( |
2145 | + ["touch", os.path.join(self.temp_dir, "sentinel")], |
2146 | + **kwargs) |
2147 | |
2148 | path = self.make_fake_package( |
2149 | control_fields={ |
2150 | @@ -490,7 +497,7 @@ |
2151 | db.add(root) |
2152 | installer = ClickInstaller(db) |
2153 | self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) |
2154 | - with mock.patch("subprocess.call") as mock_call: |
2155 | + with mock.patch("subprocess.check_output") as mock_call: |
2156 | mock_call.side_effect = call_side_effect |
2157 | self.assertRaises( |
2158 | subprocess.CalledProcessError, installer.install, path) |
2159 | @@ -611,7 +618,7 @@ |
2160 | os.path.exists(ClickInstaller(None)._preload_path()), |
2161 | "preload bits not built; installing packages will fail") |
2162 | @mock.patch("gi.repository.Click.package_install_hooks") |
2163 | - @mock.patch("click.install.ClickInstaller._dpkg_architecture") |
2164 | + @mock.patch("click.install._dpkg_architecture") |
2165 | def test_single_architecture(self, mock_dpkg_architecture, |
2166 | mock_package_install_hooks): |
2167 | with self.run_in_subprocess( |
2168 | @@ -648,7 +655,7 @@ |
2169 | os.path.exists(ClickInstaller(None)._preload_path()), |
2170 | "preload bits not built; installing packages will fail") |
2171 | @mock.patch("gi.repository.Click.package_install_hooks") |
2172 | - @mock.patch("click.install.ClickInstaller._dpkg_architecture") |
2173 | + @mock.patch("click.install._dpkg_architecture") |
2174 | def test_multiple_architectures(self, mock_dpkg_architecture, |
2175 | mock_package_install_hooks): |
2176 | with self.run_in_subprocess( |
2177 | |
2178 | === modified file 'debian/changelog' |
2179 | --- debian/changelog 2014-11-14 12:29:14 +0000 |
2180 | +++ debian/changelog 2014-12-03 23:19:34 +0000 |
2181 | @@ -1,3 +1,124 @@ |
2182 | +click (0.4.35+ppa19) vivid; urgency=low |
2183 | + |
2184 | + * fix frameworks mapping |
2185 | + * remove duplicated information from auto-generated manifest.json |
2186 | + when package.yaml is used |
2187 | + |
2188 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 03 Dec 2014 16:47:40 +0100 |
2189 | + |
2190 | +click (0.4.35+ppa18) vivid; urgency=low |
2191 | + |
2192 | + [ Barry Warsaw ] |
2193 | + * add install progress meter |
2194 | + |
2195 | + [ Michael Vogt ] |
2196 | + * make dpkg output in install quiet unless "--verbose" is given |
2197 | + |
2198 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 03 Dec 2014 10:16:43 +0100 |
2199 | + |
2200 | +click (0.4.35+ppa17) vivid; urgency=low |
2201 | + |
2202 | + * add support for packages.yaml "binaries:" |
2203 | + |
2204 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 02 Dec 2014 16:37:12 +0100 |
2205 | + |
2206 | +click (0.4.35+ppa16) vivid; urgency=low |
2207 | + |
2208 | + * click/build.py: |
2209 | + - make yaml service description optional |
2210 | + |
2211 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 02 Dec 2014 08:49:21 +0100 |
2212 | + |
2213 | +click (0.4.35+ppa15) vivid; urgency=low |
2214 | + |
2215 | + * more fixes |
2216 | + |
2217 | + -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 28 Nov 2014 14:42:18 +0100 |
2218 | + |
2219 | +click (0.4.35+ppa14) vivid; urgency=low |
2220 | + |
2221 | + * fix test failure |
2222 | + |
2223 | + -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 28 Nov 2014 13:44:31 +0100 |
2224 | + |
2225 | +click (0.4.35+ppa13) vivid; urgency=low |
2226 | + |
2227 | + * add support for "services" in packages.yaml |
2228 | + |
2229 | + -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 28 Nov 2014 13:39:32 +0100 |
2230 | + |
2231 | +click (0.4.35+ppa12) vivid; urgency=low |
2232 | + |
2233 | + * use "integration" instead of "hooks" in the packages.yaml |
2234 | + |
2235 | + -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 28 Nov 2014 08:35:31 +0100 |
2236 | + |
2237 | +click (0.4.35+ppa11) vivid; urgency=low |
2238 | + |
2239 | + * use /apps prefix instead of /opt/com.ubuntu.click |
2240 | + |
2241 | + -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 27 Nov 2014 09:05:37 +0100 |
2242 | + |
2243 | +click (0.4.35+ppa10) vivid; urgency=low |
2244 | + |
2245 | + * merge fixes from lp:~mvo/click/yaml-manifest |
2246 | + |
2247 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 26 Nov 2014 17:57:51 +0100 |
2248 | + |
2249 | +click (0.4.35+ppa9) vivid; urgency=low |
2250 | + |
2251 | + * merged lp:~mvo/click/yaml-manifest |
2252 | + |
2253 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 26 Nov 2014 16:50:25 +0100 |
2254 | + |
2255 | +click (0.4.35+ppa8) vivid; urgency=low |
2256 | + |
2257 | + * merge lp:~mvo/click/add-checksum-to-manifest |
2258 | + |
2259 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 25 Nov 2014 17:58:24 +0100 |
2260 | + |
2261 | +click (0.4.35+ppa7) vivid; urgency=low |
2262 | + |
2263 | + * fix typos in click update |
2264 | + |
2265 | + -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 24 Nov 2014 17:32:42 +0100 |
2266 | + |
2267 | +click (0.4.35+ppa6) vivid; urgency=low |
2268 | + |
2269 | + * send application/hal+json header (as requested by the u1 team) |
2270 | + |
2271 | + -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 20 Nov 2014 15:55:43 +0100 |
2272 | + |
2273 | +click (0.4.35+ppa5) vivid; urgency=low |
2274 | + |
2275 | + * do not fail if logind is not available |
2276 | + |
2277 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 19 Nov 2014 22:17:20 +0100 |
2278 | + |
2279 | +click (0.4.35+ppa4) vivid; urgency=low |
2280 | + |
2281 | + * merge lp:~mvo/click/lp1394256-run-user-hooks |
2282 | + |
2283 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 19 Nov 2014 21:54:54 +0100 |
2284 | + |
2285 | +click (0.4.35+ppa3) vivid; urgency=low |
2286 | + |
2287 | + * merged lp:~jamesodhunt/click/add-checksum-to-manifest |
2288 | + |
2289 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 19 Nov 2014 16:46:54 +0100 |
2290 | + |
2291 | +click (0.4.35+ppa2) vivid; urgency=low |
2292 | + |
2293 | + * fix click update for anonymous clicks |
2294 | + |
2295 | + -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 17 Nov 2014 16:57:23 +0100 |
2296 | + |
2297 | +click (0.4.35+ppa1) vivid; urgency=low |
2298 | + |
2299 | + * merged with vivid |
2300 | + |
2301 | + -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 14 Nov 2014 17:43:35 +0100 |
2302 | + |
2303 | click (0.4.35) vivid; urgency=low |
2304 | |
2305 | [ Michael Vogt ] |
2306 | @@ -8,6 +129,53 @@ |
2307 | |
2308 | -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Fri, 14 Nov 2014 12:29:14 +0000 |
2309 | |
2310 | +click (0.4.34.2+ppa7) vivid; urgency=low |
2311 | + |
2312 | + * fix tests |
2313 | + |
2314 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 12 Nov 2014 10:43:51 +0100 |
2315 | + |
2316 | +click (0.4.34.2+ppa6) vivid; urgency=low |
2317 | + |
2318 | + * send correct frameworks/architecture to the click store |
2319 | + |
2320 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 12 Nov 2014 10:01:19 +0100 |
2321 | + |
2322 | +click (0.4.34.2+ppa5) vivid; urgency=low |
2323 | + |
2324 | + * add support for 2fa |
2325 | + |
2326 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 11 Nov 2014 15:32:33 +0100 |
2327 | + |
2328 | +click (0.4.34.2+ppa4) vivid; urgency=low |
2329 | + |
2330 | + * merged lp:~mvo/click/sso+acquire |
2331 | + |
2332 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 05 Nov 2014 14:58:39 +0100 |
2333 | + |
2334 | +click (0.4.34.2+ppa3) vivid; urgency=low |
2335 | + |
2336 | + * pep8/pyflakes |
2337 | + |
2338 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 05 Nov 2014 14:47:32 +0100 |
2339 | + |
2340 | +click (0.4.34.2+ppa2) vivid; urgency=low |
2341 | + |
2342 | + * support "click update click1 click2" |
2343 | + * support "click update --user=USER" |
2344 | + * support "click update --all-users" |
2345 | + * show new status after "click update" |
2346 | + * merge latest lp:~mvo/click/sso changes to support anonymous |
2347 | + downloads |
2348 | + |
2349 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 05 Nov 2014 14:18:24 +0100 |
2350 | + |
2351 | +click (0.4.34.2+ppa1) vivid; urgency=low |
2352 | + |
2353 | + * merged lp:click/devel |
2354 | + |
2355 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 04 Nov 2014 10:15:13 +0100 |
2356 | + |
2357 | click (0.4.34.2) vivid; urgency=medium |
2358 | |
2359 | [ Michael Vogt ] |
2360 | @@ -41,6 +209,44 @@ |
2361 | |
2362 | -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 03 Nov 2014 12:49:25 +0000 |
2363 | |
2364 | +click (0.4.34~ppa6) utopic; urgency=low |
2365 | + |
2366 | + * merge fixes from acquire/repository |
2367 | + |
2368 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 21 Oct 2014 17:44:56 -0400 |
2369 | + |
2370 | +click (0.4.34~ppa5) utopic; urgency=low |
2371 | + |
2372 | + * add python3-oauthlib, python3-prettytable to the build-depends |
2373 | + |
2374 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 21 Oct 2014 13:59:25 -0400 |
2375 | + |
2376 | +click (0.4.34~ppa4) utopic; urgency=low |
2377 | + |
2378 | + * fix tests |
2379 | + |
2380 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 21 Oct 2014 12:48:25 -0400 |
2381 | + |
2382 | +click (0.4.34~ppa3) utopic; urgency=medium |
2383 | + |
2384 | + [ Michael Vogt ] |
2385 | + * Demote ubuntu-app-launch-tools from a click recommends to a suggests, |
2386 | + since they are not needed on server installs. |
2387 | + * Use dh-systemd to enable click system and user hook integration |
2388 | + (LP: #1379657). |
2389 | + * merged lp:~mvo/click/sso+acquire |
2390 | + * hack XB-Task: ubuntu-core |
2391 | + |
2392 | + [ Colin Watson ] |
2393 | + * Make "click info" always try opening the input file as a package and |
2394 | + only try to interpret it as a file in an installed package if that |
2395 | + fails, rather than guessing by the input file extension. |
2396 | + |
2397 | + [ Pete Woods ] |
2398 | + * Include Canonical's cmake-extras project in the schroot. |
2399 | + |
2400 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 21 Oct 2014 11:54:52 -0400 |
2401 | + |
2402 | click (0.4.33) utopic; urgency=medium |
2403 | |
2404 | [ Pete Woods ] |
2405 | |
2406 | === modified file 'debian/click.postinst' |
2407 | --- debian/click.postinst 2014-10-14 09:47:48 +0000 |
2408 | +++ debian/click.postinst 2014-12-03 23:19:34 +0000 |
2409 | @@ -8,8 +8,8 @@ |
2410 | clickpkg |
2411 | fi |
2412 | |
2413 | - mkdir -p -m 755 /opt/click.ubuntu.com |
2414 | - chown clickpkg:clickpkg /opt/click.ubuntu.com |
2415 | + mkdir -p -m 755 /apps |
2416 | + chown clickpkg:clickpkg /apps |
2417 | |
2418 | # dh-systemd has no support yet for user systemd units |
2419 | # so we need to do this manually here |
2420 | |
2421 | === modified file 'debian/control' |
2422 | --- debian/control 2014-10-10 07:10:23 +0000 |
2423 | +++ debian/control 2014-12-03 23:19:34 +0000 |
2424 | @@ -3,7 +3,7 @@ |
2425 | Priority: optional |
2426 | Maintainer: Colin Watson <cjwatson@ubuntu.com> |
2427 | Standards-Version: 3.9.5 |
2428 | -Build-Depends: debhelper (>= 9~), dh-autoreconf, intltool, python3:any (>= 3.2), python3-all:any, python3-setuptools, python3-apt, python3-debian, python3-gi, python3:any (>= 3.3) | python3-mock, pep8, python3-pep8, pyflakes, python3-sphinx, pkg-config, valac, gobject-introspection (>= 0.6.7), libgirepository1.0-dev (>= 0.6.7), libglib2.0-dev (>= 2.34), gir1.2-glib-2.0, libjson-glib-dev (>= 0.10), libgee-0.8-dev, libpackagekit-glib2-dev (>= 0.7.2), python3-coverage, python3-six, dh-systemd (>= 1.3) |
2429 | +Build-Depends: debhelper (>= 9~), dh-autoreconf, intltool, python3:any (>= 3.2), python3-all:any, python3-setuptools, python3-apt, python3-debian, python3-gi, python3:any (>= 3.3) | python3-mock, pep8, python3-pep8, pyflakes, python3-sphinx, pkg-config, valac, gobject-introspection (>= 0.6.7), libgirepository1.0-dev (>= 0.6.7), libglib2.0-dev (>= 2.34), gir1.2-glib-2.0, libjson-glib-dev (>= 0.10), libgee-0.8-dev, libpackagekit-glib2-dev (>= 0.7.2), python3-coverage, python3-six, python3-pycurl, python3-dbus, dh-systemd (>= 1.3), python3-prettytable, python3-oauthlib, python3-yaml, python3-progressbar |
2430 | Vcs-Bzr: https://code.launchpad.net/~ubuntu-managed-branches/click/click |
2431 | Vcs-Browser: http://bazaar.launchpad.net/~ubuntu-managed-branches/click/click/files |
2432 | X-Auto-Uploader: no-rewrite-version |
2433 | @@ -14,7 +14,7 @@ |
2434 | Package: click |
2435 | Architecture: any |
2436 | Pre-Depends: ${misc:Pre-Depends} |
2437 | -Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, python3-click (= ${binary:Version}), adduser |
2438 | +Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, python3-click (= ${binary:Version}), adduser, python3-prettytable, python3-pycurl, python3-dbus, python3-oauthlib, python3-yaml, python3-progressbar |
2439 | Recommends: click-apparmor |
2440 | Suggests: click-reviewers-tools (>= 0.9), ubuntu-app-launch-tools | upstart-app-launch-tools |
2441 | Conflicts: click-package |
2442 | @@ -25,6 +25,7 @@ |
2443 | the file system, suitable for third-party applications. |
2444 | . |
2445 | This package provides common files, including the main click program. |
2446 | +XB-Task: ubuntu-core |
2447 | |
2448 | Package: click-dev |
2449 | Architecture: any |
2450 | @@ -40,7 +41,7 @@ |
2451 | Package: python3-click |
2452 | Section: python |
2453 | Architecture: any |
2454 | -Depends: ${misc:Depends}, ${python3:Depends}, gir1.2-click-0.4 (= ${binary:Version}), gir1.2-glib-2.0, python3-apt, python3-debian, python3-gi |
2455 | +Depends: ${misc:Depends}, ${python3:Depends}, gir1.2-click-0.4 (= ${binary:Version}), gir1.2-glib-2.0, python3-apt, python3-debian, python3-gi, python3-pycurl |
2456 | Conflicts: python3-click-package |
2457 | Replaces: python3-click-package |
2458 | Provides: python3-click-package |
2459 | |
2460 | === modified file 'debian/rules' |
2461 | --- debian/rules 2014-10-10 07:10:23 +0000 |
2462 | +++ debian/rules 2014-12-03 23:19:34 +0000 |
2463 | @@ -24,7 +24,8 @@ |
2464 | confflags := \ |
2465 | --with-python-interpreters='$(PY3)' \ |
2466 | --with-systemdsystemunitdir=/lib/systemd/system \ |
2467 | - --with-systemduserunitdir=/usr/lib/systemd/user |
2468 | + --with-systemduserunitdir=/usr/lib/systemd/user \ |
2469 | + --with-default-root=/apps |
2470 | ifeq ($(PACKAGEKIT),no) |
2471 | confflags += --disable-packagekit |
2472 | endif |
2473 | |
2474 | === modified file 'lib/click/user.vala' |
2475 | --- lib/click/user.vala 2014-09-29 13:23:12 +0000 |
2476 | +++ lib/click/user.vala 2014-12-03 23:19:34 +0000 |
2477 | @@ -33,8 +33,21 @@ |
2478 | * symlinks per user. |
2479 | */ |
2480 | |
2481 | + |
2482 | namespace Click { |
2483 | |
2484 | + struct LogindUser { |
2485 | + uint32 uid; |
2486 | + string name; |
2487 | + string ObjectPath; |
2488 | + } |
2489 | + |
2490 | + /* the logind dbus interface */ |
2491 | + [DBus (name = "org.freedesktop.login1.Manager")] |
2492 | + interface LogindManager : Object { |
2493 | + public abstract LogindUser[] ListUsers () throws IOError; |
2494 | + } |
2495 | + |
2496 | /* Pseudo-usernames selected to be invalid as a real username, and alluding |
2497 | * to group syntaxes used in other systems. |
2498 | */ |
2499 | @@ -596,6 +609,35 @@ |
2500 | if (! is_pseudo_user) |
2501 | package_install_hooks (db, package, |
2502 | old_version, version, name); |
2503 | + |
2504 | + // run user hooks for all logged in users |
2505 | + if (name == ALL_USERS) |
2506 | + run_user_hooks_for_all_logged_in_users (package, old_version, version); |
2507 | + } |
2508 | + |
2509 | + private void |
2510 | + run_user_hooks_for_all_logged_in_users (string package, string? old_version, |
2511 | + string version) throws IOError |
2512 | + { |
2513 | + try { |
2514 | + LogindManager logind = Bus.get_proxy_sync ( |
2515 | + BusType.SYSTEM, |
2516 | + "org.freedesktop.login1", |
2517 | + "/org/freedesktop/login1"); |
2518 | + var users = logind.ListUsers(); |
2519 | + foreach (LogindUser user in users) |
2520 | + { |
2521 | + // FIXME: ideally we would read from /etc/adduser.conf |
2522 | + if(user.uid >= 1000 && user.uid <= 30000) |
2523 | + { |
2524 | + package_install_hooks (db, package, |
2525 | + old_version, version, user.name); |
2526 | + } |
2527 | + } |
2528 | + } catch (Error e) { |
2529 | + warning ("Can not connect to logind"); |
2530 | + return; |
2531 | + } |
2532 | } |
2533 | |
2534 | private bool |
2535 | |
2536 | === modified file 'setup.py.in' |
2537 | --- setup.py.in 2014-06-30 16:48:31 +0000 |
2538 | +++ setup.py.in 2014-12-03 23:19:34 +0000 |
2539 | @@ -1,5 +1,6 @@ |
2540 | #! /usr/bin/env python3 |
2541 | |
2542 | +import glob |
2543 | import sys |
2544 | |
2545 | from setuptools import find_packages, setup |
2546 | @@ -18,6 +19,8 @@ |
2547 | if sys.version < "3.3": |
2548 | require('mock') |
2549 | require('chardet') |
2550 | +require('click.paths') |
2551 | +import click.paths |
2552 | |
2553 | if "@GCOVR@": |
2554 | require('coverage') |
2555 | @@ -47,6 +50,11 @@ |
2556 | license="GNU GPL", |
2557 | packages=find_packages(), |
2558 | scripts=['bin/click'], |
2559 | + data_files=[ |
2560 | + # we need to remove the prefix here or we add it twice |
2561 | + (click.paths.acquire_methods_dir[1:].lstrip(sys.prefix), |
2562 | + glob.glob("acquire/*")), |
2563 | + ], |
2564 | install_requires=requirements, |
2565 | cmdclass={"test": test_extra}, |
2566 | test_suite="click.tests", |