feat: initial working version
This commit is contained in:
parent
09b9d3af60
commit
c2eb3a6a72
9 changed files with 410 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -21,3 +21,4 @@
|
|||
# Go workspace file
|
||||
go.work
|
||||
|
||||
*.wasm
|
82
build/go_build.go
Normal file
82
build/go_build.go
Normal 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
15
go.mod
Normal 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
20
go.sum
Normal 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
22
main.go
Normal 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
159
tool/go_tool.go
Normal 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
31
tool/go_tool_state.go
Normal 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
71
tool/go_tool_test.go
Normal 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
9
tool/state.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package tool
|
||||
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateDefault State = ""
|
||||
StateLocal State = "local"
|
||||
StateGlobal State = "global"
|
||||
)
|
Loading…
Reference in a new issue