Skip to content

1.11 Step | InstallPythonRequirements

Omisen edited this page Aug 4, 2026 · 8 revisions

Installa le dipendenze pip nel virtualenv. Vive in src/steps/install_python_requirements.rs. Terzo e ultimo dei sotto-step dei sorgenti. Tutte le install come utente odoo, dal pip del venv.

La sua particolarità: non ha un undo proprio.


Il cuore concettuale: nessun undo proprio

I pacchetti pip vivono dentro <install_dir>/sandbox (il venv). Se il venv è nostro, l'undo di 1.10 CreateVirtualenv (rm -rf sandbox) li rimuove tutti. Quindi:

undo di questo step → NO-OP documentato. Disinstallare i pacchetti uno a uno sarebbe ridondante e fragile: la rimozione del venv è l'undo corretto.

Un undo in meno da scrivere, per la ragione giusta. Se il venv era Preexisting, non tocchiamo comunque i pacchetti di un venv altrui.


Ciclo di vita

Fase Comportamento
snapshot leggero: lo stato rilevante per il rollback è quello del venv (1.10), non un PreState per-pacchetto
run legge requirements.txt (assente → errore), poi la sequenza pip qui sotto. dry_run → log
undo NO-OP (vedi sopra)

Workaround Cython/gevent (fix reale, da preservare)

Cython 3 ha rimosso il tipo long di Python 2; gevent (richiesto da Odoo 18) usa ancora quel codice nei .pyx. pip costruisce le wheel in un ambiente isolato che ignora il Cython del venv. La soluzione (fedele al Bash) è una sequenza precisa:

1. pip install --upgrade pip wheel setuptools          # setuptools NON è decorativo, vedi sotto
2. pip install "Cython<3"                              # nel venv
3. pip install --no-build-isolation -r <righe gevent+greenlet>
4. pip install --prefer-binary -r <requirements senza quelle righe>

Perché setuptools al passo 1. Da Python 3.12 venv non lo semina più, ma il passo 3 usa --no-build-isolation, cioè chiede a pip di costruire con ciò che trova nel venv. Senza, il backend di build non esiste e pip muore con BackendUnavailable. Il python3-setuptools di sistema non c'entra: il venv è isolato.

  • Le righe di gevent e greenlet vengono passate a pip in un file di requirements, verbatim: vedi sotto, è la parte che ha richiesto due giri di CI per venire giusta.
  • Il passaggio 4 usa un requirements filtrato senza quelle righe, altrimenti pip le ricompilerebbe da sorgente nell'env isolato, ignorando il Cython<3.

Quale gevent: lo decide pip, non noi (A-R6-3)

Il requirements.txt di Odoo 18 non pinna una versione di gevent. Ne pinna quattro, una per versione di Python, e altrettante di greenlet — con i marker d'ambiente, e annotate da Odoo stesso con la release Ubuntu:

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)

La prima versione di questo step prendeva la prima riga che iniziasse con gevent e ne buttava via il marker. Su Ubuntu 22.04 è la riga giusta per coincidenza (Python 3.10 è la prima); su 24.04 sceglieva ancora Jammy, e la CI moriva così:

gevent   → _greenlet_primitives.c:273: fatal error: longintrepr.h: No such file
greenlet → PyThreadState has no member 'recursion_limit'

longintrepr.h è stato reso privato in Python 3.12 e i campi di PyThreadState rinominati: gevent 21.8.0 e greenlet 1.1.x sono anteriori a 3.12 e non possono compilarci contro. Non era l'ambiente di build: era la versione sbagliata.

Il marker si buttava via per un vincolo autoinflitto. È vero che --no-build-isolation non tollera un marker su argv — ma passando un file di requirements i marker restano, e a valutarli è pip: il software il cui mestiere è esattamente quello. Noi smettiamo di scegliere.

greenlet sta nel gruppo insieme a gevent perché installando gevent da solo pip risolveva greenlet dalla metadata di gevent (greenlet>=1.1,<2) invece che dal pin di Odoo — ed è così che greenlet 1.1.x finiva a compilare contro Python 3.12.

Effetto collaterale gradito: con la versione giusta, su Noble esiste la wheel precompilata (gevent-24.2.1-cp312-manylinux…), quindi non si compila affatto e --no-build-isolation resta inerte. Il workaround Cython<3 serve dove serve davvero — Jammy, dove per gevent 21.8.0 la wheel non esiste.

Non è un residuo: è un fix reale. extract_gevent_spec e filter_out_gevent sono funzioni pure, testate a parte; la sequenza pip è verificata in ordine.


La cache di pip vive nel nostro perimetro (A-R5-3)

