Development
Upgrading frappe-nix
Move a bench or an app repository to a newer frappe-nix, what the shell reconciles for you on entry, and what to commit afterwards.
On this page
To pick up a newer frappe-nix, update the pin and reload the shell:
nix flake update frappe-nix # or `nix flake update` for every pin
direnv reload # or: nix develop --no-pure-evalThat is the whole procedure.
What the shell reconciles on entry
A frappe-nix bump can ask something new of a bench's workspace root. The case so far is frappe-runtime becoming a required dependency: a name in [project].dependencies, a [tool.uv.sources] entry and a build backend named in [tool.uv.extra-build-dependencies]. A bench from before the bump has no way to know.
So enterShell reconciles pyproject.toml on every entry, with the same ensure-root step frappe-init runs:
It adds what is missing and changes nothing that is already there. Your name, your
requires-python, your override list and your comments stay, because the file is edited with tomlkit and not rewritten.It runs
uv lockonly when that changed the file. On the common path it is one TOML parse and says nothing.
When it does change something, it says so, lists what it added, and tells you to commit pyproject.toml and uv.lock and re-enter the shell, because the shell you are in was built from the previous lock. Until you do, devenv up runs whatever shape the old lock supports. A bench whose lock predates the runtime gets the split web, socketio, worker and scheduler processes for that session, and the banner says so. Nothing about that shape is degraded: it is runtime.enable = false, which checks.socket covers.
If uv lock fails, because there is no network or because the new requirement genuinely conflicts with an app's pins, both files are put back exactly as they were, mtimes included, and the shell opens anyway. A pyproject.toml that declares what uv.lock does not carry would fail the next evaluation (see Dependencies and locks), which is the one outcome this hook exists to avoid: you would be back to fixing the shell from outside it. The next entry retries. To resolve a conflict by hand, make the listed additions in pyproject.toml, add the override uv asks for, and run uv lock in the shell.
It does not touch [tool.uv.extra-build-dependencies] beyond adding a frappe-nix entry a hand-kept list lacks. A bench scaffolded before frappe-nix derived that table from uv.lock keeps its list until you move it; see Moving a bench to the derived list. If the bench's pythonOverrides lists lib.overrides.pycups, python-ldap, pyvips or cairocffi, evaluation warns: every bench gets those fixes now, so remove them. The aliases are kept for all of 1.x, removed in 2.0.0.
What it deliberately does not do
The shell hook does not register submodules, vendor apps, edit .gitignore or common_site_config.json, or git add anything. That is the reconciler's job, and the reconciler still does all of it for the cases a shell hook should not decide:
nix run github:Avunu/frappe-nix -- -yThe scaffolder's --frappe-version and --site are now --version and --default-site. The old names work, with a warning, through 1.x, until frappe-nix 2.0.0; see Renamed flags.
Some options were renamed to the names frappe-nix shares with odoo-nix (python-devops-nix's CONVENTIONS.md). The old names still work, with an evaluation warning naming the new one, through 1.x, until frappe-nix 2.0.0:
| Old | New |
|---|---|
benchName | name |
siteName | defaultSite |
restore.withFiles | restore.files |
devguard.mail.smtpPort | mailpit.port |
devguard.mail.port | mailpit.port |
devguard.mail.host | mailpit.host |
devguard.mail.httpPort | mailpit.httpPort |
devguard.mail.pop3.* | mailpit.pop3.* |
runtime.dev | watch.python |
assets.reassert.hooks | watch.reassert.hooks |
assets.reassert.debounceMs | watch.reassert.debounceMs |
devguard.scheduler.enable | devguard.jobs.enable |
devguard.scheduler.blockServerScripts | devguard.jobs.blockServerScripts |
devguard.scheduler.extraBlockedJobs | devguard.jobs.extraBlocked |
The last of those follow the development shell's option tree (python-devops-nix's CONVENTIONS.md): watch is what the shell does when a file changes, mailpit is the catcher as a service, devguard is guard policy only, and ports has one attribute per service (ports.http, ports.db, ports.smtp, ports.mailUi, ports.pop3, ports.socketio) that the rest of the module reads. Two behaviours changed with them:
Mailpit runs whether or not the mail guard is on (
mailpit.enable, defaulttrue). Before, it ran only withdevguard.mail.enable, and turning the guard off also stopped the catcher. The server, workers and scheduler start after it whenever it runs.watch.python = falsekeeps serving/assetsand/files.runtime.dev = falseused to turn both off, because frappe-runtime's--devdid both. The runtime now has a separate--serve-assets, which the shell always passes. Withruntime.enable = false,watch.python = falserunsbench serve --noreload.
The scheduler guard's environment variables moved with it: FRAPPE_DEVGUARD_JOBS_EXTRA_BLOCKED (was FRAPPE_DEVGUARD_SCHEDULER_EXTRA_BLOCKED_JOBS), FRAPPE_DEVGUARD_JOBS_BLOCK_SERVER_SCRIPTS, FRAPPE_DEVGUARD_JOBS_ENABLE and FRAPPE_DEVGUARD_DISABLE=jobs. The old ones are read, with a warning, for the same period. Its log lines still read frappe_devguard[scheduler].
The NixOS module moved the same way: services.frappe is now services.frappe-nix, and a site's extraConfig and extraConfigFiles are now settings and settingsFiles, the nixpkgs name for a block that becomes the framework's config file (site_config.json). The old names still work, with a NixOS warning, through 1.x, until frappe-nix 2.0.0.
The environment variables frappe-nix's own tools read are all FRAPPE_NIX_* now. The old names are still read, with a warning, through 1.x, until frappe-nix 2.0.0:
| Old | New |
|---|---|
FRAPPE_OFFLINE_MIGRATE | FRAPPE_NIX_OFFLINE_MIGRATE |
FRAPPE_OFFLINE_MIGRATE_ROW_THRESHOLD | FRAPPE_NIX_OFFLINE_MIGRATE_ROW_THRESHOLD |
FRAPPE_OFFLINE_MIGRATE_PT_OSC | FRAPPE_NIX_OFFLINE_MIGRATE_PT_OSC |
FRAPPE_BENCH_UPDATE_FORCE | FRAPPE_NIX_BENCH_UPDATE_FORCE |
FRAPPE_WEB_SOCKET | FRAPPE_NIX_WEB_SOCKET |
FRAPPE_REPLICA_DB_SOCKET | FRAPPE_NIX_REPLICA_DB_SOCKET |
FRAPPE_LOG_LEVEL | FRAPPE_NIX_LOG_LEVEL |
FRAPPE_TEST_ADMIN_PASSWORD | FRAPPE_NIX_TEST_ADMIN_PASSWORD |
services.frappe-nix sets both FRAPPE_NIX_WEB_SOCKET and FRAPPE_WEB_SOCKET, and both log-level names, in the same window, so a bench package built from an older frappe-nix than the module still finds them.
Frappe's own variables (FRAPPE_SITE, FRAPPE_DB_SOCKET, FRAPPE_REDIS_CACHE and the rest) keep their names, and so do the grafts' switches (FRAPPE_DEVGUARD_*, FRAPPE_UNIXSOCK_ENABLED).
A change to the options a bench's flake.nix sets cannot be reconciled from inside the shell. nodeOfflineHashes was one such. It stays an evaluation error that names the edit.
In app mode
In an app repository the pins are flake inputs, so a newer frappe-nix is the same nix flake update frappe-nix. Moving the Frappe pin or a sibling is nix flake update frappe, then nix run .#relock, and you commit flake.lock with nix/.
The bench root's dev group gained coverage and unittest-xml-reporting, which frappe-test runs the tests under. An app that has not opted in to the app standards keeps the root it had: each of the two stays out of it unless the root package in the committed nix/uv.lock already lists it in its dev group. On version-15 and version-16 alike, the generated root, the dev environment and the lock stay exactly as they were, and nix run .#relock leaves nix/uv.lock as it is. A version-16 lock does have both packages, through Frappe's test extra, but that does not put them in the dev environment.
frappe-test then has no coverage to run under and says so (exit 10): pass --no-coverage, or opt in and run nix run .#relock, which adds them.
Ports are leased at devenv up
The ports a bench's shell evaluates are now the ports it prefers. devenv up leases them before anything starts (The development shell). Nothing changes for a bench whose preferred ports are free, which is the usual case. When one is taken:
An app-mode bench moves to the next free port instead of failing to bind it, or answering readiness checks from another bench's nginx. Find the ports in force with
frappe-nix-ports, not withports.*ormailpit.*values read from Nix, which are the preferred ones.A bench-mode bench stops before starting anything, naming the port and the bench or process that holds it. Before, nginx alone failed to bind and the rest came up.
To keep an app-mode bench on its exact ports, refusing a taken one as
devenv updid before for an app that opted in, setports.strict = truein the flake or exportFRAPPE_NIX_STRICT_PORTS=1.
FRAPPE_NIX_PORT_OFFSET, ports.offset, ports.base and ports.worktreeSalt keep their meaning: they choose the preferred ports.
The runtime moves with frappe-nix
The unified runtime's source is the runtime/ directory of this repository, so a bump of frappe-nix is also a bump of the runtime, with no relock in any bench. The one exception is a runtime release that adds a new dependency. That needs one relock:
nix run .#relock -- --upgrade-package frappe-runtimeNote the flag: a plain uv lock keeps an already resolved git revision and reports success without changing anything. See The unified runtime.