From c5f48becf863436cfc63df207ec7be8e8ac1b961 Mon Sep 17 00:00:00 2001 From: EnumDev Date: Thu, 9 Oct 2025 12:35:58 +0300 Subject: [PATCH] Change remove subcommand behaviour to show error when breaking installed package dependencies --- src/bpm/main.go | 12 +++++++--- src/bpmlib/dependencies.go | 11 ++++------ src/bpmlib/errors.go | 8 +++++++ src/bpmlib/general.go | 45 ++++++++++++++++++++++++++++++-------- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/bpm/main.go b/src/bpm/main.go index eb2c221..25890ab 100644 --- a/src/bpm/main.go +++ b/src/bpm/main.go @@ -92,7 +92,6 @@ func main() { currentFlagSet.BoolP("verbose", "v", false, "Show additional information about the current operation") currentFlagSet.BoolP("force", "f", false, "Bypass warnings during package removal") currentFlagSet.BoolP("yes", "y", false, "Enter 'yes' in all prompts") - currentFlagSet.BoolP("unused", "u", false, "Remove packages only if they are not required as dependencies") currentFlagSet.BoolP("cleanup", "n", false, "Additionally remove all unused dependencies") setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("bpm %s ", subcommand), "Remove the specified packages", os.Args[2:]) @@ -496,7 +495,6 @@ func removePackages() { verbose, _ := currentFlagSet.GetBool("verbose") force, _ := currentFlagSet.GetBool("force") yesAll, _ := currentFlagSet.GetBool("yes") - removeUnused, _ := currentFlagSet.GetBool("unused") cleanupPackages, _ := currentFlagSet.GetBool("cleanup") // Get packages @@ -527,8 +525,16 @@ func removePackages() { } // Create remove operation - operation, err := bpmlib.RemovePackages(rootDir, removeUnused, cleanupPackages, packages...) + operation, err := bpmlib.RemovePackages(rootDir, force, cleanupPackages, packages...) if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + log.Printf("Error: %s", err) + exitCode = 1 + return + } else if errors.As(err, &bpmlib.PackageRemovalDependencyErr{}) { + for pkg, dependants := range err.(bpmlib.PackageRemovalDependencyErr).RequiredPackages { + fmt.Printf("The following packages depend on package (%s): %s\n", pkg, strings.Join(dependants, ", ")) + } + log.Printf("Error: %s", err) exitCode = 1 return diff --git a/src/bpmlib/dependencies.go b/src/bpmlib/dependencies.go index beecdf7..68c143c 100644 --- a/src/bpmlib/dependencies.go +++ b/src/bpmlib/dependencies.go @@ -198,12 +198,9 @@ func GetPackageDependants(pkgName string, rootDir string) ([]string, error) { continue } - // Get installed package dependencies - dependencies := installedPkg.PkgInfo.GetDependencies(false, true) - // Add installed package to list if its dependencies include pkgName - if slices.ContainsFunc(dependencies, func(p pkgInstallationReason) bool { - return p.PkgName == pkgName + if slices.ContainsFunc(installedPkg.PkgInfo.Depends, func(n string) bool { + return n == pkgName }) { ret = append(ret, installedPkgName) continue @@ -212,8 +209,8 @@ func GetPackageDependants(pkgName string, rootDir string) ([]string, error) { // Loop through each virtual package for _, vpkg := range pkg.PkgInfo.Provides { // Add installed package to list if its dependencies contain a provided virtual package - if slices.ContainsFunc(dependencies, func(p pkgInstallationReason) bool { - return p.PkgName == vpkg + if slices.ContainsFunc(installedPkg.PkgInfo.Depends, func(n string) bool { + return n == vpkg }) { ret = append(ret, installedPkgName) break diff --git a/src/bpmlib/errors.go b/src/bpmlib/errors.go index 8b46b94..e4b54ad 100644 --- a/src/bpmlib/errors.go +++ b/src/bpmlib/errors.go @@ -40,3 +40,11 @@ type PackageScriptErr struct { func (e PackageScriptErr) Error() string { return fmt.Sprintf("could not execute package script (%s) for package (%s): %s", e.packageScript, e.packageName, e.err) } + +type PackageRemovalDependencyErr struct { + RequiredPackages map[string][]string +} + +func (e PackageRemovalDependencyErr) Error() string { + return "removing these package would break other installed packages" +} diff --git a/src/bpmlib/general.go b/src/bpmlib/general.go index d7cb128..7d4ee0f 100644 --- a/src/bpmlib/general.go +++ b/src/bpmlib/general.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "maps" "os" "path" "slices" @@ -186,7 +187,7 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason, } // RemovePackages removes the specified packages from the given root directory -func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencies bool, packages ...string) (operation *BPMOperation, err error) { +func RemovePackages(rootDir string, force, cleanupDependencies bool, packages ...string) (operation *BPMOperation, err error) { operation = &BPMOperation{ Actions: make([]OperationAction, 0), UnresolvedDepends: make([]string, 0), @@ -204,14 +205,6 @@ func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencie operation.AppendAction(&RemovePackageAction{BpmPackage: bpmpkg}) } - // Do not remove packages which other packages depend on - if removeUnusedPackagesOnly { - err := operation.RemoveNeededPackages() - if err != nil { - return nil, fmt.Errorf("could not skip needed packages: %s", err) - } - } - // Do package cleanup if cleanupDependencies { err := operation.Cleanup(true) @@ -219,6 +212,40 @@ func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencie return nil, fmt.Errorf("could not perform cleanup for operation: %s", err) } } + + // Return error if other packages depend on removed ones + if !force { + // Get packages and their dependants + packageDepndants := make(map[string][]string, 0) + for _, action := range operation.Actions { + dependants, err := GetPackageDependants(action.(*RemovePackageAction).BpmPackage.PkgInfo.Name, rootDir) + if err != nil { + return nil, fmt.Errorf("could not get package dependants: %s", err) + } + + packageDepndants[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = dependants + } + + // Remove dependant packages from map if they are to be removed by this operation + for pkg, required := range packageDepndants { + required = slices.DeleteFunc(required, func(pkgName string) bool { + _, ok := packageDepndants[pkgName] + return ok + }) + packageDepndants[pkg] = required + } + + // Remove empty keys from map + maps.DeleteFunc(packageDepndants, func(pkg string, required []string) bool { + return len(required) == 0 + }) + + // Return error + if len(packageDepndants) != 0 { + return nil, PackageRemovalDependencyErr{RequiredPackages: packageDepndants} + } + } + return operation, nil }