Skip to main content

sp1_cli/
lib.rs

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
11/// The latest version (github tag) of the toolchain that is supported by our build system.
12pub 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
20/// Send an HTTP request with retries and exponential backoff.
21///
22/// Retries on network errors and non-success HTTP status codes, up to `MAX_RETRIES` times
23/// with exponential backoff starting at `INITIAL_BACKOFF_SECS`.
24pub(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    // We don't want to operate on the musl toolchain, even if the CLI was compiled with musl
87    if target.environment == target_lexicon::Environment::Musl {
88        target.environment = target_lexicon::Environment::Gnu;
89    }
90
91    target.to_string()
92}
93
94/// Find the toolchain asset for the given target in the GitHub releases API and return
95/// its API download URL. Using the API URL (with `Accept: application/octet-stream`)
96/// instead of the browser download URL ensures authentication works correctly, including
97/// for draft releases.
98pub 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}