#!/usr/bin/env python3 # Packages a full release build that can be unzipped and you'll have your SS14 client or server. import os import shutil import subprocess import sys import zipfile import argparse 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 SHARED_IGNORED_RESOURCES = { "ss13model.7z", "ResourcePack.zip", "buildResourcePack.py", "CONTENT_GOES_HERE" } CLIENT_IGNORED_RESOURCES = { "Maps", "emotes.xml" } SERVER_IGNORED_RESOURCES = { "Textures", "Fonts" } GODOT = None def main(): global GODOT parser = argparse.ArgumentParser( description="Packages the SS14 content repo for release on all platforms.") parser.add_argument("--platform", "-p", action="store", choices=["windows", "mac", "linux"], nargs="*", help="Which platform to build for. If not provided, all platforms will be built") parser.add_argument("--godot", action="store", help="Path to the Godot executable used for exporting.") parser.add_argument("--windows-godot-build", action="store") args = parser.parse_args() platforms = args.platform GODOT = args.godot if not GODOT: print("No Godot executable passed.") exit(1) if not platforms: platforms = ["windows", "mac", "linux"] if os.path.exists("release"): print(Fore.BLUE + Style.DIM + "Cleaning old release packages (release/)..." + Style.RESET_ALL) shutil.rmtree("release") os.mkdir("release") copy_godot_scenes() if "windows" in platforms: wipe_bin() if not args.windows_godot_build: print("No --window-godot-build passed") exit(1) build_windows(args.windows_godot_build) if "linux" in platforms: wipe_bin() build_linux() if "mac" in platforms: wipe_bin() build_macos() def wipe_bin(): print(Fore.BLUE + Style.DIM + "Clearing old build artifacts (if any)..." + Style.RESET_ALL) if os.path.exists(p("engine", "bin")): shutil.rmtree(p("engine", "bin")) if os.path.exists("bin"): shutil.rmtree("bin") def build_windows(godot_build): # Run a full build. print(Fore.GREEN + "Building project for Windows amd64..." + Style.RESET_ALL) subprocess.run(["msbuild", "SpaceStation14Content.sln", "/m", "/p:Configuration=Release", "/p:Platform=x64", "/nologo", "/v:m", "/p:TargetOS=Windows", "/t:Rebuild" ], check=True) print(Fore.GREEN + "Packaging Windows amd64 client..." + Style.RESET_ALL) os.makedirs("bin/win_export", exist_ok=True) subprocess.run([GODOT, "--verbose", "--export-debug", "win", "../../bin/win_export/SS14.Client.exe"], cwd="engine/SS14.Client.Godot") client_zip = zipfile.ZipFile(p("release", "SS14.Client_Windows_amd64.zip"), "w", compression=zipfile.ZIP_DEFLATED) # Write the launcher batch script. client_zip.writestr("spess.bat", "cd godot\ncall SS14.Client.exe --path SS14.Client.Godot") # Add a /godot/ directory to the zip and put the pck in there. client_zip.write(p("bin", "win_export"), "godot") client_zip.write(p("bin", "win_export", "SS14.Client.pck"), p("godot", "SS14.Client.pck")) # Write in the other files like mono dlls into the /godot/ directory. copy_dir_into_zip(godot_build, "godot", client_zip) # Copy the main client files into /bin/Client. copy_dir_into_zip(p("engine", "bin", "Client"), p("bin", "Client"), client_zip) # Copy all resources into /bin/Client/Resources. copy_resources(p("bin", "Client", "Resources"), client_zip, server=False) # Cool we're done. client_zip.close() print(Fore.GREEN + "Packaging Windows amd64 server..." + Style.RESET_ALL) server_zip = zipfile.ZipFile(p("release", "SS14.Server_Windows_amd64.zip"), "w", compression=zipfile.ZIP_DEFLATED) copy_dir_into_zip(p("engine", "bin", "Server"), "", server_zip) copy_resources(p("Resources"), server_zip, server=True) server_zip.close() def build_macos(): print(Fore.GREEN + "Building project for macOS amd64..." + Style.RESET_ALL) subprocess.run(["msbuild", "SpaceStation14Content.sln", "/m", "/p:Configuration=Release", "/p:Platform=x64", "/nologo", "/v:m", "/p:TargetOS=MacOS", "/t:Rebuild" ], check=True) print(Fore.GREEN + "Packaging macOS amd64 client..." + Style.RESET_ALL) # Client has to go in an app bundle. subprocess.run([GODOT, "--verbose", "--export-debug", "mac", "../../release/SS14.Client_macOS_amd64.zip"], cwd="engine/SS14.Client.Godot", check=True) client_zip = zipfile.ZipFile(p("release", "SS14.Client_macOS_amd64.zip"), "a", compression=zipfile.ZIP_DEFLATED) contents = p("Space Station 14.app", "Contents") # Copy the main client files into /Space Station 14.app/Contents/bin/Client. # Yes Apple would have me put this shit in /Space Station 14.app/Contents/Resources but screw that. copy_dir_into_zip(p("engine", "bin", "Client"), p(contents, "bin", "Client"), client_zip) # Copy all resources into /Space Station 14.app/Contents/bin/Client/Resources. copy_resources(p(contents, "bin", "Client", "Resources"), client_zip, server=False) client_zip.close() print(Fore.GREEN + "Packaging macOS amd64 server..." + Style.RESET_ALL) server_zip = zipfile.ZipFile(p("release", "SS14.Server_macOS_amd64.zip"), "w", compression=zipfile.ZIP_DEFLATED) copy_dir_into_zip(p("engine", "bin", "Server"), "", server_zip) copy_resources(p("Resources"), server_zip, server=True) server_zip.close() def build_linux(): print(Fore.GREEN + "Building project for Linux amd64..." + Style.RESET_ALL) subprocess.run(["msbuild", "SpaceStation14Content.sln", "/m", "/p:Configuration=Release", "/p:Platform=x64", "/nologo", "/v:m", "/p:TargetOS=Linux", "/t:Rebuild" ], check=True) # NOTE: Temporarily disabled because I can't test it. # Package client. #print(Fore.GREEN + "Packaging Linux amd64 client..." + Style.RESET_ALL) # package_zip(p("bin", "Client"), p( # "release", "SS14.Client_linux_amd64.zip")) print(Fore.GREEN + "Packaging Linux amd64 server..." + Style.RESET_ALL) server_zip = zipfile.ZipFile(p("release", "SS14.Server_Linux_amd64.zip"), "w", compression=zipfile.ZIP_DEFLATED) copy_dir_into_zip(p("engine", "bin", "Server"), "", server_zip) copy_resources(p("Resources"), server_zip, server=True) server_zip.close() def copy_resources(target, zipf, server): # Content repo goes FIRST so that it won't override engine files as that's forbidden. do_resource_copy(target, "Resources", zipf, server) do_resource_copy(target, p("engine", "Resources"), zipf, server) def do_resource_copy(target, source, zipf, server): for filename in os.listdir(source): if filename in SHARED_IGNORED_RESOURCES \ or filename in (SERVER_IGNORED_RESOURCES if server else CLIENT_IGNORED_RESOURCES): 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): 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) 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_godot_scenes(): target_dir = p("engine", "SS14.Client.Godot", "Scenes") from_dir = p("Resources", "Scenes") for path in os.listdir(from_dir): if path.startswith("."): continue frompath = p(from_dir, path) targetpath = p(target_dir, path) if os.path.exists(targetpath): if os.path.isfile(targetpath): os.remove(targetpath) elif os.path.isdir(targetpath): shutil.rmtree(targetpath) else: print("So, what the hell is {} exactly and how did it get there?" .format(targetpath)) exit(1) copy_dir_or_file(frompath, targetpath) def copy_dir_or_file(src, dst): """ 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()