diff --git a/SOURCES/0004-copr-dont-traceback-on-empty-lines-in-etcos-release.patch b/SOURCES/0004-copr-dont-traceback-on-empty-lines-in-etcos-release.patch new file mode 100644 index 0000000..60a4db5 --- /dev/null +++ b/SOURCES/0004-copr-dont-traceback-on-empty-lines-in-etcos-release.patch @@ -0,0 +1,33 @@ +From ebacba86979d16cdb92ace9d7dc601a85c97b5db Mon Sep 17 00:00:00 2001 +From: Jakub Kadlcik +Date: Tue, 12 Oct 2021 18:30:47 +0200 +Subject: [PATCH] copr: don't traceback on empty lines in /etc/os-release + +Fix RHBZ 1994944 +--- + plugins/copr.py | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/plugins/copr.py b/plugins/copr.py +index 4644495..8841f03 100644 +--- a/plugins/copr.py ++++ b/plugins/copr.py +@@ -50,8 +50,13 @@ except ImportError: + with open('/etc/os-release') as os_release_file: + os_release_data = {} + for line in os_release_file: +- os_release_key, os_release_value = line.rstrip().split('=') +- os_release_data[os_release_key] = os_release_value.strip('"') ++ try: ++ os_release_key, os_release_value = line.rstrip().split('=') ++ os_release_data[os_release_key] = os_release_value.strip('"') ++ except ValueError: ++ # Skip empty lines and everything that is not a simple ++ # variable assignment ++ pass + return (os_release_data['NAME'], os_release_data['VERSION_ID'], None) + + PLUGIN_CONF = 'copr' +-- +libgit2 1.0.1 + diff --git a/SOURCES/0005-reposync-Use-fail_fastFalse-when-downloading-packages-RhBug2009894.patch b/SOURCES/0005-reposync-Use-fail_fastFalse-when-downloading-packages-RhBug2009894.patch new file mode 100644 index 0000000..1e5a989 --- /dev/null +++ b/SOURCES/0005-reposync-Use-fail_fastFalse-when-downloading-packages-RhBug2009894.patch @@ -0,0 +1,40 @@ +From b60f27006cdbdd14fb480aa22610fcd32bfe41e5 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Wed, 6 Oct 2021 13:40:55 +0200 +Subject: [PATCH] reposync: Use fail_fast=False when downloading packages (RhBug:2009894) + += changelog = +msg: Reposync does not stop downloading packages on the first error +type: bugfix +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=2009894 +--- + dnf-plugins-core.spec | 2 +- + plugins/reposync.py | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec +index 83ae6ae..cef836f 100644 +--- a/dnf-plugins-core.spec ++++ b/dnf-plugins-core.spec +@@ -1,4 +1,4 @@ +-%{?!dnf_lowest_compatible: %global dnf_lowest_compatible 4.2.22} ++%{?!dnf_lowest_compatible: %global dnf_lowest_compatible 4.9.2} + %global dnf_plugins_extra 2.0.0 + %global hawkey_version 0.46.1 + %global yum_utils_subpackage_name dnf-utils +diff --git a/plugins/reposync.py b/plugins/reposync.py +index 66c76a7..0ff936f 100644 +--- a/plugins/reposync.py ++++ b/plugins/reposync.py +@@ -303,7 +303,7 @@ class RepoSyncCommand(dnf.cli.Command): + progress, 0) + payloads = [RPMPayloadLocation(pkg, progress, self.pkg_download_path(pkg)) + for pkg in pkglist] +- base._download_remote_payloads(payloads, drpm, progress, None) ++ base._download_remote_payloads(payloads, drpm, progress, None, False) + + def print_urls(self, pkglist): + for pkg in pkglist: +-- +libgit2 1.0.1 + diff --git a/SOURCES/0006-copr-migrate-all-calls-to-APIv3.patch b/SOURCES/0006-copr-migrate-all-calls-to-APIv3.patch new file mode 100644 index 0000000..b23ce54 --- /dev/null +++ b/SOURCES/0006-copr-migrate-all-calls-to-APIv3.patch @@ -0,0 +1,101 @@ +From 54b7c5f91b4ad1db1f716f25cc7973ec7542f0d4 Mon Sep 17 00:00:00 2001 +From: Jakub Kadlcik +Date: Tue, 12 Oct 2021 12:54:05 +0200 +Subject: [PATCH] copr: migrate all calls to APIv3 + +In the latest Copr release we dropped all APIv1 code from frontend. +https://docs.pagure.org/copr.copr/release-notes/2021-10-01.html + +Unfortunatelly we frogot to migrate DNF copr plugin to APIv3 and +therefore the following commands started failing with 404. + + dnf copr search tests + dnf copr list --available-by-user frostyx +--- + plugins/copr.py | 40 +++++++++++++++++----------------------- + 1 file changed, 17 insertions(+), 23 deletions(-) + +diff --git a/plugins/copr.py b/plugins/copr.py +index 8841f03..7fc6c6f 100644 +--- a/plugins/copr.py ++++ b/plugins/copr.py +@@ -355,51 +355,45 @@ Bugzilla. In case of problems, contact the owner of this repository. + "Re-enable the project to fix this.")) + + def _list_user_projects(self, user_name): +- # http://copr.fedorainfracloud.org/api/coprs/ignatenkobrain/ +- api_path = "/api/coprs/{}/".format(user_name) +- res = self.base.urlopen(self.copr_url + api_path, mode='w+') ++ # https://copr.fedorainfracloud.org/api_3/project/list?ownername=ignatenkobrain ++ api_path = "/api_3/project/list?ownername={0}".format(user_name) ++ url = self.copr_url + api_path ++ res = self.base.urlopen(url, mode='w+') + try: + json_parse = json.loads(res.read()) + except ValueError: + raise dnf.exceptions.Error( + _("Can't parse repositories for username '{}'.") + .format(user_name)) + self._check_json_output(json_parse) + section_text = _("List of {} coprs").format(user_name) + self._print_match_section(section_text) +- i = 0 +- while i < len(json_parse["repos"]): +- msg = "{0}/{1} : ".format(user_name, +- json_parse["repos"][i]["name"]) +- desc = json_parse["repos"][i]["description"] +- if not desc: +- desc = _("No description given") ++ ++ for item in json_parse["items"]: ++ msg = "{0}/{1} : ".format(user_name, item["name"]) ++ desc = item["description"] or _("No description given") + msg = self.base.output.fmtKeyValFill(ucd(msg), desc) + print(msg) +- i += 1 + + def _search(self, query): +- # http://copr.fedorainfracloud.org/api/coprs/search/tests/ +- api_path = "/api/coprs/search/{}/".format(query) +- res = self.base.urlopen(self.copr_url + api_path, mode='w+') ++ # https://copr.fedorainfracloud.org/api_3/project/search?query=tests ++ api_path = "/api_3/project/search?query={}".format(query) ++ url = self.copr_url + api_path ++ res = self.base.urlopen(url, mode='w+') + try: + json_parse = json.loads(res.read()) + except ValueError: + raise dnf.exceptions.Error(_("Can't parse search for '{}'." + ).format(query)) + self._check_json_output(json_parse) + section_text = _("Matched: {}").format(query) + self._print_match_section(section_text) +- i = 0 +- while i < len(json_parse["repos"]): +- msg = "{0}/{1} : ".format(json_parse["repos"][i]["username"], +- json_parse["repos"][i]["coprname"]) +- desc = json_parse["repos"][i]["description"] +- if not desc: +- desc = _("No description given.") ++ ++ for item in json_parse["items"]: ++ msg = "{0} : ".format(item["full_name"]) ++ desc = item["description"] or _("No description given.") + msg = self.base.output.fmtKeyValFill(ucd(msg), desc) + print(msg) +- i += 1 + + def _print_match_section(self, text): + formatted = self.base.output.fmtSection(text) +@@ -624,7 +618,7 @@ Bugzilla. In case of problems, contact the owner of this repository. + + @classmethod + def _check_json_output(cls, json_obj): +- if json_obj["output"] != "ok": ++ if "error" in json_obj: + raise dnf.exceptions.Error("{}".format(json_obj["error"])) + + @classmethod +-- +libgit2 1.0.1 + diff --git a/SOURCES/0007-groups-manager-More-benevolent-resolving-of-packages-RhBug2013633.patch b/SOURCES/0007-groups-manager-More-benevolent-resolving-of-packages-RhBug2013633.patch new file mode 100644 index 0000000..274f1c5 --- /dev/null +++ b/SOURCES/0007-groups-manager-More-benevolent-resolving-of-packages-RhBug2013633.patch @@ -0,0 +1,31 @@ +From 5c8f753503be87e5d6237be12eec2330236d78ed Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Mon, 8 Nov 2021 16:51:56 +0100 +Subject: [PATCH] groups-manager: More benevolent resolving of packages (RhBug:2013633) + += changelog = +msg: groups-manager uses for matching packages full NEVRA and not only name. +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=2013633 +--- + plugins/groups_manager.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/plugins/groups_manager.py b/plugins/groups_manager.py +index 382df37..12da183 100644 +--- a/plugins/groups_manager.py ++++ b/plugins/groups_manager.py +@@ -254,7 +254,9 @@ class GroupsManagerCommand(dnf.cli.Command): + # find packages according to specifications from command line + packages = set() + for pkg_spec in self.opts.packages: +- q = self.base.sack.query().filterm(name__glob=pkg_spec).latest() ++ subj = dnf.subject.Subject(pkg_spec) ++ q = subj.get_best_query(self.base.sack, with_nevra=True, ++ with_provides=False, with_filenames=False).latest() + if not q: + logger.warning(_("No match for argument: {}").format(pkg_spec)) + continue +-- +libgit2 1.1.0 + diff --git a/SOURCES/0008-versionlock-fix-multi-pkg-lock-RhBug2013324.patch b/SOURCES/0008-versionlock-fix-multi-pkg-lock-RhBug2013324.patch new file mode 100644 index 0000000..d517175 --- /dev/null +++ b/SOURCES/0008-versionlock-fix-multi-pkg-lock-RhBug2013324.patch @@ -0,0 +1,95 @@ +From 0030ea94dd261b66cac6f08c9dfa99e3d8ee3648 Mon Sep 17 00:00:00 2001 +From: Nicola Sella +Date: Mon, 1 Nov 2021 18:29:40 +0100 +Subject: [PATCH] [versionlock] fix multi pkg lock (RhBug:2013324) + += changelog = +msg: [versionlock] Fix: Multiple package-name-spec arguments don't lock +correctly (RhBug:2001039) +type: bugfix +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=2013324 +--- + plugins/versionlock.py | 57 +++++++++++++++++++++++++++++++++------------------------ + 1 file changed, 33 insertions(+), 24 deletions(-) + +diff --git a/plugins/versionlock.py b/plugins/versionlock.py +index c89a75d..77b7f91 100644 +--- a/plugins/versionlock.py ++++ b/plugins/versionlock.py +@@ -167,25 +167,27 @@ class VersionLockCommand(dnf.cli.Command): + cmd = self.opts.subcommand + + if cmd == 'add': +- (entry, entry_cmd) = _search_locklist(self.opts.package) +- if entry == '': +- _write_locklist(self.base, self.opts.package, self.opts.raw, True, +- "\n# Added lock on %s\n" % time.ctime(), +- ADDING_SPEC, '') +- elif cmd != entry_cmd: +- raise dnf.exceptions.Error(ALREADY_EXCLUDED.format(entry)) +- else: +- logger.info("%s %s", EXISTING_SPEC, entry) ++ results = _search_locklist(self.opts.package) ++ for entry, entry_cmd in results: ++ if entry_cmd == '': ++ _write_locklist(self.base, [entry], self.opts.raw, True, ++ "\n# Added lock on %s\n" % time.ctime(), ++ ADDING_SPEC, '') ++ elif cmd != entry_cmd: ++ raise dnf.exceptions.Error(ALREADY_EXCLUDED.format(entry)) ++ else: ++ logger.info("%s %s", EXISTING_SPEC, entry) + elif cmd == 'exclude': +- (entry, entry_cmd) = _search_locklist(self.opts.package) +- if entry == '': +- _write_locklist(self.base, self.opts.package, self.opts.raw, False, +- "\n# Added exclude on %s\n" % time.ctime(), +- EXCLUDING_SPEC, '!') +- elif cmd != entry_cmd: +- raise dnf.exceptions.Error(ALREADY_LOCKED.format(entry)) +- else: +- logger.info("%s %s", EXISTING_SPEC, entry) ++ results = _search_locklist(self.opts.package) ++ for entry, entry_cmd in results: ++ if entry_cmd == '': ++ _write_locklist(self.base, [entry], self.opts.raw, False, ++ "\n# Added exclude on %s\n" % time.ctime(), ++ EXCLUDING_SPEC, '!') ++ elif cmd != entry_cmd: ++ raise dnf.exceptions.Error(ALREADY_LOCKED.format(entry)) ++ else: ++ logger.info("%s %s", EXISTING_SPEC, entry) + elif cmd == 'list': + for pat in _read_locklist(): + print(pat) +@@ -233,14 +235,21 @@ def _read_locklist(): + + + def _search_locklist(package): ++ results = [] + found = action = '' + locked_specs = _read_locklist() +- for ent in locked_specs: +- if _match(ent, package): +- found = ent +- action = 'exclude' if ent.startswith('!') else 'add' +- break +- return (found, action) ++ for pkg in package: ++ match = False ++ for ent in locked_specs: ++ found = action = '' ++ if _match(ent, [pkg]): ++ found = ent ++ action = 'exclude' if ent.startswith('!') else 'add' ++ results.append((found, action)) ++ match = True ++ if not match: ++ results.append((pkg, action)) ++ return results + + + def _write_locklist(base, args, raw, try_installed, comment, info, prefix): +-- +libgit2 1.1.0 + diff --git a/SOURCES/0009-Update-documentation-for-adding-specific-version-RhBug2013332.patch b/SOURCES/0009-Update-documentation-for-adding-specific-version-RhBug2013332.patch new file mode 100644 index 0000000..445c330 --- /dev/null +++ b/SOURCES/0009-Update-documentation-for-adding-specific-version-RhBug2013332.patch @@ -0,0 +1,31 @@ +From ed05ce74cfb9151ea5218da0f8b9eccb70c00f70 Mon Sep 17 00:00:00 2001 +From: Nicola Sella +Date: Thu, 11 Nov 2021 13:48:39 +0100 +Subject: [PATCH] Update documentation for adding specific version (RhBug:2013332) + +=changelog= +msg: [versionlock] update documentation for adding specifi version +type: bugfix +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=2013332 +--- + doc/versionlock.rst | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/doc/versionlock.rst b/doc/versionlock.rst +index 061ce80..1ac7196 100644 +--- a/doc/versionlock.rst ++++ b/doc/versionlock.rst +@@ -97,6 +97,10 @@ Subcommands + Adding versionlock on: mutt-5:1.11.4-1.fc30.* + Adding versionlock on: mutt-5:1.12.1-3.fc30.* + ++ .. note:: Be careful when adding specific versions ++ ++ If you add a package specifying a version with ``dnf versionlock mutt-5:1.11.4-1.fc30.x86_64`` then, if you run ``dnf versionlock add mutt`` ++ versionlock will not add ``mutt-5:1.12.1-3.fc30.x86_64``. + + ``dnf versionlock exclude `` + Add an exclude (within versionlock) for the available packages matching the spec. It means that +-- +libgit2 1.1.0 + diff --git a/SOURCES/0010-needs-restarting-Fix-wrong-boot-time-RhBug1960437.patch b/SOURCES/0010-needs-restarting-Fix-wrong-boot-time-RhBug1960437.patch new file mode 100644 index 0000000..3090d60 --- /dev/null +++ b/SOURCES/0010-needs-restarting-Fix-wrong-boot-time-RhBug1960437.patch @@ -0,0 +1,25 @@ +From dc13ed6bab62a38ef74b00376e2ba05c82115e47 Mon Sep 17 00:00:00 2001 +From: Nicola Sella +Date: Thu, 8 Jul 2021 15:54:21 +0200 +Subject: [PATCH] [needs-restarting] Fix wrong boot time (RhBug:1960437) + +--- + plugins/needs_restarting.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/plugins/needs_restarting.py b/plugins/needs_restarting.py +index 1fedb73..91dbe66 100644 +--- a/plugins/needs_restarting.py ++++ b/plugins/needs_restarting.py +@@ -199,7 +199,7 @@ class ProcessStart(object): + + @staticmethod + def get_boot_time(): +- return int(os.stat('/proc/1/cmdline').st_mtime) ++ return int(os.stat('/proc/1').st_mtime) + + @staticmethod + def get_sc_clk_tck(): +-- +libgit2 1.1.0 + diff --git a/SOURCES/0011-Add-new-command-modulesync-RhBug1868047.patch b/SOURCES/0011-Add-new-command-modulesync-RhBug1868047.patch new file mode 100644 index 0000000..9dfc812 --- /dev/null +++ b/SOURCES/0011-Add-new-command-modulesync-RhBug1868047.patch @@ -0,0 +1,439 @@ +From 6ea94d9c768eb45975f314e11ab9dd88284fa380 Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Mon, 27 Sep 2021 11:29:01 +0200 +Subject: [PATCH] Add new command modulesync (RhBug:1868047) + +It will download module metadata from all enabled repositories, +module artifacts and profiles of matching modules. Then it creates +a repository. + += changelog = +msg: Add a new subpackage with modulesync command. The command +downloads packages from modules and/or creates a repository with modular +data. +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1868047 +--- + dnf-plugins-core.spec | 20 ++++++++++++++++++++ + doc/CMakeLists.txt | 1 + + doc/conf.py | 1 + + doc/index.rst | 1 + + doc/modulesync.rst | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + plugins/CMakeLists.txt | 1 + + plugins/modulesync.py | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + 7 files changed, 335 insertions(+) + create mode 100644 doc/modulesync.rst + create mode 100644 plugins/modulesync.py + +diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec +index cef836f..afdbcbb 100644 +--- a/dnf-plugins-core.spec ++++ b/dnf-plugins-core.spec +@@ -402,6 +402,19 @@ versions of those packages. This allows you to e.g. protect packages from being + updated by newer versions. + %endif + ++%if %{with python3} ++%package -n python3-dnf-plugin-modulesync ++Summary: Download module metadata and packages and create repository ++Requires: python3-%{name} = %{version}-%{release} ++Requires: createrepo_c >= 0.17.4 ++Provides: dnf-plugin-modulesync = %{version}-%{release} ++Provides: dnf-command(modulesync) ++ ++%description -n python3-dnf-plugin-modulesync ++Download module metadata from all enabled repositories, module artifacts and profiles of matching modules and create ++repository. ++%endif ++ + %prep + %autosetup + %if %{with python2} +@@ -762,6 +775,13 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/ + %endif + %endif + ++%if %{with python3} ++%files -n python3-dnf-plugin-modulesync ++%{python3_sitelib}/dnf-plugins/modulesync.* ++%{python3_sitelib}/dnf-plugins/__pycache__/modulesync.* ++%{_mandir}/man8/dnf-modulesync.* ++%endif ++ + %changelog + * Mon Apr 12 2021 Nicola Sella - 4.0.21-1 + - Add missing command line option to documentation +diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt +index 3fb665d..ff84cf8 100644 +--- a/doc/CMakeLists.txt ++++ b/doc/CMakeLists.txt +@@ -28,6 +28,7 @@ INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf-builddep.8 + ${CMAKE_CURRENT_BINARY_DIR}/dnf-generate_completion_cache.8 + ${CMAKE_CURRENT_BINARY_DIR}/dnf-groups-manager.8 + ${CMAKE_CURRENT_BINARY_DIR}/dnf-leaves.8 ++ ${CMAKE_CURRENT_BINARY_DIR}/dnf-modulesync.8 + ${CMAKE_CURRENT_BINARY_DIR}/dnf-needs-restarting.8 + ${CMAKE_CURRENT_BINARY_DIR}/dnf-repoclosure.8 + ${CMAKE_CURRENT_BINARY_DIR}/dnf-repodiff.8 +diff --git a/doc/conf.py b/doc/conf.py +index 645185a..41d6936 100644 +--- a/doc/conf.py ++++ b/doc/conf.py +@@ -254,6 +254,7 @@ man_pages = [ + ('groups-manager', 'dnf-groups-manager', u'DNF groups-manager Plugin', AUTHORS, 8), + ('leaves', 'dnf-leaves', u'DNF leaves Plugin', AUTHORS, 8), + ('local', 'dnf-local', u'DNF local Plugin', AUTHORS, 8), ++ ('modulesync', 'dnf-modulesync', u'DNF modulesync Plugin', AUTHORS, 8), + ('needs_restarting', 'dnf-needs-restarting', u'DNF needs_restarting Plugin', AUTHORS, 8), + ('repoclosure', 'dnf-repoclosure', u'DNF repoclosure Plugin', AUTHORS, 8), + ('repodiff', 'dnf-repodiff', u'DNF repodiff Plugin', AUTHORS, 8), +diff --git a/doc/index.rst b/doc/index.rst +index 7213253..07f6052 100644 +--- a/doc/index.rst ++++ b/doc/index.rst +@@ -37,6 +37,7 @@ This documents core plugins of DNF: + leaves + local + migrate ++ modulesync + needs_restarting + post-transaction-actions + repoclosure +diff --git a/doc/modulesync.rst b/doc/modulesync.rst +new file mode 100644 +index 0000000..2837287 +--- /dev/null ++++ b/doc/modulesync.rst +@@ -0,0 +1,103 @@ ++.. ++ Copyright (C) 2015 Red Hat, Inc. ++ ++ This copyrighted material is made available to anyone wishing to use, ++ modify, copy, or redistribute it subject to the terms and conditions of ++ the GNU General Public License v.2, or (at your option) any later version. ++ This program is distributed in the hope that it will be useful, but WITHOUT ++ ANY WARRANTY expressed or implied, including the implied warranties 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. Any Red Hat trademarks that are incorporated in the ++ source code or documentation are not subject to the GNU General Public ++ License and may only be used or replicated with the express permission of ++ Red Hat, Inc. ++ ++==================== ++DNF modulesync Plugin ++==================== ++ ++Download packages from modules and/or create a repository with modular data. ++ ++-------- ++Synopsis ++-------- ++ ++``dnf modulesync [options] [...]`` ++ ++----------- ++Description ++----------- ++ ++`modulesync` downloads packages from modules according to provided arguments and creates a repository with modular data ++in working directory. In environment with modules it is recommend to use the command for redistribution of packages, ++because DNF does not allow installation of modular packages without modular metadata on the system (Fail-safe ++mechanism). The command without an argument creates a repository like `createrepo_c` but with modular metadata collected ++from all available repositories. ++ ++See examples. ++ ++--------- ++Arguments ++--------- ++ ++```` ++ Module specification for the package to download. The argument is an optional. ++ ++------- ++Options ++------- ++ ++All general DNF options are accepted. Namely, the ``--destdir`` option can be used to specify directory where packages ++will be downloaded and the new repository created. See `Options` in :manpage:`dnf(8)` for details. ++ ++ ++``-n, --newest-only`` ++ Download only packages from the newest modules. ++ ++``--enable_source_repos`` ++ Enable repositories with source packages ++ ++``--enable_debug_repos`` ++ Enable repositories with debug-info and debug-source packages ++ ++``--resolve`` ++ Resolve and download needed dependencies ++ ++-------- ++Examples ++-------- ++ ++``dnf modulesync nodejs`` ++ Download packages from `nodejs` module and crete a repository with modular metadata in working directory ++ ++``dnf download nodejs`` ++ ++``dnf modulesync`` ++ The first `download` command downloads nodejs package into working directory. In environment with modules `nodejs` ++ package can be a modular package therefore when I create a repository I have to insert also modular metadata ++ from available repositories to ensure 100% functionality. Instead of `createrepo_c` use `dnf modulesync` ++ to create a repository in working directory with nodejs package and modular metadata. ++ ++``dnf --destdir=/tmp/my-temp modulesync nodejs:14/minimal --resolve`` ++ Download package required for installation of `minimal` profile from module `nodejs` and stream `14` into directory ++ `/tmp/my-temp` and all required dependencies. Then it will create a repository in `/tmp/my-temp` directory with ++ previously downloaded packages and modular metadata from all available repositories. ++ ++``dnf module install nodejs:14/minimal --downloadonly --destdir=/tmp/my-temp`` ++ ++``dnf modulesync --destdir=/tmp/my-temp`` ++ The first `dnf module install` command downloads package from required for installation of `minimal` profile from module ++ `nodejs` and stream `14` into directory `/tmp/my-temp`. The second command `dnf modulesync` will create ++ a repository in `/tmp/my-temp` directory with previously downloaded packages and modular metadata from all ++ available repositories. In comparison to `dnf --destdir=/tmp/my-temp modulesync nodejs:14/minimal --resolve` it will ++ only download packages required for installation on current system. ++ ++ ++-------- ++See Also ++-------- ++ ++* :manpage:`dnf(8)`, DNF Command Reference +diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt +index f66d3df..59f148f 100644 +--- a/plugins/CMakeLists.txt ++++ b/plugins/CMakeLists.txt +@@ -22,6 +22,7 @@ INSTALL (FILES repograph.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) + INSTALL (FILES repomanage.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) + INSTALL (FILES reposync.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) + INSTALL (FILES show_leaves.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) ++INSTALL (FILES modulesync.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) + INSTALL (FILES versionlock.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) + + ADD_SUBDIRECTORY (dnfpluginscore) +diff --git a/plugins/modulesync.py b/plugins/modulesync.py +new file mode 100644 +index 0000000..c1c33e4 +--- /dev/null ++++ b/plugins/modulesync.py +@@ -0,0 +1,208 @@ ++# Copyright (C) 2021 Red Hat, Inc. ++# ++# This copyrighted material is made available to anyone wishing to use, ++# modify, copy, or redistribute it subject to the terms and conditions of ++# the GNU General Public License v.2, or (at your option) any later version. ++# This program is distributed in the hope that it will be useful, but WITHOUT ++# ANY WARRANTY expressed or implied, including the implied warranties 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. Any Red Hat trademarks that are incorporated in the ++# source code or documentation are not subject to the GNU General Public ++# License and may only be used or replicated with the express permission of ++# Red Hat, Inc. ++# ++ ++from __future__ import absolute_import ++from __future__ import unicode_literals ++from dnfpluginscore import _, P_, logger ++from dnf.cli.option_parser import OptionParser ++ ++import os ++import shutil ++import subprocess ++ ++import dnf ++import dnf.cli ++import dnf.i18n ++import hawkey ++ ++ ++@dnf.plugin.register_command ++class SyncToolCommand(dnf.cli.Command): ++ ++ aliases = ['modulesync'] ++ summary = _('Download packages from modules and/or create a repository with modular data') ++ ++ def __init__(self, cli): ++ super(SyncToolCommand, self).__init__(cli) ++ ++ @staticmethod ++ def set_argparser(parser): ++ parser.add_argument('module', nargs='*', metavar=_('MODULE'), ++ help=_('modules to download')) ++ parser.add_argument("--enable_source_repos", action='store_true', ++ help=_('enable repositories with source packages')) ++ parser.add_argument("--enable_debug_repos", action='store_true', ++ help=_('enable repositories with debug-info and debug-source packages')) ++ parser.add_argument('--resolve', action='store_true', ++ help=_('resolve and download needed dependencies')) ++ parser.add_argument('-n', '--newest-only', default=False, action='store_true', ++ help=_('download only packages from newest modules')) ++ ++ def configure(self): ++ # setup sack and populate it with enabled repos ++ demands = self.cli.demands ++ demands.sack_activation = True ++ demands.available_repos = True ++ ++ demands.load_system_repo = False ++ ++ if self.opts.enable_source_repos: ++ self.base.repos.enable_source_repos() ++ ++ if self.opts.enable_debug_repos: ++ self.base.repos.enable_debug_repos() ++ ++ if self.opts.destdir: ++ self.base.conf.destdir = self.opts.destdir ++ else: ++ self.base.conf.destdir = dnf.i18n.ucd(os.getcwd()) ++ ++ def run(self): ++ """Execute the util action here.""" ++ ++ pkgs = self.base.sack.query().filterm(empty=True) ++ no_matched_spec = [] ++ for module_spec in self.opts.module: ++ try: ++ pkgs = pkgs.union(self._get_packages_from_modules(module_spec)) ++ except dnf.exceptions.Error: ++ no_matched_spec.append(module_spec) ++ if no_matched_spec: ++ msg = P_("Unable to find a match for argument: '{}'", "Unable to find a match for arguments: '{}'", ++ len(no_matched_spec)).format("' '".join(no_matched_spec)) ++ raise dnf.exceptions.Error(msg) ++ ++ if self.opts.resolve: ++ pkgs = pkgs.union(self._get_providers_of_requires(pkgs)) ++ ++ # download rpms ++ self._do_downloads(pkgs) ++ ++ # Create a repository at destdir with modular data ++ remove_tmp_moduleyamls_files = [] ++ for repo in self.base.repos.iter_enabled(): ++ module_md_path = repo.get_metadata_path('modules') ++ if module_md_path: ++ filename = "".join([repo.id, "-", os.path.basename(module_md_path)]) ++ dest_path = os.path.join(self.base.conf.destdir, filename) ++ shutil.copy(module_md_path, dest_path) ++ remove_tmp_moduleyamls_files.append(dest_path) ++ args = ["createrepo_c", "--update", "--unique-md-filenames", self.base.conf.destdir] ++ p = subprocess.run(args) ++ if p.returncode: ++ msg = _("Creation of repository failed with return code {}. All downloaded content was kept on the system") ++ msg = msg.format(p.returncode) ++ raise dnf.exceptions.Error(msg) ++ for file_path in remove_tmp_moduleyamls_files: ++ os.remove(file_path) ++ ++ def _do_downloads(self, pkgs): ++ """ ++ Perform the download for a list of packages ++ """ ++ pkg_dict = {} ++ for pkg in pkgs: ++ pkg_dict.setdefault(str(pkg), []).append(pkg) ++ ++ to_download = [] ++ ++ for pkg_list in pkg_dict.values(): ++ pkg_list.sort(key=lambda x: (x.repo.priority, x.repo.cost)) ++ to_download.append(pkg_list[0]) ++ if to_download: ++ self.base.download_packages(to_download, self.base.output.progress) ++ ++ def _get_packages_from_modules(self, module_spec): ++ """Gets packages from modules matching module spec ++ 1. From module artifacts ++ 2. From module profiles""" ++ result_query = self.base.sack.query().filterm(empty=True) ++ module_base = dnf.module.module_base.ModuleBase(self.base) ++ module_list, nsvcap = module_base.get_modules(module_spec) ++ if self.opts.newest_only: ++ module_list = self.base._moduleContainer.getLatestModules(module_list, False) ++ for module in module_list: ++ for artifact in module.getArtifacts(): ++ query = self.base.sack.query(flags=hawkey.IGNORE_EXCLUDES).filterm(nevra_strict=artifact) ++ if query: ++ result_query = result_query.union(query) ++ else: ++ msg = _("No match for artifact '{0}' from module '{1}'").format( ++ artifact, module.getFullIdentifier()) ++ logger.warning(msg) ++ if nsvcap.profile: ++ profiles_set = module.getProfiles(nsvcap.profile) ++ else: ++ profiles_set = module.getProfiles() ++ if profiles_set: ++ for profile in profiles_set: ++ for pkg_name in profile.getContent(): ++ query = self.base.sack.query(flags=hawkey.IGNORE_EXCLUDES).filterm(name=pkg_name) ++ # Prefer to add modular providers selected by argument ++ if result_query.intersection(query): ++ continue ++ # Add all packages with the same name as profile described ++ elif query: ++ result_query = result_query.union(query) ++ else: ++ msg = _("No match for package name '{0}' in profile {1} from module {2}")\ ++ .format(pkg_name, profile.getName(), module.getFullIdentifier()) ++ logger.warning(msg) ++ if not module_list: ++ msg = _("No mach for argument '{}'").format(module_spec) ++ raise dnf.exceptions.Error(msg) ++ ++ return result_query ++ ++ def _get_providers_of_requires(self, to_test, done=None, req_dict=None): ++ done = done if done else to_test ++ # req_dict = {} {req : set(pkgs)} ++ if req_dict is None: ++ req_dict = {} ++ test_requires = [] ++ for pkg in to_test: ++ for require in pkg.requires: ++ if require not in req_dict: ++ test_requires.append(require) ++ req_dict.setdefault(require, set()).add(pkg) ++ ++ if self.opts.newest_only: ++ # Prepare cache with all packages related affected by modular filtering ++ names = set() ++ for module in self.base._moduleContainer.getModulePackages(): ++ for artifact in module.getArtifacts(): ++ name, __, __ = artifact.rsplit("-", 2) ++ names.add(name) ++ modular_related = self.base.sack.query(flags=hawkey.IGNORE_EXCLUDES).filterm(provides=names) ++ ++ requires = self.base.sack.query().filterm(empty=True) ++ for require in test_requires: ++ q = self.base.sack.query(flags=hawkey.IGNORE_EXCLUDES).filterm(provides=require) ++ ++ if not q: ++ # TODO(jmracek) Shell we end with an error or with RC 1? ++ logger.warning((_("Unable to satisfy require {}").format(require))) ++ else: ++ if self.opts.newest_only: ++ if not modular_related.intersection(q): ++ q.filterm(latest_per_arch_by_priority=1) ++ requires = requires.union(q.difference(done)) ++ done = done.union(requires) ++ if requires: ++ done = self._get_providers_of_requires(requires, done=done, req_dict=req_dict) ++ ++ return done +-- +libgit2 1.1.0 + diff --git a/SPECS/dnf-plugins-core.spec b/SPECS/dnf-plugins-core.spec index 4fda121..78f18bc 100644 --- a/SPECS/dnf-plugins-core.spec +++ b/SPECS/dnf-plugins-core.spec @@ -1,6 +1,6 @@ -%{?!dnf_lowest_compatible: %global dnf_lowest_compatible 4.2.22} +%{?!dnf_lowest_compatible: %global dnf_lowest_compatible 4.7.0-6} %global dnf_plugins_extra 2.0.0 -%global hawkey_version 0.46.1 +%global hawkey_version 0.63.0-6 %global yum_utils_subpackage_name dnf-utils %if 0%{?rhel} > 7 %global yum_utils_subpackage_name yum-utils @@ -34,7 +34,7 @@ Name: dnf-plugins-core Version: 4.0.21 -Release: 2%{?dist} +Release: 10%{?dist} Summary: Core Plugins for DNF License: GPLv2+ URL: https://github.com/rpm-software-management/dnf-plugins-core @@ -42,6 +42,14 @@ Source0: %{url}/archive/%{version}/%{name}-%{version}.tar.gz Patch1: 0001-versionlock-Do-not-exclude-locked-obsoleters-RhBug1957280.patch Patch2: 0002-repomanage-Allow-running-only-with-metadata.patch Patch3: 0003-repomanage-Enhance-repomanage-documentation-RhBug1898293.patch +Patch4: 0004-copr-dont-traceback-on-empty-lines-in-etcos-release.patch +Patch5: 0005-reposync-Use-fail_fastFalse-when-downloading-packages-RhBug2009894.patch +Patch6: 0006-copr-migrate-all-calls-to-APIv3.patch +Patch7: 0007-groups-manager-More-benevolent-resolving-of-packages-RhBug2013633.patch +Patch8: 0008-versionlock-fix-multi-pkg-lock-RhBug2013324.patch +Patch9: 0009-Update-documentation-for-adding-specific-version-RhBug2013332.patch +Patch10: 0010-needs-restarting-Fix-wrong-boot-time-RhBug1960437.patch +Patch11: 0011-Add-new-command-modulesync-RhBug1868047.patch BuildArch: noarch BuildRequires: cmake @@ -404,6 +412,19 @@ versions of those packages. This allows you to e.g. protect packages from being updated by newer versions. %endif +%if %{with python3} +%package -n python3-dnf-plugin-modulesync +Summary: Download module metadata and packages and create repository +Requires: python3-%{name} = %{version}-%{release} +Requires: createrepo_c >= 0.17.4 +Provides: dnf-plugin-modulesync = %{version}-%{release} +Provides: dnf-command(modulesync) + +%description -n python3-dnf-plugin-modulesync +Download module metadata from all enabled repositories, module artifacts and profiles of matching modules and create +repository. +%endif + %prep %autosetup -p1 %if %{with python2} @@ -764,7 +785,38 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/ %endif %endif +%if %{with python3} +%files -n python3-dnf-plugin-modulesync +%{python3_sitelib}/dnf-plugins/modulesync.* +%{python3_sitelib}/dnf-plugins/__pycache__/modulesync.* +%{_mandir}/man8/dnf-modulesync.* +%endif + %changelog +* Fri Jan 14 2022 Pavla Kratochvilova - 4.0.21-10 +- Rebuild with new release number + +* Tue Jan 11 2022 Pavla Kratochvilova - 4.0.21-9 +- Add new command modulesync (RhBug:1868047) + +* Thu Jan 06 2022 Pavla Kratochvilova - 4.0.21-8 +- [needs-restarting] Fix wrong boot time (RhBug:1960437,2022389) + +* Wed Dec 1 2021 Pavla Kratochvilova - 4.0.21-7 +- [groups-manager] Use full NEVRA for matching packages instead of only name (RhBug:2013633) +- [versionlock] Fix: Multiple package-name-spec arguments don't lock (RhBug:2013324) +- [versionlock] Update documentation for adding specifi version (RhBug:2013332) + +* Tue Nov 23 2021 Pavla Kratochvilova - 4.0.21-6 +- Increase dependency on dnf as it's required by reposync (RhBug:2023739) + +* Fri Nov 12 2021 Pavla Kratochvilova - 4.0.21-5 +- [copr] Migrate all calls to APIv3 (RhBug:2021821) + +* Tue Nov 09 2021 Pavla Kratochvilova - 4.0.21-4 +- [reposync] Don't stop downloading packages on the first error (RhBug:2009894) +- [copr] Fix traceback of copr search (RhBug:2019868) + * Tue Jul 27 2021 Pavla Kratochvilova - 4.0.21-2 - [versionlock] Locking obsoleted package does not make the obsoleter unavailable (RhBug:1957280) - [repomanage] Allow running with metadata only