Skip to content

Multi distribution support

Omisen edited this page Aug 17, 2026 · 3 revisions

How the same installer works on Ubuntu, Debian and Fedora without the steps knowing which distribution they are running on. If you are looking for “what do I install”, you want the Quick start; this page explains the how, and it is the page to read before adding a family or touching a package name.


What is supported, and what “supported” means

Family Minimum Exercised in CI, full cycle
Ubuntu ≥ 22.04 22.04, 24.04 (native runners)
Debian ≥ 11 11 and 12 (containers — a probe: without systemd as PID 1 it stops at setup-postgres)
Fedora ≥ 40 41 and 44 (privileged container with systemd as PID 1)

“Exercised” has a precise meaning here: the integration CI really installs, checks that Odoo answers over HTTP, then runs invok rollback and checks that nothing is left. The thresholds are open upwards: a newer release is accepted with a warning, never refused — a refusal without evidence blocks the good case, and a blocked installation is a certain harm while the avoided one was hypothetical.


The two boundaries (and why there are two)

The divergence between families is not one thing but two, and keeping them apart is what prevents an abstraction that abstracts two different things.

Boundary Answers Lives in
PackageManager which commands install and remove, and what the packages are called here src/packaging/ (apt.rs, dnf.rs)
Distro where the files live, which concepts exist, which tool governs the firewall src/distro/ (debian.rs, fedora.rs)

Neither is a second door onto the system: both are obtained from SystemOps (ops.packages(), ops.distro()), so what touches the machine stays in one place and the tests still mock only one thing.

Steps never match on the distribution. They ask a boundary, and the boundary answers for the family being run on.

The family is re-read, not re-derived

OsFamily is persisted in the manifest and re-read by the rollback command. It is not re-detected when the rollback starts, and it is not inferred from “which package manager is installed”: the manifest might describe an installation made elsewhere, and a machine with both managers would answer wrongly in silence. It is the same rule that governs artifact ownership — who created what is re-read from the manifest, not re-derived from the system.


What actually differs, family by family

