From ea4d89345bd13d69b559b1f09dfaa2267b43343e Mon Sep 17 00:00:00 2001 From: John-Michael Mulesa Date: Tue, 3 Feb 2026 10:11:41 -0500 Subject: [PATCH 1/7] Implement package `Provides` capability. Adds support for virtual package provision and alternative dependency satisfaction. Changes: - goolib: Add Provides field to PkgSpec. - install: Implement isSatisfied to resolve dependencies via providers. - install: Update conflict resolution to check providers. - client: Add FindSatisfyingRepoLatest for repo-side provider resolution, prioritizing direct name matches over providers. - system: Add system_darwin.go stubs for macOS compatibility. - tests: Add comprehensive unit and integration tests for new logic. --- client/client.go | 124 +++++++++++++++++++++++ client/client_test.go | 142 ++++++++++++++++++++++++++ goolib/goospec.go | 1 + goolib/goospec_test.go | 4 + install/install.go | 115 +++++++++++++++++---- install/install_test.go | 219 ++++++++++++++++++++++++++++++++++++++++ system/system_darwin.go | 86 ++++++++++++++++ 7 files changed, 671 insertions(+), 20 deletions(-) create mode 100644 system/system_darwin.go diff --git a/client/client.go b/client/client.go index dd56c58..635f269 100644 --- a/client/client.go +++ b/client/client.go @@ -430,6 +430,130 @@ func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (string, return "", "", "", fmt.Errorf("no versions of package %s found in any repo", name) } +// FindSatisfyingRepoLatest returns the latest version of a package that satisfies the package info, +// along with its repo. It checks both direct name matches and "Provides" entries. +// The archs are searched in order. +func FindSatisfyingRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (*goolib.PkgSpec, string, error) { + name := pi.Name + if pi.Arch != "" { + archs = []string{pi.Arch} + name = fmt.Sprintf("%s.%s", pi.Name, pi.Arch) + } + + for _, a := range archs { + psmDirect := make(map[string][]*goolib.PkgSpec) + psmProvides := make(map[string][]*goolib.PkgSpec) + + for u, r := range rm { + for _, p := range r.Packages { + ps := p.PackageSpec + if ps.Arch != a { + continue + } + + // Check exact match + if ps.Name == pi.Name { + if satisfiesVersion(ps.Version, pi.Ver) { + psmDirect[u] = append(psmDirect[u], ps) + } + // If exact match, we don't check provides for THIS package. + continue + } + + // Check provides + for _, prov := range ps.Provides { + if satisfiesProvider(prov, pi.Name, pi.Ver) { + psmProvides[u] = append(psmProvides[u], ps) + break + } + } + } + } + + // If direct matches exist, use ONLY them. + if len(psmDirect) > 0 { + pkg, repo := pickBest(psmDirect, rm) + if pkg != nil { + return pkg, repo, nil + } + } + + // If no direct matches, check providers. + // Note: This matches Arch behavior (prefer real package). + if len(psmProvides) > 0 { + pkg, repo := pickBest(psmProvides, rm) + if pkg != nil { + return pkg, repo, nil + } + } + } + + return nil, "", fmt.Errorf("no package found satisfying %s in any repo", name) +} + +func satisfiesVersion(pkgVer, reqVer string) bool { + if reqVer == "" { + return true + } + c, err := goolib.Compare(pkgVer, reqVer) + if err != nil { + logger.Errorf("Error comparing versions %s vs %s: %v", pkgVer, reqVer, err) + return false + } + return c >= 0 +} + +func satisfiesProvider(prov, reqName, reqVer string) bool { + pName := prov + pVer := "" + if i := strings.Index(prov, "="); i != -1 { + pName = prov[:i] + pVer = prov[i+1:] + } + + if pName != reqName { + return false + } + + if reqVer == "" { + return true + } + + // If provider is unversioned, it satisfies dependency? + // Arch says yes. + if pVer == "" { + return true + } + + return satisfiesVersion(pVer, reqVer) +} + +func pickBest(psm map[string][]*goolib.PkgSpec, rm RepoMap) (*goolib.PkgSpec, string) { + var bestPkg *goolib.PkgSpec + var repoURL string + var pri priority.Value + + for u, pl := range psm { + for _, pkg := range pl { + q := rm[u].Priority + c := 1 + if bestPkg != nil { + var err error + if c, err = goolib.ComparePriorityVersion(q, pkg.Version, pri, bestPkg.Version); err != nil { + logger.Errorf("compare of %s to %s failed with error: %v", pkg.Version, bestPkg.Version, err) + continue + } + } + if c == 1 { + repoURL = u + bestPkg = pkg + pri = q + } + } + } + return bestPkg, repoURL +} + // WhatRepo returns what repo a package is in. // Name, Arch, and Ver fields of PackageInfo must be provided. func WhatRepo(pi goolib.PackageInfo, rm RepoMap) (string, error) { diff --git a/client/client_test.go b/client/client_test.go index dae7542..cd8f3f2 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -32,6 +32,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/googet/v2/goolib" "github.com/google/googet/v2/oswrap" + "github.com/google/googet/v2/priority" "github.com/google/logger" ) @@ -409,3 +410,144 @@ func TestFindRepoSpecNoMatch(t *testing.T) { t.Error("did not get expected error when running FindRepoSpec") } } + +func TestFindSatisfyingRepoLatest(t *testing.T) { + rm := RepoMap{ + "repo1": Repo{ + Priority: priority.Value(500), + Packages: []goolib.RepoSpec{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "real_pkg", + Version: "2.0.0", + Arch: "noarch", + Provides: []string{ + "virtual_pkg", + "virtual_versioned=1.0.0", + }, + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "real_pkg_old", + Version: "1.0.0", + Arch: "noarch", + Provides: []string{ + "virtual_pkg", + }, + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "other_pkg", + Version: "1.0.0", + Arch: "noarch", + }, + }, + }, + }, + } + + tests := []struct { + name string + pi goolib.PackageInfo + wantName string + wantVer string + wantError bool + }{ + { + name: "Direct match", + pi: goolib.PackageInfo{Name: "other_pkg", Arch: "noarch"}, + wantName: "other_pkg", + wantVer: "1.0.0", + }, + { + name: "Provider match unversioned", + pi: goolib.PackageInfo{Name: "virtual_pkg", Arch: "noarch"}, + wantName: "real_pkg", + wantVer: "2.0.0", // latest real_pkg + }, + { + name: "Provider match matched version", + pi: goolib.PackageInfo{Name: "virtual_versioned", Ver: "1.0.0", Arch: "noarch"}, + wantName: "real_pkg", + wantVer: "2.0.0", + }, + { + name: "Provider match unsatisfied version", + pi: goolib.PackageInfo{Name: "virtual_versioned", Ver: "2.0.0", Arch: "noarch"}, + wantError: true, + }, + { + name: "No match", + pi: goolib.PackageInfo{Name: "missing_pkg", Arch: "noarch"}, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + spec, _, err := FindSatisfyingRepoLatest(tt.pi, rm, []string{"noarch"}) + if tt.wantError { + if err == nil { + t.Errorf("FindSatisfyingRepoLatest(%v) wanted error, got nil", tt.pi) + } + return + } + if err != nil { + t.Fatalf("FindSatisfyingRepoLatest(%v) unexpected error: %v", tt.pi, err) + } + if spec.Name != tt.wantName { + t.Errorf("FindSatisfyingRepoLatest(%v) name = %q, want %q", tt.pi, spec.Name, tt.wantName) + } + if spec.Version != tt.wantVer { // Simplified check, assumes simple version strings in test + t.Errorf("FindSatisfyingRepoLatest(%v) version = %q, want %q", tt.pi, spec.Version, tt.wantVer) + } + }) + } +} + +func TestFindSatisfyingRepoLatest_Priority(t *testing.T) { + // Setup repo with both direct match and provider. + // direct match: version 1.0.0 + // provider: version 2.0.0 (provides it) + // direct match should win despite lower version. + + rm := RepoMap{ + "repo1": Repo{ + Priority: priority.Value(500), + Packages: []goolib.RepoSpec{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "real_pkg", + Version: "1.0.0", + Arch: "noarch", + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "2.0.0", + Arch: "noarch", + Provides: []string{ + "real_pkg", + }, + }, + }, + }, + }, + } + + pi := goolib.PackageInfo{Name: "real_pkg", Arch: "noarch"} + spec, _, err := FindSatisfyingRepoLatest(pi, rm, []string{"noarch"}) + if err != nil { + t.Fatalf("FindSatisfyingRepoLatest failed: %v", err) + } + + if spec.Name != "real_pkg" { + t.Errorf("Expected direct match 'real_pkg', got '%s'", spec.Name) + } + if spec.Version != "1.0.0" { + t.Errorf("Expected version '1.0.0', got '%s'", spec.Version) + } +} diff --git a/goolib/goospec.go b/goolib/goospec.go index c4943f2..7a54209 100644 --- a/goolib/goospec.go +++ b/goolib/goospec.go @@ -89,6 +89,7 @@ type PkgSpec struct { PkgDependencies map[string]string `json:",omitempty"` Replaces []string Conflicts []string + Provides []string Install ExecFile Uninstall ExecFile Verify ExecFile diff --git a/goolib/goospec_test.go b/goolib/goospec_test.go index 17274b3..e0641a3 100644 --- a/goolib/goospec_test.go +++ b/goolib/goospec_test.go @@ -454,6 +454,7 @@ func TestMarshal(t *testing.T) { Source: "github", Replaces: []string{"foo"}, Conflicts: []string{"bar"}, + Provides: []string{"baz"}, Install: ExecFile{ Path: "install.ps1", }, @@ -479,6 +480,9 @@ func TestMarshal(t *testing.T) { "Conflicts": [ "bar" ], + "Provides": [ + "baz" + ], "Install": { "Path": "install.ps1" }, diff --git a/install/install.go b/install/install.go index d471c7f..df18aab 100644 --- a/install/install.go +++ b/install/install.go @@ -56,18 +56,69 @@ func minInstalled(pi goolib.PackageInfo, db *googetdb.GooDB) (bool, error) { return false, nil } +// isSatisfied reports whether the package dependency is satisfied by an installed package +// or a package that provides it. +func isSatisfied(pi goolib.PackageInfo, db *googetdb.GooDB) (bool, error) { + // First check if the package itself is installed. + ok, err := minInstalled(pi, db) + if err != nil { + return false, err + } + if ok { + return true, nil + } + + // Check if any installed package provides this dependency. + pkgs, err := db.FetchPkgs("") + if err != nil { + return false, err + } + for _, p := range pkgs { + if p.PackageSpec == nil { + continue + } + for _, prov := range p.PackageSpec.Provides { + pName := prov + pVer := "" + // We support "name=version" format for providers. + if i := strings.Index(prov, "="); i != -1 { + pName = prov[:i] + pVer = prov[i+1:] + } + + if pName == pi.Name { + // If we just need the name, or if the provider has no version (implies all versions), + // or if the provider version satisfies the requirement. + if pi.Ver == "" || pVer == "" { + return true, nil + } + c, err := goolib.Compare(pVer, pi.Ver) + if err != nil { + logger.Errorf("Error comparing versions for provider %s: %v", prov, err) + continue + } + if c > -1 { + return true, nil + } + } + } + } + return false, nil +} + func resolveConflicts(ps *goolib.PkgSpec, db *googetdb.GooDB) error { // Check for any conflicting packages. // TODO(ajackura): Make sure no conflicting packages are listed as // dependencies or subdependancies. for _, pkg := range ps.Conflicts { pi := goolib.PkgNameSplit(pkg) - ins, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: pi.Ver}, db) + // Check if the conflicting package or a provider of it is installed. + sat, err := isSatisfied(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: pi.Ver}, db) if err != nil { return err } - if ins { - return fmt.Errorf("cannot install, conflict with installed package: %s", pi) + if sat { + return fmt.Errorf("cannot install, conflict with installed package or provider: %s", pi) } } return nil @@ -106,29 +157,23 @@ func installDeps(ctx context.Context, ps *goolib.PkgSpec, cache string, rm clien // Check for and install any dependencies. for p, ver := range ps.PkgDependencies { pi := goolib.PkgNameSplit(p) - ok, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, db) + ok, err := isSatisfied(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, db) if err != nil { return err } else if ok { - logger.Infof("Dependency met: %s.%s with version greater than %s installed", pi.Name, pi.Arch, ver) + logger.Infof("Dependency met: %s.%s with version greater than %s installed or provided", pi.Name, pi.Arch, ver) continue } - v, repo, arch, err := client.FindRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ""}, rm, archs) + + spec, repo, err := client.FindSatisfyingRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, rm, archs) if err != nil { return err } - c, err := goolib.Compare(v, ver) - if err != nil { + + logger.Infof("Dependency found: %s.%s %s (provides %s) is available", spec.Name, spec.Arch, spec.Version, pi.Name) + if err := FromRepo(ctx, goolib.PackageInfo{Name: spec.Name, Arch: spec.Arch, Ver: spec.Version}, repo, cache, rm, archs, dbOnly, downloader, db); err != nil { return err } - if c > -1 { - logger.Infof("Dependency found: %s.%s %s is available", pi.Name, arch, v) - if err := FromRepo(ctx, goolib.PackageInfo{Name: pi.Name, Arch: arch, Ver: v}, repo, cache, rm, archs, dbOnly, downloader, db); err != nil { - return err - } - continue - } - return fmt.Errorf("cannot resolve dependency, %s.%s version %s or greater not installed and not available in any repo", pi.Name, arch, ver) } return resolveReplacements(ctx, ps, dbOnly, downloader, db) } @@ -137,7 +182,37 @@ func installDeps(ctx context.Context, ps *goolib.PkgSpec, cache string, rm clien func FromRepo(ctx context.Context, pi goolib.PackageInfo, repo, cache string, rm client.RepoMap, archs []string, dbOnly bool, downloader *client.Downloader, db *googetdb.GooDB) error { logger.Infof("Starting install of %s.%s.%s", pi.Name, pi.Arch, pi.Ver) fmt.Printf("Installing %s.%s.%s and dependencies...\n", pi.Name, pi.Arch, pi.Ver) - rs, err := client.FindRepoSpec(pi, rm[repo]) + var rs goolib.RepoSpec + var err error + // If the version is empty, we need to find the latest version. + // We also try FindSatisfyingRepoLatest to handle providers if finding by exact name fails or if we want latest compatible. + // But FromRepo is typically called with a specific version (from installDeps or user input). + // If called with specific version, we usually want THAT version. + // Users might run "googet install virtual_pkg". In that case pi.Ver might be empty. + if pi.Ver == "" { + spec, repoURL, err := client.FindSatisfyingRepoLatest(pi, rm, archs) + if err != nil { + return err + } + repo = repoURL + // We found a satisfying package using the new logic. Use its real name and version. + // NOTE: This might switch the name from a virtual one to the real one. + pi.Name = spec.Name + pi.Arch = spec.Arch + pi.Ver = spec.Version + rs, err = client.FindRepoSpec(goolib.PackageInfo{Name: spec.Name, Arch: spec.Arch, Ver: spec.Version}, rm[repo]) + } else { + // Even if name is virtual, if version is specified, we might need to resolve it? + // But existing FindRepoSpec relies on exact name match inside the repo object. + // If "googet install virtual_pkg.noarch.1.0.0", FindRepoSpec won't find it if it's virtual. + // So we SHOULD use FindSatisfyingRepoLatest (or similar) here too if exact match fails? + // For now, let's keep original behavior for explicit version invocations unless we want to support "install virtual=1.0". + // Actually, let's try strict match first, if fail, try satisfying logic? + // But FindRepoSpec takes a Repo struct, not RepoMap. + // Let's stick to simple flow for now (installDeps usage). + rs, err = client.FindRepoSpec(pi, rm[repo]) + } + if err != nil { return err } @@ -202,13 +277,13 @@ func FromDisk(pkgPath, cache string, dbOnly, shouldReinstall bool, db *googetdb. } for p, ver := range zs.PkgDependencies { pi := goolib.PkgNameSplit(p) - if ok, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, db); err != nil { + if ok, err := isSatisfied(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, db); err != nil { return err } else if ok { - logger.Infof("Dependency met: %s.%s with version greater than %s installed", pi.Name, pi.Arch, ver) + logger.Infof("Dependency met: %s.%s with version greater than %s installed or provided", pi.Name, pi.Arch, ver) continue } - return fmt.Errorf("package dependency %s %s (min version %s) not installed", pi.Name, pi.Arch, ver) + return fmt.Errorf("package dependency %s %s (min version %s) not installed and not provided", pi.Name, pi.Arch, ver) } for _, pkg := range zs.Replaces { pi := goolib.PkgNameSplit(pkg) diff --git a/install/install_test.go b/install/install_test.go index 7d7a914..694d6d5 100644 --- a/install/install_test.go +++ b/install/install_test.go @@ -28,6 +28,7 @@ import ( "github.com/google/googet/v2/googetdb" "github.com/google/googet/v2/goolib" "github.com/google/googet/v2/oswrap" + "github.com/google/googet/v2/priority" "github.com/google/googet/v2/settings" "github.com/google/logger" ) @@ -288,3 +289,221 @@ func TestResolveDst(t *testing.T) { } } } + +func TestIsSatisfied(t *testing.T) { + settings.Initialize(t.TempDir(), false) + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Provides: []string{"libfoo", "libbar=1.5.0"}, + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "real_pkg", + Version: "2.0.0@1", + Arch: "noarch", + }, + }, + } + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + t.Fatalf("googetdb.NewDB: %v", err) + } + defer db.Close() + if err := db.WriteStateToDB(state); err != nil { + t.Fatalf("WriteStateToDB: %v", err) + } + + tests := []struct { + name string + pi goolib.PackageInfo + want bool + }{ + { + name: "Directly installed package", + pi: goolib.PackageInfo{Name: "real_pkg", Arch: "noarch", Ver: "1.0.0"}, + want: true, + }, + { + name: "Provided package without version", + pi: goolib.PackageInfo{Name: "libfoo", Arch: "noarch", Ver: "1.0.0"}, + want: true, + }, + { + name: "Provided package with satisfied version", + pi: goolib.PackageInfo{Name: "libbar", Arch: "noarch", Ver: "1.0.0"}, + want: true, + }, + { + name: "Provided package with unsatisfied version", + pi: goolib.PackageInfo{Name: "libbar", Arch: "noarch", Ver: "2.0.0"}, + want: false, + }, + { + name: "Not installed and not provided", + pi: goolib.PackageInfo{Name: "missing_pkg", Arch: "noarch", Ver: "1.0.0"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := isSatisfied(tt.pi, db) + if err != nil { + t.Fatalf("isSatisfied error: %v", err) + } + if got != tt.want { + t.Errorf("isSatisfied(%v) = %v, want %v", tt.pi, got, tt.want) + } + }) + } +} + +func TestResolveConflicts_Provides(t *testing.T) { + settings.Initialize(t.TempDir(), false) + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Provides: []string{"libconflict"}, + }, + }, + } + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + t.Fatalf("googetdb.NewDB: %v", err) + } + defer db.Close() + if err := db.WriteStateToDB(state); err != nil { + t.Fatalf("WriteStateToDB: %v", err) + } + + ps := &goolib.PkgSpec{ + Name: "conflicting_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Conflicts: []string{"libconflict"}, + } + + err = resolveConflicts(ps, db) + if err == nil { + t.Error("resolveConflicts expected error, got nil") + } else { + expectedErr := "cannot install, conflict with installed package or provider: libconflict" + if err.Error() != expectedErr { + t.Errorf("resolveConflicts error = %q, want %q", err.Error(), expectedErr) + } + } +} + +func TestFromRepo_SatisfiedByProvider(t *testing.T) { + // This is a more integration-level test to ensure installDeps uses isSatisfied. + // We mock the DB state and call installDeps directly or via a wrapper if accessible. + // installDeps is unexported, but we are in package install. + + settings.Initialize(t.TempDir(), false) + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Provides: []string{"libvirt"}, + }, + }, + } + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + t.Fatalf("googetdb.NewDB: %v", err) + } + defer db.Close() + if err := db.WriteStateToDB(state); err != nil { + t.Fatalf("WriteStateToDB: %v", err) + } + + // Package wanting libvirt + ps := &goolib.PkgSpec{ + Name: "consumer_pkg", + Version: "1.0.0@1", + Arch: "noarch", + PkgDependencies: map[string]string{"libvirt": "1.0.0"}, + } + + // We pass empty repo map and downloader because we expect it NOT to try downloading deps + // since they are satisfied. + err = installDeps(nil, ps, "", nil, nil, false, nil, db) + if err != nil { + t.Errorf("installDeps failed: %v", err) + } +} + +func TestFromRepo_SatisfiedByUninstalledProvider(t *testing.T) { + settings.Initialize(t.TempDir(), false) + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + t.Fatalf("googetdb.NewDB: %v", err) + } + defer db.Close() + + // Repo state with provider + rm := client.RepoMap{ + "repo1": client.Repo{ + Priority: priority.Value(500), + Packages: []goolib.RepoSpec{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Provides: []string{"libvirt"}, + }, + }, + }, + }, + } + + // Package wanting libvirt + ps := &goolib.PkgSpec{ + Name: "consumer_pkg", + Version: "1.0.0@1", + Arch: "noarch", + PkgDependencies: map[string]string{"libvirt": "1.0.0"}, + } + + // We pass a valid rm but nil downloader. + // installDeps -> FindSatisfyingRepoLatest (finds provider_pkg) -> FromRepo (provider_pkg) -> download... + // Since downloader is nil, FromRepo might fail or panic if we go deep. + // But FromRepo calls client.FindRepoSpec first. + // We want to verify it TRIES to install provider_pkg. + // The best way is to catch the error and check if it's related to downloading "provider_pkg". + // Or define a mock downloader if possible? client.Downloader is a struct, hard to mock methods. + // However, FromRepo fails if downloader is nil probably. + + // We can't easily mock downloader without changing code significantly. + // But we can verify it fails at download stage, NOT at resolution stage. + + downloader, _ := client.NewDownloader("") + err = installDeps(nil, ps, "", rm, []string{"noarch"}, false, downloader, db) + + // We expect an error because download will fail (invalid URL/Source). + if err == nil { + t.Error("installDeps expected error, got nil") + } else { + // If resolution failed, it would say "cannot resolve dependency". + // If resolution succeeded, it proceeds to download and fails there. + // "unsupported protocol scheme" or "client.Get" error etc. + // Or "no source specified" + errMsg := err.Error() + if errMsg == "cannot resolve dependency, libvirt.noarch version 1.0.0 or greater not installed and not available in any repo" { + t.Errorf("installDeps failed to resolve provider: %v", err) + } + // Any other error means it TRIED to install it (provider found). + t.Logf("Got expected error (confirming resolution success): %v", err) + } +} diff --git a/system/system_darwin.go b/system/system_darwin.go new file mode 100644 index 0000000..2e17f5c --- /dev/null +++ b/system/system_darwin.go @@ -0,0 +1,86 @@ +//go:build darwin +// +build darwin + +/* +Copyright 2016 Google Inc. All Rights Reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package system + +import ( + "fmt" + "os" + "strconv" + "syscall" + + "github.com/google/googet/v2/client" + "github.com/google/googet/v2/goolib" +) + +// Install performs a system specfic install given a package extraction directory and a PkgSpec struct. +func Install(dir string, ps *goolib.PkgSpec) error { + return nil +} + +// Uninstall performs a system specfic uninstall given a package extraction directory and a PkgSpec struct. +func Uninstall(dir string, ps *client.PackageState) error { + return nil +} + +// InstallableArchs returns a slice of archs supported by this machine. +func InstallableArchs() ([]string, error) { + return []string{"noarch", "x86_64", "arm64"}, nil +} + +// AppAssociation returns empty strings and is a stub of the Windows implementation. +func AppAssociation(ps *goolib.PkgSpec, installSource string) (string, string) { + return "", "" +} + +// IsAdmin returns nil and is a stub of the Windows implementation +func IsAdmin() error { + return nil +} + +// isGooGetRunning checks if the process with the given PID is running and is a googet process. +func isGooGetRunning(pid int) (bool, error) { + // Stub for Darwin, assuming not running to avoid complexity with ps or sysctl + return false, nil +} + +// lock attempts to obtain an exclusive lock on the provided file. +func lock(f *os.File) (func(), error) { + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + return nil, err + } + cleanup := func() { + syscall.Flock(int(f.Fd()), syscall.LOCK_UN) + f.Close() + os.Remove(f.Name()) + } + + if err := f.Truncate(0); err != nil { + cleanup() + return nil, fmt.Errorf("failed to truncate lockfile: %v", err) + } + if _, err := f.WriteString(strconv.Itoa(os.Getpid())); err != nil { + cleanup() + return nil, fmt.Errorf("failed to write PID to lockfile: %v", err) + } + + // Downgrade to shared lock so that other processes can read the PID. + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil { + cleanup() + return nil, fmt.Errorf("failed to downgrade to shared lock: %v", err) + } + return cleanup, nil +} From f2280ede7ee3c0f78dd1336113ee32b621166357 Mon Sep 17 00:00:00 2001 From: John-Michael Mulesa Date: Tue, 3 Feb 2026 10:23:42 -0500 Subject: [PATCH 2/7] Fix formatting. --- client/client.go | 14 +++++++------- client/client_test.go | 6 +++--- install/install.go | 8 ++++---- install/install_test.go | 10 +++++----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/client/client.go b/client/client.go index 635f269..349b32d 100644 --- a/client/client.go +++ b/client/client.go @@ -443,7 +443,7 @@ func FindSatisfyingRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) for _, a := range archs { psmDirect := make(map[string][]*goolib.PkgSpec) psmProvides := make(map[string][]*goolib.PkgSpec) - + for u, r := range rm { for _, p := range r.Packages { ps := p.PackageSpec @@ -487,7 +487,7 @@ func FindSatisfyingRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) } } } - + return nil, "", fmt.Errorf("no package found satisfying %s in any repo", name) } @@ -510,21 +510,21 @@ func satisfiesProvider(prov, reqName, reqVer string) bool { pName = prov[:i] pVer = prov[i+1:] } - + if pName != reqName { return false } - + if reqVer == "" { return true } - - // If provider is unversioned, it satisfies dependency? + + // If provider is unversioned, it satisfies dependency? // Arch says yes. if pVer == "" { return true } - + return satisfiesVersion(pVer, reqVer) } diff --git a/client/client_test.go b/client/client_test.go index cd8f3f2..f9f18e9 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -512,7 +512,7 @@ func TestFindSatisfyingRepoLatest_Priority(t *testing.T) { // direct match: version 1.0.0 // provider: version 2.0.0 (provides it) // direct match should win despite lower version. - + rm := RepoMap{ "repo1": Repo{ Priority: priority.Value(500), @@ -537,13 +537,13 @@ func TestFindSatisfyingRepoLatest_Priority(t *testing.T) { }, }, } - + pi := goolib.PackageInfo{Name: "real_pkg", Arch: "noarch"} spec, _, err := FindSatisfyingRepoLatest(pi, rm, []string{"noarch"}) if err != nil { t.Fatalf("FindSatisfyingRepoLatest failed: %v", err) } - + if spec.Name != "real_pkg" { t.Errorf("Expected direct match 'real_pkg', got '%s'", spec.Name) } diff --git a/install/install.go b/install/install.go index df18aab..45ee380 100644 --- a/install/install.go +++ b/install/install.go @@ -164,12 +164,12 @@ func installDeps(ctx context.Context, ps *goolib.PkgSpec, cache string, rm clien logger.Infof("Dependency met: %s.%s with version greater than %s installed or provided", pi.Name, pi.Arch, ver) continue } - + spec, repo, err := client.FindSatisfyingRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, rm, archs) if err != nil { return err } - + logger.Infof("Dependency found: %s.%s %s (provides %s) is available", spec.Name, spec.Arch, spec.Version, pi.Name) if err := FromRepo(ctx, goolib.PackageInfo{Name: spec.Name, Arch: spec.Arch, Ver: spec.Version}, repo, cache, rm, archs, dbOnly, downloader, db); err != nil { return err @@ -207,12 +207,12 @@ func FromRepo(ctx context.Context, pi goolib.PackageInfo, repo, cache string, rm // If "googet install virtual_pkg.noarch.1.0.0", FindRepoSpec won't find it if it's virtual. // So we SHOULD use FindSatisfyingRepoLatest (or similar) here too if exact match fails? // For now, let's keep original behavior for explicit version invocations unless we want to support "install virtual=1.0". - // Actually, let's try strict match first, if fail, try satisfying logic? + // Actually, let's try strict match first, if fail, try satisfying logic? // But FindRepoSpec takes a Repo struct, not RepoMap. // Let's stick to simple flow for now (installDeps usage). rs, err = client.FindRepoSpec(pi, rm[repo]) } - + if err != nil { return err } diff --git a/install/install_test.go b/install/install_test.go index 694d6d5..1aa85ef 100644 --- a/install/install_test.go +++ b/install/install_test.go @@ -406,7 +406,7 @@ func TestFromRepo_SatisfiedByProvider(t *testing.T) { // This is a more integration-level test to ensure installDeps uses isSatisfied. // We mock the DB state and call installDeps directly or via a wrapper if accessible. // installDeps is unexported, but we are in package install. - + settings.Initialize(t.TempDir(), false) state := []client.PackageState{ { @@ -476,7 +476,7 @@ func TestFromRepo_SatisfiedByUninstalledProvider(t *testing.T) { PkgDependencies: map[string]string{"libvirt": "1.0.0"}, } - // We pass a valid rm but nil downloader. + // We pass a valid rm but nil downloader. // installDeps -> FindSatisfyingRepoLatest (finds provider_pkg) -> FromRepo (provider_pkg) -> download... // Since downloader is nil, FromRepo might fail or panic if we go deep. // But FromRepo calls client.FindRepoSpec first. @@ -484,13 +484,13 @@ func TestFromRepo_SatisfiedByUninstalledProvider(t *testing.T) { // The best way is to catch the error and check if it's related to downloading "provider_pkg". // Or define a mock downloader if possible? client.Downloader is a struct, hard to mock methods. // However, FromRepo fails if downloader is nil probably. - + // We can't easily mock downloader without changing code significantly. // But we can verify it fails at download stage, NOT at resolution stage. - + downloader, _ := client.NewDownloader("") err = installDeps(nil, ps, "", rm, []string{"noarch"}, false, downloader, db) - + // We expect an error because download will fail (invalid URL/Source). if err == nil { t.Error("installDeps expected error, got nil") From 95a7be06874c1c0215eb9ca2f37496fd35f230fb Mon Sep 17 00:00:00 2001 From: John-Michael Mulesa Date: Tue, 3 Feb 2026 19:46:05 -0500 Subject: [PATCH 3/7] Update googet.goospec release for Provides. --- googet.goospec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/googet.goospec b/googet.goospec index d0a3739..fa262a4 100644 --- a/googet.goospec +++ b/googet.goospec @@ -1,4 +1,4 @@ -{{$version := "3.1.0@0" -}} +{{$version := "3.2.0@0" -}} { "name": "googet", "version": "{{$version}}", @@ -15,6 +15,7 @@ "path": "install.ps1" }, "releaseNotes": [ + "3.2.0 - Add Provides functionality and field to the GooGet PkgSpec.", "3.1.0 - Introduce a dry_run flag for update, install, and remove subcommands.", "3.0.0 - Replace googet state file with sqlite database. Add json output for installed command.", "2.21.0 - Add arm64 arch support.", From 5e9f66726276afffd70b0ef0c6ce4d34a7d61f82 Mon Sep 17 00:00:00 2001 From: John-Michael Mulesa Date: Wed, 4 Feb 2026 16:16:05 -0500 Subject: [PATCH 4/7] Refactor package dependency resolution and cleanup API. - Consolidate package search logic to support both direct and provider matches. - Update search functions to return full package specification. - Export provider satisfaction logic for reuse across packages. - Refactor install and cli packages to use updated client API. --- cli/install/install.go | 4 +- cli/latest/latest.go | 3 +- cli/update/update.go | 12 ++--- client/client.go | 99 +++++++++-------------------------------- client/client_test.go | 27 ++++++----- download/download.go | 4 +- install/install.go | 30 +++++-------- install/install_test.go | 20 ++------- 8 files changed, 64 insertions(+), 135 deletions(-) diff --git a/cli/install/install.go b/cli/install/install.go index 535a156..46083d6 100644 --- a/cli/install/install.go +++ b/cli/install/install.go @@ -185,9 +185,11 @@ func (i *installer) installFromRepo(ctx context.Context, name string, archs []st if pi.Ver == "" { var err error - if pi.Ver, _, pi.Arch, err = client.FindRepoLatest(pi, i.repoMap, archs); err != nil { + var spec *goolib.PkgSpec + if spec, _, pi.Arch, err = client.FindRepoLatest(pi, i.repoMap, archs); err != nil { return fmt.Errorf("can't resolve version for package %q: %v", pi.Name, err) } + pi.Ver = spec.Version } if _, err := goolib.ParseVersion(pi.Ver); err != nil { return fmt.Errorf("invalid package version %q: %v", pi.Ver, err) diff --git a/cli/latest/latest.go b/cli/latest/latest.go index b5d7c72..0830fbf 100644 --- a/cli/latest/latest.go +++ b/cli/latest/latest.go @@ -88,11 +88,12 @@ func (cmd *latestCmd) Execute(ctx context.Context, flags *flag.FlagSet, _ ...int } rm := downloader.AvailableVersions(ctx, repos, settings.CacheDir(), settings.CacheLife) - v, _, a, err := client.FindRepoLatest(pi, rm, settings.Archs) + spec, _, a, err := client.FindRepoLatest(pi, rm, settings.Archs) if err != nil { logger.Errorf("Failed to find package: %v", err) return subcommands.ExitFailure } + v := spec.Version if !cmd.compare { fmt.Println(v) return subcommands.ExitSuccess diff --git a/cli/update/update.go b/cli/update/update.go index 4e6fa0c..40a7359 100644 --- a/cli/update/update.go +++ b/cli/update/update.go @@ -122,13 +122,13 @@ func updates(pm client.PackageMap, rm client.RepoMap) []goolib.PackageInfo { var ud []goolib.PackageInfo for p, ver := range pm { pi := goolib.PkgNameSplit(p) - v, r, _, err := client.FindRepoLatest(pi, rm, settings.Archs) + spec, r, _, err := client.FindRepoLatest(pi, rm, settings.Archs) if err != nil { // This error is because this installed package is not available in a repo. logger.Info(err) continue } - c, err := goolib.ComparePriorityVersion(rm[r].Priority, v, priority.Default, ver) + c, err := goolib.ComparePriorityVersion(rm[r].Priority, spec.Version, priority.Default, ver) if err != nil { logger.Error(err) continue @@ -139,7 +139,7 @@ func updates(pm client.PackageMap, rm client.RepoMap) []goolib.PackageInfo { } // The versions might actually be the same even though the priorities are different, // so do another check to skip reinstall of the same version. - c, err = goolib.Compare(v, ver) + c, err = goolib.Compare(spec.Version, ver) if err != nil { logger.Error(err) continue @@ -152,9 +152,9 @@ func updates(pm client.PackageMap, rm client.RepoMap) []goolib.PackageInfo { if c == -1 { op = "Downgrade" } - fmt.Printf(" %s, %s --> %s from %s\n", p, ver, v, r) - logger.Infof("%s for package %s, %s installed and %s available from %s.", op, p, ver, v, r) - ud = append(ud, goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: v}) + fmt.Printf(" %s, %s --> %s from %s\n", p, ver, spec.Version, r) + logger.Infof("%s for package %s, %s installed and %s available from %s.", op, p, ver, spec.Version, r) + ud = append(ud, goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: spec.Version}) } return ud } diff --git a/client/client.go b/client/client.go index 349b32d..f32a8c9 100644 --- a/client/client.go +++ b/client/client.go @@ -378,62 +378,38 @@ func FindRepoSpec(pi goolib.PackageInfo, repo Repo) (goolib.RepoSpec, error) { return goolib.RepoSpec{}, fmt.Errorf("no match found for package %s.%s.%s in repo", pi.Name, pi.Arch, pi.Ver) } -// latest returns the version and repo having the greatest (priority, version) from the set of +// latest returns the package spec and repo having the greatest (priority, version) from the set of // package specs in psm. -func latest(psm map[string][]*goolib.PkgSpec, rm RepoMap) (string, string) { - var ver, repoURL string +func latest(psm map[string][]*goolib.PkgSpec, rm RepoMap) (*goolib.PkgSpec, string) { + var bestPkg *goolib.PkgSpec + var repoURL string var pri priority.Value for u, pl := range psm { for _, pkg := range pl { q := rm[u].Priority c := 1 - if ver != "" { + if bestPkg != nil { var err error - if c, err = goolib.ComparePriorityVersion(q, pkg.Version, pri, ver); err != nil { - logger.Errorf("compare of %s to %s failed with error: %v", pkg.Version, ver, err) + if c, err = goolib.ComparePriorityVersion(q, pkg.Version, pri, bestPkg.Version); err != nil { + logger.Errorf("compare of %s to %s failed with error: %v", pkg.Version, bestPkg.Version, err) continue } } if c == 1 { repoURL = u - ver = pkg.Version + bestPkg = pkg pri = q } } } - return ver, repoURL + return bestPkg, repoURL } // FindRepoLatest returns the latest version of a package along with its repo and arch. +// It checks both direct name matches and "Provides" entries. // The archs are searched in order; if a matching package is found for any arch, it is // returned immediately even if a later arch might have a later version. -func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (string, string, string, error) { - psm := make(map[string][]*goolib.PkgSpec) - name := pi.Name - if pi.Arch != "" { - archs = []string{pi.Arch} - name = fmt.Sprintf("%s.%s", pi.Name, pi.Arch) - } - for _, a := range archs { - for u, r := range rm { - for _, p := range r.Packages { - if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == a { - psm[u] = append(psm[u], p.PackageSpec) - } - } - } - if len(psm) != 0 { - v, r := latest(psm, rm) - return v, r, a, nil - } - } - return "", "", "", fmt.Errorf("no versions of package %s found in any repo", name) -} - -// FindSatisfyingRepoLatest returns the latest version of a package that satisfies the package info, -// along with its repo. It checks both direct name matches and "Provides" entries. -// The archs are searched in order. -func FindSatisfyingRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (*goolib.PkgSpec, string, error) { +func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (*goolib.PkgSpec, string, string, error) { name := pi.Name if pi.Arch != "" { archs = []string{pi.Arch} @@ -456,13 +432,13 @@ func FindSatisfyingRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) if satisfiesVersion(ps.Version, pi.Ver) { psmDirect[u] = append(psmDirect[u], ps) } - // If exact match, we don't check provides for THIS package. + // Skip checking Provides if the package itself is a direct match. continue } // Check provides for _, prov := range ps.Provides { - if satisfiesProvider(prov, pi.Name, pi.Ver) { + if SatisfiesProvider(prov, pi.Name, pi.Ver) { psmProvides[u] = append(psmProvides[u], ps) break } @@ -470,25 +446,24 @@ func FindSatisfyingRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) } } - // If direct matches exist, use ONLY them. + // Prioritize direct package matches over virtual package providers. if len(psmDirect) > 0 { - pkg, repo := pickBest(psmDirect, rm) + pkg, repo := latest(psmDirect, rm) if pkg != nil { - return pkg, repo, nil + return pkg, repo, a, nil } } // If no direct matches, check providers. // Note: This matches Arch behavior (prefer real package). if len(psmProvides) > 0 { - pkg, repo := pickBest(psmProvides, rm) + pkg, repo := latest(psmProvides, rm) if pkg != nil { - return pkg, repo, nil + return pkg, repo, a, nil } } } - - return nil, "", fmt.Errorf("no package found satisfying %s in any repo", name) + return nil, "", "", fmt.Errorf("no package found satisfying %s in any repo", name) } func satisfiesVersion(pkgVer, reqVer string) bool { @@ -503,7 +478,8 @@ func satisfiesVersion(pkgVer, reqVer string) bool { return c >= 0 } -func satisfiesProvider(prov, reqName, reqVer string) bool { +// SatisfiesProvider checks if a provider string satisfies a requirement. +func SatisfiesProvider(prov, reqName, reqVer string) bool { pName := prov pVer := "" if i := strings.Index(prov, "="); i != -1 { @@ -515,12 +491,7 @@ func satisfiesProvider(prov, reqName, reqVer string) bool { return false } - if reqVer == "" { - return true - } - - // If provider is unversioned, it satisfies dependency? - // Arch says yes. + // Unversioned providers satisfy any version requirement. if pVer == "" { return true } @@ -528,32 +499,6 @@ func satisfiesProvider(prov, reqName, reqVer string) bool { return satisfiesVersion(pVer, reqVer) } -func pickBest(psm map[string][]*goolib.PkgSpec, rm RepoMap) (*goolib.PkgSpec, string) { - var bestPkg *goolib.PkgSpec - var repoURL string - var pri priority.Value - - for u, pl := range psm { - for _, pkg := range pl { - q := rm[u].Priority - c := 1 - if bestPkg != nil { - var err error - if c, err = goolib.ComparePriorityVersion(q, pkg.Version, pri, bestPkg.Version); err != nil { - logger.Errorf("compare of %s to %s failed with error: %v", pkg.Version, bestPkg.Version, err) - continue - } - } - if c == 1 { - repoURL = u - bestPkg = pkg - pri = q - } - } - } - return bestPkg, repoURL -} - // WhatRepo returns what repo a package is in. // Name, Arch, and Ver fields of PackageInfo must be provided. func WhatRepo(pi goolib.PackageInfo, rm RepoMap) (string, error) { diff --git a/client/client_test.go b/client/client_test.go index f9f18e9..7a134c6 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -236,14 +236,17 @@ func TestFindRepoLatest(t *testing.T) { }, } { t.Run(tt.desc, func(t *testing.T) { - gotVersion, gotRepo, gotArch, err := FindRepoLatest(tt.pi, tt.rm, tt.archs) + gotSpec, gotRepo, gotArch, err := FindRepoLatest(tt.pi, tt.rm, tt.archs) if err != nil && !tt.wantErr { t.Fatalf("FindRepoLatest(%v, %v, %v) failed: %v", tt.pi, tt.rm, tt.archs, err) } else if err == nil && tt.wantErr { t.Fatalf("FindRepoLatest(%v, %v, %v) got nil error, wanted non-nil", tt.pi, tt.rm, tt.archs) } - if gotVersion != tt.wantVersion { - t.Errorf("FindRepoLatest(%v, %v, %v) got version: %q, want %q", tt.pi, tt.rm, tt.archs, gotVersion, tt.wantVersion) + if err != nil { + return + } + if gotSpec.Version != tt.wantVersion { + t.Errorf("FindRepoLatest(%v, %v, %v) got version: %q, want %q", tt.pi, tt.rm, tt.archs, gotSpec.Version, tt.wantVersion) } if gotArch != tt.wantArch { t.Errorf("FindRepoLatest(%v, %v, %v) got arch: %q, want %q", tt.pi, tt.rm, tt.archs, gotArch, tt.wantArch) @@ -411,7 +414,7 @@ func TestFindRepoSpecNoMatch(t *testing.T) { } } -func TestFindSatisfyingRepoLatest(t *testing.T) { +func TestFindRepoLatest_Provides(t *testing.T) { rm := RepoMap{ "repo1": Repo{ Priority: priority.Value(500), @@ -487,27 +490,27 @@ func TestFindSatisfyingRepoLatest(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - spec, _, err := FindSatisfyingRepoLatest(tt.pi, rm, []string{"noarch"}) + spec, _, _, err := FindRepoLatest(tt.pi, rm, []string{"noarch"}) if tt.wantError { if err == nil { - t.Errorf("FindSatisfyingRepoLatest(%v) wanted error, got nil", tt.pi) + t.Errorf("FindRepoLatest(%v) wanted error, got nil", tt.pi) } return } if err != nil { - t.Fatalf("FindSatisfyingRepoLatest(%v) unexpected error: %v", tt.pi, err) + t.Fatalf("FindRepoLatest(%v) unexpected error: %v", tt.pi, err) } if spec.Name != tt.wantName { - t.Errorf("FindSatisfyingRepoLatest(%v) name = %q, want %q", tt.pi, spec.Name, tt.wantName) + t.Errorf("FindRepoLatest(%v) name = %q, want %q", tt.pi, spec.Name, tt.wantName) } if spec.Version != tt.wantVer { // Simplified check, assumes simple version strings in test - t.Errorf("FindSatisfyingRepoLatest(%v) version = %q, want %q", tt.pi, spec.Version, tt.wantVer) + t.Errorf("FindRepoLatest(%v) version = %q, want %q", tt.pi, spec.Version, tt.wantVer) } }) } } -func TestFindSatisfyingRepoLatest_Priority(t *testing.T) { +func TestFindRepoLatest_Priority(t *testing.T) { // Setup repo with both direct match and provider. // direct match: version 1.0.0 // provider: version 2.0.0 (provides it) @@ -539,9 +542,9 @@ func TestFindSatisfyingRepoLatest_Priority(t *testing.T) { } pi := goolib.PackageInfo{Name: "real_pkg", Arch: "noarch"} - spec, _, err := FindSatisfyingRepoLatest(pi, rm, []string{"noarch"}) + spec, _, _, err := FindRepoLatest(pi, rm, []string{"noarch"}) if err != nil { - t.Fatalf("FindSatisfyingRepoLatest failed: %v", err) + t.Fatalf("FindRepoLatest failed: %v", err) } if spec.Name != "real_pkg" { diff --git a/download/download.go b/download/download.go index 97d24fe..6c8b2b9 100644 --- a/download/download.go +++ b/download/download.go @@ -151,11 +151,11 @@ func FromRepo(ctx context.Context, rs goolib.RepoSpec, repo, dir string, downloa // Latest downloads the latest available version of a package. func Latest(ctx context.Context, name, dir string, rm client.RepoMap, archs []string, downloader *client.Downloader) (string, string, error) { - ver, repo, arch, err := client.FindRepoLatest(goolib.PackageInfo{Name: name, Arch: "", Ver: ""}, rm, archs) + spec, repo, arch, err := client.FindRepoLatest(goolib.PackageInfo{Name: name, Arch: "", Ver: ""}, rm, archs) if err != nil { return "", "", err } - rs, err := client.FindRepoSpec(goolib.PackageInfo{Name: name, Arch: arch, Ver: ver}, rm[repo]) + rs, err := client.FindRepoSpec(goolib.PackageInfo{Name: name, Arch: arch, Ver: spec.Version}, rm[repo]) if err != nil { return "", "", err } diff --git a/install/install.go b/install/install.go index 45ee380..299af24 100644 --- a/install/install.go +++ b/install/install.go @@ -165,7 +165,7 @@ func installDeps(ctx context.Context, ps *goolib.PkgSpec, cache string, rm clien continue } - spec, repo, err := client.FindSatisfyingRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, rm, archs) + spec, repo, _, err := client.FindRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, rm, archs) if err != nil { return err } @@ -184,32 +184,22 @@ func FromRepo(ctx context.Context, pi goolib.PackageInfo, repo, cache string, rm fmt.Printf("Installing %s.%s.%s and dependencies...\n", pi.Name, pi.Arch, pi.Ver) var rs goolib.RepoSpec var err error - // If the version is empty, we need to find the latest version. - // We also try FindSatisfyingRepoLatest to handle providers if finding by exact name fails or if we want latest compatible. - // But FromRepo is typically called with a specific version (from installDeps or user input). - // If called with specific version, we usually want THAT version. - // Users might run "googet install virtual_pkg". In that case pi.Ver might be empty. + // If no version is specified, resolve the latest version handling both + // direct matches and providers. if pi.Ver == "" { - spec, repoURL, err := client.FindSatisfyingRepoLatest(pi, rm, archs) + spec, repoURL, _, err := client.FindRepoLatest(pi, rm, archs) if err != nil { return err } repo = repoURL - // We found a satisfying package using the new logic. Use its real name and version. - // NOTE: This might switch the name from a virtual one to the real one. + // Use the resolved real package name and version. pi.Name = spec.Name pi.Arch = spec.Arch pi.Ver = spec.Version rs, err = client.FindRepoSpec(goolib.PackageInfo{Name: spec.Name, Arch: spec.Arch, Ver: spec.Version}, rm[repo]) } else { - // Even if name is virtual, if version is specified, we might need to resolve it? - // But existing FindRepoSpec relies on exact name match inside the repo object. - // If "googet install virtual_pkg.noarch.1.0.0", FindRepoSpec won't find it if it's virtual. - // So we SHOULD use FindSatisfyingRepoLatest (or similar) here too if exact match fails? - // For now, let's keep original behavior for explicit version invocations unless we want to support "install virtual=1.0". - // Actually, let's try strict match first, if fail, try satisfying logic? - // But FindRepoSpec takes a Repo struct, not RepoMap. - // Let's stick to simple flow for now (installDeps usage). + // When a specific version is requested, look for an exact match in the repository. + // Virtual package resolution is not currently supported for specific versions. rs, err = client.FindRepoSpec(pi, rm[repo]) } @@ -589,19 +579,19 @@ func listDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, dl []goolib dl = append(dl, pi) for d, v := range rs.PackageSpec.PkgDependencies { di := goolib.PkgNameSplit(d) - ver, repo, arch, err := client.FindRepoLatest(di, rm, archs) + spec, repo, arch, err := client.FindRepoLatest(di, rm, archs) di.Arch = arch if err != nil { return nil, fmt.Errorf("cannot resolve dependency %s.%s.%s: %v", di.Name, di.Arch, di.Ver, err) } - c, err := goolib.Compare(ver, v) + c, err := goolib.Compare(spec.Version, v) if err != nil { return nil, err } if c == -1 { return nil, fmt.Errorf("cannot resolve dependency, %s.%s version %s or greater not installed and not available in any repo", pi.Name, pi.Arch, pi.Ver) } - di.Ver = ver + di.Ver = spec.Version dl, err = listDeps(di, rm, repo, dl, archs) if err != nil { return nil, err diff --git a/install/install_test.go b/install/install_test.go index 1aa85ef..b862784 100644 --- a/install/install_test.go +++ b/install/install_test.go @@ -476,18 +476,8 @@ func TestFromRepo_SatisfiedByUninstalledProvider(t *testing.T) { PkgDependencies: map[string]string{"libvirt": "1.0.0"}, } - // We pass a valid rm but nil downloader. - // installDeps -> FindSatisfyingRepoLatest (finds provider_pkg) -> FromRepo (provider_pkg) -> download... - // Since downloader is nil, FromRepo might fail or panic if we go deep. - // But FromRepo calls client.FindRepoSpec first. - // We want to verify it TRIES to install provider_pkg. - // The best way is to catch the error and check if it's related to downloading "provider_pkg". - // Or define a mock downloader if possible? client.Downloader is a struct, hard to mock methods. - // However, FromRepo fails if downloader is nil probably. - - // We can't easily mock downloader without changing code significantly. - // But we can verify it fails at download stage, NOT at resolution stage. - + // We pass a valid rm but nil downloader to verify that resolution succeeds (finding provider_pkg) + // but download fails. If resolution failed, we'd get a "cannot resolve dependency" error. downloader, _ := client.NewDownloader("") err = installDeps(nil, ps, "", rm, []string{"noarch"}, false, downloader, db) @@ -495,10 +485,8 @@ func TestFromRepo_SatisfiedByUninstalledProvider(t *testing.T) { if err == nil { t.Error("installDeps expected error, got nil") } else { - // If resolution failed, it would say "cannot resolve dependency". - // If resolution succeeded, it proceeds to download and fails there. - // "unsupported protocol scheme" or "client.Get" error etc. - // Or "no source specified" + // Verify that the error is not a resolution error. + // Any other error implies resolution succeeded and it failed at the download stage. errMsg := err.Error() if errMsg == "cannot resolve dependency, libvirt.noarch version 1.0.0 or greater not installed and not available in any repo" { t.Errorf("installDeps failed to resolve provider: %v", err) From d76d9bdce34dcd6f24f2f7f04c96bf6c5db135ef Mon Sep 17 00:00:00 2001 From: John-Michael Mulesa Date: Wed, 4 Feb 2026 16:19:42 -0500 Subject: [PATCH 5/7] Remove system_darwin.go for a future PR. --- system/system_darwin.go | 86 ----------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 system/system_darwin.go diff --git a/system/system_darwin.go b/system/system_darwin.go deleted file mode 100644 index 2e17f5c..0000000 --- a/system/system_darwin.go +++ /dev/null @@ -1,86 +0,0 @@ -//go:build darwin -// +build darwin - -/* -Copyright 2016 Google Inc. All Rights Reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package system - -import ( - "fmt" - "os" - "strconv" - "syscall" - - "github.com/google/googet/v2/client" - "github.com/google/googet/v2/goolib" -) - -// Install performs a system specfic install given a package extraction directory and a PkgSpec struct. -func Install(dir string, ps *goolib.PkgSpec) error { - return nil -} - -// Uninstall performs a system specfic uninstall given a package extraction directory and a PkgSpec struct. -func Uninstall(dir string, ps *client.PackageState) error { - return nil -} - -// InstallableArchs returns a slice of archs supported by this machine. -func InstallableArchs() ([]string, error) { - return []string{"noarch", "x86_64", "arm64"}, nil -} - -// AppAssociation returns empty strings and is a stub of the Windows implementation. -func AppAssociation(ps *goolib.PkgSpec, installSource string) (string, string) { - return "", "" -} - -// IsAdmin returns nil and is a stub of the Windows implementation -func IsAdmin() error { - return nil -} - -// isGooGetRunning checks if the process with the given PID is running and is a googet process. -func isGooGetRunning(pid int) (bool, error) { - // Stub for Darwin, assuming not running to avoid complexity with ps or sysctl - return false, nil -} - -// lock attempts to obtain an exclusive lock on the provided file. -func lock(f *os.File) (func(), error) { - if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { - return nil, err - } - cleanup := func() { - syscall.Flock(int(f.Fd()), syscall.LOCK_UN) - f.Close() - os.Remove(f.Name()) - } - - if err := f.Truncate(0); err != nil { - cleanup() - return nil, fmt.Errorf("failed to truncate lockfile: %v", err) - } - if _, err := f.WriteString(strconv.Itoa(os.Getpid())); err != nil { - cleanup() - return nil, fmt.Errorf("failed to write PID to lockfile: %v", err) - } - - // Downgrade to shared lock so that other processes can read the PID. - if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil { - cleanup() - return nil, fmt.Errorf("failed to downgrade to shared lock: %v", err) - } - return cleanup, nil -} From 3d2e905326150d5e225c6431c70fc620bcb4ed28 Mon Sep 17 00:00:00 2001 From: John-Michael Mulesa Date: Wed, 4 Feb 2026 18:48:47 -0500 Subject: [PATCH 6/7] Refactor provider satisfaction logic. - Use strings.Cut in client.SatisfiesProvider and simplify version checks - Deduplicate provider logic in install package by reusing client.SatisfiesProvider --- client/client.go | 14 ++------------ install/install.go | 24 ++---------------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/client/client.go b/client/client.go index f32a8c9..b241178 100644 --- a/client/client.go +++ b/client/client.go @@ -467,7 +467,7 @@ func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (*goolib. } func satisfiesVersion(pkgVer, reqVer string) bool { - if reqVer == "" { + if reqVer == "" || pkgVer == "" { return true } c, err := goolib.Compare(pkgVer, reqVer) @@ -480,22 +480,12 @@ func satisfiesVersion(pkgVer, reqVer string) bool { // SatisfiesProvider checks if a provider string satisfies a requirement. func SatisfiesProvider(prov, reqName, reqVer string) bool { - pName := prov - pVer := "" - if i := strings.Index(prov, "="); i != -1 { - pName = prov[:i] - pVer = prov[i+1:] - } + pName, pVer, _ := strings.Cut(prov, "=") if pName != reqName { return false } - // Unversioned providers satisfy any version requirement. - if pVer == "" { - return true - } - return satisfiesVersion(pVer, reqVer) } diff --git a/install/install.go b/install/install.go index 299af24..eab1bf2 100644 --- a/install/install.go +++ b/install/install.go @@ -78,28 +78,8 @@ func isSatisfied(pi goolib.PackageInfo, db *googetdb.GooDB) (bool, error) { continue } for _, prov := range p.PackageSpec.Provides { - pName := prov - pVer := "" - // We support "name=version" format for providers. - if i := strings.Index(prov, "="); i != -1 { - pName = prov[:i] - pVer = prov[i+1:] - } - - if pName == pi.Name { - // If we just need the name, or if the provider has no version (implies all versions), - // or if the provider version satisfies the requirement. - if pi.Ver == "" || pVer == "" { - return true, nil - } - c, err := goolib.Compare(pVer, pi.Ver) - if err != nil { - logger.Errorf("Error comparing versions for provider %s: %v", prov, err) - continue - } - if c > -1 { - return true, nil - } + if client.SatisfiesProvider(prov, pi.Name, pi.Ver) { + return true, nil } } } From f1d69f291f19f559af27e62f29d1b91caa7bccbc Mon Sep 17 00:00:00 2001 From: John-Michael Mulesa Date: Thu, 5 Feb 2026 18:38:45 -0500 Subject: [PATCH 7/7] Cleanup fixes. --- client/client.go | 6 +----- install/install_test.go | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/client/client.go b/client/client.go index b241178..c8ab0e6 100644 --- a/client/client.go +++ b/client/client.go @@ -482,11 +482,7 @@ func satisfiesVersion(pkgVer, reqVer string) bool { func SatisfiesProvider(prov, reqName, reqVer string) bool { pName, pVer, _ := strings.Cut(prov, "=") - if pName != reqName { - return false - } - - return satisfiesVersion(pVer, reqVer) + return pName == reqName && satisfiesVersion(pVer, reqVer) } // WhatRepo returns what repo a package is in. diff --git a/install/install_test.go b/install/install_test.go index b862784..3561ef2 100644 --- a/install/install_test.go +++ b/install/install_test.go @@ -476,8 +476,8 @@ func TestFromRepo_SatisfiedByUninstalledProvider(t *testing.T) { PkgDependencies: map[string]string{"libvirt": "1.0.0"}, } - // We pass a valid rm but nil downloader to verify that resolution succeeds (finding provider_pkg) - // but download fails. If resolution failed, we'd get a "cannot resolve dependency" error. + // Verify that dependency resolution succeeds (finding provider_pkg); the download + // is expected to fail due to an invalid repository URL. downloader, _ := client.NewDownloader("") err = installDeps(nil, ps, "", rm, []string{"noarch"}, false, downloader, db)