click-0.4.21.1ubuntu0.2/0000775000000000000000000000000012320742335011376 5ustar click-0.4.21.1ubuntu0.2/pk-plugin/0000775000000000000000000000000012320742335013304 5ustar click-0.4.21.1ubuntu0.2/pk-plugin/com.ubuntu.click.policy.in0000664000000000000000000000215212320742124020311 0ustar Click https://launchpad.net/click package-x-generic <_description gettext-domain="click">Install package <_message gettext-domain="click">To install software, you need to authenticate. auth_self auth_self yes <_description gettext-domain="click">Remove package <_message gettext-domain="click">To remove software, you need to authenticate. auth_self auth_self yes click-0.4.21.1ubuntu0.2/pk-plugin/README0000664000000000000000000000171712320742124014166 0ustar This plugin is experimental, although it does minimally work. To make it usable on Ubuntu Touch when connected remotely (adb/ssh) you may need to override PolicyKit's defaults: $ sudo cat /etc/polkit-1/localauthority/50-local.d/10-click.pkla [Allow installation of Click packages] Identity=unix-user:phablet Action=com.ubuntu.click.package-install;com.ubuntu.click.package-remove ResultAny=yes (This is now installed in /var/lib/polkit-1/localauthority/10-vendor.d/com.ubuntu.click.pkla, for the time being.) Once that's done, install packagekit and packagekit-plugin-click, and you should be able to do things like: $ pkcon -p install-local foo.click I have not done any work on figuring out how to make this work on systems with aptdaemon. If you want to try this on a normal Ubuntu desktop system, then for the time being I recommend creating an LXC container for the purpose: $ sudo lxc-create -t ubuntu -n saucy-click -- -r saucy -a amd64 -b $USER click-0.4.21.1ubuntu0.2/pk-plugin/Makefile.am0000664000000000000000000000135212320742124015335 0ustar plugindir = @pkpluginlibdir@/packagekit-plugins plugin_LTLIBRARIES = libpk_plugin_click.la libpk_plugin_click_la_SOURCES = pk-plugin-click.c libpk_plugin_click_la_CPPFLAGS = -I$(top_builddir)/lib/click libpk_plugin_click_la_CFLAGS = @PKPLUGIN_CFLAGS@ libpk_plugin_click_la_LIBADD = \ $(top_builddir)/lib/click/libclick-0.4.la \ @PKPLUGIN_LIBS@ libpk_plugin_click_la_LDFLAGS = -avoid-version polkit_policydir = $(datadir)/polkit-1/actions dist_polkit_policy_DATA = com.ubuntu.click.policy @INTLTOOL_POLICY_RULE@ EXTRA_DIST = com.ubuntu.click.policy.in DISTCLEANFILES = com.ubuntu.click.policy polkit_localauthoritydir = $(localstatedir)/lib/polkit-1/localauthority/10-vendor.d dist_polkit_localauthority_DATA = com.ubuntu.click.pkla click-0.4.21.1ubuntu0.2/pk-plugin/pk-plugin-click.c0000664000000000000000000005754112320742124016451 0ustar /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2010-2013 Matthias Klumpp * Copyright (C) 2011 Richard Hughes * Copyright (C) 2013 Canonical Ltd. * * 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 . */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #define I_KNOW_THE_PACKAGEKIT_GLIB2_API_IS_SUBJECT_TO_CHANGE #include #define I_KNOW_THE_PACKAGEKIT_PLUGIN_API_IS_SUBJECT_TO_CHANGE #include #include "click.h" struct PkPluginPrivate { guint dummy; }; #define DEFAULT_PATH \ "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" /** * click_is_click_file: * * Check if a given file is a Click package file. */ static gboolean click_is_click_file (const gchar *filename) { gboolean ret = FALSE; GFile *file; GFileInfo *info = NULL; const gchar *content_type; file = g_file_new_for_path (filename); info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (!info) goto out; content_type = g_file_info_get_content_type (info); if (strcmp (content_type, "application/x-click") == 0) ret = TRUE; out: g_clear_object (&info); g_clear_object (&file); return ret; } static gchar ** click_filter_click_files (PkTransaction *transaction, gchar **files) { gchar **native_files = NULL; gchar **click_files = NULL; GPtrArray *native = NULL; GPtrArray *click = NULL; gint i; gboolean ret = FALSE; /* Are there any Click packages at all? If not, we can bail out * early. */ for (i = 0; files[i]; ++i) { ret = click_is_click_file (files[i]); if (ret) break; } if (!ret) goto out; /* Find and filter Click packages. */ native = g_ptr_array_new_with_free_func (g_free); click = g_ptr_array_new_with_free_func (g_free); for (i = 0; files[i]; ++i) { ret = click_is_click_file (files[i]); g_ptr_array_add (ret ? click : native, g_strdup (files[i])); } native_files = pk_ptr_array_to_strv (native); click_files = pk_ptr_array_to_strv (click); pk_transaction_set_full_paths (transaction, native_files); out: g_strfreev (native_files); if (native) g_ptr_array_unref (native); if (click) g_ptr_array_unref (click); return click_files; } static gboolean click_pkid_data_is_click (const gchar *data) { gchar **tokens; gboolean ret; tokens = g_strsplit (data, ",", 2); ret = g_strcmp0 (tokens[0], "local:click") == 0 || g_strcmp0 (tokens[0], "installed:click") == 0; g_strfreev (tokens); return ret; } /** * click_is_click_package: * * Check if a given PackageKit package-id is a Click package. */ static gboolean click_is_click_package (const gchar *package_id) { gchar **parts = NULL; gboolean ret = FALSE; parts = pk_package_id_split (package_id); if (!parts) goto out; ret = click_pkid_data_is_click (parts[PK_PACKAGE_ID_DATA]); out: g_strfreev (parts); return ret; } static gchar ** click_filter_click_packages (PkTransaction *transaction, gchar **package_ids) { gchar **native_package_ids = NULL; gchar **click_package_ids = NULL; GPtrArray *native = NULL; GPtrArray *click = NULL; gint i; gboolean ret = FALSE; /* Are there any Click packages at all? If not, we can bail out * early. */ for (i = 0; package_ids[i]; ++i) { ret = click_is_click_package (package_ids[i]); if (ret) break; } if (!ret) goto out; /* Find and filter Click packages. */ native = g_ptr_array_new_with_free_func (g_free); click = g_ptr_array_new_with_free_func (g_free); for (i = 0; package_ids[i]; ++i) { ret = click_is_click_package (package_ids[i]); g_ptr_array_add (ret ? click : native, g_strdup (package_ids[i])); } native_package_ids = pk_ptr_array_to_strv (native); click_package_ids = pk_ptr_array_to_strv (click); pk_transaction_set_package_ids (transaction, native_package_ids); out: g_strfreev (native_package_ids); if (native) g_ptr_array_unref (native); if (click) g_ptr_array_unref (click); return click_package_ids; } /** * click_get_username_for_uid: * * Return the username corresponding to a given user ID, or NULL. The * caller is responsible for freeing the result. */ static gchar * click_get_username_for_uid (uid_t uid) { struct passwd pwbuf, *pwbufp; char *buf = NULL; size_t buflen; gchar *username = NULL; buflen = sysconf (_SC_GETPW_R_SIZE_MAX); if (buflen == -1) buflen = 1024; buf = g_malloc (buflen); for (;;) { int ret; /* TODO: getpwuid_r is apparently a portability headache; * see glib/gio/glocalfileinfo.c. But for now we only care * about Linux. */ ret = getpwuid_r (uid, &pwbuf, buf, buflen, &pwbufp); if (pwbufp) break; if (ret != ERANGE) goto out; buflen *= 2; /* TODO: check overflow */ buf = g_realloc (buf, buflen); } username = g_strdup (pwbuf.pw_name); out: g_free (buf); return username; } /** * click_get_envp: * * Return the environment needed by click. This is the same as the * environment we got, except with a reasonable PATH (PackageKit clears its * environment by default). */ static gchar ** click_get_envp (void) { gchar **environ; gchar **env_item; guint env_len; environ = g_get_environ (); env_len = 0; for (env_item = environ; env_item && *env_item; ++env_item) { if (strncmp (*env_item, "PATH=", sizeof ("PATH=") - 1) == 0) return environ; ++env_len; } env_len = environ ? g_strv_length (environ) : 0; environ = g_realloc_n (environ, env_len + 2, sizeof (*environ)); environ[env_len] = g_strdup ("PATH=" DEFAULT_PATH); environ[env_len + 1] = NULL; return environ; } static void click_pk_error (PkPlugin *plugin, PkErrorEnum code, const char *summary, const char *extra) { if (pk_backend_job_get_is_error_set (plugin->job)) { /* PK already has an error; just log this. */ g_warning ("%s", summary); if (extra) g_warning ("%s", extra); } else if (extra) pk_backend_job_error_code (plugin->job, code, "%s\n%s", summary, extra); else pk_backend_job_error_code (plugin->job, code, "%s", summary); } static JsonParser * click_get_manifest (PkPlugin *plugin, const gchar *filename) { gboolean ret; gchar **argv = NULL; gint i; gchar **envp = NULL; gchar *manifest_text = NULL; gchar *click_stderr = NULL; gint click_status; JsonParser *parser = NULL; argv = g_malloc0_n (4, sizeof (*argv)); i = 0; argv[i++] = g_strdup ("click"); argv[i++] = g_strdup ("info"); argv[i++] = g_strdup (filename); envp = click_get_envp (); ret = g_spawn_sync (NULL, argv, envp, G_SPAWN_SEARCH_PATH, NULL, NULL, &manifest_text, &click_stderr, &click_status, NULL); if (!ret) goto out; if (!g_spawn_check_exit_status (click_status, NULL)) { gchar *summary = g_strdup_printf ("\"click info %s\" failed.", filename); click_pk_error (plugin, PK_ERROR_ENUM_INTERNAL_ERROR, summary, click_stderr); g_free (summary); goto out; } parser = json_parser_new (); if (!parser) goto out; json_parser_load_from_data (parser, manifest_text, -1, NULL); out: g_strfreev (argv); g_strfreev (envp); g_free (manifest_text); g_free (click_stderr); return parser; } static gchar * click_get_field_string (JsonObject *manifest, const gchar *field) { JsonNode *node; node = json_object_get_member (manifest, field); if (!node) return NULL; return json_node_dup_string (node); } static JsonObject * click_get_field_object (JsonObject *manifest, const gchar *field) { JsonNode *node; node = json_object_get_member (manifest, field); if (!node) return NULL; /* Note that this does not take a reference. */ return json_node_get_object (node); } static gboolean click_get_field_boolean (JsonObject *manifest, const gchar *field, gboolean def) { JsonNode *node; node = json_object_get_member (manifest, field); if (!node) return def; return json_node_get_boolean (node); } static JsonArray * click_get_list (PkPlugin *plugin, PkTransaction *transaction) { gchar *username = NULL; ClickUser *registry = NULL; JsonArray *array = NULL; GError *error = NULL; username = click_get_username_for_uid (pk_transaction_get_uid (transaction)); registry = click_user_new_for_user (NULL, username, &error); if (error) { click_pk_error (plugin, PK_ERROR_ENUM_INTERNAL_ERROR, "Unable to read Click database.", error->message); goto out; } array = click_user_get_manifests (registry, &error); if (error) { click_pk_error (plugin, PK_ERROR_ENUM_INTERNAL_ERROR, "Unable to get Click package manifests.", error->message); goto out; } out: if (error) g_error_free (error); g_clear_object (®istry); g_free (username); return array; } static gchar * click_build_pkid_data (const gchar *data_prefix, JsonObject *manifest) { gint n_elements = 0; gchar **elements = NULL; gint i; JsonObject *hooks; GList *hooks_members = NULL, *hooks_iter; gchar *data = NULL; hooks = click_get_field_object (manifest, "hooks"); n_elements = 3; /* data_prefix, removable, terminator */ if (hooks) n_elements += json_object_get_size (hooks); elements = g_new0 (gchar *, n_elements); if (!elements) goto out; i = 0; elements[i++] = g_strdup (data_prefix); /* A missing "_removable" entry in the manifest means that we just * installed the package, so it must be removable. */ if (click_get_field_boolean (manifest, "_removable", TRUE)) elements[i++] = g_strdup ("removable=1"); else elements[i++] = g_strdup ("removable=0"); if (hooks) { hooks_members = json_object_get_members (hooks); for (hooks_iter = hooks_members; hooks_iter; hooks_iter = hooks_iter->next) { g_assert (i < n_elements - 1); elements[i++] = g_strdup_printf ("app_name=%s", (gchar *) hooks_iter->data); } } elements[i] = NULL; data = g_strjoinv (",", elements); out: g_strfreev (elements); if (hooks_members) g_list_free (hooks_members); return data; } static gchar * click_build_pkid (PkPlugin *plugin, JsonObject *manifest, const gchar *data_prefix) { gchar *name = NULL; gchar *version = NULL; gchar *architecture = NULL; gchar *data = NULL; gchar *pkid = NULL; if (!manifest) goto out; name = click_get_field_string (manifest, "name"); if (!name) goto out; version = click_get_field_string (manifest, "version"); if (!version) goto out; architecture = click_get_field_string (manifest, "architecture"); if (!architecture) architecture = g_strdup (""); data = click_build_pkid_data (data_prefix, manifest); pkid = pk_package_id_build (name, version, architecture, data); out: g_free (name); g_free (version); g_free (architecture); g_free (data); return pkid; } static gboolean click_split_pkid (const gchar *package_id, gchar **name, gchar **version, gchar **architecture) { gchar **parts = NULL; gboolean ret = FALSE; parts = pk_package_id_split (package_id); if (!parts) goto out; if (!click_pkid_data_is_click (parts[PK_PACKAGE_ID_DATA])) goto out; if (name) *name = g_strdup (parts[PK_PACKAGE_ID_NAME]); if (version) *version = g_strdup (parts[PK_PACKAGE_ID_VERSION]); if (architecture) *architecture = g_strdup (parts[PK_PACKAGE_ID_ARCH]); ret = TRUE; out: g_strfreev (parts); return ret; } static gboolean click_install_file (PkPlugin *plugin, PkTransaction *transaction, const gchar *filename) { gboolean ret = FALSE; gchar **argv = NULL; gint i; gchar *username = NULL; gchar **envp = NULL; gchar *click_stderr = NULL; gint click_status; JsonParser *parser = NULL; JsonObject *manifest; gchar *pkid = NULL; argv = g_malloc0_n (6, sizeof (*argv)); i = 0; argv[i++] = g_strdup ("click"); argv[i++] = g_strdup ("install"); username = click_get_username_for_uid (pk_transaction_get_uid (transaction)); if (username) argv[i++] = g_strdup_printf ("--user=%s", username); /* TODO: make --force-missing-framework configurable */ argv[i++] = g_strdup (filename); envp = click_get_envp (); ret = g_spawn_sync (NULL, argv, envp, G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL, NULL, NULL, NULL, &click_stderr, &click_status, NULL); if (!ret) goto out; if (!g_spawn_check_exit_status (click_status, NULL)) { gchar *summary = g_strdup_printf ("%s failed to install.", filename); click_pk_error (plugin, PK_ERROR_ENUM_PACKAGE_FAILED_TO_INSTALL, summary, click_stderr); g_free (summary); ret = FALSE; goto out; } parser = click_get_manifest (plugin, filename); if (parser) { manifest = json_node_get_object (json_parser_get_root (parser)); pkid = click_build_pkid (plugin, manifest, "installed:click"); } if (!pk_backend_job_get_is_error_set (plugin->job)) { pk_backend_job_package (plugin->job, PK_INFO_ENUM_INSTALLED, pkid, "summary goes here"); ret = TRUE; } out: g_strfreev (argv); g_free (username); g_strfreev (envp); g_free (click_stderr); g_clear_object (&parser); g_free (pkid); return ret; } static void click_install_files (PkPlugin *plugin, PkTransaction *transaction, gchar **filenames) { gboolean ret = FALSE; gint i; for (i = 0; filenames[i]; ++i) { g_debug ("Click: installing %s", filenames[i]); ret = click_install_file (plugin, transaction, filenames[i]); if (!ret) break; } } static void click_get_packages_one (JsonArray *array, guint index, JsonNode *element_node, gpointer data) { PkPlugin *plugin; JsonObject *manifest; const gchar *title = NULL; gchar *pkid = NULL; plugin = (PkPlugin *) data; manifest = json_node_get_object (element_node); if (!manifest) return; if (json_object_has_member (manifest, "title")) title = json_object_get_string_member (manifest, "title"); if (!title) title = ""; pkid = click_build_pkid (plugin, manifest, "installed:click"); if (pkid) pk_backend_job_package (plugin->job, PK_INFO_ENUM_INSTALLED, pkid, title); g_free (pkid); } static void click_get_packages (PkPlugin *plugin, PkTransaction *transaction) { JsonArray *array = NULL; array = click_get_list (plugin, transaction); if (!array) return; json_array_foreach_element (array, click_get_packages_one, plugin); json_array_unref (array); } static gboolean click_remove_package (PkPlugin *plugin, PkTransaction *transaction, const gchar *package_id) { gboolean ret = FALSE; gchar *username = NULL; gchar *name = NULL; gchar *version = NULL; ClickDB *db = NULL; ClickUser *registry = NULL; gchar *old_version = NULL; GError *error = NULL; gchar *summary = NULL; username = click_get_username_for_uid (pk_transaction_get_uid (transaction)); if (!username) { g_error ("Click: cannot remove packages without a username"); goto out; } if (!click_split_pkid (package_id, &name, &version, NULL)) { g_error ("Click: cannot parse package ID '%s'", package_id); goto out; } db = click_db_new (); click_db_read (db, NULL, &error); if (error) { summary = g_strdup_printf ("Unable to read Click database while removing %s.", package_id); click_pk_error (plugin, PK_ERROR_ENUM_PACKAGE_FAILED_TO_REMOVE, summary, error->message); goto out; } registry = click_user_new_for_user (db, username, &error); if (error) { summary = g_strdup_printf ("Unable to read Click database while removing %s.", package_id); click_pk_error (plugin, PK_ERROR_ENUM_PACKAGE_FAILED_TO_REMOVE, summary, error->message); goto out; } old_version = click_user_get_version (registry, name, &error); if (error) { summary = g_strdup_printf ("Unable to get current version of Click package %s.", name); click_pk_error (plugin, PK_ERROR_ENUM_PACKAGE_FAILED_TO_REMOVE, summary, error->message); goto out; } if (strcmp (old_version, version) != 0) { summary = g_strdup_printf ("Not removing Click package %s %s; does not match " "current version %s.", name, version, old_version); click_pk_error (plugin, PK_ERROR_ENUM_PACKAGE_FAILED_TO_REMOVE, summary, NULL); goto out; } click_user_remove (registry, name, &error); if (error) { summary = g_strdup_printf ("Failed to remove %s.", package_id); click_pk_error (plugin, PK_ERROR_ENUM_PACKAGE_FAILED_TO_REMOVE, summary, error->message); goto out; } click_db_maybe_remove (db, name, version, &error); if (error) { summary = g_strdup_printf ("Failed to remove %s.", package_id); click_pk_error (plugin, PK_ERROR_ENUM_PACKAGE_FAILED_TO_REMOVE, summary, error->message); goto out; } /* TODO: remove data? */ ret = TRUE; out: g_free (summary); if (error) g_error_free (error); g_free (old_version); g_clear_object (®istry); g_clear_object (&db); g_free (version); g_free (name); g_free (username); return ret; } static void click_remove_packages (PkPlugin *plugin, PkTransaction *transaction, gchar **package_ids) { gboolean ret = FALSE; gint i; for (i = 0; package_ids[i]; ++i) { g_debug ("Click: removing %s", package_ids[i]); ret = click_remove_package (plugin, transaction, package_ids[i]); if (!ret) break; } } struct click_search_data { PkPlugin *plugin; gchar **values; gboolean search_details; }; static void click_search_emit (PkPlugin *plugin, JsonObject *manifest, const gchar *title) { gchar *package_id; package_id = click_build_pkid (plugin, manifest, "installed:click"); if (!package_id) return; g_debug ("Found package: %s", package_id); pk_backend_job_package (plugin->job, PK_INFO_ENUM_INSTALLED, package_id, title); g_free (package_id); } static void click_search_one (JsonArray *array, guint index, JsonNode *element_node, gpointer vdata) { struct click_search_data *data; JsonObject *manifest; const gchar *name = NULL; const gchar *title = NULL; const gchar *description = NULL; gchar **value; data = (struct click_search_data *) vdata; manifest = json_node_get_object (element_node); if (!manifest) return; name = json_object_get_string_member (manifest, "name"); if (!name) return; if (data->search_details && json_object_has_member (manifest, "title")) title = json_object_get_string_member (manifest, "title"); if (!title) title = ""; if (data->search_details && json_object_has_member (manifest, "description")) description = json_object_get_string_member (manifest, "description"); if (!description) description = ""; for (value = data->values; *value; ++value) { if (strcasestr (name, *value)) { click_search_emit (data->plugin, manifest, title); break; } if (data->search_details && (strcasestr (title, *value) || strcasestr (description, *value))) { click_search_emit (data->plugin, manifest, title); break; } } } static void click_search (PkPlugin *plugin, PkTransaction *transaction, gchar **values, gboolean search_details) { JsonArray *array = NULL; struct click_search_data data; array = click_get_list (plugin, transaction); if (!array) return; data.plugin = plugin; data.values = values; data.search_details = search_details; json_array_foreach_element (array, click_search_one, &data); json_array_unref (array); } static void click_skip_native_backend (PkPlugin *plugin) { if (!pk_backend_job_get_is_error_set (plugin->job)) pk_backend_job_set_exit_code (plugin->job, PK_EXIT_ENUM_SKIP_TRANSACTION); } /** * pk_plugin_get_description: */ const gchar * pk_plugin_get_description (void) { return "Support for Click packages"; } /** * pk_plugin_initialize: */ void pk_plugin_initialize (PkPlugin *plugin) { /* create private area */ plugin->priv = PK_TRANSACTION_PLUGIN_GET_PRIVATE (PkPluginPrivate); /* tell PK we might be able to handle these */ pk_backend_implement (plugin->backend, PK_ROLE_ENUM_INSTALL_FILES); pk_backend_implement (plugin->backend, PK_ROLE_ENUM_GET_PACKAGES); pk_backend_implement (plugin->backend, PK_ROLE_ENUM_REMOVE_PACKAGES); } /** * pk_plugin_transaction_content_types: */ void pk_plugin_transaction_content_types (PkPlugin *plugin, PkTransaction *transaction) { pk_transaction_add_supported_content_type (transaction, "application/x-click"); } /** * pk_plugin_transaction_started: */ void pk_plugin_transaction_started (PkPlugin *plugin, PkTransaction *transaction) { PkRoleEnum role; gchar **full_paths = NULL; gchar **package_ids = NULL; gchar **click_data = NULL; gchar **values; PkBitfield flags; gboolean simulating; g_debug ("Processing transaction"); pk_backend_job_reset (plugin->job); pk_transaction_signals_reset (transaction, plugin->job); pk_backend_job_set_status (plugin->job, PK_STATUS_ENUM_SETUP); role = pk_transaction_get_role (transaction); flags = pk_transaction_get_transaction_flags (transaction); simulating = pk_bitfield_contain (flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE); switch (role) { case PK_ROLE_ENUM_INSTALL_FILES: /* TODO: Simulation needs to be smarter - backend * needs to Simulate() with remaining packages. */ full_paths = pk_transaction_get_full_paths (transaction); click_data = click_filter_click_files (transaction, full_paths); if (!simulating && click_data) click_install_files (plugin, transaction, click_data); full_paths = pk_transaction_get_full_paths (transaction); if (g_strv_length (full_paths) == 0) click_skip_native_backend (plugin); break; case PK_ROLE_ENUM_GET_PACKAGES: /* TODO: Handle simulation? */ if (!simulating) click_get_packages (plugin, transaction); break; case PK_ROLE_ENUM_REMOVE_PACKAGES: package_ids = pk_transaction_get_package_ids (transaction); click_data = click_filter_click_packages (transaction, package_ids); if (!simulating && click_data) click_remove_packages (plugin, transaction, click_data); package_ids = pk_transaction_get_package_ids (transaction); if (g_strv_length (package_ids) == 0) click_skip_native_backend (plugin); break; case PK_ROLE_ENUM_SEARCH_NAME: case PK_ROLE_ENUM_SEARCH_DETAILS: values = pk_transaction_get_values (transaction); click_search (plugin, transaction, values, role == PK_ROLE_ENUM_SEARCH_DETAILS); break; default: break; } g_strfreev (click_data); } /** * pk_plugin_transaction_get_action: **/ const gchar * pk_plugin_transaction_get_action (PkPlugin *plugin, PkTransaction *transaction, const gchar *action_id) { const gchar *install_actions[] = { "org.freedesktop.packagekit.package-install", "org.freedesktop.packagekit.package-install-untrusted", NULL }; const gchar *remove_action = "org.freedesktop.packagekit.package-remove"; const gchar **install_action; gchar **full_paths; gchar **package_ids; gint i; if (!action_id) return NULL; for (install_action = install_actions; *install_action; ++install_action) { if (strcmp (action_id, *install_action) == 0) { /* Use an action with weaker auth requirements if * and only if all the packages in the list are * Click files. */ full_paths = pk_transaction_get_full_paths (transaction); for (i = 0; full_paths[i]; ++i) { if (!click_is_click_file (full_paths[i])) break; } if (!full_paths[i]) return "com.ubuntu.click.package-install"; } } if (strcmp (action_id, remove_action) == 0) { /* Use an action with weaker auth requirements if and only * if all the packages in the list are Click packages. */ package_ids = pk_transaction_get_package_ids (transaction); for (i = 0; package_ids[i]; ++i) { if (!click_is_click_package (package_ids[i])) break; } if (!package_ids[i]) return "com.ubuntu.click.package-remove"; } return action_id; } click-0.4.21.1ubuntu0.2/pk-plugin/com.ubuntu.click.pkla0000664000000000000000000000023012320742124017327 0ustar [Allow installation of Click packages] Identity=unix-user:phablet Action=com.ubuntu.click.package-install;com.ubuntu.click.package-remove ResultAny=yes click-0.4.21.1ubuntu0.2/bin/0000775000000000000000000000000012320742335012146 5ustar click-0.4.21.1ubuntu0.2/bin/click0000775000000000000000000000462212320742124013161 0ustar #! /usr/bin/python3 # 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 . """Operations on Click packages.""" from __future__ import print_function from optparse import OptionParser import os import signal import sys from textwrap import dedent # Support running from the build tree. sys.path.insert(0, os.path.join(sys.path[0], os.pardir)) from click import commands def fix_stdout(): if sys.version >= "3": # Force encoding to UTF-8 even in non-UTF-8 locales. import io sys.stdout = io.TextIOWrapper( sys.stdout.detach(), encoding="UTF-8", line_buffering=True) else: # Avoid having to do .encode("UTF-8") everywhere. import codecs sys.stdout = codecs.EncodedFile(sys.stdout, "UTF-8") def null_decode(input, errors="strict"): return input, len(input) sys.stdout.decode = null_decode def main(): fix_stdout() # Python's default handling of SIGPIPE is not helpful to us. signal.signal(signal.SIGPIPE, signal.SIG_DFL) parser = OptionParser(dedent("""\ %%prog COMMAND [options] Commands are as follows ('%%prog COMMAND --help' for more): %s""") % commands.help_text()) parser.disable_interspersed_args() _, args = parser.parse_args() if not args: parser.print_help() return 0 command = args[0] args = args[1:] if command == "help": if args and args[0] in commands.all_commands: mod = commands.load_command(args[0]) mod.run(["--help"]) else: parser.print_help() return 0 if command not in commands.all_commands: parser.error("unknown command: %s" % command) mod = commands.load_command(command) return mod.run(args) if __name__ == "__main__": sys.exit(main()) click-0.4.21.1ubuntu0.2/m4/0000775000000000000000000000000012320742335011716 5ustar click-0.4.21.1ubuntu0.2/schroot/0000775000000000000000000000000012320742335013057 5ustar click-0.4.21.1ubuntu0.2/schroot/Makefile.am0000664000000000000000000000011212320742124015101 0ustar schroot_clickdir = @sysconfdir@/schroot/click schroot_click_DATA = fstab click-0.4.21.1ubuntu0.2/schroot/fstab0000664000000000000000000000113212320742124014072 0ustar # fstab: static file system information for chroots. # Note that the mount point will be prefixed by the chroot path # (CHROOT_PATH) # # /proc /proc none rw,bind 0 0 /sys /sys none rw,bind 0 0 /dev /dev none rw,bind 0 0 /dev/pts /dev/pts none rw,bind 0 0 /home /home none rw,rbind 0 0 /tmp /tmp none rw,bind 0 0 click-0.4.21.1ubuntu0.2/debian/0000775000000000000000000000000012607741701012624 5ustar click-0.4.21.1ubuntu0.2/debian/click.dirs0000664000000000000000000000003312320742124014560 0ustar usr/share/click/frameworks click-0.4.21.1ubuntu0.2/debian/click-doc.docs0000664000000000000000000000002012320742124015306 0ustar doc/_build/html click-0.4.21.1ubuntu0.2/debian/libclick-0.4-0.symbols0000664000000000000000000001006412320742142016437 0ustar 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_manifest@Base 0.4.18 click_db_get_manifest_as_string@Base 0.4.21 click_db_get_manifests@Base 0.4.18 click_db_get_manifests_as_string@Base 0.4.21 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_has_package_version@Base 0.4.18 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_framework_error_quark@Base 0.4.18 click_framework_get_base_name@Base 0.4.18 click_framework_get_base_version@Base 0.4.18 click_framework_get_fields@Base 0.4.18 click_framework_get_field@Base 0.4.18 click_framework_get_frameworks@Base 0.4.18 click_framework_get_name@Base 0.4.18 click_framework_get_type@Base 0.4.18 click_framework_has_framework@Base 0.4.18 click_framework_open@Base 0.4.18 click_get_db_dir@Base 0.4.17 click_get_frameworks_dir@Base 0.4.18 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_manifest@Base 0.4.18 click_single_db_get_manifest_as_string@Base 0.4.21 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_has_package_version@Base 0.4.18 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_manifest@Base 0.4.18 click_user_get_manifest_as_string@Base 0.4.21 click_user_get_manifests@Base 0.4.18 click_user_get_manifests_as_string@Base 0.4.21 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 click-0.4.21.1ubuntu0.2/debian/copyright0000664000000000000000000000674612320742124014564 0ustar Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: click Upstream-Contact: Source: https://launchpad.net/click Files: * Copyright: 2013, Canonical Ltd. License: GPL-3 Files: click/test/helpers.py Copyright: 2013, Canonical Ltd. 2007-2012 Michael Foord. License: Python Files: pk-plugin/* Copyright: 2010-2013, Matthias Klumpp 2011, Richard Hughes 2013, Canonical Ltd. License: GPL-3 License: GPL-3 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 copy of the GNU General Public License version 3 is available in /usr/share/common-licenses/GPL-3. License: Python 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. . 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. . 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. . 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. . 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. . 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. . 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. . 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. click-0.4.21.1ubuntu0.2/debian/packagekit-check0000775000000000000000000000031112320742124015713 0ustar #! /bin/sh set -e packagekit_version="$(dpkg-query -f '${Version}\n' -W libpackagekit-glib2-dev || echo 0)" if dpkg --compare-versions "$packagekit_version" ge 0.8.10; then echo yes else echo no fi click-0.4.21.1ubuntu0.2/debian/control0000664000000000000000000001070212320742124014217 0ustar Source: click Section: admin 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-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 X-Python-Version: >= 2.7 X-Python3-Version: >= 3.2 Package: click Architecture: any Pre-Depends: ${misc:Pre-Depends} Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, python3-click (= ${binary:Version}), adduser Recommends: click-apparmor, upstart-app-launch-tools Conflicts: click-package Replaces: click-package Provides: click-package Description: Click packages Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . This package provides common files, including the main click program. Package: click-dev Architecture: any Multi-Arch: foreign Depends: ${misc:Depends}, ${perl:Depends}, python3-click (= ${binary:Version}) Recommends: debootstrap, schroot Description: build Click packages Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . click-dev provides support for building these packages. Package: python3-click Section: python Architecture: any 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 Description: Click packages (Python 3 interface) Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . 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, libjson-glib-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}, libclick-0.4-0 (= ${binary:Version}) 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 Depends: ${misc:Depends}, ${sphinxdoc:Depends} Conflicts: click-package-doc Replaces: click-package-doc Provides: click-package-doc Description: Click packages (documentation) Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . This package provides documentation for click. Package: packagekit-plugin-click Architecture: any Pre-Depends: ${misc:Pre-Depends} Depends: ${shlibs:Depends}, ${misc:Depends}, click (= ${binary:Version}) Description: Click packages (PackageKit plugin) 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 PackageKit plugin adding support for Click packages. click-0.4.21.1ubuntu0.2/debian/libclick-0.4-dev.install0000664000000000000000000000012412320742124017030 0ustar usr/include usr/lib/*/libclick*.so usr/lib/*/pkgconfig/click-*.pc usr/share/gir-1.0 click-0.4.21.1ubuntu0.2/debian/click.lintian-overrides0000664000000000000000000000005412320742124017260 0ustar click: description-starts-with-package-name click-0.4.21.1ubuntu0.2/debian/packagekit-plugin-click.docs0000664000000000000000000000002112320742124020141 0ustar pk-plugin/README click-0.4.21.1ubuntu0.2/debian/changelog0000664000000000000000000007164712607740342014515 0ustar click (0.4.21.1ubuntu0.2) trusty-security; urgency=medium * SECURITY UPDATE: fix privilege escalation via crafted data.tar.gz that can be used to install alternate security policy than what is defined - click/install.py: Forbid installing packages with data tarball members whose names do not start with "./". Based on patch from Colin Watson. - CVE-2015-XXXX - LP: #1506467 -- Jamie Strandboge Thu, 15 Oct 2015 10:05:35 -0500 click (0.4.21.1) trusty; urgency=medium [ Colin Watson ] * When a hook command fails, include the command in the error message. * Don't allow failure of a single hook to prevent other hooks being run. * Log hook failures to stderr and exit non-zero, rather than propagating an exception which is then logged as a click crash. -- Ubuntu daily release Tue, 08 Apr 2014 09:41:55 +0000 click (0.4.21) trusty; urgency=medium * Add *_as_string variants of manifest methods, for clients that already have their own JSON parsing libraries and don't want to use JSON-GLib. * Write to stderr and exit non-zero when chrooted commands fail, rather than propagating an exception which is then logged as a click crash (LP: #1298457). * Make the get_manifests family of functions log errors about individual manifests to stderr rather than crashing (LP: #1297519). * Don't run user hooks until dbus has started; the content-hub hook needs to modify gsettings. * Don't rely on PyGObject supporting default None arguments; this was only added in 3.11.1. -- Colin Watson Tue, 08 Apr 2014 10:13:37 +0100 click (0.4.20) trusty; urgency=medium [ Colin Watson ] * Create system hook symlinks for all installed packages, not just current versions. This avoids missing AppArmor profiles when there are unregistered user-installed versions of packages lying around. -- Ubuntu daily release Mon, 24 Mar 2014 16:16:37 +0000 click (0.4.19) trusty; urgency=medium [ Colin Watson ] * Set Click.User.ensure_db visibility back to private, since it's no longer used by Click.Hook. (The C ABI is unaffected.) * Add brief documentation on Click's multiple-database scheme, based on my recent mail to ubuntu-phone. * Fix a few potential GLib critical messages from the PackageKit plugin. * Make libclick-0.4-dev depend on libjson-glib-dev for . * Add Requires.private to click-0.4.pc, so that programs built against libclick pick up the proper CFLAGS including glib and json-glib. * chroot: Allow creating 14.04 chroots. * Include _directory and _removable dynamic manifest keys in "click info" output (LP: #1293788). * Document -f and -s options to "click chroot" in click(1). * chroot: Fix code to make /finish.sh executable. * chroot: Make /usr/sbin/policy-rc.d executable in the chroot, as otherwise it has no effect. * chroot: Run apt-get dist-upgrade on the chroot before trying to install the basic build tool set. Fixes chroot creation for saucy. [ Benjamin Zeller ] * Take pkexec env vars into account when creating a chroot. [ Dimitri John Ledkov ] * Add session management to click chroot. -- Ubuntu daily release Tue, 18 Mar 2014 14:27:53 +0000 click (0.4.18.3) trusty; urgency=medium [ Colin Watson ] * Take a slightly different approach to fixing "click hook run-user": only try to update user registration symlinks if they already exist in the overlay database. -- Ubuntu daily release Wed, 12 Mar 2014 12:02:47 +0000 click (0.4.18.2) trusty; urgency=medium * Make "click hook run-user" ensure that the user registration directory exists before dropping privileges and trying to create symlinks in it (LP: #1291192). -- Colin Watson Wed, 12 Mar 2014 11:59:31 +0000 click (0.4.18.1) trusty; urgency=medium [ Colin Watson ] * If a user attempts to install a version of a package that is already installed in an underlay database, then just register the appropriate version for them rather than unpacking another copy. * Make "click hook run-system" and "click hook run-user" consistently use the bottom-most unpacked copy of a given version of a package, and update hook symlinks and user registration symlinks if necessary. -- Ubuntu daily release Tue, 11 Mar 2014 17:22:10 +0000 click (0.4.18) trusty; urgency=medium * Give gir1.2-click-0.4 an exact-versioned dependency on libclick-0.4-0. * Use is_symlink helper method in a few more places. * Add a similar is_dir helper method. * Ignore extraneous non-directories when walking a database root in Click.DB.get_packages and Click.DB.gc. * Make the PackageKit plugin tolerate the "_removable" dynamic manifest key being changed to a boolean in the future. * Document that users of "_removable" should tolerate it being a boolean. * Use libclick when removing packages, listing packages, or searching packages via the PackageKit plugin. * Add libclick interfaces to get package manifests, both individually (LP: #1287692) and for all installed packages (LP: #1287693). * Override description-starts-with-package-name Lintian error for click; this is describing the system as a whole rather than naming the package. * Add libclick interfaces to get the list of frameworks supported by the current system (LP: #1271633) and various properties of those frameworks (LP: #1287694). -- Colin Watson Tue, 11 Mar 2014 17:18:07 +0000 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 ] * hooks: Fix expansion of "$$" in hook patterns to conform to the documented behaviour of expanding to the single character "$". * Move version detection out of configure.ac into a separate get-version script, since intltool-update has trouble with the previous approach. * Stop using unittest2 if available; the relevant improvements were integrated into the standard library's unittest in Python 2.7, and we no longer support 2.6. * user: When setting the registered version of a package to the version in an underlay database (e.g. a preinstalled version vs. one in the user-installed area), remove the overlay link rather than setting a new one equal to the underlay; this was always the intended behaviour but didn't work that way due to a typo. * Add Python 3.4 to list of tested versions. * Call setup.py from the top-level Makefile.am rather than from debian/rules, to make the build system a bit more unified. * Drop AM_GNU_GETTEXT and call intltoolize before autoreconf in autogen.sh; this fixes a bug whereby "make" after "./configure" always immediately needed to run aclocal. * Build-depend on python3-pep8 so that test_pep8_clean doesn't need to be skipped when running under Python 3. This can safely be removed for backports to precise. * Simplify click -> python3-click dependency given that both are Architecture: any. * Tighten packagekit-plugin-click -> click dependency to require a matching version. * Use dh_install --fail-missing to avoid future mistakes. * Sync up substvar use with what debhelper actually generates for us: add ${misc:Pre-Depends} to click and packagekit-plugin-click, and remove ${python3:Depends} from click-dev. * Reset SIGPIPE handling from Python's default of raising an exception to the Unix default of terminating the process (LP: #1285790). -- Ubuntu daily release Tue, 04 Mar 2014 15:23:45 +0000 click (0.4.15) trusty; urgency=medium [ Stéphane Graber ] * Set X-Auto-Uploader to no-rewrite-version * Set Vcs-Bzr to the new target branch -- Ubuntu daily release Thu, 30 Jan 2014 16:12:17 +0000 click (0.4.14) trusty; urgency=low [ Colin Watson ] * chroot: Print help if no subcommand given (LP: #1260669). * chroot: Recommend debootstrap from click-dev, and explicitly check for it in "click chroot create" (LP: #1260487). * chroot: Check for root in "create" and "destroy" (LP: #1260671). * hooks: Add a ${short-id} expansion to hook patterns; this is valid only in user-level or single-version hooks, and expands to a new "short application ID" without the version (LP: #1251635). * hooks: Strip any trailing slashes from the end of patterns, as they cause confusion with symlink-to-directory semantics and can never be useful (LP: #1253855). * install: Extend the interpretation of "framework" a little bit to allow a Click package to declare that it requires multiple frameworks. This will allow splitting up the SDK framework declarations into more fine-grained elements. * Policy version 3.9.5: no changes required. * build: Enforce only a single framework declaration for now, by request. [ Zoltan Balogh ] * Add qtmultimedia5-dev to the SDK framework list. [ Dimitri John Ledkov ] * chroot: Add "cmake" to build_pkgs, as it is expected for cmake to be available on any (Ubuntu) framework. -- Colin Watson Thu, 23 Jan 2014 17:30:54 +0000 click (0.4.13) trusty; urgency=low [ Robert Bruce Park ] * Ignore click packages when building click packages. [ Colin Watson ] * If "click build" or "click buildsource" is given a directory as the value of its -m/--manifest option, interpret that as indicating the "manifest.json" file in that directory (LP: #1251604). * Ensure correct permissions on /opt/click.ubuntu.com at boot, since a system image update may have changed clickpkg's UID/GID (LP: #1259253). -- Colin Watson Tue, 10 Dec 2013 14:33:42 +0000 click (0.4.12) trusty; urgency=low [ Colin Watson ] * Adjust top-level "click help" entry for "install" to point to pkcon. * Fix hook installation tests to test Unicode manifests properly. * Read version and date from debian/changelog when building documentation. * Declare click-dev Multi-Arch: foreign (LP: #1238796). * Build-depend on python3:any/python3-all:any rather than python3/python3-all. [ Brian Murray, Colin Watson ] * Add chroot management support. -- Colin Watson Thu, 21 Nov 2013 14:46:16 +0000 click (0.4.11) saucy; urgency=low * Drop --force-missing-framework from PackageKit plugin now that /usr/share/click/frameworks/ubuntu-sdk-13.10.framework is in ubuntu-sdk-libs. * Show a neater error message when a package's framework is not installed (LP: #1236671). * Show a neater error message when building a package whose manifest file cannot be parsed (LP: #1236669). * Show a neater error message when running "click install" with insufficient privileges (LP: #1236673). -- Colin Watson Fri, 11 Oct 2013 12:07:06 +0100 click (0.4.10) saucy; urgency=low * When removing packages, only drop privileges after ensuring the existence of the database directory (LP: #1233280). -- Colin Watson Mon, 30 Sep 2013 18:12:14 +0100 click (0.4.9) saucy; urgency=low * Explicitly build-depend on pkg-config, since it's needed even if the PackageKit/GLib-related build-dependencies are removed for backporting. * Remove some stray documentation references to Ubuntu 13.04. * Ensure that the user's overlay database directory exists when unregistering a preinstalled package (LP: #1232066). * Support packages containing code for multiple architectures, and document the "architecture" manifest field (LP: #1214380, #1214864). * Correctly pass through return values of commands as the exit status of the "click" wrapper. * Extend "click info" to take a registered package name as an alternative to a path to a Click package file (LP: #1232118). * Force unpacked files to be owner-writeable (LP: #1232128). -- Colin Watson Mon, 30 Sep 2013 15:24:49 +0100 click (0.4.8) saucy; urgency=low * Show a proper error message if "click build" or "click buildsource" is called on a directory that does not exist or does not contain a manifest file, rather than crashing (LP: #1228619). * Restore missing newlines after JSON dumps in "click info" and "click list --manifest". * Tidy up use of PackageKit IDs; local:click should refer to uninstalled packages, while installed:click refers to installed packages. * Expose application names and whether a package is removable via the PackageKit API: the IDs of installed applications are now formed as comma-separated key/value pairs, e.g. "installed:click,removable=1,app_name=foo,app_name=bar" (LP: #1209329). * Rename ClickUser.__setitem__ to ClickUser.set_version and ClickUser.__delitem__ to ClickUser.remove; with multiple databases it was impossible for these methods to fulfil the normal contract for mutable mappings, since deleting an item might simply expose an item in an underlying database. * Allow unregistering preinstalled packages. A preinstalled package cannot in general actually be removed from disk, but unregistering it for a user records it as being hidden from that user's list of registered packages. Reinstalling the same version unhides it. * Consolidate ClickInstaller.audit_control into ClickInstaller.audit. * Validate the shipped md5sums file in "click verify" (LP: #1217333). -- Colin Watson Tue, 24 Sep 2013 15:21:48 +0100 click (0.4.7) saucy; urgency=low * Run system hooks when removing a package from the file system (LP: #1227681). * If a hook symlink is already correct, don't unnecessarily remove and recreate it. * Improve "click hook install-system" and "click hook install-user" to remove any stale symlinks they find, and to run Exec commands only once per hook. This significantly speeds up system and session startup when lots of applications are installed (LP: #1227604). * Rename "click hook install-system" and "click hook install-user" to "click hook run-system" and "click hook run-user" respectively, to better fit their semantics. (I expect these commands only to have been used internally by click's own Upstart jobs.) * Filter version control metadata and editor backup files out of binary packages in "click build" (LP: #1223640). -- Colin Watson Fri, 20 Sep 2013 18:07:13 +0100 click (0.4.6) saucy; urgency=low * Make sure all unpacked files and directories are group- and world-readable, and (if owner-executable) also group- and world-executable (LP: #1226553). -- Colin Watson Tue, 17 Sep 2013 13:37:06 +0100 click (0.4.5) saucy; urgency=low * Document --force-missing-framework option in the error message produced when a package's required framework is not present. * Make "click pkgdir" exit 1 if a directory for the given package name or path is not found, rather than letting the exception propagate (LP: #1225923). * Run system hooks at boot time, in particular so that AppArmor profiles for packages in /custom are generated and loaded (LP: #1223085). -- Colin Watson Mon, 16 Sep 2013 20:55:28 +0100 click (0.4.4) saucy; urgency=low * Amend "click help install" to recommend using "pkcon install-local". * Run hooks when removing a per-user package registration. * Adjust usage lines for "click help verify" and "click help pkgdir" to indicate that options are allowed. * Add a click(1) manual page. * Use json.dump and json.load in most places rather than json.dumps and json.loads (which unnecessarily construct strings). * Add "click unregister", which unregisters a package for a user and removes it from disk if it is not being used. * Add RemovePackage support to the PackageKit plugin, mapped to "click unregister". * Attempt to remove the old version of a package after installing or registering a new one. * Remove code supporting PackageKit 0.7 API, and instead arrange to disable the PackageKit plugin if the new API is not available, since we don't need to build it on Ubuntu 12.04 LTS. * Report errors from click subprocesses in PackageKit plugin (LP: #1218483). * Implement PackageKit search by name and by details. * Reserve manifest keys starting with an underscore for use as dynamic properties of installed packages. * Add the dynamic key "_directory" to "click list --manifest" output, showing the directory where each package is unpacked (LP: #1221760). * Add the dynamic key "_removable" to "click list --manifest" output, which is 1 if a package is unpacked in a location from which it can be removed, otherwise 0. -- Colin Watson Mon, 09 Sep 2013 13:37:39 +0100 click (0.4.3) saucy; urgency=low * Add support for multiple installation root directories, configured in /etc/click/databases/. Define /usr/share/click/preinstalled, /custom/click, and /opt/click.ubuntu.com by default. * Add --all-users option to "click install" and "click register": this registers the installed package for a special pseudo-user "@all", making it visible to all users. * Add "click hook install-user", which runs all user-level hooks for all packages for a given user. This is useful at session startup to catch up with packages that may have been preinstalled and registered for all users. * Run "click hook install-user" on session startup from an Upstart user job. * Avoid calling "click desktophook" if /usr/share/click/hooks/upstart-app-launch-desktop.hook exists. * Force umask to a sane value when dropping privileges (022 for clickpkg, current-umask | 002 for other users; LP: #1215480). * Use aa-exec-click rather than aa-exec in .desktop files generated by "click desktophook" (LP: #1197047). -- Colin Watson Wed, 04 Sep 2013 17:01:58 +0100 click (0.4.2) saucy; urgency=low * Suppress dpkg calls to lchown when not running as root (LP: #1220125). -- Colin Watson Tue, 03 Sep 2013 10:12:29 +0100 click (0.4.1) saucy; urgency=low [ Sergio Schvezov ] * Compare mtimes for desktop files, not stat objects. -- Colin Watson Mon, 02 Sep 2013 14:54:49 +0100 click (0.4.0) saucy; urgency=low [ Colin Watson ] * Add "installed-size" as a mandatory field in the control area's "manifest" file; it should not be present in source manifest files, and is generated automatically by "click build". * Add an optional "icon" manifest key. * Consistently call clickpreload_init from preloaded functions in case they happen to be called before libclickpreload's constructor. * Run dpkg with --force-bad-path so that /sbin and /usr/sbin are not required to be on $PATH; we don't use the tools dpkg gets from there. [ Loïc Minier ] * Add fopen64 wrapper (LP: #1218674). -- Colin Watson Fri, 30 Aug 2013 17:59:34 +0100 click (0.3.4) saucy; urgency=low * Make "click desktophook" tolerate dangling symlinks in ~/.local/share/applications/. -- Colin Watson Wed, 28 Aug 2013 18:00:55 +0200 click (0.3.3) saucy; urgency=low * Recommend click-apparmor from click (suggested by Jamie Strandboge). -- Colin Watson Wed, 28 Aug 2013 12:17:23 +0200 click (0.3.2) saucy; urgency=low [ Jamie Strandboge ] * Document maintainer as an optional field. [ Matthias Klumpp ] * Support PackageKit 0.8 API. -- Colin Watson Tue, 27 Aug 2013 21:07:02 +0200 click (0.3.1) saucy; urgency=low [ Colin Watson ] * Fix some more failures with mock 0.7.2. * Work around the lack of a python-apt backport of apt_pkg.TagFile(sequence, bytes=True) to precise. [ Jamie Strandboge ] * Codify allowed characters for "application ID". * Fix typos in apparmor hook example. -- Colin Watson Tue, 13 Aug 2013 10:10:11 +0200 click (0.3.0) saucy; urgency=low * Insert a new "_click-binary" ar member immediately after "debian-binary"; this allows detecting the MIME type of a Click package even when it doesn't have the extension ".click" (LP: #1205346). * Declare the application/x-click MIME type, since the shared-mime-info upstream would rather not take the patch there at this point (https://bugs.freedesktop.org/show_bug.cgi?id=66689). * Make removal of old links for single-version hooks work even when the application ID is not a prefix of the pattern's basename. * Add an optional Hook-Name field to hook files, thereby allowing multiple hooks to attach to the same virtual name. * Rename click's own "desktop" hook to "click-desktop", making use of the new Hook-Name facility. -- Colin Watson Tue, 06 Aug 2013 11:08:46 +0100 click (0.2.10) saucy; urgency=low * Force click's stdout encoding to UTF-8 regardless of the locale. * Don't encode non-ASCII characters in JSON dumps. * Treat manifests as UTF-8. -- Colin Watson Tue, 30 Jul 2013 15:14:16 +0100 click (0.2.9) saucy; urgency=low * Tolerate dangling source symlinks in "click desktophook". * Handle the case where the clickpkg user cannot read the .click file, using some LD_PRELOAD trickery to allow passing it as a file descriptor opened by the privileged process (LP: #1204523). * Remove old links for single-version hooks when installing new versions (LP: #1206115). -- Colin Watson Mon, 29 Jul 2013 16:56:42 +0100 click (0.2.8) saucy; urgency=low * Check in advance whether the root is writable by the clickpkg user, not just by root, and do so in a way less vulnerable to useful exception text being eaten by a subprocess preexec_fn (LP: #1204570). * Actually install /var/lib/polkit-1/localauthority/10-vendor.d/com.ubuntu.click.pkla in the packagekit-plugin-click binary package. -- Colin Watson Thu, 25 Jul 2013 17:40:49 +0100 click (0.2.7) saucy; urgency=low * Fix error message when rejecting "_" from a package name or version (LP: #1204560). -- Colin Watson Wed, 24 Jul 2013 16:42:59 +0100 click (0.2.6) saucy; urgency=low * Adjust written .desktop files to avoid tickling some bugs in Unity 8's parsing. -- Colin Watson Wed, 24 Jul 2013 08:03:08 +0100 click (0.2.5) saucy; urgency=low * Ensure that ~/.local/share/applications exists if we need to write any .desktop files. -- Colin Watson Wed, 24 Jul 2013 07:44:44 +0100 click (0.2.4) saucy; urgency=low * Mangle Icon in .desktop files to point to an absolute path within the package unpack directory if necessary. * Add a "--" separator between aa-exec's options and the subsidiary command, per Jamie Strandboge. -- Colin Watson Tue, 23 Jul 2013 23:38:29 +0100 click (0.2.3) saucy; urgency=low * Set Path in generated .desktop files to the top-level package directory. * Revert part of geteuid() change in 0.2.2; ClickUser._drop_privileges and ClickUser._regain_privileges need to check the real UID, or else they will never regain privileges. * When running a hook, set HOME to the home directory of the user the hook is running as. -- Colin Watson Tue, 23 Jul 2013 22:57:03 +0100 click (0.2.2) saucy; urgency=low * dh_click: Support --name option. * Avoid ClickUser.__iter__ infecting its caller with dropped privileges. * Use geteuid() rather than getuid() in several places to check whether we need to drop or regain privileges. * Add a user-level hook to create .desktop files in ~/.local/share/applications/. (This should probably move to some other package at some point.) -- Colin Watson Tue, 23 Jul 2013 19:36:44 +0100 click (0.2.1) saucy; urgency=low * Fix "click help list". * Remove HOME from environment when running dpkg, so that it doesn't try to read .dpkg.cfg from it (which may fail when dropping privileges from root and produce a warning message). * Refuse to install .click directories at any level, not just the top. * Add "click pkgdir" command to print the top-level package directory from either a package name or a path within a package; based on work by Ted Gould, for which thanks. -- Colin Watson Mon, 22 Jul 2013 09:36:19 +0100 click (0.2.0) saucy; urgency=low * Revise and implement hooks specification. While many things have changed, the previous version was never fully implemented. However, I have incremented the default Click-Version value to 0.2 to reflect the design work. - The "hooks" manifest key now contains a dictionary keyed by application name. This means manifest authors have to repeat themselves much less in common cases. - There is now an explicit distinction between system-level and user-level hooks. System-level hooks may reflect multiple concurrent versions, and require a user name. - Hook symlinks are now named by a combination of the Click package name, the application name, and the Click package version. - The syntax of Pattern has changed to make it easier to extend with new substitutions. * Reject '_' and '/' characters in all of package name, application name, and package version. -- Colin Watson Fri, 19 Jul 2013 13:11:31 +0100 click (0.1.7) saucy; urgency=low * Correct name of .pkla file (now /var/lib/polkit-1/localauthority/10-vendor.d/com.ubuntu.click.pkla). -- Colin Watson Thu, 18 Jul 2013 17:00:46 +0100 click (0.1.6) saucy; urgency=low * Move defaults for frameworks and hooks directories to click.paths. * Install /var/lib/polkit-1/localauthority/10-vendor.d/10-click.pkla to allow the phablet user to install Click packages without being known to logind, as a temporary workaround. -- Colin Watson Thu, 18 Jul 2013 16:55:08 +0100 click (0.1.5) saucy; urgency=low * Fix infinite recursion in ClickUser.click_pw. * When all the files requested for installation are Click packages, override org.freedesktop.packagekit.package-install* PolicyKit actions to com.ubuntu.click.package-install, defined with a more open default policy. (This requires some backports to PackageKit, not in the archive yet.) -- Colin Watson Wed, 17 Jul 2013 15:46:48 +0100 click (0.1.4) saucy; urgency=low * Add support for per-user package registration. * Move install log file from $root/.click.log to $root/.click/log. * Add an autotools-based build system for our C components. * Initial version of a PackageKit plugin, in a new packagekit-plugin-click package; still experimental. * Restore compatibility with Python 3.2 (LP: #1200670). * Adjust tests to pass with mock 0.7.2 (as in Ubuntu 12.04 LTS). * Make the default root directory a configure option. * Add a simple "click list" command. -- Colin Watson Mon, 15 Jul 2013 15:55:48 +0100 click (0.1.3) saucy; urgency=low * Rename to click, per Mark Shuttleworth. -- Colin Watson Thu, 27 Jun 2013 15:57:25 +0100 click-package (0.1.2) saucy; urgency=low * Disable dh_sphinxdoc for builds that are not building architecture-independent packages. -- Colin Watson Tue, 25 Jun 2013 18:57:47 +0100 click-package (0.1.1) saucy; urgency=low * clickpackage.tests.test_install: Set NO_PKG_MANGLE when building fake packages, to avoid having Maintainer fields mangled on the buildds. -- Colin Watson Tue, 25 Jun 2013 17:32:00 +0100 click-package (0.1) saucy; urgency=low * Initial release. -- Colin Watson Mon, 24 Jun 2013 14:43:21 +0100 click-0.4.21.1ubuntu0.2/debian/click.postinst0000664000000000000000000000052412320742124015507 0ustar #! /bin/sh set -e if [ "$1" = configure ]; then if ! getent passwd clickpkg >/dev/null; then adduser --quiet --system --home /nonexistent --no-create-home \ --disabled-password --shell /bin/false --group \ clickpkg fi mkdir -p -m 755 /opt/click.ubuntu.com chown clickpkg:clickpkg /opt/click.ubuntu.com fi #DEBHELPER# exit 0 click-0.4.21.1ubuntu0.2/debian/click.manpages0000664000000000000000000000002712320742124015415 0ustar doc/_build/man/click.1 click-0.4.21.1ubuntu0.2/debian/packagekit-plugin-click.install0000664000000000000000000000015112320742124020663 0ustar usr/lib/*/packagekit-plugins/*.so usr/share/polkit-1/actions var/lib/polkit-1/localauthority/10-vendor.d click-0.4.21.1ubuntu0.2/debian/click.sharedmimeinfo0000664000000000000000000000113112320742124016611 0ustar Click package click-0.4.21.1ubuntu0.2/debian/click.install0000664000000000000000000000013712320742124015272 0ustar etc/click etc/init lib/systemd usr/bin/click usr/lib/*/click usr/lib/systemd usr/share/upstart click-0.4.21.1ubuntu0.2/debian/source/0000775000000000000000000000000012320742335014120 5ustar click-0.4.21.1ubuntu0.2/debian/source/format0000664000000000000000000000001512320742124015323 0ustar 3.0 (native) click-0.4.21.1ubuntu0.2/debian/rules0000775000000000000000000000323312320742124013675 0ustar #! /usr/bin/make -f PACKAGEKIT := $(shell debian/packagekit-check) ifeq ($(PACKAGEKIT),yes) EXTRA_DH_OPTIONS := else 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,gir,python3,sphinxdoc \ --buildsystem autoconf $(EXTRA_DH_OPTIONS) PY3REQUESTED := $(shell py3versions -r) PY3DEFAULT := $(shell py3versions -d) # Run setup.py with the default python3 last so that the scripts use # #!/usr/bin/python3 and not #!/usr/bin/python3.X. PY3 := $(filter-out $(PY3DEFAULT),$(PY3REQUESTED)) python3 confflags := \ --with-python-interpreters='$(PY3)' \ --with-systemdsystemunitdir=/lib/systemd/system \ --with-systemduserunitdir=/usr/lib/systemd/user ifeq ($(PACKAGEKIT),no) confflags += --disable-packagekit endif override_dh_autoreconf: dh_autoreconf -- ./autogen.sh override_dh_auto_configure: dh_auto_configure -- $(confflags) override_dh_auto_build: dh_auto_build $(MAKE) -C doc html man override_dh_auto_clean: dh_auto_clean $(MAKE) -C doc clean PYTHON_INSTALL_FLAGS := \ --force --root=$(CURDIR)/debian/tmp \ --no-compile -O0 --install-layout=deb override_dh_auto_install: dh_auto_install -- PYTHON_INSTALL_FLAGS='$(PYTHON_INSTALL_FLAGS)' rm -f debian/tmp/usr/lib/*/click/*.la override_dh_install: dh_install -X .la --fail-missing DH_AUTOSCRIPTDIR=debhelper debhelper/dh_click --name=click-desktop # Sphinx documentation is architecture-independent. override_dh_sphinxdoc-arch: override_dh_makeshlibs: dh_makeshlibs -- -c4 click-0.4.21.1ubuntu0.2/debian/click.click-desktop.click-hook0000664000000000000000000000027412320742124020404 0ustar User-Level: yes Pattern: ${home}/.local/share/click/hooks/desktop/${id}.desktop Exec: [ -e /usr/share/click/hooks/upstart-app-launch-desktop.hook ] || click desktophook Hook-Name: desktop click-0.4.21.1ubuntu0.2/debian/gir1.2-click-0.4.install0000664000000000000000000000004312320742124016565 0ustar usr/lib/*/girepository-1.0 usr/lib click-0.4.21.1ubuntu0.2/debian/click.postrm0000664000000000000000000000017312320742124015150 0ustar #! /bin/sh set -e if [ "$1" = purge ]; then deluser --quiet --system clickpkg >/dev/null || true fi #DEBHELPER# exit 0 click-0.4.21.1ubuntu0.2/debian/compat0000664000000000000000000000000212320742124014012 0ustar 9 click-0.4.21.1ubuntu0.2/debian/libclick-0.4-0.install0000664000000000000000000000003112320742124016406 0ustar usr/lib/*/libclick*.so.* click-0.4.21.1ubuntu0.2/debian/python3-click.install0000664000000000000000000000002012320742124016663 0ustar usr/lib/python3 click-0.4.21.1ubuntu0.2/debian/click-dev.install0000664000000000000000000000014512320742124016045 0ustar etc/schroot/click usr/bin/dh_click usr/share/debhelper usr/share/man/man1/dh_click.1 usr/share/perl5 click-0.4.21.1ubuntu0.2/get-version0000775000000000000000000000012712320742124013562 0ustar #! /bin/sh perl -e '$_ = <>; chomp; s/.*\((.*)\).*/\1/; print; exit' $@ endif click-0.4.21.1ubuntu0.2/init/systemd/click-system-hooks.service.in0000664000000000000000000000032112320742124021540 0ustar [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 click-0.4.21.1ubuntu0.2/init/systemd/click-user-hooks.service.in0000664000000000000000000000031212320742124021172 0ustar [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 click-0.4.21.1ubuntu0.2/init/Makefile.am0000664000000000000000000000003212320742124014364 0ustar SUBDIRS = systemd upstart click-0.4.21.1ubuntu0.2/init/upstart/0000775000000000000000000000000012320742335014043 5ustar click-0.4.21.1ubuntu0.2/init/upstart/Makefile.am0000664000000000000000000000053112320742124016072 0ustar 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" $< > $@ click-0.4.21.1ubuntu0.2/init/upstart/click-system-hooks.conf.in0000664000000000000000000000022712320742124021044 0ustar description "Run Click system-level hooks" author "Colin Watson " start on filesystem task exec @bindir@/click hook run-system click-0.4.21.1ubuntu0.2/init/upstart/click-user-hooks.conf.in0000664000000000000000000000026012320742132020472 0ustar description "Run Click user-level hooks" author "Colin Watson " start on starting xsession-init and started dbus task exec @bindir@/click hook run-user click-0.4.21.1ubuntu0.2/Makefile.am0000664000000000000000000000150112320742124013423 0ustar SUBDIRS = lib preload click conf debhelper init po schroot if PACKAGEKIT SUBDIRS += pk-plugin endif ACLOCAL_AMFLAGS = -I m4 all-local: set -e; for python in $(PYTHON_INTERPRETERS); do \ $$python setup.py build; \ done check-local: set -e; for python in $(PYTHON_INTERPRETERS); do \ $$python setup.py test; \ done # setuptools likes to leave some debris around, which confuses things. install-exec-hook: find build -name \*.pyc -print0 | xargs -0r rm -f find build -name __pycache__ -print0 | xargs -0r rm -rf find build -name \*.egg-info -print0 | xargs -0r rm -rf set -e; for python in $(PYTHON_INTERPRETERS); do \ $$python setup.py install $(PYTHON_INSTALL_FLAGS); \ done clean-local: rm -rf build *.egg-info .tox find -name \*.pyc -print0 | xargs -0r rm -f find -name __pycache__ -print0 | xargs -0r rm -rf click-0.4.21.1ubuntu0.2/po/0000775000000000000000000000000012320742335012014 5ustar click-0.4.21.1ubuntu0.2/po/Makevars0000664000000000000000000000344112320742124013506 0ustar # Makefile variables for PO directory in any package using GNU gettext. # Usually the message domain is the same as the package name. DOMAIN = $(PACKAGE) # These two variables depend on the location of this directory. subdir = po top_builddir = .. # These options get passed to xgettext. XGETTEXT_OPTIONS = --keyword=_ --keyword=N_ # This is the copyright holder that gets inserted into the header of the # $(DOMAIN).pot file. Set this to the copyright holder of the surrounding # package. (Note that the msgstr strings, extracted from the package's # sources, belong to the copyright holder of the package.) Translators are # expected to transfer the copyright for their translations to this person # or entity, or to disclaim their copyright. The empty string stands for # the public domain; in this case the translators are expected to disclaim # their copyright. COPYRIGHT_HOLDER = Canonical Ltd. # This is the email address or URL to which the translators shall report # bugs in the untranslated strings: # - Strings which are not entire sentences, see the maintainer guidelines # in the GNU gettext documentation, section 'Preparing Strings'. # - Strings which use unclear terms or require additional context to be # understood. # - Strings which make invalid assumptions about notation of date, time or # money. # - Pluralisation problems. # - Incorrect English spelling. # - Incorrect formatting. # It can be your email address, or a mailing list address where translators # can write to without being subscribed, or the URL of a web page through # which the translators can contact you. MSGID_BUGS_ADDRESS = Colin Watson # This is the list of locale categories, beyond LC_MESSAGES, for which the # message catalogs shall be used. It is usually empty. EXTRA_LOCALE_CATEGORIES = click-0.4.21.1ubuntu0.2/po/POTFILES.in0000664000000000000000000000004512320742124013564 0ustar pk-plugin/com.ubuntu.click.policy.in click-0.4.21.1ubuntu0.2/po/click.pot0000664000000000000000000000175412320742124013630 0ustar # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: Colin Watson \n" "POT-Creation-Date: 2013-09-08 02:17+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../pk-plugin/com.ubuntu.click.policy.in.h:1 msgid "Install package" msgstr "" #: ../pk-plugin/com.ubuntu.click.policy.in.h:2 msgid "To install software, you need to authenticate." msgstr "" #: ../pk-plugin/com.ubuntu.click.policy.in.h:3 msgid "Remove package" msgstr "" #: ../pk-plugin/com.ubuntu.click.policy.in.h:4 msgid "To remove software, you need to authenticate." msgstr "" click-0.4.21.1ubuntu0.2/autogen.sh0000775000000000000000000000033012320742124013367 0ustar #! /bin/sh set -e intltoolize --copy --force --automake autoreconf -fi # We want to keep po/click.pot in the source package. sed -i '/rm .*\$(GETTEXT_PACKAGE)\.pot/s/ \$(GETTEXT_PACKAGE)\.pot//' \ po/Makefile.in.in click-0.4.21.1ubuntu0.2/click/0000775000000000000000000000000012607737750012500 5ustar click-0.4.21.1ubuntu0.2/click/__init__.py0000664000000000000000000000000012320742124014556 0ustar click-0.4.21.1ubuntu0.2/click/chroot.py0000664000000000000000000003625412320742132014340 0ustar # Copyright (C) 2013 Canonical Ltd. # Authors: Colin Watson , # Brian Murray # # 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 . """Chroot management for building Click packages.""" from __future__ import print_function __metaclass__ = type __all__ = [ "ClickChroot", "ClickChrootException", ] import os import pwd import re import shutil import stat import subprocess import sys framework_base = { "ubuntu-sdk-13.10": "ubuntu-sdk-13.10", "ubuntu-sdk-14.04-html-dev1": "ubuntu-sdk-14.04", "ubuntu-sdk-14.04-papi-dev1": "ubuntu-sdk-14.04", "ubuntu-sdk-14.04-qml-dev1": "ubuntu-sdk-14.04", } framework_series = { "ubuntu-sdk-13.10": "saucy", "ubuntu-sdk-14.04": "trusty", } # Please keep the lists of package names sorted. extra_packages = { "ubuntu-sdk-13.10": [ "libqt5opengl5-dev:TARGET", "libqt5svg5-dev:TARGET", "libqt5v8-5-dev:TARGET", "libqt5webkit5-dev:TARGET", "libqt5xmlpatterns5-dev:TARGET", "qt3d5-dev:TARGET", "qt5-default:TARGET", "qt5-qmake:TARGET", "qtbase5-dev:TARGET", "qtdeclarative5-dev:TARGET", "qtmultimedia5-dev:TARGET", "qtquick1-5-dev:TARGET", "qtscript5-dev:TARGET", "qtsensors5-dev:TARGET", "qttools5-dev:TARGET", ], "ubuntu-sdk-14.04": [ "cmake", "libqt5svg5-dev:TARGET", "libqt5webkit5-dev:TARGET", "libqt5xmlpatterns5-dev:TARGET", "qt3d5-dev:TARGET", "qt5-default:TARGET", "qtbase5-dev:TARGET", "qtdeclarative5-dev:TARGET", "qtdeclarative5-dev-tools", "qtlocation5-dev:TARGET", "qtmultimedia5-dev:TARGET", "qtscript5-dev:TARGET", "qtsensors5-dev:TARGET", "qttools5-dev:TARGET", "qttools5-dev-tools:TARGET", ], } primary_arches = ["amd64", "i386"] non_meta_re = re.compile(r'^[a-zA-Z0-9+,./:=@_-]+$') def shell_escape(command): escaped = [] for arg in command: if non_meta_re.match(arg): escaped.append(arg) else: escaped.append("'%s'" % arg.replace("'", "'\\''")) return " ".join(escaped) class ClickChrootException(Exception): pass class ClickChroot: def __init__(self, target_arch, framework, name=None, series=None, session=None): self.target_arch = target_arch self.framework = framework if name is None: name = "click" self.name = name if series is None: series = framework_series[self.framework_base] self.series = series self.session = session self.native_arch = subprocess.check_output( ["dpkg", "--print-architecture"], universal_newlines=True).strip() self.chroots_dir = "/var/lib/schroot/chroots" # this doesn't work because we are running this under sudo if 'DEBOOTSTRAP_MIRROR' in os.environ: self.archive = os.environ['DEBOOTSTRAP_MIRROR'] else: self.archive = "http://archive.ubuntu.com/ubuntu" if "SUDO_USER" in os.environ: self.user = os.environ["SUDO_USER"] elif "PKEXEC_UID" in os.environ: self.user = pwd.getpwuid(int(os.environ["PKEXEC_UID"])).pw_name else: self.user = pwd.getpwuid(os.getuid()).pw_name self.dpkg_architecture = self._dpkg_architecture() def _dpkg_architecture(self): dpkg_architecture = {} command = ["dpkg-architecture", "-a%s" % self.target_arch] env = dict(os.environ) env["CC"] = "true" lines = subprocess.check_output( command, env=env, universal_newlines=True).splitlines() for line in lines: try: key, value = line.split("=", 1) except ValueError: continue dpkg_architecture[key] = value return dpkg_architecture def _generate_sources(self, series, native_arch, target_arch, components): ports_mirror = "http://ports.ubuntu.com/ubuntu-ports" pockets = ['%s' % series] for pocket in ['updates', 'security']: pockets.append('%s-%s' % (series, pocket)) sources = [] if target_arch not in primary_arches: for pocket in pockets: sources.append("deb [arch=%s] %s %s %s" % (target_arch, ports_mirror, pocket, components)) sources.append("deb-src %s %s %s" % (ports_mirror, pocket, components)) if native_arch in primary_arches: for pocket in pockets: sources.append("deb [arch=%s] %s %s %s" % (native_arch, self.archive, pocket, components)) sources.append("deb-src %s %s %s" % (self.archive, pocket, components)) return sources @property def framework_base(self): if self.framework in framework_base: return framework_base[self.framework] else: return self.framework @property def full_name(self): return "%s-%s-%s" % (self.name, self.framework_base, self.target_arch) @property def full_session_name(self): return "%s-%s" % (self.full_name, self.session) def exists(self): command = ["schroot", "-c", self.full_name, "-i"] with open("/dev/null", "w") as devnull: return subprocess.call( command, stdout=devnull, stderr=devnull) == 0 def _make_executable(self, path): mode = stat.S_IMODE(os.stat(path).st_mode) os.chmod(path, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) def create(self): if self.exists(): raise ClickChrootException( "Chroot %s already exists" % self.full_name) components = ["main", "restricted", "universe", "multiverse"] mount = "%s/%s" % (self.chroots_dir, self.full_name) proxy = None if not proxy and "http_proxy" in os.environ: proxy = os.environ["http_proxy"] if not proxy: proxy = subprocess.check_output( 'unset x; eval "$(apt-config shell x Acquire::HTTP::Proxy)"; echo "$x"', shell=True, universal_newlines=True).strip() target_tuple = self.dpkg_architecture["DEB_HOST_GNU_TYPE"] build_pkgs = [ "build-essential", "fakeroot", "apt-utils", "g++-%s" % target_tuple, "pkg-config-%s" % target_tuple, "cmake", "dpkg-cross", "libc-dev:%s" % self.target_arch ] for package in extra_packages.get(self.framework_base, []): package = package.replace(":TARGET", ":%s" % self.target_arch) build_pkgs.append(package) os.makedirs(mount) subprocess.check_call([ "debootstrap", "--arch", self.native_arch, "--variant=buildd", "--components=%s" % ','.join(components), self.series, mount, self.archive ]) sources = self._generate_sources(self.series, self.native_arch, self.target_arch, ' '.join(components)) with open("%s/etc/apt/sources.list" % mount, "w") as sources_list: for line in sources: print(line, file=sources_list) shutil.copy2("/etc/localtime", "%s/etc/" % mount) shutil.copy2("/etc/timezone", "%s/etc/" % mount) chroot_config = "/etc/schroot/chroot.d/%s" % self.full_name with open(chroot_config, "w") as target: admin_user = "root" print("[%s]" % self.full_name, file=target) print("description=Build chroot for click packages on %s" % self.target_arch, file=target) for key in ("users", "root-users", "source-root-users"): print("%s=%s,%s" % (key, admin_user, self.user), file=target) print("type=directory", file=target) print("profile=default", file=target) print("setup.fstab=click/fstab", file=target) print("# Not protocols or services see ", file=target) print("# debian bug 557730", file=target) print("setup.nssdatabases=sbuild/nssdatabases", file=target) print("union-type=overlayfs", file=target) print("directory=%s" % mount, file=target) daemon_policy = "%s/usr/sbin/policy-rc.d" % mount with open(daemon_policy, "w") as policy: print("#!/bin/sh", file=policy) print("while true; do", file=policy) print(' case "$1" in', file=policy) print(" -*) shift ;;", file=policy) print(" makedev) exit 0;;", file=policy) print(" x11-common) exit 0;;", file=policy) print(" *) exit 101;;", file=policy) print(" esac", file=policy) print("done", file=policy) self._make_executable(daemon_policy) os.remove("%s/sbin/initctl" % mount) os.symlink("%s/bin/true" % mount, "%s/sbin/initctl" % mount) finish_script = "%s/finish.sh" % mount with open(finish_script, 'w') as finish: print("#!/bin/bash", file=finish) print("set -e", file=finish) if proxy: print("mkdir -p /etc/apt/apt.conf.d", file=finish) print("cat > /etc/apt/apt.conf.d/99-click-chroot-proxy < # 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 . """Helper functions to turn json-glib objects into Python objects.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'ClickJsonError', 'json_array_to_python', 'json_node_to_python', 'json_object_to_python', ] from gi.repository import Json class ClickJsonError(Exception): pass def json_array_to_python(array): return [json_node_to_python(element) for element in array.get_elements()] def json_object_to_python(obj): ret = {} for name in obj.get_members(): ret[name] = json_node_to_python(obj.get_member(name)) return ret def json_node_to_python(node): node_type = node.get_node_type() if node_type == Json.NodeType.ARRAY: return json_array_to_python(node.get_array()) elif node_type == Json.NodeType.OBJECT: return json_object_to_python(node.get_object()) elif node_type == Json.NodeType.NULL: return None elif node_type == Json.NodeType.VALUE: return node.get_value() else: raise ClickJsonError( "Unknown JSON node type \"%s\"" % node_type.value_nick) click-0.4.21.1ubuntu0.2/click/Makefile.am0000664000000000000000000000052212320742124014512 0ustar SUBDIRS = tests noinst_SCRIPTS = paths.py CLEANFILES = $(noinst_SCRIPTS) do_subst = sed \ -e 's,[@]sysconfdir[@],$(sysconfdir),g' \ -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' \ -e 's,[@]pkglibdir[@],$(pkglibdir),g' \ -e 's,[@]DEFAULT_ROOT[@],$(DEFAULT_ROOT),g' paths.py: paths.py.in Makefile $(do_subst) < $(srcdir)/paths.py.in > $@ click-0.4.21.1ubuntu0.2/click/install.py0000664000000000000000000004170212607737665014531 0ustar # 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 . """Installing Click packages.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'ClickInstaller', 'ClickInstallerAuditError', 'ClickInstallerError', 'ClickInstallerPermissionDenied', ] from functools import partial import grp import inspect import json import os import pwd import shutil import stat import subprocess import sys import tempfile from contextlib import closing import apt_pkg from debian.debfile import DebFile as _DebFile from debian.debian_support import Version from gi.repository import Click from click.paths import preload_path from click.preinst import static_preinst_matches from click.versions import spec_version try: _DebFile.close DebFile = _DebFile except AttributeError: # Yay! The Ubuntu 13.04 version of python-debian 0.1.21 # debian.debfile.DebFile has a .close() method but the PyPI version of # 0.1.21 does not. It's worse than that because DebFile.close() really # delegates to DebPart.close() and *that's* missing in the PyPI version. # To get that working, we have to reach inside the object and name mangle # the attribute. class DebFile(_DebFile): def close(self): self.control._DebPart__member.close() self.data._DebPart__member.close() apt_pkg.init_system() class ClickInstallerError(Exception): pass class ClickInstallerPermissionDenied(ClickInstallerError): pass class ClickInstallerAuditError(ClickInstallerError): pass class ClickInstaller: def __init__(self, db, force_missing_framework=False): self.db = db self.force_missing_framework = force_missing_framework def _preload_path(self): if "CLICK_PACKAGE_PRELOAD" in os.environ: return os.environ["CLICK_PACKAGE_PRELOAD"] my_path = inspect.getsourcefile(ClickInstaller) preload = os.path.join( os.path.dirname(my_path), os.pardir, "preload", ".libs", "libclickpreload.so") if os.path.exists(preload): return os.path.abspath(preload) return preload_path def _dpkg_architecture(self): return subprocess.check_output( ["dpkg", "--print-architecture"], universal_newlines=True).rstrip("\n") def extract(self, path, target): command = ["dpkg-deb", "-R", path, target] with open(path, "rb") as fd: env = dict(os.environ) preloads = [self._preload_path()] if "LD_PRELOAD" in env: preloads.append(env["LD_PRELOAD"]) env["LD_PRELOAD"] = " ".join(preloads) env["CLICK_BASE_DIR"] = target env["CLICK_PACKAGE_PATH"] = path env["CLICK_PACKAGE_FD"] = str(fd.fileno()) env.pop("HOME", None) kwargs = {} if sys.version >= "3.2": kwargs["pass_fds"] = (fd.fileno(),) subprocess.check_call(command, env=env, **kwargs) def audit(self, path, slow=False, check_arch=False): with closing(DebFile(filename=path)) as package: control_fields = package.control.debcontrol() try: click_version = Version(control_fields["Click-Version"]) except KeyError: raise ClickInstallerAuditError("No Click-Version field") if click_version > spec_version: raise ClickInstallerAuditError( "Click-Version: %s newer than maximum supported version " "%s" % (click_version, spec_version)) for field in ( "Pre-Depends", "Depends", "Recommends", "Suggests", "Enhances", "Conflicts", "Breaks", "Provides", ): if field in control_fields: raise ClickInstallerAuditError( "%s field is forbidden in Click packages" % field) scripts = package.control.scripts() if ("preinst" in scripts and static_preinst_matches(scripts["preinst"])): scripts.pop("preinst", None) if scripts: raise ClickInstallerAuditError( "Maintainer scripts are forbidden in Click packages " "(found: %s)" % " ".join(sorted(scripts))) if not package.control.has_file("manifest"): raise ClickInstallerAuditError("Package has no manifest") with package.control.get_file("manifest", encoding="UTF-8") as f: manifest = json.load(f) try: package_name = manifest["name"] except KeyError: raise ClickInstallerAuditError('No "name" entry in manifest') # TODO: perhaps just do full name validation? if "/" in package_name: raise ClickInstallerAuditError( 'Invalid character "/" in "name" entry: %s' % package_name) if "_" in package_name: raise ClickInstallerAuditError( 'Invalid character "_" in "name" entry: %s' % package_name) try: package_version = manifest["version"] except KeyError: raise ClickInstallerAuditError( 'No "version" entry in manifest') # TODO: perhaps just do full version validation? if "/" in package_version: raise ClickInstallerAuditError( 'Invalid character "/" in "version" entry: %s' % package_version) if "_" in package_version: raise ClickInstallerAuditError( 'Invalid character "_" in "version" entry: %s' % package_version) try: framework = manifest["framework"] except KeyError: raise ClickInstallerAuditError( 'No "framework" entry in manifest') try: parsed_framework = apt_pkg.parse_depends(framework) except ValueError: raise ClickInstallerAuditError( 'Could not parse framework "%s"' % framework) for or_dep in parsed_framework: if len(or_dep) > 1: raise ClickInstallerAuditError( 'Alternative dependencies in framework "%s" not yet ' 'allowed' % framework) if or_dep[0][1] or or_dep[0][2]: raise ClickInstallerAuditError( 'Version relationship in framework "%s" not yet ' 'allowed' % framework) if not self.force_missing_framework: missing_frameworks = [] for or_dep in parsed_framework: if not Click.Framework.has_framework(or_dep[0][0]): missing_frameworks.append(or_dep[0][0]) if len(missing_frameworks) > 1: raise ClickInstallerAuditError( 'Frameworks %s not present on system (use ' '--force-missing-framework option to override)' % ", ".join('"%s"' % f for f in missing_frameworks)) elif missing_frameworks: raise ClickInstallerAuditError( 'Framework "%s" not present on system (use ' '--force-missing-framework option to override)' % missing_frameworks[0]) if check_arch: architecture = manifest.get("architecture", "all") if architecture != "all": dpkg_architecture = self._dpkg_architecture() if isinstance(architecture, list): if dpkg_architecture not in architecture: raise ClickInstallerAuditError( 'Package architectures "%s" not compatible ' 'with system architecture "%s"' % (" ".join(architecture), dpkg_architecture)) elif architecture != dpkg_architecture: raise ClickInstallerAuditError( 'Package architecture "%s" not compatible ' 'with system architecture "%s"' % (architecture, dpkg_architecture)) # This isn't ideally quick, since it has to decompress the data # part of the package, but dpkg's path filtering code assumes # that all paths start with "./" so we must check it before # passing the package to dpkg. for data_name in package.data: if data_name != "." and not data_name.startswith("./"): raise ClickInstallerAuditError( 'File name "%s" in package does not start with "./"' % data_name) if slow: temp_dir = tempfile.mkdtemp(prefix="click") try: self.extract(path, temp_dir) command = [ "md5sum", "-c", "--quiet", os.path.join("DEBIAN", "md5sums"), ] subprocess.check_call(command, cwd=temp_dir) finally: shutil.rmtree(temp_dir) return package_name, package_version 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.umask(0o022) def _euid_access(self, username, path, mode): """Like os.access, but for the effective UID.""" # TODO: Dropping privileges and calling # os.access(effective_ids=True) ought to work, but for some reason # appears not to return False when it should. It seems that we need # a subprocess to check this reliably. At least we don't have to # exec anything. pid = os.fork() if pid == 0: # child self._drop_privileges(username) os._exit(0 if os.access(path, mode) else 1) else: # parent _, status = os.waitpid(pid, 0) return status == 0 def _check_write_permissions(self, path): while True: if os.path.exists(path): break path = os.path.dirname(path) if path == "/": break if not self._euid_access("clickpkg", path, os.W_OK): raise ClickInstallerPermissionDenied( 'Cannot acquire permission to write to %s; either run as root ' 'with --user, or use "pkcon install-local" instead' % path) def _install_preexec(self, inst_dir): self._drop_privileges("clickpkg") admin_dir = os.path.join(inst_dir, ".click") if not os.path.exists(admin_dir): os.makedirs(admin_dir) with open(os.path.join(admin_dir, "available"), "w"): pass with open(os.path.join(admin_dir, "status"), "w"): pass os.mkdir(os.path.join(admin_dir, "info")) os.mkdir(os.path.join(admin_dir, "updates")) os.mkdir(os.path.join(admin_dir, "triggers")) def _unpack(self, path, user=None, all_users=False): package_name, package_version = self.audit(path, check_arch=True) # Is this package already unpacked in an underlay (non-topmost) # database? if self.db.has_package_version(package_name, package_version): overlay = self.db.get(self.db.props.size - 1) if not overlay.has_package_version(package_name, package_version): return package_name, package_version, None 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.props.overlay) 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: pw = pwd.getpwnam("clickpkg") os.chown(root_click, pw.pw_uid, pw.pw_gid) # TODO: sandbox so that this can only write to the unpack directory command = [ "dpkg", # We normally run dpkg as non-root. "--force-not-root", # /sbin and /usr/sbin may not necessarily be on $PATH; we don't # use the tools dpkg gets from there. "--force-bad-path", # We check the package architecture ourselves in audit(). "--force-architecture", "--instdir", inst_dir, "--admindir", os.path.join(inst_dir, ".click"), "--path-exclude", "*/.click/*", "--log", os.path.join(root_click, "log"), "--no-triggers", "--install", path, ] with open(path, "rb") as fd: env = dict(os.environ) preloads = [self._preload_path()] if "LD_PRELOAD" in env: preloads.append(env["LD_PRELOAD"]) env["LD_PRELOAD"] = " ".join(preloads) env["CLICK_BASE_DIR"] = self.db.props.overlay env["CLICK_PACKAGE_PATH"] = path env["CLICK_PACKAGE_FD"] = str(fd.fileno()) env.pop("HOME", None) kwargs = {} if sys.version >= "3.2": kwargs["pass_fds"] = (fd.fileno(),) subprocess.check_call( command, preexec_fn=partial(self._install_preexec, inst_dir), env=env, **kwargs) for dirpath, dirnames, filenames in os.walk(inst_dir): for entry in dirnames + filenames: entry_path = os.path.join(dirpath, entry) entry_mode = os.stat(entry_path).st_mode new_entry_mode = entry_mode | stat.S_IRGRP | stat.S_IROTH if entry_mode & stat.S_IXUSR: new_entry_mode |= stat.S_IXGRP | stat.S_IXOTH if new_entry_mode != entry_mode: try: os.chmod(entry_path, new_entry_mode) except OSError: pass current_path = os.path.join(package_dir, "current") if os.path.islink(current_path): old_version = os.readlink(current_path) if "/" in old_version: old_version = None else: old_version = None Click.package_install_hooks( self.db, package_name, old_version, package_version, user_name=None) new_path = os.path.join(package_dir, "current.new") 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. # http://bugs.python.org/issue18108 pw = pwd.getpwnam("clickpkg") os.chown(new_path, pw.pw_uid, pw.pw_gid, follow_symlinks=False) os.rename(new_path, current_path) return package_name, package_version, old_version def install(self, path, user=None, all_users=False): package_name, package_version, old_version = self._unpack( path, user=user, all_users=all_users) if user is not None or 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: self.db.maybe_remove(package_name, old_version) click-0.4.21.1ubuntu0.2/click/versions.py0000664000000000000000000000132312320742124014700 0ustar # 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 versioning.""" spec_version = "0.4" click-0.4.21.1ubuntu0.2/click/arfile.py0000664000000000000000000000634612320742124014304 0ustar # 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 . """Basic support for writing ar archive files. We do things this way so that Click packages can be created with minimal dependencies (e.g. on non-Ubuntu systems). No read support is needed, since Click packages are always installed on systems that have dpkg. Some method names and general approach come from the tarfile module in Python's standard library; details of the format come from dpkg. """ from __future__ import print_function __metaclass__ = type __all__ = [ 'ArFile', ] import os import shutil import time class ArFile: def __init__(self, name=None, mode="w", fileobj=None): if mode != "w": raise ValueError("only mode 'w' is supported") self.mode = mode self.real_mode = "wb" if fileobj: if name is None and hasattr(fileobj, "name"): name = fileobj.name if hasattr(fileobj, "mode"): if fileobj.mode != "wb": raise ValueError("fileobj must be opened with mode='wb'") self._mode = fileobj.mode self.opened_fileobj = False else: fileobj = open(name, self.real_mode) self.opened_fileobj = True self.name = name self.fileobj = fileobj self.closed = False def close(self): if self.opened_fileobj: self.fileobj.close() self.closed = True def _check(self): if self.closed: raise IOError("ArFile %s is closed" % self.name) def __enter__(self): self._check() return self def __exit__(self, *args): self.close() def add_magic(self): self.fileobj.write(b"!\n") def add_header(self, name, size): if len(name) > 15: raise ValueError("ar member name '%s' length too long" % name) if size > 9999999999: raise ValueError("ar member size %d too large" % size) header = ("%-16s%-12u0 0 100644 %-10d`\n" % ( name, int(time.time()), size)).encode() assert len(header) == 60 # sizeof(struct ar_hdr) self.fileobj.write(header) def add_data(self, name, data): size = len(data) self.add_header(name, size) self.fileobj.write(data) if size & 1: self.fileobj.write(b"\n") # padding def add_file(self, name, path): with open(path, "rb") as fobj: size = os.fstat(fobj.fileno()).st_size self.add_header(name, size) shutil.copyfileobj(fobj, self.fileobj) if size & 1: self.fileobj.write(b"\n") # padding click-0.4.21.1ubuntu0.2/click/commands/0000775000000000000000000000000012320742335014264 5ustar click-0.4.21.1ubuntu0.2/click/commands/info.py0000664000000000000000000000457512320742132015577 0ustar # 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 . """Show manifest information for a Click package.""" from __future__ import print_function from contextlib import closing import json from optparse import OptionParser import sys from gi.repository import Click from click.install import DebFile from click.json_helpers import json_object_to_python def get_manifest(options, arg): if "/" not in arg: db = Click.DB() db.read(db_dir=None) 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): return json_object_to_python(registry.get_manifest(arg)) with closing(DebFile(filename=arg)) as package: with package.control.get_file( "manifest", encoding="UTF-8") as manifest_file: manifest = json.load(manifest_file) keys = list(manifest) for key in keys: if key.startswith("_"): del manifest[key] return manifest def run(argv): parser = OptionParser("%prog info [options] PATH") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help="look up PACKAGE-NAME for USER (if you have permission; " "default: current user)") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need file name") try: manifest = get_manifest(options, args[0]) except Exception as e: print(e, file=sys.stderr) return 1 json.dump( manifest, sys.stdout, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": ")) print() return 0 click-0.4.21.1ubuntu0.2/click/commands/__init__.py0000664000000000000000000000247112320742124016375 0ustar # 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 commands.""" import importlib all_commands = ( "build", "buildsource", "chroot", "contents", "desktophook", "hook", "info", "install", "list", "pkgdir", "register", "unregister", "verify", ) hidden_commands = ( "desktophook", ) def load_command(command): return importlib.import_module("click.commands.%s" % command) def help_text(): lines = [] for command in all_commands: if command in hidden_commands: continue mod = load_command(command) lines.append(" %-21s %s" % (command, mod.__doc__.splitlines()[0])) return "\n".join(lines) click-0.4.21.1ubuntu0.2/click/commands/chroot.py0000664000000000000000000001275212320742132016136 0ustar #! /usr/bin/python3 # Copyright (C) 2013 Canonical Ltd. # Author: Brian Murray # 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 . """Use and manage a Click chroot.""" from __future__ import print_function from argparse import ArgumentParser, REMAINDER import os from click.chroot import ClickChroot from click import osextras def requires_root(parser): if os.getuid() != 0: parser.error("must be run as root; try sudo") def create(parser, args): if not osextras.find_on_path("debootstrap"): parser.error( "debootstrap not installed and configured; install click-dev and " "debootstrap") requires_root(parser) chroot = ClickChroot(args.architecture, args.framework, series=args.series) return chroot.create() def install(parser, args): packages = args.packages chroot = ClickChroot(args.architecture, args.framework) return chroot.install(*packages) def destroy(parser, args): requires_root(parser) # ask for confirmation? chroot = ClickChroot(args.architecture, args.framework) return chroot.destroy() def execute(parser, args): program = args.program if not program: program = ["/bin/bash"] chroot = ClickChroot( args.architecture, args.framework, session=args.session) return chroot.run(*program) def maint(parser, args): program = args.program if not program: program = ["/bin/bash"] chroot = ClickChroot( args.architecture, args.framework, session=args.session) return chroot.maint(*program) def upgrade(parser, args): chroot = ClickChroot(args.architecture, args.framework) return chroot.upgrade() def begin_session(parser, args): chroot = ClickChroot( args.architecture, args.framework, session=args.session) return chroot.begin_session() def end_session(parser, args): chroot = ClickChroot( args.architecture, args.framework, session=args.session) return chroot.end_session() def run(argv): parser = ArgumentParser("click chroot") subparsers = parser.add_subparsers( description="management subcommands", help="valid commands") parser.add_argument( "-a", "--architecture", required=True, help="architecture for the chroot") parser.add_argument( "-f", "--framework", default="ubuntu-sdk-13.10", help="framework for the chroot (default: ubuntu-sdk-13.10)") parser.add_argument( "-s", "--series", help="series to use for a newly-created chroot (defaults to a series " "appropriate for the framework)") create_parser = subparsers.add_parser( "create", help="create a chroot of the provided architecture") create_parser.set_defaults(func=create) destroy_parser = subparsers.add_parser( "destroy", help="destroy the chroot") destroy_parser.set_defaults(func=destroy) upgrade_parser = subparsers.add_parser( "upgrade", help="upgrade the chroot") upgrade_parser.set_defaults(func=upgrade) install_parser = subparsers.add_parser( "install", help="install packages in the chroot") install_parser.add_argument( "packages", nargs="+", help="packages to install") install_parser.set_defaults(func=install) execute_parser = subparsers.add_parser( "run", help="run a program in the chroot") execute_parser.add_argument( "-n", "--session-name", dest='session', help="persistent chroot session name to run a program in") execute_parser.add_argument( "program", nargs=REMAINDER, help="program to run with arguments") execute_parser.set_defaults(func=execute) maint_parser = subparsers.add_parser( "maint", help="run a maintenance command in the chroot") maint_parser.add_argument( "-n", "--session-name", dest='session', help="persistent chroot session name to run a maintenance command in") maint_parser.add_argument( "program", nargs=REMAINDER, help="program to run with arguments") maint_parser.set_defaults(func=maint) begin_parser = subparsers.add_parser( "begin-session", help="begin a persistent chroot session") begin_parser.add_argument( "session", help="new session name") begin_parser.set_defaults(func=begin_session) end_parser = subparsers.add_parser( "end-session", help="end a persistent chroot session") end_parser.add_argument( "session", help="session name to end") end_parser.set_defaults(func=end_session) args = parser.parse_args(argv) if not hasattr(args, "func"): parser.print_help() return 1 if (not osextras.find_on_path("schroot") or not os.path.exists("/etc/schroot/click/fstab")): parser.error( "schroot not installed and configured; install click-dev and " "schroot") return args.func(parser, args) click-0.4.21.1ubuntu0.2/click/commands/unregister.py0000664000000000000000000000450512320742132017024 0ustar # 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 . """Unregister an installed Click package for a user.""" from __future__ import print_function from optparse import OptionParser import os import sys from gi.repository import Click def run(argv): parser = OptionParser("%prog unregister [options] PACKAGE-NAME [VERSION]") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help="unregister package for USER (default: $SUDO_USER, if known)") parser.add_option( "--all-users", default=False, action="store_true", help="unregister package that was previously registered for all users") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package name") if os.geteuid() != 0: parser.error( "click unregister must be started as root, since it may need to " "remove packages from disk") if options.user is None and "SUDO_USER" in os.environ: options.user = os.environ["SUDO_USER"] db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) package = args[0] 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" % (package, old_version, args[1]), file=sys.stderr) sys.exit(1) registry.remove(package) db.maybe_remove(package, old_version) # TODO: remove data return 0 click-0.4.21.1ubuntu0.2/click/commands/desktophook.py0000664000000000000000000001362512320742124017173 0ustar # 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 desktop hook. (Temporary; do not rely on this.)""" from __future__ import print_function import errno import io import json from optparse import OptionParser import os from gi.repository import Click from click import osextras COMMENT = \ '# Generated by "click desktophook"; changes here will be overwritten.' def desktop_entries(directory, only_ours=False): for entry in osextras.listdir_force(directory): if not entry.endswith(".desktop"): continue path = os.path.join(directory, entry) if only_ours: try: with io.open(path, encoding="UTF-8") as f: if COMMENT not in f.read(): continue except Exception: continue yield entry def split_entry(entry): entry = entry[:-8] # strip .desktop return entry.split("_", 2) def older(source_path, target_path): """Return True iff source_path is older than target_path. It's also OK for target_path to be missing. """ try: source_mtime = os.stat(source_path).st_mtime except OSError as e: if e.errno == errno.ENOENT: return False try: target_mtime = os.stat(target_path).st_mtime except OSError as e: if e.errno == errno.ENOENT: return True return source_mtime < target_mtime def read_hooks_for(path, package, app_name): try: 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: return json.load(manifest).get("hooks", {}).get(app_name, {}) except Exception: return {} def quote_for_desktop_exec(s): """Quote a string for Exec in a .desktop file. The rules are fairly awful. See: http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html """ for c in s: if c in " \t\n\"'\\><~|&;$*?#()`%": break else: return s quoted = [] for c in s: if c in "\"`$\\": quoted.append("\\" + c) elif c == "%": quoted.append("%%") else: quoted.append(c) escaped = [] for c in "".join(quoted): if c == "\\": escaped.append("\\\\") else: escaped.append(c) return '"%s"' % "".join(escaped) # 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): 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 = Click.find_package_directory(source_path) written_comment = False seen_path = False for line in source: if not line.rstrip("\n") or line.startswith("#"): # Comment target.write(line) elif line.startswith("["): # Group header target.write(line) if not written_comment: print(COMMENT, file=target) elif "=" not in line: # Who knows? target.write(line) else: key, value = line.split("=", 1) key = key.strip() value = value.strip() if key == "Exec": target.write( "%s=aa-exec-click -p %s -- %s\n" % (key, quote_for_desktop_exec(profile), value)) elif key == "Path": target.write("%s=%s\n" % (key, source_dir)) seen_path = True elif key == "Icon": icon_path = os.path.join(source_dir, value) if os.path.exists(icon_path): target.write("%s=%s\n" % (key, icon_path)) else: target.write("%s=%s\n" % (key, value)) else: target.write("%s=%s\n" % (key, value)) if not seen_path: target.write("Path=%s\n" % source_dir) def run(argv): parser = OptionParser("%prog desktophook [options]") parser.parse_args(argv) source_dir = os.path.expanduser("~/.local/share/click/hooks/desktop") target_dir = os.path.expanduser("~/.local/share/applications") source_entries = set(desktop_entries(source_dir)) target_entries = set(desktop_entries(target_dir, only_ours=True)) for new_entry in source_entries: package, app_name, version = split_entry(new_entry) source_path = os.path.join(source_dir, new_entry) target_path = os.path.join(target_dir, new_entry) if older(source_path, target_path): hooks = read_hooks_for(source_path, package, app_name) if "apparmor" in hooks: profile = "%s_%s_%s" % (package, app_name, version) else: profile = "unconfined" write_desktop_file(target_path, source_path, profile) for remove_entry in target_entries - source_entries: os.unlink(os.path.join(target_dir, remove_entry)) return 0 click-0.4.21.1ubuntu0.2/click/commands/install.py0000664000000000000000000000431212320742132016277 0ustar # 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 . """Install a Click package (low-level; consider pkcon instead).""" from __future__ import print_function from optparse import OptionParser import sys from textwrap import dedent from gi.repository import Click from click.install import ClickInstaller, ClickInstallerError def run(argv): parser = OptionParser(dedent("""\ %prog install [options] PACKAGE-FILE This is a low-level tool; to install a package as an ordinary user you should generally use "pkcon install-local PACKAGE-FILE" instead.""")) parser.add_option( "--root", metavar="PATH", help="install packages underneath PATH") parser.add_option( "--force-missing-framework", action="store_true", default=False, help="install despite missing system framework") parser.add_option( "--user", metavar="USER", help="register package for USER") parser.add_option( "--all-users", default=False, action="store_true", help="register package for all users") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package file name") db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) package_path = args[0] installer = ClickInstaller(db, options.force_missing_framework) try: installer.install( package_path, user=options.user, all_users=options.all_users) except ClickInstallerError as e: print("Cannot install %s: %s" % (package_path, e), file=sys.stderr) return 1 return 0 click-0.4.21.1ubuntu0.2/click/commands/hook.py0000664000000000000000000000572412320742132015601 0ustar # 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 . """Install or remove a Click system hook.""" from __future__ import print_function from optparse import OptionParser import sys from textwrap import dedent from gi.repository import Click, GLib per_hook_subcommands = { "install": "install", "remove": "remove", } def run(argv): parser = OptionParser(dedent("""\ %prog hook [options] SUBCOMMAND [...] Subcommands are as follows: install HOOK remove HOOK run-system run-user [--user=USER]""")) parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help=( "run user-level hooks for USER (default: current user; only " "applicable to run-user)")) options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need subcommand (install, remove, run-system, run-user)") subcommand = args[0] if subcommand in per_hook_subcommands: if len(args) < 2: parser.error("need hook name") db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) name = args[1] hook = Click.Hook.open(db, name) getattr(hook, per_hook_subcommands[subcommand])(user_name=None) elif subcommand == "run-system": db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) try: Click.run_system_hooks(db) except GLib.GError as e: if e.domain == "click_hooks_error-quark": print(e.message, file=sys.stderr) return 1 else: raise elif subcommand == "run-user": db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) try: Click.run_user_hooks(db, user_name=options.user) except GLib.GError as e: if e.domain == "click_hooks_error-quark": print(e.message, file=sys.stderr) return 1 else: raise else: parser.error( "unknown subcommand '%s' (known: install, remove, run-system," "run-user)" % subcommand) return 0 click-0.4.21.1ubuntu0.2/click/commands/register.py0000664000000000000000000000371012320742132016456 0ustar # 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 . """Register an installed Click package for a user.""" from __future__ import print_function from optparse import OptionParser from gi.repository import Click, GLib def run(argv): parser = OptionParser("%prog register [options] PACKAGE-NAME VERSION") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help="register package for USER (default: current user)") parser.add_option( "--all-users", default=False, action="store_true", help="register package for all users") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package name") if len(args) < 2: parser.error("need version") db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) package = args[0] version = args[1] 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) return 0 click-0.4.21.1ubuntu0.2/click/commands/contents.py0000664000000000000000000000206712320742124016474 0ustar # 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 . """Show the file-list contents of a Click package file.""" from __future__ import print_function from optparse import OptionParser import subprocess def run(argv): parser = OptionParser("%prog contents [options] PATH") _, args = parser.parse_args(argv) if len(args) < 1: parser.error("need file name") path = args[0] subprocess.check_call(["dpkg-deb", "-c", path]) return 0 click-0.4.21.1ubuntu0.2/click/commands/build.py0000664000000000000000000000365012320742124015735 0ustar # 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 . """Build a Click package.""" from __future__ import print_function from optparse import OptionParser import os import sys from click.build import ClickBuildError, ClickBuilder def run(argv): parser = OptionParser("%prog build [options] DIRECTORY") parser.add_option( "-m", "--manifest", metavar="PATH", default="manifest.json", help="read package manifest from PATH (default: manifest.json)") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need directory") directory = args[0] if not os.path.isdir(directory): parser.error('directory "%s" does not exist' % directory) if os.path.isdir(os.path.join(directory, options.manifest)): options.manifest = os.path.join(options.manifest, "manifest.json") if not os.path.exists(os.path.join(directory, options.manifest)): parser.error( 'directory "%s" does not contain manifest file "%s"' % (directory, options.manifest)) builder = ClickBuilder() builder.add_file(directory, "./") try: path = builder.build(".", manifest_path=options.manifest) except ClickBuildError as e: print(e, file=sys.stderr) return 1 print("Successfully built package in '%s'." % path) return 0 click-0.4.21.1ubuntu0.2/click/commands/verify.py0000664000000000000000000000246212320742124016142 0ustar #! /usr/bin/python3 # 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 . """Verify a Click package.""" from __future__ import print_function from optparse import OptionParser from click.install import ClickInstaller def run(argv): parser = OptionParser("%prog verify [options] PACKAGE-FILE") parser.add_option( "--force-missing-framework", action="store_true", default=False, help="ignore missing system framework") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package file name") package_path = args[0] installer = ClickInstaller(None, options.force_missing_framework) installer.audit(package_path, slow=True) return 0 click-0.4.21.1ubuntu0.2/click/commands/pkgdir.py0000664000000000000000000000336312320742132016116 0ustar #! /usr/bin/python3 # Copyright (C) 2013 Canonical Ltd. # 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 . """Print the directory where a Click package is unpacked.""" from __future__ import print_function from optparse import OptionParser import sys from gi.repository import Click def run(argv): parser = OptionParser("%prog pkgdir [options] {PACKAGE-NAME|PATH}") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help="look up PACKAGE-NAME for USER (if you have permission; " "default: current user)") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package name") try: if "/" in args[0]: print(Click.find_package_directory(args[0])) else: db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) package_name = args[0] 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 return 0 click-0.4.21.1ubuntu0.2/click/commands/buildsource.py0000664000000000000000000000362612320742124017161 0ustar # 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 . """Build a Click source package.""" from __future__ import print_function from optparse import OptionParser import os import sys from click.build import ClickBuildError, ClickSourceBuilder def run(argv): parser = OptionParser("%prog buildsource [options] DIRECTORY") parser.add_option( "-m", "--manifest", metavar="PATH", help="read package manifest from PATH") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need directory") directory = args[0] if not os.path.isdir(directory): parser.error('directory "%s" does not exist' % directory) if os.path.isdir(os.path.join(directory, options.manifest)): options.manifest = os.path.join(options.manifest, "manifest.json") if not os.path.exists(os.path.join(directory, options.manifest)): parser.error( 'directory "%s" does not contain manifest file "%s"' % (directory, options.manifest)) builder = ClickSourceBuilder() builder.add_file(directory, "./") try: path = builder.build(".", manifest_path=options.manifest) except ClickBuildError as e: print(e, file=sys.stderr) return 1 print("Successfully built source package in '%s'." % path) return 0 click-0.4.21.1ubuntu0.2/click/commands/list.py0000664000000000000000000000423412320742132015607 0ustar # 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 . """List installed Click packages.""" from __future__ import print_function import json from optparse import OptionParser import sys from gi.repository import Click from click.json_helpers import json_array_to_python def list_packages(options): db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) if options.all: return json_array_to_python(db.get_manifests(all_versions=True)) else: registry = Click.User.for_user(db, name=options.user) return json_array_to_python(registry.get_manifests()) def run(argv): parser = OptionParser("%prog list [options]") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--all", default=False, action="store_true", help="list all installed packages") parser.add_option( "--user", metavar="USER", help="list packages registered by USER (if you have permission)") parser.add_option( "--manifest", default=False, action="store_true", help="format output as a JSON array of manifests") options, _ = parser.parse_args(argv) json_output = list_packages(options) if options.manifest: json.dump( json_output, sys.stdout, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": ")) print() else: for manifest in json_output: print("%s\t%s" % (manifest["name"], manifest["version"])) return 0 click-0.4.21.1ubuntu0.2/click/tests/0000775000000000000000000000000012607741622013633 5ustar click-0.4.21.1ubuntu0.2/click/tests/test_install.py0000664000000000000000000010070712607741622016717 0ustar # 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 . """Unit tests for click.install.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickInstaller', ] from contextlib import ( closing, contextmanager, ) import hashlib import json import os import shutil import stat import subprocess import tarfile from unittest import skipUnless from debian.deb822 import Deb822 from gi.repository import Click from click.arfile import ArFile from click.build import ClickBuilder from click.install import ( ClickInstaller, ClickInstallerAuditError, ClickInstallerPermissionDenied, ) from click.preinst import static_preinst from click.tests.helpers import TestCase, mkfile, mock, touch from click.versions import spec_version @contextmanager def mock_quiet_subprocess_call(): original_call = subprocess.call def side_effect(*args, **kwargs): if "TEST_VERBOSE" in os.environ: return original_call(*args, **kwargs) else: with open("/dev/null", "w") as devnull: return original_call( *args, stdout=devnull, stderr=devnull, **kwargs) with mock.patch("subprocess.call") as mock_call: mock_call.side_effect = side_effect yield mock_call class TestClickInstaller(TestCase): def setUp(self): super(TestClickInstaller, self).setUp() self.use_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): """Build a fake package with given contents.""" control_fields = {} if control_fields is None else control_fields control_scripts = {} if control_scripts is None else control_scripts data_files = {} if data_files is None else data_files data_dir = os.path.join(self.temp_dir, "fake-package") control_dir = os.path.join(self.temp_dir, "DEBIAN") with mkfile(os.path.join(control_dir, "control")) as control: for key, value in control_fields.items(): print('%s: %s' % (key.title(), value), file=control) print(file=control) if manifest is not None: with mkfile(os.path.join(control_dir, "manifest")) as f: json.dump(manifest, f) print(file=f) for name, contents in control_scripts.items(): with mkfile(os.path.join(control_dir, name)) as script: script.write(contents) Click.ensuredir(data_dir) for name, path in data_files.items(): Click.ensuredir(os.path.dirname(os.path.join(data_dir, name))) if path is None: touch(os.path.join(data_dir, name)) elif os.path.isdir(path): shutil.copytree(path, os.path.join(data_dir, name)) else: shutil.copy2(path, os.path.join(data_dir, name)) package_path = '%s.click' % data_dir ClickBuilder()._pack( self.temp_dir, control_dir, data_dir, package_path) return package_path def _setup_frameworks(self, preloads, frameworks_dir=None, frameworks=[]): if frameworks_dir is None: frameworks_dir = os.path.join(self.temp_dir, "frameworks") shutil.rmtree(frameworks_dir, ignore_errors=True) Click.ensuredir(frameworks_dir) for framework in frameworks: touch(os.path.join(frameworks_dir, "%s.framework" % framework)) preloads["click_get_frameworks_dir"].side_effect = ( lambda: self.make_string(frameworks_dir)) def test_audit_no_click_version(self): path = self.make_fake_package() self.assertRaisesRegex( ClickInstallerAuditError, "No Click-Version field", ClickInstaller(self.db).audit, path) def test_audit_bad_click_version(self): path = self.make_fake_package(control_fields={"Click-Version": "|"}) self.assertRaises(ValueError, ClickInstaller(self.db).audit, path) def test_audit_new_click_version(self): path = self.make_fake_package(control_fields={"Click-Version": "999"}) self.assertRaisesRegex( ClickInstallerAuditError, "Click-Version: 999 newer than maximum supported version .*", ClickInstaller(self.db).audit, path) def test_audit_forbids_depends(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={ "Click-Version": "0.2", "Depends": "libc6", }) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self.assertRaisesRegex( ClickInstallerAuditError, "Depends field is forbidden in Click packages", ClickInstaller(self.db).audit, path) def test_audit_forbids_maintscript(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, control_scripts={ "preinst": "#! /bin/sh\n", "postinst": "#! /bin/sh\n", }) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self.assertRaisesRegex( ClickInstallerAuditError, r"Maintainer scripts are forbidden in Click packages " r"\(found: postinst preinst\)", ClickInstaller(self.db).audit, path) def test_audit_requires_manifest(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, control_scripts={"preinst": static_preinst}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self.assertRaisesRegex( ClickInstallerAuditError, "Package has no manifest", ClickInstaller(self.db).audit, path) def test_audit_invalid_manifest_json(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, control_scripts={"manifest": "{", "preinst": static_preinst}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self.assertRaises(ValueError, ClickInstaller(self.db).audit, path) def test_audit_no_name(self): path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={}) self.assertRaisesRegex( ClickInstallerAuditError, 'No "name" entry in manifest', ClickInstaller(self.db).audit, path) def test_audit_name_bad_character(self): path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={"name": "../evil"}) self.assertRaisesRegex( ClickInstallerAuditError, 'Invalid character "/" in "name" entry: ../evil', ClickInstaller(self.db).audit, path) def test_audit_no_version(self): path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={"name": "test-package"}) self.assertRaisesRegex( ClickInstallerAuditError, 'No "version" entry in manifest', ClickInstaller(self.db).audit, path) def test_audit_no_framework(self): path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={"name": "test-package", "version": "1.0"}, control_scripts={"preinst": static_preinst}) self.assertRaisesRegex( ClickInstallerAuditError, 'No "framework" entry in manifest', ClickInstaller(self.db).audit, path) def test_audit_missing_framework(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "missing", }, control_scripts={"preinst": static_preinst}) self._setup_frameworks(preloads, frameworks=["present"]) self.assertRaisesRegex( ClickInstallerAuditError, 'Framework "missing" not present on system.*', ClickInstaller(self.db).audit, path) def test_audit_missing_framework_force(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "missing", }) self._setup_frameworks(preloads, frameworks=["present"]) ClickInstaller(self.db, True).audit(path) def test_audit_passes_correct_package(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) installer = ClickInstaller(self.db) self.assertEqual(("test-package", "1.0"), installer.audit(path)) def test_audit_multiple_frameworks(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.4"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-14.04-basic, ubuntu-sdk-14.04-webapps", }, control_scripts={"preinst": static_preinst}) installer = ClickInstaller(self.db) self._setup_frameworks(preloads, frameworks=["dummy"]) self.assertRaisesRegex( ClickInstallerAuditError, 'Frameworks "ubuntu-sdk-14.04-basic", ' '"ubuntu-sdk-14.04-webapps" not present on system.*', installer.audit, path) self._setup_frameworks( preloads, frameworks=["dummy", "ubuntu-sdk-14.04-basic"]) self.assertRaisesRegex( ClickInstallerAuditError, 'Framework "ubuntu-sdk-14.04-webapps" not present on ' 'system.*', installer.audit, path) self._setup_frameworks( preloads, frameworks=[ "dummy", "ubuntu-sdk-14.04-basic", "ubuntu-sdk-14.04-webapps", ]) self.assertEqual(("test-package", "1.0"), installer.audit(path)) def test_audit_missing_dot_slash(self): # Manually construct a package with data paths that do not start # with "./", which could be used to bypass path filtering. with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={".click/tmp.ci/manifest": None}) # Repack without the leading "./". data_dir = os.path.join(self.temp_dir, "fake-package") data_tar_path = os.path.join(self.temp_dir, "data.tar.gz") control_tar_path = os.path.join(self.temp_dir, "control.tar.gz") package_path = '%s.click' % data_dir with closing(tarfile.TarFile.open( name=data_tar_path, mode="w:gz", format=tarfile.GNU_FORMAT )) as data_tar: data_tar.add( os.path.join(data_dir, ".click"), arcname=".click") with ArFile(name=package_path, mode="w") as package: package.add_magic() package.add_data("debian-binary", b"2.0\n") package.add_data( "_click-binary", ("%s\n" % spec_version).encode("UTF-8")) package.add_file("control.tar.gz", control_tar_path) package.add_file("data.tar.gz", data_tar_path) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer = ClickInstaller(self.db) self.assertRaisesRegex( ClickInstallerAuditError, 'File name ".click" in package does not start with "./"', installer.audit, path) def test_audit_broken_md5sums(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={ "preinst": static_preinst, "md5sums": "%s foo" % ("0" * 32), }, data_files={"foo": None}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer = ClickInstaller(self.db) self.assertRaises( subprocess.CalledProcessError, installer.audit, path, slow=True) def test_audit_matching_md5sums(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() data_path = os.path.join(self.temp_dir, "foo") with mkfile(data_path) as data: print("test", file=data) with open(data_path, "rb") as data: data_md5sum = hashlib.md5(data.read()).hexdigest() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={ "preinst": static_preinst, "md5sums": "%s foo" % data_md5sum, }, data_files={"foo": data_path}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer = ClickInstaller(self.db) self.assertEqual( ("test-package", "1.0"), installer.audit(path, slow=True)) def test_no_write_permission(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}) write_mask = ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) installer = ClickInstaller(self.db) temp_dir_mode = os.stat(self.temp_dir).st_mode try: os.chmod(self.temp_dir, temp_dir_mode & write_mask) self.assertRaises( ClickInstallerPermissionDenied, installer.install, path) finally: os.chmod(self.temp_dir, temp_dir_mode) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @mock.patch("gi.repository.Click.package_install_hooks") def test_install(self, mock_package_install_hooks): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.0", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={"foo": None}) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) self.assertCountEqual([".click", "test-package"], os.listdir(root)) package_dir = os.path.join(root, "test-package") self.assertCountEqual(["1.0", "current"], os.listdir(package_dir)) inst_dir = os.path.join(package_dir, "current") self.assertTrue(os.path.islink(inst_dir)) self.assertEqual("1.0", os.readlink(inst_dir)) self.assertCountEqual([".click", "foo"], os.listdir(inst_dir)) status_path = os.path.join(inst_dir, ".click", "status") with open(status_path) as status_file: # .readlines() avoids the need for a python-apt backport to # Ubuntu 12.04 LTS. status = list(Deb822.iter_paragraphs(status_file.readlines())) self.assertEqual(1, len(status)) self.assertEqual({ "Package": "test-package", "Status": "install ok installed", "Version": "1.0", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, status[0]) mock_package_install_hooks.assert_called_once_with( db, "test-package", None, "1.0", user_name=None) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") def test_sandbox(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() original_call = subprocess.call def call_side_effect(*args, **kwargs): if "TEST_VERBOSE" in os.environ: return original_call( ["touch", os.path.join(self.temp_dir, "sentinel")], **kwargs) else: with open("/dev/null", "w") as devnull: return original_call( ["touch", os.path.join(self.temp_dir, "sentinel")], stdout=devnull, stderr=devnull, **kwargs) path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.0", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={"foo": None}) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock.patch("subprocess.call") as mock_call: mock_call.side_effect = call_side_effect self.assertRaises( subprocess.CalledProcessError, installer.install, path) self.assertFalse( os.path.exists(os.path.join(self.temp_dir, "sentinel"))) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @mock.patch("gi.repository.Click.package_install_hooks") def test_upgrade(self, mock_package_install_hooks): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() os.environ["TEST_QUIET"] = "1" path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={"foo": None}) root = os.path.join(self.temp_dir, "root") package_dir = os.path.join(root, "test-package") inst_dir = os.path.join(package_dir, "current") os.makedirs(os.path.join(package_dir, "1.0")) os.symlink("1.0", inst_dir) db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) self.assertCountEqual([".click", "test-package"], os.listdir(root)) self.assertCountEqual(["1.1", "current"], os.listdir(package_dir)) self.assertTrue(os.path.islink(inst_dir)) self.assertEqual("1.1", os.readlink(inst_dir)) self.assertCountEqual([".click", "foo"], os.listdir(inst_dir)) status_path = os.path.join(inst_dir, ".click", "status") with open(status_path) as status_file: # .readlines() avoids the need for a python-apt backport to # Ubuntu 12.04 LTS. status = list(Deb822.iter_paragraphs(status_file.readlines())) self.assertEqual(1, len(status)) self.assertEqual({ "Package": "test-package", "Status": "install ok installed", "Version": "1.1", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, status[0]) mock_package_install_hooks.assert_called_once_with( db, "test-package", "1.0", "1.1", user_name=None) def _get_mode(self, path): return stat.S_IMODE(os.stat(path).st_mode) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @mock.patch("gi.repository.Click.package_install_hooks") def test_world_readable(self, mock_package_install_hooks): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() owner_only_file = os.path.join(self.temp_dir, "owner-only-file") touch(owner_only_file) os.chmod(owner_only_file, stat.S_IRUSR | stat.S_IWUSR) owner_only_dir = os.path.join(self.temp_dir, "owner-only-dir") os.mkdir(owner_only_dir, stat.S_IRWXU) path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={ "world-readable-file": owner_only_file, "world-readable-dir": owner_only_dir, }) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) inst_dir = os.path.join(root, "test-package", "current") self.assertEqual( stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH, self._get_mode(os.path.join(inst_dir, "world-readable-file"))) self.assertEqual( stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH, self._get_mode(os.path.join(inst_dir, "world-readable-dir"))) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @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): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() mock_dpkg_architecture.return_value = "armhf" path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "armhf", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", "architecture": "armhf", }, control_scripts={"preinst": static_preinst}) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) self.assertTrue( os.path.exists(os.path.join(root, "test-package", "current"))) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @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): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() mock_dpkg_architecture.return_value = "armhf" path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "multi", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", "architecture": ["armhf", "i386"], }, control_scripts={"preinst": static_preinst}) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) self.assertTrue( os.path.exists(os.path.join(root, "test-package", "current"))) def test_reinstall_preinstalled(self): # Attempting to reinstall a preinstalled version shouldn't actually # reinstall it in an overlay database (which would cause # irreconcilable confusion about the correct target for system hook # symlinks), but should instead simply update the user registration. path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.4", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}) underlay = os.path.join(self.temp_dir, "underlay") overlay = os.path.join(self.temp_dir, "overlay") db = Click.DB() db.add(underlay) installer = ClickInstaller(db, True) with mock_quiet_subprocess_call(): installer.install(path, all_users=True) underlay_unpacked = os.path.join(underlay, "test-package", "1.1") self.assertTrue(os.path.exists(underlay_unpacked)) all_link = os.path.join( underlay, ".click", "users", "@all", "test-package") self.assertTrue(os.path.islink(all_link)) self.assertEqual(underlay_unpacked, os.readlink(all_link)) db.add(overlay) registry = Click.User.for_user(db, "test-user") registry.remove("test-package") user_link = os.path.join( overlay, ".click", "users", "test-user", "test-package") self.assertTrue(os.path.islink(user_link)) self.assertEqual("@hidden", os.readlink(user_link)) installer = ClickInstaller(db, True) with mock_quiet_subprocess_call(): installer.install(path, user="test-user") overlay_unpacked = os.path.join(overlay, "test-package", "1.1") self.assertFalse(os.path.exists(overlay_unpacked)) self.assertEqual("1.1", registry.get_version("test-package")) click-0.4.21.1ubuntu0.2/click/tests/__init__.py0000664000000000000000000000241312320742124015732 0ustar 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) click-0.4.21.1ubuntu0.2/click/tests/config.py.in0000664000000000000000000000153512320742124016051 0ustar # 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@ click-0.4.21.1ubuntu0.2/click/tests/gimock.py0000664000000000000000000005017412320742124015453 0ustar # 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", "json-glib-1.0"], 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]() click-0.4.21.1ubuntu0.2/click/tests/test_osextras.py0000664000000000000000000001311612320742124017104 0ustar # 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 . """Unit tests for click.osextras.""" from __future__ import print_function __all__ = [ 'TestOSExtrasNative', 'TestOSExtrasPython', ] import os from gi.repository import Click, GLib from click import osextras from click.tests.helpers import TestCase, mock, touch class TestOSExtrasBaseMixin: def test_ensuredir_previously_missing(self): new_dir = os.path.join(self.temp_dir, "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) 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(self.mod.find_on_path("ls")) def test_find_on_path_present_executable(self): bin_dir = os.path.join(self.temp_dir, "bin") program = os.path.join(bin_dir, "program") touch(program) os.chmod(program, 0o755) os.environ["PATH"] = bin_dir 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(self.mod.find_on_path("program")) 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) 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") self.mod.unlink_force(path) self.assertFalse(os.path.exists(path)) def test_symlink_file_present(self): path = os.path.join(self.temp_dir, "link") touch(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) 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") 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, self.mod.get_umask()) os.umask(0o002) 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) click-0.4.21.1ubuntu0.2/click/tests/test_framework.py0000664000000000000000000000767112320742124017242 0ustar # 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.framework.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickFramework', ] import os from gi.repository import Click from click.tests.helpers import TestCase class TestClickFramework(TestCase): def setUp(self): super(TestClickFramework, self).setUp() self.use_temp_dir() def _setup_frameworks(self, preloads, frameworks_dir=None, frameworks={}): if frameworks_dir is None: frameworks_dir = os.path.join(self.temp_dir, "frameworks") Click.ensuredir(frameworks_dir) for framework_name in frameworks: framework_path = os.path.join( frameworks_dir, "%s.framework" % framework_name) with open(framework_path, "w") as framework: for key, value in frameworks[framework_name].items(): print("%s: %s" % (key, value), file=framework) preloads["click_get_frameworks_dir"].side_effect = ( lambda: self.make_string(frameworks_dir)) def test_open(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() self._setup_frameworks(preloads, frameworks={"framework-1": {}}) Click.Framework.open("framework-1") self.assertRaisesFrameworkError( Click.FrameworkError.NO_SUCH_FRAMEWORK, Click.Framework.open, "framework-2") def test_has_framework(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() self._setup_frameworks(preloads, frameworks={"framework-1": {}}) self.assertTrue(Click.Framework.has_framework ("framework-1")); self.assertFalse(Click.Framework.has_framework ("framework-2")); def test_get_frameworks(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() self._setup_frameworks( preloads, frameworks={"ubuntu-sdk-13.10": {}, "ubuntu-sdk-14.04": {}}) self.assertEqual( ["ubuntu-sdk-13.10", "ubuntu-sdk-14.04"], sorted( f.props.name for f in Click.Framework.get_frameworks ())) def test_fields(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() self._setup_frameworks( preloads, frameworks={ "ubuntu-sdk-14.04-qml": { "base-name": "ubuntu-sdk", "base-version": "14.04", }}) framework = Click.Framework.open("ubuntu-sdk-14.04-qml") self.assertCountEqual( ["base-name", "base-version"], framework.get_fields()) self.assertEqual("ubuntu-sdk", framework.get_field("base-name")) self.assertEqual("14.04", framework.get_field("base-version")) self.assertRaisesFrameworkError( Click.FrameworkError.MISSING_FIELD, framework.get_field, "nonexistent") self.assertEqual("ubuntu-sdk", framework.get_base_name()) self.assertEqual("14.04", framework.get_base_version()) click-0.4.21.1ubuntu0.2/click/tests/test_arfile.py0000664000000000000000000000633412320742124016502 0ustar # 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 . """Unit tests for click.arfile.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestArFile', ] import os import subprocess from click.arfile import ArFile from click.tests.helpers import TestCase, touch class TestArFile(TestCase): def setUp(self): super(TestArFile, self).setUp() self.use_temp_dir() def test_init_rejects_mode_r(self): self.assertRaises(ValueError, ArFile, mode="r") def test_init_name(self): path = os.path.join(self.temp_dir, "foo.a") with ArFile(name=path, mode="w") as arfile: self.assertEqual("w", arfile.mode) self.assertEqual("wb", arfile.real_mode) self.assertEqual(path, arfile.name) self.assertEqual(path, arfile.fileobj.name) self.assertTrue(arfile.opened_fileobj) self.assertFalse(arfile.closed) def test_init_rejects_readonly_fileobj(self): path = os.path.join(self.temp_dir, "foo.a") touch(path) with open(path, "rb") as fileobj: self.assertRaises(ValueError, ArFile, fileobj=fileobj) def test_init_fileobj(self): path = os.path.join(self.temp_dir, "foo.a") with open(path, "wb") as fileobj: arfile = ArFile(fileobj=fileobj) self.assertEqual("w", arfile.mode) self.assertEqual("wb", arfile.real_mode) self.assertEqual(path, arfile.name) self.assertEqual(fileobj, arfile.fileobj) self.assertFalse(arfile.opened_fileobj) self.assertFalse(arfile.closed) def test_writes_valid_ar_file(self): member_path = os.path.join(self.temp_dir, "member") with open(member_path, "wb") as member: member.write(b"\x00\x01\x02\x03\x04\x05\x06\x07") path = os.path.join(self.temp_dir, "foo.a") with ArFile(name=path, mode="w") as arfile: arfile.add_magic() arfile.add_data("data-member", b"some data") arfile.add_file("file-member", member_path) extract_path = os.path.join(self.temp_dir, "extract") os.mkdir(extract_path) subprocess.call(["ar", "x", path], cwd=extract_path) self.assertCountEqual( ["data-member", "file-member"], os.listdir(extract_path)) with open(os.path.join(extract_path, "data-member"), "rb") as member: self.assertEqual(b"some data", member.read()) with open(os.path.join(extract_path, "file-member"), "rb") as member: self.assertEqual( b"\x00\x01\x02\x03\x04\x05\x06\x07", member.read()) click-0.4.21.1ubuntu0.2/click/tests/test_user.py0000664000000000000000000004537712320742132016227 0ustar # 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 . """Unit tests for click.user.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickUser', ] import json import os from gi.repository import Click from click.json_helpers import json_array_to_python, json_object_to_python from click.tests.helpers import TestCase, mkfile class TestClickUser(TestCase): def setUp(self): super(TestClickUser, self).setUp() self.use_temp_dir() self.db = Click.DB() self.db.add(self.temp_dir) def _setUpMultiDB(self): 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( 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) with mkfile(os.path.join(a_1_0, ".click", "info", "a.manifest")) as m: json.dump({"name": "a", "version": "1.0"}, m) b_2_0 = os.path.join(self.temp_dir, "custom", "b", "2.0") os.makedirs(b_2_0) with mkfile(os.path.join(b_2_0, ".click", "info", "b.manifest")) as m: json.dump({"name": "b", "version": "2.0"}, m) a_1_1 = os.path.join(self.temp_dir, "click", "a", "1.1") os.makedirs(a_1_1) with mkfile(os.path.join(a_1_1, ".click", "info", "a.manifest")) as m: json.dump({"name": "a", "version": "1.1"}, m) c_0_1 = os.path.join(self.temp_dir, "click", "c", "0.1") os.makedirs(c_0_1) with mkfile(os.path.join(c_0_1, ".click", "info", "c.manifest")) as m: json.dump({"name": "c", "version": "0.1"}, m) os.makedirs(user_dbs[0]) os.symlink(a_1_0, os.path.join(user_dbs[0], "a")) os.symlink(b_2_0, os.path.join(user_dbs[0], "b")) 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, 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(None, None) self.assertEqual( os.path.join(db_root, ".click", "users", "test-user"), registry.get_overlay_db()) def test_get_overlay_db(self): self.assertEqual( os.path.join(self.temp_dir, ".click", "users", "user"), Click.User.for_user(self.db, "user").get_overlay_db()) 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, None) 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_get_package_names_multiple_root(self): _, registry = self._setUpMultiDB() self.assertCountEqual( ["a", "b", "c"], list(registry.get_package_names())) 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_get_version_multiple_root(self): _, registry = self._setUpMultiDB() 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 = 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 = 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.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 = 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") self.assertTrue(os.path.islink(path)) self.assertEqual( os.path.join(self.temp_dir, "a", "1.1"), os.readlink(path)) def test_set_version_multiple_root(self): user_dbs, registry = self._setUpMultiDB() 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.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.get(1).props.root, "a", "1.2"), os.readlink(a_overlay)) 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.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.get(1).props.root, "b", "2.1"), os.readlink(b_overlay)) 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.get(1).props.root, "c", "0.2"), os.readlink(c_overlay)) 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.get(1).props.root, "d", "3.0"), os.readlink(d_overlay)) def test_set_version_restore_to_underlay(self): user_dbs, registry = self._setUpMultiDB() a_underlay = os.path.join(user_dbs[0], "a") a_overlay = os.path.join(user_dbs[1], "a") # 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.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.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.get(0).props.root, "a", "1.0"), os.readlink(a_underlay)) self.assertFalse(os.path.islink(a_overlay)) def test_remove_missing(self): registry = Click.User.for_user(self.db, "user") self.assertRaisesUserError( Click.UserError.NO_SUCH_PACKAGE, registry.remove, "a") def test_remove(self): 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)) def test_remove_multiple_root(self): user_dbs, registry = self._setUpMultiDB() registry.remove("a") self.assertFalse(os.path.exists(os.path.join(user_dbs[1], "a"))) # Exposed underlay. 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.assertFalse(registry.has_package_name("b")) registry.remove("c") self.assertFalse(os.path.exists(os.path.join(user_dbs[1], "c"))) 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 = 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(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 = Click.User.for_user(multi_db, "user") self.assertEqual("1.0", registry.get_version("a")) registry.remove("a") self.assertFalse(registry.has_package_name("a")) self.assertEqual( "@hidden", os.readlink(os.path.join(user_dbs[1], "a"))) 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.get_overlay_db(), "a"), registry.get_path("a")) def test_get_path_multiple_root(self): user_dbs, registry = self._setUpMultiDB() 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_get_manifest(self): registry = Click.User.for_user(self.db, "user") manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") manifest_obj = {"name": "a", "version": "1.0"} with mkfile(manifest_path) as manifest: json.dump(manifest_obj, manifest) manifest_obj["_directory"] = os.path.join( registry.get_overlay_db(), "a") manifest_obj["_removable"] = 1 registry.set_version("a", "1.0") self.assertEqual( manifest_obj, json_object_to_python(registry.get_manifest("a"))) self.assertEqual( manifest_obj, json.loads(registry.get_manifest_as_string("a"))) def test_get_manifest_multiple_root(self): user_dbs, registry = self._setUpMultiDB() expected_a = { "name": "a", "version": "1.1", "_directory": os.path.join(user_dbs[1], "a"), "_removable": 1, } self.assertEqual( expected_a, json_object_to_python(registry.get_manifest("a"))) self.assertEqual( expected_a, json.loads(registry.get_manifest_as_string("a"))) expected_b = { "name": "b", "version": "2.0", "_directory": os.path.join(user_dbs[0], "b"), "_removable": 1, } self.assertEqual( expected_b, json_object_to_python(registry.get_manifest("b"))) self.assertEqual( expected_b, json.loads(registry.get_manifest_as_string("b"))) expected_c = { "name": "c", "version": "0.1", "_directory": os.path.join(user_dbs[1], "c"), "_removable": 1, } self.assertEqual( expected_c, json_object_to_python(registry.get_manifest("c"))) self.assertEqual( expected_c, json.loads(registry.get_manifest_as_string("c"))) self.assertRaisesUserError( Click.UserError.NO_SUCH_PACKAGE, registry.get_path, "d") def test_get_manifests(self): registry = Click.User.for_user(self.db, "user") a_manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") a_manifest_obj = {"name": "a", "version": "1.0"} with mkfile(a_manifest_path) as a_manifest: json.dump(a_manifest_obj, a_manifest) registry.set_version("a", "1.0") b_manifest_path = os.path.join( self.temp_dir, "b", "2.0", ".click", "info", "b.manifest") b_manifest_obj = {"name": "b", "version": "2.0"} with mkfile(b_manifest_path) as b_manifest: json.dump(b_manifest_obj, b_manifest) registry.set_version("b", "2.0") a_manifest_obj["_directory"] = os.path.join( registry.get_overlay_db(), "a") a_manifest_obj["_removable"] = 1 b_manifest_obj["_directory"] = os.path.join( registry.get_overlay_db(), "b") b_manifest_obj["_removable"] = 1 self.assertEqual( [a_manifest_obj, b_manifest_obj], json_array_to_python(registry.get_manifests())) self.assertEqual( [a_manifest_obj, b_manifest_obj], json.loads(registry.get_manifests_as_string())) def test_get_manifests_multiple_root(self): user_dbs, registry = self._setUpMultiDB() a_manifest_obj = { "name": "a", "version": "1.1", "_directory": os.path.join(user_dbs[1], "a"), "_removable": 1, } b_manifest_obj = { "name": "b", "version": "2.0", "_directory": os.path.join(user_dbs[0], "b"), "_removable": 1, } c_manifest_obj = { "name": "c", "version": "0.1", "_directory": os.path.join(user_dbs[1], "c"), "_removable": 1, } self.assertEqual( [a_manifest_obj, c_manifest_obj, b_manifest_obj], json_array_to_python(registry.get_manifests())) self.assertEqual( [a_manifest_obj, c_manifest_obj, b_manifest_obj], json.loads(registry.get_manifests_as_string())) registry.remove("b") self.assertEqual( "@hidden", os.readlink(os.path.join(user_dbs[1], "b"))) self.assertEqual( [a_manifest_obj, c_manifest_obj], json_array_to_python(registry.get_manifests())) self.assertEqual( [a_manifest_obj, c_manifest_obj], json.loads(registry.get_manifests_as_string())) 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.is_removable("a")) def test_is_removable_multiple_root(self): user_dbs, registry = self._setUpMultiDB() 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.assertFalse(registry.has_package_name("b")) self.assertTrue(os.path.islink(b_overlay)) self.assertEqual("@hidden", os.readlink(b_overlay)) 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.assertTrue(registry.has_package_name("b")) self.assertTrue(os.path.islink(b_overlay)) self.assertEqual( os.path.join(self.multi_db.get(0).props.root, "b", "2.0"), os.readlink(b_overlay)) self.assertEqual("2.0", registry.get_version("b")) self.assertEqual(b_overlay, registry.get_path("b")) self.assertTrue(registry.is_removable("b")) click-0.4.21.1ubuntu0.2/click/tests/Makefile.am0000664000000000000000000000054312320742124015657 0ustar 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 json-glib-1.0 \ --pkg click-0.4 \ -I$(top_builddir)/lib/click -L$(top_builddir)/lib/click \ --accept-unprefixed --warn-all \ $< --output $@ click-0.4.21.1ubuntu0.2/click/tests/gimock_types.py0000664000000000000000000000632112320742124016672 0ustar # 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)) click-0.4.21.1ubuntu0.2/click/tests/test_hooks.py0000664000000000000000000015126012320742132016361 0ustar # 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 . """Unit tests for click.hooks.""" from __future__ import print_function __metaclass__ = type __all__ = [ "TestClickHookSystemLevel", "TestClickHookUserLevel", "TestClickPatternFormatter", "TestPackageInstallHooks", "TestPackageRemoveHooks", ] from functools import partial from itertools import takewhile import json import os from textwrap import dedent 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 _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", Click.pattern_format("foo.${key}", self._make_variant(key="bar"))) self.assertEqual( "foo.barbaz", 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", Click.pattern_format("x${key}y", self._make_variant())) def test_preserves_unmatched_dollar(self): 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("$", 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"}, Click.pattern_possible_expansion( "x_abc_1", "x_${id}_${num}", self._make_variant(num="1")).unpack()) self.assertIsNone( 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 = 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 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 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 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 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 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", user_name=None)) def test_short_id_valid_with_single_version(self): 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", user_name=None)) 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 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", user_name=None) 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 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", user_name=None) 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_install_package_uses_deepest_copy(self): # If the same version of a package is unpacked in multiple # databases, then we make sure the link points to the deepest copy, # even if it already points somewhere else. It is important to be # consistent about this since system hooks may only have a single # target for any given application ID. 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) underlay = os.path.join(self.temp_dir, "underlay") overlay = os.path.join(self.temp_dir, "overlay") db = Click.DB() db.add(underlay) db.add(overlay) os.makedirs(os.path.join(underlay, "org.example.package", "1.0")) os.makedirs(os.path.join(overlay, "org.example.package", "1.0")) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") underlay_target_path = os.path.join( underlay, "org.example.package", "1.0", "foo") overlay_target_path = os.path.join( overlay, "org.example.package", "1.0", "foo") os.symlink(overlay_target_path, symlink_path) hook = Click.Hook.open(db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo", user_name=None) self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(underlay_target_path, os.readlink(symlink_path)) def test_upgrade(self): 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", user_name=None) 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 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", user_name=None) self.assertFalse(os.path.exists(symlink_path)) def test_install(self): 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(user_name=None) 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 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(user_name=None) 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") 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.0", ".click", "info", "test-2.manifest")) as f: json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f) 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_1_0 = os.path.join( self.temp_dir, "test-2_test2-app_1.0.test") path_2_1_1 = 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(user_name=None) 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_1_0)) self.assertEqual( os.path.join(self.temp_dir, "test-2", "1.0", "target-2"), os.readlink(path_2_1_0)) self.assertTrue(os.path.lexists(path_2_1_1)) self.assertEqual( os.path.join(self.temp_dir, "test-2", "1.1", "target-2"), os.readlink(path_2_1_1)) self.assertFalse(os.path.lexists(path_3)) class TestClickHookUserLevel(TestClickHookBase): def test_open(self): 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 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 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 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) 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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)) 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(user_name=None) 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(user_name=None) 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)) def test_sync_without_user_db(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" 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) with mkfile(os.path.join( self.temp_dir, "test-package", "1.0", ".click", "info", "test-package.manifest")) as f: json.dump({"hooks": {"test-app": {"test": "target"}}}, f) all_users_db = Click.User.for_all_users(self.db) all_users_db.set_version("test-package", "1.0") 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.assertFalse(os.path.exists(os.path.join( self.temp_dir, ".click", "users", "test-user", "test-package"))) def test_sync_uses_deepest_copy(self): # If the same version of a package is unpacked in multiple # databases, then we make sure the user link points to the deepest # copy, even if it already points somewhere else. It is important # to be consistent about this since system hooks may only have a # single target for any given application ID, and user links must # match system hooks so that (for example) the version of an # application run by a user has a matching system AppArmor profile. 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) underlay = os.path.join(self.temp_dir, "underlay") overlay = os.path.join(self.temp_dir, "overlay") db = Click.DB() db.add(underlay) db.add(overlay) underlay_unpacked = os.path.join(underlay, "test-package", "1.0") overlay_unpacked = os.path.join(overlay, "test-package", "1.0") os.makedirs(underlay_unpacked) os.makedirs(overlay_unpacked) manifest = {"hooks": {"test-app": {"test": "foo"}}} with mkfile(os.path.join( underlay_unpacked, ".click", "info", "test-package.manifest")) as f: json.dump(manifest, f) with mkfile(os.path.join( overlay_unpacked, ".click", "info", "test-package.manifest")) as f: json.dump(manifest, f) underlay_user_link = os.path.join( underlay, ".click", "users", "@all", "test-package") overlay_user_link = os.path.join( overlay, ".click", "users", "test-user", "test-package") Click.ensuredir(os.path.dirname(underlay_user_link)) os.symlink(underlay_unpacked, underlay_user_link) Click.ensuredir(os.path.dirname(overlay_user_link)) os.symlink(overlay_unpacked, overlay_user_link) symlink_path = os.path.join( self.temp_dir, "test-package_test-app_1.0.test") underlay_target_path = os.path.join(underlay_user_link, "foo") overlay_target_path = os.path.join(overlay_user_link, "foo") os.symlink(overlay_target_path, symlink_path) hook = Click.Hook.open(db, "test") hook.sync(user_name="test-user") self.assertTrue(os.path.islink(underlay_user_link)) self.assertEqual( underlay_unpacked, os.readlink(underlay_user_link)) self.assertFalse(os.path.islink(overlay_user_link)) self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(underlay_target_path, os.readlink(symlink_path)) class TestPackageInstallHooks(TestClickHookBase): def test_removes_old_hooks(self): 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", user_name=None) 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): 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", user_name=None) 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): 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", user_name=None) 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): 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", user_name=None) self.assertFalse(os.path.lexists(unity_path)) self.assertFalse(os.path.lexists(yelp_docs_path)) self.assertFalse(os.path.lexists(yelp_other_path)) click-0.4.21.1ubuntu0.2/click/tests/preload.h0000664000000000000000000000544612320742124015431 0ustar #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_frameworks_dir: * * Attributes: (headers glib.h) */ gchar *click_get_frameworks_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); click-0.4.21.1ubuntu0.2/click/tests/test_query.py0000664000000000000000000000324012320742124016376 0ustar # 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") click-0.4.21.1ubuntu0.2/click/tests/test_scripts.py0000664000000000000000000000333512320742124016725 0ustar # 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 . """Test that all top-level scripts work.""" __metaclass__ = type __all__ = [ 'TestScripts', ] import os import subprocess from unittest import skipIf from click.tests.helpers import TestCase class TestScripts(TestCase): @skipIf('SKIP_SLOW_TESTS' in os.environ, 'Skipping slow tests') def test_scripts(self): self.longMessage = True paths = [] for dirpath, _, filenames in os.walk("bin"): filenames = [ n for n in filenames if not n.startswith(".") and not n.endswith("~")] for filename in filenames: paths.append(os.path.join(dirpath, filename)) for path in paths: subp = subprocess.Popen( [path, "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) err = subp.communicate()[1] self.assertEqual("", err, "%s --help produced error output" % path) self.assertEqual( 0, subp.returncode, "%s --help exited non-zero" % path) click-0.4.21.1ubuntu0.2/click/tests/helpers.py0000664000000000000000000002234212320742124015640 0ustar # 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 . # # This file contains code from Python 3.3, released under the Python license # (http://docs.python.org/3/license.html). """Testing helpers.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestCase', 'mkfile', 'touch', ] import contextlib import os import shutil import sys import tempfile import unittest try: from unittest import mock except ImportError: import mock from gi.repository import Click, GLib from click.tests import gimock class TestCase(gimock.GIMockTestCase): def setUp(self): super(TestCase, self).setUp() self.temp_dir = None self.save_env = dict(os.environ) self.maxDiff = None def tearDown(self): for key in set(os.environ) - set(self.save_env): del os.environ[key] for key, value in os.environ.items(): if value != self.save_env[key]: os.environ[key] = self.save_env[key] for key in set(self.save_env) - set(os.environ): os.environ[key] = self.save_env[key] def use_temp_dir(self): if self.temp_dir is not None: return self.temp_dir self.temp_dir = tempfile.mkdtemp(prefix="click") self.addCleanup(shutil.rmtree, self.temp_dir) return self.temp_dir # Monkey-patch for Python 2/3 compatibility. if not hasattr(unittest.TestCase, 'assertCountEqual'): assertCountEqual = unittest.TestCase.assertItemsEqual if not hasattr(unittest.TestCase, 'assertRegex'): assertRegex = unittest.TestCase.assertRegexpMatches # Renamed in Python 3.2 to omit the trailing 'p'. 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 assertRaisesFrameworkError(self, code, callableObj, *args, **kwargs): self.assertRaisesGError( "click_framework_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 # mock.call. Since it's so convenient, monkey-patch a partial backport # (from Python 3.3 unittest.mock) into place here. class _ANY(object): "A helper object that compares equal to everything." def __eq__(self, other): return True def __ne__(self, other): return False def __repr__(self): return '' mock.ANY = _ANY() class _Call(tuple): """ A tuple for holding the results of a call to a mock, either in the form `(args, kwargs)` or `(name, args, kwargs)`. If args or kwargs are empty then a call tuple will compare equal to a tuple without those values. This makes comparisons less verbose:: _Call(('name', (), {})) == ('name',) _Call(('name', (1,), {})) == ('name', (1,)) _Call(((), {'a': 'b'})) == ({'a': 'b'},) The `_Call` object provides a useful shortcut for comparing with call:: _Call(((1, 2), {'a': 3})) == call(1, 2, a=3) _Call(('foo', (1, 2), {'a': 3})) == call.foo(1, 2, a=3) If the _Call has no name then it will match any name. """ def __new__(cls, value=(), name=None, parent=None, two=False, from_kall=True): name = '' args = () kwargs = {} _len = len(value) if _len == 3: name, args, kwargs = value elif _len == 2: first, second = value if isinstance(first, str): name = first if isinstance(second, tuple): args = second else: kwargs = second else: args, kwargs = first, second elif _len == 1: value, = value if isinstance(value, str): name = value elif isinstance(value, tuple): args = value else: kwargs = value if two: return tuple.__new__(cls, (args, kwargs)) return tuple.__new__(cls, (name, args, kwargs)) def __init__(self, value=(), name=None, parent=None, two=False, from_kall=True): self.name = name self.parent = parent self.from_kall = from_kall def __eq__(self, other): if other is mock.ANY: return True try: len_other = len(other) except TypeError: return False self_name = '' if len(self) == 2: self_args, self_kwargs = self else: self_name, self_args, self_kwargs = self other_name = '' if len_other == 0: other_args, other_kwargs = (), {} elif len_other == 3: other_name, other_args, other_kwargs = other elif len_other == 1: value, = other if isinstance(value, tuple): other_args = value other_kwargs = {} elif isinstance(value, str): other_name = value other_args, other_kwargs = (), {} else: other_args = () other_kwargs = value else: # len 2 # could be (name, args) or (name, kwargs) or (args, kwargs) first, second = other if isinstance(first, str): other_name = first if isinstance(second, tuple): other_args, other_kwargs = second, {} else: other_args, other_kwargs = (), second else: other_args, other_kwargs = first, second if self_name and other_name != self_name: return False # this order is important for ANY to work! return (other_args, other_kwargs) == (self_args, self_kwargs) def __ne__(self, other): return not self.__eq__(other) def __call__(self, *args, **kwargs): if self.name is None: return _Call(('', args, kwargs), name='()') name = self.name + '()' return _Call((self.name, args, kwargs), name=name, parent=self) def __getattr__(self, attr): if self.name is None: return _Call(name=attr, from_kall=False) name = '%s.%s' % (self.name, attr) return _Call(name=name, parent=self, from_kall=False) mock.call = _Call(from_kall=False) @contextlib.contextmanager def mkfile(path, mode="w"): Click.ensuredir(os.path.dirname(path)) with open(path, mode) as f: yield f @contextlib.contextmanager def mkfile_utf8(path, mode="w"): Click.ensuredir(os.path.dirname(path)) if sys.version < "3": import codecs with codecs.open(path, mode, "UTF-8") as f: yield f else: # io.open is available from Python 2.6, but we only use it with # Python 3 because it raises exceptions when passed bytes. import io with io.open(path, mode, encoding="UTF-8") as f: yield f def touch(path): with mkfile(path, mode="a"): pass click-0.4.21.1ubuntu0.2/click/tests/test_database.py0000664000000000000000000007173212320742132017007 0ustar # 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 . """Unit tests for click.database.""" from __future__ import print_function __metaclass__ = type __all__ = [ "TestClickDB", "TestClickSingleDB", ] from functools import partial from itertools import takewhile import json import os from gi.repository import Click from click.json_helpers import json_array_to_python, json_object_to_python 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 = 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.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")) os.makedirs(os.path.join(self.temp_dir, "a", "1.1")) a_current = os.path.join(self.temp_dir, "a", "current") os.symlink("1.1", a_current) os.makedirs(os.path.join(self.temp_dir, "b", "0.1")) b_current = os.path.join(self.temp_dir, "b", "current") os.symlink("0.1", b_current) os.makedirs(os.path.join(self.temp_dir, "c", "2.0")) self.assertEqual([ ("a", "1.1", a_current), ("b", "0.1", b_current), ], 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")) os.makedirs(os.path.join(self.temp_dir, "a", "1.1")) os.symlink("1.1", os.path.join(self.temp_dir, "a", "current")) os.makedirs(os.path.join(self.temp_dir, "b", "0.1")) os.symlink("0.1", os.path.join(self.temp_dir, "b", "current")) os.makedirs(os.path.join(self.temp_dir, "c", "2.0")) self.assertEqual([ ("a", "1.0", os.path.join(self.temp_dir, "a", "1.0")), ("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")), ], self._installed_packages_tuplify( self.db.get_packages(all_versions=True))) def test_packages_all_ignores_non_directory(self): os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) touch(os.path.join(self.temp_dir, "file")) self.assertEqual([ ("a", "1.0", os.path.join(self.temp_dir, "a", "1.0")), ], self._installed_packages_tuplify( self.db.get_packages(all_versions=True))) def test_manifest(self): manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") manifest_obj = {"name": "a", "version": "1.0", "hooks": {"a-app": {}}} with mkfile(manifest_path) as manifest: json.dump(manifest_obj, manifest) manifest_obj["_directory"] = os.path.join(self.temp_dir, "a", "1.0") self.assertEqual( manifest_obj, json_object_to_python(self.db.get_manifest("a", "1.0"))) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, self.db.get_manifest, "a", "1.1") self.assertEqual( manifest_obj, json.loads(self.db.get_manifest_as_string("a", "1.0"))) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, self.db.get_manifest_as_string, "a", "1.1") 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 test_gc_ignores_non_directory(self): 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) 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) touch(os.path.join(self.temp_dir, "file")) self.db.gc() self.assertTrue(os.path.exists(a_path)) def _make_ownership_test(self): path = os.path.join(self.temp_dir, "a", "1.0") touch(os.path.join(path, ".click", "info", "a.manifest")) os.symlink("1.0", os.path.join(self.temp_dir, "a", "current")) user_path = os.path.join( self.temp_dir, ".click", "users", "test-user", "a") os.makedirs(os.path.dirname(user_path)) os.symlink(path, user_path) touch(os.path.join(self.temp_dir, ".click", "log")) 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(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) self._make_ownership_test() self.db.ensure_ownership() 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): def setUp(self): 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) print("root = /a", file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = /b", file=b) 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_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 = Click.DB() self.assertEqual(0, db.props.size) def test_add(self): db = Click.DB() self.assertEqual(0, db.props.size) db.add("/new/root") 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: print("[Click Database]", file=f) print("root = /custom", file=f) 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 = 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: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) 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 = 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.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")) # The deepest copy of the same package/version is still preferred. self.assertEqual( os.path.join(self.temp_dir, "a", "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.get_path("pkg", "1.1")) def test_packages_current(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) 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 = 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")) pkg1_current = os.path.join(self.temp_dir, "b", "pkg1", "current") os.symlink("1.1", pkg1_current) os.makedirs(os.path.join(self.temp_dir, "b", "pkg2", "0.1")) pkg2_current = os.path.join(self.temp_dir, "b", "pkg2", "current") os.symlink("0.1", pkg2_current) self.assertEqual([ ("pkg1", "1.1", pkg1_current, True), ("pkg2", "0.1", pkg2_current, True), ], 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: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) 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 = Click.DB() db.read(db_dir=self.temp_dir) self.assertEqual([], list(db.get_packages(all_versions=True))) 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")) os.symlink("1.1", os.path.join(self.temp_dir, "b", "pkg1", "current")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg2", "0.1")) os.symlink("0.1", os.path.join(self.temp_dir, "b", "pkg2", "current")) self.assertEqual([ ("pkg1", "1.1", os.path.join(self.temp_dir, "b", "pkg1", "1.1"), True), ("pkg2", "0.1", os.path.join(self.temp_dir, "b", "pkg2", "0.1"), True), ("pkg1", "1.0", os.path.join(self.temp_dir, "a", "pkg1", "1.0"), False), ], self._installed_packages_tuplify( db.get_packages(all_versions=True))) def test_manifest(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) 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 = Click.DB() db.read(db_dir=self.temp_dir) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_manifest, "pkg", "1.0") self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_manifest_as_string, "pkg", "1.0") a_manifest_path = os.path.join( self.temp_dir, "a", "pkg", "1.0", ".click", "info", "pkg.manifest") a_manifest_obj = {"name": "pkg", "version": "1.0"} with mkfile(a_manifest_path) as a_manifest: json.dump(a_manifest_obj, a_manifest) a_manifest_obj["_directory"] = os.path.join( self.temp_dir, "a", "pkg", "1.0") self.assertEqual( a_manifest_obj, json_object_to_python(db.get_manifest("pkg", "1.0"))) self.assertEqual( a_manifest_obj, json.loads(db.get_manifest_as_string("pkg", "1.0"))) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_manifest, "pkg", "1.1") self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_manifest_as_string, "pkg", "1.1") b_manifest_path = os.path.join( self.temp_dir, "b", "pkg", "1.1", ".click", "info", "pkg.manifest") b_manifest_obj = {"name": "pkg", "version": "1.1"} with mkfile(b_manifest_path) as b_manifest: json.dump(b_manifest_obj, b_manifest) b_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg", "1.1") self.assertEqual( b_manifest_obj, json_object_to_python(db.get_manifest("pkg", "1.1"))) self.assertEqual( b_manifest_obj, json.loads(db.get_manifest_as_string("pkg", "1.1"))) def test_manifests_current(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) 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 = Click.DB() db.read(db_dir=self.temp_dir) self.assertEqual( [], json_array_to_python(db.get_manifests(all_versions=False))) self.assertEqual( [], json.loads(db.get_manifests_as_string(all_versions=False))) a_pkg1_manifest_path = os.path.join( self.temp_dir, "a", "pkg1", "1.0", ".click", "info", "pkg1.manifest") a_pkg1_manifest_obj = {"name": "pkg1", "version": "1.0"} with mkfile(a_pkg1_manifest_path) as a_pkg1_manifest: json.dump(a_pkg1_manifest_obj, a_pkg1_manifest) os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current")) b_pkg1_manifest_path = os.path.join( self.temp_dir, "b", "pkg1", "1.1", ".click", "info", "pkg1.manifest") b_pkg1_manifest_obj = {"name": "pkg1", "version": "1.1"} with mkfile(b_pkg1_manifest_path) as b_pkg1_manifest: json.dump(b_pkg1_manifest_obj, b_pkg1_manifest) os.symlink("1.1", os.path.join(self.temp_dir, "b", "pkg1", "current")) b_pkg2_manifest_path = os.path.join( self.temp_dir, "b", "pkg2", "0.1", ".click", "info", "pkg2.manifest") b_pkg2_manifest_obj = {"name": "pkg2", "version": "0.1"} with mkfile(b_pkg2_manifest_path) as b_pkg2_manifest: json.dump(b_pkg2_manifest_obj, b_pkg2_manifest) os.symlink("0.1", os.path.join(self.temp_dir, "b", "pkg2", "current")) b_pkg1_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg1", "1.1") b_pkg1_manifest_obj["_removable"] = 1 b_pkg2_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg2", "0.1") b_pkg2_manifest_obj["_removable"] = 1 self.assertEqual( [b_pkg1_manifest_obj, b_pkg2_manifest_obj], json_array_to_python(db.get_manifests(all_versions=False))) self.assertEqual( [b_pkg1_manifest_obj, b_pkg2_manifest_obj], json.loads(db.get_manifests_as_string(all_versions=False))) def test_manifests_all(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) 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 = Click.DB() db.read(db_dir=self.temp_dir) self.assertEqual( [], json_array_to_python(db.get_manifests(all_versions=True))) self.assertEqual( [], json.loads(db.get_manifests_as_string(all_versions=True))) a_pkg1_manifest_path = os.path.join( self.temp_dir, "a", "pkg1", "1.0", ".click", "info", "pkg1.manifest") a_pkg1_manifest_obj = {"name": "pkg1", "version": "1.0"} with mkfile(a_pkg1_manifest_path) as a_pkg1_manifest: json.dump(a_pkg1_manifest_obj, a_pkg1_manifest) os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current")) b_pkg1_manifest_path = os.path.join( self.temp_dir, "b", "pkg1", "1.1", ".click", "info", "pkg1.manifest") b_pkg1_manifest_obj = {"name": "pkg1", "version": "1.1"} with mkfile(b_pkg1_manifest_path) as b_pkg1_manifest: json.dump(b_pkg1_manifest_obj, b_pkg1_manifest) os.symlink("1.1", os.path.join(self.temp_dir, "b", "pkg1", "current")) b_pkg2_manifest_path = os.path.join( self.temp_dir, "b", "pkg2", "0.1", ".click", "info", "pkg2.manifest") b_pkg2_manifest_obj = {"name": "pkg2", "version": "0.1"} with mkfile(b_pkg2_manifest_path) as b_pkg2_manifest: json.dump(b_pkg2_manifest_obj, b_pkg2_manifest) os.symlink("0.1", os.path.join(self.temp_dir, "b", "pkg2", "current")) a_pkg1_manifest_obj["_directory"] = os.path.join( self.temp_dir, "a", "pkg1", "1.0") a_pkg1_manifest_obj["_removable"] = 0 b_pkg1_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg1", "1.1") b_pkg1_manifest_obj["_removable"] = 1 b_pkg2_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg2", "0.1") b_pkg2_manifest_obj["_removable"] = 1 self.assertEqual( [b_pkg1_manifest_obj, b_pkg2_manifest_obj, a_pkg1_manifest_obj], json_array_to_python(db.get_manifests(all_versions=True))) self.assertEqual( [b_pkg1_manifest_obj, b_pkg2_manifest_obj, a_pkg1_manifest_obj], json.loads(db.get_manifests_as_string(all_versions=True))) click-0.4.21.1ubuntu0.2/click/tests/test_static.py0000664000000000000000000000522212320742124016522 0ustar # 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 . """Test compliance with various static analysis tools.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestStatic', ] import os import sys from unittest import skipIf from pkg_resources import resource_filename try: import pep8 except ImportError: pep8 = None try: import pyflakes import pyflakes.api import pyflakes.reporter except ImportError: pyflakes = None from click.tests.helpers import TestCase class TestStatic(TestCase): def all_paths(self): paths = [] start_dir = os.path.dirname(resource_filename('click', '__init__.py')) for dirpath, dirnames, filenames in os.walk(start_dir): for ignore in ('doc', ".bzr", "__pycache__"): if ignore in dirnames: dirnames.remove(ignore) filenames = [ n for n in filenames if not n.startswith(".") and not n.endswith("~")] if dirpath.split(os.sep)[-1] == "bin": for filename in filenames: paths.append(os.path.join(dirpath, filename)) else: for filename in filenames: if filename.endswith(".py"): paths.append(os.path.join(dirpath, filename)) return paths @skipIf('SKIP_SLOW_TESTS' in os.environ, 'Skipping slow tests') @skipIf(pep8 is None, 'No pep8 package available') def test_pep8_clean(self): # https://github.com/jcrocholl/pep8/issues/103 pep8_style = pep8.StyleGuide(ignore='E123') result = pep8_style.check_files(self.all_paths()) self.assertEqual(result.total_errors, 0) @skipIf('SKIP_SLOW_TESTS' in os.environ, 'Skipping slow tests') @skipIf(pyflakes is None, 'No pyflakes package available') def test_pyflakes_clean(self): reporter = pyflakes.reporter.Reporter(sys.stdout, sys.stderr) warnings = pyflakes.api.checkRecursive(self.all_paths(), reporter) self.assertEqual(0, warnings) click-0.4.21.1ubuntu0.2/click/tests/test_build.py0000664000000000000000000003042312320742124016333 0ustar # 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 . """Unit tests for click.build.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickBuilder', 'TestClickSourceBuilder', ] import json import os import stat import subprocess import tarfile from textwrap import dedent from click.build import ClickBuildError, ClickBuilder, ClickSourceBuilder from click.preinst import static_preinst from click.tests.helpers import TestCase, mkfile, touch # BAW 2013-04-15: Some tests require umask 022. Use this decorator to # temporarily tweak the process's umask. The test -- or system -- should # probably be made more robust instead. def umask(force_umask): def decorator(func): def wrapper(*args, **kws): old_umask = os.umask(force_umask) try: return func(*args, **kws) finally: os.umask(old_umask) return wrapper return decorator class TestClickBuilderBaseMixin: def test_read_manifest(self): self.use_temp_dir() manifest_path = os.path.join(self.temp_dir, "manifest.json") with mkfile(manifest_path) as manifest: print(dedent("""\ { "name": "com.ubuntu.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "framework": "ubuntu-sdk-13.10" }"""), file=manifest) self.builder.read_manifest(manifest_path) self.assertEqual("com.ubuntu.test", self.builder.name) self.assertEqual("1.0", self.builder.version) self.assertEqual("Foo Bar ", self.builder.maintainer) self.assertEqual("test title", self.builder.title) self.assertEqual("all", self.builder.architecture) def test_add_file(self): self.builder.add_file("/nonexistent", "target") self.assertEqual({"/nonexistent": "target"}, self.builder.file_map) def test_epochless_version(self): self.use_temp_dir() manifest_path = os.path.join(self.temp_dir, "manifest.json") for version, epochless_version in ( ("1.0", "1.0"), ("1:1.2.3", "1.2.3"), ): with mkfile(manifest_path) as manifest: print(dedent("""\ { "name": "com.ubuntu.test", "version": "%s", "maintainer": "Foo Bar ", "title": "test title", "framework": "ubuntu-sdk-13.10" }""") % version, file=manifest) self.builder.read_manifest(manifest_path) self.assertEqual(epochless_version, self.builder.epochless_version) def test_manifest_syntax_error(self): self.use_temp_dir() manifest_path = os.path.join(self.temp_dir, "manifest.json") with mkfile(manifest_path) as manifest: # The comma after the "name" entry is intentionally missing. print(dedent("""\ { "name": "com.ubuntu.test" "version": "1.0" }"""), file=manifest) self.assertRaises( ClickBuildError, self.builder.read_manifest, manifest_path) class TestClickBuilder(TestCase, TestClickBuilderBaseMixin): def setUp(self): super(TestClickBuilder, self).setUp() self.builder = ClickBuilder() def extract_field(self, path, name): return subprocess.check_output( ["dpkg-deb", "-f", path, name], universal_newlines=True).rstrip("\n") @umask(0o22) def test_build(self): self.use_temp_dir() scratch = os.path.join(self.temp_dir, "scratch") with mkfile(os.path.join(scratch, "bin", "foo")) as f: f.write("test /bin/foo\n") os.symlink("foo", os.path.join(scratch, "bin", "bar")) touch(os.path.join(scratch, ".git", "config")) with mkfile(os.path.join(scratch, "toplevel")) as f: f.write("test /toplevel\n") with mkfile(os.path.join(scratch, "manifest.json")) as f: json.dump({ "name": "com.ubuntu.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "architecture": "all", "framework": "ubuntu-sdk-13.10", }, f) # build() overrides this back to 0o644 os.fchmod(f.fileno(), 0o600) self.builder.add_file(scratch, "/") path = os.path.join(self.temp_dir, "com.ubuntu.test_1.0_all.click") self.assertEqual(path, self.builder.build(self.temp_dir)) self.assertTrue(os.path.exists(path)) for key, value in ( ("Package", "com.ubuntu.test"), ("Version", "1.0"), ("Click-Version", "0.4"), ("Architecture", "all"), ("Maintainer", "Foo Bar "), ("Description", "test title"), ): self.assertEqual(value, self.extract_field(path, key)) self.assertNotEqual( "", self.extract_field(path, "Installed-Size")) control_path = os.path.join(self.temp_dir, "control") subprocess.check_call(["dpkg-deb", "-e", path, control_path]) manifest_path = os.path.join(control_path, "manifest") self.assertEqual(0o644, stat.S_IMODE(os.stat(manifest_path).st_mode)) with open(os.path.join(scratch, "manifest.json")) as source, \ open(manifest_path) as target: source_json = json.load(source) target_json = json.load(target) self.assertNotEqual("", target_json["installed-size"]) del target_json["installed-size"] self.assertEqual(source_json, target_json) with open(os.path.join(control_path, "md5sums")) as md5sums: self.assertRegex( md5sums.read(), r"^" r"eb774c3ead632b397d6450d1df25e001 bin/bar\n" r"eb774c3ead632b397d6450d1df25e001 bin/foo\n" r"49327ce6306df8a87522456b14a179e0 toplevel\n" r"$") with open(os.path.join(control_path, "preinst")) as preinst: self.assertEqual(static_preinst, preinst.read()) contents = subprocess.check_output( ["dpkg-deb", "-c", path], universal_newlines=True) self.assertRegex(contents, r"^drwxr-xr-x root/root 0 .* \./\n") self.assertRegex( contents, "\nlrwxrwxrwx root/root 0 .* \./bin/bar -> foo\n") self.assertRegex( contents, "\n-rw-r--r-- root/root 14 .* \./bin/foo\n") self.assertRegex( contents, "\n-rw-r--r-- root/root 15 .* \./toplevel\n") extract_path = os.path.join(self.temp_dir, "extract") subprocess.check_call(["dpkg-deb", "-x", path, extract_path]) for rel_path in ( os.path.join("bin", "foo"), "toplevel", ): with open(os.path.join(scratch, rel_path)) as source, \ open(os.path.join(extract_path, rel_path)) as target: self.assertEqual(source.read(), target.read()) self.assertTrue( os.path.islink(os.path.join(extract_path, "bin", "bar"))) self.assertEqual( "foo", os.readlink(os.path.join(extract_path, "bin", "bar"))) def test_build_excludes_dot_click(self): self.use_temp_dir() scratch = os.path.join(self.temp_dir, "scratch") touch(os.path.join(scratch, ".click", "evil-file")) with mkfile(os.path.join(scratch, "manifest.json")) as f: json.dump({ "name": "com.ubuntu.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "architecture": "all", "framework": "ubuntu-sdk-13.10", }, f) self.builder.add_file(scratch, "/") path = self.builder.build(self.temp_dir) extract_path = os.path.join(self.temp_dir, "extract") subprocess.check_call(["dpkg-deb", "-x", path, extract_path]) self.assertEqual([], os.listdir(extract_path)) def test_build_multiple_architectures(self): self.use_temp_dir() scratch = os.path.join(self.temp_dir, "scratch") with mkfile(os.path.join(scratch, "manifest.json")) as f: json.dump({ "name": "com.ubuntu.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "architecture": ["armhf", "i386"], "framework": "ubuntu-sdk-13.10", }, f) self.builder.add_file(scratch, "/") path = os.path.join(self.temp_dir, "com.ubuntu.test_1.0_multi.click") self.assertEqual(path, self.builder.build(self.temp_dir)) self.assertTrue(os.path.exists(path)) self.assertEqual("multi", self.extract_field(path, "Architecture")) control_path = os.path.join(self.temp_dir, "control") subprocess.check_call(["dpkg-deb", "-e", path, control_path]) manifest_path = os.path.join(control_path, "manifest") with open(os.path.join(scratch, "manifest.json")) as source, \ open(manifest_path) as target: source_json = json.load(source) target_json = json.load(target) del target_json["installed-size"] self.assertEqual(source_json, target_json) def test_build_multiple_frameworks(self): self.use_temp_dir() scratch = os.path.join(self.temp_dir, "scratch") with mkfile(os.path.join(scratch, "manifest.json")) as f: json.dump({ "name": "com.ubuntu.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "architecture": "all", "framework": "ubuntu-sdk-14.04-basic, ubuntu-sdk-14.04-webapps", }, f) self.builder.add_file(scratch, "/") self.assertRaisesRegex( ClickBuildError, 'Multiple dependencies in framework "ubuntu-sdk-14.04-basic, ' 'ubuntu-sdk-14.04-webapps" not yet allowed', self.builder.build, self.temp_dir) class TestClickSourceBuilder(TestCase, TestClickBuilderBaseMixin): def setUp(self): super(TestClickSourceBuilder, self).setUp() self.builder = ClickSourceBuilder() @umask(0o22) def test_build(self): self.use_temp_dir() scratch = os.path.join(self.temp_dir, "scratch") touch(os.path.join(scratch, "bin", "foo")) touch(os.path.join(scratch, ".git", "config")) with mkfile(os.path.join(scratch, "manifest.json")) as f: json.dump({ "name": "com.ubuntu.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "architecture": "all", "framework": "ubuntu-sdk-13.10", }, f) # build() overrides this back to 0o644 os.fchmod(f.fileno(), 0o600) self.builder.add_file(scratch, "./") path = os.path.join(self.temp_dir, "com.ubuntu.test_1.0.tar.gz") self.assertEqual(path, self.builder.build(self.temp_dir)) self.assertTrue(os.path.exists(path)) with tarfile.open(path, mode="r:gz") as tar: self.assertCountEqual( [".", "./bin", "./bin/foo", "./manifest.json"], tar.getnames()) self.assertTrue(tar.getmember("./bin/foo").isfile()) click-0.4.21.1ubuntu0.2/click/osextras.py0000664000000000000000000000513312320742124014703 0ustar # 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 . """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', 'find_on_path', 'unlink_force', ] import errno import os try: # Python 3.3 from shutil import which def find_on_path(command): # http://bugs.python.org/issue17012 path = os.environ.get('PATH', os.pathsep) return which(command, path=os.environ.get('PATH', path)) is not None except ImportError: # Python 2 def find_on_path(command): """Is command on the executable search path?""" if 'PATH' not in os.environ: return False path = os.environ['PATH'] for element in path.split(os.pathsep): if not element: continue filename = os.path.join(element, command) if os.path.isfile(filename) and os.access(filename, os.X_OK): return True return False def ensuredir(directory): if not os.path.isdir(directory): os.makedirs(directory) def listdir_force(directory): try: return os.listdir(directory) except OSError as e: if e.errno == errno.ENOENT: return [] raise def unlink_force(path): """Unlink path, without worrying about whether it exists.""" try: os.unlink(path) except OSError as e: if e.errno != errno.ENOENT: raise def symlink_force(source, link_name): """Create symlink link_name -> source, even if link_name exists.""" unlink_force(link_name) os.symlink(source, link_name) def get_umask(): mask = os.umask(0) os.umask(mask) return mask click-0.4.21.1ubuntu0.2/click/build.py0000664000000000000000000003044112320742124014132 0ustar # 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 . """Building Click packages.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'ClickBuildError', 'ClickBuilder', 'ClickSourceBuilder', ] import contextlib import hashlib import io import json import os import re import shutil import subprocess import sys import tarfile import tempfile from textwrap import dedent try: import apt_pkg apt_pkg.init_system() except ImportError: # "click build" is required to work with only the Python standard library. pass from click import osextras from click.arfile import ArFile from click.preinst import static_preinst from click.versions import spec_version @contextlib.contextmanager def make_temp_dir(): temp_dir = tempfile.mkdtemp(prefix="click") try: os.chmod(temp_dir, 0o755) yield temp_dir finally: shutil.rmtree(temp_dir) class FakerootTarFile(tarfile.TarFile): """A version of TarFile which pretends all files are owned by root:root.""" def gettarinfo(self, *args, **kwargs): tarinfo = super(FakerootTarFile, self).gettarinfo(*args, **kwargs) tarinfo.uid = tarinfo.gid = 0 tarinfo.uname = tarinfo.gname = "root" return tarinfo class ClickBuildError(Exception): pass class ClickBuilderBase: def __init__(self): self.file_map = {} def add_file(self, source_path, dest_path): self.file_map[source_path] = dest_path def read_manifest(self, manifest_path): with io.open(manifest_path, encoding="UTF-8") as manifest: try: self.manifest = json.load(manifest) except Exception as e: raise ClickBuildError( "Error reading manifest from %s: %s" % (manifest_path, e)) keys = sorted(self.manifest) for key in keys: if key.startswith("_"): print( "Ignoring reserved dynamic key '%s'." % key, file=sys.stderr) del self.manifest[key] @property def name(self): return self.manifest["name"] @property def version(self): return self.manifest["version"] @property def epochless_version(self): return re.sub(r"^\d+:", "", self.version) @property def maintainer(self): return self.manifest["maintainer"] @property def title(self): return self.manifest["title"] @property def architecture(self): manifest_arch = self.manifest.get("architecture", "all") if isinstance(manifest_arch, list): return "multi" else: return manifest_arch class ClickBuilder(ClickBuilderBase): # TODO: This should be configurable, or at least extensible. _ignore_patterns = [ "*.click", ".*.sw?", "*~", ",,*", ".[#~]*", ".arch-ids", ".arch-inventory", ".be", ".bzr", ".bzr-builddeb", ".bzr.backup", ".bzr.tags", ".bzrignore", ".cvsignore", ".git", ".gitattributes", ".gitignore", ".gitmodules", ".hg", ".hgignore", ".hgsigs", ".hgtags", ".shelf", ".svn", "CVS", "DEADJOE", "RCS", "_MTN", "_darcs", "{arch}", ] def list_files(self, root_path): for dirpath, _, filenames in os.walk(root_path): rel_dirpath = os.path.relpath(dirpath, root_path) if rel_dirpath == ".": rel_dirpath = "" for filename in filenames: yield os.path.join(rel_dirpath, filename) def _filter_dot_click(self, tarinfo): """Filter out attempts to include .click at the top level.""" if tarinfo.name == './.click' or tarinfo.name.startswith('./.click/'): return None return tarinfo def _pack(self, temp_dir, control_dir, data_dir, package_path): data_tar_path = os.path.join(temp_dir, "data.tar.gz") with contextlib.closing(FakerootTarFile.open( name=data_tar_path, mode="w:gz", format=tarfile.GNU_FORMAT )) as data_tar: data_tar.add(data_dir, arcname="./", filter=self._filter_dot_click) control_tar_path = os.path.join(temp_dir, "control.tar.gz") control_tar = tarfile.open( name=control_tar_path, mode="w:gz", format=tarfile.GNU_FORMAT) control_tar.add(control_dir, arcname="./") control_tar.close() with ArFile(name=package_path, mode="w") as package: package.add_magic() package.add_data("debian-binary", b"2.0\n") package.add_data( "_click-binary", ("%s\n" % spec_version).encode("UTF-8")) package.add_file("control.tar.gz", control_tar_path) package.add_file("data.tar.gz", data_tar_path) def _validate_framework(self, framework): """Apply policy checks to framework declarations.""" try: apt_pkg except NameError: return try: parsed_framework = apt_pkg.parse_depends(framework) except ValueError: raise ClickBuildError('Could not parse framework "%s"' % framework) if len(parsed_framework) > 1: raise ClickBuildError( 'Multiple dependencies in framework "%s" not yet allowed' % framework) for or_dep in parsed_framework: if len(or_dep) > 1: raise ClickBuildError( 'Alternative dependencies in framework "%s" not yet ' 'allowed' % framework) if or_dep[0][1] or or_dep[0][2]: raise ClickBuildError( 'Version relationship in framework "%s" not yet allowed' % framework) def build(self, dest_dir, manifest_path="manifest.json"): with make_temp_dir() as temp_dir: # Prepare data area. root_path = os.path.join(temp_dir, "data") for source_path, dest_path in self.file_map.items(): if dest_path.startswith("/"): dest_path = dest_path[1:] real_dest_path = os.path.join(root_path, dest_path) shutil.copytree( source_path, real_dest_path, symlinks=True, ignore=shutil.ignore_patterns(*self._ignore_patterns)) # Prepare control area. control_dir = os.path.join(temp_dir, "DEBIAN") osextras.ensuredir(control_dir) if os.path.isabs(manifest_path): full_manifest_path = manifest_path else: full_manifest_path = os.path.join(root_path, manifest_path) self.read_manifest(full_manifest_path) if "framework" in self.manifest: self._validate_framework(self.manifest["framework"]) du_output = subprocess.check_output( ["du", "-k", "-s", "--apparent-size", "."], cwd=temp_dir, universal_newlines=True).rstrip("\n") match = re.match(r"^(\d+)\s+\.$", du_output) if not match: raise Exception("du gave unexpected output '%s'" % du_output) installed_size = match.group(1) self.manifest["installed-size"] = installed_size control_path = os.path.join(control_dir, "control") osextras.ensuredir(os.path.dirname(control_path)) with io.open(control_path, "w", encoding="UTF-8") as control: print(dedent("""\ Package: %s Version: %s Click-Version: %s Architecture: %s Maintainer: %s Installed-Size: %s Description: %s""" % ( self.name, self.version, spec_version, self.architecture, self.maintainer, installed_size, self.title)), file=control) # Control file names must not contain a dot, hence "manifest" # rather than "manifest.json" in the control area. real_manifest_path = os.path.join(control_dir, "manifest") with io.open( real_manifest_path, "w", encoding="UTF-8") as manifest: print( json.dumps( self.manifest, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": ")), file=manifest) os.unlink(full_manifest_path) os.chmod(real_manifest_path, 0o644) md5sums_path = os.path.join(control_dir, "md5sums") with open(md5sums_path, "w") as md5sums: for path in sorted(self.list_files(root_path)): md5 = hashlib.md5() with open(os.path.join(root_path, path), "rb") as f: while True: buf = f.read(16384) if not buf: break md5.update(buf) print("%s %s" % (md5.hexdigest(), path), file=md5sums) preinst_path = os.path.join(control_dir, "preinst") with open(preinst_path, "w") as preinst: preinst.write(static_preinst) # Pack everything up. package_name = "%s_%s_%s.click" % ( self.name, self.epochless_version, self.architecture) package_path = os.path.join(dest_dir, package_name) self._pack(temp_dir, control_dir, root_path, package_path) return package_path class ClickSourceBuilder(ClickBuilderBase): # From @Dpkg::Source::Package::tar_ignore_default_pattern. # TODO: This should be configurable, or at least extensible. _ignore_patterns = [ "*.a", "*.click", "*.la", "*.o", "*.so", ".*.sw?", "*~", ",,*", ".[#~]*", ".arch-ids", ".arch-inventory", ".be", ".bzr", ".bzr-builddeb", ".bzr.backup", ".bzr.tags", ".bzrignore", ".cvsignore", ".deps", ".git", ".gitattributes", ".gitignore", ".gitmodules", ".hg", ".hgignore", ".hgsigs", ".hgtags", ".shelf", ".svn", "CVS", "DEADJOE", "RCS", "_MTN", "_darcs", "{arch}", ] def build(self, dest_dir, manifest_path=None): with make_temp_dir() as temp_dir: root_path = os.path.join(temp_dir, "source") for source_path, dest_path in self.file_map.items(): if dest_path.startswith("/"): dest_path = dest_path[1:] real_dest_path = os.path.join(root_path, dest_path) shutil.copytree( source_path, real_dest_path, symlinks=True, ignore=shutil.ignore_patterns(*self._ignore_patterns)) real_manifest_path = os.path.join(root_path, "manifest.json") if manifest_path is not None: shutil.copy2(manifest_path, real_manifest_path) os.chmod(real_manifest_path, 0o644) self.read_manifest(real_manifest_path) package_name = "%s_%s.tar.gz" % (self.name, self.epochless_version) package_path = os.path.join(dest_dir, package_name) with contextlib.closing(FakerootTarFile.open( name=package_path, mode="w:gz", format=tarfile.GNU_FORMAT )) as tar: tar.add(root_path, arcname="./") return package_path click-0.4.21.1ubuntu0.2/click/paths.py.in0000664000000000000000000000134112320742124014554 0ustar # 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 paths.""" preload_path = "@pkglibdir@/libclickpreload.so" click-0.4.21.1ubuntu0.2/click/preinst.py0000664000000000000000000000356212320742124014523 0ustar # 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 . """Preinst for Click packages. In general there is a rule that Click packages may not have maintainer scripts. However, there is one exception: a static preinst used to cause dpkg to fail if people attempt to install Click packages directly using dpkg rather than via "click install". This avoids accidents, since Click packages use a different root of their filesystem tarball. """ from __future__ import print_function __metaclass__ = type __all__ = [ 'static_preinst', 'static_preinst_matches', ] _older_static_preinst = """\ #! /bin/sh echo "Click packages may not be installed directly using dpkg." echo "Use click-install instead." exit 1 """ _old_static_preinst = """\ #! /bin/sh echo "Click packages may not be installed directly using dpkg." echo "Use 'click-package install' instead." exit 1 """ static_preinst = """\ #! /bin/sh echo "Click packages may not be installed directly using dpkg." echo "Use 'click install' instead." exit 1 """ def static_preinst_matches(preinst): for allow_preinst in ( _older_static_preinst, _old_static_preinst, static_preinst, ): if preinst == allow_preinst.encode(): return True return False click-0.4.21.1ubuntu0.2/doc/0000775000000000000000000000000012320742335012143 5ustar click-0.4.21.1ubuntu0.2/doc/Makefile0000664000000000000000000001273012320742124013602 0ustar # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ClickPackages.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ClickPackages.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/ClickPackages" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ClickPackages" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." click-0.4.21.1ubuntu0.2/doc/_static/0000775000000000000000000000000012320742335013571 5ustar click-0.4.21.1ubuntu0.2/doc/constraints.rst0000664000000000000000000000220512320742124015237 0ustar ================== Design constraints ================== Building packages ================= * Building packages should not require any more than the Python standard library. In particular, it should not require dpkg, python-debian, or any other such Debian-specific tools. Rationale: We want people to be able to build Click packages easily on any platform (or at least any platform that can manage a Python installation, which is not too onerous a requirement). Installing packages =================== * For the purpose of rapid prototyping, package installation is also implemented in Python. This may of course use Debian/Ubuntu-specific tools, since it will always be running on an Ubuntu system. In future, it will probably be re-implemented in C for performance. * Reading the system dpkg database is forbidden. This is partly to ensure strict separation, and partly because the system dpkg database is large and therefore slow to read. * Nothing should require root, although it may be acceptable to make use of root-only facilities if available (but remembering to pay attention to performance). click-0.4.21.1ubuntu0.2/doc/_build/0000775000000000000000000000000012320742335013401 5ustar click-0.4.21.1ubuntu0.2/doc/todo.rst0000664000000000000000000000304412320742124013637 0ustar ===== To do ===== * hook that gets notified about all installations * dbus interface etc. as backend for UI * method may not be feasible because caller may want to go away * but where do we send a completion/failure signal back to? * some way to manage shared data files * association with developer ID, to allow sharing of data * debug symbols * define exit statuses for "click install" * command to generate manifest template, like ``dh_make`` * check whether a package contains compiled code for an architecture not listed in the "architecture" manifest field Delta updates ============= It would be helpful to have some kind of delta update format. Tools such as ``rsync`` and ``zsync`` are probably the wrong answer. There's no particular reason to keep the .click file around as an rsync target, particularly since the unpacked application directory is kept pristine, and many devices won't have the kind of disk space where you want to keep 4.2GB files around just for the sake of it. We could do something ad-hoc with ``xdelta`` or ``bsdiff`` or whatever. `debdelta `_ seems like a good possibility. We're already using the .deb format, and debdelta is capable of doing patch upgrades without having the old .deb around (though it will need minor adjustments to cope with the different installation location of Click packages). Under the hood, it uses xdelta/bsdiff/etc. and can be extended with other backends if need be. If we used this then we could take advantage of a good deal of existing code. click-0.4.21.1ubuntu0.2/doc/manpage.rst0000664000000000000000000002614512320742124014311 0ustar ===== click ===== SYNOPSIS ======== click command [options] [arguments] DESCRIPTION =========== *Click* is a packaging format for Ubuntu Touch applications, independent of the packaging format used to deliver the underlying system. The *click* program is the basic tool used to build, install, remove, and otherwise manipulate these packages. *click*'s various functions are available via a number of commands, described in detail below. While *click* supports per-user installation, packages are normally unpacked as a special ``clickpkg`` user, to ensure that applications cannot modify their own code; it is a design goal to ensure that *click* can be used to install untrusted code which is then confined using `AppArmor `_. As such, *click* should normally be run as root (e.g. using ``sudo``) when installing packages; it will drop privileges as needed. COMMAND OVERVIEW ================ :: click build DIRECTORY click buildsource DIRECTORY click chroot click contents PATH click hook install HOOK click hook remove HOOK click hook run-system click hook run-user click info PATH click install PACKAGE-FILE click list click pkgdir {PACKAGE-NAME|PATH} click register PACKAGE-NAME VERSION click unregister PACKAGE-NAME [VERSION] click verify PACKAGE-FILE COMMANDS ======== click build DIRECTORY --------------------- Build a Click package from the contents of DIRECTORY. The build directory must contain a JSON-formatted manifest, described further in Click's file-format documentation; by default, this is expected to be in ``manifest.json`` at the top level of the build directory. The resulting ``.click`` file is written to the current directory, so to avoid confusion you should generally ensure that your working directory is not inside the build directory when running this command. While it is possible to build a new version of a Click package by unpacking and repacking an existing package, this is not normally recommended because it requires some care to put the manifest file back in the right place. It is best to keep your application's code in separate revision control rather than relying on recovering it from packages. Options: -m PATH, --manifest=PATH Read package manifest from PATH (default: ``manifest.json``). click buildsource DIRECTORY --------------------------- Build a source package in ``.tar.gz`` format from the contents of DIRECTORY. This allows you to distribute source code in the case where your package contains compiled code (and so the Click package does not constitute its own source). The resulting ``.tar.gz`` file is written to the current directory, so to avoid confusion you should generally ensure that your working directory is not inside the build directory when running this command. Options: -m PATH, --manifest=PATH Read package manifest from PATH (default: ``manifest.json``). click chroot ------------ Manage chroot environments for cross-building Click packages. Options: -a ARCH, --architecture ARCH Set the target architecture. -f FRAMEWORK, --framework FRAMEWORK Set the target framework (default: ubuntu-sdk-13.10). -s SERIES, --series SERIES Set the target series for newly-created chroots (default: a series appropriate for the framework). This option is mainly for debugging; use -f instead. Subcommands: begin-session SESSION Begin a persistent chroot session. create Create a chroot. destroy Destroy a chroot. end-session SESSION End a persistent chroot session. install PACKAGES Install packages in the chroot. maint [-n SESSION] COMMAND ARGUMENTS Run a maintenance command in the chroot. Unlike ``run``, this runs its command as root inside the chroot, and its effects on the chroot will persist after ``click chroot maint`` exits. If a session name is given, run the command in that session. The session must previously have been created by ``click chroot begin-session``. run [-n SESSION] COMMAND ARGUMENTS Run a program in the chroot. If a session name is given, run the command in that session. The session must previously have been created by ``click chroot begin-session``. upgrade Upgrade the chroot. click contents PATH ------------------- Display the contents of the Click package in PATH as a file listing. click hook install HOOK ----------------------- Install files associated with HOOK for any Click packages that attach to it. This is normally only called by maintainer scripts of system packages, by way of dh_click(1). Options: --root=PATH Look for additional packages in PATH. click hook remove HOOK ---------------------- Remove files associated with HOOK for any Click packages that attach to it. This is normally only called by maintainer scripts of system packages, by way of dh_click(1). Options: --root=PATH Look for additional packages in PATH. click hook run-system ------------------------- Run all system-level hooks for all installed Click 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. Options: --root=PATH Look for additional packages in PATH. click hook run-user ----------------------- Run all user-level hooks for all Click packages registered for a given user. This is useful at session startup to catch up with packages that may have been preinstalled and registered for all users. Options: --root=PATH Look for additional packages in PATH. --user=USER Run user-level hooks for USER (default: current user). click info {PACKAGE-NAME|PACKAGE-FILE} -------------------------------------- When given a package name (that is, a string containing no ``/`` characters), display the manifest for that package, if it is registered for the current user. When given a path (that is, a string containing at least one ``/`` character, or a string containing no ``/`` characters that is not a registered package name), attempt to treat that as a path to a file containing a Click package and display the manifest for that package. Options: --root=PATH Look for additional packages in PATH. --user=USER List packages registered by USER (if you have permission). click install PACKAGE-FILE -------------------------- Install the Click package in PACKAGE-FILE. This is a low-level tool; to install a package as an ordinary user you should generally use ``pkcon install-local PACKAGE-FILE`` or some higher-level user interface instead, which take care to use the correct set of options. (Do not use ``sudo`` when invoking ``pkcon``, as it needs to know the calling user.) ``click install`` may be used to preinstall a package in an image such that it will be available to all users by default. When doing this, you should normally install it to one of the databases defined in ``/etc/click/databases/`` other than the default of ``/opt/click.ubuntu.com``. For example: sudo click install --root=/custom/click --all-users foo.click The ``--force-missing-framework`` option is necessary while working with development versions of SDKs which have not yet put a framework declaration in place. Options: --root=PATH Install packages underneath PATH. --force-missing-framework Install despite missing system framework. --user=USER Register package for USER. --all-users Register package for all users. click list ---------- Display a list of installed packages, either as one package per line with each line containing a package name and version separated by a tab (the default), or as a JSON array of manifests. By default, ``click list`` shows only packages registered for the current user. The ``--all`` option causes it to show all installed packages, regardless of user registrations. Options: --root=PATH Look for additional packages in PATH. --all List all installed packages. --user=USER List packages registered by USER (if you have permission). --manifest Format output as a JSON array of manifests. click pkgdir {PACKAGE-NAME|PATH} -------------------------------- When given a package name (that is, a string containing no ``/`` characters), display the directory where that package is installed, if it is registered for the current user. When given a path (that is, a string containing at least one ``/`` character), attempt to treat that as a path to a file within a Click package and print the top-level directory where that package is installed, if one exists. This is particularly useful in hooks that need to find the top-level package directory based on a symbolic link to a single file within it. Exits zero if and only if a directory for the given package name or path was found. Options: --root=PATH Look for additional packages in PATH. --user=USER List packages registered by USER (if you have permission). click register PACKAGE-NAME VERSION ----------------------------------- Register an installed Click package for a user. This will normally cause user-level hooks to be run for that user, which are needed for things such as making the application's ``.desktop`` file available to the user interface. Options: --root=PATH Look for additional packages in PATH. --user=USER Register package for USER (default: current user). --all-users Register package for all users. click unregister PACKAGE-NAME [VERSION] --------------------------------------- Unregister an installed Click package for a user, and remove it entirely if no other users still have it registered and if it does not appear to be running. This will normally cause user-level hooks to be run for that user, which are needed for things such as removing the application's ``.desktop`` file from the user interface. If a version is specified, then the registered version must match it in order to be removed. Options: --root=PATH Look for additional packages in PATH. --user=USER Unregister package for USER (default: ``$SUDO_USER``, if known). --all-users Unregister package that was previously registered for all users. click verify PACKAGE-FILE ------------------------- Verify the Click package in PACKAGE-FILE. The ``--force-missing-framework`` option is necessary while working with development versions of SDKs which have not yet put a framework declaration in place. Options: --root=PATH Install packages underneath PATH. --force-missing-framework Install despite missing system framework. click-0.4.21.1ubuntu0.2/doc/index.rst0000664000000000000000000000527712320742124014013 0ustar .. Click Packages documentation master file, created by sphinx-quickstart on Mon Apr 15 11:34:57 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. ============== Click packages ============== *Click* is the code name used to describe a packaging format for Ubuntu mobile applications. This format specifies how individual apps are delivered to mobile devices, how they are packed into distributable format, and how they are installed on a mobile device by a system provided package manager. At a minimum they assume that a system framework exists providing all the necessary infrastructure and dependencies needed in order to install and run such apps. The click packaging format is completely independent from facilities to do full-system installations or upgrades. Compatibility ============= Currently, this package should remain compatible with Python 2.7, 3.2, 3.3, and 3.4; Ubuntu 12.04 LTS, Ubuntu 13.10, and Ubuntu 14.04 LTS. Dependencies ------------ For Ubuntu 13.10, make sure you have the *python2.7* and *python3.3* packages installed. Unless you upgraded from a previous version of Ubuntu and haven't removed it yet, you won't have Python 3.2 available. Build it from source if necessary, install them say into ``/usr/local``, and make sure it is on your ``$PATH``. You'll need *gcc* in order to build the preload shared library. Assuming you have this, do the following:: $ (cd preload && make) You'll need *tox* (Ubuntu package *python-tox*) installed in order to run the full test suite. You should be able to just say:: $ tox to run the full suite. Use tox's ``-e`` option to run the tests against a subset of Python versions. You shouldn't have to install anything manually into the virtual environments that tox creates, but you might have to if you don't have all the dependencies installed in your system Pythons. You'll need the *mock* and *python-debian* libraries. For Ubuntu 13.10, apt-get install the following packages:: * python-mock * python-debian * python3-debian Testing ======= After all of the above is installed, you can run ``tox`` to run the test suite against all supported Python versions. The ``./run-tests`` scripts just does an additional check to make sure you've got the preload shared library built. Documentation ============= To build the HTML version of the documentation, you'll need Sphinx (Ubuntu package *python-sphinx*). Then do:: $ (cd doc && make html) Contents: .. toctree:: :maxdepth: 2 file-format.rst constraints.rst hooks.rst databases.rst todo.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` click-0.4.21.1ubuntu0.2/doc/databases.rst0000664000000000000000000000676212320742124014633 0ustar ========= Databases ========= (This is a lightly-edited copy of a brain-dump sent by Colin Watson to the ubuntu-phone mailing list, preserved here since it may be useful.) Click has multiple databases where packages may be unpacked: by default we have the "core" database for core apps (``/usr/share/click/preinstalled/``), the "custom" database for carrier/OEM customisations (``/custom/click/``), and the "default" database for user-installed applications (``/opt/click.ubuntu.com/``), although these are configurable in ``/etc/click/databases/``. Each database may have multiple unpacked versions of any given package. Each database may also have user registrations, which live in ``.click/users/`` relative to the database root. Each user has a subdirectory of that, which contains symlinks to the versions of each package they have registered. This means that on a tablet, say, I can install an app without it also showing up on my children's accounts; they'd need to install it separately, although the disk space for the unpacked copy of the app would be shared. There was an idea early on that we'd deal with preinstalled apps by going round and registering them all for all active users on first boot. This would have lots of problems for the packaging system, though. Most notably, doing it that way makes it hard for a user to remove an app and make it stick, because it would tend to reappear on system updates. You can probably fudge your way around this somehow, but it gets very fiddly and easy to get wrong. What we do instead is: we have an ``@all`` pseudo-user which you can register packages for, typically in the core database (``click register --root=/usr/share/click/preinstalled --all-users``). If a user wants to remove a package, we do this by creating a deliberately broken symlink pointing to ``@hidden`` in their user registration area in ``/opt/click.ubuntu.com/.click/users/$USERNAME/``. When click is asked to list the set of packages for a given user, it walks its way down the list of databases from top (default) to bottom (core). For each database, it checks registrations for that user, followed by registrations for ``@all``. It takes the first registration for any given package name that it finds. If that registration is ``@hidden``, then it ignores the package, otherwise it must be a link to the unpacked copy of the appropriate version of the package. There are still some things that can't be done just with static files in the image and instead have to be done at boot time and on session startup: we have to make sure the right AppArmor profiles are loaded, do things to the user's home directory like creating .desktop files, and that kind of thing. We run ``click hook run-system`` at boot time and ``click hook run-user`` on session startup, and these deal with running hooks for whatever packages are visible in context, according to the rules above. The effect of all this is that we can hide a core app for a carrier by doing this as root when preparing their custom overlay image:: click unregister --root=/custom/click --all-users PACKAGE-NAME This will create a symlink ``/custom/click/.click/users/@all/PACKAGE-NAME`` pointing to ``@hidden``. Unless a user explicitly installs the app in question, the effect of this will be that it's as if the app just isn't there. It shouldn't incur any more than a negligible cost at startup (basically just a readlink call); at the moment I think we might still create an AppArmor profile for it, which isn't free, but that can be fixed easily enough. click-0.4.21.1ubuntu0.2/doc/file-format.rst0000664000000000000000000002067312320742124015106 0ustar ======================================== "Click" package file format, version 0.4 ======================================== This specification covers a packaging format intended for use by self-contained third-party applications. It is intentionally designed to make it easy to create such packages and for the archive of packages to be able to scale to very large numbers, as well as to ensure that packages do not execute any unverified code as root during installation and that installed packages are sandboxable. This implementation proposal uses the existing dpkg as its core, although that is entirely concealed from both users and application developers. The author believes that using something based on dpkg will allow us to reuse substantial amounts of package-management-related code elsewhere, not least the many years of careful design and bug-fixing of dpkg itself; although there are clearly several things we need to adjust. General format ============== The top-level binary format for Click packages is an ar archive containing control and data tar archives, as for .deb packages: see deb(5) for full details. The deb(5) format permits the insertion of underscore-prefixed ar members, so a "_click-binary" member should be inserted immediately after "debian-binary"; its contents should be the current version number of this specification followed by a newline. This makes it possible to assign a MIME type to Click packages without having to rely solely on their extension. Despite the similar format, the file extension for these packages is .click, to discourage attempts to install using dpkg directly (although it is still possible to use dpkg to inspect these files). Click packages should not be thought of as .deb packages, although they share tooling. Do not rely on the file extension remaining .click; it may change in the future. Control area ============ control ------- Every Click package must include the following control fields: * Click-Version: the current version number of this specification The package manager must refuse to process packages where any of these fields are missing or unparseable. It must refuse to process packages where Click-Version compares newer than the corresponding version it implements (according to rules equivalent to "dpkg --compare-versions"). It may refuse to process packages whose Click-Version field has an older major number than the version it implements (although future developers are encouraged to maintain the maximum possible degree of compatibility with packages in the wild). Several other fields are copied from the manifest, to ease interoperation with Debian package manipulation tools. The manifest is the primary location for these fields, and Click-aware tools must not rely on their presence in the control file. All dependency relations are forbidden. Packages implicitly depend on the entire contents of the Click system framework they declare. manifest -------- There must be a "manifest" file in the control area (typically corresponding to "manifest.json" in source trees), which must be a dictionary represented as UTF-8-encoded JSON. It must include the following keys: * name: unique name for the application * version: version number of the application * framework: the system framework(s) for which the package was built * installed-size: the size of the unpacked package in KiB; this should not be set directly in the source tree, but will be generated automatically by "click build" using "du -k -s --apparent-size" The package manager must refuse to process packages where any of these fields are missing or unparseable. It must refuse to process packages where the value of "framework" does not declare a framework implemented by the system on which the package is being installed. The value of "name" identifies the application, following Debian source package name rules; every package in the app store has a unique "name" identifier, and the app store will reject clashes. It is the developer's responsibility to choose a unique identifier. The recommended approach is to follow the Java package name convention, i.e. "com.mydomain.myapp", starting with the reverse of an Internet domain name owned by the person or organisation developing the application; note that it is not necessary for the application to contain any Java code in order to use this convention. The value of "version" provides a unique version for the application, following Debian version numbering rules. See deb-version(5) for full details. The syntax of "framework" is formally that of a Debian dependency relationship field. Currently, only a simple name is permitted, e.g. "framework": "ubuntu-sdk-13.10", or a list of simple names all of which must be satisfied, e.g. "framework": "ubuntu-sdk-14.04-qml, ubuntu-sdk-14.04-webapps"; version relationships and alternative dependencies are not currently allowed. At the moment, ``click build`` will only allow building with a single simple name, pending policy decisions. The manifest may contain arbitrary additional optional keys; new optional keys may be defined without changing the version number of this specification. The following are currently recognised: * title: short (one-line) synopsis of the application * description: extended description of the application; may be multi-paragraph * maintainer: name and email address of maintainer of the application * architecture: one of the following: * "all", indicating a package containing no compiled code * a dpkg architecture name (e.g. "armhf") as a string, indicating a package that will only run on that architecture * a list of dpkg architecture names, indicating a package that will run on any of those architectures * hooks: see :doc:`hooks` * icon: icon to display in interfaces listing click packages; if the name refers to an existing file when resolved relative to the base directory of the package, the given file will be used; if not, the algorithm described in the `Icon Theme Specification `_ will be used to locate the icon Keys beginning with the two characters "x-" are reserved for local extensions: this file format will never define such keys to have any particular meaning. Keys beginning with an underscore ("_") are reserved for use as dynamic properties of installed packages. They must not appear in packages' manifest files, and attempts to set them there will be ignored. The following dynamic keys are currently defined: * _directory: the directory where a package is unpacked * _removable: 1 if a package is unpacked in a location from which it can be removed, otherwise 0 (this may be changed to a proper boolean in future; client code should be careful to permit either) Maintainer scripts ------------------ Maintainer scripts are forbidden, with one exception: see below. (If they are permitted in future, they will at most be required to consist only of verified debhelper-generated fragments that can be statically analysed.) Packages in Click system frameworks are encouraged to provide file triggers where appropriate (e.g. "interest /usr/share/facility"); these will be processed as normal for dpkg file triggers. The exception to maintainer scripts being forbidden is that a Click package may contain a preinst script with the effect of causing direct calls to dpkg to refuse to install it. The package manager must enforce the permitted text of this script. Data area ========= Unlike .debs, each package installs in a self-contained directory, and the filesystem tarball must be based at the root of that directory. The package must not assume any particular installation directory: if it needs to know where it is installed, it should look at argv[0] or similar. Within each package installation directory, the ".click" subdirectory will be used for metadata. This directory must not be present at the top level of package filesystem tarballs; the package manager should silently filter it out if present. (Rationale: scanning the filesystem tarball in advance is likely to impose a performance cost, especially for large packages.) The package manager should ensure that all unpacked files and directories are group- and world-readable, and (if owner-executable) also group- and world-executable. (Rationale: since packages are unpacked as a dedicated user not used when running applications, and since packages cannot write to their own unpack directories, any files that aren't world-readable are unusable.) click-0.4.21.1ubuntu0.2/doc/hooks.rst0000664000000000000000000002471012320742124014020 0ustar ===== Hooks ===== Rationale --------- Of course, any sensible packaging format needs a hook mechanism of some kind; just unpacking a filesystem tarball isn't going to cut it. But part of the point of Click packages is to make packages easier to audit by removing their ability to run code at installation time. How do we resolve this? For most application packages, the code that needs to be run is to integrate with some system package; for instance, a package that provides an icon may need to update icon caches. Thus, the best way to achieve both these goals at once is to make sure the code for this is always in the integrated-with package. dpkg triggers are useful prior art for this approach. In general they get a lot of things right. The code to process a trigger runs in the postinst, which encourages an approach where trigger processing is a subset of full package configuration and shares code with it. Furthermore, the express inability to pass any user data through the trigger activation mechanism itself ensures that triggers must operate in a "catch up" style, ensuring that whatever data store they manage is up to date with the state of the parts of the file system they use as input. This naturally results in a system where the user can install integrating and integrated-with packages in either order and get the same result, a valuable property which developers are nevertheless unlikely to test explicitly in every case and which must therefore be encouraged by design. There are two principal problems with dpkg triggers (aside from the point that not all integrated-with packages use them, which is irrelevant because they don't support any hypothetical future hook mechanisms either). The first is that the inability to pass user data through trigger activation means that there is no way to indicate where an integrating package is installed, which matters when the hook files it provides cannot be in a single location under /usr/ but might be under /opt/ or even in per-user directories. The second is that processing dpkg triggers requires operating on the system dpkg database, which is large and therefore slow. Let us consider an example of the sort that might in future be delivered as a Click package, and one which is simple but not too simple. Our example package (com.ubuntu.example) delivers an AppArmor profile and two .desktop files. These are consumed by apparmor and desktop-integration (TBD) respectively, and each lists the corresponding directory looking for files to consume. We must assume that in the general case it will be at least inconvenient to cause the integrated-with packages to look in multiple directories, especially when the list of possible directories is not fixed, so we need a way to cause files to exist in those directories. On the other hand, we cannot unpack directly into those directories, because that takes us back to using dpkg itself, and is incompatible with system image updates where the root file system is read-only. What we can do with reasonable safety is populate symlink farms. Specification ------------- * Only system packages (i.e. .debs) may declare hooks. Click packages must be declarative in that they may not include code executed outside AppArmor confinement, which precludes declaring hooks. * "System-level hooks" are those which operate on the full set of installed package/version combinations. They may run as any (system) user. (Example: AppArmor profile handling.) * "User-level hooks" are those which operate on the set of packages registered by a given user. They run as that user, and thus would generally be expected to keep their state in the user's home directory or some similar user-owned file system location. (Example: desktop file handling.) * System-level and user-level hooks share a namespace. * A Click package may contain one or more applications (the common case will be only one). Each application has a name. * An "application ID" is a string unique to each application instance: it is made up of the Click package name, the application name (must consist only of characters for a Debian source package name, Debian version and [A-Z]), and the Click package version joined by underscores, e.g. ``com.ubuntu.clock_alarm_0.1``. * A "short application ID" is a string unique to each application, but not necessarily to each instance of it: it is made up of the Click package name and the application name (must consist only of characters for a Debian source package name, Debian version and [A-Z]) joined by an underscore, e.g. ``com.ubuntu.clock_alarm``. It is only valid in user-level hooks, or in system-level hooks with ``Single-Version: yes``. * An integrated-with system package may add ``*.hook`` files to ``/usr/share/click/hooks/``. These are standard Debian-style control files with the following keys: User-Level: yes (optional) If the ``User-Level`` key is present with the value ``yes``, the hook is a user-level hook. Pattern: (required) The value of ``Pattern`` is a string containing one or more substitution placeholders, as follows: ``${id}`` The application ID. ``${short-id}`` The short application ID (user-level or single-version hooks only). ``${user}`` The user name (user-level hooks only). ``${home}`` The user's home directory (user-level hooks only). ``$$`` The character '``$``'. At least one ``${id}`` or ``${short-id}`` substitution is required. For user-level hooks, at least one of ``${user}`` and ``${home}`` must be present. On install, the package manager creates the target path as a symlink to a path provided by the Click package; on upgrade, it changes the target path to be a symlink to the path in the new version of the Click package; on removal, it unlinks the target path. The terms "install", "upgrade", and "removal" are taken to refer to the status of the hook rather than of the package. That is, when upgrading between two versions of a package, if the old version uses a given hook but the new version does not, then that is a removal; if the old version does not use a given hook but the new version does, then that is an install; if both versions use a given hook, then that is an upgrade. For system-level hooks, one target path exists for each unpacked version, unless "``Single-Version: yes``" is used (see below). For user-level hooks, a target path exists only for the current version registered by each user for each package. Upgrades of user-level hooks may leave the symlink pointed at the same target (since the target will itself be via a ``current`` symlink in the user registration directory). ``Exec`` commands in hooks should take care to check the modification timestamp of the target. Exec: (optional) If the ``Exec`` key is present, its value is executed as if passed to the shell after the above symlink is modified. A non-zero exit status is an error; hook implementors must be careful to make commands in ``Exec`` fields robust. Note that this command intentionally takes no arguments, and will be run on install, upgrade, and removal; it must be written such that it causes the system to catch up with the current state of all installed hooks. ``Exec`` commands must be idempotent. Trigger: yes (optional) It will often be valuable to execute a dpkg trigger after installing a Click package to avoid code duplication between system and Click package handling, although we must do so asynchronously and any errors must not block the installation of Click packages. If "``Trigger: yes``" is set in a ``*.hook`` file, then "``click install``" will activate an asynchronous D-Bus service at the end of installation, passing the names of all the changed paths resulting from Pattern key expansions; this will activate any file triggers matching those paths, and process all the packages that enter the triggers-pending state as a result. User: (required, system-level hooks only) System-level hooks are run as the user whose name is specified as the value of ``User``. There is intentionally no default for this key, to encourage hook authors to run their hooks with the least appropriate privilege. Single-Version: yes (optional, system-level hooks only) By default, system-level hooks support multiple versions of packages, so target paths may exist at multiple versions. "``Single-Version: yes``" causes only the current version of each package to have a target path. Hook-Name: (optional) The value of ``Hook-Name`` is the name that Click packages may use to attach to this hook. By default, this is the base name of the ``*.hook`` file, with the ``.hook`` extension removed. Multiple hooks may use the same hook-name, in which case all those hooks will be run when installing, upgrading, or removing a Click package that attaches to that name. * A Click package may attach to zero or more hooks, by including a "hooks" entry in its manifest. If present, this must be a dictionary mapping application names to hook sets; each hook set is itself a dictionary mapping hook names to paths. The hook names are used to look up ``*.hook`` files with matching hook-names (see ``Hook-Name`` above). The paths are relative to the directory where the Click package is unpacked, and are used as symlink targets by the package manager when creating symlinks according to the ``Pattern`` field in ``*.hook`` files. * There is a dh_click program which installs the ``*.hook`` files in system packages and adds maintainer script fragments to cause click to catch up with any newly-provided hooks. It may be invoked using ``dh $@ --with click``. Examples -------- :: /usr/share/click/hooks/apparmor.hook: Pattern: /var/lib/apparmor/clicks/${id}.json Exec: /usr/bin/aa-clickhook User: root /usr/share/click/hooks/click-desktop.hook: User-Level: yes Pattern: /opt/click.ubuntu.com/.click/desktop-files/${user}_${id}.desktop Exec: click desktophook Hook-Name: desktop com.ubuntu.example/manifest.json: "hooks": { "example-app": { "apparmor": "apparmor/example-app.json", "desktop": "example-app.desktop" } } TODO: copy rather than symlink, for additional robustness? click-0.4.21.1ubuntu0.2/doc/_templates/0000775000000000000000000000000012320742335014300 5ustar click-0.4.21.1ubuntu0.2/doc/conf.py0000664000000000000000000002121712320742124013441 0ustar # -*- coding: utf-8 -*- # # Click Packages documentation build configuration file, created by # sphinx-quickstart on Mon Apr 15 11:34:57 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. from datetime import datetime import io import os import re import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Click Packages' copyright = u'2013, Canonical Ltd.' changelog_header = re.compile(r'\w[-+0-9a-z.]* \(([^\(\) \t]+)\)') changelog_trailer = re.compile( r'^ \-\- .* <.*> ?((\w+\,\s*)?\d{1,2}\s+\w+\s+\d{4}\s+\d{1,2}:\d\d:\d\d)' r'\s+[-+]\d{4}(\s+\([^\\\(\)]\))?\s*$') with io.open('../debian/changelog', encoding="UTF-8") as changelog: line = changelog.readline() match = changelog_header.match(line) if match is None: raise ValueError( "Failed to parse first line of debian/changelog: '%s'" % line) click_version = match.group(1) for line in changelog: match = changelog_trailer.match(line) if match is not None: click_datetime = datetime.strptime( match.group(1), "%a, %d %b %Y %H:%M:%S") break elif changelog_header.match(line) is not None: break else: raise ValueError( "Failed to find trailer line in first entry of debian/changelog") # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = ".".join(click_version.split(".", 2)[:2]) # The full version, including alpha/beta/rc tags. release = click_version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: today = click_datetime.strftime("%Y-%m-%d") # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'ClickPackagesdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'ClickPackages.tex', u'Click Packages Documentation', u'Colin Watson', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('manpage', 'click', u'package management tool for Ubuntu Touch', [u'Colin Watson'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Click', u'Click Packages Documentation', u'Colin Watson', 'Click', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' click-0.4.21.1ubuntu0.2/run-click0000775000000000000000000000013412320742124013205 0ustar #! /bin/sh set -e LD_LIBRARY_PATH=lib/click/.libs GI_TYPELIB_PATH=lib/click bin/click "$@" click-0.4.21.1ubuntu0.2/debhelper/0000775000000000000000000000000012320742335013330 5ustar click-0.4.21.1ubuntu0.2/debhelper/click.pm0000664000000000000000000000023412320742124014746 0ustar #! /usr/bin/perl # debhelper sequence file for click use warnings; use strict; use Debian::Debhelper::Dh_Lib; insert_after("dh_install", "dh_click"); 1; click-0.4.21.1ubuntu0.2/debhelper/Makefile.am0000664000000000000000000000047212320742124015363 0ustar perllibdir = $(perl_vendorlib)/Debian/Debhelper/Sequence debhelperdir = $(datadir)/debhelper/autoscripts bin_SCRIPTS = dh_click man1_MANS = dh_click.1 dist_perllib_DATA = click.pm dist_debhelper_DATA = postinst-click prerm-click CLEANFILES = $(man1_MANS) %.1: % pod2man -c Debhelper -r $(PACKAGE_VERSION) $< $@ click-0.4.21.1ubuntu0.2/debhelper/postinst-click0000664000000000000000000000013512320742124016214 0ustar if [ "$1" = "configure" ] && which click >/dev/null 2>&1; then click hook install #HOOK# fi click-0.4.21.1ubuntu0.2/debhelper/prerm-click0000664000000000000000000000010212320742124015450 0ustar if which click >/dev/null 2>&1; then click hook remove #HOOK# fi click-0.4.21.1ubuntu0.2/debhelper/dh_click0000775000000000000000000000447012320742124015017 0ustar #! /usr/bin/perl -w =head1 NAME dh_click - install system hooks for click =cut use strict; use Debian::Debhelper::Dh_Lib; =head1 SYNOPSIS B [S>] =head1 DESCRIPTION dh_click is a debhelper program that is responsible for installing system hooks for B. It also automatically generates the F and F commands needed to interface with the Ubuntu B package. These commands are inserted into the maintainer scripts by L. =head1 FILES =over 4 =item debian/I.click-hook Click package hook files, installed into usr/share/click/hooks/I.hook in the package build directory. See F for their format. =back =head1 OPTIONS =over 4 =item B<-n>, B<--noscripts> Do not modify F/F scripts. =item B<--name=>I Install the hook using the filename I instead of the default filename, which is the package name. When this parameter is used, B looks for and installs files named F, instead of the usual F. =back =head1 EXAMPLES dh_click is usually called indirectly in a rules file via the dh command. %: dh $@ --with click You must build-depend on at least debhelper (>= 7.0.8) to use this form, and in any case you must build-depend on click-dev to use this program at all. It can also be called directly at any time before C, usually in a binary-arch or binary-indep rule. =cut init(); # PROMISE: DH NOOP WITHOUT click-hook foreach my $package (@{$dh{DOPACKAGES}}) { my $tmp=tmpdir($package); my $click_hook=pkgfile($package,"click-hook"); my $hookname=$package; if (defined $dh{NAME}) { $hookname=$dh{NAME}; } if ($click_hook ne '') { if (! -d "$tmp/usr/share/click/hooks") { doit("install","-d","$tmp/usr/share/click/hooks"); } doit("install","-p","-m644",$click_hook,"$tmp/usr/share/click/hooks/$hookname.hook"); if (! $dh{NOSCRIPTS}) { autoscript($package,"postinst","postinst-click","s/#HOOK#/$hookname/"); autoscript($package,"prerm","prerm-click","s/#HOOK#/$hookname/"); } } } =head1 SEE ALSO L This program is a part of click. =head1 AUTHOR Colin Watson Copyright (C) 2013 Canonical Ltd., licensed under the GNU GPL v3. =cut click-0.4.21.1ubuntu0.2/run-tests0000775000000000000000000000035212320742124013264 0ustar #! /bin/sh # Depends: # python, # python-mock, # python3, # python3 (>= 3.3) | python3-mock # python-tox if ! [ -r preload/.libs/libclickpreload.so ]; then echo "W: preload bits not built; will skip some tests" >&2 fi tox click-0.4.21.1ubuntu0.2/tox.ini0000664000000000000000000000017212320742124012705 0ustar [tox] envlist = py27,py32,py33,py34 [testenv] commands = python -m unittest discover -vv click.tests sitepackages = True click-0.4.21.1ubuntu0.2/configure.ac0000664000000000000000000001107312320742124013662 0ustar AC_INIT([click],m4_esyscmd([./get-version]),[https://launchpad.net/click]) AC_CONFIG_SRCDIR([preload/clickpreload.c]) AC_CONFIG_AUX_DIR([build-aux]) 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]) IT_PROG_INTLTOOL AC_PROG_CC CFLAGS="$CFLAGS -Wall" AC_ARG_WITH([python-interpreters], [AS_HELP_STRING([--with-python-interpreters], [install for these Python interpreters (space-separated, default: python3)])], [PYTHON_INTERPRETERS="$withval"], [PYTHON_INTERPRETERS=python3]) AC_SUBST([PYTHON_INTERPRETERS]) AC_ARG_WITH([default-root], [AS_HELP_STRING([--with-default-root], [set default root path for installed packages (default: /opt/click.ubuntu.com)])], [DEFAULT_ROOT="$withval"], [DEFAULT_ROOT=/opt/click.ubuntu.com]) AC_SUBST([DEFAULT_ROOT]) click_save_LIBS="$LIBS" AC_SEARCH_LIBS([dlopen], [dl]) AC_SUBST([PRELOAD_LIBS], ["$LIBS"]) LIBS="$click_save_LIBS" AC_CACHE_CHECK([for Perl vendor library directory], [click_cv_perl_vendorlib], [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]) if test "x$enable_packagekit" = xyes; then PKG_CHECK_MODULES([PKPLUGIN], [ gio-2.0 glib-2.0 >= 2.34 gobject-2.0 >= 2.28 json-glib-1.0 >= 0.10 packagekit-plugin >= 0.8.10 ]) AC_SUBST([PKPLUGIN_CFLAGS]) AC_SUBST([PKPLUGIN_LIBS]) AC_CACHE_CHECK([for packagekit-plugin library directory], [click_cv_pkpluginlibdir], [click_cv_pkpluginlibdir=`$PKG_CONFIG --variable=libdir packagekit-plugin`]) AC_SUBST([pkpluginlibdir], ["$click_cv_pkpluginlibdir"]) 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 ]) AC_CONFIG_FILES([lib/click/valac-wrapper], [chmod +x lib/click/valac-wrapper]) AC_CONFIG_FILES([setup.py], [chmod +x setup.py]) AC_OUTPUT click-0.4.21.1ubuntu0.2/lib/0000775000000000000000000000000012320742335012144 5ustar click-0.4.21.1ubuntu0.2/lib/Makefile.am0000664000000000000000000000002012320742124014164 0ustar SUBDIRS = click click-0.4.21.1ubuntu0.2/lib/click/0000775000000000000000000000000012320742335013231 5ustar click-0.4.21.1ubuntu0.2/lib/click/query.vala0000664000000000000000000000300212320742124015232 0ustar /* 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 (is_dir (info_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); } } click-0.4.21.1ubuntu0.2/lib/click/framework.vala0000664000000000000000000000625112320742132016072 0ustar /* 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 . */ /* Click frameworks. */ namespace Click { public errordomain FrameworkError { /** * Requested framework does not exist. */ NO_SUCH_FRAMEWORK, /** * Missing hook field. */ MISSING_FIELD } public class Framework : Object { public string name { get; construct; } private Gee.Map fields; private Framework (string name) { Object (name: name); } /** * Framework.open: * @name: The name of the framework to open. * * Returns: (transfer full): A newly-allocated #Click.Framework. * * Since: 0.4.18 */ public static Framework open (string name) throws FrameworkError { var path = Path.build_filename (get_frameworks_dir (), @"$name.framework"); try { var framework = new Framework (name); framework.fields = parse_deb822_file (path); return framework; } catch (Error e) { throw new FrameworkError.NO_SUCH_FRAMEWORK ("No click framework '%s' installed", name); } } /** * has_framework: * @name: A framework name. * * Returns: True if a framework by this name exists, otherwise false. * * Since: 0.4.18 */ public static bool has_framework (string name) { var path = Path.build_filename (get_frameworks_dir (), @"$name.framework"); return exists (path); } /** * get_frameworks: * * Returns: (element-type ClickFramework) (transfer full): A #List * of all #Click.Framework instances installed on the system. * * Since: 0.4.18 */ public static List get_frameworks () { var ret = new List (); Click.Dir dir; try { dir = Click.Dir.open (get_frameworks_dir ()); } catch (FileError e) { return ret; } foreach (var entry in dir) { if (! entry.has_suffix (".framework")) continue; try { ret.prepend (open (entry[0:-10])); } catch (Error e) { continue; } } ret.reverse (); return ret; } /** * get_fields: * * Returns: A list of field names defined by this framework. * * Since: 0.4.18 */ 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 FrameworkError { string value = fields[key.down ()]; if (value == null) throw new FrameworkError.MISSING_FIELD ("Framework '%s' has no field named '%s'", name, key); return value; } public string? get_base_name () { return fields["base-name"]; } public string? get_base_version () { return fields["base-version"]; } } } click-0.4.21.1ubuntu0.2/lib/click/click-0.4.pc.in0000664000000000000000000000172212320742124015544 0ustar # 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/ Requires.private: glib-2.0 gobject-2.0 json-glib-1.0 Libs: -L${libdir} -lclick-0.4 Cflags: -I${includedir}/click-0.4 click-0.4.21.1ubuntu0.2/lib/click/database.vala0000664000000000000000000005241612320742132015645 0ustar /* 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, /** * Package manifest cannot be parsed. */ BAD_MANIFEST } 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); } /** * has_package_version: * @package: A package name. * @version: A version string. * * Returns: True if this version of this package is unpacked in this * database, otherwise false. * * Since: 0.4.18 */ public bool has_package_version (string package, string version) { try { get_path (package, version); return true; } catch (DatabaseError e) { return false; } } /** * 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); if (! is_dir (package_path)) continue; foreach (var version in Click.Dir.open (package_path)) { var version_path = Path.build_filename (package_path, version); if (is_symlink (version_path) || ! is_dir (version_path)) continue; ret.prepend(new InstalledPackage (package, version, version_path)); } } else { var current_path = Path.build_filename (root, package, "current"); if (! is_symlink (current_path)) continue; var version = FileUtils.read_link (current_path); if (! ("/" in version)) ret.prepend(new InstalledPackage (package, version, current_path)); } } ret.reverse (); return ret; } /** * get_manifest: * @package: A package name. * @version: A version string. * * Returns: A #Json.Object containing the manifest of this version * of this package. The manifest may include additional dynamic * keys (starting with an underscore) corresponding to dynamic * properties of installed packages. * * Since: 0.4.18 */ public Json.Object get_manifest (string package, string version) throws DatabaseError { /* Extract the raw manifest from the file system. */ var path = get_path (package, version); var manifest_path = Path.build_filename (path, ".click", "info", @"$package.manifest"); var parser = new Json.Parser (); try { parser.load_from_file (manifest_path); } catch (Error e) { throw new DatabaseError.BAD_MANIFEST ("Failed to parse manifest in %s: %s", manifest_path, e.message); } var node = parser.get_root (); if (node.get_node_type () != Json.NodeType.OBJECT) throw new DatabaseError.BAD_MANIFEST ("Manifest in %s is not a JSON object", manifest_path); var manifest = node.dup_object (); /* Set up dynamic keys. */ var to_remove = new List (); foreach (var name in manifest.get_members ()) { if (name.has_prefix ("_")) to_remove.prepend (name); } foreach (var name in to_remove) manifest.remove_member (name); manifest.set_string_member ("_directory", path); return manifest; } /** * get_manifest_as_string: * @package: A package name. * @version: A version string. * * Returns: A JSON string containing the serialised manifest of this * version of this package. The manifest may include additional * dynamic keys (starting with an underscore) corresponding to * dynamic properties of installed packages. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifest_as_string (string package, string version) throws DatabaseError { var manifest = get_manifest (package, version); var node = new Json.Node (Json.NodeType.OBJECT); node.set_object (manifest); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } /* * 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 (is_symlink (current_path) && 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); if (! is_dir (package_path)) continue; 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 (is_dir (path)) 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 { foreach (var single_db in db) { try { return single_db.get_path (package, version); } catch (DatabaseError e) { } } throw new DatabaseError.DOES_NOT_EXIST ("%s %s does not exist in any database", package, version); } /** * has_package_version: * @package: A package name. * @version: A version string. * * Returns: True if this version of this package is unpacked, * otherwise false. */ public bool has_package_version (string package, string version) { try { get_path (package, version); return true; } catch (DatabaseError e) { return false; } } /** * 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; } /** * get_manifest: * @package: A package name. * @version: A version string. * * Returns: A #Json.Object containing the manifest of this version * of this package. * * Since: 0.4.18 */ public Json.Object get_manifest (string package, string version) throws DatabaseError { foreach (var single_db in db) { try { return single_db.get_manifest (package, version); } catch (DatabaseError e) { if (e is DatabaseError.BAD_MANIFEST) throw e; } } throw new DatabaseError.DOES_NOT_EXIST ("%s %s does not exist in any database", package, version); } /** * get_manifest_as_string: * @package: A package name. * @version: A version string. * * Returns: A JSON string containing the serialised manifest of this * version of this package. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifest_as_string (string package, string version) throws DatabaseError { var manifest = get_manifest (package, version); var node = new Json.Node (Json.NodeType.OBJECT); node.set_object (manifest); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } /** * get_manifests: * @all_versions: If true, return manifests for all versions, not * just current ones. * * Returns: A #Json.Array containing manifests of all packages in * this database. The manifest may include additional dynamic keys * (starting with an underscore) corresponding to dynamic properties * of installed packages. * * Since: 0.4.18 */ public Json.Array get_manifests (bool all_versions = false) throws Error { var ret = new Json.Array (); foreach (var inst in get_packages (all_versions)) { Json.Object obj; try { obj = get_manifest (inst.package, inst.version); } catch (DatabaseError e) { warning ("%s", e.message); continue; } /* This should really be a boolean, but it was * mistakenly made an int when the "_removable" key * was first created. We may change this in future. */ obj.set_int_member ("_removable", inst.writeable ? 1 : 0); ret.add_object_element (obj); } return ret; } /** * get_manifests_as_string: * @all_versions: If true, return manifests for all versions, not * just current ones. * * Returns: A JSON string containing a serialised array of manifests * of all packages in this database. The manifest may include * additional dynamic keys (starting with an underscore) * corresponding to dynamic properties of installed packages. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifests_as_string (bool all_versions = false) throws Error { var manifests = get_manifests (all_versions); var node = new Json.Node (Json.NodeType.ARRAY); node.set_array (manifests); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } 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 (); } } } click-0.4.21.1ubuntu0.2/lib/click/Makefile.am0000664000000000000000000000423212320742124015262 0ustar 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 \ deb822.vala \ framework.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 \ deb822.c \ framework.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 $@ click-0.4.21.1ubuntu0.2/lib/click/hooks.vala0000664000000000000000000007444412320742132015231 0ustar /* 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, /** * Hook command failed. */ COMMAND_FAILED, /** * Some hooks were not run successfully. */ INCOMPLETE } 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 { internal get; construct; } private Gee.Map fields; private Hook (DB db, string name) { Object (db: db, name: name); } /** * 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 = parse_deb822_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 = parse_deb822_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); try { Process.check_exit_status (exit_status); } catch (Error e) { throw new HooksError.COMMAND_FAILED ("Hook command '%s' failed: %s", fields["exec"], e.message); } } 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 (is_symlink (link) && 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 (true)) 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); var overlay_path = Path.build_filename (user_db.get_overlay_db (), package); user_db.drop_privileges (); try { if (exists (overlay_path)) user_db.raw_set_version (package, version); 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 (); string[] failed = {}; foreach (var hook in Hook.open_all (db)) { if (! hook.is_user_level) { try { hook.sync (); } catch (HooksError e) { warning ("System-level hook %s failed: %s", hook.name, e.message); failed += hook.name; } } } if (failed.length != 0) throw new HooksError.INCOMPLETE ("Some system-level hooks failed: %s", string.joinv (", ", failed)); } /** * 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 (); string[] failed = {}; foreach (var hook in Hook.open_all (db)) { if (hook.is_user_level) { try { hook.sync (user_name); } catch (HooksError e) { warning ("User-level hook %s failed: %s", hook.name, e.message); failed += hook.name; } } } if (failed.length != 0) throw new HooksError.INCOMPLETE ("Some user-level hooks failed: %s", string.joinv (", ", failed)); } } click-0.4.21.1ubuntu0.2/lib/click/osextras.vala0000664000000000000000000001143512320742124015746 0ustar /* 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 (is_dir (directory)) 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); } private bool is_dir (string path) { return FileUtils.test (path, FileTest.IS_DIR); } } click-0.4.21.1ubuntu0.2/lib/click/click.sym0000664000000000000000000000514412320742132015047 0ustar click_database_error_quark click_db_add click_db_ensure_ownership click_db_gc click_db_get click_db_get_manifest click_db_get_manifest_as_string click_db_get_manifests click_db_get_manifests_as_string click_db_get_overlay click_db_get_packages click_db_get_path click_db_get_size click_db_get_type click_db_has_package_version 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_framework_error_quark click_framework_get_base_name click_framework_get_base_version click_framework_get_fields click_framework_get_field click_framework_get_frameworks click_framework_get_name click_framework_get_type click_framework_has_framework click_framework_open click_get_db_dir click_get_frameworks_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_manifest click_single_db_get_manifest_as_string click_single_db_get_packages click_single_db_get_path click_single_db_get_root click_single_db_get_type click_single_db_has_package_version 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_manifest click_user_get_manifest_as_string click_user_get_manifests click_user_get_manifests_as_string 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 click-0.4.21.1ubuntu0.2/lib/click/deb822.vala0000664000000000000000000000353212320742124015063 0ustar /* 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 . */ /* Simple deb822-like file parsing. */ namespace Click { private Regex? field_re = null; private Regex? blank_re = null; /** * parse_deb822_file: * @path: Path to a file. * * A very simple deb822-like file parser. * * Note that this only supports a single paragraph composed only of simple * (non-folded/multiline) fields, which is fortunately all we need in Click. * * Returns: A mapping of field names to values. */ private Gee.Map parse_deb822_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; } } click-0.4.21.1ubuntu0.2/lib/click/user.vala0000664000000000000000000004623212320742132015056 0ustar /* 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 (is_dir (path)) { 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 (is_dir (path)) /* 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); } /** * raw_set_version: * @package: A package name. * @version: A version string. * * Set the version of @package to @version, without running any * hooks. Must be run with dropped privileges. */ internal void raw_set_version (string package, string version) throws Error { assert (dropped_privileges_count > 0); var user_db = get_overlay_db (); var path = Path.build_filename (user_db, package); var new_path = Path.build_filename (user_db, @".$package.new"); var target = db.get_path (package, version); var done = false; if (is_valid_link (path)) { unlink_force (path); try { if (get_version (package) == version) done = true; } catch (UserError e) { } } if (done) return; 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)); } /** * 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. */ ensure_db (); string? old_version = null; try { old_version = get_version (package); } catch (UserError e) { } drop_privileges (); try { raw_set_version (package, version); } 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); } /** * get_manifest: * @package: A package name. * * Returns: A #Json.Object containing a package's manifest. * * Since: 0.4.18 */ public Json.Object get_manifest (string package) throws Error { var obj = db.get_manifest (package, get_version (package)); /* Adjust _directory to point to the user registration path. */ obj.set_string_member ("_directory", get_path (package)); /* This should really be a boolean, but it was mistakenly * made an int when the "_removable" key was first created. * We may change this in future. */ obj.set_int_member ("_removable", is_removable (package) ? 1 : 0); return obj; } /** * get_manifest_as_string: * @package: A package name. * * Returns: A JSON string containing a package's serialised * manifest. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifest_as_string (string package) throws Error { var manifest = get_manifest (package); var node = new Json.Node (Json.NodeType.OBJECT); node.set_object (manifest); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } /** * get_manifests: * * Returns: A #Json.Array containing manifests of all packages * registered for this user. The manifest may include additional * dynamic keys (starting with an underscore) corresponding to * dynamic properties of installed packages. * * Since: 0.4.18 */ public Json.Array get_manifests () throws Error /* API-compatibility */ { var ret = new Json.Array (); foreach (var package in get_package_names ()) { try { ret.add_object_element (get_manifest (package)); } catch (Error e) { warning ("%s", e.message); } } return ret; } /** * get_manifests_as_string: * * Returns: A JSON string containing a serialised array of manifests * of all packages registered for this user. The manifest may * include additional dynamic keys (starting with an underscore) * corresponding to dynamic properties of installed packages. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifests_as_string () throws Error /* API-compatibility */ { var manifests = get_manifests (); var node = new Json.Node (Json.NodeType.ARRAY); node.set_array (manifests); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } /** * 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; } } } click-0.4.21.1ubuntu0.2/lib/click/paths.vala.in0000664000000000000000000000247312320742124015624 0ustar /* 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"; internal static const string frameworks_dir = "@pkgdatadir@/frameworks"; /** * 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; } /** * get_frameworks_dir: * * Returns: The Click frameworks directory. */ public string get_frameworks_dir () { return frameworks_dir; } } click-0.4.21.1ubuntu0.2/lib/click/valac-wrapper.in0000664000000000000000000000303312320742124016320 0ustar #! /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/\( * * 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); } click-0.4.21.1ubuntu0.2/build-aux/0000775000000000000000000000000012320742335013270 5ustar click-0.4.21.1ubuntu0.2/MANIFEST.in0000664000000000000000000000003312320742124013124 0ustar recursive-include debian * click-0.4.21.1ubuntu0.2/preload/0000775000000000000000000000000012320742335013024 5ustar click-0.4.21.1ubuntu0.2/preload/Makefile.am0000664000000000000000000000025612320742124015057 0ustar pkglib_LTLIBRARIES = libclickpreload.la libclickpreload_la_SOURCES = clickpreload.c libclickpreload_la_LIBADD = @PRELOAD_LIBS@ libclickpreload_la_LDFLAGS = -avoid-version click-0.4.21.1ubuntu0.2/preload/clickpreload.c0000664000000000000000000003335612320742124015632 0ustar /* 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 . */ /* Stub out a few syscalls that are unhelpful when installing Click * packages. This is roughly akin to the effect of using all of fakechroot, * fakeroot, and eatmydata, but a few orders of magnitude simpler. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include static int (*libc_chmod) (const char *, mode_t) = (void *) 0; static int (*libc_chown) (const char *, uid_t, gid_t) = (void *) 0; static int (*libc_execvp) (const char *, char * const []) = (void *) 0; static int (*libc_fchmod) (int, mode_t) = (void *) 0; static int (*libc_fchown) (int, uid_t, gid_t) = (void *) 0; static FILE *(*libc_fopen) (const char *, const char *) = (void *) 0; static FILE *(*libc_fopen64) (const char *, const char *) = (void *) 0; static struct group *(*libc_getgrnam) (const char *) = (void *) 0; static struct passwd *(*libc_getpwnam) (const char *) = (void *) 0; static int (*libc_lchown) (const char *, uid_t, gid_t) = (void *) 0; static int (*libc_link) (const char *, const char *) = (void *) 0; static int (*libc_mkdir) (const char *, mode_t) = (void *) 0; static int (*libc_mkfifo) (const char *, mode_t) = (void *) 0; static int (*libc_mknod) (const char *, mode_t, dev_t) = (void *) 0; static int (*libc_open) (const char *, int, mode_t) = (void *) 0; static int (*libc_open64) (const char *, int, mode_t) = (void *) 0; static int (*libc_symlink) (const char *, const char *) = (void *) 0; static int (*libc___xstat) (int, const char *, struct stat *) = (void *) 0; static int (*libc___xstat64) (int, const char *, struct stat64 *) = (void *) 0; uid_t euid; struct passwd root_pwd; struct group root_grp; const char *base_path; size_t base_path_len; const char *package_path; int package_fd; #define GET_NEXT_SYMBOL(name) \ do { \ libc_##name = dlsym (RTLD_NEXT, #name); \ if (dlerror ()) \ _exit (1); \ } while (0) static void __attribute__ ((constructor)) clickpreload_init (void) { const char *package_fd_str; /* Clear any old error conditions, albeit unlikely, as per dlsym(2) */ dlerror (); GET_NEXT_SYMBOL (chmod); GET_NEXT_SYMBOL (chown); GET_NEXT_SYMBOL (execvp); GET_NEXT_SYMBOL (fchmod); GET_NEXT_SYMBOL (fchown); GET_NEXT_SYMBOL (fopen); GET_NEXT_SYMBOL (fopen64); GET_NEXT_SYMBOL (getgrnam); GET_NEXT_SYMBOL (getpwnam); GET_NEXT_SYMBOL (lchown); GET_NEXT_SYMBOL (link); GET_NEXT_SYMBOL (mkdir); GET_NEXT_SYMBOL (mkfifo); GET_NEXT_SYMBOL (mknod); GET_NEXT_SYMBOL (open); GET_NEXT_SYMBOL (open64); GET_NEXT_SYMBOL (symlink); GET_NEXT_SYMBOL (__xstat); GET_NEXT_SYMBOL (__xstat64); euid = geteuid (); /* dpkg only cares about these fields. */ root_pwd.pw_uid = 0; root_grp.gr_gid = 0; base_path = getenv ("CLICK_BASE_DIR"); base_path_len = base_path ? strlen (base_path) : 0; package_path = getenv ("CLICK_PACKAGE_PATH"); package_fd_str = getenv ("CLICK_PACKAGE_FD"); package_fd = atoi (package_fd_str); } /* dpkg calls chown/fchown/lchown to set permissions of extracted files. If * we aren't running as root, we don't care. */ int chown (const char *path, uid_t owner, gid_t group) { if (euid != 0) return 0; if (!libc_chown) clickpreload_init (); return (*libc_chown) (path, owner, group); } int fchown (int fd, uid_t owner, gid_t group) { if (euid != 0) return 0; if (!libc_fchown) clickpreload_init (); return (*libc_fchown) (fd, owner, group); } int lchown (const char *path, uid_t owner, gid_t group) { if (euid != 0) return 0; if (!libc_lchown) clickpreload_init (); return (*libc_lchown) (path, owner, group); } /* Similarly, we don't much care about passwd/group lookups when we aren't * root. (This could be more sanely replaced by having dpkg cache those * lookups itself.) */ struct passwd *getpwnam (const char *name) { if (!libc_getpwnam) clickpreload_init (); /* also needed for root_pwd */ if (euid != 0) return &root_pwd; return (*libc_getpwnam) (name); } struct group *getgrnam (const char *name) { if (!libc_getgrnam) clickpreload_init (); /* also needed for root_grp */ if (euid != 0) return &root_grp; return (*libc_getgrnam) (name); } /* dpkg calls chroot to run maintainer scripts when --instdir is used (which * we use so that we can have independently-rooted filesystem tarballs). * However, there is exactly one maintainer script ever used by Click * packages, and that's a static preinst which doesn't touch the filesystem * except to be executed with /bin/sh. Chrooting for this causes more * problems than it solves. */ int chroot (const char *path) { return 0; } /* dpkg executes the static preinst. We don't want it. */ int execvp (const char *file, char * const argv[]) { if (strcmp (file, "/.click/tmp.ci/preinst") == 0) _exit (0); if (!libc_execvp) clickpreload_init (); return (*libc_execvp) (file, argv); } /* dpkg calls fsync/sync_file_range quite a lot. However, Click packages * never correspond to essential system facilities, so it's OK to compromise * perfect write reliability in the face of hostile filesystem * implementations for performance. * * (Note that dpkg only started using fsync/sync_file_range relatively * recently, and on many reasonable filesystem configurations using those * functions buys us nothing; most of dpkg's reliability comes from other * strategies, such as careful unpack and renaming into place.) */ int fsync (int fd) { return 0; } int sync_file_range(int fd, off64_t offset, off64_t nbytes, unsigned int flags) { return 0; } /* Sandboxing: * * We try to insulate against dpkg getting confused enough by malformed * archives to write outside the instdir. This is not full confinement, and * generally for system security it should be sufficient to run "click * install" as a specialised user; as such we don't necessarily wrap all * possible relevant functions here. The main purpose of this is just to * provide a useful error message if dpkg gets confused. */ static void clickpreload_assert_path_in_instdir (const char *verb, const char *pathname) { if (strncmp (pathname, base_path, base_path_len) == 0 && (pathname[base_path_len] == '\0' || pathname[base_path_len] == '/')) return; /* When building click in a chroot with pkgbinarymangler, dpkg-deb is in * fact a wrapper shell script, and bash checks at startup whether it * can open /dev/tty for writing. This is harmless, so allow it. */ if (strcmp (verb, "write-open") == 0 && strcmp (pathname, "/dev/tty") == 0) return; fprintf (stderr, "Sandbox failure: 'click install' not permitted to %s '%s'\n", verb, pathname); exit (1); } int link (const char *oldpath, const char *newpath) { if (!libc_link) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("make hard link", newpath); return (*libc_link) (oldpath, newpath); } int mkdir (const char *pathname, mode_t mode) { if (!libc_mkdir) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("mkdir", pathname); return (*libc_mkdir) (pathname, mode); } int mkfifo (const char *pathname, mode_t mode) { if (!libc_mkfifo) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("mkfifo", pathname); return (*libc_mkfifo) (pathname, mode); } int mknod (const char *pathname, mode_t mode, dev_t dev) { if (!libc_mknod) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("mknod", pathname); return (*libc_mknod) (pathname, mode, dev); } int symlink (const char *oldpath, const char *newpath) { if (!libc_symlink) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("make symbolic link", newpath); return (*libc_symlink) (oldpath, newpath); } /* As well as write sandboxing, our versions of fopen, open, and stat also * trap accesses to the package path and turn them into accesses to a fixed * file descriptor instead. With some cooperation from click.install, this * allows dpkg to read packages in paths not readable by the clickpkg user. * * We cannot do this entirely perfectly. In particular, we have to seek to * the start of the file on open, but the file offset is shared among all * duplicates of a file descriptor. Let's hope that dpkg doesn't open the * .deb multiple times and expect to have independent file offsets ... */ FILE *fopen (const char *pathname, const char *mode) { int for_reading = (strncmp (mode, "r", 1) == 0 && strncmp (mode, "r+", 2) != 0); if (!libc_fopen) clickpreload_init (); /* also needed for package_path */ if (for_reading && package_path && strcmp (pathname, package_path) == 0) { int dup_fd = dup (package_fd); lseek (dup_fd, 0, SEEK_SET); /* also changes offset of package_fd */ return fdopen (dup_fd, mode); } if (!for_reading) clickpreload_assert_path_in_instdir ("write-fdopen", pathname); return (*libc_fopen) (pathname, mode); } FILE *fopen64 (const char *pathname, const char *mode) { int for_reading = (strncmp (mode, "r", 1) == 0 && strncmp (mode, "r+", 2) != 0); if (!libc_fopen64) clickpreload_init (); /* also needed for package_path */ if (for_reading && package_path && strcmp (pathname, package_path) == 0) { int dup_fd = dup (package_fd); lseek (dup_fd, 0, SEEK_SET); /* also changes offset of package_fd */ return fdopen (dup_fd, mode); } if (!for_reading) clickpreload_assert_path_in_instdir ("write-fdopen", pathname); return (*libc_fopen64) (pathname, mode); } int open (const char *pathname, int flags, ...) { int for_writing = ((flags & O_WRONLY) || (flags & O_RDWR)); mode_t mode = 0; int ret; if (!libc_open) clickpreload_init (); /* also needed for package_path */ if (!for_writing && package_path && strcmp (pathname, package_path) == 0) { int dup_fd = dup (package_fd); lseek (dup_fd, 0, SEEK_SET); /* also changes offset of package_fd */ return dup_fd; } if (for_writing) clickpreload_assert_path_in_instdir ("write-open", pathname); if (flags & O_CREAT) { va_list argv; va_start (argv, flags); mode = va_arg (argv, mode_t); va_end (argv); } ret = (*libc_open) (pathname, flags, mode); return ret; } int open64 (const char *pathname, int flags, ...) { int for_writing = ((flags & O_WRONLY) || (flags & O_RDWR)); mode_t mode = 0; int ret; if (!libc_open64) clickpreload_init (); /* also needed for package_path */ if (!for_writing && package_path && strcmp (pathname, package_path) == 0) { int dup_fd = dup (package_fd); lseek (dup_fd, 0, SEEK_SET); /* also changes offset of package_fd */ return dup_fd; } if (for_writing) clickpreload_assert_path_in_instdir ("write-open", pathname); if (flags & O_CREAT) { va_list argv; va_start (argv, flags); mode = va_arg (argv, mode_t); va_end (argv); } ret = (*libc_open64) (pathname, flags, mode); return ret; } int __xstat (int ver, const char *pathname, struct stat *buf) { if (!libc___xstat) clickpreload_init (); /* also needed for package_path */ if (package_path && strcmp (pathname, package_path) == 0) return __fxstat (ver, package_fd, buf); return (*libc___xstat) (ver, pathname, buf); } int __xstat64 (int ver, const char *pathname, struct stat64 *buf) { if (!libc___xstat64) clickpreload_init (); /* also needed for package_path */ if (package_path && strcmp (pathname, package_path) == 0) return __fxstat64 (ver, package_fd, buf); return (*libc___xstat64) (ver, pathname, buf); } /* As well as write sandboxing, our versions of chmod and fchmod also * prevent the 0200 (u+w) permission bit from being removed from unpacked * files. dpkg normally expects to be run as root which can override DAC * write permissions, so a mode 04xx file is not normally a problem for it, * but it is a problem when running dpkg as non-root. Since unpacked * packages are non-writeable from the point of view of the package's code, * forcing u+w is safe. */ int chmod (const char *path, mode_t mode) { if (!libc_chmod) clickpreload_init (); /* also needed for package_path */ clickpreload_assert_path_in_instdir ("chmod", path); mode |= S_IWUSR; return (*libc_chmod) (path, mode); } int fchmod (int fd, mode_t mode) { if (!libc_fchmod) clickpreload_init (); mode |= S_IWUSR; return (*libc_fchmod) (fd, mode); } click-0.4.21.1ubuntu0.2/setup.py.in0000775000000000000000000000126412320742124013517 0ustar #! /usr/bin/env python3 import sys from setuptools import find_packages, setup requirements = [] def require(package, pypi_name=None): try: __import__(package) except ImportError: requirements.append(package if pypi_name is None else pypi_name) require('debian', 'python-debian') if sys.version < "3.3": require('mock') require('chardet') setup( name="click", version="@PACKAGE_VERSION@", description="Click package manager", author="Colin Watson", author_email="cjwatson@ubuntu.com", license="GNU GPL", packages=find_packages(), scripts=['bin/click'], install_requires=requirements, test_suite="click.tests", )