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 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 pub fn get_artifact(&self, version: &Version) -> Option<&String> {
99 if let Some(artifact) = self.releases.get(version) {
101 return Some(artifact);
102 }
103
104 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 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#[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
135mod 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#[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 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 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
214pub 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 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 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
280fn 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
292pub(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 return Ok(Url::parse(&format!(
331 "{MACOS_AARCH64_URL_PREFIX}/{artifact}"
332 ))?);
333 } else {
334 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 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 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}