#![cfg_attr(
not(any(test, feature = "cli", feature = "resolc")),
warn(unused_crate_dependencies)
)]
use constants::Platform;
use fs::FsPaths;
use semver::Version;
use std::collections::{BTreeMap, BTreeSet};
mod constants;
mod errors;
mod fs;
mod releases;
pub use errors::Error;
pub use releases::{Binary, BinaryInfo};
use releases::{Build, Releases};
pub struct VersionManager {
pub(crate) fs: Box<dyn FsPaths>,
releases: Releases,
offline: bool,
}
impl VersionManager {
pub fn new(offline: bool) -> Result<Self, Error> {
let fspaths = fs::DataDir::new()?;
let releases = if offline {
Self::get_releases_offline(&fspaths)?
} else {
Self::get_releases()?
};
Ok(Self {
offline,
fs: Box::new(fspaths),
releases,
})
}
#[cfg(test)]
pub fn new_in_temp() -> Self {
use test::TempDir;
let releases = Self::get_releases().expect("no network");
VersionManager {
offline: false,
fs: Box::new(TempDir::new().unwrap()),
releases,
}
}
fn get_releases() -> Result<Releases, Error> {
let url = Platform::get()?.download_url(false)?;
let nightly_url = Platform::get()?.download_url(true)?;
Releases::new(url)
.and_then(|releases| Releases::new(nightly_url).map(|nightlies| (releases, nightlies)))
.map(|(mut releases, mut nightlies)| {
releases.merge(&mut nightlies);
releases
})
}
fn get_releases_offline(data: &impl FsPaths) -> Result<Releases, Error> {
let installed = data.installed_versions()?;
if installed.is_empty() {
return Err(Error::NoVersionsInstalled);
}
let releases = BTreeMap::from_iter(installed.iter().map(|data| {
(
data.version.clone(),
format!("{}+{}", data.name, data.long_version),
)
}));
let latest_release = installed
.iter()
.max_by(|a, b| a.version.cmp(&b.version))
.map(|x| &x.version)
.cloned()
.expect("Cant be empty");
Ok(Releases {
builds: installed,
releases,
latest_release,
})
}
pub fn is_installed(&self, resolc_version: &Version) -> bool {
self.fs.path().join(resolc_version.to_string()).exists()
}
pub fn get(
&self,
resolc_version: &Version,
solc_version: Option<Version>,
) -> Result<Binary, Error> {
let releases = &self.releases;
let build = releases.get_build(resolc_version)?;
if let Some(solc_version) = solc_version {
build.check_solc_compat(&solc_version)?;
};
if self
.fs
.path()
.to_path_buf()
.join(resolc_version.to_string())
.join(&build.name)
.exists()
{
Ok(build.clone().into_local(self.fs.path()))
} else {
Err(Error::NotInstalled {
version: resolc_version.clone(),
})
}
}
pub fn get_or_install(
&self,
resolc_version: &Version,
solc_version: Option<Version>,
) -> Result<Binary, Error> {
match self.get(resolc_version, solc_version) {
bin @ Ok(_) => {
return bin;
}
err @ Err(Error::SolcVersionNotSupported { .. }) => return err,
_ => (),
}
if self.offline {
return Err(Error::CantInstallOffline);
}
let build = self.releases.get_build(resolc_version)?;
let binary = build.download_binary()?;
self.fs.install_version(build, &binary)?;
Ok(build.clone().into_local(self.fs.path()))
}
pub fn remove(&self, version: &Version) -> Result<(), Error> {
if !self
.fs
.path()
.to_path_buf()
.join(version.to_string())
.exists()
{
return Err(Error::NotInstalled {
version: version.clone(),
});
}
self.fs.remove_version(version)
}
pub fn get_default(&self) -> Result<Binary, Error> {
let version = self.fs.get_default_version().map_err(|e| match e {
Error::IoError(_) => Error::DefaultVersionNotSet,
e => e,
})?;
self.get(&version, None)
}
pub fn set_default(&self, version: &Version) -> Result<(), Error> {
let _ = self.get(version, None)?;
self.fs.set_default_version(version)
}
pub fn list_available(&self, solc_version: Option<Version>) -> Result<Vec<Binary>, Error> {
let releases = &self.releases;
let mut installed_versions = BTreeSet::new();
let installed: Result<Vec<Binary>, Error> = self
.fs
.installed_versions()?
.into_iter()
.filter_map(|build| {
if let Some(solc_version) = &solc_version {
build.check_solc_compat(solc_version).ok()?;
Some(build)
} else {
Some(build)
}
})
.map(|x| {
installed_versions.insert(x.version.clone());
Ok::<releases::Binary, Error>(x.into_local(self.fs.path()))
})
.collect();
let mut available: Vec<Binary> = releases
.builds
.iter()
.filter(|build| !installed_versions.contains(&build.version))
.cloned()
.map(|build| build.into_remote())
.collect();
let mut installed = installed?;
installed.append(&mut available);
installed.sort_by(|a, b| Version::cmp(a.version(), b.version()));
Ok(installed)
}
}
#[cfg(test)]
mod test {
use std::{
path::{Path, PathBuf},
process::{Command, Stdio},
};
use expect_test::expect;
use semver::Version;
use crate::{Binary, Error, FsPaths, VersionManager};
#[derive(Clone)]
pub struct TempDir {
path: PathBuf,
}
impl FsPaths for TempDir {
fn new() -> Result<Self, Error> {
use tempfile::tempdir;
let path = tempdir()?.into_path();
Ok(Self { path })
}
fn path(&self) -> &Path {
self.path.as_path()
}
}
pub fn get_version_for_path(path: &Path) -> String {
let mut cmd = Command::new(path);
cmd.arg("--version")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped());
let output = cmd.output().expect("Should not fail");
assert!(output.status.success());
String::from_utf8(output.stdout).unwrap()
}
#[test]
fn install() {
let manager = VersionManager::new_in_temp();
if let Binary::Local { path, .. } = manager
.get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
.expect("should be installed")
{
let version = get_version_for_path(&path);
let expected = expect![[r#"
Solidity frontend for the revive compiler version 0.1.0-dev.13+commit.ad33153.llvm-18.1.8
"#]];
expected.assert_eq(&version);
} else {
panic!()
}
}
#[test]
fn set_default_and_remove() {
let manager = VersionManager::new_in_temp();
let bin = manager
.get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
.unwrap();
manager
.set_default(bin.version())
.expect("should be installed");
manager
.remove(bin.version())
.expect("removed default version");
expect!["Default version of Resolc is not set"].assert_eq(&format!(
"{}",
manager.get_default().expect_err("error should happen")
));
}
#[test]
fn get_set_default() {
let manager = VersionManager::new_in_temp();
let bin = manager
.get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
.unwrap();
manager
.set_default(bin.version())
.expect("should be installed");
if let Binary::Local { path, .. } = manager.get_default().expect("should be installed") {
let version = get_version_for_path(&path);
let expected = expect![[r#"
Solidity frontend for the revive compiler version 0.1.0-dev.13+commit.ad33153.llvm-18.1.8
"#]];
expected.assert_eq(&version);
} else {
panic!()
}
}
#[test]
fn list_available() {
let manager = VersionManager::new_in_temp();
let result = manager.list_available(None).unwrap();
let expected = expect![[r#"
[
Remote {
version: "0.1.0-dev.13",
solc_req: ">=0.8.0, <=0.8.29",
},
Remote {
version: "0.1.0-dev.14",
solc_req: ">=0.8.0, <=0.8.29",
},
Remote {
version: "0.1.0-dev.15",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.1.0-dev.16",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.1.0",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.2.0",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.8",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.9",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.15",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.18",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.23",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.9",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.10",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.12",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.13",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.20",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.9.3",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.9.29",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.9.30",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.4.0",
solc_req: ">=0.8.0, <=0.8.30",
},
]"#]];
expected.assert_eq(&format!("{result:#?}"));
manager
.get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
.unwrap();
manager
.set_default(&Version::parse("0.1.0-dev.13").unwrap())
.expect("should be installed");
let mut result = manager.list_available(None).unwrap();
for bin in result.iter_mut() {
if let Binary::Local { path, .. } = bin {
*path = PathBuf::new();
}
}
let expected = expect![[r#"
[
Installed {
path: "",
version: "0.1.0-dev.13",
solc_req: ">=0.8.0, <=0.8.29",
},
Remote {
version: "0.1.0-dev.14",
solc_req: ">=0.8.0, <=0.8.29",
},
Remote {
version: "0.1.0-dev.15",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.1.0-dev.16",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.1.0",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.2.0",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.8",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.9",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.15",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.18",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.7.23",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.9",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.10",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.12",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.13",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.8.20",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.9.3",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.9.29",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0-nightly.2025.9.30",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.3.0",
solc_req: ">=0.8.0, <=0.8.30",
},
Remote {
version: "0.4.0",
solc_req: ">=0.8.0, <=0.8.30",
},
]"#]];
expected.assert_eq(&format!("{result:#?}"));
}
#[test]
fn bad_solc_version() {
let manager = VersionManager::new_in_temp();
manager
.get_or_install(&semver::Version::parse("0.1.0-dev.13").unwrap(), None)
.unwrap();
let result = manager.get_or_install(
&semver::Version::parse("0.1.0-dev.13").unwrap(),
Some(semver::Version::parse("0.4.14").unwrap()),
);
let expected = expect![[r#"
Err(
SolcVersionNotSupported {
solc_version: Version {
major: 0,
minor: 4,
patch: 14,
},
resolc_version: Version {
major: 0,
minor: 1,
patch: 0,
pre: Prerelease("dev.13"),
},
supported_range: VersionReq {
comparators: [
Comparator {
op: GreaterEq,
major: 0,
minor: Some(
8,
),
patch: Some(
0,
),
pre: Prerelease(""),
},
Comparator {
op: LessEq,
major: 0,
minor: Some(
8,
),
patch: Some(
29,
),
pre: Prerelease(""),
},
],
},
},
)"#]];
expected.assert_eq(&format!("{result:#?}"));
}
#[test]
fn concurrent() {
let temp_dir = TempDir::new().unwrap();
let temp_dir2 = temp_dir.clone();
let thread_1 = std::thread::spawn(move || {
let manager = VersionManager {
offline: false,
fs: Box::new(temp_dir),
releases: VersionManager::get_releases().expect("no network"),
};
manager
.get_or_install(&semver::Version::parse("0.1.0-dev.13").unwrap(), None)
.unwrap()
});
let thread_2 = std::thread::spawn(move || {
let manager2 = VersionManager {
offline: false,
fs: Box::new(temp_dir2),
releases: VersionManager::get_releases().expect("no network"),
};
manager2
.get_or_install(&semver::Version::parse("0.1.0-dev.13").unwrap(), None)
.unwrap()
});
thread_1.join().unwrap();
thread_2.join().unwrap();
}
}