/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2019-2024 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 2.1 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "config.h" #include #include #include #include "ascli-utils.h" typedef enum { ASC_REPORT_MODE_UNKNOWN, ASC_REPORT_MODE_NONE, ASC_REPORT_MODE_ERROR_SUMMARY, ASC_REPORT_MODE_SHORT, ASC_REPORT_MODE_FULL } AscReportMode; static void composecli_add_report_hint (GString *report, AscHint *hint) { const gchar *tag = asc_hint_get_tag (hint); switch (asc_hint_get_severity (hint)) { case AS_ISSUE_SEVERITY_ERROR: g_string_append (report, "E: "); break; case AS_ISSUE_SEVERITY_WARNING: g_string_append (report, "W: "); break; case AS_ISSUE_SEVERITY_INFO: g_string_append (report, "I: "); break; case AS_ISSUE_SEVERITY_PEDANTIC: g_string_append (report, "P: "); break; default: g_string_append (report, "U: "); } if (ascli_get_output_colored ()) { switch (asc_hint_get_severity (hint)) { case AS_ISSUE_SEVERITY_ERROR: g_string_append_printf (report, "%c[%dm%s%c[%dm", 0x1B, 31, tag, 0x1B, 0); break; case AS_ISSUE_SEVERITY_WARNING: g_string_append_printf (report, "%c[%dm%s%c[%dm", 0x1B, 33, tag, 0x1B, 0); break; case AS_ISSUE_SEVERITY_INFO: g_string_append_printf (report, "%c[%dm%s%c[%dm", 0x1B, 32, tag, 0x1B, 0); break; case AS_ISSUE_SEVERITY_PEDANTIC: g_string_append_printf (report, "%c[%dm%s%c[%dm", 0x1B, 37, tag, 0x1B, 0); break; default: g_string_append_printf (report, "%c[%dm%s%c[%dm", 0x1B, 35, tag, 0x1B, 0); } } else { g_string_append (report, tag); } } static void composecli_print_hints_report (GPtrArray *results, const gchar *title, AscReportMode mode) { g_autoptr(GString) report = NULL; g_return_if_fail (results != NULL); if (mode == ASC_REPORT_MODE_NONE) return; report = g_string_new (""); for (guint i = 0; i < results->len; i++) { g_autofree const gchar **issue_cids = NULL; AscResult *result = ASC_RESULT (g_ptr_array_index (results, i)); issue_cids = asc_result_get_component_ids_with_hints (result); for (guint j = 0; issue_cids[j] != NULL; j++) { gboolean entry_added = FALSE; gsize start_len = 0; GPtrArray *hints = asc_result_get_hints (result, issue_cids[j]); start_len = report->len; if (ascli_get_output_colored ()) g_string_append_printf (report, "\n%c[%dm%s%c[%dm\n", 0x1B, 1, issue_cids[j], 0x1B, 0); else g_string_append_printf (report, "\n%s\n", issue_cids[j]); for (guint k = 0; k < hints->len; k++) { AscHint *hint = ASC_HINT (g_ptr_array_index (hints, k)); if (mode == ASC_REPORT_MODE_ERROR_SUMMARY && !asc_hint_is_error (hint)) continue; /* pedantic hints are usually not important enough to be displayed here */ if (asc_hint_get_severity (hint) == AS_ISSUE_SEVERITY_PEDANTIC) continue; g_string_append (report, " "); composecli_add_report_hint (report, hint); g_string_append_c (report, '\n'); entry_added = TRUE; if (mode == ASC_REPORT_MODE_FULL) { g_autofree gchar *text = NULL; g_autofree gchar *text_md_wrap = NULL; g_autoptr(GString) text_md = NULL; text = asc_hint_format_explanation (hint); text_md = g_string_new (text); as_gstring_replace (text_md, "", "`", 0); as_gstring_replace (text_md, "", "`", 0); as_gstring_replace (text_md, "
", "\n", 0); as_gstring_replace (text_md, "", "", 0); as_gstring_replace (text_md, "", "", 0); as_gstring_replace (text_md, "<", "<", 0); as_gstring_replace (text_md, ">", ">", 0); text_md_wrap = ascli_format_long_output (text_md->str, 100, 5); g_string_append (report, text_md_wrap); g_string_append_c (report, '\n'); } } if (!entry_added) g_string_truncate (report, start_len); } } /* don't print anything if we have no report */ if (report->len < 2) return; /* trim trailing newline */ g_string_erase (report, 0, 1); if (title != NULL) { g_string_prepend_c (report, '\n'); g_string_prepend (report, title); } g_print ("%s", report->str); } int main (int argc, char **argv) { g_autoptr(GOptionContext) option_context = NULL; gboolean ret; gboolean verbose = FALSE; gboolean no_color = FALSE; gboolean show_version = FALSE; gboolean no_net = FALSE; AscReportMode report_mode; g_autofree gchar *report_mode_str = NULL; g_autofree gchar *origin = NULL; g_autofree gchar *res_root_dir = NULL; g_autofree gchar *mdata_dir = NULL; g_autofree gchar *icons_dir = NULL; g_autofree gchar *media_dir = NULL; g_autofree gchar *hints_dir = NULL; g_autofree gchar *media_baseurl = NULL; g_autofree gchar *prefix = NULL; g_autofree gchar *components_str = NULL; g_autofree gchar *icon_policy_str = NULL; g_autofree gchar *custom_keys_str = NULL; gboolean no_partial_urls = FALSE; g_autoptr(GError) error = NULL; g_autoptr(AscCompose) compose = NULL; AscComposeFlags compose_flags; GPtrArray *results; /* clang-format off */ const GOptionEntry options[] = { { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, /* TRANSLATORS: ascompose flag description for: --verbose */ _("Show extra debugging information."), NULL }, { "no-color", '\0', 0, G_OPTION_ARG_NONE, &no_color, /* TRANSLATORS: ascompose flag description for: --no-color */ _("Don\'t show colored output."), NULL }, { "version", '\0', 0, G_OPTION_ARG_NONE, &show_version, /* TRANSLATORS: ascompose flag description for: --version */ _("Show the program version."), NULL }, { "no-net", '\0', 0, G_OPTION_ARG_NONE, &no_net, /* TRANSLATORS: ascompose flag description for: --no-net */ _("Do not use the network at all, not even for URL validity checks."), NULL }, { "print-report", '\0', 0, G_OPTION_ARG_STRING, &report_mode_str, /* TRANSLATORS: ascompose flag description for: --print-report */ _("Set mode of the issue report that is printed to the console."), "MODE" }, { "prefix", '\0', 0, G_OPTION_ARG_FILENAME, &prefix, /* TRANSLATORS: ascompose flag description for: --prefix */ _("Override the default prefix (`/usr` by default)."), "DIR" }, { "result-root", '\0', 0, G_OPTION_ARG_FILENAME, &res_root_dir, /* TRANSLATORS: ascompose flag description for: --result-root */ _("Set the result output directory."), "DIR" }, { "data-dir", '\0', 0, G_OPTION_ARG_FILENAME, &mdata_dir, /* TRANSLATORS: ascompose flag description for: --data-dir, `catalog metadata` is an AppStream term */ _("Override the catalog metadata output directory."), "DIR" }, { "icons-dir", '\0', 0, G_OPTION_ARG_FILENAME, &icons_dir, /* TRANSLATORS: ascompose flag description for: --icons-dir */ _("Override the icon output directory."), "DIR" }, { "media-dir", '\0', 0, G_OPTION_ARG_FILENAME, &media_dir, /* TRANSLATORS: ascompose flag description for: --media-dir */ _("Set the media output directory (for media data to be served by a webserver)."), "DIR" }, { "hints-dir", '\0', 0, G_OPTION_ARG_FILENAME, &hints_dir, /* TRANSLATORS: ascompose flag description for: --hints-dir */ _("Set a directory where HTML and text issue reports will be stored."), "DIR" }, { "origin", '\0', 0, G_OPTION_ARG_STRING, &origin, /* TRANSLATORS: ascompose flag description for: --origin */ _("Set the origin name"), "NAME" }, { "media-baseurl", '\0', 0, G_OPTION_ARG_STRING, &media_baseurl, /* TRANSLATORS: ascompose flag description for: --media-baseurl */ _("Set the URL where the exported media content will be hosted."), "NAME" }, { "no-partial-urls", '\0', 0, G_OPTION_ARG_NONE, &no_partial_urls, /* TRANSLATORS: ascompose flag description for: --no-partial-urls */ _("Makes all URLs in output data complete URLs and avoids the use of a shared URL prefix for all metadata."), NULL }, { "icon-policy", '\0', 0, G_OPTION_ARG_STRING, &icon_policy_str, /* TRANSLATORS: ascompose flag description for: --icon-policy */ _("An icon-policy string to set how icon sizes should be handled (refer to the man page for details)."), "POLICY-STRING" }, { "allow-custom", '\0', 0, G_OPTION_ARG_STRING, &custom_keys_str, /* TRANSLATORS: ascompose flag description for: --allow-custom */ _("A comma-separated list of custom keys that should be propagated to the output data."), "CUSTOM-KEY-NAMES" }, { "components", '\0', 0, G_OPTION_ARG_STRING, &components_str, /* TRANSLATORS: ascompose flag description for: --components */ _("A comma-separated list of component-IDs to accept."), "COMPONENT-IDs" }, { NULL } }; /* clang-format on */ setlocale (LC_ALL, ""); bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); textdomain (GETTEXT_PACKAGE); option_context = g_option_context_new (" - SOURCE-DIRECTORIES"); 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 of appstream-compose */ g_print ("%s: %s\n", _("Failed to parse arguments"), error->message); return EXIT_FAILURE; } if (verbose) g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); ascli_set_output_colored (!no_color); if (show_version) { if (g_strcmp0 (as_version_string (), PACKAGE_VERSION) == 0) { /* TRANSLATORS: Output if appstreamcli --version is executed. */ ascli_print_stdout (_("AppStream version: %s"), PACKAGE_VERSION); } else { /* TRANSLATORS: Output if appstreamcli --version is run and the CLI and libappstream versions differ. */ ascli_print_stdout ( _("AppStream CLI tool version: %s\nAppStream library version: %s"), PACKAGE_VERSION, as_version_string ()); } return EXIT_SUCCESS; } /* determine report mode */ report_mode = ASC_REPORT_MODE_UNKNOWN; if (report_mode_str == NULL) report_mode = ASC_REPORT_MODE_ERROR_SUMMARY; else if (g_strcmp0 (report_mode_str, "full") == 0) report_mode = ASC_REPORT_MODE_FULL; else if (g_strcmp0 (report_mode_str, "short") == 0) report_mode = ASC_REPORT_MODE_SHORT; else if (g_strcmp0 (report_mode_str, "on-error") == 0) report_mode = ASC_REPORT_MODE_ERROR_SUMMARY; else if (g_strcmp0 (report_mode_str, "none") == 0) report_mode = ASC_REPORT_MODE_NONE; if (report_mode == ASC_REPORT_MODE_UNKNOWN) { /* TRANSLATORS: invalid value for the --print-report CLI option */ ascli_print_stderr ( _("Invalid value for `--print-report` option: %s\n" "Possible values are:\n" "`on-error` - only prints a short report if the run failed (default)\n" "`short` - generates an abridged report\n" "`full` - a detailed report will be printed"), report_mode_str); return EXIT_FAILURE; } /* create compose engine */ compose = asc_compose_new (); /* modify flags */ compose_flags = asc_compose_get_flags (compose); if (no_net) as_flags_remove (compose_flags, ASC_COMPOSE_FLAG_ALLOW_NET); if (no_partial_urls) as_flags_add (compose_flags, ASC_COMPOSE_FLAG_NO_PARTIAL_URLS); asc_compose_set_flags (compose, compose_flags); /* sanity checks & defaults */ if (prefix == NULL) prefix = g_strdup ("/usr"); asc_compose_set_prefix (compose, prefix); if (res_root_dir == NULL && (mdata_dir == NULL || icons_dir == NULL)) { if (argc == 2) { /* we have only one unit as parameter, assume it as target path for convenience & compatibility */ res_root_dir = g_strdup (argv[1]); ascli_print_stdout ( _("Automatically selected '%s' as data output location."), res_root_dir); } else { /* TRANSLATORS: We don't have a destination directory for compose */ g_printerr ("%s\n", _("No destination directory set, please provide a data output location!")); return EXIT_FAILURE; } } if (origin == NULL) { g_autofree gchar *tmp = NULL; origin = g_strdup ("example"); /* TRANSLATORS: Information message of appstream-compose */ tmp = g_strdup_printf ("Metadata origin not set, using '%s'", origin); if (ascli_get_output_colored ()) ascli_print_stderr ("%c[%dm%s%c[%dm: %s", 0x1B, 33, _("WARNING"), 0x1B, 0, tmp); else ascli_print_stderr ("%s: %s", _("WARNING"), tmp); } asc_compose_set_origin (compose, origin); if (mdata_dir == NULL) mdata_dir = g_build_filename (res_root_dir, prefix, "share/swcatalog/xml", NULL); asc_compose_set_data_result_dir (compose, mdata_dir); if (icons_dir == NULL) icons_dir = g_build_filename (res_root_dir, prefix, "share/swcatalog/icons", origin, NULL); asc_compose_set_icons_result_dir (compose, icons_dir); /* optional */ asc_compose_set_hints_result_dir (compose, hints_dir); asc_compose_set_media_result_dir (compose, media_dir); asc_compose_set_media_baseurl (compose, media_baseurl); /* we need at least one unit to process */ if (argc <= 1) { g_autofree gchar *tmp = NULL; tmp = g_option_context_get_help (option_context, TRUE, NULL); g_printerr ("%s", tmp); return EXIT_FAILURE; } /* set icon policy */ if (icon_policy_str != NULL) { AscIconPolicy *icon_policy = asc_compose_get_icon_policy (compose); if (!asc_icon_policy_from_string (icon_policy, icon_policy_str, &error)) { g_printerr ("%s: %s\n", _("Unable to set icon policy"), error->message); return EXIT_FAILURE; } } /* allow custom keys in catalog output, if requested */ if (custom_keys_str != NULL) { g_auto(GStrv) custom_keys = NULL; custom_keys = g_strsplit (custom_keys_str, ",", -1); for (guint i = 0; custom_keys[i] != NULL; i++) asc_compose_add_custom_allowed (compose, custom_keys[i]); } /* add allowlist for components */ if (components_str != NULL) { g_auto(GStrv) cid_allowlist = NULL; g_autofree gchar *cid_list = NULL; guint i; cid_allowlist = g_strsplit (components_str, ",", -1); for (i = 0; cid_allowlist[i] != NULL; i++) asc_compose_add_allowed_cid (compose, cid_allowlist[i]); cid_list = g_strjoinv (", ", cid_allowlist); if (i > 1) /* TRANSLATORS: Information about as-compose allowlist */ ascli_print_stdout (_("Only accepting components: %s"), cid_list); else /* TRANSLATORS: Information about as-compose allowlist */ ascli_print_stdout (_("Only accepting component: %s"), cid_list); } if (argc > 2) /* TRANSLATORS: Information about as-compose units to be processed */ g_print ("%s\n", _("Processing directories:")); else /* TRANSLATORS: Information about as-compose units to be processed */ g_print ("%s ", _("Processing directory:")); /* add locations for data processing */ for (guint i = 1; i < (guint) argc; i++) { g_autoptr(AscDirectoryUnit) dirunit = NULL; const gchar *dir_path = argv[i]; if (!g_file_test (dir_path, G_FILE_TEST_IS_DIR)) { /* TRANSLATORS: Error message */ g_printerr ("%s: %s\n", _("Can not process invalid directory"), dir_path); return EXIT_FAILURE; } dirunit = asc_directory_unit_new (dir_path); asc_compose_add_unit (compose, ASC_UNIT (dirunit)); if (argc > 2) g_print (" • %s\n", dir_path); else g_print ("%s\n", dir_path); } /* TRANSLATORS: Information message */ g_print ("%s\n", _("Composing metadata...")); results = asc_compose_run (compose, NULL, &error); if (results == NULL) { /* TRANSLATORS: Error message */ g_printerr ("%s: %s\n", _("Failed to compose AppStream metadata"), error->message); return EXIT_FAILURE; } if (asc_compose_has_errors (compose)) { /* TRANSLATORS: appstream-compose failed to include all data */ g_print ("%s\n", _("Run failed, some data was ignored.")); /* TRANSLATORS: information message of appstream-compose */ composecli_print_hints_report (results, _("Errors were raised during this compose run:"), report_mode); g_printerr ("%s\n", _("Refer to the generated issue report data for details on the individual problems.")); return EXIT_FAILURE; } else { composecli_print_hints_report (results, _("Overview of generated hints:"), report_mode); /* TRANSLATORS: Information message */ g_print ("%s\n", _("Success!")); return EXIT_SUCCESS; } }