Skip to content

Commit cf83ce0

Browse files
Zayn995claude
andcommitted
Ship the signed python.org launcher instead of PyInstaller (1.21.0)
S2Tweaker.exe is now pythonw.exe from python.org, byte for byte, signed by the Python Software Foundation. python3XX._pth next to it pins the module search path to _internal, and _internal/sitecustomize.py (tools/launcher.py in this repository) starts the GUI. The tool's own code ships as readable .py files; the standard library as a .pyc zip compiled from the same python.org installation, without socket, ssl, asyncio and sqlite3, and without any OpenSSL library. Every DLL, PYD and EXE in the folder is signed by the PSF or Microsoft except repak.exe, which the CI compiles from source. Why: 1.20.0 was flagged by two engines on VirusTotal (Microsoft Trojan:Win32/Wacatac.B!ml, Zillya Dropper.Agent.Win32.746397) although its launcher was byte for byte PyInstaller's official runw.exe, which scans clean on its own. The detections were aimed at the archive PyInstaller appends, i.e. at PyInstaller itself, and no setting inside PyInstaller could change that. VirusTotal, 5 September 2026, local build of this change (Python 3.14.2, with the repak.exe from the 1.20.0 CI build): S2Tweaker_v1.21.0-local.zip: 0/66 https://www.virustotal.com/gui/file/887d836efe79acf821a578f8824d6da7e200286c249f07fd0e9db55adea4ac44 S2Tweaker.exe (= pythonw.exe 3.14.2): 0/70, "File distributed by Python Software Foundation" https://www.virustotal.com/gui/file/58b39b6d8dc9f51a94f1a3143e49b7498fb804a101f2b33baa14bd72d45298f8 For comparison, 1.20.0: ZIP 1/65, EXE 2/70. The release build comes from the GitHub Actions workflow (Python 3.12) and is scanned separately. Changes: - tools/build_exe.py: assembles the folder from the python.org installation (no PyInstaller), verifies signatures, hash equality with pythonw.exe and forbidden files, and runs a self-test on a copy. - tools/launcher.py: the start-up module, shipped as sitecustomize.py, including the self-test the build uses. - tests/test_build_layout.py: builds and checks the layout (in run_all). - .github/workflows/build.yml: independent PowerShell cross-check of the hash, the signatures and the absence of networking modules. - gui.py, pakio.py: assets and repak are found next to the package; no PyInstaller-specific paths any more. - main.py, requirements.txt, build.bat, make_release_zips.py adjusted; tools/make_version_file.py removed (the exe keeps Python's version resource; the tool's version is in the window title). - README, release/README.txt, FAQ, THIRD_PARTY_LICENSES.txt and docs/CODE_SIGNING_POLICY.md updated. The shipped README and the signing policy still described the update button and the Oodle download, both gone since 1.19.x. - tests/run_all.py runs test_no_network.py instead of the deleted test_update_check.py. Cosmetic price: the exe shows the Python icon and Python's version info, because changing either would break the signature. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent dfd6790 commit cf83ce0

20 files changed

Lines changed: 1064 additions & 336 deletions

.github/workflows/build.yml

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22
#
33
# Warum es das gibt (04.09.2026): der Nexus-Support konnte die hochgeladene
44
# Datei nicht freigeben, weil Virenscanner sie als Malware markierten. Teil
5-
# der Antwort darauf ist Nachvollziehbarkeit — jede veroeffentlichte EXE
5+
# der Antwort darauf ist Nachvollziehbarkeit — jede veroeffentlichte Datei
66
# soll beweisbar aus DIESEM oeffentlichen Quellcode stammen und nicht aus
7-
# einem Ordner auf einem Laptop. Das ist ausserdem die Voraussetzung fuer
8-
# kostenloses Code-Signing durch die SignPath Foundation, die nur
9-
# Artefakte aus einer automatisierten Bau-Kette signiert.
7+
# einem Ordner auf einem Laptop.
108
#
11-
# Die Bau-Anweisung selbst steht NICHT hier, sondern in tools/build_exe.py
9+
# Seit 1.21.0 (05.09.2026) gibt es keinen PyInstaller mehr: S2Tweaker.exe
10+
# ist die unveraenderte, von der Python Software Foundation signierte
11+
# pythonw.exe der python.org-Installation, und der Python-Code liegt lesbar
12+
# daneben. Die Bau-Anweisung steht NICHT hier, sondern in tools/build_exe.py
1213
# (dasselbe Skript, das build.bat lokal aufruft) — sonst haetten wir zwei
13-
# Kopien, die auseinanderlaufen.
14+
# Kopien, die auseinanderlaufen. Das Skript prueft seinen Ordner selbst
15+
# (Signaturen, Hash-Gleichheit mit pythonw.exe, verbotene Dateien) und
16+
# startet ihn einmal probeweise. Der Schritt "Gegenprobe" unten wiederholt
17+
# die wichtigsten Punkte unabhaengig davon mit Windows-Bordmitteln.
1418
name: build
1519

1620
on:
@@ -43,56 +47,49 @@ jobs:
4347
run: python tools/build_repak.py
4448

4549
- name: Tests ohne Spieldaten
46-
# Die volle Batterie (32 Suiten) braucht den vanilla/-Ordner mit
47-
# extrahierten GameData. Die duerfen nie ins Repo (GSC-Copyright),
48-
# also laeuft hier nur, was ohne sie auskommt; der Rest vor jedem
49-
# Release lokal.
50+
# Die volle Batterie braucht den vanilla/-Ordner mit extrahierten
51+
# GameData. Die duerfen nie ins Repo (GSC-Copyright), also laeuft
52+
# hier nur, was ohne sie auskommt; der Rest vor jedem Release lokal.
5053
run: python tests/run_ci.py
5154

52-
- name: EXE bauen
55+
- name: Programmordner bauen (inkl. Gegenprobe und Selbsttest)
5356
run: python tools/build_exe.py
5457