pip mette la sua cache in $HOME/.cache, e l'$HOME dell'utente odoo è /opt/odoo — la directory che l'installer, se la trova già esistente, marca Preexisting e non tocca mai. Risultato misurato in CI: dopo un rollback completo, /opt/odoo/.cache restava lì. Cache rigenerabili, ma la promessa è «il sistema torna esattamente com'era».

La correzione è preventiva, non una pulizia: ogni invocazione passa --cache-dir <install_dir>/sandbox/.pip-cache, cioè dentro il venv, che l'undo di 1.10 CreateVirtualenv rimuove per intero con un rm -rf. Niente nasce fuori dal perimetro, quindi non serve inseguire nulla con euristiche di cancellazione dentro la home del cliente. E resta una cache: un secondo giro dell'installer la ritrova e non riscarica le wheel.

Perché non <install_dir>/.pip-cache, un livello più su: l'undo di 1.9 CloneOdooRepo rimuove install_dir solo se vuota. Una cache lì la bloccherebbe — il residuo si sposterebbe di un livello invece di sparire.

Copre pip, non tutto. La prima installazione arrivata in fondo in CI ha mostrato che /opt/odoo/.cache rinasce comunque: nella home dell'utente odoo scrivono anche odoo-bin e il servizio (fontconfig, e il selfcheck di pip fino alla 23, che ignora --cache-dir). Inseguire i produttori è una battaglia che si perde; la copertura generale è 1.3b SetupCacheDir, che possiede la directory. Questo --cache-dir resta perché tiene comunque fuori dalla home le centinaia di MB di wheel.


Note di design

  • Tutte le install via sudo -u odoo <venv>/bin/pip (privilegio minimo), con --cache-dir nel venv.
  • Snapshot serializza solo un flag "installato" per il riepilogo — non guida alcun undo.
  • Test: undo no-op (nessuna disinstallazione, la rimozione pip è di 1.10), sequenza gevent/Cython in ordine, requirements.txt mancante → errore, estrazione/filtro gevent puri.

Dove nascono i file di requirements temporanei

I due file che passiamo a pip --requirement (le righe gevent/greenlet, e il resto filtrato) nascono dentro <install_dir>/sandbox, non in /tmp.

Il motivo è preciso: qui root scrive un file che pip legge come utente odoo — due operazioni distinte, con una finestra in mezzo. In /tmp, che è world-writable, e con un nome fisso scritto nel sorgente, chi controlla un utente locale qualsiasi può sostituire quel file nella finestra e far installare a pip pacchetti arbitrari nel venv: esecuzione di codice come il proprietario del filestore e del database. Non è il caso del symlink — che fs.protected_symlinks mitiga — ma la sostituzione del contenuto, e lì il kernel non aiuta.

sandbox toglie il presupposto invece di difendersi dall'attacco: è di proprietà di odoo e non è scrivibile da terzi, quindi nessun terzo può creare o sostituire nulla al suo interno. In più il file nasce e muore dentro il perimetro reversibile — l'undo di CreateVirtualenv fa rm -rf sandbox — quindi un'esecuzione interrotta non lascia residui fuori.

Restano comunque il nome imprevedibile e la creazione fail-closed (O_EXCL | O_NOFOLLOW). Il chown finale non è decorativo: il file nasce 0600 root e chi deve leggerlo è odoo.


Quando il build non può riuscire: dirlo, invece di lasciare parlare gcc

Odoo pinna gevent e greenlet per versione di Python. Se l'interprete è più recente di tutti i suoi pin, per quella versione non esiste una wheel già compilata: pip costruisce dai sorgenti, e il C generato da Cython non regge gli header di un CPython più nuovo. Non è un problema di compilatore né di pacchetti di sistema mancanti — è la versione, e nessun flag di build la aggira.

Due cose succedono allora, e nessuna delle due è un rifiuto:

  1. Il preflight avvisa, prima della conferma, dicendo quale interprete c'è e cosa si romperà.
  2. Se il build salta davvero, l'errore porta la causa davanti all'output di pip — che resta per intero, perché spiegare non è nascondere la prova — e mostra le righe che quella versione di Odoo dichiara, così si vede a colpo d'occhio che per questo interprete non ce n'è una.

La domanda «Odoo ha un pin per questo Python?» non è rispondibile dal requirements.txt: i marker sono aperti verso l'alto, quindi la riga applicabile è applicabile e pip la sceglie correttamente. Che per quell'interprete manchi la wheel sta su PyPI, non nel file. Per questo l'installer non previene con un controllo che fingerebbe di saperlo: spiega nel momento in cui il fallimento c'è.

La via d'uscita vera è a monte — installare il venv su un interprete coperto dai pin, che è ciò che fa il preflight dove la distribuzione ne impacchetta uno: vedi Supporto multi-distribuzione. Deviare dai pin di Odoo non è fra le opzioni: sarebbe una combinazione che nessuno ha provato.

Clone this wiki locally