From 4921c5c98716dd505395eef6b8d7551463a2723f Mon Sep 17 00:00:00 2001 From: Hoblovski Date: Wed, 17 Sep 2025 21:10:11 +0800 Subject: [PATCH] feat: auto env setup --- lang/cxx/lib.go | 5 +++ lang/parse.go | 81 +++++++++++++++++++++++++++++----------------- lang/python/lib.go | 68 +++++++++++++++++++++++++++++++++++++- 3 files changed, 124 insertions(+), 30 deletions(-) diff --git a/lang/cxx/lib.go b/lang/cxx/lib.go index 06fdad74..dceba3f2 100644 --- a/lang/cxx/lib.go +++ b/lang/cxx/lib.go @@ -15,6 +15,7 @@ package cxx import ( + "fmt" "time" "github.com/cloudwego/abcoder/lang/uniast" @@ -23,6 +24,10 @@ import ( const MaxWaitDuration = 5 * time.Minute +func InstallLanguageServer() (string, error) { + return "", fmt.Errorf("please install clangd-18 manually. See https://releases.llvm.org/ (clangd is in clang-extra)") +} + func GetDefaultLSP() (lang uniast.Language, name string) { return uniast.Cxx, "clangd-18" } diff --git a/lang/parse.go b/lang/parse.go index 54d77347..7dcc22d5 100644 --- a/lang/parse.go +++ b/lang/parse.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/cloudwego/abcoder/lang/register" "os" "os/exec" "path/filepath" @@ -33,6 +32,7 @@ import ( "github.com/cloudwego/abcoder/lang/log" "github.com/cloudwego/abcoder/lang/lsp" "github.com/cloudwego/abcoder/lang/python" + "github.com/cloudwego/abcoder/lang/register" "github.com/cloudwego/abcoder/lang/rust" "github.com/cloudwego/abcoder/lang/uniast" ) @@ -138,40 +138,63 @@ func checkRepoPath(repoPath string, language uniast.Language) (openfile string, } func checkLSP(language uniast.Language, lspPath string, args ParseOptions) (l uniast.Language, s string, err error) { - switch language { - case uniast.Rust: - l, s = rust.GetDefaultLSP() - case uniast.Cxx: - l, s = cxx.GetDefaultLSP() - case uniast.Python: - l, s = python.GetDefaultLSP() - case uniast.Java: - l, s = java.GetDefaultLSP(args.LspOptions) - case uniast.Golang: - l = uniast.Golang - s = "" - if _, err := exec.LookPath("go"); err != nil { - if _, err := os.Stat(lspPath); os.IsNotExist(err) { - log.Error("Go compiler not found, please make it excutable!\n", lspPath) - return uniast.Unknown, "", err + if lspPath != "" { + // designated LSP + l = language + s = lspPath + } else { + // default LSP + switch language { + case uniast.Rust: + l, s = rust.GetDefaultLSP() + case uniast.Cxx: + l, s = cxx.GetDefaultLSP() + case uniast.Python: + l, s = python.GetDefaultLSP() + case uniast.Java: + l, s = java.GetDefaultLSP(args.LspOptions) + case uniast.Golang: + if _, err := exec.LookPath("go"); err != nil { + if _, err := os.Stat(lspPath); os.IsNotExist(err) { + log.Error("Go compiler not found, please make it excutable!\n", lspPath) + return uniast.Unknown, "", err + } } + return uniast.Golang, "", nil + default: + return uniast.Unknown, "", fmt.Errorf("unsupported language: %s", language) } - return - default: - return uniast.Unknown, "", fmt.Errorf("unsupported language: %s", language) } - // check if lsp excutable - if lspPath != "" { - if _, err := exec.LookPath(lspPath); err != nil { - if _, err := os.Stat(lspPath); os.IsNotExist(err) { - log.Error("Language server %s not found, please make it excutable!\n", lspPath) - return uniast.Unknown, "", err - } + + // lsp already installed + if absLspPath, err := exec.LookPath(s); err == nil { + return l, absLspPath, nil + } + + // install the lsp. + log.Error("Language server %s not found. Trying to auto install.\n", s) + s, err = installLanguageServer(language) + if err == nil { + if absLspPath, err := exec.LookPath(s); err == nil { + log.Error("Auto installation ok. lspPath=%s.", absLspPath) + return l, absLspPath, nil } - s = lspPath } - return + // install failed or broken (lsp not in PATH) + log.Info("Failed to install language server %s: %+w.\n", s, err) + return uniast.Unknown, "", err +} + +func installLanguageServer(language uniast.Language) (string, error) { + switch language { + case uniast.Cxx: + return cxx.InstallLanguageServer() + case uniast.Python: + return python.InstallLanguageServer() + default: + return "", fmt.Errorf("auto installation not supported for language: %s", language) + } } func collectSymbol(ctx context.Context, cli *lsp.LSPClient, repoPath string, opts collect.CollectOption) (repo *uniast.Repository, err error) { diff --git a/lang/python/lib.go b/lang/python/lib.go index 7b19d4ff..17b15a91 100644 --- a/lang/python/lib.go +++ b/lang/python/lib.go @@ -15,16 +15,82 @@ package python import ( + "fmt" + "os" + "os/exec" + "regexp" + "strconv" + "strings" "time" + "github.com/cloudwego/abcoder/lang/log" "github.com/cloudwego/abcoder/lang/uniast" "github.com/cloudwego/abcoder/lang/utils" ) const MaxWaitDuration = 5 * time.Second +const lspName = "pylsp" + +func CheckPythonVersion() error { + // Check python3 command availability and get version. + output, err := exec.Command("python3", "--version").CombinedOutput() + if err != nil { + return fmt.Errorf("python3 not found: %w. Do you have it installed? Or is it `python` but not aliased?", err) + } + + // The regex is corrected to handle a capital 'P' and correctly capture the minor version. + format := `^Python 3\.(\d+)\..*$` + ptn := regexp.MustCompile(format) + matches := ptn.FindStringSubmatch(strings.TrimSpace(string(output))) + if len(matches) < 2 { + return fmt.Errorf("unexpected `python3 --version` output format: %q", output) + } + subver, err := strconv.ParseInt(matches[1], 10, 64) + if err != nil { + return fmt.Errorf("failed to parse python version from `python3 --version` output %q: %w", output, err) + } + if subver < 9 { + return fmt.Errorf("python version 3.%d is not supported; 3.9 or higher is required", subver) + } + return nil +} + +func InstallLanguageServer() (string, error) { + if _, err := os.Stat("go.mod"); os.IsNotExist(err) { + log.Error("Auto installation requires working directory to be /path/to/abcoder/") + return "", fmt.Errorf("bad cwd") + } + if err := CheckPythonVersion(); err != nil { + log.Error("python version check failed: %v", err) + return "", err + } + // git submodule init + log.Error("Installing pylsp...") + if err := exec.Command("git", "submodule", "init").Run(); err != nil { + log.Error("git submodule init failed: %v", err) + return "", err + } + // git submodule update + if err := exec.Command("git", "submodule", "update").Run(); err != nil { + log.Error("git submodule update failed: %v", err) + return "", err + } + // python -m pip install -e projectRoot/pylsp + log.Error("Running `python3 -m pip install -e pylsp/` .\nThis might take some time, make sure the network connection is ok.") + if err := exec.Command("python3", "-m", "pip", "install", "-e", "pylsp/").Run(); err != nil { + log.Error("python -m pip install failed: %v", err) + return "", err + } + if err := exec.Command("pylsp", "--version").Run(); err != nil { + log.Error("`pylsp --version` failed: %v", err) + return "", err + } + log.Error("pylsp installed.") + return lspName, nil +} func GetDefaultLSP() (lang uniast.Language, name string) { - return uniast.Python, "pylsp" + return uniast.Python, lspName } func CheckRepo(repo string) (string, time.Duration) {