{ description = "Serve a folder of Markdown as gemtext, Nex, Gopher, WML, and XHTML-MP"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; wapdown = { url = "git+ssh://git@code.randogoth.com:2222/randogoth/wapdown.git"; flake = false; }; md2txt = { url = "git+ssh://git@code.randogoth.com:2222/randogoth/md2txt.git"; flake = false; }; }; outputs = { self, nixpkgs, flake-utils, wapdown, md2txt }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; python = pkgs.python313; wapdownPkg = python.pkgs.buildPythonPackage { pname = "wapdown"; version = "0.1.0"; pyproject = true; src = wapdown; build-system = [ python.pkgs.setuptools python.pkgs.wheel ]; }; md2txtPkg = python.pkgs.buildPythonPackage { pname = "md2txt"; version = "0.1.0"; pyproject = true; src = md2txt; build-system = [ python.pkgs.setuptools python.pkgs.wheel ]; dependencies = [ python.pkgs.pyfiglet python.pkgs.pyphen ]; }; smolwebPkg = python.pkgs.buildPythonApplication { pname = "smolweb"; version = "0.1.0"; pyproject = true; src = ./.; build-system = [ python.pkgs.setuptools python.pkgs.wheel ]; dependencies = [ wapdownPkg md2txtPkg ]; nativeCheckInputs = [ python.pkgs.pytestCheckHook ]; # Needs a real openssl binary on PATH at runtime (cert # autogeneration shells out to it) as well as at test time. makeWrapperArgs = [ "--prefix" "PATH" ":" "${pkgs.openssl}/bin" ]; }; in { packages.default = smolwebPkg; devShells.default = pkgs.mkShell { packages = [ python pkgs.uv pkgs.openssl pkgs.netcat-gnu pkgs.libxml2 ]; }; } ) // { nixosModules.default = { config, lib, pkgs, ... }: let cfg = config.services.smolweb; in { options.services.smolweb = with lib; { enable = mkEnableOption "the smolweb capsule server"; package = mkOption { type = types.package; default = self.packages.${pkgs.system}.default; description = "smolweb package to run."; }; root = mkOption { type = types.path; description = "Folder of Markdown to serve."; }; host = mkOption { type = types.str; description = "Hostname this server answers to."; }; geminiAddr = mkOption { type = types.str; default = ":1965"; description = "Gemini listen address; empty disables it."; }; spartanAddr = mkOption { type = types.str; default = ":300"; description = "Spartan listen address; empty disables it."; }; httpAddr = mkOption { type = types.str; default = ":8080"; description = "HTTP listen address; empty disables it."; }; nexAddr = mkOption { type = types.str; default = ":1900"; description = "Nex listen address; empty disables it."; }; gopherAddr = mkOption { type = types.str; default = ":70"; description = "Gopher listen address; empty disables it."; }; }; config = lib.mkIf cfg.enable { systemd.services.smolweb = { description = "smolweb capsule server"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = lib.concatStringsSep " " ( [ "${cfg.package}/bin/smolweb" "--root" cfg.root "--host" cfg.host "--cert" "/var/lib/smolweb/cert.pem" "--key" "/var/lib/smolweb/key.pem" ] ++ lib.optionals (cfg.geminiAddr != "") [ "--gemini" cfg.geminiAddr ] ++ lib.optionals (cfg.spartanAddr != "") [ "--spartan" cfg.spartanAddr ] ++ lib.optionals (cfg.httpAddr != "") [ "--http" cfg.httpAddr ] ++ lib.optionals (cfg.nexAddr != "") [ "--nex" cfg.nexAddr ] ++ lib.optionals (cfg.gopherAddr != "") [ "--gopher" cfg.gopherAddr ] ); DynamicUser = true; StateDirectory = "smolweb"; # Spartan's default port (300), Gopher's default port (70), # and Gemini's TLS handshake all need this rather than # running the whole service as root -- DynamicUser alone # can't bind a privileged port. AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; Restart = "on-failure"; RestartSec = "5s"; }; }; }; }; }; }