Add hook functionality #9

Merged
EnumDev merged 3 commits from hooks into develop 2025-04-07 12:20:25 +00:00
3 changed files with 244 additions and 6 deletions

40
main.go
View File

@ -224,6 +224,7 @@ func resolveCommand() {
operation := utils.BPMOperation{
Actions: make([]utils.OperationAction, 0),
UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string),
RootDir: rootDir,
ForceInstallationReason: ir,
}
@ -238,7 +239,7 @@ func resolveCommand() {
if !reinstall && utils.IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) && utils.GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() == bpmpkg.PkgInfo.GetFullVersion() {
continue
}
operation.Actions = append(operation.Actions, &utils.InstallPackageAction{
operation.AppendAction(&utils.InstallPackageAction{
File: pkg,
IsDependency: false,
BpmPackage: bpmpkg,
@ -261,7 +262,7 @@ func resolveCommand() {
if !reinstall && utils.IsPackageInstalled(entry.Info.Name, rootDir) && utils.GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() == entry.Info.GetFullVersion() {
continue
}
operation.Actions = append(operation.Actions, &utils.FetchPackageAction{
operation.AppendAction(&utils.FetchPackageAction{
IsDependency: false,
RepositoryEntry: entry,
})
@ -327,6 +328,13 @@ func resolveCommand() {
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
// Executing hooks
fmt.Println("Running hooks...")
err = operation.RunHooks(verbose)
if err != nil {
log.Fatalf("Error: could not run hooks: %s\n", err)
}
case update:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
@ -355,6 +363,7 @@ func resolveCommand() {
operation := utils.BPMOperation{
Actions: make([]utils.OperationAction, 0),
UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string),
RootDir: rootDir,
ForceInstallationReason: utils.Unknown,
}
@ -378,7 +387,7 @@ func resolveCommand() {
} else {
comparison := utils.ComparePackageVersions(*entry.Info, *installedInfo)
if comparison > 0 || reinstall {
operation.Actions = append(operation.Actions, &utils.FetchPackageAction{
operation.AppendAction(&utils.FetchPackageAction{
IsDependency: false,
RepositoryEntry: entry,
})
@ -421,6 +430,13 @@ func resolveCommand() {
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
// Executing hooks
fmt.Println("Running hooks...")
err = operation.RunHooks(verbose)
if err != nil {
log.Fatalf("Error: could not run hooks: %s\n", err)
}
case sync:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
@ -455,6 +471,7 @@ func resolveCommand() {
operation := &utils.BPMOperation{
Actions: make([]utils.OperationAction, 0),
UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string),
RootDir: rootDir,
}
@ -464,7 +481,7 @@ func resolveCommand() {
if bpmpkg == nil {
continue
}
operation.Actions = append(operation.Actions, &utils.RemovePackageAction{BpmPackage: bpmpkg})
operation.AppendAction(&utils.RemovePackageAction{BpmPackage: bpmpkg})
}
// Skip needed packages if the --unused flag is on
@ -502,6 +519,13 @@ func resolveCommand() {
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
// Executing hooks
fmt.Println("Running hooks...")
err = operation.RunHooks(verbose)
if err != nil {
log.Fatalf("Error: could not run hooks: %s\n", err)
}
case cleanup:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
@ -510,6 +534,7 @@ func resolveCommand() {
operation := &utils.BPMOperation{
Actions: make([]utils.OperationAction, 0),
UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string),
RootDir: rootDir,
}
@ -538,6 +563,13 @@ func resolveCommand() {
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
// Executing hooks
fmt.Println("Running hooks...")
err = operation.RunHooks(verbose)
if err != nil {
log.Fatalf("Error: could not run hooks: %s\n", err)
}
case file:
files := subcommandArgs
if len(files) == 0 {

155
utils/hooks.go Normal file
View File

@ -0,0 +1,155 @@
package utils
import (
"errors"
"fmt"
"gopkg.in/yaml.v3"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strings"
"syscall"
)
type BPMHook struct {
SourcePath string
SourceContent string
TriggerOperations []string `yaml:"trigger_operations"`
TargetType string `yaml:"target_type"`
Targets []string `yaml:"targets"`
Depends []string `yaml:"depends"`
Run string `yaml:"run"`
}
// CreateHook returns a BPMHook instance based on the content of the given string
func CreateHook(sourcePath string) (*BPMHook, error) {
// Read hook from source path
bytes, err := os.ReadFile(sourcePath)
if err != nil {
return nil, err
}
// Create base hook structure
hook := &BPMHook{
SourcePath: sourcePath,
SourceContent: string(bytes),
TriggerOperations: nil,
TargetType: "",
Targets: nil,
Depends: nil,
Run: "",
}
// Unmarshal yaml string
err = yaml.Unmarshal(bytes, hook)
if err != nil {
return nil, err
}
// Ensure hook is valid
if err := hook.IsValid(); err != nil {
return nil, err
}
return hook, nil
}
// IsValid ensures hook is valid
func (hook *BPMHook) IsValid() error {
ValidOperations := []string{"install", "upgrade", "remove"}
// Return error if any trigger operation is not valid or none are given
if len(hook.TriggerOperations) == 0 {
return errors.New("no trigger operations specified")
}
for _, operation := range hook.TriggerOperations {
if !slices.Contains(ValidOperations, operation) {
return errors.New("trigger operation '" + operation + "' is not valid")
}
}
if hook.TargetType != "package" && hook.TargetType != "path" {
return errors.New("target type '" + hook.TargetType + "' is not valid")
}
if len(hook.Run) == 0 {
return errors.New("command to run is empty")
}
// Return nil as hook is valid
return nil
}
// Execute hook if all conditions are met
func (hook *BPMHook) Execute(packageChanges map[string]string, verbose bool, rootDir string) error {
// Check if package dependencies are met
installedPackages, err := GetInstalledPackages(rootDir)
if err != nil {
return err
}
for _, depend := range hook.Depends {
if !slices.Contains(installedPackages, depend) {
return nil
}
}
// Get modified files slice
modifiedFiles := make([]*PackageFileEntry, 0)
for pkg := range packageChanges {
modifiedFiles = append(modifiedFiles, GetPackageFiles(pkg, rootDir)...)
}
// Check if any targets are met
targetMet := false
for _, target := range hook.Targets {
if targetMet {
break
}
if hook.TargetType == "package" {
for change, operation := range packageChanges {
if target == change && slices.Contains(hook.TriggerOperations, operation) {
targetMet = true
break
}
}
} else {
glob, err := filepath.Glob(path.Join(rootDir, target))
if err != nil {
return err
}
for _, change := range modifiedFiles {
if slices.Contains(glob, path.Join(rootDir, change.Path)) {
targetMet = true
break
}
}
}
}
if !targetMet {
return nil
}
// Execute the command
splitCommand := strings.Split(hook.Run, " ")
cmd := exec.Command(splitCommand[0], splitCommand[1:]...)
// Setup subprocess environment
cmd.Dir = "/"
// Run hook in chroot if using the -R flag
if rootDir != "/" {
cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir}
}
if verbose {
fmt.Printf("Running hook (%s) with run command: %s\n", hook.SourcePath, strings.Join(splitCommand, " "))
}
err = cmd.Run()
if err != nil {
return err
}
return nil
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"path"
"slices"
"strings"
)
@ -12,6 +13,7 @@ import (
type BPMOperation struct {
Actions []OperationAction
UnresolvedDepends []string
Changes map[string]string
RootDir string
ForceInstallationReason InstallationReason
}
@ -35,12 +37,35 @@ func (operation *BPMOperation) ActionsContainPackage(pkg string) bool {
return false
}
func (operation *BPMOperation) AppendAction(action OperationAction) {
operation.InsertActionAt(len(operation.Actions), action)
}
func (operation *BPMOperation) InsertActionAt(index int, action OperationAction) {
if len(operation.Actions) == index { // nil or empty slice or after last element
operation.Actions = append(operation.Actions, action)
} else {
operation.Actions = append(operation.Actions[:index+1], operation.Actions[index:]...) // index < len(a)
operation.Actions[index] = action
}
if action.GetActionType() == "install" {
pkgInfo := action.(*InstallPackageAction).BpmPackage.PkgInfo
if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) {
operation.Changes[pkgInfo.Name] = "install"
} else {
operation.Changes[pkgInfo.Name] = "upgrade"
}
} else if action.GetActionType() == "fetch" {
pkgInfo := action.(*FetchPackageAction).RepositoryEntry.Info
if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) {
operation.Changes[pkgInfo.Name] = "install"
} else {
operation.Changes[pkgInfo.Name] = "upgrade"
}
} else if action.GetActionType() == "remove" {
operation.Changes[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = "remove"
}
operation.Actions = append(operation.Actions[:index+1], operation.Actions[index:]...) // index < len(a)
operation.Actions[index] = action
}
func (operation *BPMOperation) RemoveAction(pkg, actionType string) {
@ -356,6 +381,32 @@ func (operation *BPMOperation) ShowOperationSummary() {
}
}
func (operation *BPMOperation) RunHooks(verbose bool) error {
// Get directory entries in hooks directory
dirEntries, err := os.ReadDir(path.Join(operation.RootDir, "var/lib/bpm/hooks"))
if err != nil {
return err
}
// Find all hooks, validate and execute them
for _, entry := range dirEntries {
if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), ".bpmhook") {
hook, err := CreateHook(path.Join(operation.RootDir, "var/lib/bpm/hooks", entry.Name()))
if err != nil {
log.Printf("Error while reading hook (%s): %s", entry.Name(), err)
}
err = hook.Execute(operation.Changes, verbose, operation.RootDir)
if err != nil {
log.Printf("Warning: could not execute hook (%s): %s\n", entry.Name(), err)
continue
}
}
}
return nil
}
func (operation *BPMOperation) Execute(verbose, force bool) error {
// Fetch packages from repositories
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {