Detect new releases of codex-acp (#39388)

Now we use GitHub Releases to detect when there's a new version of
codex-acp out, and we notify the user in the same way we do for the
other external agents.

This also moves `github_download.rs` out of the `languages` crate and
into `http_client`, because now we're not just using it for language
servers anymore, we're also using it for external agents.

Release Notes:

- N/A

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
Richard Feldman
2025-10-03 12:10:40 +02:00
committed by GitHub
co-authored by Lukas Wirth
parent 3ab5103de1
commit 662ec9977f
12 changed files with 69 additions and 83 deletions
+2 -3
View File
@@ -3,6 +3,7 @@ use async_trait::async_trait;
use futures::StreamExt;
use gpui::{App, AsyncApp};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
pub use language::*;
use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName};
use project::lsp_store::clangd_ext;
@@ -11,8 +12,6 @@ use smol::fs;
use std::{env::consts, path::PathBuf, sync::Arc};
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
use crate::github_download::{GithubBinaryMetadata, download_server_binary};
pub struct CLspAdapter;
impl CLspAdapter {
@@ -119,7 +118,7 @@ impl LspInstaller for CLspAdapter {
}
}
download_server_binary(
delegate,
&*delegate.http_client(),
&url,
expected_digest.as_deref(),
&container_dir,
-190
View File
@@ -1,190 +0,0 @@
use std::{path::Path, pin::Pin, task::Poll};
use anyhow::{Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader};
use http_client::github::AssetKind;
use language::LspAdapterDelegate;
use sha2::{Digest, Sha256};
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub(crate) struct GithubBinaryMetadata {
pub(crate) metadata_version: u64,
pub(crate) digest: Option<String>,
}
impl GithubBinaryMetadata {
pub(crate) async fn read_from_file(metadata_path: &Path) -> Result<GithubBinaryMetadata> {
let metadata_content = async_fs::read_to_string(metadata_path)
.await
.with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
serde_json::from_str(&metadata_content)
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))
}
pub(crate) async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
let metadata_content = serde_json::to_string(self)
.with_context(|| format!("serializing metadata for {metadata_path:?}"))?;
async_fs::write(metadata_path, metadata_content.as_bytes())
.await
.with_context(|| format!("writing metadata file at {metadata_path:?}"))?;
Ok(())
}
}
pub(crate) async fn download_server_binary(
delegate: &dyn LspAdapterDelegate,
url: &str,
digest: Option<&str>,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<(), anyhow::Error> {
log::info!("downloading github artifact from {url}");
let mut response = delegate
.http_client()
.get(url, Default::default(), true)
.await
.with_context(|| format!("downloading release from {url}"))?;
let body = response.body_mut();
match digest {
Some(expected_sha_256) => {
let temp_asset_file = tempfile::NamedTempFile::new()
.with_context(|| format!("creating a temporary file for {url}"))?;
let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts();
let mut writer = HashingWriter {
writer: async_fs::File::from(temp_asset_file),
hasher: Sha256::new(),
};
futures::io::copy(&mut BufReader::new(body), &mut writer)
.await
.with_context(|| {
format!("saving archive contents into the temporary file for {url}",)
})?;
let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
anyhow::ensure!(
asset_sha_256 == expected_sha_256,
"{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",
);
writer
.writer
.seek(std::io::SeekFrom::Start(0))
.await
.with_context(|| format!("seeking temporary file {destination_path:?}",))?;
stream_file_archive(&mut writer.writer, url, destination_path, asset_kind)
.await
.with_context(|| {
format!("extracting downloaded asset for {url} into {destination_path:?}",)
})?;
}
None => stream_response_archive(body, url, destination_path, asset_kind)
.await
.with_context(|| {
format!("extracting response for asset {url} into {destination_path:?}",)
})?,
}
Ok(())
}
async fn stream_response_archive(
response: impl AsyncRead + Unpin,
url: &str,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<()> {
match asset_kind {
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
AssetKind::Zip => {
util::archive::extract_zip(destination_path, response).await?;
}
};
Ok(())
}
async fn stream_file_archive(
file_archive: impl AsyncRead + AsyncSeek + Unpin,
url: &str,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<()> {
match asset_kind {
AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?,
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
#[cfg(not(windows))]
AssetKind::Zip => {
util::archive::extract_seekable_zip(destination_path, file_archive).await?;
}
#[cfg(windows)]
AssetKind::Zip => {
util::archive::extract_zip(destination_path, file_archive).await?;
}
};
Ok(())
}
async fn extract_tar_gz(
destination_path: &Path,
url: &str,
from: impl AsyncRead + Unpin,
) -> Result<(), anyhow::Error> {
let decompressed_bytes = GzipDecoder::new(BufReader::new(from));
let archive = async_tar::Archive::new(decompressed_bytes);
archive
.unpack(&destination_path)
.await
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
Ok(())
}
async fn extract_gz(
destination_path: &Path,
url: &str,
from: impl AsyncRead + Unpin,
) -> Result<(), anyhow::Error> {
let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from));
let mut file = smol::fs::File::create(&destination_path)
.await
.with_context(|| {
format!("creating a file {destination_path:?} for a download from {url}")
})?;
futures::io::copy(&mut decompressed_bytes, &mut file)
.await
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
Ok(())
}
struct HashingWriter<W: AsyncWrite + Unpin> {
writer: W,
hasher: Sha256,
}
impl<W: AsyncWrite + Unpin> AsyncWrite for HashingWriter<W> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<std::result::Result<usize, std::io::Error>> {
match Pin::new(&mut self.writer).poll_write(cx, buf) {
Poll::Ready(Ok(n)) => {
self.hasher.update(&buf[..n]);
Poll::Ready(Ok(n))
}
other => other,
}
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.writer).poll_flush(cx)
}
fn poll_close(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<std::result::Result<(), std::io::Error>> {
Pin::new(&mut self.writer).poll_close(cx)
}
}
-1
View File
@@ -20,7 +20,6 @@ use crate::{
mod bash;
mod c;
mod css;
mod github_download;
mod go;
mod json;
mod package_json;
+3 -4
View File
@@ -26,6 +26,7 @@ use std::env::consts;
use util::fs::{make_file_executable, remove_matching};
use util::rel_path::RelPath;
use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
use parking_lot::Mutex;
use std::str::FromStr;
use std::{
@@ -37,8 +38,6 @@ use std::{
use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, maybe};
use crate::github_download::{GithubBinaryMetadata, download_server_binary};
pub(crate) struct PyprojectTomlManifestProvider;
impl ManifestProvider for PyprojectTomlManifestProvider {
@@ -272,7 +271,7 @@ impl LspInstaller for TyLspAdapter {
}
download_server_binary(
delegate,
&*delegate.http_client(),
&url,
expected_digest.as_deref(),
&destination_path,
@@ -2116,7 +2115,7 @@ impl LspInstaller for RuffLspAdapter {
}
download_server_binary(
delegate,
&*delegate.http_client(),
&url,
expected_digest.as_deref(),
&destination_path,
+2 -2
View File
@@ -5,6 +5,7 @@ use futures::StreamExt;
use gpui::{App, AppContext, AsyncApp, SharedString, Task};
use http_client::github::AssetKind;
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
pub use language::*;
use lsp::{InitializeParams, LanguageServerBinary};
use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
@@ -26,7 +27,6 @@ use util::merge_json_value_into;
use util::rel_path::RelPath;
use util::{ResultExt, maybe};
use crate::github_download::{GithubBinaryMetadata, download_server_binary};
use crate::language_settings::language_settings;
pub struct RustLspAdapter;
@@ -484,7 +484,7 @@ impl LspInstaller for RustLspAdapter {
}
download_server_binary(
delegate,
&*delegate.http_client(),
&url,
expected_digest.as_deref(),
&destination_path,
+3 -2
View File
@@ -5,6 +5,7 @@ use collections::HashMap;
use futures::future::join_all;
use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use http_client::github_download::download_server_binary;
use itertools::Itertools as _;
use language::{
ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
@@ -25,7 +26,7 @@ use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, fs::remove_matching, maybe};
use util::{merge_json_value_into, rel_path::RelPath};
use crate::{PackageJson, PackageJsonData, github_download::download_server_binary};
use crate::{PackageJson, PackageJsonData};
pub(crate) struct TypeScriptContextProvider {
fs: Arc<dyn Fs>,
@@ -853,7 +854,7 @@ impl LspInstaller for EsLintLspAdapter {
remove_matching(&container_dir, |_| true).await;
download_server_binary(
delegate,
&*delegate.http_client(),
&version.url,
None,
&destination_path,