fix(packaging): bundle embedded python runtime and dependencies for standalone Windows distribution
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+97
-16
@@ -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,21 +334,8 @@ 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...")
|
||||
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")
|
||||
|
||||
@@ -275,6 +343,7 @@ def build_windows():
|
||||
|
||||
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 = []
|
||||
|
||||
Reference in New Issue
Block a user