Axle-Runtime - Python Wheel enhancement library
This is a companion project to Wheel Axle/bdist_axle
Problem
- Python wheels do not support symlinks.
- PIP installation procedure is not locally extensible and does not allow adding post-install hooks.
Solution
WARNING: THIS IS EXPERIMENTAL BETA SOFTWARE. THERE ARE NO WARRANTIES OF ANY KIND. USE AT YOUR OWN RISK. ADDITIONAL INCLUDED DISCLAIMERS ALSO APPLY.
Wheel Axle Runtime library gets site.py to execute installed code on Python interpreter startup, facilitating the
"post-install hook" functionality. Specially-crafted wheels carry two startup files, and whichever one the running
interpreter honors calls into this library:
<distribution name and version>.pth, whoseimportlinesite.pyexecutes up to Python 3.17<distribution name and version>.start, a PEP 829 startup entry point file honored from Python 3.15 on
Python Invariants
Up to Python 3.14 the core functionality relied on the following Python behaviors:
site.pyprocesses .pth filessite.pyexecutes .pth import lines- the
.pthfile's line is executed in a frame exposing the.pthfile path as a local variablefullname
The first two of those had not changed for 18 years when PEP 829 deprecated import line execution in Python 3.15.
The transition it defines is:
| Python | .pth import lines |
.start entry points |
|---|---|---|
| up to 3.14 | executed | ignored |
| 3.15 - 3.17 | executed, unless a matching .start file exists |
executed |
| 3.18 - 3.19 | silently ignored | executed |
| 3.20+ | ignored with a warning | executed |
PEP 829 leaves a startup entry point with no way of learning which distribution asked for it: it is called with no
arguments, and it explicitly
rejects passing any, while the name of a startup file is not
constrained to the distribution that installed it. What both kinds of startup file do have in common is that site
runs them while holding the path of the file being processed in a local variable of the calling frame - fullname up
to Python 3.14, filename from Python 3.15 on. That path, and nothing else, is what ties the call to a distribution,
so each startup file only ever finalizes the distribution it was installed with.
Compatibility
Wheels built by bdist_axle before 0.0.13 carry a .pth line referencing fullname, a local variable that
Python 3.15 no longer provides. On Python 3.15 those wheels fail at every interpreter startup with
NameError: name 'fullname' is not defined and their symlinks are never created. Such wheels have to be rebuilt with
wheel-axle 0.0.13 or later - there is no way to repair them at runtime, as the distributions that are affected are
precisely the ones whose startup files this library never gets to run.
Implementation
Once the distribution-specific startup file is activated by the Python interpreter, the Wheel Axle Runtime behaves as follows:
- The library checks whether a file
.dist-info/axle.doneexists. If it does it is the indication that the post-install hook has executed successfully and nothing more is to be done, terminating all further processing. - A process-wide inter-thread lock is acquired.
- An OS-wide inter-process file lock is acquired on a file
.dist-info/axle.lck. - Once the locks are acquired the
.dist-info/axle.doneexistence is rechecked (double-checked locking optimization). - Now that in-process and inter-process race conditions are excluded the post-install work can begin.
- Registered
installersare run in sequence. Installers should be idempotent. The following installers are currently implemented:- LibPython installer checks for the presence of
.dist-info/require-libpython.- The installer determines the location of the installation: venv, user or other.
- The list of all libpython library files is located from the
sys.base_exec_prefix. - If the installation is either venv or user and the link to the libpython library doesn't exist the symlink is created.
- Symlinks installer processes
.dist-info/symlinks.txt, if any.- Based on the location of the distribution being finalized the current installation
schemaand its paths are determined. Currently, installation into a virtual environment or user location is supported and tested. - For each symlink the target path is resolved and
realpathis used to determine the final target path. - If the symlink path and symlink target path are within one of the permitted schema locations the symlink is created. Otherwise, an exception is raised and the processing is aborted.
- After all symlinks are created, the
.dist-info/RECORDfile is updated to reflect the created symlinks.
- Based on the location of the distribution being finalized the current installation
- Axle installer finalizes the installation. This installer is always executed last.
- The
.dist-info/RECORDis updated with.dist-info/axle.donefile record. .dist-info/axle.doneis created.<distribution name and version>.pthand<distribution name and version>.startare then removed. If a file cannot be removed it is left in place. This can happen on Windows, since the files in question are likely opened for exclusive reading on Windows.
- The
- LibPython installer checks for the presence of
- Any failure anywhere in the above process will result in an abort, an error message, and a retry the next time the startup file will be activated.
Security
There are several security requirements and implications of having post-install hooks implemented this way.
- The installation requires write permissions to the distribution. This will be a problem if the package is installed
as
rootin locations such as/usror/usr/local, or is otherwise not write-permitted, unless the post-install hook is also ran with the sufficient privileges. This is generally acceptable as the primary use is considered to be installation into virtual envs and user locations. That said, simply runningpython -c passor any other python invocation that does activatesite.pyunder the required privileges will finalize post-install procedures. - There is an attempt to ensure that that axle wheels symlinks and targets don't extend beyond the allowed
schemalocations. Those attempts are superficial and have not been formally verified. For example, it may be possible to escape the path validation/confinement by:- hacking symlink creation order
- hacking symlink directory targets
- exploiting OS-specific
realpathimplementation idiosyncrasies (i.e.strictvs not, and what is considered strict)
TODOs
- Support schema detection for
prefixinstallations. - Validate and verify Windows support.
Metadata
Release files for wheel-axle-runtime 0.0.13
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| wheel_axle_runtime-0.0.13.tar.gz | 20.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wheel_axle_runtime-0.0.13-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 36.9 kB
Release files / wheel_axle_runtime-0.0.13.tar.gz
| Download URL | wheel_axle_runtime-0.0.13.tar.gz |
|---|---|
| Size | 20.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b2132bf83bcdeb66370506a2c72e8ad1197432dcc0030f33a038d3533966931d
|
|
BLAKE2b-256 checksum How to use checksums |
e1127f3227d8aa80370c5550127ca4ab45e57cd88b60d0cb1bc6c7b915d1331a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / wheel_axle_runtime-0.0.13-py3-none-any.whl
| Download URL | wheel_axle_runtime-0.0.13-py3-none-any.whl |
|---|---|
| Size | 16.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b27bb8bb34fdeced7b373286bcd43e4f425b21a330b0619b6c52fc9c16855a28
|
|
BLAKE2b-256 checksum How to use checksums |
5ab23d41ce3639f952b50e4da22269ec663f1524c69a16fc7b6ca0549f4d3168
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|