diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index d8f40b84..8242b84f 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -24,17 +24,98 @@ jobs: test: strategy: matrix: - os: [macos-13, macos-13, macos-13-xlarge, macos-14, macos-latest, ubuntu-22.04, ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} + include: + # macOS Intel/ARM configurations + + - os: macos-13 + runner: macos-13 + cgo_enabled: 0 + + - os: macos-13-xlarge + runner: macos-13 + cgo_enabled: 0 + + - os: macos-14 + runner: macos-14 + cgo_enabled: 0 + + - os: macos-latest + runner: macos-latest + cgo_enabled: 0 + + # Ubuntu configurations + + - os: ubuntu-22.04 + runner: ubuntu-22.04 + cgo_enabled: 0 + + - os: ubuntu-latest + runner: ubuntu-latest + cgo_enabled: 0 + + # Windows configurations + + - os: windows-latest + runner: windows-latest + cgo_enabled: 0 + + # Alpine Linux container configurations + - os: alpine-latest + runner: ubuntu-latest # Host runner for the container + container: golang:1.23-alpine3.19 + cgo_enabled: 1 + + runs-on: ${{ matrix.runner }} + container: ${{ matrix.container }} defaults: run: working-directory: "go" + steps: - name: Git checkout uses: actions/checkout@v6 - - name: Set up Go + + - name: Set up Go (non-musl) + if: matrix.container == null uses: actions/setup-go@v6 with: go-version: 1.23 - - name: Go code test - run: go test ./... + cache: true + + - name: Install musl dependencies (Alpine container) + if: matrix.container != null + run: | + apk add --no-cache \ + musl-dev \ + gcc \ + git \ + make + + - name: Go mod tidy + run: go mod tidy + + - name: Setup build tags for Alpine + if: matrix.container != null + run: | + echo "GO_BUILD_TAGS=musl netgo static osusergo" >> $GITHUB_ENV + echo "GO_LDFLAGS=-linkmode external -extldflags '-static'" >> $GITHUB_ENV + + - name: Go code test (CGO_ENABLED=${{ matrix.cgo_enabled }}) + if: matrix.os != 'windows-latest' + run: | + if [ -n "${{ matrix.container }}" ]; then + BUILD_TAGS="$GO_BUILD_TAGS" + EXTRA_LDFLAGS="$GO_LDFLAGS" + fi + + CGO_ENABLED=${{ matrix.cgo_enabled }} \ + go test ./... -v \ + -tags="${BUILD_TAGS}" \ + -ldflags="${EXTRA_LDFLAGS}" + env: + CGO_LDFLAGS: ${{ matrix.cgo_enabled == '1' && '-static' || '' }} + + - name: Go code test (CGO_ENABLED=${{ matrix.cgo_enabled }}) on Windows + if: matrix.os == 'windows-latest' + run: | + go test ./... -v diff --git a/go/.gitignore b/go/.gitignore index 9a61abeb..a5943b7e 100644 --- a/go/.gitignore +++ b/go/.gitignore @@ -1,4 +1,3 @@ - !lib/**/*.dylib !lib/**/*.dll !lib/**/*.lib diff --git a/go/include/kcl.h b/go/include/kcl.h new file mode 100644 index 00000000..7b1bf6fd --- /dev/null +++ b/go/include/kcl.h @@ -0,0 +1,29 @@ +#ifndef _KCL_H +#define _KCL_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +typedef uintptr_t KclServiceHandle; + +KclServiceHandle kcl_service_new(uint64_t plugin_agent); +void kcl_service_delete(KclServiceHandle svc); +uint8_t* kcl_service_call_with_length( + KclServiceHandle svc, + const char* method, + const char* args, + uint32_t args_len, + uint32_t* out_len +); +void kcl_free(uint8_t* ptr, uint32_t len); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif /* _KCL_H */ diff --git a/go/install/install.go b/go/install/install.go index 689f0d1f..f9b5565a 100644 --- a/go/install/install.go +++ b/go/install/install.go @@ -3,7 +3,6 @@ package install import ( "fmt" "os" - "os/exec" "path/filepath" "runtime" @@ -12,13 +11,6 @@ import ( const KCL_VERSION = "v0.12.1" -func findPath(name string) string { - if path, err := exec.LookPath(name); err == nil { - return path - } - return "" -} - func getVersion() string { return fmt.Sprintf("%s-%s-%s", KCL_VERSION, runtime.GOOS, runtime.GOARCH) } diff --git a/go/install/install_bin_unix.go b/go/install/install_bin_unix.go deleted file mode 100644 index 35be6a35..00000000 --- a/go/install/install_bin_unix.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package install - -import ( - "fmt" - "os" - "path/filepath" - "runtime" -) - -func installBin(binDir, binName string, content []byte, versionMatched bool) error { - binPath := findPath(binName) - if binPath == "" || !versionMatched { - if runtime.GOOS == "windows" { - binName += ".exe" - } - binPath = filepath.Join(binDir, binName) - err := os.MkdirAll(binDir, 0777) - if err != nil { - return err - } - - for pass := 0; ; pass++ { - tmpFullPath := fmt.Sprintf("%s~%d", binPath, pass) - tmpFile, err := os.OpenFile(tmpFullPath, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0755) - if err != nil { - if os.IsExist(err) { - continue - } - return err - } - defer func() { - tmpFile.Close() - _ = os.Remove(tmpFullPath) - }() - - if _, err = tmpFile.Write(content); err != nil { - return err - } - if err := os.Rename(tmpFullPath, binPath); err != nil { - return err - } - fileMode := os.FileMode(0777) - os.Chmod(binPath, fileMode) - break - } - } - return nil -} diff --git a/go/install/install_bin_windows.go b/go/install/install_bin_windows.go deleted file mode 100644 index 6b5272ad..00000000 --- a/go/install/install_bin_windows.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build windows -// +build windows - -package install - -import ( - "os" - "path/filepath" - "runtime" -) - -func installBin(binDir, binName string, content []byte, versionMatched bool) error { - binPath := findPath(binName) - if binPath == "" || !versionMatched { - if runtime.GOOS == "windows" { - binName += ".exe" - } - binPath = filepath.Join(binDir, binName) - err := os.MkdirAll(binDir, 0777) - if err != nil { - return err - } - binFile, err := os.Create(binPath) - defer func() { - binFile.Close() - }() - if err != nil { - return err - } - _, err = binFile.Write(content) - if err != nil { - return err - } - fileMode := os.FileMode(0777) - os.Chmod(binPath, fileMode) - } - return nil -} diff --git a/go/lib/linux-musl-amd64/libkcl.a b/go/lib/linux-musl-amd64/libkcl.a new file mode 100644 index 00000000..1533550b Binary files /dev/null and b/go/lib/linux-musl-amd64/libkcl.a differ diff --git a/go/lib/linux-musl-arm64/libkcl.a b/go/lib/linux-musl-arm64/libkcl.a new file mode 100644 index 00000000..693c0ea1 Binary files /dev/null and b/go/lib/linux-musl-arm64/libkcl.a differ diff --git a/go/native/client.go b/go/native/client.go index 13b59941..55712765 100644 --- a/go/native/client.go +++ b/go/native/client.go @@ -3,71 +3,18 @@ package native import ( "bytes" "errors" - "runtime" "strings" - "sync" "unsafe" - "github.com/ebitengine/purego" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "kcl-lang.io/lib/go/api" - "kcl-lang.io/lib/go/plugin" -) - -var libInit sync.Once - -var ( - client *NativeServiceClient - lib uintptr - serviceNew func(uint64) uintptr - serviceDelete func(uintptr) - serviceCall func(uintptr, string, string, uint, *uint) uintptr - free func(uintptr, uint) ) type validator interface { Validate() error } -type NativeServiceClient struct { - svc uintptr -} - -func initClient(pluginAgent uint64) { - libInit.Do(func() { - lib, err := loadServiceNativeLib() - if err != nil { - panic(err) - } - purego.RegisterLibFunc(&serviceNew, lib, "kcl_service_new") - purego.RegisterLibFunc(&serviceDelete, lib, "kcl_service_delete") - purego.RegisterLibFunc(&serviceCall, lib, "kcl_service_call_with_length") - purego.RegisterLibFunc(&free, lib, "kcl_free") - client = new(NativeServiceClient) - client.svc = serviceNew(pluginAgent) - runtime.SetFinalizer(client, func(x *NativeServiceClient) { - if x != nil { - x.Close() - } - }) - }) -} - -func NewNativeServiceClient() api.ServiceClient { - return NewNativeServiceClientWithPluginAgent(plugin.GetInvokeJsonProxyPtr()) -} - -func NewNativeServiceClientWithPluginAgent(pluginAgent uint64) *NativeServiceClient { - initClient(pluginAgent) - return client -} - -func (x *NativeServiceClient) Close() { - serviceDelete(x.svc) - closeLibrary(lib) -} - func cApiCall[I interface { *TI proto.Message @@ -131,16 +78,6 @@ func (c *NativeServiceClient) ExecProgram(in *api.ExecProgramArgs) (*api.ExecPro return cApiCall[*api.ExecProgramArgs, *api.ExecProgramResult](c, "KclService.ExecProgram", in) } -// Depreciated: Please use the env.EnableFastEvalMode() and c.ExecutProgram method and will be removed in v0.12.1. -func (c *NativeServiceClient) BuildProgram(in *api.BuildProgramArgs) (*api.BuildProgramResult, error) { - return cApiCall[*api.BuildProgramArgs, *api.BuildProgramResult](c, "KclService.BuildProgram", in) -} - -// Depreciated: Please use the env.EnableFastEvalMode() and c.ExecutProgram method and will be removed in v0.12.1. -func (c *NativeServiceClient) ExecArtifact(in *api.ExecArtifactArgs) (*api.ExecProgramResult, error) { - return cApiCall[*api.ExecArtifactArgs, *api.ExecProgramResult](c, "KclService.ExecArtifact", in) -} - func (c *NativeServiceClient) ParseFile(in *api.ParseFileArgs) (*api.ParseFileResult, error) { return cApiCall[*api.ParseFileArgs, *api.ParseFileResult](c, "KclService.ParseFile", in) } diff --git a/go/native/client_test.go b/go/native/client_test.go index 2d244106..f08731bb 100644 --- a/go/native/client_test.go +++ b/go/native/client_test.go @@ -61,7 +61,7 @@ n = Name {name = "name"}` // Sample KCL source code } } -func ParseFileASTJson(filename string, src interface{}) (result string, err error) { +func ParseFileASTJson(filename string, src any) (result string, err error) { var code string if src != nil { switch src := src.(type) { diff --git a/go/native/native_musl_amd64.go b/go/native/native_musl_amd64.go new file mode 100644 index 00000000..9a00def5 --- /dev/null +++ b/go/native/native_musl_amd64.go @@ -0,0 +1,86 @@ +//go:build musl && amd64 +// +build musl,amd64 + +package native + +/* +#cgo LDFLAGS: -L${SRCDIR}/../lib/linux-musl-amd64 -lkcl -static +#include +#include "../include/kcl.h" +*/ +import "C" +import ( + "runtime" + "sync" + "unsafe" + + "kcl-lang.io/lib/go/api" + "kcl-lang.io/lib/go/plugin" +) + +var libInit sync.Once + +var ( + client *NativeServiceClient + serviceNew func(uint64) uintptr + serviceDelete func(uintptr) + serviceCall func(uintptr, string, string, uint, *uint) uintptr + free func(uintptr, uint) +) + +type NativeServiceClient struct { + svc uintptr +} + +func initClient(pluginAgent uint64) { + libInit.Do(func() { + serviceNew = func(agent uint64) uintptr { + return uintptr(C.kcl_service_new(C.uint64_t(agent))) + } + serviceDelete = func(svc uintptr) { + C.kcl_service_delete(C.uintptr_t(svc)) + } + serviceCall = func(svc uintptr, method, args string, argsLen uint, outSize *uint) uintptr { + cSvc := C.KclServiceHandle(svc) + cMethod := C.CString(method) + defer C.free(unsafe.Pointer(cMethod)) + cArgs := C.CString(args) + defer C.free(unsafe.Pointer(cArgs)) + cArgsLen := C.uint32_t(argsLen) + cOutSize := (*C.uint32_t)(unsafe.Pointer(outSize)) + + var cResultPtr *C.uint8_t + cResultPtr = C.kcl_service_call_with_length(cSvc, cMethod, cArgs, cArgsLen, cOutSize) + return uintptr(unsafe.Pointer(cResultPtr)) + } + free = func(ptr uintptr, len uint) { + cPtr := (*C.uint8_t)(unsafe.Pointer(ptr)) + cLen := C.uint32_t(len) + C.kcl_free(cPtr, cLen) + } + client = &NativeServiceClient{ + svc: serviceNew(pluginAgent), + } + runtime.SetFinalizer(client, func(x *NativeServiceClient) { + if x != nil { + x.Close() + } + }) + }) +} + +func NewNativeServiceClient() api.ServiceClient { + return NewNativeServiceClientWithPluginAgent(plugin.GetInvokeJsonProxyPtr()) +} + +func NewNativeServiceClientWithPluginAgent(pluginAgent uint64) *NativeServiceClient { + initClient(pluginAgent) + return client +} + +func (x *NativeServiceClient) Close() { + if x.svc != 0 { + serviceDelete(x.svc) + x.svc = 0 + } +} diff --git a/go/native/native_musl_arm64.go b/go/native/native_musl_arm64.go new file mode 100644 index 00000000..47508a65 --- /dev/null +++ b/go/native/native_musl_arm64.go @@ -0,0 +1,81 @@ +//go:build musl && arm64 +// +build musl,arm64 + +package native + +/* +#cgo LDFLAGS: -L${SRCDIR}/../lib/linux-musl-arm64 -lkcl -static +#include +#include "../include/kcl.h" +*/ +import "C" +import ( + "runtime" + "sync" + "unsafe" + + "kcl-lang.io/lib/go/api" + "kcl-lang.io/lib/go/plugin" +) + +var libInit sync.Once + +var ( + client *NativeServiceClient + serviceNew func(uint64) uintptr + serviceDelete func(uintptr) + serviceCall func(uintptr, string, string, uint, *uint) uintptr + free func(uintptr, uint) +) + +type NativeServiceClient struct { + svc uintptr +} + +func initClient(pluginAgent uint64) { + libInit.Do(func() { + serviceNew = func(agent uint64) uintptr { + return uintptr(C.kcl_service_new(C.uint64_t(agent))) + } + serviceDelete = func(svc uintptr) { + C.kcl_service_delete(C.uintptr_t(svc)) + } + serviceCall = func(svc uintptr, method, args string, argsLen uint, outSize *uint) uintptr { + cSvc := C.uintptr_t(svc) + cMethod := C.CString(method) + defer C.free(unsafe.Pointer(cMethod)) + cArgs := C.CString(args) + defer C.free(unsafe.Pointer(cArgs)) + cArgsLen := C.uint(argsLen) + cOutSize := (*C.uint)(unsafe.Pointer(outSize)) + return uintptr(C.kcl_service_call_with_length(cSvc, cMethod, cArgs, cArgsLen, cOutSize)) + } + free = func(ptr uintptr, len uint) { + C.kcl_free(C.uintptr_t(ptr), C.uint(len)) + } + client = &NativeServiceClient{ + svc: serviceNew(pluginAgent), + } + runtime.SetFinalizer(client, func(x *NativeServiceClient) { + if x != nil { + x.Close() + } + }) + }) +} + +func NewNativeServiceClient() api.ServiceClient { + return NewNativeServiceClientWithPluginAgent(plugin.GetInvokeJsonProxyPtr()) +} + +func NewNativeServiceClientWithPluginAgent(pluginAgent uint64) *NativeServiceClient { + initClient(pluginAgent) + return client +} + +func (x *NativeServiceClient) Close() { + if x.svc != 0 { + serviceDelete(x.svc) + x.svc = 0 + } +} diff --git a/go/native/native_nonmusl.go b/go/native/native_nonmusl.go new file mode 100644 index 00000000..f55331e5 --- /dev/null +++ b/go/native/native_nonmusl.go @@ -0,0 +1,69 @@ +//go:build !musl && (darwin || freebsd || linux || windows) +// +build !musl +// +build darwin freebsd linux windows + +package native + +import ( + "runtime" + "sync" + + "github.com/ebitengine/purego" + "kcl-lang.io/lib/go/api" + "kcl-lang.io/lib/go/plugin" +) + +var libInit sync.Once + +var ( + client *NativeServiceClient + lib uintptr + serviceNew func(uint64) uintptr + serviceDelete func(uintptr) + serviceCall func(uintptr, string, string, uint, *uint) uintptr + free func(uintptr, uint) +) + +type NativeServiceClient struct { + svc uintptr +} + +func initClient(pluginAgent uint64) { + libInit.Do(func() { + var err error + lib, err = loadServiceNativeLib() + if err != nil { + panic(err) + } + purego.RegisterLibFunc(&serviceNew, lib, "kcl_service_new") + purego.RegisterLibFunc(&serviceDelete, lib, "kcl_service_delete") + purego.RegisterLibFunc(&serviceCall, lib, "kcl_service_call_with_length") + purego.RegisterLibFunc(&free, lib, "kcl_free") + + client = new(NativeServiceClient) + client.svc = serviceNew(pluginAgent) + runtime.SetFinalizer(client, func(x *NativeServiceClient) { + if x != nil { + x.Close() + } + }) + }) +} + +func NewNativeServiceClient() api.ServiceClient { + return NewNativeServiceClientWithPluginAgent(plugin.GetInvokeJsonProxyPtr()) +} + +func NewNativeServiceClientWithPluginAgent(pluginAgent uint64) *NativeServiceClient { + initClient(pluginAgent) + return client +} + +func (x *NativeServiceClient) Close() { + if x.svc != 0 { + serviceDelete(x.svc) + x.svc = 0 + } + closeLibrary(lib) + lib = 0 +} diff --git a/go/plugin/api_test.go b/go/plugin/api_test.go index 3d15c81f..d039a0b8 100644 --- a/go/plugin/api_test.go +++ b/go/plugin/api_test.go @@ -36,14 +36,14 @@ func init() { } func TestPlugin_strings_join(t *testing.T) { - result_json := Invoke("kcl_plugin.strings.join", []interface{}{"KCL", "KCL", 123}, nil) + result_json := Invoke("kcl_plugin.strings.join", []any{"KCL", "KCL", 123}, nil) if result_json != `"KCL.KCL.123"` { t.Fatal(result_json) } } func TestPlugin_strings_panic(t *testing.T) { - result_json := Invoke("kcl_plugin.strings.panic", []interface{}{"KCL", "KCL", 123}, nil) + result_json := Invoke("kcl_plugin.strings.panic", []any{"KCL", "KCL", 123}, nil) if result_json != `{"__kcl_PanicInfo__":"[KCL KCL 123]"}` { t.Fatal(result_json) } diff --git a/go/plugin/plugin.go b/go/plugin/plugin.go index 95f846e2..bcf8bead 100644 --- a/go/plugin/plugin.go +++ b/go/plugin/plugin.go @@ -41,7 +41,7 @@ func GetInvokeJsonProxyPtr() uint64 { return ptr } -func Invoke(method string, args []interface{}, kwargs map[string]interface{}) (result_json string) { +func Invoke(method string, args []any, kwargs map[string]any) (result_json string) { var args_json, kwargs_json string if len(args) > 0 { diff --git a/go/plugin/spec.go b/go/plugin/spec.go index 1f552665..433de0b3 100644 --- a/go/plugin/spec.go +++ b/go/plugin/spec.go @@ -37,14 +37,14 @@ type MethodType struct { // MethodArgs represents the arguments passed to a KCL Plugin method. // It includes a list of positional arguments and a map of keyword arguments. type MethodArgs struct { - Args []interface{} // List of positional arguments - KwArgs map[string]interface{} // Map of keyword arguments + Args []any // List of positional arguments + KwArgs map[string]any // Map of keyword arguments } // MethodResult represents the result returned from a KCL Plugin method. // It holds the value of the result. type MethodResult struct { - V interface{} // Result value + V any // Result value } // ParseMethodArgs parses JSON strings for positional and keyword arguments @@ -53,7 +53,7 @@ type MethodResult struct { // kwargs_json: JSON string of keyword arguments func ParseMethodArgs(args_json, kwargs_json string) (*MethodArgs, error) { p := &MethodArgs{ - KwArgs: make(map[string]interface{}), + KwArgs: make(map[string]any), } if args_json != "" { if err := json.Unmarshal([]byte(args_json), &p.Args); err != nil { @@ -82,12 +82,12 @@ func (p *MethodArgs) GetCallArg(index int, key string) any { } // Arg returns the positional argument at the specified index. -func (p *MethodArgs) Arg(i int) interface{} { +func (p *MethodArgs) Arg(i int) any { return p.Args[i] } // KwArg returns the keyword argument with the given name. -func (p *MethodArgs) KwArg(name string) interface{} { +func (p *MethodArgs) KwArg(name string) any { return p.KwArgs[name] }