diff --git a/flake.nix b/flake.nix index 12cd5d8..56a9176 100644 --- a/flake.nix +++ b/flake.nix @@ -103,5 +103,17 @@ )) // { nixosModules.default = import ./nixos/module.nix self; + + nixosConfigurations.test-vm = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + "${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix" + self.nixosModules.default + ./nixos/vm.nix + ]; + }; + + packages.x86_64-linux.test-vm = + self.nixosConfigurations.test-vm.config.system.build.vm; }; } diff --git a/nixos/vm.nix b/nixos/vm.nix new file mode 100644 index 0000000..09a5c71 --- /dev/null +++ b/nixos/vm.nix @@ -0,0 +1,135 @@ +# NixOS test VM for RadiantWave — mirrors production kiosk setup. +# +# Build & run: +# nixos-rebuild build-vm --flake .#test-vm +# ./result/bin/run-test-vm-vm +# +# This replicates the production kiosk config from KioskConfig: +# - cage as Wayland compositor (fullscreen kiosk) +# - PipeWire audio (with a virtual sink for device-switch testing) +# - NetworkManager +# - Auto-login as kiosk user +# - RadiantWave started via loginShellInit (same as production) + +{ config, lib, pkgs, ... }: + +let + rhemaApp = config.services.rhemawave.package; +in +{ + # ── Hostname ────────────────────────────────────────────────────────────── + networking.hostName = "radiantwave-test"; + + # ── RadiantWave module ──────────────────────────────────────────────────── + services.rhemawave.enable = true; + + # ── Boot ────────────────────────────────────────────────────────────────── + boot.loader.systemd-boot.enable = lib.mkForce false; + boot.loader.efi.canTouchEfiVariables = lib.mkForce false; + + # ── Packages (matches production) ───────────────────────────────────────── + environment.systemPackages = [ + pkgs.cage pkgs.rsync pkgs.dbus pkgs.mesa pkgs.git pkgs.tailscale pkgs.sqlite + ]; + + # ── Audio (PipeWire + virtual sink for device-switch testing) ───────────── + services.pipewire = { + enable = true; + alsa.enable = true; + pulse.enable = true; + extraConfig.pipewire."30-virtual-sink" = { + "context.modules" = [ + { + name = "libpipewire-module-loopback"; + args = { + "node.name" = "virtual-speaker"; + "node.description" = "Virtual Speaker"; + "audio.position" = [ "FL" "FR" ]; + }; + } + ]; + }; + }; + + # ── Seat (matches production) ───────────────────────────────────────────── + services.seatd.enable = true; + hardware.graphics.enable = true; + + # ── Networking ──────────────────────────────────────────────────────────── + services.resolved.enable = false; + networking.networkmanager.enable = true; + systemd.services.NetworkManager-wait-online.enable = false; + systemd.network.wait-online.enable = false; + + # ── SSH ─────────────────────────────────────────────────────────────────── + services.openssh.enable = true; + services.openssh.settings.PermitRootLogin = "prohibit-password"; + + # ── User (matches production) ───────────────────────────────────────────── + users.users.kiosk = { + isNormalUser = true; + uid = 1000; + extraGroups = [ "audio" "video" "input" "networkmanager" "wheel" ]; + }; + services.getty.autologinUser = "kiosk"; + + # ── Data directory ──────────────────────────────────────────────────────── + systemd.tmpfiles.rules = [ + "d /home/kiosk/.local/share/rhemawave 0770 kiosk kiosk -" + ]; + + # ── RadiantWave service (via loginShellInit, same as production) ────────── + environment.loginShellInit = '' + if [ "$(tty)" = "/dev/tty1" ]; then + export GODEBUG=netdns=cgo + export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + + # Smart Network Wait + for i in {1..10}; do + if ${pkgs.iputils}/bin/ping -c 1 8.8.8.8 &> /dev/null; then + break + fi + sleep 1 + done + + # Graphics fixes (matches production) + export SDL_VIDEODRIVER=wayland + export QT_QPA_PLATFORM=wayland + export XDG_SESSION_TYPE=wayland + export WLR_RENDERER=pixman + export WLR_NO_HARDWARE_CURSORS=1 + + # Asset sync + start app (same as production) + mkdir -p /home/kiosk/.local/share/rhemawave + ${pkgs.rsync}/bin/rsync -ru ${rhemaApp}/share/rhemawave/ /home/kiosk/.local/share/rhemawave/ + + cage -- ${rhemaApp}/bin/rhemawave -d -s > /tmp/kiosk.log 2>&1 + fi + ''; + + # ── VM settings ─────────────────────────────────────────────────────────── + virtualisation = { + memorySize = 4096; + cores = 4; + graphics = true; + forwardPorts = [ + { from = "host"; host.port = 2222; guest.port = 22; } + ]; + qemu.options = [ + "-device intel-hda" + "-device hda-duplex" + "-vga virtio" + ]; + }; + + services.logrotate.enable = false; + + # ── Nix settings ────────────────────────────────────────────────────────── + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.accept-flake-config = true; + + # ── Firewall (open for local dev) ───────────────────────────────────────── + networking.firewall.enable = false; + + system.stateVersion = "24.11"; +}