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,6 +163,88 @@ func FindGoExecutable() (string, error) { return "", fmt.Errorf("go command not found in PATH or common installation locations") } +func CheckGoVersion(customGoPath string) GoVersionCheckResult { + var goPath string + var err error + + if customGoPath != "" { + goPath = customGoPath + } else { + goPath, err = FindGoExecutable() + if err != nil { + return GoVersionCheckResult{ + GoStatus: "notfound", + GoPath: "", + GoVersion: "", + ErrorString: "", + } + } + } + + cmd := exec.Command(goPath, "version") + output, err := cmd.Output() + if err != nil { + return GoVersionCheckResult{ + GoStatus: "error", + GoPath: goPath, + GoVersion: "", + ErrorString: fmt.Sprintf("failed to run 'go version': %v", err), + } + } + + versionStr := strings.TrimSpace(string(output)) + + versionRegex := regexp.MustCompile(`go(1\.\d+)`) + matches := versionRegex.FindStringSubmatch(versionStr) + if len(matches) < 2 { + return GoVersionCheckResult{ + GoStatus: "error", + GoPath: goPath, + GoVersion: versionStr, + ErrorString: fmt.Sprintf("unable to parse go version from: %s", versionStr), + } + } + + goVersion := matches[1] + + minorRegex := regexp.MustCompile(`1\.(\d+)`) + minorMatches := minorRegex.FindStringSubmatch(goVersion) + if len(minorMatches) < 2 { + 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 { + 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 @@ -170,57 +259,36 @@ func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) { } } - var goPath string - var err error + result := CheckGoVersion(opts.GoPath) - if opts.GoPath != "" { - goPath = 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 { - oc.Printf("Using custom go path: %s", opts.GoPath) - } - } 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) + 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) } - // 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) - } - - // 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) + matches := versionRegex.FindStringSubmatch(result.GoVersion) if len(matches) < 2 { - return nil, fmt.Errorf("unable to parse go version from: %s", versionStr) + return nil, fmt.Errorf("unable to parse go version from: %s", result.GoVersion) } - 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) - } - - 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) - } + var err error // Check if node is available if opts.NodePath != "" {