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 go 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 } }