diff --git a/CMakeLists.txt b/CMakeLists.txt index b031aea84..c8c9ca16a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ option(WITH_LIBDNF5_CLI "Build library for working with a terminal in a command- option(WITH_DNF5 "Build dnf5 command-line package manager" ON) option(WITH_DNF5_PLUGINS "Build plugins for dnf5 command-line package manager" ON) option(WITH_PLUGIN_ACTIONS "Build a dnf5 actions plugin" ON) +option(WITH_PLUGIN_APPSTREAM "Build with plugin to install repo's Appstream metadata" ON) option(WITH_PLUGIN_RHSM "Build a libdnf5 rhsm (Red Hat Subscription Manager) plugin" OFF) option(WITH_PYTHON_PLUGINS_LOADER "Build a special dnf5 plugin that loads Python plugins. Requires WITH_PYTHON3=ON." ON) diff --git a/dnf5.spec b/dnf5.spec index 08112d8f0..62019eb5a 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -77,6 +77,7 @@ Provides: dnf5-command(versionlock) %bcond_without dnf5 %bcond_without dnf5_plugins %bcond_without plugin_actions +%bcond_without plugin_appstream %bcond_without plugin_rhsm %bcond_without python_plugins_loader @@ -604,6 +605,24 @@ Libdnf5 plugin that allows to run actions (external executables) on hooks. %{_mandir}/man8/libdnf5-actions.8.* %endif +# ========== libdnf5-plugin-appstream ========== + +%if %{with plugin_appstream} + +%package -n libdnf5-plugin-appstream +Summary: Libdnf5 plugin to install repo Appstream data +License: LGPL-2.1-or-later +Requires: libdnf5%{?_isa} = %{version}-%{release} +BuildRequires: pkgconfig(appstream) >= 0.16 + +%description -n libdnf5-plugin-appstream +Libdnf5 plugin that installs repository's Appstream data, for repositories which provide them. + +%files -n libdnf5-plugin-appstream +%{_libdir}/libdnf5/plugins/appstream.so +%config %{_sysconfdir}/dnf/libdnf5-plugins/appstream.conf + +%endif # ========== libdnf5-plugin-plugin_rhsm ========== @@ -805,6 +824,7 @@ automatically and regularly from systemd timers, cron jobs or similar. -DWITH_LIBDNF5_CLI=%{?with_libdnf_cli:ON}%{!?with_libdnf_cli:OFF} \ -DWITH_DNF5=%{?with_dnf5:ON}%{!?with_dnf5:OFF} \ -DWITH_PLUGIN_ACTIONS=%{?with_plugin_actions:ON}%{!?with_plugin_actions:OFF} \ + -DWITH_PLUGIN_APPSTREAM=%{?with_plugin_appstream:ON}%{!?with_plugin_appstream:OFF} \ -DWITH_PLUGIN_RHSM=%{?with_plugin_rhsm:ON}%{!?with_plugin_rhsm:OFF} \ -DWITH_PYTHON_PLUGINS_LOADER=%{?with_python_plugins_loader:ON}%{!?with_python_plugins_loader:OFF} \ \ diff --git a/include/libdnf5/conf/const.hpp b/include/libdnf5/conf/const.hpp index db349c204..2163d54d7 100644 --- a/include/libdnf5/conf/const.hpp +++ b/include/libdnf5/conf/const.hpp @@ -64,9 +64,15 @@ constexpr const char * METADATA_TYPE_OTHER = "other"; constexpr const char * METADATA_TYPE_PRESTO = "presto"; constexpr const char * METADATA_TYPE_UPDATEINFO = "updateinfo"; constexpr const char * METADATA_TYPE_ALL = "all"; +constexpr const char * METADATA_TYPE_APPSTREAM = "appstream"; const std::set OPTIONAL_METADATA_TYPES{ - METADATA_TYPE_COMPS, METADATA_TYPE_FILELISTS, METADATA_TYPE_OTHER, METADATA_TYPE_PRESTO, METADATA_TYPE_UPDATEINFO}; + METADATA_TYPE_COMPS, + METADATA_TYPE_FILELISTS, + METADATA_TYPE_OTHER, + METADATA_TYPE_PRESTO, + METADATA_TYPE_UPDATEINFO, + METADATA_TYPE_APPSTREAM}; } // namespace libdnf5 diff --git a/include/libdnf5/repo/repo.hpp b/include/libdnf5/repo/repo.hpp index 53c8d678e..291359840 100644 --- a/include/libdnf5/repo/repo.hpp +++ b/include/libdnf5/repo/repo.hpp @@ -314,6 +314,9 @@ class LIBDNF_API Repo { // @replaces libdnf:repo/Repo.hpp:method:Repo.downloadMetadata(const std::string & destdir) void download_metadata(const std::string & destdir); + /// Returns a list of pairs of the rpmmd type and filename of the Appstream data of the repo + std::vector> get_appstream_metadata() const; + private: class LIBDNF_LOCAL Impl; friend class RepoSack; diff --git a/libdnf5-plugins/CMakeLists.txt b/libdnf5-plugins/CMakeLists.txt index f97124f09..e8c99de03 100644 --- a/libdnf5-plugins/CMakeLists.txt +++ b/libdnf5-plugins/CMakeLists.txt @@ -2,5 +2,6 @@ set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_C_VISIBILITY_PRESET hidden) add_subdirectory("actions") +add_subdirectory("appstream") add_subdirectory("python_plugins_loader") add_subdirectory("rhsm") diff --git a/libdnf5-plugins/appstream/CMakeLists.txt b/libdnf5-plugins/appstream/CMakeLists.txt new file mode 100644 index 000000000..a71ffbe96 --- /dev/null +++ b/libdnf5-plugins/appstream/CMakeLists.txt @@ -0,0 +1,17 @@ +if(NOT WITH_PLUGIN_APPSTREAM) + return() +endif() + +add_library(appstream_plugin MODULE appstream.cpp) + +# disable the 'lib' prefix in order to create appstream.so +set_target_properties(appstream_plugin PROPERTIES PREFIX "") +set_target_properties(appstream_plugin PROPERTIES OUTPUT_NAME "appstream") + +pkg_check_modules(APPSTREAM REQUIRED appstream>=0.16) +include_directories(${APPSTREAM_INCLUDE_DIRS}) +target_link_libraries(appstream_plugin PRIVATE ${APPSTREAM_LIBRARIES}) +target_link_libraries(appstream_plugin PRIVATE libdnf5) + +install(TARGETS appstream_plugin LIBRARY DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}/libdnf5/plugins/") +install(FILES "appstream.conf" DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}/dnf/libdnf5-plugins") diff --git a/libdnf5-plugins/appstream/appstream.conf b/libdnf5-plugins/appstream/appstream.conf new file mode 100644 index 000000000..a32ac3a78 --- /dev/null +++ b/libdnf5-plugins/appstream/appstream.conf @@ -0,0 +1,3 @@ +[main] +name = appstream +enabled = 1 diff --git a/libdnf5-plugins/appstream/appstream.cpp b/libdnf5-plugins/appstream/appstream.cpp new file mode 100644 index 000000000..a31baac3b --- /dev/null +++ b/libdnf5-plugins/appstream/appstream.cpp @@ -0,0 +1,128 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf 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. + +Libdnf 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 libdnf. If not, see . +*/ + +#include +#include +#include +#include +#include +#include + +#include + +using namespace libdnf5; + +namespace { + +constexpr const char * PLUGIN_NAME{"appstream"}; +constexpr libdnf5::plugin::Version PLUGIN_VERSION{.major = 1, .minor = 0, .micro = 0}; + +constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr}; +constexpr const char * attrs_value[]{"Milan Crha", "mcrha@redhat.com", "install repo Appstream data."}; + +class AppstreamPlugin : public plugin::IPlugin { +public: + AppstreamPlugin(libdnf5::plugin::IPluginData & data, libdnf5::ConfigParser &) : IPlugin(data) {} + virtual ~AppstreamPlugin() = default; + + PluginAPIVersion get_api_version() const noexcept override { return PLUGIN_API_VERSION; } + + const char * get_name() const noexcept override { return PLUGIN_NAME; } + + plugin::Version get_version() const noexcept override { return PLUGIN_VERSION; } + + const char * const * get_attributes() const noexcept override { return attrs; } + + const char * get_attribute(const char * attribute) const noexcept override { + for (size_t i = 0; attrs[i]; ++i) { + if (std::strcmp(attribute, attrs[i]) == 0) { + return attrs_value[i]; + } + } + return nullptr; + } + + void repos_loaded() override { + Base & base = get_base(); + repo::RepoQuery repos(base); + repos.filter_enabled(true); + for (const auto & repo : repos) { + auto type = repo->get_type(); + if (type == repo::Repo::Type::AVAILABLE || type == repo::Repo::Type::SYSTEM) { + install_appstream(repo.get()); + } + } + } + +private: + void install_appstream(libdnf5::repo::Repo * repo); +}; + +void AppstreamPlugin::install_appstream(libdnf5::repo::Repo * repo) { + libdnf5::Base & base = get_base(); + if (!repo->get_config().get_main_config().get_optional_metadata_types_option().get_value().contains( + libdnf5::METADATA_TYPE_APPSTREAM)) + return; + + std::string repo_id = repo->get_config().get_id(); + auto appstream_metadata = repo->get_appstream_metadata(); + for (auto & item : appstream_metadata) { + const std::string path = item.second; + GError * local_error = NULL; + + if (!as_utils_install_metadata_file( + AS_METADATA_LOCATION_CACHE, path.c_str(), repo_id.c_str(), NULL, &local_error)) { + base.get_logger()->debug( + "Failed to install Appstream metadata file '{}' for repo '{}': {}", + path, + repo_id, + local_error ? local_error->message : "Unknown error"); + } + + g_clear_error(&local_error); + } +} + +} // namespace + + +PluginAPIVersion libdnf_plugin_get_api_version(void) { + return PLUGIN_API_VERSION; +} + +const char * libdnf_plugin_get_name(void) { + return PLUGIN_NAME; +} + +plugin::Version libdnf_plugin_get_version(void) { + return PLUGIN_VERSION; +} + +plugin::IPlugin * libdnf_plugin_new_instance( + [[maybe_unused]] LibraryVersion library_version, + libdnf5::plugin::IPluginData & data, + libdnf5::ConfigParser & parser) try { + return new AppstreamPlugin(data, parser); +} catch (...) { + return nullptr; +} + +void libdnf_plugin_delete_instance(plugin::IPlugin * plugin_object) { + delete plugin_object; +} diff --git a/libdnf5/repo/repo.cpp b/libdnf5/repo/repo.cpp index 83392efd4..3b9e8dcc7 100644 --- a/libdnf5/repo/repo.cpp +++ b/libdnf5/repo/repo.cpp @@ -52,7 +52,6 @@ extern "C" { #include #include - namespace libdnf5::repo { static void is_readable_rpm(const std::string & fn) { @@ -203,6 +202,9 @@ void Repo::read_metadata_cache() { p_impl->downloader->load_local(); } +std::vector> Repo::get_appstream_metadata() const { + return get_downloader().get_appstream_metadata(); +} bool Repo::is_in_sync() { if (!p_impl->config.get_metalink_option().empty() && !p_impl->config.get_metalink_option().get_value().empty()) { @@ -413,6 +415,13 @@ void Repo::load_available_repo() { auto optional_metadata = p_impl->config.get_main_config().get_optional_metadata_types_option().get_value(); const bool all_metadata = optional_metadata.contains(libdnf5::METADATA_TYPE_ALL); + if (all_metadata || optional_metadata.contains(libdnf5::METADATA_TYPE_APPSTREAM)) { + auto appstream_metadata = p_impl->downloader->get_appstream_metadata(); + for (auto & item : appstream_metadata) { + p_impl->solv_repo->load_repo_ext(RepodataType::APPSTREAM, item.first, *p_impl->downloader.get()); + } + } + if (all_metadata || optional_metadata.contains(libdnf5::METADATA_TYPE_FILELISTS)) { p_impl->solv_repo->load_repo_ext(RepodataType::FILELISTS, *p_impl->downloader.get()); } diff --git a/libdnf5/repo/repo_downloader.cpp b/libdnf5/repo/repo_downloader.cpp index c47ad8aaa..8ecea6419 100644 --- a/libdnf5/repo/repo_downloader.cpp +++ b/libdnf5/repo/repo_downloader.cpp @@ -388,6 +388,25 @@ const std::string & RepoDownloader::get_metadata_path(const std::string & metada return it != metadata_paths.end() ? it->second : empty; } +bool RepoDownloader::is_appstream_metadata_type(const std::string & type) const { + /* TODO: make the list configurable with this default */ + return utils::string::starts_with(type, "appstream") || utils::string::starts_with(type, "appdata"); +} + +std::vector> RepoDownloader::get_appstream_metadata() const { + std::vector> appstream_metadata; + /* The RepoDownloader::common_handle_setup() sets the expected names, + check for the starts_with() only here, to avoid copying the list here. */ + + for (auto it = metadata_paths.begin(); it != metadata_paths.end(); it++) { + const std::string type = it->first; + const std::string path = it->second; + + if (is_appstream_metadata_type(type)) + appstream_metadata.push_back(std::pair(type, path)); + } + return appstream_metadata; +} LibrepoHandle RepoDownloader::init_local_handle() { LibrepoHandle h; @@ -497,8 +516,34 @@ void RepoDownloader::common_handle_setup(LibrepoHandle & h) { // download the rest metadata added by 3rd parties for (auto & item : optional_metadata) { - dlist.push_back(item.c_str()); + // the appstream metadata is a "virtual" type, the list + // of the types is read from the repomd file + if (item.compare(libdnf5::METADATA_TYPE_APPSTREAM) != 0) + dlist.push_back(item.c_str()); } + if (optional_metadata.extract(libdnf5::METADATA_TYPE_APPSTREAM)) { + // ideally, the repomd.xml file should be read and every type matching is_appstream_metadata_type() + // would be added from it, but the content is not known at this point and when it is known, then + // it's too late, thus declare some "expected" types to be downloaded here + dlist.push_back("appstream"); + dlist.push_back("appstream-icons"); + dlist.push_back("appstream-icons-48x48"); + dlist.push_back("appstream-icons-48x48@2"); + dlist.push_back("appstream-icons-64x64"); + dlist.push_back("appstream-icons-64x64@2"); + dlist.push_back("appstream-icons-128x128"); + dlist.push_back("appstream-icons-128x128@2"); + // consult the prefixes with the is_appstream_metadata_type() + dlist.push_back("appdata"); + dlist.push_back("appdata-icons"); + dlist.push_back("appdata-icons-48x48"); + dlist.push_back("appdata-icons-48x48@2"); + dlist.push_back("appdata-icons-64x64"); + dlist.push_back("appdata-icons-64x64@2"); + dlist.push_back("appdata-icons-128x128"); + dlist.push_back("appdata-icons-128x128@2"); + } + dlist.push_back(nullptr); h.set_opt(LRO_YUMDLIST, dlist.data()); } diff --git a/libdnf5/repo/repo_downloader.hpp b/libdnf5/repo/repo_downloader.hpp index 1c14b272b..3f2fba090 100644 --- a/libdnf5/repo/repo_downloader.hpp +++ b/libdnf5/repo/repo_downloader.hpp @@ -56,6 +56,7 @@ class RepoDownloader { static constexpr const char * MD_FILENAME_GROUP_GZ = "group_gz"; static constexpr const char * MD_FILENAME_GROUP = "group"; static constexpr const char * MD_FILENAME_MODULES = "modules"; + static constexpr const char * MD_FILENAME_APPSTREAM = "appstream"; RepoDownloader(const libdnf5::BaseWeakPtr & base, const ConfigRepo & config, Repo::Type repo_type); @@ -74,6 +75,7 @@ class RepoDownloader { void * get_user_data() const noexcept; const std::string & get_metadata_path(const std::string & metadata_type) const; + std::vector> get_appstream_metadata() const; private: @@ -99,6 +101,7 @@ class RepoDownloader { time_t get_system_epoch() const; std::set get_optional_metadata() const; + bool is_appstream_metadata_type(const std::string & type) const; libdnf5::BaseWeakPtr base; const ConfigRepo & config; diff --git a/libdnf5/repo/solv_repo.cpp b/libdnf5/repo/solv_repo.cpp index 78657e1d4..7497de9ee 100644 --- a/libdnf5/repo/solv_repo.cpp +++ b/libdnf5/repo/solv_repo.cpp @@ -179,6 +179,9 @@ static const char * repodata_type_to_name(RepodataType type) { return RepoDownloader::MD_FILENAME_GROUP; case RepodataType::OTHER: return RepoDownloader::MD_FILENAME_OTHER; + case RepodataType::APPSTREAM: + libdnf_throw_assertion("No static filename for RepodataType::APPSTREAM"); + break; } libdnf_throw_assertion("Unknown RepodataType: {}", utils::to_underlying(type)); @@ -197,6 +200,8 @@ static int repodata_type_to_flags(RepodataType type) { return 0; case RepodataType::OTHER: return REPO_EXTEND_SOLVABLES | REPO_LOCALPOOL; + case RepodataType::APPSTREAM: + return 0; } libdnf_throw_assertion("Unknown RepodataType: {}", utils::to_underlying(type)); @@ -313,17 +318,22 @@ void SolvRepo::load_system_repo_ext(RepodataType type) { case RepodataType::OTHER: case RepodataType::PRESTO: case RepodataType::UPDATEINFO: + case RepodataType::APPSTREAM: throw SolvError(M_("Unsupported extended repodata type for the system repo: \"{}\"."), type_name); } } - void SolvRepo::load_repo_ext(RepodataType type, const RepoDownloader & downloader) { + std::string type_name = ""; + load_repo_ext(type, type_name, downloader); +} + +void SolvRepo::load_repo_ext(RepodataType type, const std::string & in_type_name, const RepoDownloader & downloader) { auto & logger = *base->get_logger(); solv::Pool & pool = type == RepodataType::COMPS ? static_cast(get_comps_pool(base)) : static_cast(get_rpm_pool(base)); - std::string type_name = repodata_type_to_name(type); + std::string type_name = in_type_name.empty() ? repodata_type_to_name(type) : in_type_name; std::string ext_fn; @@ -375,6 +385,8 @@ void SolvRepo::load_repo_ext(RepodataType type, const RepoDownloader & downloade case RepodataType::OTHER: res = repo_add_rpmmd(repo, ext_file.get(), 0, REPO_EXTEND_SOLVABLES); break; + case RepodataType::APPSTREAM: + break; } if (res != 0) { @@ -388,9 +400,9 @@ void SolvRepo::load_repo_ext(RepodataType type, const RepoDownloader & downloade if (config.get_build_cache_option().get_value()) { if (type == RepodataType::COMPS) { - write_ext(comps_repo->nrepodata - 1, type); + write_ext(comps_repo->nrepodata - 1, type, type_name); } else { - write_ext(repo->nrepodata - 1, type); + write_ext(repo->nrepodata - 1, type, type_name); } } } @@ -610,14 +622,13 @@ void SolvRepo::write_main(bool load_after_write) { } -void SolvRepo::write_ext(Id repodata_id, RepodataType type) { +void SolvRepo::write_ext(Id repodata_id, RepodataType type, const std::string & type_name) { libdnf_assert(repodata_id != 0, "0 is not a valid repodata id"); auto & logger = *base->get_logger(); solv::Pool & pool = type == RepodataType::COMPS ? static_cast(get_comps_pool(base)) : static_cast(get_rpm_pool(base)); - const std::string type_name = repodata_type_to_name(type); const auto solvfile_path = solv_file_path(type_name.c_str()); const auto solvfile_parent_dir = solvfile_path.parent_path(); diff --git a/libdnf5/repo/solv_repo.hpp b/libdnf5/repo/solv_repo.hpp index ae6e62e47..9f64efee6 100644 --- a/libdnf5/repo/solv_repo.hpp +++ b/libdnf5/repo/solv_repo.hpp @@ -52,7 +52,7 @@ struct SolvUserdata { namespace libdnf5::repo { using LibsolvRepo = ::Repo; -enum class RepodataType { FILELISTS, PRESTO, UPDATEINFO, COMPS, OTHER }; +enum class RepodataType { FILELISTS, PRESTO, UPDATEINFO, COMPS, OTHER, APPSTREAM }; class SolvError : public Error { @@ -73,6 +73,7 @@ class SolvRepo { /// Loads additional metadata (filelist, others, ...) from available repo. void load_repo_ext(RepodataType type, const RepoDownloader & downloader); + void load_repo_ext(RepodataType type, const std::string & in_type_name, const RepoDownloader & downloader); /// Loads system repository into the pool. /// @@ -128,7 +129,7 @@ class SolvRepo { void write_main(bool load_after_write); /// Writes libsolv's .solvx cache file with extended libsolv repodata. - void write_ext(Id repodata_id, RepodataType type); + void write_ext(Id repodata_id, RepodataType type, const std::string & type_name); std::string solv_file_name(const char * type = nullptr); std::filesystem::path solv_file_path(const char * type = nullptr);