From 3ef48cbdd52b6d50da48ebaf501db73002f8ae4c Mon Sep 17 00:00:00 2001 From: Steveice10 <1269164+Steveice10@users.noreply.github.com> Date: Mon, 22 Jan 2024 23:36:49 -0800 Subject: [PATCH] rpcs3qt: Add macOS support to the updater. --- 3rdparty/7z/CMakeLists.txt | 2 +- Utilities/File.cpp | 69 ++++++++++++++++++++++++++++- Utilities/File.h | 8 ++++ rpcs3/CMakeLists.txt | 3 +- rpcs3/rpcs3qt/main_window.cpp | 4 +- rpcs3/rpcs3qt/update_manager.cpp | 76 ++++++++++++++++++++++++-------- rpcs3/update_helper.sh | 14 ++++++ 7 files changed, 152 insertions(+), 24 deletions(-) create mode 100755 rpcs3/update_helper.sh diff --git a/3rdparty/7z/CMakeLists.txt b/3rdparty/7z/CMakeLists.txt index fb77d6773c..0479923188 100644 --- a/3rdparty/7z/CMakeLists.txt +++ b/3rdparty/7z/CMakeLists.txt @@ -1,5 +1,5 @@ # 7z sdk -if(WIN32) +if(WIN32 OR APPLE) add_library(3rdparty_7z STATIC EXCLUDE_FROM_ALL src/7zAlloc.c src/7zArcIn.c diff --git a/Utilities/File.cpp b/Utilities/File.cpp index 80720e3646..36e762781c 100644 --- a/Utilities/File.cpp +++ b/Utilities/File.cpp @@ -357,6 +357,12 @@ namespace fs return false; } + bool device_base::create_symlink(const std::string&) + { + g_tls_error = error::readonly; + return false; + } + bool device_base::rename(const std::string&, const std::string&) { g_tls_error = error::readonly; @@ -581,6 +587,7 @@ bool fs::get_stat(const std::string& path, stat_t& info) } info.is_directory = (attrs.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + info.is_symlink = (attrs.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; info.is_writable = (attrs.dwFileAttributes & FILE_ATTRIBUTE_READONLY) == 0; info.size = attrs.nFileSizeLow | (u64{attrs.nFileSizeHigh} << 32); info.atime = to_time(attrs.ftLastAccessTime); @@ -595,6 +602,7 @@ bool fs::get_stat(const std::string& path, stat_t& info) } info.is_directory = S_ISDIR(file_info.st_mode); + info.is_symlink = S_ISLNK(file_info.st_mode); info.is_writable = file_info.st_mode & 0200; // HACK: approximation info.size = file_info.st_size; info.atime = file_info.st_atime; @@ -648,6 +656,23 @@ bool fs::is_dir(const std::string& path) return true; } +bool fs::is_symlink(const std::string& path) +{ + fs::stat_t info{}; + if (!fs::get_stat(path, info)) + { + return false; + } + + if (!info.is_symlink) + { + g_tls_error = error::exist; + return false; + } + + return true; +} + bool fs::statfs(const std::string& path, fs::device_stat& info) { if (auto device = get_virtual_device(path)) @@ -794,6 +819,33 @@ bool fs::remove_dir(const std::string& path) return true; } +bool fs::create_symlink(const std::string& path, const std::string& target) +{ + if (auto device = get_virtual_device(path)) + { + return device->create_symlink(path); + } + +#ifdef _WIN32 + const DWORD flags = is_dir(target) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0; + if (!CreateSymbolicLinkW(to_wchar(path).get(), to_wchar(target).get(), flags)) + { + g_tls_error = to_error(GetLastError()); + return false; + } + + return true; +#else + if (::symlink(target.c_str(), path.c_str()) != 0) + { + g_tls_error = to_error(errno); + return false; + } + + return true; +#endif +} + bool fs::rename(const std::string& from, const std::string& to, bool overwrite) { if (from.empty() || to.empty()) @@ -2105,8 +2157,21 @@ const std::string& fs::get_temp_dir() dir = wchar_to_utf8(buf); #else - // TODO - dir = get_cache_dir(); + const char* tmp_dir = getenv("TMPDIR"); + if (tmp_dir == nullptr || tmp_dir[0] == '\0') + { + // Fall back to cache directory + dir = get_cache_dir(); + } + else + { + dir = tmp_dir; + if (!dir.ends_with("/")) + { + // Ensure path ends with a separator + dir += "/"; + } + } #endif return dir; diff --git a/Utilities/File.h b/Utilities/File.h index 07607bea2d..448fb24a4d 100644 --- a/Utilities/File.h +++ b/Utilities/File.h @@ -66,6 +66,7 @@ namespace fs struct stat_t { bool is_directory; + bool is_symlink; bool is_writable; u64 size; s64 atime; @@ -155,6 +156,7 @@ namespace fs virtual bool statfs(const std::string& path, device_stat& info) = 0; virtual bool remove_dir(const std::string& path); virtual bool create_dir(const std::string& path); + virtual bool create_symlink(const std::string& path); virtual bool rename(const std::string& from, const std::string& to); virtual bool remove(const std::string& path); virtual bool trunc(const std::string& path, u64 length); @@ -197,6 +199,9 @@ namespace fs // Check whether the directory exists and is NOT a file bool is_dir(const std::string& path); + // Check whether the path points to an existing symlink + bool is_symlink(const std::string& path); + // Get filesystem information bool statfs(const std::string& path, device_stat& info); @@ -209,6 +214,9 @@ namespace fs // Create directories bool create_path(const std::string& path); + // Create symbolic link + bool create_symlink(const std::string& path, const std::string& target); + // Rename (move) file or directory bool rename(const std::string& from, const std::string& to, bool overwrite); diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index 53429411ca..191993d9d2 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -45,7 +45,8 @@ if(WIN32) target_compile_definitions(rpcs3 PRIVATE UNICODE _UNICODE) elseif(APPLE) add_executable(rpcs3 MACOSX_BUNDLE) - target_sources(rpcs3 PRIVATE rpcs3.icns) + target_sources(rpcs3 PRIVATE rpcs3.icns update_helper.sh) + set_source_files_properties(update_helper.sh PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_target_properties(rpcs3 PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/rpcs3.plist.in") diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 9def9f672e..47d2ced4df 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -241,7 +241,7 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot) } }); -#if defined(_WIN32) || defined(__linux__) +#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__) if (const auto update_value = m_gui_settings->GetValue(gui::m_check_upd_start).toString(); update_value != gui::update_off) { const bool in_background = with_cli_boot || update_value == gui::update_bkg; @@ -3028,7 +3028,7 @@ void main_window::CreateConnects() connect(ui->updateAct, &QAction::triggered, this, [this]() { -#if !defined(_WIN32) && !defined(__linux__) +#if !defined(_WIN32) && !defined(__linux__) && !defined(__APPLE__) QMessageBox::warning(this, tr("Auto-updater"), tr("The auto-updater isn't available for your OS currently.")); return; #endif diff --git a/rpcs3/rpcs3qt/update_manager.cpp b/rpcs3/rpcs3qt/update_manager.cpp index 75f23f7dd0..ffba93a447 100644 --- a/rpcs3/rpcs3qt/update_manager.cpp +++ b/rpcs3/rpcs3qt/update_manager.cpp @@ -20,16 +20,19 @@ #include #include +#if defined(_WIN32) || defined(__APPLE__) +#include <7z.h> +#include <7zAlloc.h> +#include <7zCrc.h> +#include <7zFile.h> +#endif + #if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX #endif #include #include -#include <7z.h> -#include <7zAlloc.h> -#include <7zCrc.h> -#include <7zFile.h> #ifndef PATH_MAX #define PATH_MAX MAX_PATH @@ -143,6 +146,8 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce os = "windows"; #elif defined(__linux__) os = "linux"; +#elif defined(__APPLE__) + os = "mac"; #else update_log.error("Your OS isn't currently supported by the auto-updater"); return false; @@ -370,13 +375,6 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) { m_downloader->update_progress_dialog(tr("Updating RPCS3")); -#ifdef __APPLE__ - Q_UNUSED(data); - Q_UNUSED(auto_accept); - update_log.error("Unsupported operating system."); - return false; -#else - if (m_expected_size != static_cast(data.size())) { update_log.error("Download size mismatch: %d expected: %d", data.size(), m_expected_size); @@ -390,13 +388,17 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) return false; } -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) // Get executable path const std::string exe_dir = fs::get_executable_dir(); - const std::string orig_path = exe_dir + "rpcs3.exe"; + const std::string orig_path = fs::get_executable_path(); +#ifdef _WIN32 const std::wstring wchar_orig_path = utf8_to_wchar(orig_path); const std::string tmpfile_path = fs::get_temp_dir() + "\\rpcs3_update.7z"; +#else + const std::string tmpfile_path = fs::get_temp_dir() + "rpcs3_update.7z"; +#endif fs::file tmpfile(tmpfile_path, fs::read + fs::write + fs::create + fs::trunc); if (!tmpfile) @@ -482,16 +484,31 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) Byte* outBuffer = nullptr; usz outBufferSize = 0; - // Creates temp folder for moving active files +#ifdef _WIN32 + // Create temp folder for moving active files const std::string tmp_folder = exe_dir + "rpcs3_old/"; +#else + // Create temp folder for extracting the new app + const std::string tmp_folder = fs::get_temp_dir() + "rpcs3_new/"; +#endif fs::create_dir(tmp_folder); for (UInt32 i = 0; i < db.NumFiles; i++) { usz offset = 0; usz outSizeProcessed = 0; - const bool isDir = SzArEx_IsDir(&db, i); - const usz len = SzArEx_GetFileNameUtf16(&db, i, nullptr); + const bool isDir = SzArEx_IsDir(&db, i); + const DWORD attribs = SzBitWithVals_Check(&db.Attribs, i) ? db.Attribs.Vals[i] : 0; +#ifdef _WIN32 + // This is commented out for now as we shouldn't need it and symlinks + // aren't well supported on Windows. Left in case it is needed in the future. + // const bool is_symlink = (attribs & FILE_ATTRIBUTE_REPARSE_POINT) != 0; + const bool is_symlink = false; +#else + const DWORD permissions = (attribs >> 16) & (S_IRWXU | S_IRWXG | S_IRWXO); + const bool is_symlink = (attribs & FILE_ATTRIBUTE_UNIX_EXTENSION) != 0 && S_ISLNK(attribs >> 16); +#endif + const usz len = SzArEx_GetFileNameUtf16(&db, i, nullptr); if (len >= PATH_MAX) { @@ -515,7 +532,12 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) temp_u8[index] = static_cast(temp_u16[index]); } temp_u8[len] = 0; - const std::string name = exe_dir + std::string(reinterpret_cast(temp_u8)); + const std::string archived_name = std::string(reinterpret_cast(temp_u8)); +#ifdef __APPLE__ + const std::string name = tmp_folder + archived_name; +#else + const std::string name = exe_dir + archived_name; +#endif if (!isDir) { @@ -537,6 +559,14 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) continue; } + if (is_symlink) + { + const std::string link_target(reinterpret_cast(outBuffer + offset), outSizeProcessed); + update_log.trace("Creating symbolic link: %s -> %s", name, link_target); + fs::create_symlink(name, link_target); + continue; + } + fs::file outfile(name, fs::read + fs::write + fs::create + fs::trunc); if (!outfile) { @@ -575,6 +605,11 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) break; } outfile.close(); + +#ifndef _WIN32 + // Apply correct file permissions. + chmod(name.c_str(), permissions); +#endif } error_free7z(); @@ -644,6 +679,12 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) #ifdef _WIN32 const int ret = _wexecl(wchar_orig_path.data(), wchar_orig_path.data(), L"--updating", nullptr); +#elif defined(__APPLE__) + // Execute helper script to replace the app and relaunch + const std::string helper_script = fmt::format("%s/Contents/Resources/update_helper.sh", orig_path); + const std::string extracted_app = fmt::format("%s/RPCS3.app", tmp_folder); + update_log.notice("Executing update helper script: '%s %s %s'", helper_script, extracted_app, orig_path); + const int ret = execl(helper_script.c_str(), helper_script.c_str(), extracted_app.c_str(), orig_path.c_str(), nullptr); #else // execv is used for compatibility with checkrt const char * const params[3] = { replace_path.c_str(), "--updating", nullptr }; @@ -656,5 +697,4 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) } return true; -#endif //def __APPLE__ } diff --git a/rpcs3/update_helper.sh b/rpcs3/update_helper.sh new file mode 100755 index 0000000000..bc51edebe6 --- /dev/null +++ b/rpcs3/update_helper.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# This script copies the new app over the old app and launches it. +# This is required since invalidating the code signing of an app by +# replacing it while it is running can result in the app being killed. + +if [ "$#" -ne 2 ]; then + echo "Usage: update_helper.sh " + exit 1 +fi +new_app="$1/" +old_app="$2/" + +cp -Rf -p "$new_app" "$old_app" +open -n -a "$2" --args --updating