Преглед на файлове

infra: Add distribution files for macOS (#3934)

This upstream macOS packing and distribution files
Mary-nyan преди 3 години
родител
ревизия
817b89767a

+ 46 - 0
distribution/macos/Info.plist

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>CFBundleDevelopmentRegion</key>
+    <string>English</string>
+    <key>CFBundleExecutable</key>
+    <string>Ryujinx</string>
+    <key>CFBundleGetInfoString</key>
+    <string>Ryujinx</string>
+    <key>CFBundleIconFile</key>
+    <string>Ryujinx.icns</string>
+    <key>CFBundleTypeExtensions</key>
+    <array>
+        <string>nca</string>
+        <string>nro</string>
+        <string>nso</string>
+        <string>nsp</string>
+        <string>xci</string>
+    </array>
+    <key>CFBundleIdentifier</key>
+    <string>org.ryujinx.Ryujinx</string>
+    <key>CFBundleInfoDictionaryVersion</key>
+    <string>6.0</string>
+    <key>CFBundleLongVersionString</key>
+    <string>%%RYUJINX_BUILD_VERSION%%-%%RYUJINX_BUILD_GIT_HASH%%"</string>
+    <key>CFBundleName</key>
+    <string>Ryujinx</string>
+    <key>CFBundlePackageType</key>
+    <string>APPL</string>
+    <key>CFBundleShortVersionString</key>
+    <string>1.1</string>
+    <key>CFBundleSignature</key>
+    <string>????</string>
+    <key>CFBundleVersion</key>
+    <string>1.1.0</string>
+    <key>NSHighResolutionCapable</key>
+    <true/>
+    <key>CSResourcesFileMapped</key>
+    <true/>
+    <key>NSHumanReadableCopyright</key>
+    <string>Copyright © 2018 - 2022 Ryujinx Team and Contributors.</string>
+    <key>LSMinimumSystemVersion</key>
+    <string>11.0</string>
+</dict>
+</plist>

BIN
distribution/macos/Ryujinx.icns


+ 609 - 0
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)

+ 95 - 0
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),
+    )

+ 51 - 0
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
+

+ 105 - 0
distribution/macos/create_macos_release.sh

@@ -0,0 +1,105 @@
+#!/bin/bash
+
+set -e
+
+if [ "$#" -ne 6 ]; then
+    echo "usage <BASE_DIR> <TEMP_DIRECTORY> <OUTPUT_DIRECTORY> <ENTITLEMENTS_FILE_PATH> <VERSION> <SOURCE_REVISION_ID>"
+    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"

+ 23 - 0
distribution/macos/entitlements.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
+"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>com.apple.security.cs.allow-jit</key>
+    <true/>
+    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+    <true/>
+    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
+    <true/>
+    <key>com.apple.security.cs.disable-library-validation</key>
+    <true/>
+    <key>com.apple.security.cs.disable-executable-page-protection</key>
+    <true/>
+    <key>com.apple.security.cs.debugger</key>
+    <true/>
+    <key>com.apple.security.get-task-allow</key>
+    <true/>
+    <key>com.apple.security.hypervisor</key>
+    <true/>
+</dict>
+</plist>

+ 24 - 0
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))