svm/
releases.rs

1use crate::{error::SvmError, platform::Platform};
2use reqwest::get;
3use semver::Version;
4use serde::{Deserialize, Serialize};
5use std::{collections::BTreeMap, sync::LazyLock};
6use url::Url;
7
8// Updating new releases:
9// 1. Update `https://github.com/nikitastupin/solc` commit for `linux/aarch64`
10// 2. Update LATEST for tests
11
12/// Base URL for all Solc releases
13/// `"SOLC_RELEASES_URL}/{platform}/list.json"`:
14/// `https://binaries.soliditylang.org/linux-amd64/list.json`
15/// `https://binaries.soliditylang.org/windows-amd64/list.json`
16/// `https://binaries.soliditylang.org/macosx-amd64/list.json`
17const SOLC_RELEASES_URL: &str = "https://binaries.soliditylang.org";
18
19const OLD_SOLC_RELEASES_DOWNLOAD_PREFIX: &str =
20    "https://raw.githubusercontent.com/crytic/solc/master/linux/amd64";
21
22const OLD_VERSION_MAX: Version = Version::new(0, 4, 9);
23
24const OLD_VERSION_MIN: Version = Version::new(0, 4, 0);
25
26static OLD_SOLC_RELEASES: LazyLock<Releases> = LazyLock::new(|| {
27    serde_json::from_str(include_str!("../list/linux-arm64-old.json"))
28        .expect("could not parse list linux-arm64-old.json")
29});
30
31const LINUX_AARCH64_MIN: Version = Version::new(0, 5, 0);
32
33static LINUX_AARCH64_URL_PREFIX: &str =
34    "https://raw.githubusercontent.com/nikitastupin/solc/99b5867237b37952d372e0dab400d6788feda315/linux/aarch64";
35
36static LINUX_AARCH64_RELEASES_URL: &str =
37    "https://raw.githubusercontent.com/nikitastupin/solc/99b5867237b37952d372e0dab400d6788feda315/linux/aarch64/list.json";
38
39// NOTE: Since version 0.8.24, universal macosx releases are available: https://binaries.soliditylang.org/macosx-amd64/list.json
40const MACOS_AARCH64_NATIVE: Version = Version::new(0, 8, 5);
41
42const UNIVERSAL_MACOS_BINARIES: Version = Version::new(0, 8, 24);
43
44static MACOS_AARCH64_URL_PREFIX: &str =
45    "https://raw.githubusercontent.com/alloy-rs/solc-builds/e4b80d33bc4d015b2fc3583e217fbf248b2014e1/macosx/aarch64";
46
47static MACOS_AARCH64_RELEASES_URL: &str =
48    "https://raw.githubusercontent.com/alloy-rs/solc-builds/e4b80d33bc4d015b2fc3583e217fbf248b2014e1/macosx/aarch64/list.json";
49
50const ANDROID_AARCH64_MIN: Version = Version::new(0, 8, 24);
51
52static ANDROID_AARCH64_URL_PREFIX: &str =
53    "https://raw.githubusercontent.com/alloy-rs/solc-builds/ac6f303a04b38e7ec507ced511fd3ed7a605179f/android/aarch64";
54
55static ANDROID_AARCH64_RELEASES_URL: &str =
56    "https://raw.githubusercontent.com/alloy-rs/solc-builds/ac6f303a04b38e7ec507ced511fd3ed7a605179f/android/aarch64/list.json";
57
58/// Defines the struct that the JSON-formatted release list can be deserialized into.
59///
60/// Both the key and value are deserialized into [`semver::Version`].
61///
62/// ```json
63/// {
64///     "builds": [
65///         {
66///             "version": "0.8.7",
67///             "sha256": "0x0xcc5c663d1fe17d4eb4aca09253787ac86b8785235fca71d9200569e662677990"
68///         }
69///     ]
70///     "releases": {
71///         "0.8.7": "solc-macosx-amd64-v0.8.7+commit.e28d00a7",
72///         "0.8.6": "solc-macosx-amd64-v0.8.6+commit.11564f7e",
73///         ...
74///     }
75/// }
76/// ```
77#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
78pub struct Releases {
79    pub builds: Vec<BuildInfo>,
80    pub releases: BTreeMap<Version, String>,
81}
82
83impl Releases {
84    /// Get the checksum of a solc version's binary if it exists.
85    pub fn get_checksum(&self, v: &Version) -> Option<Vec<u8>> {
86        for build in self.builds.iter() {
87            if build.version.eq(v) {
88                return Some(build.sha256.clone());
89            }
90        }
91        None
92    }
93
94    /// Returns the artifact of the version if any
95    pub fn get_artifact(&self, version: &Version) -> Option<&String> {
96        self.releases.get(version)
97    }
98
99    /// Returns a sorted list of all versions
100    pub fn into_versions(self) -> Vec<Version> {
101        let mut versions = self.releases.into_keys().collect::<Vec<_>>();
102        versions.sort_unstable();
103        versions
104    }
105}
106
107/// Build info contains the SHA256 checksum of a solc binary.
108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
109pub struct BuildInfo {
110    pub version: Version,
111    #[serde(with = "hex_string")]
112    pub sha256: Vec<u8>,
113}
114
115/// Helper serde module to serialize and deserialize bytes as hex.
116mod hex_string {
117    use super::*;
118    use serde::{de, Deserializer, Serializer};
119
120    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
121    where
122        D: Deserializer<'de>,
123    {
124        hex::decode(String::deserialize(deserializer)?).map_err(de::Error::custom)
125    }
126
127    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
128    where
129        S: Serializer,
130        T: AsRef<[u8]>,
131    {
132        serializer.serialize_str(&hex::encode_prefixed(value))
133    }
134}
135
136/// Blocking version of [`all_releases`].
137#[cfg(feature = "blocking")]
138pub fn blocking_all_releases(platform: Platform) -> Result<Releases, SvmError> {
139    match platform {
140        Platform::LinuxAarch64 => {
141            Ok(reqwest::blocking::get(LINUX_AARCH64_RELEASES_URL)?.json::<Releases>()?)
142        }
143        Platform::MacOsAarch64 => {
144            // The supported versions for both macos-amd64 and macos-aarch64 are the same.
145            //
146            // 1. For version >= 0.8.5 we fetch native releases from
147            // https://github.com/alloy-rs/solc-builds
148            //
149            // 2. For version <= 0.8.4 we fetch releases from https://binaries.soliditylang.org and
150            // require Rosetta support.
151            //
152            // Note: Since 0.8.24 universal macosx releases are available
153            let mut native =
154                reqwest::blocking::get(MACOS_AARCH64_RELEASES_URL)?.json::<Releases>()?;
155            let mut releases = reqwest::blocking::get(format!(
156                "{}/{}/list.json",
157                SOLC_RELEASES_URL,
158                Platform::MacOsAmd64,
159            ))?
160            .json::<Releases>()?;
161            releases.builds.retain(|b| {
162                b.version < MACOS_AARCH64_NATIVE || b.version > UNIVERSAL_MACOS_BINARIES
163            });
164            releases
165                .releases
166                .retain(|v, _| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
167            releases.builds.extend_from_slice(&native.builds);
168
169            releases.releases.append(&mut native.releases);
170            Ok(releases)
171        }
172        Platform::AndroidAarch64 => {
173            Ok(reqwest::blocking::get(ANDROID_AARCH64_RELEASES_URL)?.json::<Releases>()?)
174        }
175        _ => {
176            let releases =
177                reqwest::blocking::get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))?
178                    .json::<Releases>()?;
179            Ok(unified_releases(releases, platform))
180        }
181    }
182}
183
184/// Fetch all releases available for the provided platform.
185pub async fn all_releases(platform: Platform) -> Result<Releases, SvmError> {
186    match platform {
187        Platform::LinuxAarch64 => Ok(get(LINUX_AARCH64_RELEASES_URL)
188            .await?
189            .json::<Releases>()
190            .await?),
191        Platform::MacOsAarch64 => {
192            // The supported versions for both macos-amd64 and macos-aarch64 are the same.
193            //
194            // 1. For version >= 0.8.5 we fetch native releases from
195            // https://github.com/alloy-rs/solc-builds
196            //
197            // 2. For version <= 0.8.4 we fetch releases from https://binaries.soliditylang.org and
198            // require Rosetta support.
199            let mut native = get(MACOS_AARCH64_RELEASES_URL)
200                .await?
201                .json::<Releases>()
202                .await?;
203            let mut releases = get(format!(
204                "{}/{}/list.json",
205                SOLC_RELEASES_URL,
206                Platform::MacOsAmd64,
207            ))
208            .await?
209            .json::<Releases>()
210            .await?;
211            releases.builds.retain(|b| {
212                b.version < MACOS_AARCH64_NATIVE || b.version > UNIVERSAL_MACOS_BINARIES
213            });
214            releases
215                .releases
216                .retain(|v, _| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
217
218            releases.builds.extend_from_slice(&native.builds);
219            releases.releases.append(&mut native.releases);
220            Ok(releases)
221        }
222        Platform::AndroidAarch64 => Ok(get(ANDROID_AARCH64_RELEASES_URL)
223            .await?
224            .json::<Releases>()
225            .await?),
226        _ => {
227            let releases = get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))
228                .await?
229                .json::<Releases>()
230                .await?;
231
232            Ok(unified_releases(releases, platform))
233        }
234    }
235}
236
237/// unifies the releases with old releases if on linux
238fn unified_releases(releases: Releases, platform: Platform) -> Releases {
239    if platform == Platform::LinuxAmd64 {
240        let mut all_releases = OLD_SOLC_RELEASES.clone();
241        all_releases.builds.extend(releases.builds);
242        all_releases.releases.extend(releases.releases);
243        all_releases
244    } else {
245        releases
246    }
247}
248
249/// Construct the URL to the Solc binary for the specified release version and target platform.
250pub(crate) fn artifact_url(
251    platform: Platform,
252    version: &Version,
253    artifact: &str,
254) -> Result<Url, SvmError> {
255    if platform == Platform::LinuxAmd64
256        && *version <= OLD_VERSION_MAX
257        && *version >= OLD_VERSION_MIN
258    {
259        return Ok(Url::parse(&format!(
260            "{OLD_SOLC_RELEASES_DOWNLOAD_PREFIX}/{artifact}"
261        ))?);
262    }
263
264    if platform == Platform::LinuxAarch64 {
265        if *version >= LINUX_AARCH64_MIN {
266            return Ok(Url::parse(&format!(
267                "{LINUX_AARCH64_URL_PREFIX}/{artifact}"
268            ))?);
269        } else {
270            return Err(SvmError::UnsupportedVersion(
271                version.to_string(),
272                platform.to_string(),
273            ));
274        }
275    }
276
277    if platform == Platform::MacOsAmd64 && *version < OLD_VERSION_MIN {
278        return Err(SvmError::UnsupportedVersion(
279            version.to_string(),
280            platform.to_string(),
281        ));
282    }
283
284    if platform == Platform::MacOsAarch64 {
285        if *version >= MACOS_AARCH64_NATIVE && *version <= UNIVERSAL_MACOS_BINARIES {
286            // fetch natively build solc binaries from `https://github.com/alloy-rs/solc-builds`
287            return Ok(Url::parse(&format!(
288                "{MACOS_AARCH64_URL_PREFIX}/{artifact}"
289            ))?);
290        } else {
291            // if version is older or universal macos binaries are available, fetch from `https://binaries.soliditylang.org`
292            return Ok(Url::parse(&format!(
293                "{}/{}/{}",
294                SOLC_RELEASES_URL,
295                Platform::MacOsAmd64,
296                artifact,
297            ))?);
298        }
299    }
300
301    if platform == Platform::AndroidAarch64 {
302        if version.ge(&ANDROID_AARCH64_MIN) {
303            return Ok(Url::parse(&format!(
304                "{ANDROID_AARCH64_URL_PREFIX}/{artifact}"
305            ))?);
306        } else {
307            return Err(SvmError::UnsupportedVersion(
308                version.to_string(),
309                platform.to_string(),
310            ));
311        }
312    }
313
314    Ok(Url::parse(&format!(
315        "{SOLC_RELEASES_URL}/{platform}/{artifact}"
316    ))?)
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_artifact_url() {
325        let version = Version::new(0, 5, 0);
326        let artifact = "solc-v0.5.0";
327        assert_eq!(
328            artifact_url(Platform::LinuxAarch64, &version, artifact).unwrap(),
329            Url::parse(&format!(
330                "https://raw.githubusercontent.com/nikitastupin/solc/99b5867237b37952d372e0dab400d6788feda315/linux/aarch64/{artifact}"
331            ))
332            .unwrap(),
333        )
334    }
335
336    #[test]
337    fn test_old_releases_deser() {
338        assert_eq!(OLD_SOLC_RELEASES.releases.len(), 10);
339        assert_eq!(OLD_SOLC_RELEASES.builds.len(), 10);
340    }
341
342    #[tokio::test]
343    async fn test_macos_aarch64() {
344        let releases = all_releases(Platform::MacOsAarch64)
345            .await
346            .expect("could not fetch releases for macos-aarch64");
347        let rosetta = Version::new(0, 8, 4);
348        let native = MACOS_AARCH64_NATIVE;
349        let url1 = artifact_url(
350            Platform::MacOsAarch64,
351            &rosetta,
352            releases.get_artifact(&rosetta).unwrap(),
353        )
354        .expect("could not fetch artifact URL");
355        let url2 = artifact_url(
356            Platform::MacOsAarch64,
357            &native,
358            releases.get_artifact(&native).unwrap(),
359        )
360        .expect("could not fetch artifact URL");
361        assert!(url1.to_string().contains(SOLC_RELEASES_URL));
362        assert!(url2.to_string().contains(MACOS_AARCH64_URL_PREFIX));
363    }
364
365    #[tokio::test]
366    async fn test_all_releases_macos_amd64() {
367        assert!(all_releases(Platform::MacOsAmd64).await.is_ok());
368    }
369
370    #[tokio::test]
371    async fn test_all_releases_macos_aarch64() {
372        assert!(all_releases(Platform::MacOsAarch64).await.is_ok());
373    }
374
375    #[tokio::test]
376    async fn test_all_releases_linux_amd64() {
377        assert!(all_releases(Platform::LinuxAmd64).await.is_ok());
378    }
379
380    #[tokio::test]
381    async fn test_all_releases_linux_aarch64() {
382        assert!(all_releases(Platform::LinuxAarch64).await.is_ok());
383    }
384
385    #[tokio::test]
386    async fn releases_roundtrip() {
387        let releases = all_releases(Platform::LinuxAmd64).await.unwrap();
388        let s = serde_json::to_string(&releases).unwrap();
389        let de_releases: Releases = serde_json::from_str(&s).unwrap();
390        assert_eq!(releases, de_releases);
391    }
392}