diff --git a/lib.nix b/lib.nix index 317fea0e..e4b26c84 100644 --- a/lib.nix +++ b/lib.nix @@ -6,7 +6,7 @@ inherit (nixlib.strings) splitString toLower; inherit (nixlib.lists) imap0 elemAt; inherit (nixlib.attrsets) listToAttrs nameValuePair; - inherit (nixlib.strings) substring fixedWidthString; + inherit (nixlib.strings) substring fixedWidthString replaceStrings; inherit (nixlib.trivial) flip toHexString bitOr; toHexStringLower = v: toLower (toHexString v); @@ -30,12 +30,14 @@ in "${part0 (part 0)}${part 1}:${part 2}ff:fe${part 3}:${part 4}${part 5}"; userIs = group: user: builtins.elem group (user.extraGroups ++ [ user.group ]); + + mkWinPath = replaceStrings [ "/" ] [ "\\" ]; in { inherit tree nixlib inputs; std = inputs.self.lib.Std.Std.compat; Std = inputs.std-fl.lib; lib = { - inherit userIs eui64 toHexStringLower hexCharToInt; + inherit mkWinPath userIs eui64 toHexStringLower hexCharToInt; }; generate = import ./generate.nix { inherit inputs tree; }; } diff --git a/modules/nixos/steam/account-switch.nix b/modules/nixos/steam/account-switch.nix new file mode 100644 index 00000000..4367748f --- /dev/null +++ b/modules/nixos/steam/account-switch.nix @@ -0,0 +1,111 @@ +{ + config, + lib, + inputs, + pkgs, + ... +}: let + inherit (inputs.self.lib.lib) userIs; + inherit (lib.options) mkOption mkEnableOption; + inherit (lib.modules) mkIf mkMerge mkDefault mkOptionDefault; + inherit (lib.attrsets) filterAttrs mapAttrsToList listToAttrs nameValuePair; + inherit (lib.lists) singleton; + cfg = config.services.steam.accountSwitch; +in { + options.services.steam.accountSwitch = with lib.types; { + enable = mkEnableOption "steam-account-switch"; + setup = mkEnableOption "steam-account-switch data"; + group = mkOption { + type = str; + default = "steamaccount"; + }; + sharePath = mkOption { + type = str; + }; + rootDir = mkOption { + type = path; + }; + binDir = mkOption { + type = path; + default = cfg.rootDir + "/bin"; + }; + gamesDir = mkOption { + type = path; + default = cfg.rootDir + "/games"; + }; + dataDir = mkOption { + type = path; + default = cfg.rootDir + "/data"; + }; + sharedDataDir = mkOption { + type = path; + default = cfg.dataDir + "/shared"; + }; + workingDir = mkOption { + type = path; + default = cfg.rootDir + "/working"; + }; + sharedWorkingDir = mkOption { + type = path; + default = cfg.workingDir + "/shared"; + }; + users = mkOption { + type = listOf str; + }; + }; + + config = let + steamUsers = filterAttrs (_: userIs cfg.group) config.users.users; + in { + services.steam.accountSwitch = { + users = mkOptionDefault ( + mapAttrsToList (_: user: user.name) steamUsers + ); + }; + services.tmpfiles = let + toplevel = { + owner = mkDefault "admin"; + group = mkDefault cfg.group; + mode = mkDefault "3775"; + }; + shared = { + inherit (toplevel) owner group; + mode = "2775"; + }; + personal = owner: { + inherit owner; + inherit (shared) group mode; + }; + setupFiles = singleton { + ${cfg.rootDir} = toplevel; + ${cfg.binDir} = toplevel; + ${cfg.binDir + "/users"} = shared; + ${cfg.dataDir} = toplevel; + ${cfg.sharedDataDir} = shared; + ${cfg.workingDir} = toplevel; + ${cfg.sharedWorkingDir} = shared; + } ++ map (owner: { + ${cfg.dataDir + "/${owner}"} = personal owner; + ${cfg.workingDir + "/${owner}"} = personal owner; + }) cfg.users; + userBinFiles = listToAttrs (map (user: nameValuePair "${cfg.binDir}/users/${user}.bat" { + inherit (toplevel) owner group; + mode = "0755"; + type = "copy"; + src = pkgs.writeTextFile { + name = "steam-${user}.bat"; + executable = true; + text = '' + setx GENSO_STEAM_USER ${user} + ''; + }; + }) cfg.users); + in { + enable = mkIf (cfg.enable || cfg.setup) true; + files = mkMerge [ + (mkIf cfg.setup (mkMerge setupFiles)) + (mkIf cfg.enable userBinFiles) + ]; + }; + }; +} diff --git a/modules/nixos/steam/beatsaber.nix b/modules/nixos/steam/beatsaber.nix new file mode 100644 index 00000000..9665e25c --- /dev/null +++ b/modules/nixos/steam/beatsaber.nix @@ -0,0 +1,269 @@ +{ + config, + pkgs, + lib, + inputs, + ... +}: let + inherit (inputs.self.lib.lib) mkWinPath userIs; + inherit (lib.options) mkOption mkEnableOption; + inherit (lib.modules) mkIf mkMerge mkDefault mkOptionDefault; + inherit (lib.strings) removePrefix replaceStrings; + inherit (lib.attrsets) filterAttrs mapAttrs' mapAttrsToList listToAttrs nameValuePair; + inherit (lib.lists) concatMap head singleton; + inherit (lib.meta) getExe; + inherit (config.services.steam) accountSwitch; + cfg = config.services.steam.beatsaber; + versionModule = { config, name, ... }: { + options = with lib.types; { + version = mkOption { + type = str; + default = name; + }; + }; + }; + + mkSharePath = path: mkWinPath ( + "%GENSO_SMB_SHARED_MOUNT%" + + "/${accountSwitch.sharePath}" + + "/${removePrefix (accountSwitch.rootDir + "/") path}" + ); + vars = '' + if "%GENSO_STEAM_INSTALL%" == "" set "GENSO_STEAM_INSTALL=C:\Program Files (x86)\Steam" + if "%GENSO_STEAM_LIBRARY_BS%" == "" set "GENSO_STEAM_LIBRARY_BS=%GENSO_STEAM_INSTALL%" + if "%GENSO_STEAM_BS_VERSION%" == "" set "GENSO_STEAM_BS_VERSION=${cfg.defaultVersion}" + if "%GENSO_SMB_HOST%" == "" set "GENSO_SMB_HOST=smb.${config.networking.domain}" + if "%GENSO_SMB_SHARED_MOUNT%" == "" set "GENSO_SMB_SHARED_MOUNT=\\%GENSO_SMB_HOST%\shared" + set "STEAM_BS_LIBRARY=%GENSO_STEAM_LIBRARY_BS%\steamapps\common\Beat Saber" + set "STEAM_BS_APPDATA=%USERPROFILE%\AppData\LocalLow\Hyperbolic Magnetism\Beat Saber" + set "STEAM_USER_DATA=${mkSharePath accountSwitch.dataDir}\%GENSO_STEAM_USER%" + set "STEAM_WORKING_DATA=${mkSharePath accountSwitch.workingDir}\%GENSO_STEAM_USER%" + set "STEAM_BINDIR=${mkSharePath accountSwitch.binDir}" + if "%GENSO_STEAM_USER%" == "" goto NOUSER + ''; + eof = '' + goto:eof + + :NOUSER + echo no steam user set + ''; + mount = '' + rmdir "%STEAM_BS_APPDATA%" + mklink /D "%STEAM_BS_APPDATA%" "%STEAM_USER_DATA%\BeatSaber\AppData" + + rmdir "%STEAM_BS_LIBRARY%" + mklink /D "%STEAM_BS_LIBRARY%" "%STEAM_WORKING_DATA%\BeatSaber\%GENSO_STEAM_BS_VERSION%" + ''; + mountbeatsaber = '' + ${vars} + ${mount} + ${eof} + ''; + launchbeatsaber = '' + ${vars} + ${mount} + cd /d "%STEAM_BS_LIBRARY%" + "%STEAM_BS_LIBRARY%\Beat Saber.exe" + ${eof} + ''; + fpfcbeatsaber = '' + ${vars} + ${mount} + cd /d "%STEAM_BS_LIBRARY%" + "%STEAM_BS_LIBRARY%\Beat Saber.exe" fpfc + ${eof} + ''; + + mkbeatsabersh = pkgs.writeShellScriptBin "mkbeatsaber.sh" '' + source ${./mkbeatsaber.sh} + ''; + mkbeatsaber = pkgs.writeShellScriptBin "mkbeatsaber" '' + set -eu + + ARG_GAME_VERSION=$1 + shift + if [[ $# -gt 0 ]]; then + ARG_USER=$1 + shift + else + ARG_USER=$(${pkgs.coreutils}/bin/id -un) + fi + + cd ${accountSwitch.workingDir} + mkdir -m2775 -p "$ARG_USER/BeatSaber/$ARG_GAME_VERSION" + chown "$ARG_USER" "$ARG_USER" "$ARG_USER/BeatSaber" + cd "$ARG_USER/BeatSaber/$ARG_GAME_VERSION" + ${getExe mkbeatsabersh} \ + "${accountSwitch.gamesDir}/BeatSaber" \ + "$ARG_GAME_VERSION" \ + "${accountSwitch.sharedDataDir}/BeatSaber" \ + "${accountSwitch.dataDir}/$ARG_USER/BeatSaber" + ''; +in { + options.services.steam.beatsaber = with lib.types; { + enable = mkEnableOption "beatsaber scripts"; + setup = mkEnableOption "beatsaber data" // { + default = accountSwitch.setup; + }; + group = mkOption { + type = str; + default = "beatsaber"; + }; + defaultVersion = mkOption { + type = str; + }; + versions = mkOption { + type = attrsOf (submodule versionModule); + default = { }; + }; + users = mkOption { + type = listOf str; + }; + }; + + config = let + in { + services.steam.beatsaber = let + bsUsers = filterAttrs (_: userIs cfg.group) config.users.users; + allVersions = mapAttrsToList (_: version: version.version) cfg.versions; + in { + defaultVersion = mkIf (allVersions != [ ]) (mkOptionDefault ( + head allVersions + )); + users = mkOptionDefault ( + mapAttrsToList (_: user: user.name) bsUsers + ); + }; + environment = mkIf cfg.enable { + systemPackages = [ + mkbeatsaber + mkbeatsabersh + ]; + }; + systemd.services = mkIf cfg.setup (listToAttrs (map (user: nameValuePair "steam-setup-beatsaber-${user}" { + script = mkMerge (mapAttrsToList (_: version: '' + ${getExe mkbeatsaber} ${version.version} ${user} + '') cfg.versions); + path = [ + pkgs.coreutils + ]; + wantedBy = [ + "multi-user.target" + ]; + after = [ + "tmpfiles.service" + ]; + serviceConfig = { + RemainAfterExit = mkOptionDefault true; + User = mkOptionDefault user; + }; + }) cfg.users)); + services.tmpfiles = let + toplevel = { + owner = mkDefault "admin"; + group = mkDefault cfg.group; + mode = mkDefault "3775"; + }; + shared = { + inherit (toplevel) owner group; + mode = mkDefault "2775"; + }; + personal = owner: { + inherit owner; + inherit (shared) group mode; + }; + bin = { + inherit (toplevel) owner group; + mode = "0755"; + type = "copy"; + }; + sharedFolders = [ + "CustomAvatars" + "CustomLevels" + "CustomNotes" + "CustomPlatforms" + "CustomSabers" + "CustomWalls" + "AppData" + "UserData" + ]; + setupFiles = [ + { + "${accountSwitch.sharedDataDir}/BeatSaber" = toplevel; + "${accountSwitch.binDir}/beatsaber" = shared; + } + (listToAttrs ( + map (folder: + nameValuePair "${accountSwitch.sharedDataDir}/BeatSaber/${folder}" shared + ) sharedFolders + )) + ] ++ concatMap (owner: + singleton { + "${accountSwitch.dataDir}/${owner}/BeatSaber" = personal owner; + "${accountSwitch.dataDir}/${owner}/BeatSaber/AppData" = personal owner; + "${accountSwitch.dataDir}/${owner}/BeatSaber/UserData" = personal owner; + } ++ mapAttrsToList (_: version: { + "${accountSwitch.dataDir}/${owner}/BeatSaber/${version.version}" = personal owner; + }) cfg.versions + ) accountSwitch.users + ++ mapAttrsToList (_: version: { + "${accountSwitch.sharedDataDir}/BeatSaber/${version.version}" = shared; + }) cfg.versions; + versionBinFiles = mapAttrs' (_: version: nameValuePair + "${accountSwitch.binDir}/beatsaber/${replaceStrings [ "." ] [ "_" ] version.version}.bat" + { + inherit (bin) owner group mode type; + src = pkgs.writeTextFile { + name = "beatsaber-${version.version}.bat"; + executable = true; + text = '' + setx GENSO_STEAM_BS_VERSION ${version.version} + ''; + }; + } + ) cfg.versions; + binFiles = { + "${accountSwitch.binDir}/beatsaber/mount.bat" = { + inherit (bin) owner group mode type; + src = pkgs.writeTextFile { + name = "beatsaber-mount.bat"; + executable = true; + text = mountbeatsaber; + }; + }; + "${accountSwitch.binDir}/beatsaber/launch.bat" = { + inherit (bin) owner group mode type; + src = pkgs.writeTextFile { + name = "beatsaber-launch.bat"; + executable = true; + text = launchbeatsaber; + }; + }; + "${accountSwitch.binDir}/beatsaber/fpfc.bat" = { + inherit (bin) owner group mode type; + src = pkgs.writeTextFile { + name = "beatsaber-fpfc.bat"; + executable = true; + text = fpfcbeatsaber; + }; + }; + "${accountSwitch.binDir}/beatsaber/ModAssistant.exe" = { + inherit (toplevel) owner group; + mode = "0755"; + type = "copy"; + src = pkgs.fetchurl { + url = "https://github.com/Assistant/ModAssistant/releases/download/v1.1.32/ModAssistant.exe"; + hash = "sha256-ozu2gYFiz+2BjptqL80DmUopbahbyGKFO1IPd7BhVPM="; + executable = true; + }; + }; + } // versionBinFiles; + in { + enable = mkIf (cfg.enable || cfg.setup) true; + files = mkMerge [ + (mkIf cfg.setup (mkMerge setupFiles)) + (mkIf cfg.enable binFiles) + ]; + }; + }; +} diff --git a/modules/nixos/steam/library.nix b/modules/nixos/steam/library.nix new file mode 100644 index 00000000..1b14f047 --- /dev/null +++ b/modules/nixos/steam/library.nix @@ -0,0 +1,46 @@ +{ + config, + lib, + ... +}: let + inherit (lib.options) mkOption mkEnableOption; + inherit (lib.modules) mkIf mkDefault; + inherit (config.services.steam) accountSwitch; + cfg = config.services.steam.library; +in { + options.services.steam.library = with lib.types; { + setup = mkEnableOption "steam library data"; + group = mkOption { + type = str; + default = accountSwitch.group; + }; + rootDir = mkOption { + type = path; + }; + steamappsDir = mkOption { + type = path; + default = cfg.rootDir + "/steamapps"; + }; + }; + + config = { + services.tmpfiles = let + toplevel = { + owner = mkDefault "admin"; + group = mkDefault cfg.group; + mode = mkDefault "3775"; + }; + shared = { + inherit (toplevel) owner group; + mode = "2775"; + }; + setupFiles = { + ${cfg.rootDir} = toplevel; + ${cfg.steamappsDir} = shared; + }; + in { + enable = mkIf cfg.setup true; + files = mkIf cfg.setup setupFiles; + }; + }; +} diff --git a/modules/nixos/steam/mkbeatsaber.sh b/modules/nixos/steam/mkbeatsaber.sh new file mode 100644 index 00000000..fb4485d1 --- /dev/null +++ b/modules/nixos/steam/mkbeatsaber.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -eu + +ARG_GAME_SRC=$1 +ARG_GAME_VERSION=$2 +ARG_SHARED_DATA=$3 +ARG_USER_DATA=$4 +shift 4 + +if ! [[ -e "$ARG_GAME_SRC/$ARG_GAME_VERSION/Beat Saber.exe" ]]; then + echo unexpected game src >&2 + exit 1 +fi + +ln -srf "$ARG_GAME_SRC/$ARG_GAME_VERSION/"*.{exe,dll} ./ +ln -srf "$ARG_GAME_SRC/$ARG_GAME_VERSION/"{MonoBleedingEdge,Plugins} ./ +rm "Beat Saber.exe" +cp "$ARG_GAME_SRC/$ARG_GAME_VERSION/Beat Saber.exe" ./ +chmod 0775 "Beat Saber.exe" + +BSDATA="Beat Saber_Data" +mkdir -pm2775 "$BSDATA" +ln -srf "$ARG_GAME_SRC/$ARG_GAME_VERSION/$BSDATA/"* "$BSDATA/" || true +ln -srf "$ARG_SHARED_DATA/CustomLevels" "$BSDATA/" +rm -f "$BSDATA/Managed" + +mkdir -pm2775 UserData + +ln -srf "$ARG_SHARED_DATA/"{CustomAvatars,CustomNotes,CustomPlatforms,CustomSabers,CustomWalls,Playlists} ./ +for shareddir in DynamicOpenVR IPA Libs Logs Plugins "$BSDATA/Managed" UserData/SongCore; do + shareddirsrc="$ARG_SHARED_DATA/$ARG_GAME_VERSION/$shareddir" + if [[ ! -e $shareddirsrc ]]; then + mkdir -pm2775 "$shareddirsrc" + if [[ $shareddir = */Managed ]]; then + cp "$ARG_GAME_SRC/$ARG_GAME_VERSION/$BSDATA/Managed/"* "$shareddirsrc/" || true + chmod 0775 "$shareddirsrc/"*.dll || true + fi + fi + ln -srf "$shareddirsrc" "./$(dirname "$shareddir")" +done +for sharedfile in IPA.exe IPA.exe.config IPA.runtimeconfig.json winhttp.dll; do + sharedfilesrc="$ARG_SHARED_DATA/$ARG_GAME_VERSION/$sharedfile" + if [[ ! -e "$sharedfilesrc" ]]; then + mkdir -pm2775 "$(dirname "$sharedfilesrc")" + if [[ $sharedfile = *.json ]]; then + echo '{}' > "$sharedfilesrc" + else + touch "$sharedfilesrc" + fi + chmod 0775 "$sharedfilesrc" || true + fi + ln -f "$sharedfilesrc" ./ +done + +for sharedfile in "Beat Saber IPA.json"; do + sharedfilesrc="$ARG_SHARED_DATA/$ARG_GAME_VERSION/UserData/$sharedfile" + if [[ ! -e "$sharedfilesrc" ]]; then + mkdir -pm2775 "$(dirname "$sharedfilesrc")" + if [[ $sharedfile = *.json ]]; then + echo '{}' > "$sharedfilesrc" + else + touch "$sharedfilesrc" + fi + fi + ln -f "$sharedfilesrc" "UserData/$(dirname "$sharedfile")" +done +ln -f "$ARG_SHARED_DATA/UserData/"*.{json,ini,proto,etag} UserData/ +ln -srf "$ARG_SHARED_DATA/UserData/"{ScoreSaber,Chroma,Nya,SongRankedBadge,HitScoreVisualizer}/ UserData/ + +SFDATA="UserData/Saber Factory" +mkdir -pm2775 "$SFDATA" +ln -srf "$ARG_SHARED_DATA/$SFDATA/"*/ "$SFDATA/" +ln -srf "$ARG_USER_DATA/$SFDATA/"*/ "$SFDATA/" +ln -f "$ARG_USER_DATA/$SFDATA/"*.json "$SFDATA/" + +for userdir in Camera2 DrinkWater Enhancements; do + userdirsrc="$ARG_USER_DATA/UserData/$userdir" + if [[ ! -e $userdirsrc ]]; then + mkdir -pm3775 "$userdirsrc" + fi + ln -srf "$userdirsrc" UserData/ +done +ln -f "$ARG_USER_DATA/UserData/"*.{json,ini,dat} UserData/ diff --git a/modules/nixos/tmpfiles.nix b/modules/nixos/tmpfiles.nix new file mode 100644 index 00000000..690d55ba --- /dev/null +++ b/modules/nixos/tmpfiles.nix @@ -0,0 +1,219 @@ +{ + config, + lib, + pkgs, + ... +}: let + inherit (lib.options) mkOption mkEnableOption; + inherit (lib.modules) mkIf mkMerge mkOptionDefault; + inherit (lib.strings) match concatStringsSep escapeShellArg optionalString; + inherit (lib.attrsets) attrValues; + inherit (lib.lists) filter; + isGroupWritable = mode: match "[234567][0-7][76][0-7]" mode != null; + isOtherWritable = mode: match "[0-7][0-7][0-7][76]" mode != null; + cfg = config.services.tmpfiles; + files = filter (file: file.enable) (attrValues cfg.files); + systemdFiles = filter (file: file.systemd.enable) files; + setupFiles = filter (file: !file.systemd.enable) files; + bindFiles = filter (file: file.type == "bind") files; + fileModule = { config, name, ... }: { + options = with lib.types; { + enable = mkEnableOption "file" // { + default = true; + }; + mkdirParent = mkEnableOption "mkdir"; + bindReadOnly = mkEnableOption "mount -oro"; + path = mkOption { + type = path; + default = name; + }; + type = mkOption { + type = enum [ "directory" "symlink" "link" "copy" "bind" ]; + default = if config.src != null then "symlink" else "directory"; + }; + mode = mkOption { + type = str; + default = "0755"; + }; + owner = mkOption { + type = str; + default = cfg.user; + }; + group = mkOption { + type = str; + default = "root"; + }; + src = mkOption { + type = nullOr path; + default = null; + }; + acls = mkOption { + type = listOf str; + }; + systemd = { + enable = mkEnableOption "systemd-tmpfiles"; + rules = mkOption { + type = listOf str; + }; + }; + setup = { + script = mkOption { + type = lines; + }; + }; + }; + config = let + acls = concatStringsSep "," config.acls; + enableAcls = config.type == "directory" && config.acls != [ ]; + systemdAclRule = "a+ ${config.path} - - - - ${acls}"; + systemdRule = { + directory = [ + "d ${config.path} ${config.mode} ${config.owner} ${config.group}" + ]; + symlink = [ + "L+ ${config.path} - - - - ${config.src}" + ]; + copy = [ + "C ${config.path} - - - - ${config.src}" + "z ${config.path} ${config.mode} ${config.owner} ${config.group} - ${config.src}" + ]; + link = throw "unsupported link for systemd tmpfiles"; + bind = throw "unsupported bind for systemd tmpfiles"; + }; + chown = "chown ${escapeShellArg config.owner}:${escapeShellArg config.group} ${escapeShellArg config.path}"; + chmod = "chmod ${escapeShellArg config.mode} ${escapeShellArg config.path}"; + parentFlag = optionalString config.mkdirParent "p"; + scriptCatch = " || EXITCODE=$?"; + scriptFail = "EXITCODE=1"; + setupScript = { + directory = '' + if [[ -d ${escapeShellArg config.path} ]]; then + ${chmod} && + ${chown}${scriptCatch} + elif [[ ! -e ${escapeShellArg config.path} ]]; then + mkdir -${parentFlag}m ${escapeShellArg config.mode} ${escapeShellArg config.path} && + ${chown}${scriptCatch} + else + echo ${escapeShellArg config.path} exists but is not a directory >&2 + ${scriptFail} + fi + ''; + symlink = '' + if [[ ! -e ${escapeShellArg config.path} || -L ${escapeShellArg config.path} ]]; then + ln -sf ${escapeShellArg config.src} ${escapeShellArg config.path}${scriptCatch} + else + echo ${escapeShellArg config.path} exists but is not a symlink >&2 + ${scriptFail} + fi + ''; + link = '' + if [[ -L ${escapeShellArg config.path} ]]; then + rm -f ${escapeShellArg config.path} + fi + ln -f ${escapeShellArg config.src} ${escapeShellArg config.path}${scriptCatch} + ''; + copy = '' + if [[ ! -e ${escapeShellArg config.path} || -f ${escapeShellArg config.path} ]]; then + cp -f ${escapeShellArg config.src} ${escapeShellArg config.path} && + ${chmod} && + ${chown}${scriptCatch} + else + echo ${escapeShellArg config.path} exists but is not a file >&2 + ${scriptFail} + fi + ''; + bind = '' + if [[ ! -e ${escapeShellArg config.src} ]]; then + echo ${escapeShellArg config.src} does not exist >&2 + ${scriptFail} + elif [[ -d $(readlink -f ${escapeShellArg config.src}) ]]; then + mkdir -p ${escapeShellArg config.path}${scriptCatch} + else + if [[ ! -e ${escapeShellArg config.path} ]]; then + touch ${escapeShellArg config.path}${scriptCatch} + fi + fi + ''; + }; + aclScript = '' + setfacl -b -m ${escapeShellArg acls} ${escapeShellArg config.path}${scriptCatch} + ''; + in { + acls = mkOptionDefault [ + (mkIf (isGroupWritable config.mode) "default:group::rwx") + (mkIf (isOtherWritable config.mode) "default:other::rwx") + ]; + setup.script = mkMerge [ + setupScript.${config.type} + (mkIf enableAcls aclScript) + ]; + systemd = { + rules = mkMerge [ + systemdRule.${config.type} + (mkIf enableAcls [ systemdAclRule ]) + ]; + }; + }; + }; +in { + options.services.tmpfiles = with lib.types; { + enable = mkEnableOption "extended tmpfiles" // { + default = cfg.files != { }; + }; + user = mkOption { + type = str; + default = if config.proxmoxLXC.privileged or true then "root" else "admin"; + }; + files = mkOption { + type = attrsOf (submodule fileModule); + default = { }; + }; + }; + config = { + systemd = mkIf cfg.enable { + tmpfiles.rules = mkMerge ( + map (file: file.systemd.rules) systemdFiles + ); + services.tmpfiles = { + path = [ pkgs.coreutils pkgs.acl ]; + script = mkMerge ( + [ '' + EXITCODE=0 + '' ] + ++ map (file: file.setup.script) setupFiles + ++ [ '' + exit $EXITCODE + '' ] + ); + wantedBy = [ + "sysinit.target" + ]; + after = [ + "local-fs.target" + ]; + before = [ + "systemd-tmpfiles-setup.service" + "systemd-tmpfiles-resetup.service" + ]; + serviceConfig = { + User = mkOptionDefault cfg.user; + RemainAfterExit = mkOptionDefault true; + }; + }; + mounts = map (file: rec { + enable = file.enable; + type = "none"; + options = mkMerge [ + "bind" + (mkIf file.bindReadOnly "ro") + ]; + what = file.src; + where = file.path; + wantedBy = [ + "tmpfiles.service" + ]; + after = wantedBy; + }) bindFiles; + }; + }; +} diff --git a/nixos/kyuuto/mount.nix b/nixos/kyuuto/mount.nix index ca2fefd6..69e42510 100644 --- a/nixos/kyuuto/mount.nix +++ b/nixos/kyuuto/mount.nix @@ -4,9 +4,10 @@ ... }: let inherit (lib.options) mkOption mkEnableOption; - inherit (lib.modules) mkIf mkMerge; - inherit (lib.strings) match concatStringsSep; - inherit (lib.lists) optional; + inherit (lib.modules) mkIf mkMerge mkDefault; + inherit (lib.strings) removePrefix; + inherit (lib.attrsets) listToAttrs nameValuePair; + inherit (config.services.steam) accountSwitch; cfg = config.kyuuto; in { options.kyuuto = with lib.types; { @@ -15,49 +16,96 @@ in { type = path; default = "/mnt/kyuuto-media"; }; - libraryDir = mkOption { + shareDir = mkOption { type = path; - default = cfg.mountDir + "/library"; + default = cfg.mountDir + "/shared"; }; transferDir = mkOption { type = path; default = cfg.mountDir + "/transfer"; }; - shareDir = mkOption { + libraryDir = mkOption { type = path; - default = cfg.mountDir + "/shared"; + default = cfg.mountDir + "/library"; + }; + gameLibraryDir = mkOption { + type = path; + default = cfg.libraryDir + "/games"; + }; + gameLibraries = mkOption { + type = listOf str; + default = [ "PC" ]; }; }; config = { - systemd.tmpfiles.rules = let - isGroupWritable = mode: match "[375][0-7][76][0-7]" mode != null; - isOtherWritable = mode: match "[375][0-7][0-7][76]" mode != null; - mkKyuutoDir = { - path, - mode ? "3775", - owner ? "guest", - group ? "kyuuto", - acls ? optional (isGroupWritable mode) "default:group::rwx" - ++ optional (isOtherWritable mode) "default:other::rwx", - }: [ - "d ${path} ${mode} ${owner} ${group}" - ] ++ optional (acls != [ ]) "a+ ${path} - - - - ${concatStringsSep "," acls}"; - in mkIf cfg.setup ( - mkKyuutoDir { path = cfg.transferDir; } - ++ mkKyuutoDir { path = cfg.shareDir; owner = "root"; } - ++ mkKyuutoDir { path = cfg.libraryDir; owner = "root"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/unsorted"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/music"; owner = "root"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/music/assorted"; owner = "sonarr"; mode = "7775"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/music/collections"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/anime"; owner = "sonarr"; mode = "7775"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/tv"; owner = "sonarr"; mode = "7775"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/movies"; owner = "radarr"; mode = "7775"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/software"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/books"; } - ++ mkKyuutoDir { path = cfg.libraryDir + "/games"; } - ); + kyuuto = { + gameLibraries = [ + "PC" + "Wii" "Gamecube" "N64" "SNES" "NES" + "NDS" "GBA" "GBC" + "PS3" "PS2" "PS1" + "PSVita" "PSP" + "Genesis" + ]; + }; + services.steam = { + library = { + setup = mkDefault cfg.setup; + rootDir = cfg.shareDir + "/steam/library"; + }; + accountSwitch = { + setup = mkDefault cfg.setup; + sharePath = removePrefix "${cfg.shareDir}/" accountSwitch.rootDir; + rootDir = cfg.shareDir + "/steam"; + }; + }; + services.tmpfiles = let + shared = { + owner = mkDefault "admin"; + group = mkDefault "kyuuto"; + mode = mkDefault "3775"; + }; + leaf = { + inherit (shared) owner group; + mode = mkDefault "2775"; + }; + setupFiles = [ + { + ${cfg.shareDir} = mkMerge [ + shared + { group = "peeps"; } + ]; + ${cfg.transferDir} = shared; + ${cfg.libraryDir} = shared; + ${cfg.libraryDir + "/unsorted"} = shared; + ${cfg.libraryDir + "/music"} = shared; + ${cfg.libraryDir + "/music/assorted"} = leaf; + ${cfg.libraryDir + "/music/collections"} = shared; + ${cfg.libraryDir + "/anime"} = leaf; + ${cfg.libraryDir + "/tv"} = leaf; + ${cfg.libraryDir + "/movies"} = leaf; + ${cfg.libraryDir + "/software"} = leaf; + ${cfg.libraryDir + "/books"} = leaf; + ${cfg.gameLibraryDir} = shared; + } + (listToAttrs ( + map (gameLibrary: nameValuePair (cfg.gameLibraryDir + "/${gameLibrary}") leaf) cfg.gameLibraries + )) + ]; + in { + enable = mkIf cfg.setup true; + files = mkMerge [ + (mkIf cfg.setup (mkMerge setupFiles)) + (mkIf accountSwitch.enable { + ${accountSwitch.gamesDir} = { + type = "bind"; + bindReadOnly = true; + src = cfg.gameLibraryDir + "/PC"; + }; + }) + ]; + }; users = let mapId = id: if config.proxmoxLXC.privileged or true then 100000 + id else id; diff --git a/nixos/kyuuto/samba.nix b/nixos/kyuuto/samba.nix index 0ee15f66..cd7bc7fd 100644 --- a/nixos/kyuuto/samba.nix +++ b/nixos/kyuuto/samba.nix @@ -78,8 +78,8 @@ in { public = false; browseable = false; "valid users" = [ "@peeps" ]; - "acl group control" = true; - "create mask" = "0664"; + "create mask" = "0775"; + "force file mode" = "3010"; "force directory mode" = "3000"; "directory mask" = "7775"; }; diff --git a/nixos/steam/account-switch.nix b/nixos/steam/account-switch.nix new file mode 100644 index 00000000..4760fe19 --- /dev/null +++ b/nixos/steam/account-switch.nix @@ -0,0 +1,10 @@ +{ + lib, + ... +}: let + inherit (lib.modules) mkDefault; +in { + services.steam.accountSwitch = { + enable = mkDefault true; + }; +} diff --git a/nixos/steam/beatsaber.nix b/nixos/steam/beatsaber.nix new file mode 100644 index 00000000..c44a0c24 --- /dev/null +++ b/nixos/steam/beatsaber.nix @@ -0,0 +1,15 @@ +{ + lib, + ... +}: let + inherit (lib.modules) mkDefault; +in { + services.steam.beatsaber = { + enable = mkDefault true; + defaultVersion = mkDefault "1.29.0"; + versions = { + "1.29.0" = { }; + "1.34.2" = { }; + }; + }; +} diff --git a/nixos/users/arc.nix b/nixos/users/arc.nix index fbf7aa99..5ca4f683 100644 --- a/nixos/users/arc.nix +++ b/nixos/users/arc.nix @@ -6,7 +6,12 @@ isNormalUser = true; autoSubUidGidRange = false; group = name; - extraGroups = [ "users" "peeps" "kyuuto" "wheel" ]; + extraGroups = [ + "users" "peeps" + "kyuuto" + "steamaccount" "beatsaber" + "wheel" + ]; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8Z6briIboxIdedPGObEWB6QEQkvxKvnMW/UVU9t/ac mew-pgp" ]; diff --git a/nixos/users/connie.nix b/nixos/users/connie.nix index 4fdb0e96..8fe160eb 100644 --- a/nixos/users/connie.nix +++ b/nixos/users/connie.nix @@ -6,7 +6,10 @@ isNormalUser = true; autoSubUidGidRange = false; group = name; - extraGroups = [ "users" "peeps" "kyuuto" ]; + extraGroups = [ + "users" "peeps" + "kyuuto" + ]; }; users.groups.connieallure = { name, ... }: { gid = config.users.users.${name}.uid; diff --git a/nixos/users/groups.nix b/nixos/users/groups.nix index b6f3fe6d..bc811e98 100644 --- a/nixos/users/groups.nix +++ b/nixos/users/groups.nix @@ -20,6 +20,19 @@ in { filterAttrs (_: user: userIs "peeps" user && userIs "kyuuto" user) config.users.users ); }; + steamaccount = { + gid = 8131; + }; + beatsaber = { + gid = 8132; + }; + + admin = { + gid = 8126; + members = mapAttrsToList (_: user: user.name) ( + filterAttrs (_: user: userIs "peeps" user && userIs "wheel" user) config.users.users + ); + }; }; users.users = { guest = { @@ -27,5 +40,10 @@ in { group = "nogroup"; isSystemUser = true; }; + admin = { + uid = 8126; + group = "admin"; + isSystemUser = true; + }; }; } diff --git a/nixos/users/kaosu.nix b/nixos/users/kaosu.nix index ab3df0e4..98891a20 100644 --- a/nixos/users/kaosu.nix +++ b/nixos/users/kaosu.nix @@ -6,7 +6,11 @@ isNormalUser = true; autoSubUidGidRange = false; group = name; - extraGroups = [ "users" "peeps" "kyuuto" ]; + extraGroups = [ + "users" "peeps" + "kyuuto" + "steamaccount" "beatsaber" + ]; }; users.groups.kaosubaloo = { name, ... }: { gid = config.users.users.${name}.uid; diff --git a/nixos/users/kat.nix b/nixos/users/kat.nix index b6e8cf90..084bdfd9 100644 --- a/nixos/users/kat.nix +++ b/nixos/users/kat.nix @@ -6,7 +6,12 @@ isNormalUser = true; autoSubUidGidRange = false; group = name; - extraGroups = [ "users" "peeps" "kyuuto" "wheel" ]; + extraGroups = [ + "users" "peeps" + "kyuuto" + "steamaccount" "beatsaber" + "wheel" + ]; openssh.authorizedKeys.keys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCocjQqiDIvzq+Qu3jkf7FXw5piwtvZ1Mihw9cVjdVcsra3U2c9WYtYrA3rS50N3p00oUqQm9z1KUrvHzdE+03ZCrvaGdrtYVsaeoCuuvw7qxTQRbItTAEsfRcZLQ5c1v/57HNYNEsjVrt8VukMPRXWgl+lmzh37dd9w45cCY1QPi+JXQQ/4i9Vc3aWSe4X6PHOEMSBHxepnxm5VNHm4PObGcVbjBf0OkunMeztd1YYA9sEPyEK3b8IHxDl34e5t6NDLCIDz0N/UgzCxSxoz+YJ0feQuZtud/YLkuQcMxW2dSGvnJ0nYy7SA5DkW1oqcy6CGDndHl5StOlJ1IF9aGh0gGkx5SRrV7HOGvapR60RphKrR5zQbFFka99kvSQgOZqSB3CGDEQGHv8dXKXIFlzX78jjWDOBT67vA/M9BK9FS2iNnBF5x6shJ9SU5IK4ySxq8qvN7Us8emkN3pyO8yqgsSOzzJT1JmWUAx0tZWG/BwKcFBHfceAPQl6pwxx28TM3BTBRYdzPJLTkAy48y6iXW6UYdfAPlShy79IYjQtEThTuIiEzdzgYdros0x3PDniuAP0KOKMgbikr0gRa6zahPjf0qqBnHeLB6nHAfaVzI0aNbhOg2bdOueE1FX0x48sjKqjOpjlIfq4WeZp9REr2YHEsoLFOBfgId5P3BPtpBQ== yubikey5" "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDPsu3vNsvBb/G+wALpstD/DnoRZ3fipAs00jtl8rzDuv96RlS7AJr4aNvG6Pt2D9SYn2wVLaiw+76mz2gOycH9/N+VCvL4/0MN9uqj+7XIcxNRo0gHVOblmi2bOXcmGKh3eRwHj1xyDwRxo9WIuBEP2bPpDPz75OXRtEdlTgvky7siSguQxJu03cb0p9hNAYhUoohNXyWW2CjDCLUQVE1+QRVUzsKq3KkPy0cHYgmZC1gRSMQyKpMt72L5tayLz3Tp/zrshucc+QO5IJeZdqMxsNAcvALsysT1J5EqxZoYH9VpWLRhSgVD6Nvn853pycJAlXQxgOCpSD3/v/JbgUe5NE+ci0o7NMy5IiHUv2gQMRIEhwBHlRGwokUPL9upx0lsjaEiPya5xQqqDKRom87xytM778ANS5CuMdQMWg9qVbpHZUHMjA0QmNkjPgq71pUDXHk5L4mZuS8wVjyjnvlw68yIJuHEc8P7QiLcjvRHFS2L9Ck8NRmPDTQXlQi9kk6LmMyu6fdevR/kZL21b+xO1e2DMyxBbNDTot8luppiiL8adgUDMwptpIne7JCWB1o9NFCbXUVgwuCCYBif6pOGSc6bGo1JTAKMflRlcy6Mi3t5H0mR2lj/sCSTWwTlP5FM4aPIq08NvW6PeuK1bFJY9fIgTwVsUnbAKOhmsMt62w== cardno:12 078 454" diff --git a/systems/hakurei/nixos.nix b/systems/hakurei/nixos.nix index 2471d0c3..d8aff520 100644 --- a/systems/hakurei/nixos.nix +++ b/systems/hakurei/nixos.nix @@ -18,6 +18,8 @@ in { nixos.base nixos.reisen-ct nixos.kyuuto + nixos.steam.account-switch + nixos.steam.beatsaber nixos.tailscale nixos.cloudflared nixos.ddclient diff --git a/systems/reimu/nixos.nix b/systems/reimu/nixos.nix index 76ee5fe3..1fa1a8e9 100644 --- a/systems/reimu/nixos.nix +++ b/systems/reimu/nixos.nix @@ -9,11 +9,17 @@ nixos.base nixos.reisen-ct nixos.kyuuto + nixos.steam.account-switch + nixos.steam.beatsaber nixos.tailscale nixos.nfs ]; kyuuto.setup = true; + services.steam = { + accountSwitch.enable = false; + beatsaber.enable = false; + }; proxmoxLXC.privileged = true; diff --git a/tree.nix b/tree.nix index 622b4bef..7c6c1812 100644 --- a/tree.nix +++ b/tree.nix @@ -30,6 +30,7 @@ }; "modules/nixos" = { functor = { + enable = true; external = with (import (inputs.arcexprs + "/modules")).nixos; [ nix systemd @@ -54,7 +55,7 @@ ]; }; }; - "modules/nixos".functor.enable = true; + "modules/nixos/steam".functor.enable = true; "modules/meta".functor.enable = true; "modules/system".functor.enable = true; "modules/home".functor.enable = true;