1use std::{
2 path::{Path, PathBuf},
3 process::Stdio,
4};
5
6use fs_err::PathExt as _;
7use miette::{Context as _, IntoDiagnostic};
8use normalize_path::NormalizePath as _;
9use which::which_global;
10
11use crate::util::PathExt as _;
12
13#[derive(Default, Debug)]
18pub struct Deployer {
19 created_symlinks: Vec<PathBuf>,
21 missing_permissions_create: Vec<(PathBuf, PathBuf)>,
23 missing_permissions_remove: Vec<PathBuf>,
25}
26
27impl Deployer {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn created_symlinks(&self) -> &Vec<PathBuf> {
33 &self.created_symlinks
34 }
35
36 pub fn failed_creations(&self) -> &Vec<(PathBuf, PathBuf)> {
37 &self.missing_permissions_create
38 }
39
40 pub fn failed_deletions(&self) -> &Vec<PathBuf> {
41 &self.missing_permissions_remove
42 }
43
44 pub fn add_created_symlink(&mut self, link_path: PathBuf) {
45 self.created_symlinks.push(link_path);
46 }
47
48 pub fn create_symlink(
50 &mut self,
51 original: impl AsRef<Path>,
52 link: impl AsRef<Path>,
53 ) -> miette::Result<()> {
54 let link = link.as_ref();
55 let original = original.as_ref();
56 tracing::trace!("Creating symlink at {} -> {}", link.abbr(), original.abbr());
57
58 if let Err(err) = symlink::symlink_auto(original, link) {
59 if err.kind() != std::io::ErrorKind::PermissionDenied {
60 return Err(err).into_diagnostic().wrap_err_with(|| {
61 format!(
62 "Failed to create symlink at {}",
63 format_symlink(link.abbr(), original.abbr())
64 )
65 })?;
66 }
67 self.missing_permissions_create
68 .push((original.to_path_buf(), link.to_path_buf()));
69 } else {
70 self.created_symlinks.push(link.to_path_buf());
71 }
72 return Ok(());
73 }
74 pub fn delete_symlink(&mut self, path: impl AsRef<Path>) -> miette::Result<()> {
76 let path = path.as_ref();
77 tracing::trace!("Deleting symlink at {}", path.abbr());
78 if !path.is_symlink() {
79 miette::bail!("Path is not a symlink: {}", path.abbr());
80 }
81 let result = if path.symlink_metadata().into_diagnostic()?.is_dir() {
82 symlink::remove_symlink_dir(path)
83 } else {
84 match symlink::remove_symlink_file(path) {
85 Ok(()) => Ok(()),
86 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => Err(e),
87 Err(e) => {
88 tracing::debug!(
89 "Failed to remove file symlink, trying dir symlink removal as fallback: {:?}", e
90 );
91 symlink::remove_symlink_dir(path)
92 }
93 }
94 };
95 match result {
96 Ok(()) => {}
97 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
98 self.missing_permissions_remove.push(path.to_path_buf());
99 }
100 Err(e) => {
101 return Err(e)
102 .into_diagnostic()
103 .wrap_err(format!("Failed to remove symlink at {}", path.abbr()))
104 }
105 }
106 Ok(())
107 }
108
109 #[tracing::instrument(skip_all, fields(
121 egg_root = egg_root.as_ref().abbr(),
122 actual_path = actual_path.as_ref().abbr(),
123 link_path = link_path.as_ref().abbr()
124 ))]
125 pub fn symlink_recursive(
126 &mut self,
127 egg_root: impl AsRef<Path>,
128 actual_path: impl AsRef<Path>,
129 link_path: &impl AsRef<Path>,
130 ) -> miette::Result<()> {
131 fn inner(
132 deployer: &mut Deployer,
133 egg_root: PathBuf,
134 actual_path: PathBuf,
135 link_path: PathBuf,
136 ) -> miette::Result<()> {
137 let actual_path = actual_path.normalize();
138 let link_path = link_path.normalize();
139 let egg_root = egg_root.normalize();
140 link_path.assert_absolute("link_path");
141 actual_path.assert_absolute("actual_path");
142 actual_path.assert_starts_with(&egg_root, "actual_path");
143 tracing::trace!(
144 "symlink_recursive({}, {})",
145 actual_path.abbr(),
146 link_path.abbr()
147 );
148
149 let actual_path = actual_path.canonical()?;
150
151 if link_path.is_symlink() {
152 let link_target = link_path.fs_err_read_link().into_diagnostic()?;
153 if link_target == actual_path {
154 deployer.add_created_symlink(link_path);
155 return Ok(());
156 } else if link_target.exists() {
157 miette::bail!(
158 "Failed to create symlink {}, as a file already exists there",
159 format_symlink(link_path.abbr(), actual_path.abbr())
160 );
161 } else if link_target.starts_with(&egg_root) {
162 tracing::info!(
163 "Removing dead symlink {}",
164 format_symlink(link_path.abbr(), link_target.abbr())
165 );
166 deployer.delete_symlink(&link_path)?;
167 cov_mark::hit!(remove_dead_symlink);
168 } else {
170 miette::bail!(
171 "Encountered dead symlink, but it doesn't target the egg dir: {}",
172 link_path.abbr(),
173 );
174 }
175 } else if link_path.exists() {
176 tracing::trace!("link_path exists as non-symlink {}", link_path.abbr());
177 if link_path.is_dir() && actual_path.is_dir() {
178 for entry in actual_path.fs_err_read_dir().into_diagnostic()? {
179 let entry = entry.into_diagnostic()?;
180 deployer.symlink_recursive(
181 &egg_root,
182 entry.path(),
183 &link_path.join(entry.file_name()),
184 )?;
185 }
186 return Ok(());
187 } else if link_path.is_dir() || actual_path.is_dir() {
188 miette::bail!(
189 "Conflicting file or directory {} with {}",
190 actual_path.abbr(),
191 link_path.abbr()
192 );
193 }
194 }
195 deployer.create_symlink(&actual_path, &link_path)?;
196 tracing::info!(
197 "created symlink {}",
198 format_symlink(link_path.abbr(), actual_path.abbr()),
199 );
200 Ok(())
201 }
202 inner(
203 self,
204 egg_root.as_ref().to_path_buf(),
205 actual_path.as_ref().to_path_buf(),
206 link_path.as_ref().to_path_buf(),
207 )
208 }
209
210 #[tracing::instrument(skip(actual_path, link_path), fields(
211 actual_path = actual_path.as_ref().abbr(),
212 link_path = link_path.as_ref().abbr()
213 ))]
214 pub fn remove_symlink_recursive(
215 &mut self,
216 actual_path: impl AsRef<Path>,
217 link_path: &impl AsRef<Path>,
218 ) -> miette::Result<()> {
219 let actual_path = actual_path.as_ref();
220 let link_path = link_path.as_ref();
221 if link_path.is_symlink() && link_path.canonical()? == actual_path {
222 tracing::info!(
223 "Removing symlink {}",
224 format_symlink(link_path.abbr(), actual_path.abbr())
225 );
226 self.delete_symlink(link_path)?;
227 } else if link_path.is_dir() && actual_path.is_dir() {
228 for entry in actual_path.fs_err_read_dir().into_diagnostic()? {
229 let entry = entry.into_diagnostic()?;
230 self.remove_symlink_recursive(entry.path(), &link_path.join(entry.file_name()))?;
231 }
232 } else if link_path.exists() {
233 miette::bail!(
234 help = "Yolk will only try to remove files that are symlinks pointing into the corresponding egg.",
235 "Tried to remove deployment of {}, but {} doesn't link to it",
236 actual_path.abbr(),
237 link_path.abbr()
238 );
239 }
240 Ok(())
241 }
242
243 pub fn try_run_elevated(self) -> miette::Result<()> {
245 if self.missing_permissions_create.is_empty() && self.missing_permissions_remove.is_empty()
246 {
247 tracing::trace!("No priviledge escalation necessary, all symlink operations succeeded");
248 return Ok(());
249 }
250 let yolk_binary = std::env::args().nth(0).unwrap_or("yolk".to_string());
251 let yolk_binary_path = if yolk_binary.starts_with('/') {
252 yolk_binary
253 } else {
254 which_global(yolk_binary)
255 .map(|x| x.to_string_lossy().to_string())
256 .unwrap_or_else(|_| "yolk".to_string())
257 };
258 let args = [yolk_binary_path, "root-manage-symlinks".to_string()]
259 .into_iter()
260 .chain(
261 self.missing_permissions_create
262 .iter()
263 .map(|(original, symlink)| {
264 [
265 "--create-symlink".to_string(),
266 format!("{}::::{}", original.display(), symlink.display()),
267 ]
268 })
269 .flatten(),
270 )
271 .chain(
272 self.missing_permissions_remove
273 .iter()
274 .map(|symlink| {
275 [
276 "--delete-symlink".to_string(),
277 symlink.to_string_lossy().to_string(),
278 ]
279 })
280 .flatten(),
281 )
282 .collect::<Vec<_>>();
283 tracing::info!(
284 "Some symlink operations require root permissions: {} {}",
285 if self.missing_permissions_create.is_empty() {
286 "".to_string()
287 } else {
288 format!(
289 "create {}",
290 self.missing_permissions_create
291 .iter()
292 .map(|x| format!("{}", x.1.display()))
293 .collect::<Vec<_>>()
294 .join(", ")
295 )
296 },
297 if self.missing_permissions_remove.is_empty() {
298 "".to_string()
299 } else {
300 format!(
301 "delete {}",
302 self.missing_permissions_remove
303 .iter()
304 .map(|x| format!("{}", x.display()))
305 .collect::<Vec<_>>()
306 .join(", ")
307 )
308 }
309 );
310 try_sudo(&args)?;
311 Ok(())
312 }
313}
314
315pub fn create_symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> miette::Result<()> {
317 let link = link.as_ref();
318 let original = original.as_ref();
319 tracing::trace!("Creating symlink at {} -> {}", link.abbr(), original.abbr());
320 symlink::symlink_auto(original, link)
321 .into_diagnostic()
322 .wrap_err_with(|| {
323 format!(
324 "Failed to create symlink at {}",
325 format_symlink(link.abbr(), original.abbr())
326 )
327 })?;
328 Ok(())
329}
330
331pub fn remove_symlink(path: impl AsRef<Path>) -> miette::Result<()> {
333 let path = path.as_ref();
334 if !path.is_symlink() {
335 miette::bail!("Path is not a symlink: {}", path.abbr());
336 }
337 if path.symlink_metadata().into_diagnostic()?.is_dir() {
338 symlink::remove_symlink_dir(path)
339 .into_diagnostic()
340 .wrap_err_with(|| format!("Failed to remove symlink dir at {}", path.abbr()))?;
341 } else {
342 let result = symlink::remove_symlink_file(path);
343 if let Err(e) = result {
344 symlink::remove_symlink_dir(path)
345 .into_diagnostic()
346 .wrap_err("Failed to remove symlink dir as fallback from symlink file")
347 .wrap_err_with(|| {
348 format!("Failed to remove symlink file at {}: {e:?}", path.abbr())
349 })?;
350 }
351 }
352 Ok(())
353}
354
355fn try_sudo(args: &[String]) -> miette::Result<()> {
356 let sudo_command = which_global("sudo")
357 .or_else(|_| which_global("doas"))
358 .or_else(|_| which_global("run0"))
359 .map_err(|_| miette::miette!("No sudo, doas, or run0 command found"))?;
360
361 let mut cmd = std::process::Command::new(sudo_command);
362 cmd.stdin(Stdio::inherit())
363 .stdout(Stdio::inherit())
364 .stderr(Stdio::piped())
365 .args(args);
366 let output = cmd.output().into_diagnostic()?;
367 if !output.status.success() {
368 tracing::error!(
369 "Failed to run command with sudo: {}",
370 String::from_utf8_lossy(&output.stderr)
371 );
372 }
373 Ok(())
374}
375
376fn format_symlink(link_path: impl AsRef<Path>, original_path: impl AsRef<Path>) -> String {
377 format!(
378 "{} -> {}",
379 link_path.as_ref().display(),
380 original_path.as_ref().display()
381 )
382}