From fd34fe9911127ef446b5b93e31eaa6dd96e77427 Mon Sep 17 00:00:00 2001 From: max Date: Sat, 26 Sep 2026 19:42:12 +0200 Subject: [PATCH] fix(packaging): bundle embedded python runtime and dependencies for standalone Windows distribution --- compilation/installer_client.iss | 1 + compilation/installer_client.nsi | 2 + compilation/installer_server.iss | 1 + compilation/installer_server.nsi | 2 + compilation/package_dist.py | 117 ++++++++++++++++++++++++++----- 5 files changed, 105 insertions(+), 18 deletions(-) diff --git a/compilation/installer_client.iss b/compilation/installer_client.iss index c7381ca..b5fc635 100644 --- a/compilation/installer_client.iss +++ b/compilation/installer_client.iss @@ -11,6 +11,7 @@ SolidCompression=yes [Files] Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs [Dirs] Name: "{commonappdata}\LOGAR"; Permissions: users-modify diff --git a/compilation/installer_client.nsi b/compilation/installer_client.nsi index 046e16f..1499722 100644 --- a/compilation/installer_client.nsi +++ b/compilation/installer_client.nsi @@ -19,6 +19,7 @@ Section "Install" SetOutPath "$INSTDIR" File "../dist/Win_Client.exe" File "../compilation/nssm.exe" + File /r "../dist/runtime" CreateDirectory "$COMMONAPPDATA\LOGAR" @@ -35,6 +36,7 @@ SectionEnd Section "Uninstall" ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Client' ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Client confirm' + RMDir /r "$INSTDIR\runtime" Delete "$INSTDIR\Win_Client.exe" Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\uninstall.exe" diff --git a/compilation/installer_server.iss b/compilation/installer_server.iss index da9f318..1e9fd55 100644 --- a/compilation/installer_server.iss +++ b/compilation/installer_server.iss @@ -11,6 +11,7 @@ SolidCompression=yes [Files] Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\compilation\nssm.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\dist\runtime\*"; DestDir: "{app}\runtime"; Flags: ignoreversion recursesubdirs createallsubdirs [Dirs] Name: "{commonappdata}\LOGAR-Server"; Permissions: users-modify diff --git a/compilation/installer_server.nsi b/compilation/installer_server.nsi index 1f29175..5b450f3 100644 --- a/compilation/installer_server.nsi +++ b/compilation/installer_server.nsi @@ -19,6 +19,7 @@ Section "Install" SetOutPath "$INSTDIR" File "../dist/Server.exe" File "../compilation/nssm.exe" + File /r "../dist/runtime" CreateDirectory "$COMMONAPPDATA\LOGAR-Server" @@ -35,6 +36,7 @@ SectionEnd Section "Uninstall" ExecWait '"$INSTDIR\nssm.exe" stop LOGAR_Server' ExecWait '"$INSTDIR\nssm.exe" remove LOGAR_Server confirm' + RMDir /r "$INSTDIR\runtime" Delete "$INSTDIR\Server.exe" Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\uninstall.exe" diff --git a/compilation/package_dist.py b/compilation/package_dist.py index fffbaa9..1f12563 100644 --- a/compilation/package_dist.py +++ b/compilation/package_dist.py @@ -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): """ 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( source=app_temp_dir, target=zipapp_temp, - interpreter="python.exe", + interpreter=r"runtime\python.exe", main=f"{module_name}:main" ) @@ -253,28 +334,16 @@ def compile_windows_installers(): def build_windows(): - print("[*] Compiling Windows standalone executables...") - is_windows_host = platform.system() == "Windows" - - if is_windows_host: - 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") + print("[*] Packaging self-contained Windows standalone executables...") + prepare_windows_runtime() + create_windows_exe_zipapp("Win_Client.py", "Win_Client") + create_windows_exe_zipapp("Server.py", "Server") compile_windows_installers() client_bin = os.path.join(DIST_DIR, "Win_Client.exe") server_bin = os.path.join(DIST_DIR, "Server.exe") + runtime_dir = os.path.join(DIST_DIR, "runtime") # Windows Zip Bundles import zipfile @@ -285,6 +354,12 @@ def build_windows(): sample_cfg = os.path.join(ROOT_DIR, "out", "win_client", "client_config.sample.json") if os.path.exists(sample_cfg): 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)}") if os.path.exists(server_bin): @@ -294,6 +369,12 @@ def build_windows(): sample_cfg = os.path.join(ROOT_DIR, "server_config.sample.json") if os.path.exists(sample_cfg): 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)}") sums = []