cos-dkms: Add pre/post action hooks

Also simplifies running the compile command since it follows the same
pattern.

BUG=b/349400483
TEST=presubmit

Change-Id: I1970e17b85cd8cd38d3a8c7062f038535b049dea
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/86300
Reviewed-by: Oleksandr Tymoshenko <ovt@google.com>
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
Tested-by: Kevin Berry <kpberry@google.com>
diff --git a/src/pkg/dkms/add.go b/src/pkg/dkms/add.go
index b71b6bd..2188a57 100644
--- a/src/pkg/dkms/add.go
+++ b/src/pkg/dkms/add.go
@@ -8,6 +8,7 @@
 
 	"cos.googlesource.com/cos/tools.git/src/pkg/fs"
 	"cos.googlesource.com/cos/tools.git/src/pkg/gcs"
+	"cos.googlesource.com/cos/tools.git/src/pkg/utils"
 	"github.com/golang/glog"
 )
 
@@ -58,7 +59,22 @@
 	}
 
 	glog.Infof("symlinking package sources from %s to %s", sourceTreeSourceDir, dkmsTreeSourceDir)
-	return os.Symlink(sourceTreeSourceDir, dkmsTreeSourceDir)
+	if err := os.Symlink(sourceTreeSourceDir, dkmsTreeSourceDir); err != nil {
+		return err
+	}
+
+	config, err := LoadConfig(pkg)
+	if err != nil {
+		return err
+	}
+
+	if config.PostAdd != "" {
+		if err := utils.RunCommandString(dkmsTreeSourceDir, config.PostAdd); err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
 
 // CachedAdd adds a package to the DKMS source tree, downloading
diff --git a/src/pkg/dkms/build.go b/src/pkg/dkms/build.go
index df1e697..2b0baa6 100644
--- a/src/pkg/dkms/build.go
+++ b/src/pkg/dkms/build.go
@@ -50,6 +50,12 @@
 		return err
 	}
 
+	if config.PreBuild != "" {
+		if utils.RunCommandString(pkg.BuildDir(), config.PreBuild); err != nil {
+			return fmt.Errorf("failed to run pre-build script: %v", err)
+		}
+	}
+
 	if options.InstallBuildDependencies {
 		if err := InstallBuildDependencies(pkg); err != nil {
 			return err
@@ -81,10 +87,16 @@
 	makeCommand := fmt.Sprintf("%s %s", config.MakeCommand, makeVariables)
 	makeCommand = strings.Trim(makeCommand, " ")
 
-	if err := Compile(buildDir, makeCommand); err != nil {
+	if err := utils.RunCommandString(buildDir, makeCommand); err != nil {
 		return fmt.Errorf("failed to compile modules: %v", err)
 	}
 
+	if config.PostBuild != "" {
+		if err := utils.RunCommandString(buildDir, config.PostBuild); err != nil {
+			return fmt.Errorf("failed to run post-build script: %v", err)
+		}
+	}
+
 	return nil
 }
 
@@ -160,23 +172,6 @@
 	return nil
 }
 
-// Compile compiles the sources in a directory using the given command.
-func Compile(dir string, makeCommand string) error {
-	glog.Infof("compiling with command: %s", makeCommand)
-	// not great, but this is more or less what standard DKMS does
-	cmd := exec.Command("bash", "-c", makeCommand)
-	cmd.Dir = dir
-	out, err := cmd.CombinedOutput()
-	if err == nil {
-		glog.Info(string(out))
-	} else {
-		glog.Error(string(out))
-		return err
-	}
-
-	return nil
-}
-
 // InstallBuildDependencies downloads and installs the kernel headers and
 // compiler toolchain for a package, if they are not already present.
 func InstallBuildDependencies(pkg *Package) error {
diff --git a/src/pkg/dkms/build_test.go b/src/pkg/dkms/build_test.go
index a661d71..55754b0 100644
--- a/src/pkg/dkms/build_test.go
+++ b/src/pkg/dkms/build_test.go
@@ -177,21 +177,6 @@
 	}
 }
 
