Change remove subcommand behaviour to show error when breaking installed package dependencies

This commit is contained in:
2025-10-09 12:35:58 +03:00
parent ac1ba2ada3
commit c5f48becf8
4 changed files with 57 additions and 19 deletions
+9 -3
View File
@@ -92,7 +92,6 @@ func main() {
currentFlagSet.BoolP("verbose", "v", false, "Show additional information about the current operation") currentFlagSet.BoolP("verbose", "v", false, "Show additional information about the current operation")
currentFlagSet.BoolP("force", "f", false, "Bypass warnings during package removal") currentFlagSet.BoolP("force", "f", false, "Bypass warnings during package removal")
currentFlagSet.BoolP("yes", "y", false, "Enter 'yes' in all prompts") 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") currentFlagSet.BoolP("cleanup", "n", false, "Additionally remove all unused dependencies")
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("bpm %s <options>", subcommand), "Remove the specified packages", os.Args[2:]) setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("bpm %s <options>", subcommand), "Remove the specified packages", os.Args[2:])
@@ -496,7 +495,6 @@ func removePackages() {
verbose, _ := currentFlagSet.GetBool("verbose") verbose, _ := currentFlagSet.GetBool("verbose")
force, _ := currentFlagSet.GetBool("force") force, _ := currentFlagSet.GetBool("force")
yesAll, _ := currentFlagSet.GetBool("yes") yesAll, _ := currentFlagSet.GetBool("yes")
removeUnused, _ := currentFlagSet.GetBool("unused")
cleanupPackages, _ := currentFlagSet.GetBool("cleanup") cleanupPackages, _ := currentFlagSet.GetBool("cleanup")
// Get packages // Get packages
@@ -527,8 +525,16 @@ func removePackages() {
} }
// Create remove operation // 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{}) { 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) log.Printf("Error: %s", err)
exitCode = 1 exitCode = 1
return return
+4 -7
View File
@@ -198,12 +198,9 @@ func GetPackageDependants(pkgName string, rootDir string) ([]string, error) {
continue continue
} }
// Get installed package dependencies
dependencies := installedPkg.PkgInfo.GetDependencies(false, true)
// Add installed package to list if its dependencies include pkgName // Add installed package to list if its dependencies include pkgName
if slices.ContainsFunc(dependencies, func(p pkgInstallationReason) bool { if slices.ContainsFunc(installedPkg.PkgInfo.Depends, func(n string) bool {
return p.PkgName == pkgName return n == pkgName
}) { }) {
ret = append(ret, installedPkgName) ret = append(ret, installedPkgName)
continue continue
@@ -212,8 +209,8 @@ func GetPackageDependants(pkgName string, rootDir string) ([]string, error) {
// Loop through each virtual package // Loop through each virtual package
for _, vpkg := range pkg.PkgInfo.Provides { for _, vpkg := range pkg.PkgInfo.Provides {
// Add installed package to list if its dependencies contain a provided virtual package // Add installed package to list if its dependencies contain a provided virtual package
if slices.ContainsFunc(dependencies, func(p pkgInstallationReason) bool { if slices.ContainsFunc(installedPkg.PkgInfo.Depends, func(n string) bool {
return p.PkgName == vpkg return n == vpkg
}) { }) {
ret = append(ret, installedPkgName) ret = append(ret, installedPkgName)
break break
+8
View File
@@ -40,3 +40,11 @@ type PackageScriptErr struct {
func (e PackageScriptErr) Error() string { func (e PackageScriptErr) Error() string {
return fmt.Sprintf("could not execute package script (%s) for package (%s): %s", e.packageScript, e.packageName, e.err) 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"
}
+36 -9
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"maps"
"os" "os"
"path" "path"
"slices" "slices"
@@ -186,7 +187,7 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
} }
// RemovePackages removes the specified packages from the given root directory // 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{ operation = &BPMOperation{
Actions: make([]OperationAction, 0), Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0), UnresolvedDepends: make([]string, 0),
@@ -204,14 +205,6 @@ func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencie
operation.AppendAction(&RemovePackageAction{BpmPackage: bpmpkg}) 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 // Do package cleanup
if cleanupDependencies { if cleanupDependencies {
err := operation.Cleanup(true) 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 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 return operation, nil
} }