diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index ee7d5a87e..c42baccf3 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -42,6 +42,11 @@ class RpcApiType { return client.wshRpcCall("captureblockscreenshot", data, opts); } + // command "checkgoversion" [call] + CheckGoVersionCommand(client: WshClient, opts?: RpcOpts): Promise { + return client.wshRpcCall("checkgoversion", null, opts); + } + // command "connconnect" [call] ConnConnectCommand(client: WshClient, data: ConnRequest, opts?: RpcOpts): Promise { return client.wshRpcCall("connconnect", data, opts); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index b6915ef90..f046fafa1 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -173,6 +173,14 @@ declare global { blockid: string; }; + // wshrpc.CommandCheckGoVersionRtnData + type CommandCheckGoVersionRtnData = { + gostatus: string; + gopath: string; + goversion: string; + errorstring?: string; + }; + // wshrpc.CommandControllerAppendOutputData type CommandControllerAppendOutputData = { blockid: string; diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 030074033..2e3c0e7cf 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -59,6 +59,12 @@ func CaptureBlockScreenshotCommand(w *wshutil.WshRpc, data wshrpc.CommandCapture return resp, err } +// command "checkgoversion", wshserver.CheckGoVersionCommand +func CheckGoVersionCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (*wshrpc.CommandCheckGoVersionRtnData, error) { + resp, err := sendRpcRequestCallHelper[*wshrpc.CommandCheckGoVersionRtnData](w, "checkgoversion", nil, opts) + return resp, err +} + // command "connconnect", wshserver.ConnConnectCommand func ConnConnectCommand(w *wshutil.WshRpc, data wshrpc.ConnRequest, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "connconnect", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index bacb82ef1..253232469 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -167,6 +167,7 @@ const ( Command_StartBuilder = "startbuilder" Command_GetBuilderStatus = "getbuilderstatus" Command_GetBuilderOutput = "getbuilderoutput" + Command_CheckGoVersion = "checkgoversion" // electron Command_ElectronEncrypt = "electronencrypt" @@ -335,6 +336,7 @@ type WshRpcInterface interface { StartBuilderCommand(ctx context.Context, data CommandStartBuilderData) error GetBuilderStatusCommand(ctx context.Context, builderId string) (*BuilderStatusData, error) GetBuilderOutputCommand(ctx context.Context, builderId string) ([]string, error) + CheckGoVersionCommand(ctx context.Context) (*CommandCheckGoVersionRtnData, error) // proc VDomRenderCommand(ctx context.Context, data vdom.VDomFrontendUpdate) chan RespOrErrorUnion[*vdom.VDomBackendUpdate] @@ -1018,6 +1020,13 @@ type BuilderStatusData struct { Version int `json:"version"` } +type CommandCheckGoVersionRtnData struct { + GoStatus string `json:"gostatus"` + GoPath string `json:"gopath"` + GoVersion string `json:"goversion"` + ErrorString string `json:"errorstring,omitempty"` +} + type CommandElectronEncryptData struct { PlainText string `json:"plaintext"` } diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 8b18d814c..572c85ab1 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -57,6 +57,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/wsl" "github.com/wavetermdev/waveterm/pkg/wslconn" "github.com/wavetermdev/waveterm/pkg/wstore" + "github.com/wavetermdev/waveterm/tsunami/build" ) var InvalidWslDistroNames = []string{"docker-desktop", "docker-desktop-data"} @@ -1073,6 +1074,21 @@ func (ws *WshServer) GetBuilderOutputCommand(ctx context.Context, builderId stri return bc.GetOutput(), nil } +func (ws *WshServer) CheckGoVersionCommand(ctx context.Context) (*wshrpc.CommandCheckGoVersionRtnData, error) { + watcher := wconfig.GetWatcher() + fullConfig := watcher.GetFullConfig() + goPath := fullConfig.Settings.TsunamiGoPath + + result := build.CheckGoVersion(goPath) + + return &wshrpc.CommandCheckGoVersionRtnData{ + GoStatus: result.GoStatus, + GoPath: result.GoPath, + GoVersion: result.GoVersion, + ErrorString: result.ErrorString, + }, nil +} + func (ws *WshServer) RecordTEventCommand(ctx context.Context, data telemetrydata.TEvent) error { err := telemetry.RecordTEvent(ctx, &data) if err != nil { diff --git a/tsunami/build/build.go b/tsunami/build/build.go index ffbfe801a..96b9972f4 100644 --- a/tsunami/build/build.go +++ b/tsunami/build/build.go @@ -119,6 +119,13 @@ func (opts BuildOpts) getNodePath() string { return "node" } +type GoVersionCheckResult struct { + GoStatus string + GoPath string + GoVersion string + ErrorString string +} + func FindGoExecutable() (string, error) { // First try the standard PATH lookup if goPath, err := exec.LookPath("go"); err == nil { @@ -156,72 +163,133 @@ func FindGoExecutable() (string, error) { return "", fmt.Errorf("go command not found in PATH or common installation locations") } -func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) { - oc := opts.OutputCapture - - if opts.SdkVersion == "" && opts.SdkReplacePath == "" { - return nil, fmt.Errorf("either SdkVersion or SdkReplacePath must be set") - } - - if opts.SdkVersion != "" { - versionRegex := regexp.MustCompile(`^v\d+\.\d+\.\d+`) - if !versionRegex.MatchString(opts.SdkVersion) { - return nil, fmt.Errorf("SdkVersion must be in semantic version format (e.g., v0.0.0), got: %s", opts.SdkVersion) - } - } - +func CheckGoVersion(customGoPath string) GoVersionCheckResult { var goPath string var err error - if opts.GoPath != "" { - goPath = opts.GoPath - if verbose { - oc.Printf("Using custom go path: %s", opts.GoPath) - } + if customGoPath != "" { + goPath = customGoPath } else { goPath, err = FindGoExecutable() if err != nil { - return nil, fmt.Errorf("go command not found: %w", err) - } - if verbose { - oc.Printf("Using go path: %s", goPath) + return GoVersionCheckResult{ + GoStatus: "notfound", + GoPath: "", + GoVersion: "", + ErrorString: "", + } } } - // Run go version command cmd := exec.Command(goPath, "version") output, err := cmd.Output() if err != nil { - return nil, fmt.Errorf("failed to run 'go version': %w", err) + return GoVersionCheckResult{ + GoStatus: "error", + GoPath: goPath, + GoVersion: "", + ErrorString: fmt.Sprintf("failed to run 'go version': %v", err), + } } - // Parse go version output and check for 1.22+ versionStr := strings.TrimSpace(string(output)) - if verbose { - oc.Printf("Found %s", versionStr) - } - // Extract version like "go1.22.0" from output versionRegex := regexp.MustCompile(`go(1\.\d+)`) matches := versionRegex.FindStringSubmatch(versionStr) if len(matches) < 2 { - return nil, fmt.Errorf("unable to parse go version from: %s", versionStr) + return GoVersionCheckResult{ + GoStatus: "error", + GoPath: goPath, + GoVersion: versionStr, + ErrorString: fmt.Sprintf("unable to parse go version from: %s", versionStr), + } } goVersion := matches[1] - // Check if version is 1.22+ minorRegex := regexp.MustCompile(`1\.(\d+)`) minorMatches := minorRegex.FindStringSubmatch(goVersion) if len(minorMatches) < 2 { - return nil, fmt.Errorf("unable to parse minor version from: %s", goVersion) + return GoVersionCheckResult{ + GoStatus: "error", + GoPath: goPath, + GoVersion: versionStr, + ErrorString: fmt.Sprintf("unable to parse minor version from: %s", goVersion), + } } minor, err := strconv.Atoi(minorMatches[1]) - if err != nil || minor < MinSupportedGoMinorVersion { - return nil, fmt.Errorf("go version 1.%d or higher required, found: %s", MinSupportedGoMinorVersion, versionStr) + if err != nil { + return GoVersionCheckResult{ + GoStatus: "error", + GoPath: goPath, + GoVersion: versionStr, + ErrorString: fmt.Sprintf("failed to parse minor version: %v", err), + } + } + + if minor < MinSupportedGoMinorVersion { + return GoVersionCheckResult{ + GoStatus: "badversion", + GoPath: goPath, + GoVersion: versionStr, + ErrorString: "", + } } + return GoVersionCheckResult{ + GoStatus: "ok", + GoPath: goPath, + GoVersion: versionStr, + ErrorString: "", + } +} + +func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) { + oc := opts.OutputCapture + + if opts.SdkVersion == "" && opts.SdkReplacePath == "" { + return nil, fmt.Errorf("either SdkVersion or SdkReplacePath must be set") + } + + if opts.SdkVersion != "" { + versionRegex := regexp.MustCompile(`^v\d+\.\d+\.\d+`) + if !versionRegex.MatchString(opts.SdkVersion) { + return nil, fmt.Errorf("SdkVersion must be in semantic version format (e.g., v0.0.0), got: %s", opts.SdkVersion) + } + } + + result := CheckGoVersion(opts.GoPath) + + switch result.GoStatus { + case "notfound": + return nil, fmt.Errorf("go command not found") + case "badversion": + return nil, fmt.Errorf("go version 1.%d or higher required, found: %s", MinSupportedGoMinorVersion, result.GoVersion) + case "error": + return nil, fmt.Errorf("%s", result.ErrorString) + case "ok": + if verbose { + if opts.GoPath != "" { + oc.Printf("Using custom go path: %s", result.GoPath) + } else { + oc.Printf("Using go path: %s", result.GoPath) + } + oc.Printf("Found %s", result.GoVersion) + } + default: + return nil, fmt.Errorf("unexpected go status: %s", result.GoStatus) + } + + versionRegex := regexp.MustCompile(`go(1\.\d+)`) + matches := versionRegex.FindStringSubmatch(result.GoVersion) + if len(matches) < 2 { + return nil, fmt.Errorf("unable to parse go version from: %s", result.GoVersion) + } + goVersion := matches[1] + + var err error + // Check if node is available if opts.NodePath != "" { // Custom node path specified - verify it's absolute and executable