Skip to main content

dev_prune/adapters/
pdm.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// PDM adapter for Python projects.
5//
6// PDM installs either into an in-project `.venv` or, in PEP 582 mode, into
7// `__pypackages__/`. Both are rebuilt by `pdm install` from `pdm.lock`, so both are
8// claimed and neither is opt-in.
9
10use super::{
11    BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier, run_command_with_timeout,
12};
13use anyhow::Result;
14use std::fs;
15use std::path::Path;
16
17/// PDM package manager adapter.
18pub struct Pdm;
19
20/// The two places PDM puts a project's dependencies.
21const PDM_ENV_DIRS: [&str; 2] = [".venv", "__pypackages__"];
22
23impl PackageManager for Pdm {
24    fn name(&self) -> &'static str {
25        "pdm"
26    }
27
28    /// The lockfile, or a `pyproject.toml` that names PDM as the build backend or
29    /// carries PDM's own settings table. A plain PEP 621 `pyproject.toml` belongs to
30    /// whichever tool actually manages it, and is not claimed here.
31    fn detect(&self, path: &Path) -> bool {
32        if path.join("pdm.lock").exists() {
33            return true;
34        }
35        fs::read_to_string(path.join("pyproject.toml"))
36            .is_ok_and(|c| c.contains("[tool.pdm]") || c.contains("pdm.backend"))
37    }
38
39    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
40        PDM_ENV_DIRS
41            .iter()
42            .map(|name| (name, path.join(name)))
43            .filter(|(_, dir)| dir.is_dir())
44            .map(|(name, dir)| BloatDir {
45                name: (*name).to_string(),
46                path: dir.clone(),
47                size_bytes: dir_size(&dir),
48                shared_bytes: 0,
49            })
50            .collect()
51    }
52
53    /// `pdm lock --check` resolves `pyproject.toml` against `pdm.lock` and exits
54    /// non-zero when they have drifted apart, without writing either file. Plain
55    /// `pdm lock` is the write side.
56    fn enforce_lockfile(&self, path: &Path, policy: EnforcePolicy) -> Result<()> {
57        enforce_two_tier(
58            &path.join("pdm.lock"),
59            "pdm",
60            &["lock", "--check"],
61            &["lock"],
62            path,
63            policy,
64        )
65    }
66
67    fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
68        run_command_with_timeout("pdm", &["install"], path, timeout)
69    }
70
71    fn lockfiles(&self) -> &'static [&'static str] {
72        &["pdm.lock"]
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use tempfile::tempdir;
80
81    #[test]
82    fn detects_on_the_lockfile() {
83        let dir = tempdir().unwrap();
84        assert!(!Pdm.detect(dir.path()));
85        fs::write(dir.path().join("pdm.lock"), "").unwrap();
86        assert!(Pdm.detect(dir.path()));
87    }
88
89    #[test]
90    fn detects_on_a_pdm_pyproject() {
91        let dir = tempdir().unwrap();
92        fs::write(dir.path().join("pyproject.toml"), "[tool.pdm]\n").unwrap();
93        assert!(Pdm.detect(dir.path()));
94    }
95
96    #[test]
97    fn leaves_a_plain_pep621_project_alone() {
98        let dir = tempdir().unwrap();
99        fs::write(
100            dir.path().join("pyproject.toml"),
101            "[project]\nname = \"x\"\n",
102        )
103        .unwrap();
104        assert!(!Pdm.detect(dir.path()));
105    }
106
107    #[test]
108    fn claims_both_environment_layouts() {
109        let dir = tempdir().unwrap();
110        assert!(Pdm.bloat_dirs(dir.path()).is_empty());
111        fs::create_dir(dir.path().join(".venv")).unwrap();
112        fs::create_dir(dir.path().join("__pypackages__")).unwrap();
113        let names: Vec<String> = Pdm
114            .bloat_dirs(dir.path())
115            .into_iter()
116            .map(|b| b.name)
117            .collect();
118        assert_eq!(names, vec![".venv", "__pypackages__"]);
119    }
120}