diff -Nru click-0.4.16/click/commands/desktophook.py click-0.4.17.2/click/commands/desktophook.py --- click-0.4.16/click/commands/desktophook.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/commands/desktophook.py 2014-03-06 16:38:26.000000000 +0000 @@ -23,8 +23,9 @@ from optparse import OptionParser import os +from gi.repository import Click + from click import osextras -from click.query import find_package_directory COMMENT = \ @@ -71,7 +72,7 @@ def read_hooks_for(path, package, app_name): try: - directory = find_package_directory(path) + directory = Click.find_package_directory(path) manifest_path = os.path.join( directory, ".click", "info", "%s.manifest" % package) with io.open(manifest_path, encoding="UTF-8") as manifest: @@ -111,10 +112,10 @@ # TODO: This is a very crude .desktop file mangler; we should instead # implement proper (de)serialisation. def write_desktop_file(target_path, source_path, profile): - osextras.ensuredir(os.path.dirname(target_path)) + Click.ensuredir(os.path.dirname(target_path)) with io.open(source_path, encoding="UTF-8") as source, \ io.open(target_path, "w", encoding="UTF-8") as target: - source_dir = find_package_directory(source_path) + source_dir = Click.find_package_directory(source_path) written_comment = False seen_path = False for line in source: diff -Nru click-0.4.16/click/commands/hook.py click-0.4.17.2/click/commands/hook.py --- click-0.4.16/click/commands/hook.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/commands/hook.py 2014-03-06 16:38:26.000000000 +0000 @@ -20,8 +20,7 @@ from optparse import OptionParser from textwrap import dedent -from click.database import ClickDB -from click.hooks import ClickHook, run_system_hooks, run_user_hooks +from gi.repository import Click per_hook_subcommands = { @@ -54,16 +53,25 @@ if subcommand in per_hook_subcommands: if len(args) < 2: parser.error("need hook name") - db = ClickDB(options.root) + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) name = args[1] - hook = ClickHook.open(db, name) + hook = Click.Hook.open(db, name) getattr(hook, per_hook_subcommands[subcommand])() elif subcommand == "run-system": - db = ClickDB(options.root) - run_system_hooks(db) + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) + Click.run_system_hooks(db) elif subcommand == "run-user": - db = ClickDB(options.root) - run_user_hooks(db, user=options.user) + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) + Click.run_user_hooks(db, user_name=options.user) else: parser.error( "unknown subcommand '%s' (known: install, remove, run-system," diff -Nru click-0.4.16/click/commands/info.py click-0.4.17.2/click/commands/info.py --- click-0.4.16/click/commands/info.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/commands/info.py 2014-03-06 16:38:26.000000000 +0000 @@ -24,18 +24,21 @@ import os import sys -from click.database import ClickDB +from gi.repository import Click + from click.install import DebFile -from click.user import ClickUser def get_manifest(options, arg): if "/" not in arg: - db = ClickDB(options.root) - registry = ClickUser(db, user=options.user) - if arg in registry: + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) + registry = Click.User.for_user(db, name=options.user) + if registry.has_package_name(arg): manifest_path = os.path.join( - registry.path(arg), ".click", "info", "%s.manifest" % arg) + registry.get_path(arg), ".click", "info", "%s.manifest" % arg) with io.open(manifest_path, encoding="UTF-8") as manifest: return json.load(manifest) diff -Nru click-0.4.16/click/commands/install.py click-0.4.17.2/click/commands/install.py --- click-0.4.16/click/commands/install.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/commands/install.py 2014-03-06 16:38:26.000000000 +0000 @@ -21,7 +21,8 @@ import sys from textwrap import dedent -from click.database import ClickDB +from gi.repository import Click + from click.install import ClickInstaller, ClickInstallerError @@ -45,7 +46,10 @@ options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package file name") - db = ClickDB(options.root) + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) package_path = args[0] installer = ClickInstaller(db, options.force_missing_framework) try: diff -Nru click-0.4.16/click/commands/list.py click-0.4.17.2/click/commands/list.py --- click-0.4.16/click/commands/list.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/commands/list.py 2014-03-06 16:38:26.000000000 +0000 @@ -23,22 +23,25 @@ import os import sys -from click.database import ClickDB -from click.user import ClickUser +from gi.repository import Click def list_packages(options): - db = ClickDB(options.root) + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) if options.all: - for package, version, path, writeable in \ - db.packages(all_versions=True): - yield package, version, path, writeable + for inst in db.get_packages(all_versions=True): + yield ( + inst.props.package, inst.props.version, inst.props.path, + inst.props.writeable) else: - registry = ClickUser(db, user=options.user) - for package, version in sorted(registry.items()): + registry = Click.User.for_user(db, name=options.user) + for package in sorted(registry.get_package_names()): yield ( - package, version, registry.path(package), - registry.removable(package)) + package, registry.get_version(package), + registry.get_path(package), registry.is_removable(package)) def run(argv): diff -Nru click-0.4.16/click/commands/pkgdir.py click-0.4.17.2/click/commands/pkgdir.py --- click-0.4.16/click/commands/pkgdir.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/commands/pkgdir.py 2014-03-06 16:38:26.000000000 +0000 @@ -21,9 +21,7 @@ from optparse import OptionParser import sys -from click.database import ClickDB -from click.query import find_package_directory -from click.user import ClickUser +from gi.repository import Click def run(argv): @@ -39,12 +37,15 @@ parser.error("need package name") try: if "/" in args[0]: - print(find_package_directory(args[0])) + print(Click.find_package_directory(args[0])) else: - db = ClickDB(options.root) + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) package_name = args[0] - registry = ClickUser(db, user=options.user) - print(registry.path(package_name)) + registry = Click.User.for_user(db, name=options.user) + print(registry.get_path(package_name)) except Exception as e: print(e, file=sys.stderr) return 1 diff -Nru click-0.4.16/click/commands/register.py click-0.4.17.2/click/commands/register.py --- click-0.4.16/click/commands/register.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/commands/register.py 2014-03-06 16:38:26.000000000 +0000 @@ -19,8 +19,7 @@ from optparse import OptionParser -from click.database import ClickDB -from click.user import ClickUser +from gi.repository import Click, GLib def run(argv): @@ -38,11 +37,20 @@ parser.error("need package name") if len(args) < 2: parser.error("need version") - db = ClickDB(options.root) + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) package = args[0] version = args[1] - registry = ClickUser(db, user=options.user, all_users=options.all_users) - old_version = registry.get(package) + if options.all_users: + registry = Click.User.for_all_users(db) + else: + registry = Click.User.for_user(db, name=options.user) + try: + old_version = registry.get_version(package) + except GLib.GError: + old_version = None registry.set_version(package, version) if old_version is not None: db.maybe_remove(package, old_version) diff -Nru click-0.4.16/click/commands/unregister.py click-0.4.17.2/click/commands/unregister.py --- click-0.4.16/click/commands/unregister.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/commands/unregister.py 2014-03-06 16:38:26.000000000 +0000 @@ -21,8 +21,7 @@ import os import sys -from click.database import ClickDB -from click.user import ClickUser +from gi.repository import Click def run(argv): @@ -44,10 +43,16 @@ "remove packages from disk") if options.user is None and "SUDO_USER" in os.environ: options.user = os.environ["SUDO_USER"] - db = ClickDB(options.root) + db = Click.DB() + db.read() + if options.root is not None: + db.add(options.root) package = args[0] - registry = ClickUser(db, user=options.user, all_users=options.all_users) - old_version = registry[package] + if options.all_users: + registry = Click.User.for_all_users(db) + else: + registry = Click.User.for_user(db, name=options.user) + old_version = registry.get_version(package) if len(args) >= 2 and old_version != args[1]: print( "Not removing %s %s; expected version %s" % diff -Nru click-0.4.16/click/database.py click-0.4.17.2/click/database.py --- click-0.4.16/click/database.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/database.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,323 +0,0 @@ -# Copyright (C) 2013 Canonical Ltd. -# Author: Colin Watson - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Click databases.""" - -from __future__ import print_function - -__metaclass__ = type -__all__ = [ - "ClickDB", - ] - - -from collections import Sequence, defaultdict -import io -import json -import os -import pwd -import shutil -import subprocess -import sys - -try: - from configparser import Error as ConfigParserError - if sys.version < "3.2": - from configparser import SafeConfigParser as ConfigParser - else: - from configparser import ConfigParser -except ImportError: - from ConfigParser import Error as ConfigParserError - from ConfigParser import SafeConfigParser as ConfigParser - -from click import osextras -from click.paths import db_dir - - -class ClickSingleDB: - def __init__(self, root, master_db): - self.root = root - self.master_db = master_db - - def path(self, package, version): - """Look up a package and version in only this database.""" - try_path = os.path.join(self.root, package, version) - if os.path.exists(try_path): - return try_path - else: - raise KeyError( - "%s %s does not exist in %s" % (package, version, self.root)) - - def packages(self, all_versions=False): - """Return all current package versions in only this database. - - If all_versions=True, return all versions, not just current ones. - """ - for package in sorted(osextras.listdir_force(self.root)): - if package == ".click": - continue - if all_versions: - package_path = os.path.join(self.root, package) - for version in sorted(osextras.listdir_force(package_path)): - version_path = os.path.join(package_path, version) - if (os.path.islink(version_path) or - not os.path.isdir(version_path)): - continue - yield package, version, version_path - else: - current_path = os.path.join(self.root, package, "current") - if os.path.islink(current_path): - version = os.readlink(current_path) - if "/" not in version: - yield package, version, current_path - - def _app_running(self, package, app_name, version): - app_id = "%s_%s_%s" % (package, app_name, version) - command = ["upstart-app-pid", app_id] - with open("/dev/null", "w") as devnull: - return subprocess.call(command, stdout=devnull) == 0 - - def _any_app_running(self, package, version): - if not osextras.find_on_path("upstart-app-pid"): - return False - manifest_path = os.path.join( - self.path(package, version), ".click", "info", - "%s.manifest" % package) - try: - with io.open(manifest_path, encoding="UTF-8") as manifest: - manifest_json = json.load(manifest) - for app_name in manifest_json.get("hooks", {}): - if self._app_running(package, app_name, version): - return True - except Exception: - pass - return False - - def _remove_unless_running(self, package, version, verbose=False): - # Circular imports. - from click.hooks import package_remove_hooks - from click.user import ClickUser, GC_IN_USE_USER - - if self._any_app_running(package, version): - gc_in_use_user_db = ClickUser(self.master_db, user=GC_IN_USE_USER) - gc_in_use_user_db.set_version(package, version) - return - - version_path = self.path(package, version) - if verbose: - print("Removing %s" % version_path) - package_remove_hooks(self, package, version) - shutil.rmtree(version_path, ignore_errors=True) - - package_path = os.path.join(self.root, package) - current_path = os.path.join(package_path, "current") - if (os.path.islink(current_path) and - os.readlink(current_path) == version): - os.unlink(current_path) - # TODO: Perhaps we should relink current to the latest remaining - # version. However, that requires version comparison, and it's - # not clear whether it's worth it given that current is mostly - # superseded by user registration. - if not os.listdir(package_path): - os.rmdir(package_path) - - def maybe_remove(self, package, version): - """Remove a package version if it is not in use. - - "In use" may mean registered for another user, or running. In the - latter case we construct a fake registration so that we can tell the - difference later between a package version that was in use at the - time of removal and one that was never registered for any user. - - (This is unfortunately complex, and perhaps some day we can require - that installations always have some kind of registration to avoid - this complexity.) - """ - # Circular imports. - from click.user import ClickUsers, GC_IN_USE_USER - - for user_name, user_db in ClickUsers(self.master_db).items(): - if user_db.get(package) == version: - if user_name == GC_IN_USE_USER: - # Previously running; we'll check this again shortly. - user_db.remove(package) - else: - # In use. - return - - self._remove_unless_running(package, version) - - def gc(self, verbose=True): - """Remove package versions with no user registrations. - - To avoid accidentally removing packages that were installed without - ever having a user registration, we only garbage-collect packages - that were not removed by ClickSingleDB.maybe_remove due to having a - running application at the time. - - (This is unfortunately complex, and perhaps some day we can require - that installations always have some kind of registration to avoid - this complexity.) - """ - # Circular import. - from click.user import ClickUser, ClickUsers, GC_IN_USE_USER - - user_reg = defaultdict(set) - gc_in_use = defaultdict(set) - for user_name, user_db in ClickUsers(self.master_db).items(): - for package, version in user_db.items(): - if user_name == GC_IN_USE_USER: - gc_in_use[package].add(version) - else: - user_reg[package].add(version) - - gc_in_use_user_db = ClickUser(self.master_db, user=GC_IN_USE_USER) - for package in sorted(osextras.listdir_force(self.root)): - if package == ".click": - continue - package_path = os.path.join(self.root, package) - for version in sorted(osextras.listdir_force(package_path)): - if version in user_reg[package]: - # In use. - continue - if version not in gc_in_use[package]: - version_path = os.path.join(package_path, version) - if verbose: - print( - "Not removing %s (never registered)." % - version_path) - continue - gc_in_use_user_db.remove(package) - self._remove_unless_running(package, version, verbose=verbose) - - def _clickpkg_paths(self): - """Yield all paths which should be owned by clickpkg.""" - if os.path.exists(self.root): - yield self.root - for package in osextras.listdir_force(self.root): - if package == ".click": - path = os.path.join(self.root, ".click") - yield path - log_path = os.path.join(path, "log") - if os.path.exists(log_path): - yield log_path - users_path = os.path.join(path, "users") - if os.path.exists(users_path): - yield users_path - else: - path = os.path.join(self.root, package) - for dirpath, dirnames, filenames in os.walk(path): - yield dirpath - for dirname in dirnames: - dirname_path = os.path.join(dirpath, dirname) - if os.path.islink(dirname_path): - yield dirname_path - for filename in filenames: - yield os.path.join(dirpath, filename) - - def ensure_ownership(self): - """Ensure correct ownership of files in the database. - - On a system that is upgraded by delivering a new system image rather - than by package upgrades, it is possible for the clickpkg UID to - change. The overlay database must then be adjusted to account for - this. - """ - pw = pwd.getpwnam("clickpkg") - try: - st = os.stat(self.root) - if st.st_uid == pw.pw_uid and st.st_gid == pw.pw_gid: - return - except OSError: - return - chown_kwargs = {} - if sys.version >= "3.3" and os.chown in os.supports_follow_symlinks: - chown_kwargs["follow_symlinks"] = False - for path in self._clickpkg_paths(): - os.chown(path, pw.pw_uid, pw.pw_gid, **chown_kwargs) - - -class ClickDB(Sequence): - def __init__(self, extra_root=None, use_system=True, override_db_dir=None): - if override_db_dir is None: - override_db_dir = db_dir - self._db = [] - if use_system: - for entry in sorted(osextras.listdir_force(override_db_dir)): - if not entry.endswith(".conf"): - continue - path = os.path.join(override_db_dir, entry) - config = ConfigParser() - try: - config.read(path) - root = config.get("Click Database", "root") - except ConfigParserError as e: - print(e, file=sys.stderr) - continue - self.add(root) - if extra_root is not None: - self.add(extra_root) - - def __getitem__(self, key): - return self._db[key] - - def __len__(self): - return len(self._db) - - def add(self, root): - self._db.append(ClickSingleDB(root, self)) - - @property - def overlay(self): - """Return the directory where changes should be written.""" - return self._db[-1].root - - def path(self, package, version): - """Look up a package and version in all databases.""" - for db in reversed(self._db): - try: - return db.path(package, version) - except KeyError: - pass - else: - raise KeyError( - "%s %s does not exist in any database" % (package, version)) - - def packages(self, all_versions=False): - """Return current package versions in all databases. - - If all_versions=True, return all versions, not just current ones. - """ - seen = set() - for db in reversed(self._db): - writeable = db is self._db[-1] - for package, version, path in \ - db.packages(all_versions=all_versions): - if all_versions: - seen_id = (package, version) - else: - seen_id = package - if seen_id not in seen: - yield package, version, path, writeable - seen.add(seen_id) - - def maybe_remove(self, package, version): - self._db[-1].maybe_remove(package, version) - - def gc(self, verbose=True): - self._db[-1].gc(verbose=verbose) - - def ensure_ownership(self): - self._db[-1].ensure_ownership() diff -Nru click-0.4.16/click/hooks.py click-0.4.17.2/click/hooks.py --- click-0.4.16/click/hooks.py 2014-03-04 15:23:35.000000000 +0000 +++ click-0.4.17.2/click/hooks.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,438 +0,0 @@ -# Copyright (C) 2013 Canonical Ltd. -# Author: Colin Watson - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Click package hooks. - -See doc/hooks.rst for the draft specification. -""" - -from __future__ import print_function - -__metaclass__ = type -__all__ = [ - "ClickHook", - "package_install_hooks", - "run_system_hooks", - "run_user_hooks", - ] - -from functools import partial -import grp -import io -import json -import os -import pwd -import re -from string import Formatter -import subprocess - -from debian.deb822 import Deb822 - -from click import osextras -from click.paths import hooks_dir -from click.user import ClickUser, ClickUsers - - -def _read_manifest_hooks(db, package, version): - if version is None: - return {} - try: - manifest_path = os.path.join( - db.path(package, version), ".click", "info", - "%s.manifest" % package) - with io.open(manifest_path, encoding="UTF-8") as manifest: - return json.load(manifest).get("hooks", {}) - except (KeyError, IOError): - return {} - - -class ClickPatternFormatter(Formatter): - """A Formatter that handles simple $-expansions. - - `${key}` is replaced by the value of the `key` argument; `$$` is - replaced by `$`. Any `$` character not followed by `{...}` or `$` is - preserved intact. - """ - _expansion_re = re.compile(r"\$(?:\$|{(.*?)})") - - def parse(self, format_string): - while True: - match = self._expansion_re.search(format_string) - if match is None: - if format_string: - yield format_string, None, None, None - return - start, end = match.span() - if format_string[match.start():match.end()] == "$$": - yield format_string[:match.start() + 1], None, None, None - else: - yield format_string[:match.start()], match.group(1), "", None - format_string = format_string[match.end():] - - def get_field(self, field_name, args, kwargs): - value = kwargs.get(field_name) - if value is None: - value = "" - return value, field_name - - def possible_expansion(self, s, format_string, *args, **kwargs): - """Check if s is a possible expansion. - - Any (keyword) arguments have the effect of binding some keys to - fixed values; unspecified keys may take any value, and will bind - greedily to the longest possible string. - - If s is a possible expansion, then this method returns a (possibly - empty) dictionary mapping all the unspecified keys to their bound - values. Otherwise, it returns None. - """ - ret = {} - regex_pieces = [] - group_names = [] - for literal_text, field_name, format_spec, conversion in \ - self.parse(format_string): - if literal_text: - regex_pieces.append(re.escape(literal_text)) - if field_name is not None: - if field_name in kwargs: - regex_pieces.append(re.escape(kwargs[field_name])) - else: - regex_pieces.append("(.*)") - group_names.append(field_name) - match = re.match("^%s$" % "".join(regex_pieces), s) - if match is None: - return None - for group in range(len(group_names)): - ret[group_names[group]] = match.group(group + 1) - return ret - - -class ClickHook(Deb822): - _formatter = ClickPatternFormatter() - - def __init__(self, db, name, sequence=None, fields=None, encoding="utf-8"): - super(ClickHook, self).__init__( - sequence=sequence, fields=fields, encoding=encoding) - self.db = db - self.name = name - - @classmethod - def open(cls, db, name): - try: - with open(os.path.join(hooks_dir, "%s.hook" % name)) as f: - return cls(db, name, f) - except IOError: - raise KeyError("No click hook '%s' installed" % name) - - @classmethod - def open_all(cls, db, hook_name=None): - for entry in osextras.listdir_force(hooks_dir): - if not entry.endswith(".hook"): - continue - try: - with open(os.path.join(hooks_dir, entry)) as f: - hook = cls(db, entry[:-5], f) - if hook_name is None or hook.hook_name == hook_name: - yield hook - except IOError: - pass - - @property - def user_level(self): - return self.get("user-level", "no") == "yes" - - @property - def single_version(self): - return self.user_level or self.get("single-version", "no") == "yes" - - @property - def hook_name(self): - return self.get("hook-name", self.name) - - def short_app_id(self, package, app_name): - # TODO: perhaps this check belongs further up the stack somewhere? - if "_" in app_name or "/" in app_name: - raise ValueError( - "Application name '%s' may not contain _ or / characters" % - app_name) - return "%s_%s" % (package, app_name) - - def app_id(self, package, version, app_name): - return "%s_%s" % (self.short_app_id(package, app_name), version) - - def _user_home(self, user): - if user is None: - return None - # TODO: make robust against removed users - # TODO: caching - return pwd.getpwnam(user).pw_dir - - def pattern(self, package, version, app_name, user=None): - app_id = self.app_id(package, version, app_name) - kwargs = { - "id": app_id, - "user": user, - "home": self._user_home(user), - } - if self.single_version: - kwargs["short-id"] = self.short_app_id(package, app_name) - return self._formatter.format(self["pattern"], **kwargs).rstrip(os.sep) - - def _drop_privileges(self, username): - if os.geteuid() != 0: - return - pw = pwd.getpwnam(username) - os.setgroups( - [g.gr_gid for g in grp.getgrall() if username in g.gr_mem]) - # Portability note: this assumes that we have [gs]etres[gu]id, which - # is true on Linux but not necessarily elsewhere. If you need to - # support something else, there are reasonably standard alternatives - # involving other similar calls; see e.g. gnulib/lib/idpriv-drop.c. - os.setresgid(pw.pw_gid, pw.pw_gid, pw.pw_gid) - os.setresuid(pw.pw_uid, pw.pw_uid, pw.pw_uid) - assert os.getresuid() == (pw.pw_uid, pw.pw_uid, pw.pw_uid) - assert os.getresgid() == (pw.pw_gid, pw.pw_gid, pw.pw_gid) - os.environ["HOME"] = pw.pw_dir - os.umask(osextras.get_umask() | 0o002) - - def _run_commands_user(self, user=None): - if self.user_level: - return user - else: - return self["user"] - - def _run_commands(self, user=None): - if "exec" in self: - drop_privileges = partial( - self._drop_privileges, self._run_commands_user(user=user)) - subprocess.check_call( - self["exec"], preexec_fn=drop_privileges, shell=True) - if self.get("trigger", "no") == "yes": - raise NotImplementedError("'Trigger: yes' not yet implemented") - - def _previous_entries(self, user=None): - """Find entries that match the structure of our links.""" - link_dir = os.path.dirname(self.pattern("", "", "", user=user)) - # TODO: This only works if the app ID only appears, at most, in the - # last component of the pattern path. - for previous_entry in osextras.listdir_force(link_dir): - previous_path = os.path.join(link_dir, previous_entry) - previous_exp = self._formatter.possible_expansion( - previous_path, self["pattern"], user=user, - home=self._user_home(user)) - if previous_exp is None or "id" not in previous_exp: - continue - previous_id = previous_exp["id"] - try: - previous_package, previous_app_name, previous_version = ( - previous_id.split("_", 2)) - yield ( - previous_path, - previous_package, previous_version, previous_app_name) - except ValueError: - continue - - def _install_link(self, package, version, app_name, relative_path, - user=None, user_db=None): - """Install a hook symlink. - - This should be called with dropped privileges if necessary. - """ - if self.user_level: - target = os.path.join(user_db.path(package), relative_path) - else: - target = os.path.join( - self.db.path(package, version), relative_path) - link = self.pattern(package, version, app_name, user=user) - if not os.path.islink(link) or os.readlink(link) != target: - osextras.ensuredir(os.path.dirname(link)) - osextras.symlink_force(target, link) - - def install_package(self, package, version, app_name, relative_path, - user=None): - if self.user_level: - user_db = ClickUser(self.db, user=user) - else: - assert user is None - - # Remove previous versions if necessary. - if self.single_version: - for path, p_package, p_version, p_app_name in \ - self._previous_entries(user=user): - if (p_package == package and p_app_name == app_name and - p_version != version): - osextras.unlink_force(path) - - if self.user_level: - with user_db._dropped_privileges(): - self._install_link( - package, version, app_name, relative_path, - user=user, user_db=user_db) - else: - self._install_link(package, version, app_name, relative_path) - self._run_commands(user=user) - - def remove_package(self, package, version, app_name, user=None): - osextras.unlink_force( - self.pattern(package, version, app_name, user=user)) - self._run_commands(user=user) - - def _all_packages(self, user=None): - """Return an iterable of all unpacked packages. - - If running a user-level hook, this returns (package, version, user) - for the current version of each package registered for each user, or - only for a single user if user is not None. - - If running a system-level hook, this returns (package, version, - None) for each version of each unpacked package. - """ - if self.user_level: - if user is not None: - user_db = ClickUser(self.db, user=user) - for package, version in user_db.items(): - yield package, version, user - else: - for user_name, user_db in ClickUsers(self.db).items(): - if user_name.startswith("@"): - continue - for package, version in user_db.items(): - yield package, version, user_name - else: - for package, version, _, _ in self.db.packages(): - yield package, version, None - - def _relevant_apps(self, user=None): - """Return an iterable of all applications relevant for this hook.""" - for package, version, user_name in self._all_packages(user=user): - manifest = _read_manifest_hooks(self.db, package, version) - for app_name, hooks in manifest.items(): - if self.hook_name in hooks: - yield ( - package, version, app_name, user_name, - hooks[self.hook_name]) - - def install(self, user=None): - for package, version, app_name, user_name, relative_path in ( - self._relevant_apps(user=user)): - self.install_package( - package, version, app_name, relative_path, user=user_name) - - def remove(self, user=None): - for package, version, app_name, user_name, _ in ( - self._relevant_apps(user=user)): - self.remove_package(package, version, app_name, user=user_name) - - def sync(self, user=None): - if self.user_level: - user_db = ClickUser(self.db, user=user) - else: - assert user is None - - seen = set() - for package, version, app_name, user_name, relative_path in ( - self._relevant_apps(user=user)): - seen.add((package, version, app_name)) - if self.user_level: - with user_db._dropped_privileges(): - self._install_link( - package, version, app_name, relative_path, - user=user_name, user_db=user_db) - else: - self._install_link(package, version, app_name, relative_path) - for path, package, version, app_name in \ - self._previous_entries(user=user): - if (package, version, app_name) not in seen: - osextras.unlink_force(path) - self._run_commands(user=user) - - -def _app_hooks(hooks): - items = set() - for app_name in hooks: - for hook_name in hooks[app_name]: - items.add((app_name, hook_name)) - return items - - -def package_install_hooks(db, package, old_version, new_version, user=None): - """Run hooks following installation or upgrade of a Click package. - - If user is None, only run system-level hooks. If user is not None, only - run user-level hooks for that user. - """ - old_manifest = _read_manifest_hooks(db, package, old_version) - new_manifest = _read_manifest_hooks(db, package, new_version) - - # Remove any targets for single-version hooks that were in the old - # manifest but not the new one. - for app_name, hook_name in sorted( - _app_hooks(old_manifest) - _app_hooks(new_manifest)): - for hook in ClickHook.open_all(db, hook_name): - if hook.user_level != (user is not None): - continue - if hook.single_version: - hook.remove_package(package, old_version, app_name, user=user) - - for app_name, app_hooks in sorted(new_manifest.items()): - for hook_name, relative_path in sorted(app_hooks.items()): - for hook in ClickHook.open_all(db, hook_name): - if hook.user_level != (user is not None): - continue - hook.install_package( - package, new_version, app_name, relative_path, user=user) - - -def package_remove_hooks(db, package, old_version, user=None): - """Run hooks following removal of a Click package. - - If user is None, only run system-level hooks. If user is not None, only - run user-level hooks for that user. - """ - old_manifest = _read_manifest_hooks(db, package, old_version) - - for app_name, app_hooks in sorted(old_manifest.items()): - for hook_name in sorted(app_hooks): - for hook in ClickHook.open_all(db, hook_name): - if hook.user_level != (user is not None): - continue - hook.remove_package(package, old_version, app_name, user=user) - - -def run_system_hooks(db): - """Run system-level hooks for all installed packages. - - This is useful when starting up from images with preinstalled packages - which may not have had their system-level hooks run properly when - building the image. It is suitable for running at system startup. - """ - db.ensure_ownership() - for hook in ClickHook.open_all(db): - if not hook.user_level: - hook.sync() - - -def run_user_hooks(db, user=None): - """Run user-level hooks for all packages registered for a user. - - This is useful to catch up with packages that may have been preinstalled - and registered for all users. It is suitable for running at session - startup. - """ - if user is None: - user = pwd.getpwuid(os.getuid()).pw_name - for hook in ClickHook.open_all(db): - if hook.user_level: - hook.sync(user=user) diff -Nru click-0.4.16/click/install.py click-0.4.17.2/click/install.py --- click-0.4.16/click/install.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/install.py 2014-03-06 16:38:26.000000000 +0000 @@ -43,12 +43,10 @@ import apt_pkg from debian.debfile import DebFile as _DebFile from debian.debian_support import Version +from gi.repository import Click -from click import osextras -from click.hooks import package_install_hooks from click.paths import frameworks_dir, preload_path from click.preinst import static_preinst_matches -from click.user import ClickUser from click.versions import spec_version @@ -313,12 +311,14 @@ def install(self, path, user=None, all_users=False): package_name, package_version = self.audit(path, check_arch=True) - package_dir = os.path.join(self.db.overlay, package_name) + package_dir = os.path.join(self.db.props.overlay, package_name) inst_dir = os.path.join(package_dir, package_version) - assert os.path.dirname(os.path.dirname(inst_dir)) == self.db.overlay + assert ( + os.path.dirname(os.path.dirname(inst_dir)) == + self.db.props.overlay) - self._check_write_permissions(self.db.overlay) - root_click = os.path.join(self.db.overlay, ".click") + self._check_write_permissions(self.db.props.overlay) + root_click = os.path.join(self.db.props.overlay, ".click") if not os.path.exists(root_click): os.makedirs(root_click) if os.geteuid() == 0: @@ -348,7 +348,7 @@ if "LD_PRELOAD" in env: preloads.append(env["LD_PRELOAD"]) env["LD_PRELOAD"] = " ".join(preloads) - env["CLICK_BASE_DIR"] = self.db.overlay + env["CLICK_BASE_DIR"] = self.db.props.overlay env["CLICK_PACKAGE_PATH"] = path env["CLICK_PACKAGE_FD"] = str(fd.fileno()) env.pop("HOME", None) @@ -379,11 +379,11 @@ old_version = None else: old_version = None - package_install_hooks( + Click.package_install_hooks( self.db, package_name, old_version, package_version) new_path = os.path.join(package_dir, "current.new") - osextras.symlink_force(package_version, new_path) + Click.symlink_force(package_version, new_path) if os.geteuid() == 0: # shutil.chown would be more convenient, but it doesn't support # follow_symlinks=False in Python 3.3. @@ -393,7 +393,10 @@ os.rename(new_path, current_path) if user is not None or all_users: - registry = ClickUser(self.db, user=user, all_users=all_users) + if all_users: + registry = Click.User.for_all_users(self.db) + else: + registry = Click.User.for_user(self.db, name=user) registry.set_version(package_name, package_version) if old_version is not None: diff -Nru click-0.4.16/click/Makefile.am click-0.4.17.2/click/Makefile.am --- click-0.4.16/click/Makefile.am 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/Makefile.am 2014-03-06 16:38:26.000000000 +0000 @@ -1,3 +1,5 @@ +SUBDIRS = tests + noinst_SCRIPTS = paths.py CLEANFILES = $(noinst_SCRIPTS) diff -Nru click-0.4.16/click/osextras.py click-0.4.17.2/click/osextras.py --- click-0.4.16/click/osextras.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/osextras.py 2014-03-06 16:38:26.000000000 +0000 @@ -13,7 +13,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Extra OS-level utility functions.""" +"""Extra OS-level utility functions. + +Usually we can instead use the functions exported from +lib/click/osextras.vala via GObject Introspection. These pure-Python +versions are preserved so that they can be used from code that needs to be +maximally portable: for example, click.build is intended to be usable even +on systems that lack GObject, as long as they have a reasonably recent +version of Python. +""" __all__ = [ 'ensuredir', diff -Nru click-0.4.16/click/paths.py.in click-0.4.17.2/click/paths.py.in --- click-0.4.16/click/paths.py.in 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/paths.py.in 2014-03-06 16:38:26.000000000 +0000 @@ -17,5 +17,3 @@ preload_path = "@pkglibdir@/libclickpreload.so" frameworks_dir = "@pkgdatadir@/frameworks" -hooks_dir = "@pkgdatadir@/hooks" -db_dir = "@sysconfdir@/click/databases" diff -Nru click-0.4.16/click/query.py click-0.4.17.2/click/query.py --- click-0.4.16/click/query.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/query.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -# Copyright (C) 2013 Canonical Ltd. -# Author: Colin Watson - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Query information about installed Click packages.""" - -from __future__ import print_function - -__metaclass__ = type -__all__ = [ - 'find_package_directory', - ] - -import os - - -def _walk_up(path): - while True: - yield path - newpath = os.path.dirname(path) - if newpath == path: - return - path = newpath - - -def find_package_directory(path): - for directory in _walk_up(os.path.realpath(path)): - if os.path.isdir(os.path.join(directory, ".click", "info")): - return directory - break - else: - raise Exception("No package directory found for %s" % path) diff -Nru click-0.4.16/click/tests/config.py.in click-0.4.17.2/click/tests/config.py.in --- click-0.4.16/click/tests/config.py.in 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/click/tests/config.py.in 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,20 @@ +# Copyright (C) 2014 Canonical Ltd. +# Author: Colin Watson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +abs_top_builddir = "@abs_top_builddir@" +STAT_OFFSET_UID = @STAT_OFFSET_UID@ +STAT_OFFSET_GID = @STAT_OFFSET_GID@ +STAT64_OFFSET_UID = @STAT64_OFFSET_UID@ +STAT64_OFFSET_GID = @STAT64_OFFSET_GID@ diff -Nru click-0.4.16/click/tests/gimock.py click-0.4.17.2/click/tests/gimock.py --- click-0.4.16/click/tests/gimock.py 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/click/tests/gimock.py 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,499 @@ +# Copyright (C) 2014 Canonical Ltd. +# Author: Colin Watson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Mock function support based on GObject Introspection. + +(Note to reviewers: I expect to rewrite this from scratch on my own time as +a more generalised set of Python modules for unit testing of C code, +although using similar core ideas. This is a first draft for the purpose of +getting Click's test suite to work expediently, rather than an interface I'm +prepared to commit to long-term.) + +Python is a versatile and concise language for writing tests, and GObject +Introspection (GI) makes it straightforward (often trivial) to bind native +code into Python. However, writing tests for native code quickly runs into +the problem of how to build mock functions. You might reasonably have code +that calls chown(), for instance, and want to test how it's called rather +than worrying about setting up a fakeroot-type environment where chown() +will work. The obvious solution is to use `LD_PRELOAD` wrappers, but there +are various problems to overcome in practice: + + * You can only set up a new `LD_PRELOAD` by going through the run-time + linker; you can't just set it for a single in-process test case. + * Generating the preloaded wrapper involves a fair bit of boilerplate code. + * Having to write per-test mock code in C is inconvenient, and makes it + difficult to get information back out of the mock (such as "how often was + this function called, and with what arguments?"). + +The first problem can be solved by a decorator that knows how to run +individual tests in a subprocess. This is made somewhat more inconvenient +by the fact that there is no way for a context manager's `__enter__` method +to avoid executing the context-managed block other than by throwing an +exception, which makes it hard to silently avoid executing the test case in +the parent process, but we can work around this at the cost of an extra line +of code per invocation. + +For the rest, a combination of GI itself and ctypes can help. We can use GI +to keep track of argument and return types of the mocked C functions in a +reasonably sane way, by parsing header files. We're operating in the other +direction from how GI is normally used, so PyGObject can't deal with +bridging the two calling conventions for us. ctypes can: but we still need +to be careful! We have to construct the callback functions in the child +process, ensure that we keep references to them, and inject function +pointers into the preloaded library via specially-named helper functions; +until those function pointers are set up we must make sure to call the libc +functions instead (since some of them might be called during Python +startup). + +The combination of all of this allows us to bridge C functions somewhat +transparently into Python. This lets you supply a Python function or method +as the mock replacement for a C library function, making it much simpler to +record state. + +It's still not perfect: + + * We're using GI in an upside-down kind of way, and we specifically need + GIR files rather than typelibs so that we can extract the original C + type, so some fiddling is required for each new function you want to + mock. + + * The subprocess arrangements are unavoidably slow and it's possible that + they may cause problems with some test runners. + + * Some C functions (such as `stat`) tend to have multiple underlying entry + points in the C library which must be preloaded independently. + + * You have to be careful about how your libraries are linked, because `ld + -Wl,-Bsymbolic-functions` prevents `LD_PRELOAD` working for intra-library + calls. + + * `ctypes should return composite types from callbacks + `_. The least awful approach for now + seems to be to construct the composite type in question, stash a + reference to it forever, and then return a pointer to it as a void *; we + can only get away with this because tests are by nature relatively + short-lived. + + * The ctypes module's handling of 64-bit pointers is basically just awful. + The right answer is probably to use a different callback-generation + framework entirely (maybe extending PyGObject so that we can get at the + pieces we need), but I've hacked around it for now. + + * It doesn't appear to be possible to install mock replacements for + functions that are called directly from Python code using their GI + wrappers. You can work around this by simply patching the GI wrapper + instead, using `mock.patch`. + +I think the benefits, in terms of local clarity of tests, are worth the +downsides. +""" + +from __future__ import print_function + +__metaclass__ = type +__all__ = ['GIMockTestCase'] + + +import contextlib +import ctypes +import fcntl +from functools import partial +import os +import pickle +import shutil +import subprocess +import sys +import tempfile +from textwrap import dedent +import traceback +import unittest +try: + from unittest import mock +except ImportError: + import mock +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree + +from click.tests.gimock_types import Stat, Stat64 + + +# Borrowed from giscanner.girparser. +CORE_NS = "http://www.gtk.org/introspection/core/1.0" +C_NS = "http://www.gtk.org/introspection/c/1.0" +GLIB_NS = "http://www.gtk.org/introspection/glib/1.0" + + +def _corens(tag): + return '{%s}%s' % (CORE_NS, tag) + + +def _glibns(tag): + return '{%s}%s' % (GLIB_NS, tag) + + +def _cns(tag): + return '{%s}%s' % (C_NS, tag) + + +# Override some c:type annotations that g-ir-scanner gets a bit wrong. +_c_type_override = { + "passwd*": "struct passwd*", + "stat*": "struct stat*", + "stat64*": "struct stat64*", + } + + +# Mapping of GI type name -> ctypes type. +_typemap = { + "GError**": ctypes.c_void_p, + "gboolean": ctypes.c_int, + "gint": ctypes.c_int, + "gint*": ctypes.POINTER(ctypes.c_int), + "gint32": ctypes.c_int32, + "gpointer": ctypes.c_void_p, + "guint": ctypes.c_uint, + "guint8**": ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)), + "guint32": ctypes.c_uint32, + "none": None, + "utf8": ctypes.c_char_p, + "utf8*": ctypes.POINTER(ctypes.c_char_p), + } + + +class GIMockTestCase(unittest.TestCase): + def setUp(self): + super(GIMockTestCase, self).setUp() + self._gimock_temp_dir = tempfile.mkdtemp(prefix="gimock") + self.addCleanup(shutil.rmtree, self._gimock_temp_dir) + self._preload_func_refs = [] + self._composite_refs = [] + self._delegate_funcs = {} + + def tearDown(self): + self._preload_func_refs = [] + self._composite_refs = [] + self._delegate_funcs = {} + + def _gir_get_type(self, obj): + ret = {} + arrayinfo = obj.find(_corens("array")) + if arrayinfo is not None: + typeinfo = arrayinfo.find(_corens("type")) + raw_ctype = arrayinfo.get(_cns("type")) + else: + typeinfo = obj.find(_corens("type")) + raw_ctype = typeinfo.get(_cns("type")) + gi_type = typeinfo.get("name") + if obj.get("direction", "in") == "out": + gi_type += "*" + if arrayinfo is not None: + gi_type += "*" + ret["gi"] = gi_type + ret["c"] = _c_type_override.get(raw_ctype, raw_ctype) + return ret + + def _parse_gir(self, path): + # A very, very crude GIR parser. We might have used + # giscanner.girparser, but it's not importable in Python 3 at the + # moment. + tree = etree.parse(path) + root = tree.getroot() + assert root.tag == _corens("repository") + assert root.get("version") == "1.2" + ns = root.find(_corens("namespace")) + assert ns is not None + funcs = {} + for func in ns.findall(_corens("function")): + name = func.get(_cns("identifier")) + # g-ir-scanner skips identifiers starting with "__", which we + # need in order to mock stat effectively. Work around this. + name = name.replace("under_under_", "__") + headers = None + for attr in func.findall(_corens("attribute")): + if attr.get("name") == "headers": + headers = attr.get("value") + break + rv = func.find(_corens("return-value")) + assert rv is not None + params = [] + paramnode = func.find(_corens("parameters")) + if paramnode is not None: + for param in paramnode.findall(_corens("parameter")): + params.append({ + "name": param.get("name"), + "type": self._gir_get_type(param), + }) + if func.get("throws", "0") == "1": + params.append({ + "name": "error", + "type": { "gi": "GError**", "c": "GError**" }, + }) + funcs[name] = { + "name": name, + "headers": headers, + "rv": self._gir_get_type(rv), + "params": params, + } + return funcs + + def _ctypes_type(self, gi_type): + return _typemap[gi_type["gi"]] + + def make_preloads(self, preloads): + rpreloads = [] + std_headers = set([ + "dlfcn.h", + # Not strictly needed, but convenient for ad-hoc debugging. + "stdio.h", + "stdint.h", + "stdlib.h", + "sys/types.h", + "unistd.h", + ]) + preload_headers = set() + funcs = self._parse_gir("click/tests/preload.gir") + for name, func in preloads.items(): + info = funcs[name] + rpreloads.append([info, func]) + headers = info["headers"] + if headers is not None: + preload_headers.update(headers.split(",")) + if "GIMOCK_SUBPROCESS" in os.environ: + return None, rpreloads + preloads_dir = os.path.join(self._gimock_temp_dir, "_preloads") + os.makedirs(preloads_dir) + c_path = os.path.join(preloads_dir, "gimockpreload.c") + with open(c_path, "w") as c: + print("#define _GNU_SOURCE", file=c) + for header in sorted(std_headers | preload_headers): + print("#include <%s>" % header, file=c) + print(file=c) + for info, _ in rpreloads: + conv = {} + conv["name"] = info["name"] + argtypes = [p["type"]["c"] for p in info["params"]] + argnames = [p["name"] for p in info["params"]] + conv["ret"] = info["rv"]["c"] + conv["bareproto"] = ", ".join(argtypes) + conv["proto"] = ", ".join( + "%s %s" % pair for pair in zip(argtypes, argnames)) + conv["args"] = ", ".join(argnames) + # The delegation scheme used here is needed because trying + # to pass pointers back and forward through ctypes is a + # recipe for having them truncated to 32 bits at the drop of + # a hat. This approach is less obvious but much safer. + print(dedent("""\ + typedef %(ret)s preloadtype_%(name)s (%(bareproto)s); + preloadtype_%(name)s *ctypes_%(name)s = (void *) 0; + preloadtype_%(name)s *real_%(name)s = (void *) 0; + static volatile int delegate_%(name)s = 0; + + extern void _gimock_init_%(name)s (preloadtype_%(name)s *f) + { + ctypes_%(name)s = f; + if (! real_%(name)s) { + /* Retry lookup in case the symbol wasn't + * resolvable until the program under test was + * loaded. + */ + dlerror (); + real_%(name)s = dlsym (RTLD_NEXT, \"%(name)s\"); + if (dlerror ()) _exit (1); + } + } + """) % conv, file=c) + if conv["ret"] == "void": + print(dedent("""\ + void %(name)s (%(proto)s) + { + if (ctypes_%(name)s) { + delegate_%(name)s = 0; + (*ctypes_%(name)s) (%(args)s); + if (! delegate_%(name)s) + return; + } + (*real_%(name)s) (%(args)s); + } + """) % conv, file=c) + else: + print(dedent("""\ + %(ret)s %(name)s (%(proto)s) + { + if (ctypes_%(name)s) { + %(ret)s ret; + delegate_%(name)s = 0; + ret = (*ctypes_%(name)s) (%(args)s); + if (! delegate_%(name)s) + return ret; + } + return (*real_%(name)s) (%(args)s); + } + """) % conv, file=c) + print(dedent("""\ + extern void _gimock_delegate_%(name)s (void) + { + delegate_%(name)s = 1; + } + """) % conv, file=c) + print(dedent("""\ + static void __attribute__ ((constructor)) + gimockpreload_init (void) + { + dlerror (); + """), file=c) + for info, _ in rpreloads: + name = info["name"] + print(" real_%s = dlsym (RTLD_NEXT, \"%s\");" % + (name, name), file=c) + print(" if (dlerror ()) _exit (1);", file=c) + print("}", file=c) + if "GIMOCK_PRELOAD_DEBUG" in os.environ: + with open(c_path) as c: + print(c.read()) + # TODO: Use libtool or similar rather than hardcoding gcc invocation. + lib_path = os.path.join(preloads_dir, "libgimockpreload.so") + cflags = subprocess.check_output([ + "pkg-config", "--cflags", "glib-2.0", "gee-0.8"], + universal_newlines=True).rstrip("\n").split() + subprocess.check_call([ + "gcc", "-O0", "-g", "-shared", "-fPIC", "-DPIC", "-I", "lib/click", + ] + cflags + [ + "-Wl,-soname", "-Wl,libgimockpreload.so", + c_path, "-ldl", "-o", lib_path, + ]) + return lib_path, rpreloads + + # Use as: + # with self.run_in_subprocess("func", ...) as (enter, preloads): + # enter() + # # test case body; preloads["func"] will be a mock.MagicMock + # # instance + @contextlib.contextmanager + def run_in_subprocess(self, *patches): + preloads = {} + for patch in patches: + preloads[patch] = mock.MagicMock() + if preloads: + lib_path, rpreloads = self.make_preloads(preloads) + else: + lib_path, rpreloads = None, None + + class ParentProcess(Exception): + pass + + def helper(lib_path, rpreloads): + if "GIMOCK_SUBPROCESS" in os.environ: + del os.environ["LD_PRELOAD"] + preload_lib = ctypes.cdll.LoadLibrary(lib_path) + delegate_cfunctype = ctypes.CFUNCTYPE(None) + for info, func in rpreloads: + signature = [info["rv"]] + [ + p["type"] for p in info["params"]] + signature = [self._ctypes_type(t) for t in signature] + cfunctype = ctypes.CFUNCTYPE(*signature) + init = getattr( + preload_lib, "_gimock_init_%s" % info["name"]) + cfunc = cfunctype(func) + self._preload_func_refs.append(cfunc) + init(cfunc) + delegate = getattr( + preload_lib, "_gimock_delegate_%s" % info["name"]) + self._delegate_funcs[info["name"]] = delegate_cfunctype( + delegate) + return + rfd, wfd = os.pipe() + # It would be cleaner to use subprocess.Popen(pass_fds=[wfd]), but + # that isn't available in Python 2.7. + if hasattr(os, "set_inheritable"): + os.set_inheritable(wfd, True) + else: + fcntl.fcntl(rfd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) + args = [ + sys.executable, "-m", "unittest", + "%s.%s.%s" % ( + self.__class__.__module__, self.__class__.__name__, + self._testMethodName)] + env = os.environ.copy() + env["GIMOCK_SUBPROCESS"] = str(wfd) + if lib_path is not None: + env["LD_PRELOAD"] = lib_path + subp = subprocess.Popen(args, close_fds=False, env=env) + os.close(wfd) + reader = os.fdopen(rfd, "rb") + subp.communicate() + exctype = pickle.load(reader) + if exctype is not None and issubclass(exctype, AssertionError): + raise AssertionError("Subprocess failed a test!") + elif exctype is not None or subp.returncode != 0: + raise Exception("Subprocess returned an error!") + reader.close() + raise ParentProcess() + + try: + yield partial(helper, lib_path, rpreloads), preloads + if "GIMOCK_SUBPROCESS" in os.environ: + wfd = int(os.environ["GIMOCK_SUBPROCESS"]) + writer = os.fdopen(wfd, "wb") + pickle.dump(None, writer) + writer.flush() + os._exit(0) + except ParentProcess: + pass + except Exception as e: + if "GIMOCK_SUBPROCESS" in os.environ: + wfd = int(os.environ["GIMOCK_SUBPROCESS"]) + writer = os.fdopen(wfd, "wb") + # It would be better to use tblib to pickle the traceback so + # that we can re-raise it properly from the parent process. + # Until that's packaged and available to us, just print the + # traceback and send the exception type. + print() + traceback.print_exc() + pickle.dump(type(e), writer) + writer.flush() + os._exit(1) + else: + raise + + def make_pointer(self, composite): + # Store a reference to a composite type and return a pointer to it, + # working around http://bugs.python.org/issue5710. + self._composite_refs.append(composite) + return ctypes.addressof(composite) + + def make_string(self, s): + # As make_pointer, but for a string. + copied = ctypes.create_string_buffer(s.encode()) + self._composite_refs.append(copied) + return ctypes.addressof(copied) + + def convert_pointer(self, composite_type, address): + # Return a ctypes composite type instance at a given address. + return composite_type.from_address(address) + + def convert_stat_pointer(self, name, address): + # As convert_pointer, but for a "struct stat *" or "struct stat64 *" + # depending on the wrapped function name. + stat_type = {"__xstat": Stat, "__xstat64": Stat64} + return self.convert_pointer(stat_type[name], address) + + def delegate_to_original(self, name): + # Cause the wrapper function to delegate to the original version + # after the callback returns. (Note that the callback still needs + # to return something type-compatible with the declared result type, + # although the return value will otherwise be ignored.) + self._delegate_funcs[name]() diff -Nru click-0.4.16/click/tests/gimock_types.py click-0.4.17.2/click/tests/gimock_types.py --- click-0.4.16/click/tests/gimock_types.py 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/click/tests/gimock_types.py 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,89 @@ +# Copyright (C) 2014 Canonical Ltd. +# Author: Colin Watson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""A collection of variously hacky ctypes definitions for use with gimock.""" + +import ctypes + +from click.tests.config import ( + STAT_OFFSET_GID, + STAT_OFFSET_UID, + STAT64_OFFSET_GID, + STAT64_OFFSET_UID, + ) + + +class Passwd(ctypes.Structure): + _fields_ = [ + ("pw_name", ctypes.c_char_p), + ("pw_passwd", ctypes.c_char_p), + ("pw_uid", ctypes.c_uint32), + ("pw_gid", ctypes.c_uint32), + ("pw_gecos", ctypes.c_char_p), + ("pw_dir", ctypes.c_char_p), + ("pw_shell", ctypes.c_char_p), + ] + + +# TODO: This is pretty awful. The layout of "struct stat" is complicated +# enough that we have to use offsetof() in configure to pick out the fields +# we care about. Fortunately, we only care about a couple of fields, and +# since this is an output parameter it doesn't matter if our structure is +# too short (if we cared about this then we could use AC_CHECK_SIZEOF to +# figure it out). +class Stat(ctypes.Structure): + _pack_ = 1 + _fields_ = [] + _fields_.append( + ("pad0", ctypes.c_ubyte * min(STAT_OFFSET_UID, STAT_OFFSET_GID))) + if STAT_OFFSET_UID < STAT_OFFSET_GID: + _fields_.append(("st_uid", ctypes.c_uint32)) + pad = (STAT_OFFSET_GID - STAT_OFFSET_UID - + ctypes.sizeof(ctypes.c_uint32)) + assert pad >= 0 + if pad > 0: + _fields_.append(("pad1", ctypes.c_ubyte * pad)) + _fields_.append(("st_gid", ctypes.c_uint32)) + else: + _fields_.append(("st_gid", ctypes.c_uint32)) + pad = (STAT_OFFSET_UID - STAT_OFFSET_GID - + ctypes.sizeof(ctypes.c_uint32)) + assert pad >= 0 + if pad > 0: + _fields_.append(("pad1", ctypes.c_ubyte * pad)) + _fields_.append(("st_uid", ctypes.c_uint32)) + + +class Stat64(ctypes.Structure): + _pack_ = 1 + _fields_ = [] + _fields_.append( + ("pad0", ctypes.c_ubyte * min(STAT64_OFFSET_UID, STAT64_OFFSET_GID))) + if STAT64_OFFSET_UID < STAT64_OFFSET_GID: + _fields_.append(("st_uid", ctypes.c_uint32)) + pad = (STAT64_OFFSET_GID - STAT64_OFFSET_UID - + ctypes.sizeof(ctypes.c_uint32)) + assert pad >= 0 + if pad > 0: + _fields_.append(("pad1", ctypes.c_ubyte * pad)) + _fields_.append(("st_gid", ctypes.c_uint32)) + else: + _fields_.append(("st_gid", ctypes.c_uint32)) + pad = (STAT64_OFFSET_UID - STAT64_OFFSET_GID - + ctypes.sizeof(ctypes.c_uint32)) + assert pad >= 0 + if pad > 0: + _fields_.append(("pad1", ctypes.c_ubyte * pad)) + _fields_.append(("st_uid", ctypes.c_uint32)) diff -Nru click-0.4.16/click/tests/helpers.py click-0.4.17.2/click/tests/helpers.py --- click-0.4.16/click/tests/helpers.py 2014-03-04 15:23:35.000000000 +0000 +++ click-0.4.17.2/click/tests/helpers.py 2014-03-06 16:38:26.000000000 +0000 @@ -39,10 +39,12 @@ except ImportError: import mock -from click import osextras +from gi.repository import Click, GLib +from click.tests import gimock -class TestCase(unittest.TestCase): + +class TestCase(gimock.GIMockTestCase): def setUp(self): super(TestCase, self).setUp() self.temp_dir = None @@ -74,6 +76,33 @@ if not hasattr(unittest.TestCase, 'assertRaisesRegex'): assertRaisesRegex = unittest.TestCase.assertRaisesRegexp + def assertRaisesGError(self, domain_name, code, callableObj, + *args, **kwargs): + with self.assertRaises(GLib.GError) as cm: + callableObj(*args, **kwargs) + self.assertEqual(domain_name, cm.exception.domain) + self.assertEqual(code, cm.exception.code) + + def assertRaisesFileError(self, code, callableObj, *args, **kwargs): + self.assertRaisesGError( + "g-file-error-quark", code, callableObj, *args, **kwargs) + + def assertRaisesDatabaseError(self, code, callableObj, *args, **kwargs): + self.assertRaisesGError( + "click_database_error-quark", code, callableObj, *args, **kwargs) + + def assertRaisesHooksError(self, code, callableObj, *args, **kwargs): + self.assertRaisesGError( + "click_hooks_error-quark", code, callableObj, *args, **kwargs) + + def assertRaisesQueryError(self, code, callableObj, *args, **kwargs): + self.assertRaisesGError( + "click_query_error-quark", code, callableObj, *args, **kwargs) + + def assertRaisesUserError(self, code, callableObj, *args, **kwargs): + self.assertRaisesGError( + "click_user_error-quark", code, callableObj, *args, **kwargs) + if not hasattr(mock, "call"): # mock 0.7.2, the version in Ubuntu 12.04 LTS, lacks mock.ANY and @@ -228,14 +257,14 @@ @contextlib.contextmanager def mkfile(path, mode="w"): - osextras.ensuredir(os.path.dirname(path)) + Click.ensuredir(os.path.dirname(path)) with open(path, mode) as f: yield f @contextlib.contextmanager def mkfile_utf8(path, mode="w"): - osextras.ensuredir(os.path.dirname(path)) + Click.ensuredir(os.path.dirname(path)) if sys.version < "3": import codecs with codecs.open(path, mode, "UTF-8") as f: diff -Nru click-0.4.16/click/tests/__init__.py click-0.4.17.2/click/tests/__init__.py --- click-0.4.16/click/tests/__init__.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/tests/__init__.py 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,39 @@ +from __future__ import print_function + +import os +import sys + +from click.tests import config + + +def _append_env_path(envname, value): + if envname in os.environ: + if value in os.environ[envname].split(":"): + return False + os.environ[envname] = "%s:%s" % (os.environ[envname], value) + else: + os.environ[envname] = value + return True + + +# Don't do any of this in interactive mode. +if not hasattr(sys, "ps1"): + _lib_click_dir = os.path.join(config.abs_top_builddir, "lib", "click") + changed = False + if _append_env_path( + "LD_LIBRARY_PATH", os.path.join(_lib_click_dir, ".libs")): + changed = True + if _append_env_path("GI_TYPELIB_PATH", _lib_click_dir): + changed = True + if changed: + # We have to re-exec ourselves to get the dynamic loader to pick up + # the new value of LD_LIBRARY_PATH. + if "-m unittest" in sys.argv[0]: + # unittest does horrible things to sys.argv in the name of + # "usefulness", making the re-exec more painful than it needs to + # be. + os.execvp( + sys.executable, [sys.executable, "-m", "unittest"] + sys.argv[1:]) + else: + os.execvp(sys.executable, [sys.executable] + sys.argv) + os._exit(1) diff -Nru click-0.4.16/click/tests/Makefile.am click-0.4.17.2/click/tests/Makefile.am --- click-0.4.16/click/tests/Makefile.am 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/click/tests/Makefile.am 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,10 @@ +noinst_DATA = preload.gir +CLEANFILES = $(noinst_DATA) + +preload.gir: preload.h + PKG_CONFIG_PATH=$(top_builddir)/lib/click g-ir-scanner \ + -n preload --nsversion 0 -l c \ + --pkg glib-2.0 --pkg gee-0.8 --pkg click-0.4 \ + -I$(top_builddir)/lib/click -L$(top_builddir)/lib/click \ + --accept-unprefixed --warn-all \ + $< --output $@ diff -Nru click-0.4.16/click/tests/preload.h click-0.4.17.2/click/tests/preload.h --- click-0.4.16/click/tests/preload.h 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/click/tests/preload.h 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,110 @@ +#include +#include + +#include + +#include "click.h" + +/** + * chown: + * + * Attributes: (headers unistd.h) + */ +extern int chown (const char *file, uid_t owner, gid_t group); + +/* Workaround for g-ir-scanner not picking up the type properly: mode_t is + * uint32_t on all glibc platforms. + */ +/** + * mkdir: + * @mode: (type guint32) + * + * Attributes: (headers sys/stat.h,sys/types.h) + */ +extern int mkdir (const char *pathname, mode_t mode); + +/** + * getpwnam: + * + * Attributes: (headers sys/types.h,pwd.h) + * Returns: (transfer none): + */ +extern struct passwd *getpwnam (const char *name); + +/** + * under_under_xstat: + * + * Attributes: (headers sys/types.h,sys/stat.h,unistd.h) + */ +extern int under_under_xstat (int ver, const char *pathname, struct stat *buf); + +/** + * under_under_xstat64: + * + * Attributes: (headers sys/types.h,sys/stat.h,unistd.h) + */ +extern int under_under_xstat64 (int ver, const char *pathname, struct stat64 *buf); + +const gchar *g_get_user_name (void); + +/** + * g_spawn_sync: + * @argv: (array zero-terminated=1): + * @envp: (array zero-terminated=1): + * @flags: (type gint) + * @child_setup: (type gpointer) + * @standard_output: (out) (array zero-terminated=1) (element-type guint8): + * @standard_error: (out) (array zero-terminated=1) (element-type guint8): + * @exit_status: (out): + * + * Attributes: (headers glib.h) + */ +gboolean g_spawn_sync (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GSpawnChildSetupFunc child_setup, + gpointer user_data, + gchar **standard_output, + gchar **standard_error, + gint *exit_status, + GError **error); + +/** + * click_find_on_path: + * + * Attributes: (headers glib.h) + */ +gboolean click_find_on_path (const gchar *command); + +/** + * click_get_db_dir: + * + * Attributes: (headers glib.h) + */ +gchar *click_get_db_dir (void); + +/** + * click_get_hooks_dir: + * + * Attributes: (headers glib.h) + */ +gchar *click_get_hooks_dir (void); + +/** + * click_get_user_home: + * + * Attributes: (headers glib.h) + */ +gchar *click_get_user_home (const gchar *user_name); + +/** + * click_package_install_hooks: + * @db: (type gpointer) + * + * Attributes: (headers glib.h,click.h) + */ +void click_package_install_hooks (ClickDB *db, const gchar *package, + const gchar *old_version, + const gchar *new_version, + const gchar *user_name, GError **error); diff -Nru click-0.4.16/click/tests/test_database.py click-0.4.17.2/click/tests/test_database.py --- click-0.4.16/click/tests/test_database.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/tests/test_database.py 2014-03-06 16:38:26.000000000 +0000 @@ -24,45 +24,46 @@ ] +from functools import partial +from itertools import takewhile import json import os -from click.database import ClickDB -from click.tests.helpers import TestCase, mkfile, mock, touch +from gi.repository import Click - -class MockPasswd: - def __init__(self, pw_uid, pw_gid): - self.pw_uid = pw_uid - self.pw_gid = pw_gid - - -class MockStatResult: - original_stat = os.stat - - def __init__(self, path, **override): - self.st = self.original_stat(path) - self.override = override - - def __getattr__(self, name): - if name in self.override: - return self.override[name] - else: - return getattr(self.st, name) +from click.tests.gimock_types import Passwd +from click.tests.helpers import TestCase, mkfile, touch class TestClickSingleDB(TestCase): def setUp(self): super(TestClickSingleDB, self).setUp() self.use_temp_dir() - self.master_db = ClickDB(extra_root=self.temp_dir, use_system=False) - self.db = self.master_db._db[-1] + self.master_db = Click.DB() + self.master_db.add(self.temp_dir) + self.db = self.master_db.get(self.master_db.props.size - 1) + self.spawn_calls = [] + + def g_spawn_sync_side_effect(self, status_map, working_directory, argv, + envp, flags, child_setup, user_data, + standard_output, standard_error, exit_status, + error): + self.spawn_calls.append(list(takewhile(lambda x: x is not None, argv))) + if argv[0] in status_map: + exit_status[0] = status_map[argv[0]] + else: + self.delegate_to_original("g_spawn_sync") + return 0 + + def _installed_packages_tuplify(self, ip): + return [(p.props.package, p.props.version, p.props.path) for p in ip] def test_path(self): path = os.path.join(self.temp_dir, "a", "1.0") os.makedirs(path) - self.assertEqual(path, self.db.path("a", "1.0")) - self.assertRaises(KeyError, self.db.path, "a", "1.1") + self.assertEqual(path, self.db.get_path("a", "1.0")) + self.assertRaisesDatabaseError( + Click.DatabaseError.DOES_NOT_EXIST, self.db.get_path, "a", "1.1") def test_packages_current(self): os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) @@ -76,7 +77,8 @@ self.assertEqual([ ("a", "1.1", a_current), ("b", "0.1", b_current), - ], list(self.db.packages())) + ], self._installed_packages_tuplify( + self.db.get_packages(all_versions=False))) def test_packages_all(self): os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) @@ -90,120 +92,145 @@ ("a", "1.1", os.path.join(self.temp_dir, "a", "1.1")), ("b", "0.1", os.path.join(self.temp_dir, "b", "0.1")), ("c", "2.0", os.path.join(self.temp_dir, "c", "2.0")), - ], list(self.db.packages(all_versions=True))) + ], self._installed_packages_tuplify( + self.db.get_packages(all_versions=True))) - @mock.patch("subprocess.call") - def test_app_running(self, mock_call): - mock_call.return_value = 0 - self.assertTrue(self.db._app_running("foo", "bar", "1.0")) - mock_call.assert_called_once_with( - ["upstart-app-pid", "foo_bar_1.0"], stdout=mock.ANY) - mock_call.return_value = 1 - self.assertFalse(self.db._app_running("foo", "bar", "1.0")) - - @mock.patch("click.osextras.find_on_path") - @mock.patch("subprocess.call") - def test_any_app_running(self, mock_call, mock_find_on_path): - manifest_path = os.path.join( - self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") - with mkfile(manifest_path) as manifest: - json.dump({"hooks": {"a-app": {}}}, manifest) - mock_call.return_value = 0 - mock_find_on_path.return_value = False - self.assertFalse(self.db._any_app_running("a", "1.0")) - mock_find_on_path.return_value = True - self.assertTrue(self.db._any_app_running("a", "1.0")) - mock_call.assert_called_once_with( - ["upstart-app-pid", "a_a-app_1.0"], stdout=mock.ANY) - mock_call.return_value = 1 - self.assertFalse(self.db._any_app_running("a", "1.0")) - - @mock.patch("click.osextras.find_on_path") - @mock.patch("subprocess.call") - def test_maybe_remove_registered(self, mock_call, mock_find_on_path): - version_path = os.path.join(self.temp_dir, "a", "1.0") - manifest_path = os.path.join( - version_path, ".click", "info", "a.manifest") - with mkfile(manifest_path) as manifest: - json.dump({"hooks": {"a-app": {}}}, manifest) - user_path = os.path.join( - self.temp_dir, ".click", "users", "test-user", "a") - os.makedirs(os.path.dirname(user_path)) - os.symlink(version_path, user_path) - mock_call.return_value = 0 - mock_find_on_path.return_value = True - self.db.maybe_remove("a", "1.0") - self.assertTrue(os.path.exists(version_path)) - self.assertTrue(os.path.exists(user_path)) - - @mock.patch("click.osextras.find_on_path") - @mock.patch("subprocess.call") - def test_maybe_remove_running(self, mock_call, mock_find_on_path): - version_path = os.path.join(self.temp_dir, "a", "1.0") - manifest_path = os.path.join( - version_path, ".click", "info", "a.manifest") - with mkfile(manifest_path) as manifest: - json.dump({"hooks": {"a-app": {}}}, manifest) - mock_call.return_value = 0 - mock_find_on_path.return_value = True - self.db.maybe_remove("a", "1.0") - gcinuse_path = os.path.join( - self.temp_dir, ".click", "users", "@gcinuse", "a") - self.assertTrue(os.path.islink(gcinuse_path)) - self.assertEqual(version_path, os.readlink(gcinuse_path)) - self.assertTrue(os.path.exists(version_path)) - self.db.maybe_remove("a", "1.0") - self.assertTrue(os.path.islink(gcinuse_path)) - self.assertEqual(version_path, os.readlink(gcinuse_path)) - self.assertTrue(os.path.exists(version_path)) - - @mock.patch("click.osextras.find_on_path") - @mock.patch("subprocess.call") - def test_maybe_remove_not_running(self, mock_call, mock_find_on_path): - version_path = os.path.join(self.temp_dir, "a", "1.0") - manifest_path = os.path.join( - version_path, ".click", "info", "a.manifest") - with mkfile(manifest_path) as manifest: - json.dump({"hooks": {"a-app": {}}}, manifest) - current_path = os.path.join(self.temp_dir, "a", "current") - os.symlink("1.0", current_path) - mock_call.return_value = 1 - mock_find_on_path.return_value = True - self.db.maybe_remove("a", "1.0") - gcinuse_path = os.path.join( - self.temp_dir, ".click", "users", "@gcinuse", "a") - self.assertFalse(os.path.islink(gcinuse_path)) - self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "a"))) - - @mock.patch("click.osextras.find_on_path") - @mock.patch("subprocess.call") - def test_gc(self, mock_call, mock_find_on_path): - a_path = os.path.join(self.temp_dir, "a", "1.0") - a_manifest_path = os.path.join(a_path, ".click", "info", "a.manifest") - with mkfile(a_manifest_path) as manifest: - json.dump({"hooks": {"a-app": {}}}, manifest) - b_path = os.path.join(self.temp_dir, "b", "1.0") - b_manifest_path = os.path.join(b_path, ".click", "info", "b.manifest") - with mkfile(b_manifest_path) as manifest: - json.dump({"hooks": {"b-app": {}}}, manifest) - c_path = os.path.join(self.temp_dir, "c", "1.0") - c_manifest_path = os.path.join(c_path, ".click", "info", "c.manifest") - with mkfile(c_manifest_path) as manifest: - json.dump({"hooks": {"c-app": {}}}, manifest) - a_user_path = os.path.join( - self.temp_dir, ".click", "users", "test-user", "a") - os.makedirs(os.path.dirname(a_user_path)) - os.symlink(a_path, a_user_path) - b_gcinuse_path = os.path.join( - self.temp_dir, ".click", "users", "@gcinuse", "b") - os.makedirs(os.path.dirname(b_gcinuse_path)) - os.symlink(b_path, b_gcinuse_path) - mock_call.return_value = 1 - mock_find_on_path.return_value = True - self.db.gc(verbose=False) - self.assertTrue(os.path.exists(a_path)) - self.assertFalse(os.path.exists(b_path)) - self.assertTrue(os.path.exists(c_path)) + def test_app_running(self): + with self.run_in_subprocess("g_spawn_sync") as (enter, preloads): + enter() + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0}) + self.assertTrue(self.db.app_running("foo", "bar", "1.0")) + self.assertEqual( + [[b"upstart-app-pid", b"foo_bar_1.0"]], self.spawn_calls) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8}) + self.assertFalse(self.db.app_running("foo", "bar", "1.0")) + + def test_any_app_running(self): + with self.run_in_subprocess( + "click_find_on_path", "g_spawn_sync", + ) as (enter, preloads): + enter() + manifest_path = os.path.join( + self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") + with mkfile(manifest_path) as manifest: + json.dump({"hooks": {"a-app": {}}}, manifest) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0}) + preloads["click_find_on_path"].return_value = False + self.assertFalse(self.db.any_app_running("a", "1.0")) + preloads["click_find_on_path"].return_value = True + self.assertTrue(self.db.any_app_running("a", "1.0")) + self.assertEqual( + [[b"upstart-app-pid", b"a_a-app_1.0"]], self.spawn_calls) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8}) + self.assertFalse(self.db.any_app_running("a", "1.0")) + + def test_maybe_remove_registered(self): + with self.run_in_subprocess( + "click_find_on_path", "g_spawn_sync", + ) as (enter, preloads): + enter() + version_path = os.path.join(self.temp_dir, "a", "1.0") + manifest_path = os.path.join( + version_path, ".click", "info", "a.manifest") + with mkfile(manifest_path) as manifest: + json.dump({"hooks": {"a-app": {}}}, manifest) + user_path = os.path.join( + self.temp_dir, ".click", "users", "test-user", "a") + os.makedirs(os.path.dirname(user_path)) + os.symlink(version_path, user_path) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0}) + preloads["click_find_on_path"].return_value = True + self.db.maybe_remove("a", "1.0") + self.assertTrue(os.path.exists(version_path)) + self.assertTrue(os.path.exists(user_path)) + + def test_maybe_remove_running(self): + with self.run_in_subprocess( + "click_find_on_path", "g_spawn_sync", + ) as (enter, preloads): + enter() + version_path = os.path.join(self.temp_dir, "a", "1.0") + manifest_path = os.path.join( + version_path, ".click", "info", "a.manifest") + with mkfile(manifest_path) as manifest: + json.dump({"hooks": {"a-app": {}}}, manifest) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0}) + preloads["click_find_on_path"].return_value = True + self.db.maybe_remove("a", "1.0") + gcinuse_path = os.path.join( + self.temp_dir, ".click", "users", "@gcinuse", "a") + self.assertTrue(os.path.islink(gcinuse_path)) + self.assertEqual(version_path, os.readlink(gcinuse_path)) + self.assertTrue(os.path.exists(version_path)) + self.db.maybe_remove("a", "1.0") + self.assertTrue(os.path.islink(gcinuse_path)) + self.assertEqual(version_path, os.readlink(gcinuse_path)) + self.assertTrue(os.path.exists(version_path)) + + def test_maybe_remove_not_running(self): + with self.run_in_subprocess( + "click_find_on_path", "g_spawn_sync", + ) as (enter, preloads): + enter() + os.environ["TEST_QUIET"] = "1" + version_path = os.path.join(self.temp_dir, "a", "1.0") + manifest_path = os.path.join( + version_path, ".click", "info", "a.manifest") + with mkfile(manifest_path) as manifest: + json.dump({"hooks": {"a-app": {}}}, manifest) + current_path = os.path.join(self.temp_dir, "a", "current") + os.symlink("1.0", current_path) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8}) + preloads["click_find_on_path"].return_value = True + self.db.maybe_remove("a", "1.0") + gcinuse_path = os.path.join( + self.temp_dir, ".click", "users", "@gcinuse", "a") + self.assertFalse(os.path.islink(gcinuse_path)) + self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "a"))) + + def test_gc(self): + with self.run_in_subprocess( + "click_find_on_path", "g_spawn_sync", + ) as (enter, preloads): + enter() + os.environ["TEST_QUIET"] = "1" + a_path = os.path.join(self.temp_dir, "a", "1.0") + a_manifest_path = os.path.join( + a_path, ".click", "info", "a.manifest") + with mkfile(a_manifest_path) as manifest: + json.dump({"hooks": {"a-app": {}}}, manifest) + b_path = os.path.join(self.temp_dir, "b", "1.0") + b_manifest_path = os.path.join( + b_path, ".click", "info", "b.manifest") + with mkfile(b_manifest_path) as manifest: + json.dump({"hooks": {"b-app": {}}}, manifest) + c_path = os.path.join(self.temp_dir, "c", "1.0") + c_manifest_path = os.path.join( + c_path, ".click", "info", "c.manifest") + with mkfile(c_manifest_path) as manifest: + json.dump({"hooks": {"c-app": {}}}, manifest) + a_user_path = os.path.join( + self.temp_dir, ".click", "users", "test-user", "a") + os.makedirs(os.path.dirname(a_user_path)) + os.symlink(a_path, a_user_path) + b_gcinuse_path = os.path.join( + self.temp_dir, ".click", "users", "@gcinuse", "b") + os.makedirs(os.path.dirname(b_gcinuse_path)) + os.symlink(b_path, b_gcinuse_path) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8}) + preloads["click_find_on_path"].return_value = True + self.db.gc() + self.assertTrue(os.path.exists(a_path)) + self.assertFalse(os.path.exists(b_path)) + self.assertTrue(os.path.exists(c_path)) def _make_ownership_test(self): path = os.path.join(self.temp_dir, "a", "1.0") @@ -215,60 +242,80 @@ os.symlink(path, user_path) touch(os.path.join(self.temp_dir, ".click", "log")) - def test_clickpkg_paths(self): - self._make_ownership_test() - self.assertCountEqual([ - self.temp_dir, - os.path.join(self.temp_dir, ".click"), - os.path.join(self.temp_dir, ".click", "log"), - os.path.join(self.temp_dir, ".click", "users"), - os.path.join(self.temp_dir, "a"), - os.path.join(self.temp_dir, "a", "1.0"), - os.path.join(self.temp_dir, "a", "1.0", ".click"), - os.path.join(self.temp_dir, "a", "1.0", ".click", "info"), - os.path.join( - self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"), - os.path.join(self.temp_dir, "a", "current"), - ], list(self.db._clickpkg_paths())) - - @mock.patch("pwd.getpwnam") - @mock.patch("os.chown") - def test_ensure_ownership_quick_if_correct(self, mock_chown, - mock_getpwnam): - mock_getpwnam.return_value = MockPasswd(pw_uid=1, pw_gid=1) - self._make_ownership_test() - with mock.patch("os.stat") as mock_stat: - mock_stat.side_effect = ( - lambda path, *args, **kwargs: MockStatResult( - path, st_uid=1, st_gid=1)) + def _set_stat_side_effect(self, preloads, side_effect, limit): + limit = limit.encode() + preloads["__xstat"].side_effect = ( + lambda ver, path, buf: side_effect( + "__xstat", limit, ver, path, buf)) + preloads["__xstat64"].side_effect = ( + lambda ver, path, buf: side_effect( + "__xstat64", limit, ver, path, buf)) + + def test_ensure_ownership_quick_if_correct(self): + def stat_side_effect(name, limit, ver, path, buf): + st = self.convert_stat_pointer(name, buf) + if path == limit: + st.st_uid = 1 + st.st_gid = 1 + return 0 + else: + self.delegate_to_original(name) + return -1 + + with self.run_in_subprocess( + "chown", "getpwnam", "__xstat", "__xstat64", + ) as (enter, preloads): + enter() + preloads["getpwnam"].side_effect = ( + lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1))) + self._set_stat_side_effect( + preloads, stat_side_effect, self.db.props.root) + + self._make_ownership_test() self.db.ensure_ownership() - self.assertFalse(mock_chown.called) + self.assertFalse(preloads["chown"].called) + + def test_ensure_ownership(self): + def stat_side_effect(name, limit, ver, path, buf): + st = self.convert_stat_pointer(name, buf) + if path == limit: + st.st_uid = 2 + st.st_gid = 2 + return 0 + else: + self.delegate_to_original(name) + return -1 + + with self.run_in_subprocess( + "chown", "getpwnam", "__xstat", "__xstat64", + ) as (enter, preloads): + enter() + preloads["getpwnam"].side_effect = ( + lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1))) + self._set_stat_side_effect( + preloads, stat_side_effect, self.db.props.root) - @mock.patch("pwd.getpwnam") - @mock.patch("os.chown") - def test_ensure_ownership(self, mock_chown, mock_getpwnam): - mock_getpwnam.return_value = MockPasswd(pw_uid=1, pw_gid=1) - self._make_ownership_test() - with mock.patch("os.stat") as mock_stat: - mock_stat.side_effect = ( - lambda path, *args, **kwargs: MockStatResult( - path, st_uid=2, st_gid=2)) + self._make_ownership_test() self.db.ensure_ownership() - self.assertCountEqual([ - self.temp_dir, - os.path.join(self.temp_dir, ".click"), - os.path.join(self.temp_dir, ".click", "log"), - os.path.join(self.temp_dir, ".click", "users"), - os.path.join(self.temp_dir, "a"), - os.path.join(self.temp_dir, "a", "1.0"), - os.path.join(self.temp_dir, "a", "1.0", ".click"), - os.path.join(self.temp_dir, "a", "1.0", ".click", "info"), - os.path.join( - self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"), - os.path.join(self.temp_dir, "a", "current"), - ], [args[0][0] for args in mock_chown.call_args_list]) - self.assertCountEqual( - [(1, 1)], set(args[0][1:] for args in mock_chown.call_args_list)) + expected_paths = [ + self.temp_dir, + os.path.join(self.temp_dir, ".click"), + os.path.join(self.temp_dir, ".click", "log"), + os.path.join(self.temp_dir, ".click", "users"), + os.path.join(self.temp_dir, "a"), + os.path.join(self.temp_dir, "a", "1.0"), + os.path.join(self.temp_dir, "a", "1.0", ".click"), + os.path.join(self.temp_dir, "a", "1.0", ".click", "info"), + os.path.join( + self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"), + os.path.join(self.temp_dir, "a", "current"), + ] + self.assertCountEqual( + [path.encode() for path in expected_paths], + [args[0][0] for args in preloads["chown"].call_args_list]) + self.assertCountEqual( + [(1, 1)], + set(args[0][1:] for args in preloads["chown"].call_args_list)) class TestClickDB(TestCase): @@ -276,6 +323,11 @@ super(TestClickDB, self).setUp() self.use_temp_dir() + def _installed_packages_tuplify(self, ip): + return [ + (p.props.package, p.props.version, p.props.path, p.props.writeable) + for p in ip] + def test_read_configuration(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) @@ -283,23 +335,27 @@ with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = /b", file=b) - db = ClickDB(extra_root="/c", override_db_dir=self.temp_dir) - self.assertEqual(3, len(db)) - self.assertEqual(["/a", "/b", "/c"], [d.root for d in db]) + db = Click.DB() + db.read(db_dir=self.temp_dir) + db.add("/c") + self.assertEqual(3, db.props.size) + self.assertEqual( + ["/a", "/b", "/c"], + [db.get(i).props.root for i in range(db.props.size)]) - def test_no_use_system(self): + def test_no_read(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = /a", file=a) - db = ClickDB(use_system=False, override_db_dir=self.temp_dir) - self.assertEqual(0, len(db)) + db = Click.DB() + self.assertEqual(0, db.props.size) def test_add(self): - db = ClickDB(use_system=False) - self.assertEqual(0, len(db)) + db = Click.DB() + self.assertEqual(0, db.props.size) db.add("/new/root") - self.assertEqual(1, len(db)) - self.assertEqual(["/new/root"], [d.root for d in db]) + self.assertEqual(1, db.props.size) + self.assertEqual("/new/root", db.get(0).props.root) def test_overlay(self): with open(os.path.join(self.temp_dir, "00_custom.conf"), "w") as f: @@ -308,8 +364,9 @@ with open(os.path.join(self.temp_dir, "99_default.conf"), "w") as f: print("[Click Database]", file=f) print("root = /opt/click.ubuntu.com", file=f) - db = ClickDB(override_db_dir=self.temp_dir) - self.assertEqual("/opt/click.ubuntu.com", db.overlay) + db = Click.DB() + db.read(db_dir=self.temp_dir) + self.assertEqual("/opt/click.ubuntu.com", db.props.overlay) def test_path(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: @@ -318,21 +375,24 @@ with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) - db = ClickDB(override_db_dir=self.temp_dir) - self.assertRaises(KeyError, db.path, "pkg", "1.0") + db = Click.DB() + db.read(db_dir=self.temp_dir) + self.assertRaisesDatabaseError( + Click.DatabaseError.DOES_NOT_EXIST, db.get_path, "pkg", "1.0") os.makedirs(os.path.join(self.temp_dir, "a", "pkg", "1.0")) self.assertEqual( os.path.join(self.temp_dir, "a", "pkg", "1.0"), - db.path("pkg", "1.0")) - self.assertRaises(KeyError, db.path, "pkg", "1.1") + db.get_path("pkg", "1.0")) + self.assertRaisesDatabaseError( + Click.DatabaseError.DOES_NOT_EXIST, db.get_path, "pkg", "1.1") os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.0")) self.assertEqual( os.path.join(self.temp_dir, "b", "pkg", "1.0"), - db.path("pkg", "1.0")) + db.get_path("pkg", "1.0")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.1")) self.assertEqual( os.path.join(self.temp_dir, "b", "pkg", "1.1"), - db.path("pkg", "1.1")) + db.get_path("pkg", "1.1")) def test_packages_current(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: @@ -341,8 +401,9 @@ with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) - db = ClickDB(override_db_dir=self.temp_dir) - self.assertEqual([], list(db.packages())) + db = Click.DB() + db.read(db_dir=self.temp_dir) + self.assertEqual([], list(db.get_packages(all_versions=False))) os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0")) os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1")) @@ -354,7 +415,8 @@ self.assertEqual([ ("pkg1", "1.1", pkg1_current, True), ("pkg2", "0.1", pkg2_current, True), - ], list(db.packages())) + ], self._installed_packages_tuplify( + db.get_packages(all_versions=False))) def test_packages_all(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: @@ -363,8 +425,9 @@ with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) - db = ClickDB(override_db_dir=self.temp_dir) - self.assertEqual([], list(db.packages())) + db = Click.DB() + db.read(db_dir=self.temp_dir) + self.assertEqual([], list(db.get_packages(all_versions=False))) os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0")) os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1")) @@ -378,4 +441,5 @@ True), ("pkg1", "1.0", os.path.join(self.temp_dir, "a", "pkg1", "1.0"), False), - ], list(db.packages(all_versions=True))) + ], self._installed_packages_tuplify( + db.get_packages(all_versions=True))) diff -Nru click-0.4.16/click/tests/test_hooks.py click-0.4.17.2/click/tests/test_hooks.py --- click-0.4.16/click/tests/test_hooks.py 2014-03-04 15:23:35.000000000 +0000 +++ click-0.4.17.2/click/tests/test_hooks.py 2014-03-06 16:38:26.000000000 +0000 @@ -27,824 +27,957 @@ ] -import contextlib +from functools import partial +from itertools import takewhile import json import os from textwrap import dedent -from click import hooks -from click.database import ClickDB -from click.hooks import ( - ClickHook, - ClickPatternFormatter, - package_install_hooks, - package_remove_hooks, - ) -from click.user import ClickUser -from click.tests.helpers import TestCase, mkfile, mkfile_utf8, mock - - -@contextlib.contextmanager -def temp_hooks_dir(new_dir): - old_dir = hooks.hooks_dir - try: - hooks.hooks_dir = new_dir - yield - finally: - hooks.hooks_dir = old_dir +from gi.repository import Click, GLib + +from click.tests.gimock_types import Passwd +from click.tests.helpers import TestCase, mkfile, mkfile_utf8 class TestClickPatternFormatter(TestCase): - def setUp(self): - super(TestClickPatternFormatter, self).setUp() - self.formatter = ClickPatternFormatter() + def _make_variant(self, **kwargs): + # pygobject's Variant creator can't handle maybe types, so we have + # to do this by hand. + builder = GLib.VariantBuilder.new(GLib.VariantType.new("a{sms}")) + for key, value in kwargs.items(): + entry = GLib.VariantBuilder.new(GLib.VariantType.new("{sms}")) + entry.add_value(GLib.Variant.new_string(key)) + entry.add_value(GLib.Variant.new_maybe( + GLib.VariantType.new("s"), + None if value is None else GLib.Variant.new_string(value))) + builder.add_value(entry.end()) + return builder.end() def test_expands_provided_keys(self): self.assertEqual( - "foo.bar", self.formatter.format("foo.${key}", key="bar")) + "foo.bar", + Click.pattern_format("foo.${key}", self._make_variant(key="bar"))) self.assertEqual( "foo.barbaz", - self.formatter.format( - "foo.${key1}${key2}", key1="bar", key2="baz")) + Click.pattern_format( + "foo.${key1}${key2}", + self._make_variant(key1="bar", key2="baz"))) def test_expands_missing_keys_to_empty_string(self): - self.assertEqual("xy", self.formatter.format("x${key}y")) + self.assertEqual( + "xy", Click.pattern_format("x${key}y", self._make_variant())) def test_preserves_unmatched_dollar(self): - self.assertEqual("$", self.formatter.format("$")) - self.assertEqual("$ {foo}", self.formatter.format("$ {foo}")) - self.assertEqual("x${y", self.formatter.format("${key}${y", key="x")) + self.assertEqual("$", Click.pattern_format("$", self._make_variant())) + self.assertEqual( + "$ {foo}", Click.pattern_format("$ {foo}", self._make_variant())) + self.assertEqual( + "x${y", + Click.pattern_format("${key}${y", self._make_variant(key="x"))) def test_double_dollar(self): - self.assertEqual("$", self.formatter.format("$$")) - self.assertEqual("${foo}", self.formatter.format("$${foo}")) - self.assertEqual("x$y", self.formatter.format("x$$${key}", key="y")) + self.assertEqual("$", Click.pattern_format("$$", self._make_variant())) + self.assertEqual( + "${foo}", Click.pattern_format("$${foo}", self._make_variant())) + self.assertEqual( + "x$y", + Click.pattern_format("x$$${key}", self._make_variant(key="y"))) def test_possible_expansion(self): self.assertEqual( {"id": "abc"}, - self.formatter.possible_expansion( - "x_abc_1", "x_${id}_${num}", num="1")) + Click.pattern_possible_expansion( + "x_abc_1", "x_${id}_${num}", + self._make_variant(num="1")).unpack()) self.assertIsNone( - self.formatter.possible_expansion( - "x_abc_1", "x_${id}_${num}", num="2")) + Click.pattern_possible_expansion( + "x_abc_1", "x_${id}_${num}", self._make_variant(num="2"))) class TestClickHookBase(TestCase): def setUp(self): super(TestClickHookBase, self).setUp() self.use_temp_dir() - self.db = ClickDB(self.temp_dir) + self.db = Click.DB() + self.db.add(self.temp_dir) + self.spawn_calls = [] + + def _setup_hooks_dir(self, preloads, hooks_dir=None): + if hooks_dir is None: + hooks_dir = self.temp_dir + preloads["click_get_hooks_dir"].side_effect = ( + lambda: self.make_string(hooks_dir)) + + def g_spawn_sync_side_effect(self, status_map, working_directory, argv, + envp, flags, child_setup, user_data, + standard_output, standard_error, exit_status, + error): + self.spawn_calls.append(list(takewhile(lambda x: x is not None, argv))) + if argv[0] in status_map: + exit_status[0] = status_map[argv[0]] + else: + self.delegate_to_original("g_spawn_sync") + return 0 class TestClickHookSystemLevel(TestClickHookBase): def test_open(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print(dedent("""\ - Pattern: /usr/share/test/${id}.test - # Comment - Exec: test-update - User: root - """), file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertCountEqual(["Pattern", "Exec", "User"], hook.keys()) - self.assertEqual("/usr/share/test/${id}.test", hook["pattern"]) - self.assertEqual("test-update", hook["exec"]) - self.assertFalse(hook.user_level) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print(dedent("""\ + Pattern: /usr/share/test/${id}.test + # Comment + Exec: test-update + User: root + """), file=f) + hook = Click.Hook.open(self.db, "test") + self.assertCountEqual( + ["pattern", "exec", "user"], hook.get_fields()) + self.assertEqual( + "/usr/share/test/${id}.test", hook.get_field("pattern")) + self.assertEqual("test-update", hook.get_field("exec")) + self.assertFalse(hook.props.is_user_level) def test_hook_name_absent(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: /usr/share/test/${id}.test", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertEqual("test", hook.hook_name) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Pattern: /usr/share/test/${id}.test", file=f) + hook = Click.Hook.open(self.db, "test") + self.assertEqual("test", hook.get_hook_name()) def test_hook_name_present(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: /usr/share/test/${id}.test", file=f) - print("Hook-Name: other", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertEqual("other", hook.hook_name) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Pattern: /usr/share/test/${id}.test", file=f) + print("Hook-Name: other", file=f) + hook = Click.Hook.open(self.db, "test") + self.assertEqual("other", hook.get_hook_name()) def test_invalid_app_id(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print(dedent("""\ - Pattern: /usr/share/test/${id}.test - # Comment - Exec: test-update - User: root - """), file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertRaises( - ValueError, hook.app_id, "package", "0.1", "app_name") - self.assertRaises( - ValueError, hook.app_id, "package", "0.1", "app/name") + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print(dedent("""\ + Pattern: /usr/share/test/${id}.test + # Comment + Exec: test-update + User: root + """), file=f) + hook = Click.Hook.open(self.db, "test") + self.assertRaisesHooksError( + Click.HooksError.BAD_APP_NAME, hook.get_app_id, + "package", "0.1", "app_name") + self.assertRaisesHooksError( + Click.HooksError.BAD_APP_NAME, hook.get_app_id, + "package", "0.1", "app/name") def test_short_id_invalid(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: /usr/share/test/${short-id}.test", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - # It would perhaps be better if unrecognised $-expansions raised - # KeyError, but they don't right now. - self.assertEqual( - "/usr/share/test/.test", - hook.pattern("package", "0.1", "app-name")) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Pattern: /usr/share/test/${short-id}.test", file=f) + hook = Click.Hook.open(self.db, "test") + # It would perhaps be better if unrecognised $-expansions raised + # KeyError, but they don't right now. + self.assertEqual( + "/usr/share/test/.test", + hook.get_pattern("package", "0.1", "app-name")) def test_short_id_valid_with_single_version(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: /usr/share/test/${short-id}.test", file=f) - print("Single-Version: yes", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertEqual( - "/usr/share/test/package_app-name.test", - hook.pattern("package", "0.1", "app-name")) - - @mock.patch("subprocess.check_call") - def test_run_commands(self, mock_check_call): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Exec: test-update", file=f) - print("User: root", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertEqual("root", hook._run_commands_user(user=None)) - hook._run_commands(user=None) - mock_check_call.assert_called_once_with( - "test-update", preexec_fn=mock.ANY, shell=True) - - def test_previous_entries(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - link_one = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - link_two = os.path.join( - self.temp_dir, "org.example.package_test-app_2.0.test") - os.symlink("dummy", link_one) - os.symlink("dummy", link_two) - os.symlink("dummy", os.path.join(self.temp_dir, "malformed")) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertCountEqual([ - (link_one, "org.example.package", "1.0", "test-app"), - (link_two, "org.example.package", "2.0", "test-app"), - ], list(hook._previous_entries())) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Pattern: /usr/share/test/${short-id}.test", file=f) + print("Single-Version: yes", file=f) + hook = Click.Hook.open(self.db, "test") + self.assertEqual( + "/usr/share/test/package_app-name.test", + hook.get_pattern("package", "0.1", "app-name")) + + def test_run_commands(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"/bin/sh": 0}) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Exec: test-update", file=f) + print("User: root", file=f) + hook = Click.Hook.open(self.db, "test") + self.assertEqual( + "root", hook.get_run_commands_user(user_name=None)) + hook.run_commands(user_name=None) + self.assertEqual( + [[b"/bin/sh", b"-c", b"test-update"]], self.spawn_calls) def test_install_package(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0")) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - hook.install_package( - "org.example.package", "1.0", "test-app", "foo/bar") - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - target_path = os.path.join( - self.temp_dir, "org.example.package", "1.0", "foo", "bar") - self.assertTrue(os.path.islink(symlink_path)) - self.assertEqual(target_path, os.readlink(symlink_path)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Pattern: %s/${id}.test" % self.temp_dir, file=f) + os.makedirs( + os.path.join(self.temp_dir, "org.example.package", "1.0")) + hook = Click.Hook.open(self.db, "test") + hook.install_package( + "org.example.package", "1.0", "test-app", "foo/bar") + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0.test") + target_path = os.path.join( + self.temp_dir, "org.example.package", "1.0", "foo", "bar") + self.assertTrue(os.path.islink(symlink_path)) + self.assertEqual(target_path, os.readlink(symlink_path)) def test_install_package_trailing_slash(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: %s/${id}/" % self.temp_dir, file=f) - os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0")) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - hook.install_package("org.example.package", "1.0", "test-app", "foo") - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0") - target_path = os.path.join( - self.temp_dir, "org.example.package", "1.0", "foo") - self.assertTrue(os.path.islink(symlink_path)) - self.assertEqual(target_path, os.readlink(symlink_path)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Pattern: %s/${id}/" % self.temp_dir, file=f) + os.makedirs( + os.path.join(self.temp_dir, "org.example.package", "1.0")) + hook = Click.Hook.open(self.db, "test") + hook.install_package( + "org.example.package", "1.0", "test-app", "foo") + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0") + target_path = os.path.join( + self.temp_dir, "org.example.package", "1.0", "foo") + self.assertTrue(os.path.islink(symlink_path)) + self.assertEqual(target_path, os.readlink(symlink_path)) def test_upgrade(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - os.symlink("old-target", symlink_path) - os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0")) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - hook.install_package( - "org.example.package", "1.0", "test-app", "foo/bar") - target_path = os.path.join( - self.temp_dir, "org.example.package", "1.0", "foo", "bar") - self.assertTrue(os.path.islink(symlink_path)) - self.assertEqual(target_path, os.readlink(symlink_path)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Pattern: %s/${id}.test" % self.temp_dir, file=f) + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0.test") + os.symlink("old-target", symlink_path) + os.makedirs( + os.path.join(self.temp_dir, "org.example.package", "1.0")) + hook = Click.Hook.open(self.db, "test") + hook.install_package( + "org.example.package", "1.0", "test-app", "foo/bar") + target_path = os.path.join( + self.temp_dir, "org.example.package", "1.0", "foo", "bar") + self.assertTrue(os.path.islink(symlink_path)) + self.assertEqual(target_path, os.readlink(symlink_path)) def test_remove_package(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - os.symlink("old-target", symlink_path) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - hook.remove_package("org.example.package", "1.0", "test-app") - self.assertFalse(os.path.exists(symlink_path)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("Pattern: %s/${id}.test" % self.temp_dir, file=f) + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0.test") + os.symlink("old-target", symlink_path) + hook = Click.Hook.open(self.db, "test") + hook.remove_package("org.example.package", "1.0", "test-app") + self.assertFalse(os.path.exists(symlink_path)) def test_install(self): - with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f: - print("Pattern: %s/${id}.new" % self.temp_dir, file=f) - with mkfile_utf8(os.path.join( - self.temp_dir, "test-1", "1.0", ".click", "info", - "test-1.manifest")) as f: - json.dump({ - "maintainer": - b"Unic\xc3\xb3de ".decode("UTF-8"), - "hooks": {"test1-app": {"new": "target-1"}}, - }, f, ensure_ascii=False) - os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current")) - with mkfile_utf8(os.path.join( - self.temp_dir, "test-2", "2.0", ".click", "info", - "test-2.manifest")) as f: - json.dump({ - "maintainer": - b"Unic\xc3\xb3de ".decode("UTF-8"), - "hooks": {"test1-app": {"new": "target-2"}}, - }, f, ensure_ascii=False) - os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current")) - with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")): - hook = ClickHook.open(self.db, "new") - hook.install() - path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new") - self.assertTrue(os.path.lexists(path_1)) - self.assertEqual( - os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), - os.readlink(path_1)) - path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new") - self.assertTrue(os.path.lexists(path_2)) - self.assertEqual( - os.path.join(self.temp_dir, "test-2", "2.0", "target-2"), - os.readlink(path_2)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir( + preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) + with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f: + print("Pattern: %s/${id}.new" % self.temp_dir, file=f) + with mkfile_utf8(os.path.join( + self.temp_dir, "test-1", "1.0", ".click", "info", + "test-1.manifest")) as f: + json.dump({ + "maintainer": + b"Unic\xc3\xb3de ".decode( + "UTF-8"), + "hooks": {"test1-app": {"new": "target-1"}}, + }, f, ensure_ascii=False) + os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current")) + with mkfile_utf8(os.path.join( + self.temp_dir, "test-2", "2.0", ".click", "info", + "test-2.manifest")) as f: + json.dump({ + "maintainer": + b"Unic\xc3\xb3de ".decode( + "UTF-8"), + "hooks": {"test1-app": {"new": "target-2"}}, + }, f, ensure_ascii=False) + os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current")) + hook = Click.Hook.open(self.db, "new") + hook.install() + path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new") + self.assertTrue(os.path.lexists(path_1)) + self.assertEqual( + os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), + os.readlink(path_1)) + path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new") + self.assertTrue(os.path.lexists(path_2)) + self.assertEqual( + os.path.join(self.temp_dir, "test-2", "2.0", "target-2"), + os.readlink(path_2)) def test_remove(self): - with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f: - print("Pattern: %s/${id}.old" % self.temp_dir, file=f) - with mkfile(os.path.join( - self.temp_dir, "test-1", "1.0", ".click", "info", - "test-1.manifest")) as f: - json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f) - os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current")) - path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old") - os.symlink( - os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), path_1) - with mkfile(os.path.join( - self.temp_dir, "test-2", "2.0", ".click", "info", - "test-2.manifest")) as f: - json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f) - os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current")) - path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old") - os.symlink( - os.path.join(self.temp_dir, "test-2", "2.0", "target-2"), path_2) - with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")): - hook = ClickHook.open(self.db, "old") - hook.remove() - self.assertFalse(os.path.exists(path_1)) - self.assertFalse(os.path.exists(path_2)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir( + preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) + with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f: + print("Pattern: %s/${id}.old" % self.temp_dir, file=f) + with mkfile(os.path.join( + self.temp_dir, "test-1", "1.0", ".click", "info", + "test-1.manifest")) as f: + json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f) + os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current")) + path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old") + os.symlink( + os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), + path_1) + with mkfile(os.path.join( + self.temp_dir, "test-2", "2.0", ".click", "info", + "test-2.manifest")) as f: + json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f) + os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current")) + path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old") + os.symlink( + os.path.join(self.temp_dir, "test-2", "2.0", "target-2"), + path_2) + hook = Click.Hook.open(self.db, "old") + hook.remove() + self.assertFalse(os.path.exists(path_1)) + self.assertFalse(os.path.exists(path_2)) def test_sync(self): - with mkfile(os.path.join(self.temp_dir, "hooks", "test.hook")) as f: - print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - with mkfile(os.path.join( - self.temp_dir, "test-1", "1.0", ".click", "info", - "test-1.manifest")) as f: - json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f) - os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current")) - with mkfile(os.path.join( - self.temp_dir, "test-2", "1.1", ".click", "info", - "test-2.manifest")) as f: - json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f) - os.symlink("1.1", os.path.join(self.temp_dir, "test-2", "current")) - path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test") - os.symlink( - os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), path_1) - path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test") - path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test") - os.symlink( - os.path.join(self.temp_dir, "test-3", "1.0", "target-3"), path_3) - with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")): - hook = ClickHook.open(self.db, "test") - hook.sync() - self.assertTrue(os.path.lexists(path_1)) - self.assertEqual( - os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), - os.readlink(path_1)) - self.assertTrue(os.path.lexists(path_2)) - self.assertEqual( - os.path.join(self.temp_dir, "test-2", "1.1", "target-2"), - os.readlink(path_2)) - self.assertFalse(os.path.lexists(path_3)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir( + preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) + with mkfile(os.path.join( + self.temp_dir, "hooks", "test.hook")) as f: + print("Pattern: %s/${id}.test" % self.temp_dir, file=f) + with mkfile(os.path.join( + self.temp_dir, "test-1", "1.0", ".click", "info", + "test-1.manifest")) as f: + json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f) + os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current")) + with mkfile(os.path.join( + self.temp_dir, "test-2", "1.1", ".click", "info", + "test-2.manifest")) as f: + json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f) + os.symlink("1.1", os.path.join(self.temp_dir, "test-2", "current")) + path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test") + os.symlink( + os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), + path_1) + path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test") + path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test") + os.symlink( + os.path.join(self.temp_dir, "test-3", "1.0", "target-3"), + path_3) + hook = Click.Hook.open(self.db, "test") + hook.sync() + self.assertTrue(os.path.lexists(path_1)) + self.assertEqual( + os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), + os.readlink(path_1)) + self.assertTrue(os.path.lexists(path_2)) + self.assertEqual( + os.path.join(self.temp_dir, "test-2", "1.1", "target-2"), + os.readlink(path_2)) + self.assertFalse(os.path.lexists(path_3)) class TestClickHookUserLevel(TestClickHookBase): def test_open(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print(dedent("""\ - User-Level: yes - Pattern: ${home}/.local/share/test/${id}.test - # Comment - Exec: test-update - """), file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertCountEqual(["User-Level", "Pattern", "Exec"], hook.keys()) - self.assertEqual( - "${home}/.local/share/test/${id}.test", hook["pattern"]) - self.assertEqual("test-update", hook["exec"]) - self.assertTrue(hook.user_level) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print(dedent("""\ + User-Level: yes + Pattern: ${home}/.local/share/test/${id}.test + # Comment + Exec: test-update + """), file=f) + hook = Click.Hook.open(self.db, "test") + self.assertCountEqual( + ["user-level", "pattern", "exec"], hook.get_fields()) + self.assertEqual( + "${home}/.local/share/test/${id}.test", + hook.get_field("pattern")) + self.assertEqual("test-update", hook.get_field("exec")) + self.assertTrue(hook.props.is_user_level) def test_hook_name_absent(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("User-Level: yes", file=f) - print("Pattern: ${home}/.local/share/test/${id}.test", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertEqual("test", hook.hook_name) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("User-Level: yes", file=f) + print("Pattern: ${home}/.local/share/test/${id}.test", file=f) + hook = Click.Hook.open(self.db, "test") + self.assertEqual("test", hook.get_hook_name()) def test_hook_name_present(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("User-Level: yes", file=f) - print("Pattern: ${home}/.local/share/test/${id}.test", file=f) - print("Hook-Name: other", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertEqual("other", hook.hook_name) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("User-Level: yes", file=f) + print("Pattern: ${home}/.local/share/test/${id}.test", file=f) + print("Hook-Name: other", file=f) + hook = Click.Hook.open(self.db, "test") + self.assertEqual("other", hook.get_hook_name()) def test_invalid_app_id(self): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print(dedent("""\ - User-Level: yes - Pattern: ${home}/.local/share/test/${id}.test - # Comment - Exec: test-update - """), file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertRaises( - ValueError, hook.app_id, "package", "0.1", "app_name") - self.assertRaises( - ValueError, hook.app_id, "package", "0.1", "app/name") - - @mock.patch("pwd.getpwnam") - def test_short_id_valid(self, mock_getpwnam): - class MockPasswd: - def __init__(self, pw_dir): - self.pw_dir = pw_dir - - mock_getpwnam.return_value = MockPasswd(pw_dir="/mock") - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("User-Level: yes", file=f) - print( - "Pattern: ${home}/.local/share/test/${short-id}.test", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertEqual( - "/mock/.local/share/test/package_app-name.test", - hook.pattern("package", "0.1", "app-name", user="mock")) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print(dedent("""\ + User-Level: yes + Pattern: ${home}/.local/share/test/${id}.test + # Comment + Exec: test-update + """), file=f) + hook = Click.Hook.open(self.db, "test") + self.assertRaisesHooksError( + Click.HooksError.BAD_APP_NAME, hook.get_app_id, + "package", "0.1", "app_name") + self.assertRaisesHooksError( + Click.HooksError.BAD_APP_NAME, hook.get_app_id, + "package", "0.1", "app/name") + + def test_short_id_valid(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "getpwnam") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["getpwnam"].side_effect = ( + lambda name: self.make_pointer(Passwd(pw_dir=b"/mock"))) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("User-Level: yes", file=f) + print( + "Pattern: ${home}/.local/share/test/${short-id}.test", + file=f) + hook = Click.Hook.open(self.db, "test") + self.assertEqual( + "/mock/.local/share/test/package_app-name.test", + hook.get_pattern( + "package", "0.1", "app-name", user_name="mock")) + + def test_run_commands(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["g_spawn_sync"].side_effect = partial( + self.g_spawn_sync_side_effect, {b"/bin/sh": 0}) + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("User-Level: yes", file=f) + print("Exec: test-update", file=f) + hook = Click.Hook.open(self.db, "test") + self.assertEqual( + "test-user", hook.get_run_commands_user(user_name="test-user")) + hook.run_commands(user_name="test-user") + self.assertEqual( + [[b"/bin/sh", b"-c", b"test-update"]], self.spawn_calls) - @mock.patch("subprocess.check_call") - def test_run_commands(self, mock_check_call): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("User-Level: yes", file=f) - print("Exec: test-update", file=f) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertEqual( - "test-user", hook._run_commands_user(user="test-user")) - hook._run_commands(user="test-user") - mock_check_call.assert_called_once_with( - "test-update", preexec_fn=mock.ANY, shell=True) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_previous_entries(self, mock_user_home): - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("User-Level: yes", file=f) - print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - link_one = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - link_two = os.path.join( - self.temp_dir, "org.example.package_test-app_2.0.test") - os.symlink("dummy", link_one) - os.symlink("dummy", link_two) - os.symlink("dummy", os.path.join(self.temp_dir, "malformed")) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - self.assertCountEqual([ - (link_one, "org.example.package", "1.0", "test-app"), - (link_two, "org.example.package", "2.0", "test-app"), - ], list(hook._previous_entries(user="test-user"))) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_install_package(self, mock_user_home): - mock_user_home.return_value = "/home/test-user" - with temp_hooks_dir(self.temp_dir): + def test_install_package(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "click_get_user_home", + ) as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["click_get_user_home"].return_value = "/home/test-user" os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.0")) - user_db = ClickUser(self.db, user="test-user") + user_db = Click.User.for_user(self.db, "test-user") user_db.set_version("org.example.package", "1.0") with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - hook = ClickHook.open(self.db, "test") - hook.install_package( - "org.example.package", "1.0", "test-app", "foo/bar", - user="test-user") - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - target_path = os.path.join( - self.temp_dir, ".click", "users", "test-user", - "org.example.package", "foo", "bar") - self.assertTrue(os.path.islink(symlink_path)) - self.assertEqual(target_path, os.readlink(symlink_path)) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_install_package_trailing_slash(self, mock_user_home): - mock_user_home.return_value = "/home/test-user" - with temp_hooks_dir(self.temp_dir): + hook = Click.Hook.open(self.db, "test") + hook.install_package( + "org.example.package", "1.0", "test-app", "foo/bar", + user_name="test-user") + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0.test") + target_path = os.path.join( + self.temp_dir, ".click", "users", "test-user", + "org.example.package", "foo", "bar") + self.assertTrue(os.path.islink(symlink_path)) + self.assertEqual(target_path, os.readlink(symlink_path)) + + def test_install_package_trailing_slash(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "click_get_user_home", + ) as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["click_get_user_home"].return_value = "/home/test-user" os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.0")) - user_db = ClickUser(self.db, user="test-user") + user_db = Click.User.for_user(self.db, "test-user") user_db.set_version("org.example.package", "1.0") with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}/" % self.temp_dir, file=f) - hook = ClickHook.open(self.db, "test") - hook.install_package( - "org.example.package", "1.0", "test-app", "foo", user="test-user") - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0") - target_path = os.path.join( - self.temp_dir, ".click", "users", "test-user", - "org.example.package", "foo") - self.assertTrue(os.path.islink(symlink_path)) - self.assertEqual(target_path, os.readlink(symlink_path)) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_install_package_removes_previous(self, mock_user_home): - mock_user_home.return_value = "/home/test-user" - with temp_hooks_dir(self.temp_dir): + hook = Click.Hook.open(self.db, "test") + hook.install_package( + "org.example.package", "1.0", "test-app", "foo", + user_name="test-user") + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0") + target_path = os.path.join( + self.temp_dir, ".click", "users", "test-user", + "org.example.package", "foo") + self.assertTrue(os.path.islink(symlink_path)) + self.assertEqual(target_path, os.readlink(symlink_path)) + + def test_install_package_removes_previous(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "click_get_user_home", + ) as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["click_get_user_home"].return_value = "/home/test-user" os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.0")) os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.1")) - user_db = ClickUser(self.db, user="test-user") + user_db = Click.User.for_user(self.db, "test-user") user_db.set_version("org.example.package", "1.0") with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - hook = ClickHook.open(self.db, "test") - hook.install_package( - "org.example.package", "1.0", "test-app", "foo/bar", - user="test-user") - hook.install_package( - "org.example.package", "1.1", "test-app", "foo/bar", - user="test-user") - old_symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.1.test") - self.assertFalse(os.path.islink(old_symlink_path)) - self.assertTrue(os.path.islink(symlink_path)) - target_path = os.path.join( - self.temp_dir, ".click", "users", "test-user", - "org.example.package", "foo", "bar") - self.assertEqual(target_path, os.readlink(symlink_path)) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_upgrade(self, mock_user_home): - mock_user_home.return_value = "/home/test-user" - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - os.symlink("old-target", symlink_path) - with temp_hooks_dir(self.temp_dir): + hook = Click.Hook.open(self.db, "test") + hook.install_package( + "org.example.package", "1.0", "test-app", "foo/bar", + user_name="test-user") + hook.install_package( + "org.example.package", "1.1", "test-app", "foo/bar", + user_name="test-user") + old_symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0.test") + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.1.test") + self.assertFalse(os.path.islink(old_symlink_path)) + self.assertTrue(os.path.islink(symlink_path)) + target_path = os.path.join( + self.temp_dir, ".click", "users", "test-user", + "org.example.package", "foo", "bar") + self.assertEqual(target_path, os.readlink(symlink_path)) + + def test_upgrade(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "click_get_user_home", + ) as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["click_get_user_home"].return_value = "/home/test-user" + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0.test") + os.symlink("old-target", symlink_path) os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.0")) - user_db = ClickUser(self.db, user="test-user") + user_db = Click.User.for_user(self.db, "test-user") user_db.set_version("org.example.package", "1.0") with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - hook = ClickHook.open(self.db, "test") - hook.install_package( - "org.example.package", "1.0", "test-app", "foo/bar", - user="test-user") - target_path = os.path.join( - self.temp_dir, ".click", "users", "test-user", - "org.example.package", "foo", "bar") - self.assertTrue(os.path.islink(symlink_path)) - self.assertEqual(target_path, os.readlink(symlink_path)) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_remove_package(self, mock_user_home): - mock_user_home.return_value = "/home/test-user" - with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: - print("User-Level: yes", file=f) - print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - symlink_path = os.path.join( - self.temp_dir, "org.example.package_test-app_1.0.test") - os.symlink("old-target", symlink_path) - with temp_hooks_dir(self.temp_dir): - hook = ClickHook.open(self.db, "test") - hook.remove_package( - "org.example.package", "1.0", "test-app", user="test-user") - self.assertFalse(os.path.exists(symlink_path)) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_install(self, mock_user_home): - mock_user_home.return_value = "/home/test-user" - with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f: - print("User-Level: yes", file=f) - print("Pattern: %s/${id}.new" % self.temp_dir, file=f) - user_db = ClickUser(self.db, user="test-user") - with mkfile_utf8(os.path.join( - self.temp_dir, "test-1", "1.0", ".click", "info", - "test-1.manifest")) as f: - json.dump({ - "maintainer": - b"Unic\xc3\xb3de ".decode("UTF-8"), - "hooks": {"test1-app": {"new": "target-1"}}, - }, f, ensure_ascii=False) - user_db.set_version("test-1", "1.0") - with mkfile_utf8(os.path.join( - self.temp_dir, "test-2", "2.0", ".click", "info", - "test-2.manifest")) as f: - json.dump({ - "maintainer": - b"Unic\xc3\xb3de ".decode("UTF-8"), - "hooks": {"test1-app": {"new": "target-2"}}, - }, f, ensure_ascii=False) - user_db.set_version("test-2", "2.0") - with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")): - hook = ClickHook.open(self.db, "new") - hook.install() - path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new") - self.assertTrue(os.path.lexists(path_1)) - self.assertEqual( - os.path.join( - self.temp_dir, ".click", "users", "test-user", "test-1", - "target-1"), - os.readlink(path_1)) - path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new") - self.assertTrue(os.path.lexists(path_2)) - self.assertEqual( - os.path.join( - self.temp_dir, ".click", "users", "test-user", "test-2", - "target-2"), - os.readlink(path_2)) - - os.unlink(path_1) - os.unlink(path_2) - hook.install(user="another-user") - self.assertFalse(os.path.lexists(path_1)) - self.assertFalse(os.path.lexists(path_2)) + hook = Click.Hook.open(self.db, "test") + hook.install_package( + "org.example.package", "1.0", "test-app", "foo/bar", + user_name="test-user") + target_path = os.path.join( + self.temp_dir, ".click", "users", "test-user", + "org.example.package", "foo", "bar") + self.assertTrue(os.path.islink(symlink_path)) + self.assertEqual(target_path, os.readlink(symlink_path)) - hook.install(user="test-user") - self.assertTrue(os.path.lexists(path_1)) - self.assertEqual( - os.path.join( - self.temp_dir, ".click", "users", "test-user", "test-1", - "target-1"), - os.readlink(path_1)) - self.assertTrue(os.path.lexists(path_2)) - self.assertEqual( - os.path.join( - self.temp_dir, ".click", "users", "test-user", "test-2", - "target-2"), - os.readlink(path_2)) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_remove(self, mock_user_home): - mock_user_home.return_value = "/home/test-user" - with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f: - print("User-Level: yes", file=f) - print("Pattern: %s/${id}.old" % self.temp_dir, file=f) - user_db = ClickUser(self.db, user="test-user") - with mkfile(os.path.join( - self.temp_dir, "test-1", "1.0", ".click", "info", - "test-1.manifest")) as f: - json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f) - user_db.set_version("test-1", "1.0") - os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current")) - path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old") - os.symlink(os.path.join(user_db.path("test-1"), "target-1"), path_1) - with mkfile(os.path.join( - self.temp_dir, "test-2", "2.0", ".click", "info", - "test-2.manifest")) as f: - json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f) - user_db.set_version("test-2", "2.0") - path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old") - os.symlink(os.path.join(user_db.path("test-2"), "target-2"), path_2) - with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")): - hook = ClickHook.open(self.db, "old") - hook.remove() - self.assertFalse(os.path.exists(path_1)) - self.assertFalse(os.path.exists(path_2)) - - @mock.patch("click.hooks.ClickHook._user_home") - def test_sync(self, mock_user_home): - mock_user_home.return_value = "/home/test-user" - with mkfile(os.path.join(self.temp_dir, "hooks", "test.hook")) as f: - print("User-Level: yes", file=f) - print("Pattern: %s/${id}.test" % self.temp_dir, file=f) - user_db = ClickUser(self.db, user="test-user") - with mkfile(os.path.join( - self.temp_dir, "test-1", "1.0", ".click", "info", - "test-1.manifest")) as f: - json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f) - user_db.set_version("test-1", "1.0") - with mkfile(os.path.join( - self.temp_dir, "test-2", "1.1", ".click", "info", - "test-2.manifest")) as f: - json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f) - user_db.set_version("test-2", "1.1") - path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test") - os.symlink( - os.path.join( - self.temp_dir, ".click", "users", "test-user", "test-1", - "target-1"), - path_1) - path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test") - path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test") - os.symlink( - os.path.join( - self.temp_dir, ".click", "users", "test-user", "test-3", - "target-3"), - path_3) - with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")): - hook = ClickHook.open(self.db, "test") - hook.sync(user="test-user") - self.assertTrue(os.path.lexists(path_1)) - self.assertEqual( - os.path.join( - self.temp_dir, ".click", "users", "test-user", "test-1", - "target-1"), - os.readlink(path_1)) - self.assertTrue(os.path.lexists(path_2)) - self.assertEqual( - os.path.join( - self.temp_dir, ".click", "users", "test-user", "test-2", - "target-2"), - os.readlink(path_2)) - self.assertFalse(os.path.lexists(path_3)) + def test_remove_package(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "click_get_user_home", + ) as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["click_get_user_home"].return_value = "/home/test-user" + with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: + print("User-Level: yes", file=f) + print("Pattern: %s/${id}.test" % self.temp_dir, file=f) + symlink_path = os.path.join( + self.temp_dir, "org.example.package_test-app_1.0.test") + os.symlink("old-target", symlink_path) + hook = Click.Hook.open(self.db, "test") + hook.remove_package( + "org.example.package", "1.0", "test-app", + user_name="test-user") + self.assertFalse(os.path.exists(symlink_path)) + + def test_install(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "click_get_user_home", + ) as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["click_get_user_home"].return_value = "/home/test-user" + with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f: + print("User-Level: yes", file=f) + print("Pattern: %s/${id}.new" % self.temp_dir, file=f) + user_db = Click.User.for_user(self.db, "test-user") + with mkfile_utf8(os.path.join( + self.temp_dir, "test-1", "1.0", ".click", "info", + "test-1.manifest")) as f: + json.dump({ + "maintainer": + b"Unic\xc3\xb3de ".decode( + "UTF-8"), + "hooks": {"test1-app": {"new": "target-1"}}, + }, f, ensure_ascii=False) + user_db.set_version("test-1", "1.0") + with mkfile_utf8(os.path.join( + self.temp_dir, "test-2", "2.0", ".click", "info", + "test-2.manifest")) as f: + json.dump({ + "maintainer": + b"Unic\xc3\xb3de ".decode( + "UTF-8"), + "hooks": {"test1-app": {"new": "target-2"}}, + }, f, ensure_ascii=False) + user_db.set_version("test-2", "2.0") + self._setup_hooks_dir( + preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) + hook = Click.Hook.open(self.db, "new") + hook.install() + path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new") + self.assertTrue(os.path.lexists(path_1)) + self.assertEqual( + os.path.join( + self.temp_dir, ".click", "users", "test-user", "test-1", + "target-1"), + os.readlink(path_1)) + path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new") + self.assertTrue(os.path.lexists(path_2)) + self.assertEqual( + os.path.join( + self.temp_dir, ".click", "users", "test-user", "test-2", + "target-2"), + os.readlink(path_2)) + + os.unlink(path_1) + os.unlink(path_2) + hook.install(user_name="another-user") + self.assertFalse(os.path.lexists(path_1)) + self.assertFalse(os.path.lexists(path_2)) + + hook.install(user_name="test-user") + self.assertTrue(os.path.lexists(path_1)) + self.assertEqual( + os.path.join( + self.temp_dir, ".click", "users", "test-user", "test-1", + "target-1"), + os.readlink(path_1)) + self.assertTrue(os.path.lexists(path_2)) + self.assertEqual( + os.path.join( + self.temp_dir, ".click", "users", "test-user", "test-2", + "target-2"), + os.readlink(path_2)) + + def test_remove(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "click_get_user_home", + ) as (enter, preloads): + enter() + self._setup_hooks_dir(preloads) + preloads["click_get_user_home"].return_value = "/home/test-user" + with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f: + print("User-Level: yes", file=f) + print("Pattern: %s/${id}.old" % self.temp_dir, file=f) + user_db = Click.User.for_user(self.db, "test-user") + with mkfile(os.path.join( + self.temp_dir, "test-1", "1.0", ".click", "info", + "test-1.manifest")) as f: + json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f) + user_db.set_version("test-1", "1.0") + os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current")) + path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old") + os.symlink( + os.path.join(user_db.get_path("test-1"), "target-1"), path_1) + with mkfile(os.path.join( + self.temp_dir, "test-2", "2.0", ".click", "info", + "test-2.manifest")) as f: + json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f) + user_db.set_version("test-2", "2.0") + path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old") + os.symlink( + os.path.join(user_db.get_path("test-2"), "target-2"), path_2) + self._setup_hooks_dir( + preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) + hook = Click.Hook.open(self.db, "old") + hook.remove() + self.assertFalse(os.path.exists(path_1)) + self.assertFalse(os.path.exists(path_2)) + + def test_sync(self): + with self.run_in_subprocess( + "click_get_hooks_dir", "click_get_user_home", + ) as (enter, preloads): + enter() + preloads["click_get_user_home"].return_value = "/home/test-user" + self._setup_hooks_dir(preloads) + with mkfile( + os.path.join(self.temp_dir, "hooks", "test.hook")) as f: + print("User-Level: yes", file=f) + print("Pattern: %s/${id}.test" % self.temp_dir, file=f) + user_db = Click.User.for_user(self.db, "test-user") + with mkfile(os.path.join( + self.temp_dir, "test-1", "1.0", ".click", "info", + "test-1.manifest")) as f: + json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f) + user_db.set_version("test-1", "1.0") + with mkfile(os.path.join( + self.temp_dir, "test-2", "1.1", ".click", "info", + "test-2.manifest")) as f: + json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f) + user_db.set_version("test-2", "1.1") + path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test") + os.symlink( + os.path.join( + self.temp_dir, ".click", "users", "test-user", "test-1", + "target-1"), + path_1) + path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test") + path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test") + os.symlink( + os.path.join( + self.temp_dir, ".click", "users", "test-user", "test-3", + "target-3"), + path_3) + self._setup_hooks_dir( + preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) + hook = Click.Hook.open(self.db, "test") + hook.sync(user_name="test-user") + self.assertTrue(os.path.lexists(path_1)) + self.assertEqual( + os.path.join( + self.temp_dir, ".click", "users", "test-user", "test-1", + "target-1"), + os.readlink(path_1)) + self.assertTrue(os.path.lexists(path_2)) + self.assertEqual( + os.path.join( + self.temp_dir, ".click", "users", "test-user", "test-2", + "target-2"), + os.readlink(path_2)) + self.assertFalse(os.path.lexists(path_3)) class TestPackageInstallHooks(TestClickHookBase): def test_removes_old_hooks(self): - hooks_dir = os.path.join(self.temp_dir, "hooks") - with mkfile(os.path.join(hooks_dir, "unity.hook")) as f: - print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f) - print("Single-Version: yes", file=f) - with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f: - print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, file=f) - print("Single-Version: yes", file=f) - print("Hook-Name: yelp", file=f) - with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f: - print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, file=f) - print("Single-Version: yes", file=f) - print("Hook-Name: yelp", file=f) - os.mkdir(os.path.join(self.temp_dir, "unity")) - unity_path = os.path.join(self.temp_dir, "unity", "test_app_1.0.scope") - os.symlink("dummy", unity_path) - os.mkdir(os.path.join(self.temp_dir, "yelp")) - yelp_docs_path = os.path.join( - self.temp_dir, "yelp", "docs-test_app_1.0.txt") - os.symlink("dummy", yelp_docs_path) - yelp_other_path = os.path.join( - self.temp_dir, "yelp", "other-test_app_1.0.txt") - os.symlink("dummy", yelp_other_path) - package_dir = os.path.join(self.temp_dir, "test") - with mkfile(os.path.join( - package_dir, "1.0", ".click", "info", "test.manifest")) as f: - json.dump( - {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}}, - f) - with mkfile(os.path.join( - package_dir, "1.1", ".click", "info", "test.manifest")) as f: - json.dump({}, f) - with temp_hooks_dir(hooks_dir): - package_install_hooks(self.db, "test", "1.0", "1.1") - self.assertFalse(os.path.lexists(unity_path)) - self.assertFalse(os.path.lexists(yelp_docs_path)) - self.assertFalse(os.path.lexists(yelp_other_path)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + hooks_dir = os.path.join(self.temp_dir, "hooks") + self._setup_hooks_dir(preloads, hooks_dir=hooks_dir) + with mkfile(os.path.join(hooks_dir, "unity.hook")) as f: + print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f) + print("Single-Version: yes", file=f) + with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f: + print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, + file=f) + print("Single-Version: yes", file=f) + print("Hook-Name: yelp", file=f) + with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f: + print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, + file=f) + print("Single-Version: yes", file=f) + print("Hook-Name: yelp", file=f) + os.mkdir(os.path.join(self.temp_dir, "unity")) + unity_path = os.path.join( + self.temp_dir, "unity", "test_app_1.0.scope") + os.symlink("dummy", unity_path) + os.mkdir(os.path.join(self.temp_dir, "yelp")) + yelp_docs_path = os.path.join( + self.temp_dir, "yelp", "docs-test_app_1.0.txt") + os.symlink("dummy", yelp_docs_path) + yelp_other_path = os.path.join( + self.temp_dir, "yelp", "other-test_app_1.0.txt") + os.symlink("dummy", yelp_other_path) + package_dir = os.path.join(self.temp_dir, "test") + with mkfile(os.path.join( + package_dir, "1.0", ".click", "info", + "test.manifest")) as f: + json.dump( + {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}}, + f) + with mkfile(os.path.join( + package_dir, "1.1", ".click", "info", + "test.manifest")) as f: + json.dump({}, f) + Click.package_install_hooks(self.db, "test", "1.0", "1.1") + self.assertFalse(os.path.lexists(unity_path)) + self.assertFalse(os.path.lexists(yelp_docs_path)) + self.assertFalse(os.path.lexists(yelp_other_path)) def test_installs_new_hooks(self): - hooks_dir = os.path.join(self.temp_dir, "hooks") - with mkfile(os.path.join(hooks_dir, "a.hook")) as f: - print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f) - with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f: - print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f) - print("Hook-Name: b", file=f) - with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f: - print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f) - print("Hook-Name: b", file=f) - os.mkdir(os.path.join(self.temp_dir, "a")) - os.mkdir(os.path.join(self.temp_dir, "b")) - package_dir = os.path.join(self.temp_dir, "test") - with mkfile(os.path.join( - package_dir, "1.0", ".click", "info", "test.manifest")) as f: - json.dump({"hooks": {}}, f) - with mkfile(os.path.join( - package_dir, "1.1", ".click", "info", "test.manifest")) as f: - json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f) - with temp_hooks_dir(hooks_dir): - package_install_hooks(self.db, "test", "1.0", "1.1") - self.assertTrue(os.path.lexists( - os.path.join(self.temp_dir, "a", "test_app_1.1.a"))) - self.assertTrue(os.path.lexists( - os.path.join(self.temp_dir, "b", "1-test_app_1.1.b"))) - self.assertTrue(os.path.lexists( - os.path.join(self.temp_dir, "b", "2-test_app_1.1.b"))) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + hooks_dir = os.path.join(self.temp_dir, "hooks") + self._setup_hooks_dir(preloads, hooks_dir=hooks_dir) + with mkfile(os.path.join(hooks_dir, "a.hook")) as f: + print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f) + with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f: + print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f) + print("Hook-Name: b", file=f) + with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f: + print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f) + print("Hook-Name: b", file=f) + os.mkdir(os.path.join(self.temp_dir, "a")) + os.mkdir(os.path.join(self.temp_dir, "b")) + package_dir = os.path.join(self.temp_dir, "test") + with mkfile(os.path.join( + package_dir, "1.0", ".click", "info", + "test.manifest")) as f: + json.dump({"hooks": {}}, f) + with mkfile(os.path.join( + package_dir, "1.1", ".click", "info", + "test.manifest")) as f: + json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f) + Click.package_install_hooks(self.db, "test", "1.0", "1.1") + self.assertTrue(os.path.lexists( + os.path.join(self.temp_dir, "a", "test_app_1.1.a"))) + self.assertTrue(os.path.lexists( + os.path.join(self.temp_dir, "b", "1-test_app_1.1.b"))) + self.assertTrue(os.path.lexists( + os.path.join(self.temp_dir, "b", "2-test_app_1.1.b"))) def test_upgrades_existing_hooks(self): - hooks_dir = os.path.join(self.temp_dir, "hooks") - with mkfile(os.path.join(hooks_dir, "a.hook")) as f: - print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f) - print("Single-Version: yes", file=f) - with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f: - print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f) - print("Single-Version: yes", file=f) - print("Hook-Name: b", file=f) - with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f: - print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f) - print("Single-Version: yes", file=f) - print("Hook-Name: b", file=f) - with mkfile(os.path.join(hooks_dir, "c.hook")) as f: - print("Pattern: %s/c/${id}.c" % self.temp_dir, file=f) - print("Single-Version: yes", file=f) - os.mkdir(os.path.join(self.temp_dir, "a")) - a_path = os.path.join(self.temp_dir, "a", "test_app_1.0.a") - os.symlink("dummy", a_path) - os.mkdir(os.path.join(self.temp_dir, "b")) - b_irrelevant_path = os.path.join( - self.temp_dir, "b", "1-test_other-app_1.0.b") - os.symlink("dummy", b_irrelevant_path) - b_1_path = os.path.join(self.temp_dir, "b", "1-test_app_1.0.b") - os.symlink("dummy", b_1_path) - b_2_path = os.path.join(self.temp_dir, "b", "2-test_app_1.0.b") - os.symlink("dummy", b_2_path) - os.mkdir(os.path.join(self.temp_dir, "c")) - package_dir = os.path.join(self.temp_dir, "test") - with mkfile(os.path.join( - package_dir, "1.0", ".click", "info", "test.manifest")) as f: - json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f) - with mkfile(os.path.join( - package_dir, "1.1", ".click", "info", "test.manifest")) as f: - json.dump( - {"hooks": { - "app": {"a": "foo.a", "b": "foo.b", "c": "foo.c"}} - }, f) - with temp_hooks_dir(hooks_dir): - package_install_hooks(self.db, "test", "1.0", "1.1") - self.assertFalse(os.path.lexists(a_path)) - self.assertTrue(os.path.lexists(b_irrelevant_path)) - self.assertFalse(os.path.lexists(b_1_path)) - self.assertFalse(os.path.lexists(b_2_path)) - self.assertTrue(os.path.lexists( - os.path.join(self.temp_dir, "a", "test_app_1.1.a"))) - self.assertTrue(os.path.lexists( - os.path.join(self.temp_dir, "b", "1-test_app_1.1.b"))) - self.assertTrue(os.path.lexists( - os.path.join(self.temp_dir, "b", "2-test_app_1.1.b"))) - self.assertTrue(os.path.lexists( - os.path.join(self.temp_dir, "c", "test_app_1.1.c"))) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + hooks_dir = os.path.join(self.temp_dir, "hooks") + self._setup_hooks_dir(preloads, hooks_dir=hooks_dir) + with mkfile(os.path.join(hooks_dir, "a.hook")) as f: + print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f) + print("Single-Version: yes", file=f) + with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f: + print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f) + print("Single-Version: yes", file=f) + print("Hook-Name: b", file=f) + with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f: + print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f) + print("Single-Version: yes", file=f) + print("Hook-Name: b", file=f) + with mkfile(os.path.join(hooks_dir, "c.hook")) as f: + print("Pattern: %s/c/${id}.c" % self.temp_dir, file=f) + print("Single-Version: yes", file=f) + os.mkdir(os.path.join(self.temp_dir, "a")) + a_path = os.path.join(self.temp_dir, "a", "test_app_1.0.a") + os.symlink("dummy", a_path) + os.mkdir(os.path.join(self.temp_dir, "b")) + b_irrelevant_path = os.path.join( + self.temp_dir, "b", "1-test_other-app_1.0.b") + os.symlink("dummy", b_irrelevant_path) + b_1_path = os.path.join(self.temp_dir, "b", "1-test_app_1.0.b") + os.symlink("dummy", b_1_path) + b_2_path = os.path.join(self.temp_dir, "b", "2-test_app_1.0.b") + os.symlink("dummy", b_2_path) + os.mkdir(os.path.join(self.temp_dir, "c")) + package_dir = os.path.join(self.temp_dir, "test") + with mkfile(os.path.join( + package_dir, "1.0", ".click", "info", + "test.manifest")) as f: + json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f) + with mkfile(os.path.join( + package_dir, "1.1", ".click", "info", + "test.manifest")) as f: + json.dump( + {"hooks": { + "app": {"a": "foo.a", "b": "foo.b", "c": "foo.c"}} + }, f) + Click.package_install_hooks(self.db, "test", "1.0", "1.1") + self.assertFalse(os.path.lexists(a_path)) + self.assertTrue(os.path.lexists(b_irrelevant_path)) + self.assertFalse(os.path.lexists(b_1_path)) + self.assertFalse(os.path.lexists(b_2_path)) + self.assertTrue(os.path.lexists( + os.path.join(self.temp_dir, "a", "test_app_1.1.a"))) + self.assertTrue(os.path.lexists( + os.path.join(self.temp_dir, "b", "1-test_app_1.1.b"))) + self.assertTrue(os.path.lexists( + os.path.join(self.temp_dir, "b", "2-test_app_1.1.b"))) + self.assertTrue(os.path.lexists( + os.path.join(self.temp_dir, "c", "test_app_1.1.c"))) class TestPackageRemoveHooks(TestClickHookBase): def test_removes_hooks(self): - hooks_dir = os.path.join(self.temp_dir, "hooks") - with mkfile(os.path.join(hooks_dir, "unity.hook")) as f: - print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f) - with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f: - print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, file=f) - print("Hook-Name: yelp", file=f) - with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f: - print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, file=f) - print("Hook-Name: yelp", file=f) - os.mkdir(os.path.join(self.temp_dir, "unity")) - unity_path = os.path.join(self.temp_dir, "unity", "test_app_1.0.scope") - os.symlink("dummy", unity_path) - os.mkdir(os.path.join(self.temp_dir, "yelp")) - yelp_docs_path = os.path.join( - self.temp_dir, "yelp", "docs-test_app_1.0.txt") - os.symlink("dummy", yelp_docs_path) - yelp_other_path = os.path.join( - self.temp_dir, "yelp", "other-test_app_1.0.txt") - os.symlink("dummy", yelp_other_path) - package_dir = os.path.join(self.temp_dir, "test") - with mkfile(os.path.join( - package_dir, "1.0", ".click", "info", "test.manifest")) as f: - json.dump( - {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}}, - f) - with temp_hooks_dir(hooks_dir): - package_remove_hooks(self.db, "test", "1.0") - self.assertFalse(os.path.lexists(unity_path)) - self.assertFalse(os.path.lexists(yelp_docs_path)) - self.assertFalse(os.path.lexists(yelp_other_path)) + with self.run_in_subprocess( + "click_get_hooks_dir") as (enter, preloads): + enter() + hooks_dir = os.path.join(self.temp_dir, "hooks") + self._setup_hooks_dir(preloads, hooks_dir=hooks_dir) + with mkfile(os.path.join(hooks_dir, "unity.hook")) as f: + print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f) + with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f: + print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, + file=f) + print("Hook-Name: yelp", file=f) + with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f: + print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, + file=f) + print("Hook-Name: yelp", file=f) + os.mkdir(os.path.join(self.temp_dir, "unity")) + unity_path = os.path.join( + self.temp_dir, "unity", "test_app_1.0.scope") + os.symlink("dummy", unity_path) + os.mkdir(os.path.join(self.temp_dir, "yelp")) + yelp_docs_path = os.path.join( + self.temp_dir, "yelp", "docs-test_app_1.0.txt") + os.symlink("dummy", yelp_docs_path) + yelp_other_path = os.path.join( + self.temp_dir, "yelp", "other-test_app_1.0.txt") + os.symlink("dummy", yelp_other_path) + package_dir = os.path.join(self.temp_dir, "test") + with mkfile(os.path.join( + package_dir, "1.0", ".click", "info", + "test.manifest")) as f: + json.dump( + {"hooks": { + "app": {"yelp": "foo.txt", "unity": "foo.scope"}} + }, f) + Click.package_remove_hooks(self.db, "test", "1.0") + self.assertFalse(os.path.lexists(unity_path)) + self.assertFalse(os.path.lexists(yelp_docs_path)) + self.assertFalse(os.path.lexists(yelp_other_path)) diff -Nru click-0.4.16/click/tests/test_install.py click-0.4.17.2/click/tests/test_install.py --- click-0.4.16/click/tests/test_install.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/tests/test_install.py 2014-03-06 16:38:26.000000000 +0000 @@ -34,10 +34,10 @@ from unittest import skipUnless from debian.deb822 import Deb822 +from gi.repository import Click -from click import install, osextras +from click import install from click.build import ClickBuilder -from click.database import ClickDB from click.install import ( ClickInstaller, ClickInstallerAuditError, @@ -68,7 +68,8 @@ def setUp(self): super(TestClickInstaller, self).setUp() self.use_temp_dir() - self.db = ClickDB(self.temp_dir) + self.db = Click.DB() + self.db.add(self.temp_dir) def make_fake_package(self, control_fields=None, manifest=None, control_scripts=None, data_files=None): @@ -90,7 +91,7 @@ for name, contents in control_scripts.items(): with mkfile(os.path.join(control_dir, name)) as script: script.write(contents) - osextras.ensuredir(data_dir) + Click.ensuredir(data_dir) for name, path in data_files.items(): if path is None: touch(os.path.join(data_dir, name)) @@ -108,11 +109,11 @@ old_dir = install.frameworks_dir try: install.frameworks_dir = os.path.join(self.temp_dir, "frameworks") - osextras.ensuredir(install.frameworks_dir) + Click.ensuredir(install.frameworks_dir) touch(os.path.join(install.frameworks_dir, "%s.framework" % name)) yield finally: - osextras.unlink_force( + Click.unlink_force( os.path.join(install.frameworks_dir, "%s.framework" % name)) install.frameworks_dir = old_dir @@ -342,7 +343,7 @@ @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") - @mock.patch("click.install.package_install_hooks") + @mock.patch("gi.repository.Click.package_install_hooks") def test_install(self, mock_package_install_hooks): path = self.make_fake_package( control_fields={ @@ -361,7 +362,8 @@ control_scripts={"preinst": static_preinst}, data_files={"foo": None}) root = os.path.join(self.temp_dir, "root") - db = ClickDB(root) + db = Click.DB() + db.add(root) installer = ClickInstaller(db) with self.make_framework("ubuntu-sdk-13.10"), \ mock_quiet_subprocess_call(): @@ -425,7 +427,9 @@ control_scripts={"preinst": static_preinst}, data_files={"foo": None}) root = os.path.join(self.temp_dir, "root") - installer = ClickInstaller(ClickDB(root)) + db = Click.DB() + db.add(root) + installer = ClickInstaller(db) with self.make_framework("ubuntu-sdk-13.10"), \ mock.patch("subprocess.call") as mock_call: mock_call.side_effect = call_side_effect @@ -437,8 +441,9 @@ @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") - @mock.patch("click.install.package_install_hooks") + @mock.patch("gi.repository.Click.package_install_hooks") def test_upgrade(self, mock_package_install_hooks): + os.environ["TEST_QUIET"] = "1" path = self.make_fake_package( control_fields={ "Package": "test-package", @@ -460,7 +465,8 @@ inst_dir = os.path.join(package_dir, "current") os.makedirs(os.path.join(package_dir, "1.0")) os.symlink("1.0", inst_dir) - db = ClickDB(root) + db = Click.DB() + db.add(root) installer = ClickInstaller(db) with self.make_framework("ubuntu-sdk-13.10"), \ mock_quiet_subprocess_call(): @@ -494,7 +500,7 @@ @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") - @mock.patch("click.install.package_install_hooks") + @mock.patch("gi.repository.Click.package_install_hooks") def test_world_readable(self, mock_package_install_hooks): owner_only_file = os.path.join(self.temp_dir, "owner-only-file") touch(owner_only_file) @@ -521,7 +527,8 @@ "world-readable-dir": owner_only_dir, }) root = os.path.join(self.temp_dir, "root") - db = ClickDB(root) + db = Click.DB() + db.add(root) installer = ClickInstaller(db) with self.make_framework("ubuntu-sdk-13.10"), \ mock_quiet_subprocess_call(): @@ -538,7 +545,7 @@ @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") - @mock.patch("click.install.package_install_hooks") + @mock.patch("gi.repository.Click.package_install_hooks") @mock.patch("click.install.ClickInstaller._dpkg_architecture") def test_single_architecture(self, mock_dpkg_architecture, mock_package_install_hooks): @@ -560,7 +567,8 @@ }, control_scripts={"preinst": static_preinst}) root = os.path.join(self.temp_dir, "root") - db = ClickDB(root) + db = Click.DB() + db.add(root) installer = ClickInstaller(db) with self.make_framework("ubuntu-sdk-13.10"), \ mock_quiet_subprocess_call(): @@ -571,7 +579,7 @@ @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") - @mock.patch("click.install.package_install_hooks") + @mock.patch("gi.repository.Click.package_install_hooks") @mock.patch("click.install.ClickInstaller._dpkg_architecture") def test_multiple_architectures(self, mock_dpkg_architecture, mock_package_install_hooks): @@ -593,7 +601,8 @@ }, control_scripts={"preinst": static_preinst}) root = os.path.join(self.temp_dir, "root") - db = ClickDB(root) + db = Click.DB() + db.add(root) installer = ClickInstaller(db) with self.make_framework("ubuntu-sdk-13.10"), \ mock_quiet_subprocess_call(): diff -Nru click-0.4.16/click/tests/test_osextras.py click-0.4.17.2/click/tests/test_osextras.py --- click-0.4.16/click/tests/test_osextras.py 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/click/tests/test_osextras.py 2014-03-06 16:38:26.000000000 +0000 @@ -17,35 +17,34 @@ from __future__ import print_function __all__ = [ - 'TestOSExtras', + 'TestOSExtrasNative', + 'TestOSExtrasPython', ] import os -from click import osextras -from click.tests.helpers import TestCase, touch +from gi.repository import Click, GLib +from click import osextras +from click.tests.helpers import TestCase, mock, touch -class TestOSExtras(TestCase): - def setUp(self): - super(TestOSExtras, self).setUp() - self.use_temp_dir() +class TestOSExtrasBaseMixin: def test_ensuredir_previously_missing(self): new_dir = os.path.join(self.temp_dir, "dir") - osextras.ensuredir(new_dir) + self.mod.ensuredir(new_dir) self.assertTrue(os.path.isdir(new_dir)) def test_ensuredir_previously_present(self): new_dir = os.path.join(self.temp_dir, "dir") os.mkdir(new_dir) - osextras.ensuredir(new_dir) + self.mod.ensuredir(new_dir) self.assertTrue(os.path.isdir(new_dir)) def test_find_on_path_missing_environment(self): os.environ.pop("PATH", None) - self.assertFalse(osextras.find_on_path("ls")) + self.assertFalse(self.mod.find_on_path("ls")) def test_find_on_path_present_executable(self): bin_dir = os.path.join(self.temp_dir, "bin") @@ -53,69 +52,112 @@ touch(program) os.chmod(program, 0o755) os.environ["PATH"] = bin_dir - self.assertTrue(osextras.find_on_path("program")) + self.assertTrue(self.mod.find_on_path("program")) def test_find_on_path_present_not_executable(self): bin_dir = os.path.join(self.temp_dir, "bin") touch(os.path.join(bin_dir, "program")) os.environ["PATH"] = bin_dir - self.assertFalse(osextras.find_on_path("program")) + self.assertFalse(self.mod.find_on_path("program")) - def test_listdir_directory_present(self): - new_dir = os.path.join(self.temp_dir, "dir") - touch(os.path.join(new_dir, "file")) - self.assertEqual(["file"], osextras.listdir_force(new_dir)) - - def test_listdir_directory_missing(self): - new_dir = os.path.join(self.temp_dir, "dir") - self.assertEqual([], osextras.listdir_force(new_dir)) - - def test_listdir_oserror(self): - not_dir = os.path.join(self.temp_dir, "file") - touch(not_dir) - self.assertRaises(OSError, osextras.listdir_force, not_dir) + def test_find_on_path_requires_regular_file(self): + bin_dir = os.path.join(self.temp_dir, "bin") + self.mod.ensuredir(os.path.join(bin_dir, "subdir")) + os.environ["PATH"] = bin_dir + self.assertFalse(self.mod.find_on_path("subdir")) def test_unlink_file_present(self): path = os.path.join(self.temp_dir, "file") touch(path) - osextras.unlink_force(path) + self.mod.unlink_force(path) self.assertFalse(os.path.exists(path)) def test_unlink_file_missing(self): path = os.path.join(self.temp_dir, "file") - osextras.unlink_force(path) + self.mod.unlink_force(path) self.assertFalse(os.path.exists(path)) - def test_unlink_oserror(self): - path = os.path.join(self.temp_dir, "dir") - os.mkdir(path) - self.assertRaises(OSError, osextras.unlink_force, path) - def test_symlink_file_present(self): path = os.path.join(self.temp_dir, "link") touch(path) - osextras.symlink_force("source", path) + self.mod.symlink_force("source", path) self.assertTrue(os.path.islink(path)) self.assertEqual("source", os.readlink(path)) def test_symlink_link_present(self): path = os.path.join(self.temp_dir, "link") os.symlink("old", path) - osextras.symlink_force("source", path) + self.mod.symlink_force("source", path) self.assertTrue(os.path.islink(path)) self.assertEqual("source", os.readlink(path)) def test_symlink_missing(self): path = os.path.join(self.temp_dir, "link") - osextras.symlink_force("source", path) + self.mod.symlink_force("source", path) self.assertTrue(os.path.islink(path)) self.assertEqual("source", os.readlink(path)) def test_umask(self): old_mask = os.umask(0o040) try: - self.assertEqual(0o040, osextras.get_umask()) + self.assertEqual(0o040, self.mod.get_umask()) os.umask(0o002) - self.assertEqual(0o002, osextras.get_umask()) + self.assertEqual(0o002, self.mod.get_umask()) finally: os.umask(old_mask) + + +class TestOSExtrasNative(TestCase, TestOSExtrasBaseMixin): + def setUp(self): + super(TestOSExtrasNative, self).setUp() + self.use_temp_dir() + self.mod = Click + + def test_dir_read_name_directory_present(self): + new_dir = os.path.join(self.temp_dir, "dir") + touch(os.path.join(new_dir, "file")) + d = Click.Dir.open(new_dir, 0) + self.assertEqual("file", d.read_name()) + self.assertIsNone(d.read_name()) + + def test_dir_read_name_directory_missing(self): + new_dir = os.path.join(self.temp_dir, "dir") + d = Click.Dir.open(new_dir, 0) + self.assertIsNone(d.read_name()) + + def test_dir_open_error(self): + not_dir = os.path.join(self.temp_dir, "file") + touch(not_dir) + self.assertRaisesFileError( + GLib.FileError.NOTDIR, Click.Dir.open, not_dir, 0) + + def test_unlink_error(self): + path = os.path.join(self.temp_dir, "dir") + os.mkdir(path) + self.assertRaisesFileError(mock.ANY, self.mod.unlink_force, path) + + +class TestOSExtrasPython(TestCase, TestOSExtrasBaseMixin): + def setUp(self): + super(TestOSExtrasPython, self).setUp() + self.use_temp_dir() + self.mod = osextras + + def test_listdir_directory_present(self): + new_dir = os.path.join(self.temp_dir, "dir") + touch(os.path.join(new_dir, "file")) + self.assertEqual(["file"], osextras.listdir_force(new_dir)) + + def test_listdir_directory_missing(self): + new_dir = os.path.join(self.temp_dir, "dir") + self.assertEqual([], osextras.listdir_force(new_dir)) + + def test_listdir_oserror(self): + not_dir = os.path.join(self.temp_dir, "file") + touch(not_dir) + self.assertRaises(OSError, osextras.listdir_force, not_dir) + + def test_unlink_oserror(self): + path = os.path.join(self.temp_dir, "dir") + os.mkdir(path) + self.assertRaises(OSError, self.mod.unlink_force, path) diff -Nru click-0.4.16/click/tests/test_query.py click-0.4.17.2/click/tests/test_query.py --- click-0.4.16/click/tests/test_query.py 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/click/tests/test_query.py 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,52 @@ +# Copyright (C) 2014 Canonical Ltd. +# Author: Colin Watson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Unit tests for click.query.""" + +from __future__ import print_function +__all__ = [ + 'TestQuery', + ] + + +import os + +from gi.repository import Click + +from click.tests.helpers import TestCase, touch + + +class TestQuery(TestCase): + def setUp(self): + super(TestQuery, self).setUp() + self.use_temp_dir() + + def test_find_package_directory_missing(self): + path = os.path.join(self.temp_dir, "nonexistent") + self.assertRaisesQueryError( + Click.QueryError.PATH, Click.find_package_directory, path) + + def test_find_package_directory(self): + info = os.path.join(self.temp_dir, ".click", "info") + path = os.path.join(self.temp_dir, "file") + Click.ensuredir(info) + touch(path) + pkgdir = Click.find_package_directory(path) + self.assertEqual(self.temp_dir, pkgdir) + + def test_find_package_directory_outside(self): + self.assertRaisesQueryError( + Click.QueryError.NO_PACKAGE_DIR, Click.find_package_directory, + "/bin") diff -Nru click-0.4.16/click/tests/test_user.py click-0.4.17.2/click/tests/test_user.py --- click-0.4.16/click/tests/test_user.py 2014-03-04 15:23:35.000000000 +0000 +++ click-0.4.17.2/click/tests/test_user.py 2014-03-06 16:38:26.000000000 +0000 @@ -25,24 +25,26 @@ import os -from click.database import ClickDB +from gi.repository import Click + from click.tests.helpers import TestCase -from click.user import ClickUser class TestClickUser(TestCase): def setUp(self): super(TestClickUser, self).setUp() self.use_temp_dir() - self.db = ClickDB(self.temp_dir, use_system=False) + self.db = Click.DB() + self.db.add(self.temp_dir) def _setUpMultiDB(self): - self.multi_db = ClickDB(use_system=False) + self.multi_db = Click.DB() self.multi_db.add(os.path.join(self.temp_dir, "custom")) self.multi_db.add(os.path.join(self.temp_dir, "click")) user_dbs = [ - os.path.join(d.root, ".click", "users", "user") - for d in self.multi_db + os.path.join( + self.multi_db.get(i).props.root, ".click", "users", "user") + for i in range(self.multi_db.props.size) ] a_1_0 = os.path.join(self.temp_dir, "custom", "a", "1.0") os.makedirs(a_1_0) @@ -58,80 +60,90 @@ os.makedirs(user_dbs[1]) os.symlink(a_1_1, os.path.join(user_dbs[1], "a")) os.symlink(c_0_1, os.path.join(user_dbs[1], "c")) - return user_dbs, ClickUser(self.multi_db, "user") + return user_dbs, Click.User.for_user(self.multi_db, "user") + + def test_new_no_db(self): + with self.run_in_subprocess( + "click_get_db_dir", "g_get_user_name") as (enter, preloads): + enter() + preloads["click_get_db_dir"].side_effect = ( + lambda: self.make_string(self.temp_dir)) + preloads["g_get_user_name"].side_effect = ( + lambda: self.make_string("test-user")) + db_root = os.path.join(self.temp_dir, "db") + os.makedirs(db_root) + with open(os.path.join(self.temp_dir, "db.conf"), "w") as f: + print("[Click Database]", file=f) + print("root = %s" % db_root, file=f) + registry = Click.User.for_user() + self.assertEqual( + os.path.join(db_root, ".click", "users", "test-user"), + registry.get_overlay_db()) - def test_overlay_db(self): + def test_get_overlay_db(self): self.assertEqual( os.path.join(self.temp_dir, ".click", "users", "user"), - ClickUser(self.db, "user").overlay_db) - - def test_iter_missing(self): - db = ClickDB( - os.path.join(self.temp_dir, "nonexistent"), use_system=False) - registry = ClickUser(db) - self.assertEqual([], list(registry)) - - def test_iter(self): - registry = ClickUser(self.db, "user") - os.makedirs(registry.overlay_db) - os.symlink("/1.0", os.path.join(registry.overlay_db, "a")) - os.symlink("/1.1", os.path.join(registry.overlay_db, "b")) - self.assertCountEqual(["a", "b"], list(registry)) - - def test_iter_multiple_root(self): - _, registry = self._setUpMultiDB() - self.assertCountEqual(["a", "b", "c"], list(registry)) + Click.User.for_user(self.db, "user").get_overlay_db()) - def test_len_missing(self): - db = ClickDB( - os.path.join(self.temp_dir, "nonexistent"), use_system=False) - registry = ClickUser(db) - self.assertEqual(0, len(registry)) - - def test_len(self): - registry = ClickUser(self.db, "user") - os.makedirs(registry.overlay_db) - os.symlink("/1.0", os.path.join(registry.overlay_db, "a")) - os.symlink("/1.1", os.path.join(registry.overlay_db, "b")) - self.assertEqual(2, len(registry)) + def test_get_package_names_missing(self): + db = Click.DB() + db.add(os.path.join(self.temp_dir, "nonexistent")) + registry = Click.User.for_user(db) + self.assertEqual([], list(registry.get_package_names())) + + def test_get_package_names(self): + registry = Click.User.for_user(self.db, "user") + os.makedirs(registry.get_overlay_db()) + os.symlink("/1.0", os.path.join(registry.get_overlay_db(), "a")) + os.symlink("/1.1", os.path.join(registry.get_overlay_db(), "b")) + self.assertCountEqual(["a", "b"], list(registry.get_package_names())) - def test_len_multiple_root(self): + def test_get_package_names_multiple_root(self): _, registry = self._setUpMultiDB() - self.assertEqual(3, len(registry)) + self.assertCountEqual( + ["a", "b", "c"], list(registry.get_package_names())) - def test_getitem_missing(self): - registry = ClickUser(self.db, "user") - self.assertRaises(KeyError, registry.__getitem__, "a") - - def test_getitem(self): - registry = ClickUser(self.db, "user") - os.makedirs(registry.overlay_db) - os.symlink("/1.0", os.path.join(registry.overlay_db, "a")) - self.assertEqual("1.0", registry["a"]) + def test_get_version_missing(self): + registry = Click.User.for_user(self.db, "user") + self.assertRaisesUserError( + Click.UserError.NO_SUCH_PACKAGE, registry.get_version, "a") + self.assertFalse(registry.has_package_name("a")) + + def test_get_version(self): + registry = Click.User.for_user(self.db, "user") + os.makedirs(registry.get_overlay_db()) + os.symlink("/1.0", os.path.join(registry.get_overlay_db(), "a")) + self.assertEqual("1.0", registry.get_version("a")) + self.assertTrue(registry.has_package_name("a")) - def test_getitem_multiple_root(self): + def test_get_version_multiple_root(self): _, registry = self._setUpMultiDB() - self.assertEqual("1.1", registry["a"]) - self.assertEqual("2.0", registry["b"]) - self.assertEqual("0.1", registry["c"]) + self.assertEqual("1.1", registry.get_version("a")) + self.assertEqual("2.0", registry.get_version("b")) + self.assertEqual("0.1", registry.get_version("c")) + self.assertTrue(registry.has_package_name("a")) + self.assertTrue(registry.has_package_name("b")) + self.assertTrue(registry.has_package_name("c")) def test_set_version_missing_target(self): - registry = ClickUser(self.db, "user") - self.assertRaises(KeyError, registry.set_version, "a", "1.0") + registry = Click.User.for_user(self.db, "user") + self.assertRaisesDatabaseError( + Click.DatabaseError.DOES_NOT_EXIST, + registry.set_version, "a", "1.0") def test_set_version_missing(self): - registry = ClickUser(self.db, "user") + registry = Click.User.for_user(self.db, "user") os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) registry.set_version("a", "1.0") - path = os.path.join(registry.overlay_db, "a") + path = os.path.join(registry.get_overlay_db(), "a") self.assertTrue(os.path.islink(path)) self.assertEqual( os.path.join(self.temp_dir, "a", "1.0"), os.readlink(path)) def test_set_version_changed(self): - registry = ClickUser(self.db, "user") - os.makedirs(registry.overlay_db) - path = os.path.join(registry.overlay_db, "a") + registry = Click.User.for_user(self.db, "user") + os.makedirs(registry.get_overlay_db()) + path = os.path.join(registry.get_overlay_db(), "a") os.symlink("/1.0", path) os.makedirs(os.path.join(self.temp_dir, "a", "1.1")) registry.set_version("a", "1.1") @@ -142,50 +154,50 @@ def test_set_version_multiple_root(self): user_dbs, registry = self._setUpMultiDB() - os.makedirs(os.path.join(self.multi_db[1].root, "a", "1.2")) + os.makedirs(os.path.join(self.multi_db.get(1).props.root, "a", "1.2")) registry.set_version("a", "1.2") a_underlay = os.path.join(user_dbs[0], "a") a_overlay = os.path.join(user_dbs[1], "a") self.assertTrue(os.path.islink(a_underlay)) self.assertEqual( - os.path.join(self.multi_db[0].root, "a", "1.0"), + os.path.join(self.multi_db.get(0).props.root, "a", "1.0"), os.readlink(a_underlay)) self.assertTrue(os.path.islink(a_overlay)) self.assertEqual( - os.path.join(self.multi_db[1].root, "a", "1.2"), + os.path.join(self.multi_db.get(1).props.root, "a", "1.2"), os.readlink(a_overlay)) - os.makedirs(os.path.join(self.multi_db[1].root, "b", "2.1")) + os.makedirs(os.path.join(self.multi_db.get(1).props.root, "b", "2.1")) registry.set_version("b", "2.1") b_underlay = os.path.join(user_dbs[0], "b") b_overlay = os.path.join(user_dbs[1], "b") self.assertTrue(os.path.islink(b_underlay)) self.assertEqual( - os.path.join(self.multi_db[0].root, "b", "2.0"), + os.path.join(self.multi_db.get(0).props.root, "b", "2.0"), os.readlink(b_underlay)) self.assertTrue(os.path.islink(b_overlay)) self.assertEqual( - os.path.join(self.multi_db[1].root, "b", "2.1"), + os.path.join(self.multi_db.get(1).props.root, "b", "2.1"), os.readlink(b_overlay)) - os.makedirs(os.path.join(self.multi_db[1].root, "c", "0.2")) + os.makedirs(os.path.join(self.multi_db.get(1).props.root, "c", "0.2")) registry.set_version("c", "0.2") c_underlay = os.path.join(user_dbs[0], "c") c_overlay = os.path.join(user_dbs[1], "c") self.assertFalse(os.path.islink(c_underlay)) self.assertTrue(os.path.islink(c_overlay)) self.assertEqual( - os.path.join(self.multi_db[1].root, "c", "0.2"), + os.path.join(self.multi_db.get(1).props.root, "c", "0.2"), os.readlink(c_overlay)) - os.makedirs(os.path.join(self.multi_db[1].root, "d", "3.0")) + os.makedirs(os.path.join(self.multi_db.get(1).props.root, "d", "3.0")) registry.set_version("d", "3.0") d_underlay = os.path.join(user_dbs[0], "d") d_overlay = os.path.join(user_dbs[1], "d") self.assertFalse(os.path.islink(d_underlay)) self.assertTrue(os.path.islink(d_overlay)) self.assertEqual( - os.path.join(self.multi_db[1].root, "d", "3.0"), + os.path.join(self.multi_db.get(1).props.root, "d", "3.0"), os.readlink(d_overlay)) def test_set_version_restore_to_underlay(self): @@ -196,29 +208,30 @@ # Initial state: 1.0 in underlay, 1.1 in overlay. self.assertTrue(os.path.islink(a_underlay)) self.assertEqual( - os.path.join(self.multi_db[0].root, "a", "1.0"), + os.path.join(self.multi_db.get(0).props.root, "a", "1.0"), os.readlink(a_underlay)) self.assertTrue(os.path.islink(a_overlay)) self.assertEqual( - os.path.join(self.multi_db[1].root, "a", "1.1"), + os.path.join(self.multi_db.get(1).props.root, "a", "1.1"), os.readlink(a_overlay)) # Setting to 1.0 (version in underlay) removes overlay link. registry.set_version("a", "1.0") self.assertTrue(os.path.islink(a_underlay)) self.assertEqual( - os.path.join(self.multi_db[0].root, "a", "1.0"), + os.path.join(self.multi_db.get(0).props.root, "a", "1.0"), os.readlink(a_underlay)) self.assertFalse(os.path.islink(a_overlay)) def test_remove_missing(self): - registry = ClickUser(self.db, "user") - self.assertRaises(KeyError, registry.remove, "a") + registry = Click.User.for_user(self.db, "user") + self.assertRaisesUserError( + Click.UserError.NO_SUCH_PACKAGE, registry.remove, "a") def test_remove(self): - registry = ClickUser(self.db, "user") - os.makedirs(registry.overlay_db) - path = os.path.join(registry.overlay_db, "a") + registry = Click.User.for_user(self.db, "user") + os.makedirs(registry.get_overlay_db()) + path = os.path.join(registry.get_overlay_db(), "a") os.symlink("/1.0", path) registry.remove("a") self.assertFalse(os.path.exists(path)) @@ -228,81 +241,90 @@ registry.remove("a") self.assertFalse(os.path.exists(os.path.join(user_dbs[1], "a"))) # Exposed underlay. - self.assertEqual("1.0", registry["a"]) + self.assertEqual("1.0", registry.get_version("a")) registry.remove("b") # Hidden. self.assertEqual( "@hidden", os.readlink(os.path.join(user_dbs[1], "b"))) - self.assertNotIn("b", registry) + self.assertFalse(registry.has_package_name("b")) registry.remove("c") self.assertFalse(os.path.exists(os.path.join(user_dbs[1], "c"))) - self.assertNotIn("c", registry) - self.assertRaises(KeyError, registry.remove, "d") + self.assertFalse(registry.has_package_name("c")) + self.assertRaisesUserError( + Click.UserError.NO_SUCH_PACKAGE, registry.remove, "d") def test_remove_multiple_root_creates_overlay_directory(self): - multi_db = ClickDB(use_system=False) + multi_db = Click.DB() multi_db.add(os.path.join(self.temp_dir, "preinstalled")) multi_db.add(os.path.join(self.temp_dir, "click")) user_dbs = [ - os.path.join(d.root, ".click", "users", "user") for d in multi_db + os.path.join(multi_db.get(i).props.root, ".click", "users", "user") + for i in range(multi_db.props.size) ] a_1_0 = os.path.join(self.temp_dir, "preinstalled", "a", "1.0") os.makedirs(a_1_0) os.makedirs(user_dbs[0]) os.symlink(a_1_0, os.path.join(user_dbs[0], "a")) self.assertFalse(os.path.exists(user_dbs[1])) - registry = ClickUser(multi_db, "user") - self.assertEqual("1.0", registry["a"]) + registry = Click.User.for_user(multi_db, "user") + self.assertEqual("1.0", registry.get_version("a")) registry.remove("a") - self.assertNotIn("a", registry) + self.assertFalse(registry.has_package_name("a")) self.assertEqual( "@hidden", os.readlink(os.path.join(user_dbs[1], "a"))) - def test_path(self): - registry = ClickUser(self.db, "user") + def test_get_path(self): + registry = Click.User.for_user(self.db, "user") os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) registry.set_version("a", "1.0") self.assertEqual( - os.path.join(registry.overlay_db, "a"), registry.path("a")) + os.path.join(registry.get_overlay_db(), "a"), + registry.get_path("a")) - def test_path_multiple_root(self): + def test_get_path_multiple_root(self): user_dbs, registry = self._setUpMultiDB() - self.assertEqual(os.path.join(user_dbs[1], "a"), registry.path("a")) - self.assertEqual(os.path.join(user_dbs[0], "b"), registry.path("b")) - self.assertEqual(os.path.join(user_dbs[1], "c"), registry.path("c")) - self.assertRaises(KeyError, registry.path, "d") + self.assertEqual( + os.path.join(user_dbs[1], "a"), registry.get_path("a")) + self.assertEqual( + os.path.join(user_dbs[0], "b"), registry.get_path("b")) + self.assertEqual( + os.path.join(user_dbs[1], "c"), registry.get_path("c")) + self.assertRaisesUserError( + Click.UserError.NO_SUCH_PACKAGE, registry.get_path, "d") - def test_removable(self): - registry = ClickUser(self.db, "user") + def test_is_removable(self): + registry = Click.User.for_user(self.db, "user") os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) registry.set_version("a", "1.0") - self.assertTrue(registry.removable("a")) + self.assertTrue(registry.is_removable("a")) - def test_removable_multiple_root(self): + def test_is_removable_multiple_root(self): user_dbs, registry = self._setUpMultiDB() - self.assertTrue(registry.removable("a")) - self.assertTrue(registry.removable("b")) - self.assertTrue(registry.removable("c")) - self.assertFalse(registry.removable("d")) + self.assertTrue(registry.is_removable("a")) + self.assertTrue(registry.is_removable("b")) + self.assertTrue(registry.is_removable("c")) + self.assertFalse(registry.is_removable("d")) def test_hidden(self): user_dbs, registry = self._setUpMultiDB() b_overlay = os.path.join(user_dbs[1], "b") registry.remove("b") - self.assertNotIn("b", registry) + self.assertFalse(registry.has_package_name("b")) self.assertTrue(os.path.islink(b_overlay)) self.assertEqual("@hidden", os.readlink(b_overlay)) - self.assertRaises(KeyError, registry.__getitem__, "b") - self.assertRaises(KeyError, registry.path, "b") - self.assertFalse(registry.removable("b")) + self.assertRaisesUserError( + Click.UserError.HIDDEN_PACKAGE, registry.get_version, "b") + self.assertRaisesUserError( + Click.UserError.HIDDEN_PACKAGE, registry.get_path, "b") + self.assertFalse(registry.is_removable("b")) registry.set_version("b", "2.0") - self.assertIn("b", registry) + self.assertTrue(registry.has_package_name("b")) self.assertTrue(os.path.islink(b_overlay)) self.assertEqual( - os.path.join(self.multi_db[0].root, "b", "2.0"), + os.path.join(self.multi_db.get(0).props.root, "b", "2.0"), os.readlink(b_overlay)) - self.assertEqual("2.0", registry["b"]) - self.assertEqual(b_overlay, registry.path("b")) - self.assertTrue(registry.removable("b")) + self.assertEqual("2.0", registry.get_version("b")) + self.assertEqual(b_overlay, registry.get_path("b")) + self.assertTrue(registry.is_removable("b")) diff -Nru click-0.4.16/click/user.py click-0.4.17.2/click/user.py --- click-0.4.16/click/user.py 2014-03-04 15:23:35.000000000 +0000 +++ click-0.4.17.2/click/user.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,344 +0,0 @@ -# Copyright (C) 2013 Canonical Ltd. -# Author: Colin Watson - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Registry of user-installed Click packages. - -Click packages are installed into per-package/version directories, so it is -quite feasible for more than one version of a given package to be installed -at once, allowing per-user installations; for instance, one user of a tablet -may be uncomfortable with granting some new permission to an app, but -another may be just fine with it. To make this useful, we also need a -registry of which users have which versions of each package installed. - -We might have chosen to use a proper database. However, a major goal of -Click packages is extreme resilience; we must never get into a situation -where some previous error in package installation or removal makes it hard -for the user to install or remove other packages. Furthermore, the simpler -application execution can be the better. So, instead, we use just about the -simplest "database" format imaginable: a directory of symlinks per user. -""" - -from __future__ import print_function - -__metaclass__ = type -__all__ = [ - 'ClickUser', - 'ClickUsers', - ] - -from collections import Mapping -from contextlib import contextmanager -import os -import pwd - -from click import osextras - - -# Pseudo-usernames selected to be invalid as a real username, and alluding -# to group syntaxes used in other systems. -ALL_USERS = "@all" -GC_IN_USE_USER = "@gcinuse" - -# Pseudo-versions. In this case the @ doesn't allude to group syntaxes, but -# since @ is conveniently invalid in version numbers we stick to the same -# prefix used for pseudo-usernames. -HIDDEN_VERSION = "@hidden" - - -def _db_top(root): - # This is deliberately outside any user's home directory so that it can - # safely be iterated etc. as root. - return os.path.join(root, ".click", "users") - - -def _db_for_user(root, user): - return os.path.join(_db_top(root), user) - - -class ClickUsers(Mapping): - def __init__(self, db): - self.db = db - self._click_pw = None - - @property - def click_pw(self): - if self._click_pw is None: - self._click_pw = pwd.getpwnam("clickpkg") - return self._click_pw - - def _ensure_db(self): - create = [] - # Only modify the last database. - path = _db_top(self.db.overlay) - while not os.path.exists(path): - create.append(path) - path = os.path.dirname(path) - for path in reversed(create): - os.mkdir(path) - if os.geteuid() == 0: - pw = self.click_pw - os.chown(path, pw.pw_uid, pw.pw_gid) - - def __iter__(self): - seen = set() - for db in self.db: - user_db = _db_top(db.root) - for entry in osextras.listdir_force(user_db): - if entry in seen: - continue - if os.path.isdir(os.path.join(user_db, entry)): - seen.add(entry) - yield entry - - def __len__(self): - count = 0 - for entry in self: - count += 1 - return count - - def __getitem__(self, user): - for db in self.db: - path = _db_for_user(db.root, user) - if os.path.isdir(path): - # We only require the user path to exist in any database; it - # doesn't matter which. - return ClickUser(self.db, user=user) - else: - raise KeyError("User %s does not exist in any database" % user) - - -class ClickUser(Mapping): - """Database of package versions registered for a single user.""" - - def __init__(self, db, user=None, all_users=False): - if user is None: - user = pwd.getpwuid(os.getuid()).pw_name - self.db = db - self.user = user - self.all_users = all_users - if self.all_users: - self.user = ALL_USERS - self._users = None - self._user_pw = None - self._dropped_privileges_count = 0 - self._old_umask = None - - @property - def pseudo_user(self): - return self.user.startswith("@") - - @property - def user_pw(self): - assert not self.pseudo_user - if self._user_pw is None: - self._user_pw = pwd.getpwnam(self.user) - return self._user_pw - - @property - def overlay_db(self): - # Only modify the last database. - return _db_for_user(self.db.overlay, self.user) - - def _ensure_db(self): - if self._users is None: - self._users = ClickUsers(self.db) - self._users._ensure_db() - path = self.overlay_db - if not os.path.exists(path): - os.mkdir(path) - if os.geteuid() == 0 and not self.pseudo_user: - pw = self.user_pw - os.chown(path, pw.pw_uid, pw.pw_gid) - - def _drop_privileges(self): - if (self._dropped_privileges_count == 0 and os.getuid() == 0 and - not self.pseudo_user): - # We don't bother with setgroups here; we only need the - # user/group of created filesystem nodes to be correct. - pw = self.user_pw - os.setegid(pw.pw_gid) - os.seteuid(pw.pw_uid) - self._old_umask = os.umask(osextras.get_umask() | 0o002) - self._dropped_privileges_count += 1 - - def _regain_privileges(self): - self._dropped_privileges_count -= 1 - if (self._dropped_privileges_count == 0 and os.getuid() == 0 and - not self.pseudo_user): - if self._old_umask is not None: - os.umask(self._old_umask) - os.seteuid(0) - os.setegid(0) - - # Note on privilege handling: - # We can normally get away without dropping privilege when reading, but - # some filesystems are strict about how much they let root work with - # user files (e.g. NFS root_squash). It is better to play it safe and - # drop privileges for any operations on the user's database. - @contextmanager - def _dropped_privileges(self): - self._drop_privileges() - try: - yield - finally: - self._regain_privileges() - - def _is_valid_link(self, path): - return os.path.islink(path) and not os.readlink(path).startswith("@") - - def __iter__(self): - # We cannot be lazy here, because otherwise calling code may - # unwittingly end up with dropped privileges. - entries = [] - hidden = set() - with self._dropped_privileges(): - for db in reversed(self.db): - user_db = _db_for_user(db.root, self.user) - for entry in osextras.listdir_force(user_db): - if entry in entries or entry in hidden: - continue - path = os.path.join(user_db, entry) - if self._is_valid_link(path): - entries.append(entry) - elif os.path.islink(path): - hidden.add(entry) - if not self.all_users: - all_users_db = _db_for_user(db.root, ALL_USERS) - for entry in osextras.listdir_force(all_users_db): - if entry in entries or entry in hidden: - continue - path = os.path.join(all_users_db, entry) - if self._is_valid_link(path): - entries.append(entry) - elif os.path.islink(path): - hidden.add(entry) - - return iter(entries) - - def __len__(self): - count = 0 - for entry in self: - count += 1 - return count - - def __getitem__(self, package): - for db in reversed(self.db): - user_db = _db_for_user(db.root, self.user) - path = os.path.join(user_db, package) - with self._dropped_privileges(): - if self._is_valid_link(path): - return os.path.basename(os.readlink(path)) - elif os.path.islink(path): - raise KeyError( - "%s is hidden for user %s" % (package, self.user)) - all_users_db = _db_for_user(db.root, ALL_USERS) - path = os.path.join(all_users_db, package) - if self._is_valid_link(path): - return os.path.basename(os.readlink(path)) - elif os.path.islink(path): - raise KeyError("%s is hidden for all users" % package) - else: - raise KeyError( - "%s does not exist in any database for user %s" % - (package, self.user)) - - def set_version(self, package, version): - # Circular import. - from click.hooks import package_install_hooks - - # Only modify the last database. - user_db = self.overlay_db - path = os.path.join(user_db, package) - new_path = os.path.join(user_db, ".%s.new" % package) - self._ensure_db() - old_version = self.get(package) - with self._dropped_privileges(): - target = self.db.path(package, version) - done = False - if self._is_valid_link(path): - osextras.unlink_force(path) - if self.get(package) == version: - done = True - if not done: - osextras.symlink_force(target, new_path) - os.rename(new_path, path) - if not self.pseudo_user: - package_install_hooks( - self.db, package, old_version, version, user=self.user) - - def remove(self, package): - # Circular import. - from click.hooks import package_remove_hooks - - # Only modify the last database. - user_db = self.overlay_db - path = os.path.join(user_db, package) - if self._is_valid_link(path): - old_version = os.path.basename(os.readlink(path)) - with self._dropped_privileges(): - osextras.unlink_force(path) - else: - try: - old_version = self[package] - self._ensure_db() - with self._dropped_privileges(): - osextras.symlink_force(HIDDEN_VERSION, path) - except KeyError: - raise KeyError( - "%s does not exist in any database for user %s" % - (package, self.user)) - if not self.pseudo_user: - package_remove_hooks(self.db, package, old_version, user=self.user) - - def path(self, package): - for db in reversed(self.db): - user_db = _db_for_user(db.root, self.user) - path = os.path.join(user_db, package) - if self._is_valid_link(path): - return path - elif os.path.islink(path): - raise KeyError( - "%s is hidden for user %s" % (package, self.user)) - all_users_db = _db_for_user(db.root, ALL_USERS) - path = os.path.join(all_users_db, package) - if self._is_valid_link(path): - return path - elif os.path.islink(path): - raise KeyError("%s is hidden for all users" % package) - else: - raise KeyError( - "%s does not exist in any database for user %s" % - (package, self.user)) - - def removable(self, package): - user_db = self.overlay_db - path = os.path.join(user_db, package) - if os.path.exists(path): - return True - elif os.path.islink(path): - # Already hidden. - return False - all_users_db = _db_for_user(self.db.overlay, ALL_USERS) - path = os.path.join(all_users_db, package) - if self._is_valid_link(path): - return True - elif os.path.islink(path): - # Already hidden. - return False - if package in self: - # Not in overlay database, but can be hidden. - return True - else: - return False diff -Nru click-0.4.16/configure.ac click-0.4.17.2/configure.ac --- click-0.4.16/configure.ac 2014-03-04 15:23:35.000000000 +0000 +++ click-0.4.17.2/configure.ac 2014-03-06 16:38:26.000000000 +0000 @@ -4,6 +4,7 @@ AC_CONFIG_MACRO_DIR([m4]) AM_INIT_AUTOMAKE([foreign]) AM_CONFIG_HEADER([config.h]) +AC_USE_SYSTEM_EXTENSIONS LT_INIT([disable-static]) AC_SUBST([GETTEXT_PACKAGE], [click]) @@ -35,6 +36,43 @@ [click_cv_perl_vendorlib=`perl -MConfig -e 'print $Config{vendorlib}'`]) AC_SUBST([perl_vendorlib], ["$click_cv_perl_vendorlib"]) +AM_PROG_VALAC +PKG_CHECK_MODULES([LIBCLICK], [ + glib-2.0 >= 2.34 + gobject-2.0 >= 2.34 + json-glib-1.0 >= 0.10 + gee-0.8 + ]) +AC_SUBST([LIBCLICK_CFLAGS]) +AC_SUBST([LIBCLICK_LIBS]) + +# Structure characteristics needed for the Python/C integration in the test +# suite. +AC_COMPUTE_INT([STAT_OFFSET_UID], [offsetof(struct stat, st_uid)], [ + AC_INCLUDES_DEFAULT + #include + ]) +AC_SUBST([STAT_OFFSET_UID]) +AC_COMPUTE_INT([STAT_OFFSET_GID], [offsetof(struct stat, st_gid)], [ + AC_INCLUDES_DEFAULT + #include + ]) +AC_SUBST([STAT_OFFSET_GID]) +AC_COMPUTE_INT([STAT64_OFFSET_UID], [offsetof(struct stat64, st_uid)], [ + AC_INCLUDES_DEFAULT + #include + ]) +AC_SUBST([STAT64_OFFSET_UID]) +AC_COMPUTE_INT([STAT64_OFFSET_GID], [offsetof(struct stat64, st_gid)], [ + AC_INCLUDES_DEFAULT + #include + ]) +AC_SUBST([STAT64_OFFSET_GID]) + +GOBJECT_INTROSPECTION_REQUIRE([0.6.7]) +VAPIGEN_VAPIDIR=`$PKG_CONFIG --variable=vapidir vapigen` +AC_SUBST([VAPIGEN_VAPIDIR]) + AC_ARG_ENABLE([packagekit], AS_HELP_STRING([--disable-packagekit], [disable PackageKit plugin]), [], [enable_packagekit=yes]) @@ -55,18 +93,54 @@ fi AM_CONDITIONAL([PACKAGEKIT], [test "x$enable_packagekit" = xyes]) +AC_ARG_ENABLE([systemd], + AS_HELP_STRING([--disable-systemd], [Disable systemd integration])) +AM_CONDITIONAL([INSTALL_SYSTEMD], [test "x$enable_systemd" != xno]) + +AC_ARG_WITH([systemdsystemunitdir], + AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd system unit files])) +if test "x$enable_systemd" != xno && test "x$with_systemdsystemunitdir" = x; then + AC_MSG_CHECKING([for systemd system unit directory]) + with_systemdsystemunitdir="$($PKG_CONFIG --variable=systemdsystemunitdir systemd)" + if test "x$with_systemdsystemunitdir" = x; then + AC_MSG_ERROR([no systemd system unit directory found]) + fi + AC_MSG_RESULT([$with_systemdsystemunitdir]) +fi +AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir]) + +AC_ARG_WITH([systemduserunitdir], + AS_HELP_STRING([--with-systemduserunitdir=DIR], [Directory for systemd user unit files])) +if test "x$enable_systemd" != xno && test "x$with_systemduserunitdir" = x; then + AC_MSG_CHECKING([for systemd user unit directory]) + with_systemduserunitdir="$($PKG_CONFIG --variable=systemduserunitdir systemd)" + if test "x$with_systemduserunitdir" = x; then + AC_MSG_ERROR([no systemd user unit directory found]) + fi + AC_MSG_RESULT([$with_systemduserunitdir]) +fi +AC_SUBST([systemduserunitdir], [$with_systemduserunitdir]) + AC_CONFIG_FILES([ Makefile click/Makefile + click/tests/Makefile + click/tests/config.py conf/Makefile conf/databases/Makefile conf/databases/99_default.conf debhelper/Makefile + init/Makefile + init/systemd/Makefile + init/upstart/Makefile + lib/Makefile + lib/click/Makefile + lib/click/click-0.4.pc pk-plugin/Makefile po/Makefile.in preload/Makefile schroot/Makefile - upstart/Makefile ]) +AC_CONFIG_FILES([lib/click/valac-wrapper], [chmod +x lib/click/valac-wrapper]) AC_CONFIG_FILES([setup.py], [chmod +x setup.py]) AC_OUTPUT diff -Nru click-0.4.16/debian/changelog click-0.4.17.2/debian/changelog --- click-0.4.16/debian/changelog 2014-03-04 15:23:45.000000000 +0000 +++ click-0.4.17.2/debian/changelog 2014-03-06 16:38:35.000000000 +0000 @@ -1,3 +1,34 @@ +click (0.4.17.2) trusty; urgency=medium + + [ Colin Watson ] + * Fix Click.User construction in "click pkgdir". + + -- Ubuntu daily release Thu, 06 Mar 2014 16:38:35 +0000 + +click (0.4.17.1) trusty; urgency=medium + + * gobject-introspection-1.0.pc is in libgirepository1.0-dev, not + gobject-introspection. Fix Build-Depends. + * Build-depend and depend on gir1.2-glib-2.0 and python3-gi. + * Map gboolean to ctypes.c_int, not ctypes.c_bool. gboolean and gint are + the same as far as glib is concerned, and ctypes does strange things + with its bool type in callbacks. + + -- Colin Watson Thu, 06 Mar 2014 16:09:33 +0000 + +click (0.4.17) trusty; urgency=medium + + * Use full path to click in Upstart jobs to save a $PATH lookup. + * Add systemd units to run Click system and user hooks at the appropriate + times. We probably won't be using these for a while, but it does no + harm to add them. + * Move an initial core of functionality (database, hooks, osextras, query, + user) from Python into a new "libclick" library, allowing + performance-critical clients to avoid the cost of starting a new Python + interpreter (LP: #1282311). + + -- Colin Watson Thu, 06 Mar 2014 14:35:26 +0000 + click (0.4.16) trusty; urgency=medium [ Colin Watson ] diff -Nru click-0.4.16/debian/click.install click-0.4.17.2/debian/click.install --- click-0.4.16/debian/click.install 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/debian/click.install 2014-03-06 16:38:26.000000000 +0000 @@ -1,5 +1,7 @@ etc/click etc/init +lib/systemd usr/bin/click usr/lib/*/click +usr/lib/systemd usr/share/upstart diff -Nru click-0.4.16/debian/control click-0.4.17.2/debian/control --- click-0.4.16/debian/control 2014-03-04 15:23:35.000000000 +0000 +++ click-0.4.17.2/debian/control 2014-03-06 16:38:26.000000000 +0000 @@ -3,7 +3,7 @@ Priority: optional Maintainer: Colin Watson Standards-Version: 3.9.5 -Build-Depends: debhelper (>= 9~), dh-autoreconf, intltool, python3:any (>= 3.2), python3-all:any, python3-setuptools, python3-apt, python3-debian, python3:any (>= 3.3) | python3-mock, pep8, python3-pep8, pyflakes, python3-sphinx, pkg-config, libglib2.0-dev (>= 2.34), libjson-glib-dev (>= 0.10), libpackagekit-glib2-dev (>= 0.7.2) +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) Vcs-Bzr: https://code.launchpad.net/~ubuntu-managed-branches/click/click Vcs-Browser: http://bazaar.launchpad.net/~ubuntu-managed-branches/click/click/files X-Auto-Uploader: no-rewrite-version @@ -38,7 +38,7 @@ Package: python3-click Section: python Architecture: any -Depends: ${misc:Depends}, ${python3:Depends}, python3-apt, python3-debian +Depends: ${misc:Depends}, ${python3:Depends}, gir1.2-click-0.4 (= ${binary:Version}), gir1.2-glib-2.0, python3-apt, python3-debian, python3-gi Conflicts: python3-click-package Replaces: python3-click-package Provides: python3-click-package @@ -49,6 +49,42 @@ This package provides Python 3 modules used by click, which may also be used directly. +Package: libclick-0.4-0 +Section: libs +Architecture: any +Multi-Arch: same +Pre-Depends: ${misc:Pre-Depends} +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: run-time Click package management library + Click is a simplified packaging format that installs in a separate part of + the file system, suitable for third-party applications. + . + This package provides a shared library for managing Click packages. + +Package: libclick-0.4-dev +Section: libdevel +Architecture: any +Multi-Arch: same +Pre-Depends: ${misc:Pre-Depends} +Depends: ${shlibs:Depends}, ${misc:Depends}, libclick-0.4-0 (= ${binary:Version}), libglib2.0-dev +Description: development files for Click package management library + Click is a simplified packaging format that installs in a separate part of + the file system, suitable for third-party applications. + . + This package provides development files needed to build programs for + managing Click packages. + +Package: gir1.2-click-0.4 +Section: introspection +Architecture: any +Depends: ${misc:Depends}, ${gir:Depends} +Description: GIR bindings for Click package management library + Click is a simplified packaging format that installs in a separate part of + the file system, suitable for third-party applications. + . + This package can be used by other packages using the GIRepository format to + generate dynamic bindings. + Package: click-doc Section: doc Architecture: all diff -Nru click-0.4.16/debian/gir1.2-click-0.4.install click-0.4.17.2/debian/gir1.2-click-0.4.install --- click-0.4.16/debian/gir1.2-click-0.4.install 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/debian/gir1.2-click-0.4.install 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1 @@ +usr/lib/*/girepository-1.0 usr/lib diff -Nru click-0.4.16/debian/libclick-0.4-0.install click-0.4.17.2/debian/libclick-0.4-0.install --- click-0.4.16/debian/libclick-0.4-0.install 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/debian/libclick-0.4-0.install 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1 @@ +usr/lib/*/libclick*.so.* diff -Nru click-0.4.16/debian/libclick-0.4-0.symbols click-0.4.17.2/debian/libclick-0.4-0.symbols --- click-0.4.16/debian/libclick-0.4-0.symbols 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/debian/libclick-0.4-0.symbols 2014-03-06 16:38:34.000000000 +0000 @@ -0,0 +1,87 @@ +libclick-0.4.so.0 libclick-0.4-0 #MINVER# +* Build-Depends-Package: libclick-0.4-dev + click_database_error_quark@Base 0.4.17 + click_db_add@Base 0.4.17 + click_db_ensure_ownership@Base 0.4.17 + click_db_gc@Base 0.4.17 + click_db_get@Base 0.4.17 + click_db_get_overlay@Base 0.4.17 + click_db_get_packages@Base 0.4.17 + click_db_get_path@Base 0.4.17 + click_db_get_size@Base 0.4.17 + click_db_get_type@Base 0.4.17 + click_db_maybe_remove@Base 0.4.17 + click_db_new@Base 0.4.17 + click_db_read@Base 0.4.17 + click_dir_get_type@Base 0.4.17 + click_dir_open@Base 0.4.17 + click_dir_read_name@Base 0.4.17 + click_ensuredir@Base 0.4.17 + click_find_on_path@Base 0.4.17 + click_find_package_directory@Base 0.4.17 + click_get_db_dir@Base 0.4.17 + click_get_hooks_dir@Base 0.4.17 + click_get_umask@Base 0.4.17 + click_hook_get_app_id@Base 0.4.17 + click_hook_get_field@Base 0.4.17 + click_hook_get_fields@Base 0.4.17 + click_hook_get_hook_name@Base 0.4.17 + click_hook_get_is_single_version@Base 0.4.17 + click_hook_get_is_user_level@Base 0.4.17 + click_hook_get_pattern@Base 0.4.17 + click_hook_get_run_commands_user@Base 0.4.17 + click_hook_get_short_app_id@Base 0.4.17 + click_hook_get_type@Base 0.4.17 + click_hook_install@Base 0.4.17 + click_hook_install_package@Base 0.4.17 + click_hook_open@Base 0.4.17 + click_hook_open_all@Base 0.4.17 + click_hook_remove@Base 0.4.17 + click_hook_remove_package@Base 0.4.17 + click_hook_run_commands@Base 0.4.17 + click_hook_sync@Base 0.4.17 + click_hooks_error_quark@Base 0.4.17 + click_installed_package_get_package@Base 0.4.17 + click_installed_package_get_path@Base 0.4.17 + click_installed_package_get_type@Base 0.4.17 + click_installed_package_get_version@Base 0.4.17 + click_installed_package_get_writeable@Base 0.4.17 + click_installed_package_new@Base 0.4.17 + click_package_install_hooks@Base 0.4.17 + click_package_remove_hooks@Base 0.4.17 + click_pattern_format@Base 0.4.17 + click_pattern_possible_expansion@Base 0.4.17 + click_query_error_quark@Base 0.4.17 + click_run_system_hooks@Base 0.4.17 + click_run_user_hooks@Base 0.4.17 + click_single_db_any_app_running@Base 0.4.17 + click_single_db_app_running@Base 0.4.17 + click_single_db_ensure_ownership@Base 0.4.17 + click_single_db_gc@Base 0.4.17 + click_single_db_get_packages@Base 0.4.17 + click_single_db_get_path@Base 0.4.17 + click_single_db_get_root@Base 0.4.17 + click_single_db_get_type@Base 0.4.17 + click_single_db_maybe_remove@Base 0.4.17 + click_single_db_new@Base 0.4.17 + click_symlink_force@Base 0.4.17 + click_unlink_force@Base 0.4.17 + click_user_error_quark@Base 0.4.17 + click_user_get_is_gc_in_use@Base 0.4.17 + click_user_get_is_pseudo_user@Base 0.4.17 + click_user_get_overlay_db@Base 0.4.17 + click_user_get_package_names@Base 0.4.17 + click_user_get_path@Base 0.4.17 + click_user_get_type@Base 0.4.17 + click_user_get_version@Base 0.4.17 + click_user_has_package_name@Base 0.4.17 + click_user_is_removable@Base 0.4.17 + click_user_new_for_all_users@Base 0.4.17 + click_user_new_for_gc_in_use@Base 0.4.17 + click_user_new_for_user@Base 0.4.17 + click_user_remove@Base 0.4.17 + click_user_set_version@Base 0.4.17 + click_users_get_type@Base 0.4.17 + click_users_get_user@Base 0.4.17 + click_users_get_user_names@Base 0.4.17 + click_users_new@Base 0.4.17 diff -Nru click-0.4.16/debian/libclick-0.4-dev.install click-0.4.17.2/debian/libclick-0.4-dev.install --- click-0.4.16/debian/libclick-0.4-dev.install 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/debian/libclick-0.4-dev.install 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,4 @@ +usr/include +usr/lib/*/libclick*.so +usr/lib/*/pkgconfig/click-*.pc +usr/share/gir-1.0 diff -Nru click-0.4.16/debian/rules click-0.4.17.2/debian/rules --- click-0.4.16/debian/rules 2014-03-04 15:23:35.000000000 +0000 +++ click-0.4.17.2/debian/rules 2014-03-06 16:38:26.000000000 +0000 @@ -7,8 +7,13 @@ EXTRA_DH_OPTIONS := -Npackagekit-plugin-click endif +# The advantages of -Wl,-Bsymbolic-functions are of limited value here, and +# they mean that the test suite's LD_PRELOAD tricks don't work properly. +export DEB_LDFLAGS_MAINT_STRIP := -Wl,-Bsymbolic-functions + %: - dh $@ --with autoreconf,python3,sphinxdoc --buildsystem autoconf $(EXTRA_DH_OPTIONS) + dh $@ --with autoreconf,gir,python3,sphinxdoc \ + --buildsystem autoconf $(EXTRA_DH_OPTIONS) PY3REQUESTED := $(shell py3versions -r) PY3DEFAULT := $(shell py3versions -d) @@ -16,7 +21,10 @@ # #!/usr/bin/python3 and not #!/usr/bin/python3.X. PY3 := $(filter-out $(PY3DEFAULT),$(PY3REQUESTED)) python3 -confflags := --with-python-interpreters='$(PY3)' +confflags := \ + --with-python-interpreters='$(PY3)' \ + --with-systemdsystemunitdir=/lib/systemd/system \ + --with-systemduserunitdir=/usr/lib/systemd/user ifeq ($(PACKAGEKIT),no) confflags += --disable-packagekit endif @@ -49,3 +57,6 @@ # Sphinx documentation is architecture-independent. override_dh_sphinxdoc-arch: + +override_dh_makeshlibs: + dh_makeshlibs -- -c4 diff -Nru click-0.4.16/init/Makefile.am click-0.4.17.2/init/Makefile.am --- click-0.4.16/init/Makefile.am 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/init/Makefile.am 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1 @@ +SUBDIRS = systemd upstart diff -Nru click-0.4.16/init/systemd/click-system-hooks.service.in click-0.4.17.2/init/systemd/click-system-hooks.service.in --- click-0.4.16/init/systemd/click-system-hooks.service.in 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/init/systemd/click-system-hooks.service.in 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,12 @@ +[Unit] +Description=Run Click system-level hooks +Documentation=man:click(1) + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=@bindir@/click hook run-system +Restart=no + +[Install] +WantedBy=multi-user.target diff -Nru click-0.4.16/init/systemd/click-user-hooks.service.in click-0.4.17.2/init/systemd/click-user-hooks.service.in --- click-0.4.16/init/systemd/click-user-hooks.service.in 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/init/systemd/click-user-hooks.service.in 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,12 @@ +[Unit] +Description=Run Click user-level hooks +Documentation=man:click(1) + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=@bindir@/click hook run-user +Restart=no + +[Install] +WantedBy=default.target diff -Nru click-0.4.16/init/systemd/Makefile.am click-0.4.17.2/init/systemd/Makefile.am --- click-0.4.16/init/systemd/Makefile.am 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/init/systemd/Makefile.am 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,11 @@ +EXTRA_DIST = click-system-hooks.service.in click-user-hooks.service.in + +CLEANFILES = click-system-hooks.service click-user-hooks.service + +if INSTALL_SYSTEMD +nodist_systemdsystemunit_DATA = click-system-hooks.service +nodist_systemduserunit_DATA = click-user-hooks.service + +%.service: %.service.in + sed -e "s,[@]bindir[@],$(bindir),g" $< > $@ +endif diff -Nru click-0.4.16/init/upstart/click-system-hooks.conf.in click-0.4.17.2/init/upstart/click-system-hooks.conf.in --- click-0.4.16/init/upstart/click-system-hooks.conf.in 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/init/upstart/click-system-hooks.conf.in 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,8 @@ +description "Run Click system-level hooks" +author "Colin Watson " + +start on filesystem + +task + +exec @bindir@/click hook run-system diff -Nru click-0.4.16/init/upstart/click-user-hooks.conf.in click-0.4.17.2/init/upstart/click-user-hooks.conf.in --- click-0.4.16/init/upstart/click-user-hooks.conf.in 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/init/upstart/click-user-hooks.conf.in 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,8 @@ +description "Run Click user-level hooks" +author "Colin Watson " + +start on starting xsession-init + +task + +exec @bindir@/click hook run-user diff -Nru click-0.4.16/init/upstart/Makefile.am click-0.4.17.2/init/upstart/Makefile.am --- click-0.4.16/init/upstart/Makefile.am 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/init/upstart/Makefile.am 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,12 @@ +EXTRA_DIST = click-system-hooks.conf.in click-user-hooks.conf.in + +CLEANFILES = click-system-hooks.conf click-user-hooks.conf + +systemdir = $(sysconfdir)/init +sessionsdir = $(prefix)/share/upstart/sessions + +system_DATA = click-system-hooks.conf +sessions_DATA = click-user-hooks.conf + +%.conf: %.conf.in + sed -e "s,[@]bindir[@],$(bindir),g" $< > $@ diff -Nru click-0.4.16/lib/click/click-0.4.pc.in click-0.4.17.2/lib/click/click-0.4.pc.in --- click-0.4.16/lib/click/click-0.4.pc.in 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/click-0.4.pc.in 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,27 @@ +# Copyright (C) 2014 Canonical Ltd. +# +# This file is part of click. +# +# click is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; version 3 of the License. +# +# click is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along +# with click. If not, see . + +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: @PACKAGE_NAME@ +Description: Click package manipulation library +Version: @PACKAGE_VERSION@ +URL: https://click.readthedocs.org/en/latest/ +Libs: -L${libdir} -lclick-0.4 +Cflags: -I${includedir}/click-0.4 diff -Nru click-0.4.16/lib/click/click.sym click-0.4.17.2/lib/click/click.sym --- click-0.4.16/lib/click/click.sym 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/click.sym 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,85 @@ +click_database_error_quark +click_db_add +click_db_ensure_ownership +click_db_gc +click_db_get +click_db_get_overlay +click_db_get_packages +click_db_get_path +click_db_get_size +click_db_get_type +click_db_maybe_remove +click_db_new +click_db_read +click_dir_get_type +click_dir_open +click_dir_read_name +click_ensuredir +click_find_on_path +click_find_package_directory +click_get_db_dir +click_get_hooks_dir +click_get_umask +click_hook_get_app_id +click_hook_get_field +click_hook_get_fields +click_hook_get_hook_name +click_hook_get_is_single_version +click_hook_get_is_user_level +click_hook_get_pattern +click_hook_get_run_commands_user +click_hook_get_short_app_id +click_hook_get_type +click_hook_install +click_hook_install_package +click_hook_open +click_hook_open_all +click_hook_remove +click_hook_remove_package +click_hook_run_commands +click_hook_sync +click_hooks_error_quark +click_installed_package_get_package +click_installed_package_get_path +click_installed_package_get_type +click_installed_package_get_version +click_installed_package_get_writeable +click_installed_package_new +click_package_install_hooks +click_package_remove_hooks +click_pattern_format +click_pattern_possible_expansion +click_query_error_quark +click_run_system_hooks +click_run_user_hooks +click_single_db_any_app_running +click_single_db_app_running +click_single_db_ensure_ownership +click_single_db_gc +click_single_db_get_packages +click_single_db_get_path +click_single_db_get_root +click_single_db_get_type +click_single_db_maybe_remove +click_single_db_new +click_symlink_force +click_unlink_force +click_user_error_quark +click_user_get_is_gc_in_use +click_user_get_is_pseudo_user +click_user_get_overlay_db +click_user_get_package_names +click_user_get_path +click_user_get_type +click_user_get_version +click_user_has_package_name +click_user_is_removable +click_user_new_for_all_users +click_user_new_for_gc_in_use +click_user_new_for_user +click_user_remove +click_user_set_version +click_users_get_type +click_users_get_user +click_users_get_user_names +click_users_new diff -Nru click-0.4.16/lib/click/database.vala click-0.4.17.2/lib/click/database.vala --- click-0.4.16/lib/click/database.vala 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/database.vala 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,602 @@ +/* Copyright (C) 2013, 2014 Canonical Ltd. + * Author: Colin Watson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Click databases. */ + +namespace Click { + +public errordomain DatabaseError { + /** + * A package/version does not exist. + */ + DOES_NOT_EXIST, + /** + * Failure to remove package. + */ + REMOVE, + /** + * Failure to ensure correct ownership of database files. + */ + ENSURE_OWNERSHIP +} + +public class InstalledPackage : Object, Gee.Hashable { + public string package { get; construct; } + public string version { get; construct; } + public string path { get; construct; } + public bool writeable { get; construct; default = true; } + + public InstalledPackage (string package, string version, string path, + bool writeable = true) + { + Object (package: package, version: version, path: path, + writeable: writeable); + } + + public uint + hash () + { + return package.hash () ^ version.hash () ^ path.hash () ^ + (writeable ? 1 : 0); + } + + public bool + equal_to (InstalledPackage obj) + { + return package == obj.package && version == obj.version && + path == obj.path && writeable == obj.writeable; + } +} + +public class SingleDB : Object { + public string root { get; construct; } + public DB master_db { private get; construct; } + + public + SingleDB (string root, DB master_db) + { + Object (root: root, master_db: master_db); + } + + private bool + show_messages () + { + return Environment.get_variable ("TEST_QUIET") == null; + } + + /** + * get_path: + * @package: A package name. + * @version: A version string. + * + * Returns: The path to this version of this package. + */ + public string + get_path (string package, string version) throws DatabaseError + { + var try_path = Path.build_filename (root, package, version); + if (exists (try_path)) + return try_path; + else + throw new DatabaseError.DOES_NOT_EXIST + ("%s %s does not exist in %s", + package, version, root); + } + + /** + * get_packages: + * @all_versions: If true, return all versions, not just current ones. + * + * Returns: A list of #InstalledPackage instances corresponding to + * package versions in only this database. + */ + public List + get_packages (bool all_versions = false) throws Error + { + var ret = new List (); + + foreach (var package in Click.Dir.open (root)) { + if (package == ".click") + continue; + if (all_versions) { + var package_path = + Path.build_filename (root, package); + foreach (var version in Click.Dir.open + (package_path)) { + var version_path = Path.build_filename + (package_path, version); + if (FileUtils.test + (version_path, + FileTest.IS_SYMLINK) || + ! FileUtils.test + (version_path, + FileTest.IS_DIR)) + continue; + ret.prepend(new InstalledPackage + (package, version, + version_path)); + } + } else { + var current_path = Path.build_filename + (root, package, "current"); + if (! FileUtils.test + (current_path, FileTest.IS_SYMLINK)) + continue; + var version = FileUtils.read_link + (current_path); + if (! ("/" in version)) + ret.prepend(new InstalledPackage + (package, version, + current_path)); + } + } + + ret.reverse (); + return ret; + } + + /* + * app_running: + * @package: A package name. + * @app_name: An application name. + * @version: A version string. + * + * Returns: True if @app_name from version @version of @package is + * known to be running, otherwise false. + */ + public bool + app_running (string package, string app_name, string version) + { + string[] command = { + "upstart-app-pid", + @"$(package)_$(app_name)_$(version)" + }; + try { + int exit_status; + Process.spawn_sync + (null, command, null, + SpawnFlags.SEARCH_PATH | + SpawnFlags.STDOUT_TO_DEV_NULL, + null, null, null, out exit_status); + return Process.check_exit_status (exit_status); + } catch (Error e) { + return false; + } + } + + /* + * any_app_running: + * @package: A package name. + * @version: A version string. + * + * Returns: True if any application from version @version of + * @package is known to be running, otherwise false. + */ + public bool + any_app_running (string package, string version) throws DatabaseError + { + if (! find_on_path ("upstart-app-pid")) + return false; + + var manifest_path = Path.build_filename + (get_path (package, version), ".click", "info", + @"$package.manifest"); + var parser = new Json.Parser (); + try { + parser.load_from_file (manifest_path); + var manifest = parser.get_root ().get_object (); + if (! manifest.has_member ("hooks")) + return false; + var hooks = manifest.get_object_member ("hooks"); + foreach (unowned string app_name in + hooks.get_members ()) { + if (app_running (package, app_name, version)) + return true; + } + } catch (Error e) { + } + return false; + } + + private void + remove_unless_running (string package, string version) throws Error + { + if (any_app_running (package, version)) { + var gc_in_use_user_db = + new User.for_gc_in_use (master_db); + gc_in_use_user_db.set_version (package, version); + return; + } + + var version_path = get_path (package, version); + if (show_messages ()) + message ("Removing %s", version_path); + package_remove_hooks (master_db, package, version); + /* In Python, we used shutil.rmtree(version_path, + * ignore_errors=True), but GLib doesn't have an obvious + * equivalent. I could write a recursive version with GLib, + * but this isn't performance-critical and it isn't worth + * the hassle for now, so just call out to "rm -rf" instead. + */ + string[] argv = { "rm", "-rf", version_path }; + int exit_status; + Process.spawn_sync (null, argv, null, SpawnFlags.SEARCH_PATH, + null, null, null, out exit_status); + Process.check_exit_status (exit_status); + + var package_path = Path.build_filename (root, package); + var current_path = Path.build_filename + (package_path, "current"); + if (FileUtils.test (current_path, FileTest.IS_SYMLINK) && + FileUtils.read_link (current_path) == version) { + if (FileUtils.unlink (current_path) < 0) + throw new DatabaseError.REMOVE + ("unlink %s failed: %s", + current_path, strerror (errno)); + /* TODO: Perhaps we should relink current to the + * latest remaining version. However, that requires + * version comparison, and it's not clear whether + * it's worth it given that current is mostly + * superseded by user registration. + */ + } + if (DirUtils.remove (package_path) < 0) { + if (errno != Posix.ENOTEMPTY && + errno != Posix.EEXIST) + throw new DatabaseError.REMOVE + ("rmdir %s failed: %s", + package_path, strerror (errno)); + } + } + + /** + * maybe_remove: + * @package: A package name. + * @version: A version string. + * + * Remove a package version if it is not in use. + * + * "In use" may mean registered for another user, or running. In + * the latter case we construct a fake registration so that we can + * tell the difference later between a package version that was in + * use at the time of removal and one that was never registered for + * any user. + * + * (This is unfortunately complex, and perhaps some day we can + * require that installations always have some kind of registration + * to avoid this complexity.) + */ + public void + maybe_remove (string package, string version) throws Error + { + var users_db = new Users (master_db); + foreach (var user_name in users_db.get_user_names ()) { + var user_db = users_db.get_user (user_name); + string reg_version; + try { + reg_version = user_db.get_version (package); + } catch (UserError e) { + continue; + } + if (reg_version == version) { + if (user_db.is_gc_in_use) + user_db.remove (package); + else + /* In use. */ + return; + } + } + + remove_unless_running (package, version); + } + + /** + * gc: + * + * Remove package versions with no user registrations. + * + * To avoid accidentally removing packages that were installed + * without ever having a user registration, we only garbage-collect + * packages that were not removed by maybe_remove() due to having a + * running application at the time. + * + * (This is unfortunately complex, and perhaps some day we can + * require that installations always have some kind of registration + * to avoid this complexity.) + */ + public void + gc () throws Error + { + var users_db = new Users (master_db); + var user_reg = new Gee.HashMultiMap (); + var gc_in_use = new Gee.HashMultiMap (); + foreach (var user_name in users_db.get_user_names ()) { + var user_db = users_db.get_user (user_name); + foreach (var package in user_db.get_package_names ()) { + var version = user_db.get_version (package); + /* Odd multimap syntax; this should really + * be more like foo[package] += version. + */ + if (user_db.is_gc_in_use) + gc_in_use[package] = version; + else + user_reg[package] = version; + } + } + + var gc_in_use_user_db = new User.for_gc_in_use (master_db); + foreach (var package in Click.Dir.open (root)) { + if (package == ".click") + continue; + var package_path = Path.build_filename (root, package); + foreach (var version in Click.Dir.open + (package_path)) { + if (version in user_reg[package]) + /* In use. */ + continue; + if (! (version in gc_in_use[package])) { + if (show_messages ()) { + var version_path = + Path.build_filename + (package_path, + version); + message ("Not removing %s " + + "(never registered).", + version_path); + } + continue; + } + gc_in_use_user_db.remove (package); + remove_unless_running (package, version); + } + } + } + + private delegate void WalkFunc (string dirpath, string[] dirnames, + string[] filenames) throws Error; + + /** + * walk: + * + * An reduced emulation of Python's os.walk. + */ + private void + walk (string top, WalkFunc func) throws Error + { + string[] dirs = {}; + string[] nondirs = {}; + foreach (var name in Click.Dir.open (top)) { + var path = Path.build_filename (top, name); + if (FileUtils.test (path, FileTest.IS_DIR)) + dirs += name; + else + nondirs += name; + } + func (top, dirs, nondirs); + foreach (var name in dirs) { + var path = Path.build_filename (top, name); + if (! is_symlink (path)) + walk (path, func); + } + } + + private delegate void ClickpkgForeachFunc (string path) + throws DatabaseError; + + /** + * foreach_clickpkg_path: + * + * Call a delegate for each path which should be owned by clickpkg. + */ + private void + foreach_clickpkg_path (ClickpkgForeachFunc func) throws Error + { + if (exists (root)) + func (root); + foreach (var package in Click.Dir.open (root)) { + var path = Path.build_filename (root, package); + if (package == ".click") { + func (path); + var log_path = Path.build_filename + (path, "log"); + if (exists (log_path)) + func (log_path); + var users_path = Path.build_filename + (path, "users"); + if (exists (users_path)) + func (users_path); + } else { + walk (path, (dp, dns, fns) => { + func (dp); + foreach (var dn in dns) { + var dnp = Path.build_filename + (dp, dn); + if (is_symlink (dnp)) + func (dnp); + } + foreach (var fn in fns) { + var fnp = Path.build_filename + (dp, fn); + func (fnp); + } + }); + } + } + } + + /** + * ensure_ownership: + * + * Ensure correct ownership of files in the database. + * + * On a system that is upgraded by delivering a new system image + * rather than by package upgrades, it is possible for the clickpkg + * UID to change. The overlay database must then be adjusted to + * account for this. + */ + public void + ensure_ownership () throws Error + { + errno = 0; + unowned Posix.Passwd? pw = Posix.getpwnam ("clickpkg"); + if (pw == null) + throw new DatabaseError.ENSURE_OWNERSHIP + ("Cannot get password file entry for " + + "clickpkg: %s", strerror (errno)); + Posix.Stat st; + if (Posix.stat (root, out st) < 0) + return; + if (st.st_uid == pw.pw_uid && st.st_gid == pw.pw_gid) + return; + foreach_clickpkg_path ((path) => { + if (Posix.chown (path, pw.pw_uid, pw.pw_gid) < 0) + throw new DatabaseError.ENSURE_OWNERSHIP + ("Cannot set ownership of %s: %s", + path, strerror (errno)); + }); + } +} + +public class DB : Object { + private Gee.ArrayList db = new Gee.ArrayList (); + + public DB () {} + + public void + read (string? db_dir = null) throws FileError + { + string real_db_dir = (db_dir == null) ? get_db_dir () : db_dir; + + foreach (var name in Click.Dir.open (real_db_dir)) { + if (! name.has_suffix (".conf")) + continue; + var path = Path.build_filename (real_db_dir, name); + var config = new KeyFile (); + string root; + try { + config.load_from_file + (path, KeyFileFlags.NONE); + root = config.get_string + ("Click Database", "root"); + } catch (Error e) { + warning ("%s", e.message); + continue; + } + assert (root != null); + add (root); + } + } + + public int size { get { return db.size; } } + + public new SingleDB + @get (int index) + { + return db.get (index); + } + + public new void + add (string root) + { + db.add (new SingleDB (root, this)); + } + + /** + * overlay: + * + * The directory where changes should be written. + */ + public string overlay { get { return db.last ().root; } } + + /** + * get_path: + * @package: A package name. + * @version: A version string. + * + * Returns: The path to this version of this package. + */ + public string + get_path (string package, string version) throws DatabaseError + { + for (int i = db.size - 1; i >= 0; --i) { + try { + return db[i].get_path (package, version); + } catch (DatabaseError e) { + } + } + throw new DatabaseError.DOES_NOT_EXIST + ("%s %s does not exist in any database", + package, version); + } + + /** + * get_packages: + * @all_versions: If true, return all versions, not just current ones. + * + * Returns: A list of #InstalledPackage instances corresponding to + * package versions in all databases. + */ + public List + get_packages (bool all_versions = false) throws Error + { + var ret = new List (); + var seen = new Gee.HashSet (); + var writeable = true; + for (int i = db.size - 1; i >= 0; --i) { + var child_packages = db[i].get_packages (all_versions); + foreach (var pkg in child_packages) { + string seen_id; + if (all_versions) + seen_id = ( + pkg.package + "_" + + pkg.version); + else + seen_id = pkg.package.dup (); + + if (! (seen_id in seen)) { + ret.prepend(new InstalledPackage + (pkg.package, pkg.version, + pkg.path, writeable)); + seen.add (seen_id); + } + } + writeable = false; + } + + ret.reverse (); + return ret; + } + + public void + maybe_remove (string package, string version) throws Error + { + db.last ().maybe_remove (package, version); + } + + public void + gc () throws Error + { + db.last ().gc (); + } + + public void + ensure_ownership () throws Error + { + db.last ().ensure_ownership (); + } +} + +} diff -Nru click-0.4.16/lib/click/hooks.vala click-0.4.17.2/lib/click/hooks.vala --- click-0.4.16/lib/click/hooks.vala 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/hooks.vala 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,1169 @@ +/* Copyright (C) 2013, 2014 Canonical Ltd. + * Author: Colin Watson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Click package hooks. + * + * See doc/hooks.rst for the draft specification. + */ + +namespace Click { + +public errordomain HooksError { + /** + * Requested hook does not exist. + */ + NO_SUCH_HOOK, + /** + * Missing hook field. + */ + MISSING_FIELD, + /** + * Invalid application name. + */ + BAD_APP_NAME, + /** + * Requested user does not exist. + */ + NO_SUCH_USER, + /** + * Failure to drop privileges. + */ + DROP_PRIVS, + /** + * Not yet implemented. + */ + NYI +} + +private Json.Object +read_manifest_hooks (DB db, string package, string? version) + throws DatabaseError +{ + if (version == null) + return new Json.Object (); + var parser = new Json.Parser (); + try { + var manifest_path = Path.build_filename + (db.get_path (package, version), ".click", "info", + @"$package.manifest"); + parser.load_from_file (manifest_path); + var manifest = parser.get_root ().get_object (); + if (! manifest.has_member ("hooks")) + return new Json.Object (); + var hooks = manifest.get_object_member ("hooks"); + return hooks.ref (); + } catch (Error e) { + return new Json.Object (); + } +} + +private class PreviousEntry : Object, Gee.Hashable { + public string path { get; construct; } + public string package { get; construct; } + public string version { get; construct; } + public string app_name { get; construct; } + + public + PreviousEntry (string path, string package, string version, + string app_name) + { + Object (path: path, package: package, version: version, + app_name: app_name); + } + + public uint + hash () + { + return path.hash () ^ package.hash () ^ version.hash () ^ + app_name.hash (); + } + + public bool + equal_to (PreviousEntry obj) + { + return path == obj.path && package == obj.package && + version == obj.version && app_name == obj.app_name; + } +} + +private class UnpackedPackage : Object, Gee.Hashable { + public string package { get; construct; } + public string version { get; construct; } + public string? user_name { get; construct; } + + public + UnpackedPackage (string package, string version, + string? user_name = null) + { + Object (package: package, version: version, + user_name: user_name); + } + + public uint + hash () + { + return package.hash () ^ version.hash () ^ + (user_name != null ? user_name.hash () : 0); + } + + public bool + equal_to (UnpackedPackage obj) + { + return package == obj.package && version == obj.version && + user_name == obj.user_name; + } +} + +private class RelevantApp : Object, Gee.Hashable { + public string package { get; construct; } + public string version { get; construct; } + public string app_name { get; construct; } + public string? user_name { get; construct; } + public string relative_path { get; construct; } + + public + RelevantApp (string package, string version, string app_name, + string? user_name, string relative_path) + { + Object (package: package, version: version, app_name: app_name, + user_name: user_name, relative_path: relative_path); + } + + public uint + hash () + { + return package.hash () ^ version.hash () ^ app_name.hash () ^ + (user_name != null ? user_name.hash () : 0) ^ + relative_path.hash (); + } + + public bool + equal_to (RelevantApp obj) + { + return package == obj.package && version == obj.version && + app_name == obj.app_name && + user_name == obj.user_name && + relative_path == obj.relative_path; + } +} + +private class AppHook : Object, Gee.Hashable, + Gee.Comparable { + public string app_name { get; construct; } + public string hook_name { get; construct; } + + public + AppHook (string app_name, string hook_name) + { + Object (app_name: app_name, hook_name: hook_name); + } + + public uint + hash () + { + return app_name.hash () ^ hook_name.hash (); + } + + public bool + equal_to (AppHook obj) + { + return app_name == obj.app_name && hook_name == obj.hook_name; + } + + public int + compare_to (AppHook obj) + { + var ret = strcmp (app_name, obj.app_name); + if (ret != 0) + return ret; + return strcmp (hook_name, obj.hook_name); + } +} + +private class ParsedPattern : Object { + public bool is_expansion { get; construct; } + public string text { get; construct; } + + public + ParsedPattern (bool is_expansion, string text) + { + Object (is_expansion: is_expansion, text: text); + } +} + +private Regex? expansion_re = null; + +/** + * pattern_parse: + * @format_string: A format string. + * + * Parse @format_string into segments. + * + * Returns: A list of #ParsedPattern segments. + */ +private Gee.List +pattern_parse (string format_string) +{ + const string EXPANSION = "\\$(?:\\$|{(.*?)})"; + var ret = new Gee.ArrayList (); + MatchInfo match_info; + var last_end = 0; + + if (expansion_re == null) { + try { + expansion_re = new Regex (EXPANSION); + } catch (RegexError e) { + error ("Could not compile regex /%s/: %s", + EXPANSION, e.message); + } + } + + expansion_re.match (format_string, 0, out match_info); + while (match_info.matches ()) { + int start, end; + var fetched = match_info.fetch_pos (0, out start, out end); + assert (fetched); + string? key = null; + if (start + 2 == end && format_string[start] == '$' && + format_string[start + 1] == '$') + ++start; + else + key = match_info.fetch (1); + if (last_end < start) { + var segment = format_string.substring + (last_end, start - last_end); + ret.add (new ParsedPattern (false, segment)); + } + if (key != null) + ret.add (new ParsedPattern (true, key)); + + last_end = end; + try { + match_info.next (); + } catch (RegexError e) { + break; + } + } + if (last_end < format_string.length) + ret.add (new ParsedPattern + (false, format_string.substring (last_end))); + + return ret; +} + +/** + * pattern_format: + * @format_string: A format string. + * @args: A #GLib.Variant of type "a{sms}", binding keys to values. + * + * Apply simple $-expansions to a string. + * + * `${key}` is replaced by the value of the `key` argument; `$$` is replaced + * by `$`. Any `$` character not followed by `{...}` is preserved intact. + * + * Returns: The expanded string. + */ +public string +pattern_format (string format_string, Variant args) +{ + string[] pieces = {}; + foreach (var segment in pattern_parse (format_string)) { + if (segment.is_expansion) { + unowned string value; + if (args.lookup (segment.text, "m&s", out value)) + pieces += value; + } else + pieces += segment.text; + } + return string.joinv ("", pieces); +} + +/** + * click_pattern_possible_expansion: + * @s: A string. + * @format_string: A format string. + * @args: A #GLib.Variant of type "a{sms}", binding keys to values. + * + * Check if @s is a possible $-expansion of @format_string. + * + * Entries in @args have the effect of binding some keys to fixed values; + * unspecified keys may take any value, and will bind greedily to the + * longest possible string. + * + * Returns: If @s is a possible expansion, then this function returns a + * (possibly empty) dictionary #GLib.Variant mapping all the unspecified + * keys to their bound values. Otherwise, it returns null. + */ +public Variant? +pattern_possible_expansion (string s, string format_string, Variant args) +{ + string[] regex_pieces = {}; + string[] group_names = {}; + foreach (var segment in pattern_parse (format_string)) { + if (segment.is_expansion) { + unowned string value; + if (args.lookup (segment.text, "m&s", out value)) + regex_pieces += Regex.escape_string (value); + else { + regex_pieces += "(.*)"; + group_names += segment.text; + } + } else + regex_pieces += Regex.escape_string (segment.text); + } + var joined = string.joinv ("", regex_pieces); + Regex compiled; + try { + compiled = new Regex ("^" + joined + "$"); + } catch (RegexError e) { + return null; + } + MatchInfo match_info; + var builder = new VariantBuilder (new VariantType ("a{ss}")); + if (compiled.match (s, 0, out match_info)) { + for (int group_i = 0; group_i < group_names.length; + ++group_i) { + var match = match_info.fetch (group_i + 1); + assert (match != null); + builder.add ("{ss}", group_names[group_i], match); + } + return builder.end (); + } else + return null; +} + +public class Hook : Object { + public DB db { private get; construct; } + public string name { private get; construct; } + + private Gee.Map fields; + private Regex? field_re = null; + private Regex? blank_re = null; + + private Hook (DB db, string name) + { + Object (db: db, name: name); + field_re = null; + blank_re = null; + } + + /** + * parse_hook_file: + * @path: Path to a hook file. + * + * A very simple deb822-like hook file parser. + * + * Note that this only supports a single paragraph composed only of simple + * (non-folded/multiline) fields, which is fortunately all we need for hook + * files. + * + * Returns: A mapping of field names to values. + */ + private Gee.Map + parse_hook_file (string path) throws Error + { + if (field_re == null) + field_re = new Regex + ("^([^:[:space:]]+)[[:space:]]*:[[:space:]]" + + "([^[:space:]].*?)[[:space:]]*$"); + if (blank_re == null) + blank_re = new Regex ("^[[:space:]]*$"); + + var ret = new Gee.HashMap (); + var channel = new IOChannel.file (path, "r"); + string line; + while (channel.read_line (out line, null, null) + == IOStatus.NORMAL && + line != null) { + MatchInfo match_info; + + if (blank_re.match (line)) + break; + + if (field_re.match (line, 0, out match_info)) { + var key = match_info.fetch (1); + var value = match_info.fetch (2); + if (key != null && value != null) + ret[key.down ()] = value; + } + } + return ret; + } + + /** + * Hook.open: + * @db: A #Click.DB. + * @name: The name of the hook to open. + * + * Returns: (transfer full): A newly-allocated #Click.Hook. + */ + public static Hook + open (DB db, string name) throws HooksError + { + var hook_path = Path.build_filename + (get_hooks_dir (), @"$name.hook"); + try { + var hook = new Hook (db, name); + hook.fields = hook.parse_hook_file (hook_path); + return hook; + } catch (Error e) { + throw new HooksError.NO_SUCH_HOOK + ("No click hook '%s' installed", name); + } + } + + /** + * open_all: + * @db: A #Click.DB. + * @hook_name: (allow-none): A string to match against Hook-Name + * fields, or null. + * + * Returns: (element-type ClickHook) (transfer full): A #List of + * #Click.Hook instances whose Hook-Name fields equal the value of + * @hook_name. + */ + public static List + open_all (DB db, string? hook_name = null) throws FileError + { + var ret = new List (); + var dir = get_hooks_dir (); + foreach (var name in Click.Dir.open (dir)) { + if (! name.has_suffix (".hook")) + continue; + var path = Path.build_filename (dir, name); + try { + var hook = new Hook (db, name[0:-5]); + hook.fields = hook.parse_hook_file (path); + if (hook_name == null || + hook.get_hook_name () == hook_name) + ret.prepend (hook); + } catch (Error e) { + continue; + } + } + ret.reverse (); + return ret; + } + + /** + * get_fields: + * + * Returns: A list of field names defined by this hook. + */ + public List + get_fields () + { + var ret = new List (); + foreach (var key in fields.keys) + ret.prepend (key); + ret.reverse (); + return ret; + } + + public string + get_field (string key) throws HooksError + { + string value = fields[key.down ()]; + if (value == null) + throw new HooksError.MISSING_FIELD + ("Hook '%s' has no field named '%s'", + name, key); + return value; + } + + /** + * is_user_level: + * + * True if this hook is a user-level hook, otherwise false. + */ + public bool is_user_level { get { + return fields["user-level"] == "yes"; + } } + + /** + * is_single_version: + * + * True if this hook is a single-version hook, otherwise false. + */ + public bool is_single_version { get { + return is_user_level || fields["single-version"] == "yes"; + } } + + /** + * get_hook_name: + * + * Returns: This hook's Hook-Name field, or the base of its file + * name with the ".hook" extension removed if that field is missing. + */ + public string + get_hook_name () { + if (fields.has_key ("hook-name")) + return fields["hook-name"]; + else + return name; + } + + /** + * get_short_app_id: + * @package: A package name. + * @app_name: An application name. + * + * Returns: The short application ID based on @package and + * @app_name. + */ + public string + get_short_app_id (string package, string app_name) throws HooksError + { + /* TODO: Perhaps this check belongs further up the stack + * somewhere? + */ + if ("_" in app_name || "/" in app_name) + throw new HooksError.BAD_APP_NAME + ("Application name '%s' may not contain _ " + + "or / characters", app_name); + return @"$(package)_$(app_name)"; + } + + /** + * get_app_id: + * @package: A package name. + * @version: A version string. + * @app_name: An application name. + * + * Returns: The application ID based on @package, @version, and + * @app_name. + */ + public string + get_app_id (string package, string version, string app_name) + throws HooksError + { + var short_app_id = get_short_app_id (package, app_name); + return @"$(short_app_id)_$(version)"; + } + + private string? + get_user_home (string? user_name) + { + if (user_name == null) + return null; + /* TODO: caching */ + unowned Posix.Passwd? pw = Posix.getpwnam (user_name); + if (pw == null) + return null; + return pw.pw_dir; + } + + /** + * get_pattern: + * @package: A package name. + * @version: A version string. + * @app_name: An application name. + * @user_name: (allow-none): A user name, or null. + */ + public string + get_pattern (string package, string version, string app_name, + string? user_name = null) throws HooksError + { + var builder = new VariantBuilder (new VariantType ("a{sms}")); + var app_id = get_app_id (package, version, app_name); + var pattern = get_field ("pattern"); + var user_home = get_user_home (user_name); + builder.add ("{sms}", "id", app_id); + builder.add ("{sms}", "user", user_name); + builder.add ("{sms}", "home", user_home); + if (is_single_version) { + var short_app_id = get_short_app_id (package, + app_name); + builder.add ("{sms}", "short-id", short_app_id); + } + var ret = pattern_format (pattern, builder.end ()); + var len = ret.length; + while (len > 0) { + if (ret[len - 1] == Path.DIR_SEPARATOR) + --len; + else + break; + } + if (len == ret.length) + return ret; + else + return ret.substring (0, len); + } + + private void + priv_drop_failure (string name) throws HooksError + { + throw new HooksError.DROP_PRIVS + ("Cannot drop privileges (%s): %s", + name, strerror (errno)); + } + + /* This function is not async-signal-safe, but runs between fork() and + * execve(). As such, it is not safe to run hooks from a multi-threaded + * process. Do not use the GLib main loop with this! + */ + private void + drop_privileges_inner (string user_name) throws HooksError + { + if (Posix.geteuid () != 0) + return; + + errno = 0; + unowned Posix.Passwd? pw = Posix.getpwnam (user_name); + if (pw == null) + throw new HooksError.NO_SUCH_USER + ("Cannot get password file entry for user " + + "'%s': %s", user_name, strerror (errno)); + Posix.gid_t[] supp = {}; + Posix.setgrent (); + unowned PosixExtra.Group? gr; + while ((gr = PosixExtra.getgrent ()) != null) { + foreach (unowned string member in gr.gr_mem) { + if (member == user_name) { + supp += gr.gr_gid; + break; + } + } + } + Posix.endgrent (); + if (PosixExtra.setgroups (supp.length, supp) < 0) + priv_drop_failure ("setgroups"); + /* Portability note: this assumes that we have + * [gs]etres[gu]id, which is true on Linux but not + * necessarily elsewhere. If you need to support something + * else, there are reasonably standard alternatives + * involving other similar calls; see e.g. + * gnulib/lib/idpriv-drop.c. + */ + if (PosixExtra.setresgid (pw.pw_gid, pw.pw_gid, pw.pw_gid) < 0) + priv_drop_failure ("setresgid"); + if (PosixExtra.setresuid (pw.pw_uid, pw.pw_uid, pw.pw_uid) < 0) + priv_drop_failure ("setresuid"); + { + Posix.uid_t ruid, euid, suid; + Posix.gid_t rgid, egid, sgid; + assert (PosixExtra.getresuid (out ruid, out euid, + out suid) == 0 && + ruid == pw.pw_uid && euid == pw.pw_uid && + suid == pw.pw_uid); + assert (PosixExtra.getresgid (out rgid, out egid, + out sgid) == 0 && + rgid == pw.pw_gid && egid == pw.pw_gid && + sgid == pw.pw_gid); + } + Environment.set_variable ("HOME", pw.pw_dir, true); + Posix.umask (get_umask () | Posix.S_IWOTH); + } + + private void + drop_privileges (string user_name) + { + try { + drop_privileges_inner (user_name); + } catch (HooksError e) { + error ("%s", e.message); + } + } + + /** + * get_run_commands_user: + * @user_name: (allow-none): A user name, or null. + * + * Returns: The user name under which this hook will be run. + */ + public string + get_run_commands_user (string? user_name = null) throws HooksError + { + if (is_user_level) + return user_name; + return get_field ("user"); + } + + /** + * run_commands: + * @user_name: (allow-none): A user name, or null. + * + * Run any commands specified by the hook to keep itself up to date. + */ + public void + run_commands (string? user_name = null) throws Error + { + if (fields.has_key ("exec")) { + string[] argv = {"/bin/sh", "-c", fields["exec"]}; + var target_user_name = get_run_commands_user + (user_name); + SpawnChildSetupFunc drop = + () => drop_privileges (target_user_name); + int exit_status; + Process.spawn_sync (null, argv, null, + SpawnFlags.SEARCH_PATH, drop, + null, null, out exit_status); + Process.check_exit_status (exit_status); + } + + if (fields["trigger"] == "yes") + throw new HooksError.NYI + ("'Trigger: yes' not yet implemented"); + } + + private List + get_previous_entries (string? user_name = null) throws Error + { + var ret = new List (); + var link_dir_path = Path.get_dirname (get_pattern + ("", "", "", user_name)); + /* TODO: This only works if the application ID only appears, at + * most, in the last component of the pattern path. + */ + foreach (var entry in Click.Dir.open (link_dir_path)) { + var path = Path.build_filename (link_dir_path, entry); + var exp_builder = new VariantBuilder + (new VariantType ("a{sms}")); + exp_builder.add ("{sms}", "user", user_name); + exp_builder.add + ("{sms}", "home", get_user_home (user_name)); + var exp = pattern_possible_expansion + (path, fields["pattern"], exp_builder.end ()); + unowned string? id = null; + if (exp != null) + exp.lookup ("id", "&s", out id); + if (id == null) + continue; + var tokens = id.split ("_", 3); + if (tokens.length < 3) + continue; + /* tokens == { package, app_name, version } */ + ret.prepend (new PreviousEntry + (path, tokens[0], tokens[2], tokens[1])); + } + ret.reverse (); + return ret; + } + + /** + * install_link: + * @package: A package name. + * @version: A version string. + * @app_name: An application name. + * @relative_path: A relative path within the unpacked package. + * @user_name: (allow-none): A user name, or null. + * @user_db: (allow-none): A #Click.User, or null. + * + * Install a hook symlink. + * + * This should be called with dropped privileges if necessary. + */ + private void + install_link (string package, string version, string app_name, + string relative_path, string? user_name = null, + User? user_db = null) throws Error + { + string path; + if (is_user_level) + path = user_db.get_path (package); + else + path = db.get_path (package, version); + var target = Path.build_filename (path, relative_path); + var link = get_pattern (package, version, app_name, user_name); + if (FileUtils.test (link, FileTest.IS_SYMLINK) && + FileUtils.read_link (link) == target) + return; + ensuredir (Path.get_dirname (link)); + symlink_force (target, link); + } + + /** + * install_package: + * @package: A package name. + * @version: A version string. + * @app_name: An application name. + * @relative_path: A relative path within the unpacked package. + * @user_name: (allow-none): A user name, or null. + * + * Run this hook in response to @package being installed. + */ + public void + install_package (string package, string version, string app_name, + string relative_path, string? user_name = null) + throws Error + { + if (! is_user_level) + assert (user_name == null); + + /* Remove previous versions if necessary. */ + if (is_single_version) { + var entries = get_previous_entries (user_name); + foreach (var prev in entries) { + if (prev.package == package && + prev.app_name == app_name && + prev.version != version) + unlink_force (prev.path); + } + } + + if (is_user_level) { + var user_db = new User.for_user (db, user_name); + user_db.drop_privileges (); + try { + install_link (package, version, app_name, + relative_path, user_name, + user_db); + } finally { + user_db.regain_privileges (); + } + } else + install_link (package, version, app_name, + relative_path); + run_commands (user_name); + } + + /** + * remove_package: + * @package: A package name. + * @version: A version string. + * @app_name: An application name. + * @user_name: (allow-none): A user name, or null. + * + * Run this hook in response to @package being removed. + */ + public void + remove_package (string package, string version, string app_name, + string? user_name = null) throws Error + { + unlink_force (get_pattern + (package, version, app_name, user_name)); + run_commands (user_name); + } + + private Gee.ArrayList + get_all_packages_for_user (string user_name, User user_db) throws Error + { + var ret = new Gee.ArrayList (); + foreach (var package in user_db.get_package_names ()) + ret.add (new UnpackedPackage + (package, user_db.get_version (package), + user_name)); + return ret; + } + + /** + * get_all_packages: + * @user_name: (allow-none): A user name, or null. + * + * Return a list of all unpacked packages. + * + * If running a user-level hook, this returns (package, version, + * user) for the current version of each package registered for each + * user, or only for a single user if user is not null. + * + * If running a system-level hook, this returns (package, version, + * null) for each version of each unpacked package. + * + * Returns: A list of all unpacked packages. + */ + private List + get_all_packages (string? user_name = null) throws Error + { + var ret = new Gee.ArrayList (); + if (is_user_level) { + if (user_name != null) { + var user_db = new User.for_user + (db, user_name); + ret.add_all (get_all_packages_for_user + (user_name, user_db)); + } else { + var users_db = new Users (db); + var user_names = users_db.get_user_names (); + foreach (var one_user_name in user_names) { + if (one_user_name.has_prefix ("@")) + continue; + var one_user_db = users_db.get_user + (one_user_name); + ret.add_all (get_all_packages_for_user + (one_user_name, one_user_db)); + } + } + } else { + foreach (var inst in db.get_packages ()) + ret.add (new UnpackedPackage + (inst.package, inst.version)); + } + /* Flatten into a List to avoid introspection problems in + * case this method is ever exposed. + */ + var ret_list = new List (); + foreach (var element in ret) + ret_list.prepend (element); + ret_list.reverse (); + return ret_list; + } + + /** + * get_relevant_apps: + * @user_name: (allow-none): A user name, or null. + * + * Returns: A list of all applications relevant for this hook. + */ + private List + get_relevant_apps (string? user_name = null) throws Error + { + var ret = new List (); + var hook_name = get_hook_name (); + foreach (var unpacked in get_all_packages (user_name)) { + var manifest = read_manifest_hooks + (db, unpacked.package, unpacked.version); + foreach (var app_name in manifest.get_members ()) { + var hooks = manifest.get_object_member + (app_name); + if (hooks.has_member (hook_name)) { + var relative_path = hooks.get_string_member + (hook_name); + ret.prepend (new RelevantApp + (unpacked.package, + unpacked.version, app_name, + unpacked.user_name, + relative_path)); + } + } + } + ret.reverse (); + return ret; + } + + /** + * install: + * @user_name: (allow-none): A user name, or null. + * + * Install files associated with this hook for any packages that + * attach to it. + */ + public void + install (string? user_name = null) throws Error + { + foreach (var app in get_relevant_apps (user_name)) + install_package (app.package, app.version, + app.app_name, app.relative_path, + app.user_name); + } + + /** + * remove: + * @user_name: (allow-none): A user name, or null. + * + * Remove files associated with this hook for any packages that + * attach to it. + */ + public void + remove (string? user_name = null) throws Error + { + foreach (var app in get_relevant_apps (user_name)) + remove_package (app.package, app.version, app.app_name, + app.user_name); + } + + /** + * sync: + * @user_name: (allow-none): A user name, or null. + * + * Run a hook for all installed packages (system-level if @user_name + * is null, otherwise user-level). + * + * This is useful to catch up with preinstalled packages. + */ + public void + sync (string? user_name = null) throws Error + { + if (! is_user_level) + assert (user_name == null); + + var seen = new Gee.HashSet (); + foreach (var app in get_relevant_apps (user_name)) { + unowned string package = app.package; + unowned string version = app.version; + unowned string app_name = app.app_name; + seen.add (@"$(package)_$(app_name)_$(version)"); + if (is_user_level) { + var user_db = new User.for_user + (db, user_name); + user_db.drop_privileges (); + try { + install_link (package, version, + app_name, + app.relative_path, + app.user_name, user_db); + } finally { + user_db.regain_privileges (); + } + } else + install_link (package, version, app_name, + app.relative_path); + } + + foreach (var prev in get_previous_entries (user_name)) { + unowned string package = prev.package; + unowned string version = prev.version; + unowned string app_name = prev.app_name; + if (! (@"$(package)_$(app_name)_$(version)" in seen)) + unlink_force (prev.path); + } + + run_commands (user_name); + } +} + +private Gee.TreeSet +get_app_hooks (Json.Object manifest) +{ + var items = new Gee.TreeSet (); /* sorted */ + foreach (var app_name in manifest.get_members ()) { + var hooks = manifest.get_object_member (app_name); + foreach (var hook_name in hooks.get_members ()) + items.add (new AppHook (app_name, hook_name)); + } + return items; +} + +/** + * package_install_hooks: + * @db: A #Click.DB. + * @package: A package name. + * @old_version: (allow-none): The old version of the package, or null. + * @new_version: The new version of the package. + * @user_name: (allow-none): A user name, or null. + * + * Run hooks following removal of a Click package. + * + * If @user_name is null, only run system-level hooks. If @user_name is not + * null, only run user-level hooks for that user. + */ +public void +package_install_hooks (DB db, string package, string? old_version, + string new_version, string? user_name = null) + throws Error +{ + var old_manifest = read_manifest_hooks (db, package, old_version); + var new_manifest = read_manifest_hooks (db, package, new_version); + + /* Remove any targets for single-version hooks that were in the old + * manifest but not the new one. + */ + var old_app_hooks = get_app_hooks (old_manifest); + var new_app_hooks = get_app_hooks (new_manifest); + foreach (var app_hook in new_app_hooks) + old_app_hooks.remove (app_hook); + foreach (var app_hook in old_app_hooks) { + foreach (var hook in Hook.open_all (db, app_hook.hook_name)) { + if (hook.is_user_level != (user_name != null)) + continue; + if (! hook.is_single_version) + continue; + hook.remove_package (package, old_version, + app_hook.app_name, user_name); + } + } + + var new_app_names = new_manifest.get_members (); + new_app_names.sort (strcmp); + foreach (var app_name in new_app_names) { + var app_hooks = new_manifest.get_object_member (app_name); + var hook_names = app_hooks.get_members (); + hook_names.sort (strcmp); + foreach (var hook_name in hook_names) { + var relative_path = app_hooks.get_string_member + (hook_name); + foreach (var hook in Hook.open_all (db, hook_name)) { + if (hook.is_user_level != (user_name != null)) + continue; + hook.install_package (package, new_version, + app_name, relative_path, + user_name); + } + } + } +} + +/** + * package_remove_hooks: + * @db: A #Click.DB. + * @package: A package name. + * @old_version: The old version of the package. + * @user_name: (allow-none): A user name, or null. + * + * Run hooks following removal of a Click package. + * + * If @user_name is null, only run system-level hooks. If @user_name is not + * null, only run user-level hooks for that user. + */ +public void +package_remove_hooks (DB db, string package, string old_version, + string? user_name = null) throws Error +{ + var old_manifest = read_manifest_hooks (db, package, old_version); + + foreach (var app_hook in get_app_hooks (old_manifest)) { + foreach (var hook in Hook.open_all (db, app_hook.hook_name)) { + if (hook.is_user_level != (user_name != null)) + continue; + hook.remove_package (package, old_version, + app_hook.app_name, user_name); + } + } +} + +/** + * run_system_hooks: + * @db: A #Click.DB. + * + * Run system-level hooks for all installed packages. + * + * This is useful when starting up from images with preinstalled packages + * which may not have had their system-level hooks run properly when + * building the image. It is suitable for running at system startup. + */ +public void +run_system_hooks (DB db) throws Error +{ + db.ensure_ownership (); + foreach (var hook in Hook.open_all (db)) { + if (! hook.is_user_level) + hook.sync (); + } +} + +/** + * run_user_hooks: + * @db: A #Click.DB. + * @user_name: (allow-none): A user name, or null to run hooks for the + * current user. + * + * Run user-level hooks for all installed packages. + * + * This is useful to catch up with packages that may have been preinstalled + * and registered for all users. It is suitable for running at session + * startup. + */ +public void +run_user_hooks (DB db, string? user_name = null) throws Error +{ + if (user_name == null) + user_name = Environment.get_user_name (); + foreach (var hook in Hook.open_all (db)) { + if (hook.is_user_level) + hook.sync (user_name); + } +} + +} diff -Nru click-0.4.16/lib/click/Makefile.am click-0.4.17.2/lib/click/Makefile.am --- click-0.4.16/lib/click/Makefile.am 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/Makefile.am 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,98 @@ +AM_CPPFLAGS = \ + -I. \ + -D_GNU_SOURCE +AM_CFLAGS = \ + $(LIBCLICK_CFLAGS) \ + $(VALA_CFLAGS) \ + -Wno-unused-but-set-variable \ + -Wno-unused-function \ + -Wno-unused-variable +VALAC = $(srcdir)/valac-wrapper +AM_VALAFLAGS = \ + -H click.h \ + --gir Click-0.4.gir \ + --library click-0.4 \ + --pkg posix \ + --pkg gee-0.8 \ + --pkg json-glib-1.0 \ + --target-glib 2.32 + +lib_LTLIBRARIES = libclick-0.4.la + +libclick_0_4_la_SOURCES = \ + database.vala \ + hooks.vala \ + osextras.vala \ + paths.vala \ + posix-extra.vapi \ + query.vala \ + user.vala + +EXTRA_libclick_0_4_la_DEPENDENCIES = \ + click.sym + +HEADER_FILES = \ + click.h + +BUILT_SOURCES = paths.vala + +CLEANFILES = \ + $(BUILT_SOURCES) \ + $(HEADER_FILES) \ + libclick_0_4_la_vala.stamp \ + click.h \ + database.c \ + hooks.c \ + osextras.c \ + paths.c \ + query.c \ + user.c + +do_subst = sed \ + -e 's,[@]sysconfdir[@],$(sysconfdir),g' \ + -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' + +paths.vala: paths.vala.in Makefile + $(do_subst) < $(srcdir)/paths.vala.in > $@ + +includeclickdir = $(includedir)/click-0.4 +includeclick_HEADERS = \ + $(HEADER_FILES) + +libclick_0_4_la_LIBADD = $(LIBCLICK_LIBS) + +libclick_0_4_la_LDFLAGS = \ + -export-dynamic \ + -export-symbols $(srcdir)/click.sym \ + -version-info 4:0:4 + +EXTRA_DIST = click-0.4.pc.in + +pkgconfigdir = $(libdir)/pkgconfig +pkgconfig_DATA = click-0.4.pc + +INTROSPECTION_COMPILER_ARGS = \ + --includedir $(srcdir) \ + --includedir $(builddir) \ + --shared-library libclick-0.4.so.0 + +girdir = $(datadir)/gir-1.0 +gir_DATA = Click-0.4.gir + +typelibdir = $(libdir)/girepository-1.0 +typelib_DATA = Click-0.4.typelib + +# We intentionally don't install a VAPI at this point; libclick is written +# in Vala for implementation convenience, but this probably won't be +# appropriate for most of its clients. The intent is that the C API is +# canonical (with its reflections via gobject-introspection). +#vapidir = $(VAPIGEN_VAPIDIR) +#vapi_DATA = click-0.4.vapi +noinst_DATA = click-0.4.vapi + +CLEANFILES += $(gir_DATA) $(typelib_DATA) $(noinst_DATA) + +$(HEADER_FILES) $(gir_DATA) $(noinst_DATA): libclick_0_4_la_vala.stamp + +%.typelib: %.gir + $(INTROSPECTION_COMPILER) $(INTROSPECTION_COMPILER_ARGS) $< -o $@ diff -Nru click-0.4.16/lib/click/osextras.vala click-0.4.17.2/lib/click/osextras.vala --- click-0.4.16/lib/click/osextras.vala 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/osextras.vala 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,213 @@ +/* Copyright (C) 2013, 2014 Canonical Ltd. + * Author: Colin Watson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Extra OS-level utility functions. */ + +namespace Click { + +/** + * find_on_path: + * @command: A command name. + * + * Returns: True if the command is on the executable search path, otherwise + * false. + */ +public bool +find_on_path (string command) +{ + unowned string? path = Environment.get_variable ("PATH"); + if (path == null) + return false; + + var elements = path.split(":"); + foreach (var element in elements) { + if (element == "") + continue; + var filename = Path.build_filename (element, command); + if (FileUtils.test (filename, FileTest.IS_REGULAR) && + FileUtils.test (filename, FileTest.IS_EXECUTABLE)) + return true; + } + + return false; +} + +/** + * ensuredir: + * @directory: A path. + * + * If @directory does not already exist, create it and its parents as + * needed. + */ +public void +ensuredir (string directory) throws FileError +{ + if (FileUtils.test (directory, FileTest.IS_DIR)) + return; + if (DirUtils.create_with_parents (directory, 0777) < 0) { + var code = FileUtils.error_from_errno (errno); + var quark = Quark.from_string ("g-file-error-quark"); + var err = new Error (quark, code, + "ensuredir %s failed: %s", + directory, strerror (errno)); + throw (FileError) err; + } +} + +/** + * unlink_force: + * @path: A path to unlink. + * + * Unlink path, without worrying about whether it exists. Errors other than + * %ENOENT will set the provided error location. + */ +public void +unlink_force (string path) throws FileError +{ + if (FileUtils.unlink (path) < 0 && errno != Posix.ENOENT) { + var code = FileUtils.error_from_errno (errno); + var quark = Quark.from_string ("g-file-error-quark"); + var err = new Error (quark, code, + "unlink %s failed: %s", + path, strerror (errno)); + throw (FileError) err; + } +} + +/** + * symlink_force: + * @target: The intended target of the symbolic link. + * @link_name: A path where the symbolic link should be created. + * + * Create a symlink link_name -> target, even if link_name exists. + */ +public void +symlink_force (string target, string link_name) throws FileError +{ + unlink_force (link_name); + /* This produces a harmless warning when compiling C code generated + * by valac 0.22.1: + * https://bugzilla.gnome.org/show_bug.cgi?id=725151 + */ + if (FileUtils.symlink (target, link_name) < 0) { + var code = FileUtils.error_from_errno (errno); + var quark = Quark.from_string ("g-file-error-quark"); + var err = new Error (quark, code, + "symlink %s -> %s failed: %s", + link_name, target, strerror (errno)); + throw (FileError) err; + } +} + +/** + * click_get_umask: + * + * Returns: The current umask. + */ +public int +get_umask () +{ + var mask = Posix.umask (0); + Posix.umask (mask); + return (int) mask; +} + +public class Dir : Object { + private SList entries; + private unowned SList cur; + + private Dir () + { + } + + /** + * open: + * @path: The path to the directory to open. + * @flags: For future use; currently must be set to 0. + * + * Like GLib.Dir.open(), but ignores %ENOENT. + */ + public static Dir? + open (string path, uint _flags = 0) throws FileError + { + Dir dir = new Dir (); + dir.entries = new SList (); + + GLib.Dir real_dir; + try { + real_dir = GLib.Dir.open (path, _flags); + string? name; + while ((name = real_dir.read_name ()) != null) + dir.entries.prepend (name); + dir.entries.sort (strcmp); + } catch (FileError e) { + if (! (e is FileError.NOENT)) + throw e; + } + + dir.cur = dir.entries; + return dir; + } + + /** + * read_name: + * + * Like GLib.Dir.read_name(), but returns entries in sorted order. + */ + public unowned string? + read_name () + { + if (cur == null) + return null; + unowned string name = cur.data; + cur = cur.next; + return name; + } + + internal class Iterator : Object { + private Dir dir; + + public Iterator (Dir dir) { + this.dir = dir; + } + + public unowned string? + next_value () + { + return dir.read_name (); + } + } + + internal Iterator + iterator () + { + return new Iterator (this); + } +} + +private bool +exists (string path) +{ + return FileUtils.test (path, FileTest.EXISTS); +} + +private bool +is_symlink (string path) +{ + return FileUtils.test (path, FileTest.IS_SYMLINK); +} + +} diff -Nru click-0.4.16/lib/click/paths.vala.in click-0.4.17.2/lib/click/paths.vala.in --- click-0.4.16/lib/click/paths.vala.in 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/paths.vala.in 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,46 @@ +/* Copyright (C) 2013, 2014 Canonical Ltd. + * Author: Colin Watson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Click paths. */ + +namespace Click { + +internal static const string hooks_dir = "@pkgdatadir@/hooks"; +internal static const string db_dir = "@sysconfdir@/click/databases"; + +/** + * get_hooks_dir: + * + * Returns: The Click hooks directory. + */ +public string +get_hooks_dir () +{ + return hooks_dir; +} + +/** + * get_db_dir: + * + * Returns: The Click database configuration directory. + */ +public string +get_db_dir () +{ + return db_dir; +} + +} diff -Nru click-0.4.16/lib/click/posix-extra.vapi click-0.4.17.2/lib/click/posix-extra.vapi --- click-0.4.16/lib/click/posix-extra.vapi 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/posix-extra.vapi 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,48 @@ +/* Copyright (C) 2013, 2014 Canonical Ltd. + * Author: Colin Watson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Things that should be in posix.vapi, but aren't. */ + +[CCode (cprefix = "", lower_case_cprefix = "")] +namespace PosixExtra { + /* https://bugzilla.gnome.org/show_bug.cgi?id=725149 */ + [Compact] + [CCode (cname = "struct group", cheader_filename = "grp.h")] + public class Group { + public string gr_name; + public string gr_passwd; + public Posix.gid_t gr_gid; + [CCode (array_length = false, array_null_terminated = true)] + public string[] gr_mem; + } + [CCode (cheader_filename = "grp.h")] + public unowned Group? getgrent (); + + [CCode (cheader_filename = "unistd.h")] + public int getresgid (out Posix.gid_t rgid, out Posix.gid_t egid, out Posix.gid_t sgid); + [CCode (cheader_filename = "unistd.h")] + public int getresuid (out Posix.uid_t ruid, out Posix.uid_t euid, out Posix.uid_t suid); + [CCode (cheader_filename = "unistd.h")] + public int setegid (Posix.gid_t egid); + [CCode (cheader_filename = "unistd.h")] + public int seteuid (Posix.uid_t euid); + [CCode (cheader_filename = "sys/types.h,grp.h,unistd.h")] + public int setgroups (size_t size, [CCode (array_length = false)] Posix.gid_t[] list); + [CCode (cheader_filename = "unistd.h")] + public int setresgid (Posix.gid_t rgid, Posix.gid_t egid, Posix.gid_t sgid); + [CCode (cheader_filename = "unistd.h")] + public int setresuid (Posix.uid_t ruid, Posix.uid_t euid, Posix.uid_t suid); +} diff -Nru click-0.4.16/lib/click/query.vala click-0.4.17.2/lib/click/query.vala --- click-0.4.16/lib/click/query.vala 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/query.vala 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,58 @@ +/* Copyright (C) 2013, 2014 Canonical Ltd. + * Author: Colin Watson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Query information about installed Click packages. */ + +namespace Click { + +public errordomain QueryError { + /** + * A path could not be canonicalised. + */ + PATH, + /** + * No package directory was found. + */ + NO_PACKAGE_DIR +} + +public string +find_package_directory (string path) throws QueryError +{ + /* We require realpath (path, NULL) to be available. */ + var dir = Posix.realpath (path); + if (dir == null) + throw new QueryError.PATH + ("Failed to canonicalize %s: %s", + path, strerror (errno)); + + do { + var info_dir = Path.build_filename (dir, ".click", "info"); + if (FileUtils.test (info_dir, FileTest.IS_DIR)) + return dir; + if (dir == ".") + break; + var new_dir = Path.get_dirname (dir); + if (new_dir == dir) + break; + dir = new_dir; + } while (dir != null); + + throw new QueryError.NO_PACKAGE_DIR + ("No package directory found for %s", path); +} + +} diff -Nru click-0.4.16/lib/click/user.vala click-0.4.17.2/lib/click/user.vala --- click-0.4.16/lib/click/user.vala 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/user.vala 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,677 @@ +/* Copyright (C) 2013, 2014 Canonical Ltd. + * Author: Colin Watson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Registry of user-installed Click packages. + * + * Click packages are installed into per-package/version directories, so it + * is quite feasible for more than one version of a given package to be + * installed at once, allowing per-user installations; for instance, one + * user of a tablet may be uncomfortable with granting some new permission + * to an app, but another may be just fine with it. To make this useful, we + * also need a registry of which users have which versions of each package + * installed. + * + * We might have chosen to use a proper database. However, a major goal of + * Click packages is extreme resilience; we must never get into a situation + * where some previous error in package installation or removal makes it + * hard for the user to install or remove other packages. Furthermore, the + * simpler application execution can be the better. So, instead, we use + * just about the simplest "database" format imaginable: a directory of + * symlinks per user. + */ + +namespace Click { + +/* Pseudo-usernames selected to be invalid as a real username, and alluding + * to group syntaxes used in other systems. + */ +private const string ALL_USERS = "@all"; +private const string GC_IN_USE_USER = "@gcinuse"; + +/* Pseudo-versions. In this case the @ doesn't allude to group syntaxes, + * but since @ is conveniently invalid in version numbers we stick to the + * same prefix used for pseudo-usernames. + */ +private const string HIDDEN_VERSION = "@hidden"; + +public errordomain UserError { + /** + * Failure to get password file entry. + */ + GETPWNAM, + /** + * Failure to create database directory. + */ + CREATE_DB, + /** + * Failure to set ownership of database directory. + */ + CHOWN_DB, + /** + * Requested user does not exist. + */ + NO_SUCH_USER, + /** + * Failure to drop privileges. + */ + DROP_PRIVS, + /** + * Failure to regain privileges. + */ + REGAIN_PRIVS, + /** + * Requested package is hidden. + */ + HIDDEN_PACKAGE, + /** + * Requested package does not exist. + */ + NO_SUCH_PACKAGE, + /** + * Failure to rename file. + */ + RENAME +} + +private string +db_top (string root) +{ + /* This is deliberately outside any user's home directory so that it + * can safely be iterated etc. as root. + */ + return Path.build_filename (root, ".click", "users"); +} + +private string +db_for_user (string root, string user) +{ + return Path.build_filename (db_top (root), user); +} + +private void +try_create (string path) throws UserError +{ + if (DirUtils.create (path, 0777) < 0) + throw new UserError.CREATE_DB + ("Cannot create database directory %s: %s", + path, strerror (errno)); +} + +private void +try_chown (string path, Posix.Passwd pw) throws UserError +{ + if (Posix.chown (path, pw.pw_uid, pw.pw_gid) < 0) + throw new UserError.CHOWN_DB + ("Cannot set ownership of database directory %s: %s", + path, strerror (errno)); +} + +public class Users : Object { + public DB db { private get; construct; } + private unowned Posix.Passwd? click_pw; + + public Users (DB db) + { + Object (db: db); + click_pw = null; + } + + /** + * get_click_pw: + * + * Returns: The password file entry for the `clickpkg` user. + */ + private unowned Posix.Passwd + get_click_pw () throws UserError + { + if (click_pw == null) { + errno = 0; + click_pw = Posix.getpwnam ("clickpkg"); + if (click_pw == null) + throw new UserError.GETPWNAM + ("Cannot get password file entry " + + "for clickpkg: %s", strerror (errno)); + } + return click_pw; + } + + internal void + ensure_db () throws UserError + { + var create = new List (); + + /* Only modify the last database. */ + var try_path = db_top (db.overlay); + while (! exists (try_path)) { + create.prepend (try_path); + try_path = Path.get_dirname (try_path); + } + + foreach (var path in create) { + try_create (path); + if (Posix.geteuid () == 0) + try_chown (path, get_click_pw ()); + } + } + + /** + * get_user_names: + * + * Returns: A list of user names with registrations. + */ + public List + get_user_names () throws Error + { + var entries = new List (); + var seen = new Gee.HashSet (); + foreach (var single_db in db) { + var users_db = db_top (single_db.root); + foreach (var entry in Click.Dir.open (users_db)) { + if (entry in seen) + continue; + var path = Path.build_filename (users_db, + entry); + if (FileUtils.test (path, FileTest.IS_DIR)) { + seen.add (entry.dup ()); + entries.prepend (entry.dup ()); + } + } + } + entries.reverse (); + return entries; + } + + /** + * get_user: + * @user_name: A user name. + * + * Returns: (transfer full): A new #ClickUser instance for @user. + */ + public User + get_user (string user_name) throws Error + { + foreach (var single_db in db) { + var path = db_for_user (single_db.root, user_name); + if (FileUtils.test (path, FileTest.IS_DIR)) + /* We only require the user path to exist in + * any database; it doesn't matter which. + */ + return new User.for_user (db, user_name); + } + throw new UserError.NO_SUCH_USER( + "User %s does not exist in any database", user_name); + } +} + +public class User : Object { + public DB db { private get; construct; } + public string name { private get; construct; } + + private Users? users; + private unowned Posix.Passwd? user_pw; + private int dropped_privileges_count; + private Posix.mode_t? old_umask; + + private User (DB? db, string? name = null) throws FileError { + DB real_db; + string real_name; + if (db != null) + real_db = db; + else { + real_db = new DB (); + real_db.read (); + } + if (name != null) + real_name = name; + else + real_name = Environment.get_user_name ().dup (); + Object (db: real_db, name: real_name); + users = null; + user_pw = null; + dropped_privileges_count = 0; + old_umask = null; + } + + public User.for_user (DB? db, string? name = null) throws FileError { + this (db, name); + } + + public User.for_all_users (DB? db) throws FileError { + this (db, ALL_USERS); + } + + public User.for_gc_in_use (DB? db) throws FileError { + this (db, GC_IN_USE_USER); + } + + /** + * True if and only if this user is a pseudo-user. + */ + public bool is_pseudo_user { get { return name.has_prefix ("@"); } } + + /** + * True if and only if this user is the pseudo-user indicating that + * a registration was in use at the time of package removal. + */ + public bool is_gc_in_use { get { return name == GC_IN_USE_USER; } } + + /** + * get_user_pw: + * + * Returns: The password file entry for this user. + */ + private unowned Posix.Passwd + get_user_pw () throws UserError + { + assert (! is_pseudo_user); + + if (user_pw == null) { + errno = 0; + user_pw = Posix.getpwnam (name); + if (user_pw == null) + throw new UserError.GETPWNAM + ("Cannot get password file entry for " + + "%s: %s", name, strerror (errno)); + } + return user_pw; + } + + /** + * get_overlay_db: + * + * Returns: The path to the overlay database for this user, i.e. the + * path where new packages will be installed. + */ + public string + get_overlay_db () + { + return db_for_user (db.overlay, name); + } + + private void + ensure_db () throws UserError + { + if (users == null) + users = new Users (db); + users.ensure_db (); + var path = get_overlay_db (); + if (! exists (path)) { + try_create (path); + if (Posix.geteuid () == 0 && ! is_pseudo_user) + try_chown (path, get_user_pw ()); + } + } + + /* Note on privilege handling: + * We can normally get away without dropping privilege when reading, + * but some filesystems are strict about how much they let root work + * with user files (e.g. NFS root_squash). It is better to play it + * safe and drop privileges for any operations on the user's + * database. + */ + + private void + priv_drop_failure (string name) throws UserError + { + throw new UserError.DROP_PRIVS + ("Cannot drop privileges (%s): %s", + name, strerror (errno)); + } + + internal void + drop_privileges () throws UserError + { + if (dropped_privileges_count == 0 && + Posix.getuid () == 0 && ! is_pseudo_user) { + /* We don't bother with setgroups here; we only need + * the user/group of created filesystem nodes to be + * correct. + */ + unowned Posix.Passwd? pw = get_user_pw (); + if (PosixExtra.setegid (pw.pw_gid) < 0) + priv_drop_failure ("setegid"); + if (PosixExtra.seteuid (pw.pw_uid) < 0) + priv_drop_failure ("seteuid"); + old_umask = Posix.umask (get_umask () | Posix.S_IWOTH); + } + + ++dropped_privileges_count; + } + + private void + priv_regain_failure (string name) + { + /* It is too dangerous to carry on from this point, even if + * the caller has an exception handler. + */ + error ("Cannot regain privileges (%s): %s", + name, strerror (errno)); + } + + internal void + regain_privileges () + { + --dropped_privileges_count; + + if (dropped_privileges_count == 0 && + Posix.getuid () == 0 && ! is_pseudo_user) { + if (old_umask != null) + Posix.umask (old_umask); + if (PosixExtra.seteuid (0) < 0) + priv_regain_failure ("seteuid"); + if (PosixExtra.setegid (0) < 0) + priv_regain_failure ("setegid"); + } + } + + private bool + is_valid_link (string path) + { + if (! is_symlink (path)) + return false; + + try { + var target = FileUtils.read_link (path); + return ! target.has_prefix ("@"); + } catch (FileError e) { + return false; + } + } + + private List + get_package_names_dropped () throws Error + { + var entries = new List (); + var hidden = new Gee.HashSet (); + for (int i = db.size - 1; i >= 0; --i) { + var user_db = db_for_user (db[i].root, name); + foreach (var entry in Click.Dir.open (user_db)) { + if (entries.find_custom (entry, strcmp) + != null || + entry in hidden) + continue; + var path = Path.build_filename (user_db, entry); + if (is_valid_link (path)) + entries.prepend (entry.dup ()); + else if (is_symlink (path)) + hidden.add (entry.dup ()); + } + + if (name != ALL_USERS) { + var all_users_db = db_for_user (db[i].root, + ALL_USERS); + foreach (var entry in Click.Dir.open + (all_users_db)) { + if (entries.find_custom (entry, strcmp) + != null || + entry in hidden) + continue; + var path = Path.build_filename + (all_users_db, entry); + if (is_valid_link (path)) + entries.prepend (entry.dup ()); + else if (is_symlink (path)) + hidden.add (entry.dup ()); + } + } + } + entries.reverse (); + return entries; + } + + /** + * get_package_names: + * + * Returns: (transfer full): A list of package names installed for + * this user. + */ + public List + get_package_names () throws Error + { + drop_privileges (); + try { + return get_package_names_dropped (); + } finally { + regain_privileges (); + } + } + + /** + * has_package_name: + * @package: A package name. + * + * Returns: True if this user has a version of @package registered, + * otherwise false. + */ + public bool + has_package_name (string package) + { + try { + get_version (package); + return true; + } catch (UserError e) { + return false; + } + } + + /** + * get_version: + * @package: A package name. + * + * Returns: The version of @package registered for this user. + */ + public string + get_version (string package) throws UserError + { + for (int i = db.size - 1; i >= 0; --i) { + var user_db = db_for_user (db[i].root, name); + var path = Path.build_filename (user_db, package); + drop_privileges (); + try { + if (is_valid_link (path)) { + try { + var target = + FileUtils.read_link + (path); + return Path.get_basename + (target); + } catch (FileError e) { + } + } else if (is_symlink (path)) + throw new UserError.HIDDEN_PACKAGE + ("%s is hidden for user %s", + package, name); + } finally { + regain_privileges (); + } + + var all_users_db = db_for_user (db[i].root, ALL_USERS); + path = Path.build_filename (all_users_db, package); + if (is_valid_link (path)) { + try { + var target = FileUtils.read_link + (path); + return Path.get_basename (target); + } catch (FileError e) { + } + } else if (is_symlink (path)) + throw new UserError.HIDDEN_PACKAGE + ("%s is hidden for all users", + package); + } + + throw new UserError.NO_SUCH_PACKAGE + ("%s does not exist in any database for user %s", + package, name); + } + + /** + * set_version: + * @package: A package name. + * @version: A version string. + * + * Register version @version of @package for this user. + */ + public void + set_version (string package, string version) throws Error + { + /* Only modify the last database. */ + var user_db = get_overlay_db (); + var path = Path.build_filename (user_db, package); + var new_path = Path.build_filename (user_db, @".$package.new"); + ensure_db (); + string? old_version = null; + try { + old_version = get_version (package); + } catch (UserError e) { + } + drop_privileges (); + try { + var target = db.get_path (package, version); + bool done = false; + if (is_valid_link (path)) { + unlink_force (path); + try { + if (get_version (package) == version) + done = true; + } catch (UserError e) { + } + } + if (! done) { + symlink_force (target, new_path); + if (FileUtils.rename (new_path, path) < 0) + throw new UserError.RENAME + ("rename %s -> %s failed: %s", + new_path, path, + strerror (errno)); + } + } finally { + regain_privileges (); + } + if (! is_pseudo_user) + package_install_hooks (db, package, + old_version, version, name); + } + + /** + * remove: + * @package: A package name. + * + * Remove this user's registration of @package. + */ + public void + remove (string package) throws Error + { + /* Only modify the last database. */ + var user_db = get_overlay_db (); + var path = Path.build_filename (user_db, package); + string old_version; + if (is_valid_link (path)) { + var target = FileUtils.read_link (path); + old_version = Path.get_basename (target); + drop_privileges (); + try { + unlink_force (path); + } finally { + regain_privileges (); + } + } else { + try { + old_version = get_version (package); + } catch (UserError e) { + throw new UserError.NO_SUCH_PACKAGE + ("%s does not exist in any database " + + "for user %s", package, name); + } + ensure_db (); + drop_privileges (); + try { + symlink_force (HIDDEN_VERSION, path); + } finally { + regain_privileges (); + } + } + if (! is_pseudo_user) + package_remove_hooks (db, package, old_version, name); + } + + /** + * get_path: + * @package: A package name. + * + * Returns: The path at which @package is registered for this user. + */ + public string + get_path (string package) throws UserError + { + for (int i = db.size - 1; i >= 0; --i) { + var user_db = db_for_user (db[i].root, name); + var path = Path.build_filename (user_db, package); + if (is_valid_link (path)) + return path; + else if (is_symlink (path)) + throw new UserError.HIDDEN_PACKAGE + ("%s is hidden for user %s", + package, name); + + var all_users_db = db_for_user (db[i].root, ALL_USERS); + path = Path.build_filename (all_users_db, package); + if (is_valid_link (path)) + return path; + else if (is_symlink (path)) + throw new UserError.HIDDEN_PACKAGE + ("%s is hidden for all users", + package); + } + + throw new UserError.NO_SUCH_PACKAGE + ("%s does not exist in any database for user %s", + package, name); + } + + /** + * is_removable: + * @package: A package name. + * + * Returns: True if @package is removable for this user, otherwise + * False. + */ + public bool + is_removable (string package) + { + var user_db = get_overlay_db (); + var path = Path.build_filename (user_db, package); + if (exists (path)) + return true; + else if (is_symlink (path)) + /* Already hidden. */ + return false; + var all_users_db = db_for_user (db.overlay, ALL_USERS); + path = Path.build_filename (all_users_db, package); + if (is_valid_link (path)) + return true; + else if (is_symlink (path)) + /* Already hidden. */ + return false; + if (has_package_name (package)) + /* Not in overlay database, but can be hidden. */ + return true; + else + return false; + } +} + +} diff -Nru click-0.4.16/lib/click/valac-wrapper.in click-0.4.17.2/lib/click/valac-wrapper.in --- click-0.4.16/lib/click/valac-wrapper.in 1970-01-01 00:00:00.000000000 +0000 +++ click-0.4.17.2/lib/click/valac-wrapper.in 2014-03-06 16:38:26.000000000 +0000 @@ -0,0 +1,51 @@ +#! /bin/sh +set -e + +# Wrapper for valac, working around the fact that the .gir files it +# generates are missing the shared-library attribute in the namespace tag. +# +# https://bugzilla.gnome.org/show_bug.cgi?id=642576 +# +# Passing --shared-library to g-ir-compiler isn't enough for us, because +# dh_girepository then fails to generate a shared library dependency. +# +# While we're here, work around showing up in our external header +# file. We're careful only to make use of it internally. + +VALAC="@VALAC@" + +"$VALAC" "$@" + +header= +gir= +library= + +# Keep this in sync with any options used in lib/click/Makefile.am. -C is +# emitted by automake. +eval set -- "$(getopt -o CH: -l gir:,library:,pkg:,target-glib: -- "$@")" || \ + { echo "$0: failed to parse valac options" >&2; exit 2; } +while :; do + case $1 in + -C) shift ;; + -H) header="$2"; shift 2 ;; + --pkg|--target-glib) shift 2 ;; + --gir) gir="$2"; shift 2 ;; + --library) library="$2"; shift 2 ;; + --) shift; break ;; + *) echo "$0: failed to parse valac options" >&2; exit 2 ;; + esac +done + +[ "$header" ] || { echo "$0: failed to find -H in valac options" >&2; exit 2; } +[ "$gir" ] || { echo "$0: failed to find --gir in valac options" >&2; exit 2; } +[ "$library" ] || \ + { echo "$0: failed to find --library in valac options" >&2; exit 2; } + +if egrep 'Gee|gee_' "$header"; then + echo "libgee should not be exposed in our public header file." >&2 + exit 1 +fi +sed -i '/^#include $/d' "$header" + +sed -i 's/\(" - -start on filesystem - -task - -exec click hook run-system diff -Nru click-0.4.16/upstart/click-user-hooks.conf click-0.4.17.2/upstart/click-user-hooks.conf --- click-0.4.16/upstart/click-user-hooks.conf 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/upstart/click-user-hooks.conf 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -description "Run Click user-level hooks" -author "Colin Watson " - -start on starting xsession-init - -task - -exec click hook run-user diff -Nru click-0.4.16/upstart/Makefile.am click-0.4.17.2/upstart/Makefile.am --- click-0.4.16/upstart/Makefile.am 2014-03-04 15:23:26.000000000 +0000 +++ click-0.4.17.2/upstart/Makefile.am 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -systemdir = $(sysconfdir)/init -sessionsdir = $(prefix)/share/upstart/sessions - -system_DATA = click-system-hooks.conf -sessions_DATA = click-user-hooks.conf