From a0819752345acef1b6bc98777b6c44e2aa41d482 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 26 Sep 2026 13:10:39 +0300 Subject: [PATCH] feat: add flake.nix for NixOS deployment --- flake.nix | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 19 +++---- uv.lock | 24 ++------- 3 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 flake.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4748833 --- /dev/null +++ b/flake.nix @@ -0,0 +1,132 @@ +{ + description = "Serve a folder of Markdown as gemtext, Spartan, 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."; + }; + }; + + 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 ] + ); + DynamicUser = true; + StateDirectory = "smolweb"; + # Spartan's default port (300) and Gemini's TLS handshake + # both 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"; + }; + }; + }; + }; + }; +} diff --git a/pyproject.toml b/pyproject.toml index 3e90063..45b7973 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,18 +12,15 @@ dependencies = [ [project.scripts] smolweb = "smolweb.cli:main" -# Local path sources: `uv.toml` cannot hold a `[sources]` table (uv only -# reads that from pyproject.toml), so there is no separate override file to -# split "reproducible" from "editable" -- this table is the only place it -# can live. Both origins are real (code.randogoth.com), but this checkout's -# wapdown/md2txt have unpushed local changes (the xhtmlmp renderer, the -# gemtext fixes) that a git source can't see yet, so path is what actually -# works right now. Once those are pushed, swap to: -# wapdown = { git = "ssh://git@code.randogoth.com:2222/randogoth/wapdown.git" } -# md2txt = { git = "ssh://git@code.randogoth.com:2222/randogoth/md2txt.git" } +# `uv.toml` cannot hold a `[sources]` table (uv only reads that from +# pyproject.toml), so there is no separate override file to split +# "reproducible" from "editable" -- this table is the only place it can +# live. Pin to `main` explicitly rather than floating, so a lockfile +# resolution is reproducible; bump the rev after a sibling change smolweb +# needs to pick up. [tool.uv.sources] -wapdown = { path = "../wapdown", editable = true } -md2txt = { path = "../md2txt", editable = true } +wapdown = { git = "ssh://git@code.randogoth.com:2222/randogoth/wapdown.git", rev = "main" } +md2txt = { git = "ssh://git@code.randogoth.com:2222/randogoth/md2txt.git", rev = "main" } [dependency-groups] dev = ["pytest>=8.0"] diff --git a/uv.lock b/uv.lock index ab10fb9..a946438 100644 --- a/uv.lock +++ b/uv.lock @@ -23,21 +23,12 @@ wheels = [ [[package]] name = "md2txt" version = "0.1.0" -source = { editable = "../md2txt" } +source = { git = "ssh://git@code.randogoth.com:2222/randogoth/md2txt.git?rev=main#9d290ed734689ede25f0b73057d642ed34edec0e" } dependencies = [ { name = "pyfiglet" }, { name = "pyphen" }, ] -[package.metadata] -requires-dist = [ - { name = "pyfiglet", specifier = ">=0.8.0" }, - { name = "pyphen", specifier = ">=0.17.2" }, -] - -[package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.0" }] - [[package]] name = "packaging" version = "26.3" @@ -115,8 +106,8 @@ dev = [ [package.metadata] requires-dist = [ - { name = "md2txt", editable = "../md2txt" }, - { name = "wapdown", editable = "../wapdown" }, + { name = "md2txt", git = "ssh://git@code.randogoth.com:2222/randogoth/md2txt.git?rev=main" }, + { name = "wapdown", git = "ssh://git@code.randogoth.com:2222/randogoth/wapdown.git?rev=main" }, ] [package.metadata.requires-dev] @@ -125,11 +116,4 @@ dev = [{ name = "pytest", specifier = ">=8.0" }] [[package]] name = "wapdown" version = "0.1.0" -source = { editable = "../wapdown" } - -[package.metadata] -requires-dist = [{ name = "lxml", marker = "extra == 'dtd'", specifier = ">=5.0" }] -provides-extras = ["dtd"] - -[package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.0" }] +source = { git = "ssh://git@code.randogoth.com:2222/randogoth/wapdown.git?rev=main#3af44fb0ac88cd1c613d2fba4436a9689f62c01a" }