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
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 if let Ok(content) = fs::read(&solc_path) {
142 if ensure_checksum(&content, version, expected_checksum).is_ok() {
143 return Ok(solc_path);
145 }
146 }
147 }
148
149 std::thread::sleep(Duration::from_millis(250));
151 continue;
152 }
153
154 Err(err)
155 }
156 };
157 }
158}
159
160fn do_install(version: &Version, binbytes: &[u8], _artifact: &str) -> Result<PathBuf, SvmError> {
161 setup_version(&version.to_string())?;
162 let installer = Installer { version, binbytes };
163
164 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
166 if _artifact.ends_with(".zip") {
167 return installer.install_zip();
168 }
169
170 installer.install()
171}
172
173fn try_lock_file(lock_path: PathBuf) -> Result<LockFile, SvmError> {
175 use fs4::fs_std::FileExt;
176 let _lock_file = fs::OpenOptions::new()
177 .create(true)
178 .truncate(true)
179 .read(true)
180 .write(true)
181 .open(&lock_path)?;
182 _lock_file.lock_exclusive()?;
183 Ok(LockFile {
184 lock_path,
185 _lock_file,
186 })
187}
188
189struct LockFile {
191 _lock_file: fs::File,
192 lock_path: PathBuf,
193}
194
195impl Drop for LockFile {
196 fn drop(&mut self) {
197 let _ = fs::remove_file(&self.lock_path);
198 }
199}
200
201fn lock_file_path(version: &Version) -> PathBuf {
203 data_dir().join(format!(".lock-solc-{version}"))
204}
205
206struct Installer<'a> {
210 version: &'a Version,
212 binbytes: &'a [u8],
214}
215
216impl Installer<'_> {
217 fn install(self) -> Result<PathBuf, SvmError> {
219 let named_temp_file = NamedTempFile::new_in(data_dir())?;
220 let (mut f, temp_path) = named_temp_file.into_parts();
221
222 #[cfg(target_family = "unix")]
223 f.set_permissions(Permissions::from_mode(0o755))?;
224 f.write_all(self.binbytes)?;
225
226 if platform::is_nixos()
227 && *self.version >= NIXOS_MIN_PATCH_VERSION
228 && *self.version <= NIXOS_MAX_PATCH_VERSION
229 {
230 patch_for_nixos(&temp_path)?;
231 }
232
233 let solc_path = version_binary(&self.version.to_string());
234
235 if cfg!(target_os = "windows") {
237 let temp_path = NamedTempFile::new_in(data_dir()).map(NamedTempFile::into_temp_path)?;
238 fs::rename(&solc_path, &temp_path).unwrap_or_default();
239 }
240
241 temp_path.persist(&solc_path)?;
242
243 Ok(solc_path)
244 }
245
246 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
249 fn install_zip(self) -> Result<PathBuf, SvmError> {
250 let solc_path = version_binary(&self.version.to_string());
251 let version_path = solc_path.parent().unwrap();
252
253 let mut content = std::io::Cursor::new(self.binbytes);
254 let mut archive = zip::ZipArchive::new(&mut content)?;
255 archive.extract(version_path)?;
256
257 std::fs::rename(version_path.join("solc.exe"), &solc_path)?;
258
259 Ok(solc_path)
260 }
261}
262
263fn patch_for_nixos(bin: &Path) -> Result<(), SvmError> {
265 let output = Command::new("nix-shell")
266 .arg("-p")
267 .arg("patchelf")
268 .arg("--run")
269 .arg(format!(
270 "patchelf --set-interpreter \"$(cat $NIX_CC/nix-support/dynamic-linker)\" {}",
271 bin.display()
272 ))
273 .output()
274 .map_err(|e| SvmError::CouldNotPatchForNixOs(String::new(), e.to_string()))?;
275
276 match output.status.success() {
277 true => Ok(()),
278 false => Err(SvmError::CouldNotPatchForNixOs(
279 String::from_utf8_lossy(&output.stdout).into_owned(),
280 String::from_utf8_lossy(&output.stderr).into_owned(),
281 )),
282 }
283}
284
285fn ensure_checksum(
286 binbytes: &[u8],
287 version: &Version,
288 expected_checksum: &[u8],
289) -> Result<(), SvmError> {
290 let mut hasher = sha2::Sha256::new();
291 hasher.update(binbytes);
292 let checksum = &hasher.finalize()[..];
293 if checksum != expected_checksum {
295 return Err(SvmError::ChecksumMismatch {
296 version: version.to_string(),
297 expected: hex::encode(expected_checksum),
298 actual: hex::encode(checksum),
299 });
300 }
301 Ok(())
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use rand::seq::IndexedRandom;
308
309 #[allow(unused)]
310 const LATEST: Version = Version::new(0, 8, 29);
311
312 #[tokio::test]
313 #[serial_test::serial]
314 async fn test_install() {
315 let versions = all_releases(platform())
316 .await
317 .unwrap()
318 .releases
319 .into_keys()
320 .collect::<Vec<Version>>();
321 let rand_version = versions.choose(&mut rand::rng()).unwrap();
322 assert!(install(rand_version).await.is_ok());
323 }
324
325 #[tokio::test]
326 #[serial_test::serial]
327 async fn can_install_while_solc_is_running() {
328 const WHICH: &str = if cfg!(target_os = "windows") {
329 "where"
330 } else {
331 "which"
332 };
333
334 let version: Version = "0.8.10".parse().unwrap();
335 let solc_path = version_binary(version.to_string().as_str());
336
337 fs::create_dir_all(solc_path.parent().unwrap()).unwrap();
338
339 let stdout = Command::new(WHICH).arg("sleep").output().unwrap().stdout;
341 let sleep_path = String::from_utf8(stdout).unwrap();
342 fs::copy(sleep_path.trim_end(), &solc_path).unwrap();
343 let mut child = Command::new(solc_path).arg("infinity").spawn().unwrap();
344
345 install(&version).await.unwrap();
347
348 child.kill().unwrap();
349 let _: std::process::ExitStatus = child.wait().unwrap();
350 }
351
352 #[cfg(feature = "blocking")]
353 #[serial_test::serial]
354 #[test]
355 fn blocking_test_install() {
356 let versions = crate::releases::blocking_all_releases(platform::platform())
357 .unwrap()
358 .into_versions();
359 let rand_version = versions.choose(&mut rand::rng()).unwrap();
360 assert!(blocking_install(rand_version).is_ok());
361 }
362
363 #[tokio::test]
364 #[serial_test::serial]
365 async fn test_version() {
366 let version = "0.8.10".parse().unwrap();
367 install(&version).await.unwrap();
368 let solc_path = version_binary(version.to_string().as_str());
369 let output = Command::new(solc_path).arg("--version").output().unwrap();
370 assert!(String::from_utf8_lossy(&output.stdout)
371 .as_ref()
372 .contains("0.8.10"));
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!(String::from_utf8_lossy(&output.stdout)
384 .as_ref()
385 .contains(&LATEST.to_string()));
386 }
387
388 #[cfg(feature = "blocking")]
389 #[serial_test::serial]
390 #[test]
391 fn blocking_test_version() {
392 let version = "0.8.10".parse().unwrap();
393 blocking_install(&version).unwrap();
394 let solc_path = version_binary(version.to_string().as_str());
395 let output = Command::new(solc_path).arg("--version").output().unwrap();
396
397 assert!(String::from_utf8_lossy(&output.stdout)
398 .as_ref()
399 .contains("0.8.10"));
400 }
401
402 #[cfg(feature = "blocking")]
403 #[test]
404 fn can_install_parallel() {
405 let version: Version = "0.8.10".parse().unwrap();
406 let cloned_version = version.clone();
407 let t = std::thread::spawn(move || blocking_install(&cloned_version));
408 blocking_install(&version).unwrap();
409 t.join().unwrap().unwrap();
410 }
411
412 #[tokio::test(flavor = "multi_thread")]
413 async fn can_install_parallel_async() {
414 let version: Version = "0.8.10".parse().unwrap();
415 let cloned_version = version.clone();
416 let t = tokio::task::spawn(async move { install(&cloned_version).await });
417 install(&version).await.unwrap();
418 t.await.unwrap().unwrap();
419 }
420
421 #[tokio::test(flavor = "multi_thread")]
423 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
424 async fn can_install_latest_native_apple_silicon() {
425 let solc = install(&LATEST).await.unwrap();
426 let output = Command::new(solc).arg("--version").output().unwrap();
427 let version_output = String::from_utf8_lossy(&output.stdout);
428 assert!(
429 version_output.contains(&LATEST.to_string()),
430 "{version_output}"
431 );
432 }
433
434 #[tokio::test(flavor = "multi_thread")]
436 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
437 async fn can_download_latest_linux_aarch64() {
438 let artifacts = all_releases(Platform::LinuxAarch64).await.unwrap();
439
440 let artifact = artifacts.releases.get(&LATEST).unwrap();
441 let download_url = artifact_url(
442 Platform::LinuxAarch64,
443 &LATEST,
444 artifact.to_string().as_str(),
445 )
446 .unwrap();
447
448 let checksum = artifacts.get_checksum(&LATEST).unwrap();
449
450 let resp = reqwest::get(download_url).await.unwrap();
451 assert!(resp.status().is_success());
452 let binbytes = resp.bytes().await.unwrap();
453 ensure_checksum(&binbytes, &LATEST, checksum).unwrap();
454 }
455
456 #[tokio::test]
457 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
458 async fn can_install_windows_zip_release() {
459 let version = "0.7.1".parse().unwrap();
460 install(&version).await.unwrap();
461 let solc_path = version_binary(version.to_string().as_str());
462 let output = Command::new(&solc_path).arg("--version").output().unwrap();
463
464 assert!(String::from_utf8_lossy(&output.stdout)
465 .as_ref()
466 .contains("0.7.1"));
467 }
468}