From b067d76e3ace52d39718615ec7cac51c4c69313d Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Thu, 7 May 2026 22:10:05 +0200 Subject: [PATCH] feat: nix module --- container/flake.lock | 128 +++++++++++++++++++++++++++++ container/flake.nix | 49 +++++++++++ flake.lock | 62 ++++++++++++++ flake.nix | 191 ++++++++++++++++++++++++++++++++++++------- justfile | 17 ++++ module.nix | 162 ++++++++++++++++++++++++++++++++++++ 6 files changed, 580 insertions(+), 29 deletions(-) create mode 100644 container/flake.lock create mode 100644 container/flake.nix create mode 100644 flake.lock create mode 100644 module.nix diff --git a/container/flake.lock b/container/flake.lock new file mode 100644 index 0000000..fb53881 --- /dev/null +++ b/container/flake.lock @@ -0,0 +1,128 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1778003029, + "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1778003029, + "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "trictrac": "trictrac" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1778123869, + "narHash": "sha256-hV9D8ET33kXjdoMXBT2bwM/j8WQM1SP/dVKZtjQKhAQ=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "592e5dedf04f0eaff1ed0f01ce5db7407d9fc7be", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "nixpkgs": "nixpkgs_4" + }, + "locked": { + "lastModified": 1778123869, + "narHash": "sha256-hV9D8ET33kXjdoMXBT2bwM/j8WQM1SP/dVKZtjQKhAQ=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "592e5dedf04f0eaff1ed0f01ce5db7407d9fc7be", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "trictrac": { + "inputs": { + "nixpkgs": "nixpkgs_3", + "rust-overlay": "rust-overlay_2" + }, + "locked": { + "path": "..", + "type": "path" + }, + "original": { + "path": "..", + "type": "path" + }, + "parent": [] + } + }, + "root": "root", + "version": 7 +} diff --git a/container/flake.nix b/container/flake.nix new file mode 100644 index 0000000..86b1559 --- /dev/null +++ b/container/flake.nix @@ -0,0 +1,49 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + # inputs.trictrac.url = "github:mmai/trictrac"; + inputs.trictrac.url = ".."; + inputs.rust-overlay.url = "github:oxalica/rust-overlay"; + + outputs = { self, nixpkgs, trictrac, rust-overlay }: + { + nixosConfigurations = { + + container = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + + modules = [ + trictrac.nixosModule + ({ pkgs, ... }: + let + hostname = "trictrac"; + in + { + boot.isContainer = true; + + # Let 'nixos-version --json' know about the Git revision + # of this flake. + system.configurationRevision = nixpkgs.lib.mkIf (self ? rev) self.rev; + system.stateVersion = "25.11"; + + # Network configuration. + networking.useDHCP = false; + networking.firewall.allowedTCPPorts = [ 80 ]; + networking.hostName = hostname; + + # rust-overlay must be applied first so trictrac.overlay can use rust-bin + nixpkgs.overlays = [ rust-overlay.overlays.default trictrac.overlay ]; + + services.trictrac = { + enable = true; + protocol = "http"; + hostname = hostname; + }; + + environment.systemPackages = with pkgs; [ neovim ]; + }) + ]; + }; + + }; + }; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d3f9da1 --- /dev/null +++ b/flake.lock @@ -0,0 +1,62 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1778003029, + "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1778123869, + "narHash": "sha256-hV9D8ET33kXjdoMXBT2bwM/j8WQM1SP/dVKZtjQKhAQ=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "592e5dedf04f0eaff1ed0f01ce5db7407d9fc7be", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 7e82fc5..fe35f92 100644 --- a/flake.nix +++ b/flake.nix @@ -1,41 +1,174 @@ - { description = "Trictrac"; - inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - # let pkgs = nixpkgs.legacyPackages.${system}; in - let pkgs = import nixpkgs { + outputs = { self, nixpkgs, rust-overlay }: + let + systems = [ "x86_64-linux" "i686-linux" "aarch64-linux" ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); + # rust-overlay must be applied before self.overlay so that rust-bin is available + nixpkgsFor = forAllSystems (system: + import nixpkgs { inherit system; - config = { allowUnfree = true; }; - }; in - { - # devShell = import ./shell.nix { inherit pkgs; }; - devShell = with pkgs; mkShell rec { + overlays = [ rust-overlay.overlays.default self.overlay ]; + } + ); + in + { + overlay = final: prev: { - nativeBuildInputs = [ - pkg-config - llvmPackages.bintools # To use lld linker + trictrac-front = + let + # WASM build needs wasm32-unknown-unknown target in the Rust toolchain + rustToolchain = final.rust-bin.stable.latest.default.override { + targets = [ "wasm32-unknown-unknown" ]; + }; + rustPlatform = final.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }; + # Must match the wasm-bindgen version in Cargo.lock + wasm-bindgen-version = "0.2.118"; + wasm-bindgen-cli = final.buildWasmBindgenCli rec { + version = wasm-bindgen-version; + src = final.fetchCrate { + pname = "wasm-bindgen-cli"; + inherit version; + hash = "sha256-ve783oYH0TGv8Z8lIPdGjItzeLDQLOT5uv/jbFOlZpI="; + }; + cargoDeps = rustPlatform.fetchCargoVendor { + inherit src; + name = "wasm-bindgen-cli-vendor"; + hash = "sha256-EYDfuBlH3zmTxACBL+sjicRna84CvoesKSQVcYiG9P0="; + }; + }; + + frontendCargoDeps = rustPlatform.fetchCargoVendor { + src = ./.; + name = "trictrac-frontend-vendor"; + hash = "sha256-W2xlFgmA8biiIaE/EbC7ebHryo1lzrQYdrOCp5Xxjn8="; + }; + in + final.stdenv.mkDerivation { + name = "trictrac-front"; + src = ./.; + + nativeBuildInputs = with final; [ + rustToolchain + lld + rustPlatform.cargoSetupHook + wasm-bindgen-cli + trunk + binaryen ]; - buildInputs = [ - cargo rustc rustfmt rustPackages.clippy # rust - # pre-commit + cargoDeps = frontendCargoDeps; - alsa-lib udev - vulkan-loader # needed for GPU acceleration - xlibsWrapper xorg.libXcursor xorg.libXrandr xorg.libXi # To use x11 feature - # libxkbcommon wayland # To use wayland feature - ]; - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs; + buildPhase = '' + runHook preBuild + export HOME=$TMPDIR - shellHook = '' - export HOST=127.0.0.1 - export PORT=7000 + # Pin tool versions so trunk finds them in PATH instead of downloading + cat >> clients/web/Trunk.toml << 'EOF' + + [tools] + wasm-bindgen = { version = "${wasm-bindgen-version}" } + wasm-opt = { version = "version_124" } + EOF + + pushd clients/web + trunk build --release --offline + popd + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp -R clients/web/dist/. $out/ + runHook postInstall ''; }; - } - ); - } + + trictrac = with final; rustPlatform.buildRustPackage { + pname = "trictrac"; + version = "0.2.0"; + src = ./.; + + nativeBuildInputs = [ pkg-config ]; + buildInputs = [ openssl ]; + + # Build only the relay server; skip WASM/bot crates + cargoBuildFlags = [ "-p" "relay-server" ]; + doCheck = false; + + cargoLock = { + lockFile = ./Cargo.lock; + # Run `nix build .#trictrac` with the fake hashes to get the correct ones + outputHashes = { + "burn-rl-0.1.0" = "sha256-XAdabwHaSqi7ldO0v8Tuj7h1EX5QBeDIUgmme2Rdzqo="; + "gym-rs-0.3.1" = "sha256-TA7sK027dbpWcsMLt+c+ggIZb0ZZvTk/e5ihvUYxmK0="; + }; + }; + + postInstall = '' + install -m 644 ${./server/relay-server/GameConfig.json} $out/GameConfig.json + ''; + + meta = with lib; { + description = "A online game of trictrac"; + homepage = "https://github.com/mmai/trictrac"; + license = licenses.gpl3; + platforms = platforms.unix; + }; + }; + + trictrac-docker = with final; + let + port = "8080"; + entrypoint = writeScript "entrypoint.sh" '' + #!${runtimeShell} + # Populate a writable working dir with static files + config + mkdir -p /var/lib/trictrac + for f in ${trictrac-front}/*; do + ln -sf "$f" "/var/lib/trictrac/$(basename "$f")" + done + cp -n ${trictrac}/GameConfig.json /var/lib/trictrac/ 2>/dev/null || true + cd /var/lib/trictrac + echo "Starting trictrac server on port ${port}" + exec ${trictrac}/bin/relay-server + ''; + in + dockerTools.buildImage { + name = "mmai/trictrac"; + tag = "latest"; + copyToRoot = buildEnv { + name = "trictrac-env"; + paths = [ busybox ]; + }; + config = { + Entrypoint = [ entrypoint ]; + ExposedPorts = { + "${port}/tcp" = { }; + }; + }; + }; + + }; + + packages = forAllSystems (system: { + inherit (nixpkgsFor.${system}) trictrac trictrac-front trictrac-docker; + }); + + defaultPackage = forAllSystems (system: self.packages.${system}.trictrac); + + # trictrac service module + nixosModule = import ./module.nix; + + }; +} diff --git a/justfile b/justfile index b308095..5029cac 100644 --- a/justfile +++ b/justfile @@ -34,6 +34,23 @@ build-relay: cp target/release/relay-server deploy cp -u server/relay-server/GameConfig.json deploy/ +# start a trictrac container with nixos-container +# `boot.enableContainers = true` must be set on local nixos system +local: + cd container && nix flake update nixpkgs trictrac && cd - + sudo nixos-container destroy trictrac + sudo nixos-container create trictrac --flake ./container/ + nixos-container start trictrac + machinectl + +docker-build: + nix build .#trictrac-docker +docker-run: docker-build + docker load < ./result + docker run mmai/trictrac -P +docker-publish: docker-build + docker push mmai/trictrac + runclibots: cargo run --bin=client_cli -- --bot random,dqnburn:./bot/models/burnrl_dqn_40.mpk #cargo run --bin=client_cli -- --bot dqn:./bot/models/dqn_model_final.json,dummy diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..984d87d --- /dev/null +++ b/module.nix @@ -0,0 +1,162 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.trictrac; +in +{ + + options = { + services.trictrac = { + enable = mkEnableOption "trictrac"; + + user = mkOption { + type = types.str; + default = "trictrac"; + description = "User under which trictrac is ran."; + }; + + group = mkOption { + type = types.str; + default = "trictrac"; + description = "Group under which trictrac is ran."; + }; + + protocol = mkOption { + type = types.enum [ "http" "https" ]; + default = "https"; + description = "Web server protocol."; + }; + + hostname = mkOption { + type = types.str; + default = "trictrac.localhost"; + description = "Public domain name of the trictrac web app."; + }; + + apiPort = mkOption { + type = types.port; + default = 8080; + description = "Port the relay server listens on."; + }; + + createDatabaseLocally = mkOption { + type = types.bool; + default = true; + example = false; + description = "Create a local PostgreSQL database for trictrac."; + }; + + }; + }; + + config = mkIf cfg.enable { + users.users.trictrac = mkIf (cfg.user == "trictrac") { + group = cfg.group; + isSystemUser = true; + }; + users.groups.trictrac = mkIf (cfg.group == "trictrac") { }; + + services.nginx = { + enable = true; + # map needed for WebSocket Connection header upgrade + appendHttpConfig = '' + upstream trictrac-api { + server 127.0.0.1:${toString cfg.apiPort}; + } + map $http_upgrade $connection_upgrade { + default upgrade; + "" close; + } + ''; + virtualHosts = + let + proxyConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 3600s; + ''; + withSSL = cfg.protocol == "https"; + in + { + "${cfg.hostname}" = { + enableACME = withSSL; + forceSSL = withSSL; + locations."/" = { + extraConfig = proxyConfig; + proxyPass = "http://trictrac-api/"; + }; + }; + }; + }; + + services.postgresql = mkIf cfg.createDatabaseLocally { + enable = mkDefault true; + ensureDatabases = [ "trictrac" ]; + ensureUsers = [ + { + name = cfg.user; + ensureDBOwnership = true; + } + ]; + # Allow the trictrac service user to connect via TCP without a password + authentication = mkAfter '' + host trictrac ${cfg.user} 127.0.0.1/32 trust + host trictrac ${cfg.user} ::1/128 trust + ''; + }; + + systemd.services.trictrac-server = + let + setupScript = pkgs.writeShellScript "trictrac-setup" '' + set -euo pipefail + # Symlink frontend static files into the state directory so the + # relay server can serve them from its working directory. + for f in ${pkgs.trictrac-front}/*; do + ln -sf "$f" "$STATE_DIRECTORY/$(basename "$f")" + done + # Seed a writable GameConfig.json on first run; admins may edit it later. + if [ ! -f "$STATE_DIRECTORY/GameConfig.json" ]; then + install -m 644 ${pkgs.trictrac}/GameConfig.json "$STATE_DIRECTORY/GameConfig.json" + fi + ''; + in + { + description = "trictrac relay server"; + after = [ "network.target" ] ++ optional cfg.createDatabaseLocally "postgresql.service"; + requires = optional cfg.createDatabaseLocally "postgresql.service"; + wantedBy = [ "multi-user.target" ]; + + environment = { + # Use TCP + trust auth (matches NixOS postgresql.authentication above) + DATABASE_URL = "postgresql://${cfg.user}@127.0.0.1/${cfg.user}"; + }; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + # systemd creates /var/lib/trictrac and sets STATE_DIRECTORY accordingly + StateDirectory = "trictrac"; + StateDirectoryMode = "0755"; + WorkingDirectory = "/var/lib/trictrac"; + ExecStartPre = "${setupScript}"; + ExecStart = "${pkgs.trictrac}/bin/relay-server"; + Restart = "on-failure"; + RestartSec = "5s"; + }; + }; + + }; + + meta = { + maintainers = with lib.maintainers; [ mmai ]; + }; +}