libver/
lib.rs

1//! > *Basic runtime version management library*
2//!
3//! This is the documentation for `libver`, which is the main backbone for
4//! [`verune`](https://codeberg.org/r6915ee/verune).
5//!
6//! Please note that this documentation is fairly empty and does not contain a
7//! lot of information. To see how each method is used, please see the `verune`
8//! source code.
9
10use serde::{Deserialize, Serialize};
11use std::{
12    collections::{HashMap, VecDeque},
13    env::{self, VarError, home_dir},
14    fs::read_to_string,
15    io::{Error, ErrorKind, Result as IoResult},
16    path::PathBuf,
17    process::Command,
18};
19
20/// Basic metadata representation for a [Runtime].
21///
22/// [Runtime]s will typically be composed of this struct in order to allow
23/// dependent crates to access additional data about a [Runtime], as well as
24/// access the search paths.
25#[derive(PartialEq, Default, Eq, Hash, Deserialize, Serialize)]
26pub struct RuntimeMetadata {
27    pub display_name: String,
28    pub search_paths: Vec<String>,
29}
30
31/// Basic I/O layer for a runtime.
32///
33/// This structure both contains data about a runtime and keeps track of its
34/// metadata. It provides various I/O operations on runtimes in a consistent
35/// manner.
36#[derive(PartialEq, Eq, Hash)]
37pub struct Runtime {
38    pub name: String,
39    pub metadata: RuntimeMetadata,
40}
41
42impl Runtime {
43    /// Creates a [Runtime] with default metadata options.
44    pub fn unsafe_new(name: String) -> Runtime {
45        Runtime {
46            name,
47            metadata: RuntimeMetadata::default(),
48        }
49    }
50
51    /// Attempts to create a [Runtime].
52    ///
53    /// Note that this method can fail in the following cases:
54    ///
55    /// - The home directory is inaccessible
56    /// - The metadata file can't be read
57    /// - The metadata file can't be deserialized
58    pub fn new(name: String) -> IoResult<Runtime> {
59        let mut buf: PathBuf = Runtime::get_runtime(&name)?;
60        buf.push("meta.ron");
61        let data: String = read_to_string(buf)?;
62        match ron::from_str::<RuntimeMetadata>(data.as_str()) {
63            Ok(metadata) => Ok(Runtime { name, metadata }),
64            Err(_) => Err(Error::new(
65                ErrorKind::InvalidData,
66                format!(
67                    "Metadata file for runtime \"{}\" is not valid runtime metadata",
68                    name
69                ),
70            )),
71        }
72    }
73
74    /// Gets the root directory for a [Runtime], and returns it if successful.
75    pub fn get_root() -> IoResult<PathBuf> {
76        if let Some(mut home) = home_dir() {
77            home.push(".ver");
78            Ok(home)
79        } else {
80            Err(Error::new(
81                ErrorKind::NotFound,
82                "Could not access home directory",
83            ))
84        }
85    }
86
87    /// Gets a [Runtime] directory, based on its name.
88    pub fn get_runtime(name: &str) -> IoResult<PathBuf> {
89        let mut buf: PathBuf = Runtime::get_root()?;
90        buf.push(name);
91        Ok(buf)
92    }
93
94    /// Gets a version directory from the current [Runtime].
95    pub fn get_version(&self, version: String) -> IoResult<PathBuf> {
96        let mut buf: PathBuf = Runtime::get_runtime(&self.name)?;
97        buf.push(version);
98        Ok(buf)
99    }
100
101    /// Checks if a version directory exists, returning it if it does.
102    pub fn get_safe_version(&self, version: String) -> IoResult<PathBuf> {
103        let path: PathBuf = self.get_version(version.to_string())?;
104        if path.try_exists()? {
105            Ok(path)
106        } else {
107            Err(Error::new(
108                ErrorKind::NotFound,
109                format!(
110                    "Version {} for runtime \"{}\" was not found",
111                    version, self.name
112                ),
113            ))
114        }
115    }
116
117    /// Gets the list of version search paths as relative paths to the version
118    /// directory, checking if each one exists.
119    pub fn get_version_search_paths(&self, version: String) -> IoResult<Vec<PathBuf>> {
120        let mut paths: Vec<PathBuf> = Vec::new();
121        let version_dir: PathBuf = self.get_safe_version(version)?;
122        for search_path in &self.metadata.search_paths {
123            let search_buf: PathBuf = search_path.into();
124            let proper_search_buf: PathBuf = version_dir.join(search_buf);
125            if proper_search_buf.try_exists()? {
126                paths.push(proper_search_buf);
127            } else {
128                return Err(Error::new(
129                    ErrorKind::NotFound,
130                    format!(
131                        "Search path \"{}\" does not exist",
132                        proper_search_buf.display()
133                    ),
134                ));
135            }
136        }
137        Ok(paths)
138    }
139}
140
141pub mod conf {
142    use crate::*;
143    use std::io::{Error, ErrorKind, Result as IoResult};
144
145    /// Reads a configuration file, and then parses it.
146    pub fn parse<T: AsRef<str>>(path: T) -> IoResult<HashMap<String, String>> {
147        let data: String = read_to_string(path.as_ref())?;
148        match ron::from_str::<HashMap<String, String>>(data.as_str()) {
149            Ok(map) => Ok(map),
150            Err(_) => Err(Error::new(
151                ErrorKind::InvalidData,
152                "Configuration file is invalid",
153            )),
154        }
155    }
156
157    /// Returns a transformed variant of a configuration file using
158    /// [Runtime::unsafe_new].
159    pub fn unsafe_collect(data: HashMap<String, String>) -> HashMap<Runtime, String> {
160        let mut parsed: HashMap<Runtime, String> = HashMap::new();
161        for (name, value) in data.iter() {
162            let runtime: Runtime = Runtime::unsafe_new(name.to_string());
163            parsed.insert(runtime, value.to_string());
164        }
165        parsed
166    }
167
168    /// Returns a transformed variant of a configuration file using [Runtime::new].
169    pub fn collect(data: HashMap<String, String>) -> IoResult<HashMap<Runtime, String>> {
170        let mut parsed: HashMap<Runtime, String> = HashMap::new();
171        for (name, value) in data.iter() {
172            let runtime: Runtime = Runtime::new(name.to_string())?;
173            parsed.insert(runtime, value.to_string());
174        }
175        Ok(parsed)
176    }
177}
178
179/// Executes a program with an environment suited to various runtime versions.
180///
181/// This method is the main backbone for commands to run under version-managed
182/// scenarios. `args` can be used as both a way to specify the command and as
183/// a way to specify the arguments, though it will fallback to system defaults
184/// if `args` is empty.
185pub fn exec(mut args: VecDeque<String>, config: HashMap<Runtime, String>) -> IoResult<Command> {
186    let mut cmd: Command = Command::new(if let Some(data) = args.pop_front() {
187        data
188    } else if let Ok(shell) = env::var("SHELL") {
189        shell
190    } else if cfg!(windows) {
191        "cmd".into()
192    } else {
193        "sh".into()
194    });
195    let mut paths: Vec<PathBuf> = Vec::with_capacity(config.len());
196    for (runtime, version) in config.iter() {
197        paths.push(runtime.get_safe_version(version.to_string())?);
198        paths.extend(runtime.get_version_search_paths(version.to_string())?);
199    }
200    let path: Result<String, VarError> = env::var("PATH");
201    cmd.args(args)
202        .env("PATH", {
203            let mut iter = paths.iter();
204            let mut prefix: String = String::new();
205            if let Some(path) = iter.next()
206                && let Some(data) = path.to_str()
207            {
208                prefix.push_str(data);
209            }
210
211            let delim: char = if cfg!(windows) { ';' } else { ':' };
212            for path in iter {
213                if let Some(data) = path.to_str() {
214                    prefix.push(delim);
215                    prefix.push_str(data);
216                }
217            }
218            if let Ok(good_path) = path {
219                prefix.push(delim);
220                prefix.push_str(good_path.as_str());
221            }
222            prefix
223        })
224        .env("VER_OVERRIDE", "1");
225    Ok(cmd)
226}