diff options
author | Alexander Larsson <alexl@redhat.com> | 2016-01-21 17:00:27 +0100 |
---|---|---|
committer | Richard Hughes <richard@hughsie.com> | 2016-01-22 11:56:15 +0000 |
commit | 688ce88229a1aaf9d285e6d8859e006fe4dfde2a (patch) | |
tree | c8ffdbee46312aae75da1c95ed56f87dc2640a9f | |
parent | 8f4e1688529b1bdc364937004e68b165103302a0 (diff) | |
download | appstream-glib-688ce88229a1aaf9d285e6d8859e006fe4dfde2a.tar.gz |
Add appstream-compose
This is a simple app that takes a prefix (default /usr) and a set of
app names. It looks for appdata, desktop and icon files with that
basename in $prefix/share/[appdata|applications|icons] and generates
appstream xml files + icons in (by default) $prefix/share/app-info.
-rw-r--r-- | client/Makefile.am | 13 | ||||
-rw-r--r-- | client/as-compose.c | 490 | ||||
-rw-r--r-- | contrib/libappstream-glib.spec.in | 2 | ||||
-rw-r--r-- | data/Makefile.am | 6 | ||||
-rw-r--r-- | data/appstream-compose.xml | 58 | ||||
-rw-r--r-- | libappstream-glib/as-utils.c | 2 | ||||
-rw-r--r-- | po/POTFILES.in | 1 |
7 files changed, 570 insertions, 2 deletions
diff --git a/client/Makefile.am b/client/Makefile.am index dc04134..8777c11 100644 --- a/client/Makefile.am +++ b/client/Makefile.am @@ -21,6 +21,7 @@ AS_BUILDER_LIBS = \ $(top_builddir)/libappstream-builder/libappstream-builder.la bin_PROGRAMS = \ + appstream-compose \ appstream-util if HAVE_BUILDER @@ -57,4 +58,16 @@ appstream_builder_CFLAGS = \ $(WARNINGFLAGS_C) endif +appstream_compose_SOURCES = \ + as-compose.c +appstream_compose_LDADD = \ + $(AS_GLIB_LIBS) \ + $(GLIB_LIBS) \ + $(SOUP_LIBS) \ + $(LIBARCHIVE_LIBS) +appstream_compose_LDFLAGS = \ + $(PIE_LDFLAGS) +appstream_compose_CFLAGS = \ + $(WARNINGFLAGS_C) + -include $(top_srcdir)/git.mk diff --git a/client/as-compose.c b/client/as-compose.c new file mode 100644 index 0000000..6bca881 --- /dev/null +++ b/client/as-compose.c @@ -0,0 +1,490 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2014-2015 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016 Alexander Larsson <alexl@redhat.com> + * + * Licensed under the GNU General Public License Version 2 + * + * 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; either version 2 of the License, or + * (at your option) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <appstream-glib.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <stdlib.h> +#include <locale.h> +#include <errno.h> + +/** + * as_util_app_log: + **/ +G_GNUC_PRINTF (2, 3) +static void +as_compose_app_log (AsApp *app, const gchar *fmt, ...) +{ + const gchar *id; + guint i; + va_list args; + g_autofree gchar *tmp = NULL; + + va_start (args, fmt); + tmp = g_strdup_vprintf (fmt, args); + va_end (args); + + /* print status */ + id = as_app_get_id (app); + g_print ("%s: ", id); + for (i = strlen (id) + 2; i < 35; i++) + g_print (" "); + g_print ("%s\n", tmp); +} + +static gboolean +add_icons (AsApp *app, + const gchar *icons_dir, + guint min_icon_size, + const gchar *prefix, + const gchar *key, + GError **error) +{ + g_autofree gchar *fn_hidpi = NULL; + g_autofree gchar *fn = NULL; + g_autofree gchar *name_hidpi = NULL; + g_autofree gchar *name = NULL; + g_autofree gchar *icon_path = NULL; + g_autofree gchar *icon_subdir = NULL; + g_autofree gchar *icon_path_hidpi = NULL; + g_autofree gchar *icon_subdir_hidpi = NULL; + g_autoptr(AsIcon) icon_hidpi = NULL; + g_autoptr(AsIcon) icon = NULL; + g_autoptr(AsImage) im = NULL; + g_autoptr(GdkPixbuf) pixbuf_hidpi = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + + /* find 64x64 icon */ + fn = as_utils_find_icon_filename_full (prefix, key, + AS_UTILS_FIND_ICON_NONE, + error); + if (fn == NULL) { + g_prefix_error (error, "Failed to find icon: "); + return FALSE; + } + + /* load the icon */ + im = as_image_new (); + if (!as_image_load_filename_full (im, fn, + 64, min_icon_size, + AS_IMAGE_LOAD_FLAG_SHARPEN, + error)) { + g_prefix_error (error, "Failed to load icon: "); + return FALSE; + } + pixbuf = g_object_ref (as_image_get_pixbuf (im)); + + /* save in target directory */ + name = g_strdup_printf ("%ix%i/%s.png", + 64, 64, + as_app_get_id_filename (AS_APP (app))); + + icon = as_icon_new (); + as_icon_set_pixbuf (icon, pixbuf); + as_icon_set_name (icon, name); + as_icon_set_kind (icon, AS_ICON_KIND_CACHED); + as_icon_set_prefix (icon, as_app_get_icon_path (AS_APP (app))); + as_app_add_icon (AS_APP (app), icon); + + icon_path = g_build_filename (icons_dir, name, NULL); + + icon_subdir = g_path_get_dirname (icon_path); + if (g_mkdir_with_parents (icon_subdir, 0755)) { + int errsv = errno; + g_set_error (error, + AS_APP_ERROR, + AS_APP_ERROR_FAILED, + "failed to create %s: %s", + icon_subdir, + strerror (errsv)); + return FALSE; + } + + /* TRANSLATORS: we've saving the icon file to disk */ + g_print ("%s %s\n", _("Saving icon"), icon_path); + if (!gdk_pixbuf_save (pixbuf, icon_path, "png", error, NULL)) + return FALSE; + + /* try to get a HiDPI icon */ + fn_hidpi = as_utils_find_icon_filename_full (prefix, key, + AS_UTILS_FIND_ICON_HI_DPI, + NULL); + if (fn_hidpi == NULL) + return TRUE; + + /* load the HiDPI icon */ + if (!as_image_load_filename_full (im, fn, + 128, 128, + AS_IMAGE_LOAD_FLAG_SHARPEN, + NULL)) { + return TRUE; + } + pixbuf_hidpi = g_object_ref (as_image_get_pixbuf (im)); + if (gdk_pixbuf_get_width (pixbuf_hidpi) <= gdk_pixbuf_get_width (pixbuf) || + gdk_pixbuf_get_height (pixbuf_hidpi) <= gdk_pixbuf_get_height (pixbuf)) + return TRUE; + as_app_add_kudo_kind (AS_APP (app), AS_KUDO_KIND_HI_DPI_ICON); + + /* save icon */ + name_hidpi = g_strdup_printf ("%ix%i/%s.png", + 128, 128, + as_app_get_id_filename (AS_APP (app))); + icon_hidpi = as_icon_new (); + as_icon_set_pixbuf (icon_hidpi, pixbuf_hidpi); + as_icon_set_name (icon_hidpi, name_hidpi); + as_icon_set_kind (icon_hidpi, AS_ICON_KIND_CACHED); + as_icon_set_prefix (icon_hidpi, as_app_get_icon_path (AS_APP (app))); + as_app_add_icon (AS_APP (app), icon_hidpi); + + icon_path_hidpi = g_build_filename (icons_dir, name_hidpi, NULL); + icon_subdir_hidpi = g_path_get_dirname (icon_path_hidpi); + if (g_mkdir_with_parents (icon_subdir_hidpi, 0755)) { + int errsv = errno; + g_set_error (error, + AS_APP_ERROR, + AS_APP_ERROR_FAILED, + "failed to create %s: %s", + icon_subdir_hidpi, + strerror (errsv)); + return FALSE; + } + + /* TRANSLATORS: we've saving the icon file to disk */ + g_print ("%s %s\n", _("Saving icon"), icon_path_hidpi); + if (!gdk_pixbuf_save (pixbuf_hidpi, icon_path_hidpi, "png", error, NULL)) + return FALSE; + return TRUE; +} + +static AsApp * +load_desktop (const gchar *prefix, + const gchar *icons_dir, + guint min_icon_size, + const gchar *app_name, + const gchar *appdata_id, + GError **error) +{ + AsIcon *icon; + g_autofree gchar *desktop_basename = NULL; + g_autofree gchar *desktop_path = NULL; + g_autoptr(AsApp) app = NULL; + + if (appdata_id != NULL) + desktop_basename = g_strdup (appdata_id); + else + desktop_basename = g_strconcat (app_name, ".desktop", NULL); + desktop_path = g_build_filename (prefix, "share/applications", desktop_basename, NULL); + + app = as_app_new (); + if (!as_app_parse_file (app, desktop_path, + AS_APP_PARSE_FLAG_USE_HEURISTICS | + AS_APP_PARSE_FLAG_ALLOW_VETO, + error)) + return NULL; + if (as_app_get_id_kind (app) == AS_ID_KIND_UNKNOWN) { + g_set_error (error, + AS_APP_ERROR, + AS_APP_ERROR_FAILED, + "%s has no recognised type", + as_app_get_id (AS_APP (app))); + return NULL; + } + + icon = as_app_get_icon_default (AS_APP (app)); + if (icon != NULL) { + g_autofree gchar *key = NULL; + key = g_strdup (as_icon_get_name (icon)); + if (as_icon_get_kind (icon) == AS_ICON_KIND_STOCK) { + as_compose_app_log (app, + "using stock icon %s", key); + } else { + g_autoptr(GError) error_local = NULL; + gboolean ret; + + g_ptr_array_set_size (as_app_get_icons (AS_APP (app)), 0); + ret = add_icons (app, + icons_dir, + min_icon_size, + prefix, + key, + error); + if (!ret) + return NULL; + } + } + + return g_steal_pointer (&app); +} + +static AsApp * +load_appdata (const gchar *prefix, const gchar *app_name, GError **error) +{ + g_autofree gchar *appdata_basename = NULL; + g_autofree gchar *appdata_path = NULL; + g_autoptr(AsApp) app = NULL; + g_autoptr(GPtrArray) problems = NULL; + AsProblemKind problem_kind; + AsProblem *problem; + guint i; + + appdata_basename = g_strconcat (app_name, + ".appdata.xml", + NULL); + appdata_path = g_build_filename (prefix, + "share", + "appdata", + appdata_basename, + NULL); + g_debug ("Looking for %s", appdata_path); + + app = as_app_new (); + if (!as_app_parse_file (app, appdata_path, + AS_APP_PARSE_FLAG_NONE, + error)) + return NULL; + if (as_app_get_id_kind (app) == AS_ID_KIND_UNKNOWN) { + g_set_error (error, + AS_APP_ERROR, + AS_APP_ERROR_FAILED, + "%s has no recognised type", + as_app_get_id (AS_APP (app))); + return NULL; + } + + problems = as_app_validate (app, + AS_APP_VALIDATE_FLAG_NO_NETWORK | + AS_APP_VALIDATE_FLAG_RELAX, + error); + if (problems == NULL) + return NULL; + for (i = 0; i < problems->len; i++) { + problem = g_ptr_array_index (problems, i); + problem_kind = as_problem_get_kind (problem); + as_compose_app_log (app, + "AppData problem: %s : %s", + as_problem_kind_to_string (problem_kind), + as_problem_get_message (problem)); + } + if (problems->len > 0) { + g_set_error (error, + AS_APP_ERROR, + AS_APP_ERROR_FAILED, + "AppData file %s was not valid", + appdata_path); + return NULL; + } + + return g_steal_pointer (&app); +} + +/** + * main: + **/ +int +main (int argc, char **argv) +{ + g_autoptr(GOptionContext) option_context = NULL; + gboolean ret; + gboolean verbose = FALSE; + g_autoptr(GError) error = NULL; + g_autofree gchar *basename = NULL; + g_autofree gchar *icons_dir = NULL; + g_autofree gchar *origin = NULL; + g_autofree gchar *xml_basename = NULL; + g_autofree gchar *output_dir = NULL; + g_autofree gchar *prefix = NULL; + g_autoptr(AsStore) store = NULL; + g_autoptr(GFile) xml_dir = NULL; + g_autoptr(GFile) xml_file = NULL; + gint min_icon_size = 32; + gdouble api_version = 0.0f; + guint i; + const GOptionEntry options[] = { + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, + /* TRANSLATORS: command line option */ + _("Show extra debugging information"), NULL }, + { "prefix", '\0', 0, G_OPTION_ARG_FILENAME, &prefix, + /* TRANSLATORS: command line option */ + _("Set the prefix"), "DIR" }, + { "output-dir", '\0', 0, G_OPTION_ARG_FILENAME, &output_dir, + /* TRANSLATORS: command line option */ + _("Set the output directory"), "DIR" }, + { "icons-dir", '\0', 0, G_OPTION_ARG_FILENAME, &icons_dir, + /* TRANSLATORS: command line option */ + _("Set the icons directory"), "DIR" }, + { "origin", '\0', 0, G_OPTION_ARG_STRING, &origin, + /* TRANSLATORS: command line option */ + _("Set the origin name"), "NAME" }, + { "min-icon-size", '\0', 0, G_OPTION_ARG_INT, &min_icon_size, + /* TRANSLATORS: command line option */ + _("Set the minimum icon size in pixels"), "ICON_SIZE" }, + { "basename", '\0', 0, G_OPTION_ARG_STRING, &basename, + /* TRANSLATORS: command line option */ + _("Set the basenames of the output files"), "NAME" }, + { "api-version", '\0', 0, G_OPTION_ARG_DOUBLE, &api_version, + /* TRANSLATORS: command line option */ + _("Set the AppStream version"), "API_VERSION" }, + { NULL} + }; + + setlocale (LC_ALL, ""); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + option_context = g_option_context_new (" - APP-IDS"); + + g_option_context_add_main_entries (option_context, options, NULL); + ret = g_option_context_parse (option_context, &argc, &argv, &error); + if (!ret) { + /* TRANSLATORS: error message */ + g_print ("%s: %s\n", _("Failed to parse arguments"), error->message); + return EXIT_FAILURE; + } + + if (verbose) + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + /* set defaults */ + if (api_version < 0.01) + api_version = 0.8; + if (prefix == NULL) + prefix = g_strdup ("/usr"); + if (output_dir == NULL) + output_dir = g_build_filename (prefix, "share/app-info/xmls", NULL); + if (icons_dir == NULL) + icons_dir = g_build_filename (prefix, "share/app-info/icons", origin, NULL); + if (origin == NULL) { + g_print ("WARNING: Metadata origin not set, using 'example'\n"); + origin = g_strdup ("example"); + } + if (basename == NULL) + basename = g_strdup (origin); + + if (argc == 1) { + g_autofree gchar *tmp = NULL; + tmp = g_option_context_get_help (option_context, TRUE, NULL); + g_print ("%s", tmp); + return EXIT_FAILURE; + } + + store = as_store_new (); + as_store_set_api_version (store, api_version); + as_store_set_origin (store, origin); + + /* load each application specified */ + for (i = 1; i < (guint) argc; i++) { + const gchar *app_name = argv[i]; + const gchar *gettext_domain; + g_auto(GStrv) intl_domains = NULL; + g_autofree gchar *locale_path = NULL; + g_autoptr(AsApp) app_appdata = NULL; + g_autoptr(AsApp) app_desktop = NULL; + + /* TRANSLATORS: we're generating the AppStream data */ + g_print ("%s %s\n", _("Processing application"), app_name); + + app_appdata = load_appdata (prefix, app_name, &error); + if (app_appdata == NULL) { + /* TRANSLATORS: the .appdata.xml file could not + * be loaded */ + g_print ("%s: %s\n", _("Error loading AppData file"), + error->message); + return EXIT_FAILURE; + } + + /* set translations: FIXME add to specification */ + gettext_domain = as_app_get_metadata_item (app_appdata, + "X-Gettext-Domain"); + if (gettext_domain != NULL) { + locale_path = g_build_filename (prefix, + "share", + "locale", + NULL); + intl_domains = g_strsplit (gettext_domain, ",", -1); + if (!as_app_gettext_search_path (app_appdata, + locale_path, + intl_domains, + 25, + NULL, + &error)) { + /* TRANSLATORS: the .mo files could not be parsed */ + g_print ("%s: %s\n", _("Error parsing translations"), + error->message); + return EXIT_FAILURE; + } + } + + as_store_add_app (store, app_appdata); + + app_desktop = load_desktop (prefix, + icons_dir, + min_icon_size, + app_name, + as_app_get_id (app_appdata), + &error); + if (app_desktop == NULL) { + /* TRANSLATORS: the .desktop file could not + * be loaded */ + g_print ("%s: %s\n", _("Error loading desktop file"), + error->message); + return EXIT_FAILURE; + } + as_store_add_app (store, app_desktop); + } + + /* create output directory */ + if (g_mkdir_with_parents (output_dir, 0755)) { + int errsv = errno; + g_print ("%s: %s\n", + /* TRANSLATORS: this is when the folder could + * not be created */ + _("Error creating output directory"), + strerror (errsv)); + return EXIT_FAILURE; + } + + xml_dir = g_file_new_for_path (output_dir); + xml_basename = g_strconcat (basename, ".xml.gz", NULL); + xml_file = g_file_get_child (xml_dir, xml_basename); + /* TRANSLATORS: we've saving the XML file to disk */ + g_print ("%s %s\n", _("Saving AppStream"), + g_file_get_path (xml_file)); + if (!as_store_to_file (store, + xml_file, + AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE | + AS_NODE_TO_XML_FLAG_FORMAT_INDENT | + AS_NODE_TO_XML_FLAG_ADD_HEADER, + NULL, &error)) { + /* TRANSLATORS: this is when the destination file + * cannot be saved for some reason */ + g_print ("%s: %s\n", _("Error saving AppStream file"), + error->message); + return EXIT_FAILURE; + } + + /* TRANSLATORS: information message */ + g_print ("%s\n", _("Done!")); + + return EXIT_SUCCESS; +} diff --git a/contrib/libappstream-glib.spec.in b/contrib/libappstream-glib.spec.in index 4fd82b5..cc1a7b8 100644 --- a/contrib/libappstream-glib.spec.in +++ b/contrib/libappstream-glib.spec.in @@ -104,9 +104,11 @@ make install DESTDIR=$RPM_BUILD_ROOT %{_libdir}/libappstream-glib.so.8* %{_libdir}/girepository-1.0/*.typelib %{_bindir}/appstream-util +%{_bindir}/appstream-compose %dir %{_datadir}/bash-completion/completions/ %{_datadir}/bash-completion/completions/appstream-util %{_mandir}/man1/appstream-util.1.gz +%{_mandir}/man1/appstream-compose.1.gz %files devel %{_libdir}/libappstream-glib.so diff --git a/data/Makefile.am b/data/Makefile.am index b902cba..5325046 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -13,7 +13,8 @@ dist_m4data_DATA += \ man_MANS = if ENABLE_MAN man_MANS += \ - appstream-util.1 + appstream-util.1 \ + appstream-compose.1 if HAVE_BUILDER man_MANS += \ appstream-builder.1 @@ -33,6 +34,8 @@ XSLTPROC_FLAGS = \ appstream-util.1: appstream-util.xml $(AM_V_GEN) xsltproc $(XSLTPROC_FLAGS) http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $< +appstream-compose.1: appstream-compose.xml + $(AM_V_GEN) xsltproc $(XSLTPROC_FLAGS) http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $< if HAVE_BUILDER appstream-builder.1: appstream-builder.xml $(AM_V_GEN) xsltproc $(XSLTPROC_FLAGS) http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $< @@ -44,6 +47,7 @@ dist_bashcompletion_DATA += appstream-builder endif EXTRA_DIST = \ + appstream-compose.xml \ appstream-util.xml \ appstream-builder.xml diff --git a/data/appstream-compose.xml b/data/appstream-compose.xml new file mode 100644 index 0000000..ec8a39f --- /dev/null +++ b/data/appstream-compose.xml @@ -0,0 +1,58 @@ +<?xml version='1.0'?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN" + "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"> + +<refentry id="appstream-compose"> + + <refentryinfo> + <title>appstream-compose</title> + <author> + <contrib>Maintainer</contrib> + <firstname>Richard</firstname> + <surname>Hughes</surname> + <email>richard@hughsie.com</email> + </author> + <copyright> + <year>2013-2014</year> + <holder>Richard Hughes</holder> + </copyright> + </refentryinfo> + + <refmeta> + <refentrytitle>appstream-compose</refentrytitle> + <manvolnum>1</manvolnum> + <refmiscinfo class="manual">User Commands</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>appstream-compose</refname> + <refpurpose>Generate AppStream metadata</refpurpose> + </refnamediv> + + <refsynopsisdiv> + <cmdsynopsis> + <command>appstream-compose</command> + <arg choice="opt" rep="repeat">OPTION</arg> + </cmdsynopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + <para> + This manual page documents briefly the <command>appstream-compose</command> command. + </para> + <para> + <command>appstream-compose</command> is a simple app that takes a + prefix (default /usr) and a set of application names. + It looks for appdata, desktop and icon files with that + basename in $prefix/share/[appdata|applications|icons] and generates + appstream xml files and icons in (by default) $prefix/share/app-info. + </para> + </refsect1> + + <refsect1> + <title>Author</title> + <para>This manual page was written by Richard Hughes <email>richard@hughsie.com</email>. + </para> + </refsect1> +</refentry> diff --git a/libappstream-glib/as-utils.c b/libappstream-glib/as-utils.c index 45a5b5f..92213db 100644 --- a/libappstream-glib/as-utils.c +++ b/libappstream-glib/as-utils.c @@ -1178,7 +1178,7 @@ as_utils_find_icon_filename_full (const gchar *destdir, prefix = g_strdup_printf ("%s/usr", destdir); if (!g_file_test (prefix, G_FILE_TEST_EXISTS)) { g_free (prefix); - prefix = g_strdup_printf ("%s/files", destdir); + prefix = g_strdup (destdir); } if (!g_file_test (prefix, G_FILE_TEST_EXISTS)) { g_set_error (error, diff --git a/po/POTFILES.in b/po/POTFILES.in index 86ec69b..48df45d 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -2,4 +2,5 @@ # List of source files containing translatable strings. # Please keep this file sorted alphabetically. client/as-builder.c +client/as-compose.c client/as-util.c |