podbox/
quadlet_install.rs1pub mod paths;
7pub mod preflight;
8pub mod units;
9
10pub use paths::{is_installed, list_installed_names};
11
12use anyhow::{Context, Result};
13use nix::fcntl::{Flock, FlockArg};
14
15use crate::codegen::quadlet;
16use crate::config::Config;
17use crate::env::HostEnv;
18use crate::podman::{PodmanVersion, podman_version};
19use crate::systemd;
20use crate::xdg::ResolvedXdgDirs;
21
22use paths::{quadlet_dir, systemd_user_dir};
23use preflight::preflight_check;
24use units::{
25 finalize_units, podman_quadlet_install_application, podman_quadlet_install_files,
26 remove_application_dir, remove_flat_units,
27};
28
29pub fn install(config: &Config, env: &HostEnv, xdg: &ResolvedXdgDirs, dry_run: bool) -> Result<()> {
31 let name = &config.container.name;
32 let ver = podman_version().unwrap_or(PodmanVersion {
33 major: 5,
34 minor: 5,
35 patch: 0,
36 });
37 let qdir = quadlet_dir();
38 let sdir = systemd_user_dir();
39 let context_dir = crate::build::build_context_dir(name);
40 let containerfile_path = context_dir.join("Containerfile");
41
42 let socket_content = quadlet::generate_socket(config);
43 let container_content = quadlet::generate_container(config, env, xdg);
44 let host_service_content = quadlet::generate_host_service(name);
45 let dbus_proxy_content = quadlet::generate_dbus_proxy_service(name, config);
46 let compositor_service_content = quadlet::generate_compositor_service(name, config);
47
48 let build_content = if !config.image.source().is_prebuilt() {
49 Some(quadlet::generate_build(config, &containerfile_path))
50 } else {
51 None
52 };
53
54 if dry_run {
55 if let Some(ref bc) = build_content {
56 println!("=== {name}.build ===");
57 println!("{bc}");
58 println!();
59 }
60 println!("=== {name}.socket ===");
61 println!("{socket_content}");
62 println!();
63 println!("=== {name}.container ===");
64 println!("{container_content}");
65 println!();
66 println!("=== {name}-host.service ===");
67 println!("{host_service_content}");
68 if let Some(ref proxy) = dbus_proxy_content {
69 println!();
70 println!("=== {name}-proxy.service ===");
71 println!("{proxy}");
72 }
73 if let Some(ref comp) = compositor_service_content {
74 println!();
75 println!("=== {name}-compositor.service ===");
76 println!("{comp}");
77 }
78 return Ok(());
79 }
80
81 let _install_lock = {
83 let lock_path = context_dir.join(".install.lock");
84 let _ = std::fs::create_dir_all(&context_dir);
85 let file = std::fs::File::create(&lock_path).with_context(|| {
86 format!("failed to create install lock at '{}'", lock_path.display())
87 })?;
88 Flock::lock(file, FlockArg::LockExclusive).map_err(|(_, e)| e)?
89 };
90
91 let _ = std::fs::create_dir_all(&context_dir);
93 std::fs::write(
94 context_dir.join(".flatpak-info"),
95 "[Application]\nname=podbox\n",
96 )?;
97
98 preflight_check(config)?;
100
101 std::fs::create_dir_all(&config.container.home).with_context(|| {
103 format!(
104 "failed to create home dir '{}'",
105 config.container.home.display()
106 )
107 })?;
108
109 if ver.at_least(6, 0) {
110 remove_flat_units(name);
112 podman_quadlet_install_application(name, &container_content, build_content.as_deref())?;
113 } else if ver.at_least(5, 6) {
114 remove_application_dir(name);
116 podman_quadlet_install_files(name, &container_content, build_content.as_deref())?;
117 } else {
118 std::fs::create_dir_all(&qdir)?;
120 if let Some(ref bc) = build_content {
121 std::fs::write(qdir.join(format!("{name}.build")), bc)?;
122 }
123 std::fs::write(qdir.join(format!("{name}.container")), container_content)?;
124 println!("Quadlet files installed to {}", qdir.display());
125 }
126
127 finalize_units(
128 name,
129 &sdir,
130 &socket_content,
131 &host_service_content,
132 dbus_proxy_content.as_deref(),
133 compositor_service_content.as_deref(),
134 config.use_wayland_proxy(),
135 )?;
136
137 for app in &config.integration.export.apps {
139 if let Err(e) = crate::export::export_app(name, app) {
140 eprintln!("Warning: auto-export app '{app}' failed: {e}");
141 }
142 }
143 for bin in &config.integration.export.bins {
144 if let Err(e) = crate::export::export_bin(name, bin) {
145 eprintln!("Warning: auto-export bin '{bin}' failed: {e}");
146 }
147 }
148
149 if config.lifecycle.autostart {
150 systemd::enable_linger()?;
151 }
152
153 Ok(())
154}
155
156pub fn uninstall(name: &str) -> Result<()> {
158 let ver = podman_version().unwrap_or(PodmanVersion {
159 major: 5,
160 minor: 5,
161 patch: 0,
162 });
163 let qdir = quadlet_dir();
164 let sdir = systemd_user_dir();
165
166 if ver.at_least(5, 6) {
167 let mut removed_via_podman = false;
168
169 for ext in ["container", "build"] {
171 let path = qdir.join(format!("{name}.{ext}"));
172 if !path.exists() {
173 continue;
174 }
175 let args: Vec<std::ffi::OsString> = vec![
176 "quadlet".into(),
177 "rm".into(),
178 format!("{name}.{ext}").into(),
179 ];
180 let output = crate::process::run_piped("podman", &args)?;
181 if output.status.success() {
182 removed_via_podman = true;
183 } else {
184 let stderr = String::from_utf8_lossy(&output.stderr);
186 eprintln!("Warning: podman quadlet rm {name}.{ext} failed: {stderr}");
187 }
188 }
189
190 let app_dir = qdir.join(name);
192 if app_dir.is_dir() {
193 if ver.at_least(6, 0) {
194 let args: Vec<std::ffi::OsString> = vec![
195 "quadlet".into(),
196 "rm".into(),
197 "--recursive".into(),
198 name.into(),
199 ];
200 let output = crate::process::run_piped("podman", &args)?;
201 if output.status.success() {
202 removed_via_podman = true;
203 } else {
204 let stderr = String::from_utf8_lossy(&output.stderr);
205 eprintln!("Warning: podman quadlet rm --recursive {name} failed: {stderr}");
206 }
207 }
208 remove_application_dir(name);
209 }
210
211 for ext in ["build", "container"] {
213 let path = qdir.join(format!("{name}.{ext}"));
214 if path.exists() {
215 std::fs::remove_file(&path)?;
216 }
217 }
218
219 if removed_via_podman {
220 println!("Quadlet files removed via podman quadlet rm.");
221 }
222 } else {
223 for ext in ["build", "container"] {
225 let path = qdir.join(format!("{name}.{ext}"));
226 if path.exists() {
227 std::fs::remove_file(&path)?;
228 }
229 }
230 remove_application_dir(name);
231 }
232
233 for unit in [
235 "socket",
236 "host.service",
237 "proxy.service",
238 "compositor.service",
239 ] {
240 let path = sdir.join(format!("{name}.{unit}"));
241 if path.exists() {
242 std::fs::remove_file(&path)?;
243 }
244 }
245
246 let dropin_dir = sdir.join(format!("{name}.service.d"));
248 if dropin_dir.is_dir() {
249 std::fs::remove_dir_all(&dropin_dir)?;
250 }
251
252 systemd::daemon_reload()?;
253 println!("Files for '{name}' removed.");
254
255 Ok(())
256}
257
258#[cfg(test)]
259mod tests {
260 use super::paths::{application_container_path, flat_container_path};
261
262 #[test]
263 fn flat_and_application_paths_differ() {
264 let flat = flat_container_path("myenv");
265 let app = application_container_path("myenv");
266 assert!(flat.to_string_lossy().ends_with("myenv.container"));
267 assert!(
268 app.to_string_lossy().ends_with("myenv/myenv.container")
269 || app.to_string_lossy().ends_with("myenv\\myenv.container")
270 );
271 assert_ne!(flat, app);
272 }
273}