diff --git a/.github/workflows/lint_everything.yaml b/.github/workflows/lint_everything.yaml index ef356eaa..e7fd943a 100644 --- a/.github/workflows/lint_everything.yaml +++ b/.github/workflows/lint_everything.yaml @@ -120,7 +120,7 @@ jobs: - name: Build nix flake run: | cd tests/nix-flake - nix build --no-write-lock-file --dry-run + nix build --no-write-lock-file --dry-run .#podman .#docker .#fhs yaml-lint: name: Yaml - Lint with yamllint runs-on: ubuntu-latest diff --git a/docs/advanced/nixos.md b/docs/advanced/nixos.md index cebc00a6..e18522c6 100644 --- a/docs/advanced/nixos.md +++ b/docs/advanced/nixos.md @@ -40,7 +40,7 @@ environment variables in camelCase: image = "docker.io/netbrain/zwift"; # The zwift game version to run version = "1.67.0"; - # Container tool to run zwift (e.g., "podman" or "docker") + # Container tool to run zwift ("podman", "docker", or "fhs" for native Wine) containerTool = "podman"; # If true, do not pull the image (use locally cached image) dontPull = false; @@ -82,6 +82,8 @@ environment variables in camelCase: debug = false; # If set, run container in privileged mode ("--privileged --security-opt label=disable") privilegedContainer = false; + # Custom Wine prefix directory (only used when containerTool = "fhs", defaults to ~/.wine-zwift) + winePrefix = ""; }; } ``` diff --git a/flake.lock b/flake.lock index 9d9152b2..a1b31611 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1770141374, - "narHash": "sha256-yD4K/vRHPwXbJf5CK3JkptBA6nFWUKNX/jlFp2eKEQc=", + "lastModified": 1784856561, + "narHash": "sha256-J+Bx1Z6Oeoj2FgnBhRMKyUhhtDoOpTgXYaVLZpDjW4A=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "41965737c1797c1d83cfb0b644ed0840a6220bd1", + "rev": "597283ad8aa0b331c788e97c4c262d58877074ef", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "nixos-26.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 5cec3394..5eddcb56 100644 --- a/flake.nix +++ b/flake.nix @@ -1,281 +1,30 @@ { description = "Easily zwift on linux"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; outputs = - { nixpkgs, self }: + { + nixpkgs, + self, + }: let - pkgs = nixpkgs.legacyPackages.x86_64-linux; - wrapPackage = - { - image, - tag, - dontCheck, - dontPull, - dontClean, - dryRun, - interactive, - containerTool, - containerExtraArgs, - zwiftUsername, - zwiftPassword, - zwiftWorkoutDir, - zwiftActivityDir, - zwiftLogDir, - zwiftScreenshotsDir, - zwiftOverrideGraphics, - zwiftOverrideResolution, - zwiftFg, - zwiftNoGameMode, - wineExperimentalWayland, - networking, - zwiftUid, - zwiftGid, - vgaDeviceFlag, - debug, - verbosity, - privilegedContainer, - }: - pkgs.stdenv.mkDerivation rec { - pname = "zwift"; - version = "0-unstable"; - - src = self.packages.x86_64-linux.zwift; - - nixosRun = pkgs.writeShellScript "zwift-nixos.sh" '' - ${pkgs.lib.optionalString (image != "") "export IMAGE='${image}'"} - ${pkgs.lib.optionalString (tag != "") "export VERSION='${tag}'"} - ${pkgs.lib.optionalString (dontCheck != "") "export DONT_CHECK=${dontCheck}"} - ${pkgs.lib.optionalString (dontPull != "") "export DONT_PULL=${dontPull}"} - ${pkgs.lib.optionalString (dontClean != "") "export DONT_CLEAN=${dontClean}"} - ${pkgs.lib.optionalString (dryRun != "") "export DRYRUN=${dryRun}"} - ${pkgs.lib.optionalString (interactive != "") "export INTERACTIVE=${interactive}"} - ${pkgs.lib.optionalString (containerTool != "") "export CONTAINER_TOOL='${containerTool}'"} - ${pkgs.lib.optionalString ( - containerExtraArgs != "" - ) "export CONTAINER_EXTRA_ARGS='${containerExtraArgs}'"} - ${pkgs.lib.optionalString (zwiftUsername != "") "export ZWIFT_USERNAME='${zwiftUsername}'"} - ${pkgs.lib.optionalString (zwiftPassword != "") "export ZWIFT_PASSWORD='${zwiftPassword}'"} - ${pkgs.lib.optionalString (zwiftWorkoutDir != "") "export ZWIFT_WORKOUT_DIR='${zwiftWorkoutDir}'"} - ${pkgs.lib.optionalString ( - zwiftActivityDir != "" - ) "export ZWIFT_ACTIVITY_DIR='${zwiftActivityDir}'"} - ${pkgs.lib.optionalString (zwiftLogDir != "") "export ZWIFT_LOG_DIR='${zwiftLogDir}'"} - ${pkgs.lib.optionalString ( - zwiftScreenshotsDir != "" - ) "export ZWIFT_SCREENSHOTS_DIR='${zwiftScreenshotsDir}'"} - ${pkgs.lib.optionalString ( - zwiftOverrideGraphics != "" - ) "export ZWIFT_OVERRIDE_GRAPHICS=${zwiftOverrideGraphics}"} - ${pkgs.lib.optionalString ( - zwiftOverrideResolution != "" - ) "export ZWIFT_OVERRIDE_RESOLUTION='${zwiftOverrideResolution}'"} - ${pkgs.lib.optionalString (zwiftFg != "") "export ZWIFT_FG=${zwiftFg}"} - ${pkgs.lib.optionalString (zwiftNoGameMode != "") "export ZWIFT_NO_GAMEMODE=${zwiftNoGameMode}"} - ${pkgs.lib.optionalString ( - wineExperimentalWayland != "" - ) "export WINE_EXPERIMENTAL_WAYLAND=${wineExperimentalWayland}"} - ${pkgs.lib.optionalString (networking != "") "export NETWORKING='${networking}'"} - ${pkgs.lib.optionalString (zwiftUid != "") "export ZWIFT_UID='${zwiftUid}'"} - ${pkgs.lib.optionalString (zwiftGid != "") "export ZWIFT_GID='${zwiftGid}'"} - ${pkgs.lib.optionalString (debug != "") "export DEBUG=${debug}"} - ${pkgs.lib.optionalString (verbosity != "") "export VERBOSITY='${verbosity}'"} - ${pkgs.lib.optionalString (vgaDeviceFlag != "") "export VGA_DEVICE_FLAG='${vgaDeviceFlag}'"} - ${pkgs.lib.optionalString ( - privilegedContainer != "" - ) "export PRIVILEGED_CONTAINER=${privilegedContainer}"} + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; - ${./src/zwift.sh} - ''; + zwift-fhs = import ./nix/zwift-fhs-package.nix { inherit pkgs; }; - nativeBuildInputs = [ pkgs.copyDesktopItems ]; + zwift-container = import ./nix/zwift-container-package.nix { inherit pkgs; }; - installPhase = '' - runHook preInstall - install -Dm755 ${nixosRun} -T $out/bin/${pname} - install -Dm644 $src/share/icons/hicolor/scalable/apps/zwift.svg \ - -T $out/share/icons/hicolor/scalable/apps/zwift.svg - runHook postInstall - ''; - - desktopItems = [ "share/applications/Zwift.desktop" ]; - }; + nixosModule = import ./nix/module.nix; in { nixosModules = { - zwift = - { config, lib, ... }: - let - inherit (lib.types) str bool enum; - inherit (lib) mkEnableOption mkOption; - in - { - options.programs.zwift = { - enable = mkEnableOption "zwift on linux"; - - image = lib.mkOption { - type = str; - default = ""; - }; - version = mkOption { - type = str; - default = ""; - }; - dontCheck = mkOption { - type = lib.types.bool; - default = false; - }; - dontPull = mkOption { - type = bool; - default = false; - }; - dontClean = mkOption { - type = bool; - default = false; - }; - dryRun = mkOption { - type = bool; - default = false; - }; - interactive = mkOption { - type = bool; - default = false; - }; - containerTool = mkOption { - type = enum [ - "docker" - "podman" - ]; - default = "podman"; - }; - containerExtraArgs = mkOption { - type = str; - default = ""; - }; - zwiftUsername = mkOption { - type = str; - default = ""; - }; - zwiftPassword = mkOption { - type = str; - default = ""; - }; - zwiftWorkoutDir = mkOption { - type = str; - default = ""; - }; - zwiftActivityDir = mkOption { - type = str; - default = ""; - }; - zwiftLogDir = mkOption { - type = str; - default = ""; - }; - zwiftScreenshotsDir = mkOption { - type = str; - default = ""; - }; - zwiftOverrideGraphics = mkOption { - type = bool; - default = false; - }; - zwiftOverrideResolution = mkOption { - type = str; - default = ""; - }; - zwiftFg = mkOption { - type = bool; - default = false; - }; - zwiftNoGameMode = mkOption { - type = bool; - default = false; - }; - wineExperimentalWayland = mkOption { - type = bool; - default = false; - }; - networking = mkOption { - type = str; - default = ""; - }; - zwiftUid = mkOption { - type = str; - default = ""; - }; - zwiftGid = mkOption { - type = str; - default = ""; - }; - vgaDeviceFlag = mkOption { - type = str; - default = ""; - }; - debug = mkOption { - type = bool; - default = false; - }; - verbosity = mkOption { - type = enum [ - "0" - "1" - "2" - "3" - ]; - default = "1"; - }; - privilegedContainer = mkOption { - type = bool; - default = false; - }; - }; - - config = lib.mkIf config.programs.zwift.enable { - virtualisation.podman.enable = lib.mkDefault (config.programs.zwift.containerTool == "podman"); - virtualisation.docker.enable = lib.mkDefault (config.programs.zwift.containerTool == "docker"); - environment = { - systemPackages = with config.programs.zwift; [ - (wrapPackage { - inherit - image - containerTool - containerExtraArgs - zwiftUsername - zwiftPassword - zwiftWorkoutDir - zwiftActivityDir - zwiftLogDir - zwiftScreenshotsDir - zwiftOverrideResolution - networking - zwiftUid - zwiftGid - vgaDeviceFlag - verbosity - ; - tag = version; - dontCheck = if dontCheck then "1" else ""; - dontPull = if dontPull then "1" else ""; - dontClean = if dontClean then "1" else ""; - dryRun = if dryRun then "1" else ""; - interactive = if interactive then "1" else ""; - zwiftOverrideGraphics = if zwiftOverrideGraphics then "1" else ""; - zwiftFg = if zwiftFg then "1" else ""; - zwiftNoGameMode = if zwiftNoGameMode then "1" else ""; - wineExperimentalWayland = if wineExperimentalWayland then "1" else ""; - debug = if debug then "1" else ""; - privilegedContainer = if privilegedContainer then "1" else ""; - }) - ]; - }; - }; - }; - default = self.nixosModules.zwift; + zwift = nixosModule; + default = nixosModule; }; - devShells.x86_64-linux.default = pkgs.mkShell { + devShells.${system}.default = pkgs.mkShell { packages = with pkgs; [ # Bash shellcheck @@ -289,8 +38,8 @@ hadolint # Markdown - nodePackages.markdownlint-cli2 - nodePackages.cspell + markdownlint-cli2 + cspell # Documentation (Jekyll) ruby @@ -312,25 +61,12 @@ ]; }; - packages.x86_64-linux = { - zwift = pkgs.stdenv.mkDerivation rec { - pname = "zwift-unwrapped"; - version = "0-unstable"; - - src = ./.; - - nativeBuildInputs = [ pkgs.copyDesktopItems ]; - - installPhase = '' - runHook preInstall - install -Dm755 $src/src/zwift.sh -T $out/bin/${pname} - install -Dm644 $src/bin/Zwift.svg -T $out/share/icons/hicolor/scalable/apps/zwift.svg - runHook postInstall - ''; - - desktopItems = [ "bin/Zwift.desktop" ]; - }; - default = self.packages.x86_64-linux.zwift; + packages.${system} = { + inherit + zwift-fhs + zwift-container + ; + default = zwift-container; }; }; } diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 00000000..c81d0de6 --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,263 @@ +# NixOS module for Zwift +{ config, + lib, + pkgs, + ... +}: +let + cfg = config.programs.zwift; + inherit (lib) + mkEnableOption + mkOption + types + mkIf + ; + wrapContainerPackage = + args: import ./zwift-container-package.nix ({ inherit pkgs; } // args); + wrapFhsPackage = + args: import ./zwift-fhs-package.nix ({ inherit pkgs; } // args); +in +{ + options.programs.zwift = { + enable = mkEnableOption "Zwift on Linux"; + + containerTool = mkOption { + type = types.enum [ + "podman" + "docker" + "fhs" + ]; + default = "podman"; + description = '' + How to run Zwift: "podman" or "docker" use a container; "fhs" uses native Wine via a FHS environment. + ''; + }; + + # FHS-only options + winePrefix = mkOption { + type = types.str; + default = ""; + description = '' + Custom Wine prefix directory. Defaults to ~/.wine-zwift if not specified. + Only applies when containerTool = "fhs". + ''; + }; + + # Container-only options + image = mkOption { + type = types.str; + default = ""; + description = "Container image to use. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + version = mkOption { + type = types.str; + default = ""; + description = "Container image tag/version. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + dontCheck = mkOption { + type = types.bool; + default = false; + description = "Skip version check. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + dontPull = mkOption { + type = types.bool; + default = false; + description = "Skip pulling the container image. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + dontClean = mkOption { + type = types.bool; + default = false; + description = "Skip cleaning up the container after exit. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + dryRun = mkOption { + type = types.bool; + default = false; + description = "Perform a dry run without actually starting Zwift. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + interactive = mkOption { + type = types.bool; + default = false; + description = "Run the container interactively. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + containerExtraArgs = mkOption { + type = types.str; + default = ""; + description = "Extra arguments passed to the container runtime. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + networking = mkOption { + type = types.str; + default = ""; + description = "Container networking mode. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + zwiftUid = mkOption { + type = types.str; + default = ""; + description = "UID to run Zwift as inside the container. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + zwiftGid = mkOption { + type = types.str; + default = ""; + description = "GID to run Zwift as inside the container. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + vgaDeviceFlag = mkOption { + type = types.str; + default = ""; + description = "VGA device flag for container GPU passthrough. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + privilegedContainer = mkOption { + type = types.bool; + default = false; + description = "Run the container in privileged mode. Only applies when containerTool = \"podman\" or \"docker\"."; + }; + + # Common options + zwiftUsername = mkOption { + type = types.str; + default = ""; + description = "Zwift account email for automatic login."; + }; + + zwiftPassword = mkOption { + type = types.str; + default = ""; + description = '' + Zwift account password for automatic login. + Consider using a secrets management solution instead of storing passwords in your config. + ''; + }; + + zwiftWorkoutDir = mkOption { + type = types.str; + default = ""; + description = "Custom directory for Zwift workouts."; + }; + + zwiftActivityDir = mkOption { + type = types.str; + default = ""; + description = "Custom directory for Zwift activities."; + }; + + zwiftLogDir = mkOption { + type = types.str; + default = ""; + description = "Custom directory for Zwift logs."; + }; + + zwiftScreenshotsDir = mkOption { + type = types.str; + default = ""; + description = "Custom directory for Zwift screenshots."; + }; + + zwiftOverrideGraphics = mkOption { + type = types.bool; + default = false; + description = "Use custom graphics configuration."; + }; + + zwiftOverrideResolution = mkOption { + type = types.str; + default = ""; + example = "1920x1080"; + description = "Override the Zwift display resolution."; + }; + + zwiftFg = mkOption { + type = types.bool; + default = false; + description = "Run Zwift in foreground mode."; + }; + + zwiftNoGameMode = mkOption { + type = types.bool; + default = false; + description = "Disable GameMode integration."; + }; + + wineExperimentalWayland = mkOption { + type = types.bool; + default = false; + description = "Enable experimental Wayland support in Wine."; + }; + + debug = mkOption { + type = types.bool; + default = false; + description = "Enable debug output."; + }; + + verbosity = mkOption { + type = enum [ + "0" + "1" + "2" + "3" + ]; + default = "1"; + }; + }; + + config = mkIf cfg.enable (lib.mkMerge [ + (mkIf (cfg.containerTool == "fhs") { + environment.systemPackages = [ + (wrapFhsPackage { + inherit (cfg) + winePrefix + ; + debug = if cfg.debug then "1" else ""; + }) + ]; + }) + + (mkIf (cfg.containerTool != "fhs") { + virtualisation.podman.enable = lib.mkDefault (cfg.containerTool == "podman"); + virtualisation.docker.enable = lib.mkDefault (cfg.containerTool == "docker"); + + environment.systemPackages = [ + (wrapContainerPackage { + inherit (cfg) + image + containerTool + containerExtraArgs + zwiftUsername + zwiftPassword + zwiftWorkoutDir + zwiftActivityDir + zwiftLogDir + zwiftScreenshotsDir + zwiftOverrideResolution + networking + zwiftUid + zwiftGid + vgaDeviceFlag + ; + tag = cfg.version; + dontCheck = if cfg.dontCheck then "1" else ""; + dontPull = if cfg.dontPull then "1" else ""; + dontClean = if cfg.dontClean then "1" else ""; + dryRun = if cfg.dryRun then "1" else ""; + interactive = if cfg.interactive then "1" else ""; + zwiftOverrideGraphics = if cfg.zwiftOverrideGraphics then "1" else ""; + zwiftFg = if cfg.zwiftFg then "1" else ""; + zwiftNoGameMode = if cfg.zwiftNoGameMode then "1" else ""; + wineExperimentalWayland = if cfg.wineExperimentalWayland then "1" else ""; + debug = if cfg.debug then "1" else ""; + privilegedContainer = if cfg.privilegedContainer then "1" else ""; + }) + ]; + }) + ]); +} diff --git a/nix/zwift-common.nix b/nix/zwift-common.nix new file mode 100644 index 00000000..02278d3c --- /dev/null +++ b/nix/zwift-common.nix @@ -0,0 +1,33 @@ +{ pkgs }: +{ + desktopItem = pkgs.makeDesktopItem { + name = "Zwift"; + desktopName = "Zwift"; + genericName = "Zwift"; + comment = "Zwift Cycling"; + exec = "zwift"; + icon = "zwift"; + terminal = true; + type = "Application"; + startupNotify = true; + categories = [ + "Game" + "Sports" + ]; + keywords = [ + "Fitness" + "Game" + "Cycling" + ]; + startupWMClass = "zwiftapp.exe"; + }; + + makeMeta = + description: with pkgs.lib; { + inherit description; + homepage = "https://github.com/netbrain/zwift"; + license = licenses.mit; + platforms = [ "x86_64-linux" ]; + mainProgram = "zwift"; + }; +} diff --git a/nix/zwift-container-package.nix b/nix/zwift-container-package.nix new file mode 100644 index 00000000..48072e16 --- /dev/null +++ b/nix/zwift-container-package.nix @@ -0,0 +1,97 @@ +{ + pkgs, + image ? "", + tag ? "", + dontCheck ? "", + dontPull ? "", + dontClean ? "", + dryRun ? "", + interactive ? "", + containerTool ? "", + containerExtraArgs ? "", + zwiftUsername ? "", + zwiftPassword ? "", + zwiftWorkoutDir ? "", + zwiftActivityDir ? "", + zwiftLogDir ? "", + zwiftScreenshotsDir ? "", + zwiftOverrideGraphics ? "", + zwiftOverrideResolution ? "", + zwiftFg ? "", + zwiftNoGameMode ? "", + wineExperimentalWayland ? "", + networking ? "", + zwiftUid ? "", + zwiftGid ? "", + vgaDeviceFlag ? "", + debug ? "", + verbosity ? "", + privilegedContainer ? "", +}: +let + common = import ./zwift-common.nix { inherit pkgs; }; + nixosRun = pkgs.writeShellScript "zwift-nixos.sh" '' + ${pkgs.lib.optionalString (image != "") "export IMAGE='${image}'"} + ${pkgs.lib.optionalString (tag != "") "export VERSION='${tag}'"} + ${pkgs.lib.optionalString (dontCheck != "") "export DONT_CHECK=${dontCheck}"} + ${pkgs.lib.optionalString (dontPull != "") "export DONT_PULL=${dontPull}"} + ${pkgs.lib.optionalString (dontClean != "") "export DONT_CLEAN=${dontClean}"} + ${pkgs.lib.optionalString (dryRun != "") "export DRYRUN=${dryRun}"} + ${pkgs.lib.optionalString (interactive != "") "export INTERACTIVE=${interactive}"} + ${pkgs.lib.optionalString (containerTool != "") "export CONTAINER_TOOL='${containerTool}'"} + ${pkgs.lib.optionalString ( + containerExtraArgs != "" + ) "export CONTAINER_EXTRA_ARGS='${containerExtraArgs}'"} + ${pkgs.lib.optionalString (zwiftUsername != "") "export ZWIFT_USERNAME='${zwiftUsername}'"} + ${pkgs.lib.optionalString (zwiftPassword != "") "export ZWIFT_PASSWORD='${zwiftPassword}'"} + ${pkgs.lib.optionalString (zwiftWorkoutDir != "") "export ZWIFT_WORKOUT_DIR='${zwiftWorkoutDir}'"} + ${pkgs.lib.optionalString ( + zwiftActivityDir != "" + ) "export ZWIFT_ACTIVITY_DIR='${zwiftActivityDir}'"} + ${pkgs.lib.optionalString (zwiftLogDir != "") "export ZWIFT_LOG_DIR='${zwiftLogDir}'"} + ${pkgs.lib.optionalString ( + zwiftScreenshotsDir != "" + ) "export ZWIFT_SCREENSHOTS_DIR='${zwiftScreenshotsDir}'"} + ${pkgs.lib.optionalString ( + zwiftOverrideGraphics != "" + ) "export ZWIFT_OVERRIDE_GRAPHICS=${zwiftOverrideGraphics}"} + ${pkgs.lib.optionalString ( + zwiftOverrideResolution != "" + ) "export ZWIFT_OVERRIDE_RESOLUTION='${zwiftOverrideResolution}'"} + ${pkgs.lib.optionalString (zwiftFg != "") "export ZWIFT_FG=${zwiftFg}"} + ${pkgs.lib.optionalString (zwiftNoGameMode != "") "export ZWIFT_NO_GAMEMODE=${zwiftNoGameMode}"} + ${pkgs.lib.optionalString ( + wineExperimentalWayland != "" + ) "export WINE_EXPERIMENTAL_WAYLAND=${wineExperimentalWayland}"} + ${pkgs.lib.optionalString (networking != "") "export NETWORKING='${networking}'"} + ${pkgs.lib.optionalString (zwiftUid != "") "export ZWIFT_UID='${zwiftUid}'"} + ${pkgs.lib.optionalString (zwiftGid != "") "export ZWIFT_GID='${zwiftGid}'"} + ${pkgs.lib.optionalString (debug != "") "export DEBUG=${debug}"} + ${pkgs.lib.optionalString (verbosity != "") "export VERBOSITY='${verbosity}'"} + ${pkgs.lib.optionalString (vgaDeviceFlag != "") "export VGA_DEVICE_FLAG='${vgaDeviceFlag}'"} + ${pkgs.lib.optionalString ( + privilegedContainer != "" + ) "export PRIVILEGED_CONTAINER=${privilegedContainer}"} + + ${../src/zwift.sh} + ''; +in +pkgs.stdenv.mkDerivation { + pname = "zwift"; + version = "0-unstable"; + + dontUnpack = true; + + nativeBuildInputs = [ pkgs.copyDesktopItems ]; + + installPhase = '' + runHook preInstall + install -Dm755 ${nixosRun} -T $out/bin/zwift + install -Dm644 ${../bin/Zwift.svg} -T $out/share/icons/hicolor/scalable/apps/zwift.svg + runHook postInstall + ''; + + desktopItems = [ common.desktopItem ]; + + meta = common.makeMeta "Run Zwift on Linux using Docker/Podman containers"; +} diff --git a/nix/zwift-fhs-package.nix b/nix/zwift-fhs-package.nix new file mode 100644 index 00000000..facb5d14 --- /dev/null +++ b/nix/zwift-fhs-package.nix @@ -0,0 +1,103 @@ +{ + pkgs, + winePrefix ? "", + debug ? "", +}: +let + common = import ./zwift-common.nix { inherit pkgs; }; + + zwift-nix-fhs = pkgs.stdenv.mkDerivation { + pname = "zwift-nix-fhs"; + version = "0-unstable"; + + dontUnpack = true; + + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + install -Dm755 ${../src/zwift-nix-fhs.sh} $out/bin/zwift-nix-fhs + + runHook postInstall + ''; + }; + + zwift-fhs = pkgs.buildFHSEnv { + name = "zwift-fhs"; + + targetPkgs = + pkgs: with pkgs; [ + # Wine + wineWow64Packages.stagingFull + winetricks + + # Networking (wget used by install script; cacert referenced in profile) + wget + cacert + + # Tools required by winetricks + cabextract + p7zip + unzip + + # Game mode + gamemode + + # Our wrapper script + zwift-nix-fhs + ]; + + + profile = '' + ${if winePrefix != "" then '' + export WINEPREFIX="${winePrefix}" + '' else '' + export WINEPREFIX="''${WINEPREFIX:-$HOME/.wine-zwift}" + ''} + export WINEDEBUG="''${WINEDEBUG:--all}" + export WINEARCH=win64 + + # Ensure SSL certificates are available + export SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + export NIX_SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + export CURL_CA_BUNDLE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + + # Ensure HOME is set (required by Wine) + export HOME="''${HOME:-/tmp}" + + # Set WINE and WINE64 explicitly for winetricks + # Modern Wine uses a unified binary, but winetricks expects wine64 + export WINE="''${WINE:-/usr/bin/wine}" + export WINE64="''${WINE64:-/usr/bin/wine}" + export WINESERVER="''${WINESERVER:-/usr/bin/wineserver}" + ''; + + runScript = "bash"; + }; + nixosRun = pkgs.writeShellScript "zwift-nixos.sh" '' + ${pkgs.lib.optionalString (winePrefix != "") "export WINE_PREFIX=${winePrefix}"} + ${pkgs.lib.optionalString (debug != "") "export DEBUG=${debug}"} + + exec ${zwift-fhs}/bin/zwift-fhs -c "zwift-nix-fhs" + ''; + +in +pkgs.stdenv.mkDerivation { + pname = "zwift"; + version = "0-unstable"; + + dontUnpack = true; + + nativeBuildInputs = [ pkgs.copyDesktopItems ]; + + installPhase = '' + runHook preInstall + install -Dm755 ${nixosRun} -T $out/bin/zwift + install -Dm644 ${../bin/Zwift.svg} -T $out/share/icons/hicolor/scalable/apps/zwift.svg + runHook postInstall + ''; + + desktopItems = [ common.desktopItem ]; + + meta = common.makeMeta "Run Zwift on Linux natively using Wine"; +} diff --git a/src/zwift-nix-fhs.sh b/src/zwift-nix-fhs.sh new file mode 100644 index 00000000..4a809aa5 --- /dev/null +++ b/src/zwift-nix-fhs.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -uo pipefail + +readonly DEBUG="${DEBUG:-0}" +if [[ ${DEBUG} -eq 1 ]]; then set -x; fi + +readonly COLORED_OUTPUT="${COLORED_OUTPUT:-0}" +if [[ -t 1 ]] || [[ ${COLORED_OUTPUT} -eq 1 ]]; then + readonly COLOR_WHITE="\033[0;37m" + readonly COLOR_RED="\033[0;31m" + readonly COLOR_GREEN="\033[0;32m" + readonly COLOR_BLUE="\033[0;34m" + readonly COLOR_YELLOW="\033[0;33m" + readonly RESET_STYLE="\033[0m" +else + readonly COLOR_WHITE="" + readonly COLOR_RED="" + readonly COLOR_GREEN="" + readonly COLOR_BLUE="" + readonly COLOR_YELLOW="" + readonly RESET_STYLE="" +fi + +readonly VERBOSITY="${VERBOSITY:-1}" +readonly CONTAINER_TOOL="nix-fhs" +readonly ZWIFT_USERNAME="${ZWIFT_USERNAME:-}" +readonly ZWIFT_PASSWORD="${ZWIFT_PASSWORD:-}" +readonly ZWIFT_OVERRIDE_RESOLUTION="${ZWIFT_OVERRIDE_RESOLUTION:-}" +readonly ZWIFT_NO_GAMEMODE="${ZWIFT_NO_GAMEMODE:-0}" +readonly WINEPREFIX="${WINEPREFIX:-${HOME}/.wine-zwift}" +readonly ZWIFT_HOME="${WINEPREFIX}/drive_c/Program Files (x86)/Zwift" + +msgbox() { + local type="${1:?}" # Type: info, ok, warning, error, debug + local msg="${2:?}" # Message: the message to display + + local timestamp="" + [[ ${VERBOSITY} -ge 2 ]] && printf -v timestamp '%(%T)T|' -1 + + case ${type} in + info) [[ ${VERBOSITY} -ge 1 ]] && echo -e "${COLOR_BLUE}[${CONTAINER_TOOL}|${timestamp}*] ${msg}${RESET_STYLE}" ;; + ok) echo -e "${COLOR_GREEN}[${CONTAINER_TOOL}|${timestamp}✓] ${msg}${RESET_STYLE}" ;; + warning) echo -e "${COLOR_YELLOW}[${CONTAINER_TOOL}|${timestamp}!] ${msg}${RESET_STYLE}" ;; + error) echo -e "${COLOR_RED}[${CONTAINER_TOOL}|${timestamp}✗] ${msg}${RESET_STYLE}" >&2 ;; + debug) [[ ${VERBOSITY} -ge 3 ]] && echo -e "${COLOR_WHITE}[${CONTAINER_TOOL}|${timestamp}◉] ${msg}${RESET_STYLE}" ;; + *) echo "msgbox - unknown type ${type}" >&2 && exit 1 ;; + esac +} + +if ! TMPDIR=$(mktemp -d); then + msgbox error "Could not create temp dir" + exit 1 +fi +readonly TMPDIR + +wine_task_info() { + local task_name="${1:?}" + wine tasklist /fo list /fi "IMAGENAME eq ${task_name}" +} + +is_wine_task_running() { + local task_name="${1:?}" + [[ -n $(wine_task_info "${task_name}" || true) ]] +} + +wait_until() { + local condition="${1:?}" + local timeout="${2:-20}" + local delay="${3:-0.1}" + local counter=1 + + while ! eval "${condition}" && [[ ${counter} -le ${timeout} ]]; do + msgbox debug "Waiting... (${counter}/${timeout})" + sleep "${delay}" + ((counter++)) + done + + eval "${condition}" +} + +wait_until_wine_task_started() { + local task_name="${1:?}" + msgbox info "Waiting for ${task_name} to start..." + wait_until "is_wine_task_running ${task_name}" +} + +install_zwift() { + # prevent wine from installing mono and gecko + msgbox info "Initializing wine" + WINEDLLOVERRIDES="mscoree,mshtml=" wineboot -u || return 1 + + msgbox info "Installing prerequisites using winetricks" + winetricks -q corefonts dotnet48 d3dcompiler_47 || return 1 + + msgbox info "Downloading and installing webview2" + wget -O "${TMPDIR}/webview2-setup.exe" https://go.microsoft.com/fwlink/p/?LinkId=2124703 || return 1 + wine "${TMPDIR}/webview2-setup.exe" /silent /install || return 1 + + msgbox info "Enabling Wayland support" + wine reg.exe add HKCU\\Software\\Wine\\Drivers /v Graphics /d x11,wayland || return 1 + + msgbox info "Downloading and installing Zwift" + wget -O "${TMPDIR}/ZwiftSetup.exe" https://cdn.zwift.com/app/ZwiftSetup.exe || return 1 + wine "${TMPDIR}/ZwiftSetup.exe" /SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /NOCANCEL || return 1 +} + +########################################## +##### Automatically stop wine server ##### + +cleanup() { + msgbox info "Stopping wine server" + wineserver -k || true + + msgbox info "Removing temp dir" + rm -rf "${TMPDIR}" || true +} + +trap cleanup EXIT + +if [[ ! -d ${ZWIFT_HOME} ]]; then + msgbox info "Zwift is not installed. Running installation first..." + if ! install_zwift; then + msgbox error "Failed to install Zwift!" + exit 1 + fi +fi + +if ! cd "${ZWIFT_HOME}"; then + msgbox error "Could not go into ${ZWIFT_HOME}. Zwift installation seems to have failed." + exit 1 +fi + +msgbox info "Starting Zwift launcher using wine" + +if ! wine start ZwiftLauncher.exe || ! wait_until_wine_task_started ZwiftLauncher.exe; then + msgbox error "Failed to start Zwift launcher using wine!" + exit 1 +fi + +msgbox ok "Zwift launcher started using wine" + +counter=1 +while is_wine_task_running ZwiftLauncher.exe; do + msgbox debug "Waiting for Zwift Launcher to exit... ($((counter++)))" + sleep 5 +done diff --git a/tests/nix-flake/configuration.nix b/tests/nix-flake/configuration.nix deleted file mode 100644 index ed248026..00000000 --- a/tests/nix-flake/configuration.nix +++ /dev/null @@ -1,6 +0,0 @@ -{ - boot.isContainer = true; - programs.zwift = { - enable = true; - }; -} diff --git a/tests/nix-flake/flake.nix b/tests/nix-flake/flake.nix index cd258d1c..4361edc4 100644 --- a/tests/nix-flake/flake.nix +++ b/tests/nix-flake/flake.nix @@ -14,15 +14,28 @@ let system = "x86_64-linux"; - nixosSystem = nixpkgs.lib.nixosSystem { - inherit system; - modules = [ - zwift.nixosModules.zwift - ./configuration.nix - ]; - }; + makeSystem = + containerTool: + nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + zwift.nixosModules.zwift + { + boot.isContainer = true; + programs.zwift = { + enable = true; + inherit containerTool; + }; + } + ]; + }; in { - packages.${system}.default = nixosSystem.config.system.build.toplevel; + packages.${system} = { + podman = (makeSystem "podman").config.system.build.toplevel; + docker = (makeSystem "docker").config.system.build.toplevel; + fhs = (makeSystem "fhs").config.system.build.toplevel; + default = (makeSystem "podman").config.system.build.toplevel; + }; }; }