From c46d96472193bf2147bcb2c9ec1ca061c1461499 Mon Sep 17 00:00:00 2001 From: Milan Crha Date: Fri, 8 Nov 2024 12:31:30 +0100 Subject: [PATCH] Support repo's Appstream data download and install Repositories can provide Appstream data for the packages they contain. This Appstream data is consumed by applications like the GNOME Software or KDE Discover, thus the users can see the packages (apps) in them. This is to be in pair with PackageKit, which does download and install the repo's Appstream data. As this adds a dependency on the `appstream` library, there is also a CMake option WITH_APPSTREAM to be able to turn the support off. The support is built by default, but the Appstream data is not downloaded unless "optional_metadata_types" config option contains "appstream". Closes https://github.com/rpm-software-management/dnf5/issues/1564 --- CMakeLists.txt | 5 ++++ dnf5.spec | 5 ++++ include/libdnf5/conf/const.hpp | 8 +++++- include/libdnf5/repo/repo.hpp | 4 +++ libdnf5/CMakeLists.txt | 7 ++++++ libdnf5/repo/repo.cpp | 36 +++++++++++++++++++++++++++ libdnf5/repo/repo_downloader.cpp | 42 ++++++++++++++++++++++++++++++++ libdnf5/repo/repo_downloader.hpp | 3 +++ libdnf5/repo/repo_sack.cpp | 1 + libdnf5/repo/solv_repo.cpp | 23 ++++++++++++----- libdnf5/repo/solv_repo.hpp | 5 ++-- 11 files changed, 130 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b031aea84..835a59c6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ option(WITH_COMPS "Build with comps groups and environments support" ON) option(WITH_MODULEMD "Build with modulemd modules support" ON) option(WITH_ZCHUNK "Build with zchunk delta compression support" ON) option(WITH_SYSTEMD "Build with systemd and D-Bus features" ON) +option(WITH_APPSTREAM "Build with appstream support" ON) option(ENABLE_SOLV_URPMREORDER "Build with support for URPM-like solution reordering?" OFF) option(ENABLE_SOLV_FOCUSNEW "Build with SOLVER_FLAG_FOCUS_NEW libsolv flag enabled to ensure new dependencies are installed in latests versions?" ON) @@ -129,6 +130,10 @@ if (WITH_MODULEMD) add_definitions(-DWITH_MODULEMD) endif() +if (WITH_APPSTREAM) + add_definitions(-DWITH_APPSTREAM) +endif() + include_directories("${PROJECT_SOURCE_DIR}/include") include_directories("${PROJECT_SOURCE_DIR}/common") diff --git a/dnf5.spec b/dnf5.spec index cb0265665..51a48a35d 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -71,6 +71,7 @@ Provides: dnf5-command(versionlock) # ========== build options ========== +%bcond_without appstream %bcond_without dnf5daemon_client %bcond_without dnf5daemon_server %bcond_without libdnf_cli @@ -143,6 +144,9 @@ BuildRequires: bash-completion BuildRequires: cmake BuildRequires: doxygen BuildRequires: gettext +%if %{with appstream} +BuildRequires: pkgconfig(appstream) >= 0.16 +%endif BuildRequires: pkgconfig(check) BuildRequires: pkgconfig(fmt) BuildRequires: pkgconfig(json-c) @@ -796,6 +800,7 @@ automatically and regularly from systemd timers, cron jobs or similar. \ -DENABLE_SOLV_FOCUSNEW=%{?with_focus_new:ON}%{!?with_focus_new:OFF} \ \ + -DWITH_APPSTREAM=%{?with_appstream:ON}%{!?with_appstream:OFF} \ -DWITH_DNF5DAEMON_CLIENT=%{?with_dnf5daemon_client:ON}%{!?with_dnf5daemon_client:OFF} \ -DWITH_DNF5DAEMON_SERVER=%{?with_dnf5daemon_server:ON}%{!?with_dnf5daemon_server:OFF} \ -DWITH_LIBDNF5_CLI=%{?with_libdnf_cli:ON}%{!?with_libdnf_cli:OFF} \ diff --git a/include/libdnf5/conf/const.hpp b/include/libdnf5/conf/const.hpp index d75610586..c3207a2a6 100644 --- a/include/libdnf5/conf/const.hpp +++ b/include/libdnf5/conf/const.hpp @@ -63,9 +63,15 @@ constexpr const char * METADATA_TYPE_FILELISTS = "filelists"; 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_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 425982882..4e547fd64 100644 --- a/include/libdnf5/repo/repo.hpp +++ b/include/libdnf5/repo/repo.hpp @@ -144,6 +144,10 @@ class LIBDNF_API Repo { // @replaces libdnf:repo/Repo.hpp:method:Repo.loadCache(bool throwExcept) void read_metadata_cache(); + /// Installs downloaded Appstream data for the repo, if available and + /// if built with the Appstream support. + void install_appstream(); + /// Checks whether the locally downloaded metadata are in sync with the origin. /// @return `true` if metadata are in sync with the origin, `false` otherwise. bool is_in_sync(); diff --git a/libdnf5/CMakeLists.txt b/libdnf5/CMakeLists.txt index d360901ee..b2a0798ce 100644 --- a/libdnf5/CMakeLists.txt +++ b/libdnf5/CMakeLists.txt @@ -81,6 +81,13 @@ if (WITH_MODULEMD) target_link_libraries(libdnf5_static PRIVATE ${LIBMODULEMD_LIBRARIES}) endif() +if (WITH_APPSTREAM) + pkg_check_modules(APPSTREAM REQUIRED appstream>=0.16) + include_directories(${APPSTREAM_INCLUDE_DIRS}) + target_link_libraries(libdnf5 PRIVATE ${APPSTREAM_LIBRARIES}) + target_link_libraries(libdnf5_static PRIVATE ${APPSTREAM_LIBRARIES}) +endif() + if (ENABLE_SOLV_FOCUSNEW) pkg_check_modules(LIBSOLV REQUIRED libsolv>=0.7.30) else() diff --git a/libdnf5/repo/repo.cpp b/libdnf5/repo/repo.cpp index c621eab67..6a407fdba 100644 --- a/libdnf5/repo/repo.cpp +++ b/libdnf5/repo/repo.cpp @@ -52,6 +52,9 @@ extern "C" { #include #include +#ifdef WITH_APPSTREAM +#include +#endif namespace libdnf5::repo { @@ -203,6 +206,31 @@ void Repo::read_metadata_cache() { p_impl->downloader->load_local(); } +void Repo::install_appstream() { +#ifdef WITH_APPSTREAM + if (!p_impl->config.get_main_config().get_optional_metadata_types_option().get_value().contains( + libdnf5::METADATA_TYPE_APPSTREAM)) + return; + + std::string repo_id = p_impl->config.get_id(); + std::vector> appstream_metadata = p_impl->downloader->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)) { + p_impl->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); + } +#endif +} bool Repo::is_in_sync() { if (!p_impl->config.get_metalink_option().empty() && !p_impl->config.get_metalink_option().get_value().empty()) { @@ -412,6 +440,14 @@ void Repo::load_available_repo() { auto optional_metadata = p_impl->config.get_main_config().get_optional_metadata_types_option().get_value(); + if (optional_metadata.contains(libdnf5::METADATA_TYPE_APPSTREAM)) { + std::vector> 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 (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 b142aabbe..9cf44c39a 100644 --- a/libdnf5/repo/repo_downloader.cpp +++ b/libdnf5/repo/repo_downloader.cpp @@ -389,6 +389,26 @@ 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 (std::map::const_iterator 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; @@ -493,6 +513,28 @@ void RepoDownloader::common_handle_setup(LibrepoHandle & h) { if (optional_metadata.extract(libdnf5::METADATA_TYPE_UPDATEINFO)) { dlist.push_back(MD_FILENAME_UPDATEINFO); } + if (get_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"); + } // download the rest metadata added by 3rd parties for (auto & item : optional_metadata) { 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/repo_sack.cpp b/libdnf5/repo/repo_sack.cpp index 62e6e92ad..fadaed389 100644 --- a/libdnf5/repo/repo_sack.cpp +++ b/libdnf5/repo/repo_sack.cpp @@ -574,6 +574,7 @@ void RepoSack::Impl::update_and_load_repos(libdnf5::repo::RepoQuery & repos, boo RepoCache(base, cache_dir).remove_attribute(RepoCache::ATTRIBUTE_EXPIRED); repo->mark_fresh(); repo->read_metadata_cache(); + repo->install_appstream(); repos_for_processing.erase(repos_for_processing.begin() + static_cast(idx)); send_to_sack_loader(repo); diff --git a/libdnf5/repo/solv_repo.cpp b/libdnf5/repo/solv_repo.cpp index 67025e452..0d9373335 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);