From ed0a589561604dd6d61ee90dd17ab617dbd16ade Mon Sep 17 00:00:00 2001 From: Manuel Naranjo Date: Mon, 13 Jan 2025 14:33:20 +0100 Subject: [PATCH] sat: sort inputs to the sat solver for determinism Make sure the sat solver output is deterministic and reproducible by making the input order stable --- .bazelrc | 3 +++ MODULE.bazel | 1 + pkg/sat/BUILD.bazel | 1 + pkg/sat/sat.go | 60 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/.bazelrc b/.bazelrc index 2d3524b..4e2ab67 100644 --- a/.bazelrc +++ b/.bazelrc @@ -12,6 +12,9 @@ common --incompatible_enable_proto_toolchain_resolution # inside the rules project we build from source common --extra_toolchains=//cmd:bazeldnf-host-toolchain +# make sure bazel doesn't complain on hosts without java +common --java_runtime_version=remotejdk_21 + # Load any settings & overrides specific to the current user from `.bazelrc.user`. # This file should appear in `.gitignore` so that settings are not shared with team members. This # should be last statement in this config so the user configuration is able to overwrite flags from diff --git a/MODULE.bazel b/MODULE.bazel index 6770cd2..e6af7b4 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -33,6 +33,7 @@ use_repo( "com_github_spf13_cobra", "io_k8s_sigs_yaml", "org_golang_x_crypto", + "org_golang_x_exp", ) # deps only needed for the repo internals diff --git a/pkg/sat/BUILD.bazel b/pkg/sat/BUILD.bazel index 2eb0ecb..2432fa2 100644 --- a/pkg/sat/BUILD.bazel +++ b/pkg/sat/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "@com_github_crillab_gophersat//explain", "@com_github_crillab_gophersat//maxsat", "@com_github_sirupsen_logrus//:logrus", + "@org_golang_x_exp//slices", ], ) diff --git a/pkg/sat/sat.go b/pkg/sat/sat.go index 1a2f9ca..7ee3bce 100644 --- a/pkg/sat/sat.go +++ b/pkg/sat/sat.go @@ -2,6 +2,7 @@ package sat import ( "bufio" + "cmp" "fmt" "io" "regexp" @@ -16,6 +17,7 @@ import ( "github.com/rmohr/bazeldnf/pkg/reducer" "github.com/rmohr/bazeldnf/pkg/rpm" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) type VarType string @@ -34,6 +36,13 @@ type VarContext struct { Version api.Version } +func varContextSort(a VarContext, b VarContext) int { + return cmp.Or( + cmp.Compare(a.Package, b.Package), + rpm.Compare(a.Version, b.Version), + ) +} + type Var struct { satVarName string varType VarType @@ -129,8 +138,15 @@ func (r *Resolver) LoadInvolvedPackages(packages []*api.Package, ignoreRegex []s deduplicated[pkg.String()] = packages[i] } } + + deduplicatedKeys := make([]string, 0, len(deduplicated)) + for k := range deduplicated { + deduplicatedKeys = append(deduplicatedKeys, k) + } + slices.Sort(deduplicatedKeys) + packages = nil - for k, _ := range deduplicated { + for _, k := range deduplicatedKeys { reducer.FixPackages(deduplicated[k]) packages = append(packages, deduplicated[k]) } @@ -146,8 +162,13 @@ func (r *Resolver) LoadInvolvedPackages(packages []*api.Package, ignoreRegex []s if !r.nobest { packages = nil - for _, v := range r.bestPackages { - packages = append(packages, v) + bestPackagesKeys := make([]string, 0, len(r.bestPackages)) + for k := range r.bestPackages { + bestPackagesKeys = append(bestPackagesKeys, k) + } + slices.Sort(bestPackagesKeys) + for _, v := range bestPackagesKeys { + packages = append(packages, r.bestPackages[v]) } } // Generate variables @@ -161,17 +182,31 @@ func (r *Resolver) LoadInvolvedPackages(packages []*api.Package, ignoreRegex []s } } - for x, _ := range r.packages { + packagesKeys := make([]string, 0, len(r.packages)) + for k := range r.packages { + packagesKeys = append(packagesKeys, k) + } + slices.Sort(packagesKeys) + + for _, x := range packagesKeys { sort.SliceStable(r.packages[x], func(i, j int) bool { return rpm.Compare(r.packages[x][i].Package.Version, r.packages[x][j].Package.Version) < 0 }) } logrus.Infof("Loaded %v packages.", len(r.pkgProvides)) + + pkgProvideKeys := make([]VarContext, 0, len(r.pkgProvides)) + for vc := range r.pkgProvides { + pkgProvideKeys = append(pkgProvideKeys, vc) + } + slices.SortFunc(pkgProvideKeys, varContextSort) + // Generate imply rules - for _, resourceVars := range r.pkgProvides { + for _, provided := range pkgProvideKeys { // Create imply rules for every package and add them to the formula // one provided dependency implies all dependencies from that package + resourceVars := r.pkgProvides[provided] bfVar := bf.And(toBFVars(resourceVars)...) var ands []bf.Formula for _, res := range resourceVars { @@ -250,8 +285,14 @@ func (res *Resolver) Resolve() (install []*api.Package, excluded []*api.Package, } } // write soft rules. We don't want to install any package - for _, pkgs := range res.packages { - weight := 1901 + packageKeys := make([]string, 0, len(res.packages)) + for k := range res.packages { + packageKeys = append(packageKeys, k) + } + + for _, name := range packageKeys { + pkgs := res.packages[name] + weight := 2999 fmt.Fprintf(pwMaxSatWriter, "c prefer %s\n", pkgs[len(pkgs)-1].Package.String()) if len(pkgs) > 1 { for _, pkg := range pkgs[0 : len(pkgs)-1] { @@ -533,6 +574,11 @@ func (r *Resolver) explodeSingleRequires(entry api.Entry, provides []*Var) (acce return nil, fmt.Errorf("Nothing can satisfy %s", entry.Name) } + slices.SortFunc(accepts, func(a *Var, b *Var) int { + // there's a single var context for a given package + return varContextSort(a.Context, b.Context) + }) + return accepts, nil }