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(false)?;
64        let nightly_url = Platform::get()?.download_url(true)?;
65        Releases::new(url)
66            .and_then(|releases| Releases::new(nightly_url).map(|nightlies| (releases, nightlies)))
67            .map(|(mut releases, mut nightlies)| {
68                releases.merge(&mut nightlies);
69                releases
70            })
71    }
72
73    fn get_releases_offline(data: &impl FsPaths) -> Result<Releases, Error> {
74        let installed = data.installed_versions()?;
75        if installed.is_empty() {
76            return Err(Error::NoVersionsInstalled);
77        }
78        let releases = BTreeMap::from_iter(installed.iter().map(|data| {
79            (
80                data.version.clone(),
81                format!("{}+{}", data.name, data.long_version),
82            )
83        }));
84
85        let latest_release = installed
86            .iter()
87            .max_by(|a, b| a.version.cmp(&b.version))
88            .map(|x| &x.version)
89            .cloned()
90            .expect("Cant be empty");
91
92        Ok(Releases {
93            builds: installed,
94            releases,
95            latest_release,
96        })
97    }
98
99    /// checks whether the requested resolc binary version is installed already
100    ///
101    /// # Arguments
102    ///
103    /// * `resolc_version` - required Resolc version
104    pub fn is_installed(&self, resolc_version: &Version) -> bool {
105        self.fs.path().join(resolc_version.to_string()).exists()
106    }
107
108    /// Returns an already present Resolc binary
109    ///
110    /// # Arguments
111    ///
112    /// * `resolc_version` - required Resolc version
113    /// * `solc_version` - optional `solc` version requirement, passing this will also check the compatibility between the two compiler versions
114    pub fn get(
115        &self,
116        resolc_version: &Version,
117        solc_version: Option<Version>,
118    ) -> Result<Binary, Error> {
119        let releases = &self.releases;
120        let build = releases.get_build(resolc_version)?;
121
122        if let Some(solc_version) = solc_version {
123            build.check_solc_compat(&solc_version)?;
124        };
125
126        if self
127            .fs
128            .path()
129            .to_path_buf()
130            .join(resolc_version.to_string())
131            .join(&build.name)
132            .exists()
133        {
134            Ok(build.clone().into_local(self.fs.path()))
135        } else {
136            Err(Error::NotInstalled {
137                version: resolc_version.clone(),
138            })
139        }
140    }
141
142    /// Returns an already present binary or installs the requested Resolc version
143    ///
144    /// # Arguments
145    ///
146    /// * `resolc_version` - required Resolc version
147    /// * `solc_version` - optional `solc` version requirement, passing this will also check the compatibility between the two compiler versions
148    pub fn get_or_install(
149        &self,
150        resolc_version: &Version,
151        solc_version: Option<Version>,
152    ) -> Result<Binary, Error> {
153        match self.get(resolc_version, solc_version) {
154            bin @ Ok(_) => {
155                return bin;
156            }
157            err @ Err(Error::SolcVersionNotSupported { .. }) => return err,
158            _ => (),
159        }
160
161        if self.offline {
162            return Err(Error::CantInstallOffline);
163        }
164        let build = self.releases.get_build(resolc_version)?;
165
166        let binary = build.download_binary()?;
167
168        self.fs.install_version(build, &binary)?;
169
170        Ok(build.clone().into_local(self.fs.path()))
171    }
172
173    /// Uninstall the listed version if it exists in path
174    pub fn remove(&self, version: &Version) -> Result<(), Error> {
175        if !self
176            .fs
177            .path()
178            .to_path_buf()
179            .join(version.to_string())
180            .exists()
181        {
182            return Err(Error::NotInstalled {
183                version: version.clone(),
184            });
185        }
186
187        self.fs.remove_version(version)
188    }
189
190    /// Returns the version used by default
191    pub fn get_default(&self) -> Result<Binary, Error> {
192        let version = self.fs.get_default_version().map_err(|e| match e {
193            Error::IoError(_) => Error::DefaultVersionNotSet,
194            e => e,
195        })?;
196
197        self.get(&version, None)
198    }
199
200    /// Sets the default used version
201    pub fn set_default(&self, version: &Version) -> Result<(), Error> {
202        let _ = self.get(version, None)?;
203        self.fs.set_default_version(version)
204    }
205
206    /// Lists all installed and available Resolc versions
207    ///
208    /// # Arguments
209    ///
210    /// * `solc_version` - optional `solc` version requirement, passing this will only return compilers that support given `solc` version.
211    pub fn list_available(&self, solc_version: Option<Version>) -> Result<Vec<Binary>, Error> {
212        let releases = &self.releases;
213        let mut installed_versions = BTreeSet::new();
214
215        let installed: Result<Vec<Binary>, Error> = self
216            .fs
217            .installed_versions()?
218            .into_iter()
219            .filter_map(|build| {
220                if let Some(solc_version) = &solc_version {
221                    build.check_solc_compat(solc_version).ok()?;
222                    Some(build)
223                } else {
224                    Some(build)
225                }
226            })
227            .map(|x| {
228                installed_versions.insert(x.version.clone());
229                Ok::<releases::Binary, Error>(x.into_local(self.fs.path()))
230            })
231            .collect();
232
233        let mut available: Vec<Binary> = releases
234            .builds
235            .iter()
236            .filter(|build| !installed_versions.contains(&build.version))
237            .cloned()
238            .map(|build| build.into_remote())
239            .collect();
240        let mut installed = installed?;
241        installed.append(&mut available);
242        installed.sort_by(|a, b| Version::cmp(a.version(), b.version()));
243        Ok(installed)
244    }
245}
246
247#[cfg(test)]
248mod test {
249    use std::{
250        path::{Path, PathBuf},
251        process::{Command, Stdio},
252    };
253
254    use expect_test::expect;
255    use semver::Version;
256
257    use crate::{Binary, Error, FsPaths, VersionManager};
258
259    /// Temp directory storage
260    #[derive(Clone)]
261    pub struct TempDir {
262        path: PathBuf,
263    }
264
265    impl FsPaths for TempDir {
266        fn new() -> Result<Self, Error> {
267            use tempfile::tempdir;
268            let path = tempdir()?.into_path();
269
270            Ok(Self { path })
271        }
272
273        fn path(&self) -> &Path {
274            self.path.as_path()
275        }
276    }
277
278    pub fn get_version_for_path(path: &Path) -> String {
279        let mut cmd = Command::new(path);
280        cmd.arg("--version")
281            .stdin(Stdio::piped())
282            .stderr(Stdio::piped())
283            .stdout(Stdio::piped());
284        let output = cmd.output().expect("Should not fail");
285        assert!(output.status.success());
286        String::from_utf8(output.stdout).unwrap()
287    }
288
289    #[test]
290    fn install() {
291        let manager = VersionManager::new_in_temp();
292
293        if let Binary::Local { path, .. } = manager
294            .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
295            .expect("should be installed")
296        {
297            let version = get_version_for_path(&path);
298            let expected = expect![[r#"
299                Solidity frontend for the revive compiler version 0.1.0-dev.13+commit.ad33153.llvm-18.1.8
300            "#]];
301            expected.assert_eq(&version);
302        } else {
303            panic!()
304        }
305    }
306
307    #[test]
308    fn set_default_and_remove() {
309        let manager = VersionManager::new_in_temp();
310        let bin = manager
311            .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
312            .unwrap();
313
314        manager
315            .set_default(bin.version())
316            .expect("should be installed");
317
318        manager
319            .remove(bin.version())
320            .expect("removed default version");
321
322        expect!["Default version of Resolc is not set"].assert_eq(&format!(
323            "{}",
324            manager.get_default().expect_err("error should happen")
325        ));
326    }
327
328    #[test]
329    fn get_set_default() {
330        let manager = VersionManager::new_in_temp();
331        let bin = manager
332            .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
333            .unwrap();
334
335        manager
336            .set_default(bin.version())
337            .expect("should be installed");
338        if let Binary::Local { path, .. } = manager.get_default().expect("should be installed") {
339            let version = get_version_for_path(&path);
340            let expected = expect![[r#"
341                Solidity frontend for the revive compiler version 0.1.0-dev.13+commit.ad33153.llvm-18.1.8
342            "#]];
343            expected.assert_eq(&version);
344        } else {
345            panic!()
346        }
347    }
348
349    #[test]
350    fn list_available() {
351        let manager = VersionManager::new_in_temp();
352
353        let result = manager.list_available(None).unwrap();
354        let expected = expect![[r#"
355            [
356                Remote {
357                    version: "0.1.0-dev.13",
358                    solc_req: ">=0.8.0, <=0.8.29",
359                },
360                Remote {
361                    version: "0.1.0-dev.14",
362                    solc_req: ">=0.8.0, <=0.8.29",
363                },
364                Remote {
365                    version: "0.1.0-dev.15",
366                    solc_req: ">=0.8.0, <=0.8.30",
367                },
368                Remote {
369                    version: "0.1.0-dev.16",
370                    solc_req: ">=0.8.0, <=0.8.30",
371                },
372                Remote {
373                    version: "0.1.0",
374                    solc_req: ">=0.8.0, <=0.8.30",
375                },
376                Remote {
377                    version: "0.2.0",
378                    solc_req: ">=0.8.0, <=0.8.30",
379                },
380                Remote {
381                    version: "0.3.0-nightly.2025.7.8",
382                    solc_req: ">=0.8.0, <=0.8.30",
383                },
384                Remote {
385                    version: "0.3.0-nightly.2025.7.9",
386                    solc_req: ">=0.8.0, <=0.8.30",
387                },
388                Remote {
389                    version: "0.3.0-nightly.2025.7.15",
390                    solc_req: ">=0.8.0, <=0.8.30",
391                },
392                Remote {
393                    version: "0.3.0-nightly.2025.7.18",
394                    solc_req: ">=0.8.0, <=0.8.30",
395                },
396                Remote {
397                    version: "0.3.0-nightly.2025.7.23",
398                    solc_req: ">=0.8.0, <=0.8.30",
399                },
400                Remote {
401                    version: "0.3.0-nightly.2025.8.9",
402                    solc_req: ">=0.8.0, <=0.8.30",
403                },
404                Remote {
405                    version: "0.3.0-nightly.2025.8.10",
406                    solc_req: ">=0.8.0, <=0.8.30",
407                },
408                Remote {
409                    version: "0.3.0-nightly.2025.8.12",
410                    solc_req: ">=0.8.0, <=0.8.30",
411                },
412                Remote {
413                    version: "0.3.0-nightly.2025.8.13",
414                    solc_req: ">=0.8.0, <=0.8.30",
415                },
416                Remote {
417                    version: "0.3.0-nightly.2025.8.20",
418                    solc_req: ">=0.8.0, <=0.8.30",
419                },
420                Remote {
421                    version: "0.3.0-nightly.2025.9.3",
422                    solc_req: ">=0.8.0, <=0.8.30",
423                },
424                Remote {
425                    version: "0.3.0-nightly.2025.9.29",
426                    solc_req: ">=0.8.0, <=0.8.30",
427                },
428                Remote {
429                    version: "0.3.0-nightly.2025.9.30",
430                    solc_req: ">=0.8.0, <=0.8.30",
431                },
432                Remote {
433                    version: "0.3.0",
434                    solc_req: ">=0.8.0, <=0.8.30",
435                },
436                Remote {
437                    version: "0.4.0",
438                    solc_req: ">=0.8.0, <=0.8.30",
439                },
440            ]"#]];
441
442        expected.assert_eq(&format!("{result:#?}"));
443        manager
444            .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
445            .unwrap();
446        manager
447            .set_default(&Version::parse("0.1.0-dev.13").unwrap())
448            .expect("should be installed");
449
450        let mut result = manager.list_available(None).unwrap();
451
452        for bin in result.iter_mut() {
453            if let Binary::Local { path, .. } = bin {
454                *path = PathBuf::new();
455            }
456        }
457
458        let expected = expect![[r#"
459            [
460                Installed {
461                    path: "",
462                    version: "0.1.0-dev.13",
463                    solc_req: ">=0.8.0, <=0.8.29",
464                },
465                Remote {
466                    version: "0.1.0-dev.14",
467                    solc_req: ">=0.8.0, <=0.8.29",
468                },
469                Remote {
470                    version: "0.1.0-dev.15",
471                    solc_req: ">=0.8.0, <=0.8.30",
472                },
473                Remote {
474                    version: "0.1.0-dev.16",
475                    solc_req: ">=0.8.0, <=0.8.30",
476                },
477                Remote {
478                    version: "0.1.0",
479                    solc_req: ">=0.8.0, <=0.8.30",
480                },
481                Remote {
482                    version: "0.2.0",
483                    solc_req: ">=0.8.0, <=0.8.30",
484                },
485                Remote {
486                    version: "0.3.0-nightly.2025.7.8",
487                    solc_req: ">=0.8.0, <=0.8.30",
488                },
489                Remote {
490                    version: "0.3.0-nightly.2025.7.9",
491                    solc_req: ">=0.8.0, <=0.8.30",
492                },
493                Remote {
494                    version: "0.3.0-nightly.2025.7.15",
495                    solc_req: ">=0.8.0, <=0.8.30",
496                },
497                Remote {
498                    version: "0.3.0-nightly.2025.7.18",
499                    solc_req: ">=0.8.0, <=0.8.30",
500                },
501                Remote {
502                    version: "0.3.0-nightly.2025.7.23",
503                    solc_req: ">=0.8.0, <=0.8.30",
504                },
505                Remote {
506                    version: "0.3.0-nightly.2025.8.9",
507                    solc_req: ">=0.8.0, <=0.8.30",
508                },
509                Remote {
510                    version: "0.3.0-nightly.2025.8.10",
511                    solc_req: ">=0.8.0, <=0.8.30",
512                },
513                Remote {
514                    version: "0.3.0-nightly.2025.8.12",
515                    solc_req: ">=0.8.0, <=0.8.30",
516                },
517                Remote {
518                    version: "0.3.0-nightly.2025.8.13",
519                    solc_req: ">=0.8.0, <=0.8.30",
520                },
521                Remote {
522                    version: "0.3.0-nightly.2025.8.20",
523                    solc_req: ">=0.8.0, <=0.8.30",
524                },
525                Remote {
526                    version: "0.3.0-nightly.2025.9.3",
527                    solc_req: ">=0.8.0, <=0.8.30",
528                },
529                Remote {
530                    version: "0.3.0-nightly.2025.9.29",
531                    solc_req: ">=0.8.0, <=0.8.30",
532                },
533                Remote {
534                    version: "0.3.0-nightly.2025.9.30",
535                    solc_req: ">=0.8.0, <=0.8.30",
536                },
537                Remote {
538                    version: "0.3.0",
539                    solc_req: ">=0.8.0, <=0.8.30",
540                },
541                Remote {
542                    version: "0.4.0",
543                    solc_req: ">=0.8.0, <=0.8.30",
544                },
545            ]"#]];
546
547        expected.assert_eq(&format!("{result:#?}"));
548    }
549
550    #[test]
551    fn bad_solc_version() {
552        let manager = VersionManager::new_in_temp();
553        manager
554            .get_or_install(&semver::Version::parse("0.1.0-dev.13").unwrap(), None)
555            .unwrap();
556        let result = manager.get_or_install(
557            &semver::Version::parse("0.1.0-dev.13").unwrap(),
558            Some(semver::Version::parse("0.4.14").unwrap()),
559        );
560        let expected = expect![[r#"
561            Err(
562                SolcVersionNotSupported {
563                    solc_version: Version {
564                        major: 0,
565                        minor: 4,
566                        patch: 14,
567                    },
568                    resolc_version: Version {
569                        major: 0,
570                        minor: 1,
571                        patch: 0,
572                        pre: Prerelease("dev.13"),
573                    },
574                    supported_range: VersionReq {
575                        comparators: [
576                            Comparator {
577                                op: GreaterEq,
578                                major: 0,
579                                minor: Some(
580                                    8,
581                                ),
582                                patch: Some(
583                                    0,
584                                ),
585                                pre: Prerelease(""),
586                            },
587                            Comparator {
588                                op: LessEq,
589                                major: 0,
590                                minor: Some(
591                                    8,
592                                ),
593                                patch: Some(
594                                    29,
595                                ),
596                                pre: Prerelease(""),
597                            },
598                        ],
599                    },
600                },
601            )"#]];
602
603        expected.assert_eq(&format!("{result:#?}"));
604    }
605
606    #[test]
607    fn concurrent() {
608        let temp_dir = TempDir::new().unwrap();
609        let temp_dir2 = temp_dir.clone();
610        let thread_1 = std::thread::spawn(move || {
611            let manager = VersionManager {
612                offline: false,
613                fs: Box::new(temp_dir),
614                releases: VersionManager::get_releases().expect("no network"),
615            };
616            manager
617                .get_or_install(&semver::Version::parse("0.1.0-dev.13").unwrap(), None)
618                .unwrap()
619        });
620        let thread_2 = std::thread::spawn(move || {
621            let manager2 = VersionManager {
622                offline: false,
623                fs: Box::new(temp_dir2),
624                releases: VersionManager::get_releases().expect("no network"),
625            };
626            manager2
627                .get_or_install(&semver::Version::parse("0.1.0-dev.13").unwrap(), None)
628                .unwrap()
629        });
630        thread_1.join().unwrap();
631        thread_2.join().unwrap();
632    }
633}