// 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 installer import ( "fmt" "io/ioutil" "os" "path/filepath" "sort" "strings" "sync" "github.com/mudler/luet/pkg/api/core/types" "github.com/mudler/luet/pkg/bus" artifact "github.com/mudler/luet/pkg/compiler/types/artifact" "github.com/mudler/luet/pkg/config" "github.com/mudler/luet/pkg/helpers" fileHelper "github.com/mudler/luet/pkg/helpers/file" "github.com/mudler/luet/pkg/helpers/match" . "github.com/mudler/luet/pkg/logger" pkg "github.com/mudler/luet/pkg/package" "github.com/mudler/luet/pkg/solver" . "github.com/logrusorgru/aurora" "github.com/pkg/errors" ) type LuetInstallerOptions struct { SolverOptions config.LuetSolverOptions Concurrency int NoDeps bool OnlyDeps bool Force bool PreserveSystemEssentialData bool FullUninstall, FullCleanUninstall bool CheckConflicts bool SolverUpgrade, RemoveUnavailableOnUpgrade, UpgradeNewRevisions bool Ask bool DownloadOnly bool Relaxed bool PackageRepositories types.LuetRepositories } type LuetInstaller struct { Options LuetInstallerOptions } type ArtifactMatch struct { Package pkg.Package Artifact *artifact.PackageArtifact Repository *LuetSystemRepository } func NewLuetInstaller(opts LuetInstallerOptions) *LuetInstaller { return &LuetInstaller{Options: opts} } // computeUpgrade returns the packages to be uninstalled and installed in a system to perform an upgrade // based on the system repositories func (l *LuetInstaller) computeUpgrade(syncedRepos Repositories, s *System) (pkg.Packages, pkg.Packages, error) { toInstall := pkg.Packages{} var uninstall pkg.Packages var err error // First match packages against repositories by priority allRepos := pkg.NewInMemoryDatabase(false) syncedRepos.SyncDatabase(allRepos) // compute a "big" world solv := solver.NewResolver(solver.Options{Type: l.Options.SolverOptions.Implementation, Concurrency: l.Options.Concurrency}, s.Database, allRepos, pkg.NewInMemoryDatabase(false), l.Options.SolverOptions.Resolver()) var solution solver.PackagesAssertions if l.Options.SolverUpgrade { uninstall, solution, err = solv.UpgradeUniverse(l.Options.RemoveUnavailableOnUpgrade) if err != nil { return uninstall, toInstall, errors.Wrap(err, "Failed solving solution for upgrade") } } else { uninstall, solution, err = solv.Upgrade(l.Options.FullUninstall, true) if err != nil { return uninstall, toInstall, errors.Wrap(err, "Failed solving solution for upgrade") } } for _, assertion := range solution { // Be sure to filter from solutions packages already installed in the system if _, err := s.Database.FindPackage(assertion.Package); err != nil && assertion.Value { toInstall = append(toInstall, assertion.Package) } } if l.Options.UpgradeNewRevisions { for _, p := range s.Database.World() { matches := syncedRepos.PackageMatches(pkg.Packages{p}) if len(matches) == 0 { // Package missing. the user should run luet upgrade --universe continue } for _, artefact := range matches[0].Repo.GetIndex() { if artefact.CompileSpec.GetPackage() == nil { return uninstall, toInstall, errors.New("Package in compilespec empty") } if artefact.CompileSpec.GetPackage().Matches(p) && artefact.CompileSpec.GetPackage().GetBuildTimestamp() != p.GetBuildTimestamp() { toInstall = append(toInstall, matches[0].Package).Unique() uninstall = append(uninstall, p).Unique() } } } } return uninstall, toInstall, nil } func packsToList(p pkg.Packages) string { var packs []string for _, pp := range p { packs = append(packs, pp.HumanReadableString()) } sort.Strings(packs) return strings.Join(packs, " ") } func matchesToList(artefacts map[string]ArtifactMatch) string { var packs []string for fingerprint, match := range artefacts { packs = append(packs, fmt.Sprintf("%s (%s)", fingerprint, match.Repository.GetName())) } sort.Strings(packs) return strings.Join(packs, " ") } // Upgrade upgrades a System based on the Installer options. Returns error in case of failure func (l *LuetInstaller) Upgrade(s *System) error { syncedRepos, err := l.SyncRepositories() if err != nil { return err } Info(":thinking: Computing upgrade, please hang tight... :zzz:") if l.Options.UpgradeNewRevisions { Info(":memo: note: will consider new build revisions while upgrading") } return l.checkAndUpgrade(syncedRepos, s) } func (l *LuetInstaller) SyncRepositories() (Repositories, error) { Spinner(32) defer SpinnerStop() syncedRepos := Repositories{} for _, r := range SystemRepositories(l.Options.PackageRepositories) { repo, err := r.Sync(false) if err != nil { return nil, errors.Wrap(err, "Failed syncing repository: "+r.GetName()) } syncedRepos = append(syncedRepos, repo) } // compute what to install and from where sort.Sort(syncedRepos) return syncedRepos, nil } func (l *LuetInstaller) Swap(toRemove pkg.Packages, toInstall pkg.Packages, s *System) error { syncedRepos, err := l.SyncRepositories() if err != nil { return err } toRemoveFinal := pkg.Packages{} for _, p := range toRemove { packs, _ := s.Database.FindPackages(p) if len(packs) == 0 { return errors.New("Package " + p.HumanReadableString() + " not found in the system") } for _, pp := range packs { toRemoveFinal = append(toRemoveFinal, pp) } } o := Option{ FullUninstall: false, Force: true, CheckConflicts: false, FullCleanUninstall: false, NoDeps: l.Options.NoDeps, OnlyDeps: false, } return l.swap(o, syncedRepos, toRemoveFinal, toInstall, s) } func (l *LuetInstaller) computeSwap(o Option, syncedRepos Repositories, toRemove pkg.Packages, toInstall pkg.Packages, s *System) (map[string]ArtifactMatch, pkg.Packages, solver.PackagesAssertions, pkg.PackageDatabase, error) { allRepos := pkg.NewInMemoryDatabase(false) syncedRepos.SyncDatabase(allRepos) toInstall = syncedRepos.ResolveSelectors(toInstall) // First check what would have been done installedtmp, err := s.Database.Copy() if err != nil { return nil, nil, nil, nil, errors.Wrap(err, "Failed create temporary in-memory db") } systemAfterChanges := &System{Database: installedtmp} packs, err := l.computeUninstall(o, systemAfterChanges, toRemove...) if err != nil && !o.Force { Error("Failed computing uninstall for ", packsToList(toRemove)) return nil, nil, nil, nil, errors.Wrap(err, "computing uninstall "+packsToList(toRemove)) } for _, p := range packs { err = systemAfterChanges.Database.RemovePackage(p) if err != nil { return nil, nil, nil, nil, errors.Wrap(err, "Failed removing package from database") } } match, packages, assertions, allRepos, err := l.computeInstall(o, syncedRepos, toInstall, systemAfterChanges) for _, p := range toInstall { assertions = append(assertions, solver.PackageAssert{Package: p.(*pkg.DefaultPackage), Value: true}) } return match, packages, assertions, allRepos, err } func (l *LuetInstaller) swap(o Option, syncedRepos Repositories, toRemove pkg.Packages, toInstall pkg.Packages, s *System) error { match, packages, assertions, allRepos, err := l.computeSwap(o, syncedRepos, toRemove, toInstall, s) if err != nil { return errors.Wrap(err, "failed computing package replacement") } if l.Options.Ask { if len(toRemove) > 0 { Info(":recycle: Packages that are going to be removed from the system:\n ", Yellow(packsToList(toRemove)).BgBlack().String()) } if len(match) > 0 { Info("Packages that are going to be installed in the system: \n ", Green(matchesToList(match)).BgBlack().String()) } Info("By going forward, you are also accepting the licenses of the packages that you are going to install in your system.") if Ask() { l.Options.Ask = false // Don't prompt anymore } else { return errors.New("Aborted by user") } } // First match packages against repositories by priority if err := l.download(syncedRepos, match); err != nil { return errors.Wrap(err, "Pre-downloading packages") } if err := l.checkFileconflicts(match, false, s); err != nil { if !l.Options.Force { return errors.Wrap(err, "file conflict found") } else { Warning("file conflict found", err.Error()) } } if l.Options.DownloadOnly { return nil } ops := l.getOpsWithOptions(toRemove, match, Option{ Force: o.Force, NoDeps: false, OnlyDeps: o.OnlyDeps, RunFinalizers: false, CheckFileConflicts: false, }, o, syncedRepos, packages, assertions, allRepos) err = l.runOps(ops, s) if err != nil { return errors.Wrap(err, "failed running installer options") } toFinalize, err := l.getFinalizers(allRepos, assertions, match, o.NoDeps) if err != nil { return errors.Wrap(err, "failed getting package to finalize") } return s.ExecuteFinalizers(toFinalize) } type Option struct { Force bool NoDeps bool CheckConflicts bool FullUninstall bool FullCleanUninstall bool OnlyDeps bool RunFinalizers bool CheckFileConflicts bool } type operation struct { Option Option Package pkg.Package } type installOperation struct { operation Reposiories Repositories Packages pkg.Packages Assertions solver.PackagesAssertions Database pkg.PackageDatabase Matches map[string]ArtifactMatch } // installerOp is the operation that is sent to the // upgradeWorker's channel (todo) type installerOp struct { Uninstall operation Install installOperation } func (l *LuetInstaller) runOps(ops []installerOp, s *System) error { all := make(chan installerOp) wg := new(sync.WaitGroup) // Do the real install for i := 0; i < l.Options.Concurrency; i++ { wg.Add(1) go l.installerOpWorker(i, wg, all, s) } for _, c := range ops { all <- c } close(all) wg.Wait() return nil } // TODO: use installerOpWorker in place of all the other workers. // This one is general enough to read a list of operations and execute them. func (l *LuetInstaller) installerOpWorker(i int, wg *sync.WaitGroup, c <-chan installerOp, s *System) error { defer wg.Done() for p := range c { if p.Uninstall.Package != nil { Debug("Replacing package inplace") toUninstall, uninstall, err := l.generateUninstallFn(p.Uninstall.Option, s, p.Uninstall.Package) if err != nil { Error("Failed to generate Uninstall function for" + err.Error()) continue //return errors.Wrap(err, "while computing uninstall") } err = uninstall() if err != nil { Error("Failed uninstall for ", packsToList(toUninstall)) continue //return errors.Wrap(err, "uninstalling "+packsToList(toUninstall)) } } if p.Install.Package != nil { artMatch := p.Install.Matches[p.Install.Package.GetFingerPrint()] ass := p.Install.Assertions.Search(p.Install.Package.GetFingerPrint()) packageToInstall, _ := p.Install.Packages.Find(p.Install.Package.GetPackageName()) err := l.install( p.Install.Option, p.Install.Reposiories, map[string]ArtifactMatch{p.Install.Package.GetFingerPrint(): artMatch}, pkg.Packages{packageToInstall}, solver.PackagesAssertions{*ass}, p.Install.Database, s, ) if err != nil { Error(err) } } } return nil } // checks wheter we can uninstall and install in place and compose installer worker ops func (l *LuetInstaller) getOpsWithOptions( toUninstall pkg.Packages, installMatch map[string]ArtifactMatch, installOpt, uninstallOpt Option, syncedRepos Repositories, toInstall pkg.Packages, solution solver.PackagesAssertions, allRepos pkg.PackageDatabase) []installerOp { resOps := []installerOp{} for _, match := range installMatch { if pack, err := toUninstall.Find(match.Package.GetPackageName()); err == nil { resOps = append(resOps, installerOp{ Uninstall: operation{Package: pack, Option: uninstallOpt}, Install: installOperation{ operation: operation{ Package: match.Package, Option: installOpt, }, Matches: installMatch, Packages: toInstall, Reposiories: syncedRepos, Assertions: solution, Database: allRepos, }, }) } else { resOps = append(resOps, installerOp{ Install: installOperation{ operation: operation{Package: match.Package, Option: installOpt}, Matches: installMatch, Reposiories: syncedRepos, Packages: toInstall, Assertions: solution, Database: allRepos, }, }) } } for _, p := range toUninstall { found := false for _, match := range installMatch { if match.Package.GetPackageName() == p.GetPackageName() { found = true } } if !found { resOps = append(resOps, installerOp{ Uninstall: operation{Package: p, Option: uninstallOpt}, }) } } return resOps } func (l *LuetInstaller) checkAndUpgrade(r Repositories, s *System) error { Spinner(32) uninstall, toInstall, err := l.computeUpgrade(r, s) if err != nil { return errors.Wrap(err, "failed computing upgrade") } SpinnerStop() if len(uninstall) > 0 { Info(":recycle: Packages that are going to be removed from the system:\n ", Yellow(packsToList(uninstall)).BgBlack().String()) } if len(toInstall) > 0 { Info(":zap:Packages that are going to be installed in the system:\n ", Green(packsToList(toInstall)).BgBlack().String()) } if len(toInstall) == 0 && len(uninstall) == 0 { Info("Nothing to do") return nil } // We don't want any conflict with the installed to raise during the upgrade. // In this way we both force uninstalls and we avoid to check with conflicts // against the current system state which is pending to deletion // E.g. you can't check for conflicts for an upgrade of a new version of A // if the old A results installed in the system. This is due to the fact that // now the solver enforces the constraints and explictly denies two packages // of the same version installed. o := Option{ FullUninstall: false, Force: true, CheckConflicts: false, FullCleanUninstall: false, NoDeps: true, OnlyDeps: false, } if l.Options.Ask { Info("By going forward, you are also accepting the licenses of the packages that you are going to install in your system.") if Ask() { l.Options.Ask = false // Don't prompt anymore return l.swap(o, r, uninstall, toInstall, s) } else { return errors.New("Aborted by user") } } return l.swap(o, r, uninstall, toInstall, s) } func (l *LuetInstaller) Install(cp pkg.Packages, s *System) error { syncedRepos, err := l.SyncRepositories() if err != nil { return err } if len(s.Database.World()) > 0 && !l.Options.Relaxed { Info(":thinking: Checking for available upgrades") if err := l.checkAndUpgrade(syncedRepos, s); err != nil { return errors.Wrap(err, "while checking upgrades before install") } } o := Option{ NoDeps: l.Options.NoDeps, Force: l.Options.Force, OnlyDeps: l.Options.OnlyDeps, CheckFileConflicts: true, RunFinalizers: true, } match, packages, assertions, allRepos, err := l.computeInstall(o, syncedRepos, cp, s) if err != nil { return err } // Check if we have to process something, or return to the user an error if len(match) == 0 { Info("No packages to install") return nil } // Resolvers might decide to remove some packages from being installed if !l.Options.SolverOptions.ResolverIsSet() { for _, p := range cp { found := false vers, _ := s.Database.FindPackageVersions(p) // If was installed, it is found, as it was filtered if len(vers) >= 1 { found = true continue } for _, m := range match { if m.Package.GetName() == p.GetName() { found = true } for _, pack := range m.Package.GetProvides() { if pack.GetName() == p.GetName() { found = true } } } if !found { return fmt.Errorf("Package '%s' not found", p.HumanReadableString()) } } } Info("Packages that are going to be installed in the system: \n ", Green(matchesToList(match)).BgBlack().String()) if l.Options.Ask { Info("By going forward, you are also accepting the licenses of the packages that you are going to install in your system.") if Ask() { l.Options.Ask = false // Don't prompt anymore return l.install(o, syncedRepos, match, packages, assertions, allRepos, s) } else { return errors.New("Aborted by user") } } return l.install(o, syncedRepos, match, packages, assertions, allRepos, s) } func (l *LuetInstaller) download(syncedRepos Repositories, toDownload map[string]ArtifactMatch) error { // Download packages into cache in parallel. all := make(chan ArtifactMatch) var wg = new(sync.WaitGroup) // Download for i := 0; i < l.Options.Concurrency; i++ { wg.Add(1) go l.downloadWorker(i, wg, all) } for _, c := range toDownload { all <- c } close(all) wg.Wait() return nil } // Reclaim adds packages to the system database // if files from artifacts in the repositories are found // in the system target func (l *LuetInstaller) Reclaim(s *System) error { syncedRepos, err := l.SyncRepositories() if err != nil { return err } var toMerge []ArtifactMatch = []ArtifactMatch{} for _, repo := range syncedRepos { for _, artefact := range repo.GetIndex() { Debug("Checking if", artefact.CompileSpec.GetPackage().HumanReadableString(), "from", repo.GetName(), "is installed") FILES: for _, f := range artefact.Files { if fileHelper.Exists(filepath.Join(s.Target, f)) { p, err := repo.GetTree().GetDatabase().FindPackage(artefact.CompileSpec.GetPackage()) if err != nil { return err } Info(":mag: Found package:", p.HumanReadableString()) toMerge = append(toMerge, ArtifactMatch{Artifact: artefact, Package: p}) break FILES } } } } for _, match := range toMerge { pack := match.Package vers, _ := s.Database.FindPackageVersions(pack) if len(vers) >= 1 { Warning("Filtering out package " + pack.HumanReadableString() + ", already reclaimed") continue } _, err := s.Database.CreatePackage(pack) if err != nil && !l.Options.Force { return errors.Wrap(err, "Failed creating package") } s.Database.SetPackageFiles(&pkg.PackageFile{PackageFingerprint: pack.GetFingerPrint(), Files: match.Artifact.Files}) Info(":zap:Reclaimed package:", pack.HumanReadableString()) } Info("Done!") return nil } func (l *LuetInstaller) computeInstall(o Option, syncedRepos Repositories, cp pkg.Packages, s *System) (map[string]ArtifactMatch, pkg.Packages, solver.PackagesAssertions, pkg.PackageDatabase, error) { var p pkg.Packages toInstall := map[string]ArtifactMatch{} allRepos := pkg.NewInMemoryDatabase(false) var solution solver.PackagesAssertions // Check if the package is installed first for _, pi := range cp { vers, _ := s.Database.FindPackageVersions(pi) if len(vers) >= 1 { // Warning("Filtering out package " + pi.HumanReadableString() + ", it has other versions already installed. Uninstall one of them first ") continue //return errors.New("Package " + pi.GetFingerPrint() + " has other versions already installed. Uninstall one of them first: " + strings.Join(vers, " ")) } p = append(p, pi) } if len(p) == 0 { return toInstall, p, solution, allRepos, nil } // First get metas from all repos (and decodes trees) // First match packages against repositories by priority // matches := syncedRepos.PackageMatches(p) // compute a "big" world syncedRepos.SyncDatabase(allRepos) p = syncedRepos.ResolveSelectors(p) var packagesToInstall pkg.Packages var err error if !o.NoDeps { solv := solver.NewResolver(solver.Options{Type: l.Options.SolverOptions.Implementation, Concurrency: l.Options.Concurrency}, s.Database, allRepos, pkg.NewInMemoryDatabase(false), l.Options.SolverOptions.Resolver()) if l.Options.Relaxed { solution, err = solv.RelaxedInstall(p) } else { solution, err = solv.Install(p) } /// TODO: PackageAssertions needs to be a map[fingerprint]pack so lookup is in O(1) if err != nil && !o.Force { return toInstall, p, solution, allRepos, errors.Wrap(err, "Failed solving solution for package") } // Gathers things to install for _, assertion := range solution { if assertion.Value { if _, err := s.Database.FindPackage(assertion.Package); err == nil { // skip matching if it is installed already continue } packagesToInstall = append(packagesToInstall, assertion.Package) } } } else if !o.OnlyDeps { for _, currentPack := range p { if _, err := s.Database.FindPackage(currentPack); err == nil { // skip matching if it is installed already continue } packagesToInstall = append(packagesToInstall, currentPack) } } // Gathers things to install for _, currentPack := range packagesToInstall { // Check if package is already installed. matches := syncedRepos.PackageMatches(pkg.Packages{currentPack}) if len(matches) == 0 { return toInstall, p, solution, allRepos, errors.New("Failed matching solutions against repository for " + currentPack.HumanReadableString() + " where are definitions coming from?!") } A: for _, artefact := range matches[0].Repo.GetIndex() { if artefact.CompileSpec.GetPackage() == nil { return toInstall, p, solution, allRepos, errors.New("Package in compilespec empty") } if matches[0].Package.Matches(artefact.CompileSpec.GetPackage()) { currentPack.SetBuildTimestamp(artefact.CompileSpec.GetPackage().GetBuildTimestamp()) // Filter out already installed if _, err := s.Database.FindPackage(currentPack); err != nil { toInstall[currentPack.GetFingerPrint()] = ArtifactMatch{Package: currentPack, Artifact: artefact, Repository: matches[0].Repo} } break A } } } return toInstall, p, solution, allRepos, nil } func (l *LuetInstaller) getFinalizers(allRepos pkg.PackageDatabase, solution solver.PackagesAssertions, toInstall map[string]ArtifactMatch, nodeps bool) ([]pkg.Package, error) { var toFinalize []pkg.Package if !nodeps { // TODO: Lower those errors as warning for _, w := range toInstall { // Finalizers needs to run in order and in sequence. ordered, err := solution.Order(allRepos, w.Package.GetFingerPrint()) if err != nil { return toFinalize, errors.Wrap(err, "While order a solution for "+w.Package.HumanReadableString()) } ORDER: for _, ass := range ordered { if ass.Value { installed, ok := toInstall[ass.Package.GetFingerPrint()] if !ok { // It was a dep already installed in the system, so we can skip it safely continue ORDER } treePackage, err := installed.Repository.GetTree().GetDatabase().FindPackage(ass.Package) if err != nil { return toFinalize, errors.Wrap(err, "Error getting package "+ass.Package.HumanReadableString()) } toFinalize = append(toFinalize, treePackage) } } } } else { for _, c := range toInstall { treePackage, err := c.Repository.GetTree().GetDatabase().FindPackage(c.Package) if err != nil { return toFinalize, errors.Wrap(err, "Error getting package "+c.Package.HumanReadableString()) } toFinalize = append(toFinalize, treePackage) } } return toFinalize, nil } func (l *LuetInstaller) checkFileconflicts(toInstall map[string]ArtifactMatch, checkSystem bool, s *System) error { Info("Checking for file conflicts..") defer s.Clean() // Release memory filesToInstall := []string{} for _, m := range toInstall { a, err := l.downloadPackage(m) if err != nil && !l.Options.Force { return errors.Wrap(err, "Failed downloading package") } files, err := a.FileList() if err != nil && !l.Options.Force { return errors.Wrapf(err, "Could not get filelist for %s", a.CompileSpec.Package.HumanReadableString()) } for _, f := range files { if helpers.Contains(filesToInstall, f) { return fmt.Errorf( "file conflict between packages to be installed", ) } if checkSystem { exists, p, err := s.ExistsPackageFile(f) if err != nil { return errors.Wrap(err, "failed checking into system db") } if exists { return fmt.Errorf( "file conflict between '%s' and '%s' ( file: %s )", p.HumanReadableString(), m.Package.HumanReadableString(), f, ) } } } filesToInstall = append(filesToInstall, files...) } return nil } func (l *LuetInstaller) install(o Option, syncedRepos Repositories, toInstall map[string]ArtifactMatch, p pkg.Packages, solution solver.PackagesAssertions, allRepos pkg.PackageDatabase, s *System) error { // Download packages in parallel first if err := l.download(syncedRepos, toInstall); err != nil { return errors.Wrap(err, "Downloading packages") } if o.CheckFileConflicts { // Check file conflicts if err := l.checkFileconflicts(toInstall, true, s); err != nil { if !l.Options.Force { return errors.Wrap(err, "file conflict found") } else { Warning("file conflict found", err.Error()) } } } if l.Options.DownloadOnly { return nil } all := make(chan ArtifactMatch) wg := new(sync.WaitGroup) // Do the real install for i := 0; i < l.Options.Concurrency; i++ { wg.Add(1) go l.installerWorker(i, wg, all, s) } for _, c := range toInstall { all <- c } close(all) wg.Wait() for _, c := range toInstall { // Annotate to the system that the package was installed _, err := s.Database.CreatePackage(c.Package) if err != nil && !o.Force { return errors.Wrap(err, "Failed creating package") } bus.Manager.Publish(bus.EventPackageInstall, c) } if !o.RunFinalizers { return nil } toFinalize, err := l.getFinalizers(allRepos, solution, toInstall, o.NoDeps) if err != nil { return errors.Wrap(err, "failed getting package to finalize") } return s.ExecuteFinalizers(toFinalize) } func (l *LuetInstaller) downloadPackage(a ArtifactMatch) (*artifact.PackageArtifact, error) { artifact, err := a.Repository.Client().DownloadArtifact(a.Artifact) if err != nil { return nil, errors.Wrap(err, "Error on download artifact") } err = artifact.Verify() if err != nil { return nil, errors.Wrap(err, "Artifact integrity check failure") } return artifact, nil } func (l *LuetInstaller) installPackage(m ArtifactMatch, s *System) error { a, err := l.downloadPackage(m) if err != nil && !l.Options.Force { return errors.Wrap(err, "Failed downloading package") } files, err := a.FileList() if err != nil && !l.Options.Force { return errors.Wrap(err, "Could not open package archive") } err = a.Unpack(s.Target, true) if err != nil && !l.Options.Force { return errors.Wrap(err, "Error met while unpacking rootfs") } // First create client and download // Then unpack to system return s.Database.SetPackageFiles(&pkg.PackageFile{PackageFingerprint: m.Package.GetFingerPrint(), Files: files}) } func (l *LuetInstaller) downloadWorker(i int, wg *sync.WaitGroup, c <-chan ArtifactMatch) error { defer wg.Done() for p := range c { // TODO: Keep trace of what was added from the tar, and save it into system _, err := l.downloadPackage(p) if err != nil { Fatal("Failed downloading package "+p.Package.GetName(), err.Error()) return errors.Wrap(err, "Failed downloading package "+p.Package.GetName()) } else { Info(":package: Package ", p.Package.HumanReadableString(), "downloaded") } } return nil } func (l *LuetInstaller) installerWorker(i int, wg *sync.WaitGroup, c <-chan ArtifactMatch, s *System) error { defer wg.Done() for p := range c { // TODO: Keep trace of what was added from the tar, and save it into system err := l.installPackage(p, s) if err != nil && !l.Options.Force { //TODO: Uninstall, rollback. Fatal("Failed installing package "+p.Package.GetName(), err.Error()) return errors.Wrap(err, "Failed installing package "+p.Package.GetName()) } if err == nil { Info(":package: Package ", p.Package.HumanReadableString(), "installed") } else if err != nil && l.Options.Force { Info(":package: Package ", p.Package.HumanReadableString(), "installed with failures (forced install)") } } return nil } func checkAndPrunePath(path string) { // check if now the target path is empty targetPath := filepath.Dir(path) fi, err := os.Lstat(targetPath) if err != nil { // Warning("Dir not found (it was before?) ", err.Error()) return } switch mode := fi.Mode(); { case mode.IsDir(): files, err := ioutil.ReadDir(targetPath) if err != nil { Warning("Failed reading folder", targetPath, err.Error()) } if len(files) != 0 { Debug("Preserving not-empty folder", targetPath) return } } if err = os.Remove(targetPath); err != nil { Warning("Failed removing file (maybe not present in the system target anymore ?)", targetPath, err.Error()) } } // We will try to cleanup every path from the file, if the folders left behind are empty func pruneEmptyFilePath(path string) { checkAndPrunePath(path) // A path is for e.g. /usr/bin/bar // we want to create an array as "/usr", "/usr/bin", "/usr/bin/bar" paths := strings.Split(path, string(os.PathSeparator)) currentPath := filepath.Join(string(os.PathSeparator), paths[0]) allPaths := []string{currentPath} for _, p := range paths[1:] { currentPath = filepath.Join(currentPath, p) allPaths = append(allPaths, currentPath) } match.ReverseAny(allPaths) for _, p := range allPaths { checkAndPrunePath(p) } } func (l *LuetInstaller) uninstall(p pkg.Package, s *System) error { var cp *config.ConfigProtect annotationDir := "" files, err := s.Database.GetPackageFiles(p) if err != nil { return errors.Wrap(err, "Failed getting installed files") } if !config.LuetCfg.ConfigProtectSkip { if p.HasAnnotation(string(pkg.ConfigProtectAnnnotation)) { dir, ok := p.GetAnnotations()[string(pkg.ConfigProtectAnnnotation)] if ok { annotationDir = dir } } cp = config.NewConfigProtect(annotationDir) cp.Map(files) } toRemove, notPresent := fileHelper.OrderFiles(s.Target, files) // Remove from target for _, f := range toRemove { target := filepath.Join(s.Target, f) if !config.LuetCfg.ConfigProtectSkip && cp.Protected(f) { Debug("Preserving protected file:", f) continue } Debug("Removing", target) if l.Options.PreserveSystemEssentialData && strings.HasPrefix(f, config.LuetCfg.GetSystem().GetSystemPkgsCacheDirPath()) || strings.HasPrefix(f, config.LuetCfg.GetSystem().GetSystemRepoDatabaseDirPath()) { Warning("Preserve ", f, " which is required by luet ( you have to delete it manually if you really need to)") continue } fi, err := os.Lstat(target) if err != nil { Warning("File not found (it was before?) ", err.Error()) continue } switch mode := fi.Mode(); { case mode.IsDir(): files, err := ioutil.ReadDir(target) if err != nil { Warning("Failed reading folder", target, err.Error()) } if len(files) != 0 { Debug("Preserving not-empty folder", target) continue } } if err = os.Remove(target); err != nil { Warning("Failed removing file (maybe not present in the system target anymore ?)", target, err.Error()) } pruneEmptyFilePath(target) } for _, f := range notPresent { target := filepath.Join(s.Target, f) if !config.LuetCfg.ConfigProtectSkip && cp.Protected(f) { Debug("Preserving protected file:", f) continue } if err = os.Remove(target); err != nil { Debug("Failed removing file (not present in the system target)", target, err.Error()) } pruneEmptyFilePath(target) } err = s.Database.RemovePackageFiles(p) if err != nil { return errors.Wrap(err, "Failed removing package files from database") } err = s.Database.RemovePackage(p) if err != nil { return errors.Wrap(err, "Failed removing package from database") } bus.Manager.Publish(bus.EventPackageUnInstall, p) Info(":recycle: ", p.GetFingerPrint(), "Removed :heavy_check_mark:") return nil } func (l *LuetInstaller) computeUninstall(o Option, s *System, packs ...pkg.Package) (pkg.Packages, error) { var toUninstall pkg.Packages // compute uninstall from all world - remove packages in parallel - run uninstall finalizer (in order) TODO - mark the uninstallation in db // Get installed definition checkConflicts := o.CheckConflicts full := o.FullUninstall // if o.Force == true { // IF forced, we want to remove the package and all its requires // checkConflicts = false // full = false // } // Create a temporary DB with the installed packages // so the solver is much faster finding the deptree // First check what would have been done installedtmp, err := s.Database.Copy() if err != nil { return toUninstall, errors.Wrap(err, "Failed create temporary in-memory db") } if !o.NoDeps { solv := solver.NewResolver(solver.Options{Type: l.Options.SolverOptions.Implementation, Concurrency: l.Options.Concurrency}, installedtmp, installedtmp, pkg.NewInMemoryDatabase(false), l.Options.SolverOptions.Resolver()) var solution pkg.Packages var err error if o.FullCleanUninstall { solution, err = solv.UninstallUniverse(packs) if err != nil { return toUninstall, errors.Wrap(err, "Could not solve the uninstall constraints. Tip: try with --solver-type qlearning or with --force, or by removing packages excluding their dependencies with --nodeps") } } else { solution, err = solv.Uninstall(checkConflicts, full, packs...) if err != nil && !l.Options.Force { return toUninstall, errors.Wrap(err, "Could not solve the uninstall constraints. Tip: try with --solver-type qlearning or with --force, or by removing packages excluding their dependencies with --nodeps") } } for _, p := range solution { toUninstall = append(toUninstall, p) } } else { toUninstall = append(toUninstall, packs...) } return toUninstall, nil } func (l *LuetInstaller) generateUninstallFn(o Option, s *System, packs ...pkg.Package) (pkg.Packages, func() error, error) { for _, p := range packs { if packs, _ := s.Database.FindPackages(p); len(packs) == 0 { return nil, nil, errors.New(fmt.Sprintf("Package %s not found in the system", p.HumanReadableString())) } } toUninstall, err := l.computeUninstall(o, s, packs...) if err != nil { return nil, nil, errors.Wrap(err, "while computing uninstall") } uninstall := func() error { for _, p := range toUninstall { err := l.uninstall(p, s) if err != nil && !o.Force { return errors.Wrap(err, "Uninstall failed") } } return nil } return toUninstall, uninstall, nil } func (l *LuetInstaller) Uninstall(s *System, packs ...pkg.Package) error { Spinner(32) o := Option{ FullUninstall: l.Options.FullUninstall, Force: l.Options.Force, CheckConflicts: l.Options.CheckConflicts, FullCleanUninstall: l.Options.FullCleanUninstall, } toUninstall, uninstall, err := l.generateUninstallFn(o, s, packs...) if err != nil { return errors.Wrap(err, "while computing uninstall") } SpinnerStop() if len(toUninstall) == 0 { Info("Nothing to do") return nil } if l.Options.Ask { Info(":recycle: Packages that are going to be removed from the system:\n ", Yellow(packsToList(toUninstall)).BgBlack().String()) if Ask() { l.Options.Ask = false // Don't prompt anymore return uninstall() } else { return errors.New("Aborted by user") } } return uninstall() }