55-
- name: Gegenprobe — Versions-Ressource und Layout
56-
# Genau die zwei Dinge, die den Virenscanner-Fehlalarm beguenstigt
57-
# haben: fehlende Herkunftsangaben und die selbstentpackende
58-
# Einzeldatei. Wenn eines davon zurueckfaellt, soll der Build rot
59-
# werden und nicht still ein Artefakt hochladen.
58+
- name: Gegenprobe — signierter Starter, keine Netzmodule
59+
# Unabhaengig vom Bau-Skript, mit Windows-Bordmitteln: der Starter
60+
# muss Byte fuer Byte die pythonw.exe des Runners sein und von der
61+
# Python Software Foundation signiert; jede DLL/PYD muss gueltig
62+
# signiert sein (einzige Ausnahme: das selbst gebaute repak.exe);
63+
# und nichts, was Netz oder TLS kann, darf im Paket liegen.
6064
shell: pwsh
6165
run: |
62-
$exe = "dist/S2Tweaker/S2Tweaker.exe"
66+
$dist = "dist/S2Tweaker"
67+
$exe = "$dist/S2Tweaker.exe"
6368
if (-not (Test-Path $exe)) { throw "EXE fehlt: $exe" }
64-
if (-not (Test-Path "dist/S2Tweaker/_internal")) {
65-
throw "_internal fehlt - wurde wieder --onefile gebaut?"
69+
$py = Join-Path $env:pythonLocation "pythonw.exe"
70+
$h1 = (Get-FileHash $exe -Algorithm SHA256).Hash
71+
$h2 = (Get-FileHash $py -Algorithm SHA256).Hash
72+
"S2Tweaker.exe : $h1"
73+
"pythonw.exe : $h2"
74+
if ($h1 -ne $h2) { throw "S2Tweaker.exe ist nicht die unveraenderte pythonw.exe" }
75+
$sig = Get-AuthenticodeSignature $exe
76+
"Signatur : $($sig.Status) / $($sig.SignerCertificate.Subject)"
77+
if ($sig.Status -ne 'Valid' -or $sig.SignerCertificate.Subject -notmatch 'Python Software Foundation') {
78+
throw "Starter nicht gueltig von der Python Software Foundation signiert"
6679
}
67-
$vi = (Get-Item $exe).VersionInfo
68-
"CompanyName : $($vi.CompanyName)"
69-
"ProductName : $($vi.ProductName)"
70-
"FileVersion : $($vi.FileVersion)"
71-
"Groesse : $((Get-Item $exe).Length) Bytes"
72-
foreach ($f in 'CompanyName', 'ProductName', 'FileVersion') {
73-
if ([string]::IsNullOrWhiteSpace($vi.$f)) {
74-
throw "Versions-Ressource unvollstaendig: $f ist leer"
75-
}
80+
$bins = Get-ChildItem $dist -Recurse -Include *.exe,*.dll,*.pyd
81+
$unsigned = @($bins | Where-Object { (Get-AuthenticodeSignature $_.FullName).Status -ne 'Valid' })
82+
"Binaerdateien : $($bins.Count), davon unsigniert: $($unsigned.Name -join ', ')"
83+
if (@($unsigned | Where-Object { $_.Name -ne 'repak.exe' }).Count -gt 0) {
84+
throw "Unsignierte Binaerdatei ausser repak.exe im Paket"
7685
}
77-
# Der Starter darf kein eingebettetes Archiv mehr sein. Die alte
78-
# Schranke von 8 MB hat das NICHT geprueft: 1.19.1 kam mit
79-
# 3,17 MB durch und bestand zu 90 % aus genau dem Archiv, das
80-
# hier ausgeschlossen sein sollte. Mit --debug noarchive liegt
81-
# der Starter bei ~345 KB; 1 MB laesst Luft und schlaegt an,
82-
# sobald der Klumpen zurueckkehrt.
83-
if ((Get-Item $exe).Length -gt 1MB) {
84-
throw "Starter ist $((Get-Item $exe).Length) Bytes gross - steckt wieder ein Archiv drin?"
85-
}
86-
# Gegenprobe am Inhalt statt nur an der Groesse: die Module
87-
# muessen als .pyc in _internal liegen.
88-
$pyc = (Get-ChildItem "dist/S2Tweaker/_internal" -Recurse -Filter *.pyc | Measure-Object).Count
89-
"Module als .pyc : $pyc"
90-
if ($pyc -lt 100) {
91-
throw "Nur $pyc .pyc-Dateien - steckt der Python-Code wieder in der EXE?"
86+
foreach ($bad in '_ssl', '_socket', '_hashlib', 'libssl', 'libcrypto', 'sqlite3', '_multiprocessing') {
87+
if (Get-ChildItem $dist -Recurse -Filter "$bad*") { throw "$bad* im Paket" }
9288
}
89+
if (-not (Test-Path "$dist/_internal/sitecustomize.py")) { throw "Starter-Modul fehlt" }
90+
"Groesse : $((Get-ChildItem $dist -Recurse -File | Measure-Object Length -Sum).Sum) Bytes"
9391
9492
- name: Programmordner als Artefakt
95-
# Genau dieses Artefakt bekommt spaeter die SignPath-Signatur.
9693
uses: actions/upload-artifact@v4
9794
with:
9895
name: S2Tweaker-${{ github.sha }}

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Architektur & Mechanik: [README.md](README.md) · Recherche: [docs/SPEC.md](docs
2424
pip install -r requirements.txt
2525
python main.py # GUI starten (Dev-Modus nutzt vanilla/, falls vorhanden)
2626
python test_generate.py # Ende-zu-Ende-Test: baut Test-Pak mit vielen Tweaks
27-
build.bat # baut dist\S2Tweaker.exe (PyInstaller)
27+
build.bat # baut dist\S2Tweaker\ (signierte pythonw.exe + _internal, KEIN PyInstaller)
2828
```
2929

3030
Hinweis: Auf manchen Rechnern zeigen `python` und `pip` auf verschiedene

README.md

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ money skip cover them fully.)
156156
## Project structure
157157

158158
```
159-
main.py entry point (also the PyInstaller entry)
159+
main.py entry point for development (python main.py)
160160
s2tweaker/
161161
gui.py customtkinter GUI (dark, English)
162162
tweaks.py Settings dataclass + one builder per feature
@@ -167,32 +167,49 @@ s2tweaker/
167167
game.py game folder auto-detection (Steam/GOG/Xbox)
168168
vendor_bin2cfg.py cfg.bin → cfg decoder (vendored, public domain)
169169
tools/repak.exe pak tool (MIT/Apache-2.0, by trumank)
170+
tools/build_exe.py assembles the program folder dist/S2Tweaker/ (no PyInstaller)
171+
tools/launcher.py shipped as _internal/sitecustomize.py: starts the GUI
170172
docs/SPEC.md research: every tweak's mechanism + sources
171-
release/README.txt end-user readme shipped with the exe
173+
release/README.txt end-user readme shipped with the program
172174
THIRD_PARTY_LICENSES.txt licences of the bundled components
173175
test_generate.py end-to-end dev test (builds a test pak)
174-
build.bat builds dist/S2Tweaker/ (exe + _internal)
176+
build.bat runs tools/build_exe.py
175177
```
176178

177179
## Building from source
178180

179181
```
180182
pip install -r requirements.txt
181183
python main.py # run the GUI directly
182-
build.bat # or build dist/S2Tweaker/S2Tweaker.exe
184+
build.bat # or assemble dist/S2Tweaker/ (needs a python.org install)
183185
```
184186

185187
`tools/repak.exe` is not the upstream release binary: `python tools/build_repak.py` rebuilds it from source (pinned tag) with the runtime Oodle download removed, so nothing in the shipped folder can fetch anything. The CI does this on every build.
186188

187-
The build is deliberately **`--onedir`, not `--onefile`**: a one-file
188-
PyInstaller exe is a self-extracting archive that unpacks itself into
189-
`%TEMP%` and runs from there, which antivirus ML heuristics read as
190-
dropper behaviour — that got the release quarantined on Nexus Mods in
191-
September 2026 and deleted by Windows Defender once. `--onedir` keeps the
192-
launcher at ~3 MB with nothing embedded, and `--version-file` stamps
193-
company/product/version into the exe (it had no version resource at all
194-
before). The tool stays portable either way: settings, cache, presets and
195-
output are created next to the exe.
189+
**There is no PyInstaller since 1.21.0.** `S2Tweaker.exe` is `pythonw.exe`
190+
from python.org, byte for byte, signed by the Python Software Foundation;
191+
`python3XX._pth` next to it pins the module search path to `_internal`,
192+
and `_internal/sitecustomize.py` (that is `tools/launcher.py`) starts the
193+
GUI. The tool's own code ships as readable `.py` files, the standard
194+
library as a `.pyc` zip compiled from the same python.org installation —
195+
without `socket`, `ssl`, `asyncio` and `sqlite3`, and without any OpenSSL
196+
library, so the package has no networking capability at all. Every DLL,
197+
PYD and EXE in the folder is signed by the PSF or Microsoft except
198+
`repak.exe`, which the CI compiles from source.
199+
200+
Why: the PyInstaller builds kept tripping antivirus heuristics. 1.20.0 was
201+
flagged by two engines on VirusTotal although its launcher was byte for
202+
byte PyInstaller's official `runw.exe`, which is clean on its own — the
203+
detections were aimed at the archive PyInstaller appends, i.e. at
204+
PyInstaller itself. The price of the switch is cosmetic: the exe shows the
205+
Python icon and Python's version info, because changing either would break
206+
the signature. The tool stays portable: settings, cache, presets and output
207+
are created next to the exe.
208+
209+
`tools/build_exe.py` verifies its own output (signatures, hash equality
210+
with `pythonw.exe`, forbidden files) and then starts a copy of the folder
211+
once as a self-test; `tests/test_build_layout.py` runs the whole build and
212+
checks the layout.
196213

197214
Python 3.12+ recommended. For development, the GUI prefers a local
198215
`vanilla/Stalker2/Content/GameLite/GameData/` folder if present (create it by
@@ -229,10 +246,11 @@ the *installed* version, and never hardcode game numbers.
229246
approves a release, and for the two third-party binaries involved.
230247
- Tool code is MIT (see [LICENSE](LICENSE)). Bundled: repak (MIT OR
231248
Apache-2.0), cfg.bin decoder based on public-domain code by
232-
joric/sdwvit/thexii. Their licence texts ship with the tool:
233-
[THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt) in the source, and the
234-
repak MIT notice is also reprinted in `release/README.txt` (the file inside
235-
the player ZIP, since repak.exe is embedded in the exe).
249+
joric/sdwvit/thexii, the Python runtime (PSF licence), Tcl/Tk (BSD-style),
250+
customtkinter (MIT), darkdetect (BSD-3), packaging (Apache-2.0 OR BSD-2).
251+
Their licence texts ship with the tool in `_internal/licenses/`, listed in
252+
[THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt); the repak MIT notice
253+
is also reprinted in `release/README.txt` (the file inside the player ZIP).
236254
- Known limits: DLC items aren't covered by the per-item weight slider;
237255
iron-sight sway is animation-driven (not cfg-tweakable); the in-game
238256
"Custom Rules" difficulty overlaps some multipliers (precedence untested).

THIRD_PARTY_LICENSES.txt

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,43 @@ S2Tweaker - third-party components and their licences
22
=====================================================
33

44
S2Tweaker itself is MIT licensed (see LICENSE). The following third-party
5-
components are bundled with it (tools/repak.exe is also embedded inside
6-
S2Tweaker.exe by PyInstaller), and their licence texts are reproduced here
7-
as those licences require.
5+
components are bundled with it, and their licence texts are reproduced here
6+
or ship in the program folder under _internal\licenses\ as those licences
7+
require.
8+
9+
10+
-------------------------------------------------------------------------
11+
The runtime in the program folder (since 1.21.0, no PyInstaller)
12+
-------------------------------------------------------------------------
13+
14+
S2Tweaker.exe is pythonw.exe from python.org, unmodified and signed by the
15+
Python Software Foundation; python3XX.dll, the extension modules (*.pyd)
16+
and the standard library in _internal\python3XX.zip come from the same
17+
installation. Licence: Python Software Foundation License (PSF), shipped
18+
as _internal\licenses\PYTHON-LICENSE.txt. Copyright (c) 2001-2026 Python
19+
Software Foundation; Copyright (c) 2000 BeOpen.com; Copyright (c)
20+
1995-2001 CNRI; Copyright (c) 1991-1995 Stichting Mathematisch Centrum.
21+
22+
Tcl/Tk (tcl86t.dll, tk86t.dll, _internal\tcl\): BSD-style licence, shipped
23+
as _internal\licenses\TCL-TK-license.terms. Copyright the Regents of the
24+
University of California, Sun Microsystems, Scriptics Corporation,
25+
ActiveState Corporation and other parties.
26+
27+
Visual C++ runtime (vcruntime140.dll, vcruntime140_1.dll): Microsoft,
28+
redistributable with applications built with Visual Studio.
29+
30+
customtkinter (_internal\customtkinter\): MIT, Copyright (c) 2023 Tom
31+
Schimansky. Licence text: _internal\licenses\customtkinter-LICENSE.
32+
33+
darkdetect (_internal\darkdetect\): BSD-3-Clause, Copyright (c) 2019
34+
Alberto Sottile. Licence text: _internal\licenses\darkdetect-LICENSE.
35+
36+
packaging (_internal\packaging\): dual licensed Apache-2.0 OR BSD-2-Clause,
37+
Copyright (c) Donald Stufft and individual contributors. Licence texts:
38+
_internal\licenses\packaging-LICENSE*.
39+
40+
None of these is modified. The tool's own code is in _internal\s2tweaker\
41+
as readable source.
842

943

1044
-------------------------------------------------------------------------
@@ -57,12 +91,9 @@ authors; adapted for S2Tweaker.
5791

5892

5993
-------------------------------------------------------------------------
60-
Runtime / build dependencies (not redistributed as source)
94+
Not bundled
6195
-------------------------------------------------------------------------
6296

63-
customtkinter (MIT, Tom Schimansky) and Python itself (PSF licence) are
64-
linked into the built executable by PyInstaller.
65-
6697
NOT part of this distribution: oo2core_9_win64.dll (Oodle, proprietary,
6798
(c) RAD Game Tools / Epic Games). It is never shipped with S2Tweaker and
6899
never redistributed by this project.

build.bat

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
@echo off
2-
rem Builds dist\S2Tweaker\ (S2Tweaker.exe + _internal), needs:
2+
rem Baut dist\S2Tweaker\ (S2Tweaker.exe + DLLs + _internal), braucht:
33
rem pip install -r requirements.txt
4+
rem und eine python.org-Installation als "python" (pythonw.exe, DLLs\,
5+
rem Lib\, tcl\ nebeneinander) - deren signierte pythonw.exe WIRD der Starter.
46
rem
5-
rem Die eigentliche Bau-Anweisung steht in tools\build_exe.py GENAU EINE
7+
rem Die eigentliche Bau-Anweisung steht in tools\build_exe.py - GENAU EINE
68
rem Stelle, weil der GitHub-Actions-Workflow (.github\workflows\build.yml)
7-
rem dasselbe Skript aufruft. Zwei Kopien der PyInstaller-Zeile waeren
8-
rem irgendwann auseinandergelaufen, und darauf beruht die Zusage, dass die
9-
rem veroeffentlichte EXE aus genau diesem Quellcode stammt.
9+
rem dasselbe Skript aufruft. Zwei Kopien waeren irgendwann
10+
rem auseinandergelaufen, und darauf beruht die Zusage, dass die
11+
rem veroeffentlichte Datei aus genau diesem Quellcode stammt.
1012
rem
11-
rem Warum --onedir und --version-file (kurz; ausfuehrlich in build_exe.py
12-
rem und docs/ROADMAP.md "Virenscanner-Fehlalarm"): die fruehere
13-
rem --onefile-EXE war ein selbstentpackendes 15-MB-Archiv, das sich beim
14-
rem Start nach %TEMP% auspackt — fuer ML-Heuristiken das Profil eines
15-
rem Droppers. Der Nexus-Support konnte die Datei deshalb nicht freigeben,
16-
rem und Windows Defender hat eine frisch gebaute EXE einmal geloescht.
17-
rem Ausserdem hatte die EXE ueberhaupt keine Versions-Angaben.
13+
rem Seit 1.21.0 OHNE PyInstaller (Begruendung im Kopf von build_exe.py,
14+
rem kurz: die Virenscanner-Treffer galten PyInstallers eigener Kennung).
15+
rem Das Skript prueft seinen Ordner selbst und startet ihn einmal
16+
rem probeweise - dabei geht kurz ein Fenster auf und wieder zu.
1817
python tools\build_exe.py || goto :error
1918
echo.
2019
echo Fertig: dist\S2Tweaker\S2Tweaker.exe

0 commit comments

Comments
 (0)