1pub mod commands;
2
3use std::time::Duration;
4
5use anyhow::{Context, Result};
6use reqwest::Client;
7use tokio::time::sleep;
8
9pub const RUSTUP_TOOLCHAIN_NAME: &str = "succinct";
10
11pub const LATEST_SUPPORTED_TOOLCHAIN_VERSION_TAG: &str = "succinct-1.93.0-64bit";
13
14pub const SP1_VERSION_MESSAGE: &str =
15 concat!("sp1", " (", env!("VERGEN_GIT_SHA"), " ", env!("VERGEN_BUILD_TIMESTAMP"), ")");
16
17const MAX_RETRIES: u32 = 3;
18const INITIAL_BACKOFF_SECS: u64 = 2;
19
20pub(crate) async fn send_with_retry(
25 client: &Client,
26 method: reqwest::Method,
27 url: &str,
28 headers: Option<reqwest::header::HeaderMap>,
29 operation: &str,
30) -> Result<reqwest::Response> {
31 let mut last_err = None;
32 for attempt in 0..=MAX_RETRIES {
33 if attempt > 0 {
34 let backoff = Duration::from_secs(INITIAL_BACKOFF_SECS << (attempt - 1));
35 eprintln!(
36 "{operation} failed, retrying in {}s (attempt {}/{})...",
37 backoff.as_secs(),
38 attempt + 1,
39 MAX_RETRIES + 1
40 );
41 sleep(backoff).await;
42 }
43 let mut request = client.request(method.clone(), url);
44 if let Some(ref headers) = headers {
45 request = request.headers(headers.clone());
46 }
47 match request.send().await {
48 Ok(res) if res.status().is_success() => return Ok(res),
49 Ok(res) => {
50 let status = res.status();
51 let body = res.text().await.unwrap_or_default();
52 last_err = Some(format!("HTTP {status}: {body}"));
53 }
54 Err(e) => {
55 last_err = Some(e.to_string());
56 }
57 }
58 }
59 anyhow::bail!(
60 "{operation} failed after {} attempts: {}",
61 MAX_RETRIES + 1,
62 last_err.unwrap_or_default()
63 )
64}
65
66#[allow(unreachable_code)]
67pub fn is_supported_target() -> bool {
68 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
69 return true;
70
71 #[cfg(all(target_arch = "aarch64", target_os = "linux"))]
72 return true;
73
74 #[cfg(all(target_arch = "x86_64", target_os = "macos"))]
75 return true;
76
77 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
78 return true;
79
80 false
81}
82
83pub fn get_target() -> String {
84 let mut target: target_lexicon::Triple = target_lexicon::HOST;
85
86 if target.environment == target_lexicon::Environment::Musl {
88 target.environment = target_lexicon::Environment::Gnu;
89 }
90
91 target.to_string()
92}
93
94pub async fn get_toolchain_asset_url(client: &Client, target: String) -> Result<String> {
99 let response = send_with_retry(
100 client,
101 reqwest::Method::GET,
102 "https://api.github.com/repos/succinctlabs/rust/releases",
103 None,
104 "Fetching GitHub releases",
105 )
106 .await?;
107
108 let all_releases: serde_json::Value =
109 response.json().await.context("Failed to parse releases response")?;
110
111 let releases = all_releases.as_array().context("GitHub API response was not a JSON array")?;
112
113 let release = releases
114 .iter()
115 .find(|release| {
116 release["tag_name"].as_str() == Some(LATEST_SUPPORTED_TOOLCHAIN_VERSION_TAG)
117 })
118 .with_context(|| {
119 format!("No release found for tag: {LATEST_SUPPORTED_TOOLCHAIN_VERSION_TAG}")
120 })?;
121
122 let expected_name = format!("rust-toolchain-{target}.tar.gz");
123 let assets = release["assets"].as_array().context("Release has no assets array")?;
124 let asset = assets
125 .iter()
126 .find(|a| a["name"].as_str() == Some(&expected_name))
127 .with_context(|| {
128 format!("No asset '{expected_name}' found in release {LATEST_SUPPORTED_TOOLCHAIN_VERSION_TAG}")
129 })?;
130
131 asset["url"].as_str().map(String::from).context("Asset has no API URL")
132}