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()?;
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 pub fn is_installed(&self, resolc_version: &Version) -> bool {
99 self.fs.path().join(resolc_version.to_string()).exists()
100 }
101
102 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 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 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 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 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 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 #[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 Remote {
352 version: "0.1.0-dev.14",
353 solc_req: ">=0.8.0, <=0.8.29",
354 },
355 ]"#]];
356
357 expected.assert_eq(&format!("{result:#?}"));
358 manager
359 .get_or_install(&Version::parse("0.1.0-dev.13").unwrap(), None)
360 .unwrap();
361 manager
362 .set_default(&Version::parse("0.1.0-dev.13").unwrap())
363 .expect("should be installed");
364
365 let mut result = manager.list_available(None).unwrap();
366
367 for bin in result.iter_mut() {
368 if let Binary::Local { path, .. } = bin {
369 *path = PathBuf::new();
370 }
371 }
372
373 let expected = expect![[r#"
374 [
375 Installed {
376 path: "",
377 version: "0.1.0-dev.13",
378 solc_req: ">=0.8.0, <=0.8.29",
379 },
380 Remote {
381 version: "0.1.0-dev.14",
382 solc_req: ">=0.8.0, <=0.8.29",
383 },
384 ]"#]];
385
386 expected.assert_eq(&format!("{result:#?}"));
387 }
388}