fix(orphans): handle alternative providers to eliminate false positives #13031

Open
jnsamyak wants to merge 1 commit from jnsamyak/tooling:fix#11460 into main
Owner

The problem was that the dependency checker didn't account for alternative providers and rich dependencies.

This fix adds two helper methods to properly handle these cases:

  1. is_requirement_satisfied_by_others() - checks if a requirement has
    alternative providers that aren't being removed. This prevents
    flagging dependencies when other valid providers exist.

  2. has_all_requirements_satisfied() - verifies that a package's
    complete set of requirements can be satisfied without the orphaned
    packages. This properly handles rich dependencies with OR clauses.

Open to discussing this approach.

Fixes: releng/tickets#11460

Signed-off-by: Samyak Jain samyak.jn11@gmail.com

The problem was that the dependency checker didn't account for alternative providers and rich dependencies. This fix adds two helper methods to properly handle these cases: 1. is_requirement_satisfied_by_others() - checks if a requirement has alternative providers that aren't being removed. This prevents flagging dependencies when other valid providers exist. 2. has_all_requirements_satisfied() - verifies that a package's complete set of requirements can be satisfied without the orphaned packages. This properly handles rich dependencies with OR clauses. Open to discussing this approach. Fixes: https://forge.fedoraproject.org/releng/tickets/issues/11460 Signed-off-by: Samyak Jain <samyak.jn11@gmail.com>
fix(orphans): handle alternative providers to eliminate false positives
All checks were successful
Orphaned CI / Run Tox Checks for Orphaned tools (pull_request) Successful in 1m18s
322f5c2164
Signed-off-by: Samyak Jain <samyak.jn11@gmail.com>
Member

Could my rmdepcheck tool be useful here?

You can use it like this to determine the consequences of removing a package from an arbitrary (set of) repositor(y/ies):

[adamw@toolbx fedora-toolbox-43 rmdepcheck (excludes-sensible %)]$ ./rmdepcheck.py --removes https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ gedit
Dependencies of other packages that would be BROKEN by the specified source package removals:
package: gedit-plugin-bookmarks-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/
  libgedit-49.so()(64bit)
package: gedit-plugin-drawspaces-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/
  libgedit-49.so()(64bit)
package: gedit-plugin-smartspaces-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/
  libgedit-49.so()(64bit)
package: gedit-plugin-wordcompletion-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/
  libgedit-49.so()(64bit)
package: gedit-plugins-data-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/
  gedit

it does this by diffing repoclosure output, which means it uses the "real" dnf/libsolv dependency resolution path, meaning its results should be consistent and correct and the tool doesn't have to laboriously re-invent dependency resolution logic.

