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