From 1298c05a194e3e6bb249b0fe5bf04adde87bd723 Mon Sep 17 00:00:00 2001 From: Alberto Massidda Date: Fri, 23 May 2025 19:15:29 +0200 Subject: [PATCH 1/6] fix GetPackageManager("") causing panic due to no pm autoselected according to the documentation, when GetPackageManager() is invoked with an empty name, "If the name is empty, the first available package manager will be returned.". But that was unimplemented --- syspkg.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/syspkg.go b/syspkg.go index bd47df4..9a73a2f 100644 --- a/syspkg.go +++ b/syspkg.go @@ -20,6 +20,7 @@ package syspkg import ( "errors" "log" + "sort" "github.com/bluet/syspkg/manager" "github.com/bluet/syspkg/manager/apt" @@ -97,8 +98,22 @@ func (s *sysPkgImpl) FindPackageManagers(include IncludeOptions) (map[string]Pac } // GetPackageManager returns a PackageManager instance by its name (e.g., "apt", "snap", "flatpak", etc.). +// if name is empty, return the first available func (s *sysPkgImpl) GetPackageManager(name string) PackageManager { - return s.pms[name] + var pm PackageManager + + if name == "" { + // get first pm available, lexicographically sorted + keys := make([]string, 0, len(s.pms)) + for k := range s.pms { + keys = append(keys, k) + } + sort.Strings(keys) + pm = s.pms[keys[0]] + } else { + pm = s.pms[name] + } + return pm } // RefreshPackageManagers refreshes the internal list of available package managers, and returns the new list. From 22a3705f2e3ded76863efb1021da54fbd02ed422 Mon Sep 17 00:00:00 2001 From: Alberto Massidda Date: Mon, 26 May 2025 13:55:49 +0000 Subject: [PATCH 2/6] fix syspkg.GetPackageManager never returning errors despite the documentation saying so --- interface.go | 2 +- syspkg.go | 16 +++++++++---- syspkg_test.go | 65 ++++++++++++++++++++++++++++++++++---------------- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/interface.go b/interface.go index 29359be..407e78f 100644 --- a/interface.go +++ b/interface.go @@ -53,7 +53,7 @@ type SysPkg interface { // If the name is empty, the first available package manager will be returned. // If no suitable package manager is found, an error is returned. // Note: only package managers that are specified in the IncludeOptions when creating the SysPkg instance (with New() method) will be returned. If you want to use package managers that are not specified in the IncludeOptions, you should use the FindPackageManagers() method to get a list of all available package managers, or use RefreshPackageManagers() with the IncludeOptions parameter to refresh the package manager list. - GetPackageManager(name string) PackageManager + GetPackageManager(name string) (PackageManager, error) // Install(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) // Delete(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) diff --git a/syspkg.go b/syspkg.go index 9a73a2f..a86d07c 100644 --- a/syspkg.go +++ b/syspkg.go @@ -14,7 +14,7 @@ // if err != nil { // log.Fatal(err) // } -// aptManager := sysPkg.GetPackageManager("apt") +// aptManager,err := sysPkg.GetPackageManager("apt") package syspkg import ( @@ -99,9 +99,14 @@ func (s *sysPkgImpl) FindPackageManagers(include IncludeOptions) (map[string]Pac // GetPackageManager returns a PackageManager instance by its name (e.g., "apt", "snap", "flatpak", etc.). // if name is empty, return the first available -func (s *sysPkgImpl) GetPackageManager(name string) PackageManager { +func (s *sysPkgImpl) GetPackageManager(name string) (PackageManager,error) { var pm PackageManager + // if there are no package managers, return before accessing non existing properties + if len(s.pms) == 0 { + return nil, errors.New("no supported package manager detected") + } + if name == "" { // get first pm available, lexicographically sorted keys := make([]string, 0, len(s.pms)) @@ -111,9 +116,12 @@ func (s *sysPkgImpl) GetPackageManager(name string) PackageManager { sort.Strings(keys) pm = s.pms[keys[0]] } else { - pm = s.pms[name] + pm,found := s.pms[name] + if !found { + return pm, errors.New("no such package manager") + } } - return pm + return pm, nil } // RefreshPackageManagers refreshes the internal list of available package managers, and returns the new list. diff --git a/syspkg_test.go b/syspkg_test.go index 4aac5d1..d3c540c 100644 --- a/syspkg_test.go +++ b/syspkg_test.go @@ -30,7 +30,7 @@ func TestNewPackageManager(t *testing.T) { AllAvailable: true, }) if err != nil { - t.Fatalf("GetPackageManagers() error: %+v", err) + t.Fatalf("FindPackageManagers() error: %+v", err) } log.Printf("pms: %+v", pms) @@ -53,73 +53,96 @@ func TestNewPackageManager(t *testing.T) { // if we are on any other distro, we should have nothing if OSInfo.Distribution == "ubuntu" || OSInfo.Distribution == "debian" || OSInfo.Distribution == "mint" || OSInfo.Distribution == "PopOS" || OSInfo.Distribution == "elementary" || OSInfo.Distribution == "Zorin" || OSInfo.Distribution == "ChromeOS" { - if _, ok := pms["apt"]; !ok && s.GetPackageManager("apt") == nil { - if _, ok := pms["snap"]; !ok && s.GetPackageManager("snap") == nil { - if _, ok := pms["flatpak"]; !ok && s.GetPackageManager("flatpak") == nil { + pm,err:=s.GetPackageManager("apt") + + if err != nil && pm == nil { + pm,err:=s.GetPackageManager("snap") + + if err!=nil && pm == nil { + pm,err:=s.GetPackageManager("flatpak") + + if err!=nil && pm == nil { t.Fatalf("apt, snap, or flatpak package manager not found") } } } } else if OSInfo.Distribution == "fedora" || OSInfo.Distribution == "centos" || OSInfo.Distribution == "rhel" || OSInfo.Distribution == "rockylinux" || OSInfo.Distribution == "almalinux" || OSInfo.Distribution == "amazon linux" || OSInfo.Distribution == "oracle linux" || OSInfo.Distribution == "scientific linux" || OSInfo.Distribution == "cloudlinux" { - if _, ok := pms["dnf"]; !ok && s.GetPackageManager("dnf") == nil { - if _, ok := pms["yum"]; !ok && s.GetPackageManager("yum") == nil { + pm,err:=s.GetPackageManager("dnf") + if err != nil && pm == nil { + pm,err:=s.GetPackageManager("yum") + if err != nil && pm == nil { t.Fatalf("dnf or yum package manager not found") } } } else if OSInfo.Distribution == "opensuse" { - if _, ok := pms["zypper"]; !ok && s.GetPackageManager("zypper") == nil { + pm,err:=s.GetPackageManager("zypper") + if err != nil && pm == nil { t.Fatalf("zypper package manager not found") } } else if OSInfo.Distribution == "alpine" { - if _, ok := pms["apk"]; !ok && s.GetPackageManager("apk") == nil { + pm,err:=s.GetPackageManager("apk") + if err != nil && pm == nil { t.Fatalf("apk package manager not found") } } else if OSInfo.Distribution == "arch" { - if _, ok := pms["pacman"]; !ok && s.GetPackageManager("pacman") == nil { + pm,err:=s.GetPackageManager("pacman") + if err != nil && pm == nil { t.Fatalf("pacman package manager not found") } } else if OSInfo.Distribution == "gentoo" { - if _, ok := pms["emerge"]; !ok && s.GetPackageManager("emerge") == nil { + pm,err:=s.GetPackageManager("emerge") + if err != nil && pm == nil { t.Fatalf("emerge package manager not found") } } else if OSInfo.Distribution == "slackware" { - if _, ok := pms["slackpkg"]; !ok && s.GetPackageManager("slackpkg") == nil { + pm,err:=s.GetPackageManager("slackpkg") + if err != nil && pm == nil { t.Fatalf("slackpkg package manager not found") } } else if OSInfo.Distribution == "void" { - if _, ok := pms["xbps"]; !ok && s.GetPackageManager("xbps") == nil { + pm,err:=s.GetPackageManager("xbps") + if err != nil && pm == nil { t.Fatalf("xbps package manager not found") } } else if OSInfo.Distribution == "solus" { - if _, ok := pms["eopkg"]; !ok && s.GetPackageManager("eopkg") == nil { + pm,err:=s.GetPackageManager("eopkg") + if err != nil && pm == nil { t.Fatalf("eopkg package manager not found") } } else if OSInfo.Distribution == "freebsd" || OSInfo.Distribution == "dragonfly" || OSInfo.Distribution == "termux" { - if _, ok := pms["pkg"]; !ok && s.GetPackageManager("pkg") == nil { + pm,err:=s.GetPackageManager("pkg") + if err != nil && pm == nil { t.Fatalf("pkg package manager not found") } } else if OSInfo.Distribution == "openbsd" || OSInfo.Distribution == "netbsd" { - if _, ok := pms["pkg_add"]; !ok && s.GetPackageManager("pkg_add") == nil { + pm,err:=s.GetPackageManager("pkg_add") + if err != nil && pm == nil { t.Fatalf("pkg_add package manager not found") } } else if OSInfo.Distribution == "macos" { - if _, ok := pms["brew"]; !ok && s.GetPackageManager("brew") == nil { + pm,err:=s.GetPackageManager("brew") + if err != nil && pm == nil { t.Fatalf("brew package manager not found") } } else if OSInfo.Distribution == "windows" { - if _, ok := pms["chocolatey"]; !ok && s.GetPackageManager("chocolatey") == nil { - if _, ok := pms["scoop"]; !ok && s.GetPackageManager("scoop") == nil { - if _, ok := pms["winget"]; !ok && s.GetPackageManager("winget") == nil { + pm,err:=s.GetPackageManager("chocolatey") + if err != nil && pm == nil { + pm,err:=s.GetPackageManager("scoop") + if err != nil && pm == nil { + pm,err:=s.GetPackageManager("winget") + if err != nil && pm == nil { t.Fatalf("chocolatey, scoop, or winget package manager not found") } } } } else if OSInfo.Distribution == "android" { - if _, ok := pms["f-droid"]; !ok && s.GetPackageManager("f-droid") == nil { + pm,err:=s.GetPackageManager("f-droid") + if err != nil && pm == nil { t.Fatalf("f-droid package manager not found") } } else if OSInfo.Distribution == "ios" { - if _, ok := pms["cydia"]; !ok && s.GetPackageManager("cydia") == nil { + pm,err:=s.GetPackageManager("cydia") + if err != nil && pm == nil { t.Fatalf("cydia package manager not found") } } else { From 524f6037b6d7de735e35cb24c06d660290d0c4e1 Mon Sep 17 00:00:00 2001 From: Alberto Massidda Date: Thu, 22 May 2025 19:12:45 +0200 Subject: [PATCH 3/6] add read only yum support (yum search, yum info) add yum tests (commented e2e yum tests as they work only on a RHEL and left unit) --- README.md | 1 + cmd/syspkg/main.go | 8 +- manager/yum/utils.go | 149 +++++++++++++++++++++++++++++++++++ manager/yum/yum.go | 140 +++++++++++++++++++++++++++++++++ manager/yum/yum_test.go | 167 ++++++++++++++++++++++++++++++++++++++++ syspkg.go | 3 + 6 files changed, 464 insertions(+), 4 deletions(-) create mode 100644 manager/yum/utils.go create mode 100644 manager/yum/yum.go create mode 100644 manager/yum/yum_test.go diff --git a/README.md b/README.md index b2943af..6e16453 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ For more examples and real use cases, see the [cmd/syspkg/](cmd/syspkg/) directo | Package Manager | Install | Remove | Search | Upgrade | List Installed | List Upgradable | Get Package Info | | --------------- | ------- | ------ | ------ | ------- | -------------- | --------------- | ---------------- | | APT | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| YUM | ❓ | ❓ | ✅ | ❓ | ✅ | ❓ | ✅ | | SNAP | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Flatpak | ❓ | ❓ | ✅ | ✅ | ✅ | ✅ | ✅ | | Your favorite package manager here! | 🚀 | 🚀 | 🚀 | 🚀 | 🚀 | 🚀 | 🚀 | diff --git a/cmd/syspkg/main.go b/cmd/syspkg/main.go index 29e768e..2fda25d 100644 --- a/cmd/syspkg/main.go +++ b/cmd/syspkg/main.go @@ -293,12 +293,12 @@ func main() { &cli.BoolFlag{ Name: "apt", Usage: "Use apt package manager", - // Hidden: true, + Hidden: false, }, &cli.BoolFlag{ Name: "yum", Usage: "Use yum package manager", - Hidden: true, + Hidden: false, }, &cli.BoolFlag{ Name: "dnf", @@ -323,12 +323,12 @@ func main() { &cli.BoolFlag{ Name: "flatpak", Usage: "Use flatpak package manager", - // Hidden: true, + Hidden: false, }, &cli.BoolFlag{ Name: "snap", Usage: "Use snap package manager", - Hidden: true, + Hidden: false, }, }, } diff --git a/manager/yum/utils.go b/manager/yum/utils.go new file mode 100644 index 0000000..49c6475 --- /dev/null +++ b/manager/yum/utils.go @@ -0,0 +1,149 @@ +// Package yum provides a package manager implementation for RedHat-based systems using +// YUM as the underlying package management tool. +package yum + +import ( + "regexp" + "strings" + + "github.com/bluet/syspkg/manager" +) + +// ParseFindOutput parses the output of `yum search packageName` command +// and returns a list of available packages that match the search query. It extracts package +// information such as name, architecture from the +// output, and stores them in a list of manager.PackageInfo objects. +// +// The output format is expected to be similar to the following example: +// +//Last metadata expiration check: 0:26:09 ago on Thu 22 May 2025 04:30:18 PM UTC. +// ==================================================Name Exactly Matched: nginx ==================================================== +//nginx.x86_64 : A high performance web server and reverse proxy server +//====================================================Name & Summary Matched: nginx================================================== +//nginx-all-modules.noarch : A meta package that installs all available Nginx modules +//nginx-core.x86_64 : nginx minimal core + +// The function first removes the "Last Metadata..." and the "=========" +// lines, and then processes each package entry line to extract relevant +// information. +func ParseFindOutput(msg string, opts *manager.Options) []manager.PackageInfo { + var packages []manager.PackageInfo + + // remove the last empty line + msg = strings.TrimSuffix(msg, "\n") + + // split output by empty lines + var lines []string = strings.Split(msg, "\n") + var packageLineRegex = regexp.MustCompile(`^[\w\d-]+\.[\w\d_]+`) + + for _, line := range lines { + if strings.HasPrefix(line, "=======") { + continue + } + if strings.HasPrefix(line, "Last metadata") { + continue + } + if packageLineRegex.MatchString(line) { + parts := strings.Fields(line) + + // if name is empty, it might be not what we want + if parts[0] == "" { + continue + } + name_arch := strings.Split(parts[0], ".") + if len(name_arch) != 2 { + continue + } + + packageInfo := manager.PackageInfo{ + Name: name_arch[0], + Arch: name_arch[1], + PackageManager: pm, + } + + packages = append(packages, packageInfo) + } + } + + return packages +} + +// ParseListInstalledOutput parses the output of `yum list --installed` command +// and returns a list of installed packages. It extracts the package name, version, +// and architecture from the output and stores them in a list of manager.PackageInfo objects. +func ParseListInstalledOutput(msg string, opts *manager.Options) []manager.PackageInfo { + var packages []manager.PackageInfo + + // remove the last empty line + msg = strings.TrimSuffix(msg, "\n") + lines := strings.Split(string(msg), "\n") + + for _, line := range lines { + if strings.HasPrefix(line, "Installed Packages") { + continue + } + + if len(line) > 0 { + parts := strings.Fields(line) + + // if name is empty, it might be not what we want + if parts[0] == "" { + continue + } + name_arch := strings.Split(parts[0], ".") + if len(name_arch) != 2 { + continue + } + name := name_arch[0] + arch := name_arch[1] + + packageInfo := manager.PackageInfo{ + Name: name, + Version: parts[1], + Status: manager.PackageStatusInstalled, + Arch: arch, + PackageManager: "yum", + } + packages = append(packages, packageInfo) + } + } + + return packages +} + +// ParsePackageInfoOutput parses the output of `yum info packageName` command +// and returns a manager.PackageInfo object containing package information such as name, version, +// architecture, and category. This function is useful for getting detailed package information. +func ParsePackageInfoOutput(msg string, opts *manager.Options) manager.PackageInfo { + var pkg manager.PackageInfo + + // remove the last empty line + msg = strings.TrimSuffix(msg, "\n") + lines := strings.Split(string(msg), "\n") + + for _, line := range lines { + if len(line) > 0 { + parts := strings.SplitN(line, ":", 2) + + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + switch key { + case "Name": + pkg.Name = value + case "Version": + pkg.Version = value + case "Architecture": + pkg.Arch = value + } + } + } + + pkg.PackageManager = "yum" + + return pkg +} diff --git a/manager/yum/yum.go b/manager/yum/yum.go new file mode 100644 index 0000000..f18b80a --- /dev/null +++ b/manager/yum/yum.go @@ -0,0 +1,140 @@ +// Package yum provides an implementation of the syspkg manager interface for the yum package manager. +// It provides an Go (golang) API interface for interacting with the YUM package manager. +// This package is a wrapper around the yum command line tool. +// +// YUM was the default package manager on RedHat-based systems such as Centos, it has been recently superseded by DNF (Dandified YUM) +// +// This package is part of the syspkg library. +package yum + +import ( + "errors" + "log" + "os" + "os/exec" + + "github.com/bluet/syspkg/manager" +) + +var pm string = "yum" + +// Constants used for yum commands +const ( + ArgsAssumeYes string = "-y" + ArgsAssumeNo string = "--assumeno" + ArgsQuiet string = "-q" + ArgsDryRun string = "" + ArgsFixBroken string = "" + ArgsPurge string = "" + ArgsAutoRemove string = "" + ArgsShowProgress string = "" +) + +// ENV_NonInteractive contains environment variables used to set non-interactive mode for yum and dpkg. +var ENV_NonInteractive []string = []string{} + +// PackageManager implements the manager.PackageManager interface for the yum package manager. +type PackageManager struct{} + +// IsAvailable checks if the yum package manager is available on the system. +func (a *PackageManager) IsAvailable() bool { + _, err := exec.LookPath(pm) + return err == nil +} + +// GetPackageManager returns the name of the yum package manager. +func (a *PackageManager) GetPackageManager() string { + return pm +} + +func (a *PackageManager) Install(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + return nil, errors.New("not implemented") +} + +func (a *PackageManager) Delete(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + return nil, errors.New("not implemented") +} + +// Refresh updates the package list using the yum package manager. +func (a *PackageManager) Refresh(opts *manager.Options) error { + cmd := exec.Command(pm, "clean", "expire-cache") + cmd.Env = ENV_NonInteractive + + if opts == nil { + opts = &manager.Options{ + DryRun: false, + Interactive: false, + Verbose: false, + } + } + if opts.Interactive { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + err := cmd.Run() + return err + } else { + out, err := cmd.Output() + if err != nil { + return err + } + if opts.Verbose { + log.Println(string(out)) + } + return nil + } +} + +// Find searches for packages matching the provided keywords using the yum package manager. +func (a *PackageManager) Find(keywords []string, opts *manager.Options) ([]manager.PackageInfo, error) { + args := append([]string{"search"}, keywords...) + cmd := exec.Command(pm, args...) + cmd.Env = ENV_NonInteractive + + out, err := cmd.Output() + if err != nil { + return nil, err + } + + return ParseFindOutput(string(out), opts), nil +} + +// ListInstalled lists all installed packages using the yum package manager. +func (a *PackageManager) ListInstalled(opts *manager.Options) ([]manager.PackageInfo, error) { + args := []string{"list", "--installed"} + cmd := exec.Command(pm, args...) + cmd.Env = ENV_NonInteractive + out, err := cmd.Output() + if err != nil { + return nil, err + } + return ParseListInstalledOutput(string(out), opts), nil +} + +func (a *PackageManager) ListUpgradable(opts *manager.Options) ([]manager.PackageInfo, error) { + return nil, errors.New("not implemented") +} +func (a *PackageManager) Upgrade(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) { + return nil, errors.New("not implemented") +} +func (a *PackageManager) UpgradeAll(opts *manager.Options) ([]manager.PackageInfo, error) { + return nil, errors.New("not implemented") +} +func (a *PackageManager) Clean(opts *manager.Options) error { + return a.Refresh(nil) +} + +// GetPackageInfo retrieves package information for the specified package using the yum package manager. +func (a *PackageManager) GetPackageInfo(pkg string, opts *manager.Options) (manager.PackageInfo, error) { + cmd := exec.Command(pm, "info", pkg) + cmd.Env = ENV_NonInteractive + out, err := cmd.Output() + if err != nil { + return manager.PackageInfo{}, err + } + return ParsePackageInfoOutput(string(out), opts), nil +} + +func (a *PackageManager) AutoRemove(opts *manager.Options) ([]manager.PackageInfo, error) { + return nil, errors.New("not implemented") +} diff --git a/manager/yum/yum_test.go b/manager/yum/yum_test.go new file mode 100644 index 0000000..f8ed752 --- /dev/null +++ b/manager/yum/yum_test.go @@ -0,0 +1,167 @@ +// yum/yum.go +package yum_test + +import ( + "testing" + + "github.com/bluet/syspkg/manager" + "github.com/bluet/syspkg/manager/yum" +) + +func TestYumPackageManagerNotAvailable(t *testing.T) { + yumManager := yum.PackageManager{} + opts := manager.Options{} + packages := []string{"nginx"} + + _, erri := yumManager.Install(packages, nil) + if erri == nil { + t.Fatal("YumPackageManager should not support installation") + } + _, errd := yumManager.Delete(packages, nil) + if errd == nil { + t.Fatal("YumPackageManager should not support removal") + } + _, errlu := yumManager.ListUpgradable(&opts) + if errlu == nil { + t.Fatal("YumPackageManager should not support list-upgadeable") + } + _, erru := yumManager.Upgrade(packages, nil) + if erru == nil { + t.Fatal("YumPackageManager should not support upgrade") + } + _, errua := yumManager.UpgradeAll(&opts) + if errua == nil { + t.Fatal("YumPackageManager should not support upgrade-all") + } + _, errar := yumManager.AutoRemove(&opts) + if errar == nil { + t.Fatal("YumPackageManager should not support autoremove") + } +} + +/* +// these e2e tests work only under a RHEL derived Linux distro that yum installed + +func TestYumPackageManagerIsAvailable(t *testing.T) { + yumManager := yum.PackageManager{} + if !yumManager.IsAvailable() { + t.Fatal("YumPackageManager is not available") + } +} + +func TestYumPackageManagerListPackages(t *testing.T) { + yumManager := yum.PackageManager{} + opts:=manager.Options{} + result,err:=yumManager.ListInstalled(&opts) + if err!=nil{ + t.Errorf("Should have been able to list correctly, %s",err) + } + if len(result)==0{ + t.Fatal("Zero packages detected, there should have been at least one") + } +} + +func TestYumPackageManagerGetPackageInfo(t *testing.T) { + yumManager := yum.PackageManager{} + opts:=manager.Options{} + packages:="rpm" + + result,err:=yumManager.GetPackageInfo(packages, &opts) + if err!=nil{ + t.Errorf("Should have been able to get info correctly, %s",err) + } + if result.Name != packages { + + t.Errorf("rpm should be present, found %+v", result) + } +} + +func TestYumPackageManagerFind(t *testing.T) { + yumManager := yum.PackageManager{} + opts:=manager.Options{} + packages:=[]string{"nginx"} + + result,err:=yumManager.Find(packages, &opts) + if err!=nil{ + t.Errorf("Should have been able to search correctly, %s",err) + } + if len(result)==0 { + t.Errorf("nginx should be present, found %+v", result) + } + if result[0].Name != packages[0] { + t.Errorf("nginx should be available, found %+v", result) + } +} +*/ + +func TestParseFindOutput(t *testing.T) { + msg := ` +Last metadata expiration check: 4:56:00 ago on Thu 22 May 2025 04:30:18 PM UTC. +============================================================================= Name Exactly Matched: nginx ============================================================================== +nginx.x86_64 : A high performance web server and reverse proxy server +============================================================================ Name & Summary Matched: nginx ============================================================================= +nginx-all-modules.noarch : A meta package that installs all available Nginx modules +nginx-core.x86_64 : nginx minimal core +nginx-filesystem.noarch : The basic directory layout for the Nginx server +nginx-mod-http-image-filter.x86_64 : Nginx HTTP image filter module +nginx-mod-http-perl.x86_64 : Nginx HTTP perl module +nginx-mod-http-xslt-filter.x86_64 : Nginx XSLT module +nginx-mod-mail.x86_64 : Nginx mail modules +nginx-mod-stream.x86_64 : Nginx stream modules +pcp-pmda-nginx.x86_64 : Performance Co-Pilot (PCP) metrics for the Nginx Webserver +` + packages := yum.ParseFindOutput(msg, nil) + if packages[0].Name != "nginx" || packages[0].Arch != "x86_64" { + t.Errorf("Expected to find nginx, found %+v", packages[0]) + } +} + +func TestParseListInstalledOutput(t *testing.T) { + msg := ` + Installed Packages +NetworkManager.x86_64 1:1.48.10-2.el9_5 @baseos +rocky-release.noarch 9.5-1.2.el9 @baseos +rpm.x86_64 4.16.1.3-34.el9.0.1 @baseos +rsync.x86_64 3.2.3-20.el9 @baseos + +` + packages := yum.ParseListInstalledOutput(msg, nil) + found := false + for _, pack := range packages { + if pack.Name != "rpm" || pack.Arch != "x86_64" || pack.Version != "4.16.1.3-34.el9.0.1" { + found = true + break + } + } + if !found { + t.Errorf("Expected to find nginx, found %+v", packages) + } +} + +func TestParsePackageInfoOutput(t *testing.T) { + msg := ` + Last metadata expiration check: 5:16:10 ago on Thu 22 May 2025 04:30:18 PM UTC. +Available Packages +Name : nginx +Epoch : 2 +Version : 1.20.1 +Release : 20.el9.0.1 +Architecture : x86_64 +Size : 36 k +Source : nginx-1.20.1-20.el9.0.1.src.rpm +Repository : appstream +Summary : A high performance web server and reverse proxy server +URL : https://nginx.org +License : BSD +Description : Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and + : IMAP protocols, with a strong focus on high concurrency, performance and low + : memory usage. + + + +` + packages := yum.ParsePackageInfoOutput(msg, nil) + if packages.Name != "nginx" || packages.Arch != "x86_64" || packages.Version != "1.20.1" { + t.Errorf("Expected to find nginx, found %+v", packages) + } +} diff --git a/syspkg.go b/syspkg.go index a86d07c..cdde621 100644 --- a/syspkg.go +++ b/syspkg.go @@ -26,6 +26,7 @@ import ( "github.com/bluet/syspkg/manager/apt" "github.com/bluet/syspkg/manager/flatpak" "github.com/bluet/syspkg/manager/snap" + "github.com/bluet/syspkg/manager/yum" // "github.com/bluet/syspkg/zypper" // "github.com/bluet/syspkg/dnf" // "github.com/bluet/syspkg/apk" @@ -42,6 +43,7 @@ type IncludeOptions struct { Dnf bool Flatpak bool Snap bool + Yum bool Zypper bool } @@ -76,6 +78,7 @@ func (s *sysPkgImpl) FindPackageManagers(include IncludeOptions) (map[string]Pac {"apt", &apt.PackageManager{}, include.Apt}, {"flatpak", &flatpak.PackageManager{}, include.Flatpak}, {"snap", &snap.PackageManager{}, include.Snap}, + {"yum", &yum.PackageManager{}, include.Yum}, // {"apk", &apk.PackageManager{}, include.Apk}, // {"dnf", &dnf.PackageManager{}, include.Dnf}, // {"zypper", &zypper.PackageManager{}, include.Zypper}, From d711045c0c42c81e1a1ea8ae155efbfb1f15a86e Mon Sep 17 00:00:00 2001 From: Alberto Massidda Date: Thu, 22 May 2025 21:55:56 +0000 Subject: [PATCH 4/6] fix osinfo not stripping double quotes from distribution name add rockylinux/9 dist id (rocky) --- osinfo/osinfo.go | 4 +++- syspkg_test.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osinfo/osinfo.go b/osinfo/osinfo.go index 1c11d93..ec6acd3 100644 --- a/osinfo/osinfo.go +++ b/osinfo/osinfo.go @@ -89,8 +89,10 @@ func getLinuxDistribution() (string, string, error) { line := scanner.Text() if strings.HasPrefix(line, "ID=") { dist = strings.TrimPrefix(line, "ID=") + dist = strings.Trim(dist, "\"") } else if strings.HasPrefix(line, "VERSION_ID=") { - distVersion = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"") + distVersion = strings.TrimPrefix(line, "VERSION_ID=") + distVersion = strings.Trim(distVersion, "\"") } } diff --git a/syspkg_test.go b/syspkg_test.go index d3c540c..03adea1 100644 --- a/syspkg_test.go +++ b/syspkg_test.go @@ -66,7 +66,7 @@ func TestNewPackageManager(t *testing.T) { } } } - } else if OSInfo.Distribution == "fedora" || OSInfo.Distribution == "centos" || OSInfo.Distribution == "rhel" || OSInfo.Distribution == "rockylinux" || OSInfo.Distribution == "almalinux" || OSInfo.Distribution == "amazon linux" || OSInfo.Distribution == "oracle linux" || OSInfo.Distribution == "scientific linux" || OSInfo.Distribution == "cloudlinux" { + } else if OSInfo.Distribution == "fedora" || OSInfo.Distribution == "centos" || OSInfo.Distribution == "rhel" || OSInfo.Distribution == "rockylinux" || OSInfo.Distribution == "rocky" || OSInfo.Distribution == "almalinux" || OSInfo.Distribution == "amazon linux" || OSInfo.Distribution == "oracle linux" || OSInfo.Distribution == "scientific linux" || OSInfo.Distribution == "cloudlinux" { pm,err:=s.GetPackageManager("dnf") if err != nil && pm == nil { pm,err:=s.GetPackageManager("yum") From c2716bf4b9d725ab9807b0404ca971d9e1e6913d Mon Sep 17 00:00:00 2001 From: Alberto Massidda Date: Mon, 26 May 2025 14:21:48 +0200 Subject: [PATCH 5/6] fix installed package method not managing correctly the split of name during line parsing --- manager/yum/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manager/yum/utils.go b/manager/yum/utils.go index 49c6475..b202fd8 100644 --- a/manager/yum/utils.go +++ b/manager/yum/utils.go @@ -86,8 +86,8 @@ func ParseListInstalledOutput(msg string, opts *manager.Options) []manager.Packa if len(line) > 0 { parts := strings.Fields(line) - // if name is empty, it might be not what we want - if parts[0] == "" { + // if it doesn't split correctly, or the name is empty, it might be not what we want + if len(parts) < 2 || parts[0] == "" { continue } name_arch := strings.Split(parts[0], ".") From 6a96bf66841326ee50abc6862184f6a79a201937 Mon Sep 17 00:00:00 2001 From: Alberto Massidda Date: Mon, 26 May 2025 13:27:57 +0000 Subject: [PATCH 6/6] remove non interactive options as confusing in this phase of read-only commands only --- manager/yum/yum.go | 7 ------- manager/yum/yum_test.go | 6 +++--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/manager/yum/yum.go b/manager/yum/yum.go index f18b80a..48c0bf0 100644 --- a/manager/yum/yum.go +++ b/manager/yum/yum.go @@ -30,9 +30,6 @@ const ( ArgsShowProgress string = "" ) -// ENV_NonInteractive contains environment variables used to set non-interactive mode for yum and dpkg. -var ENV_NonInteractive []string = []string{} - // PackageManager implements the manager.PackageManager interface for the yum package manager. type PackageManager struct{} @@ -58,7 +55,6 @@ func (a *PackageManager) Delete(pkgs []string, opts *manager.Options) ([]manager // Refresh updates the package list using the yum package manager. func (a *PackageManager) Refresh(opts *manager.Options) error { cmd := exec.Command(pm, "clean", "expire-cache") - cmd.Env = ENV_NonInteractive if opts == nil { opts = &manager.Options{ @@ -89,7 +85,6 @@ func (a *PackageManager) Refresh(opts *manager.Options) error { func (a *PackageManager) Find(keywords []string, opts *manager.Options) ([]manager.PackageInfo, error) { args := append([]string{"search"}, keywords...) cmd := exec.Command(pm, args...) - cmd.Env = ENV_NonInteractive out, err := cmd.Output() if err != nil { @@ -103,7 +98,6 @@ func (a *PackageManager) Find(keywords []string, opts *manager.Options) ([]manag func (a *PackageManager) ListInstalled(opts *manager.Options) ([]manager.PackageInfo, error) { args := []string{"list", "--installed"} cmd := exec.Command(pm, args...) - cmd.Env = ENV_NonInteractive out, err := cmd.Output() if err != nil { return nil, err @@ -127,7 +121,6 @@ func (a *PackageManager) Clean(opts *manager.Options) error { // GetPackageInfo retrieves package information for the specified package using the yum package manager. func (a *PackageManager) GetPackageInfo(pkg string, opts *manager.Options) (manager.PackageInfo, error) { cmd := exec.Command(pm, "info", pkg) - cmd.Env = ENV_NonInteractive out, err := cmd.Output() if err != nil { return manager.PackageInfo{}, err diff --git a/manager/yum/yum_test.go b/manager/yum/yum_test.go index f8ed752..ec6a217 100644 --- a/manager/yum/yum_test.go +++ b/manager/yum/yum_test.go @@ -1,4 +1,4 @@ -// yum/yum.go +// yum/yum_test.go package yum_test import ( @@ -128,13 +128,13 @@ rsync.x86_64 packages := yum.ParseListInstalledOutput(msg, nil) found := false for _, pack := range packages { - if pack.Name != "rpm" || pack.Arch != "x86_64" || pack.Version != "4.16.1.3-34.el9.0.1" { + if pack.Name == "rpm" || pack.Arch == "x86_64" || pack.Version == "4.16.1.3-34.el9.0.1" { found = true break } } if !found { - t.Errorf("Expected to find nginx, found %+v", packages) + t.Errorf("Expected to find rpm, but not found. Found instead %+v", packages) } }