1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2014-2016 Richard Hughes <richard@hughsie.com>
4 * Copyright (C) 2016 Alexander Larsson <alexl@redhat.com>
5 *
6 * SPDX-License-Identifier: GPL-2.0+
7 */
8
9 #include "config.h"
10
11 #include <appstream-glib.h>
12 #include <glib.h>
13 #include <glib/gi18n.h>
14 #include <stdlib.h>
15 #include <locale.h>
16 #include <errno.h>
17
18 G_GNUC_PRINTF (2, 3)
19 static void
as_compose_app_log(AsApp * app,const gchar * fmt,...)20 as_compose_app_log (AsApp *app, const gchar *fmt, ...)
21 {
22 const gchar *id;
23 gsize i;
24 va_list args;
25 g_autofree gchar *tmp = NULL;
26
27 va_start (args, fmt);
28 tmp = g_strdup_vprintf (fmt, args);
29 va_end (args);
30
31 /* print status */
32 id = as_app_get_id (app);
33 g_print ("%s: ", id);
34 for (i = strlen (id) + 2; i < 35; i++)
35 g_print (" ");
36 g_print ("%s\n", tmp);
37 }
38
39 static gboolean
add_icons(AsApp * app,const gchar * icons_dir,guint min_icon_size,const gchar * prefix,const gchar * key,GError ** error)40 add_icons (AsApp *app,
41 const gchar *icons_dir,
42 guint min_icon_size,
43 const gchar *prefix,
44 const gchar *key,
45 GError **error)
46 {
47 g_autofree gchar *fn_hidpi = NULL;
48 g_autofree gchar *fn = NULL;
49 g_autofree gchar *name_hidpi = NULL;
50 g_autofree gchar *name = NULL;
51 g_autofree gchar *icon_path = NULL;
52 g_autofree gchar *icon_subdir = NULL;
53 g_autofree gchar *icon_path_hidpi = NULL;
54 g_autofree gchar *icon_subdir_hidpi = NULL;
55 g_autoptr(AsIcon) icon_hidpi = NULL;
56 g_autoptr(AsIcon) icon = NULL;
57 g_autoptr(AsImage) im = NULL;
58 g_autoptr(GdkPixbuf) pixbuf_hidpi = NULL;
59 g_autoptr(GdkPixbuf) pixbuf = NULL;
60 g_autoptr(GError) error_local = NULL;
61
62 /* find 64x64 icon */
63 fn = as_utils_find_icon_filename_full (prefix, key,
64 AS_UTILS_FIND_ICON_NONE,
65 error);
66 if (fn == NULL) {
67 g_prefix_error (error, "Failed to find icon: ");
68 return FALSE;
69 }
70
71 /* load the icon */
72 im = as_image_new ();
73 if (!as_image_load_filename_full (im, fn,
74 64, min_icon_size,
75 AS_IMAGE_LOAD_FLAG_ALWAYS_RESIZE |
76 AS_IMAGE_LOAD_FLAG_ONLY_SUPPORTED |
77 AS_IMAGE_LOAD_FLAG_SHARPEN,
78 error)) {
79 g_prefix_error (error, "Failed to load icon: ");
80 return FALSE;
81 }
82 pixbuf = g_object_ref (as_image_get_pixbuf (im));
83
84 /* save in target directory */
85 name = g_strdup_printf ("%s.png", as_app_get_id_filename (AS_APP (app)));
86
87 icon = as_icon_new ();
88 as_icon_set_pixbuf (icon, pixbuf);
89 as_icon_set_name (icon, name);
90 as_icon_set_kind (icon, AS_ICON_KIND_CACHED);
91 as_icon_set_prefix (icon, as_app_get_icon_path (AS_APP (app)));
92 as_app_add_icon (AS_APP (app), icon);
93
94 icon_path = g_build_filename (icons_dir, "64x64", name, NULL);
95
96 icon_subdir = g_path_get_dirname (icon_path);
97 if (g_mkdir_with_parents (icon_subdir, 0755)) {
98 int errsv = errno;
99 g_set_error (error,
100 AS_APP_ERROR,
101 AS_APP_ERROR_FAILED,
102 "failed to create %s: %s",
103 icon_subdir,
104 strerror (errsv));
105 return FALSE;
106 }
107
108 /* TRANSLATORS: we've saving the icon file to disk */
109 g_print ("%s %s\n", _("Saving icon"), icon_path);
110 if (!gdk_pixbuf_save (pixbuf, icon_path, "png", error, NULL))
111 return FALSE;
112
113 /* try to get a HiDPI icon */
114 fn_hidpi = as_utils_find_icon_filename_full (prefix, key,
115 AS_UTILS_FIND_ICON_HI_DPI,
116 NULL);
117 if (fn_hidpi == NULL) {
118 g_debug ("no HiDPI icon found with key %s in %s", key, prefix);
119 return TRUE;
120 }
121
122 /* load the HiDPI icon */
123 g_debug ("trying to load %s", fn_hidpi);
124 if (!as_image_load_filename_full (im, fn_hidpi,
125 128, 128,
126 AS_IMAGE_LOAD_FLAG_ALWAYS_RESIZE |
127 AS_IMAGE_LOAD_FLAG_SHARPEN,
128 &error_local)) {
129 g_debug ("failed to load HiDPI icon: %s", error_local->message);
130 return TRUE;
131 }
132 pixbuf_hidpi = g_object_ref (as_image_get_pixbuf (im));
133 if (gdk_pixbuf_get_width (pixbuf_hidpi) <= gdk_pixbuf_get_width (pixbuf) ||
134 gdk_pixbuf_get_height (pixbuf_hidpi) <= gdk_pixbuf_get_height (pixbuf)) {
135 g_debug ("HiDPI icon no larger than normal icon");
136 return TRUE;
137 }
138 as_app_add_kudo_kind (AS_APP (app), AS_KUDO_KIND_HI_DPI_ICON);
139
140 /* save icon */
141 name_hidpi = g_strdup_printf ("%s.png", as_app_get_id_filename (AS_APP (app)));
142 icon_hidpi = as_icon_new ();
143 as_icon_set_pixbuf (icon_hidpi, pixbuf_hidpi);
144 as_icon_set_name (icon_hidpi, name_hidpi);
145 as_icon_set_kind (icon_hidpi, AS_ICON_KIND_CACHED);
146 as_icon_set_prefix (icon_hidpi, as_app_get_icon_path (AS_APP (app)));
147 as_app_add_icon (AS_APP (app), icon_hidpi);
148
149 icon_path_hidpi = g_build_filename (icons_dir, "128x128", name_hidpi, NULL);
150 icon_subdir_hidpi = g_path_get_dirname (icon_path_hidpi);
151 if (g_mkdir_with_parents (icon_subdir_hidpi, 0755)) {
152 int errsv = errno;
153 g_set_error (error,
154 AS_APP_ERROR,
155 AS_APP_ERROR_FAILED,
156 "failed to create %s: %s",
157 icon_subdir_hidpi,
158 strerror (errsv));
159 return FALSE;
160 }
161
162 /* TRANSLATORS: we've saving the icon file to disk */
163 g_print ("%s %s\n", _("Saving icon"), icon_path_hidpi);
164 if (!gdk_pixbuf_save (pixbuf_hidpi, icon_path_hidpi, "png", error, NULL))
165 return FALSE;
166 return TRUE;
167 }
168
169 static AsApp *
load_desktop(const gchar * prefix,const gchar * icons_dir,guint min_icon_size,const gchar * app_name,const gchar * desktop_path,GError ** error)170 load_desktop (const gchar *prefix,
171 const gchar *icons_dir,
172 guint min_icon_size,
173 const gchar *app_name,
174 const gchar *desktop_path,
175 GError **error)
176 {
177 AsIcon *icon;
178 g_autoptr(AsApp) app = NULL;
179
180 app = as_app_new ();
181 if (!as_app_parse_file (app, desktop_path,
182 AS_APP_PARSE_FLAG_USE_HEURISTICS |
183 AS_APP_PARSE_FLAG_ALLOW_VETO,
184 error))
185 return NULL;
186 if (as_app_get_kind (app) == AS_APP_KIND_UNKNOWN) {
187 g_set_error (error,
188 AS_APP_ERROR,
189 AS_APP_ERROR_FAILED,
190 "%s has no recognised type",
191 as_app_get_id (AS_APP (app)));
192 return NULL;
193 }
194
195 icon = as_app_get_icon_default (AS_APP (app));
196 if (icon != NULL) {
197 g_autofree gchar *key = NULL;
198 key = g_strdup (as_icon_get_name (icon));
199 if (as_icon_get_kind (icon) == AS_ICON_KIND_STOCK) {
200 as_compose_app_log (app,
201 "using stock icon %s", key);
202 } else {
203 g_autoptr(GError) error_local = NULL;
204 gboolean ret;
205
206 g_ptr_array_set_size (as_app_get_icons (AS_APP (app)), 0);
207 ret = add_icons (app,
208 icons_dir,
209 min_icon_size,
210 prefix,
211 key,
212 error);
213 if (!ret)
214 return NULL;
215 }
216 }
217
218 return g_steal_pointer (&app);
219 }
220
221 static gchar *
get_appdata_filename(const gchar * prefix,const gchar * app_name)222 get_appdata_filename (const gchar *prefix, const gchar *app_name)
223 {
224 const gchar *dirs[] = { "metainfo", "appdata", NULL };
225 const gchar *exts[] = { ".metainfo.xml", ".appdata.xml", NULL };
226
227 /* fall back to the legacy path and extensions */
228 for (guint j = 0; dirs[j] != NULL; j++) {
229 for (guint i = 0; exts[i] != NULL; i++) {
230 g_autofree gchar *basename = NULL;
231 g_autofree gchar *tmp = NULL;
232 basename = g_strconcat (app_name, exts[i], NULL);
233 tmp = g_build_filename (prefix,
234 "share",
235 dirs[j],
236 basename,
237 NULL);
238 if (g_file_test (tmp, G_FILE_TEST_EXISTS))
239 return g_steal_pointer (&tmp);
240 }
241 }
242 return NULL;
243 }
244
245 static AsApp *
load_appdata(const gchar * prefix,const gchar * app_name,GError ** error)246 load_appdata (const gchar *prefix, const gchar *app_name, GError **error)
247 {
248 g_autofree gchar *appdata_path = NULL;
249 g_autoptr(AsApp) app = NULL;
250 g_autoptr(GPtrArray) problems = NULL;
251 AsProblemKind problem_kind;
252 AsProblem *problem;
253 guint i;
254
255 appdata_path = get_appdata_filename (prefix, app_name);
256 if (appdata_path == NULL) {
257 g_set_error (error,
258 AS_APP_ERROR,
259 AS_APP_ERROR_FAILED,
260 "no file found for %s", app_name);
261 return NULL;
262 }
263 g_debug ("looking for appdata path '%s'", appdata_path);
264
265 app = as_app_new ();
266 if (!as_app_parse_file (app, appdata_path,
267 AS_APP_PARSE_FLAG_USE_HEURISTICS,
268 error))
269 return NULL;
270 if (as_app_get_kind (app) == AS_APP_KIND_UNKNOWN) {
271 g_set_error (error,
272 AS_APP_ERROR,
273 AS_APP_ERROR_FAILED,
274 "%s has no recognised type",
275 as_app_get_id (AS_APP (app)));
276 return NULL;
277 }
278
279 problems = as_app_validate (app,
280 AS_APP_VALIDATE_FLAG_NO_NETWORK |
281 AS_APP_VALIDATE_FLAG_RELAX,
282 error);
283 if (problems == NULL)
284 return NULL;
285 for (i = 0; i < problems->len; i++) {
286 problem = g_ptr_array_index (problems, i);
287 problem_kind = as_problem_get_kind (problem);
288 as_compose_app_log (app,
289 "AppData problem: %s : %s",
290 as_problem_kind_to_string (problem_kind),
291 as_problem_get_message (problem));
292 }
293 if (problems->len > 0) {
294 g_set_error (error,
295 AS_APP_ERROR,
296 AS_APP_ERROR_FAILED,
297 "AppData file %s was not valid",
298 appdata_path);
299 return NULL;
300 }
301
302 return g_steal_pointer (&app);
303 }
304
305 int
main(int argc,char ** argv)306 main (int argc, char **argv)
307 {
308 g_autoptr(GOptionContext) option_context = NULL;
309 gboolean ret;
310 gboolean verbose = FALSE;
311 g_autoptr(GError) error = NULL;
312 g_autofree gchar *basename = NULL;
313 g_autofree gchar *icons_dir = NULL;
314 g_autofree gchar *origin = NULL;
315 g_autofree gchar *xml_basename = NULL;
316 g_autofree gchar *output_dir = NULL;
317 g_autofree gchar *prefix = NULL;
318 g_autoptr(AsStore) store = NULL;
319 g_autoptr(GFile) xml_dir = NULL;
320 g_autoptr(GFile) xml_file = NULL;
321 guint min_icon_size = 32;
322 guint i;
323 const GOptionEntry options[] = {
324 { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose,
325 /* TRANSLATORS: command line option */
326 _("Show extra debugging information"), NULL },
327 { "prefix", '\0', 0, G_OPTION_ARG_FILENAME, &prefix,
328 /* TRANSLATORS: command line option */
329 _("Set the prefix"), "DIR" },
330 { "output-dir", '\0', 0, G_OPTION_ARG_FILENAME, &output_dir,
331 /* TRANSLATORS: command line option */
332 _("Set the output directory"), "DIR" },
333 { "icons-dir", '\0', 0, G_OPTION_ARG_FILENAME, &icons_dir,
334 /* TRANSLATORS: command line option */
335 _("Set the icons directory"), "DIR" },
336 { "origin", '\0', 0, G_OPTION_ARG_STRING, &origin,
337 /* TRANSLATORS: command line option */
338 _("Set the origin name"), "NAME" },
339 { "min-icon-size", '\0', 0, G_OPTION_ARG_INT, &min_icon_size,
340 /* TRANSLATORS: command line option */
341 _("Set the minimum icon size in pixels"), "ICON_SIZE" },
342 { "basename", '\0', 0, G_OPTION_ARG_STRING, &basename,
343 /* TRANSLATORS: command line option */
344 _("Set the basenames of the output files"), "NAME" },
345 { NULL}
346 };
347
348 setlocale (LC_ALL, "");
349 bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
350 bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
351 textdomain (GETTEXT_PACKAGE);
352 option_context = g_option_context_new (" - APP-IDS");
353
354 g_option_context_add_main_entries (option_context, options, NULL);
355 ret = g_option_context_parse (option_context, &argc, &argv, &error);
356 if (!ret) {
357 /* TRANSLATORS: error message */
358 g_print ("%s: %s\n", _("Failed to parse arguments"), error->message);
359 return EXIT_FAILURE;
360 }
361
362 if (verbose)
363 g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
364
365 /* set defaults */
366 if (prefix == NULL)
367 prefix = g_strdup ("/usr");
368 if (output_dir == NULL)
369 output_dir = g_build_filename (prefix, "share/app-info/xmls", NULL);
370 if (icons_dir == NULL)
371 icons_dir = g_build_filename (prefix, "share/app-info/icons", origin, NULL);
372 if (origin == NULL) {
373 g_print ("WARNING: Metadata origin not set, using 'example'\n");
374 origin = g_strdup ("example");
375 }
376 if (basename == NULL)
377 basename = g_strdup (origin);
378
379 if (argc == 1) {
380 g_autofree gchar *tmp = NULL;
381 tmp = g_option_context_get_help (option_context, TRUE, NULL);
382 g_print ("%s", tmp);
383 return EXIT_FAILURE;
384 }
385
386 store = as_store_new ();
387 as_store_set_api_version (store, 0.8);
388 as_store_set_origin (store, origin);
389
390 /* load each application specified */
391 for (i = 1; i < (guint) argc; i++) {
392 AsLaunchable *launchable;
393 const gchar *app_name = argv[i];
394 g_auto(GStrv) intl_domains = NULL;
395 g_autoptr(AsApp) app_appdata = NULL;
396 g_autoptr(AsApp) app_desktop = NULL;
397 g_autoptr(GString) desktop_basename = NULL;
398 g_autofree gchar *desktop_path = NULL;
399
400 /* TRANSLATORS: we're generating the AppStream data */
401 g_print ("%s %s\n", _("Processing application"), app_name);
402
403 app_appdata = load_appdata (prefix, app_name, &error);
404 if (app_appdata == NULL) {
405 /* TRANSLATORS: the .appdata.xml file could not
406 * be loaded */
407 g_print ("%s: %s\n", _("Error loading AppData file"),
408 error->message);
409 return EXIT_FAILURE;
410 }
411
412 /* set translations */
413 if (!as_app_builder_search_translations (app_appdata,
414 prefix,
415 25,
416 AS_APP_BUILDER_FLAG_NONE,
417 NULL,
418 &error)) {
419 /* TRANSLATORS: the .mo files could not be parsed */
420 g_print ("%s: %s\n", _("Error parsing translations"),
421 error->message);
422 return EXIT_FAILURE;
423 }
424
425 /* auto-add kudos */
426 if (!as_app_builder_search_kudos (app_appdata,
427 prefix,
428 AS_APP_BUILDER_FLAG_NONE,
429 &error)) {
430 /* TRANSLATORS: we could not auto-add the kudo */
431 g_print ("%s: %s\n", _("Error parsing kudos"),
432 error->message);
433 return EXIT_FAILURE;
434 }
435
436 /* auto-add provides */
437 if (!as_app_builder_search_provides (app_appdata,
438 prefix,
439 AS_APP_BUILDER_FLAG_NONE,
440 &error)) {
441 /* TRANSLATORS: we could not auto-add the provides */
442 g_print ("%s: %s\n", _("Error parsing provides"),
443 error->message);
444 return EXIT_FAILURE;
445 }
446
447 as_store_add_app (store, app_appdata);
448
449 /* use the ID from the AppData file if it was found */
450 launchable = as_app_get_launchable_by_kind (app_appdata,
451 AS_LAUNCHABLE_KIND_DESKTOP_ID);
452 if (launchable != NULL) {
453 desktop_basename = g_string_new (as_launchable_get_value (launchable));
454 } else {
455 const gchar *appdata_id = as_app_get_id (app_appdata);
456
457 /* append the .desktop suffix if using a new-style name */
458 desktop_basename = g_string_new (appdata_id != NULL ? appdata_id : app_name);
459 if (!g_str_has_suffix (desktop_basename->str, ".desktop"))
460 g_string_append (desktop_basename, ".desktop");
461 }
462
463 if (!g_str_has_suffix (desktop_basename->str, ".desktop")) {
464 /* TRANSLATORS: not a valid desktop filename */
465 g_print ("%s: %s\n", _("Invalid desktop filename"),
466 desktop_basename->str);
467 return EXIT_FAILURE;
468 }
469
470 desktop_path = g_build_filename (prefix, "share", "applications",
471 desktop_basename->str, NULL);
472 g_debug ("looking for desktop path '%s'", desktop_path);
473
474 if (g_file_test (desktop_path, G_FILE_TEST_EXISTS)) {
475 app_desktop = load_desktop (prefix,
476 icons_dir,
477 min_icon_size,
478 app_name,
479 desktop_path,
480 &error);
481 if (app_desktop == NULL) {
482 /* TRANSLATORS: the .desktop file could not
483 * be loaded */
484 g_print ("%s: %s\n", _("Error loading desktop file"),
485 error->message);
486 return EXIT_FAILURE;
487 }
488
489 /* if the appdata <name> exists, do not inherit from
490 * the desktop file as it may be prefixed */
491 if (g_hash_table_size (as_app_get_names (app_appdata)) > 0)
492 g_hash_table_remove_all (as_app_get_names (app_desktop));
493 if (g_hash_table_size (as_app_get_comments (app_appdata)) > 0)
494 g_hash_table_remove_all (as_app_get_comments (app_desktop));
495
496 /* does the app already exist with a launchable that matches this ID */
497 if (g_strcmp0 (as_app_get_id (app_appdata), as_app_get_id (app_desktop)) != 0) {
498 g_debug ("fixing up ID for desktop merge");
499 as_app_set_id (app_desktop, as_app_get_id (app_appdata));
500 }
501
502 as_store_add_app (store, app_desktop);
503 }
504 }
505
506 /* create output directory */
507 if (g_mkdir_with_parents (output_dir, 0755)) {
508 int errsv = errno;
509 g_print ("%s: %s\n",
510 /* TRANSLATORS: this is when the folder could
511 * not be created */
512 _("Error creating output directory"),
513 strerror (errsv));
514 return EXIT_FAILURE;
515 }
516
517 xml_dir = g_file_new_for_path (output_dir);
518 xml_basename = g_strconcat (basename, ".xml.gz", NULL);
519 xml_file = g_file_get_child (xml_dir, xml_basename);
520 /* TRANSLATORS: we've saving the XML file to disk */
521 g_print ("%s %s\n", _("Saving AppStream"),
522 g_file_get_path (xml_file));
523 if (!as_store_to_file (store,
524 xml_file,
525 AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE |
526 AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
527 AS_NODE_TO_XML_FLAG_ADD_HEADER,
528 NULL, &error)) {
529 /* TRANSLATORS: this is when the destination file
530 * cannot be saved for some reason */
531 g_print ("%s: %s\n", _("Error saving AppStream file"),
532 error->message);
533 return EXIT_FAILURE;
534 }
535
536 /* TRANSLATORS: information message */
537 g_print ("%s\n", _("Done!"));
538
539 return EXIT_SUCCESS;
540 }
541