1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::thread::sleep;
5use std::time::Duration;
6
7use anyhow::{Context, Result, bail};
8
9use crate::changes::safe_text;
10use crate::workspace::read_project;
11
12fn parent_workspace(directory: &Path) -> Result<Option<PathBuf>> {
13 for candidate in directory.ancestors() {
14 let path = candidate.join("pyproject.toml");
15 if !path.is_file() {
16 continue;
17 }
18 let project = read_project(&path)?;
19 if project
20 .document
21 .get("tool")
22 .and_then(|v| v.get("uv"))
23 .and_then(|v| v.get("workspace"))
24 .is_some()
25 {
26 return Ok((candidate != directory).then(|| candidate.to_owned()));
27 }
28 }
29 Ok(None)
30}
31
32pub fn lock_project(directory: &Path) -> Result<()> {
33 let directory = directory.canonicalize()?;
34 let recovery = format!(
35 "Requirement files were updated. uv.lock may be stale or changed. Resolve the error and run uv lock in {}, or review and restore the affected files.",
36 safe_text(&directory.to_string_lossy(), false)
37 );
38 lock_inner(&directory).with_context(|| recovery)
39}
40
41fn lock_inner(directory: &Path) -> Result<()> {
42 match fs::metadata(directory.join("uv.lock")) {
43 Ok(metadata) if metadata.is_file() => (),
44 Ok(_) => return Ok(()),
45 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
46 Err(error) => return Err(error.into()),
47 }
48 if let Some(parent) = parent_workspace(directory)? {
49 eprintln!(
50 "Skipping uv lock: parent workspace found at {}. Requirement files were updated; lockfiles were not changed. Use -w to update and lock the workspace, or review and lock manually.",
51 safe_text(&parent.to_string_lossy(), false)
52 );
53 return Ok(());
54 }
55 println!(
56 "Running uv lock in {}...",
57 safe_text(&directory.to_string_lossy(), false)
58 );
59 let mut child = Command::new("uv")
60 .arg("lock")
61 .current_dir(directory)
62 .env_remove("UV_PROJECT")
63 .env_remove("UV_FROZEN")
64 .stdin(Stdio::null())
65 .spawn()
66 .context("uv lock could not start")?;
67 loop {
68 if let Err(error) = crate::check_interrupt() {
69 let _ = child.kill();
70 let _ = child.wait();
71 return Err(
72 error.context("Interrupted during uv lock; lock completion is not confirmed")
73 );
74 }
75 if let Some(status) = child.try_wait()? {
76 if !status.success() {
77 bail!("uv lock failed ({status})");
78 }
79 println!("uv lock completed.");
80 return Ok(());
81 }
82 sleep(Duration::from_millis(50));
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn parent_workspace_guard_stops_at_local_workspace() {
92 let temp = tempfile::tempdir().unwrap();
93 fs::write(
94 temp.path().join("pyproject.toml"),
95 "[tool.uv.workspace]\nmembers=[]",
96 )
97 .unwrap();
98 let member = temp.path().join("member");
99 fs::create_dir(&member).unwrap();
100 fs::write(member.join("pyproject.toml"), "[project]\ndependencies=[]").unwrap();
101 assert_eq!(
102 parent_workspace(&member).unwrap(),
103 Some(temp.path().to_owned())
104 );
105 fs::write(
106 member.join("pyproject.toml"),
107 "[tool.uv.workspace]\nmembers=[]",
108 )
109 .unwrap();
110 assert!(parent_workspace(&member).unwrap().is_none());
111 }
112
113 #[test]
114 fn missing_or_directory_lockfile_does_not_start_uv() {
115 let temp = tempfile::tempdir().unwrap();
116 lock_project(temp.path()).unwrap();
117 fs::create_dir(temp.path().join("uv.lock")).unwrap();
118 lock_project(temp.path()).unwrap();
119 }
120}