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/// Blocking version of [`install`]
26#[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    // lock file to indicate that installation of this solc version will be in progress.
55    let lock_path = lock_file_path(version);
56    // wait until lock file is released, possibly by another parallel thread trying to install the
57    // same version of solc.
58    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
68/// Installs the provided version of Solc in the machine.
69///
70/// Returns the path to the solc file.
71pub 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    // lock file to indicate that installation of this solc version will be in progress.
101    let lock_path = lock_file_path(version);
102    // wait until lock file is released, possibly by another parallel thread trying to install the
103    // same version of solc.
104    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
114/// Same as [`do_install`] but retries "text file busy" errors.
115fn 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                // installation failed
128                if retries > 2 {
129                    return Err(err);
130                }
131                retries += 1;
132                // check if this failed due to a text file busy, which indicates that a different process started using the target file
133                if err.to_string().to_lowercase().contains("text file busy") {
134                    // 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
135                    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                                // checksum of the existing file matches the expected release checksum
140                                return Ok(solc_path);
141                            }
142                        }
143                    }
144
145                    // retry after some time
146                    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    // Solc versions <= 0.7.1 are .zip files for Windows only
161    #[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
169/// Creates the file and locks it exclusively, this will block if the file is currently locked
170fn 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
185/// Represents a lockfile that's removed once dropped
186struct 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
197/// Returns the lockfile to use for a specific file
198fn lock_file_path(version: &Version) -> PathBuf {
199    data_dir().join(format!(".lock-solc-{version}"))
200}
201
202// Installer type that copies binary data to the appropriate solc binary file:
203// 1. create target file to copy binary data
204// 2. copy data
205struct Installer<'a> {
206    // version of solc
207    version: &'a Version,
208    // binary data of the solc executable
209    binbytes: &'a [u8],
210}
211
212impl Installer<'_> {
213    /// Installs the solc version at the version specific destination and returns the path to the installed solc file.
214    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        // Windows requires that the old file be moved out of the way first.
229        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    /// Extracts the solc archive at the version specified destination and returns the path to the
240    /// installed solc binary.
241    #[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
256/// Patch the given binary to use the dynamic linker provided by nixos.
257fn 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    // checksum does not match
287    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        // Overwrite solc with `sleep` and call it with `infinity`.
333        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 should not fail with "text file busy".
339        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    // ensures we can download the latest universal solc for apple silicon
415    #[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    // ensures we can download the latest native solc for linux aarch64
428    #[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}