#![cfg(not(target_os = "none"))]
use std::{path::PathBuf, time::Duration};
use anyhow::{bail, Result};
use reqwest::Client;
pub use semver::Version;
use semver::VersionReq;
use tokio::{select, time::sleep};
mod gitee;
mod github;
use gitee::gitee_get_release;
use github::github_get_release;
use tokio_util::sync::CancellationToken;
#[derive(Debug, Clone)]
pub struct ReleaseDep {
pub name: String,
pub version: Version,
pub binary: PathBuf,
}
#[derive(Debug, Clone)]
pub struct Config<'a> {
pub package: &'a str,
pub version: &'a str,
pub repo: &'a [&'static str],
pub download_dir: Option<&'a str>,
pub timeout: Option<Duration>,
}
pub fn get_release(config: Config<'_>) -> Result<ReleaseDep> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
runtime.block_on(_get_release(config))
}
pub struct OneConfig {
pub package: String,
pub version: VersionReq,
pub url_info: UrlInfo,
pub download_dir: Option<PathBuf>,
}
async fn _get_release(config: Config<'_>) -> Result<ReleaseDep> {
let package = config.package.to_string();
let version = VersionReq::parse(config.version).unwrap();
let download_dir = config.download_dir.map(PathBuf::from);
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let cancel = CancellationToken::new();
for &repo in config.repo {
let url_info = match parse_url(repo) {
Ok(v) => v,
Err(e) => bail!("url {repo} parse error: {e}"),
};
println!("Parsed URL: {url_info:?}");
let one_config = OneConfig {
package: package.clone(),
version: version.clone(),
url_info: url_info.clone(),
download_dir: download_dir.clone(),
};
match url_info.provider {
RepoProvider::Gitee => {
tokio::spawn({
let tx = tx.clone();
let cancel = cancel.clone();
async move {
let result = select! {
_ = cancel.cancelled() => {
println!("Gitee cancelled");
return;
}
result = gitee_get_release(one_config) => result,
};
match result {
Ok(v) => {
let _ = tx.try_send(v);
}
Err(e) => {
eprintln!("Gitee fetch task failed: {e}");
}
}
}
});
}
RepoProvider::Github => {
tokio::spawn({
let tx = tx.clone();
let cancel = cancel.clone();
async move {
let result = select! {
_ = cancel.cancelled() => {
println!("Github cancelled");
return;
}
result = github_get_release(one_config) => result,
};
match result {
Ok(v) => {
let _ = tx.try_send(v);
}
Err(e) => {
eprintln!("Gitee fetch task failed: {e}");
}
}
}
});
}
}
}
let release = if let Some(d) = config.timeout {
select! {
result = rx.recv() => {
match result {
Some(release) => release,
None => bail!("No release found"),
}
}
_ = sleep(d) => {
bail!("Operation timed out after {:?}", d);
}
}
} else {
match rx.recv().await {
Some(release) => release,
None => bail!("No release found"),
}
};
Ok(release)
}
pub async fn download_binary(
client: &Client,
url: &str,
filename: &str,
download_dir: Option<&str>,
provider: RepoProvider,
) -> Result<PathBuf> {
println!("Downloading binary from: {url}");
let response = client.get(url).send().await?;
if !response.status().is_success() {
bail!("Failed to download binary: {}", response.status());
}
let base_dir = if let Some(download_dir) = download_dir {
PathBuf::from(download_dir)
} else {
std::env::temp_dir()
};
let provider_dir = match provider {
RepoProvider::Gitee => "gitee",
RepoProvider::Github => "github",
};
let dir = base_dir.join(provider_dir);
std::fs::create_dir_all(&dir)?;
let binary_path = dir.join(filename);
let bytes = response.bytes().await?;
std::fs::write(&binary_path, bytes)?;
let absolute_path = binary_path.canonicalize()?;
println!("Binary downloaded to: {absolute_path:?}");
Ok(absolute_path)
}
#[derive(Debug, Clone, Copy)]
pub enum RepoProvider {
Gitee,
Github,
}
#[derive(Debug, Clone)]
pub struct UrlInfo {
pub provider: RepoProvider,
pub repo_base: String,
pub owner: String,
pub repo: String,
}
fn parse_url(repo_url: &str) -> Result<UrlInfo> {
let url = repo_url
.strip_prefix("https://")
.or_else(|| repo_url.strip_prefix("http://"))
.unwrap_or(repo_url)
.strip_suffix(".git")
.unwrap_or(
repo_url
.strip_prefix("https://")
.or_else(|| repo_url.strip_prefix("http://"))
.unwrap_or(repo_url),
);
let parts: Vec<&str> = url.split('/').collect();
if parts.len() < 3 {
bail!("Invalid repository URL: {repo_url}");
}
let repo_base = parts[0].to_string();
let owner = parts[1].to_string();
let repo = parts[2].to_string();
let provider = match repo_base.as_str() {
"gitee.com" => RepoProvider::Gitee,
"github.com" => RepoProvider::Github,
_ => bail!("Unsupported repository provider: {repo_base}"),
};
Ok(UrlInfo {
provider,
repo_base,
owner,
repo,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_url() {
let gitee_url = "https://gitee.com/zr233/somehal";
let github_url = "https://github.com/rcore-os/somehal";
let gitee_info = parse_url(gitee_url).unwrap();
assert!(matches!(gitee_info.provider, RepoProvider::Gitee));
assert_eq!(gitee_info.owner, "zr233");
assert_eq!(gitee_info.repo, "somehal");
let github_info = parse_url(github_url).unwrap();
assert!(matches!(github_info.provider, RepoProvider::Github));
assert_eq!(github_info.owner, "rcore-os");
assert_eq!(github_info.repo, "somehal");
}
}