483 lines
19 KiB
Python
483 lines
19 KiB
Python
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 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=None,
|
|
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("[*] 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")
|
|
|
|
compile_windows_installers()
|
|
|
|
client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
|
|
server_bin = os.path.join(DIST_DIR, "Server.exe")
|
|
|
|
# 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")
|
|
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")
|
|
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()
|