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(SvmError::UnknownVersion)?;
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 .releases
81 .get(version)
82 .ok_or(SvmError::UnknownVersion)?;
83 let download_url = artifact_url(platform::platform(), version, artifact.to_string().as_str())?;
84
85 let expected_checksum = artifacts
86 .get_checksum(version)
87 .unwrap_or_else(|| panic!("checksum not available: {:?}", version.to_string()));
88
89 let res = reqwest::Client::builder()
90 .timeout(REQUEST_TIMEOUT)
91 .build()
92 .expect("reqwest::Client::new()")
93 .get(download_url.clone())
94 .send()
95 .await?;
96
97 if !res.status().is_success() {
98 return Err(SvmError::UnsuccessfulResponse(download_url, res.status()));
99 }
100
101 let binbytes = res.bytes().await?;
102 ensure_checksum(&binbytes, version, &expected_checksum)?;
103
104 let lock_path = lock_file_path(version);
106 let _lock = try_lock_file(lock_path)?;
109
110 do_install_and_retry(
111 version,
112 &binbytes,
113 artifact.to_string().as_str(),
114 &expected_checksum,
115 )
116}
117
118fn do_install_and_retry(
120 version: &Version,
121 binbytes: &[u8],
122 artifact: &str,
123 expected_checksum: &[u8],
124) -> Result<PathBuf, SvmError> {
125 let mut retries = 0;
126
127 loop {
128 return match do_install(version, binbytes, artifact) {
129 Ok(path) => Ok(path),
130 Err(err) => {
131 if retries > 2 {
133 return Err(err);
134 }
135 retries += 1;
136 if err.to_string().to_lowercase().contains("text file busy") {
138 let solc_path = version_binary(&version.to_string());
140 if solc_path.exists()
141 && let Ok(content) = fs::read(&solc_path)
142 && ensure_checksum(&content, version, expected_checksum).is_ok()
143 {
144 return Ok(solc_path);
146 }
147
148 std::thread::sleep(Duration::from_millis(250));
150 continue;
151 }
152
153 Err(err)
154 }
155 };
156 }
157}
158
159fn do_install(version: &Version, binbytes: &[u8], _artifact: &str) -> Result<PathBuf, SvmError> {
160 setup_version(&version.to_string())?;
161 let installer = Installer { version, binbytes };
162
163 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
165 if _artifact.ends_with(".zip") {
166 return installer.install_zip();
167 }
168
169 installer.install()
170}
171
172fn try_lock_file(lock_path: PathBuf) -> Result<LockFile, SvmError> {
174 let _lock_file = fs::OpenOptions::new()
175 .create(true)
176 .truncate(true)
177 .read(true)
178 .write(true)
179 .open(&lock_path)?;
180 _lock_file.lock()?;
181 Ok(LockFile {
182 lock_path,
183 _lock_file,
184 })
185}
186
187struct LockFile {
189 _lock_file: fs::File,
190 lock_path: PathBuf,
191}
192
193impl Drop for LockFile {
194 fn drop(&mut self) {
195 let _ = fs::remove_file(&self.lock_path);
196 }
197}
198
199fn lock_file_path(version: &Version) -> PathBuf {
201 data_dir().join(format!(".lock-solc-{version}"))
202}
203
204struct Installer<'a> {
208 version: &'a Version,
210 binbytes: &'a [u8],
212}
213
214impl Installer<'_> {
215 fn install(self) -> Result<PathBuf, SvmError> {
217 let named_temp_file = NamedTempFile::new_in(data_dir())?;
218 let (mut f, temp_path) = named_temp_file.into_parts();
219
220 #[cfg(target_family = "unix")]
221 f.set_permissions(Permissions::from_mode(0o755))?;
222 f.write_all(self.binbytes)?;
223
224 if platform::is_nixos()
225 && *self.version >= NIXOS_MIN_PATCH_VERSION
226 && *self.version <= NIXOS_MAX_PATCH_VERSION
227 {
228 patch_for_nixos(&temp_path)?;
229 }
230
231 let solc_path = version_binary(&self.version.to_string());
232
233 if cfg!(target_os = "windows") {
235 let temp_path = NamedTempFile::new_in(data_dir()).map(NamedTempFile::into_temp_path)?;
236 fs::rename(&solc_path, &temp_path).unwrap_or_default();
237 }
238
239 temp_path.persist(&solc_path)?;
240
241 Ok(solc_path)
242 }
243
244 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
247 fn install_zip(self) -> Result<PathBuf, SvmError> {
248 let solc_path = version_binary(&self.version.to_string());
249 let version_path = solc_path.parent().unwrap();
250
251 let mut content = std::io::Cursor::new(self.binbytes);
252 let mut archive = zip::ZipArchive::new(&mut content)?;
253 archive.extract(version_path)?;
254
255 std::fs::rename(version_path.join("solc.exe"), &solc_path)?;
256
257 Ok(solc_path)
258 }
259}
260
261fn patch_for_nixos(bin: &Path) -> Result<(), SvmError> {
263 let output = Command::new("nix-shell")
264 .arg("-p")
265 .arg("patchelf")
266 .arg("--run")
267 .arg(format!(
268 "patchelf --set-interpreter \"$(cat $NIX_CC/nix-support/dynamic-linker)\" {}",
269 bin.display()
270 ))
271 .output()
272 .map_err(|e| SvmError::CouldNotPatchForNixOs(String::new(), e.to_string()))?;
273
274 match output.status.success() {
275 true => Ok(()),
276 false => Err(SvmError::CouldNotPatchForNixOs(
277 String::from_utf8_lossy(&output.stdout).into_owned(),
278 String::from_utf8_lossy(&output.stderr).into_owned(),
279 )),
280 }
281}
282
283fn ensure_checksum(
284 binbytes: &[u8],
285 version: &Version,
286 expected_checksum: &[u8],
287) -> Result<(), SvmError> {
288 let mut hasher = sha2::Sha256::new();
289 hasher.update(binbytes);
290 let checksum = &hasher.finalize()[..];
291 if checksum != expected_checksum {
293 return Err(SvmError::ChecksumMismatch {
294 version: version.to_string(),
295 expected: hex::encode(expected_checksum),
296 actual: hex::encode(checksum),
297 });
298 }
299 Ok(())
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use rand::seq::IndexedRandom;
306
307 #[allow(unused)]
308 const LATEST: Version = Version::new(0, 8, 30);
309
310 #[tokio::test]
311 #[serial_test::serial]
312 async fn test_install() {
313 let versions = all_releases(platform())
314 .await
315 .unwrap()
316 .releases
317 .into_keys()
318 .collect::<Vec<Version>>();
319 let rand_version = versions.choose(&mut rand::rng()).unwrap();
320 assert!(install(rand_version).await.is_ok());
321 }
322
323 #[tokio::test]
324 #[serial_test::serial]
325 async fn can_install_while_solc_is_running() {
326 const WHICH: &str = if cfg!(target_os = "windows") {
327 "where"
328 } else {
329 "which"
330 };
331
332 let version: Version = "0.8.10".parse().unwrap();
333 let solc_path = version_binary(version.to_string().as_str());
334
335 fs::create_dir_all(solc_path.parent().unwrap()).unwrap();
336
337 let stdout = Command::new(WHICH).arg("sleep").output().unwrap().stdout;
339 let sleep_path = String::from_utf8(stdout).unwrap();
340 fs::copy(sleep_path.trim_end(), &solc_path).unwrap();
341 let mut child = Command::new(solc_path).arg("infinity").spawn().unwrap();
342
343 install(&version).await.unwrap();
345
346 child.kill().unwrap();
347 let _: std::process::ExitStatus = child.wait().unwrap();
348 }
349
350 #[cfg(feature = "blocking")]
351 #[serial_test::serial]
352 #[test]
353 fn blocking_test_install() {
354 let versions = crate::releases::blocking_all_releases(platform::platform())
355 .unwrap()
356 .into_versions();
357 let rand_version = versions.choose(&mut rand::rng()).unwrap();
358 assert!(blocking_install(rand_version).is_ok());
359 }
360
361 #[tokio::test]
362 #[serial_test::serial]
363 async fn test_version() {
364 let version = "0.8.10".parse().unwrap();
365 install(&version).await.unwrap();
366 let solc_path = version_binary(version.to_string().as_str());
367 let output = Command::new(solc_path).arg("--version").output().unwrap();
368 assert!(
369 String::from_utf8_lossy(&output.stdout)
370 .as_ref()
371 .contains("0.8.10")
372 );
373 }
374
375 #[cfg(feature = "blocking")]
376 #[serial_test::serial]
377 #[test]
378 fn blocking_test_latest() {
379 blocking_install(&LATEST).unwrap();
380 let solc_path = version_binary(LATEST.to_string().as_str());
381 let output = Command::new(solc_path).arg("--version").output().unwrap();
382
383 assert!(
384 String::from_utf8_lossy(&output.stdout)
385 .as_ref()
386 .contains(&LATEST.to_string())
387 );
388 }
389
390 #[cfg(feature = "blocking")]
391 #[serial_test::serial]
392 #[test]
393 fn blocking_test_version() {
394 let version = "0.8.10".parse().unwrap();
395 blocking_install(&version).unwrap();
396 let solc_path = version_binary(version.to_string().as_str());
397 let output = Command::new(solc_path).arg("--version").output().unwrap();
398
399 assert!(
400 String::from_utf8_lossy(&output.stdout)
401 .as_ref()
402 .contains("0.8.10")
403 );
404 }
405
406 #[cfg(feature = "blocking")]
407 #[test]
408 fn can_install_parallel() {
409 let version: Version = "0.8.10".parse().unwrap();
410 let cloned_version = version.clone();
411 let t = std::thread::spawn(move || blocking_install(&cloned_version));
412 blocking_install(&version).unwrap();
413 t.join().unwrap().unwrap();
414 }
415
416 #[tokio::test(flavor = "multi_thread")]
417 async fn can_install_parallel_async() {
418 let version: Version = "0.8.10".parse().unwrap();
419 let cloned_version = version.clone();
420 let t = tokio::task::spawn(async move { install(&cloned_version).await });
421 install(&version).await.unwrap();
422 t.await.unwrap().unwrap();
423 }
424
425 #[tokio::test(flavor = "multi_thread")]
427 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
428 async fn can_install_latest_native_apple_silicon() {
429 let solc = install(&LATEST).await.unwrap();
430 let output = Command::new(solc).arg("--version").output().unwrap();
431 let version_output = String::from_utf8_lossy(&output.stdout);
432 assert!(
433 version_output.contains(&LATEST.to_string()),
434 "{version_output}"
435 );
436 }
437
438 #[tokio::test(flavor = "multi_thread")]
440 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
441 async fn can_download_latest_linux_aarch64() {
442 let artifacts = all_releases(Platform::LinuxAarch64).await.unwrap();
443
444 let artifact = artifacts.releases.get(&LATEST).unwrap();
445 let download_url = artifact_url(
446 Platform::LinuxAarch64,
447 &LATEST,
448 artifact.to_string().as_str(),
449 )
450 .unwrap();
451
452 let checksum = artifacts.get_checksum(&LATEST).unwrap();
453
454 let resp = reqwest::get(download_url).await.unwrap();
455 assert!(resp.status().is_success());
456 let binbytes = resp.bytes().await.unwrap();
457 ensure_checksum(&binbytes, &LATEST, checksum).unwrap();
458 }
459
460 #[tokio::test]
461 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
462 async fn can_install_windows_zip_release() {
463 let version = "0.7.1".parse().unwrap();
464 install(&version).await.unwrap();
465 let solc_path = version_binary(version.to_string().as_str());
466 let output = Command::new(&solc_path).arg("--version").output().unwrap();
467
468 assert!(
469 String::from_utf8_lossy(&output.stdout)
470 .as_ref()
471 .contains("0.7.1")
472 );
473 }
474}