Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f03617b
refactor(nix): replace hardcoded package enumeration with readDir dis…
cameronraysmith Feb 18, 2026
015dfcb
refactor(justfile): remove pnt-cli defaults from Rust recipes
cameronraysmith Feb 18, 2026
3cb6b5d
refactor(justfile): remove pnt-cli defaults from container recipes
cameronraysmith Feb 18, 2026
8a13df6
feat(justfile): add maturin field to list-packages-json output
cameronraysmith Feb 18, 2026
d64722e
feat(justfile): add list-maturin-packages convenience recipe
cameronraysmith Feb 18, 2026
b5f4df4
feat(ci): add is_maturin field to discover-packages output
cameronraysmith Feb 18, 2026
6814d4f
chore(beads): close pnt-kaf.1, pnt-kaf.4, pnt-kaf.5 after wave 1 impl…
cameronraysmith Feb 18, 2026
21a0288
refactor(devshell): replace hardcoded hasCli with discovery-based has…
cameronraysmith Feb 18, 2026
5292a0a
refactor(containers): replace hardcoded container defs with maturin p…
cameronraysmith Feb 18, 2026
9a4d8bb
refactor(mergify): replace hardcoded package names with wildcard regex
cameronraysmith Feb 18, 2026
f705185
chore(beads): close pnt-kaf.2, pnt-kaf.3, pnt-kaf.6 after wave 2 impl…
cameronraysmith Feb 18, 2026
65f1533
chore(beads): close pnt-kaf.7 after integration verification
cameronraysmith Feb 18, 2026
05836ec
chore(beads): checkpoint pnt-kaf epic completion
cameronraysmith Feb 18, 2026
8eb9bf3
chore(beads): checkpoint pnt-kaf with follow-up template substitution…
cameronraysmith Feb 18, 2026
823609e
chore(beads): create pnt-2nw epic for repo-name/package-name disambig…
cameronraysmith Feb 18, 2026
d25ef29
chore(beads): checkpoint pnt-2nw planning session
cameronraysmith Feb 18, 2026
d7d6e19
chore(beads): update pnt-2nw issues with definitive classification co…
cameronraysmith Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

10 changes: 2 additions & 8 deletions .github/mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,8 @@ shared:
- check-success=nix (x86_64-linux, ubuntu-latest, devshells)
- check-skipped=nix (x86_64-linux, ubuntu-latest, devshells)
- or:
- "check-success~=^test-python \\(python-nix-template, packages/python-nix-template.* / test$"
- "check-skipped~=^test-python \\(python-nix-template, packages/python-nix-template.* / test$"
- or:
- "check-success~=^test-python \\(pnt-functional, packages/pnt-functional.* / test$"
- "check-skipped~=^test-python \\(pnt-functional, packages/pnt-functional.* / test$"
- or:
- "check-success~=^test-python \\(pnt-cli, packages/pnt-cli.* / test$"
- "check-skipped~=^test-python \\(pnt-cli, packages/pnt-cli.* / test$"
- "check-success~=^test-python \\(.* / test$"
- "check-skipped~=^test-python \\(.* / test$"
- or:
- check-success=test-omnix-template
- check-skipped=test-omnix-template
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ jobs:
d=$(dirname "$f")
n=$(basename "$d")

# Detect maturin/pyo3 package by Cargo.toml presence
is_maturin="false"
if [ -f "$d/Cargo.toml" ]; then is_maturin="true"; fi

