Skip to content

Commit 816747e

Browse files
authored
✨ feat(api): add site_bin_dir property (#443)
Applications that install system-wide executables need a standard location that mirrors `user_bin_dir`. Currently, `user_bin_dir` provides `~/.local/bin` on Unix/macOS and `%LOCALAPPDATA%\Programs` on Windows, but there's no corresponding site-wide equivalent. This creates API inconsistency since all other directory types (data, config, cache, state, log, runtime) have both user and site variants. ✨ Package managers like Chocolatey, pip, and uv need a consistent answer for where to install system-wide binaries. The implementation follows platform conventions researched from official documentation. Unix/Linux uses `/usr/local/bin` per [FHS 3.0](https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html), which designates this path for locally-installed software distinct from distribution packages in `/usr/bin`. macOS uses `/usr/local/bin` as the standard Homebrew and user installation location, since `/usr/bin` is read-only on modern macOS per [Apple's documentation](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html). Windows uses `%ProgramData%\bin` to mirror the `site_data_dir` pattern, following [Chocolatey's precedent](https://docs.chocolatey.org/en-us/choco/setup) of using `%ProgramData%\Chocolatey\bin` for system-wide package binaries. Android aliases to `user_bin_dir` since the platform has no system-wide installation concept per [Android's storage documentation](https://developer.android.com/guide/topics/data/data-storage). 🔍 The change also implements `use_site_for_root` support on Unix, allowing `user_bin_dir` to redirect to `site_bin_dir` when running as root. This matches the behavior of other `user_*` properties and provides a consistent experience for tools that need to install binaries differently based on privilege level. Closes #434
1 parent 7a47ac4 commit 816747e

13 files changed

Lines changed: 79 additions & 0 deletions

File tree

docs/platforms.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,30 @@ Default paths
341341
This property does not append ``appname`` or ``version``. It returns the directory
342342
where user-installed executables and scripts are placed.
343343

344+
``site_bin_dir``
345+
~~~~~~~~~~~~~~~~
346+
347+
.. list-table::
348+
:widths: 20 80
349+
350+
* - Linux
351+
- ``/usr/local/bin``
352+
* - macOS
353+
- ``/usr/local/bin``
354+
* - Windows
355+
- ``C:\ProgramData\bin``
356+
* - Android
357+
- Same as ``user_bin_dir``
358+
359+
.. note::
360+
361+
This property does not append ``appname`` or ``version``. It returns the directory
362+
where system-wide executables and scripts are placed. On Unix/Linux, this follows
363+
the `FHS 3.0 <https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html>`_
364+
standard for locally-installed software. On Windows, it mirrors the ``site_data_dir``
365+
pattern using ``%ProgramData%``, following the precedent set by
366+
`Chocolatey <https://docs.chocolatey.org/en-us/choco/setup>`_.
367+
344368
macOS
345369
-----
346370

src/platformdirs/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,11 @@ def user_bin_dir() -> str:
340340
return PlatformDirs().user_bin_dir
341341

342342

343+
def site_bin_dir() -> str:
344+
""":returns: bin directory shared by users"""
345+
return PlatformDirs().site_bin_dir
346+
347+
343348
def user_applications_dir() -> str:
344349
""":returns: applications directory tied to the user"""
345350
return PlatformDirs().user_applications_dir
@@ -698,6 +703,11 @@ def user_bin_path() -> Path:
698703
return PlatformDirs().user_bin_path
699704

700705

706+
def site_bin_path() -> Path:
707+
""":returns: bin path shared by users"""
708+
return PlatformDirs().site_bin_path
709+
710+
701711
def user_applications_path() -> Path:
702712
""":returns: applications path tied to the user"""
703713
return PlatformDirs().user_applications_path
@@ -777,6 +787,8 @@ def site_runtime_path(
777787
"__version_info__",
778788
"site_applications_dir",
779789
"site_applications_path",
790+
"site_bin_dir",
791+
"site_bin_path",
780792
"site_cache_dir",
781793
"site_cache_path",
782794
"site_config_dir",

src/platformdirs/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"user_videos_dir",
1717
"user_music_dir",
1818
"user_bin_dir",
19+
"site_bin_dir",
1920
"user_applications_dir",
2021
"user_runtime_dir",
2122
"site_data_dir",

src/platformdirs/android.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ def user_bin_dir(self) -> str:
118118
""":return: bin directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/files/bin``"""
119119
return os.path.join(cast("str", _android_folder()), "files", "bin") # noqa: PTH118
120120

121+
@property
122+
def site_bin_dir(self) -> str:
123+
""":return: bin directory shared by users, same as `user_bin_dir`"""
124+
return self.user_bin_dir
125+
121126
@property
122127
def user_applications_dir(self) -> str:
123128
""":return: applications directory tied to the user, same as `user_data_dir`"""

src/platformdirs/api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ def user_desktop_dir(self) -> str:
209209
def user_bin_dir(self) -> str:
210210
""":return: bin directory tied to the user"""
211211

212+
@property
213+
@abstractmethod
214+
def site_bin_dir(self) -> str:
215+
""":return: bin directory shared by users"""
216+
212217
@property
213218
@abstractmethod
214219
def user_applications_dir(self) -> str:
@@ -314,6 +319,11 @@ def user_bin_path(self) -> Path:
314319
""":return: bin path tied to the user"""
315320
return Path(self.user_bin_dir)
316321

322+
@property
323+
def site_bin_path(self) -> Path:
324+
""":return: bin path shared by users"""
325+
return Path(self.site_bin_dir)
326+
317327
@property
318328
def user_applications_path(self) -> Path:
319329
""":return: applications path tied to the user"""

src/platformdirs/macos.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ def user_bin_dir(self) -> str:
135135
""":return: bin directory tied to the user, e.g. ``~/.local/bin``"""
136136
return os.path.expanduser("~/.local/bin") # noqa: PTH111
137137

138+
@property
139+
def site_bin_dir(self) -> str:
140+
""":return: bin directory shared by users, e.g. ``/usr/local/bin``"""
141+
return "/usr/local/bin"
142+
138143
@property
139144
def user_applications_dir(self) -> str:
140145
""":return: applications directory tied to the user, e.g. ``~/Applications``"""

src/platformdirs/unix.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ def user_bin_dir(self) -> str:
140140
""":return: bin directory tied to the user, e.g. ``~/.local/bin``"""
141141
return os.path.expanduser("~/.local/bin") # noqa: PTH111
142142

143+
@property
144+
def site_bin_dir(self) -> str:
145+
""":return: bin directory shared by users, e.g. ``/usr/local/bin``"""
146+
return "/usr/local/bin"
147+
143148
@property
144149
def user_applications_dir(self) -> str:
145150
""":return: applications directory tied to the user, e.g. ``~/.local/share/applications``"""
@@ -266,6 +271,11 @@ def user_runtime_dir(self) -> str:
266271
""":return: runtime directory tied to the user, or site equivalent when root with ``use_site_for_root``"""
267272
return self.site_runtime_dir if self._use_site else super().user_runtime_dir
268273

274+
@property
275+
def user_bin_dir(self) -> str:
276+
""":return: bin directory tied to the user, or site equivalent when root with ``use_site_for_root``"""
277+
return self.site_bin_dir if self._use_site else super().user_bin_dir
278+
269279

270280
def _get_user_media_dir(env_var: str, fallback_tilde_path: str) -> str:
271281
if media_dir := _get_user_dirs_folder(env_var):

src/platformdirs/windows.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ def user_bin_dir(self) -> str:
146146
""":return: bin directory tied to the user, e.g. ``%LOCALAPPDATA%\\Programs``"""
147147
return os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Programs")) # noqa: PTH118
148148

149+
@property
150+
def site_bin_dir(self) -> str:
151+
""":return: bin directory shared by users, e.g. ``C:\\ProgramData\\bin``"""
152+
return os.path.normpath(os.path.join(get_win_folder("CSIDL_COMMON_APPDATA"), "bin")) # noqa: PTH118
153+
149154
@property
150155
def user_applications_dir(self) -> str:
151156
""":return: applications directory tied to the user, e.g. ``Start Menu\\Programs``"""

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"user_videos_dir",
2020
"user_music_dir",
2121
"user_bin_dir",
22+
"site_bin_dir",
2223
"user_applications_dir",
2324
"user_runtime_dir",
2425
"site_data_dir",

tests/test_android.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def test_android(mocker: MockerFixture, params: dict[str, Any], func: str) -> No
6262
"user_music_dir": "/storage/emulated/0/Music",
6363
"user_desktop_dir": "/storage/emulated/0/Desktop",
6464
"user_bin_dir": "/data/data/com.example/files/bin",
65+
"site_bin_dir": "/data/data/com.example/files/bin",
6566
"user_applications_dir": f"/data/data/com.example/files{suffix}",
6667
"site_applications_dir": f"/data/data/com.example/files{suffix}",
6768
"user_runtime_dir": f"/data/data/com.example/cache{suffix}{'' if not params.get('opinion', True) else val}",

0 commit comments

Comments
 (0)