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
8const 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
37const 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#[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 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 pub fn get_artifact(&self, version: &Version) -> Option<&String> {
100 if let Some(artifact) = self.releases.get(version) {
102 return Some(artifact);
103 }
104
105 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 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#[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
136mod 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#[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 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 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
215pub 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 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 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
281fn 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
293pub(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 return Ok(Url::parse(&format!(
332 "{MACOS_AARCH64_URL_PREFIX}/{artifact}"
333 ))?);
334 } else {
335 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 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 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}