1use crate::{
2 SvmError, all_releases, data_dir, platform, releases::artifact_url, setup_data_dir,
3 setup_version, version_binary,
4};
5use semver::Version;
6use sha2::Digest;
7use std::{
8 fs,
9 io::Write,
10 path::{Path, PathBuf},
11 process::Command,
12 time::Duration,
13};
14use tempfile::NamedTempFile;
15
16#[cfg(target_family = "unix")]
17use std::{fs::Permissions, os::unix::fs::PermissionsExt};
18
19const REQUEST_TIMEOUT: Duration = Duration::from_secs(600);
21
22const NIXOS_MIN_PATCH_VERSION: Version = Version::new(0, 7, 6);
24
25const NIXOS_MAX_PATCH_VERSION: Version = Version::new(0, 8, 28);
28
29#[cfg(feature = "blocking")]
31pub fn blocking_install(version: &Version) -> Result<PathBuf, SvmError> {
32 setup_data_dir()?;
33
34 let artifacts = crate::blocking_all_releases(platform::platform())?;
35 let artifact = artifacts
36 .get_artifact(version)
37 .ok_or_else(|| SvmError::UnknownVersion(version.clone()))?;
38 let download_url = artifact_url(platform::platform(), version, artifact.to_string().as_str())?;
39
40 let expected_checksum = artifacts
41 .get_checksum(version)
42 .unwrap_or_else(|| panic!("checksum not available: {:?}", version.to_string()));
43
44 let res = reqwest::blocking::Client::builder()
45 .timeout(REQUEST_TIMEOUT)
46 .build()
47 .expect("reqwest::Client::new()")
48 .get(download_url.clone())
49 .send()?;
50
51 if !res.status().is_success() {
52 return Err(SvmError::UnsuccessfulResponse(download_url, res.status()));
53 }
54
55 let binbytes = res.bytes()?;
56 ensure_checksum(&binbytes, version, &expected_checksum)?;
57
58 let lock_path = lock_file_path(version);
60 let _lock = try_lock_file(lock_path)?;
63
64 do_install_and_retry(
65 version,
66 &binbytes,
67 artifact.to_string().as_str(),
68 &expected_checksum,
69 )
70}
71
72pub async fn install(version: &Version) -> Result<PathBuf, SvmError> {
76 setup_data_dir()?;
77
78 let artifacts = all_releases(platform::platform()).await?;
79 let artifact = artifacts
80 .get_artifact(version)
81 .ok_or_else(|| SvmError::UnknownVersion(version.clone()))?;
82 let download_url = artifact_url(platform::platform(), version, artifact.to_string().as_str())?;
83
84 let expected_checksum = artifacts
85 .get_checksum(version)
86 .unwrap_or_else(|| panic!("checksum not available: {:?}", version.to_string()));
87
88 let res = reqwest::Client::builder()
89 .timeout(REQUEST_TIMEOUT)
90 .build()
91 .expect("reqwest::Client::new()")
92 .get(download_url.clone())
93 .send()
94 .await?;
95
96 if !res.status().is_success() {
97 return Err(SvmError::UnsuccessfulResponse(download_url, res.status()));
98 }
99
100 let binbytes = res.bytes().await?;
101 ensure_checksum(&binbytes, version, &expected_checksum)?;
102
103 let lock_path = lock_file_path(version);
105 let _lock = try_lock_file(lock_path)?;
108
109 do_install_and_retry(
110 version,
111 &binbytes,
112 artifact.to_string().as_str(),
113 &expected_checksum,
114 )
115}
116
117fn do_install_and_retry(
119 version: &Version,
120 binbytes: &[u8],
121 artifact: &str,
122 expected_checksum: &[u8],
123) -> Result<PathBuf, SvmError> {
124 let mut retries = 0;
125
126 loop {
127 return match do_install(version, binbytes, artifact) {
128 Ok(path) => Ok(path),
129 Err(err) => {
130 if retries > 2 {
132 return Err(err);
133 }
134 retries += 1;
135 if err.to_string().to_lowercase().contains("text file busy") {
137 let solc_path = version_binary(&version.to_string());
139 if solc_path.exists()
140 && let Ok(content) = fs::read(&solc_path)
141 && ensure_checksum(&content, version, expected_checksum).is_ok()
142 {
143 return Ok(solc_path);
145 }
146
147 std::thread::sleep(Duration::from_millis(250));
149 continue;
150 }
151
152 Err(err)
153 }
154 };
155 }
156}
157
158fn do_install(version: &Version, binbytes: &[u8], _artifact: &str) -> Result<PathBuf, SvmError> {
159 setup_version(&version.to_string())?;
160 let installer = Installer { version, binbytes };
161
162 #[cfg(target_os = "windows")]
164 if _artifact.ends_with(".zip") {
165 return installer.install_zip();
166 }
167
168 installer.install()
169}
170
171fn try_lock_file(lock_path: PathBuf) -> Result<LockFile, SvmError> {
173 let _lock_file = fs::OpenOptions::new()
174 .create(true)
175 .truncate(true)
176 .read(true)
177 .write(true)
178 .open(&lock_path)?;
179 _lock_file.lock()?;
180 Ok(LockFile {
181 lock_path,
182 _lock_file,
183 })
184}
185
186struct LockFile {
188 _lock_file: fs::File,
189 lock_path: PathBuf,
190}
191
192impl Drop for LockFile {
193 fn drop(&mut self) {
194 let _ = fs::remove_file(&self.lock_path);
195 }
196}
197
198fn lock_file_path(version: &Version) -> PathBuf {
200 data_dir().join(format!(".lock-solc-{version}"))
201}
202
203struct Installer<'a> {
207 version: &'a Version,
209 binbytes: &'a [u8],
211}
212
213impl Installer<'_> {
214 fn install(self) -> Result<PathBuf, SvmError> {
216 let named_temp_file = NamedTempFile::new_in(data_dir())?;
217 let (mut f, temp_path) = named_temp_file.into_parts();
218
219 #[cfg(target_family = "unix")]
220 f.set_permissions(Permissions::from_mode(0o755))?;
221 f.write_all(self.binbytes)?;
222
223 if platform::is_nixos()
224 && *self.version >= NIXOS_MIN_PATCH_VERSION
225 && *self.version <= NIXOS_MAX_PATCH_VERSION
226 {
227 patch_for_nixos(&temp_path)?;
228 }
229
230 let solc_path = version_binary(&self.version.to_string());
231
232 if cfg!(target_os = "windows") {
234 let temp_path = NamedTempFile::new_in(data_dir()).map(NamedTempFile::into_temp_path)?;
235 fs::rename(&solc_path, &temp_path).unwrap_or_default();
236 }
237
238 temp_path.persist(&solc_path)?;
239
240 Ok(solc_path)
241 }
242
243 #[cfg(target_os = "windows")]
246 fn install_zip(self) -> Result<PathBuf, SvmError> {
247 let solc_path = version_binary(&self.version.to_string());
248 let version_path = solc_path.parent().unwrap();
249
250 let mut content = std::io::Cursor::new(self.binbytes);
251 let mut archive = zip::ZipArchive::new(&mut content)?;
252 archive.extract(version_path)?;
253
254 std::fs::rename(version_path.join("solc.exe"), &solc_path)?;
255
256 Ok(solc_path)
257 }
258}
259
260fn patch_for_nixos(bin: &Path) -> Result<(), SvmError> {
262 let output = Command::new("nix-shell")
263 .arg("-p")
264 .arg("patchelf")
265 .arg("--run")
266 .arg(format!(
267 "patchelf --set-interpreter \"$(cat $NIX_CC/nix-support/dynamic-linker)\" {}",
268 bin.display()
269 ))
270 .output()
271 .map_err(|e| SvmError::CouldNotPatchForNixOs(String::new(), e.to_string()))?;
272
273 match output.status.success() {
274 true => Ok(()),
275 false => Err(SvmError::CouldNotPatchForNixOs(
276 String::from_utf8_lossy(&output.stdout).into_owned(),
277 String::from_utf8_lossy(&output.stderr).into_owned(),
278 )),
279 }
280}
281
282fn ensure_checksum(
283 binbytes: &[u8],
284 version: &Version,
285 expected_checksum: &[u8],
286) -> Result<(), SvmError> {
287 let mut hasher = sha2::Sha256::new();
288 hasher.update(binbytes);
289 let checksum = &hasher.finalize()[..];
290 if checksum != expected_checksum {
292 return Err(SvmError::ChecksumMismatch {
293 version: version.to_string(),
294 expected: hex::encode(expected_checksum),
295 actual: hex::encode(checksum),
296 });
297 }
298 Ok(())
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use rand::seq::IndexedRandom;
305
306 #[allow(unused)]
307 const LATEST: Version = Version::new(0, 8, 30);
308
309 #[tokio::test]
310 #[serial_test::serial]
311 async fn test_install() {
312 let versions = all_releases(platform())
313 .await
314 .unwrap()
315 .releases
316 .into_keys()
317 .collect::<Vec<Version>>();
318 let rand_version = versions.choose(&mut rand::rng()).unwrap();
319 assert!(install(rand_version).await.is_ok());
320 }
321
322 #[tokio::test]
323 #[serial_test::serial]
324 async fn can_install_while_solc_is_running() {
325 const WHICH: &str = if cfg!(target_os = "windows") {
326 "where"
327 } else {
328 "which"
329 };
330 const CMD: &str = if cfg!(target_os = "windows") {
332 "timeout"
333 } else {
334 "sleep"
335 };
336
337 let version: Version = "0.8.10".parse().unwrap();
338 let solc_path = version_binary(version.to_string().as_str());
339
340 fs::create_dir_all(solc_path.parent().unwrap()).unwrap();
341
342 let stdout = Command::new(WHICH).arg(CMD).output().unwrap().stdout;
344 let cmd_path = String::from_utf8(stdout).unwrap();
345 let cmd_path = cmd_path.lines().next().unwrap_or(&cmd_path);
347 fs::copy(cmd_path.trim_end(), &solc_path).unwrap();
348
349 let mut child = if cfg!(target_os = "windows") {
350 Command::new(&solc_path)
351 .args(["/t", "3600"])
352 .spawn()
353 .unwrap()
354 } else {
355 Command::new(&solc_path).arg("infinity").spawn().unwrap()
356 };
357
358 install(&version).await.unwrap();
360
361 child.kill().unwrap();
362 let _: std::process::ExitStatus = child.wait().unwrap();
363 }
364
365 #[cfg(feature = "blocking")]
366 #[serial_test::serial]
367 #[test]
368 fn blocking_test_install() {
369 let versions = crate::releases::blocking_all_releases(platform::platform())
370 .unwrap()
371 .into_versions();
372 let rand_version = versions.choose(&mut rand::rng()).unwrap();
373 assert!(blocking_install(rand_version).is_ok());
374 }
375
376 #[tokio::test]
377 #[serial_test::serial]
378 async fn test_version() {
379 let version = "0.8.10".parse().unwrap();
380 install(&version).await.unwrap();
381 let solc_path = version_binary(version.to_string().as_str());
382 let output = Command::new(solc_path).arg("--version").output().unwrap();
383 assert!(
384 String::from_utf8_lossy(&output.stdout)
385 .as_ref()
386 .contains("0.8.10")
387 );
388 }
389
390 #[cfg(feature = "blocking")]
391 #[serial_test::serial]
392 #[test]
393 fn blocking_test_latest() {
394 blocking_install(&LATEST).unwrap();
395 let solc_path = version_binary(LATEST.to_string().as_str());
396 let output = Command::new(solc_path).arg("--version").output().unwrap();
397
398 assert!(
399 String::from_utf8_lossy(&output.stdout)
400 .as_ref()
401 .contains(&LATEST.to_string())
402 );
403 }
404
405 #[cfg(feature = "blocking")]
406 #[serial_test::serial]
407 #[test]
408 fn blocking_test_version() {
409 let version = "0.8.10".parse().unwrap();
410 blocking_install(&version).unwrap();
411 let solc_path = version_binary(version.to_string().as_str());
412 let output = Command::new(solc_path).arg("--version").output().unwrap();
413
414 assert!(
415 String::from_utf8_lossy(&output.stdout)
416 .as_ref()
417 .contains("0.8.10")
418 );
419 }
420
421 #[cfg(feature = "blocking")]
422 #[test]
423 fn can_install_parallel() {
424 let version: Version = "0.8.10".parse().unwrap();
425 let cloned_version = version.clone();
426 let t = std::thread::spawn(move || blocking_install(&cloned_version));
427 blocking_install(&version).unwrap();
428 t.join().unwrap().unwrap();
429 }
430
431 #[tokio::test(flavor = "multi_thread")]
432 async fn can_install_parallel_async() {
433 let version: Version = "0.8.10".parse().unwrap();
434 let cloned_version = version.clone();
435 let t = tokio::task::spawn(async move { install(&cloned_version).await });
436 install(&version).await.unwrap();
437 t.await.unwrap().unwrap();
438 }
439
440 #[tokio::test(flavor = "multi_thread")]
442 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
443 async fn can_install_latest_native_apple_silicon() {
444 let solc = install(&LATEST).await.unwrap();
445 let output = Command::new(solc).arg("--version").output().unwrap();
446 let version_output = String::from_utf8_lossy(&output.stdout);
447 assert!(
448 version_output.contains(&LATEST.to_string()),
449 "{version_output}"
450 );
451 }
452
453 #[tokio::test(flavor = "multi_thread")]
455 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
456 async fn can_download_latest_linux_aarch64() {
457 let artifacts = all_releases(Platform::LinuxAarch64).await.unwrap();
458
459 let artifact = artifacts.releases.get(&LATEST).unwrap();
460 let download_url = artifact_url(
461 Platform::LinuxAarch64,
462 &LATEST,
463 artifact.to_string().as_str(),
464 )
465 .unwrap();
466
467 let checksum = artifacts.get_checksum(&LATEST).unwrap();
468
469 let resp = reqwest::get(download_url).await.unwrap();
470 assert!(resp.status().is_success());
471 let binbytes = resp.bytes().await.unwrap();
472 ensure_checksum(&binbytes, &LATEST, checksum).unwrap();
473 }
474
475 #[tokio::test]
476 #[cfg(target_os = "windows")]
477 async fn can_install_windows_zip_release() {
478 let version = "0.7.1".parse().unwrap();
479 install(&version).await.unwrap();
480 let solc_path = version_binary(version.to_string().as_str());
481 let output = Command::new(&solc_path).arg("--version").output().unwrap();
482
483 assert!(
484 String::from_utf8_lossy(&output.stdout)
485 .as_ref()
486 .contains("0.7.1")
487 );
488 }
489
490 #[cfg(feature = "blocking")]
491 #[serial_test::serial]
492 #[test]
493 #[ignore]
494 fn blocking_test_0_8_31_pre() {
495 let version = "0.8.31-pre.1".parse().unwrap();
496 blocking_install(&version).unwrap();
497 let solc_path = version_binary(version.to_string().as_str());
498 let output = Command::new(solc_path).arg("--version").output().unwrap();
499
500 assert!(
501 String::from_utf8_lossy(&output.stdout)
502 .as_ref()
503 .contains(&version.to_string())
504 );
505 }
506}