1#![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#[derive(Debug, Clone)]
24pub struct ReleaseDep {
25 pub name: String,
27 pub version: Version,
29 pub binary: PathBuf,
31}
32
33#[derive(Debug, Clone)]
35pub struct Config<'a> {
36 pub package: &'a str,
38 pub version: &'a str,
40 pub repo: &'a [&'static str],
42 pub download_dir: Option<&'a str>,
44 pub timeout: Option<Duration>,
46}
47
48pub 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
161pub 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 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 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 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 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
221fn parse_url(repo_url: &str) -> Result<UrlInfo> {
223 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 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}