#!/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 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_WINDOWS = "win-x64" PLATFORM_LINUX = "linux-x64" PLATFORM_LINUX_ARM64 = "linux-arm64" PLATFORM_MACOS = "osx-x64" TARGET_FRAMEWORK = "net9.0" def main() -> None: parser = argparse.ArgumentParser( description="Packages the Robust.Client.WebView module for release on all platforms.") parser.add_argument("--platform", "-p", action="store", choices=[PLATFORM_WINDOWS, PLATFORM_MACOS, PLATFORM_LINUX, PLATFORM_LINUX_ARM64], 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 = args.platform skip_build = args.skip_build if not platforms: platforms = [PLATFORM_WINDOWS, PLATFORM_LINUX] if os.path.exists("release"): print(Fore.BLUE + Style.DIM + "Cleaning old release packages (release/Robust.Client.WebView_*)..." + Style.RESET_ALL) for past in glob.glob("release/Robust.Client.WebView_*"): os.remove(past) else: os.mkdir("release") if PLATFORM_WINDOWS in platforms: if not skip_build: wipe_bin() build_windows(skip_build) if PLATFORM_LINUX in platforms: if not skip_build: wipe_bin() build_linux(skip_build) def wipe_bin(): print(Fore.BLUE + Style.DIM + "Clearing old build artifacts (if any)..." + Style.RESET_ALL) RCWebViewBin = p("Robust.Client.WebView", "bin") if os.path.exists(RCWebViewBin): shutil.rmtree(RCWebViewBin) def build_windows(skip_build: bool) -> None: # Run a full build. print(Fore.GREEN + "Building project for Windows x64..." + Style.RESET_ALL) base_bin = p("Robust.Client.WebView", "bin", "Release", TARGET_FRAMEWORK) if not skip_build: build_client_rid("Windows", "win-x64") build_client("Windows") if sys.platform != "win32": subprocess.run(["Tools/exe_set_subsystem.py", p(base_bin, "Robust.Client.WebView.exe"), "2"]) print(Fore.GREEN + "Packaging win-x64..." + Style.RESET_ALL) client_zip = zipfile.ZipFile( p("release", "Robust.Client.WebView_win-x64.zip"), "w", compression=zipfile.ZIP_DEFLATED) files_to_copy = [ "Robust.Client.WebView.dll", "Robust.Client.WebView.exe", "Robust.Client.WebView.runtimeconfig.json", "Robust.Client.WebView.pdb", "Robust.Client.WebView.deps.json", "SpaceWizards.CefGlue.dll", "SpaceWizards.CefGlue.pdb", # These are copies of regular Robust dlls that Robust.Client.WebView needs when ran on its own. "Robust.Client.dll", "Robust.Shared.dll", "Robust.Shared.Maths.dll", "SharpZstd.Interop.dll", ] for f in files_to_copy: client_zip.write(p(base_bin, "win-x64", f), f) copy_dir_into_zip(p(base_bin, "runtimes", "win-x64", "native"), "", client_zip, { "e_sqlite3.dll", "fluidsynth.dll", "freetype6.dll", "glfw3.dll", "libglib-2.0-0.dll", "libgobject-2.0-0.dll", "libgthread-2.0-0.dll", "libinstpatch-2.dll", "libintl-8.dll", "libsndfile-1.dll", "openal32.dll", "swnfd.dll", "zstd.pdb", "zstd.dll", "zlib1.dll", "libsodium.dll" }) # Cool we're done. client_zip.close() def build_linux(skip_build: bool) -> None: # Run a full build. print(Fore.GREEN + "Building project for Linux x64..." + Style.RESET_ALL) base_bin = p("Robust.Client.WebView", "bin", "Release", TARGET_FRAMEWORK) if not skip_build: build_client_rid("Linux", "linux-x64") build_client("Linux") print(Fore.GREEN + "Packaging linux-x64..." + Style.RESET_ALL) client_zip = zipfile.ZipFile( p("release", "Robust.Client.WebView_linux-x64.zip"), "w", compression=zipfile.ZIP_DEFLATED) files_to_copy = [ "Robust.Client.WebView.dll", "Robust.Client.WebView.runtimeconfig.json", "Robust.Client.WebView.pdb", "Robust.Client.WebView", "Robust.Client.WebView.deps.json", "SpaceWizards.CefGlue.dll", "SpaceWizards.CefGlue.pdb", # These are copies of regular Robust dlls that Robust.Client.WebView needs when ran on its own. "Robust.Client.dll", "Robust.Shared.dll", "Robust.Shared.Maths.dll", "SharpZstd.Interop.dll", ] for f in files_to_copy: client_zip.write(p(base_bin, "linux-x64", f), f) copy_dir_into_zip(p(base_bin, "runtimes", "linux-x64", "native"), "", client_zip, { "libglfw.so.3", "libe_sqlite3.so", "libopenal.so", "libswnfd.so", }) # Cool we're done. client_zip.close() def build_client(target_os: str) -> None: # Running a publish will fold all the natives in the runtime directories into one folder. # This basically nukes the entire setup. # As bypass, we do an all-platform build and then copy the natives manually. base = [ "dotnet", "build", "-c", "Release", f"/p:TargetOS={target_os}", "/p:FullRelease=True", "--no-self-contained", ] subprocess.run(base + ["Robust.Client.WebView/Robust.Client.WebView.csproj"], check=True) def build_client_rid(target_os: str, rid: str) -> None: base = [ "dotnet", "build", "-c", "Release", "-r", rid, f"/p:TargetOS={target_os}", "/p:FullRelease=True", "--no-self-contained", ] subprocess.run(base + ["Robust.Client.WebView/Robust.Client.WebView.csproj"], check=True) 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()