ci(release): cross-compile and publish both Linux and Windows release binaries from Linux node
Release Binaries & Packages / Build & Release Standalone Binaries (Linux & Windows Cross-Compilation) (push) Canceled after 0s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m32s

This commit is contained in:
2026-09-25 23:24:29 +02:00
parent 19c60616b1
commit 344d57ff49
3 changed files with 170 additions and 152 deletions
+155 -22
View File
@@ -12,6 +12,7 @@ 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)
@@ -20,6 +21,22 @@ def clean_and_prep():
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 = [
@@ -38,25 +55,15 @@ def build_pyinstaller_binary(script_path, binary_name):
raise RuntimeError(f"Failed to build {binary_name}")
print(f"[+] Successfully compiled {binary_name}")
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_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)
shutil.copy(os.path.join(SRC_DIR, "Linux_Client.py"), os.path.join(app_dir, "Linux_Client.py"))
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,
@@ -67,7 +74,9 @@ def build_linux_zipapp_fallback():
# Server zipapp
srv_dir = os.path.join(BUILD_TEMP, "linux_srv")
os.makedirs(srv_dir, exist_ok=True)
shutil.copy(os.path.join(SRC_DIR, "Server.py"), os.path.join(srv_dir, "Server.py"))
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,
@@ -76,13 +85,109 @@ def build_linux_zipapp_fallback():
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 compile_inno_setup_installers():
"""Compiles Inno Setup installers if iscc or innosetup wrapper is available."""
iscc_cmd = None
if shutil.which("iscc"):
iscc_cmd = ["iscc"]
elif shutil.which("innosetup"):
iscc_cmd = ["innosetup"]
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"):
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 build_windows():
print("[*] Compiling Windows standalone executables...")
win_client_script = os.path.join(SRC_DIR, "Win_Client.py")
build_pyinstaller_binary(win_client_script, "Win_Client")
is_windows_host = platform.system() == "Windows"
server_script = os.path.join(SRC_DIR, "Server.py")
build_pyinstaller_binary(server_script, "Server")
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_inno_setup_installers()
client_bin = os.path.join(DIST_DIR, "Win_Client.exe")
server_bin = os.path.join(DIST_DIR, "Server.exe")
@@ -103,10 +208,18 @@ def build_windows():
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"]:
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...")
is_linux_host = platform.system() == "Linux"
@@ -164,12 +277,29 @@ def build_linux():
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=["windows", "win", "linux", "auto"],
choices=["all", "windows", "win", "linux", "auto"],
default="auto",
help="Target platform to compile binaries for (default: auto-detect)"
)
@@ -177,19 +307,21 @@ def main(target=None):
target = args.target
if target == "auto":
target = "windows" if platform.system() == "Windows" else "linux"
target = "all"
elif target == "win":
target = "windows"
print("=" * 60)
print(" LOGAR Binary Packaging")
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 == "windows":
if target == "all":
build_all()
elif target == "windows":
build_windows()
elif target == "linux":
build_linux()
@@ -206,5 +338,6 @@ def main(target=None):
print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)")
print("=" * 60)
if __name__ == "__main__":
main()
+1
View File
@@ -5,4 +5,5 @@ fastapi>=0.110.0
uvicorn>=0.28.0
websockets>=11.0.0
pydantic>=2.6.0
distlib>=0.3.8
pywin32>=306; sys_platform == "win32"