-
Notifications
You must be signed in to change notification settings - Fork 0
1.11 Step | InstallPythonRequirements
Installs the pip dependencies into the virtualenv. It lives in
src/steps/install_python_requirements.rs. The third and last of the source sub-steps. Every install runs as the odoo user, through the venv'spip.Its peculiarity: it has no undo of its own.
The pip packages live inside <install_dir>/sandbox (the venv). If the venv is ours, the undo of
1.10 CreateVirtualenv (rm -rf sandbox) removes them all. Therefore:
this step's
undo→ a documented NO-OP. Uninstalling the packages one by one would be redundant and fragile: removing the venv is the correct undo.
One undo fewer to write, for the right reason. And if the venv was Preexisting, we do not touch
somebody else's venv's packages either.
| Phase | Behaviour |
|---|---|
| snapshot | lightweight: the state relevant to the rollback is the venv's (1.10), not a per-package PreState
|
| run | reads requirements.txt (missing → error), then the pip sequence below. dry_run → log |
| undo | NO-OP (see above) |
Cython 3 removed Python 2's long type; gevent (required by Odoo 18) still uses that code in its
.pyx files. pip builds wheels in an isolated environment that ignores the venv's Cython. The
solution (faithful to Bash) is a precise sequence:
1. pip install --upgrade pip wheel "setuptools<81" # setuptools is NOT decorative, see below
2. pip install "Cython<3" # into the venv
3. pip install --no-build-isolation -r <gevent+greenlet lines>
4. pip install --prefer-binary -r <requirements without those lines>
Why
setuptoolsat step 1. From Python 3.12venvno longer seeds it, but step 3 uses--no-build-isolation, i.e. it asks pip to build with what it finds in the venv. Without it the build backend does not exist and pip dies withBackendUnavailable. The systempython3-setuptoolsis irrelevant: the venv is isolated.
Why it is bounded, since 3.2.0. What this step owes the venv is that setuptools exists, never that it is the newest — and the difference stopped being academic when setuptools 82 removed
pkg_resources, which Odoo 16 imports on its first line. An unbounded--upgradewalked into a virtualenv that already worked (Ubuntu 22.04 seeds a setuptools that has the module) and replaced it with one that did not, soinitialize-odoo-databasedied on the first line of Odoo that ever runs.The bound sits below 81, not 82: 81 keeps the module but warns on every import — twice per Odoo 16 start, in a stable branch that does not filter it — and setuptools' own message names the pin that stops it, so the number is read rather than picked.
It is unconditional on purpose. Applying it only where needed would mean either reading the cloned sources, which cannot tell importing
pkg_resourcesfrom mentioning it (Odoo 17 names it inside atry/except, Odoo 19 only inside warning filters), or a table keyed on the Odoo version — a second source of truth of the kind that diverges in silence. With no discriminant there is nothing that can answer wrongly, and the ceiling costs nothing elsewhere: this setuptools serves our build without isolation and nothing else, since pip's isolated builds fetch their own.
- The gevent and greenlet lines are handed to pip in a requirements file, verbatim: see below — that is the part that took two rounds of CI to get right.
- Step 4 uses a filtered requirements file without those lines, otherwise pip would rebuild them from source in the isolated environment, ignoring Cython<3.
Odoo 18's requirements.txt does not pin one gevent version. It pins four, one per Python
version, and as many greenlets — with environment markers, and annotated by Odoo itself with the Ubuntu
release:
gevent==21.8.0 ; … python_version == '3.10' # (Jammy)
gevent==24.2.1 ; … python_version >= '3.12' and < '3.13' # (Noble)
greenlet==1.1.2 ; … python_version == '3.10' # (Jammy)
greenlet==3.0.3 ; … python_version >= '3.12' and < '3.13' # (Noble)
The first version of this step took the first line starting with gevent and threw its marker
away. On Ubuntu 22.04 that is the right line by coincidence (Python 3.10 comes first); on 24.04 it
still picked Jammy, and the CI died like this:
gevent → _greenlet_primitives.c:273: fatal error: longintrepr.h: No such file
greenlet → PyThreadState has no member 'recursion_limit'
longintrepr.h was made private in Python 3.12 and PyThreadState's fields renamed: gevent 21.8.0 and
greenlet 1.1.x predate 3.12 and cannot compile against it. It was not the build environment: it was the
wrong version.
The marker was thrown away because of a self-inflicted constraint. It is true that
--no-build-isolationdoes not tolerate a marker on argv — but passing a requirements file keeps the markers, and it is pip that evaluates them: the software whose job that is exactly. We stop choosing.
greenlet is in the group alongside gevent because installing gevent alone made pip resolve greenlet
from gevent's metadata (greenlet>=1.1,<2) instead of from Odoo's pin — which is how greenlet
1.1.x ended up compiling against Python 3.12.
A welcome side effect: with the right version, Noble has a prebuilt wheel
(gevent-24.2.1-cp312-manylinux…), so nothing is compiled at all and --no-build-isolation stays
inert. The Cython<3 workaround does its work where it is genuinely needed — Jammy, where gevent 21.8.0
has no wheel.
This is not a leftover: it is a real fix. extract_gevent_spec and filter_out_gevent are pure
functions, tested separately; the pip sequence is verified in order.
pip puts its cache in $HOME/.cache, and the odoo user's $HOME is /opt/odoo — the directory
the installer marks Preexisting and never touches when it finds it already there. Measured
result in CI: after a complete rollback, /opt/odoo/.cache was still there. Regenerable caches, but
the promise is “the system goes back to exactly what it was”.
The correction is preventive, not a cleanup: every invocation passes
--cache-dir <install_dir>/sandbox/.pip-cache, i.e. inside the venv, which
1.10 CreateVirtualenv's undo removes wholesale with an rm -rf.
Nothing is born outside the perimeter, so nothing has to be chased with deletion heuristics inside the
customer's home. And it remains a cache: a second run of the installer finds it and does not re-download
the wheels.
Why not
<install_dir>/.pip-cache, one level up: 1.9 CloneOdooRepo's undo removesinstall_dironly if empty. A cache there would block it — the leftover would move one level instead of disappearing.
It covers pip, not everything. The first installation to reach the end in CI showed that
/opt/odoo/.cachecomes back regardless:odoo-binand the service write into theodoouser's home too (fontconfig, andpip'sselfcheckup to version 23, which ignores--cache-dir). Chasing the producers is a losing battle; the general cover is 1.3b SetupCacheDir, which owns the directory. This--cache-dirstays because it still keeps hundreds of MB of wheels out of the home.
The two files handed to pip --requirement (the gevent/greenlet lines, and the filtered remainder) are
born inside <install_dir>/sandbox, not in /tmp.
The reason is precise: here root writes a file that pip reads as the odoo user — two distinct
operations, with a window in between. In /tmp, which is world-writable, and with a fixed name written
in the source, anyone controlling any local account can replace that file within the window and have
pip install arbitrary packages into the venv: code execution as the owner of the filestore and the
database. It is not the symlink case — which fs.protected_symlinks mitigates — but replacement of the
contents, and there the kernel does not help.
sandbox removes the premise instead of defending against the attack: it belongs to odoo and is not
writable by third parties, so no third party can create or replace anything inside it. On top of that
the file is born and dies inside the reversible perimeter — CreateVirtualenv's undo does
rm -rf sandbox — so an interrupted run leaves no residue outside.
The unpredictable name and the fail-closed creation (O_EXCL | O_NOFOLLOW) remain regardless. The final
chown is not decorative: the file is born 0600 root and the one who has to read it is odoo.
Odoo pins gevent and greenlet per Python version. If the interpreter is newer than all its pins, there is no prebuilt wheel for that version: pip builds from source, and the C generated by Cython does not survive a newer CPython's headers. It is not a compiler problem nor a missing system package — it is the version, and no build flag gets around it.
Two things happen then, and neither is a refusal:
- The preflight warns, before the confirmation, saying which interpreter is there and what will break.
- If the build does fail, the error puts the cause in front of pip's output — which is kept in full, because explaining is not hiding the evidence — and shows the lines that Odoo version declares, so it is immediately visible that there is none for this interpreter.
Corrected in 3.2.0, and the correction is the interesting part. That message used to appear only on a Python newer than the newest one tested. The case it had been written for turned out to happen on exactly the newest tested one: Odoo 16 on Fedora builds on Python 3.13 — equal, not newer — and dies precisely this way. So the explanation was silent on the very failure it existed for, and the reader got three hundred lines of
gccand nothing else.The question was wrong rather than the threshold. “Is this Python newer than the ones we test?” is about us; what breaks the build is “does this Odoo pin a wheel for this interpreter?”, which varies per Odoo version. Now there are two registers: above the tested Python the message asserts the cause, as before; otherwise it states the facts — the interpreter the venv is on, and the lines that Odoo version declares — and draws no conclusion. Facts cannot be wrong, which is what keeps the original objection satisfied: if the build failed for a missing compiler instead, nothing there has misled anyone.
The question “does Odoo have a pin for this Python?” cannot be answered from requirements.txt: the
markers are open upwards, so the applicable line is applicable and pip picks it correctly. That the
wheel is missing for that interpreter is a fact about PyPI, not about the file. That is why the
installer does not pre-empt this with a check that would pretend to know: it explains at the moment
the failure occurs.
The real way out is upstream — building the venv on an interpreter the pins cover, which is what the preflight does where the distribution packages one: see Multi-distribution support. Deviating from Odoo's pins is not among the options: it would be a combination nobody has tested.
- Every install goes through
sudo -u odoo <venv>/bin/pip(least privilege), with--cache-dirinside the venv. - The snapshot serialises only an “installed” flag for the summary — it drives no undo.
- Tests: no-op undo (no uninstall; removing pip's work belongs to 1.10), the gevent/Cython sequence
in order, missing
requirements.txt→ error, pure gevent extraction and filtering.
Start here
Key concepts
References
For developers
Technical detail — how it works inside
Steps:
- 1.1 PrepareOptRoot
- 1.2 CreateOdooUser
- 1.3 SetupLogDir
- 1.3b SetupCacheDir
- 1.4 AptPackages (delta)
- 1.5 InstallWkhtmltopdf
- 1.6 SetupPostgres
- 1.7 CreateDbRole
- 1.8 CreateDatabase
- 1.9 CloneOdooRepo
- 1.10 CreateVirtualenv
- 1.11 InstallPythonRequirements
- 1.12 GenerateConfig
- 1.12b SetupDataDir
- 1.13 InitializeOdooDatabase
- 1.14 SetupSystemd
- 1.15 Nginx (6 sub-steps)
- 1.16 WriteControlScript + PatchBashrc
Cross-cutting: