feat: initial working version

This commit is contained in:
Peter 2023-07-01 13:18:12 +02:00
parent 09b9d3af60
commit c2eb3a6a72
No known key found for this signature in database
9 changed files with 410 additions and 0 deletions

1
.gitignore vendored
View file

@ -21,3 +21,4 @@
# Go workspace file
go.work
*.wasm

82
build/go_build.go Normal file
View file

@ -0,0 +1,82 @@
package build
import (
sdk "code.icb4dc0.de/buildr/wasi-module-sdk-go"
"code.icb4dc0.de/buildr/wasi-module-sdk-go/exec"
"path/filepath"
"strings"
"golang.org/x/exp/slog"
)
const (
defaultArgsLength = 6
goArchAndGoOS = 2
)
var (
_ sdk.Module = (*GoBuild)(nil)
// _ sdk.Helper = (*GoBuild)(nil)
)
type GoBuild struct {
Env map[string]string `hcl:"environment,optional"`
Binary string `hcl:"binary"`
Main string `hcl:"main"`
GoOS string `hcl:"goos"`
GoArch string `hcl:"goarch"`
Flags []string `hcl:"flags,optional"`
LdFlags []string `hcl:"ldflags,optional"`
}
func (g GoBuild) Type() string {
return "go_build"
}
func (g GoBuild) Category() sdk.Category {
return sdk.CategoryBuild
}
func (g GoBuild) Execute(ctx sdk.ExecutionContext) (err error) {
logger := ctx.Logger()
buildArgs := make([]string, 1, defaultArgsLength+len(g.Flags))
buildArgs[0] = "build"
if ldFlags := strings.Join(g.LdFlags, " "); ldFlags != "" {
buildArgs = append(buildArgs, "-ldflags", ldFlags)
}
buildArgs = append(buildArgs, g.Flags...)
buildArgs = append(
buildArgs,
"-o",
filepath.Join(ctx.OutDir(), g.Binary),
g.Main,
)
logger.Info(
"Executing go build",
slog.String("args", strings.Join(buildArgs, " ")),
)
cmd := exec.NewCommand(
"go",
buildArgs...,
)
if err != nil {
return err
}
if g.Env == nil {
g.Env = make(map[string]string, goArchAndGoOS)
}
g.Env["GOARCH"] = g.GoArch
g.Env["GOOS"] = g.GoOS
cmd.AddEnv(g.Env)
return cmd.Run()
}

15
go.mod Normal file
View file

@ -0,0 +1,15 @@
module code.icb4dc0.de/buildr/golang-plugin
go 1.20
require (
code.icb4dc0.de/buildr/wasi-module-sdk-go v0.0.0-20230629182727-3bf4797b6abd
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc
)
require (
github.com/tetratelabs/tinymem v0.1.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)
replace code.icb4dc0.de/buildr/wasi-module-sdk-go => ../wasi-module-sdk-go

20
go.sum Normal file
View file

@ -0,0 +1,20 @@
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/tetratelabs/tinymem v0.1.0 h1:Qza1JAg9lquPPJ/CIei5qQYx7t18KLie83O2WR6CM58=
github.com/tetratelabs/tinymem v0.1.0/go.mod h1:WFFTZFhLod6lTL+UetFAopVbGaB+KFsVcIY+RUv7NeY=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

22
main.go Normal file
View file

@ -0,0 +1,22 @@
//go:generate tinygo build -o golang.wasm -gc=leaking --no-debug -target=wasi main.go
package main
import (
"code.icb4dc0.de/buildr/golang-plugin/build"
"code.icb4dc0.de/buildr/golang-plugin/tool"
sdk "code.icb4dc0.de/buildr/wasi-module-sdk-go"
)
func init() {
sdk.Register(sdk.CategoryTool, "go_tool", sdk.ModuleFactoryFunc(func() sdk.Module {
return new(tool.GoTool)
}))
sdk.Register(sdk.CategoryBuild, "go_build", sdk.ModuleFactoryFunc(func() sdk.Module {
return new(build.GoBuild)
}))
}
func main() {
}

159
tool/go_tool.go Normal file
View file

@ -0,0 +1,159 @@
package tool
import (
sdk "code.icb4dc0.de/buildr/wasi-module-sdk-go"
"code.icb4dc0.de/buildr/wasi-module-sdk-go/exec"
"context"
"fmt"
"golang.org/x/exp/slog"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
)
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))
stateEncoder = sdk.NewJSONStateEncoder[GoToolState](ctx)
state = g.state()
)
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()))
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
}
}

31
tool/go_tool_state.go Normal file
View file

@ -0,0 +1,31 @@
package tool
import (
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
type GoToolState struct {
Env map[string]string `json:"env,omitempty"`
InstalledVersion string `json:"installed_version"`
BuildArgs []string `json:"build_args,omitempty"`
}
func (s GoToolState) Equals(other GoToolState) bool {
if s.InstalledVersion != other.InstalledVersion {
return false
}
if len(s.BuildArgs) != len(other.BuildArgs) {
return false
}
slices.Sort(s.BuildArgs)
slices.Sort(other.BuildArgs)
if !slices.Equal(s.BuildArgs, other.BuildArgs) {
return false
}
return maps.Equal(s.Env, other.Env)
}

71
tool/go_tool_test.go Normal file
View file

@ -0,0 +1,71 @@
package tool_test
import (
"code.icb4dc0.de/buildr/golang-plugin/tool"
"testing"
)
func TestGoTool_BinaryName(t *testing.T) {
t.Parallel()
type fields struct {
BinaryNameOverride string
Repository string
Version string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Override",
fields: fields{
BinaryNameOverride: "go-buildr",
Repository: "code.icb4dc0.de/buildr/buildr",
},
want: "go-buildr",
},
{
name: "No override, no inline-version, no version suffix",
fields: fields{
Repository: "code.icb4dc0.de/buildr/buildr",
},
want: "buildr",
},
{
name: "No override, no inline-version, version suffix",
fields: fields{
Repository: "code.icb4dc0.de/buildr/buildr/v2",
},
want: "buildr",
},
{
name: "No override, inline-version, no version suffix",
fields: fields{
Repository: "code.icb4dc0.de/buildr/buildr@latest",
},
want: "buildr",
},
{
name: "No override, inline-version, version suffix",
fields: fields{
Repository: "code.icb4dc0.de/buildr/buildr/v2@latest",
},
want: "buildr",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
g := tool.GoTool{
BinaryNameOverride: tt.fields.BinaryNameOverride,
Repository: tt.fields.Repository,
Version: tt.fields.Version,
}
if got := g.BinaryName(); got != tt.want {
t.Errorf("BinaryName() = %v, want %v", got, tt.want)
}
})
}
}

9
tool/state.go Normal file
View file

@ -0,0 +1,9 @@
package tool
type State string
const (
StateDefault State = ""
StateLocal State = "local"
StateGlobal State = "global"
)