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))