{ 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: { toml-dataclass = prev.toml-dataclass.overrideAttrs (old: { nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ final.resolveBuildSystem { setuptools = [ ]; }; }); }; # 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 ]) ++ [ python ]; 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; settingsFormat = pkgs.formats.toml { }; 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 ''; }; settings = mkOption { default = { }; description = '' Additional settings for Mycal. ''; type = types.submodule { freeformType = settingsFormat.type; options = { name = mkOption { type = types.str; description = '' Name of person for whom this calendar is created. ''; }; timezone = mkOption { type = types.str; default = "Europe/Berlin"; description = '' Default timezone for the calendar. ''; }; update_interval = mkOption { type = types.int; default = 3600; description = '' Interval in seconds between calendar cache updates. ''; }; calendars = mkOption { default = [ ]; description = '' Configuration for multiple calendars. See https://git.alin.ovh/mycal/blob/main/config.toml.example ''; type = types.listOf (types.submodule { freeformType = settingsFormat.type; }); }; email = mkOption { type = types.str; example = "john.doe@example.com"; description = '' Email address of the user. ''; }; }; }; }; }; config = mkIf cfg.enable { systemd.services.mycal = { description = "Mycal Calendar Service"; environment = { CONFIG_FILE = settingsFormat.generate "mycal-config.toml" cfg.settings; TZPATH = "${pkgs.tzdata}/share/zoneinfo"; }; serviceConfig = { ExecStart = '' ${cfg.venv}/bin/uvicorn \ --host ${cfg.host} \ --port ${toString cfg.port} \ --ws none \ 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" ]; }; }; }; }; }; }