phynx — Python as the Nix Evaluator
A Python program constructs derivations (.drv) directly and hands them to
the nix store/daemon, which remains the build and caching engine. The nix
language evaluator is embedded (nix C API) and used only as a library —
to call into nixpkgs functions (mkShell, stdenv.mkDerivation, lib.*)
when their output is needed. This is the Guix architecture with full
access to nixpkgs kept. Full design rationale: docs/design.md.
from phynx import drv, store, nixpkgs
pkgs = nixpkgs() # ONE long-lived embedded evaluator
mytool = drv( # a .drv constructed directly, no nix-lang
name="mytool-1.0",
system="x86_64-linux",
builder=pkgs.bash.out("bin/bash"),
args=["-e", store.text("build.sh", "gcc -o $out/bin/mytool $src")],
env={"src": store.file("./mytool.c")},
)
# full Python is legal — no restricted subset:
probes = [drv(name=f"probe-{index}", ...) for index in range(10)]
shell = pkgs.mkShell(buildInputs=[pkgs.hello, mytool]) # hook into nix-lang
store.build(shell)
Quick start: build your first derivation
Write a plain Python file; the module attribute named default is the
build target:
# mybuild.py
from phynx import drv
default = drv(
name="greeting",
system="x86_64-linux",
builder="/bin/sh", # the build sandbox provides /bin/sh
args=["-c", "echo hello > $out"], # nix sets $out to the output path
)
Build it (registers the .drv, realises it, prints the output path):
$ bin/phynx build mybuild.py
out /nix/store/…-greeting
$ cat /nix/store/…-greeting
hello
Or skip the CLI and build from Python directly:
from phynx import drv, store
greeting = drv(name="greeting", system="x86_64-linux",
builder="/bin/sh", args=["-c", "echo hello > $out"])
print(store.build(greeting)) # {'out': '/nix/store/…-greeting'}
A runnable version ships in the repo: bin/phynx build examples/hello_chain.py (derivation chain), bin/phynx shell examples/dev_shell.py (mkShell mixing nixpkgs and phynx derivations).
Requirements
- nix ≥ 2.28 on
PATHwith thenix-commandexperimental feature and a working store/daemon. The embedded evaluator loads the C API shared libraries (libnixexprc.so, …) from the active nix installation; override the location withPHYNX_NIX_LIB_DIR. - For
nixpkgs(): a resolvablenixpkgsflake registry entry (or passnixpkgs(path="/path/to/nixpkgs")). - Python ≥ 3.12, no Python dependencies (stdlib + ctypes only).
The API
| call | effect |
|---|---|
drv(name=, system=, builder=, args=, env=, outputs=, fixed=) |
construct + register a derivation; returns a Drv handle |
store.text(name, contents) |
add an inline script/text to the store |
store.file(path, name=None, mode="nar") |
add a local file/directory to the store |
store.build(target) |
realise a Drv, .drv path, or evaluator value; returns {output: path} |
nixpkgs(ref=..., path=..., config=...) |
the embedded evaluator's nixpkgs attrset (lazy) |
eval_nix(expression) |
evaluate one nix expression |
primop(function) |
wrap a Python callable as a nix value, passable into nix code |
register_primop(function, name=) |
expose a Python callable as builtins.<name> (call before first eval) |
FixedOutput(hash_hex=, hash_algorithm=, ingestion_method=) |
fixed-output spec for drv(fixed=...) |
Session() |
an isolated session (own registry/evaluator); module-level calls use a default session |
Handles compose across both worlds:
mytool.out,mytool.output("dev"),mytool.out("bin/mytool")— output references; stringify to store paths.pkgs.helloused indrv(env=...)becomes a scanned dependency;pkgs.hello.as_drv()gives the explicitDrvhandle.- a phynx
Drvinsidepkgs.mkShell(buildInputs=[...])is injected as a real derivation value (import /nix/store/….drvunder the hood).
Dependency tracking (string contexts)
Nix strings carry a context tracking derivation references; Python strings
do not. phynx keeps a registry of every store path the session has touched
(derivation outputs, store.file/store.text results, derivations pulled
out of the evaluator) and scans builder/args/env strings for registered
paths at serialization time, populating inputDrvs/inputSrcs. A store
path that never passed through the session is not recognized — route
local files through store.file and nixpkgs packages through pkgs.<name>.
Where store paths come from
nix derivation add verifies output paths but does not compute them for
the caller, so phynx computes them itself (ATerm serialization +
hashDerivationModulo + nix base32) — the same choice Guix and Tvix made.
The algorithms are frozen by every store path in existence, and every
registration is verified by nix, which recomputes the paths and rejects a
mismatch: nix stays the authority, phynx only precomputes what nix checks.
CLI
phynx build script.py [--attr NAME] # run the script, build the target
phynx shell script.py [--attr NAME] # run the script, exec nix develop
The target is --attr NAME, else a module attribute named default, else
the last derivation the script created. See examples/hello_chain.py and
examples/dev_shell.py.
bin/phynx is a self-contained launcher for running straight from a
checkout (no install needed): it puts src/ on PYTHONPATH, pins
PHYNX_NIX_LIB_DIR from the active nix installation, picks the project
virtualenv's python when present (any python3 works — stdlib only), and
execs the CLI. Symlink it onto your PATH if you like.
Layout
| module | role |
|---|---|
phynx/hashing.py |
nix base32, hash folding, store-path rules |
phynx/aterm.py |
.drv ATerm serialization + parser |
phynx/derivation.py |
Derivation/Drv/OutputRef, hashDerivationModulo, drv() engine |
phynx/registry.py |
session path registry + reference scanning |
phynx/backend.py |
store registration/build backends (CLI today, C API capable later) |
phynx/capi.py |
ctypes bindings over the nix C API shared libraries |
phynx/evaluator.py |
embedded evaluator, NixValue, marshalling, primops, nixpkgs hook |
phynx/session.py |
session object + default-session facade |
phynx/cli.py |
phynx build / phynx shell |
Known limits
structuredAttrs, content-addressed (floating/deferred) and impure derivations are not yet representable throughdrv().- Strings assembled inside nix code lose their context when extracted to Python; pull the derivation value itself (or its outputs) across instead.
phynxoperates on the store imperatively; it is not a flake citizen (see design §4.5 for the escape hatch).
Tests
uv run pytest
Unit tests cover hashing/ATerm/scanning; integration tests build real derivations against the local daemon and verify phynx's path computation against nixpkgs' own derivations (skipped when nix or nixpkgs is absent).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file phynx-0.1.0.tar.gz.
File metadata
- Download URL: phynx-0.1.0.tar.gz
- Upload date:
- Size: 40.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.8.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84e0e517e40f2ddff70ea6f67ac52a04c923c62a47f3a14b6fe83f7fe3fc36cf
|
|
| MD5 |
eef96f84bf71e314e4c324945bb2e2ae
|
|
| BLAKE2b-256 |
47af6c966761381a1caa7db406d13c3449578df4e05562dc65d18e4cc4bdb97e
|
File details
Details for the file phynx-0.1.0-py3-none-any.whl.
File metadata
- Download URL: phynx-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.8.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f909968438d57720c046cb6d7d7a459e4b7a480f8dd21c91b75d412ec19fff1
|
|
| MD5 |
c8b38cca6e3aacade87dd120a07eaf9b
|
|
| BLAKE2b-256 |
aebf2174de293e4b70f58555127b5d2963e527d52efa7cc4d12215775e58f3ec
|