diff --git a/cli/install/install.go b/cli/install/install.go index b6c53b4..b0e4000 100644 --- a/cli/install/install.go +++ b/cli/install/install.go @@ -198,7 +198,11 @@ func (i *installer) installFromRepo(ctx context.Context, name string, archs []st if pi.Ver == "" { var err error var spec *goolib.PkgSpec - if spec, _, pi.Arch, err = client.FindRepoLatest(pi, i.repoMap, archs); err != nil { + installedArch, isLocked, _, _, err := i.db.InstalledLockState(pi.Name) + if err != nil { + logger.Infof("Error fetching installed package state: %v, proceeding without lock", err) + } + if spec, _, pi.Arch, err = client.FindRepoLatest(pi, i.repoMap, archs, installedArch, isLocked); err != nil { return fmt.Errorf("can't resolve version for package %q: %v", pi.Name, err) } pi.Ver = spec.Version @@ -262,7 +266,7 @@ func (i *installer) reinstall(ctx context.Context, pi goolib.PackageInfo, ps cli } func (i *installer) enumerateDeps(pi goolib.PackageInfo, r string, archs []string, dryRun bool) (*bytes.Buffer, error) { - dl, err := install.ListDeps(pi, i.repoMap, r, archs) + dl, err := install.ListDeps(pi, i.repoMap, r, archs, i.db) if err != nil { return nil, fmt.Errorf("error listing dependencies for %s.%s.%s: %v", pi.Name, pi.Arch, pi.Ver, err) } diff --git a/cli/latest/latest.go b/cli/latest/latest.go index 0830fbf..2735f4c 100644 --- a/cli/latest/latest.go +++ b/cli/latest/latest.go @@ -87,8 +87,28 @@ func (cmd *latestCmd) Execute(ctx context.Context, flags *flag.FlagSet, _ ...int return subcommands.ExitFailure } + var installedArch string + var isLocked bool + var ver string + var pkgFound bool + + if cmd.compare { + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + logger.Errorf("Failed to open database: %v", err) + return subcommands.ExitFailure + } + defer db.Close() + + installedArch, isLocked, ver, pkgFound, err = db.InstalledLockState(pi.Name) + if err != nil { + logger.Errorf("Failed fetching installed package state: %v", err) + return subcommands.ExitFailure + } + } + rm := downloader.AvailableVersions(ctx, repos, settings.CacheDir(), settings.CacheLife) - spec, _, a, err := client.FindRepoLatest(pi, rm, settings.Archs) + spec, _, a, err := client.FindRepoLatest(pi, rm, settings.Archs, installedArch, isLocked) if err != nil { logger.Errorf("Failed to find package: %v", err) return subcommands.ExitFailure @@ -98,29 +118,7 @@ func (cmd *latestCmd) Execute(ctx context.Context, flags *flag.FlagSet, _ ...int fmt.Println(v) return subcommands.ExitSuccess } - - db, err := googetdb.NewDB(settings.DBFile()) - if err != nil { - logger.Errorf("Failed to open database: %v", err) - return subcommands.ExitFailure - } - defer db.Close() - - state, err := db.FetchPkgs("") - if err != nil { - logger.Errorf("Failed fetching installed packages: %v", err) - return subcommands.ExitFailure - } pi.Arch = a - var ver string - pkgFound := false - for _, p := range state { - if p.Match(pi) { - ver = p.PackageSpec.Version - pkgFound = true - break - } - } status := packageStatus{ PackageName: pi.Name, diff --git a/cli/update/update.go b/cli/update/update.go index 1883e4f..e4e42e2 100644 --- a/cli/update/update.go +++ b/cli/update/update.go @@ -92,7 +92,7 @@ func (cmd *updateCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interfa } rm := downloader.AvailableVersions(ctx, repos, cache, settings.CacheLife) - ud := updates(state.PackageMap(), rm) + ud := updates(state, rm) if ud == nil { fmt.Println("No updates available for any installed packages.") return subcommands.ExitSuccess @@ -126,43 +126,46 @@ func (cmd *updateCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interfa return exitCode } -func updates(pm client.PackageMap, rm client.RepoMap) []goolib.PackageInfo { +func updates(state client.GooGetState, rm client.RepoMap) []goolib.PackageInfo { fmt.Println("Searching for available updates...") var ud []goolib.PackageInfo - for p, ver := range pm { - pi := goolib.PkgNameSplit(p) - spec, r, _, err := client.FindRepoLatest(pi, rm, settings.Archs) + for _, p := range state { + if p.PackageSpec == nil { + continue + } + pi := goolib.PackageInfo{Name: p.PackageSpec.Name, Arch: p.PackageSpec.Arch} + spec, r, _, err := client.FindRepoLatest(pi, rm, settings.Archs, p.PackageSpec.Arch, p.PackageSpec.LockArch) 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, spec.Version, priority.Default, ver) + c, err := goolib.ComparePriorityVersion(rm[r].Priority, spec.Version, priority.Default, p.PackageSpec.Version) if err != nil { logger.Error(err) continue } if c < 1 { - logger.Infof("%s - highest priority version already installed", p) + logger.Infof("%s.%s - highest priority version already installed", p.PackageSpec.Name, p.PackageSpec.Arch) continue } // 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(spec.Version, ver) + c, err = goolib.Compare(spec.Version, p.PackageSpec.Version) if err != nil { logger.Error(err) continue } if c == 0 { - logger.Infof("%s - same version installed", p) + logger.Infof("%s.%s - same version installed", p.PackageSpec.Name, p.PackageSpec.Arch) continue } op := "Upgrade" if c == -1 { op = "Downgrade" } - 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) + fmt.Printf(" %s.%s, %s --> %s from %s\n", p.PackageSpec.Name, p.PackageSpec.Arch, p.PackageSpec.Version, spec.Version, r) + logger.Infof("%s for package %s.%s, %s installed and %s available from %s.", op, p.PackageSpec.Name, p.PackageSpec.Arch, p.PackageSpec.Version, spec.Version, r) ud = append(ud, goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: spec.Version}) } return ud diff --git a/cli/update/update_test.go b/cli/update/update_test.go index 48c1476..d5e654e 100644 --- a/cli/update/update_test.go +++ b/cli/update/update_test.go @@ -29,16 +29,16 @@ func captureStdout(f func()) string { func TestUpdates(t *testing.T) { for _, tc := range []struct { - name string - pm client.PackageMap - rm client.RepoMap - want []goolib.PackageInfo + name string + state client.GooGetState + rm client.RepoMap + want []goolib.PackageInfo }{ { name: "upgrade to later version", - pm: client.PackageMap{ - "foo.x86_32": "1.0", - "bar.x86_32": "2.0", + state: client.GooGetState{ + {PackageSpec: &goolib.PkgSpec{Name: "foo", Version: "1.0", Arch: "x86_32"}}, + {PackageSpec: &goolib.PkgSpec{Name: "bar", Version: "2.0", Arch: "x86_32"}}, }, rm: client.RepoMap{ "stable": client.Repo{ @@ -53,9 +53,9 @@ func TestUpdates(t *testing.T) { }, { name: "rollback to earlier version", - pm: client.PackageMap{ - "foo.x86_32": "2.0", - "bar.x86_32": "2.0", + state: client.GooGetState{ + {PackageSpec: &goolib.PkgSpec{Name: "foo", Version: "2.0", Arch: "x86_32"}}, + {PackageSpec: &goolib.PkgSpec{Name: "bar", Version: "2.0", Arch: "x86_32"}}, }, rm: client.RepoMap{ "stable": client.Repo{ @@ -76,8 +76,8 @@ func TestUpdates(t *testing.T) { }, { name: "no change if rollback version already installed", - pm: client.PackageMap{ - "foo.x86_32": "1.0", + state: client.GooGetState{ + {PackageSpec: &goolib.PkgSpec{Name: "foo", Version: "1.0", Arch: "x86_32"}}, }, rm: client.RepoMap{ "stable": client.Repo{ @@ -98,7 +98,9 @@ func TestUpdates(t *testing.T) { }, { name: "no updates available", - pm: client.PackageMap{"foo.x86_32": "1.0"}, + state: client.GooGetState{ + {PackageSpec: &goolib.PkgSpec{Name: "foo", Version: "1.0", Arch: "x86_32"}}, + }, rm: client.RepoMap{ "stable": client.Repo{ Priority: priority.Default, @@ -116,11 +118,11 @@ func TestUpdates(t *testing.T) { var pi []goolib.PackageInfo captureStdout(func() { - pi = updates(tc.pm, tc.rm) + pi = updates(tc.state, tc.rm) }) if diff := cmp.Diff(pi, tc.want); diff != "" { - t.Errorf("updates(%v, %v) got unexpected diff (-got +want):\n%v", tc.pm, tc.rm, diff) + t.Errorf("updates(%v, %v) got unexpected diff (-got +want):\n%v", tc.state, tc.rm, diff) } }) } diff --git a/client/client.go b/client/client.go index c8ab0e6..80ec7ba 100644 --- a/client/client.go +++ b/client/client.go @@ -27,6 +27,7 @@ import ( "net/url" "os" "path/filepath" + "slices" "strings" "time" @@ -357,8 +358,7 @@ func decode(index io.ReadCloser, ct, url, cf string) ([]goolib.RepoSpec, error) return nil, err } - // The .url files aren't used by googet but help developers and the - // curious figure out which file belongs to which repo/URL. + // The .url files aren't used by googet but help identify which file belongs to which repo/URL. mf := fmt.Sprintf("%s.url", strings.TrimSuffix(cf, filepath.Ext(cf))) if err = ioutil.WriteFile(mf, []byte(url), 0644); err != nil { logger.Errorf("Failed to write '%s': %v", mf, err) @@ -407,62 +407,96 @@ func latest(psm map[string][]*goolib.PkgSpec, rm RepoMap) (*goolib.PkgSpec, stri // 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) (*goolib.PkgSpec, string, string, error) { +// The search order is: +// 1. Repo Priority (High > Low) +// 2. Version (New > Old) +// 3. Architecture Preference (as defined by archs slice order) +func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string, installedArch string, isLocked bool) (*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) + archPref := make(map[string]int) + for i, a := range archs { + archPref[a] = i + } - for u, r := range rm { - for _, p := range r.Packages { - ps := p.PackageSpec - if ps.Arch != a { - continue - } + type candidate struct { + spec *goolib.PkgSpec + repo string + priority priority.Value + } + var directCandidates []candidate + var providesCandidates []candidate - // 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 + for u, r := range rm { + for _, p := range r.Packages { + ps := p.PackageSpec + + if _, ok := archPref[ps.Arch]; !ok { + continue + } + + if ps.Name == pi.Name { + if satisfiesVersion(ps.Version, pi.Ver) { + directCandidates = append(directCandidates, candidate{ps, u, r.Priority}) } + continue + } - // Check provides - for _, prov := range ps.Provides { - if SatisfiesProvider(prov, pi.Name, pi.Ver) { - psmProvides[u] = append(psmProvides[u], ps) - break - } + for _, prov := range ps.Provides { + if SatisfiesProvider(prov, pi.Name, pi.Ver) { + providesCandidates = append(providesCandidates, candidate{ps, u, r.Priority}) + break } } } + } - // 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 - } + cmpFunc := func(a, b candidate) int { + c, err := goolib.ComparePriorityVersion(a.priority, a.spec.Version, b.priority, b.spec.Version) + if err != nil { + logger.Errorf("Error comparing priority/version: %v", err) + return 0 + } + if c != 0 { + return -c // reverse for descending order + } + if archPref[a.spec.Arch] < archPref[b.spec.Arch] { + return -1 } + if archPref[a.spec.Arch] > archPref[b.spec.Arch] { + return 1 + } + return 0 + } - // 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 + bestCandidate := func(list []candidate) (*goolib.PkgSpec, string, string, error) { + if len(list) == 0 { + return nil, "", "", fmt.Errorf("no package found") + } + slices.SortFunc(list, cmpFunc) + for _, cand := range list { + if isLocked && cand.spec.Arch != installedArch && cand.spec.LockArch { + continue // Ignore this candidate } + return cand.spec, cand.repo, cand.spec.Arch, nil } + return nil, "", "", fmt.Errorf("no package found satisfying lock conditions") } + + for _, cands := range [][]candidate{directCandidates, providesCandidates} { + if len(cands) == 0 { + continue + } + spec, repo, arch, err := bestCandidate(cands) + if err == nil { + return spec, repo, arch, nil + } + } + return nil, "", "", fmt.Errorf("no package found satisfying %s in any repo", name) } diff --git a/client/client_test.go b/client/client_test.go index 7a134c6..ff21404 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -173,8 +173,8 @@ func TestFindRepoLatest(t *testing.T) { {PackageSpec: &goolib.PkgSpec{Name: "bar_pkg", Version: "2.3.0@1", Arch: "noarch"}}, }}, }, - wantVersion: "1.2.3@4", - wantArch: "noarch", + wantVersion: "3.0.0@1", + wantArch: "arm64", wantRepo: "foo_repo", }, { @@ -234,9 +234,215 @@ func TestFindRepoLatest(t *testing.T) { wantArch: "noarch", wantRepo: "high_priority_repo", }, + { + desc: "version priority over arch", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"noarch", "x86_64"}, + rm: RepoMap{ + "foo_repo": Repo{Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "x86_64"}}, + }}, + }, + wantVersion: "2.0.0@1", + wantArch: "x86_64", + wantRepo: "foo_repo", + }, + { + desc: "priority wins over version", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"noarch"}, + rm: RepoMap{ + "high_pri": Repo{ + Priority: 1000, + Packages: []goolib.RepoSpec{{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}}}, + }, + "low_pri": Repo{ + Priority: 500, + Packages: []goolib.RepoSpec{{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "noarch"}}}, + }, + }, + wantVersion: "1.0.0@1", + wantArch: "noarch", + wantRepo: "high_pri", + }, + { + desc: "version wins over arch", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"x86_64", "x86_32"}, + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_64"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "x86_32"}}, + }, + }, + }, + wantVersion: "2.0.0@1", + wantArch: "x86_32", + wantRepo: "repo", + }, + { + desc: "arch wins tie", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"x86_64", "x86_32"}, + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_64"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_32"}}, + }, + }, + }, + wantVersion: "1.0.0@1", + wantArch: "x86_64", + wantRepo: "repo", + }, + { + desc: "cross arch upgrade", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"x86_32", "x86_64"}, // Prefer 32-bit + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_32"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "x86_64"}}, + }, + }, + }, + wantVersion: "2.0.0@1", + wantArch: "x86_64", + wantRepo: "repo", + }, + { + desc: "complex mix", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"x86_64", "x86_32"}, + rm: RepoMap{ + "high_pri": Repo{ + Priority: 1000, + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_64"}}, + }, + }, + "med_pri": Repo{ + Priority: 500, + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "3.0.0@1", Arch: "x86_64"}}, + }, + }, + "low_pri": Repo{ // Should win if version was primary + Priority: 100, + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "4.0.0@1", Arch: "x86_64"}}, + }, + }, + }, + wantVersion: "1.0.0@1", + wantArch: "x86_64", + wantRepo: "high_pri", + }, + { + desc: "system_windows amd64 default preference", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"x86_64", "x86_32", "noarch"}, + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_64"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_32"}}, + }, + }, + }, + wantVersion: "1.0.0@1", + wantArch: "x86_64", + wantRepo: "repo", + }, + { + desc: "system_windows amd64 version override", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"x86_64", "x86_32", "noarch"}, + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_64"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "x86_32"}}, + }, + }, + }, + wantVersion: "2.0.0@1", + wantArch: "x86_32", + wantRepo: "repo", + }, + { + desc: "system_windows arm64 default preference", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"arm64", "x86_64", "x86_32", "noarch"}, + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "arm64"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_64"}}, + }, + }, + }, + wantVersion: "1.0.0@1", + wantArch: "arm64", + wantRepo: "repo", + }, + { + desc: "system_windows arm64 version override", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"arm64", "x86_64", "x86_32", "noarch"}, + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "arm64"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "x86_64"}}, + }, + }, + }, + wantVersion: "2.0.0@1", + wantArch: "x86_64", + wantRepo: "repo", + }, + { + desc: "system_windows 386 default preference", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"x86_32", "noarch"}, + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "x86_32"}}, + }, + }, + }, + wantVersion: "1.0.0@1", + wantArch: "x86_32", + wantRepo: "repo", + }, + { + desc: "system_windows 386 version override", + pi: goolib.PackageInfo{Name: "foo_pkg"}, + archs: []string{"x86_32", "noarch"}, + rm: RepoMap{ + "repo": Repo{ + Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "x86_32"}}, + }, + }, + }, + wantVersion: "2.0.0@1", + wantArch: "x86_32", + wantRepo: "repo", + }, } { t.Run(tt.desc, func(t *testing.T) { - gotSpec, gotRepo, gotArch, err := FindRepoLatest(tt.pi, tt.rm, tt.archs) + gotSpec, gotRepo, gotArch, err := FindRepoLatest(tt.pi, tt.rm, tt.archs, "", false) 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 { @@ -490,7 +696,7 @@ func TestFindRepoLatest_Provides(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - spec, _, _, err := FindRepoLatest(tt.pi, rm, []string{"noarch"}) + spec, _, _, err := FindRepoLatest(tt.pi, rm, []string{"noarch"}, "", false) if tt.wantError { if err == nil { t.Errorf("FindRepoLatest(%v) wanted error, got nil", tt.pi) @@ -542,7 +748,7 @@ func TestFindRepoLatest_Priority(t *testing.T) { } pi := goolib.PackageInfo{Name: "real_pkg", Arch: "noarch"} - spec, _, _, err := FindRepoLatest(pi, rm, []string{"noarch"}) + spec, _, _, err := FindRepoLatest(pi, rm, []string{"noarch"}, "", false) if err != nil { t.Fatalf("FindRepoLatest failed: %v", err) } @@ -554,3 +760,70 @@ func TestFindRepoLatest_Priority(t *testing.T) { t.Errorf("Expected version '1.0.0', got '%s'", spec.Version) } } + +func TestFindRepoLatest_LockArch(t *testing.T) { + tests := []struct { + name string + installedArch string + isLocked bool + rm RepoMap + wantVersion string + wantArch string + }{ + { + name: "locked, ignore newer locked cross-arch", + installedArch: "noarch", + isLocked: true, + rm: RepoMap{ + "repo1": Repo{Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0", Arch: "x86_64", LockArch: true}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0", Arch: "noarch"}}, + }}, + }, + wantVersion: "1.0.0", + wantArch: "noarch", + }, + { + name: "locked, accept newer unlocked cross-arch", + installedArch: "noarch", + isLocked: true, + rm: RepoMap{ + "repo1": Repo{Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0", Arch: "x86_64", LockArch: true}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "3.0.0", Arch: "x86_64", LockArch: false}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0", Arch: "noarch"}}, + }}, + }, + wantVersion: "3.0.0", + wantArch: "x86_64", + }, + { + name: "unlocked, take newest", + installedArch: "noarch", + isLocked: false, + rm: RepoMap{ + "repo1": Repo{Packages: []goolib.RepoSpec{ + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0", Arch: "x86_64", LockArch: true}}, + {PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0", Arch: "noarch"}}, + }}, + }, + wantVersion: "2.0.0", + wantArch: "x86_64", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + spec, _, _, err := FindRepoLatest(goolib.PackageInfo{Name: "foo_pkg"}, tt.rm, []string{"noarch", "x86_64"}, tt.installedArch, tt.isLocked) + if err != nil { + t.Fatalf("FindRepoLatest failed: %v", err) + } + if spec.Version != tt.wantVersion { + t.Errorf("got version %q, want %q", spec.Version, tt.wantVersion) + } + if spec.Arch != tt.wantArch { + t.Errorf("got arch %q, want %q", spec.Arch, tt.wantArch) + } + }) + } +} diff --git a/download/download.go b/download/download.go index 6c8b2b9..7690879 100644 --- a/download/download.go +++ b/download/download.go @@ -151,7 +151,7 @@ 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) { - spec, 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, "", false) if err != nil { return "", "", err } diff --git a/googet.goospec b/googet.goospec index 8fbf3ba..d71ca3c 100644 --- a/googet.goospec +++ b/googet.goospec @@ -1,4 +1,4 @@ -{{$version := "3.2.1@0" -}} +{{$version := "3.3.0@0" -}} { "name": "googet", "version": "{{$version}}", @@ -15,7 +15,9 @@ "path": "install.ps1" }, "releaseNotes": [ - "3.2.1 - Refactor: Update GooDB.FetchPkg to accept goolib.PackageInfo.", + "3.3.0 - Refactor: Update FindRepoLatest to prioritize repo priority, version, and architecture with lock support.", + "3.3.0 - Refactor: Add file ownership conflict checks to prevent overwrites.", + "3.2.1 - Refactor: Update GooDB.FetchPkg to accept goolib.PackageInfo.", "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.", diff --git a/googetdb/googetdb.go b/googetdb/googetdb.go index e74b1b2..5ec0547 100644 --- a/googetdb/googetdb.go +++ b/googetdb/googetdb.go @@ -214,6 +214,20 @@ func (g *GooDB) FetchPkgs(pkgName string) (client.GooGetState, error) { return state, nil } +// InstalledLockState returns the architecture, lock status, and version of the installed package. +// If the package is not installed, it returns empty string and false. +func (g *GooDB) InstalledLockState(pkgName string) (installedArch string, isLocked bool, ver string, pkgFound bool, err error) { + pkgs, err := g.FetchPkgs(pkgName) + if err != nil { + return "", false, "", false, err + } + if len(pkgs) == 0 { + return "", false, "", false, nil + } + // Assume only one arch is installed, or pick the first one. + return pkgs[0].PackageSpec.Arch, pkgs[0].PackageSpec.LockArch, pkgs[0].PackageSpec.Version, true, nil +} + // readState reads the JSON installed package state from the given path, // retrying with a .bak extension if the first read fails. // diff --git a/goolib/goospec.go b/goolib/goospec.go index d13dd25..7f8e29b 100644 --- a/goolib/goospec.go +++ b/goolib/goospec.go @@ -76,9 +76,12 @@ var validArch = []string{"noarch", "x86_64", "x86_32", "arm", "arm64"} // PkgSpec is an individual package specification. type PkgSpec struct { - Name string - Version string - Arch string + Name string + Version string + Arch string + // LockArch, if true, prevents upgrading an installed package to a different architecture + // unless the newer candidate version sets LockArch to false (or leaves it default). + LockArch bool `json:",omitempty"` ReleaseNotes []string `json:",omitempty"` Description string `json:",omitempty"` License string `json:",omitempty"` diff --git a/install/install.go b/install/install.go index 98c02ef..5b95038 100644 --- a/install/install.go +++ b/install/install.go @@ -145,7 +145,11 @@ func installDeps(ctx context.Context, ps *goolib.PkgSpec, cache string, rm clien continue } - spec, repo, _, err := client.FindRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, rm, archs) + installedArch, isLocked, _, _, err := db.InstalledLockState(pi.Name) + if err != nil { + logger.Infof("Error fetching installed package state: %v, proceeding without lock", err) + } + spec, repo, _, err := client.FindRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, rm, archs, installedArch, isLocked) if err != nil { return err } @@ -167,7 +171,11 @@ func FromRepo(ctx context.Context, pi goolib.PackageInfo, repo, cache string, rm // If no version is specified, resolve the latest version handling both // direct matches and providers. if pi.Ver == "" { - spec, repoURL, _, err := client.FindRepoLatest(pi, rm, archs) + installedArch, isLocked, _, _, err := db.InstalledLockState(pi.Name) + if err != nil { + logger.Infof("Error fetching installed package state: %v, proceeding without lock", err) + } + spec, repoURL, _, err := client.FindRepoLatest(pi, rm, archs, installedArch, isLocked) if err != nil { return err } @@ -579,7 +587,7 @@ func installPkg(pkg string, ps *goolib.PkgSpec, dbOnly, force bool, db *googetdb return insFiles, nil } -func listDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, dl []goolib.PackageInfo, archs []string) ([]goolib.PackageInfo, error) { +func listDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, dl []goolib.PackageInfo, archs []string, db *googetdb.GooDB) ([]goolib.PackageInfo, error) { rs, err := client.FindRepoSpec(pi, rm[repo]) if err != nil { return nil, err @@ -587,7 +595,11 @@ 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) - spec, repo, arch, err := client.FindRepoLatest(di, rm, archs) + installedArch, isLocked, _, _, err := db.InstalledLockState(di.Name) + if err != nil { + logger.Infof("Error fetching installed package state: %v, proceeding without lock", err) + } + spec, repo, arch, err := client.FindRepoLatest(di, rm, archs, installedArch, isLocked) di.Arch = arch if err != nil { return nil, fmt.Errorf("cannot resolve dependency %s.%s.%s: %v", di.Name, di.Arch, di.Ver, err) @@ -600,7 +612,7 @@ func listDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, dl []goolib 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 = spec.Version - dl, err = listDeps(di, rm, repo, dl, archs) + dl, err = listDeps(di, rm, repo, dl, archs, db) if err != nil { return nil, err } @@ -609,7 +621,7 @@ func listDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, dl []goolib } // ListDeps returns a list of dependencies and subdependancies for a package. -func ListDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, archs []string) ([]goolib.PackageInfo, error) { +func ListDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, archs []string, db *googetdb.GooDB) ([]goolib.PackageInfo, error) { logger.Infof("Building dependency list for %s.%s.%s", pi.Name, pi.Arch, pi.Ver) - return listDeps(pi, rm, repo, nil, archs) + return listDeps(pi, rm, repo, nil, archs, db) } diff --git a/system/system_windows.go b/system/system_windows.go index 362dc14..ce28313 100644 --- a/system/system_windows.go +++ b/system/system_windows.go @@ -397,27 +397,24 @@ func width() (int, error) { return int(p[0].AddressWidth), nil } -// InstallableArchs returns a slice of archs supported by this machine. +// InstallableArchs returns a slice of archs supported by this machine, +// ordered by preference (native architecture first, followed by compatible architectures, and lastly "noarch"). // WMI errors are logged but not returned. func InstallableArchs() ([]string, error) { switch { case runtime.GOARCH == "386": // Check if this is indeed a 32bit system. aw, err := width() - if err != nil { - return []string{"noarch", "x86_32"}, nil - } - if int(aw) == 32 { - return []string{"noarch", "x86_32"}, nil + if err != nil || int(aw) == 32 { + return []string{"x86_32", "noarch"}, nil } - return []string{"noarch", "x86_64", "x86_32"}, nil + return []string{"x86_64", "x86_32", "noarch"}, nil case runtime.GOARCH == "amd64": - // TODO: Add check for 32bit support, make sure it works with servers and client OS's. - return []string{"noarch", "x86_32", "x86_64"}, nil + return []string{"x86_64", "x86_32", "noarch"}, nil case runtime.GOARCH == "arm": - return []string{"noarch", "arm"}, nil + return []string{"arm", "noarch"}, nil case runtime.GOARCH == "arm64": - return []string{"noarch", "x86_32", "x86_64", "arm64"}, nil + return []string{"arm64", "x86_64", "x86_32", "noarch"}, nil default: return nil, fmt.Errorf("runtime %s not supported", runtime.GOARCH) }