Merge lp:~snappy-dev/click/default-apparmor into lp:click

Proposed by Barry Warsaw on 2014-12-03
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
Reviewer Review Type Date Requested Status
Jamie Strandboge 2014-12-03 Pending
Michael Vogt 2014-12-03 Pending
Review via email: mp+243608@code.launchpad.net

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 on 2014-12-03

Create default apparmor profiles if the yaml doesn't explicitly name
integration:<app>:apparmor{,-profile}

With tests!

573. By Michael Vogt on 2014-12-03

merged lp:~mvo/click/yaml-manifest

572. By Michael Vogt on 2014-12-03

make dpkg output in install quiet unless "--verbose" is given

571. By Michael Vogt on 2014-12-03

merged lp:~snappy-dev/click/progressmeter (thanks to Barry Warsaw)

570. By Michael Vogt on 2014-12-03

add support for packages.yaml "binaries:"

569. By Michael Vogt on 2014-12-02

merge previous upload

568. By Michael Vogt on 2014-12-02

merged lp:~mvo/click/yaml-manifest

567. By Michael Vogt on 2014-11-28

more fixes

566. By Michael Vogt on 2014-11-28

fix test failure

565. By Michael Vogt on 2014-11-28

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",

Subscribers

People subscribed via source and target branches