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/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64";
34
35static LINUX_AARCH64_RELEASES_URL: &str = "https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/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    /// Checks for exact version match or for prerelease.
80    pub fn get_checksum(&self, v: &Version) -> Option<Vec<u8>> {
81        let matches = |build_info: &BuildInfo| {
82            let matched_release = build_info.version == *v;
83
84            let matched_prelease = !v.pre.is_empty()
85                && build_info.version == Version::new(v.major, v.minor, v.patch)
86                && build_info.prerelease.as_deref() == Some(v.pre.as_str());
87
88            matched_release || matched_prelease
89        };
90
91        self.builds
92            .iter()
93            .find(|build_info| matches(build_info))
94            .map(|build_info| build_info.sha256.clone())
95    }
96
97    /// Returns the artifact of the version if any, by looking it up in releases or in builds (if
98    /// a prerelease).
99    pub fn get_artifact(&self, version: &Version) -> Option<&String> {
100        // Check version artifact in releases.
101        if let Some(artifact) = self.releases.get(version) {
102            return Some(artifact);
103        }
104
105        // If we didn't find any artifact under releases, look up builds for prerelease.
106        if !version.pre.is_empty()
107            && let Some(build_info) = self.builds.iter().find(|b| {
108                b.version == Version::new(version.major, version.minor, version.patch)
109                    && b.prerelease == Some(version.pre.to_string())
110            })
111        {
112            return build_info.path.as_ref();
113        }
114
115        None
116    }
117
118    /// Returns a sorted list of all versions
119    pub fn into_versions(self) -> Vec<Version> {
120        let mut versions = self.releases.into_keys().collect::<Vec<_>>();
121        versions.sort_unstable();
122        versions
123    }
124}
125
126/// Build info contains the SHA256 checksum of a solc binary.
127#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
128pub struct BuildInfo {
129    pub version: Version,
130    #[serde(with = "hex_string")]
131    pub sha256: Vec<u8>,
132    pub path: Option<String>,
133    pub prerelease: Option<String>,
134}
135
136/// Helper serde module to serialize and deserialize bytes as hex.
137mod hex_string {
138    use super::*;
139    use serde::{Deserializer, Serializer, de};
140
141    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
142    where
143        D: Deserializer<'de>,
144    {
145        hex::decode(String::deserialize(deserializer)?).map_err(de::Error::custom)
146    }
147
148    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: Serializer,
151        T: AsRef<[u8]>,
152    {
153        serializer.serialize_str(&hex::encode_prefixed(value))
154    }
155}
156
157/// Blocking version of [`all_releases`].
158#[cfg(feature = "blocking")]
159pub fn blocking_all_releases(platform: Platform) -> Result<Releases, SvmError> {
160    match platform {
161        Platform::LinuxAarch64 => {
162            Ok(reqwest::blocking::get(LINUX_AARCH64_RELEASES_URL)?.json::<Releases>()?)
163        }
164        Platform::MacOsAarch64 => {
165            // The supported versions for both macos-amd64 and macos-aarch64 are the same.
166            //
167            // 1. For version >= 0.8.5 we fetch native releases from
168            // https://github.com/alloy-rs/solc-builds
169            //
170            // 2. For version <= 0.8.4 we fetch releases from https://binaries.soliditylang.org and
171            // require Rosetta support.
172            //
173            // Note: Since 0.8.24 universal macosx releases are available
174            let mut native =
175                reqwest::blocking::get(MACOS_AARCH64_RELEASES_URL)?.json::<Releases>()?;
176            let mut releases = reqwest::blocking::get(format!(
177                "{}/{}/list.json",
178                SOLC_RELEASES_URL,
179                Platform::MacOsAmd64,
180            ))?
181            .json::<Releases>()?;
182            releases.builds.retain(|b| {
183                b.version < MACOS_AARCH64_NATIVE || b.version > UNIVERSAL_MACOS_BINARIES
184            });
185            releases
186                .releases
187                .retain(|v, _| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
188            releases.builds.extend_from_slice(&native.builds);
189
190            releases.releases.append(&mut native.releases);
191            Ok(releases)
192        }
193        Platform::AndroidAarch64 => {
194            Ok(reqwest::blocking::get(ANDROID_AARCH64_RELEASES_URL)?.json::<Releases>()?)
195        }
196        Platform::WindowsAarch64 => {
197            // Windows ARM64 uses x64 binaries via emulation
198            // Solidity does not provide native ARM64 Windows binaries
199            let releases = reqwest::blocking::get(format!(
200                "{SOLC_RELEASES_URL}/{}/list.json",
201                Platform::WindowsAmd64
202            ))?
203            .json::<Releases>()?;
204            Ok(unified_releases(releases, platform))
205        }
206        _ => {
207            let releases =
208                reqwest::blocking::get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))?
209                    .json::<Releases>()?;
210            Ok(unified_releases(releases, platform))
211        }
212    }
213}
214
215/// Fetch all releases available for the provided platform.
216pub async fn all_releases(platform: Platform) -> Result<Releases, SvmError> {
217    match platform {
218        Platform::LinuxAarch64 => Ok(get(LINUX_AARCH64_RELEASES_URL)
219            .await?
220            .json::<Releases>()
221            .await?),
222        Platform::MacOsAarch64 => {
223            // The supported versions for both macos-amd64 and macos-aarch64 are the same.
224            //
225            // 1. For version >= 0.8.5 we fetch native releases from
226            // https://github.com/alloy-rs/solc-builds
227            //
228            // 2. For version <= 0.8.4 we fetch releases from https://binaries.soliditylang.org and
229            // require Rosetta support.
230            let mut native = get(MACOS_AARCH64_RELEASES_URL)
231                .await?
232                .json::<Releases>()
233                .await?;
234            let mut releases = get(format!(
235                "{}/{}/list.json",
236                SOLC_RELEASES_URL,
237                Platform::MacOsAmd64,
238            ))
239            .await?
240            .json::<Releases>()
241            .await?;
242            releases.builds.retain(|b| {
243                b.version < MACOS_AARCH64_NATIVE || b.version > UNIVERSAL_MACOS_BINARIES
244            });
245            releases
246                .releases
247                .retain(|v, _| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
248
249            releases.builds.extend_from_slice(&native.builds);
250            releases.releases.append(&mut native.releases);
251            Ok(releases)
252        }
253        Platform::AndroidAarch64 => Ok(get(ANDROID_AARCH64_RELEASES_URL)
254            .await?
255            .json::<Releases>()
256            .await?),
257        Platform::WindowsAarch64 => {
258            // Windows ARM64 uses x64 binaries via emulation
259            // Solidity does not provide native ARM64 Windows binaries
260            let releases = get(format!(
261                "{SOLC_RELEASES_URL}/{}/list.json",
262                Platform::WindowsAmd64
263            ))
264            .await?
265            .json::<Releases>()
266            .await?;
267
268            Ok(unified_releases(releases, platform))
269        }
270        _ => {
271            let releases = get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))
272                .await?
273                .json::<Releases>()
274                .await?;
275
276            Ok(unified_releases(releases, platform))
277        }
278    }
279}
280
281/// unifies the releases with old releases if on linux
282fn unified_releases(releases: Releases, platform: Platform) -> Releases {
283    if platform == Platform::LinuxAmd64 {
284        let mut all_releases = OLD_SOLC_RELEASES.clone();
285        all_releases.builds.extend(releases.builds);
286        all_releases.releases.extend(releases.releases);
287        all_releases
288    } else {
289        releases
290    }
291}
292
293/// Construct the URL to the Solc binary for the specified release version and target platform.
294pub(crate) fn artifact_url(
295    platform: Platform,
296    version: &Version,
297    artifact: &str,
298) -> Result<Url, SvmError> {
299    if platform == Platform::LinuxAmd64
300        && *version <= OLD_VERSION_MAX
301        && *version >= OLD_VERSION_MIN
302    {
303        return Ok(Url::parse(&format!(
304            "{OLD_SOLC_RELEASES_DOWNLOAD_PREFIX}/{artifact}"
305        ))?);
306    }
307
308    if platform == Platform::LinuxAarch64 {
309        if *version >= LINUX_AARCH64_MIN {
310            return Ok(Url::parse(&format!(
311                "{LINUX_AARCH64_URL_PREFIX}/{artifact}"
312            ))?);
313        } else {
314            return Err(SvmError::UnsupportedVersion(
315                version.to_string(),
316                platform.to_string(),
317            ));
318        }
319    }
320
321    if platform == Platform::MacOsAmd64 && *version < OLD_VERSION_MIN {
322        return Err(SvmError::UnsupportedVersion(
323            version.to_string(),
324            platform.to_string(),
325        ));
326    }
327
328    if platform == Platform::MacOsAarch64 {
329        if *version >= MACOS_AARCH64_NATIVE && *version <= UNIVERSAL_MACOS_BINARIES {
330            // fetch natively build solc binaries from `https://github.com/alloy-rs/solc-builds`
331            return Ok(Url::parse(&format!(
332                "{MACOS_AARCH64_URL_PREFIX}/{artifact}"
333            ))?);
334        } else {
335            // if version is older or universal macos binaries are available, fetch from `https://binaries.soliditylang.org`
336            return Ok(Url::parse(&format!(
337                "{}/{}/{}",
338                SOLC_RELEASES_URL,
339                Platform::MacOsAmd64,
340                artifact,
341            ))?);
342        }
343    }
344
345    if platform == Platform::AndroidAarch64 {
346        if version.ge(&ANDROID_AARCH64_MIN) {
347            return Ok(Url::parse(&format!(
348                "{ANDROID_AARCH64_URL_PREFIX}/{artifact}"
349            ))?);
350        } else {
351            return Err(SvmError::UnsupportedVersion(
352                version.to_string(),
353                platform.to_string(),
354            ));
355        }
356    }
357
358    if platform == Platform::WindowsAarch64 {
359        // Windows ARM64 uses x64 binaries via emulation
360        // Solidity does not provide native ARM64 Windows binaries
361        return Ok(Url::parse(&format!(
362            "{SOLC_RELEASES_URL}/{}/{artifact}",
363            Platform::WindowsAmd64,
364        ))?);
365    }
366
367    Ok(Url::parse(&format!(
368        "{SOLC_RELEASES_URL}/{platform}/{artifact}"
369    ))?)
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_artifact_url() {
378        let version = Version::new(0, 5, 0);
379        let artifact = "solc-v0.5.0";
380        assert_eq!(
381            artifact_url(Platform::LinuxAarch64, &version, artifact).unwrap(),
382            Url::parse(&format!(
383                "https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64/{artifact}"
384            ))
385            .unwrap(),
386        )
387    }
388
389    #[test]
390    fn test_old_releases_deser() {
391        assert_eq!(OLD_SOLC_RELEASES.releases.len(), 10);
392        assert_eq!(OLD_SOLC_RELEASES.builds.len(), 10);
393    }
394
395    #[tokio::test]
396    async fn test_macos_aarch64() {
397        let releases = all_releases(Platform::MacOsAarch64)
398            .await
399            .expect("could not fetch releases for macos-aarch64");
400        let rosetta = Version::new(0, 8, 4);
401        let native = MACOS_AARCH64_NATIVE;
402        let url1 = artifact_url(
403            Platform::MacOsAarch64,
404            &rosetta,
405            releases.get_artifact(&rosetta).unwrap(),
406        )
407        .expect("could not fetch artifact URL");
408        let url2 = artifact_url(
409            Platform::MacOsAarch64,
410            &native,
411            releases.get_artifact(&native).unwrap(),
412        )
413        .expect("could not fetch artifact URL");
414        assert!(url1.to_string().contains(SOLC_RELEASES_URL));
415        assert!(url2.to_string().contains(MACOS_AARCH64_URL_PREFIX));
416    }
417
418    #[tokio::test]
419    async fn test_all_releases_macos_amd64() {
420        assert!(all_releases(Platform::MacOsAmd64).await.is_ok());
421    }
422
423    #[tokio::test]
424    async fn test_all_releases_macos_aarch64() {
425        assert!(all_releases(Platform::MacOsAarch64).await.is_ok());
426    }
427
428    #[tokio::test]
429    async fn test_all_releases_linux_amd64() {
430        assert!(all_releases(Platform::LinuxAmd64).await.is_ok());
431    }
432
433    #[tokio::test]
434    async fn test_all_releases_linux_aarch64() {
435        assert!(all_releases(Platform::LinuxAarch64).await.is_ok());
436    }
437
438    #[tokio::test]
439    async fn test_all_releases_windows_aarch64() {
440        let releases = all_releases(Platform::WindowsAarch64).await;
441        assert!(releases.is_ok());
442        // Check also that we got the windows-amd64 release
443        let releases = releases.unwrap();
444        let latest = releases.releases.keys().max().unwrap();
445        let artifact = releases.get_artifact(latest).unwrap();
446        let url = artifact_url(Platform::WindowsAarch64, latest, artifact).unwrap();
447        assert!(url.to_string().contains("windows-amd64"));
448    }
449
450    #[tokio::test]
451    async fn releases_roundtrip() {
452        let releases = all_releases(Platform::LinuxAmd64).await.unwrap();
453        let s = serde_json::to_string(&releases).unwrap();
454        let de_releases: Releases = serde_json::from_str(&s).unwrap();
455        assert_eq!(releases, de_releases);
456    }
457}