Files
RobustToolbox/Tools/package_client_build.py
CaasGit d579c68082 fix(tools): Fix packaging libs and zips on linux. (#6327)
Both OSX and Linux builds were not excluding two libs from their zip
file due to a missing comma. You can see that they both exist within the
live client manifests.

Also fix the MS dlls having basically no date set on the file which is
throwing an error when trying to zip them.
2025-12-05 16:48:35 +01:00

317 lines
9.0 KiB
Python
Executable File

#!/usr/bin/env python3
# Packages a full release build of the client that can be loaded by the launcher.
# Native libraries are not included.
import os
import shutil
import subprocess
import sys
import zipfile
import argparse
import glob
from enum import StrEnum
from typing import List, Optional
try:
from colorama import init, Fore, Style
init()
except ImportError:
# Just give an empty string for everything, no colored logging.
class ColorDummy(object):
def __getattr__(self, name):
return ""
Fore = ColorDummy()
Style = ColorDummy()
p = os.path.join
PLATFORM_WIN = "win"
PLATFORM_LINUX = "linux"
PLATFORM_OSX = "osx"
PLATFORM_FREEBSD = "freebsd"
TARGET_OS_WINDOWS = "Windows"
TARGET_OS_MACOS = "MacOS"
TARGET_OS_LINUX = "Linux"
TARGET_OS_FREEBSD = "FreeBSD"
class TargetOS(StrEnum):
Windows = "Windows"
MacOS = "MacOS"
Linux = "Linux"
FreeBSD = "FreeBSD"
RID_WIN_X64 = f"{PLATFORM_WIN}-x64"
RID_WIN_ARM64 = f"{PLATFORM_WIN}-arm64"
RID_LINUX_X64 = f"{PLATFORM_LINUX}-x64"
RID_LINUX_ARM64 = f"{PLATFORM_LINUX}-arm64"
RID_OSX_X64 = f"{PLATFORM_OSX}-x64"
RID_OSX_ARM64 = f"{PLATFORM_OSX}-arm64"
RID_FREEBSD_X64 = f"{PLATFORM_FREEBSD}-x64"
RID_FREEBSD_ARM64 = f"{PLATFORM_FREEBSD}-arm64"
DEFAULT_RIDS = [RID_WIN_X64, RID_WIN_ARM64, RID_LINUX_X64, RID_LINUX_ARM64, RID_OSX_X64, RID_OSX_ARM64, RID_FREEBSD_X64, RID_FREEBSD_ARM64]
ALL_RIDS = [RID_WIN_X64, RID_WIN_ARM64, RID_LINUX_X64, RID_LINUX_ARM64, RID_OSX_X64, RID_OSX_ARM64, RID_FREEBSD_X64, RID_FREEBSD_ARM64]
IGNORED_RESOURCES = {
".gitignore",
".directory",
".DS_Store"
}
IGNORED_FILES_WINDOWS = {
"libGLESv2.dll",
"openal32.dll",
"e_sqlite3.dll",
"libsndfile-1.dll",
"libglib-2.0-0.dll",
"freetype6.dll",
"libinstpatch-2.dll",
"fluidsynth.dll",
"libgobject-2.0-0.dll",
"libintl-8.dll",
"Robust.Client.exe",
"glfw3.dll",
"libgthread-2.0-0.dll",
"libEGL.dll",
"swnfd.dll",
"zstd.dll",
"zstd.pdb",
"libsodium.dll",
"zlib1.dll",
"SDL3.dll",
"OpenAL32.dll",
"libfluidsynth-3.dll"
}
IGNORED_FILES_MACOS = {
"libe_sqlite3.dylib",
"libfreetype.6.dylib",
"libglfw.3.dylib",
"Robust.Client",
"libswnfd.dylib",
"zstd.dylib",
"libsodium.dylib",
"libopenal.1.dylib",
"libSDL3.0.dylib",
"libfluidsynth.3.dylib",
"libzstd.1.dylib"
}
IGNORED_FILES_LINUX = {
"libe_sqlite3.so",
"libopenal.so",
"libglfw.so.3",
"Robust.Client",
"libswnfd.so",
"zstd.so",
"libsodium.so",
"libopenal.so.1",
"libSDL3.so.0",
"libfluidsynth.so.3",
"libzstd.so.1"
}
def main() -> None:
parser = argparse.ArgumentParser(
description="Packages the Robust client repo for release on all platforms.")
parser.add_argument("--platform",
"-p",
action="store",
choices=ALL_RIDS,
nargs="*",
help="Which platform to build for. If not provided, all platforms will be built")
parser.add_argument("--skip-build",
action="store_true",
help=argparse.SUPPRESS)
args = parser.parse_args()
platforms: list[str] = args.platform
skip_build: bool = args.skip_build
if not platforms:
platforms = DEFAULT_RIDS
# Validate that nobody put invalid platform names in.
for rid in platforms:
if rid not in ALL_RIDS:
print(Fore.RED + f"Invalid platform specified: '{rid}'" + Style.RESET_ALL)
exit(1)
if os.path.exists("release"):
print(Fore.BLUE + Style.DIM +
"Cleaning old release packages (release/Robust.Client_*)..." + Style.RESET_ALL)
for past in glob.glob("release/Robust.Client_*"):
os.remove(past)
else:
os.mkdir("release")
for platform in platforms:
build_for_platform(platform, skip_build)
def build_for_platform(rid: str, skip_build: bool):
print(Fore.GREEN + f"Building for platform '{rid}'..." + Style.RESET_ALL)
if not skip_build:
wipe_bin()
platform = rid.split('-', maxsplit=2)[0]
if platform == PLATFORM_WIN:
build_windows(rid, skip_build)
elif platform == PLATFORM_LINUX:
build_linux_like(rid, TargetOS.Linux, skip_build)
elif platform == PLATFORM_OSX:
build_macos(rid, skip_build)
elif platform == PLATFORM_FREEBSD:
build_linux_like(rid, TargetOS.FreeBSD, skip_build)
def wipe_bin():
print(Fore.BLUE + Style.DIM +
"Clearing old build artifacts (if any)..." + Style.RESET_ALL)
if os.path.exists("bin"):
shutil.rmtree("bin")
def build_windows(rid: str, skip_build: bool) -> None:
if not skip_build:
publish_client(rid, TargetOS.Windows)
if sys.platform != "win32":
subprocess.run(["Tools/exe_set_subsystem.py", p("bin", "Client", rid, "publish", "Robust.Client"), "2"])
print(Fore.GREEN + f"Packaging {rid} client..." + Style.RESET_ALL)
client_zip = zipfile.ZipFile(
p("release", f"Robust.Client_{rid}.zip"), "w",
compression=zipfile.ZIP_DEFLATED)
copy_dir_into_zip(p("bin", "Client", rid, "publish"), "", client_zip, IGNORED_FILES_WINDOWS)
copy_resources("Resources", client_zip)
# Cool we're done.
client_zip.close()
def build_macos(rid: str, skip_build: bool) -> None:
if not skip_build:
publish_client(rid, TargetOS.MacOS)
print(Fore.GREEN + f"Packaging {rid} client..." + Style.RESET_ALL)
# Client has to go in an app bundle.
client_zip = zipfile.ZipFile(p("release", f"Robust.Client_{rid}.zip"), "a",
compression=zipfile.ZIP_DEFLATED)
contents = p("Space Station 14.app", "Contents", "Resources")
copy_dir_into_zip(p("BuildFiles", "Mac", "Space Station 14.app"), "Space Station 14.app", client_zip)
copy_dir_into_zip(p("bin", "Client", rid, "publish"), contents, client_zip, IGNORED_FILES_MACOS)
copy_resources(p(contents, "Resources"), client_zip)
client_zip.close()
def build_linux_like(rid: str, target_os: TargetOS, skip_build: bool) -> None:
if not skip_build:
publish_client(rid, target_os)
print(Fore.GREEN + "Packaging %s client..." % rid + Style.RESET_ALL)
client_zip = zipfile.ZipFile(
p("release", "Robust.Client_%s.zip" % rid), "w",
compression=zipfile.ZIP_DEFLATED, strict_timestamps=False)
copy_dir_into_zip(p("bin", "Client", rid, "publish"), "", client_zip, IGNORED_FILES_LINUX)
copy_resources("Resources", client_zip)
# Cool we're done.
client_zip.close()
def publish_client(runtime: str, target_os: TargetOS) -> None:
base = [
"dotnet", "publish",
"--runtime", runtime,
"--no-self-contained",
"-c", "Release",
f"/p:TargetOS={target_os}",
"/p:FullRelease=True",
"/p:UseAppHost=False"
]
subprocess.run(base + ["Robust.Client/Robust.Client.csproj"], check=True)
def copy_resources(target, zipf):
do_resource_copy(target, "Resources", zipf, IGNORED_RESOURCES)
def do_resource_copy(target, source, zipf, ignore_set):
for filename in os.listdir(source):
if filename in ignore_set:
continue
path = p(source, filename)
target_path = p(target, filename)
if os.path.isdir(path):
copy_dir_into_zip(path, target_path, zipf)
else:
zipf.write(path, target_path)
def zip_entry_exists(zipf, name):
try:
# Trick ZipInfo into sanitizing the name for us so this awful module stops spewing warnings.
zinfo = zipfile.ZipInfo.from_file("Resources", name)
zipf.getinfo(zinfo.filename)
except KeyError:
return False
return True
def copy_dir_into_zip(directory, basepath, zipf, ignored={}):
if basepath and not zip_entry_exists(zipf, basepath):
zipf.write(directory, basepath)
for root, _, files in os.walk(directory):
relpath = os.path.relpath(root, directory)
if relpath != "." and not zip_entry_exists(zipf, p(basepath, relpath)):
zipf.write(root, p(basepath, relpath))
for filename in files:
zippath = p(basepath, relpath, filename)
filepath = p(root, filename)
if filename in ignored:
continue
message = "{dim}{diskroot}{sep}{zipfile}{dim} -> {ziproot}{sep}{zipfile}".format(
sep=os.sep + Style.NORMAL,
dim=Style.DIM,
diskroot=directory,
ziproot=zipf.filename,
zipfile=os.path.normpath(zippath))
print(Fore.CYAN + message + Style.RESET_ALL)
zipf.write(filepath, zippath)
def copy_dir_or_file(src: str, dst: str):
"""
Just something from src to dst. If src is a dir it gets copied recursively.
"""
if os.path.isfile(src):
shutil.copy2(src, dst)
elif os.path.isdir(src):
shutil.copytree(src, dst)
else:
raise IOError("{} is neither file nor directory. Can't copy.".format(src))
if __name__ == '__main__':
main()