use crate::codegen::distros::{DistroFamily, detect_host_locale, detect_host_shell};
use crate::config::Config;
use crate::error::PodboxError;
pub fn generate(config: &Config, _guest_binary_name: &str) -> Result<String, PodboxError> {
if config.image.source().is_prebuilt() {
return Ok(generate_prebuilt(config));
}
generate_custom(config)
}
fn generate_prebuilt(config: &Config) -> String {
let builder = ContainerfileBuilder::new(&config.image.base, &config.container.name);
builder
.add_user_packages(config.image.packages.install.clone())
.add_run_commands(config.image.run.commands.clone())
.set_shell(&config.container.shell)
.build()
}
fn generate_custom(config: &Config) -> Result<String, PodboxError> {
let distro = DistroFamily::from_base_image(&config.image.base);
let host_shell = detect_host_shell();
let host_locale = detect_host_locale();
let builder = ContainerfileBuilder::new(&config.image.base, &config.container.name);
let builder = builder
.add_base_packages(distro, host_shell.as_deref(), host_locale.as_deref())
.add_user_packages(config.image.packages.install.clone())
.add_run_commands(config.image.run.commands.clone())
.add_guest_binary()?;
Ok(builder.build())
}
struct ContainerfileBuilder {
base_image: String,
container_name: String,
packages: Vec<String>,
run_commands: Vec<String>,
has_guest_binary: bool,
env_vars: Vec<(String, String)>,
forced_shell: Option<String>,
}
impl ContainerfileBuilder {
fn new(base_image: &str, container_name: &str) -> Self {
Self {
base_image: base_image.to_string(),
container_name: container_name.to_string(),
packages: Vec::new(),
run_commands: Vec::new(),
has_guest_binary: false,
env_vars: Vec::new(),
forced_shell: None,
}
}
fn add_base_packages(
mut self,
distro: DistroFamily,
host_shell: Option<&str>,
host_locale: Option<&str>,
) -> Self {
let mut pkgs = distro.base_packages(host_shell);
let locale_pkgs = distro.locale_packages();
for pkg in locale_pkgs {
if !pkgs.contains(&pkg) {
pkgs.push(pkg);
}
}
self.packages = pkgs;
if let Some(locale) = host_locale {
self.env_vars.push(("LANG".into(), locale.to_string()));
self.env_vars.push(("LC_ALL".into(), locale.to_string()));
self.env_vars.push(("LC_CTYPE".into(), locale.to_string()));
}
self
}
fn add_user_packages(mut self, pkgs: Vec<String>) -> Self {
for pkg in pkgs {
if !self.packages.contains(&pkg) {
self.packages.push(pkg);
}
}
self
}
fn add_run_commands(mut self, cmds: Vec<String>) -> Self {
self.run_commands = cmds;
self
}
fn add_guest_binary(mut self) -> Result<Self, PodboxError> {
if crate::guest::PODBOX_GUEST.is_none() {
return Err(PodboxError::GuestBinaryUnavailable);
}
self.has_guest_binary = true;
Ok(self)
}
fn set_shell(mut self, shell: &str) -> Self {
self.forced_shell = Some(shell.to_string());
self
}
fn build(self) -> String {
let distro = DistroFamily::from_base_image(&self.base_image);
let mut lines = Vec::new();
lines.push(format!("FROM {}", self.base_image));
lines.push(String::new());
if !self.packages.is_empty() {
let pkgs = self.packages.join(" ");
let clean = distro.clean_cmd();
let cmd = if clean.is_empty() {
format!("{} {}", distro.install_cmd(), pkgs)
} else {
format!("{} {} && {}", distro.install_cmd(), pkgs, clean)
};
lines.push(format!("RUN {cmd}"));
lines.push(String::new());
}
for cmd in &self.run_commands {
lines.push(format!("RUN {cmd}"));
}
if !self.run_commands.is_empty() {
lines.push(String::new());
}
if let Some(locale) = self
.env_vars
.iter()
.find(|(k, _)| k == "LANG")
.map(|(_, v)| v.as_str())
{
match distro {
DistroFamily::DebianLike | DistroFamily::ArchLike => {
let (name, charset) = locale.split_once('.').unwrap_or((locale, "UTF-8"));
lines.push(format!(
"RUN localedef -i {name} -f {charset} {locale} || true"
));
lines.push(String::new());
}
DistroFamily::FedoraLike | DistroFamily::SuseLike => {
}
DistroFamily::AlpineLike | DistroFamily::Unknown => {}
}
}
if self.has_guest_binary {
lines.push("COPY podbox-guest /usr/local/bin/podbox-guest".into());
lines.push("RUN chmod +x /usr/local/bin/podbox-guest".into());
lines.push(String::new());
}
for (key, value) in &self.env_vars {
lines.push(format!("ENV {key}={value}"));
}
lines.push(format!("ENV PODBOX_CONTAINER={}", self.container_name));
lines.push(format!("ENV PODBOX_HOST_VERSION={}", crate::VERSION));
lines.push(String::new());
lines.push("ENTRYPOINT [\"/usr/local/bin/podbox-guest\", \"--entry\"]".into());
lines.push(format!("CMD [\"{}\"]", self.default_shell()));
lines.push(String::new());
lines.join("\n")
}
fn default_shell(&self) -> &str {
if let Some(ref shell) = self.forced_shell {
return shell;
}
self.packages
.iter()
.find_map(|p| match p.as_str() {
"fish" => Some("fish"),
"zsh" => Some("zsh"),
"bash" => Some("bash"),
_ => None,
})
.unwrap_or("fish")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::distros::DistroFamily;
#[test]
fn test_builder_debian() {
let builder = ContainerfileBuilder::new("debian:12", "test").add_base_packages(
DistroFamily::DebianLike,
Some("/usr/bin/fish"),
Some("en_US.UTF-8"),
);
let cf = builder.build();
assert!(cf.contains("apt-get update"));
assert!(cf.contains("sudo"));
assert!(cf.contains("fish"));
assert!(cf.contains("locales"));
assert!(cf.contains("ENV LANG=en_US.UTF-8"));
assert!(cf.contains("localedef -i en_US -f UTF-8 en_US.UTF-8"));
assert!(cf.contains("ENV PODBOX_CONTAINER=test"));
}
#[test]
fn test_builder_fedora() {
let builder = ContainerfileBuilder::new("fedora:41", "test").add_base_packages(
DistroFamily::FedoraLike,
Some("/usr/bin/zsh"),
None,
);
let cf = builder.build();
assert!(cf.contains("dnf install -y"));
assert!(cf.contains("sudo"));
assert!(cf.contains("zsh"));
assert!(cf.contains("ENV PODBOX_CONTAINER=test"));
assert!(!cf.contains("localedef"));
}
#[test]
fn test_builder_arch() {
let builder = ContainerfileBuilder::new("archlinux:latest", "test").add_base_packages(
DistroFamily::ArchLike,
Some("/bin/bash"),
None,
);
let cf = builder.build();
assert!(cf.contains("pacman -Syu --noconfirm"));
assert!(cf.contains("bash"));
assert!(cf.contains("bash-completion"));
assert!(cf.contains("ENV PODBOX_CONTAINER=test"));
assert!(!cf.contains("localedef"));
}
#[test]
fn test_builder_arch_with_locale() {
let builder = ContainerfileBuilder::new("archlinux:latest", "test").add_base_packages(
DistroFamily::ArchLike,
Some("/bin/bash"),
Some("en_US.UTF-8"),
);
let cf = builder.build();
assert!(cf.contains("localedef -i en_US -f UTF-8 en_US.UTF-8"));
}
#[test]
fn test_builder_alpine() {
let builder = ContainerfileBuilder::new("alpine:3.20", "test").add_base_packages(
DistroFamily::AlpineLike,
None,
None,
);
let cf = builder.build();
assert!(cf.contains("apk add --no-cache"));
assert!(cf.contains("sudo"));
assert!(cf.contains("ENV PODBOX_CONTAINER=test"));
}
}