rvm/
lib.rs

1//! # Revive Version Manager library code
2//!
3//! Used by the `rvm` binary, but can be used separately in libraries and applications
4#![cfg_attr(
5    not(any(test, feature = "cli", feature = "resolc")),
6    warn(unused_crate_dependencies)
7)]
8
9use constants::Platform;
10use fs::FsPaths;
11use semver::Version;
12use std::collections::{BTreeMap, BTreeSet};
13
14mod constants;
15mod errors;
16mod fs;
17mod releases;
18pub use errors::Error;
19pub use releases::{Binary, BinaryInfo};
20use releases::{Build, Releases};
21
22/// Version manager responsible for handling Resolc installation.
23pub struct VersionManager {
24    pub(crate) fs: Box<dyn FsPaths>,
25    releases: Releases,
26    offline: bool,
27}
28
29impl VersionManager {
30    /// Instantiate the version manager
31    ///
32    /// # Arguments
33    ///
34    /// * `offline` - run in offline mode.
35    pub fn new(offline: bool) -> Result<Self, Error> {
36        let fspaths = fs::DataDir::new()?;
37        let releases = if offline {
38            Self::get_releases_offline(&fspaths)?
39        } else {
40            Self::get_releases()?
41        };
42        Ok(Self {
43            offline,
44            fs: Box::new(fspaths),
45            releases,
46        })
47    }
48
49    #[cfg(test)]
50    /// For use in tests
51    pub fn new_in_temp() -> Self {
52        use test::TempDir;
53        let releases = Self::get_releases().expect("no network");
54
55        VersionManager {
56            offline: false,
57            fs: Box::new(TempDir::new().unwrap()),
58            releases,
59        }
60    }
61
62    fn get_releases() -> Result<Releases, Error> {
63        let url = Platform::get()?.download_url()?;
64        Releases::new(url)
65    }
66
67    fn get_releases_offline(data: &impl FsPaths) -> Result<Releases, Error> {
68        let installed = data.installed_versions()?;
69        if installed.is_empty() {
70            return Err(Error::NoVersionsInstalled);
71        }
72        let releases = BTreeMap::from_iter(installed.iter().map(|data| {
73            (
74                data.version.clone(),
75                format!("{}+{}", data.name, data.long_version),
76            )
77        }));
78
79        let latest_release = installed
80            .iter()
81            .max_by(|a, b| a.version.cmp(&b.version))
82            .map(|x| &x.version)
83            .cloned()
84            .expect("Cant be empty");
85
86        Ok(Releases {
87            builds: installed,
88            releases,
89            latest_release,
90        })
91    }
92
93    /// checks whether the requested resolc binary version is installed already
94    ///
95    /// # Arguments
96    ///
97    /// * `resolc_version` - required Resolc version
98    pub fn is_installed(&self, resolc_version: &Version) -> bool {
99        self.fs.path().join(resolc_version.to_string()).exists()
100    }
101
102    /// Returns an already present Resolc binary
103    ///
104    /// # Arguments
105    ///
106    /// * `resolc_version` - required Resolc version
107    /// * `solc_version` - optional `solc` version requirement, passing this will also check the compatibility between the two compiler versions
108    pub fn get(
109        &self,
110        resolc_version: &Version,
111        solc_version: Option<Version>,
112    ) -> Result<Binary, Error> {
113        let releases = &self.releases;
114        let build = releases.get_build(resolc_version)?;
115
116        if let Some(solc_version) = solc_version {
117            build.check_solc_compat(&solc_version)?;
118        };
119
120        if self
121            .fs
122            .path()
123            .to_path_buf()
124            .join(resolc_version.to_string())
125            .join(&build.name)
126            .exists()
127        {
128            Ok(build.clone().into_local(self.fs.path()))
129        } else {
130            Err(Error::NotInstalled {
131                version: resolc_version.clone(),
132            })
133        }
134    }
135
136    /// Returns an already present binary or installs the requested Resolc version
137    ///
138    /// # Arguments
139    ///
140    /// * `resolc_version` - required Resolc version
141    /// * `solc_version` - optional `solc` version requirement, passing this will also check the compatibility between the two compiler versions
142    pub fn get_or_install(
143        &self,
144        resolc_version: &Version,
145        solc_version: Option<Version>,
146    ) -> Result<Binary, Error> {
147        if let bin @ Ok(_) = self.get(resolc_version, solc_version) {
148            return bin;
149        }
150
151        if self.offline {
152            return Err(Error::CantInstallOffline);
153        }
154
155        let build = self.releases.get_build(resolc_version)?;
156
157        let binary = build.download_binary()?;
158
159        self.fs.install_version(build, &binary)?;
160
161        Ok(build.clone().into_local(self.fs.path()))
162    }
163
164    /// Uninstall the listed version if it exists in path
165    pub fn remove(&self, version: &Version) -> Result<(), Error> {
166        if !self
167            .fs
168            .path()
169            .to_path_buf()
170            .join(version.to_string())
171            .exists()
172        {
173            return Err(Error::NotInstalled {
174                version: version.clone(),
175            });
176        }
177
178        self.fs.remove_version(version)
179    }
180
181    /// Returns the version used by default
182    pub fn get_default(&self) -> Result<Binary, Error> {
183        let version = self.fs.get_default_version().map_err(|e| match e {
184            Error::IoError(_) => Error::DefaultVersionNotSet,
185            e => e,
186        })?;
187
188        self.get(&version, None)
189    }
190
191    /// Sets the default used version
192    pub fn set_default(&self, version: &Version) -> Result<(), Error> {
193        let _ = self.get(version, None)?;
194        self.fs.set_default_version(version)
195    }
196
197    /// Lists all installed and available Resolc versions
198    ///
199    /// # Arguments
200    ///
201    /// * `solc_version` - optional `solc` version requirement, passing this will only return compilers that support given `solc` version.
202    pub fn list_available(&self, solc_version: Option<Version>) -> Result<Vec<Binary>, Error> {
203        let releases = &self.releases;
204        let mut installed_versions = BTreeSet::new();
205
206        let installed: Result<Vec<Binary>, Error> = self
207            .fs
208            .installed_versions()?
209            .into_iter()
210            .filter_map(|build| {
211                if let Some(solc_version) = &solc_version {
212                    build.check_solc_compat(solc_version).ok()?;
213                    Some(build)
214                } else {
215                    Some(build)
216                }
217            })
218            .map(|x| {
219                installed_versions.insert(x.version.clone());
220                Ok::<releases::Binary, Error>(x.into_local(self.fs.path()))
221            })
222            .collect();
223
224        let mut available: Vec<Binary> = releases
225            .builds
226            .iter()
227            .filter(|build| !installed_versions.contains(&build.version))
228            .cloned()
229            .map(|build| build.into_remote())
230            .collect();
231        let mut installed = installed?;
232        installed.append(&mut available);
233        installed.sort();
234        Ok(installed)
235    }
236}
237
238#[cfg(test)]
239mod test {
240    use std::{
241        path::{Path, PathBuf},
242        process::{Command, Stdio},
243    };
244
245    use expect_test::expect;
246    use semver::Version;
247
248    use crate::{Binary, Error, FsPaths, VersionManager};
249
250    /// Temp directory storage
251    #[derive(Clone)]
252    pub struct TempDir {
253        path: PathBuf,
254    }
255
256    impl FsPaths for TempDir {
257        fn new() -> Result<Self, Error> {
258            use tempfile::tempdir;
259            let path = tempdir()?.into_path();
260
261            Ok(Self { path })
262        }
263
264        fn path(&self) -> &Path {
265            self.path.as_path()
266        }
267    }
268
269    pub fn get_version_for_path(path: &Path) -> String {
270        let mut cmd = Command::new(path);
271        cmd.arg("--version")
272            .stdin(Stdio::piped())
273            .stderr(Stdio::piped())
274            .stdout(Stdio::piped());
275        let output = cmd.output().expect("Should not fail");
276        assert!(output.status.success());
277        String::from_utf8(output.stdout).unwrap()
278    }
279
280    #[test]
281    fn install() {
282        let manager = VersionManager::new_in_temp();
283
284        if let Binary::Local { path, .. } = manager
285            .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
286            .expect("should be installed")
287        {
288            let version = get_version_for_path(&path);
289            let expected = expect![[r#"
290                Solidity frontend for the revive compiler version 0.1.0-dev.13+commit.ad33153.llvm-18.1.8
291            "#]];
292            expected.assert_eq(&version);
293        } else {
294            panic!()
295        }
296    }
297
298    #[test]
299    fn set_default_and_remove() {
300        let manager = VersionManager::new_in_temp();
301        let bin = manager
302            .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
303            .unwrap();
304
305        manager
306            .set_default(bin.version())
307            .expect("should be installed");
308
309        manager
310            .remove(bin.version())
311            .expect("removed default version");
312
313        expect!["Default version of Resolc is not set"].assert_eq(&format!(
314            "{}",
315            manager.get_default().expect_err("error should happen")
316        ));
317    }
318
319    #[test]
320    fn get_set_default() {
321        let manager = VersionManager::new_in_temp();
322        let bin = manager
323            .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
324            .unwrap();
325
326        manager
327            .set_default(bin.version())
328            .expect("should be installed");
329        if let Binary::Local { path, .. } = manager.get_default().expect("should be installed") {
330            let version = get_version_for_path(&path);
331            let expected = expect![[r#"
332                Solidity frontend for the revive compiler version 0.1.0-dev.13+commit.ad33153.llvm-18.1.8
333            "#]];
334            expected.assert_eq(&version);
335        } else {
336            panic!()
337        }
338    }
339
340    #[test]
341    fn list_available() {
342        let manager = VersionManager::new_in_temp();
343
344        let result = manager.list_available(None).unwrap();
345        let expected = expect![[r#"
346            [
347                Remote {
348                    version: "0.1.0-dev.13",
349                    solc_req: ">=0.8.0, <=0.8.29",
350                },
351            ]"#]];
352
353        expected.assert_eq(&format!("{result:#?}"));
354        manager
355            .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
356            .unwrap();
357        manager
358            .set_default(&Version::parse("0.1.0-dev.13").unwrap())
359            .expect("should be installed");
360
361        let mut result = manager.list_available(None).unwrap();
362
363        for bin in result.iter_mut() {
364            if let Binary::Local { path, .. } = bin {
365                *path = PathBuf::new();
366            }
367        }
368
369        let expected = expect![[r#"
370            [
371                Installed {
372                    path: "",
373                    version: "0.1.0-dev.13",
374                    solc_req: ">=0.8.0, <=0.8.29",
375                },
376            ]"#]];
377
378        expected.assert_eq(&format!("{result:#?}"));
379    }
380}