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    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        Platform::WindowsAarch64 => {
170            // Windows ARM64 uses x64 binaries via emulation
171            // Solidity does not provide native ARM64 Windows binaries
172            let releases = reqwest::blocking::get(format!(
173                "{SOLC_RELEASES_URL}/{}/list.json",
174                Platform::WindowsAmd64
175            ))?
176            .json::<Releases>()?;
177            Ok(unified_releases(releases, platform))
178        }
179        _ => {
180            let releases =
181                reqwest::blocking::get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))?
182                    .json::<Releases>()?;
183            Ok(unified_releases(releases, platform))
184        }
185    }
186}
187
188/// Fetch all releases available for the provided platform.
189pub async fn all_releases(platform: Platform) -> Result<Releases, SvmError> {
190    match platform {
191        Platform::LinuxAarch64 => Ok(get(LINUX_AARCH64_RELEASES_URL)
192            .await?
193            .json::<Releases>()
194            .await?),
195        Platform::MacOsAarch64 => {
196            // The supported versions for both macos-amd64 and macos-aarch64 are the same.
197            //
198            // 1. For version >= 0.8.5 we fetch native releases from
199            // https://github.com/alloy-rs/solc-builds
200            //
201            // 2. For version <= 0.8.4 we fetch releases from https://binaries.soliditylang.org and
202            // require Rosetta support.
203            let mut native = get(MACOS_AARCH64_RELEASES_URL)
204                .await?
205                .json::<Releases>()
206                .await?;
207            let mut releases = get(format!(
208                "{}/{}/list.json",
209                SOLC_RELEASES_URL,
210                Platform::MacOsAmd64,
211            ))
212            .await?
213            .json::<Releases>()
214            .await?;
215            releases.builds.retain(|b| {
216                b.version < MACOS_AARCH64_NATIVE || b.version > UNIVERSAL_MACOS_BINARIES
217            });
218            releases
219                .releases
220                .retain(|v, _| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
221
222            releases.builds.extend_from_slice(&native.builds);
223            releases.releases.append(&mut native.releases);
224            Ok(releases)
225        }
226        Platform::AndroidAarch64 => Ok(get(ANDROID_AARCH64_RELEASES_URL)
227            .await?
228            .json::<Releases>()
229            .await?),
230        Platform::WindowsAarch64 => {
231            // Windows ARM64 uses x64 binaries via emulation
232            // Solidity does not provide native ARM64 Windows binaries
233            let releases = get(format!(
234                "{SOLC_RELEASES_URL}/{}/list.json",
235                Platform::WindowsAmd64
236            ))
237            .await?
238            .json::<Releases>()
239            .await?;
240
241            Ok(unified_releases(releases, platform))
242        }
243        _ => {
244            let releases = get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))
245                .await?
246                .json::<Releases>()
247                .await?;
248
249            Ok(unified_releases(releases, platform))
250        }
251    }
252}
253
254/// unifies the releases with old releases if on linux
255fn unified_releases(releases: Releases, platform: Platform) -> Releases {
256    if platform == Platform::LinuxAmd64 {
257        let mut all_releases = OLD_SOLC_RELEASES.clone();
258        all_releases.builds.extend(releases.builds);
259        all_releases.releases.extend(releases.releases);
260        all_releases
261    } else {
262        releases
263    }
264}
265
266/// Construct the URL to the Solc binary for the specified release version and target platform.
267pub(crate) fn artifact_url(
268    platform: Platform,
269    version: &Version,
270    artifact: &str,
271) -> Result<Url, SvmError> {
272    if platform == Platform::LinuxAmd64
273        && *version <= OLD_VERSION_MAX
274        && *version >= OLD_VERSION_MIN
275    {
276        return Ok(Url::parse(&format!(
277            "{OLD_SOLC_RELEASES_DOWNLOAD_PREFIX}/{artifact}"
278        ))?);
279    }
280
281    if platform == Platform::LinuxAarch64 {
282        if *version >= LINUX_AARCH64_MIN {
283            return Ok(Url::parse(&format!(
284                "{LINUX_AARCH64_URL_PREFIX}/{artifact}"
285            ))?);
286        } else {
287            return Err(SvmError::UnsupportedVersion(
288                version.to_string(),
289                platform.to_string(),
290            ));
291        }
292    }
293
294    if platform == Platform::MacOsAmd64 && *version < OLD_VERSION_MIN {
295        return Err(SvmError::UnsupportedVersion(
296            version.to_string(),
297            platform.to_string(),
298        ));
299    }
300
301    if platform == Platform::MacOsAarch64 {
302        if *version >= MACOS_AARCH64_NATIVE && *version <= UNIVERSAL_MACOS_BINARIES {
303            // fetch natively build solc binaries from `https://github.com/alloy-rs/solc-builds`
304            return Ok(Url::parse(&format!(
305                "{MACOS_AARCH64_URL_PREFIX}/{artifact}"
306            ))?);
307        } else {
308            // if version is older or universal macos binaries are available, fetch from `https://binaries.soliditylang.org`
309            return Ok(Url::parse(&format!(
310                "{}/{}/{}",
311                SOLC_RELEASES_URL,
312                Platform::MacOsAmd64,
313                artifact,
314            ))?);
315        }
316    }
317
318    if platform == Platform::AndroidAarch64 {
319        if version.ge(&ANDROID_AARCH64_MIN) {
320            return Ok(Url::parse(&format!(
321                "{ANDROID_AARCH64_URL_PREFIX}/{artifact}"
322            ))?);
323        } else {
324            return Err(SvmError::UnsupportedVersion(
325                version.to_string(),
326                platform.to_string(),
327            ));
328        }
329    }
330
331    if platform == Platform::WindowsAarch64 {
332        // Windows ARM64 uses x64 binaries via emulation
333        // Solidity does not provide native ARM64 Windows binaries
334        return Ok(Url::parse(&format!(
335            "{SOLC_RELEASES_URL}/{}/{artifact}",
336            Platform::WindowsAmd64,
337        ))?);
338    }
339
340    Ok(Url::parse(&format!(
341        "{SOLC_RELEASES_URL}/{platform}/{artifact}"
342    ))?)
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_artifact_url() {
351        let version = Version::new(0, 5, 0);
352        let artifact = "solc-v0.5.0";
353        assert_eq!(
354            artifact_url(Platform::LinuxAarch64, &version, artifact).unwrap(),
355            Url::parse(&format!(
356                "https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64/{artifact}"
357            ))
358            .unwrap(),
359        )
360    }
361
362    #[test]
363    fn test_old_releases_deser() {
364        assert_eq!(OLD_SOLC_RELEASES.releases.len(), 10);
365        assert_eq!(OLD_SOLC_RELEASES.builds.len(), 10);
366    }
367
368    #[tokio::test]
369    async fn test_macos_aarch64() {
370        let releases = all_releases(Platform::MacOsAarch64)
371            .await
372            .expect("could not fetch releases for macos-aarch64");
373        let rosetta = Version::new(0, 8, 4);
374        let native = MACOS_AARCH64_NATIVE;
375        let url1 = artifact_url(
376            Platform::MacOsAarch64,
377            &rosetta,
378            releases.get_artifact(&rosetta).unwrap(),
379        )
380        .expect("could not fetch artifact URL");
381        let url2 = artifact_url(
382            Platform::MacOsAarch64,
383            &native,
384            releases.get_artifact(&native).unwrap(),
385        )
386        .expect("could not fetch artifact URL");
387        assert!(url1.to_string().contains(SOLC_RELEASES_URL));
388        assert!(url2.to_string().contains(MACOS_AARCH64_URL_PREFIX));
389    }
390
391    #[tokio::test]
392    async fn test_all_releases_macos_amd64() {
393        assert!(all_releases(Platform::MacOsAmd64).await.is_ok());
394    }
395
396    #[tokio::test]
397    async fn test_all_releases_macos_aarch64() {
398        assert!(all_releases(Platform::MacOsAarch64).await.is_ok());
399    }
400
401    #[tokio::test]
402    async fn test_all_releases_linux_amd64() {
403        assert!(all_releases(Platform::LinuxAmd64).await.is_ok());
404    }
405
406    #[tokio::test]
407    async fn test_all_releases_linux_aarch64() {
408        assert!(all_releases(Platform::LinuxAarch64).await.is_ok());
409    }
410
411    #[tokio::test]
412    async fn test_all_releases_windows_aarch64() {
413        let releases = all_releases(Platform::WindowsAarch64).await;
414        assert!(releases.is_ok());
415        // Check also that we got the windows-amd64 release
416        let releases = releases.unwrap();
417        let latest = releases.releases.keys().max().unwrap();
418        let artifact = releases.get_artifact(latest).unwrap();
419        let url = artifact_url(Platform::WindowsAarch64, latest, artifact).unwrap();
420        assert!(url.to_string().contains("windows-amd64"));
421    }
422
423    #[tokio::test]
424    async fn releases_roundtrip() {
425        let releases = all_releases(Platform::LinuxAmd64).await.unwrap();
426        let s = serde_json::to_string(&releases).unwrap();
427        let de_releases: Releases = serde_json::from_str(&s).unwrap();
428        assert_eq!(releases, de_releases);
429    }
430}