1#![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
22pub struct VersionManager {
24 pub(crate) fs: Box<dyn FsPaths>,
25 releases: Releases,
26 offline: bool,
27}
28
29impl VersionManager {
30 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 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 pub fn is_installed(&self, resolc_version: &Version) -> bool {
105 self.fs.path().join(resolc_version.to_string()).exists()
106 }
107
108 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 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 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 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 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 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 #[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}