diff --git a/pkg/package/database.go b/pkg/package/database.go index c3bc28f4..af94dac2 100644 --- a/pkg/package/database.go +++ b/pkg/package/database.go @@ -15,4 +15,6 @@ package pkg +// Database is a merely simple in-memory db. +// FIXME: Use a proper structure or delegate to third-party var Database map[string]string = map[string]string{} diff --git a/pkg/package/package.go b/pkg/package/package.go index a455ac87..83335263 100644 --- a/pkg/package/package.go +++ b/pkg/package/package.go @@ -23,10 +23,13 @@ import ( "hash/crc32" "github.com/crillab/gophersat/bf" + version "github.com/hashicorp/go-version" "github.com/jinzhu/copier" ) +// Package is a package interface (TBD) +// FIXME: Currently some of the methods are returning DefaultPackages due to JSON serialization of the package type Package interface { Encode() (string, error) SetState(state State) Package @@ -39,8 +42,14 @@ type Package interface { GetRequires() []*DefaultPackage GetConflicts() []*DefaultPackage + Expand([]Package) ([]Package, error) + + GetName() string + GetVersion() string + RequiresContains(Package) bool } +// DefaultPackage represent a standard package definition type DefaultPackage struct { Name string Version string @@ -51,16 +60,21 @@ type DefaultPackage struct { IsSet bool } -type PackageUse []string +// State represent the package state type State string +// NewPackage returns a new package func NewPackage(name, version string, requires []*DefaultPackage, conflicts []*DefaultPackage) *DefaultPackage { return &DefaultPackage{Name: name, Version: version, PackageRequires: requires, PackageConflicts: conflicts} } + +// GetFingerPrint returns a UUID of the package. +// FIXME: this needs to be unique, now just name is generalized func (p *DefaultPackage) GetFingerPrint() string { return p.Name } +// AddUse adds a use to a package func (p *DefaultPackage) AddUse(use string) { for _, v := range p.UseFlags { if v == use { @@ -70,6 +84,7 @@ func (p *DefaultPackage) AddUse(use string) { p.UseFlags = append(p.UseFlags, use) } +// RemoveUse removes a use to a package func (p *DefaultPackage) RemoveUse(use string) { for i := len(p.UseFlags) - 1; i >= 0; i-- { @@ -80,6 +95,8 @@ func (p *DefaultPackage) RemoveUse(use string) { } +// Encode encodes the package to string. +// It returns an ID which can be used to retrieve the package later on. func (p *DefaultPackage) Encode() (string, error) { res, err := json.Marshal(p) if err != nil { @@ -103,7 +120,12 @@ func (p *DefaultPackage) IsFlagged(b bool) Package { func (p *DefaultPackage) Flagged() bool { return p.IsSet } - +func (p *DefaultPackage) GetName() string { + return p.Name +} +func (p *DefaultPackage) GetVersion() string { + return p.Version +} func (p *DefaultPackage) SetState(state State) Package { p.State = state return p @@ -128,6 +150,30 @@ func (p *DefaultPackage) Clone() Package { return new } +func (p *DefaultPackage) Expand(world []Package) ([]Package, error) { + + var versionsInWorld []Package + for _, w := range world { + if w.GetName() == p.GetName() { + + v, err := version.NewVersion(w.GetVersion()) + if err != nil { + return nil, err + } + constraints, err := version.NewConstraint(p.GetVersion()) + if err != nil { + return nil, err + } + if constraints.Check(v) { + versionsInWorld = append(versionsInWorld, w) + } + + } + } + + return versionsInWorld, nil +} + func DecodePackage(ID string) (Package, error) { pa, ok := Database[ID] @@ -158,6 +204,20 @@ func NormalizeFlagged(p Package) { } } +func (p *DefaultPackage) RequiresContains(s Package) bool { + for _, re := range p.GetRequires() { + if re.GetFingerPrint() == s.GetFingerPrint() { + return true + } + + if re.RequiresContains(s) { + return true + } + } + + return false +} + func (p *DefaultPackage) BuildFormula() ([]bf.Formula, error) { encodedA, err := p.IsFlagged(true).Encode() if err != nil { diff --git a/pkg/package/package_suite_test.go b/pkg/package/package_suite_test.go new file mode 100644 index 00000000..edca8444 --- /dev/null +++ b/pkg/package/package_suite_test.go @@ -0,0 +1,28 @@ +// Copyright © 2019 Ettore Di Giacinto +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, see . + +package pkg_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestSolver(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Package Suite") +} diff --git a/pkg/package/package_test.go b/pkg/package/package_test.go new file mode 100644 index 00000000..0e04cfd7 --- /dev/null +++ b/pkg/package/package_test.go @@ -0,0 +1,39 @@ +// Copyright © 2019 Ettore Di Giacinto +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, see . + +package pkg_test + +import ( + . "github.com/mudler/luet/pkg/package" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Package", func() { + Context("Simple package", func() { + a := NewPackage("A", ">=1.0", []*DefaultPackage{}, []*DefaultPackage{}) + a1 := NewPackage("A", "1.0", []*DefaultPackage{}, []*DefaultPackage{}) + a11 := NewPackage("A", "1.1", []*DefaultPackage{}, []*DefaultPackage{}) + a01 := NewPackage("A", "0.1", []*DefaultPackage{}, []*DefaultPackage{}) + It("Expands correctly", func() { + lst, err := a.Expand([]Package{a1, a11, a01}) + Expect(err).ToNot(HaveOccurred()) + Expect(lst).To(ContainElement(a11)) + Expect(lst).To(ContainElement(a1)) + Expect(lst).ToNot(ContainElement(a01)) + Expect(len(lst)).To(Equal(2)) + }) + }) +}) diff --git a/pkg/solver/decoder.go b/pkg/solver/decoder.go index fda5c7d3..9d6f8327 100644 --- a/pkg/solver/decoder.go +++ b/pkg/solver/decoder.go @@ -19,11 +19,15 @@ import ( pkg "github.com/mudler/luet/pkg/package" ) +// PackageAssert represent a package assertion. +// It is composed of a Package and a Value which is indicating the absence or not +// of the associated package state. type PackageAssert struct { Package pkg.Package Value bool } +// DecodeModel decodes a model from the SAT solver to package assertions (PackageAssert) func DecodeModel(model map[string]bool) ([]PackageAssert, error) { ass := make([]PackageAssert, 0) for k, v := range model { diff --git a/pkg/solver/solver.go b/pkg/solver/solver.go index 1fae8e4d..860524aa 100644 --- a/pkg/solver/solver.go +++ b/pkg/solver/solver.go @@ -22,19 +22,24 @@ import ( pkg "github.com/mudler/luet/pkg/package" ) -type State interface{ Encode() string } - +// PackageSolver is an interface to a generic package solving algorithm type PackageSolver interface { SetWorld(p []pkg.Package) Install(p []pkg.Package) ([]PackageAssert, error) Uninstall(candidate pkg.Package) ([]pkg.Package, error) + ConflictsWithInstalled(p pkg.Package) (bool, error) + ConflictsWith(p pkg.Package, ls []pkg.Package) (bool, error) } + +// Solver is the default solver for luet type Solver struct { Wanted []pkg.Package Installed []pkg.Package World []pkg.Package } +// NewSolver accepts as argument two lists of packages, the first is the initial set, +// the second represent all the known packages. func NewSolver(init []pkg.Package, w []pkg.Package) PackageSolver { for _, v := range init { pkg.NormalizeFlagged(v) @@ -45,6 +50,8 @@ func NewSolver(init []pkg.Package, w []pkg.Package) PackageSolver { return &Solver{Installed: init, World: w} } +// SetWorld is a setter for the list of all known packages to the solver + func (s *Solver) SetWorld(p []pkg.Package) { s.World = p } @@ -59,17 +66,35 @@ func (s *Solver) noRulesWorld() bool { return true } -func (s *Solver) BuildWorld() (bf.Formula, error) { +func (s *Solver) BuildInstalled() (bf.Formula, error) { var formulas []bf.Formula - // for _, p := range s.Installed { - // solvable, err := p.BuildFormula() - // if err != nil { - // return nil, err - // } - // //f = bf.And(f, solvable) - // formulas = append(formulas, solvable...) + for _, p := range s.Installed { + solvable, err := p.BuildFormula() + if err != nil { + return nil, err + } + //f = bf.And(f, solvable) + formulas = append(formulas, solvable...) + + } + return bf.And(formulas...), nil + +} + +// BuildWorld builds the formula which olds the requirements from the package definitions +// which are available (global state) +func (s *Solver) BuildWorld(includeInstalled bool) (bf.Formula, error) { + var formulas []bf.Formula + // NOTE: This block should be enabled in case of very old systems with outdated world sets + if includeInstalled { + solvable, err := s.BuildInstalled() + if err != nil { + return nil, err + } + //f = bf.And(f, solvable) + formulas = append(formulas, solvable) + } - // } for _, p := range s.World { solvable, err := p.BuildFormula() if err != nil { @@ -80,13 +105,75 @@ func (s *Solver) BuildWorld() (bf.Formula, error) { return bf.And(formulas...), nil } -// world is ok with Px (installed-x-th) and removal of package (candidate?) +func (s *Solver) ConflictsWith(p pkg.Package, ls []pkg.Package) (bool, error) { + pkg.NormalizeFlagged(p) + var formulas []bf.Formula + + if s.noRulesWorld() { + return false, nil + } + + encodedP, err := p.IsFlagged(true).Encode() + if err != nil { + return false, err + } + P := bf.Var(encodedP) + + r, err := s.BuildWorld(false) + if err != nil { + return false, err + } + formulas = append(formulas, bf.And(bf.Not(P), r)) + + for _, i := range ls { + if i.GetFingerPrint() == p.GetFingerPrint() { + continue + } + // XXX: Skip check on any of its requires ? ( Drop to avoid removing system packages when selecting an uninstall) + // if i.RequiresContains(p) { + // fmt.Println("Requires found") + // continue + // } + + encodedI, err := i.Encode() + if err != nil { + return false, err + } + I := bf.Var(encodedI) + formulas = append(formulas, bf.And(I, r)) + } + model := bf.Solve(bf.And(formulas...)) + if model == nil { + return true, nil + } + + return false, nil + +} + +func (s *Solver) ConflictsWithInstalled(p pkg.Package) (bool, error) { + return s.ConflictsWith(p, s.Installed) +} + +// Uninstall takes a candidate package and return a list of packages that would be removed +// in order to purge the candidate. Returns error if unsat. +// XXX: this should be turned in unsat/sat instead of computing the reverse set +// e.g. world is ok with Px (installed-x-th) and removal of package (candidate?) // collect unsatisfieds and repeat until we get no more unsatisfieds func (s *Solver) Uninstall(candidate pkg.Package) ([]pkg.Package, error) { var res []pkg.Package + + // Build a fake "Installed" - Candidate and its requires tree + var InstalledMinusCandidate []pkg.Package + for _, i := range s.Installed { + if i.GetFingerPrint() != candidate.GetFingerPrint() && !candidate.RequiresContains(i) { + InstalledMinusCandidate = append(InstalledMinusCandidate, i) + } + } + + // Get the requirements to install the candidate saved := s.Installed s.Installed = []pkg.Package{} - asserts, err := s.Install([]pkg.Package{candidate}) if err != nil { return nil, err @@ -95,7 +182,24 @@ func (s *Solver) Uninstall(candidate pkg.Package) ([]pkg.Package, error) { for _, a := range asserts { if a.Value && a.Package.Flagged() { - res = append(res, a.Package.IsFlagged(false)) + + c, err := s.ConflictsWithInstalled(a.Package) + if err != nil { + return nil, err + } + if !c { // If doesn't conflict with installed we just consider it for removal + res = append(res, a.Package.IsFlagged(false)) + } else { + // If does conficlits, give it another chance checking conflicts if in case we didn't installed our candidate and all the requires in the system + c, err := s.ConflictsWith(a.Package, InstalledMinusCandidate) + if err != nil { + return nil, err + } + if !c { + res = append(res, a.Package.IsFlagged(false)) + } + } + } } @@ -103,10 +207,10 @@ func (s *Solver) Uninstall(candidate pkg.Package) ([]pkg.Package, error) { return res, nil } +// BuildFormula builds the main solving formula that is evaluated by the sat solver. func (s *Solver) BuildFormula() (bf.Formula, error) { - //f := bf.True var formulas []bf.Formula - r, err := s.BuildWorld() + r, err := s.BuildWorld(false) if err != nil { return nil, err } @@ -146,6 +250,7 @@ func (s *Solver) solve(f bf.Formula) (map[string]bool, bf.Formula, error) { return model, f, nil } +// Solve builds the formula given the current state and returns package assertions func (s *Solver) Solve() ([]PackageAssert, error) { f, err := s.BuildFormula() @@ -162,6 +267,8 @@ func (s *Solver) Solve() ([]PackageAssert, error) { return DecodeModel(model) } +// Install given a list of packages, returns package assertions to indicate the packages that must be installed in the system in order +// to statisfy all the constraints func (s *Solver) Install(coll []pkg.Package) ([]PackageAssert, error) { for _, v := range coll { v.IsFlagged(false) diff --git a/pkg/solver/solver_test.go b/pkg/solver/solver_test.go index 3a186628..837dfab1 100644 --- a/pkg/solver/solver_test.go +++ b/pkg/solver/solver_test.go @@ -150,6 +150,126 @@ var _ = Describe("Solver", func() { Expect(len(solution)).To(Equal(1)) }) + It("Find conflicts", func() { + + C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + A := pkg.NewPackage("A", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + + B := pkg.NewPackage("B", "", []*pkg.DefaultPackage{A}, []*pkg.DefaultPackage{}) + + s := NewSolver([]pkg.Package{A, B, C, D}, []pkg.Package{A, B, C, D}) + val, err := s.ConflictsWithInstalled(A) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(BeTrue()) + + }) + + It("Find nested conflicts", func() { + C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + A := pkg.NewPackage("A", "", []*pkg.DefaultPackage{D}, []*pkg.DefaultPackage{}) + + B := pkg.NewPackage("B", "", []*pkg.DefaultPackage{A}, []*pkg.DefaultPackage{}) + + s := NewSolver([]pkg.Package{A, B, C, D}, []pkg.Package{A, B, C, D}) + val, err := s.ConflictsWithInstalled(D) + Expect(err).ToNot(HaveOccurred()) + Expect(val).To(BeTrue()) + }) + + It("Doesn't find nested conflicts", func() { + C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + A := pkg.NewPackage("A", "", []*pkg.DefaultPackage{D}, []*pkg.DefaultPackage{}) + + B := pkg.NewPackage("B", "", []*pkg.DefaultPackage{A}, []*pkg.DefaultPackage{}) + + s := NewSolver([]pkg.Package{A, B, C, D}, []pkg.Package{A, B, C, D}) + val, err := s.ConflictsWithInstalled(C) + Expect(err).ToNot(HaveOccurred()) + Expect(val).ToNot(BeTrue()) + }) + + It("Doesn't find conflicts", func() { + C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + A := pkg.NewPackage("A", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + + B := pkg.NewPackage("B", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + + s := NewSolver([]pkg.Package{A, B, C, D}, []pkg.Package{A, B, C, D}) + val, err := s.ConflictsWithInstalled(C) + Expect(err).ToNot(HaveOccurred()) + Expect(val).ToNot(BeTrue()) + }) + It("Uninstalls simple packages not in world correctly", func() { + C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + B := pkg.NewPackage("B", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + A := pkg.NewPackage("A", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + + s := NewSolver([]pkg.Package{A, B, C, D}, []pkg.Package{B, C, D}) + + solution, err := s.Uninstall(A) + Expect(err).ToNot(HaveOccurred()) + + Expect(solution).To(ContainElement(A.IsFlagged(false))) + + // Expect(solution).To(ContainElement(PackageAssert{Package: C.IsFlagged(true), Value: true})) + Expect(len(solution)).To(Equal(1)) + }) + + It("Uninstalls complex packages not in world correctly", func() { + C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + B := pkg.NewPackage("B", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + A := pkg.NewPackage("A", "", []*pkg.DefaultPackage{B}, []*pkg.DefaultPackage{}) + + s := NewSolver([]pkg.Package{A, B, C, D}, []pkg.Package{B, C, D}) + + solution, err := s.Uninstall(A) + Expect(err).ToNot(HaveOccurred()) + + Expect(solution).To(ContainElement(A.IsFlagged(false))) + + Expect(len(solution)).To(Equal(1)) + }) + + It("Uninstalls complex packages correctly, even if shared deps are required by system packages", func() { + D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + B := pkg.NewPackage("B", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + A := pkg.NewPackage("A", "", []*pkg.DefaultPackage{B}, []*pkg.DefaultPackage{}) + C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{B}, []*pkg.DefaultPackage{}) + + s := NewSolver([]pkg.Package{A, B, C, D}, []pkg.Package{A, B, C, D}) + + solution, err := s.Uninstall(A) + Expect(err).ToNot(HaveOccurred()) + + Expect(solution).To(ContainElement(A.IsFlagged(false))) + Expect(solution).ToNot(ContainElement(B.IsFlagged(false))) + + Expect(len(solution)).To(Equal(1)) + }) + + It("Uninstalls complex packages in world correctly", func() { + C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + B := pkg.NewPackage("B", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) + A := pkg.NewPackage("A", "", []*pkg.DefaultPackage{C}, []*pkg.DefaultPackage{}) + + s := NewSolver([]pkg.Package{A, C, D}, []pkg.Package{A, B, C, D}) + + solution, err := s.Uninstall(A) + Expect(err).ToNot(HaveOccurred()) + + Expect(solution).To(ContainElement(A.IsFlagged(false))) + Expect(solution).To(ContainElement(C.IsFlagged(false))) + + Expect(len(solution)).To(Equal(2)) + }) + It("Uninstalls complex package correctly", func() { C := pkg.NewPackage("C", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) D := pkg.NewPackage("D", "", []*pkg.DefaultPackage{}, []*pkg.DefaultPackage{}) @@ -162,10 +282,8 @@ var _ = Describe("Solver", func() { solution, err := s.Uninstall(A) Expect(solution).To(ContainElement(A.IsFlagged(false))) Expect(solution).To(ContainElement(B.IsFlagged(false))) - //Expect(solution).To(ContainElement(C.IsFlagged(true))) Expect(solution).To(ContainElement(D.IsFlagged(false))) - // Expect(solution).To(ContainElement(PackageAssert{Package: C.IsFlagged(true), Value: true})) Expect(len(solution)).To(Equal(3)) Expect(err).ToNot(HaveOccurred())