main
  1{
  2  lib,
  3  fetchFromGitHub,
  4  fetchurl,
  5  stdenv,
  6  python3,
  7  python3Packages,
  8  wineWow64Packages,
  9  unzip,
 10  makeWrapper,
 11  nix-update-script,
 12  protontricks,
 13}: let
 14  version = "7.0.4";
 15
 16  mo2-lint-src = fetchFromGitHub {
 17    owner = "Furglitch";
 18    repo = "modorganizer2-linux-installer";
 19    tag = version;
 20    hash = "sha256-fcoJ9CeDQ+iNsXd/6otKvdqyfVVANir3O5ApSSkx2Kc=";
 21  };
 22
 23  python-embed = fetchurl {
 24    url = "https://www.python.org/ftp/python/3.13.0/python-3.13.0-embed-amd64.zip";
 25    hash = "sha256-AcMtBzdDIkCtzwu8HTIyfwl206HhQnd0vI/ryPHAMRE=";
 26  };
 27
 28  get-pip-script = fetchurl {
 29    url = "https://raw.githubusercontent.com/pypa/get-pip/refs/tags/26.1.1/public/get-pip.py";
 30    hash = "sha256-ZpBLzLh442PbYjbqkA5pNeUH3LiH6fF49iEu3+f0anY=";
 31  };
 32
 33  windows-python-deps = stdenv.mkDerivation {
 34    name = "mo2-win-pip-wheels";
 35
 36    outputHashMode = "recursive";
 37    outputHashAlgo = "sha256";
 38    outputHash = "sha256-n9/lI0r3vi0KzkXlkKImOLBOBJu8OT2lYSr/l2gY+dI=";
 39
 40    nativeBuildInputs = [python3 python3Packages.pip];
 41
 42    buildCommand = ''
 43      mkdir -p $out
 44      export PIP_CACHE_DIR=$TMPDIR/pip-cache
 45
 46      pip download \
 47        --platform win_amd64 \
 48        --python-version 3.13 \
 49        --only-binary=:all: \
 50        --dest $out \
 51        altgraph==0.17.5 \
 52        colorama==0.4.6 \
 53        loguru==0.7.3 \
 54        packaging==26.2 \
 55        pefile==2024.8.26 \
 56        pip==26.1.2 \
 57        pyinstaller==6.21.0 \
 58        pyinstaller_hooks_contrib==2026.6 \
 59        pywin32-ctypes==0.2.3 \
 60        pyyaml==6.0.3 \
 61        setuptools==82.0.1 \
 62        win32-setctime==1.2.0
 63    '';
 64  };
 65
 66  mo2-redirector = stdenv.mkDerivation {
 67    name = "mo2-redirector.exe";
 68
 69    src = mo2-lint-src;
 70
 71    nativeBuildInputs = [wineWow64Packages.stable unzip];
 72
 73    buildPhase = ''
 74      runHook preBuild
 75
 76      export XDG_CACHE_HOME=$TMPDIR/.cache
 77      export FONTCONFIG_CACHE=$XDG_CACHE_HOME/fontconfig
 78      mkdir -p $FONTCONFIG_CACHE
 79
 80      export WINEPREFIX=$TMPDIR/wine
 81      export WINEARCH=win64
 82      export WINEDEBUG=-all
 83
 84      wineboot --init
 85      wineserver -w
 86
 87      mkdir -p $WINEPREFIX/drive_c/python313
 88
 89      unzip -q ${python-embed} -d $WINEPREFIX/drive_c/python313
 90      sed -i 's/#import site/import site/' $WINEPREFIX/drive_c/python313/python313._pth
 91
 92      wine "C:\\python313\\python.exe" Z:${get-pip-script} --no-index \
 93        --find-links=Z:${windows-python-deps} \
 94        --no-warn-script-location
 95
 96      wine "C:\\python313\\python.exe" -m pip install --no-index \
 97        --find-links=Z:${windows-python-deps} \
 98        pyinstaller loguru pyyaml
 99
100      wine "C:\\python313\\python.exe" -m PyInstaller \
101        --onefile \
102        --noconsole \
103        --name mo2-redirector.exe \
104        --paths src \
105        --hidden-import loguru \
106        --hidden-import yaml \
107        --hidden-import configparser \
108        --add-data "configs;cfg" \
109        --icon Z:$PWD/.github/README/logo.ico \
110        Z:$PWD/src/redirector/__init__.py
111
112      runHook postBuild
113    '';
114
115    installPhase = ''
116      runHook preInstall
117
118      mkdir -p $out/dist
119      cp dist/mo2-redirector.exe $out/dist/
120
121      runHook postInstall
122    '';
123  };
124
125  runtime-python-deps = python3.withPackages (ps:
126    with ps; [
127      certifi
128      click
129      python-dotenv
130      inquirerpy
131      loguru
132      packaging
133      patool
134      psutil
135      pydantic
136      pyyaml
137      requests
138      send2trash
139      websockets
140      (python3Packages.toPythonModule protontricks)
141    ]);
142
143  mo2-lint = stdenv.mkDerivation (finalAttrs: {
144    name = "mo2-lint";
145    pname = "mo2-lint";
146
147    inherit version;
148    src = mo2-lint-src;
149
150    passthru.updateScript = nix-update-script {};
151
152    nativeBuildInputs = [makeWrapper];
153
154    postPatch = ''
155      # Fix fallback path: upstream uses Path(__file__).resolve() which points
156      # to the py file itself, but joinpath needs a directory. Add .parent so the
157      # fallback resolves to src/mo2-lint/ where the cfg/dist symlinks live.
158      # `|| true` for forward-compat if upstream fixes this in a future release.
159      sed -i 's/Path(__file__)/Path(__file__).parent/g' src/mo2-lint/util/internal_file.py || true
160
161      # Nix store files have 0o444 (read-only). When copy2 places config files
162      # from the store into ~/.config/mo2-lint/, they must be made writable.
163      sed -i 's/copy2(src, dest)/copy2(src, dest); import os; os.chmod(dest, 0o644)/g' src/mo2-lint/__init__.py
164
165      # Same read-only Nix store issue for downloaded mod archives etc.
166      sed -i 's/copy2(internal_path, output)/copy2(internal_path, output); import os; os.chmod(output, 0o644)/g' src/mo2-lint/util/nexus/install_handler.py
167
168      # click uses sys.argv[0] for prog_name in help/version output. Set it to
169      # the package name instead of a nix store path or plain "python".
170      sed -i '1s/^/import sys; sys.argv[0] = "${finalAttrs.pname}"\n/' src/mo2-lint/__init__.py
171
172      # pre_init() is called at module import time (before CLI arg parsing) and
173      # hardcodes TRACE, dumping ~40 lines of config parse dumps and trace
174      # messages. Bump to WARNING so only real issues show during startup.
175      sed -i 's/add_loggers(log_level="TRACE", script="mo2-lint", process="pre-check")/add_loggers(log_level="WARNING", script="mo2-lint", process="pre-check")/' src/mo2-lint/__init__.py
176    '';
177
178    buildPhase = ''
179      runHook preBuild
180
181      mkdir -p dist
182      ln -s ${mo2-redirector}/dist/mo2-redirector.exe dist/
183
184      runHook postBuild
185    '';
186
187    installPhase = ''
188      runHook preInstall
189
190      mkdir -p $out/share/mo2-lint
191      cp -r src configs dist $out/share/mo2-lint/
192      ln -s $out/share/mo2-lint/configs $out/share/mo2-lint/src/mo2-lint/util/cfg
193      ln -s $out/share/mo2-lint/dist $out/share/mo2-lint/src/mo2-lint/util/dist
194
195      mkdir -p $out/bin
196      makeWrapper ${runtime-python-deps}/bin/python $out/bin/${finalAttrs.pname} \
197        --add-flags "$out/share/mo2-lint/src/mo2-lint/__init__.py" \
198        --prefix PYTHONPATH : "$out/share/mo2-lint/src" \
199        --set MEIPASS "$out/share/mo2-lint" \
200        --set _MEIPASS2 "$out/share/mo2-lint" \
201        --set-default LOGURU_LEVEL "INFO"
202      makeWrapper ${runtime-python-deps}/bin/python $out/share/mo2-lint/dist/nxm-handler \
203        --add-flags "$out/share/mo2-lint/src/nxm-handler/__init__.py" \
204        --prefix PYTHONPATH : "$out/share/mo2-lint/src" \
205        --set MEIPASS "$out/share/mo2-lint" \
206        --set _MEIPASS2 "$out/share/mo2-lint" \
207        --set-default LOGURU_LEVEL "INFO"
208
209      runHook postInstall
210    '';
211
212    meta = {
213      description = "An easy-to-use Mod Organizer 2 installer for Linux, rewrited in Python.";
214      homepage = "https://github.com/Furglitch/modorganizer2-linux-installer";
215      licence = lib.licenses.gpl3Only;
216      platforms = ["x86_64-linux"];
217    };
218  });
219in
220  mo2-lint