build: migrate to devenv

- Add devenv.nix with Python/uv setup and git-hooks
- Add devenv.yaml and devenv.lock
- Refactor flake.nix to use devenv integration
- Update .envrc from flake to devenv
- Add devenv artifacts to .gitignore
This commit is contained in:
Konstantin Fickel 2026-02-15 17:31:02 +01:00
parent f9ed0463f7
commit b4848bb661
Signed by: kfickel
GPG key ID: A793722F9933C1A5
7 changed files with 373 additions and 184 deletions

311
flake.nix
View file

@ -1,8 +1,8 @@
{
description = "Hello world flake using uv2nix";
description = "Using Markdown Files to organize your life as a @Tag-Stream";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
@ -21,70 +21,135 @@
inputs.uv2nix.follows = "uv2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
git-hooks = {
url = "github:cachix/git-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
uv2nix,
pyproject-nix,
uv2nix,
pyproject-build-systems,
git-hooks,
...
}:
let
inherit (nixpkgs) lib;
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
# Load a uv workspace from a workspace root.
# Uv2nix treats all uv projects as workspace projects.
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
# Create package overlay from workspace.
overlay = workspace.mkPyprojectOverlay {
# Prefer prebuilt binary wheels as a package source.
# Sdists are less likely to "just work" because of the metadata missing from uv.lock.
# Binary wheels are more likely to, but may still require overrides for library dependencies.
sourcePreference = "wheel"; # or sourcePreference = "sdist";
# Optionally customise PEP 508 environment
# environ = {
# platform_release = "5.10.65";
# };
sourcePreference = "wheel";
};
# Extend generated overlay with build fixups
#
# Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds.
# This is an additional overlay implementing build fixups.
# See:
# - https://pyproject-nix.github.io/uv2nix/FAQ.html
pyprojectOverrides = _final: _prev: {
# Implement build fixups here.
# Note that uv2nix is _not_ using Nixpkgs buildPythonPackage.
# It's using https://pyproject-nix.github.io/pyproject.nix/build.html
editableOverlay = workspace.mkEditablePyprojectOverlay {
root = "$REPO_ROOT";
};
# This example is only using x86_64-linux
pkgs = nixpkgs.legacyPackages.x86_64-linux;
pythonSets = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) stdenv;
# Use Python 3.14 from nixpkgs
python = pkgs.python314;
baseSet = pkgs.callPackage pyproject-nix.build.packages {
python = pkgs.python313;
};
# Construct package set
pythonSet =
# Use base package set from pyproject.nix builders
(pkgs.callPackage pyproject-nix.build.packages {
inherit python;
}).overrideScope
(
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]
);
pyprojectOverrides = _final: prev: {
streamd = prev.streamd.overrideAttrs (old: {
passthru = old.passthru // {
tests = (old.passthru.tests or { }) // {
pytest = stdenv.mkDerivation {
name = "${_final.streamd.name}-pytest";
inherit (_final.streamd) src;
nativeBuildInputs = [
(_final.mkVirtualEnv "streamd-pytest-env" {
streamd = [ "dev" ];
})
];
dontConfigure = true;
buildPhase = ''
runHook preBuild
# Exit code 5 means no tests collected — allow it so the
# check succeeds on an empty test suite.
pytest || [ $? -eq 5 ]
runHook postBuild
'';
installPhase = ''
runHook preInstall
touch $out
runHook postInstall
'';
};
};
};
});
};
in
baseSet.overrideScope (
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]
)
);
mkGitHooksCheck =
system:
let
pkgs = nixpkgs.legacyPackages.${system};
pythonSet = pythonSets.${system};
venv = pythonSet.mkVirtualEnv "streamd-check-env" workspace.deps.default;
in
git-hooks.lib.${system}.run {
src = ./.;
hooks = {
basedpyright = {
enable = true;
entry = "${pkgs.basedpyright}/bin/basedpyright --pythonpath ${venv}/bin/python --project ${
pkgs.writeText "pyrightconfig.json" (
builtins.toJSON {
reportMissingTypeStubs = false;
reportUnnecessaryTypeIgnoreComment = false;
}
)
}";
files = "\\.py$";
types = [ "file" ];
};
ruff.enable = true;
ruff-format.enable = true;
commitizen.enable = true;
};
};
in
{
packages = forAllSystems (
system:
let
pythonSet = pythonSets.${system};
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication;
in
rec {
streamd = mkApplication {
venv = pythonSet.mkVirtualEnv "streamd-env" workspace.deps.default;
package = pythonSet.streamd;
};
default = streamd;
}
);
homeManagerModules.default =
{
lib,
@ -93,176 +158,78 @@
...
}:
let
cfg = config.programs.streamer;
cfg = config.programs.streamd;
in
{
options.programs.streamer = {
enable = lib.mkEnableOption "streamer";
options.programs.streamd = {
enable = lib.mkEnableOption "streamd";
base-folder = lib.mkOption {
type = lib.types.str;
description = "Base Folder of Streamer";
description = "Base Folder of streamd";
};
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.system}.streamer;
defaultText = lib.literalExpression "inputs.streamer.packages.\${pkgs.system}.streamer";
description = "The package to use for the streamer binary.";
default = self.packages.${pkgs.system}.streamd;
defaultText = lib.literalExpression "inputs.streamd.packages.\${pkgs.system}.streamd";
description = "The package to use for the streamd binary.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
(lib.hm.assertions.assertPlatform "programs.streamer" pkgs lib.platforms.linux)
(lib.hm.assertions.assertPlatform "programs.streamd" pkgs lib.platforms.linux)
];
home.packages = [ cfg.package ];
xdg.configFile."streamer/config.yaml".source =
(pkgs.formats.yaml { }).generate "streamer-configuration"
xdg.configFile."streamd/config.yaml".source =
(pkgs.formats.yaml { }).generate "streamd-configuration"
{
base_folder = cfg.base-folder;
};
home.shellAliases.s = "streamer";
home.shellAliases.s = "streamd";
};
};
# Package a virtual environment as our main application.
#
# Enable no optional dependencies for production build.
packages.x86_64-linux =
checks = forAllSystems (
system:
let
streamer = pythonSet.mkVirtualEnv "streamer-env" workspace.deps.default;
pythonSet = pythonSets.${system};
in
{
inherit streamer;
default = streamer;
};
inherit (pythonSet.streamd.passthru.tests) pytest;
pre-commit = mkGitHooksCheck system;
}
);
# Make streamer runnable with `nix run`
apps.x86_64-linux = {
default = {
type = "app";
program = "${self.packages.x86_64-linux.default}/bin/streamer";
};
};
# This example provides two different modes of development:
# - Impurely using uv to manage virtual environments
# - Pure development using uv2nix to manage virtual environments
devShells.x86_64-linux = {
# It is of course perfectly OK to keep using an impure virtualenv workflow and only use uv2nix to build packages.
# This devShell simply adds Python and undoes the dependency leakage done by Nixpkgs Python infrastructure.
impure = pkgs.mkShell {
packages = with pkgs; [
python
uv
pre-commit
bashInteractive
];
env = {
# Prevent uv from managing Python downloads
UV_PYTHON_DOWNLOADS = "never";
# Force uv to use nixpkgs Python interpreter
UV_PYTHON = python.interpreter;
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
# Python libraries often load native shared objects using dlopen(3).
# Setting LD_LIBRARY_PATH makes the dynamic library loader aware of libraries without using RPATH for lookup.
LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1;
};
shellHook = ''
unset PYTHONPATH
'';
};
# This devShell uses uv2nix to construct a virtual environment purely from Nix, using the same dependency specification as the application.
# The notable difference is that we also apply another overlay here enabling editable mode ( https://setuptools.pypa.io/en/latest/userguide/development_mode.html ).
#
# This means that any changes done to your local files do not require a rebuild.
#
# Note: Editable package support is still unstable and subject to change.
uv2nix =
let
# Create an overlay enabling editable mode for all local dependencies.
editableOverlay = workspace.mkEditablePyprojectOverlay {
# Use environment variable
root = "$REPO_ROOT";
# Optional: Only enable editable for these packages
# members = [ "streamer" ];
};
# Override previous set with our overrideable overlay.
editablePythonSet = pythonSet.overrideScope (
lib.composeManyExtensions [
editableOverlay
# Apply fixups for building an editable package of your workspace packages
(final: prev: {
streamer = prev.streamer.overrideAttrs (old: {
# It's a good idea to filter the sources going into an editable build
# so the editable package doesn't have to be rebuilt on every change.
src = lib.fileset.toSource {
root = old.src;
fileset = lib.fileset.unions [
(old.src + "/pyproject.toml")
(old.src + "/README.md")
(old.src + "/src/streamer/__init__.py")
];
};
# Hatchling (our build system) has a dependency on the `editables` package when building editables.
#
# In normal Python flows this dependency is dynamically handled, and doesn't need to be explicitly declared.
# This behaviour is documented in PEP-660.
#
# With Nix the dependency needs to be explicitly declared.
nativeBuildInputs =
old.nativeBuildInputs
++ final.resolveBuildSystem {
editables = [ ];
};
});
})
]
);
# Build virtual environment, with local packages being editable.
#
# Enable all optional dependencies for development.
virtualenv = editablePythonSet.mkVirtualEnv "streamer-dev-env" workspace.deps.all;
in
pkgs.mkShell {
packages = with pkgs; [
devShells = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
pythonSet = pythonSets.${system}.overrideScope editableOverlay;
virtualenv = pythonSet.mkVirtualEnv "streamd-dev-env" workspace.deps.all;
in
{
default = pkgs.mkShell {
packages = [
virtualenv
uv
pre-commit
bashInteractive
pkgs.uv
];
env = {
# Don't create venv using uv
UV_NO_SYNC = "1";
# Force uv to use Python interpreter from venv
UV_PYTHON = "${virtualenv}/bin/python";
# Prevent uv from downloading managed Python's
UV_PYTHON = pythonSet.python.interpreter;
UV_PYTHON_DOWNLOADS = "never";
};
shellHook = ''
# Undo dependency propagation by nixpkgs.
unset PYTHONPATH
# Get repository root using git. This is expanded at runtime by the editable `.pth` machinery.
export REPO_ROOT=$(git rev-parse --show-toplevel)
${(mkGitHooksCheck system).shellHook}
'';
};
};
}
);
};
}