Skip to main content

check_updates/
locking.rs

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}