Scaffolding
A repository of addons
Develop a repository of Odoo addons on its own, OCA style, with OCB and every repository it depends on as flake inputs, a generated uv lock and a dev shell that installs all of its addons.
On this page
A repository of addons is what OCA publishes: a flat set of addon folders at the root, one branch per Odoo series, mounted by the projects that deploy them. Addons mode gives such a repository a development shell of its own. OCB and the repositories its addons depend on are flake inputs, the uv project is generated into nix/, and odoo db provision installs every addon in the repository.
The layout
.
├── flake.nix, flake.lock OCB and the dependencies as inputs; pins
├── .odoo-series the series this branch targets, e.g. 18.0
├── nix/
│ ├── pyproject.toml generated by the relock; commit it
│ └── uv.lock generated by the relock; commit it
├── my_addon/ your addons, at the root
├── my_other_addon/
└── .odoo-nix/ links the dev shell makes (gitignored)
├── odoo -> /nix/store/…-source
└── modules/<name> -> /nix/store/…-sourceThe shell also links odoo.conf, odools.toml and .venv at the root, as in a project. templates/addons/ in odoo-nix has the flake.nix, .envrc, .gitignore and nix/pyproject.toml to start from, and nix run github:Avunu/odoo-nix -- --addons writes them into the repository (where they are missing), with .odoo-series, then runs the first relock. List the repositories the addons depend on in the flake before that relock, or run it again after (nix run .#relock). See Scaffolder.
The flake
{
inputs = {
odoo-nix.url = "github:Avunu/odoo-nix/release-2";
nixpkgs.follows = "odoo-nix/nixpkgs";
ocb = { url = "github:OCA/OCB/18.0"; flake = false; };
oca-edi = { url = "github:OCA/edi/18.0"; flake = false; };
oca-queue = { url = "github:OCA/queue/18.0"; flake = false; };
};
outputs = { odoo-nix, ... }@inputs:
odoo-nix.lib.mkFlake { inherit inputs; } ({ ... }: {
imports = [ odoo-nix.flakeModules.default ];
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
perSystem = { lib, ... }: {
odoo-nix = {
enable = true;
name = "edi-addons";
workspaceRoot = ./.;
odooSeries = lib.fileContents ./.odoo-series;
addons.enable = true;
coreSource = inputs.ocb;
externalSources = [
{ name = "edi"; src = inputs.oca-edi; }
{ name = "queue"; src = inputs.oca-queue; }
];
};
};
});
}List every repository the addons' depends reach, through other repositories too: a dependency that is in none of them is taken to be part of Odoo. Other repositories of addons are inputs the same way as OCA's, on their series branch.
First lock and daily use
nix flake lock
nix run .#relock # writes nix/pyproject.toml and nix/uv.lock
git add flake.lock nix/
direnv allow # or: nix develop --no-pure-eval
devenv up # PostgreSQL, Odoo, Mailpit
odoo db provision # every installable addon here, and what it depends on
odoo test my_addonThe relock works in a clone whose shell has never started. It links OCB and the external sources under .odoo-nix/, seeds nix/pyproject.toml if there is none, and locks. Run odoo project relock again after any of these:
an input moves (
nix flake update);an addon gains a
dependson a packaged module;an addon gains an
external_dependencies.pythonentry.
Nothing detects a forgotten relock: nix/pyproject.toml only changes when the relock runs, so the stale-lock audit has nothing to compare against, and a dependency missing from the lock fails when the addon is installed.
What addons mode changes
| A project | Addons mode | |
|---|---|---|
layout | odoo, modules, custom | .odoo-nix/odoo, .odoo-nix/modules, . |
| OCB | a submodule, or coreSource | coreSource, required |
| Module repositories | submodules under modules/ | externalSources, linked under .odoo-nix/modules/ |
pyproject.toml, uv.lock | at the root, partly hand-written | in nix/, generated |
| Install set | modules.txt | every installable addon at the root |
odoo module add, add-bundle, remove, odoo project update | manage submodules and modules.txt | refused, with what to do instead |
Packaged modules of the external sources (an OCA addon's pyproject.toml) are built once as wheels from the store; your addons stay live on addons_path. Unpackaged ones, yours included, contribute their external_dependencies.python to the lock and are walked for their depends. Store paths are read-only, so editing an external repository means overriding its input, for example nix develop --override-input oca-edi path:../edi, which takes a snapshot each time the shell is entered.
externalSources without addons mode
externalSources works in a project too. It mounts a repository from a flake input instead of a submodule, so its pin lives in flake.lock. Keep the name the submodule had: the lock's paths stay the same. The shell links it as modules/<name>, a link into the store, so add /modules/<name> (no trailing slash) to .gitignore. Two roots must not both provide an installable module. If they do, evaluation fails and names the module and both roots; add the module to exclude on the entry it should not come from.