import os import sys import shutil import zipapp import hashlib import platform import subprocess import argparse ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) SRC_DIR = os.path.join(ROOT_DIR, "src") DIST_DIR = os.path.join(ROOT_DIR, "dist") BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp") def clean_and_prep(): if os.path.exists(DIST_DIR): shutil.rmtree(DIST_DIR) os.makedirs(DIST_DIR, exist_ok=True) if os.path.exists(BUILD_TEMP): shutil.rmtree(BUILD_TEMP) os.makedirs(BUILD_TEMP, exist_ok=True) def calculate_sha256(filepath): h = hashlib.sha256() with open(filepath, "rb") as f: while chunk := f.read(65536): h.update(chunk) return h.hexdigest() def write_checksum_file(filename, digest, binary_filename): out_path = os.path.join(DIST_DIR, filename) with open(out_path, "w", encoding="utf-8") as f: f.write(f"{digest} {binary_filename}\n") print(f"[+] Generated checksum file: {filename} ({digest[:16]}...)") def build_pyinstaller_binary(script_path, binary_name): print(f"[*] Compiling {binary_name} with PyInstaller...") cmd = [ sys.executable, "-m", "PyInstaller", "--onefile", "--clean", "--distpath", DIST_DIR, "--workpath", os.path.join(BUILD_TEMP, f"work_{binary_name}"), "--specpath", os.path.join(BUILD_TEMP, f"spec_{binary_name}"), "--name", binary_name, script_path ] res = subprocess.run(cmd, capture_output=True, text=True) if res.returncode != 0: print(f"[!] Compilation error for {binary_name}:\n{res.stderr}") raise RuntimeError(f"Failed to build {binary_name}") print(f"[+] Successfully compiled {binary_name}") def build_linux_zipapp_fallback(): print("[*] Packaging Linux standalone zipapp fallback binaries...") # Linux Client zipapp app_dir = os.path.join(BUILD_TEMP, "linux_app") os.makedirs(app_dir, exist_ok=True) for f in os.listdir(SRC_DIR): if f.endswith(".py"): shutil.copy(os.path.join(SRC_DIR, f), os.path.join(app_dir, f)) client_out = os.path.join(DIST_DIR, "Linux_Client.bin") zipapp.create_archive( source=app_dir, target=client_out, interpreter="/usr/bin/env python3", main="Linux_Client:main" ) # Server zipapp srv_dir = os.path.join(BUILD_TEMP, "linux_srv") os.makedirs(srv_dir, exist_ok=True) for f in os.listdir(SRC_DIR): if f.endswith(".py"): shutil.copy(os.path.join(SRC_DIR, f), os.path.join(srv_dir, f)) server_out = os.path.join(DIST_DIR, "Server.bin") zipapp.create_archive( source=srv_dir, target=server_out, interpreter="/usr/bin/env python3", main="Server:main" ) 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 the standard Python Windows 64-bit PE launcher stub (t64.exe) to a compiled zipapp archive. """ app_temp_dir = os.path.join(BUILD_TEMP, f"win_app_{exe_name}") os.makedirs(app_temp_dir, exist_ok=True) for f in os.listdir(SRC_DIR): if f.endswith(".py"): shutil.copy(os.path.join(SRC_DIR, f), os.path.join(app_temp_dir, f)) module_name = os.path.splitext(script_name)[0] zipapp_temp = os.path.join(BUILD_TEMP, f"{exe_name}.zip") zipapp.create_archive( source=app_temp_dir, target=zipapp_temp, interpreter=r"runtime\python.exe", main=f"{module_name}:main" ) stub_bytes = b"" try: import pip._vendor.distlib.resources as distlib_res finder = distlib_res.finder("pip._vendor.distlib") res = finder.find("t64.exe") if res: stub_bytes = res.bytes except Exception: pass if not stub_bytes: try: import distlib.resources as distlib_res finder = distlib_res.finder("distlib") res = finder.find("t64.exe") if res: stub_bytes = res.bytes except Exception: pass out_exe_path = os.path.join(DIST_DIR, f"{exe_name}.exe") with open(out_exe_path, "wb") as out_f: if stub_bytes: out_f.write(stub_bytes) with open(zipapp_temp, "rb") as in_f: out_f.write(in_f.read()) print(f"[+] Successfully cross-compiled Windows PE executable: {exe_name}.exe (PE Stub attached: {bool(stub_bytes)})") def ensure_makensis(): """Finds or attempts to install makensis on the current system.""" for cmd in ["makensis", "/usr/bin/makensis", "/usr/local/bin/makensis", r"C:\Program Files (x86)\NSIS\makensis.exe", r"C:\Program Files\NSIS\makensis.exe"]: p = shutil.which(cmd) if not os.path.isabs(cmd) else cmd if p and (os.path.exists(p) or shutil.which(p)): return p if platform.system() == "Linux": print("[*] Attempting to install 'nsis' package dynamically...") sudo = ["sudo"] if hasattr(os, "geteuid") and os.geteuid() != 0 and shutil.which("sudo") else [] if shutil.which("apt-get"): try: subprocess.run(sudo + ["apt-get", "update", "--allow-releaseinfo-change", "-o", "Acquire::Check-Valid-Until=false", "-y"], check=False) subprocess.run(sudo + ["apt-get", "-f", "install", "-y"], check=False) subprocess.run(sudo + ["apt-get", "install", "-y", "--fix-broken", "--fix-missing", "--no-install-recommends", "nsis", "nsis-common"], check=False) except Exception as e: print(f"[!] Warning: Dynamic apt install failed: {e}") elif shutil.which("apk"): try: subprocess.run(sudo + ["apk", "add", "--no-cache", "nsis"], check=False) except Exception: pass elif shutil.which("dnf"): try: subprocess.run(sudo + ["dnf", "install", "-y", "mingw32-nsis", "nsis"], check=False) except Exception: pass for cmd in ["makensis", "/usr/bin/makensis", "/usr/local/bin/makensis"]: p = shutil.which(cmd) if not os.path.isabs(cmd) else cmd if p and (os.path.exists(p) or shutil.which(p)): return p return None def ensure_libpython(): """Attempts to install libpython shared library on Debian/Ubuntu for PyInstaller.""" if platform.system() == "Linux" and shutil.which("apt-get"): py_ver = f"{sys.version_info.major}.{sys.version_info.minor}" sudo = ["sudo"] if hasattr(os, "geteuid") and os.geteuid() != 0 and shutil.which("sudo") else [] try: subprocess.run(sudo + ["apt-get", "update", "--allow-releaseinfo-change", "-o", "Acquire::Check-Valid-Until=false", "-y"], check=False) subprocess.run(sudo + ["apt-get", "-f", "install", "-y"], check=False) subprocess.run(sudo + ["apt-get", "install", "-y", "--fix-broken", "--fix-missing", "--no-install-recommends", f"libpython{py_ver}", "python3-dev", f"python{py_ver}-dev", "libpython3-dev"], check=False) except Exception: pass def compile_nsis_installers(): """Compiles NSIS installers if makensis is available (native Linux/Windows).""" makensis_bin = ensure_makensis() if not makensis_bin: print("[!] Note: NSIS compiler (makensis) not found in PATH or standard directories.") return False makensis_cmd = [makensis_bin] compilation_dir = os.path.join(ROOT_DIR, "compilation") success = True for nsi in ["installer_client.nsi", "installer_server.nsi"]: nsi_path = os.path.join(compilation_dir, nsi) if os.path.exists(nsi_path): print(f"[*] Compiling NSIS script: {nsi} via {makensis_cmd[0]}...") try: subprocess.run(makensis_cmd + [nsi_path], cwd=compilation_dir, check=True) print(f"[+] Successfully compiled NSIS installer for {nsi}") except Exception as e: print(f"[!] Warning: Failed to compile {nsi} with {makensis_cmd}: {e}") success = False return success def compile_inno_setup_installers(): """Compiles Inno Setup installers if iscc or innosetup wrapper is available.""" iscc_cmd = None user_iscc = os.path.expanduser(r"~\AppData\Local\Programs\Inno Setup 6\ISCC.exe") if shutil.which("iscc"): iscc_cmd = ["iscc"] elif shutil.which("innosetup"): iscc_cmd = ["innosetup"] elif os.path.exists(user_iscc): iscc_cmd = [user_iscc] elif os.path.exists("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"): iscc_cmd = ["C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"] elif os.path.exists("C:\\Program Files\\Inno Setup 6\\ISCC.exe"): iscc_cmd = ["C:\\Program Files\\Inno Setup 6\\ISCC.exe"] elif shutil.which("wine") and os.path.exists("/opt/innosetup/app/ISCC.exe"): iscc_cmd = ["wine", "/opt/innosetup/app/ISCC.exe"] elif shutil.which("wine"): wine_iscc = os.path.expanduser("~/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe") if os.path.exists(wine_iscc): iscc_cmd = ["wine", wine_iscc] if not iscc_cmd: print("[!] Note: Inno Setup compiler (iscc / innosetup) not available on host. Skipping installer .exe creation.") return False for iss in ["installer_client.iss", "installer_server.iss"]: iss_path = os.path.join(ROOT_DIR, "compilation", iss) if os.path.exists(iss_path): print(f"[*] Compiling Inno Setup script: {iss}...") try: subprocess.run(iscc_cmd + [iss_path], check=True) print(f"[+] Successfully compiled {iss}") except Exception as e: print(f"[!] Warning: Failed to compile {iss} with {iscc_cmd}: {e}") return True def compile_windows_installers(): if not compile_nsis_installers(): compile_inno_setup_installers() def build_windows(): 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 if os.path.exists(client_bin): client_zip = os.path.join(DIST_DIR, "LOGAR-Windows-Client.zip") with zipfile.ZipFile(client_zip, "w", zipfile.ZIP_DEFLATED) as zf: zf.write(client_bin, arcname="Win_Client.exe") 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): server_zip = os.path.join(DIST_DIR, "LOGAR-Windows-Server.zip") with zipfile.ZipFile(server_zip, "w", zipfile.ZIP_DEFLATED) as zf: zf.write(server_bin, arcname="Server.exe") 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 = [] if os.path.exists(client_bin): client_hash = calculate_sha256(client_bin) write_checksum_file("win_client_sha256sum", client_hash, "Win_Client.exe") write_checksum_file("win_agent_sha256sum", client_hash, "Win_Client.exe") sums.append(f"{client_hash} Win_Client.exe") else: print(f"[!] Warning: Expected {client_bin} was not found.") if os.path.exists(server_bin): server_hash = calculate_sha256(server_bin) write_checksum_file("win_server_sha256sum", server_hash, "Server.exe") sums.append(f"{server_hash} Server.exe") else: print(f"[!] Warning: Expected {server_bin} was not found.") # Inno Setup output checksums if present for inst in ["LOGAR-Client-Setup.exe", "LOGAR-Server-Setup.exe", "LOGAR-Windows-Client.zip", "LOGAR-Windows-Server.zip"]: inst_path = os.path.join(DIST_DIR, inst) if os.path.exists(inst_path): inst_hash = calculate_sha256(inst_path) sums.append(f"{inst_hash} {inst}") sums_path = os.path.join(DIST_DIR, "SHA256SUMS_windows.txt") with open(sums_path, "w", encoding="utf-8") as f: f.write("\n".join(sums) + "\n") def build_linux(): print("[*] Compiling Linux standalone binaries and packaging installers...") is_linux_host = platform.system() == "Linux" if is_linux_host: ensure_libpython() try: linux_client_script = os.path.join(SRC_DIR, "Linux_Client.py") build_pyinstaller_binary(linux_client_script, "Linux_Client.bin") server_script = os.path.join(SRC_DIR, "Server.py") build_pyinstaller_binary(server_script, "Server.bin") except Exception as e: print(f"[!] PyInstaller compilation encountered an issue ({e}). Falling back to standalone zipapp packaging...") build_linux_zipapp_fallback() # Normalize extensions in case PyInstaller dropped .bin for name in ["Linux_Client", "Server"]: plain_path = os.path.join(DIST_DIR, name) bin_path = os.path.join(DIST_DIR, f"{name}.bin") if os.path.exists(plain_path) and not os.path.exists(bin_path): os.rename(plain_path, bin_path) # Ensure executable permissions on Linux for b in ["Linux_Client.bin", "Server.bin"]: p = os.path.join(DIST_DIR, b) if os.path.exists(p): try: os.chmod(p, 0o755) except Exception: pass else: print("[!] Note: Host platform is not Linux. Generating executable zipapps for Linux target.") build_linux_zipapp_fallback() # Copy Linux installation scripts directly to dist for sh in ["install_linux_client.sh", "install_linux_server.sh"]: sh_src = os.path.join(ROOT_DIR, "compilation", sh) if os.path.exists(sh_src): sh_dst = os.path.join(DIST_DIR, sh) shutil.copy(sh_src, sh_dst) print(f"[+] Copied Linux installer script: {sh}") # Package Linux tarballs import tarfile client_bin = os.path.join(DIST_DIR, "Linux_Client.bin") server_bin = os.path.join(DIST_DIR, "Server.bin") if os.path.exists(client_bin): client_tar = os.path.join(DIST_DIR, "LOGAR-Linux-Client.tar.gz") with tarfile.open(client_tar, "w:gz") as tf: tf.add(client_bin, arcname="Linux_Client.bin") client_sh = os.path.join(DIST_DIR, "install_linux_client.sh") if os.path.exists(client_sh): tf.add(client_sh, arcname="install_linux_client.sh") print(f"[+] Packaged Linux Client Tarball: {os.path.basename(client_tar)}") if os.path.exists(server_bin): server_tar = os.path.join(DIST_DIR, "LOGAR-Linux-Server.tar.gz") with tarfile.open(server_tar, "w:gz") as tf: tf.add(server_bin, arcname="Server.bin") server_sh = os.path.join(DIST_DIR, "install_linux_server.sh") if os.path.exists(server_sh): tf.add(server_sh, arcname="install_linux_server.sh") print(f"[+] Packaged Linux Server Tarball: {os.path.basename(server_tar)}") sums = [] if os.path.exists(client_bin): client_hash = calculate_sha256(client_bin) write_checksum_file("linux_client_sha256sum", client_hash, "Linux_Client.bin") write_checksum_file("linux_agent_sha256sum", client_hash, "Linux_Client.bin") sums.append(f"{client_hash} Linux_Client.bin") else: print(f"[!] Warning: Expected {client_bin} was not found.") if os.path.exists(server_bin): server_hash = calculate_sha256(server_bin) write_checksum_file("linux_server_sha256sum", server_hash, "Server.bin") sums.append(f"{server_hash} Server.bin") else: print(f"[!] Warning: Expected {server_bin} was not found.") for item in ["install_linux_client.sh", "install_linux_server.sh", "LOGAR-Linux-Client.tar.gz", "LOGAR-Linux-Server.tar.gz"]: item_path = os.path.join(DIST_DIR, item) if os.path.exists(item_path): item_hash = calculate_sha256(item_path) sums.append(f"{item_hash} {item}") sums_path = os.path.join(DIST_DIR, "SHA256SUMS_linux.txt") with open(sums_path, "w", encoding="utf-8") as f: f.write("\n".join(sums) + "\n") def build_all(): print("[*] Building standalone distribution artifacts for both Linux and Windows...") build_linux() build_windows() # Consolidated master checksums file all_sums = [] for f in sorted(os.listdir(DIST_DIR)): fpath = os.path.join(DIST_DIR, f) if os.path.isfile(fpath) and not f.endswith(".txt") and not "sha256sum" in f: h = calculate_sha256(fpath) all_sums.append(f"{h} {f}") with open(os.path.join(DIST_DIR, "SHA256SUMS.txt"), "w", encoding="utf-8") as f: f.write("\n".join(all_sums) + "\n") def main(target=None): if target is None: parser = argparse.ArgumentParser(description="LOGAR Standalone Binary Compiler & Packager") parser.add_argument( "--target", "-t", choices=["all", "windows", "win", "linux", "auto"], default="auto", help="Target platform to compile binaries for (default: auto-detect)" ) args, _ = parser.parse_known_args() target = args.target if target == "auto": target = "all" elif target == "win": target = "windows" print("=" * 60) print(" LOGAR Binary Packaging & Cross-Compilation") print(f" Host Platform: {platform.system()} ({platform.machine()})") print(f" Target Platform: {target.upper()}") print("=" * 60) clean_and_prep() if target == "all": build_all() elif target == "windows": build_windows() elif target == "linux": build_linux() else: raise ValueError(f"Unsupported target: {target}") # Clean temporary build directory if os.path.exists(BUILD_TEMP): shutil.rmtree(BUILD_TEMP, ignore_errors=True) print("\n[+] Binary shipping artifacts assembled in 'dist/':") for f in sorted(os.listdir(DIST_DIR)): sz = os.path.getsize(os.path.join(DIST_DIR, f)) print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)") print("=" * 60) if __name__ == "__main__": main()