Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cli/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,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)
Expand Down
3 changes: 2 additions & 1 deletion cli/latest/latest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions cli/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,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
Expand All @@ -146,7 +146,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
Expand All @@ -159,9 +159,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
}
87 changes: 71 additions & 16 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,56 +378,111 @@ 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)
func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (*goolib.PkgSpec, string, 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 {
if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == a {
psm[u] = append(psm[u], p.PackageSpec)
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)
}
// 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) {
psmProvides[u] = append(psmProvides[u], ps)
break
}
}
}
}
if len(psm) != 0 {
v, r := latest(psm, rm)
return v, r, a, nil

// Prioritize direct package matches over virtual package providers.
if len(psmDirect) > 0 {
pkg, repo := latest(psmDirect, rm)
if pkg != 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 := latest(psmProvides, rm)
if pkg != nil {
return pkg, repo, a, nil
}
}
}
return nil, "", "", fmt.Errorf("no package found satisfying %s in any repo", name)
}

func satisfiesVersion(pkgVer, reqVer string) bool {
if reqVer == "" || pkgVer == "" {
return true
}
return "", "", "", fmt.Errorf("no versions of package %s found in any repo", name)
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
}

// SatisfiesProvider checks if a provider string satisfies a requirement.
func SatisfiesProvider(prov, reqName, reqVer string) bool {
pName, pVer, _ := strings.Cut(prov, "=")

return pName == reqName && satisfiesVersion(pVer, reqVer)
}

// WhatRepo returns what repo a package is in.
Expand Down
151 changes: 148 additions & 3 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -235,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)
Expand Down Expand Up @@ -409,3 +413,144 @@ func TestFindRepoSpecNoMatch(t *testing.T) {
t.Error("did not get expected error when running FindRepoSpec")
}
}

func TestFindRepoLatest_Provides(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 := FindRepoLatest(tt.pi, rm, []string{"noarch"})
if tt.wantError {
if err == nil {
t.Errorf("FindRepoLatest(%v) wanted error, got nil", tt.pi)
}
return
}
if err != nil {
t.Fatalf("FindRepoLatest(%v) unexpected error: %v", tt.pi, err)
}
if 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("FindRepoLatest(%v) version = %q, want %q", tt.pi, spec.Version, tt.wantVer)
}
})
}
}

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)
// 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 := FindRepoLatest(pi, rm, []string{"noarch"})
if err != nil {
t.Fatalf("FindRepoLatest 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)
}
}
4 changes: 2 additions & 2 deletions download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion googet.goospec
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{$version := "3.1.0@0" -}}
{{$version := "3.2.0@0" -}}
{
"name": "googet",
"version": "{{$version}}",
Expand All @@ -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.",
Expand Down
Loading
Loading