Skip to content

Commit 1a82a49

Browse files
committed
Find uv and PDM global installs in -g scans
scan -g, get -g, apply -g, rollback -g and vex -g looked for uv tool envs and uv-managed Pythons only at fixed paths, and never looked for PDM's global installs at all. On Windows every uv tool env was missed (uv uses %APPDATA%, not %LOCALAPPDATA%). On every OS, UV_TOOL_DIR, UV_PYTHON_INSTALL_DIR and XDG_DATA_HOME were ignored. `pdm add -g` packages in the global project venv, and packages in PDM-managed interpreters, were silently left out. Global discovery now follows each tool's own path rules: uv's override env vars and state dir, and PDM's platformdirs config/data dirs plus the global_project.path, python.install_root and venv.location settings and the global project's .pdm-python. Fixes #449 Fixes #451 Assisted-by: Claude Code:claude-opus-5-5
1 parent 597c034 commit 1a82a49

2 files changed

Lines changed: 556 additions & 66 deletions

File tree

‎crates/socket-patch-core/src/crawlers/python_crawler.rs‎

Lines changed: 226 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,8 +1321,8 @@ fn run_site_query() -> Option<String> {
13211321
/// Get global/system Python `site-packages` directories.
13221322
///
13231323
/// Queries `python3` for site-packages paths, then checks well-known system
1324-
/// locations including Homebrew, conda, uv tools, pipx venvs, pip --user,
1325-
/// etc.
1324+
/// locations including Homebrew, conda, uv tools and interpreters, pipx
1325+
/// venvs, PDM's global project and interpreters, pip --user, etc.
13261326
pub async fn get_global_python_site_packages() -> Vec<PathBuf> {
13271327
let mut results = Vec::new();
13281328
let mut seen = HashSet::new();
@@ -1498,48 +1498,10 @@ pub async fn get_global_python_site_packages() -> Vec<PathBuf> {
14981498
let miniconda = home_dir.join("miniconda3");
14991499
scan_well_known(&miniconda, "site-packages", &mut seen, &mut results).await;
15001500

1501-
// uv tools — platform-specific install root.
1502-
#[cfg(target_os = "macos")]
1503-
{
1504-
// Legacy/secondary location only: uv follows XDG conventions on
1505-
// macOS (`uv tool dir` → ~/.local/share/uv/tools, covered by the
1506-
// not(windows) scan below), but older layouts used the platform
1507-
// data dir, so keep scanning it too.
1508-
let uv_base = home_dir
1509-
.join("Library")
1510-
.join("Application Support")
1511-
.join("uv")
1512-
.join("tools");
1513-
let uv_matches =
1514-
find_python_dirs(&uv_base, &["*", "lib", "python3.*", "site-packages"]).await;
1515-
for m in uv_matches {
1516-
add_path(m, &mut seen, &mut results);
1517-
}
1518-
}
1519-
#[cfg(windows)]
1520-
{
1521-
// %LOCALAPPDATA%\uv\tools
1522-
if let Ok(local) = std::env::var("LOCALAPPDATA") {
1523-
let uv_base = PathBuf::from(local).join("uv").join("tools");
1524-
let uv_matches = find_python_dirs(&uv_base, &["*", "Lib", "site-packages"]).await;
1525-
for m in uv_matches {
1526-
add_path(m, &mut seen, &mut results);
1527-
}
1528-
}
1529-
}
1530-
#[cfg(not(windows))]
1531-
{
1532-
// uv uses XDG paths on BOTH Linux and macOS (`uv tool dir` →
1533-
// ~/.local/share/uv/tools; verified against a real uv install —
1534-
// macOS does NOT get an Application Support tool dir).
1535-
let uv_base = home_dir
1536-
.join(".local")
1537-
.join("share")
1538-
.join("uv")
1539-
.join("tools");
1540-
let uv_matches =
1541-
find_python_dirs(&uv_base, &["*", "lib", "python3.*", "site-packages"]).await;
1542-
for m in uv_matches {
1501+
// uv tool envs (`uv tool install`): one venv per tool under every
1502+
// root uv may use (see `uv_dir_candidates`).
1503+
for tools in uv_dir_candidates(&home_dir, "UV_TOOL_DIR", "tools") {
1504+
for m in find_child_env_site_packages(&tools).await {
15431505
add_path(m, &mut seen, &mut results);
15441506
}
15451507
}
@@ -1564,38 +1526,236 @@ pub async fn get_global_python_site_packages() -> Vec<PathBuf> {
15641526
}
15651527
}
15661528

1567-
// uv-managed Python interpreters (`uv python install 3.X`) live at:
1568-
// Linux/macOS: ~/.local/share/uv/python/cpython-3.X.*/lib/python3.X/site-packages/
1569-
// Windows: %LOCALAPPDATA%\uv\python\cpython-3.X.*\Lib\site-packages\
1570-
// The typical flow is `uv venv` + `uv pip install`, where the venv layout
1571-
// is already covered by `find_local_venv_site_packages`. But power users
1529+
// uv-managed Python interpreters (`uv python install 3.X`), one per
1530+
// child of uv's python dir (`cpython-3.X.*-<platform>`). The typical
1531+
// flow is `uv venv` + `uv pip install`, where the venv layout is
1532+
// already covered by `find_local_venv_site_packages`. But power users
15721533
// can install packages directly into the managed interpreter (e.g. via
1573-
// `<uv-python>/bin/pip install ...`), and globally-discovered crawls
1574-
// should surface those.
1534+
// `uv pip install --system --python <uv-python>`), and globally
1535+
// discovered crawls should surface those.
1536+
for python in uv_dir_candidates(&home_dir, "UV_PYTHON_INSTALL_DIR", "python") {
1537+
for m in find_child_env_site_packages(&python).await {
1538+
add_path(m, &mut seen, &mut results);
1539+
}
1540+
}
1541+
1542+
// PDM's global project (`pdm add -g`) and PDM-managed interpreters
1543+
// (`pdm python install`).
1544+
for m in pdm_global_site_packages(&home_dir).await {
1545+
add_path(m, &mut seen, &mut results);
1546+
}
1547+
1548+
results
1549+
}
1550+
1551+
/// `site-packages` of every environment directly under `parent`:
1552+
/// `<parent>/<env>/lib{,64}/python3.X/site-packages` on Unix and
1553+
/// `<parent>\<env>\Lib\site-packages` on Windows.
1554+
async fn find_child_env_site_packages(parent: &Path) -> Vec<PathBuf> {
1555+
#[cfg(not(windows))]
1556+
{
1557+
let mut matches =
1558+
find_python_dirs(parent, &["*", "lib", "python3.*", "site-packages"]).await;
1559+
matches
1560+
.extend(find_python_dirs(parent, &["*", "lib64", "python3.*", "site-packages"]).await);
1561+
matches
1562+
}
1563+
#[cfg(windows)]
1564+
{
1565+
find_python_dirs(parent, &["*", "Lib", "site-packages"]).await
1566+
}
1567+
}
1568+
1569+
/// `site-packages` of the one environment (venv or interpreter) whose
1570+
/// prefix is `prefix`, in the same layouts as
1571+
/// [`find_child_env_site_packages`].
1572+
async fn find_env_site_packages(prefix: &Path) -> Vec<PathBuf> {
1573+
#[cfg(not(windows))]
1574+
{
1575+
let mut matches = find_python_dirs(prefix, &["lib", "python3.*", "site-packages"]).await;
1576+
matches.extend(find_python_dirs(prefix, &["lib64", "python3.*", "site-packages"]).await);
1577+
matches
1578+
}
1579+
#[cfg(windows)]
1580+
{
1581+
find_python_dirs(prefix, &["Lib", "site-packages"]).await
1582+
}
1583+
}
1584+
1585+
/// `$var` as a directory when it is set to an absolute path. platformdirs
1586+
/// and uv both ignore a relative `XDG_*` value, per the XDG spec.
1587+
#[cfg(not(windows))]
1588+
fn absolute_env_dir(var: &str) -> Option<PathBuf> {
1589+
std::env::var_os(var)
1590+
.map(PathBuf::from)
1591+
.filter(|p| p.is_absolute())
1592+
}
1593+
1594+
/// The directories uv may keep `bucket` (`tools` or `python`) in, most
1595+
/// specific first.
1596+
///
1597+
/// uv (`StateStore::from_settings`) uses `$override_var` (`UV_TOOL_DIR`
1598+
/// or `UV_PYTHON_INSTALL_DIR`) when set, made absolute against the cwd.
1599+
/// Otherwise it uses `<data dir>/uv/<bucket>`, where the data dir is
1600+
/// `$XDG_DATA_HOME` (absolute only) or `~/.local/share` on Linux and
1601+
/// macOS, and `%APPDATA%` on Windows (`uv tool dir` prints
1602+
/// `%APPDATA%\uv\tools` there). The legacy roots that older layouts used
1603+
/// are returned too: `~/Library/Application Support/uv` on macOS, and
1604+
/// `%LOCALAPPDATA%\uv`, which earlier socket-patch releases scanned, on
1605+
/// Windows. Callers skip the ones that don't exist.
1606+
#[cfg_attr(windows, allow(unused_variables))]
1607+
fn uv_dir_candidates(home_dir: &Path, override_var: &str, bucket: &str) -> Vec<PathBuf> {
1608+
let mut dirs = Vec::new();
1609+
if let Some(dir) = std::env::var_os(override_var).filter(|v| !v.is_empty()) {
1610+
let dir = PathBuf::from(dir);
1611+
dirs.push(std::path::absolute(&dir).unwrap_or(dir));
1612+
}
15751613
#[cfg(not(windows))]
15761614
{
1577-
let uv_python = home_dir
1578-
.join(".local")
1579-
.join("share")
1615+
if let Some(xdg) = absolute_env_dir("XDG_DATA_HOME") {
1616+
dirs.push(xdg.join("uv").join(bucket));
1617+
}
1618+
dirs.push(
1619+
home_dir
1620+
.join(".local")
1621+
.join("share")
1622+
.join("uv")
1623+
.join(bucket),
1624+
);
1625+
}
1626+
#[cfg(target_os = "macos")]
1627+
dirs.push(
1628+
home_dir
1629+
.join("Library")
1630+
.join("Application Support")
15801631
.join("uv")
1581-
.join("python");
1582-
let uv_matches =
1583-
find_python_dirs(&uv_python, &["*", "lib", "python3.*", "site-packages"]).await;
1584-
for m in uv_matches {
1585-
add_path(m, &mut seen, &mut results);
1632+
.join(bucket),
1633+
);
1634+
#[cfg(windows)]
1635+
for var in ["APPDATA", "LOCALAPPDATA"] {
1636+
if let Some(base) = std::env::var_os(var).filter(|v| !v.is_empty()) {
1637+
dirs.push(PathBuf::from(base).join("uv").join(bucket));
15861638
}
15871639
}
1640+
dirs
1641+
}
1642+
1643+
/// The directories platformdirs may resolve for PDM's per-user config
1644+
/// (`xdg_var` = `XDG_CONFIG_HOME`, `unix_default` = `.config`) or data
1645+
/// (`XDG_DATA_HOME`, `.local/share`) dir, most specific first:
1646+
/// `$xdg_var/pdm` (absolute only; platformdirs 4.4+ also honors it on
1647+
/// macOS), `~/<unix_default>/pdm` on Linux,
1648+
/// `~/Library/Application Support/pdm` on macOS, and
1649+
/// `%LOCALAPPDATA%\pdm\pdm` on Windows.
1650+
#[cfg_attr(windows, allow(unused_variables))]
1651+
fn pdm_dir_candidates(home_dir: &Path, xdg_var: &str, unix_default: &Path) -> Vec<PathBuf> {
1652+
let mut dirs = Vec::new();
1653+
#[cfg(not(windows))]
1654+
if let Some(xdg) = absolute_env_dir(xdg_var) {
1655+
dirs.push(xdg.join("pdm"));
1656+
}
1657+
#[cfg(all(not(target_os = "macos"), not(windows)))]
1658+
dirs.push(home_dir.join(unix_default).join("pdm"));
1659+
#[cfg(target_os = "macos")]
1660+
dirs.push(
1661+
home_dir
1662+
.join("Library")
1663+
.join("Application Support")
1664+
.join("pdm"),
1665+
);
15881666
#[cfg(windows)]
1589-
{
1590-
if let Ok(local) = std::env::var("LOCALAPPDATA") {
1591-
let uv_python = PathBuf::from(local).join("uv").join("python");
1592-
let uv_matches = find_python_dirs(&uv_python, &["*", "Lib", "site-packages"]).await;
1593-
for m in uv_matches {
1594-
add_path(m, &mut seen, &mut results);
1667+
if let Some(local) = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty()) {
1668+
dirs.push(PathBuf::from(local).join("pdm").join("pdm"));
1669+
}
1670+
dirs
1671+
}
1672+
1673+
/// `site-packages` of PDM's global installs:
1674+
///
1675+
/// - The global project's environment (`pdm add -g`). The project lives
1676+
/// at the `global_project.path` setting, by default
1677+
/// `<user config dir>/pdm/global-project`. Its environment is the
1678+
/// in-project `.venv`, an out-of-tree venv under `venv.location`
1679+
/// (default `<user data dir>/pdm/venvs`) named
1680+
/// `<project dir name>-<hash>-<python>`, or whatever interpreter
1681+
/// `pdm use -g` recorded in its `.pdm-python`.
1682+
/// - PDM-managed interpreters (`pdm python install`), one per child of
1683+
/// `python.install_root` (default `<user data dir>/pdm/python`).
1684+
///
1685+
/// The settings come from PDM's global config file, `$PDM_CONFIG_FILE`
1686+
/// or `<user config dir>/pdm/config.toml`. Every candidate is collected,
1687+
/// and the ones that don't exist yield nothing.
1688+
async fn pdm_global_site_packages(home_dir: &Path) -> Vec<PathBuf> {
1689+
let config_dirs = pdm_dir_candidates(home_dir, "XDG_CONFIG_HOME", Path::new(".config"));
1690+
let data_dirs = pdm_dir_candidates(
1691+
home_dir,
1692+
"XDG_DATA_HOME",
1693+
&Path::new(".local").join("share"),
1694+
);
1695+
1696+
let mut config_files: Vec<PathBuf> = std::env::var_os("PDM_CONFIG_FILE")
1697+
.filter(|v| !v.is_empty())
1698+
.map(PathBuf::from)
1699+
.into_iter()
1700+
.collect();
1701+
config_files.extend(config_dirs.iter().map(|d| d.join("config.toml")));
1702+
1703+
let mut projects = Vec::new();
1704+
let mut install_roots = Vec::new();
1705+
let mut venv_roots = Vec::new();
1706+
let var = |name: &str| std::env::var(name).ok();
1707+
for file in &config_files {
1708+
let Ok(text) = read_regular_to_string(file).await else {
1709+
continue;
1710+
};
1711+
let Ok(doc) = text.parse::<toml_edit::DocumentMut>() else {
1712+
continue;
1713+
};
1714+
let setting = |table: &str, key: &str| {
1715+
doc.get(table)
1716+
.and_then(|t| t.get(key))
1717+
.and_then(|v| v.as_str())
1718+
.filter(|v| !v.is_empty())
1719+
.map(|v| expand_home(v, &var))
1720+
};
1721+
projects.extend(setting("global_project", "path"));
1722+
install_roots.extend(setting("python", "install_root"));
1723+
venv_roots.extend(setting("venv", "location"));
1724+
}
1725+
projects.extend(config_dirs.iter().map(|d| d.join("global-project")));
1726+
install_roots.extend(data_dirs.iter().map(|d| d.join("python")));
1727+
venv_roots.extend(data_dirs.iter().map(|d| d.join("venvs")));
1728+
1729+
let mut results = Vec::new();
1730+
for project in &projects {
1731+
results.extend(find_env_site_packages(&project.join(".venv")).await);
1732+
if let Some(name) = project.file_name().and_then(|n| n.to_str()) {
1733+
let prefix = format!("{name}-");
1734+
for root in &venv_roots {
1735+
for entry in crate::utils::fs::list_dir_entries(root).await {
1736+
if entry.file_name().to_string_lossy().starts_with(&prefix) {
1737+
results.extend(find_env_site_packages(&root.join(entry.file_name())).await);
1738+
}
1739+
}
1740+
}
1741+
}
1742+
// `.pdm-python` names the interpreter: `<prefix>/bin/python3` in a
1743+
// venv or Unix install, `<prefix>\Scripts\python.exe` in a Windows
1744+
// venv, `<prefix>\python.exe` in a Windows install.
1745+
if let Ok(text) = read_regular_to_string(&project.join(".pdm-python")).await {
1746+
let interpreter = PathBuf::from(text.trim());
1747+
let prefixes = interpreter.is_absolute().then_some(&interpreter);
1748+
for prefix in prefixes
1749+
.into_iter()
1750+
.flat_map(|i| i.ancestors().skip(1).take(2))
1751+
{
1752+
results.extend(find_env_site_packages(prefix).await);
15951753
}
15961754
}
15971755
}
1598-
1756+
for root in &install_roots {
1757+
results.extend(find_child_env_site_packages(root).await);
1758+
}
15991759
results
16001760
}
16011761

0 commit comments

Comments
 (0)