Skip to content

Commit

Permalink
Add go version to gopls cache key (#20922)
Browse files Browse the repository at this point in the history
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
#8188 we first check if we can
find gopls in the PATH before downloading and caching gopls, but the
issue still exists.
  • Loading branch information
nilskch authored Dec 9, 2024
1 parent e58cdca commit ce9e462
Showing 1 changed file with 25 additions and 12 deletions.
37 changes: 25 additions & 12 deletions crates/languages/src/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::{
ffi::{OsStr, OsString},
ops::Range,
path::PathBuf,
process::Output,
str,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Expand All @@ -35,8 +36,8 @@ impl GoLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("gopls");
}

static GOPLS_VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create GOPLS_VERSION_REGEX"));
static VERSION_REGEX: LazyLock<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(|| {
Regex::new(r#"[.*+?^${}()|\[\]\\]"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX")
Expand Down Expand Up @@ -111,11 +112,18 @@ impl super::LspAdapter for GoLspAdapter {
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> 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 this = *self;

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 metadata.is_file() {
remove_matching(&container_dir, |entry| {
Expand All @@ -139,8 +147,6 @@ impl super::LspAdapter for GoLspAdapter {

let gobin_dir = container_dir.join("gobin");
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)
.env("GO111MODULE", "on")
.env("GOBIN", &gobin_dir)
Expand All @@ -164,13 +170,8 @@ impl super::LspAdapter for GoLspAdapter {
.output()
.await
.context("failed to run installed gopls binary")?;
let version_stdout = str::from_utf8(&version_output.stdout)
.context("gopls version produced invalid utf8 output")?;
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}"));
let gopls_version = parse_version_output(&version_output)?;
let binary_path = container_dir.join(format!("gopls_{gopls_version}_go_{go_version}"));
fs::rename(&installed_binary_path, &binary_path).await?;

Ok(LanguageServerBinary {
Expand Down Expand Up @@ -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> {
maybe!(async {
let mut last_binary_path = None;
Expand Down

0 comments on commit ce9e462

Please sign in to comment.