svm/
install.rs

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
19/// The timeout to use for requests to the source (10 minutes).
20const REQUEST_TIMEOUT: Duration = Duration::from_secs(600);
21
22/// Version beyond which solc binaries are not fully static, hence need to be patched for NixOS.
23const NIXOS_MIN_PATCH_VERSION: Version = Version::new(0, 7, 6);
24
25/// Version beyond which solc binaries are fully static again, hence no patching is needed for NixOS.
26/// See <https://github.com/ethereum/solidity/releases/tag/v0.8.29>
27const NIXOS_MAX_PATCH_VERSION: Version = Version::new(0, 8, 28);
28
29/// Blocking version of [`install`]
30#[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    // lock file to indicate that installation of this solc version will be in progress.
59    let lock_path = lock_file_path(version);
60    // wait until lock file is released, possibly by another parallel thread trying to install the
61    // same version of solc.
62    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
72/// Installs the provided version of Solc in the machine.
73///
74/// Returns the path to the solc file.
75pub 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    // lock file to indicate that installation of this solc version will be in progress.
105    let lock_path = lock_file_path(version);
106    // wait until lock file is released, possibly by another parallel thread trying to install the
107    // same version of solc.
108    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
118/// Same as [`do_install`] but retries "text file busy" errors.
119fn 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                // installation failed
132                if retries > 2 {
133                    return Err(err);
134                }
135                retries += 1;
136                // check if this failed due to a text file busy, which indicates that a different process started using the target file
137                if err.to_string().to_lowercase().contains("text file busy") {
138                    // busy solc can be in use for a while (e.g. if compiling a large project), so we check if the file exists and has the correct checksum
139                    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                                // checksum of the existing file matches the expected release checksum
144                                return Ok(solc_path);
145                            }
146                        }
147                    }
148
149                    // retry after some time
150                    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    // Solc versions <= 0.7.1 are .zip files for Windows only
165    #[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
173/// Creates the file and locks it exclusively, this will block if the file is currently locked
174fn 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
189/// Represents a lockfile that's removed once dropped
190struct 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
201/// Returns the lockfile to use for a specific file
202fn lock_file_path(version: &Version) -> PathBuf {
203    data_dir().join(format!(".lock-solc-{version}"))
204}
205
206// Installer type that copies binary data to the appropriate solc binary file:
207// 1. create target file to copy binary data
208// 2. copy data
209struct Installer<'a> {
210    // version of solc
211    version: &'a Version,
212    // binary data of the solc executable
213    binbytes: &'a [u8],
214}
215
216impl Installer<'_> {
217    /// Installs the solc version at the version specific destination and returns the path to the installed solc file.
218    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        // Windows requires that the old file be moved out of the way first.
236        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    /// Extracts the solc archive at the version specified destination and returns the path to the
247    /// installed solc binary.
248    #[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
263/// Patch the given binary to use the dynamic linker provided by nixos.
264fn 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    // checksum does not match
294    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        // Overwrite solc with `sleep` and call it with `infinity`.
340        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 should not fail with "text file busy".
346        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    // ensures we can download the latest universal solc for apple silicon
422    #[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    // ensures we can download the latest native solc for linux aarch64
435    #[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}