use once_cell::sync::Lazy;
use semver::{Version, VersionReq};
use sha2::Digest;
use std::{
ffi::OsString,
fs,
io::{Cursor, ErrorKind, Write},
path::PathBuf,
process::Command,
};
use std::time::Duration;
#[cfg(target_family = "unix")]
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
mod error;
pub use error::SolcVmError;
mod platform;
pub use platform::{platform, Platform};
mod releases;
pub use releases::{all_releases, Releases};
#[cfg(feature = "blocking")]
pub use releases::blocking_all_releases;
pub static SVM_DATA_DIR: Lazy<PathBuf> = Lazy::new(|| {
#[cfg(test)]
{
let dir = tempfile::tempdir().expect("could not create temp directory");
dir.path().join(".svm")
}
#[cfg(not(test))]
{
resolve_data_dir()
}
});
fn resolve_data_dir() -> PathBuf {
let home_dir = dirs::home_dir()
.expect("could not detect user home directory")
.join(".svm");
let data_dir = dirs::data_dir().expect("could not detect user data directory");
if !home_dir.as_path().exists() && data_dir.as_path().exists() {
data_dir.join("svm")
} else {
home_dir
}
}
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
static NIXOS_PATCH_REQ: Lazy<VersionReq> = Lazy::new(|| VersionReq::parse(">=0.7.6").unwrap());
struct Installer {
version: Version,
binbytes: Vec<u8>,
}
impl Installer {
fn install(&self) -> Result<PathBuf, SolcVmError> {
let version_path = version_path(self.version.to_string().as_str());
let solc_path = version_path.join(format!("solc-{}", self.version));
let mut f = fs::File::create(&solc_path)?;
#[cfg(target_family = "unix")]
f.set_permissions(Permissions::from_mode(0o777))?;
let mut content = Cursor::new(&self.binbytes);
std::io::copy(&mut content, &mut f)?;
if platform::is_nixos() && NIXOS_PATCH_REQ.matches(&self.version) {
patch_for_nixos(solc_path)
} else {
Ok(solc_path)
}
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
fn install_zip(&self) -> Result<PathBuf, SolcVmError> {
let version_path = version_path(self.version.to_string().as_str());
let solc_path = version_path.join(&format!("solc-{}", self.version));
let mut content = Cursor::new(&self.binbytes);
let mut archive = zip::ZipArchive::new(&mut content)?;
archive.extract(version_path.as_path())?;
std::fs::rename(version_path.join("solc.exe"), solc_path.as_path())?;
Ok(solc_path)
}
}
pub fn patch_for_nixos(bin: PathBuf) -> Result<PathBuf, SolcVmError> {
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(bin),
false => Err(SolcVmError::CouldNotPatchForNixOs(
String::from_utf8(output.stdout).expect("Found invalid UTF-8 when parsing stdout"),
String::from_utf8(output.stderr).expect("Found invalid UTF-8 when parsing stderr"),
)),
}
}
pub fn version_path(version: &str) -> PathBuf {
let mut version_path = SVM_DATA_DIR.to_path_buf();
version_path.push(version);
version_path
}
pub fn global_version_path() -> PathBuf {
let mut global_version_path = SVM_DATA_DIR.to_path_buf();
global_version_path.push(".global-version");
global_version_path
}
pub fn current_version() -> Result<Option<Version>, SolcVmError> {
let v = fs::read_to_string(global_version_path().as_path())?;
Ok(Version::parse(v.trim_end_matches('\n').to_string().as_str()).ok())
}
pub fn use_version(version: &Version) -> Result<(), SolcVmError> {
let mut v = fs::File::create(global_version_path().as_path())?;
v.write_all(version.to_string().as_bytes())?;
Ok(())
}
pub fn unset_global_version() -> Result<(), SolcVmError> {
let mut v = fs::File::create(global_version_path().as_path())?;
v.write_all("".as_bytes())?;
Ok(())
}
pub fn installed_versions() -> Result<Vec<Version>, SolcVmError> {
let home_dir = SVM_DATA_DIR.to_path_buf();
let mut versions = vec![];
for v in fs::read_dir(home_dir)? {
let v = v?;
if v.file_name() != OsString::from(".global-version".to_string()) {
versions.push(Version::parse(
v.path()
.file_name()
.ok_or(SolcVmError::UnknownVersion)?
.to_str()
.ok_or(SolcVmError::UnknownVersion)?
.to_string()
.as_str(),
)?);
}
}
versions.sort();
Ok(versions)
}
#[cfg(feature = "blocking")]
pub fn blocking_all_versions() -> Result<Vec<Version>, SolcVmError> {
Ok(releases::blocking_all_releases(platform::platform())?.into_versions())
}
pub async fn all_versions() -> Result<Vec<Version>, SolcVmError> {
Ok(releases::all_releases(platform::platform())
.await?
.into_versions())
}
#[cfg(feature = "blocking")]
pub fn blocking_install(version: &Version) -> Result<PathBuf, SolcVmError> {
setup_data_dir()?;
let artifacts = releases::blocking_all_releases(platform::platform())?;
let artifact = artifacts
.get_artifact(version)
.ok_or(SolcVmError::UnknownVersion)?;
let download_url =
releases::artifact_url(platform::platform(), version, artifact.to_string().as_str())?;
let 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(SolcVmError::UnsuccessfulResponse(
download_url,
res.status(),
));
}
let binbytes = res.bytes()?;
ensure_checksum(&binbytes, version, checksum)?;
let lock_path = lock_file_path(version);
let _lock = try_lock_file(lock_path)?;
do_install(
version.clone(),
binbytes.to_vec(),
artifact.to_string().as_str(),
)
}
pub async fn install(version: &Version) -> Result<PathBuf, SolcVmError> {
setup_data_dir()?;
let artifacts = releases::all_releases(platform::platform()).await?;
let artifact = artifacts
.releases
.get(version)
.ok_or(SolcVmError::UnknownVersion)?;
let download_url =
releases::artifact_url(platform::platform(), version, artifact.to_string().as_str())?;
let 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(SolcVmError::UnsuccessfulResponse(
download_url,
res.status(),
));
}
let binbytes = res.bytes().await?;
ensure_checksum(&binbytes, version, checksum)?;
let lock_path = lock_file_path(version);
let _lock = try_lock_file(lock_path)?;
do_install(
version.clone(),
binbytes.to_vec(),
artifact.to_string().as_str(),
)
}
fn do_install(
version: Version,
binbytes: Vec<u8>,
_artifact: &str,
) -> Result<PathBuf, SolcVmError> {
let installer = {
setup_version(version.to_string().as_str())?;
Installer { version, binbytes }
};
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
if _artifact.ends_with(".zip") {
return installer.install_zip();
}
installer.install()
}
pub fn remove_version(version: &Version) -> Result<(), SolcVmError> {
fs::remove_dir_all(version_path(version.to_string().as_str()))?;
Ok(())
}
pub fn setup_data_dir() -> Result<PathBuf, SolcVmError> {
let svm_dir = SVM_DATA_DIR.to_path_buf();
if !svm_dir.as_path().exists() {
fs::create_dir_all(svm_dir.clone()).or_else(|err| match err.kind() {
ErrorKind::AlreadyExists => Ok(()),
_ => Err(err),
})?;
}
if !svm_dir.as_path().is_dir() {
return Err(SolcVmError::IoError(std::io::Error::new(
ErrorKind::AlreadyExists,
"svm directory is not a directory",
)));
}
let mut global_version = SVM_DATA_DIR.to_path_buf();
global_version.push(".global-version");
if !global_version.as_path().exists() {
fs::File::create(global_version.as_path())?;
}
Ok(svm_dir)
}
fn setup_version(version: &str) -> Result<(), SolcVmError> {
let v = version_path(version);
if !v.exists() {
fs::create_dir_all(v.as_path())?
}
Ok(())
}
fn ensure_checksum(
binbytes: impl AsRef<[u8]>,
version: &Version,
expected_checksum: Vec<u8>,
) -> Result<(), SolcVmError> {
let mut hasher = sha2::Sha256::new();
hasher.update(binbytes);
let cs = &hasher.finalize()[..];
if cs != expected_checksum {
return Err(SolcVmError::ChecksumMismatch {
version: version.to_string(),
expected: hex::encode(&expected_checksum),
actual: hex::encode(cs),
});
}
Ok(())
}
fn try_lock_file(lock_path: PathBuf) -> Result<LockFile, SolcVmError> {
use fs2::FileExt;
let _lock_file = fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&lock_path)?;
_lock_file.lock_exclusive()?;
Ok(LockFile {
lock_path,
_lock_file,
})
}
struct LockFile {
_lock_file: fs::File,
lock_path: PathBuf,
}
impl Drop for LockFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.lock_path);
}
}
fn lock_file_path(version: &Version) -> PathBuf {
SVM_DATA_DIR.join(format!(".lock-solc-{version}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
platform::Platform,
releases::{all_releases, artifact_url},
};
use rand::seq::SliceRandom;
use reqwest::Url;
use std::process::{Command, Stdio};
const LATEST: Version = Version::new(0, 8, 24);
#[tokio::test]
async fn test_data_dir_resolution() {
let home_dir = dirs::home_dir().unwrap().join(".svm");
let data_dir = dirs::data_dir();
let resolved_dir = resolve_data_dir();
if home_dir.as_path().exists() || data_dir.is_none() {
assert_eq!(resolved_dir.as_path(), home_dir.as_path());
} else {
assert_eq!(resolved_dir.as_path(), data_dir.unwrap().join("svm"));
}
}
#[tokio::test]
async fn test_artifact_url() {
let version = Version::new(0, 5, 0);
let artifact = "solc-v0.5.0";
assert_eq!(
artifact_url(Platform::LinuxAarch64, &version, artifact).unwrap(),
Url::parse(&format!(
"https://github.com/nikitastupin/solc/raw/7687d6ce15553292adbb3e6c565eafea6e0caf85/linux/aarch64/{artifact}"
))
.unwrap(),
)
}
#[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_path(version.to_string().as_str()).join(format!("solc-{version}"));
let output = Command::new(solc_path)
.arg("--version")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.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_path(version.to_string().as_str()).join(format!("solc-{version}"));
let output = Command::new(solc_path)
.arg("--version")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.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();
}
#[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 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(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_path(version.to_string().as_str()).join(&format!("solc-{}", version));
let output = Command::new(&solc_path)
.arg("--version")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.output()
.unwrap();
assert!(String::from_utf8_lossy(&output.stdout)
.as_ref()
.contains("0.7.1"));
}
}