Add .tar.bz2 archive support for ACP agent server downloads (#52188)

## Summary

- Added `TarBz2` variant to `AssetKind` enum for `.tar.bz2` / `.tbz2`
archives
- Implemented `extract_tar_bz2` using the `bzip2` feature of
`async-compression` (already a workspace dependency, just enabled the
feature flag)
- Wired up both streaming and file-based extraction paths in
`github_download.rs`
- Added `.tar.bz2` / `.tbz2` URL detection in both
`LocalExtensionArchiveAgent` and `LocalRegistryArchiveAgent`

This unblocks ACP registry entries (like Goose) that only ship
`.tar.bz2` archives.

Reference: https://github.com/block/goose/issues/8047

## Test plan

- [ ] Verify `cargo check` and `clippy` pass (confirmed locally)
- [ ] Test downloading an ACP agent that ships a `.tar.bz2` archive
(e.g., Goose)
- [ ] Verify existing `.tar.gz` and `.zip` agent downloads still work

Release Notes:

- Added support for `.tar.bz2` archives in ACP agent server downloads,
unblocking registry entries like Goose that only ship bzip2-compressed
tarballs.

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
Vincenzo Palazzo 2026-03-25 08:54:23 +01:00 committed by GitHub
parent 32161265d3
commit 3d5ec540f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 52 additions and 12 deletions

18
Cargo.lock generated
View file

@ -2435,6 +2435,15 @@ dependencies = [
"libc",
]
[[package]]
name = "bzip2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
dependencies = [
"libbz2-rs-sys",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
@ -3465,6 +3474,7 @@ version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23"
dependencies = [
"bzip2 0.6.1",
"compression-core",
"deflate64",
"flate2",
@ -9706,6 +9716,12 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libbz2-rs-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]]
name = "libc"
version = "0.2.182"
@ -22447,7 +22463,7 @@ checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"aes",
"byteorder",
"bzip2",
"bzip2 0.4.4",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",

View file

@ -492,7 +492,7 @@ ashpd = { version = "0.13", default-features = false, features = [
] }
async-channel = "2.5.0"
async-compat = "0.2.1"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-compression = { version = "0.4", features = ["bzip2", "gzip", "futures-io"] }
async-dispatcher = "0.1"
async-fs = "2.1"
async-lock = "2.1"

View file

@ -144,6 +144,7 @@ pub async fn get_release_by_tag_name(
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum AssetKind {
TarGz,
TarBz2,
Gz,
Zip,
}
@ -158,6 +159,7 @@ pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -
"{tag}.{extension}",
extension = match kind {
AssetKind::TarGz => "tar.gz",
AssetKind::TarBz2 => "tar.bz2",
AssetKind::Gz => "gz",
AssetKind::Zip => "zip",
}

View file

@ -5,7 +5,7 @@ use std::{
};
use anyhow::{Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_compression::futures::bufread::{BzDecoder, GzipDecoder};
use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader};
use sha2::{Digest, Sha256};
@ -119,7 +119,7 @@ async fn extract_to_staging(
fn staging_path(parent: &Path, asset_kind: AssetKind) -> Result<PathBuf> {
match asset_kind {
AssetKind::TarGz | AssetKind::Zip => {
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Zip => {
let dir = tempfile::Builder::new()
.prefix(".tmp-github-download-")
.tempdir_in(parent)
@ -141,7 +141,7 @@ fn staging_path(parent: &Path, asset_kind: AssetKind) -> Result<PathBuf> {
async fn cleanup_staging_path(staging_path: &Path, asset_kind: AssetKind) {
match asset_kind {
AssetKind::TarGz | AssetKind::Zip => {
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Zip => {
if let Err(err) = async_fs::remove_dir_all(staging_path).await {
log::warn!("failed to remove staging directory {staging_path:?}: {err:?}");
}
@ -170,6 +170,7 @@ async fn stream_response_archive(
) -> Result<()> {
match asset_kind {
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
AssetKind::TarBz2 => extract_tar_bz2(destination_path, url, response).await?,
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
AssetKind::Zip => {
util::archive::extract_zip(destination_path, response).await?;
@ -186,6 +187,7 @@ async fn stream_file_archive(
) -> Result<()> {
match asset_kind {
AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?,
AssetKind::TarBz2 => extract_tar_bz2(destination_path, url, file_archive).await?,
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
#[cfg(not(windows))]
AssetKind::Zip => {
@ -213,6 +215,20 @@ async fn extract_tar_gz(
Ok(())
}
async fn extract_tar_bz2(
destination_path: &Path,
url: &str,
from: impl AsyncRead + Unpin,
) -> Result<(), anyhow::Error> {
let decompressed_bytes = BzDecoder::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,

View file

@ -448,7 +448,7 @@ impl LspInstaller for TyLspAdapter {
async_fs::create_dir_all(&destination_path).await?;
let server_path = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz | AssetKind::Gz => destination_path
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => destination_path
.join(Self::build_asset_name()?.0)
.join("ty"),
AssetKind::Zip => destination_path.clone().join("ty.exe"),
@ -538,7 +538,7 @@ impl LspInstaller for TyLspAdapter {
let path = last.context("no cached binary")?;
let path = match TyLspAdapter::GITHUB_ASSET_KIND {
AssetKind::TarGz | AssetKind::Gz => {
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => {
path.join(Self::build_asset_name()?.0).join("ty")
}
AssetKind::Zip => path.join("ty.exe"),
@ -2520,7 +2520,7 @@ impl LspInstaller for RuffLspAdapter {
} = latest_version;
let destination_path = container_dir.join(format!("ruff-{name}"));
let server_path = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz | AssetKind::Gz => destination_path
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => destination_path
.join(Self::build_asset_name()?.0)
.join("ruff"),
AssetKind::Zip => destination_path.clone().join("ruff.exe"),
@ -2610,7 +2610,7 @@ impl LspInstaller for RuffLspAdapter {
let path = last.context("no cached binary")?;
let path = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz | AssetKind::Gz => {
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => {
path.join(Self::build_asset_name()?.0).join("ruff")
}
AssetKind::Zip => path.join("ruff.exe"),

View file

@ -202,6 +202,7 @@ impl RustLspAdapter {
async fn build_asset_name() -> String {
let extension = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => "tar.gz",
AssetKind::TarBz2 => "tar.bz2",
AssetKind::Gz => "gz",
AssetKind::Zip => "zip",
};
@ -706,7 +707,7 @@ impl LspInstaller for RustLspAdapter {
} = version;
let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
let server_path = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
};
@ -1273,8 +1274,8 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
None => return Ok(None),
};
let path = match RustLspAdapter::GITHUB_ASSET_KIND {
AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => path, // Tar and gzip extract in place.
AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
};
anyhow::Ok(Some(LanguageServerBinary {

View file

@ -1125,6 +1125,8 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
AssetKind::Zip
} else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
AssetKind::TarGz
} else if archive_url.ends_with(".tar.bz2") || archive_url.ends_with(".tbz2") {
AssetKind::TarBz2
} else {
anyhow::bail!("unsupported archive type in URL: {}", archive_url);
};
@ -1311,6 +1313,8 @@ impl ExternalAgentServer for LocalRegistryArchiveAgent {
AssetKind::Zip
} else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
AssetKind::TarGz
} else if archive_url.ends_with(".tar.bz2") || archive_url.ends_with(".tbz2") {
AssetKind::TarBz2
} else {
anyhow::bail!("unsupported archive type in URL: {}", archive_url);
};

View file

@ -26,6 +26,7 @@ accepted = [
"OpenSSL",
"Zlib",
"BSL-1.0",
"bzip2-1.0.6",
]
[procinfo.clarify]