{ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; pyproject-nix = { url = "github:pyproject-nix/pyproject.nix"; inputs.nixpkgs.follows = "nixpkgs"; }; uv2nix = { url = "github:pyproject-nix/uv2nix"; inputs.pyproject-nix.follows = "pyproject-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; pyproject-build-systems = { url = "github:pyproject-nix/build-system-pkgs"; inputs.pyproject-nix.follows = "pyproject-nix"; inputs.uv2nix.follows = "uv2nix"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self , nixpkgs , flake-utils , uv2nix , pyproject-nix , pyproject-build-systems , ... }: flake-utils.lib.eachDefaultSystem (system: let inherit (nixpkgs) lib; # 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"; # }; }; pkgs = import nixpkgs { inherit system; }; python = pkgs.python3; # 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 }; # 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 ] ); in { packages = { default = pythonSet.mkVirtualEnv "mycal" workspace.deps.default; inherit python pythonSet; }; devShells = { default = pkgs.mkShell { packages = with pkgs; [ uv ]; inputsFrom = [ self.packages.${pkgs.system}.default ]; 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 ''; }; }; }) // { nixosModules = { mycal = { config, lib, pkgs, ... }: let cfg = config.services.mycal; pkg = cfg.venv; python = self.packages.${pkgs.system}.python; inherit (lib) mkEnableOption mkOption mkIf types; in { options.services.mycal = { enable = mkEnableOption "Mycal calendar anonymiser"; port = mkOption { type = types.port; default = 8000; description = '' Port to listen on. ''; }; host = mkOption { type = types.str; default = "127.0.0.1"; description = '' Host to bind to. ''; }; venv = mkOption { type = lib.types.package; default = self.packages.${pkgs.system}.default; description = '' Mycal virtual environment package ''; }; timezone = mkOption { type = types.str; default = "Europe/Berlin"; description = '' Default timezone for the calendar. ''; }; name = mkOption { type = types.str; description = '' Name of person for whom this calendar is created. ''; }; calendar = mkOption { default = { }; type = types.submodule { options = { file = mkOption { type = with types; nullOr str; default = null; example = "/path/to/calendar/file"; description = '' Path to the calendar file. Preferred over URL. ''; }; url = mkOption { type = with types; nullOr str; default = null; example = "https://example.com/calendar.ics"; description = '' URL to the calendar file. ''; }; }; }; }; }; config = mkIf cfg.enable { systemd.services.mycal = { description = "Mycal Calendar Service"; environment = { TZ = cfg.timezone; NAME = cfg.name; CALENDAR_FILE = cfg.calendar.file; CALENDAR_URL = cfg.calendar.url; PYTHONPATH = "${python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/${python.sitePackages}"; }; serviceConfig = { ExecStart = '' ${lib.getExe python.pkgs.gunicorn} \ --bind ${cfg.host}:${toString cfg.port} \ --workers 1 \ mycal:app ''; Restart = "on-failure"; DynamicUser = true; StateDirectory = "mycal"; RuntimeDirectory = "mycal"; BindReadOnlyPaths = [ "${config.environment.etc."ssl/certs/ca-certificates.crt".source}:/etc/ssl/certs/ca-certificates.crt" builtins.storeDir "-/etc/resolv.conf" "-/etc/nsswitch.conf" "-/etc/hosts" "-/etc/localtime" ]; }; wantedBy = [ "multi-user.target" ]; }; }; }; }; }; }