Skip to main content

release_dep/
lib.rs

1//! # Release Dependency Manager
2//!
3//! 这个库提供了从多个 Git 仓库(Gitee, GitHub)获取指定包的 release 版本的功能。
4//! 支持按照 semver 版本语义进行匹配,并自动下载匹配的二进制文件。
5#![cfg(not(target_os = "none"))]
6
7use std::{path::PathBuf, time::Duration};
8
9use anyhow::{bail, Result};
10use reqwest::Client;
11pub use semver::Version;
12use semver::VersionReq;
13use tokio::{select, time::sleep};
14
15mod gitee;
16mod github;
17
18use gitee::gitee_get_release;
19use github::github_get_release;
20use tokio_util::sync::CancellationToken;
21
22/// 表示一个 release 依赖项
23#[derive(Debug, Clone)]
24pub struct ReleaseDep {
25    /// 包名
26    pub name: String,
27    /// 版本号
28    pub version: Version,
29    /// 二进制文件路径
30    pub binary: PathBuf,
31}
32
33/// 配置结构体,用于指定要获取的包信息
34#[derive(Debug, Clone)]
35pub struct Config<'a> {
36    /// 包名
37    pub package: &'a str,
38    /// 期望的版本号(semver 格式)
39    pub version: &'a str,
40    /// 仓库 URL 列表,按优先级排序
41    pub repo: &'a [&'static str],
42    /// 下载文件夹地址,None 表示使用系统临时目录
43    pub download_dir: Option<&'a str>,
44    /// 超时时间(秒),None 表示使用默认的 30 秒
45    pub timeout: Option<Duration>,
46}
47
48/// 获取指定配置的 release 依赖项
49///
50/// 这个函数会并行地从多个仓库中搜索匹配的 release,
51/// 返回第一个找到的兼容版本。
52pub fn get_release(config: Config<'_>) -> Result<ReleaseDep> {
53    let runtime = tokio::runtime::Builder::new_multi_thread()
54        .enable_all()
55        .build()
56        .expect("Failed to create Tokio runtime");
57    runtime.block_on(_get_release(config))
58}
59
60pub struct OneConfig {
61    pub package: String,
62    pub version: VersionReq,
63    pub url_info: UrlInfo,
64    pub download_dir: Option<PathBuf>,
65}
66
67async fn _get_release(config: Config<'_>) -> Result<ReleaseDep> {
68    let package = config.package.to_string();
69    let version = VersionReq::parse(config.version).unwrap();
70    let download_dir = config.download_dir.map(PathBuf::from);
71
72    let (tx, mut rx) = tokio::sync::mpsc::channel(1);
73    let cancel = CancellationToken::new();
74
75    for &repo in config.repo {
76        let url_info = match parse_url(repo) {
77            Ok(v) => v,
78            Err(e) => bail!("url {repo} parse error: {e}"),
79        };
80        println!("Parsed URL: {url_info:?}");
81
82        let one_config = OneConfig {
83            package: package.clone(),
84            version: version.clone(),
85            url_info: url_info.clone(),
86            download_dir: download_dir.clone(),
87        };
88
89        match url_info.provider {
90            RepoProvider::Gitee => {
91                tokio::spawn({
92                    let tx = tx.clone();
93                    let cancel = cancel.clone();
94                    async move {
95                        let result = select! {
96                            _ = cancel.cancelled() => {
97                                println!("Gitee cancelled");
98                                return;
99                            }
100                            result = gitee_get_release(one_config) => result,
101                        };
102                        match result {
103                            Ok(v) => {
104                                let _ = tx.try_send(v);
105                            }
106                            Err(e) => {
107                                eprintln!("Gitee fetch task failed: {e}");
108                            }
109                        }
110                    }
111                });
112            }
113            RepoProvider::Github => {
114                tokio::spawn({
115                    let tx = tx.clone();
116                    let cancel = cancel.clone();
117                    async move {
118                        let result = select! {
119                            _ = cancel.cancelled() => {
120                                println!("Github cancelled");
121                                return;
122                            }
123                            result = github_get_release(one_config) => result,
124                        };
125                        match result {
126                            Ok(v) => {
127                                let _ = tx.try_send(v);
128                            }
129                            Err(e) => {
130                                eprintln!("Gitee fetch task failed: {e}");
131                            }
132                        }
133                    }
134                });
135            }
136        }
137    }
138
139    let release = if let Some(d) = config.timeout {
140        select! {
141            result = rx.recv() => {
142                match result {
143                    Some(release) => release,
144                    None => bail!("No release found"),
145                }
146            }
147            _ = sleep(d) => {
148                bail!("Operation timed out after {:?}", d);
149            }
150        }
151    } else {
152        match rx.recv().await {
153            Some(release) => release,
154            None => bail!("No release found"),
155        }
156    };
157
158    Ok(release)
159}
160
161/// 下载二进制文件的辅助函数
162pub async fn download_binary(
163    client: &Client,
164    url: &str,
165    filename: &str,
166    download_dir: Option<&str>,
167    provider: RepoProvider,
168) -> Result<PathBuf> {
169    println!("Downloading binary from: {url}");
170
171    let response = client.get(url).send().await?;
172
173    if !response.status().is_success() {
174        bail!("Failed to download binary: {}", response.status());
175    }
176
177    // 选择下载目录
178    let base_dir = if let Some(download_dir) = download_dir {
179        PathBuf::from(download_dir)
180    } else {
181        std::env::temp_dir()
182    };
183
184    // 根据 provider 创建子文件夹
185    let provider_dir = match provider {
186        RepoProvider::Gitee => "gitee",
187        RepoProvider::Github => "github",
188    };
189
190    let dir = base_dir.join(provider_dir);
191
192    // 确保目录存在
193    std::fs::create_dir_all(&dir)?;
194
195    let binary_path = dir.join(filename);
196
197    let bytes = response.bytes().await?;
198    std::fs::write(&binary_path, bytes)?;
199
200    // 返回绝对路径
201    let absolute_path = binary_path.canonicalize()?;
202
203    println!("Binary downloaded to: {absolute_path:?}");
204    Ok(absolute_path)
205}
206
207#[derive(Debug, Clone, Copy)]
208pub enum RepoProvider {
209    Gitee,
210    Github,
211}
212
213#[derive(Debug, Clone)]
214pub struct UrlInfo {
215    pub provider: RepoProvider,
216    pub repo_base: String,
217    pub owner: String,
218    pub repo: String,
219}
220
221/// 从仓库URL中提取owner和repo名称
222fn parse_url(repo_url: &str) -> Result<UrlInfo> {
223    // 移除协议前缀和可能的.git后缀
224    let url = repo_url
225        .strip_prefix("https://")
226        .or_else(|| repo_url.strip_prefix("http://"))
227        .unwrap_or(repo_url)
228        .strip_suffix(".git")
229        .unwrap_or(
230            repo_url
231                .strip_prefix("https://")
232                .or_else(|| repo_url.strip_prefix("http://"))
233                .unwrap_or(repo_url),
234        );
235
236    // 分割路径,格式应该是: domain.com/owner/repo
237    let parts: Vec<&str> = url.split('/').collect();
238    if parts.len() < 3 {
239        bail!("Invalid repository URL: {repo_url}");
240    }
241    let repo_base = parts[0].to_string();
242    let owner = parts[1].to_string();
243    let repo = parts[2].to_string();
244
245    let provider = match repo_base.as_str() {
246        "gitee.com" => RepoProvider::Gitee,
247        "github.com" => RepoProvider::Github,
248        _ => bail!("Unsupported repository provider: {repo_base}"),
249    };
250
251    Ok(UrlInfo {
252        provider,
253        repo_base,
254        owner,
255        repo,
256    })
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_parse_url() {
265        let gitee_url = "https://gitee.com/zr233/somehal";
266        let github_url = "https://github.com/rcore-os/somehal";
267
268        let gitee_info = parse_url(gitee_url).unwrap();
269        assert!(matches!(gitee_info.provider, RepoProvider::Gitee));
270        assert_eq!(gitee_info.owner, "zr233");
271        assert_eq!(gitee_info.repo, "somehal");
272
273        let github_info = parse_url(github_url).unwrap();
274        assert!(matches!(github_info.provider, RepoProvider::Github));
275        assert_eq!(github_info.owner, "rcore-os");
276        assert_eq!(github_info.repo, "somehal");
277    }
278}