mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-24 13:39:08 +00:00
## 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>
194 lines
5.6 KiB
Rust
194 lines
5.6 KiB
Rust
use crate::{HttpClient, HttpRequestExt};
|
|
use anyhow::{Context as _, Result, anyhow, bail};
|
|
use futures::AsyncReadExt;
|
|
use http::Request;
|
|
use serde::Deserialize;
|
|
use std::sync::Arc;
|
|
use url::Url;
|
|
|
|
const GITHUB_API_URL: &str = "https://api.github.com";
|
|
|
|
pub struct GitHubLspBinaryVersion {
|
|
pub name: String,
|
|
pub url: String,
|
|
pub digest: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct GithubRelease {
|
|
pub tag_name: String,
|
|
#[serde(rename = "prerelease")]
|
|
pub pre_release: bool,
|
|
pub assets: Vec<GithubReleaseAsset>,
|
|
pub tarball_url: String,
|
|
pub zipball_url: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct GithubReleaseAsset {
|
|
pub name: String,
|
|
pub browser_download_url: String,
|
|
pub digest: Option<String>,
|
|
}
|
|
|
|
pub async fn latest_github_release(
|
|
repo_name_with_owner: &str,
|
|
require_assets: bool,
|
|
pre_release: bool,
|
|
http: Arc<dyn HttpClient>,
|
|
) -> anyhow::Result<GithubRelease> {
|
|
let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases");
|
|
|
|
let request = Request::get(&url)
|
|
.follow_redirects(crate::RedirectPolicy::FollowAll)
|
|
.when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
|
|
builder.header("Authorization", format!("Bearer {}", token))
|
|
})
|
|
.body(Default::default())?;
|
|
|
|
let mut response = http
|
|
.send(request)
|
|
.await
|
|
.context("error fetching latest release")?;
|
|
|
|
let mut body = Vec::new();
|
|
response
|
|
.body_mut()
|
|
.read_to_end(&mut body)
|
|
.await
|
|
.context("error reading latest release")?;
|
|
|
|
if response.status().is_client_error() {
|
|
let text = String::from_utf8_lossy(body.as_slice());
|
|
bail!(
|
|
"status error {}, response: {text:?}",
|
|
response.status().as_u16()
|
|
);
|
|
}
|
|
|
|
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
|
|
Ok(releases) => releases,
|
|
|
|
Err(err) => {
|
|
log::error!("Error deserializing: {err:?}");
|
|
log::error!(
|
|
"GitHub API response text: {:?}",
|
|
String::from_utf8_lossy(body.as_slice())
|
|
);
|
|
anyhow::bail!("error deserializing latest release: {err:?}");
|
|
}
|
|
};
|
|
|
|
let mut release = releases
|
|
.into_iter()
|
|
.filter(|release| !require_assets || !release.assets.is_empty())
|
|
.find(|release| release.pre_release == pre_release)
|
|
.context("finding a prerelease")?;
|
|
release.assets.iter_mut().for_each(|asset| {
|
|
if let Some(digest) = &mut asset.digest
|
|
&& let Some(stripped) = digest.strip_prefix("sha256:")
|
|
{
|
|
*digest = stripped.to_owned();
|
|
}
|
|
});
|
|
Ok(release)
|
|
}
|
|
|
|
pub async fn get_release_by_tag_name(
|
|
repo_name_with_owner: &str,
|
|
tag: &str,
|
|
http: Arc<dyn HttpClient>,
|
|
) -> anyhow::Result<GithubRelease> {
|
|
let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases/tags/{tag}");
|
|
|
|
let request = Request::get(&url)
|
|
.follow_redirects(crate::RedirectPolicy::FollowAll)
|
|
.when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
|
|
builder.header("Authorization", format!("Bearer {}", token))
|
|
})
|
|
.body(Default::default())?;
|
|
|
|
let mut response = http
|
|
.send(request)
|
|
.await
|
|
.context("error fetching latest release")?;
|
|
|
|
let mut body = Vec::new();
|
|
let status = response.status();
|
|
response
|
|
.body_mut()
|
|
.read_to_end(&mut body)
|
|
.await
|
|
.context("error reading latest release")?;
|
|
|
|
if status.is_client_error() {
|
|
let text = String::from_utf8_lossy(body.as_slice());
|
|
bail!(
|
|
"status error {}, response: {text:?}",
|
|
response.status().as_u16()
|
|
);
|
|
}
|
|
|
|
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
|
|
log::error!("Error deserializing: {err:?}");
|
|
log::error!(
|
|
"GitHub API response text: {:?}",
|
|
String::from_utf8_lossy(body.as_slice())
|
|
);
|
|
anyhow!("error deserializing GitHub release: {err:?}")
|
|
})?;
|
|
|
|
Ok(release)
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum AssetKind {
|
|
TarGz,
|
|
TarBz2,
|
|
Gz,
|
|
Zip,
|
|
}
|
|
|
|
pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
|
|
let mut url = Url::parse(&format!(
|
|
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
|
|
))?;
|
|
// We're pushing this here, because tags may contain `/` and other characters
|
|
// that need to be escaped.
|
|
let asset_filename = format!(
|
|
"{tag}.{extension}",
|
|
extension = match kind {
|
|
AssetKind::TarGz => "tar.gz",
|
|
AssetKind::TarBz2 => "tar.bz2",
|
|
AssetKind::Gz => "gz",
|
|
AssetKind::Zip => "zip",
|
|
}
|
|
);
|
|
url.path_segments_mut()
|
|
.map_err(|()| anyhow!("cannot modify url path segments"))?
|
|
.push(&asset_filename);
|
|
Ok(url.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::github::{AssetKind, build_asset_url};
|
|
|
|
#[test]
|
|
fn test_build_asset_url() {
|
|
let tag = "release/2.3.5";
|
|
let repo_name_with_owner = "microsoft/vscode-eslint";
|
|
|
|
let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
|
|
assert_eq!(
|
|
tarball,
|
|
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
|
|
);
|
|
|
|
let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
|
|
assert_eq!(
|
|
zip,
|
|
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
|
|
);
|
|
}
|
|
}
|