Add go version to gopls cache key (#20922)

Closes #8071

Release Notes:

- Changed the Go integration to check whether an existing `gopls` was compiled for the current `go` version.

Previously we cached gopls (the go language server) as a file called
`gopls_{GOPLS_VERSION}`. The go version that gopls was built with is
crucial, so we need to cache the go version as well.

It's actually super interesting and very clever; gopls uses go to parse
the AST and do all the analyzation etc. Go exposes its internals in its
standard lib (`go/parser`, `go/types`, ...), which gopls uses to analyze
the user code. So if there is a new go release that contains new
syntax/features/etc. (the libraries `go/parser`, `go/types`, ...
change), we can rebuild the same version of `gopls` with the new version
of go (with the updated `go/xxx` libraries) to support the new language
features.

We had some issues around that (e.g., range over integers introduced in
go1.22, or custom iterators in go1.23) where we never updated gopls,
because we were on the latest gopls version, but built with an old go
version.

After this PR gopls will be cached under the name
`gopls_{GOPLS_VERSION}_go_{GO_VERSION}`.

Most users do not see this issue anymore, because after
https://github.com/zed-industries/zed/pull/8188 we first check if we can
find gopls in the PATH before downloading and caching gopls, but the
issue still exists.
This commit is contained in:
Nils Koch 2024-12-09 11:56:01 +00:00 committed by GitHub
parent e58cdca044
commit ce9e4629be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -15,6 +15,7 @@ use std::{
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
ops::Range, ops::Range,
path::PathBuf, path::PathBuf,
process::Output,
str, str,
sync::{ sync::{
atomic::{AtomicBool, Ordering::SeqCst}, atomic::{AtomicBool, Ordering::SeqCst},
@ -35,8 +36,8 @@ impl GoLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("gopls"); const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("gopls");
} }
static GOPLS_VERSION_REGEX: LazyLock<Regex> = static VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create GOPLS_VERSION_REGEX")); LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create VERSION_REGEX"));
static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| { static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"[.*+?^${}()|\[\]\\]"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX") Regex::new(r#"[.*+?^${}()|\[\]\\]"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX")
@ -111,11 +112,18 @@ impl super::LspAdapter for GoLspAdapter {
container_dir: PathBuf, container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate, delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> { ) -> Result<LanguageServerBinary> {
let go = delegate.which("go".as_ref()).await.unwrap_or("go".into());
let go_version_output = util::command::new_smol_command(&go)
.args(["version"])
.output()
.await
.context("failed to get go version via `go version` command`")?;
let go_version = parse_version_output(&go_version_output)?;
let version = version.downcast::<Option<String>>().unwrap(); let version = version.downcast::<Option<String>>().unwrap();
let this = *self; let this = *self;
if let Some(version) = *version { if let Some(version) = *version {
let binary_path = container_dir.join(format!("gopls_{version}")); let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}"));
if let Ok(metadata) = fs::metadata(&binary_path).await { if let Ok(metadata) = fs::metadata(&binary_path).await {
if metadata.is_file() { if metadata.is_file() {
remove_matching(&container_dir, |entry| { remove_matching(&container_dir, |entry| {
@ -139,8 +147,6 @@ impl super::LspAdapter for GoLspAdapter {
let gobin_dir = container_dir.join("gobin"); let gobin_dir = container_dir.join("gobin");
fs::create_dir_all(&gobin_dir).await?; fs::create_dir_all(&gobin_dir).await?;
let go = delegate.which("go".as_ref()).await.unwrap_or("go".into());
let install_output = util::command::new_smol_command(go) let install_output = util::command::new_smol_command(go)
.env("GO111MODULE", "on") .env("GO111MODULE", "on")
.env("GOBIN", &gobin_dir) .env("GOBIN", &gobin_dir)
@ -164,13 +170,8 @@ impl super::LspAdapter for GoLspAdapter {
.output() .output()
.await .await
.context("failed to run installed gopls binary")?; .context("failed to run installed gopls binary")?;
let version_stdout = str::from_utf8(&version_output.stdout) let gopls_version = parse_version_output(&version_output)?;
.context("gopls version produced invalid utf8 output")?; let binary_path = container_dir.join(format!("gopls_{gopls_version}_go_{go_version}"));
let version = GOPLS_VERSION_REGEX
.find(version_stdout)
.with_context(|| format!("failed to parse golps version output '{version_stdout}'"))?
.as_str();
let binary_path = container_dir.join(format!("gopls_{version}"));
fs::rename(&installed_binary_path, &binary_path).await?; fs::rename(&installed_binary_path, &binary_path).await?;
Ok(LanguageServerBinary { Ok(LanguageServerBinary {
@ -366,6 +367,18 @@ impl super::LspAdapter for GoLspAdapter {
} }
} }
fn parse_version_output(output: &Output) -> Result<&str> {
let version_stdout =
str::from_utf8(&output.stdout).context("version command produced invalid utf8 output")?;
let version = VERSION_REGEX
.find(version_stdout)
.with_context(|| format!("failed to parse version output '{version_stdout}'"))?
.as_str();
Ok(version)
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> { async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
maybe!(async { maybe!(async {
let mut last_binary_path = None; let mut last_binary_path = None;