Metadata-Version: 2.4
Name: phynx
Version: 0.1.0
Summary: Python as the nix evaluator: construct derivations directly, hook into nixpkgs via the nix C API
Author: zokrezyl
License-Expression: LGPL-2.1-or-later
License-File: LICENSE
Keywords: build,derivation,nix,nixpkgs,store
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# 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`.

```python
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:

```python
# 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:

```python
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 `PATH` with the `nix-command` experimental 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 with `PHYNX_NIX_LIB_DIR`.
- For `nixpkgs()`: a resolvable `nixpkgs` flake registry entry (or pass
  `nixpkgs(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.hello` used in `drv(env=...)` becomes a scanned dependency;
  `pkgs.hello.as_drv()` gives the explicit `Drv` handle.
- a phynx `Drv` inside `pkgs.mkShell(buildInputs=[...])` is injected as a
  real derivation value (`import /nix/store/….drv` under 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 through `drv()`.
- Strings assembled *inside* nix code lose their context when extracted to
  Python; pull the derivation value itself (or its outputs) across instead.
- `phynx` operates 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).
