From 4e30e32a9c81b67d09fb128de78a10faa4e518cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Wed, 20 May 2026 21:07:41 +0200 Subject: [PATCH] temp --- flake.lock | 158 ++++++++++++++- flake.nix | 331 ++++++-------------------------- nix/module-container.nix | 152 +++++++++++++++ nix/module-fhs.nix | 69 +++++++ nix/module-options-common.nix | 81 ++++++++ nix/zwift-common.nix | 33 ++++ nix/zwift-container-package.nix | 95 +++++++++ nix/zwift-fhs-package.nix | 243 +++++++++++++++++++++++ nix/zwift-scripts.nix | 21 ++ src/run_zwift.sh | 10 +- src/update_zwift.sh | 32 +-- src/zwift-nix-fhs.sh | 104 ++++++++++ 12 files changed, 1045 insertions(+), 284 deletions(-) create mode 100644 nix/module-container.nix create mode 100644 nix/module-fhs.nix create mode 100644 nix/module-options-common.nix create mode 100644 nix/zwift-common.nix create mode 100644 nix/zwift-container-package.nix create mode 100644 nix/zwift-fhs-package.nix create mode 100644 nix/zwift-scripts.nix create mode 100644 src/zwift-nix-fhs.sh diff --git a/flake.lock b/flake.lock index 9d9152b2..43802237 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,60 @@ { "nodes": { + "fenix": { + "inputs": { + "nixpkgs": "nixpkgs_2", + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1696746049, + "narHash": "sha256-y68CsoHDfR2P9PyT63Pk60HEd0tU+/5LFYAT1Fjubbo=", + "owner": "nix-community", + "repo": "fenix", + "rev": "96867a9600eb4756164f1bfbe0882c11e10640d9", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "naersk": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1694081375, + "narHash": "sha256-vzJXOUnmkMCm3xw8yfPP5m8kypQ3BhAIRe4RRCWpzy8=", + "owner": "nix-community", + "repo": "naersk", + "rev": "3f976d822b7b37fc6fb8e6f157c2dd05e7e94e89", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1770141374, @@ -16,9 +71,110 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1696604326, + "narHash": "sha256-YXUNI0kLEcI5g8lqGMb0nh67fY9f2YoJsILafh6zlMo=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "87828a0e03d1418e848d3dd3f3014a632e4a4f64", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1696693680, + "narHash": "sha256-PH0HQTkqyj7DmdPKPwrrXwVURLBqzZs4nqnDw9q8mhg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "945559664c1dc5836173ee12896ba421d9b37181", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1696693680, + "narHash": "sha256-PH0HQTkqyj7DmdPKPwrrXwVURLBqzZs4nqnDw9q8mhg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "945559664c1dc5836173ee12896ba421d9b37181", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "runfromprocess-rs": "runfromprocess-rs" + } + }, + "runfromprocess-rs": { + "inputs": { + "fenix": "fenix", + "flake-utils": "flake-utils", + "naersk": "naersk", + "nixpkgs": "nixpkgs_4" + }, + "locked": { + "lastModified": 1696783670, + "narHash": "sha256-tEmg1sFIJmWZfLLLcOmcLk0SSxgfnPBUtvkhnddfx98=", + "owner": "quietvoid", + "repo": "runfromprocess-rs", + "rev": "a3d003c07d1bd11ff93c4cac96d2c3aa5deb8471", + "type": "github" + }, + "original": { + "owner": "quietvoid", + "repo": "runfromprocess-rs", + "rev": "a3d003c07d1bd11ff93c4cac96d2c3aa5deb8471", + "type": "github" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1696592032, + "narHash": "sha256-kY1temv39OjasB81KHdoMqpxh21F3kStwxE51eqViJ0=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "b1f89a84ab350091e6c20cfe30c2fab8d76b80e4", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 5cec3394..0498d407 100644 --- a/flake.nix +++ b/flake.nix @@ -1,281 +1,54 @@ { description = "Easily zwift on linux"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + runfromprocess-rs.url = "github:quietvoid/runfromprocess-rs?rev=a3d003c07d1bd11ff93c4cac96d2c3aa5deb8471"; + }; outputs = - { nixpkgs, self }: + { + nixpkgs, + runfromprocess-rs, + 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}"} - - ${./src/zwift.sh} - ''; - - nativeBuildInputs = [ pkgs.copyDesktopItems ]; + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + + zwift = import ./nix/zwift-fhs-package.nix { + inherit + pkgs + system + runfromprocess-rs + ; + zwift-icon = ./bin/Zwift.svg; + }; - 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 - ''; + # Container-based package with all defaults (for nix run .#zwift-container) + zwift-container = import ./nix/zwift-container-package.nix { + inherit pkgs; + zwift-sh = ./src/zwift.sh; + zwift-icon = ./bin/Zwift.svg; + }; - desktopItems = [ "share/applications/Zwift.desktop" ]; - }; + # NixOS modules + nixosModuleFhs = import ./nix/module-fhs.nix { zwift-package = zwift; }; + nixosModuleContainer = import ./nix/module-container.nix { + zwift-sh = ./src/zwift.sh; + zwift-icon = ./bin/Zwift.svg; + }; 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-container = nixosModuleContainer; + zwift-fhs = nixosModuleFhs; + default = nixosModuleContainer; }; - devShells.x86_64-linux.default = pkgs.mkShell { + # Development shell + devShells.${system}.default = pkgs.mkShell { packages = with pkgs; [ # Bash shellcheck @@ -312,8 +85,16 @@ ]; }; - packages.x86_64-linux = { - zwift = pkgs.stdenv.mkDerivation rec { + # Packages + packages.${system} = { + inherit + zwift + zwift-container + ; + default = zwift; + + # Legacy package for compatibility + zwift-unwrapped = pkgs.stdenv.mkDerivation { pname = "zwift-unwrapped"; version = "0-unstable"; @@ -323,14 +104,26 @@ installPhase = '' runHook preInstall - install -Dm755 $src/src/zwift.sh -T $out/bin/${pname} + install -Dm755 $src/src/zwift.sh -T $out/bin/zwift-unwrapped 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; + }; + + # Apps + apps.${system} = { + zwift = { + type = "app"; + program = "${zwift}/bin/zwift"; + }; + zwift-container = { + type = "app"; + program = "${zwift-container}/bin/zwift"; + }; + default = self.apps.${system}.zwift; }; }; } diff --git a/nix/module-container.nix b/nix/module-container.nix new file mode 100644 index 00000000..c8650680 --- /dev/null +++ b/nix/module-container.nix @@ -0,0 +1,152 @@ +# NixOS module for container-based Zwift (Docker/Podman) +{ zwift-sh, zwift-icon }: +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.programs.zwift-container; + inherit (lib) + mkEnableOption + mkOption + types + mkIf + ; + commonOptions = import ./module-options-common.nix { inherit lib; }; + + wrapPackage = + args: import ./zwift-container-package.nix ({ inherit pkgs zwift-sh zwift-icon; } // args); +in +{ + options.programs.zwift-container = { + enable = mkEnableOption "Zwift on Linux (container)"; + + image = mkOption { + type = types.str; + default = ""; + description = "Container image to use."; + }; + + version = mkOption { + type = types.str; + default = ""; + description = "Container image tag/version."; + }; + + dontCheck = mkOption { + type = types.bool; + default = false; + description = "Skip version check."; + }; + + dontPull = mkOption { + type = types.bool; + default = false; + description = "Skip pulling the container image."; + }; + + dontClean = mkOption { + type = types.bool; + default = false; + description = "Skip cleaning up the container after exit."; + }; + + dryRun = mkOption { + type = types.bool; + default = false; + description = "Perform a dry run without actually starting Zwift."; + }; + + interactive = mkOption { + type = types.bool; + default = false; + description = "Run the container interactively."; + }; + + containerTool = mkOption { + type = types.enum [ + "docker" + "podman" + ]; + default = "podman"; + description = "Container runtime to use (docker or podman)."; + }; + + containerExtraArgs = mkOption { + type = types.str; + default = ""; + description = "Extra arguments passed to the container runtime."; + }; + + networking = mkOption { + type = types.str; + default = ""; + description = "Container networking mode."; + }; + + zwiftUid = mkOption { + type = types.str; + default = ""; + description = "UID to run Zwift as inside the container."; + }; + + zwiftGid = mkOption { + type = types.str; + default = ""; + description = "GID to run Zwift as inside the container."; + }; + + vgaDeviceFlag = mkOption { + type = types.str; + default = ""; + description = "VGA device flag for container GPU passthrough."; + }; + + privilegedContainer = mkOption { + type = types.bool; + default = false; + description = "Run the container in privileged mode."; + }; + } + // commonOptions; + + config = mkIf cfg.enable { + virtualisation.podman.enable = lib.mkDefault (cfg.containerTool == "podman"); + virtualisation.docker.enable = lib.mkDefault (cfg.containerTool == "docker"); + + environment.systemPackages = [ + (wrapPackage { + 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/module-fhs.nix b/nix/module-fhs.nix new file mode 100644 index 00000000..3ce75209 --- /dev/null +++ b/nix/module-fhs.nix @@ -0,0 +1,69 @@ +# NixOS module for FHS based Zwift +{ zwift-package }: +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.programs.zwift; + inherit (lib) + mkEnableOption + mkOption + types + mkIf + ; + commonOptions = import ./module-options-common.nix { inherit lib; }; +in +{ + options.programs.zwift = { + enable = mkEnableOption "Zwift on Linux (FHS)"; + + package = mkOption { + type = types.package; + default = zwift-package; + description = "The Zwift package to use."; + }; + + winePrefix = mkOption { + type = types.str; + default = ""; + description = '' + Custom Wine prefix directory. + Defaults to ~/.wine-zwift if not specified. + ''; + }; + } + // commonOptions; + + config = mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + + # Create a wrapper script with configured environment variables + environment.etc."zwift/config".text = '' + # Zwift configuration (auto-generated by NixOS module) + ${lib.optionalString (cfg.zwiftUsername != "") "export ZWIFT_USERNAME=\"${cfg.zwiftUsername}\""} + ${lib.optionalString (cfg.zwiftPassword != "") "export ZWIFT_PASSWORD=\"${cfg.zwiftPassword}\""} + ${lib.optionalString (cfg.winePrefix != "") "export WINEPREFIX=\"${cfg.winePrefix}\""} + ${lib.optionalString ( + cfg.zwiftWorkoutDir != "" + ) "export ZWIFT_WORKOUT_DIR=\"${cfg.zwiftWorkoutDir}\""} + ${lib.optionalString ( + cfg.zwiftActivityDir != "" + ) "export ZWIFT_ACTIVITY_DIR=\"${cfg.zwiftActivityDir}\""} + ${lib.optionalString (cfg.zwiftLogDir != "") "export ZWIFT_LOG_DIR=\"${cfg.zwiftLogDir}\""} + ${lib.optionalString ( + cfg.zwiftScreenshotsDir != "" + ) "export ZWIFT_SCREENSHOTS_DIR=\"${cfg.zwiftScreenshotsDir}\""} + ${lib.optionalString cfg.zwiftOverrideGraphics "export ZWIFT_OVERRIDE_GRAPHICS=1"} + ${lib.optionalString ( + cfg.zwiftOverrideResolution != "" + ) "export ZWIFT_OVERRIDE_RESOLUTION=\"${cfg.zwiftOverrideResolution}\""} + ${lib.optionalString cfg.zwiftFg "export ZWIFT_FG=1"} + ${lib.optionalString cfg.zwiftNoGameMode "export ZWIFT_NO_GAMEMODE=1"} + ${lib.optionalString cfg.wineExperimentalWayland "export WINE_EXPERIMENTAL_WAYLAND=1"} + ${lib.optionalString cfg.debug "export DEBUG=1"} + ''; + }; +} diff --git a/nix/module-options-common.nix b/nix/module-options-common.nix new file mode 100644 index 00000000..6dfbd156 --- /dev/null +++ b/nix/module-options-common.nix @@ -0,0 +1,81 @@ +{ lib }: +let + inherit (lib) mkOption types; +in +{ + 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."; + }; +} 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..2b4773a6 --- /dev/null +++ b/nix/zwift-container-package.nix @@ -0,0 +1,95 @@ +{ + pkgs, + zwift-sh, + zwift-icon, + 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 ? "", + 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 (vgaDeviceFlag != "") "export VGA_DEVICE_FLAG=${vgaDeviceFlag}"} + ${pkgs.lib.optionalString ( + privilegedContainer != "" + ) "export PRIVILEGED_CONTAINER=${privilegedContainer}"} + + ${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 ${zwift-icon} -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..73bf01fa --- /dev/null +++ b/nix/zwift-fhs-package.nix @@ -0,0 +1,243 @@ +{ + pkgs, + system, + runfromprocess-rs, + zwift-icon, +}: +let + common = import ./zwift-common.nix { inherit pkgs; }; + + runfromprocess = runfromprocess-rs.packages.${system}.x86_64-pc-windows-gnu; + + zwift-scripts = import ./zwift-scripts.nix { + inherit pkgs; + }; + + zwift-fhs = pkgs.buildFHSEnv { + name = "zwift-fhs"; + + # Enable 32-bit support for Wine + multiArch = true; + + targetPkgs = + pkgs: with pkgs; [ + # Wine (staging has better compatibility than devel/unstable) + wineWowPackages.stagingFull + winetricks + + # Graphics - Vulkan + vulkan-loader + vulkan-tools + + # Graphics - OpenGL/Mesa + mesa + libGL + libGLU + libglvnd + + # X11 libraries + xorg.libX11 + xorg.libXext + xorg.libXrandr + xorg.libXrender + xorg.libXcursor + xorg.libXfixes + xorg.libXi + xorg.libXcomposite + xorg.libXdamage + xorg.libXtst + xorg.libXScrnSaver + xorg.libxcb + xorg.libICE + xorg.libSM + xorg.xrandr + + # Wayland + wayland + libxkbcommon + wayland-protocols + + # Audio + pulseaudio + pipewire + alsa-lib + alsa-plugins + libpulseaudio + + # System libraries + glib + glibc + dbus + systemd + fontconfig + freetype + + # Networking + curl + wget + cacert + + # Process utilities + procps + coreutils + gnugrep + gawk + gnused + findutils + file + which + bash + + # Tools required by winetricks + cabextract + p7zip + unzip + zenity + + # Game mode + gamemode + + # Additional libraries often needed by Wine + openssl + gnutls + libgpg-error + sqlite + libxml2 + ncurses + zlib + libpng + libjpeg + SDL2 + openal + + # Desktop integration + gsettings-desktop-schemas + gtk3 + gdk-pixbuf + + # The runfromprocess Windows binary + runfromprocess + + # Our wrapper scripts + zwift-scripts + ]; + + # 32-bit packages for Wine compatibility + multiPkgs = + pkgs: with pkgs; [ + # Graphics + vulkan-loader + mesa + libGL + libGLU + libglvnd + + # X11 + xorg.libX11 + xorg.libXext + xorg.libXrandr + xorg.libXrender + xorg.libXcursor + xorg.libXfixes + xorg.libXi + xorg.libXcomposite + xorg.libXdamage + xorg.libXtst + xorg.libxcb + + # Audio + pulseaudio + alsa-lib + alsa-plugins + libpulseaudio + + # System + glib + glibc + dbus + fontconfig + freetype + + # Wine dependencies + gnutls + openssl + ncurses + zlib + libpng + libjpeg + SDL2 + openal + + # GTK + gtk3 + gdk-pixbuf + ]; + + profile = '' + 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" + + # Set up Vulkan ICD path + export VK_ICD_FILENAMES="''${VK_ICD_FILENAMES:-}" + + # Ensure HOME is set (required by Wine) + export HOME="''${HOME:-/tmp}" + + # Set up XDG directories + export XDG_DATA_HOME="''${XDG_DATA_HOME:-$HOME/.local/share}" + export XDG_CONFIG_HOME="''${XDG_CONFIG_HOME:-$HOME/.config}" + export XDG_CACHE_HOME="''${XDG_CACHE_HOME:-$HOME/.cache}" + + # Create Wine directories if needed + mkdir -p "''${WINEPREFIX}" 2>/dev/null || true + + # 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"; + }; + +in +pkgs.stdenv.mkDerivation { + pname = "zwift"; + version = "0-unstable"; + + dontUnpack = true; + + nativeBuildInputs = [ pkgs.copyDesktopItems ]; + + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + cat > $out/bin/zwift << 'EOF' + #!/usr/bin/env bash + exec ${zwift-fhs}/bin/zwift-fhs -c "zwift-nix-fhs $*" + EOF + chmod +x $out/bin/zwift + + cat > $out/bin/zwift-auth << 'EOF' + #!/usr/bin/env bash + exec ${zwift-fhs}/bin/zwift-fhs -c "zwift-auth" + EOF + chmod +x $out/bin/zwift-auth + + install -Dm644 ${zwift-icon} -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/nix/zwift-scripts.nix b/nix/zwift-scripts.nix new file mode 100644 index 00000000..a44029c5 --- /dev/null +++ b/nix/zwift-scripts.nix @@ -0,0 +1,21 @@ +{ + pkgs, +}: +pkgs.stdenv.mkDerivation { + pname = "zwift-scripts"; + version = "0-unstable"; + + dontUnpack = true; + + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + install -Dm755 ${../src/zwift-auth.sh} $out/bin/zwift-auth + install -Dm755 ${../src/run_zwift.sh} $out/bin/zwift-run + install -Dm755 ${../src/zwift-nix-fhs.sh} $out/bin/zwift-nix-fhs + install -Dm775 ${../src/update_zwift.sh} $out/bin/zwift-update + + runHook postInstall + ''; +} diff --git a/src/run_zwift.sh b/src/run_zwift.sh index bf0b794f..4338f058 100755 --- a/src/run_zwift.sh +++ b/src/run_zwift.sh @@ -28,8 +28,14 @@ readonly ZWIFT_PASSWORD="${ZWIFT_PASSWORD:-}" readonly ZWIFT_OVERRIDE_RESOLUTION="${ZWIFT_OVERRIDE_RESOLUTION:-}" readonly ZWIFT_NO_GAMEMODE="${ZWIFT_NO_GAMEMODE:-0}" -readonly WINE_USER_HOME="/home/user/.wine/drive_c/users/user" -readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" +if [[ ${CONTAINER_TOOL} == "nix-fhs" ]]; then + readonly WINEPREFIX="${WINEPREFIX:-${HOME}/.wine-zwift}" + readonly WINE_USER_HOME="${WINEPREFIX}/drive_c/users/${USER}" +else + readonly WINEPREFIX="/home/user/.wine" + readonly WINE_USER_HOME="${WINEPREFIX}/drive_c/users/user" +fi +readonly ZWIFT_HOME="${WINEPREFIX}/drive_c/Program Files (x86)/Zwift" readonly ZWIFT_DOCS="${WINE_USER_HOME}/AppData/Local/Zwift" readonly ZWIFT_PREFS="${ZWIFT_DOCS}/prefs.xml" diff --git a/src/update_zwift.sh b/src/update_zwift.sh index df975b25..0626adb9 100755 --- a/src/update_zwift.sh +++ b/src/update_zwift.sh @@ -22,10 +22,16 @@ else fi readonly VERBOSITY="${VERBOSITY:-1}" -readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" +readonly CONTAINER_TOOL="${CONTAINER_TOOL:-}" -readonly WINE_USER_HOME="/home/user/.wine/drive_c/users/user" -readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" +if [[ ${CONTAINER_TOOL} == "nix-fhs" ]]; then + readonly WINEPREFIX="${WINEPREFIX:-${HOME}/.wine-zwift}" + readonly WINE_USER_HOME="${WINEPREFIX}/drive_c/users/${USER}" +else + readonly WINEPREFIX="/home/user/.wine" + readonly WINE_USER_HOME="${WINEPREFIX}/drive_c/users/user" +fi +readonly ZWIFT_HOME="${WINEPREFIX}/drive_c/Program Files (x86)/Zwift" readonly ZWIFT_DOCS="${WINE_USER_HOME}/AppData/Local/Zwift" msgbox() { @@ -98,7 +104,7 @@ update_zwift_using_launcher() { msgbox ok "Zwift launcher started using wine" local counter=1 - local max_iterations=60 # 60 * 5s = 5 minutes max + local max_iterations=120 # 120 * 5s = 10 minutes max # also stop if launcher exits before update finishes, so we don't hang forever while [[ ${zwift_current_version} != "${zwift_latest_version}" ]] && [[ ${counter} -le ${max_iterations} ]] && is_wine_task_running ZwiftLauncher.exe; do @@ -157,14 +163,16 @@ cleanup() { msgbox info "Stopping wine server" wineserver -k || true # important, Zwift launcher won't stop until wine server is killed - msgbox info "Removing installation artifacts" - # remove downloads and cache - rm -- "${ZWIFT_HOME}/ZwiftSetup.exe" || true - rm -- "${ZWIFT_HOME}/webview2-setup.exe" || true - rm -rf -- "${WINE_USER_HOME}/Downloads/Zwift" || true - rm -rf -- "/home/user/.cache/wine*" || true - # remove Zwift documents because it causes permission errors with podman - rm -rf -- "${ZWIFT_DOCS}" || true + if [[ ${CONTAINER_TOOL} != "nix-fhs" ]]; then + msgbox info "Removing installation artifacts" + # remove downloads and cache + rm -- "${ZWIFT_HOME}/ZwiftSetup.exe" || true + rm -- "${ZWIFT_HOME}/webview2-setup.exe" || true + rm -rf -- "${WINE_USER_HOME}/Downloads/Zwift" || true + rm -rf -- "/home/user/.cache/wine*" || true + # remove Zwift documents because it causes permission errors with podman + rm -rf -- "${ZWIFT_DOCS}" || true + fi } trap cleanup EXIT diff --git a/src/zwift-nix-fhs.sh b/src/zwift-nix-fhs.sh new file mode 100644 index 00000000..634f6326 --- /dev/null +++ b/src/zwift-nix-fhs.sh @@ -0,0 +1,104 @@ +#!/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}" + +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}[${timestamp}*] ${msg}${RESET_STYLE}" ;; + ok) echo -e "${COLOR_GREEN}[${timestamp}✓] ${msg}${RESET_STYLE}" ;; + warning) echo -e "${COLOR_YELLOW}[${timestamp}!] ${msg}${RESET_STYLE}" ;; + error) echo -e "${COLOR_RED}[${timestamp}✗] ${msg}${RESET_STYLE}" >&2 ;; + debug) [[ ${VERBOSITY} -ge 3 ]] && echo -e "${COLOR_WHITE}[${timestamp}◉] ${msg}${RESET_STYLE}" ;; + *) echo "msgbox - unknown type ${type}" >&2 && exit 1 ;; + esac +} + +readonly USER_CONFIG_DIR="${HOME}/.config/zwift" + +load_config_file() { + local config_file="${1:?}" + if [[ -f ${config_file} ]]; then + set -a + # shellcheck source=/dev/null + source "${config_file}" + set +a + fi +} +mkdir -p "${USER_CONFIG_DIR}" +load_config_file "${USER_CONFIG_DIR}/config" +load_config_file "${USER_CONFIG_DIR}/${USER}-config" + +show_help() { + echo "Zwift for Linux (Native)" + echo "" + echo "Usage: zwift [OPTIONS]" + echo "" + echo "Options:" + echo " --install Install Zwift (first-time setup)" + echo " --help Show this help message" + echo "" + echo "Environment Variables:" + echo " WINEPREFIX Wine prefix directory (default: ~/.wine-zwift)" + echo " ZWIFT_USERNAME Zwift account email" + echo " ZWIFT_PASSWORD Zwift account password" + echo " ZWIFT_OVERRIDE_RESOLUTION Override resolution (e.g., 1920x1080)" + echo " ZWIFT_NO_GAMEMODE Set to 1 to disable GameMode" + echo " WINE_EXPERIMENTAL_WAYLAND Set to 1 for Wayland support" + echo " DEBUG Set to 1 for debug output" +} + +case "${1:-}" in + --install) + export CONTAINER_TOOL="nix-fhs" + exec zwift-update --install + ;; + --help | -h) + show_help + exit 0 + ;; + "") + _wineprefix="${WINEPREFIX:-${HOME}/.wine-zwift}" + _zwift_home="${_wineprefix}/drive_c/Program Files (x86)/Zwift" + export CONTAINER_TOOL="nix-fhs" + export ZWIFT_NO_GAMEMODE=1 + if [[ ! -d ${_zwift_home} ]]; then + msgbox info "Zwift is not installed. Running installation first..." + zwift-update --install || exit 1 + else + zwift-update || exit 1 + fi + exec zwift-run + ;; + *) + echo "Unknown option: ${1}" + show_help + exit 1 + ;; +esac