# Read per-package CI metadata from .ci.json if present
ci_json="$d/.ci.json"
if [ -f "$ci_json" ]; then
Expand All @@ -199,9 +203,10 @@ jobs:
jq -nc \
--arg name "$n" \
--arg path "$d" \
--argjson is_maturin "$is_maturin" \
--arg build_images "$build_images" \
--argjson images "$images" \
'{name: $name, path: $path, "build-images": $build_images, images: ($images | tojson)}'
'{name: $name, path: $path, "is_maturin": $is_maturin, "build-images": $build_images, images: ($images | tojson)}'
done | jq -sc '.')
echo "packages=$PACKAGES" >> "$GITHUB_OUTPUT"
echo "Discovered packages: $PACKAGES"
Expand Down
24 changes: 15 additions & 9 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,15 @@ list-packages-json:
@ls -d packages/*/pyproject.toml | while read f; do \
d=$(dirname "$f"); \
n=$(basename "$d"); \
printf '{"name":"%s","path":"%s"}\n' "$n" "$d"; \
if [ -f "$d/Cargo.toml" ]; then m=true; else m=false; fi; \
printf '{"name":"%s","path":"%s","maturin":%s}\n' "$n" "$d" "$m"; \
done | jq -sc '.'

# Discover maturin (Rust/pyo3) packages as JSON array
[group('CI/CD')]
list-maturin-packages:
@just list-packages-json | jq '[.[] | select(.maturin)]'

# Sync dependencies for a package via uv
[group('CI/CD')]
ci-sync package:
Expand Down Expand Up @@ -293,17 +299,17 @@ conda-check package="python-nix-template": (conda-lint package) (conda-type pack

# Build production container image
[group('containers')]
container-build-production CONTAINER="pnt-cli":
container-build-production CONTAINER:
nix build ".#{{CONTAINER}}ProductionImage" -L

# Load production container to local Docker daemon
[group('containers')]
container-load-production CONTAINER="pnt-cli":
container-load-production CONTAINER:
nix run ".#{{CONTAINER}}ProductionImage.copyToDockerDaemon"

# Push production container manifest (requires registry auth)
[group('containers')]
container-push-production CONTAINER="pnt-cli" VERSION="0.0.0" +TAGS="":
container-push-production CONTAINER VERSION="0.0.0" +TAGS="":
VERSION={{VERSION}} TAGS={{TAGS}} nix run --impure ".#{{CONTAINER}}Manifest" -L

# Display container CI matrix
Expand Down Expand Up @@ -406,27 +412,27 @@ check package="python-nix-template": (lint package) (type package) (test package

# Build Rust crates for a package
[group('rust')]
cargo-build package="pnt-cli":
cargo-build package:
cd packages/{{package}}/crates && cargo build

# Run Rust tests via cargo test
[group('rust')]
cargo-test package="pnt-cli":
cargo-test package:
cd packages/{{package}}/crates && cargo test

# Run Rust clippy lints
[group('rust')]
cargo-clippy package="pnt-cli":
cargo-clippy package:
cd packages/{{package}}/crates && cargo clippy --all-targets -- --deny warnings

# Run Rust tests via cargo-nextest
[group('rust')]
cargo-nextest package="pnt-cli":
cargo-nextest package:
cd packages/{{package}}/crates && cargo nextest run --no-tests=pass

# Run all Rust checks (clippy, test)
[group('rust')]
cargo-check package="pnt-cli": (cargo-clippy package) (cargo-test package)
cargo-check package: (cargo-clippy package) (cargo-test package)
@printf "\nAll Rust checks passed for {{package}}.\n"

## Secrets
Expand Down
28 changes: 17 additions & 11 deletions modules/containers.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,23 @@ let
gitHubOrg = "sciexp";
repoName = "python-nix-template";

hasCli = builtins.pathExists ../packages/pnt-cli;

# Production container definitions (nix2container)
# Add new containers here; containerMatrix auto-discovers them.
productionContainerDefs = lib.optionalAttrs hasCli {
pnt-cli = {
name = "pnt-cli";
entrypoint = "pnt-cli";
description = "pnt-cli with pyo3 native bindings";
};
};
# Discover maturin packages from packages/ directory. Each subdirectory
# containing a Cargo.toml is a maturin/pyo3 package that gets a production
# container. This mirrors the discovery logic in python.nix.
packageDirs = builtins.readDir ../packages;
packageNames = builtins.filter (name: packageDirs.${name} == "directory") (
builtins.attrNames packageDirs
);
isMaturin = name: builtins.pathExists (../packages + "/${name}/Cargo.toml");
maturinPackageNames = builtins.filter isMaturin packageNames;

# Production container definitions derived from discovered maturin packages.
# Each maturin package exposes a CLI binary via pyo3 and gets a container.
productionContainerDefs = lib.genAttrs maturinPackageNames (name: {
inherit name;
entrypoint = name;
description = "${name} with pyo3 native bindings";
});
in
{
perSystem =
Expand Down
7 changes: 3 additions & 4 deletions modules/devshell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@
packageWorkspaces,
editablePythonSets,
pythonVersions,
hasMaturinPackages ? false,
...
}:
let
hasCli = builtins.pathExists ../packages/pnt-cli;

# Merge deps from all independent package workspaces, unioning extras lists
# for shared dependency names rather than silently dropping via //
allDeps = lib.foldlAttrs (
Expand Down Expand Up @@ -55,10 +54,10 @@
sops
ssh-to-age
]
++ lib.optionals hasCli (
++ lib.optionals hasMaturinPackages (
with pkgs;
[
# Rust tooling for pnt-cli pyo3 extension
# Rust tooling for maturin/pyo3 packages
cargo
rustc
clippy
Expand Down
98 changes: 46 additions & 52 deletions modules/python.nix
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@
py313 = pkgs.python313;
};

# Discover packages from packages/ directory. Each subdirectory is a
# Python package. Packages containing Cargo.toml are maturin/pyo3
# packages requiring a corresponding nix/packages/{name}/default.nix
# module that exports { overlay, checks }.
packageDirs = builtins.readDir ../packages;
packageNames = builtins.filter (name: packageDirs.${name} == "directory") (
builtins.attrNames packageDirs
);

isMaturin = name: builtins.pathExists (../packages + "/${name}/Cargo.toml");
maturinPackageNames = builtins.filter isMaturin packageNames;
purePackageNames = builtins.filter (name: !(isMaturin name)) packageNames;
hasMaturinPackages = maturinPackageNames != [ ];

# Load each Python package independently (no root workspace).
# Each package directory contains its own pyproject.toml and uv.lock,
# resolved independently following the LangChain federation model.
Expand All @@ -39,39 +53,24 @@
};
};

hasFunctional = builtins.pathExists ../packages/pnt-functional;
hasCli = builtins.pathExists ../packages/pnt-cli;

packageWorkspaces = {
python-nix-template = loadPackage "python-nix-template" ../packages/python-nix-template;
}
// lib.optionalAttrs hasCli {
pnt-cli = loadPackage "pnt-cli" ../packages/pnt-cli;
}
// lib.optionalAttrs hasFunctional {
pnt-functional = loadPackage "pnt-functional" ../packages/pnt-functional;
};

# Per-package Nix modules with optional Rust overlays.
# Packages with Rust extensions get a dedicated module in nix/packages/
# that encapsulates crane configuration and exports an overlay + checks.
# When pnt-cli is absent (pyo3-package: false), returns an inert module
# with no-op overlay and empty checks to avoid import path errors.
emptyModule = {
overlay = _final: _prev: { };
checks = { };
};
packageWorkspaces = lib.genAttrs packageNames (name: loadPackage name (../packages + "/${name}"));

# Per-package Nix modules with Rust overlays. Each maturin package
# requires nix/packages/{name}/default.nix exporting { overlay, checks }.
# Eval-time error when Cargo.toml exists but the module is missing.
mkPackageModule =
python:
if hasCli then
import ../nix/packages/pnt-cli {
name: python:
let
modulePath = ../nix/packages + "/${name}/default.nix";
in
if builtins.pathExists modulePath then
import modulePath {
inherit pkgs lib python;
crane = inputs.crane;
inherit (inputs) crane-maturin pyproject-nix;
}
else
emptyModule;
throw "Maturin package ${name} requires nix/packages/${name}/default.nix";

# Compose per-package uv2nix overlays with shared overrides.
#
Expand All @@ -92,49 +91,38 @@
[
inputs.pyproject-build-systems.overlays.default
]
++ lib.optional hasCli packageWorkspaces.pnt-cli.overlay
++ lib.optional hasFunctional packageWorkspaces.pnt-functional.overlay
++ map (name: packageWorkspaces.${name}.overlay) packageNames
++ map (name: (mkPackageModule name python).overlay) maturinPackageNames
++ [
packageWorkspaces.python-nix-template.overlay
# Rust integration overlay for pnt-cli (crane + maturin)
(mkPackageModule python).overlay
packageOverrides
sdistOverrides
]
)
);

# Editable set excludes pnt-cli: maturin packages are incompatible with
# uv2nix's editable overlay (pyprojectFixupEditableHook expects EDITABLE_ROOT
# which maturin's build process does not set). pnt-cli is built as a regular
# wheel in the devshell; use `maturin develop` for iterative Rust development.
# Editable set excludes maturin packages: maturin packages are
# incompatible with uv2nix's editable overlay (pyprojectFixupEditableHook
# expects EDITABLE_ROOT which maturin's build process does not set).
# Maturin packages are built as regular wheels in the devshell; use
# `maturin develop` for iterative Rust development.
mkEditablePythonSet =
python:
(mkPythonSet python).overrideScope (
lib.composeManyExtensions (
lib.optional hasFunctional packageWorkspaces.pnt-functional.editableOverlay
map (name: packageWorkspaces.${name}.editableOverlay) purePackageNames
++ [
packageWorkspaces.python-nix-template.editableOverlay
(
final: prev:
{
python-nix-template = prev.python-nix-template.overrideAttrs (old: {
nativeBuildInputs =
old.nativeBuildInputs
++ final.resolveBuildSystem {
editables = [ ];
};
});
}
// lib.optionalAttrs hasFunctional {
pnt-functional = prev.pnt-functional.overrideAttrs (old: {
lib.genAttrs purePackageNames (
name:
prev.${name}.overrideAttrs (old: {
nativeBuildInputs =
old.nativeBuildInputs
++ final.resolveBuildSystem {
editables = [ ];
};
});
}
})
)
)
]
)
Expand All @@ -144,17 +132,23 @@
editablePythonSets = lib.mapAttrs (_: mkEditablePythonSet) pythonVersions;

# Rust checks from per-package modules (using default Python version)
rustChecks = (mkPackageModule pythonVersions.py313).checks;
rustChecks = lib.foldl' (
acc: name: acc // (mkPackageModule name pythonVersions.py313).checks
) { } maturinPackageNames;
in
{
checks = lib.optionalAttrs hasCli rustChecks;
checks = lib.optionalAttrs hasMaturinPackages rustChecks;

_module.args = {
inherit
packageWorkspaces
pythonSets
editablePythonSets
pythonVersions
packageNames
maturinPackageNames
purePackageNames
hasMaturinPackages
;
defaultPython = pythonVersions.py313;
};
Expand Down
Loading