diff --git a/main.go b/main.go index 2c115ba..7db4691 100644 --- a/main.go +++ b/main.go @@ -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 { diff --git a/utils/hooks.go b/utils/hooks.go new file mode 100644 index 0000000..28294f8 --- /dev/null +++ b/utils/hooks.go @@ -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 +} diff --git a/utils/operations.go b/utils/operations.go index fe5bef5..ab3adeb 100644 --- a/utils/operations.go +++ b/utils/operations.go @@ -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 {