diff --git a/.gitignore b/.gitignore
index 55c39d405..682680f38 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,3 +95,6 @@ __pycache__
MSBuild/Robust.Custom.targets
.idea/
+
+
+release/
diff --git a/BuildFiles/Mac/Space Station 14.app/Contents/Info.plist b/BuildFiles/Mac/Space Station 14.app/Contents/Info.plist
new file mode 100644
index 000000000..4c1070941
--- /dev/null
+++ b/BuildFiles/Mac/Space Station 14.app/Contents/Info.plist
@@ -0,0 +1,20 @@
+
+
+
+
+ CFBundleName
+ SS14
+ CFBundleDisplayName
+ Space Station 14
+ CFBundleExecutable
+ SS14
+
+ CFBundleIconFile
+ ss14
+
+
diff --git a/BuildFiles/Mac/Space Station 14.app/Contents/MacOS/SS14 b/BuildFiles/Mac/Space Station 14.app/Contents/MacOS/SS14
new file mode 100755
index 000000000..3c47c0b1f
--- /dev/null
+++ b/BuildFiles/Mac/Space Station 14.app/Contents/MacOS/SS14
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+# cd to file containing script or something?
+BASEDIR=$(dirname "$0")
+echo "$BASEDIR"
+cd "$BASEDIR"
+
+exec ../Resources/Robust.Client "$@"
diff --git a/BuildFiles/Mac/Space Station 14.app/Contents/Resources/ss14.icns b/BuildFiles/Mac/Space Station 14.app/Contents/Resources/ss14.icns
new file mode 100644
index 000000000..cba0912b0
Binary files /dev/null and b/BuildFiles/Mac/Space Station 14.app/Contents/Resources/ss14.icns differ
diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj
index e6b595a9f..c32393a69 100644
--- a/Robust.Client/Robust.Client.csproj
+++ b/Robust.Client/Robust.Client.csproj
@@ -4,7 +4,7 @@
false
true
- Exe
+ WinExe
false
../bin/Client
NU1701
diff --git a/Tools/exe_set_subsystem.py b/Tools/exe_set_subsystem.py
new file mode 100755
index 000000000..511a4752d
--- /dev/null
+++ b/Tools/exe_set_subsystem.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+# exe_set_subsystem
+
+# Copyright (c) 2020 20kdc
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import sys
+import struct
+
+if len(sys.argv) != 3:
+ print("exe_set_subsystem.py ")
+ print(" alters EXE in-place to change it's subsystem to SUBSYSTEM")
+ print("")
+ print("SUBSYSTEM values:")
+ print(" 2: GUI")
+ print(" 3: Console")
+ sys.exit(1)
+
+file = open(sys.argv[1], "r+b")
+if file.read(2) != b"MZ":
+ print("Header must be 'MZ'.")
+ sys.exit(2)
+file.seek(0x3C)
+
+peSignatureOfs = struct.unpack(" None:
+ parser = argparse.ArgumentParser(
+ description="Packages the Robust client repo for release on all platforms.")
+ parser.add_argument("--platform",
+ "-p",
+ action="store",
+ choices=[PLATFORM_WINDOWS, PLATFORM_MACOS, PLATFORM_LINUX],
+ 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_MACOS, PLATFORM_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")
+
+ 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)
+
+ if PLATFORM_LINUX_ARM64 in platforms:
+ if not skip_build:
+ wipe_bin()
+ build_linux_arm64(skip_build)
+
+ if PLATFORM_MACOS in platforms:
+ if not skip_build:
+ wipe_bin()
+ build_macos(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(skip_build: bool) -> None:
+ # Run a full build.
+ print(Fore.GREEN + "Building project for Windows x64..." + Style.RESET_ALL)
+
+ if not skip_build:
+ publish_client("win-x64", "Windows")
+ subprocess.run(["Tools/download_natives.py", "x64", "Windows", "_", p("bin", "Client", "win-x64")])
+ if sys.platform != "win32":
+ subprocess.run(["Tools/exe_set_subsystem.py", p("bin", "Client", "win-x64", "publish", "Robust.Client"), "2"])
+
+
+ print(Fore.GREEN + "Packaging Windows x64 client..." + Style.RESET_ALL)
+
+ client_zip = zipfile.ZipFile(
+ p("release", "Robust.Client_win-x64.zip"), "w",
+ compression=zipfile.ZIP_DEFLATED)
+
+ copy_dir_into_zip(p("bin", "Client", "win-x64", "publish"), "", client_zip)
+ copy_resources("Resources", client_zip)
+ # Cool we're done.
+ client_zip.close()
+
+def build_macos(skip_build: bool) -> None:
+ print(Fore.GREEN + "Building project for macOS x64..." + Style.RESET_ALL)
+
+ if not skip_build:
+ publish_client("osx-x64", "MacOS")
+ subprocess.run(["Tools/download_natives.py", "x64", "MacOS", "_", p("bin", "Client", "osx-x64")])
+
+ print(Fore.GREEN + "Packaging macOS x64 client..." + Style.RESET_ALL)
+ # Client has to go in an app bundle.
+ client_zip = zipfile.ZipFile(p("release", "Robust.Client_osx-x64.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", "osx-x64", "publish"), contents, client_zip)
+ copy_resources(p(contents, "Resources"), client_zip)
+ 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)
+
+ if not skip_build:
+ publish_client("linux-x64", "Linux")
+ subprocess.run(["Tools/download_natives.py", "x64", "Linux", "_", p("bin", "Client", "linux-x64")])
+
+ print(Fore.GREEN + "Packaging Linux x64 client..." + Style.RESET_ALL)
+
+ client_zip = zipfile.ZipFile(
+ p("release", "Robust.Client_linux-x64.zip"), "w",
+ compression=zipfile.ZIP_DEFLATED)
+
+ copy_dir_into_zip(p("bin", "Client", "linux-x64", "publish"), "", client_zip)
+ copy_resources("Resources", client_zip)
+ # Cool we're done.
+ client_zip.close()
+
+
+
+def build_linux_arm64(skip_build: bool) -> None:
+ # Run a full build.
+ # TODO: Linux-arm64 is currently server-only.
+ pass
+""" print(Fore.GREEN + "Building project for Linux ARM64 (SERVER ONLY)..." + Style.RESET_ALL)
+
+ if not skip_build:
+ subprocess.run([
+ "dotnet",
+ "build",
+ "SpaceStation14.sln",
+ "-c", "Release",
+ "--nologo",
+ "/v:m",
+ "/p:TargetOS=Linux",
+ "/t:Rebuild",
+ "/p:FullRelease=True"
+ ], check=True)
+
+ publish_client("linux-arm64", "Linux", True)
+
+ print(Fore.GREEN + "Packaging Linux ARM64 server..." + Style.RESET_ALL)
+ server_zip = zipfile.ZipFile(p("release", "SS14.Server_Linux_ARM64.zip"), "w",
+ compression=zipfile.ZIP_DEFLATED)
+ copy_dir_into_zip(p("RobustToolbox", "bin", "Server", "linux-arm64", "publish"), "", server_zip)
+ copy_resources(p("Resources"), server_zip, server=True)
+ server_zip.close()"""
+
+
+def publish_client(runtime: str, target_os: str) -> None:
+ base = [
+ "dotnet", "publish",
+ "--runtime", runtime,
+ "--no-self-contained",
+ "-c", "Release",
+ f"/p:TargetOS={target_os}",
+ "/p:FullRelease=True"
+ ]
+
+ 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):
+ 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_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()