Skip to content

engine: resources: packagekit: Use signal info for installed state#950

Open
karpfediem wants to merge 1 commit into
purpleidea:masterfrom
karpfediem:feature/packagekit-installed-state
Open

engine: resources: packagekit: Use signal info for installed state#950
karpfediem wants to merge 1 commit into
purpleidea:masterfrom
karpfediem:feature/packagekit-installed-state

Conversation

@karpfediem

Copy link
Copy Markdown
Contributor

Fix installed-state detection for the PackageKit package resource.

PackageKit resolve results expose the package state (installed/absent, or other classification) in the Package signal info argument:

Package(info, package_id, summary)

The package_id is only the package identifier:

name;version;arch;data

mgmt was using the package ID data field to decide whether a package was installed. That is backend-specific and breaks with the apt backend (Debian, ...).

Observed installed package data:

Debian 13 / apt:
Package(1, "qemu-utils;1:10.0.8+ds-0+deb13u1+b2;amd64;auto:debian-stable-main", ...)

Fedora 42 / dnf:
Package(1, "bash;5.2.37-1.fc42;x86_64;installed:anaconda", ...)

In both cases info=1 is PK_INFO_ENUM_INSTALLED.
For an available package that is not installed (e.g. sl) it shows 2:

Package(2, "sl;5.02-1+b1;amd64;debian-stable-main", ...)

See the enum source in PackageKit

typedef enum {
	PK_INFO_ENUM_UNKNOWN, // index 0
	PK_INFO_ENUM_INSTALLED, // index 1
	PK_INFO_ENUM_AVAILABLE, // index 2
// ...

The package ID data field still shows a difference between backends:

apt: auto:debian-stable-main
dnf: installed:anaconda

The apt backend intentionally uses manual: / auto: package ID data for installed packages. So the portable state source is the PackageKit info enum, not the package ID string.

Minimal reproduction

pkg "qemu-utils" {
    state => "installed",
}

On Debian with qemu-utils already installed, upstream mgmt repeatedly applies the package because it classifies the apt package ID as not installed:

engine: pkg[qemu-utils]: Check: pkg[qemu-utils]
engine: pkg[qemu-utils]: Apply: pkg[qemu-utils]
engine: pkg[qemu-utils]: Set(installed): pkg[qemu-utils]...
engine: pkg[qemu-utils]: Set(installed) success: pkg[qemu-utils]
engine: pkg[qemu-utils]: Check: pkg[qemu-utils]
engine: pkg[qemu-utils]: Apply: pkg[qemu-utils]
...
error: max runtime reached

Fix

Preserve the PackageKit signal info value while resolving packages, and use that value for installed-state checks.

ResolvePackages still returns package IDs as before; the retained signal info is only used internally where mgmt needs package state.

There is no fallback to package ID data parsing for installed state.

Tests

The unit test covers the new condition directly:

info=1, package_id=...;auto:debian-stable-main => installed
info=2, package_id=...;debian-stable-main      => not installed

Focused test run:

nix shell nixpkgs#go --command env CGO_ENABLED=0 go test -tags 'noaugeas novirt' ./engine/resources/packagekit

Manual smoke runs with the patched binary:

Debian 13, PackageKit 1.3.1, pkg "qemu-utils": converged.
Fedora 42, PackageKit 1.3.4, pkg "bash": converged.

@karpfediem karpfediem force-pushed the feature/packagekit-installed-state branch from 9288ffd to ce437fa Compare June 30, 2026 16:28
PackageKit resolve results expose package state in the Package signal
info argument:

    Package(info, package_id, summary)

The package_id is only the package identifier:

    name;version;arch;data

mgmt was using the package ID data field to decide whether a package was
installed. That is backend-specific. Debian's apt backend can report an
installed package as:

    Package(1, "qemu-utils;...;amd64;auto:debian-stable-main", ...)

while Fedora's dnf backend can report:

    Package(1, "bash;...;x86_64;installed:anaconda", ...)

In both cases info=1 is PK_INFO_ENUM_INSTALLED. The difference is only
the backend-specific package ID data field.

This caused already-installed Debian packages to be classified as absent,
so mgmt repeatedly applied the package resource instead of converging.

Preserve the PackageKit signal info value while resolving packages and use
it for installed-state checks. ResolvePackages still returns package IDs;
the retained signal info is only used internally where mgmt needs package
state. There is no fallback to package ID data parsing for installed state.

Co-authored-by: Codex GPT 5.5 <noreply@openai.com>
@karpfediem karpfediem force-pushed the feature/packagekit-installed-state branch from ce437fa to 60802bd Compare June 30, 2026 17:33
@purpleidea

Copy link
Copy Markdown
Owner

PS: Without this patch ... how did this ever work on Debian??? Because I know it did.

@karpfediem

Copy link
Copy Markdown
Contributor Author

PS: Without this patch ... how did this ever work on Debian??? Because I know it did.

Did some digging with help of AI:

PackageKit's aptcc backend used to emit installed packages as installed:<repo> since 2014:
PackageKit/PackageKit@62ae5b7

Then in 2022 it changed apt package IDs to encode apt install mode instead:
manual: or auto:
PackageKit/PackageKit@cfd297a
(with the first release containing that commit being 1.2.5)

Debian PackageKit (currently) behavior
bullseye 1.2.2 installed:
bookworm 1.2.6 manual: / auto:
trixie 1.3.1 manual: / auto:

So Debian did work, at least with older PackageKit.

I also believe it worked even on bookworm before.
But that could be explained by the fact that this change was (I would say hidden) inside a patch release.

In any case, the fix now is to just use the Package(info, package_id, summary) signal as it is intended in the current PackageKit.

@purpleidea

Copy link
Copy Markdown
Owner

Nice! Just needs the fixups I mentioned in my review before merge.

return obj.Info == packageInfoInstalledValue
}

func (obj *Conn) resolvePackagesWithInfo(ctx context.Context, packages []string, filter uint64) ([]resolvedPackage, error) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should return ([]*resolvedPackage, error)

}, true
}

func (obj resolvedPackage) installed() bool {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ptr for the method receiver please

return packageIDs, nil
}

func newResolvedPackage(body []interface{}) (*resolvedPackage, bool) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a short docstring here and below.

Let's also move this to the bottom right after func (obj *Conn) PackagesToPackageIDs

The rough convention I have for ordering is main struct with all it's methods, secondary structs with their methods, pub helper funcs, then private helper funcs.

I might have gotten this wrong or made exceptions in some places, it's not a hard rule, but just some attempt at organization.

if len(body) < 2 {
return nil, false
}
info, _ := body[0].(uint32)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please pull out the other values from the comment below and the doc comment, eg:

// format is: name;version;arch;data

and

//pkg_int, ok := signal.Body[0].(int)

If you don't want to use them, at least write the code but leave commented.

We need to check "ok" in all cases too!

Newest bool
}

type resolvedPackage struct {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring plz here and for the fields.

if err != nil {
return nil, err
}
packageIDs := make([]string, 0, len(resolved))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!


// PackageKit's Package signal uses sequential PkInfoEnum values, not the
// bitfield values used by the PkInfoEnum constants above.
const packageInfoInstalledValue uint32 = 1

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there other values here? Add them if they exist. Move up right above type PkError struct { just under the typedef consts. (But with the other consts.)

@purpleidea

Copy link
Copy Markdown
Owner

PS: I am stupid and forgot to click "submit review" I thought it happened with the comment. In any case, sorry about that. Patch LGTM, just needs a few fixups mentioned above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants