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