diff --git a/src/setup/nix.md b/src/setup/nix.md index 985912e..0b59168 100644 --- a/src/setup/nix.md +++ b/src/setup/nix.md @@ -1,169 +1,355 @@ # Can I deploy Lute on NixOS? -Deploy Lute on NixOS in three simple steps using [compose2nix](https://github.com/aksiksi/compose2nix). - -## 1. Create `docker-compose.yml` - -```yaml -name: 'lute' -services: - lute: - image: jzohrab/lute3:latest - ports: - - 5006:5001 - volumes: - - /var/lib/lute/data:/lute_data - - /var/lib/lute/backup:/lute_backup -``` +Yes, you can deploy Lute on NixOS by creating a dedicated Nix module. This approach encapsulates the application's configuration and dependencies, making it easy to manage. -## 2. Convert to NixOS Module +## Installation -```bash -$ nix run github:aksiksi/compose2nix -``` +### 1. Create the Lute Module -## 3. Import the Module +First, create a new file named `lute.nix` in your NixOS configuration directory (e.g., `/etc/nixos/lute.nix`). This file defines all the necessary packages and the systemd service for Lute. -Add to your NixOS configuration: ```nix -imports = [ - ./docker-compose.nix -]; -``` +# /etc/nixos/lute.nix -That's it! NixOS will handle all the necessary setup, including creating and setting permissions for the directories, when you rebuild your configuration. +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.services.lute; + + python = pkgs.python311; + pythonPackages = python.pkgs; + + platformdirs-pinned = let + pname = "platformdirs"; + version = "3.11.0"; + in + pythonPackages.buildPythonApplication { + inherit pname version; + format = "pyproject"; + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-z47lKjr9uWUHLcxlJDPgx+PkDPXqFHfNSzsdLrdUlbM="; + }; + nativeBuildInputs = with pythonPackages; [ + hatchling + hatch-vcs + ]; + meta = with pkgs.lib; { + description = "A small library for determining appropriate platform-specific dirs"; + homepage = "https://github.com/platformdirs/platformdirs"; + license = licenses.mit; + }; + }; + waitress-pinned = let + pname = "waitress"; + version = "2.1.2"; + in + pythonPackages.buildPythonApplication { + inherit pname version; + format = "pyproject"; + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-eApAgsX7wP3movz+Xibm78Ho9CVzCGPAQIV2l4H1Hro="; + }; + nativeBuildInputs = with pythonPackages; [ + setuptools + ]; + meta = with pkgs.lib; { + description = "A production-quality pure-Python WSGI server"; + homepage = "https://github.com/Pylons/waitress"; + license = licenses.mit; + }; + }; -# Updating under NixOS + chardet-pkg = let + pname = "chardet"; + version = "5.2.0"; + in + pythonPackages.buildPythonApplication { + inherit pname version; + format = "pyproject"; + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-Gztv9HmoxBS8P6LAhSmVaVxKAm3NbQYzst0JLKOcHPc="; + }; + nativeBuildInputs = with pythonPackages; [ + setuptools + ]; + meta = with pkgs.lib; { + description = "Universal character encoding detector for Python"; + homepage = "https://github.com/chardet/chardet"; + license = licenses.lgpl21Only; + }; + }; -If you install using docker/podman, you can update as root using + subtitle-parser-pkg = let + pname = "subtitle_parser"; + version = "2.0.1"; + in + pythonPackages.buildPythonApplication { + inherit pname version; + format = "pyproject"; + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-EhWQdYbG/72979WcDuTzUCam2tbPh5CPP0pwsbtZ41M="; + }; + nativeBuildInputs = with pythonPackages; [ + poetry-core + ]; + propagatedBuildInputs = with pythonPackages; [ + chardet-pkg + future + ]; + meta = with pkgs.lib; { + description = "A Python library for parsing subtitle files"; + homepage = "https://github.com/emre/subtitle-parser"; + license = licenses.mit; + }; + }; -```bash -$ podman pull lute3:latest -``` + openepub-pkg = let + pname = "openepub"; + version = "0.0.9"; + in + pythonPackages.buildPythonApplication { + inherit pname version; + format = "pyproject"; + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-qki9VVUn/zMXIkzQ+Zu5D3CXGpBbO97xRVfttYMrR9s="; + }; + nativeBuildInputs = with pythonPackages; [ + hatchling + ]; + propagatedBuildInputs = with pythonPackages; [ + beautifulsoup4 + xmltodict + ]; + meta = with pkgs.lib; { + description = "A simple epub parser"; + homepage = "https://github.com/gaohongnan/openepub"; + license = licenses.mit; + }; + }; -and then restarting the system service or server + natto-py-pkg = let + pname = "natto-py"; + version = "1.0.1"; + in + pythonPackages.buildPythonApplication { + inherit pname version; + format = "setuptools"; + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-dgEDuzlyMu4DPJkk0TV+MrFCu+Ey/GpDuM+C3WtlToY="; + }; + nativeBuildInputs = [pkgs.mecab]; + propagatedBuildInputs = with pythonPackages; [cffi]; -if you want to do it automatically, here is a nix module to do so: + meta = with pkgs.lib; { + description = "A Python wrapper for MeCab"; + homepage = "https://github.com/buruzaemon/natto-py"; + license = licenses.bsd3; + }; + }; + jaconv-pkg = let + pname = "jaconv"; + version = "0.3.4"; + in + pythonPackages.buildPythonApplication { + inherit pname version; + format = "setuptools"; + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-nnxV8/Cw4tutYvbJ+gww/G//27eCl5VVCdkIVrOjHW0="; + }; + meta = with pkgs.lib; { + description = "A Japanese character converter"; + homepage = "https://github.com/ikegami-yukino/jaconv"; + license = licenses.mit; + }; + }; -```nix -{ - config, - lib, - pkgs, - ... -}: -with lib; let - cfg = config.services.containerUpdater; + lute-pkg = let + pname = "lute3"; + version = "3.10.1"; + in + pythonPackages.buildPythonApplication { + inherit pname version; + format = "pyproject"; + + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-gqwoyINuP54ve6R2OonLUT2oZYmpjvUopyWbJ+stJrE="; + }; + + nativeBuildInputs = with pythonPackages; [ + flit-core + ]; + + propagatedBuildInputs = with pythonPackages; [ + flask + flask-sqlalchemy + sqlalchemy + pyyaml + flask-wtf + platformdirs-pinned + requests + beautifulsoup4 + toml + waitress-pinned + openepub-pkg + pyparsing + pypdf + subtitle-parser-pkg + ahocorapy + natto-py-pkg + jaconv-pkg + ]; + + postInstall = '' + mkdir -p $out/${python.sitePackages}/lute/data + ''; + + meta = with pkgs.lib; { + description = "A language learning web application (v3)"; + homepage = "https://github.com/LuteOrg/lute-v3"; + license = licenses.mit; + }; + }; in { - options.services.containerUpdater = { - enable = mkEnableOption "automatic container updates"; + options.services.lute = { + enable = lib.mkEnableOption "enable lute language server"; + + package = lib.mkOption { + type = lib.types.package; + default = lute-pkg; + description = "The Lute package to use for the service."; + }; - updateTime = mkOption { - type = types.str; - default = "Mon 02:00"; - description = "When to run the container updates (systemd calendar format)"; + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/lute/data"; + description = "Directory to store Lute's database and user files."; }; - extraFlags = mkOption { - type = types.listOf types.str; - default = []; - description = "Additional flags to pass to podman pull"; + backupDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/lute/backup"; + description = "Directory where Lute will store its backups."; }; - containers = mkOption { - type = types.listOf types.str; - default = []; - description = "List of specific containers to update. If empty, updates all containers."; + port = lib.mkOption { + type = lib.types.port; + default = 5006; + description = "Port for Lute to listen on."; }; - restartContainers = mkOption { - type = types.bool; + openFirewall = lib.mkOption { + type = lib.types.bool; default = false; - description = "Whether to restart containers after updating"; + description = "Whether to automatically open the configured port in the firewall."; }; }; - config = mkIf cfg.enable { - systemd.services.containerUpdater = { - description = "Update container images"; - path = [pkgs.podman]; - script = '' - # Function to update containers - update_containers() { - local containers=() - - # If specific containers are specified, use those - if [ ''${#containerList[@]} -gt 0 ]; then - containers=("''${containerList[@]}") - else - # Otherwise get all running containers - while IFS= read -r line; do - containers+=("$line") - done < <(podman ps -a --format="{{.Image}}" | sort -u) - fi - - # Update each container - for image in "''${containers[@]}"; do - echo "Updating container image: $image" - podman pull ''${pullFlags[@]} "$image" - - if [ "$RESTART_CONTAINERS" = "true" ]; then - container_ids=$(podman ps -q --filter "ancestor=$image") - if [ -n "$container_ids" ]; then - echo "Restarting containers using image: $image" - echo "$container_ids" | xargs -r podman restart - fi - fi - done - } - - # Set up environment variables - declare -a containerList=(${escapeShellArgs cfg.containers}) - declare -a pullFlags=(${escapeShellArgs cfg.extraFlags}) - RESTART_CONTAINERS="${boolToString cfg.restartContainers}" - - # Run the update - update_containers + config = lib.mkIf cfg.enable { + environment.etc."lute.yml" = { + text = '' + # Lute configuration file, managed by NixOS. + ENV: prod + DBNAME: lute.db + DATAPATH: ${cfg.dataDir} + BACKUP_PATH: ${cfg.backupDir} + MECAB_DICPATH: "${pkgs.mecab}/lib/mecab/dic/ipadic" ''; + }; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = false; - }; + users.users.lute = { + isSystemUser = true; + group = "lute"; + description = "Lute service user"; }; + users.groups.lute = {}; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0755 lute lute -" + "d '${cfg.backupDir}' 0755 lute lute -" + ]; - systemd.timers.containerUpdater = { - wantedBy = ["timers.target"]; - timerConfig = { - OnCalendar = cfg.updateTime; - Persistent = true; - Unit = "containerUpdater.service"; + systemd.services.lute = { + description = "Lute V3 Language Server"; + after = ["network-online.target"]; + wants = ["network-online.target"]; + wantedBy = ["multi-user.target"]; + + serviceConfig = { + User = "lute"; + Group = "lute"; + ExecStart = "${cfg.package}/bin/lute --port ${toString cfg.port} --config /etc/lute.yml"; + Type = "simple"; + Restart = "on-failure"; + RestartSec = "5s"; + WorkingDirectory = cfg.dataDir; }; }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [cfg.port]; }; } - ``` -Just import it into your configuration, +### 2. Import and Enable the Module -and then set the following options: +Next, open your main `configuration.nix` file and import the `lute.nix` module. Then, enable and configure the service. ```nix +# /etc/nixos/configuration.nix + { config, pkgs, ... }: + { - imports = [ ./container-updater.nix ]; - - services.containerUpdater = { - enable = true; - containers = [ "lute3:latest" ]; # Specific containers to update + imports = + [ + ./hardware-configuration.nix + ./lute.nix # <-- IMPORT THE MODULE + ]; - # Optional configurations: - # updateTime = "Mon 02:00"; # When to run updates - # extraFlags = [ ]; # Additional podman pull flags - # restartContainers = true; # Automatically restart containers after update + # ... other configuration ... + + # 2. ENABLE AND CONFIGURE THE LUTE SERVICE + services.lute = { + enable = true; + # dataDir = "/var/lib/lute/data"; # Optional: Change the default data directory + # backupDir = "/var/lib/lute/backup"; # Optional: Change the default backup directory + # port = 5006; # Optional: Change the default port + # openFirewall = true; # Optional: Open the port in the firewall }; + + # ... other configuration ... } ``` + +### 3. Rebuild Your System + +Finally, apply the new configuration by running the standard rebuild command: + +```bash +sudo nixos-rebuild switch +``` + +NixOS will build and install Lute and its dependencies, and the service will start automatically. You can then access Lute at `http://localhost:5006` (or the custom port you configured). + +## Updating Lute + +Because the version is pinned in the module for reproducibility, you must update it manually when a new version is released. + +1. **Update Version**: Find the latest version number on the Lute PyPI page. Open your `/etc/nixos/lute.nix` file and update the `version` string in the `lute-pkg` definition. + +2. **Update Hash**: Run `sudo nixos-rebuild switch`. The build will fail because the hash for the new version is incorrect. The error message will provide the correct hash. Look for a line that says `got: sha256-...` and copy the new hash. + +3. **Finalize**: Paste the new hash into the `hash` attribute for `lute-pkg` in `lute.nix` and run `sudo nixos-rebuild switch` one more time. The build will now succeed, and your Lute service will be updated.