-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: split out lib.rs into more modules (#117)
No functional changes
- Loading branch information
Showing
4 changed files
with
509 additions
and
490 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,365 @@ | ||
use crate::{ | ||
all_releases, data_dir, platform, releases::artifact_url, setup_data_dir, setup_version, | ||
version_binary, SvmError, | ||
}; | ||
use semver::Version; | ||
use sha2::Digest; | ||
use std::{ | ||
fs, | ||
io::Write, | ||
path::{Path, PathBuf}, | ||
process::Command, | ||
time::Duration, | ||
}; | ||
|
||
#[cfg(target_family = "unix")] | ||
use std::{fs::Permissions, os::unix::fs::PermissionsExt}; | ||
|
||
/// The timeout to use for requests to the source | ||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120); | ||
|
||
/// Version beyond which solc binaries are not fully static, hence need to be patched for NixOS. | ||
const NIXOS_MIN_PATCH_VERSION: Version = Version::new(0, 7, 6); | ||
|
||
/// Blocking version of [`install`] | ||
#[cfg(feature = "blocking")] | ||
pub fn blocking_install(version: &Version) -> Result<PathBuf, SvmError> { | ||
setup_data_dir()?; | ||
|
||
let artifacts = crate::blocking_all_releases(platform::platform())?; | ||
let artifact = artifacts | ||
.get_artifact(version) | ||
.ok_or(SvmError::UnknownVersion)?; | ||
let download_url = artifact_url(platform::platform(), version, artifact.to_string().as_str())?; | ||
|
||
let expected_checksum = artifacts | ||
.get_checksum(version) | ||
.unwrap_or_else(|| panic!("checksum not available: {:?}", version.to_string())); | ||
|
||
let res = reqwest::blocking::Client::builder() | ||
.timeout(REQUEST_TIMEOUT) | ||
.build() | ||
.expect("reqwest::Client::new()") | ||
.get(download_url.clone()) | ||
.send()?; | ||
|
||
if !res.status().is_success() { | ||
return Err(SvmError::UnsuccessfulResponse(download_url, res.status())); | ||
} | ||
|
||
let binbytes = res.bytes()?; | ||
ensure_checksum(&binbytes, version, &expected_checksum)?; | ||
|
||
// lock file to indicate that installation of this solc version will be in progress. | ||
let lock_path = lock_file_path(version); | ||
// wait until lock file is released, possibly by another parallel thread trying to install the | ||
// same version of solc. | ||
let _lock = try_lock_file(lock_path)?; | ||
|
||
do_install(version, &binbytes, artifact.to_string().as_str()) | ||
} | ||
|
||
/// Installs the provided version of Solc in the machine. | ||
/// | ||
/// Returns the path to the solc file. | ||
pub async fn install(version: &Version) -> Result<PathBuf, SvmError> { | ||
setup_data_dir()?; | ||
|
||
let artifacts = all_releases(platform::platform()).await?; | ||
let artifact = artifacts | ||
.releases | ||
.get(version) | ||
.ok_or(SvmError::UnknownVersion)?; | ||
let download_url = artifact_url(platform::platform(), version, artifact.to_string().as_str())?; | ||
|
||
let expected_checksum = artifacts | ||
.get_checksum(version) | ||
.unwrap_or_else(|| panic!("checksum not available: {:?}", version.to_string())); | ||
|
||
let res = reqwest::Client::builder() | ||
.timeout(REQUEST_TIMEOUT) | ||
.build() | ||
.expect("reqwest::Client::new()") | ||
.get(download_url.clone()) | ||
.send() | ||
.await?; | ||
|
||
if !res.status().is_success() { | ||
return Err(SvmError::UnsuccessfulResponse(download_url, res.status())); | ||
} | ||
|
||
let binbytes = res.bytes().await?; | ||
ensure_checksum(&binbytes, version, &expected_checksum)?; | ||
|
||
// lock file to indicate that installation of this solc version will be in progress. | ||
let lock_path = lock_file_path(version); | ||
// wait until lock file is released, possibly by another parallel thread trying to install the | ||
// same version of solc. | ||
let _lock = try_lock_file(lock_path)?; | ||
|
||
do_install(version, &binbytes, artifact.to_string().as_str()) | ||
} | ||
|
||
fn do_install(version: &Version, binbytes: &[u8], _artifact: &str) -> Result<PathBuf, SvmError> { | ||
setup_version(&version.to_string())?; | ||
let installer = Installer { version, binbytes }; | ||
|
||
// Solc versions <= 0.7.1 are .zip files for Windows only | ||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))] | ||
if _artifact.ends_with(".zip") { | ||
return installer.install_zip(); | ||
} | ||
|
||
installer.install() | ||
} | ||
|
||
/// Creates the file and locks it exclusively, this will block if the file is currently locked | ||
fn try_lock_file(lock_path: PathBuf) -> Result<LockFile, SvmError> { | ||
use fs4::FileExt; | ||
let _lock_file = fs::OpenOptions::new() | ||
.create(true) | ||
.truncate(true) | ||
.read(true) | ||
.write(true) | ||
.open(&lock_path)?; | ||
_lock_file.lock_exclusive()?; | ||
Ok(LockFile { | ||
lock_path, | ||
_lock_file, | ||
}) | ||
} | ||
|
||
/// Represents a lockfile that's removed once dropped | ||
struct LockFile { | ||
_lock_file: fs::File, | ||
lock_path: PathBuf, | ||
} | ||
|
||
impl Drop for LockFile { | ||
fn drop(&mut self) { | ||
let _ = fs::remove_file(&self.lock_path); | ||
} | ||
} | ||
|
||
/// Returns the lockfile to use for a specific file | ||
fn lock_file_path(version: &Version) -> PathBuf { | ||
data_dir().join(format!(".lock-solc-{version}")) | ||
} | ||
|
||
// Installer type that copies binary data to the appropriate solc binary file: | ||
// 1. create target file to copy binary data | ||
// 2. copy data | ||
struct Installer<'a> { | ||
// version of solc | ||
version: &'a Version, | ||
// binary data of the solc executable | ||
binbytes: &'a [u8], | ||
} | ||
|
||
impl Installer<'_> { | ||
/// Installs the solc version at the version specific destination and returns the path to the installed solc file. | ||
fn install(self) -> Result<PathBuf, SvmError> { | ||
let solc_path = version_binary(&self.version.to_string()); | ||
|
||
let mut f = fs::File::create(&solc_path)?; | ||
#[cfg(target_family = "unix")] | ||
f.set_permissions(Permissions::from_mode(0o755))?; | ||
f.write_all(self.binbytes)?; | ||
|
||
if platform::is_nixos() && *self.version >= NIXOS_MIN_PATCH_VERSION { | ||
patch_for_nixos(&solc_path)?; | ||
} | ||
|
||
Ok(solc_path) | ||
} | ||
|
||
/// Extracts the solc archive at the version specified destination and returns the path to the | ||
/// installed solc binary. | ||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))] | ||
fn install_zip(self) -> Result<PathBuf, SvmError> { | ||
let solc_path = version_binary(&self.version.to_string()); | ||
let version_path = solc_path.parent().unwrap(); | ||
|
||
let mut content = std::io::Cursor::new(self.binbytes); | ||
let mut archive = zip::ZipArchive::new(&mut content)?; | ||
archive.extract(version_path)?; | ||
|
||
std::fs::rename(version_path.join("solc.exe"), &solc_path)?; | ||
|
||
Ok(solc_path) | ||
} | ||
} | ||
|
||
/// Patch the given binary to use the dynamic linker provided by nixos. | ||
fn patch_for_nixos(bin: &Path) -> Result<(), SvmError> { | ||
let output = Command::new("nix-shell") | ||
.arg("-p") | ||
.arg("patchelf") | ||
.arg("--run") | ||
.arg(format!( | ||
"patchelf --set-interpreter \"$(cat $NIX_CC/nix-support/dynamic-linker)\" {}", | ||
bin.display() | ||
)) | ||
.output() | ||
.expect("Failed to execute command"); | ||
|
||
match output.status.success() { | ||
true => Ok(()), | ||
false => Err(SvmError::CouldNotPatchForNixOs( | ||
String::from_utf8_lossy(&output.stdout).into_owned(), | ||
String::from_utf8_lossy(&output.stderr).into_owned(), | ||
)), | ||
} | ||
} | ||
|
||
fn ensure_checksum( | ||
binbytes: &[u8], | ||
version: &Version, | ||
expected_checksum: &[u8], | ||
) -> Result<(), SvmError> { | ||
let mut hasher = sha2::Sha256::new(); | ||
hasher.update(binbytes); | ||
let checksum = &hasher.finalize()[..]; | ||
// checksum does not match | ||
if checksum != expected_checksum { | ||
return Err(SvmError::ChecksumMismatch { | ||
version: version.to_string(), | ||
expected: hex::encode(expected_checksum), | ||
actual: hex::encode(checksum), | ||
}); | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::{ | ||
platform::Platform, | ||
releases::{all_releases, artifact_url}, | ||
}; | ||
use rand::seq::SliceRandom; | ||
|
||
const LATEST: Version = Version::new(0, 8, 24); | ||
|
||
#[tokio::test] | ||
async fn test_install() { | ||
let versions = all_releases(platform()) | ||
.await | ||
.unwrap() | ||
.releases | ||
.into_keys() | ||
.collect::<Vec<Version>>(); | ||
let rand_version = versions.choose(&mut rand::thread_rng()).unwrap(); | ||
assert!(install(rand_version).await.is_ok()); | ||
} | ||
|
||
#[cfg(feature = "blocking")] | ||
#[test] | ||
fn blocking_test_install() { | ||
let versions = crate::releases::blocking_all_releases(platform::platform()) | ||
.unwrap() | ||
.into_versions(); | ||
let rand_version = versions.choose(&mut rand::thread_rng()).unwrap(); | ||
assert!(blocking_install(rand_version).is_ok()); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_version() { | ||
let version = "0.8.10".parse().unwrap(); | ||
install(&version).await.unwrap(); | ||
let solc_path = version_binary(version.to_string().as_str()); | ||
let output = Command::new(solc_path).arg("--version").output().unwrap(); | ||
assert!(String::from_utf8_lossy(&output.stdout) | ||
.as_ref() | ||
.contains("0.8.10")); | ||
} | ||
|
||
#[cfg(feature = "blocking")] | ||
#[test] | ||
fn blocking_test_version() { | ||
let version = "0.8.10".parse().unwrap(); | ||
blocking_install(&version).unwrap(); | ||
let solc_path = version_binary(version.to_string().as_str()); | ||
let output = Command::new(solc_path).arg("--version").output().unwrap(); | ||
|
||
assert!(String::from_utf8_lossy(&output.stdout) | ||
.as_ref() | ||
.contains("0.8.10")); | ||
} | ||
|
||
#[cfg(feature = "blocking")] | ||
#[test] | ||
fn can_install_parallel() { | ||
let version: Version = "0.8.10".parse().unwrap(); | ||
let cloned_version = version.clone(); | ||
let t = std::thread::spawn(move || blocking_install(&cloned_version)); | ||
blocking_install(&version).unwrap(); | ||
t.join().unwrap().unwrap(); | ||
} | ||
|
||
#[tokio::test(flavor = "multi_thread")] | ||
async fn can_install_parallel_async() { | ||
let version: Version = "0.8.10".parse().unwrap(); | ||
let cloned_version = version.clone(); | ||
let t = tokio::task::spawn(async move { install(&cloned_version).await }); | ||
install(&version).await.unwrap(); | ||
t.await.unwrap().unwrap(); | ||
} | ||
|
||
// ensures we can download the latest native solc for apple silicon | ||
#[tokio::test(flavor = "multi_thread")] | ||
async fn can_download_latest_native_apple_silicon() { | ||
let artifacts = all_releases(Platform::MacOsAarch64).await.unwrap(); | ||
|
||
let artifact = artifacts.releases.get(&LATEST).unwrap(); | ||
let download_url = artifact_url( | ||
Platform::MacOsAarch64, | ||
&LATEST, | ||
artifact.to_string().as_str(), | ||
) | ||
.unwrap(); | ||
|
||
let expected_checksum = artifacts.get_checksum(&LATEST).unwrap(); | ||
|
||
let resp = reqwest::get(download_url).await.unwrap(); | ||
assert!(resp.status().is_success()); | ||
let binbytes = resp.bytes().await.unwrap(); | ||
ensure_checksum(&binbytes, &LATEST, &expected_checksum).unwrap(); | ||
} | ||
|
||
// ensures we can download the latest native solc for linux aarch64 | ||
#[tokio::test(flavor = "multi_thread")] | ||
#[cfg(all(target_os = "linux", target_arch = "aarch64"))] | ||
async fn can_download_latest_linux_aarch64() { | ||
let artifacts = all_releases(Platform::LinuxAarch64).await.unwrap(); | ||
|
||
let artifact = artifacts.releases.get(&LATEST).unwrap(); | ||
let download_url = artifact_url( | ||
Platform::LinuxAarch64, | ||
&LATEST, | ||
artifact.to_string().as_str(), | ||
) | ||
.unwrap(); | ||
|
||
let checksum = artifacts.get_checksum(&LATEST).unwrap(); | ||
|
||
let resp = reqwest::get(download_url).await.unwrap(); | ||
assert!(resp.status().is_success()); | ||
let binbytes = resp.bytes().await.unwrap(); | ||
ensure_checksum(&binbytes, &LATEST, checksum).unwrap(); | ||
} | ||
|
||
#[tokio::test] | ||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))] | ||
async fn can_install_windows_zip_release() { | ||
let version = "0.7.1".parse().unwrap(); | ||
install(&version).await.unwrap(); | ||
let solc_path = version_binary(version.to_string().as_str()); | ||
let output = Command::new(&solc_path).arg("--version").output().unwrap(); | ||
|
||
assert!(String::from_utf8_lossy(&output.stdout) | ||
.as_ref() | ||
.contains("0.7.1")); | ||
} | ||
} |
Oops, something went wrong.