Configure packaging and release workflow to ship exclusively compiled binaries
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
name: Release Shippables
|
name: Release Binaries
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -18,11 +18,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
- name: Assemble Shippables
|
- name: Install Build Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pyinstaller -r requirements.txt
|
||||||
|
|
||||||
|
- name: Compile Standalone Binaries
|
||||||
run: |
|
run: |
|
||||||
python package_dist.py
|
python package_dist.py
|
||||||
|
|
||||||
- name: Publish Release
|
- name: Publish Release (Binaries Only)
|
||||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
|||||||
+81
-37
@@ -1,35 +1,63 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
import tarfile
|
import zipapp
|
||||||
import zipfile
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
|
||||||
DIST_DIR = "dist"
|
DIST_DIR = os.path.abspath("dist")
|
||||||
OUT_DIR = "out"
|
OUT_DIR = os.path.abspath("out")
|
||||||
|
BUILD_TEMP = os.path.abspath("build_temp")
|
||||||
|
|
||||||
def make_zip(source_dir, output_zip):
|
def clean_and_prep():
|
||||||
with zipfile.ZipFile(output_zip, "w", zipfile.ZIP_DEFLATED) as zf:
|
if os.path.exists(DIST_DIR):
|
||||||
for root, _, files in os.walk(source_dir):
|
shutil.rmtree(DIST_DIR)
|
||||||
for file in files:
|
os.makedirs(DIST_DIR, exist_ok=True)
|
||||||
full_path = os.path.join(root, file)
|
if os.path.exists(BUILD_TEMP):
|
||||||
rel_path = os.path.relpath(full_path, source_dir)
|
shutil.rmtree(BUILD_TEMP)
|
||||||
zf.write(full_path, rel_path)
|
os.makedirs(BUILD_TEMP, exist_ok=True)
|
||||||
|
|
||||||
def make_tar_gz(source_dir, output_tar):
|
def build_pyinstaller_binary(script_path, binary_name):
|
||||||
with tarfile.open(output_tar, "w:gz") as tf:
|
print(f"[*] Compiling {binary_name} with PyInstaller...")
|
||||||
for root, _, files in os.walk(source_dir):
|
cmd = [
|
||||||
for file in files:
|
sys.executable, "-m", "PyInstaller",
|
||||||
full_path = os.path.join(root, file)
|
"--onefile",
|
||||||
rel_path = os.path.relpath(full_path, source_dir)
|
"--clean",
|
||||||
tf.add(full_path, arcname=rel_path)
|
"--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 generate_checksums(dist_dir):
|
def build_linux_zipapp_binary():
|
||||||
checksum_file = os.path.join(dist_dir, "SHA256SUMS.txt")
|
print("[*] Packaging Linux_Client.bin executable binary...")
|
||||||
|
app_dir = os.path.join(BUILD_TEMP, "linux_app")
|
||||||
|
os.makedirs(app_dir, exist_ok=True)
|
||||||
|
shutil.copy(os.path.join(OUT_DIR, "linux_client", "Linux_Client.py"), os.path.join(app_dir, "Linux_Client.py"))
|
||||||
|
|
||||||
|
bin_output = os.path.join(DIST_DIR, "Linux_Client.bin")
|
||||||
|
zipapp.create_archive(
|
||||||
|
source=app_dir,
|
||||||
|
target=bin_output,
|
||||||
|
interpreter="/usr/bin/env python3",
|
||||||
|
main="Linux_Client:main"
|
||||||
|
)
|
||||||
|
print(f"[+] Successfully generated {bin_output}")
|
||||||
|
|
||||||
|
def generate_checksums():
|
||||||
|
checksum_file = os.path.join(DIST_DIR, "SHA256SUMS.txt")
|
||||||
lines = []
|
lines = []
|
||||||
for fname in sorted(os.listdir(dist_dir)):
|
for fname in sorted(os.listdir(DIST_DIR)):
|
||||||
if fname == "SHA256SUMS.txt":
|
if fname == "SHA256SUMS.txt":
|
||||||
continue
|
continue
|
||||||
fpath = os.path.join(dist_dir, fname)
|
fpath = os.path.join(DIST_DIR, fname)
|
||||||
if os.path.isfile(fpath):
|
if os.path.isfile(fpath):
|
||||||
with open(fpath, "rb") as f:
|
with open(fpath, "rb") as f:
|
||||||
digest = hashlib.sha256(f.read()).hexdigest()
|
digest = hashlib.sha256(f.read()).hexdigest()
|
||||||
@@ -38,29 +66,45 @@ def generate_checksums(dist_dir):
|
|||||||
f.write("\n".join(lines) + "\n")
|
f.write("\n".join(lines) + "\n")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.makedirs(DIST_DIR, exist_ok=True)
|
print("=" * 60)
|
||||||
|
print(" LOGAR Binary Packaging (Binaries Only)")
|
||||||
|
print(f" Platform: {platform.system()} ({platform.machine()})")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
server_src = os.path.join(OUT_DIR, "server")
|
clean_and_prep()
|
||||||
win_src = os.path.join(OUT_DIR, "win_client")
|
|
||||||
linux_src = os.path.join(OUT_DIR, "linux_client")
|
|
||||||
|
|
||||||
print("[*] Packaging Server shippable...")
|
is_windows = platform.system() == "Windows"
|
||||||
make_tar_gz(server_src, os.path.join(DIST_DIR, "logar-server.tar.gz"))
|
|
||||||
make_zip(server_src, os.path.join(DIST_DIR, "logar-server.zip"))
|
|
||||||
|
|
||||||
print("[*] Packaging Windows Client shippable...")
|
if is_windows:
|
||||||
make_zip(win_src, os.path.join(DIST_DIR, "logar-win-client.zip"))
|
# Build Windows client executable
|
||||||
|
win_client_script = os.path.join(OUT_DIR, "win_client", "Win_Client.py")
|
||||||
|
build_pyinstaller_binary(win_client_script, "Win_Client")
|
||||||
|
|
||||||
print("[*] Packaging Linux Client shippable...")
|
# Build Windows server executable
|
||||||
make_tar_gz(linux_src, os.path.join(DIST_DIR, "logar-linux-client.tar.gz"))
|
server_script = os.path.join(OUT_DIR, "server", "Server.py")
|
||||||
|
build_pyinstaller_binary(server_script, "Server")
|
||||||
|
|
||||||
print("[*] Generating SHA-256 checksums...")
|
# Build Linux client standalone binary
|
||||||
generate_checksums(DIST_DIR)
|
build_linux_zipapp_binary()
|
||||||
|
else:
|
||||||
|
# On Linux runner: Build native Linux binaries
|
||||||
|
linux_client_script = os.path.join(OUT_DIR, "linux_client", "Linux_Client.py")
|
||||||
|
build_pyinstaller_binary(linux_client_script, "Linux_Client.bin")
|
||||||
|
|
||||||
print(f"[+] All release artifacts assembled in '{DIST_DIR}/':")
|
server_script = os.path.join(OUT_DIR, "server", "Server.py")
|
||||||
|
build_pyinstaller_binary(server_script, "Server.bin")
|
||||||
|
|
||||||
|
generate_checksums()
|
||||||
|
|
||||||
|
# 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 os.listdir(DIST_DIR):
|
for f in os.listdir(DIST_DIR):
|
||||||
sz = os.path.getsize(os.path.join(DIST_DIR, f))
|
sz = os.path.getsize(os.path.join(DIST_DIR, f))
|
||||||
print(f" - {f} ({sz} bytes)")
|
print(f" - {f} ({sz / (1024*1024):.2f} MB)" if sz > 1024*1024 else f" - {f} ({sz} bytes)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import mimetypes
|
||||||
|
import package_dist
|
||||||
|
|
||||||
|
DEFAULT_GITEA_URL = "https://gitea.eibl.tech"
|
||||||
|
DEFAULT_REPO = "me0nline/LOGAR"
|
||||||
|
|
||||||
|
def upload_file_to_release(base_url, repo, release_id, token, file_path):
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets?name={urllib.parse.quote(filename)}"
|
||||||
|
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
file_bytes = f.read()
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, data=file_bytes, method="POST")
|
||||||
|
req.add_header("Authorization", f"token {token}")
|
||||||
|
req.add_header("Content-Type", "application/octet-stream")
|
||||||
|
req.add_header("Accept", "application/json")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
data = json.loads(resp.read().decode("utf-8"))
|
||||||
|
print(f"[+] Attached {filename} ({len(file_bytes)} bytes) to release.")
|
||||||
|
return data
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
err = e.read().decode("utf-8", errors="ignore")
|
||||||
|
print(f"[!] Error uploading {filename}: HTTP {e.code} - {err}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_or_get_release(base_url, repo, tag, token, title=None, notes=None):
|
||||||
|
url = f"{base_url}/api/v1/repos/{repo}/releases"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if release exists
|
||||||
|
check_url = f"{base_url}/api/v1/repos/{repo}/releases/tags/{urllib.parse.quote(tag)}"
|
||||||
|
check_req = urllib.request.Request(check_url, headers={"Authorization": f"token {token}"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(check_req) as resp:
|
||||||
|
existing = json.loads(resp.read().decode("utf-8"))
|
||||||
|
print(f"[*] Found existing release for tag {tag} (ID: {existing['id']})")
|
||||||
|
return existing["id"]
|
||||||
|
except urllib.error.HTTPError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create new release
|
||||||
|
payload = {
|
||||||
|
"tag_name": tag,
|
||||||
|
"name": title or f"LOGAR Release {tag}",
|
||||||
|
"body": notes or f"Automated binary release for {tag}.",
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": False
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST")
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
created = json.loads(resp.read().decode("utf-8"))
|
||||||
|
print(f"[+] Created release {tag} (ID: {created['id']})")
|
||||||
|
return created["id"]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Upload LOGAR compiled binaries directly to Gitea Release")
|
||||||
|
parser.add_argument("--tag", required=True, help="Release tag name (e.g. v1.0.0)")
|
||||||
|
parser.add_argument("--token", required=True, help="Gitea Personal Access Token")
|
||||||
|
parser.add_argument("--url", default=DEFAULT_GITEA_URL, help="Base Gitea instance URL")
|
||||||
|
parser.add_argument("--repo", default=DEFAULT_REPO, help="Repository owner/name")
|
||||||
|
parser.add_argument("--skip-build", action="store_true", help="Skip running package_dist.py before upload")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.skip_build:
|
||||||
|
print("[*] Assembling compiled binaries...")
|
||||||
|
package_dist.main()
|
||||||
|
|
||||||
|
dist_dir = os.path.abspath("dist")
|
||||||
|
if not os.path.exists(dist_dir) or not os.listdir(dist_dir):
|
||||||
|
print("[!] No binaries found in dist/. Run package_dist.py first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"[*] Connecting to Gitea: {args.url} (repo: {args.repo})...")
|
||||||
|
release_id = create_or_get_release(args.url, args.repo, args.tag, args.token)
|
||||||
|
|
||||||
|
print(f"[*] Uploading binary assets from '{dist_dir}'...")
|
||||||
|
for f in sorted(os.listdir(dist_dir)):
|
||||||
|
fpath = os.path.join(dist_dir, f)
|
||||||
|
if os.path.isfile(fpath):
|
||||||
|
upload_file_to_release(args.url, args.repo, release_id, args.token, fpath)
|
||||||
|
|
||||||
|
print(f"\n[+] Release successfully published with binary assets: {args.url}/{args.repo}/releases/tag/{args.tag}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user