16 Commits
Author SHA1 Message Date
EnumDev 45c536b326 Allow additional GOFLAGS in makefile 2025-10-09 16:42:03 +03:00
EnumDev 705aec8e6e Resolve virtual packages in remove subcommand 2025-10-09 14:14:14 +03:00
EnumDev 34fb7ce1d6 Ensure virtual packages are sorted alphabetically 2025-10-09 14:08:22 +03:00
EnumDev fee0e6a96f Make cleanup flag in the remove subcommand use the config option for
make dependencies
2025-10-09 12:37:51 +03:00
EnumDev c5f48becf8 Change remove subcommand behaviour to show error when breaking installed package dependencies 2025-10-09 12:35:58 +03:00
EnumDev ac1ba2ada3 Improve confirmation prompts 2025-10-08 20:46:44 +03:00
EnumDev 5fb8211933 Validate package names using regex 2025-10-07 20:50:08 +03:00
EnumDev 4110ad9c5c Create different output directory for each split package during
compilation
2025-10-07 19:00:17 +03:00
EnumDev dcff59d560 Add 'all' flag to cleanup subcommand 2025-10-07 18:24:14 +03:00
EnumDev 1594eec5ce Remove subcommand will now run without specifying any packages 2025-10-07 18:20:48 +03:00
EnumDev 0c30d85589 The 'cleanup' flag for the remove subcommand will now use 'n' for its
shorthand form similar to the 'cleanup' subcommand
2025-10-07 18:19:30 +03:00
EnumDev 768c75c9bd Cleanup subcommand will now remove optional dependencies. Optional dependencies will now be installed as manual when passing the 'optional' flag to the install subcommand 2025-10-07 18:17:42 +03:00
EnumDev abc6674b97 Run 'cleanup' subcommand when 'compile' subcommand encounters an error 2025-10-06 20:10:20 +03:00
EnumDev 1d858076fd Add 'yes' flag to cleanup subcommand 2025-10-06 20:09:36 +03:00
EnumDev 5909370407 Improve operation summary 2025-10-06 16:38:03 +03:00
EnumDev 5a78fd3002 Show source package contents during package installation 2025-10-06 13:33:18 +03:00
11 changed files with 405 additions and 133 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
+1
View File
@@ -1,4 +1,5 @@
ignore_packages: [] ignore_packages: []
show_source_package_contents: always
cleanup_make_dependencies: true cleanup_make_dependencies: true
databases: databases:
- name: example-database - name: example-database
+163 -59
View File
@@ -92,8 +92,7 @@ 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", "c", 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:])
removePackages() removePackages()
@@ -103,6 +102,8 @@ func main() {
currentFlagSet.StringP("root", "R", "/", "Operate on specified root directory") currentFlagSet.StringP("root", "R", "/", "Operate on specified root directory")
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 cleanup") currentFlagSet.BoolP("force", "f", false, "Bypass warnings during package cleanup")
currentFlagSet.BoolP("yes", "y", false, "Enter 'yes' in all prompts")
currentFlagSet.BoolP("all", "a", false, "Perform all types of cleanup")
currentFlagSet.BoolP("depends", "d", false, "Perform a dependency cleanup") currentFlagSet.BoolP("depends", "d", false, "Perform a dependency cleanup")
currentFlagSet.BoolP("make-depends", "m", false, "Perform a make dependency cleanup") currentFlagSet.BoolP("make-depends", "m", false, "Perform a make dependency cleanup")
currentFlagSet.BoolP("compilation-files", "c", false, "Perform a cleanup of compilation files") currentFlagSet.BoolP("compilation-files", "c", false, "Perform a cleanup of compilation files")
@@ -431,21 +432,45 @@ 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
} }
} }
// Fetch packages
err = operation.FetchPackages()
if err != nil {
log.Printf("Error: could not fetch packages for operation: %s\n", err)
exitCode = 1
return
}
if bpmlib.MainBPMConfig.ShowSourcePackageContents == "always" || bpmlib.MainBPMConfig.ShowSourcePackageContents == "install-only" {
// Show source package contents
sourcePackagesShown, err := operation.ShowSourcePackageContent()
if err != nil {
log.Printf("Error: could not show source package content: %s\n", err)
exitCode = 1
return
}
// Confirmation Prompt
if sourcePackagesShown > 0 && !yesAll {
if !showConfirmationPrompt("Do you wish to continue?", false) {
fmt.Println("Cancelling package installation...")
exitCode = 1
return
}
}
}
// Execute operation // Execute operation
err = operation.Execute(verbose, force) err = operation.Execute(verbose, force)
if err != nil { if err != nil {
@@ -470,15 +495,10 @@ 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
packages := currentFlagSet.Args() packages := currentFlagSet.Args()
if len(packages) == 0 {
fmt.Println("No packages were given to remove")
return
}
// Check for required permissions // Check for required permissions
if os.Getuid() != 0 { if os.Getuid() != 0 {
@@ -505,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
@@ -527,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
@@ -561,6 +591,7 @@ func doCleanup() {
verbose, _ := currentFlagSet.GetBool("verbose") verbose, _ := currentFlagSet.GetBool("verbose")
force, _ := currentFlagSet.GetBool("force") force, _ := currentFlagSet.GetBool("force")
yesAll, _ := currentFlagSet.GetBool("yes") yesAll, _ := currentFlagSet.GetBool("yes")
all, _ := currentFlagSet.GetBool("all")
cleanupDepends, _ := currentFlagSet.GetBool("depends") cleanupDepends, _ := currentFlagSet.GetBool("depends")
cleanupMakeDepends, _ := currentFlagSet.GetBool("make-depends") cleanupMakeDepends, _ := currentFlagSet.GetBool("make-depends")
cleanupCompilationFiles, _ := currentFlagSet.GetBool("compilation-files") cleanupCompilationFiles, _ := currentFlagSet.GetBool("compilation-files")
@@ -568,7 +599,13 @@ func doCleanup() {
cleanupFetchedPackages, _ := currentFlagSet.GetBool("fetched-packages") cleanupFetchedPackages, _ := currentFlagSet.GetBool("fetched-packages")
// Set default behaviour // Set default behaviour
if !isFlagSet(currentFlagSet, "depends") && !isFlagSet(currentFlagSet, "make-depends") && !isFlagSet(currentFlagSet, "compilation-files") && !isFlagSet(currentFlagSet, "binary-packages") && !isFlagSet(currentFlagSet, "fetched-packages") { if all {
cleanupDepends = true
cleanupMakeDepends = bpmlib.MainBPMConfig.CleanupMakeDependencies
cleanupCompilationFiles = true
cleanupBinaryPackages = true
cleanupFetchedPackages = true
} else if !isFlagSet(currentFlagSet, "depends") && !isFlagSet(currentFlagSet, "make-depends") && !isFlagSet(currentFlagSet, "compilation-files") && !isFlagSet(currentFlagSet, "binary-packages") && !isFlagSet(currentFlagSet, "fetched-packages") {
cleanupDepends = true cleanupDepends = true
cleanupMakeDepends = bpmlib.MainBPMConfig.CleanupMakeDependencies cleanupMakeDepends = bpmlib.MainBPMConfig.CleanupMakeDependencies
cleanupCompilationFiles = false cleanupCompilationFiles = false
@@ -631,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
@@ -684,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
@@ -761,16 +797,45 @@ 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
} }
} }
// Fetch packages
err = operation.FetchPackages()
if err != nil {
log.Printf("Error: could not fetch packages for operation: %s\n", err)
exitCode = 1
return
}
if bpmlib.MainBPMConfig.ShowSourcePackageContents == "always" {
// Show source package contents
sourcePackagesShown, err := operation.ShowSourcePackageContent()
if err != nil {
log.Printf("Error: could not show source package content: %s\n", err)
exitCode = 1
return
}
// Confirmation Prompt
if sourcePackagesShown > 0 && !yesAll {
if !showConfirmationPrompt("Do you wish to continue?", false) {
fmt.Println("Cancelling package installation...")
exitCode = 1
return
}
}
}
// Execute operation // Execute operation
err = operation.Execute(verbose, force) err = operation.Execute(verbose, force)
if err != nil { if err != nil {
@@ -982,9 +1047,39 @@ func compilePackage() {
} }
} }
// Setup cleanup function
cleanupFunc := func() {
if installSrcPkgDepends && len(unmetDepends) > 0 {
// Get path to current executable
executable, err := os.Executable()
if err != nil {
log.Printf("Warning: could not get path to executable: %s\n", err)
}
// Run 'bpm cleanup' using the set privilege escalator command
cmd := exec.Command(bpmlib.CompilationBPMConfig.PrivilegeEscalatorCmd, executable, "cleanup")
if yesAll {
cmd.Args = slices.Insert(cmd.Args, 3, "-y")
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if verbose {
fmt.Println("Running command: " + cmd.String())
}
err = cmd.Run()
if err != nil {
log.Printf("Warning: dependency cleanup command failed: %s\n", err)
}
}
}
// Get current working directory // Get current working directory
workdir, err := os.Getwd() workdir, err := os.Getwd()
if err != nil { if err != nil {
// Remove unused packages
cleanupFunc()
log.Printf("Error: could not get working directory: %s", err) log.Printf("Error: could not get working directory: %s", err)
exitCode = 1 exitCode = 1
return return
@@ -993,6 +1088,9 @@ func compilePackage() {
// Get user home directory // Get user home directory
homedir, err := os.UserHomeDir() homedir, err := os.UserHomeDir()
if err != nil { if err != nil {
// Remove unused packages
cleanupFunc()
log.Printf("Error: could not get user home directory: %s", err) log.Printf("Error: could not get user home directory: %s", err)
exitCode = 1 exitCode = 1
return return
@@ -1025,11 +1123,17 @@ func compilePackage() {
// Ensure output directory exists and is a directory // Ensure output directory exists and is a directory
stat, err := os.Stat(outputDirectory) stat, err := os.Stat(outputDirectory)
if err != nil { if err != nil {
// Remove unused packages
cleanupFunc()
log.Printf("Error: could not stat output directory (%s): %s", outputDirectory, err) log.Printf("Error: could not stat output directory (%s): %s", outputDirectory, err)
exitCode = 1 exitCode = 1
return return
} }
if !stat.IsDir() { if !stat.IsDir() {
// Remove unused packages
cleanupFunc()
log.Printf("Error: output directory (%s) is not a directory", outputDirectory) log.Printf("Error: output directory (%s) is not a directory", outputDirectory)
exitCode = 1 exitCode = 1
return return
@@ -1037,6 +1141,9 @@ func compilePackage() {
outputBpmPackages, err := bpmlib.CompileSourcePackage(sourcePackage, outputDirectory, skipChecks) outputBpmPackages, err := bpmlib.CompileSourcePackage(sourcePackage, outputDirectory, skipChecks)
if err != nil { if err != nil {
// Remove unused packages
cleanupFunc()
log.Printf("Error: could not compile source package (%s): %s", sourcePackage, err) log.Printf("Error: could not compile source package (%s): %s", sourcePackage, err)
exitCode = 1 exitCode = 1
return return
@@ -1057,33 +1164,7 @@ func compilePackage() {
} }
// Remove unused packages // Remove unused packages
if installSrcPkgDepends && len(unmetDepends) > 0 { cleanupFunc()
// Get path to current executable
executable, err := os.Executable()
if err != nil {
log.Printf("Error: could not get path to executable: %s\n", err)
exitCode = 1
return
}
// Run 'bpm cleanup' using the set privilege escalator command
cmd := exec.Command(bpmlib.CompilationBPMConfig.PrivilegeEscalatorCmd, executable, "cleanup")
if yesAll {
cmd.Args = slices.Insert(cmd.Args, 3, "-y")
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if verbose {
fmt.Println("Running command: " + cmd.String())
}
err = cmd.Run()
if err != nil {
log.Printf("Error: dependency cleanup command failed: %s\n", err)
exitCode = 1
return
}
}
} }
} }
@@ -1125,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
}
+96 -23
View File
@@ -132,7 +132,6 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
env = append(env, "HOME="+tempDirectory) env = append(env, "HOME="+tempDirectory)
env = append(env, "BPM_WORKDIR="+tempDirectory) env = append(env, "BPM_WORKDIR="+tempDirectory)
env = append(env, "BPM_SOURCE="+path.Join(tempDirectory, "source")) env = append(env, "BPM_SOURCE="+path.Join(tempDirectory, "source"))
env = append(env, "BPM_OUTPUT="+path.Join(tempDirectory, "output"))
env = append(env, "BPM_PKG_NAME="+bpmpkg.PkgInfo.Name) env = append(env, "BPM_PKG_NAME="+bpmpkg.PkgInfo.Name)
env = append(env, "BPM_PKG_VERSION="+bpmpkg.PkgInfo.Version) env = append(env, "BPM_PKG_VERSION="+bpmpkg.PkgInfo.Version)
env = append(env, "BPM_PKG_REVISION="+strconv.Itoa(bpmpkg.PkgInfo.Revision)) env = append(env, "BPM_PKG_REVISION="+strconv.Itoa(bpmpkg.PkgInfo.Revision))
@@ -188,6 +187,21 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
packagesToCompile = append(packagesToCompile, bpmpkg.PkgInfo) packagesToCompile = append(packagesToCompile, bpmpkg.PkgInfo)
} }
// Create output directories for each package
for _, pkg := range packagesToCompile {
// Create new output directory
err = os.Mkdir(path.Join(tempDirectory, "output_"+pkg.Name), 0755)
if err != nil {
return nil, err
}
// Change output directory owner
err = os.Chown(path.Join(tempDirectory, "output_"+pkg.Name), uid, gid)
if err != nil {
return nil, err
}
}
// Compile each package // Compile each package
for _, pkg := range packagesToCompile { for _, pkg := range packagesToCompile {
// Get package function name // Get package function name
@@ -196,26 +210,6 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
packageFunctionName = "package_" + pkg.Name packageFunctionName = "package_" + pkg.Name
} }
// Remove output directory if it already exists
if _, err := os.Stat(path.Join(tempDirectory, "output")); err == nil {
err := os.RemoveAll(path.Join(tempDirectory, "output"))
if err != nil {
return nil, err
}
}
// Create new output directory
err = os.Mkdir(path.Join(tempDirectory, "output"), 0755)
if err != nil {
return nil, err
}
// Change output directory owner
err = os.Chown(path.Join(tempDirectory, "output"), uid, gid)
if err != nil {
return nil, err
}
// Execute package function in source.sh script and generate package file list // Execute package function in source.sh script and generate package file list
cmd = exec.Command("bash", "-c", cmd = exec.Command("bash", "-c",
"set -a\n"+ // Source and export functions and variables in source.sh script "set -a\n"+ // Source and export functions and variables in source.sh script
@@ -224,10 +218,11 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
"echo \"Running "+packageFunctionName+"() function\"\n"+ "echo \"Running "+packageFunctionName+"() function\"\n"+
"( cd \"$BPM_SOURCE\" && fakeroot -s \"$BPM_WORKDIR\"/fakeroot_file bash -e -c '"+packageFunctionName+"' ) || exit 1\n"+ // Run package() function "( cd \"$BPM_SOURCE\" && fakeroot -s \"$BPM_WORKDIR\"/fakeroot_file bash -e -c '"+packageFunctionName+"' ) || exit 1\n"+ // Run package() function
"fakeroot -i \"$BPM_WORKDIR\"/fakeroot_file find \"$BPM_OUTPUT\" -mindepth 1 -printf \"%P %#m %U %G %s\\n\" > \"$BPM_WORKDIR\"/pkg.files") // Create package file list "fakeroot -i \"$BPM_WORKDIR\"/fakeroot_file find \"$BPM_OUTPUT\" -mindepth 1 -printf \"%P %#m %U %G %s\\n\" > \"$BPM_WORKDIR\"/pkg.files") // Create package file list
cmd.Dir = tempDirectory cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = env cmd.Env = append(env, "BPM_OUTPUT="+path.Join(tempDirectory, "output_"+pkg.Name))
if os.Getuid() == 0 { if os.Getuid() == 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
@@ -238,7 +233,7 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
} }
// Create gzip-compressed archive for the package files // Create gzip-compressed archive for the package files
cmd = exec.Command("bash", "-c", "find output -printf \"%P\\n\" | fakeroot -i \"$BPM_WORKDIR\"/fakeroot_file tar -czf files.tar.gz --no-recursion -C output -T -") cmd = exec.Command("bash", "-c", fmt.Sprintf("find %s -printf \"%%P\\n\" | fakeroot -i %s/fakeroot_file tar -czf files.tar.gz --no-recursion -C %s -T -", "output_"+pkg.Name, tempDirectory, "output_"+pkg.Name))
cmd.Dir = tempDirectory cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@@ -330,6 +325,12 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
return nil, err return nil, err
} }
// Remove output directory
err = os.RemoveAll(path.Join(tempDirectory, "output_"+pkg.Name))
if err != nil {
return nil, err
}
outputBpmPackages[pkgInfo.Name] = outputFilename outputBpmPackages[pkgInfo.Name] = outputFilename
} }
@@ -515,3 +516,75 @@ func downloadPackageFiles(pkgInfo *PackageInfo, tempDirectory string) error {
return nil return nil
} }
func showPackageFiles(archiveFilename string) error {
// Read BPM archive
bpmpkg, err := ReadPackage(archiveFilename)
if err != nil {
return err
}
// Ensure package type is 'source'
if bpmpkg.PkgInfo.Type != "source" {
return fmt.Errorf("cannot compile a non-source package")
}
printTarballContent := func(filename string) error {
// Get tarball reader for file
reader, err := readTarballFile(archiveFilename, filename)
if err != nil {
return err
}
// Read file content
data, err := io.ReadAll(reader.tarReader)
if err != nil {
return err
}
// Print file name and content
fmt.Println("==============================")
fmt.Printf("%s: %s\n", bpmpkg.PkgInfo.Name, filename)
fmt.Println("==============================")
fmt.Printf("%s\n", data)
// Close reader
err = reader.file.Close()
if err != nil {
return err
}
return nil
}
// Print pkg.info content
err = printTarballContent("pkg.info")
if err != nil {
return err
}
// Print source files contents
files, err := listTarballContent(archiveFilename)
if err != nil {
return err
}
for _, file := range files {
if !strings.HasPrefix(file, "source-files/") {
continue
}
// Print source file content
err = printTarballContent(file)
if err != nil {
return err
}
}
// Print source.sh content
err = printTarballContent("source.sh")
if err != nil {
return err
}
return nil
}
+2
View File
@@ -8,6 +8,7 @@ import (
type MainBPMConfigStruct struct { type MainBPMConfigStruct struct {
IgnorePackages []string `yaml:"ignore_packages"` IgnorePackages []string `yaml:"ignore_packages"`
ShowSourcePackageContents string `yaml:"show_source_package_contents"`
CleanupMakeDependencies bool `yaml:"cleanup_make_dependencies"` CleanupMakeDependencies bool `yaml:"cleanup_make_dependencies"`
Databases []configDatabase `yaml:"databases"` Databases []configDatabase `yaml:"databases"`
} }
@@ -31,6 +32,7 @@ func ReadConfig() (err error) {
// Set default config options // Set default config options
MainBPMConfig = MainBPMConfigStruct{ MainBPMConfig = MainBPMConfigStruct{
ShowSourcePackageContents: "always",
CleanupMakeDependencies: true, CleanupMakeDependencies: true,
} }
+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]]
}
} }
} }
+11 -14
View File
@@ -31,7 +31,7 @@ func (pkgInfo *PackageInfo) GetDependencies(includeMakeDepends, includeOptionalD
}) { }) {
allDepends = append(allDepends, pkgInstallationReason{ allDepends = append(allDepends, pkgInstallationReason{
PkgName: depend, PkgName: depend,
InstallationReason: InstallationReasonDependency, InstallationReason: InstallationReasonManual,
}) })
} }
} }
@@ -51,23 +51,23 @@ func (pkgInfo *PackageInfo) GetDependencies(includeMakeDepends, includeOptionalD
return allDepends return allDepends
} }
func (pkgInfo *PackageInfo) GetAllDependencies(includeMakeDepends, includeOptionalDepends bool, rootDir string) (resolved []string) { func (pkgInfo *PackageInfo) GetDependenciesRecursive(includeMakeDepends bool, rootDir string) (resolved []string) {
// Initialize slices // Initialize slices
resolved = make([]string, 0) resolved = make([]string, 0)
unresolved := make([]string, 0) unresolved := make([]string, 0)
// Call unexported function // Call unexported function
pkgInfo.getAllDependencies(&resolved, &unresolved, includeMakeDepends, includeOptionalDepends, rootDir) pkgInfo.getDependenciesRecursive(&resolved, &unresolved, includeMakeDepends, rootDir)
return resolved return resolved
} }
func (pkgInfo *PackageInfo) getAllDependencies(resolved *[]string, unresolved *[]string, includeMakeDepends, includeOptionalDepends bool, rootDir string) { func (pkgInfo *PackageInfo) getDependenciesRecursive(resolved *[]string, unresolved *[]string, includeMakeDepends bool, rootDir string) {
// Add current package name to unresolved slice // Add current package name to unresolved slice
*unresolved = append(*unresolved, pkgInfo.Name) *unresolved = append(*unresolved, pkgInfo.Name)
// Loop through all dependencies // Loop through all dependencies
for _, pkgIR := range pkgInfo.GetDependencies(includeMakeDepends, includeOptionalDepends) { for _, pkgIR := range pkgInfo.GetDependencies(includeMakeDepends, false) {
depend := pkgIR.PkgName depend := pkgIR.PkgName
if isVirtual, p := IsVirtualPackage(depend, rootDir); isVirtual { if isVirtual, p := IsVirtualPackage(depend, rootDir); isVirtual {
@@ -86,7 +86,7 @@ func (pkgInfo *PackageInfo) getAllDependencies(resolved *[]string, unresolved *[
dependInfo := GetPackageInfo(depend, rootDir) dependInfo := GetPackageInfo(depend, rootDir)
if dependInfo != nil { if dependInfo != nil {
dependInfo.getAllDependencies(resolved, unresolved, includeMakeDepends, includeOptionalDepends, rootDir) dependInfo.getDependenciesRecursive(resolved, unresolved, includeMakeDepends, rootDir)
} }
} }
} }
@@ -155,7 +155,7 @@ func resolvePackageDependenciesFromDatabase(resolved *[]pkgInstallationReason, u
} }
// Resolve the dependencies of this dependency // Resolve the dependencies of this dependency
resolvePackageDependenciesFromDatabase(resolved, unresolved, entry.Info, checkMake, checkOptional, ignoreInstalled, verbose, rootDir) resolvePackageDependenciesFromDatabase(resolved, unresolved, entry.Info, checkMake, false, ignoreInstalled, verbose, rootDir)
// Move dependency from the unresolved slice to the resolved slice // Move dependency from the unresolved slice to the resolved slice
if !slices.ContainsFunc(*resolved, func(p pkgInstallationReason) bool { if !slices.ContainsFunc(*resolved, func(p pkgInstallationReason) bool {
@@ -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
} }
+63 -17
View File
@@ -8,6 +8,7 @@ import (
"path" "path"
"slices" "slices"
"strings" "strings"
"text/tabwriter"
) )
type BPMOperation struct { type BPMOperation struct {
@@ -15,7 +16,9 @@ type BPMOperation struct {
UnresolvedDepends []string UnresolvedDepends []string
Changes map[string]string Changes map[string]string
RootDir string RootDir string
compiledPackages map[string]string compiledPackages map[string]string
hasFetchedPackages bool
} }
func (operation *BPMOperation) ActionsContainPackage(pkg string) bool { func (operation *BPMOperation) ActionsContainPackage(pkg string) bool {
@@ -227,7 +230,7 @@ func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error {
} }
keepPackages = append(keepPackages, pkg.Name) keepPackages = append(keepPackages, pkg.Name)
resolved := pkg.GetAllDependencies(!cleanupMakeDepends, true, operation.RootDir) resolved := pkg.GetDependenciesRecursive(!cleanupMakeDepends, operation.RootDir)
for _, value := range resolved { for _, value := range resolved {
if !slices.Contains(keepPackages, value) { if !slices.Contains(keepPackages, value) {
keepPackages = append(keepPackages, value) keepPackages = append(keepPackages, value)
@@ -333,6 +336,9 @@ func (operation *BPMOperation) ShowOperationSummary() {
return return
} }
writer := tabwriter.NewWriter(os.Stdout, 6, 4, 6, ' ', 0)
fmt.Fprintln(writer, "Name\tVersion\tAction\tInstallation Reason\tFrom Source")
for _, value := range operation.Actions { for _, value := range operation.Actions {
var pkgInfo *PackageInfo var pkgInfo *PackageInfo
var installationReason = InstallationReasonUnknown var installationReason = InstallationReasonUnknown
@@ -347,41 +353,40 @@ func (operation *BPMOperation) ShowOperationSummary() {
pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info
} else { } else {
pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo
fmt.Printf("%s: %s (Remove)\n", pkgInfo.Name, pkgInfo.GetFullVersion()) fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Remove", "-", "-")
continue continue
} }
installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir) installationReasonStr := ""
additionalInfo := ""
switch installationReason { switch installationReason {
case InstallationReasonManual: case InstallationReasonManual:
additionalInfo = "(Manual)" installationReasonStr = "Manual"
case InstallationReasonDependency: case InstallationReasonDependency:
additionalInfo = "(Dependency)" installationReasonStr = "Dependency"
case InstallationReasonMakeDependency: case InstallationReasonMakeDependency:
additionalInfo = "(Make dependency)" installationReasonStr = "Make dependency"
default: default:
additionalInfo = "(Unknown)" installationReasonStr = "Unknown"
}
if pkgInfo.Type == "source" {
additionalInfo += " (From Source)"
} }
installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir)
if installedInfo == nil { if installedInfo == nil {
fmt.Printf("%s: %s (Install) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo) fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Install", installationReasonStr, pkgInfo.Type == "source")
} else { } else {
comparison := ComparePackageVersions(*pkgInfo, *installedInfo) comparison := ComparePackageVersions(*pkgInfo, *installedInfo)
if comparison < 0 { if comparison < 0 {
fmt.Printf("%s: %s -> %s (Downgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), additionalInfo) fmt.Fprintf(writer, "%s\t%s -> %s\t%s\t%s\t%t\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), "Downgrade", installationReasonStr, pkgInfo.Type == "source")
} else if comparison > 0 { } else if comparison > 0 {
fmt.Printf("%s: %s -> %s (Upgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), additionalInfo) fmt.Fprintf(writer, "%s\t%s -> %s\t%s\t%s\t%t\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), "Upgrade", installationReasonStr, pkgInfo.Type == "source")
} else { } else {
fmt.Printf("%s: %s (Reinstall) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo) fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Reinstall", installationReasonStr, pkgInfo.Type == "source")
} }
} }
} }
writer.Flush()
fmt.Println()
if operation.RootDir != "/" { if operation.RootDir != "/" {
fmt.Println("Warning: Operating in " + operation.RootDir) fmt.Println("Warning: Operating in " + operation.RootDir)
} }
@@ -395,6 +400,34 @@ func (operation *BPMOperation) ShowOperationSummary() {
} }
} }
func (operation *BPMOperation) ShowSourcePackageContent() (sourcePackagesShown int, err error) {
// Fetch packages
if !operation.hasFetchedPackages {
err = operation.FetchPackages()
if err != nil {
return 0, err
}
}
for _, action := range operation.Actions {
if action.GetActionType() != "install" {
continue
}
if action.(*InstallPackageAction).BpmPackage.PkgInfo.Type != "source" {
continue
}
err = showPackageFiles(action.(*InstallPackageAction).File)
if err != nil {
return 0, err
}
sourcePackagesShown++
}
return sourcePackagesShown, nil
}
func (operation *BPMOperation) RunHooks(verbose bool) error { func (operation *BPMOperation) RunHooks(verbose bool) error {
// Return if hooks directory does not exist // Return if hooks directory does not exist
if stat, err := os.Stat(path.Join(operation.RootDir, "var/lib/bpm/hooks")); err != nil || !stat.IsDir() { if stat, err := os.Stat(path.Join(operation.RootDir, "var/lib/bpm/hooks")); err != nil || !stat.IsDir() {
@@ -426,7 +459,7 @@ func (operation *BPMOperation) RunHooks(verbose bool) error {
return nil return nil
} }
func (operation *BPMOperation) Execute(verbose, force bool) (err error) { func (operation *BPMOperation) FetchPackages() (err error) {
// Fetch packages from databases // Fetch packages from databases
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "fetch" return action.GetActionType() == "fetch"
@@ -492,6 +525,19 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
} }
} }
operation.hasFetchedPackages = true
return nil
}
func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
// Fetch packages
if !operation.hasFetchedPackages {
err = operation.FetchPackages()
if err != nil {
return err
}
}
// Determine words to be used for the following message // Determine words to be used for the following message
words := make([]string, 0) words := make([]string, 0)
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
+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