diff --git a/distribution/macos/Info.plist b/distribution/macos/Info.plist
new file mode 100644
index 00000000..c3e1e08e
--- /dev/null
+++ b/distribution/macos/Info.plist
@@ -0,0 +1,46 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ Ryujinx
+ CFBundleGetInfoString
+ Ryujinx
+ CFBundleIconFile
+ Ryujinx.icns
+ CFBundleTypeExtensions
+
+ nca
+ nro
+ nso
+ nsp
+ xci
+
+ CFBundleIdentifier
+ org.ryujinx.Ryujinx
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLongVersionString
+ %%RYUJINX_BUILD_VERSION%%-%%RYUJINX_BUILD_GIT_HASH%%"
+ CFBundleName
+ Ryujinx
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.1
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.1.0
+ NSHighResolutionCapable
+
+ CSResourcesFileMapped
+
+ NSHumanReadableCopyright
+ Copyright © 2018 - 2022 Ryujinx Team and Contributors.
+ LSMinimumSystemVersion
+ 11.0
+
+
diff --git a/distribution/macos/Ryujinx.icns b/distribution/macos/Ryujinx.icns
new file mode 100644
index 00000000..f54a9aeb
Binary files /dev/null and b/distribution/macos/Ryujinx.icns differ
diff --git a/distribution/macos/bundle_fix_up.py b/distribution/macos/bundle_fix_up.py
new file mode 100644
index 00000000..a8e3ac76
--- /dev/null
+++ b/distribution/macos/bundle_fix_up.py
@@ -0,0 +1,609 @@
+import argparse
+import hashlib
+import os
+from pathlib import Path
+import platform
+import shutil
+import struct
+import subprocess
+from typing import List, Optional, Tuple
+
+parser = argparse.ArgumentParser(description="Fixup for MacOS application bundle")
+parser.add_argument("input_directory", help="Input directory (Application path)")
+parser.add_argument("executable_sub_path", help="Main executable sub path")
+
+# Use Apple LLVM on Darwin, otherwise standard LLVM.
+if platform.system() == "Darwin":
+ OTOOL = "otool"
+ INSTALL_NAME_TOOL = "install_name_tool"
+else:
+ OTOOL = shutil.which("llvm-otool")
+ if OTOOL is None:
+ for llvm_ver in [15, 14, 13]:
+ otool_path = shutil.which(f"llvm-otool-{llvm_ver}")
+ if otool_path is not None:
+ OTOOL = otool_path
+ INSTALL_NAME_TOOL = shutil.which(f"llvm-install-name-tool-{llvm_ver}")
+ break
+ else:
+ INSTALL_NAME_TOOL = shutil.which("llvm-install-name-tool")
+
+
+args = parser.parse_args()
+
+
+def get_dylib_id(dylib_path: Path) -> str:
+ res = subprocess.check_output([OTOOL, "-D", str(dylib_path.absolute())]).decode(
+ "utf-8"
+ )
+
+ return res.split("\n")[1]
+
+
+def get_dylib_dependencies(dylib_path: Path) -> List[str]:
+ output = (
+ subprocess.check_output([OTOOL, "-L", str(dylib_path.absolute())])
+ .decode("utf-8")
+ .split("\n")[1:]
+ )
+
+ res = []
+
+ for line in output:
+ line = line.strip()
+ index = line.find(" (compatibility version ")
+ if index == -1:
+ continue
+
+ line = line[:index]
+
+ res.append(line)
+
+ return res
+
+
+def replace_dylib_id(dylib_path: Path, new_id: str):
+ subprocess.check_call(
+ [INSTALL_NAME_TOOL, "-id", new_id, str(dylib_path.absolute())]
+ )
+
+
+def change_dylib_link(dylib_path: Path, old: str, new: str):
+ subprocess.check_call(
+ [INSTALL_NAME_TOOL, "-change", old, new, str(dylib_path.absolute())]
+ )
+
+
+def add_dylib_rpath(dylib_path: Path, rpath: str):
+ subprocess.check_call(
+ [INSTALL_NAME_TOOL, "-add_rpath", rpath, str(dylib_path.absolute())]
+ )
+
+
+def fixup_dylib(
+ dylib_path: Path,
+ replacement_path: str,
+ search_path: List[str],
+ content_directory: Path,
+):
+ dylib_id = get_dylib_id(dylib_path)
+ new_dylib_id = replacement_path + "/" + os.path.basename(dylib_id)
+ replace_dylib_id(dylib_path, new_dylib_id)
+
+ dylib_dependencies = get_dylib_dependencies(dylib_path)
+ dylib_new_mapping = {}
+
+ for dylib_dependency in dylib_dependencies:
+ if (
+ not dylib_dependency.startswith("@executable_path")
+ and not dylib_dependency.startswith("/usr/lib")
+ and not dylib_dependency.startswith("/System/Library")
+ ):
+ dylib_dependency_name = os.path.basename(dylib_dependency)
+ library_found = False
+ for library_base_path in search_path:
+ lib_path = Path(os.path.join(library_base_path, dylib_dependency_name))
+
+ if lib_path.exists():
+ target_replacement_path = get_path_related_to_target_exec(
+ content_directory, lib_path
+ )
+
+ dylib_new_mapping[dylib_dependency] = (
+ target_replacement_path
+ + "/"
+ + os.path.basename(dylib_dependency)
+ )
+ library_found = True
+
+ if not library_found:
+ raise Exception(
+ f"{dylib_id}: Cannot find dependency {dylib_dependency_name} for fixup"
+ )
+
+ for key in dylib_new_mapping:
+ change_dylib_link(dylib_path, key, dylib_new_mapping[key])
+
+
+FILE_TYPE_ASSEMBLY = 1
+
+ALIGN_REQUIREMENTS = 4096
+
+
+def parse_embedded_string(data: bytes) -> Tuple[bytes, str]:
+ first_byte = data[0]
+
+ if (first_byte & 0x80) == 0:
+ size = first_byte
+ data = data[1:]
+ else:
+ second_byte = data[1]
+
+ assert (second_byte & 0x80) == 0
+
+ size = (second_byte << 7) | (first_byte & 0x7F)
+
+ data = data[2:]
+
+ res = data[:size].decode("utf-8")
+ data = data[size:]
+
+ return (data, res)
+
+
+def write_embedded_string(file, string: str):
+ raw_str = string.encode("utf-8")
+ raw_str_len = len(raw_str)
+
+ assert raw_str_len < 0x7FFF
+
+ if raw_str_len > 0x7F:
+ file.write(struct.pack("b", raw_str_len & 0x7F | 0x80))
+ file.write(struct.pack("b", raw_str_len >> 7))
+ else:
+ file.write(struct.pack("b", raw_str_len))
+
+ file.write(raw_str)
+
+
+class BundleFileEntry(object):
+ offset: int
+ size: int
+ compressed_size: int
+ file_type: int
+ relative_path: str
+ data: bytes
+
+ def __init__(
+ self,
+ offset: int,
+ size: int,
+ compressed_size: int,
+ file_type: int,
+ relative_path: str,
+ data: bytes,
+ ) -> None:
+ self.offset = offset
+ self.size = size
+ self.compressed_size = compressed_size
+ self.file_type = file_type
+ self.relative_path = relative_path
+ self.data = data
+
+ def write(self, file):
+ self.offset = file.tell()
+
+ if (
+ self.file_type == FILE_TYPE_ASSEMBLY
+ and (self.offset % ALIGN_REQUIREMENTS) != 0
+ ):
+ padding_size = ALIGN_REQUIREMENTS - (self.offset % ALIGN_REQUIREMENTS)
+ file.write(b"\0" * padding_size)
+ self.offset += padding_size
+
+ file.write(self.data)
+
+ def write_header(self, file):
+ file.write(
+ struct.pack(
+ "QQQb", self.offset, self.size, self.compressed_size, self.file_type
+ )
+ )
+ write_embedded_string(file, self.relative_path)
+
+
+class BundleManifest(object):
+ major: int
+ minor: int
+ bundle_id: str
+ deps_json: BundleFileEntry
+ runtimeconfig_json: BundleFileEntry
+ flags: int
+ files: List[BundleFileEntry]
+
+ def __init__(
+ self,
+ major: int,
+ minor: int,
+ bundle_id: str,
+ deps_json: BundleFileEntry,
+ runtimeconfig_json: BundleFileEntry,
+ flags: int,
+ files: List[BundleFileEntry],
+ ) -> None:
+ self.major = major
+ self.minor = minor
+ self.bundle_id = bundle_id
+ self.deps_json = deps_json
+ self.runtimeconfig_json = runtimeconfig_json
+ self.flags = flags
+ self.files = files
+
+ def write(self, file) -> int:
+ for bundle_file in self.files:
+ bundle_file.write(file)
+
+ bundle_header_offset = file.tell()
+ file.write(struct.pack("iiI", self.major, self.minor, len(self.files)))
+ write_embedded_string(file, self.bundle_id)
+
+ if self.deps_json is not None:
+ deps_json_location_offset = self.deps_json.offset
+ deps_json_location_size = self.deps_json.size
+ else:
+ deps_json_location_offset = 0
+ deps_json_location_size = 0
+
+ if self.runtimeconfig_json is not None:
+ runtimeconfig_json_location_offset = self.runtimeconfig_json.offset
+ runtimeconfig_json_location_size = self.runtimeconfig_json.size
+ else:
+ runtimeconfig_json_location_offset = 0
+ runtimeconfig_json_location_size = 0
+
+ file.write(
+ struct.pack("qq", deps_json_location_offset, deps_json_location_size)
+ )
+ file.write(
+ struct.pack(
+ "qq",
+ runtimeconfig_json_location_offset,
+ runtimeconfig_json_location_size,
+ )
+ )
+ file.write(struct.pack("q", self.flags))
+
+ for bundle_file in self.files:
+ bundle_file.write_header(file)
+
+ return bundle_header_offset
+
+
+def read_file_entry(
+ raw_data: bytes, header_bytes: bytes
+) -> Tuple[bytes, BundleFileEntry]:
+ (
+ offset,
+ size,
+ compressed_size,
+ file_type,
+ ) = struct.unpack("QQQb", header_bytes[:0x19])
+ (header_bytes, relative_path) = parse_embedded_string(header_bytes[0x19:])
+
+ target_size = compressed_size
+
+ if target_size == 0:
+ target_size = size
+
+ return (
+ header_bytes,
+ BundleFileEntry(
+ offset,
+ size,
+ compressed_size,
+ file_type,
+ relative_path,
+ raw_data[offset : offset + target_size],
+ ),
+ )
+
+
+def get_dotnet_bundle_data(data: bytes) -> Optional[Tuple[int, int, BundleManifest]]:
+ offset = data.find(hashlib.sha256(b".net core bundle\n").digest())
+
+ if offset == -1:
+ return None
+
+ raw_header_offset = data[offset - 8 : offset]
+ (header_offset,) = struct.unpack("q", raw_header_offset)
+ header_bytes = data[header_offset:]
+
+ (
+ major,
+ minor,
+ files_count,
+ ) = struct.unpack("iiI", header_bytes[:0xC])
+ header_bytes = header_bytes[0xC:]
+
+ (header_bytes, bundle_id) = parse_embedded_string(header_bytes)
+
+ # v2 header
+ (
+ deps_json_location_offset,
+ deps_json_location_size,
+ ) = struct.unpack("qq", header_bytes[:0x10])
+ (
+ runtimeconfig_json_location_offset,
+ runtimeconfig_json_location_size,
+ ) = struct.unpack("qq", header_bytes[0x10:0x20])
+ (flags,) = struct.unpack("q", header_bytes[0x20:0x28])
+ header_bytes = header_bytes[0x28:]
+
+ files = []
+
+ deps_json = None
+ runtimeconfig_json = None
+
+ for _ in range(files_count):
+ (header_bytes, file_entry) = read_file_entry(data, header_bytes)
+
+ files.append(file_entry)
+
+ if file_entry.offset == deps_json_location_offset:
+ deps_json = file_entry
+ elif file_entry.offset == runtimeconfig_json_location_offset:
+ runtimeconfig_json = file_entry
+
+ file_entry = files[0]
+
+ return (
+ file_entry.offset,
+ header_offset,
+ BundleManifest(
+ major, minor, bundle_id, deps_json, runtimeconfig_json, flags, files
+ ),
+ )
+
+
+LC_SYMTAB = 0x2
+LC_SEGMENT_64 = 0x19
+LC_CODE_SIGNATURE = 0x1D
+
+
+def fixup_linkedit(file, data: bytes, new_size: int):
+ offset = 0
+
+ (
+ macho_magic,
+ macho_cputype,
+ macho_cpusubtype,
+ macho_filetype,
+ macho_ncmds,
+ macho_sizeofcmds,
+ macho_flags,
+ macho_reserved,
+ ) = struct.unpack("IiiIIIII", data[offset : offset + 0x20])
+
+ offset += 0x20
+
+ linkedit_offset = None
+ symtab_offset = None
+ codesign_offset = None
+
+ for _ in range(macho_ncmds):
+ (cmd, cmdsize) = struct.unpack("II", data[offset : offset + 8])
+
+ if cmd == LC_SEGMENT_64:
+ (
+ cmd,
+ cmdsize,
+ segname_raw,
+ vmaddr,
+ vmsize,
+ fileoff,
+ filesize,
+ maxprot,
+ initprot,
+ nsects,
+ flags,
+ ) = struct.unpack("II16sQQQQiiII", data[offset : offset + 72])
+ segname = segname_raw.decode("utf-8").split("\0")[0]
+
+ if segname == "__LINKEDIT":
+ linkedit_offset = offset
+ elif cmd == LC_SYMTAB:
+ symtab_offset = offset
+ elif cmd == LC_CODE_SIGNATURE:
+ codesign_offset = offset
+
+ offset += cmdsize
+ pass
+
+ assert linkedit_offset is not None and symtab_offset is not None
+
+ # If there is a codesign section, clean it up.
+ if codesign_offset is not None:
+ (
+ codesign_cmd,
+ codesign_cmdsize,
+ codesign_dataoff,
+ codesign_datasize,
+ ) = struct.unpack("IIII", data[codesign_offset : codesign_offset + 16])
+ file.seek(codesign_offset)
+ file.write(b"\0" * codesign_cmdsize)
+
+ macho_ncmds -= 1
+ macho_sizeofcmds -= codesign_cmdsize
+ file.seek(0)
+ file.write(
+ struct.pack(
+ "IiiIIIII",
+ macho_magic,
+ macho_cputype,
+ macho_cpusubtype,
+ macho_filetype,
+ macho_ncmds,
+ macho_sizeofcmds,
+ macho_flags,
+ macho_reserved,
+ )
+ )
+
+ file.seek(codesign_dataoff)
+ file.write(b"\0" * codesign_datasize)
+
+ (
+ symtab_cmd,
+ symtab_cmdsize,
+ symtab_symoff,
+ symtab_nsyms,
+ symtab_stroff,
+ symtab_strsize,
+ ) = struct.unpack("IIIIII", data[symtab_offset : symtab_offset + 24])
+
+ symtab_strsize = new_size - symtab_stroff
+
+ new_symtab = struct.pack(
+ "IIIIII",
+ symtab_cmd,
+ symtab_cmdsize,
+ symtab_symoff,
+ symtab_nsyms,
+ symtab_stroff,
+ symtab_strsize,
+ )
+
+ file.seek(symtab_offset)
+ file.write(new_symtab)
+
+ (
+ linkedit_cmd,
+ linkedit_cmdsize,
+ linkedit_segname_raw,
+ linkedit_vmaddr,
+ linkedit_vmsize,
+ linkedit_fileoff,
+ linkedit_filesize,
+ linkedit_maxprot,
+ linkedit_initprot,
+ linkedit_nsects,
+ linkedit_flags,
+ ) = struct.unpack("II16sQQQQiiII", data[linkedit_offset : linkedit_offset + 72])
+
+ linkedit_filesize = new_size - linkedit_fileoff
+ linkedit_vmsize = linkedit_filesize
+
+ new_linkedit = struct.pack(
+ "II16sQQQQiiII",
+ linkedit_cmd,
+ linkedit_cmdsize,
+ linkedit_segname_raw,
+ linkedit_vmaddr,
+ linkedit_vmsize,
+ linkedit_fileoff,
+ linkedit_filesize,
+ linkedit_maxprot,
+ linkedit_initprot,
+ linkedit_nsects,
+ linkedit_flags,
+ )
+ file.seek(linkedit_offset)
+ file.write(new_linkedit)
+
+
+def write_bundle_data(
+ output,
+ old_bundle_base_offset: int,
+ new_bundle_base_offset: int,
+ bundle: BundleManifest,
+) -> int:
+ # Write bundle data
+ bundle_header_offset = bundle.write(output)
+ total_size = output.tell()
+
+ # Patch the header position
+ offset = file_data.find(hashlib.sha256(b".net core bundle\n").digest())
+ output.seek(offset - 8)
+ output.write(struct.pack("q", bundle_header_offset))
+
+ return total_size - new_bundle_base_offset
+
+
+input_directory: Path = Path(args.input_directory)
+content_directory: Path = Path(os.path.join(args.input_directory, "Contents"))
+executable_path: Path = Path(os.path.join(content_directory, args.executable_sub_path))
+
+
+def get_path_related_to_other_path(a: Path, b: Path) -> str:
+ temp = b
+
+ parts = []
+
+ while temp != a:
+ temp = temp.parent
+ parts.append(temp.name)
+
+ parts.remove(parts[-1])
+ parts.reverse()
+
+ return "/".join(parts)
+
+
+def get_path_related_to_target_exec(input_directory: Path, path: Path):
+ return "@executable_path/../" + get_path_related_to_other_path(
+ input_directory, path
+ )
+
+
+search_path = [
+ Path(os.path.join(content_directory, "Frameworks")),
+ Path(os.path.join(content_directory, "Resources/lib")),
+]
+
+
+for path in content_directory.rglob("**/*.dylib"):
+ current_search_path = [path.parent]
+ current_search_path.extend(search_path)
+
+ fixup_dylib(
+ path,
+ get_path_related_to_target_exec(content_directory, path),
+ current_search_path,
+ content_directory,
+ )
+
+for path in content_directory.rglob("**/*.so"):
+ current_search_path = [path.parent]
+ current_search_path.extend(search_path)
+
+ fixup_dylib(
+ path,
+ get_path_related_to_target_exec(content_directory, path),
+ current_search_path,
+ content_directory,
+ )
+
+
+with open(executable_path, "rb") as input:
+ file_data = input.read()
+
+
+(bundle_base_offset, bundle_header_offset, bundle) = get_dotnet_bundle_data(file_data)
+
+add_dylib_rpath(executable_path, "@executable_path/../Frameworks/")
+
+# Recent "vanilla" version of LLVM (LLVM 13 and upper) seems to really dislike how .NET package its assemblies.
+# As a result, after execution of install_name_tool it will have "fixed" the symtab resulting in a missing .NET bundle...
+# To mitigate that, we check if the bundle offset inside the binary is valid after install_name_tool and readd .NET bundle if not.
+output_file_size = os.stat(executable_path).st_size
+if output_file_size < bundle_header_offset:
+ print("LLVM broke the .NET bundle, readding bundle data...")
+ with open(executable_path, "r+b") as output:
+ file_data = output.read()
+ bundle_data_size = write_bundle_data(
+ output, bundle_base_offset, output_file_size, bundle
+ )
+
+ # Now patch the __LINKEDIT section
+ new_size = output_file_size + bundle_data_size
+ fixup_linkedit(output, file_data, new_size)
diff --git a/distribution/macos/construct_universal_dylib.py b/distribution/macos/construct_universal_dylib.py
new file mode 100644
index 00000000..b6c3770c
--- /dev/null
+++ b/distribution/macos/construct_universal_dylib.py
@@ -0,0 +1,95 @@
+import argparse
+import os
+from pathlib import Path
+import platform
+import shutil
+import subprocess
+
+parser = argparse.ArgumentParser(
+ description="Construct Universal dylibs for nuget package"
+)
+parser.add_argument(
+ "arm64_input_directory", help="ARM64 Input directory containing dylibs"
+)
+parser.add_argument(
+ "x86_64_input_directory", help="x86_64 Input directory containing dylibs"
+)
+parser.add_argument("output_directory", help="Output directory")
+parser.add_argument("rglob", help="rglob")
+
+args = parser.parse_args()
+
+# Use Apple LLVM on Darwin, otherwise standard LLVM.
+if platform.system() == "Darwin":
+ LIPO = "lipo"
+else:
+ LIPO = shutil.which("llvm-lipo")
+
+ if LIPO is None:
+ for llvm_ver in [15, 14, 13]:
+ lipo_path = shutil.which(f"llvm-lipo-{llvm_ver}")
+ if lipo_path is not None:
+ LIPO = lipo_path
+ break
+
+if LIPO is None:
+ raise Exception("Cannot find a valid location for LLVM lipo!")
+
+arm64_input_directory: Path = Path(args.arm64_input_directory)
+x86_64_input_directory: Path = Path(args.x86_64_input_directory)
+output_directory: Path = Path(args.output_directory)
+rglob = args.rglob
+
+
+def get_new_name(
+ input_directory: Path, output_directory: str, input_dylib_path: Path
+) -> Path:
+ input_component = str(input_dylib_path).replace(str(input_directory), "")[1:]
+ return Path(os.path.join(output_directory, input_component))
+
+
+def is_fat_file(dylib_path: Path) -> str:
+ res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode(
+ "utf-8"
+ )
+
+ return not res.split("\n")[0].startswith("Non-fat file")
+
+
+def construct_universal_dylib(
+ arm64_input_dylib_path: Path, x86_64_input_dylib_path: Path, output_dylib_path: Path
+):
+ if output_dylib_path.exists() or output_dylib_path.is_symlink():
+ os.remove(output_dylib_path)
+
+ os.makedirs(output_dylib_path.parent, exist_ok=True)
+
+ if arm64_input_dylib_path.is_symlink():
+ os.symlink(
+ os.path.basename(arm64_input_dylib_path.resolve()), output_dylib_path
+ )
+ else:
+ if is_fat_file(arm64_input_dylib_path) or not x86_64_input_dylib_path.exists():
+ with open(output_dylib_path, "wb") as dst:
+ with open(arm64_input_dylib_path, "rb") as src:
+ dst.write(src.read())
+ else:
+ subprocess.check_call(
+ [
+ LIPO,
+ str(arm64_input_dylib_path.absolute()),
+ str(x86_64_input_dylib_path.absolute()),
+ "-output",
+ str(output_dylib_path.absolute()),
+ "-create",
+ ]
+ )
+
+
+print(rglob)
+for path in arm64_input_directory.rglob("**/*.dylib"):
+ construct_universal_dylib(
+ path,
+ get_new_name(arm64_input_directory, x86_64_input_directory, path),
+ get_new_name(arm64_input_directory, output_directory, path),
+ )
diff --git a/distribution/macos/create_app_bundle.sh b/distribution/macos/create_app_bundle.sh
new file mode 100755
index 00000000..8076303c
--- /dev/null
+++ b/distribution/macos/create_app_bundle.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+set -e
+
+PUBLISH_DIRECTORY=$1
+OUTPUT_DIRECTORY=$2
+ENTITLEMENTS_FILE_PATH=$3
+
+APP_BUNDLE_DIRECTORY=$OUTPUT_DIRECTORY/Ryujinx.app
+
+rm -rf $APP_BUNDLE_DIRECTORY
+mkdir -p $APP_BUNDLE_DIRECTORY/Contents
+mkdir $APP_BUNDLE_DIRECTORY/Contents/Frameworks
+mkdir $APP_BUNDLE_DIRECTORY/Contents/MacOS
+mkdir $APP_BUNDLE_DIRECTORY/Contents/Resources
+
+# Copy executables first
+cp $PUBLISH_DIRECTORY/Ryujinx.Ava $APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx
+chmod u+x $APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx
+
+# Then all libraries
+cp $PUBLISH_DIRECTORY/*.dylib $APP_BUNDLE_DIRECTORY/Contents/Frameworks
+
+# Then resources
+cp Info.plist $APP_BUNDLE_DIRECTORY/Contents
+cp Ryujinx.icns $APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns
+cp -r $PUBLISH_DIRECTORY/THIRDPARTY.md $APP_BUNDLE_DIRECTORY/Contents/Resources
+
+echo -n "APPL????" > $APP_BUNDLE_DIRECTORY/Contents/PkgInfo
+
+# Fixup libraries and executable
+python3 bundle_fix_up.py $APP_BUNDLE_DIRECTORY MacOS/Ryujinx
+
+# Now sign it
+if ! [ -x "$(command -v codesign)" ];
+then
+ if ! [ -x "$(command -v rcodesign)" ];
+ then
+ echo "Cannot find rcodesign on your system, please install rcodesign."
+ exit 1
+ fi
+
+ # NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
+ # cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
+ echo "Usign rcodesign for ad-hoc signing"
+ rcodesign sign --entitlements-xml-path $ENTITLEMENTS_FILE_PATH $APP_BUNDLE_DIRECTORY
+else
+ echo "Usign codesign for ad-hoc signing"
+ codesign --entitlements $ENTITLEMENTS_FILE_PATH -f --deep -s - $APP_BUNDLE_DIRECTORY
+fi
+
diff --git a/distribution/macos/create_macos_release.sh b/distribution/macos/create_macos_release.sh
new file mode 100755
index 00000000..545baf20
--- /dev/null
+++ b/distribution/macos/create_macos_release.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+set -e
+
+if [ "$#" -ne 6 ]; then
+ echo "usage "
+ exit 1
+fi
+
+mkdir -p $1
+mkdir -p $2
+mkdir -p $3
+
+BASE_DIR=$(readlink -f $1)
+TEMP_DIRECTORY=$(readlink -f $2)
+OUTPUT_DIRECTORY=$(readlink -f $3)
+ENTITLEMENTS_FILE_PATH=$(readlink -f $4)
+VERSION=$5
+SOURCE_REVISION_ID=$6
+
+RELEASE_TAR_FILE_NAME=Ryujinx-$VERSION-macos_universal.app.tar
+ARM64_APP_BUNDLE=$TEMP_DIRECTORY/output_arm64/Ryujinx.app
+X64_APP_BUNDLE=$TEMP_DIRECTORY/output_x64/Ryujinx.app
+UNIVERSAL_APP_BUNDLE=$OUTPUT_DIRECTORY/Ryujinx.app
+EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
+
+rm -rf $TEMP_DIRECTORY
+mkdir -p $TEMP_DIRECTORY
+
+DOTNET_COMMON_ARGS="-p:DebugType=embedded -p:Version=$VERSION -p:SourceRevisionId=$SOURCE_REVISION_ID -p:ExtraDefineConstants=DISABLE_UPDATER --self-contained true"
+
+dotnet restore
+dotnet build -c Release Ryujinx.Ava
+dotnet publish -c Release -r osx-arm64 -o $TEMP_DIRECTORY/publish_arm64 $DOTNET_COMMON_ARGS Ryujinx.Ava
+dotnet publish -c Release -r osx-x64 -o $TEMP_DIRECTORY/publish_x64 $DOTNET_COMMON_ARGS Ryujinx.Ava
+
+# Get ride of the support library for ARMeilleur for x64 (that's only for arm64)
+rm -rf $TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib
+
+# Get ride of libsoundio from arm64 builds as we don't have a arm64 variant
+# TODO: remove this once done
+rm -rf $TEMP_DIRECTORY/publish_arm64/libsoundio.dylib
+
+pushd $BASE_DIR/distribution/macos
+./create_app_bundle.sh $TEMP_DIRECTORY/publish_x64 $TEMP_DIRECTORY/output_x64 $ENTITLEMENTS_FILE_PATH
+./create_app_bundle.sh $TEMP_DIRECTORY/publish_arm64 $TEMP_DIRECTORY/output_arm64 $ENTITLEMENTS_FILE_PATH
+popd
+
+rm -rf $UNIVERSAL_APP_BUNDLE
+mkdir -p $OUTPUT_DIRECTORY
+
+# Let's copy one of the two different app bundle and remove the executable
+cp -R $ARM64_APP_BUNDLE $UNIVERSAL_APP_BUNDLE
+rm $UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH
+
+# Make it libraries universal
+python3 $BASE_DIR/distribution/macos/construct_universal_dylib.py $ARM64_APP_BUNDLE $X64_APP_BUNDLE $UNIVERSAL_APP_BUNDLE "**/*.dylib"
+
+if ! [ -x "$(command -v lipo)" ];
+then
+ if ! [ -x "$(command -v llvm-lipo-14)" ];
+ then
+ LIPO=llvm-lipo
+ else
+ LIPO=llvm-lipo-14
+ fi
+else
+ LIPO=lipo
+fi
+
+# Make it the executable universal
+$LIPO $ARM64_APP_BUNDLE/$EXECUTABLE_SUB_PATH $X64_APP_BUNDLE/$EXECUTABLE_SUB_PATH -output $UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH -create
+
+# Patch up the Info.plist to have appropriate version
+sed -r -i.bck "s/\%\%RYUJINX_BUILD_VERSION\%\%/$VERSION/g;" $UNIVERSAL_APP_BUNDLE/Contents/Info.plist
+sed -r -i.bck "s/\%\%RYUJINX_BUILD_GIT_HASH\%\%/$SOURCE_REVISION_ID/g;" $UNIVERSAL_APP_BUNDLE/Contents/Info.plist
+rm $UNIVERSAL_APP_BUNDLE/Contents/Info.plist.bck
+
+# Now sign it
+if ! [ -x "$(command -v codesign)" ];
+then
+ if ! [ -x "$(command -v rcodesign)" ];
+ then
+ echo "Cannot find rcodesign on your system, please install rcodesign."
+ exit 1
+ fi
+
+ # NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
+ # cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
+ echo "Usign rcodesign for ad-hoc signing"
+ rcodesign sign --entitlements-xml-path $ENTITLEMENTS_FILE_PATH $UNIVERSAL_APP_BUNDLE
+else
+ echo "Usign codesign for ad-hoc signing"
+ codesign --entitlements $ENTITLEMENTS_FILE_PATH -f --deep -s - $UNIVERSAL_APP_BUNDLE
+fi
+
+echo "Creating archive"
+pushd $OUTPUT_DIRECTORY
+tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf $RELEASE_TAR_FILE_NAME Ryujinx.app 1> /dev/null
+python3 $BASE_DIR/distribution/misc/add_tar_exec.py $RELEASE_TAR_FILE_NAME "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
+gzip -9 < $RELEASE_TAR_FILE_NAME > $RELEASE_TAR_FILE_NAME.gz
+rm $RELEASE_TAR_FILE_NAME
+popd
+
+echo "Done"
diff --git a/distribution/macos/entitlements.xml b/distribution/macos/entitlements.xml
new file mode 100644
index 00000000..bf318507
--- /dev/null
+++ b/distribution/macos/entitlements.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.cs.disable-executable-page-protection
+
+ com.apple.security.cs.debugger
+
+ com.apple.security.get-task-allow
+
+ com.apple.security.hypervisor
+
+
+
diff --git a/distribution/misc/add_tar_exec.py b/distribution/misc/add_tar_exec.py
new file mode 100644
index 00000000..fe659c16
--- /dev/null
+++ b/distribution/misc/add_tar_exec.py
@@ -0,0 +1,24 @@
+import argparse
+from io import BytesIO
+import tarfile
+
+parser = argparse.ArgumentParser(
+ description="Add the main binary to a tar and force it to be executable"
+)
+parser.add_argument("input_tar_file", help="input tar file")
+parser.add_argument("main_binary_path", help="Main executable path")
+parser.add_argument("main_binary_tar_path", help="Main executable tar path")
+
+args = parser.parse_args()
+input_tar_file = args.input_tar_file
+main_binary_path = args.main_binary_path
+main_binary_tar_path = args.main_binary_tar_path
+
+with open(main_binary_path, "rb") as f:
+ with tarfile.open(input_tar_file, "a") as tar:
+ data = f.read()
+ tar_info = tarfile.TarInfo(main_binary_tar_path)
+ tar_info.mode = 0o755
+ tar_info.size = len(data)
+
+ tar.addfile(tar_info, BytesIO(data))