golang-plugin/tool/go_tool.go

168 lines
3.7 KiB
Go

package tool
import (
"context"
"fmt"
"log/slog"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
sdk "code.icb4dc0.de/buildr/wasi-module-sdk-go"
"code.icb4dc0.de/buildr/wasi-module-sdk-go/exec"
)
const (
goInstall = 2
stateAccessTimeout = 100 * time.Millisecond
)
var (
_ sdk.Module = (*GoTool)(nil)
moduleVersionRegexp = regexp.MustCompile(`^v\d$`)
)
type GoTool struct {
Env map[string]string `hcl:"environment,optional"`
BinaryNameOverride string `hcl:"binary_name"`
Repository string `hcl:"repository"`
Version string `hcl:"version"`
State State `hcl:"state,optional"`
BuildArgs []string `hcl:"build_args,optional"`
}
func (g GoTool) BinaryName() string {
if g.BinaryNameOverride != "" {
return g.BinaryNameOverride
}
repo := g.Repository
if atIdx := strings.LastIndex(repo, "@"); atIdx != -1 {
repo = repo[:atIdx]
}
u, err := url.Parse(repo)
if err != nil {
return ""
}
remain, binPackageName := path.Split(u.Path)
if moduleVersionRegexp.MatchString(binPackageName) {
_, binPackageName = path.Split(remain[:len(remain)-1])
}
return binPackageName
}
func (g GoTool) Execute(ctx sdk.ExecutionContext) error {
var (
binName = g.BinaryName()
stateKey = fmt.Sprintf("%s.state", binName)
logger = ctx.Logger().With(
slog.String("tool_name", binName),
slog.String("repository", g.Repository),
slog.String("version", g.Version),
)
stateEncoder = sdk.NewJSONStateEncoder[GoToolState](ctx)
state = g.state()
)
logger.Info("Ensuring got tool is installed")
if p, err := exec.LookPath(binName); state == StateGlobal && err == nil {
logger.Info(
"Found tool installation",
slog.String("tool_path", p),
)
return nil
}
stateCtx, cancel := context.WithTimeout(ctx, stateAccessTimeout)
currentState, _, _, err := stateEncoder.Get(stateCtx, stateKey)
cancel()
if err != nil {
logger.Warn("Failed to get state", slog.String("err", err.Error()))
}
desiredState := GoToolState{
InstalledVersion: g.Version,
BuildArgs: g.BuildArgs,
Env: g.Env,
}
existingToolPath := filepath.Join(ctx.BinariesDir(), binName)
if _, err = os.Stat(existingToolPath); err == nil && desiredState.Equals(currentState) {
logger.Info("Tool is already installed according to state", slog.String("tool_path", existingToolPath))
return nil
}
logger.Debug("Installing Go tool", slog.String("out_dir", ctx.OutDir()))
if _, err = exec.LookPath("go"); err != nil {
return fmt.Errorf("failed to lookup path for 'go' binary: %w", err)
}
args := make([]string, 0, goInstall+len(g.BuildArgs))
args = append(args, "install")
args = append(args, g.BuildArgs...)
args = append(args, fmt.Sprintf("%s@%s", g.Repository, g.version()))
logger.Debug("Installing Go tool", slog.String("args", strings.Join(args, ", ")))
cmd := exec.NewCommand(
"go",
args...,
)
if err != nil {
return err
}
if g.Env == nil {
g.Env = make(map[string]string)
}
g.Env["GOBIN"] = ctx.OutDir()
cmd.AddEnv(g.Env)
if err := cmd.Run(); err != nil {
return err
}
stateCtx, cancel = context.WithTimeout(ctx, stateAccessTimeout)
defer cancel()
return stateEncoder.Set(stateCtx, stateKey, desiredState)
}
func (g GoTool) Category() sdk.Category {
return sdk.CategoryTool
}
func (g GoTool) Type() string {
return "go_tool"
}
func (g GoTool) version() string {
if g.Version == "" {
return "latest"
}
return g.Version
}
func (g GoTool) state() State {
//nolint:exhaustive // handled by default
switch g.State {
case StateGlobal:
return StateGlobal
default:
return StateLocal
}
}