From a69188738f532727517c558c30ddd98a22b7d250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Tue, 14 Jul 2026 12:10:07 +0200 Subject: [PATCH 1/6] refactor: extract Nix package and module definitions from flake.nix Move the container package wrapper, common derivation helpers, and NixOS module out of flake.nix into separate files under nix/. flake.nix becomes a thin orchestrator. No behavioral change. --- flake.nix | 278 ++------------------------------ nix/module.nix | 233 ++++++++++++++++++++++++++ nix/zwift-common.nix | 33 ++++ nix/zwift-container-package.nix | 97 +++++++++++ 4 files changed, 375 insertions(+), 266 deletions(-) create mode 100644 nix/module.nix create mode 100644 nix/zwift-common.nix create mode 100644 nix/zwift-container-package.nix diff --git a/flake.nix b/flake.nix index 5cec3394..10ea98f5 100644 --- a/flake.nix +++ b/flake.nix @@ -6,276 +6,20 @@ outputs = { 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}"} - - ${./src/zwift.sh} - ''; + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; - 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 @@ -312,8 +56,11 @@ ]; }; - packages.x86_64-linux = { - zwift = pkgs.stdenv.mkDerivation rec { + packages.${system} = { + inherit zwift-container; + default = zwift-container; + + zwift-unwrapped = pkgs.stdenv.mkDerivation rec { pname = "zwift-unwrapped"; version = "0-unstable"; @@ -330,7 +77,6 @@ desktopItems = [ "bin/Zwift.desktop" ]; }; - default = self.packages.x86_64-linux.zwift; }; }; } diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 00000000..2ff28ccd --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,233 @@ +# 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); +in +{ + options.programs.zwift = { + enable = mkEnableOption "Zwift on Linux"; + + containerTool = mkOption { + type = types.enum [ + "podman" + "docker" + ]; + default = "podman"; + description = ''How to run Zwift: "podman" or "docker".''; + }; + + 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."; + }; + + 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."; + }; + + 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 { + 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"; +} From eda470450bebafe8fa6777c02988653f5d6754d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Tue, 14 Jul 2026 12:12:01 +0200 Subject: [PATCH 2/6] feat: add FHS backend Add support for running Zwift natively using Wine in a FHS environment as an alternative to containers. Introduces src/zwift-nix-fhs.sh as the FHS launch script, extends run_zwift.sh and update_zwift.sh to handle the nix-fhs container tool, adds nix/zwift-fhs-package.nix, wires up the new runfromprocess-rs input, and extends the NixOS module with containerTool = "fhs". --- flake.lock | 158 ++++++++++++++++++++++- flake.nix | 27 +++- nix/module.nix | 141 +++++++++++++-------- nix/zwift-fhs-package.nix | 257 ++++++++++++++++++++++++++++++++++++++ src/run_zwift.sh | 10 +- src/update_zwift.sh | 32 +++-- src/zwift-nix-fhs.sh | 104 +++++++++++++++ 7 files changed, 659 insertions(+), 70 deletions(-) create mode 100644 nix/zwift-fhs-package.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 10ea98f5..7ad0ae93 100644 --- a/flake.nix +++ b/flake.nix @@ -1,17 +1,33 @@ { 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 system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; + zwift-fhs = import ./nix/zwift-fhs-package.nix { + inherit + pkgs + system + runfromprocess-rs + ; + }; + zwift-container = import ./nix/zwift-container-package.nix { inherit pkgs; }; - nixosModule = import ./nix/module.nix; + nixosModule = import ./nix/module.nix { zwift-fhs-package = zwift-fhs; }; in { nixosModules = { @@ -57,7 +73,10 @@ }; packages.${system} = { - inherit zwift-container; + inherit + zwift-fhs + zwift-container + ; default = zwift-container; zwift-unwrapped = pkgs.stdenv.mkDerivation rec { diff --git a/nix/module.nix b/nix/module.nix index 2ff28ccd..5c0cf9ae 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -1,4 +1,5 @@ # NixOS module for Zwift +{ zwift-fhs-package }: { config, lib, pkgs, @@ -23,89 +24,104 @@ in type = types.enum [ "podman" "docker" + "fhs" ]; default = "podman"; - description = ''How to run Zwift: "podman" or "docker".''; + 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."; + description = "Container image to use. Only applies when containerTool = \"podman\" or \"docker\"."; }; version = mkOption { type = types.str; default = ""; - description = "Container image tag/version."; + description = "Container image tag/version. Only applies when containerTool = \"podman\" or \"docker\"."; }; dontCheck = mkOption { type = types.bool; default = false; - description = "Skip version check."; + description = "Skip version check. Only applies when containerTool = \"podman\" or \"docker\"."; }; dontPull = mkOption { type = types.bool; default = false; - description = "Skip pulling the container image."; + 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."; + 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."; + 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."; + 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."; + 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."; + 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."; + 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."; + 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."; + 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."; + description = "Run the container in privileged mode. Only applies when containerTool = \"podman\" or \"docker\"."; }; + # Common options zwiftUsername = mkOption { type = types.str; default = ""; @@ -193,41 +209,64 @@ in }; }; - config = mkIf cfg.enable { - 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 ""; - }) - ]; - }; + config = mkIf cfg.enable (lib.mkMerge [ + (mkIf (cfg.containerTool == "fhs") { + environment.systemPackages = [ zwift-fhs-package ]; + + 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"} + ''; + }) + + (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-fhs-package.nix b/nix/zwift-fhs-package.nix new file mode 100644 index 00000000..3878e867 --- /dev/null +++ b/nix/zwift-fhs-package.nix @@ -0,0 +1,257 @@ +{ + pkgs, + system, + runfromprocess-rs, +}: +let + common = import ./zwift-common.nix { inherit pkgs; }; + + runfromprocess = runfromprocess-rs.packages.${system}.x86_64-pc-windows-gnu; + + zwift-scripts = 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 + ''; + }; + + 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 ${../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/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 From 04248afbd90bd034e7c281ab793b0a3cf783c6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Tue, 14 Jul 2026 12:13:49 +0200 Subject: [PATCH 3/6] test/ci: cover FHS backend Parametrize the NixOS test flake to build all three containerTool values (podman, docker, fhs) and update CI to check all three. --- .github/workflows/lint_everything.yaml | 2 +- tests/nix-flake/configuration.nix | 6 ------ tests/nix-flake/flake.nix | 29 +++++++++++++++++++------- 3 files changed, 22 insertions(+), 15 deletions(-) delete mode 100644 tests/nix-flake/configuration.nix 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/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; + }; }; } From 9e6933a02b85aff6d67e27ff18f382a74d730682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Tue, 14 Jul 2026 12:14:38 +0200 Subject: [PATCH 4/6] docs: document FHS backend and new options Update the NixOS documentation with containerTool = "fhs" and winePrefix. --- docs/advanced/nixos.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 = ""; }; } ``` From 7b3bda061017faa6e938820bd207fcc4450b7732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Sat, 25 Jul 2026 19:54:56 +0200 Subject: [PATCH 5/6] Rely on ZwiftLauncher to start zwift with FHS --- flake.lock | 166 ++------------------------------------ flake.nix | 39 ++------- nix/module.nix | 29 +++---- nix/zwift-fhs-package.nix | 106 +++++++++++------------- src/run_zwift.sh | 10 +-- src/update_zwift.sh | 32 +++----- src/zwift-nix-fhs.sh | 166 ++++++++++++++++++++++++-------------- 7 files changed, 183 insertions(+), 365 deletions(-) diff --git a/flake.lock b/flake.lock index 43802237..a1b31611 100644 --- a/flake.lock +++ b/flake.lock @@ -1,180 +1,24 @@ { "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, - "narHash": "sha256-yD4K/vRHPwXbJf5CK3JkptBA6nFWUKNX/jlFp2eKEQc=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "41965737c1797c1d83cfb0b644ed0840a6220bd1", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "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=", + "lastModified": 1784856561, + "narHash": "sha256-J+Bx1Z6Oeoj2FgnBhRMKyUhhtDoOpTgXYaVLZpDjW4A=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "945559664c1dc5836173ee12896ba421d9b37181", + "rev": "597283ad8aa0b331c788e97c4c262d58877074ef", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "nixos-26.05", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "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" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 7ad0ae93..5eddcb56 100644 --- a/flake.nix +++ b/flake.nix @@ -1,33 +1,22 @@ { description = "Easily zwift on linux"; - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - - runfromprocess-rs.url = "github:quietvoid/runfromprocess-rs?rev=a3d003c07d1bd11ff93c4cac96d2c3aa5deb8471"; - }; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; outputs = { nixpkgs, - runfromprocess-rs, self, }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; - zwift-fhs = import ./nix/zwift-fhs-package.nix { - inherit - pkgs - system - runfromprocess-rs - ; - }; + zwift-fhs = import ./nix/zwift-fhs-package.nix { inherit pkgs; }; zwift-container = import ./nix/zwift-container-package.nix { inherit pkgs; }; - nixosModule = import ./nix/module.nix { zwift-fhs-package = zwift-fhs; }; + nixosModule = import ./nix/module.nix; in { nixosModules = { @@ -49,8 +38,8 @@ hadolint # Markdown - nodePackages.markdownlint-cli2 - nodePackages.cspell + markdownlint-cli2 + cspell # Documentation (Jekyll) ruby @@ -78,24 +67,6 @@ zwift-container ; default = zwift-container; - - zwift-unwrapped = 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" ]; - }; }; }; } diff --git a/nix/module.nix b/nix/module.nix index 5c0cf9ae..c81d0de6 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -1,5 +1,4 @@ # NixOS module for Zwift -{ zwift-fhs-package }: { config, lib, pkgs, @@ -15,6 +14,8 @@ let ; wrapContainerPackage = args: import ./zwift-container-package.nix ({ inherit pkgs; } // args); + wrapFhsPackage = + args: import ./zwift-fhs-package.nix ({ inherit pkgs; } // args); in { options.programs.zwift = { @@ -211,24 +212,14 @@ in config = mkIf cfg.enable (lib.mkMerge [ (mkIf (cfg.containerTool == "fhs") { - environment.systemPackages = [ zwift-fhs-package ]; - - 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"} - ''; + environment.systemPackages = [ + (wrapFhsPackage { + inherit (cfg) + winePrefix + ; + debug = if cfg.debug then "1" else ""; + }) + ]; }) (mkIf (cfg.containerTool != "fhs") { diff --git a/nix/zwift-fhs-package.nix b/nix/zwift-fhs-package.nix index 3878e867..789c58f4 100644 --- a/nix/zwift-fhs-package.nix +++ b/nix/zwift-fhs-package.nix @@ -1,15 +1,13 @@ { pkgs, - system, - runfromprocess-rs, + winePrefix ? "", + debug ? "", }: let common = import ./zwift-common.nix { inherit pkgs; }; - runfromprocess = runfromprocess-rs.packages.${system}.x86_64-pc-windows-gnu; - - zwift-scripts = pkgs.stdenv.mkDerivation { - pname = "zwift-scripts"; + zwift-nix-fhs = pkgs.stdenv.mkDerivation { + pname = "zwift-nix-fhs"; version = "0-unstable"; dontUnpack = true; @@ -18,10 +16,7 @@ let 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 ''; @@ -36,7 +31,7 @@ let targetPkgs = pkgs: with pkgs; [ # Wine (staging has better compatibility than devel/unstable) - wineWowPackages.stagingFull + wineWow64Packages.stagingFull winetricks # Graphics - Vulkan @@ -50,21 +45,21 @@ let 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 + libx11 + libxext + libxrandr + libxrender + libxcursor + libxfixes + libxi + libxcomposite + libxdamage + libxtst + libxscrnsaver + libxcb + libice + libsm + xrandr # Wayland wayland @@ -72,11 +67,9 @@ let wayland-protocols # Audio - pulseaudio pipewire alsa-lib alsa-plugins - libpulseaudio # System libraries glib @@ -129,11 +122,8 @@ let gtk3 gdk-pixbuf - # The runfromprocess Windows binary - runfromprocess - - # Our wrapper scripts - zwift-scripts + # Our wrapper script + zwift-nix-fhs ]; # 32-bit packages for Wine compatibility @@ -147,23 +137,21 @@ let libglvnd # X11 - xorg.libX11 - xorg.libXext - xorg.libXrandr - xorg.libXrender - xorg.libXcursor - xorg.libXfixes - xorg.libXi - xorg.libXcomposite - xorg.libXdamage - xorg.libXtst - xorg.libxcb + libx11 + libxext + libxrandr + libxrender + libxcursor + libxfixes + libxi + libxcomposite + libxdamage + libxtst + libxcb # Audio - pulseaudio alsa-lib alsa-plugins - libpulseaudio # System glib @@ -188,7 +176,11 @@ let ]; profile = '' - export WINEPREFIX="''${WINEPREFIX:-$HOME/.wine-zwift}" + ${if winePrefix != "" then '' + export WINEPREFIX="${winePrefix}" + '' else '' + export WINEPREFIX="''${WINEPREFIX:-$HOME/.wine-zwift}" + ''} export WINEDEBUG="''${WINEDEBUG:--all}" export WINEARCH=win64 @@ -220,6 +212,12 @@ let 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 { @@ -232,22 +230,8 @@ pkgs.stdenv.mkDerivation { 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 -Dm755 ${nixosRun} -T $out/bin/zwift install -Dm644 ${../bin/Zwift.svg} -T $out/share/icons/hicolor/scalable/apps/zwift.svg - runHook postInstall ''; diff --git a/src/run_zwift.sh b/src/run_zwift.sh index 4338f058..bf0b794f 100755 --- a/src/run_zwift.sh +++ b/src/run_zwift.sh @@ -28,14 +28,8 @@ readonly ZWIFT_PASSWORD="${ZWIFT_PASSWORD:-}" readonly ZWIFT_OVERRIDE_RESOLUTION="${ZWIFT_OVERRIDE_RESOLUTION:-}" readonly ZWIFT_NO_GAMEMODE="${ZWIFT_NO_GAMEMODE:-0}" -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 WINE_USER_HOME="/home/user/.wine/drive_c/users/user" +readonly ZWIFT_HOME="/home/user/.wine/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 0626adb9..df975b25 100755 --- a/src/update_zwift.sh +++ b/src/update_zwift.sh @@ -22,16 +22,10 @@ else fi readonly VERBOSITY="${VERBOSITY:-1}" -readonly CONTAINER_TOOL="${CONTAINER_TOOL:-}" +readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" -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 WINE_USER_HOME="/home/user/.wine/drive_c/users/user" +readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" readonly ZWIFT_DOCS="${WINE_USER_HOME}/AppData/Local/Zwift" msgbox() { @@ -104,7 +98,7 @@ update_zwift_using_launcher() { msgbox ok "Zwift launcher started using wine" local counter=1 - local max_iterations=120 # 120 * 5s = 10 minutes max + local max_iterations=60 # 60 * 5s = 5 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 @@ -163,16 +157,14 @@ cleanup() { msgbox info "Stopping wine server" wineserver -k || true # important, Zwift launcher won't stop until wine server is killed - 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 + 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 } trap cleanup EXIT diff --git a/src/zwift-nix-fhs.sh b/src/zwift-nix-fhs.sh index 634f6326..4a809aa5 100644 --- a/src/zwift-nix-fhs.sh +++ b/src/zwift-nix-fhs.sh @@ -22,6 +22,13 @@ else 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 @@ -31,74 +38,109 @@ msgbox() { [[ ${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}" ;; + 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 } -readonly USER_CONFIG_DIR="${HOME}/.config/zwift" +if ! TMPDIR=$(mktemp -d); then + msgbox error "Could not create temp dir" + exit 1 +fi +readonly TMPDIR -load_config_file() { - local config_file="${1:?}" - if [[ -f ${config_file} ]]; then - set -a - # shellcheck source=/dev/null - source "${config_file}" - set +a - fi +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}" } -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" + +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 } -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 +########################################## +##### 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 - ;; -esac + 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 From 0b6f777e0051e835d5104513aec00474808e135f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Sun, 26 Jul 2026 18:55:39 +0200 Subject: [PATCH 6/6] Simplify FHS building --- nix/zwift-fhs-package.nix | 142 +------------------------------------- 1 file changed, 2 insertions(+), 140 deletions(-) diff --git a/nix/zwift-fhs-package.nix b/nix/zwift-fhs-package.nix index 789c58f4..facb5d14 100644 --- a/nix/zwift-fhs-package.nix +++ b/nix/zwift-fhs-package.nix @@ -25,155 +25,28 @@ let 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) + # Wine wineWow64Packages.stagingFull winetricks - # Graphics - Vulkan - vulkan-loader - vulkan-tools - - # Graphics - OpenGL/Mesa - mesa - libGL - libGLU - libglvnd - - # X11 libraries - libx11 - libxext - libxrandr - libxrender - libxcursor - libxfixes - libxi - libxcomposite - libxdamage - libxtst - libxscrnsaver - libxcb - libice - libsm - xrandr - - # Wayland - wayland - libxkbcommon - wayland-protocols - - # Audio - pipewire - alsa-lib - alsa-plugins - - # System libraries - glib - glibc - dbus - systemd - fontconfig - freetype - - # Networking - curl + # Networking (wget used by install script; cacert referenced in profile) 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 - # Our wrapper script zwift-nix-fhs ]; - # 32-bit packages for Wine compatibility - multiPkgs = - pkgs: with pkgs; [ - # Graphics - vulkan-loader - mesa - libGL - libGLU - libglvnd - - # X11 - libx11 - libxext - libxrandr - libxrender - libxcursor - libxfixes - libxi - libxcomposite - libxdamage - libxtst - libxcb - - # Audio - alsa-lib - alsa-plugins - - # System - glib - glibc - dbus - fontconfig - freetype - - # Wine dependencies - gnutls - openssl - ncurses - zlib - libpng - libjpeg - SDL2 - openal - - # GTK - gtk3 - gdk-pixbuf - ]; profile = '' ${if winePrefix != "" then '' @@ -189,20 +62,9 @@ let 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}"