Skip to main content

podbox/codegen/
distros.rs

1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum DistroFamily {
3    DebianLike,
4    FedoraLike,
5    ArchLike,
6    AlpineLike,
7    SuseLike,
8    Unknown,
9}
10
11impl DistroFamily {
12    pub fn from_base_image(base: &str) -> Self {
13        let base_lower = base.to_lowercase();
14        if base_lower.contains("debian")
15            || base_lower.contains("ubuntu")
16            || base_lower.contains("mint")
17            || base_lower.contains("kali")
18            || base_lower.contains("pop")
19            || base_lower.contains("elementary")
20        {
21            Self::DebianLike
22        } else if base_lower.contains("fedora")
23            || base_lower.contains("rhel")
24            || base_lower.contains("centos")
25            || base_lower.contains("rocky")
26            || base_lower.contains("alma")
27            || base_lower.contains("nobara")
28        {
29            Self::FedoraLike
30        } else if base_lower.contains("arch")
31            || base_lower.contains("cachy")
32            || base_lower.contains("manjaro")
33            || base_lower.contains("endeavouros")
34            || base_lower.contains("garuda")
35        {
36            Self::ArchLike
37        } else if base_lower.contains("alpine") {
38            Self::AlpineLike
39        } else if base_lower.contains("opensuse") || base_lower.contains("suse") {
40            Self::SuseLike
41        } else {
42            Self::Unknown
43        }
44    }
45
46    pub fn manager(&self) -> crate::config::PackageManager {
47        match self {
48            Self::DebianLike => crate::config::PackageManager::Apt,
49            Self::FedoraLike => crate::config::PackageManager::Dnf,
50            Self::ArchLike => crate::config::PackageManager::Pacman,
51            Self::AlpineLike => crate::config::PackageManager::Apk,
52            Self::SuseLike => crate::config::PackageManager::Zypper,
53            Self::Unknown => crate::config::PackageManager::Dnf,
54        }
55    }
56
57    pub fn install_cmd(&self) -> &'static str {
58        match self {
59            Self::DebianLike => "apt-get update && apt-get install -y --no-install-recommends",
60            Self::FedoraLike => "dnf install -y",
61            Self::ArchLike => "pacman -Syu --noconfirm",
62            Self::AlpineLike => "apk add --no-cache",
63            Self::SuseLike => "zypper install -y",
64            Self::Unknown => "dnf install -y",
65        }
66    }
67
68    pub fn clean_cmd(&self) -> &'static str {
69        match self {
70            Self::DebianLike => "rm -rf /var/lib/apt/lists/*",
71            Self::FedoraLike => "dnf clean all",
72            Self::ArchLike => "pacman -Scc --noconfirm",
73            Self::AlpineLike => "",
74            Self::SuseLike => "zypper clean --all",
75            Self::Unknown => "dnf clean all",
76        }
77    }
78
79    pub fn base_packages(&self, host_shell: Option<&str>) -> Vec<String> {
80        let common: Vec<String> = [
81            "sudo",
82            "curl",
83            "tar",
84            "unzip",
85            "wget",
86            "which",
87            "coreutils",
88            "diffutils",
89            "findutils",
90            "grep",
91            "sed",
92            "gawk",
93            "bash-completion",
94        ]
95        .into_iter()
96        .map(String::from)
97        .collect();
98
99        let mut pkgs = common;
100
101        if let Some(shell) = host_shell {
102            let shell_pkgs = Self::shell_packages(self, shell);
103            for pkg in shell_pkgs {
104                if !pkgs.contains(&pkg) {
105                    pkgs.push(pkg);
106                }
107            }
108        }
109
110        pkgs
111    }
112
113    fn shell_packages(&self, shell_path: &str) -> Vec<String> {
114        let shell_name = shell_path.split('/').next_back().unwrap_or("");
115        let mut pkgs = Vec::new();
116
117        match shell_name {
118            "bash" => {
119                pkgs.push("bash".into());
120                pkgs.push("bash-completion".into());
121            }
122            "zsh" => {
123                pkgs.push("zsh".into());
124                match self {
125                    Self::DebianLike => pkgs.push("zsh-common".into()),
126                    Self::FedoraLike => pkgs.push("zsh".into()),
127                    Self::ArchLike => pkgs.push("zsh-completions".into()),
128                    Self::AlpineLike => pkgs.push("zsh".into()),
129                    Self::SuseLike => pkgs.push("zsh".into()),
130                    Self::Unknown => pkgs.push("zsh".into()),
131                }
132            }
133            "fish" => {
134                pkgs.push("fish".into());
135            }
136            "sh" | "dash" => match self {
137                Self::DebianLike => pkgs.push("dash".into()),
138                Self::FedoraLike => pkgs.push("dash".into()),
139                Self::ArchLike => pkgs.push("dash".into()),
140                Self::AlpineLike => pkgs.push("dash".into()),
141                Self::SuseLike => pkgs.push("dash".into()),
142                Self::Unknown => pkgs.push("dash".into()),
143            },
144            _ => {}
145        }
146
147        pkgs
148    }
149
150    pub fn remove_cmd(&self, packages: &[String]) -> String {
151        if packages.is_empty() {
152            return String::new();
153        }
154        let pkgs = packages.join(" ");
155        match self {
156            Self::DebianLike => {
157                format!(
158                    "apt-get purge -y {} && apt-get autoremove -y && {}",
159                    pkgs,
160                    Self::clean_cmd(self)
161                )
162            }
163            Self::FedoraLike => {
164                format!("dnf remove -y {} && {}", pkgs, Self::clean_cmd(self))
165            }
166            Self::ArchLike => {
167                format!(
168                    "pacman -Rns --noconfirm {} && {}",
169                    pkgs,
170                    Self::clean_cmd(self)
171                )
172            }
173            Self::AlpineLike => {
174                format!("apk del {} && {}", pkgs, Self::clean_cmd(self))
175            }
176            Self::SuseLike => {
177                format!("zypper rm {} && {}", pkgs, Self::clean_cmd(self))
178            }
179            Self::Unknown => {
180                format!("dnf remove -y {} && {}", pkgs, Self::clean_cmd(self))
181            }
182        }
183    }
184
185    pub fn locale_packages(&self) -> Vec<String> {
186        match self {
187            Self::DebianLike => vec!["locales".into()],
188            Self::FedoraLike => vec!["glibc-all-langpacks".into()],
189            Self::ArchLike => vec!["glibc".into()],
190            Self::AlpineLike => vec!["musl-locales".into()],
191            Self::SuseLike => vec!["glibc-all-langpacks".into()],
192            Self::Unknown => vec!["locales".into()],
193        }
194    }
195}
196
197pub fn detect_host_shell() -> Option<String> {
198    std::env::var("SHELL").ok().filter(|s| !s.is_empty())
199}
200
201pub fn detect_host_locale() -> Option<String> {
202    std::env::var("LANG")
203        .ok()
204        .or_else(|| std::env::var("LC_ALL").ok())
205        .or_else(|| std::env::var("LC_CTYPE").ok())
206        .filter(|s| !s.is_empty())
207}
208
209pub fn detect_package_manager(image: &str) -> crate::config::PackageManager {
210    DistroFamily::from_base_image(image).manager()
211}
212
213pub fn is_tty() -> bool {
214    nix::unistd::isatty(std::io::stdin()).unwrap_or(false)
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_distro_from_base_image() {
223        assert_eq!(
224            DistroFamily::from_base_image("debian:12"),
225            DistroFamily::DebianLike
226        );
227        assert_eq!(
228            DistroFamily::from_base_image("ubuntu:26.04"),
229            DistroFamily::DebianLike
230        );
231        assert_eq!(
232            DistroFamily::from_base_image("fedora:41"),
233            DistroFamily::FedoraLike
234        );
235        assert_eq!(
236            DistroFamily::from_base_image("cachy-latest"),
237            DistroFamily::ArchLike
238        );
239        assert_eq!(
240            DistroFamily::from_base_image("archlinux:latest"),
241            DistroFamily::ArchLike
242        );
243        assert_eq!(
244            DistroFamily::from_base_image("alpine:3.20"),
245            DistroFamily::AlpineLike
246        );
247        assert_eq!(
248            DistroFamily::from_base_image("opensuse/tumbleweed:latest"),
249            DistroFamily::SuseLike
250        );
251        assert_eq!(
252            DistroFamily::from_base_image("unknown:latest"),
253            DistroFamily::Unknown
254        );
255    }
256
257    #[test]
258    fn test_debian_base_packages() {
259        let pkgs = DistroFamily::DebianLike.base_packages(Some("/usr/bin/fish"));
260        assert!(pkgs.contains(&"sudo".into()));
261        assert!(pkgs.contains(&"curl".into()));
262        assert!(pkgs.contains(&"fish".into()));
263    }
264
265    #[test]
266    fn test_fedora_base_packages() {
267        let pkgs = DistroFamily::FedoraLike.base_packages(Some("/usr/bin/zsh"));
268        assert!(pkgs.contains(&"sudo".into()));
269        assert!(pkgs.contains(&"zsh".into()));
270        assert!(pkgs.contains(&"zsh-completions".into()) || pkgs.contains(&"zsh".into()));
271    }
272
273    #[test]
274    fn test_arch_base_packages() {
275        let pkgs = DistroFamily::ArchLike.base_packages(Some("/bin/bash"));
276        assert!(pkgs.contains(&"sudo".into()));
277        assert!(pkgs.contains(&"bash".into()));
278        assert!(pkgs.contains(&"bash-completion".into()));
279    }
280
281    #[test]
282    fn test_alpine_base_packages() {
283        let pkgs = DistroFamily::AlpineLike.base_packages(None);
284        assert!(pkgs.contains(&"sudo".into()));
285        assert!(pkgs.contains(&"curl".into()));
286    }
287
288    #[test]
289    fn test_install_cmd() {
290        assert_eq!(
291            DistroFamily::DebianLike.install_cmd(),
292            "apt-get update && apt-get install -y --no-install-recommends"
293        );
294        assert_eq!(DistroFamily::FedoraLike.install_cmd(), "dnf install -y");
295        assert_eq!(
296            DistroFamily::ArchLike.install_cmd(),
297            "pacman -Syu --noconfirm"
298        );
299        assert_eq!(DistroFamily::AlpineLike.install_cmd(), "apk add --no-cache");
300        assert_eq!(DistroFamily::SuseLike.install_cmd(), "zypper install -y");
301    }
302}