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