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
33// NOTE: Since version 0.8.31, Linux aarch64 releases are available: https://binaries.soliditylang.org/linux-arm64/list.json
34const LINUX_AARCH64_BINARIES: Version = Version::new(0, 8, 31);
35
36// NOTE: The official Solidity releases uses the platform name "linux-arm64"
37// instead of the "linux-aarch64" naming which is used by SVM.
38static LINUX_AARCH64_PLATFORM: &str = "linux-arm64";
39
40static LINUX_AARCH64_URL_PREFIX: &str = "https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64";
41
42static LINUX_AARCH64_RELEASES_URL: &str = "https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64/list.json";
43
44// NOTE: Since version 0.8.24, universal macosx releases are available: https://binaries.soliditylang.org/macosx-amd64/list.json
45const MACOS_AARCH64_NATIVE: Version = Version::new(0, 8, 5);
46
47const UNIVERSAL_MACOS_BINARIES: Version = Version::new(0, 8, 24);
48
49static MACOS_AARCH64_URL_PREFIX: &str = "https://raw.githubusercontent.com/alloy-rs/solc-builds/e4b80d33bc4d015b2fc3583e217fbf248b2014e1/macosx/aarch64";
50
51static MACOS_AARCH64_RELEASES_URL: &str = "https://raw.githubusercontent.com/alloy-rs/solc-builds/e4b80d33bc4d015b2fc3583e217fbf248b2014e1/macosx/aarch64/list.json";
52
53const ANDROID_AARCH64_MIN: Version = Version::new(0, 8, 24);
54
55static ANDROID_AARCH64_URL_PREFIX: &str = "https://raw.githubusercontent.com/alloy-rs/solc-builds/ac6f303a04b38e7ec507ced511fd3ed7a605179f/android/aarch64";
56
57static ANDROID_AARCH64_RELEASES_URL: &str = "https://raw.githubusercontent.com/alloy-rs/solc-builds/ac6f303a04b38e7ec507ced511fd3ed7a605179f/android/aarch64/list.json";
58
59/// Defines the struct that the JSON-formatted release list can be deserialized into.
60///
61/// Both the key and value are deserialized into [`semver::Version`].
62///
63/// ```json
64/// {
65///     "builds": [
66///         {
67///             "version": "0.8.7",
68///             "sha256": "0x0xcc5c663d1fe17d4eb4aca09253787ac86b8785235fca71d9200569e662677990"
69///         }
70///     ]
71///     "releases": {
72///         "0.8.7": "solc-macosx-amd64-v0.8.7+commit.e28d00a7",
73///         "0.8.6": "solc-macosx-amd64-v0.8.6+commit.11564f7e",
74///         ...
75///     }
76/// }
77/// ```
78#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
79pub struct Releases {
80    pub builds: Vec<BuildInfo>,
81    pub releases: BTreeMap<Version, String>,
82}
83
84impl Releases {
85    /// Get the checksum of a solc version's binary if it exists.
86    /// Checks for exact version match or for prerelease.
87    pub fn get_checksum(&self, v: &Version) -> Option<Vec<u8>> {
88        let matches = |build_info: &BuildInfo| {
89            build_info.version == Version::new(v.major, v.minor, v.patch)
90                && build_info
91                    .prerelease
92                    .as_deref()
93                    .unwrap_or("")
94                    .eq_ignore_ascii_case(v.pre.as_str())
95        };
96
97        self.builds
98            .iter()
99            .find(|build_info| matches(build_info))
100            .map(|build_info| build_info.sha256.clone())
101    }
102
103    /// Returns the artifact of the version if any, by looking it up in releases or in builds (if
104    /// a prerelease).
105    pub fn get_artifact(&self, version: &Version) -> Option<&String> {
106        // Check version artifact in releases.
107        if let Some(artifact) = self.releases.get(version) {
108            return Some(artifact);
109        }
110
111        // If we didn't find any artifact under releases, look up builds for prerelease.
112        if !version.pre.is_empty()
113            && let Some(build_info) = self.builds.iter().find(|b| {
114                b.version == Version::new(version.major, version.minor, version.patch)
115                    && b.prerelease == Some(version.pre.to_string())
116            })
117        {
118            return build_info.path.as_ref();
119        }
120
121        None
122    }
123
124    /// Returns a sorted list of all versions
125    pub fn into_versions(self) -> Vec<Version> {
126        let mut versions = self.releases.into_keys().collect::<Vec<_>>();
127        versions.sort_unstable();
128        versions
129    }
130
131    /// Retain versions matching a predicate.
132    fn retain_versions(&mut self, mut pred: impl FnMut(&Version) -> bool) {
133        self.builds.retain(|build| pred(&build.version));
134        self.releases.retain(|version, _| pred(version));
135    }
136
137    /// Extends releases with another.
138    fn extend(&mut self, other: Self) {
139        self.builds.extend(other.builds);
140        self.releases.extend(other.releases);
141    }
142}
143
144/// Build info contains the SHA256 checksum of a solc binary.
145#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
146pub struct BuildInfo {
147    pub version: Version,
148    #[serde(with = "hex_string")]
149    pub sha256: Vec<u8>,
150    pub path: Option<String>,
151    pub prerelease: Option<String>,
152}
153
154/// Helper serde module to serialize and deserialize bytes as hex.
155mod hex_string {
156    use super::*;
157    use serde::{Deserializer, Serializer, de};
158
159    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
160    where
161        D: Deserializer<'de>,
162    {
163        hex::decode(String::deserialize(deserializer)?).map_err(de::Error::custom)
164    }
165
166    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
167    where
168        S: Serializer,
169        T: AsRef<[u8]>,
170    {
171        serializer.serialize_str(&hex::encode_prefixed(value))
172    }
173}
174
175/// Blocking version of [`all_releases`].
176#[cfg(feature = "blocking")]
177pub fn blocking_all_releases(platform: Platform) -> Result<Releases, SvmError> {
178    match platform {
179        Platform::LinuxAarch64 => {
180            // Prior to version 0.8.31, releases for Linux arm64 builds was provided by the
181            // `nikitastupin/solc` repository. From 0.8.31 (inclusive) and onwards, official
182            // binary releases are provided by the Solidity project.
183            let mut releases =
184                reqwest::blocking::get(LINUX_AARCH64_RELEASES_URL)?.json::<Releases>()?;
185            releases.retain_versions(|v| *v < LINUX_AARCH64_BINARIES);
186            fix_build_prerelease(&mut releases);
187            let mut official = reqwest::blocking::get(format!(
188                "{SOLC_RELEASES_URL}/{LINUX_AARCH64_PLATFORM}/list.json",
189            ))?
190            .json::<Releases>()?;
191            // Filtering older releases isn't strinctly necessary here, but do it just in case
192            // Solidity retroactively starts adding older release binaries for linux-aarch64.
193            official.retain_versions(|v| *v >= LINUX_AARCH64_BINARIES);
194            releases.extend(official);
195            Ok(releases)
196        }
197        Platform::MacOsAarch64 => {
198            // The supported versions for both macos-amd64 and macos-aarch64 are the same.
199            //
200            // 1. For version >= 0.8.5 we fetch native releases from
201            // https://github.com/alloy-rs/solc-builds
202            //
203            // 2. For version <= 0.8.4 we fetch releases from https://binaries.soliditylang.org and
204            // require Rosetta support.
205            //
206            // Note: Since 0.8.24 universal macosx releases are available
207            let native = reqwest::blocking::get(MACOS_AARCH64_RELEASES_URL)?.json::<Releases>()?;
208            let mut releases = reqwest::blocking::get(format!(
209                "{}/{}/list.json",
210                SOLC_RELEASES_URL,
211                Platform::MacOsAmd64,
212            ))?
213            .json::<Releases>()?;
214            releases
215                .retain_versions(|v| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
216            releases.extend(native);
217            Ok(releases)
218        }
219        Platform::AndroidAarch64 => {
220            Ok(reqwest::blocking::get(ANDROID_AARCH64_RELEASES_URL)?.json::<Releases>()?)
221        }
222        Platform::WindowsAarch64 => {
223            // Windows ARM64 uses x64 binaries via emulation
224            // Solidity does not provide native ARM64 Windows binaries
225            let releases = reqwest::blocking::get(format!(
226                "{SOLC_RELEASES_URL}/{}/list.json",
227                Platform::WindowsAmd64
228            ))?
229            .json::<Releases>()?;
230            Ok(unified_releases(releases, platform))
231        }
232        _ => {
233            let releases =
234                reqwest::blocking::get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))?
235                    .json::<Releases>()?;
236            Ok(unified_releases(releases, platform))
237        }
238    }
239}
240
241/// Fetch all releases available for the provided platform.
242pub async fn all_releases(platform: Platform) -> Result<Releases, SvmError> {
243    match platform {
244        Platform::LinuxAarch64 => {
245            // Prior to version 0.8.31, releases for Linux arm64 builds was provided by the
246            // `nikitastupin/solc` repository. From 0.8.31 (inclusive) and onwards, official
247            // binary releases are provided by the Solidity project.
248            let mut releases = get(LINUX_AARCH64_RELEASES_URL)
249                .await?
250                .json::<Releases>()
251                .await?;
252            releases.retain_versions(|v| *v < LINUX_AARCH64_BINARIES);
253            fix_build_prerelease(&mut releases);
254            let mut official = get(format!(
255                "{SOLC_RELEASES_URL}/{LINUX_AARCH64_PLATFORM}/list.json",
256            ))
257            .await?
258            .json::<Releases>()
259            .await?;
260            // Filtering older releases isn't strinctly necessary here, but do it just in case
261            // Solidity retroactively starts adding older release binaries for linux-aarch64.
262            official.retain_versions(|v| *v >= LINUX_AARCH64_BINARIES);
263            releases.extend(official);
264            Ok(releases)
265        }
266        Platform::MacOsAarch64 => {
267            // The supported versions for both macos-amd64 and macos-aarch64 are the same.
268            //
269            // 1. For version >= 0.8.5 we fetch native releases from
270            // https://github.com/alloy-rs/solc-builds
271            //
272            // 2. For version <= 0.8.4 we fetch releases from https://binaries.soliditylang.org and
273            // require Rosetta support.
274            let native = get(MACOS_AARCH64_RELEASES_URL)
275                .await?
276                .json::<Releases>()
277                .await?;
278            let mut releases = get(format!(
279                "{}/{}/list.json",
280                SOLC_RELEASES_URL,
281                Platform::MacOsAmd64,
282            ))
283            .await?
284            .json::<Releases>()
285            .await?;
286            releases
287                .retain_versions(|v| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
288
289            releases.extend(native);
290            Ok(releases)
291        }
292        Platform::AndroidAarch64 => Ok(get(ANDROID_AARCH64_RELEASES_URL)
293            .await?
294            .json::<Releases>()
295            .await?),
296        Platform::WindowsAarch64 => {
297            // Windows ARM64 uses x64 binaries via emulation
298            // Solidity does not provide native ARM64 Windows binaries
299            let releases = get(format!(
300                "{SOLC_RELEASES_URL}/{}/list.json",
301                Platform::WindowsAmd64
302            ))
303            .await?
304            .json::<Releases>()
305            .await?;
306
307            Ok(unified_releases(releases, platform))
308        }
309        _ => {
310            let releases = get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))
311                .await?
312                .json::<Releases>()
313                .await?;
314
315            Ok(unified_releases(releases, platform))
316        }
317    }
318}
319
320/// unifies the releases with old releases if on linux
321fn unified_releases(releases: Releases, platform: Platform) -> Releases {
322    if platform == Platform::LinuxAmd64 {
323        let mut all_releases = OLD_SOLC_RELEASES.clone();
324        all_releases.extend(releases);
325        all_releases
326    } else {
327        releases
328    }
329}
330
331/// Fixes build pre-release info for certain release lists.
332///
333/// In particular, the release list from the `nikitastupin/solc` repository does correctly label
334/// `prerelease` information in the build array, so populate it based on the `version`.
335fn fix_build_prerelease(releases: &mut Releases) {
336    for build in &mut releases.builds {
337        build.prerelease = if build.version.pre.is_empty() {
338            None
339        } else {
340            Some(build.version.pre.as_str().to_owned())
341        };
342    }
343}
344
345/// Construct the URL to the Solc binary for the specified release version and target platform.
346pub(crate) fn artifact_url(
347    platform: Platform,
348    version: &Version,
349    artifact: &str,
350) -> Result<Url, SvmError> {
351    if platform == Platform::LinuxAmd64
352        && *version <= OLD_VERSION_MAX
353        && *version >= OLD_VERSION_MIN
354    {
355        return Ok(Url::parse(&format!(
356            "{OLD_SOLC_RELEASES_DOWNLOAD_PREFIX}/{artifact}"
357        ))?);
358    }
359
360    if platform == Platform::LinuxAarch64 {
361        if *version >= LINUX_AARCH64_BINARIES {
362            return Ok(Url::parse(&format!(
363                "{SOLC_RELEASES_URL}/{LINUX_AARCH64_PLATFORM}/{artifact}"
364            ))?);
365        } else if *version >= LINUX_AARCH64_MIN {
366            return Ok(Url::parse(&format!(
367                "{LINUX_AARCH64_URL_PREFIX}/{artifact}"
368            ))?);
369        } else {
370            return Err(SvmError::UnsupportedVersion(
371                version.to_string(),
372                platform.to_string(),
373            ));
374        }
375    }
376
377    if platform == Platform::MacOsAmd64 && *version < OLD_VERSION_MIN {
378        return Err(SvmError::UnsupportedVersion(
379            version.to_string(),
380            platform.to_string(),
381        ));
382    }
383
384    if platform == Platform::MacOsAarch64 {
385        if *version >= MACOS_AARCH64_NATIVE && *version <= UNIVERSAL_MACOS_BINARIES {
386            // fetch natively build solc binaries from `https://github.com/alloy-rs/solc-builds`
387            return Ok(Url::parse(&format!(
388                "{MACOS_AARCH64_URL_PREFIX}/{artifact}"
389            ))?);
390        } else {
391            // if version is older or universal macos binaries are available, fetch from `https://binaries.soliditylang.org`
392            return Ok(Url::parse(&format!(
393                "{}/{}/{}",
394                SOLC_RELEASES_URL,
395                Platform::MacOsAmd64,
396                artifact,
397            ))?);
398        }
399    }
400
401    if platform == Platform::AndroidAarch64 {
402        if version.ge(&ANDROID_AARCH64_MIN) {
403            return Ok(Url::parse(&format!(
404                "{ANDROID_AARCH64_URL_PREFIX}/{artifact}"
405            ))?);
406        } else {
407            return Err(SvmError::UnsupportedVersion(
408                version.to_string(),
409                platform.to_string(),
410            ));
411        }
412    }
413
414    if platform == Platform::WindowsAarch64 {
415        // Windows ARM64 uses x64 binaries via emulation
416        // Solidity does not provide native ARM64 Windows binaries
417        return Ok(Url::parse(&format!(
418            "{SOLC_RELEASES_URL}/{}/{artifact}",
419            Platform::WindowsAmd64,
420        ))?);
421    }
422
423    Ok(Url::parse(&format!(
424        "{SOLC_RELEASES_URL}/{platform}/{artifact}"
425    ))?)
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_artifact_url() {
434        let version = Version::new(0, 5, 0);
435        let artifact = "solc-v0.5.0";
436        assert_eq!(
437            artifact_url(Platform::LinuxAarch64, &version, artifact).unwrap(),
438            Url::parse(&format!(
439                "https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64/{artifact}"
440            ))
441            .unwrap(),
442        )
443    }
444
445    #[test]
446    fn test_old_releases_deser() {
447        assert_eq!(OLD_SOLC_RELEASES.releases.len(), 10);
448        assert_eq!(OLD_SOLC_RELEASES.builds.len(), 10);
449    }
450
451    #[tokio::test]
452    async fn test_macos_aarch64() {
453        let releases = all_releases(Platform::MacOsAarch64)
454            .await
455            .expect("could not fetch releases for macos-aarch64");
456        let rosetta = Version::new(0, 8, 4);
457        let native = MACOS_AARCH64_NATIVE;
458        let url1 = artifact_url(
459            Platform::MacOsAarch64,
460            &rosetta,
461            releases.get_artifact(&rosetta).unwrap(),
462        )
463        .expect("could not fetch artifact URL");
464        let url2 = artifact_url(
465            Platform::MacOsAarch64,
466            &native,
467            releases.get_artifact(&native).unwrap(),
468        )
469        .expect("could not fetch artifact URL");
470        assert!(url1.to_string().contains(SOLC_RELEASES_URL));
471        assert!(url2.to_string().contains(MACOS_AARCH64_URL_PREFIX));
472    }
473
474    #[tokio::test]
475    async fn test_all_releases_macos_amd64() {
476        assert!(all_releases(Platform::MacOsAmd64).await.is_ok());
477    }
478
479    #[tokio::test]
480    async fn test_all_releases_macos_aarch64() {
481        assert!(all_releases(Platform::MacOsAarch64).await.is_ok());
482    }
483
484    #[tokio::test]
485    async fn test_all_releases_linux_amd64() {
486        assert!(all_releases(Platform::LinuxAmd64).await.is_ok());
487    }
488
489    #[tokio::test]
490    async fn test_all_releases_linux_aarch64() {
491        let releases = all_releases(Platform::LinuxAarch64)
492            .await
493            .expect("could not fetch releases for linux-aarch64");
494        let thirdparty = LINUX_AARCH64_MIN;
495        let url1 = artifact_url(
496            Platform::LinuxAarch64,
497            &thirdparty,
498            releases.get_artifact(&thirdparty).unwrap(),
499        )
500        .expect("could not fetch artifact URL");
501        let prerelease = Version::parse("0.8.31-pre.1").expect("failed to parse version");
502        let url2 = artifact_url(
503            Platform::LinuxAarch64,
504            &prerelease,
505            releases.get_artifact(&prerelease).unwrap(),
506        )
507        .expect("could not fetch artifact URL");
508        let official = LINUX_AARCH64_BINARIES;
509        let url3 = artifact_url(
510            Platform::LinuxAarch64,
511            &official,
512            releases.get_artifact(&official).unwrap(),
513        )
514        .expect("could not fetch artifact URL");
515        assert!(url1.to_string().contains(LINUX_AARCH64_URL_PREFIX));
516        assert!(url2.to_string().contains(LINUX_AARCH64_URL_PREFIX));
517        assert!(url3.to_string().contains(SOLC_RELEASES_URL));
518    }
519
520    #[tokio::test]
521    async fn test_all_releases_windows_aarch64() {
522        let releases = all_releases(Platform::WindowsAarch64).await;
523        assert!(releases.is_ok());
524        // Check also that we got the windows-amd64 release
525        let releases = releases.unwrap();
526        let latest = releases.releases.keys().max().unwrap();
527        let artifact = releases.get_artifact(latest).unwrap();
528        let url = artifact_url(Platform::WindowsAarch64, latest, artifact).unwrap();
529        assert!(url.to_string().contains("windows-amd64"));
530    }
531
532    #[tokio::test]
533    async fn releases_roundtrip() {
534        let releases = all_releases(Platform::LinuxAmd64).await.unwrap();
535        let s = serde_json::to_string(&releases).unwrap();
536        let de_releases: Releases = serde_json::from_str(&s).unwrap();
537        assert_eq!(releases, de_releases);
538    }
539}