Could my [rmdepcheck](https://forge.fedoraproject.org/quality/rmdepcheck) tool be useful here? You can use it like this to determine the consequences of removing a package from an arbitrary (set of) repositor(y/ies): ``` [adamw@toolbx fedora-toolbox-43 rmdepcheck (excludes-sensible %)]$ ./rmdepcheck.py --removes https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ gedit Dependencies of other packages that would be BROKEN by the specified source package removals: package: gedit-plugin-bookmarks-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ libgedit-49.so()(64bit) package: gedit-plugin-drawspaces-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ libgedit-49.so()(64bit) package: gedit-plugin-smartspaces-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ libgedit-49.so()(64bit) package: gedit-plugin-wordcompletion-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ libgedit-49.so()(64bit) package: gedit-plugins-data-49.0-2.fc44.x86_64 from https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ gedit ``` it does this by diffing repoclosure output, which means it uses the "real" dnf/libsolv dependency resolution path, meaning its results should be consistent and correct and the tool doesn't have to laboriously re-invent dependency resolution logic.
Member

@adamwill, yeah, I actually did want to talk to you about replacing this script's dep solving code with rmdepcheck. Currently, I don't think it'd work here because this script needs a structured mapping of which other packages would be broken by which orphaned packages so it can get a list of affected package maintainers. rmdepcheck just prints a textual summary of the resolution failures AFAICT.

@adamwill, yeah, I actually did want to talk to you about replacing this script's dep solving code with rmdepcheck. Currently, I don't think it'd work here because this script needs a structured mapping of which other packages would be broken by which orphaned packages so it can get a list of affected package maintainers. rmdepcheck just prints a textual summary of the resolution failures AFAICT.
gotmax23 left a comment

Thanks for looking into to this! I was aware of the issue but never got around to looking into it because I was worried about performance impacts of additional queries and wanted to prioritize a complete rewrite to simplify the code or at least a libdnf5 port of the existing code. But hopefully, we can find a workable solution in the meantime.

I am concerned that this PR is not the correct solution and will create false negatives, though. I left comments explaining the issue in more detail.

In any case, it would be helpful if you ran the old and new versions of the script and compared execution time to see what kind of performance impact the changes would have.

Thanks for looking into to this! I was aware of the issue but never got around to looking into it because I was worried about performance impacts of additional queries and wanted to prioritize a complete rewrite to simplify the code or at least a libdnf5 port of the existing code. But hopefully, we can find a workable solution in the meantime. I am concerned that this PR is not the correct solution and will create false negatives, though. I left comments explaining the issue in more detail. In any case, it would be helpful if you ran the old and new versions of the script and compared execution time to see what kind of performance impact the changes would have.
@ -401,0 +423,4 @@
"""
# Check each requirement of the package
for req in package.requires:
req_str = str(req).split()[0] # Get base requirement name
Member

This logic does not seem quite right to me, and I believe it'll create false negatives. You can't just discard the version constraints and use the "base requirement name" and then assume that another package could satisfy the requirement. Let's say python3-foo and python3-foo2 both Provide python3dist(foo), but the former is python3dist(foo) = 3 and the latter is python3dist(foo) = 2. Let's say that python3-foo2 is an orphan. Let's also say that package bar Requires python3dist(foo) < 3. That means that only python3-foo2 (the provider of python3dist(foo) = 2) would be able to satisfy bar's dependency. But this code just sees that a package other than the orphaned python3-foo2 can satisfy the base requirement name and removes bar from the affected packages list even though the version constraints in the full requirement string makes it so the other package cannot satisfy it.

This logic does not seem quite right to me, and I believe it'll create false negatives. You can't just discard the version constraints and use the "base requirement name" and then assume that another package could satisfy the requirement. Let's say `python3-foo` and `python3-foo2` both Provide `python3dist(foo)`, but the former is `python3dist(foo) = 3` and the latter is `python3dist(foo) = 2`. Let's say that `python3-foo2` is an orphan. Let's also say that package `bar` Requires `python3dist(foo) < 3`. That means that only `python3-foo2` (the provider of `python3dist(foo) = 2`) would be able to satisfy `bar`'s dependency. But this code just sees that a package other than the orphaned `python3-foo2` can satisfy the base requirement name and removes `bar` from the affected packages list even though the version constraints in the full requirement string makes it so the other package can**not** satisfy it.
Member

Also, the splitting on spaces like this will break rich dependencies with parentheses and create an invalid requirement string.

Also, the splitting on spaces like this will break rich dependencies with parentheses and create an invalid requirement string.
Member

If this is changed to operate on the full requirement string and the is_requirement_satisfied_by_others() check is completely removed, I think this would actually solve the issue.

If this is changed to operate on the full requirement string and the `is_requirement_satisfied_by_others()` check is completely removed, I think this would actually solve the issue.
@ -468,0 +493,4 @@
# Check if this provide is satisfied by other packages
# (not the orphaned package or other packages being removed)
if self.is_requirement_satisfied_by_others(base_provide, rpms):
# This requirement has alternative providers, skip it
Member

Even if the base_provide does have alternative providers, we can't skip it here yet because different packages can have version constraints that disqualify the other providers. See the example case in my comment on def has_all_requirements_satisfied().

Even if the `base_provide` does have alternative providers, we can't skip it here yet because different packages can have version constraints that disqualify the other providers. See the example case in my comment on `def has_all_requirements_satisfied()`.
Member

@gotmax23 wrote in #13031 (comment):

@adamwill, yeah, I actually did want to talk to you about replacing this script's dep solving code with rmdepcheck. Currently, I don't think it'd work here because this script needs a structured mapping of which other packages would be broken by which orphaned packages so it can get a list of affected package maintainers. rmdepcheck just prints a textual summary of the resolution failures AFAICT.

rmdepcheck has a JSON output mode:

[adamw@toolbx fedora-toolbox-43 rmdepcheck (excludes-sensible %)]$ ./rmdepcheck.py --json --removes https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ gedit
{
    "newerrors": [
        [
            "gedit-plugin-bookmarks-49.0-2.fc44.x86_64",
            "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/",
            "libgedit-49.so()(64bit)"
        ],
        [
            "gedit-plugin-drawspaces-49.0-2.fc44.x86_64",
            "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/",
            "libgedit-49.so()(64bit)"
        ],
        [
            "gedit-plugin-smartspaces-49.0-2.fc44.x86_64",
            "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/",
            "libgedit-49.so()(64bit)"
        ],
        [
            "gedit-plugin-wordcompletion-49.0-2.fc44.x86_64",
            "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/",
            "libgedit-49.so()(64bit)"
        ],
        [
            "gedit-plugins-data-49.0-2.fc44.x86_64",
            "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/",
            "gedit"
        ]
    ]
}

The first element of each output dep tuple is a package NVR. Currently this is parsed out of dnf repoclosure text output, but I have a ticket to switch to using dnf repoclosure JSON output once it's more widely available. I need to document the output format, but you'd always want the newerrors list for this purpose.

It doesn't currently infer source packages from binary for you, but it could do that, and I've filed myself a ticket to add it as a convenience feature.

If you run rmdepcheck --removes and specify multiple removes it can't really tell you which of them causes a given dep issue without a bunch of extra fancy stuff I don't really want to put in. But it runs very fast (~30 seconds per run, usually) so you can probably just run it on one package at a time. I could actually implement a sort of 'serial mode' where it would run on one package at a time internally so it could give you info per removal, I guess; that would be even faster as it wouldn't have to redo the metadata downloads which it does on each run.

another option would be to just borrow the "repo modification logic" out of rmdepcheck and use it here, I guess; it's very simple these days. I do vaguely have the idea that I could make it exportable from rmdepcheck for other things to use also, I'd have to look at how that should work.

@gotmax23 wrote in https://forge.fedoraproject.org/releng/tooling/pulls/13031#issuecomment-675619: > @adamwill, yeah, I actually did want to talk to you about replacing this script's dep solving code with rmdepcheck. Currently, I don't think it'd work here because this script needs a structured mapping of which other packages would be broken by which orphaned packages so it can get a list of affected package maintainers. rmdepcheck just prints a textual summary of the resolution failures AFAICT. rmdepcheck has a JSON output mode: ``` [adamw@toolbx fedora-toolbox-43 rmdepcheck (excludes-sensible %)]$ ./rmdepcheck.py --json --removes https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/ gedit { "newerrors": [ [ "gedit-plugin-bookmarks-49.0-2.fc44.x86_64", "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/", "libgedit-49.so()(64bit)" ], [ "gedit-plugin-drawspaces-49.0-2.fc44.x86_64", "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/", "libgedit-49.so()(64bit)" ], [ "gedit-plugin-smartspaces-49.0-2.fc44.x86_64", "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/", "libgedit-49.so()(64bit)" ], [ "gedit-plugin-wordcompletion-49.0-2.fc44.x86_64", "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/", "libgedit-49.so()(64bit)" ], [ "gedit-plugins-data-49.0-2.fc44.x86_64", "https://kojipkgs.fedoraproject.org/repos/f45-build/latest/x86_64/", "gedit" ] ] } ``` The first element of each output dep tuple is a package NVR. Currently this is parsed out of `dnf repoclosure` text output, but I have a [ticket](https://forge.fedoraproject.org/quality/rmdepcheck/issues/28) to switch to using `dnf repoclosure` JSON output once it's more widely available. I need to document the output format, but you'd always want the `newerrors` list for this purpose. It doesn't currently infer source packages from binary for you, but it *could* do that, and I've filed myself [a ticket](https://forge.fedoraproject.org/quality/rmdepcheck/issues/31) to add it as a convenience feature. If you run `rmdepcheck --removes` and specify *multiple* removes it can't really tell you which of them causes a given dep issue without a bunch of extra fancy stuff I don't really want to put in. But it runs very fast (~30 seconds per run, usually) so you can probably just run it on one package at a time. I *could* actually implement a sort of 'serial mode' where it would run on one package at a time internally so it could give you info per removal, I guess; that would be even faster as it wouldn't have to redo the metadata downloads which it does on each run. another option would be to just borrow the "repo modification logic" out of rmdepcheck and use it here, I guess; it's very simple these days. I do vaguely have the idea that I could make it exportable from rmdepcheck for other things to use also, I'd have to look at how that should work.
All checks were successful
Orphaned CI / Run Tox Checks for Orphaned tools (pull_request) Successful in 1m18s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u fix#11460:jnsamyak-fix#11460
git switch jnsamyak-fix#11460

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff jnsamyak-fix#11460
git switch jnsamyak-fix#11460
git rebase main
git switch main
git merge --ff-only jnsamyak-fix#11460
git switch jnsamyak-fix#11460
git rebase main
git switch main
git merge --no-ff jnsamyak-fix#11460
git switch main
git merge --squash jnsamyak-fix#11460
git switch main
git merge --ff-only jnsamyak-fix#11460
git switch main
git merge jnsamyak-fix#11460
git push origin main
Sign in to join this conversation.
No reviewers
releng/Owners
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
releng/tooling!13031
No description provided.