// Mgmt // Copyright (C) 2013-2020+ James Shubin and the project contributors // Written by James Shubin and the project contributors // // This program 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 3 of the License, or // (at your option) any later version. // // This program 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 this program. If not, see . // Package packagekit provides an interface to interact with packagekit. // See: https://www.freedesktop.org/software/PackageKit/gtk-doc/index.html for // more information. package packagekit import ( "fmt" "runtime" "strings" engineUtil "github.com/purpleidea/mgmt/engine/util" "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" "github.com/godbus/dbus" ) // global tweaks of verbosity and code path const ( Paranoid = false // enable if you see any ghosts ) // constants which might need to be tweaked or which contain special dbus // strings. const ( // FIXME: if PkBufferSize is too low, install seems to drop signals PkBufferSize = 1000 // TODO: the PkSignalTimeout value might be too low PkSignalPackageTimeout = 60 // 60 seconds, arbitrary PkSignalDestroyTimeout = 15 // 15 seconds, arbitrary PkPath = "/org/freedesktop/PackageKit" PkIface = "org.freedesktop.PackageKit" PkIfaceTransaction = PkIface + ".Transaction" ) var ( // PkArchMap contains the mapping from PackageKit arch to GOARCH. // GOARCH's: 386, amd64, arm, arm64, mips64, mips64le, ppc64, ppc64le PkArchMap = map[string]string{ // map of PackageKit arch to GOARCH // TODO: add more values // noarch "noarch": "ANY", // special value "ANY" (noarch as seen in Fedora) "any": "ANY", // special value "ANY" ('any' as seen in ArchLinux) "all": "ANY", // special value "ANY" ('all' as seen in Debian) // fedora "x86_64": "amd64", "aarch64": "arm64", // debian, from: https://www.debian.org/ports/ "amd64": "amd64", "arm64": "arm64", "i386": "386", "i486": "386", "i586": "386", "i686": "386", } ) // type enum_filter uint64 // https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c const ( //static const PkEnumMatch enum_filter[] PkFilterEnumUnknown uint64 = 1 << iota // "unknown" PkFilterEnumNone // "none" PkFilterEnumInstalled // "installed" PkFilterEnumNotInstalled // "~installed" PkFilterEnumDevelopment // "devel" PkFilterEnumNotDevelopment // "~devel" PkFilterEnumGui // "gui" PkFilterEnumNotGui // "~gui" PkFilterEnumFree // "free" PkFilterEnumNotFree // "~free" PkFilterEnumVisible // "visible" PkFilterEnumNotVisible // "~visible" PkFilterEnumSupported // "supported" PkFilterEnumNotSupported // "~supported" PkFilterEnumBasename // "basename" PkFilterEnumNotBasename // "~basename" PkFilterEnumNewest // "newest" PkFilterEnumNotNewest // "~newest" PkFilterEnumArch // "arch" PkFilterEnumNotArch // "~arch" PkFilterEnumSource // "source" PkFilterEnumNotSource // "~source" PkFilterEnumCollections // "collections" PkFilterEnumNotCollections // "~collections" PkFilterEnumApplication // "application" PkFilterEnumNotApplication // "~application" PkFilterEnumDownloaded // "downloaded" PkFilterEnumNotDownloaded // "~downloaded" ) // constants from packagekit c library. const ( //static const PkEnumMatch enum_transaction_flag[] PkTransactionFlagEnumNone uint64 = 1 << iota // "none" PkTransactionFlagEnumOnlyTrusted // "only-trusted" PkTransactionFlagEnumSimulate // "simulate" PkTransactionFlagEnumOnlyDownload // "only-download" PkTransactionFlagEnumAllowReinstall // "allow-reinstall" PkTransactionFlagEnumJustReinstall // "just-reinstall" PkTransactionFlagEnumAllowDowngrade // "allow-downgrade" ) // constants from packagekit c library. const ( //typedef enum PkInfoEnumUnknown uint64 = 1 << iota PkInfoEnumInstalled PkInfoEnumAvailable PkInfoEnumLow PkInfoEnumEnhancement PkInfoEnumNormal PkInfoEnumBugfix PkInfoEnumImportant PkInfoEnumSecurity PkInfoEnumBlocked PkInfoEnumDownloading PkInfoEnumUpdating PkInfoEnumInstalling PkInfoEnumRemoving PkInfoEnumCleanup PkInfoEnumObsoleting PkInfoEnumCollectionInstalled PkInfoEnumCollectionAvailable PkInfoEnumFinished PkInfoEnumReinstalling PkInfoEnumDowngrading PkInfoEnumPreparing PkInfoEnumDecompressing PkInfoEnumUntrusted PkInfoEnumTrusted PkInfoEnumUnavailable PkInfoEnumLast ) // Conn is a wrapper struct so we can pass bus connection around in the struct. type Conn struct { conn *dbus.Conn Debug bool Logf func(format string, v ...interface{}) } // PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in // the map values. type PkPackageIDActionData struct { Found bool Installed bool Version string PackageID string Newest bool } // NewBus returns a new bus connection. func NewBus() *Conn { // if we share the bus with others, we will get each others messages!! bus, err := util.SystemBusPrivateUsable() // don't share the bus connection! if err != nil { return nil } return &Conn{ conn: bus, } } // GetBus gets the dbus connection object. func (obj *Conn) GetBus() *dbus.Conn { return obj.conn } // Close closes the dbus connection object. func (obj *Conn) Close() error { return obj.conn.Close() } // matchSignal is an internal helper to add signal matches to the bus. It should // only be called once. func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) { if obj.Debug { obj.Logf("matchSignal(%v, %v, %s, %v)", ch, path, iface, signals) } // eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep bus := obj.GetBus().BusObject() var argsList []string // cleanup function should be called when done or when AddMatch errors removeSignals := func() error { var errList error for i := len(argsList) - 1; i >= 0; i-- { // last in first out call := bus.Call(engineUtil.DBusRemoveMatch, 0, argsList[i]) errList = errwrap.Append(errList, call.Err) } return errList } // TODO: if we make this call many times, we seem to receive signals // that many times... Maybe this should be an object singleton? var call *dbus.Call pathStr := fmt.Sprintf("%s", path) if len(signals) == 0 { args := fmt.Sprintf("type='signal', path='%s', interface='%s'", pathStr, iface) argsList = append(argsList, args) call = bus.Call(engineUtil.DBusAddMatch, 0, args) } else { for _, signal := range signals { args := fmt.Sprintf("type='signal', path='%s', interface='%s', member='%s'", pathStr, iface, signal) argsList = append(argsList, args) if call = bus.Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil { break // fail if any one fails } } } if call.Err != nil { defer removeSignals() // ignore the error return nil, call.Err } // The caller has to make sure that ch is sufficiently buffered; if a // message arrives when a write to c is not possible, it is discarded! // This can be disastrous if we're waiting for a "Finished" signal! obj.GetBus().Signal(ch) return removeSignals, nil } // WatchChanges gets a signal anytime an event happens. func (obj *Conn) WatchChanges() (chan *dbus.Signal, error) { ch := make(chan *dbus.Signal, PkBufferSize) // NOTE: the TransactionListChanged signal fires much more frequently, // but with much less specificity. If we're missing events, report the // issue upstream! The UpdatesChanged signal is what hughsie suggested var signal = "UpdatesChanged" removeSignals, err := obj.matchSignal(ch, PkPath, PkIface, []string{signal}) if err != nil { return nil, err } defer removeSignals() // ignore the error if Paranoid { // TODO: this filtering might not be necessary anymore... // try to handle the filtering inside this function! rch := make(chan *dbus.Signal) go func() { loop: for { select { case event := <-ch: // "A receive from a closed channel returns the // zero value immediately": if i get nil here, // it means the channel was closed by someone!! if event == nil { // shared bus issue? obj.Logf("Hrm, channel was closed!") break loop // TODO: continue? } // i think this was caused by using the shared // bus, but we might as well leave it in for now if event.Path != PkPath || event.Name != fmt.Sprintf("%s.%s", PkIface, signal) { obj.Logf("Woops: Event: %+v", event) continue } rch <- event // forward... } } defer close(ch) }() return rch, nil } return ch, nil } // CreateTransaction creates and returns a transaction path. func (obj *Conn) CreateTransaction() (dbus.ObjectPath, error) { if obj.Debug { obj.Logf("CreateTransaction()") } var interfacePath dbus.ObjectPath bus := obj.GetBus().Object(PkIface, PkPath) call := bus.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath) if call != nil { return "", call } if obj.Debug { obj.Logf("CreateTransaction(): %v", interfacePath) } return interfacePath, nil } // ResolvePackages runs the PackageKit Resolve method and returns the result. func (obj *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) { packageIDs := []string{} ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( interfacePath, err := obj.CreateTransaction() // emits Destroy on close if err != nil { return []string{}, err } // add signal matches for Package and Finished which will always be last var signals = []string{"Package", "Finished", "Error", "Destroy"} removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) if err != nil { return nil, err } defer removeSignals() if obj.Debug { obj.Logf("ResolvePackages(): Object(%s, %v)", PkIface, interfacePath) } bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path call := bus.Call(FmtTransactionMethod("Resolve"), 0, filter, packages) if obj.Debug { obj.Logf("ResolvePackages(): Call: Success!") } if call.Err != nil { return []string{}, call.Err } loop: for { // FIXME: add a timeout option to error in case signals are dropped! select { case signal := <-ch: if obj.Debug { obj.Logf("ResolvePackages(): Signal: %+v", signal) } if signal.Path != interfacePath { obj.Logf("Woops: Signal.Path: %+v", signal.Path) continue loop } if signal.Name == FmtTransactionMethod("Package") { //pkg_int, ok := signal.Body[0].(int) packageID, ok := signal.Body[1].(string) // format is: name;version;arch;data if !ok { continue loop } //comment, ok := signal.Body[2].(string) for _, p := range packageIDs { if packageID == p { continue loop // duplicate! } } packageIDs = append(packageIDs, packageID) } else if signal.Name == FmtTransactionMethod("Finished") { // TODO: should we wait for the Destroy signal? break loop } else if signal.Name == FmtTransactionMethod("Destroy") { // should already be broken break loop } else { return []string{}, fmt.Errorf("error in body: %v", signal.Body) } } } return packageIDs, nil } // IsInstalledList queries a list of packages to see if they are installed. func (obj *Conn) IsInstalledList(packages []string) ([]bool, error) { var filter uint64 // initializes at the "zero" value of 0 filter += PkFilterEnumArch // always search in our arch packageIDs, err := obj.ResolvePackages(packages, filter) if err != nil { return nil, errwrap.Wrapf(err, "error resolving packages") } var m = make(map[string]int) for _, packageID := range packageIDs { s := strings.Split(packageID, ";") //if len(s) != 4 { continue } // this would be a bug! pkg := s[0] flags := strings.Split(s[3], ":") for _, f := range flags { if f == "installed" { if _, exists := m[pkg]; !exists { m[pkg] = 0 } m[pkg]++ // if we see pkg installed, increment break } } } var r []bool for _, p := range packages { if value, exists := m[p]; exists { r = append(r, value > 0) // at least 1 means installed } else { r = append(r, false) } } return r, nil } // IsInstalled returns if a package is installed. // TODO: this could be optimized by making the resolve call directly func (obj *Conn) IsInstalled(pkg string) (bool, error) { p, e := obj.IsInstalledList([]string{pkg}) if len(p) != 1 { return false, e } return p[0], nil } // InstallPackages installs a list of packages by packageID. func (obj *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) error { ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( interfacePath, err := obj.CreateTransaction() // emits Destroy on close if err != nil { return err } var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) if err != nil { return err } defer removeSignals() bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path call := bus.Call(FmtTransactionMethod("RefreshCache"), 0, false) if call.Err != nil { return call.Err } call = bus.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs) if call.Err != nil { return call.Err } timeout := -1 // disabled initially finished := false loop: for { select { case signal := <-ch: if signal.Path != interfacePath { obj.Logf("Woops: Signal.Path: %+v", signal.Path) continue loop } if signal.Name == FmtTransactionMethod("ErrorCode") { return fmt.Errorf("error in body: %v", signal.Body) } else if signal.Name == FmtTransactionMethod("Package") { // a package was installed... // only start the timer once we're here... timeout = PkSignalPackageTimeout } else if signal.Name == FmtTransactionMethod("Finished") { finished = true timeout = PkSignalDestroyTimeout // wait a bit } else if signal.Name == FmtTransactionMethod("Destroy") { return nil // success } else { return fmt.Errorf("error in body: %v", signal.Body) } case <-util.TimeAfterOrBlock(timeout): if finished { obj.Logf("Timeout: InstallPackages: Waiting for 'Destroy'") return nil // got tired of waiting for Destroy } return fmt.Errorf("timeout installing packages: %s", strings.Join(packageIDs, ", ")) } } } // RemovePackages removes a list of packages by packageID. func (obj *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error { var allowDeps = true // TODO: configurable var autoremove = false // unsupported on GNU/Linux ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( interfacePath, err := obj.CreateTransaction() // emits Destroy on close if err != nil { return err } var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) if err != nil { return err } defer removeSignals() bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path call := bus.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove) if call.Err != nil { return call.Err } loop: for { // FIXME: add a timeout option to error in case signals are dropped! select { case signal := <-ch: if signal.Path != interfacePath { obj.Logf("Woops: Signal.Path: %+v", signal.Path) continue loop } if signal.Name == FmtTransactionMethod("ErrorCode") { return fmt.Errorf("error in body: %v", signal.Body) } else if signal.Name == FmtTransactionMethod("Package") { // a package was installed... continue loop } else if signal.Name == FmtTransactionMethod("Finished") { // TODO: should we wait for the Destroy signal? break loop } else if signal.Name == FmtTransactionMethod("Destroy") { // should already be broken break loop } else { return fmt.Errorf("error in body: %v", signal.Body) } } } return nil } // UpdatePackages updates a list of packages to versions that are specified. func (obj *Conn) UpdatePackages(packageIDs []string, transactionFlags uint64) error { ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( interfacePath, err := obj.CreateTransaction() if err != nil { return err } var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) if err != nil { return err } defer removeSignals() bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path call := bus.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs) if call.Err != nil { return call.Err } loop: for { // FIXME: add a timeout option to error in case signals are dropped! select { case signal := <-ch: if signal.Path != interfacePath { obj.Logf("Woops: Signal.Path: %+v", signal.Path) continue loop } if signal.Name == FmtTransactionMethod("ErrorCode") { return fmt.Errorf("error in body: %v", signal.Body) } else if signal.Name == FmtTransactionMethod("Package") { } else if signal.Name == FmtTransactionMethod("Finished") { // TODO: should we wait for the Destroy signal? break loop } else if signal.Name == FmtTransactionMethod("Destroy") { // should already be broken break loop } else { return fmt.Errorf("error in body: %v", signal.Body) } } } return nil } // GetFilesByPackageID gets the list of files that are contained inside a list // of packageIDs. func (obj *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) { // NOTE: the maximum number of files in an RPM is 52116 in Fedora 23 // https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( interfacePath, err := obj.CreateTransaction() if err != nil { return } var signals = []string{"Files", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) if err != nil { return } defer removeSignals() bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path call := bus.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs) if call.Err != nil { err = call.Err return } files = make(map[string][]string) loop: for { // FIXME: add a timeout option to error in case signals are dropped! select { case signal := <-ch: if signal.Path != interfacePath { obj.Logf("Woops: Signal.Path: %+v", signal.Path) continue loop } if signal.Name == FmtTransactionMethod("ErrorCode") { err = fmt.Errorf("error in body: %v", signal.Body) return // one signal returned per packageID found... } else if signal.Name == FmtTransactionMethod("Files") { if len(signal.Body) != 2 { // bad data continue loop } var ok bool var key string var fileList []string if key, ok = signal.Body[0].(string); !ok { continue loop } if fileList, ok = signal.Body[1].([]string); !ok { continue loop // failed conversion } files[key] = fileList // build up map } else if signal.Name == FmtTransactionMethod("Finished") { // TODO: should we wait for the Destroy signal? break loop } else if signal.Name == FmtTransactionMethod("Destroy") { // should already be broken break loop } else { err = fmt.Errorf("error in body: %v", signal.Body) return } } } return } // GetUpdates gets a list of packages that are installed and which can be // updated, mod filter. func (obj *Conn) GetUpdates(filter uint64) ([]string, error) { if obj.Debug { obj.Logf("GetUpdates()") } packageIDs := []string{} ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( interfacePath, err := obj.CreateTransaction() if err != nil { return nil, err } var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress" ? removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) if err != nil { return nil, err } defer removeSignals() bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path call := bus.Call(FmtTransactionMethod("GetUpdates"), 0, filter) if call.Err != nil { return nil, call.Err } loop: for { // FIXME: add a timeout option to error in case signals are dropped! select { case signal := <-ch: if signal.Path != interfacePath { obj.Logf("Woops: Signal.Path: %+v", signal.Path) continue loop } if signal.Name == FmtTransactionMethod("ErrorCode") { return nil, fmt.Errorf("error in body: %v", signal.Body) } else if signal.Name == FmtTransactionMethod("Package") { //pkg_int, ok := signal.Body[0].(int) packageID, ok := signal.Body[1].(string) // format is: name;version;arch;data if !ok { continue loop } //comment, ok := signal.Body[2].(string) for _, p := range packageIDs { // optional? if packageID == p { continue loop // duplicate! } } packageIDs = append(packageIDs, packageID) } else if signal.Name == FmtTransactionMethod("Finished") { // TODO: should we wait for the Destroy signal? break loop } else if signal.Name == FmtTransactionMethod("Destroy") { // should already be broken break loop } else { return nil, fmt.Errorf("error in body: %v", signal.Body) } } } return packageIDs, nil } // PackagesToPackageIDs is a helper function that *might* be generally useful // outside mgmt. The packageMap input has the package names as keys and // requested states as values. These states can be: installed, uninstalled, // newest or a requested version str. func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint64) (map[string]*PkPackageIDActionData, error) { count := 0 packages := make([]string, len(packageMap)) for k := range packageMap { // lol, golang has no hash.keys() function! packages[count] = k count++ } if !(filter&PkFilterEnumArch == PkFilterEnumArch) { filter += PkFilterEnumArch // always search in our arch } if obj.Debug { obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", ")) } resolved, err := obj.ResolvePackages(packages, filter) if err != nil { return nil, errwrap.Wrapf(err, "error resolving") } found := make([]bool, count) // default false installed := make([]bool, count) version := make([]string, count) usePackageID := make([]string, count) newest := make([]bool, count) // default true for i := range newest { newest[i] = true // assume, for now } var index int for _, packageID := range resolved { index = -1 //obj.Logf("* %v", packageID) // format is: name;version;arch;data s := strings.Split(packageID, ";") //if len(s) != 4 { continue } // this would be a bug! pkg, ver, arch, data := s[0], s[1], s[2], s[3] // we might need to allow some of this, eg: i386 .deb on amd64 b, err := IsMyArch(arch) if err != nil { return nil, errwrap.Wrapf(err, "arch error") } else if !b { continue } for i := range packages { // find pkg if it exists if pkg == packages[i] { index = i } } if index == -1 { // can't find what we're looking for continue } state := packageMap[pkg] // lookup the requested state/version if state == "" { return nil, fmt.Errorf("empty package state for: `%s`", pkg) } found[index] = true stateIsVersion := (state != "installed" && state != "uninstalled" && state != "newest") // must be a ver. string if stateIsVersion { if state == ver && ver != "" { // we match what we want... usePackageID[index] = packageID } } if FlagInData("installed", data) { installed[index] = true version[index] = ver // state of "uninstalled" matched during CheckApply, and // states of "installed" and "newest" for fileList if !stateIsVersion { usePackageID[index] = packageID // save for later } } else { // not installed... if !stateIsVersion { // if there is more than one result, eg: there // is the old and newest version of a package, // then this section can run more than once... // in that case, don't worry, we'll choose the // right value in the "updates" section below! usePackageID[index] = packageID } } } // we can't determine which packages are "newest", without searching // for each one individually, so instead we check if any updates need // to be done, and if so, anything that needs updating isn't newest! // if something isn't installed, we can't verify it with this method // FIXME: https://github.com/hughsie/PackageKit/issues/116 updates, err := obj.GetUpdates(filter) if err != nil { return nil, errwrap.Wrapf(err, "updates error") } for _, packageID := range updates { //obj.Logf("* %v", packageID) // format is: name;version;arch;data s := strings.Split(packageID, ";") //if len(s) != 4 { continue } // this would be a bug! pkg, _, _, _ := s[0], s[1], s[2], s[3] for index := range packages { // find pkg if it exists if pkg == packages[index] { state := packageMap[pkg] // lookup newest[index] = false if state == "installed" || state == "newest" { // fix up in case above wasn't correct! usePackageID[index] = packageID } break } } } // skip if the "newest" filter was used, otherwise we might need fixing // this check is for packages that need to verify their "newest" status // we need to know this so we can install the correct newest packageID! recursion := make(map[string]*PkPackageIDActionData) if !(filter&PkFilterEnumNewest == PkFilterEnumNewest) { checkPackages := []string{} filteredPackageMap := make(map[string]string) for index, pkg := range packages { state := packageMap[pkg] // lookup the requested state/version if !found[index] || installed[index] { // skip these, they're okay continue } if !(state == "newest" || state == "installed") { continue } checkPackages = append(checkPackages, pkg) filteredPackageMap[pkg] = packageMap[pkg] // check me! } // we _could_ do a second resolve and then parse like this... //resolved, e := obj.ResolvePackages(..., filter+PkFilterEnumNewest) // but that's basically what recursion here could do too! if len(checkPackages) > 0 { if obj.Debug { obj.Logf("PackagesToPackageIDs(): Recurse: %s", strings.Join(checkPackages, ", ")) } recursion, err = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest) if err != nil { return nil, errwrap.Wrapf(err, "recursion error") } } } // fix up and build result format result := make(map[string]*PkPackageIDActionData) for index, pkg := range packages { if !found[index] || !installed[index] { newest[index] = false // make the results more logical! } // prefer recursion results if present if lookup, ok := recursion[pkg]; ok { result[pkg] = lookup } else { result[pkg] = &PkPackageIDActionData{ Found: found[index], Installed: installed[index], Version: version[index], PackageID: usePackageID[index], Newest: newest[index], } } } return result, nil } // FilterPackageIDs returns a list of packageIDs which match the set of package // names in packages. func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) { result := []string{} for _, k := range packages { p, ok := m[k] // lookup single package // package doesn't exist, this is an error! if !ok || !p.Found || p.PackageID == "" { return nil, fmt.Errorf("can't find package named '%s'", k) } result = append(result, p.PackageID) } return result, nil } // FilterState returns a map of whether each package queried matches the // particular state. func FilterState(m map[string]*PkPackageIDActionData, packages []string, state string) (result map[string]bool, err error) { result = make(map[string]bool) pkgs := []string{} // bad pkgs that don't have a bool state for _, k := range packages { p, ok := m[k] // lookup single package // package doesn't exist, this is an error! if !ok || !p.Found { return nil, fmt.Errorf("can't find package named '%s'", k) } var b bool if state == "installed" { b = p.Installed } else if state == "uninstalled" { b = !p.Installed } else if state == "newest" { b = p.Newest } else { // we can't filter "version" state in this function pkgs = append(pkgs, k) continue } result[k] = b // save } if len(pkgs) > 0 { err = fmt.Errorf("can't filter non-boolean state on: %s", strings.Join(pkgs, ",")) } return result, err } // FilterPackageState returns all packages that are in package and match the // specific state. func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) { result = []string{} for _, k := range packages { p, ok := m[k] // lookup single package // package doesn't exist, this is an error! if !ok || !p.Found { return nil, fmt.Errorf("can't find package named '%s'", k) } b := false if state == "installed" && p.Installed { b = true } else if state == "uninstalled" && !p.Installed { b = true } else if state == "newest" && p.Newest { b = true } else if state == p.Version { b = true } if b { result = append(result, k) } } return result, err } // FlagInData asks whether a flag exists inside the data portion of a packageID // field? func FlagInData(flag, data string) bool { flags := strings.Split(data, ":") for _, f := range flags { if f == flag { return true } } return false } // FmtTransactionMethod builds the transaction method string properly. func FmtTransactionMethod(method string) string { return fmt.Sprintf("%s.%s", PkIfaceTransaction, method) } // IsMyArch determines if a PackageKit architecture matches the current os arch. func IsMyArch(arch string) (bool, error) { goarch, ok := PkArchMap[arch] if !ok { // if you get this error, please update the PkArchMap const return false, fmt.Errorf("arch '%s', not found", arch) } if goarch == "ANY" { // special value that corresponds to noarch return true, nil } return goarch == runtime.GOARCH, nil }