7 Commits
7 changed files with 125 additions and 58 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ ROOT_COMPILATION_GID ?= 65534
build: build:
mkdir -p build mkdir -p build
cd src/bpm; $(GO) build -ldflags "-w -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationUID=$(ROOT_COMPILATION_UID)' -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationGID=$(ROOT_COMPILATION_GID)'" -o ../../build/bpm git.enumerated.dev/bubble-package-manager/bpm/src/bpm cd src/bpm; $(GO) build $(GOFLAGS) -ldflags "-w -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationUID=$(ROOT_COMPILATION_UID)' -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationGID=$(ROOT_COMPILATION_GID)'" -o ../../build/bpm git.enumerated.dev/bubble-package-manager/bpm/src/bpm
install: build/bpm config/ install: build/bpm config/
# Create directory # Create directory
+57 -36
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:])
@@ -433,15 +432,12 @@ func installPackages() {
// Confirmation Prompt // Confirmation Prompt
if !yesAll { if !yesAll {
reader := bufio.NewReader(os.Stdin) prompt := "Do you wish to install this package?"
if len(operation.Actions) == 1 { if len(operation.Actions) != 1 {
fmt.Printf("Do you wish to install this package? [y\\N] ") prompt = fmt.Sprintf("Do you wish to install all %d packages?", len(operation.Actions))
} else {
fmt.Printf("Do you wish to install these %d packages? [y\\N] ", len(operation.Actions))
} }
text, _ := reader.ReadString('\n') if !showConfirmationPrompt(prompt, false) {
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package installation...") fmt.Println("Cancelling package installation...")
exitCode = 1 exitCode = 1
return return
@@ -467,11 +463,7 @@ func installPackages() {
// Confirmation Prompt // Confirmation Prompt
if sourcePackagesShown > 0 && !yesAll { if sourcePackagesShown > 0 && !yesAll {
reader := bufio.NewReader(os.Stdin) if !showConfirmationPrompt("Do you wish to continue?", false) {
fmt.Printf("Are you sure you wish to continue? [y\\N] ")
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package installation...") fmt.Println("Cancelling package installation...")
exitCode = 1 exitCode = 1
return return
@@ -503,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
@@ -534,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
@@ -556,10 +555,12 @@ func removePackages() {
// Confirmation Prompt // Confirmation Prompt
if !yesAll { if !yesAll {
fmt.Printf("Are you sure you wish to remove all %d packages? [y\\N] ", len(operation.Actions)) prompt := "Do you wish to remove this package?"
reader := bufio.NewReader(os.Stdin) if len(operation.Actions) != 1 {
text, _ := reader.ReadString('\n') prompt = fmt.Sprintf("Do you wish to remove all %d packages?", len(operation.Actions))
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { }
if !showConfirmationPrompt(prompt, false) {
fmt.Println("Cancelling package removal...") fmt.Println("Cancelling package removal...")
exitCode = 1 exitCode = 1
return return
@@ -667,10 +668,12 @@ func doCleanup() {
// Confirmation Prompt // Confirmation Prompt
if !yesAll { if !yesAll {
fmt.Printf("Are you sure you wish to remove all %d packages? [y\\N] ", len(operation.Actions)) prompt := "Do you wish to remove this package?"
reader := bufio.NewReader(os.Stdin) if len(operation.Actions) != 1 {
text, _ := reader.ReadString('\n') prompt = fmt.Sprintf("Do you wish to remove all %d packages?", len(operation.Actions))
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { }
if !showConfirmationPrompt(prompt, false) {
fmt.Println("Cancelling package removal...") fmt.Println("Cancelling package removal...")
exitCode = 1 exitCode = 1
return return
@@ -720,10 +723,7 @@ func syncDatabases() {
// Confirmation Prompt // Confirmation Prompt
if !yesAll { if !yesAll {
fmt.Printf("Are you sure you wish to sync all databases? [y\\N] ") if !showConfirmationPrompt("Do you wish to sync all databases?", false) {
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling database synchronization...") fmt.Println("Cancelling database synchronization...")
exitCode = 1 exitCode = 1
return return
@@ -797,10 +797,12 @@ func updatePackages() {
// Confirmation Prompt // Confirmation Prompt
if !yesAll { if !yesAll {
fmt.Printf("Are you sure you wish to update all %d packages? [y\\N] ", len(operation.Actions)) prompt := "Do you wish to update this package?"
reader := bufio.NewReader(os.Stdin) if len(operation.Actions) != 1 {
text, _ := reader.ReadString('\n') prompt = fmt.Sprintf("Do you wish to update all %d packages?", len(operation.Actions))
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { }
if !showConfirmationPrompt(prompt, false) {
fmt.Println("Cancelling package update...") fmt.Println("Cancelling package update...")
exitCode = 1 exitCode = 1
return return
@@ -826,11 +828,7 @@ func updatePackages() {
// Confirmation Prompt // Confirmation Prompt
if sourcePackagesShown > 0 && !yesAll { if sourcePackagesShown > 0 && !yesAll {
reader := bufio.NewReader(os.Stdin) if !showConfirmationPrompt("Do you wish to continue?", false) {
fmt.Printf("Are you sure you wish to continue? [y\\N] ")
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package installation...") fmt.Println("Cancelling package installation...")
exitCode = 1 exitCode = 1
return return
@@ -1208,3 +1206,26 @@ func isFlagSet(flagSet *flag.FlagSet, name string) bool {
}) })
return found return found
} }
func showConfirmationPrompt(prompt string, defaultTo bool) bool {
reader := bufio.NewReader(os.Stdin)
if defaultTo {
fmt.Printf("%s [Y/n] ", prompt)
} else {
fmt.Printf("%s [y/N] ", prompt)
}
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
if len(text) > 0 {
switch text[0] {
case 'y', 'Y':
return true
case 'n', 'N':
return false
}
}
return defaultTo
}
+3 -3
View File
@@ -6,6 +6,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"slices"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -217,9 +218,8 @@ func FindReplacement(pkg string) *BPMDatabaseEntry {
func ResolveVirtualPackage(vpkg string) *BPMDatabaseEntry { func ResolveVirtualPackage(vpkg string) *BPMDatabaseEntry {
for _, db := range BPMDatabases { for _, db := range BPMDatabases {
if v, ok := db.VirtualPackages[vpkg]; ok { if v, ok := db.VirtualPackages[vpkg]; ok {
for _, pkg := range v { slices.Sort(v)
return db.Entries[pkg] return db.Entries[v[0]]
}
} }
} }
+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"
}
+40 -10
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),
@@ -198,27 +199,56 @@ func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencie
// Search for packages // Search for packages
for _, pkg := range packages { for _, pkg := range packages {
bpmpkg := GetPackage(pkg, rootDir) bpmpkg := GetPackage(pkg, rootDir)
if isVirutal, vpkg := IsVirtualPackage(pkg, rootDir); isVirutal {
bpmpkg = GetPackage(vpkg, rootDir)
}
if bpmpkg == nil { if bpmpkg == nil {
continue continue
} }
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(MainBPMConfig.CleanupMakeDependencies)
if err != nil { if err != nil {
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
} }
+12 -1
View File
@@ -10,6 +10,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"regexp"
"slices" "slices"
"sort" "sort"
"strconv" "strconv"
@@ -461,11 +462,21 @@ func ReadPackageInfo(contents string) (*PackageInfo, error) {
} }
} }
// Ensure package name is valid
if match, _ := regexp.MatchString("^[a-zA-Z0-9._-]+$", pkgInfo.Name); !match {
return nil, fmt.Errorf("package name (%s) is invalid", pkgInfo.Name)
}
// Setup split package information // Setup split package information
for i, splitPkg := range pkgInfo.SplitPackages { for i, splitPkg := range pkgInfo.SplitPackages {
// Ensure split package contains a name // Ensure split package contains a name
if splitPkg.Name == "" { if splitPkg.Name == "" {
return nil, fmt.Errorf("invalid split package name: %s", splitPkg.Name) return nil, fmt.Errorf("package name (%s) is invalid", splitPkg.Name)
}
// Ensure split package name is valid
if match, _ := regexp.MatchString("^[a-zA-Z0-9._-]+$", splitPkg.Name); !match {
return nil, fmt.Errorf("package name (%s) is invalid", splitPkg.Name)
} }
// Turn split package into yaml data // Turn split package into yaml data