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