Aspect Debian / Ubuntu Fedora
Manager apt-get / dpkg dnf / rpm
PostgreSQL marker postgresql postgresql-server (the client is not enough to say the server is there)
PostgreSQL cluster created by the package's postinst does not exist until you create it: needs postgresql-setup --initdb
wkhtmltopdf .deb per codename (jammy, bullseye, bookworm) the fedora37 .rpm, the only one built for this family in the pinned release
Nginx vhost sites-available + symlink in sites-enabled conf.d/*.conf, no symlink
Nginx default site the sites-enabled/default file/symlink, removed and restored by the rollback not a file: a block inside nginx.conf, and it is left alone
SELinux not applicable httpd_can_network_connect must be turned on, or the proxy answers 502 with a valid nginx -t
Firewall ufw firewalld

Two consequences worth reading in full, because they are choices and not details:

  • The Nginx default site on Fedora is not removed. There the default server is a server { listen 80 default_server; … } block inside /etc/nginx/nginx.conf: removing it would mean rewriting the main configuration of a customer's service. The practical effect, declared: on a Fedora with a freshly installed Nginx, a request to a hostname that does not match NGINX_SERVER_NAME still gets the welcome page instead of Odoo.
  • The PostgreSQL cluster is a state axis of its own (the fourth in setup-postgres): if we initialised it, the rollback can remove it — but only with --aggressive-rollback and only if it hosts no other databases. An empty data directory is an inert leftover; somebody else's data is not.

Package names: groups of alternatives, not lists

One need (“we want the libjpeg headers”) has different names on different distributions, and sometimes different names on different releases of the same distribution. So each family's catalogue is not a list of names but a list of alternative groups, resolved at runtime in this order:

  1. an alternative that is already installed wins (we do not add libtiff5-dev to a machine that has libtiff-dev, and the delta stays honest);
  2. otherwise the first one with a real candidate;
  3. otherwise the first one that is installable anyway — a purely virtual name, used only as a fallback, because a virtual name cannot be removed and a delta containing one would lie to the rollback.

If no alternative is available the error arrives in the snapshot, before mutating, with the unresolved group in the message. And if the package index cannot be queried, the message says so rather than sending you off to look for the rename of a package that is sitting perfectly where it always was: blindness is not absence.

A test ties the two catalogues together: every declared need must be covered by both families. Without it, a dependency gets added to one only and the gap surfaces when a VM stops compiling — the most expensive place possible.


The Python interpreter: chosen, not endured

Odoo pins gevent and greenlet per Python version. On an interpreter newer than its pins there is no prebuilt wheel: pip tries to build from source and the generated C does not survive a newer CPython's headers. That is the case on Fedora ≥ 43, where the system python3 is 3.14.

The installer's rule, in one line:

the system python3 if Odoo's pins cover it, otherwise the newest interpreter packaged by the distribution that they do cover.

On Fedora 44 that means python3.13 (with its headers), installed for the occasion and removed by the rollback like any other package in the delta — it is brought in by install-system-dependencies, whose undo purges, and not by bootstrap-prerequisites, which leaves what it adds installed.

On Debian and Ubuntu nothing changes: the base repositories have one Python and it is inside the pins. If one day it were not, the installer does not refuse: it warns at preflight, and if pip fails to build it says the cause is the Python version — not the build environment, not a missing package.

The combination that needed an interpreter of its own: Odoo 16 on Fedora

Every current Fedora would otherwise build the venv on Python 3.13 — as the system interpreter (41, 42) or as the newest one within the ceiling (>= 43). Odoo 16 has no pin for 3.13: its newest gevent line is written for python_version >= '3.12' and stops there, with nothing above it, so it keeps selecting gevent==24.2.1 — released before 3.13 existed, and with no wheel for it. pip would compile from source, and the C an older Cython generates does not survive 3.13's headers: the build dies on _PyLong_AsByteArray, whose signature changed in that release.

Odoo 17, 18 and 19 pin gevent==24.11.1 for >= '3.13', which ships a wheel.

So the ceiling is per Odoo version, not one number for all of them: newest_tested_python reads 3.12 for 16 and 3.13 for the rest, and choose_python takes it from there. On Fedora that means python3.12 — packaged by 41 through 44 — is installed for the occasion, the venv is built on it, and the rollback takes it back with the rest of the delta. On Ubuntu and Debian nothing changes: their system Python is already below both ceilings, so the exception never fires.

The number is a measurement, not a judgement about age: 16 is the only branch whose newest gevent bracket has no upper bound, which is why it is the only one with an exception. It is read from Odoo's own file, and the command to redo it is in the doc-comment:

curl -fsSL https://raw.githubusercontent.com/odoo/odoo/16.0/requirements.txt | grep -E '^gevent'

Proven on a real Fedora 44 before the entry was added to CI: venv on 3.12.13, gevent 24.2.1 installed from a wheel, the service up and answering HTTP 200, and a rollback that removed python3.12 and python3.12-devel along with everything else, leaving the system interpreter at 3.14.6 untouched. A CI job then installs and rolls back Odoo 16 on fedora:41, and a test refuses to let a version with its own ceiling exist without such a job — a ceiling nobody executes is a promise.

If a combination ever does fall outside the pins, the installer still does not refuse: it warns at preflight, and if pip fails it names the interpreter and prints the gevent/greenlet lines that Odoo version declares, so the cause is readable instead of buried in the compiler's output.

Everything downstream of the virtualenv knows nothing about this choice: the systemd unit, the control script and the database initialisation all go through <venv>/bin/python3, which the venv creates under that name whatever the base interpreter was.


Where parity ends, and how we know

The integration CI matrix is designed so that every diverging branch is executed by someone:

Job What it exercises that the others do not
native (Ubuntu 22.04 / 24.04) full life cycle on a non-pristine machine (preinstalled packages that must survive)
container (Debian 11 / 12) package names and the wkhtmltopdf pin on genuinely minimal images
fedora:41 the rpm family with a system Python covered by the pins
fedora:44 the rpm family with the alternative interpreter (the other branch of the choice)
fedora:41 / nginx firewalld and SELinux — the only place they really run
nginx (Ubuntu) the two natures of the default site, restored byte for byte
preexisting-user an odoo user already present, with and without a home
interrupt a real SIGINT during the installation

What is not covered by a real run is declared where it lives rather than hidden: currently the precondition on a divergent PGDATA (a machine with a drop-in that moves the cluster) lives only in the mock tests.


If you need to add a family

  1. OsFamily (a new variant) and the threshold in checks::validate_os;
  2. a PackageManager backend with its catalogue — every need, or the parity test fails;
  3. a Distro backend with the Nginx layout, firewall, SELinux and PostgreSQL cluster;
  4. the TOFU wkhtmltopdf pins for that family's packages (fail-closed: no pin, no install);
  5. an entry in the integration.yml matrix, because a family nobody runs is a promise, not a feature.

The Step trait is not touched: if it seems necessary, that is the signal that the divergence has ended up in the wrong place.

Clone this wiki locally