fix(packaging): bundle embedded python runtime and dependencies for standalone Windows distribution
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m9s
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Successful in 7m5s

This commit is contained in:
2026-09-26 19:42:12 +02:00
parent a7ff1c845d
commit fd34fe9911
5 changed files with 105 additions and 18 deletions
+1
View File
@@ -11,6 +11,7 @@ SolidCompression=yes
[Files] [Files]
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
[Dirs] [Dirs]
Name: "{commonappdata}\LOGAR"; Permissions: users-modify Name: "{commonappdata}\LOGAR"; Permissions: users-modify
+2
View File
@@ -19,6 +19,7 @@ Section "Install"
SetOutPath "$INSTDIR" SetOutPath "$INSTDIR"
File "../dist/Win_Client.exe" File "../dist/Win_Client.exe"
File "../compilation/nssm.exe" File "../compilation/nssm.exe"
File /r "../dist/runtime"
CreateDirectory "$COMMONAPPDATA\LOGAR" CreateDirectory "$COMMONAPPDATA\LOGAR"
@@ -35,6 +36,7 @@ SectionEnd
Section "Uninstall" Section "Uninstall"
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client' ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client'
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm' ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm'
RMDir /r "$INSTDIR\runtime"
Delete "$INSTDIR\Win_Client.exe" Delete "$INSTDIR\Win_Client.exe"
Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\nssm.exe"
Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\uninstall.exe"
+1
View File
@@ -11,6 +11,7 @@ SolidCompression=yes
[Files] [Files]
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs
[Dirs] [Dirs]
Name: "{commonappdata}\LOGAR-Server"; Permissions: users-modify Name: "{commonappdata}\LOGAR-Server"; Permissions: users-modify
+2
View File
@@ -19,6 +19,7 @@ Section "Install"
SetOutPath "$INSTDIR" SetOutPath "$INSTDIR"
File "../dist/Server.exe" File "../dist/Server.exe"
File "../compilation/nssm.exe" File "../compilation/nssm.exe"
File /r "../dist/runtime"
CreateDirectory "$COMMONAPPDATA\LOGAR-Server" CreateDirectory "$COMMONAPPDATA\LOGAR-Server"
@@ -35,6 +36,7 @@ SectionEnd
Section "Uninstall" Section "Uninstall"
ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server' ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server'
ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm' ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm'
RMDir /r "$INSTDIR\runtime"
Delete "$INSTDIR\Server.exe" Delete "$INSTDIR\Server.exe"
Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\nssm.exe"
Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\uninstall.exe"
+99 -18
View File
@@ -86,6 +86,87 @@ def build_linux_zipapp_fallback():
) )
def prepare_windows_runtime():
"""
Downloads the official Python 3.11 embeddable 64-bit runtime, configures ._pth,
and downloads/unpacks all required dependencies (wheels) for standalone execution.
"""
runtime_dir = os.path.join(DIST_DIR, "runtime")
if os.path.exists(runtime_dir):
return runtime_dir
os.makedirs(runtime_dir, exist_ok=True)
site_packages = os.path.join(runtime_dir, "site-packages")
os.makedirs(site_packages, exist_ok=True)
# 1. Download & Extract Python 3.11 embeddable zip
embed_zip_name = "python-3.11.9-embed-amd64.zip"
embed_zip_path = os.path.join(BUILD_TEMP, embed_zip_name)
embed_url = f"https://www.python.org/ftp/python/3.11.9/{embed_zip_name}"
if not os.path.exists(embed_zip_path):
print(f"[*] Downloading Python 3.11 embeddable runtime from {embed_url}...")
import urllib.request
urllib.request.urlretrieve(embed_url, embed_zip_path)
import zipfile
with zipfile.ZipFile(embed_zip_path, "r") as zf:
zf.extractall(runtime_dir)
# 2. Configure python311._pth
pth_file = os.path.join(runtime_dir, "python311._pth")
with open(pth_file, "w", encoding="utf-8") as f:
f.write("python311.zip\n.\nsite-packages\nsite-packages/win32\nsite-packages/win32/lib\nsite-packages/Pythonwin\nimport site\n")
# 3. Download and unpack Windows AMD64 dependencies
wheels_dir = os.path.join(BUILD_TEMP, "win_wheels")
os.makedirs(wheels_dir, exist_ok=True)
print("[*] Fetching required Windows x64 dependency wheels...")
# Build PGPy 0.6.0 wheel from git (supports modern cryptography)
try:
subprocess.run([
sys.executable, "-m", "pip", "wheel", "--no-deps",
"git+https://github.com/SecurityInnovation/PGPy.git",
"--wheel-dir", wheels_dir
], check=False, capture_output=True)
except Exception:
pass
# Download binary wheels for win_amd64 cp311
deps = [
"cryptography", "fastapi", "uvicorn", "pydantic",
"pywin32", "websockets", "pyasn1", "six", "cffi", "pycparser"
]
pip_cmd = [
sys.executable, "-m", "pip", "download",
"--only-binary=:all:",
"--platform", "win_amd64",
"--python-version", "311",
"--implementation", "cp",
"--abi", "cp311",
"--dest", wheels_dir
] + deps
subprocess.run(pip_cmd, check=False, capture_output=True)
for whl in os.listdir(wheels_dir):
if whl.endswith(".whl"):
whl_path = os.path.join(wheels_dir, whl)
with zipfile.ZipFile(whl_path, "r") as zf:
zf.extractall(site_packages)
# 4. Copy pywin32 system DLLs to runtime root for immediate DLL loader resolution
sys32_dir = os.path.join(site_packages, "pywin32_system32")
if os.path.exists(sys32_dir):
for dll in os.listdir(sys32_dir):
if dll.lower().endswith(".dll"):
shutil.copy(os.path.join(sys32_dir, dll), runtime_dir)
print("[+] Successfully assembled standalone Windows Python 3.11 runtime environment.")
return runtime_dir
def create_windows_exe_zipapp(script_name, exe_name): def create_windows_exe_zipapp(script_name, exe_name):
""" """
Cross-compilation helper: Creates a standalone Windows .exe by prepending Cross-compilation helper: Creates a standalone Windows .exe by prepending
@@ -103,7 +184,7 @@ def create_windows_exe_zipapp(script_name, exe_name):
zipapp.create_archive( zipapp.create_archive(
source=app_temp_dir, source=app_temp_dir,
target=zipapp_temp, target=zipapp_temp,
interpreter="python.exe", interpreter=r"runtime\python.exe",
main=f"{module_name}:main" main=f"{module_name}:main"
) )
@@ -253,28 +334,16 @@ def compile_windows_installers():
def build_windows(): def build_windows():
print("[*] Compiling Windows standalone executables...") print("[*] Packaging self-contained Windows standalone executables...")
is_windows_host = platform.system() == "Windows" prepare_windows_runtime()
create_windows_exe_zipapp("Win_Client.py", "Win_Client")
if is_windows_host: create_windows_exe_zipapp("Server.py", "Server")
win_client_script = os.path.join(SRC_DIR, "Win_Client.py")
server_script = os.path.join(SRC_DIR, "Server.py")
try:
build_pyinstaller_binary(win_client_script, "Win_Client")
build_pyinstaller_binary(server_script, "Server")
except Exception as e:
print(f"[!] PyInstaller on Windows failed ({e}). Falling back to PE launcher stub packaging...")
create_windows_exe_zipapp("Win_Client.py", "Win_Client")
create_windows_exe_zipapp("Server.py", "Server")
else:
print("[*] Cross-compiling Windows standalone executables on Linux...")
create_windows_exe_zipapp("Win_Client.py", "Win_Client")
create_windows_exe_zipapp("Server.py", "Server")
compile_windows_installers() compile_windows_installers()
client_bin = os.path.join(DIST_DIR, "Win_Client.exe") client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
server_bin = os.path.join(DIST_DIR, "Server.exe") server_bin = os.path.join(DIST_DIR, "Server.exe")
runtime_dir = os.path.join(DIST_DIR, "runtime")
# Windows Zip Bundles # Windows Zip Bundles
import zipfile import zipfile
@@ -285,6 +354,12 @@ def build_windows():
sample_cfg = os.path.join(ROOT_DIR, "out", "win_client", "client_config.sample.json") sample_cfg = os.path.join(ROOT_DIR, "out", "win_client", "client_config.sample.json")
if os.path.exists(sample_cfg): if os.path.exists(sample_cfg):
zf.write(sample_cfg, arcname="client_config.sample.json") zf.write(sample_cfg, arcname="client_config.sample.json")
if os.path.exists(runtime_dir):
for root, _, files in os.walk(runtime_dir):
for file in files:
full_p = os.path.join(root, file)
rel_p = os.path.relpath(full_p, DIST_DIR)
zf.write(full_p, arcname=rel_p)
print(f"[+] Packaged Windows Client Zip: {os.path.basename(client_zip)}") print(f"[+] Packaged Windows Client Zip: {os.path.basename(client_zip)}")
if os.path.exists(server_bin): if os.path.exists(server_bin):
@@ -294,6 +369,12 @@ def build_windows():
sample_cfg = os.path.join(ROOT_DIR, "server_config.sample.json") sample_cfg = os.path.join(ROOT_DIR, "server_config.sample.json")
if os.path.exists(sample_cfg): if os.path.exists(sample_cfg):
zf.write(sample_cfg, arcname="server_config.sample.json") zf.write(sample_cfg, arcname="server_config.sample.json")
if os.path.exists(runtime_dir):
for root, _, files in os.walk(runtime_dir):
for file in files:
full_p = os.path.join(root, file)
rel_p = os.path.relpath(full_p, DIST_DIR)
zf.write(full_p, arcname=rel_p)
print(f"[+] Packaged Windows Server Zip: {os.path.basename(server_zip)}") print(f"[+] Packaged Windows Server Zip: {os.path.basename(server_zip)}")
sums = [] sums = []