@@ -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.
13261326pub 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