-func TestCompile(t *testing.T) {
-	buildDir := t.TempDir()
-	if err := fs.CopyDir("testdata/source-tree/mymodule-1.0", buildDir, 0777); err != nil {
-		t.Fatalf("%v", err)
-	}
-
-	if err := Compile(buildDir, "touch mymodule.ko"); err != nil {
-		t.Fatalf("%v", err)
-	}
-
-	if !fs.IsFile(path.Join(buildDir, "mymodule.ko")) {
-		t.Fatalf("expected mymodule.ko to exist after compile command was run")
-	}
-}
-
 func TestInstallBuildDependencies(t *testing.T) {
 	downloader := fakeDownloader{}
 	ctx := context.Background()
diff --git a/src/pkg/dkms/install.go b/src/pkg/dkms/install.go
index dfd3656..614da45 100644
--- a/src/pkg/dkms/install.go
+++ b/src/pkg/dkms/install.go
@@ -11,6 +11,7 @@
 
 	"cos.googlesource.com/cos/tools.git/src/pkg/fs"
 	"cos.googlesource.com/cos/tools.git/src/pkg/gcs"
+	"cos.googlesource.com/cos/tools.git/src/pkg/utils"
 	"github.com/golang/glog"
 )
 
@@ -45,6 +46,12 @@
 		return err
 	}
 
+	if config.PreInstall != "" {
+		if utils.RunCommandString(pkg.BuildDir(), config.PreInstall); err != nil {
+			return fmt.Errorf("failed to run pre-install script: %v", err)
+		}
+	}
+
 	var modulesToInstall []Module
 	if options.ForceVersionOverride {
 		modulesToInstall = config.Modules
@@ -82,6 +89,12 @@
 		}
 	}
 
+	if config.PostInstall != "" {
+		if utils.RunCommandString(pkg.BuildDir(), config.PostInstall); err != nil {
+			return fmt.Errorf("failed to run post-install script: %v", err)
+		}
+	}
+
 	return nil
 }
 
diff --git a/src/pkg/dkms/remove.go b/src/pkg/dkms/remove.go
index 9ba83f2..3e46dea 100644
--- a/src/pkg/dkms/remove.go
+++ b/src/pkg/dkms/remove.go
@@ -2,9 +2,11 @@
 
 import (
 	"context"
+	"fmt"
 	"os"
 
 	"cos.googlesource.com/cos/tools.git/src/pkg/gcs"
+	"cos.googlesource.com/cos/tools.git/src/pkg/utils"
 	"github.com/golang/glog"
 )
 
@@ -22,9 +24,26 @@
 		return nil
 	}
 
+	// We must load the config before the package is removed in order to use
+	// the correct post-remove script, if applicable
+	config, err := LoadConfig(pkg)
+	if err != nil {
+		return err
+	}
+
 	sourceDir := pkg.SourceDir()
 	glog.Info("removing package sources", sourceDir)
-	return os.Remove(sourceDir)
+	if err := os.Remove(sourceDir); err != nil {
+		return fmt.Errorf("failed to remove package sources: %v", err)
+	}
+
+	if config.PostRemove != "" {
+		if err := utils.RunCommandString(sourceDir, config.PostRemove); err != nil {
+			return fmt.Errorf("failed to run post-remove script: %v", err)
+		}
+	}
+
+	return nil
 }
 
 // Remove removes a package from the local DKMS source tree and from the cache.
diff --git a/src/pkg/utils/shell.go b/src/pkg/utils/shell.go
index d137aa8..4d6aa90 100644
--- a/src/pkg/utils/shell.go
+++ b/src/pkg/utils/shell.go
@@ -3,13 +3,37 @@
 import (
 	"bufio"
 	"fmt"
-	"github.com/golang/glog"
 	"os"
 	"os/exec"
 	"regexp"
 	"strings"
+
+	"github.com/golang/glog"
 )
 
+// RunCommandString runs a command string from a provided directory and logs
+// the combined stdout and stderr.
+// If there is no error, the output will be logged at Info level. If there is
+// an error, the output will be logged at Error level.
+func RunCommandString(dir string, command string) error {
+	glog.Info(fmt.Sprintf("running command: %s", command))
+	cmd := exec.Command("bash", "-c", command)
+	cmd.Dir = dir
+	out, err := cmd.CombinedOutput()
+	if err == nil {
+		if len(out) > 0 {
+			glog.Info(string(out))
+		}
+	} else {
+		if len(out) > 0 {
+			glog.Error(string(out))
+		}
+		return err
+	}
+
+	return nil
+}
+
 // SourceFile sources a bash file and returns the shell variables as a map.
 func SourceFile(file string) (map[string]string, error) {
 	contents, err := os.ReadFile(file)