Skip to main content

asimov_env/envs/
ruby.rs

1// This is free and unencumbered software released into the public domain.
2
3use crate::{env::Env, paths::ruby_env};
4use std::{
5    borrow::Cow,
6    fs::ReadDir,
7    io::Result,
8    path::PathBuf,
9    process::{Command, ExitStatus},
10};
11
12pub struct RubyEnv {
13    venv: Option<PathBuf>,
14}
15
16impl Default for RubyEnv {
17    fn default() -> Self {
18        Self::at(ruby_env())
19    }
20}
21
22impl Env for RubyEnv {
23    fn path(&self) -> Option<&std::path::PathBuf> {
24        self.venv.as_ref()
25    }
26
27    fn is_initialized(&self) -> bool {
28        match self.venv {
29            None => ruby().is_some(),
30            Some(ref path) => path.is_dir(),
31        }
32    }
33
34    fn initialize(&self) -> Result<()> {
35        if self.is_initialized() {
36            return Ok(());
37        }
38
39        let Some(ref path) = self.venv else {
40            return Ok(());
41        };
42
43        // Create the directory if it doesn't exist:
44        std::fs::create_dir_all(path)?;
45
46        Ok(())
47    }
48
49    fn available_modules(&self) -> std::io::Result<Vec<String>> {
50        Ok(vec![]) // TODO
51    }
52
53    fn installed_modules(&self) -> std::io::Result<Vec<String>> {
54        let mut result = vec![];
55        for entry in self.gems_dir()? {
56            let Ok(entry) = entry else {
57                continue; // skip invalid entries
58            };
59            if !entry.path().is_dir() {
60                continue; // skip non-directory entries
61            }
62            let name = entry.file_name();
63            let Some(name) = name.to_str() else {
64                continue; // skip non-UTF-8 entries
65            };
66            let Some(name) = name.strip_prefix("asimov-") else {
67                continue;
68            };
69            let Some(pos) = name.find("-module-") else {
70                continue;
71            };
72            result.push(name[..pos].to_string());
73        }
74        Ok(result)
75    }
76
77    fn install_module(
78        &self,
79        module_name: impl ToString,
80        verbosity: Option<u8>,
81    ) -> Result<ExitStatus> {
82        if !self.is_initialized() {
83            self.initialize()?;
84        }
85
86        let package_name = format!("asimov-{}-module", module_name.to_string());
87
88        self.gem_command("install", verbosity.unwrap_or(0))
89            .args(["--prerelease", "--no-document", &package_name])
90            .status()
91    }
92
93    fn uninstall_module(
94        &self,
95        module_name: impl ToString,
96        verbosity: Option<u8>,
97    ) -> Result<ExitStatus> {
98        if !self.is_initialized() {
99            return Ok(ExitStatus::default());
100        }
101
102        let package_name = format!("asimov-{}-module", module_name.to_string());
103
104        self.gem_command("uninstall", verbosity.unwrap_or(0))
105            .args(["--all", "--executables", &package_name])
106            .status()
107    }
108}
109
110impl RubyEnv {
111    pub fn system() -> Self {
112        Self { venv: None }
113    }
114
115    pub fn at(path: PathBuf) -> Self {
116        Self { venv: Some(path) }
117    }
118
119    pub fn ruby(&self) -> Command {
120        match self.venv {
121            None => Command::new(ruby().unwrap().as_ref()),
122            Some(ref path) => {
123                let venv_ruby = path.join("bin/ruby");
124                Command::new(if venv_ruby.is_file() {
125                    venv_ruby
126                } else {
127                    PathBuf::from(ruby().unwrap().as_ref()) // TODO: remove the allocation
128                })
129            },
130        }
131    }
132
133    pub fn gem(&self) -> Command {
134        let mut command = self.ruby();
135        command.args(["-S", "gem"]);
136        command
137    }
138
139    pub fn gem_command(&self, subcommand: &str, verbosity: u8) -> Command {
140        let mut command = self.gem();
141        command.arg(subcommand).args(Self::gem_verbosity(verbosity));
142        if let Some(ref path) = self.venv {
143            command.args(["--install-dir", path.to_str().unwrap()]);
144        }
145        command
146    }
147
148    /// Returns the verbosity flags for `gem` from a normalized level.
149    pub fn gem_verbosity(verbosity: u8) -> Vec<&'static str> {
150        match verbosity {
151            0 => vec!["--silent"],
152            1 => vec!["--quiet"], // -q
153            2 => vec![],
154            _ => vec!["--verbose"], // -V
155        }
156    }
157
158    pub fn gems_dir(&self) -> std::io::Result<ReadDir> {
159        let Some(ref path) = self.gems_path() else {
160            return Err(std::io::Error::new(
161                std::io::ErrorKind::NotFound,
162                "`gems` directory not found",
163            ));
164        };
165        std::fs::read_dir(path)
166    }
167
168    pub fn gems_path(&self) -> Option<PathBuf> {
169        let Some(ref path) = self.venv else {
170            return None;
171        };
172        let path = path.join("gems");
173        if !path.is_dir() {
174            return None; // no gems installed
175        }
176        Some(path)
177    }
178}
179
180pub fn ruby() -> Option<Cow<'static, str>> {
181    getenv::ruby()
182        .map(Cow::from)
183        .or_else(|| Some(Cow::from("ruby")))
184}
185
186pub fn gem() -> Option<Cow<'static, str>> {
187    getenv::gem()
188        .map(Cow::from)
189        .or_else(|| Some(Cow::from("gem")))
190}