Files
imhex/main/updater/source/main.cpp
2025-12-21 11:47:21 +01:00

336 lines
14 KiB
C++

#include <hex/api_urls.hpp>
#include <hex/api/imhex_api/system.hpp>
#include <hex/api/task_manager.hpp>
#include <hex/helpers/http_requests.hpp>
#include <hex/helpers/utils.hpp>
#include <hex/helpers/default_paths.hpp>
#include <hex/helpers/logger.hpp>
#include <hex/helpers/fs.hpp>
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
#include <optional>
using namespace std::literals::string_literals;
std::string getArtifactUrl(std::string_view artifactEnding, hex::ImHexApi::System::UpdateType updateType) {
// Get the latest version info from the ImHex API
const auto response = hex::HttpRequest("GET",
GitHubApiURL + "/releases"s
).execute().get();
const auto &data = response.getData();
// Make sure we got a valid response
if (!response.isSuccess()) {
hex::log::error("Failed to get latest version info: ({}) {}", response.getStatusCode(), data);
return { };
}
try {
const auto json = nlohmann::json::parse(data);
for (const auto &release : json) {
if (updateType == hex::ImHexApi::System::UpdateType::Stable && !release["target_commitish"].get<std::string>().starts_with("releases/v"))
continue;
if (updateType == hex::ImHexApi::System::UpdateType::Nightly && release["tag_name"].get<std::string>() != "nightly")
continue;
// Loop over all assets in the release
for (const auto &asset : release["assets"]) {
// Check if the asset name ends with the specified artifact ending
if (asset["name"].get<std::string>().ends_with(artifactEnding)) {
return asset["browser_download_url"].get<std::string>();
}
}
}
hex::log::error("No suitable artifact found for ending: {}", artifactEnding);
return { };
} catch (const std::exception &e) {
hex::log::error("Failed to parse latest version info: {}", e.what());
return { };
}
}
std::optional<std::fs::path> downloadUpdate(const std::string &url) {
// Download the update
const auto response = hex::HttpRequest("GET", url).downloadFile().get();
// Make sure we got a valid response
if (!response.isSuccess()) {
hex::log::error("Failed to download update");
return std::nullopt;
}
const auto &data = response.getData();
const auto updateFileName = wolv::util::splitString(url, "/").back();
// Write the update to a file
std::fs::path filePath;
{
// Loop over all available paths
wolv::io::File file;
for (const auto &path : hex::paths::Updates.write()) {
// Remove any existing update files
wolv::io::fs::remove(path / updateFileName);
// If a valid location hasn't been found already, try to create a new file
if (!file.isValid())
file = wolv::io::File(path / updateFileName, wolv::io::File::Mode::Create);
}
// If the update data can't be written to any of the default paths, the update cannot continue
if (!file.isValid()) {
hex::log::error("Failed to create update file");
return std::nullopt;
}
hex::log::info("Writing update to file: {}", file.getPath().string());
// Write the downloaded update data to the file
file.writeVector(data);
// Save the path to the update file
filePath = file.getPath();
}
return filePath;
}
#if defined(__x86_64__) || defined(_X86_) || defined(_AMD64_) || defined(_M_X64)
#define ARCH_DEPENDENT(x86_64, arm64) x86_64
#elif defined(__arm64__) || defined(_ARM64_) || defined(__aarch64__) || defined(_M_ARM64)
#define ARCH_DEPENDENT(x86_64, arm64) arm64
#else
#error "Unsupported architecture for updater"
#endif
std::string_view getUpdateArtifactEnding() {
#if defined (OS_WINDOWS)
if (!hex::ImHexApi::System::isPortableVersion()) {
return ARCH_DEPENDENT("Windows-x86_64.msi", "Windows-arm64.msi");
}
#elif defined (OS_MACOS)
return ARCH_DEPENDENT("macOS-x86_64.dmg", "macOS-arm64.dmg");
#elif defined (OS_LINUX)
if (hex::getEnvironmentVariable("APPIMAGE").has_value()) {
return ARCH_DEPENDENT("x86_64.AppImage", "arm64.AppImage");
} else if (hex::getEnvironmentVariable("FLATPAK_BINARY").has_value()) {
return ARCH_DEPENDENT("x86_64.flatpak", "arm64.flatpak");
} else if (hex::getEnvironmentVariable("SNAP").has_value()) {
return ARCH_DEPENDENT("x86_64.snap", "arm64.snap");
} else if (hex::executeCommand("grep -q 'ID=ubuntu' /etc/os-release") == 0) {
if (hex::executeCommand("grep 'VERSION_ID=\"24.04\"' /etc/os-release") == 0)
return ARCH_DEPENDENT("Ubuntu-24.04-x86_64.deb", "");
else if (hex::executeCommand("grep -q 'VERSION_ID=\"25.04\"' /etc/os-release") == 0)
return ARCH_DEPENDENT("Ubuntu-25.04-x86_64.deb", "");
} else if (hex::executeCommand("grep -q 'ID=fedora' /etc/os-release") == 0) {
if (hex::executeCommand("grep -q 'VERSION_ID=\"41\"' /etc/os-release") == 0)
return ARCH_DEPENDENT("Fedora-41-x86_64.rpm", "");
else if (hex::executeCommand("grep -q 'VERSION_ID=\"42\"' /etc/os-release") == 0)
return ARCH_DEPENDENT("Fedora-42-x86_64.rpm", "");
else if (hex::executeCommand("grep -q 'VERSION_ID=\"43\"' /etc/os-release") == 0)
return ARCH_DEPENDENT("Fedora-43-x86_64.rpm", "");
else if (hex::executeCommand("grep -q 'VERSION_ID=\"rawhide\"' /etc/os-release") == 0)
return ARCH_DEPENDENT("Fedora-rawhide-x86_64.rpm", "");
} else if (hex::executeCommand("grep -q '^NAME=\"Arch Linux\"' /etc/os-release") == 0) {
return ARCH_DEPENDENT("ArchLinux-x86_64.pkg.tar.zst", "");
}
#endif
return "";
}
auto updateCommand(std::string command) {
return [command](const std::fs::path &updatePath) -> bool {
const auto formattedCommand = fmt::format(fmt::runtime(command), updatePath.string());
hex::log::info("Starting update process with command: '{}'", formattedCommand);
return hex::executeCommand(formattedCommand) == 0;
};
}
auto updateMacOSBundle(const std::fs::path &updatePath) {
// Mount the DMG
auto mountCmd = fmt::format("hdiutil attach \"{}\" -nobrowse", updatePath.string());
auto mountOutput = hex::executeCommandWithOutput(mountCmd);
if (!mountOutput.has_value()) {
hex::log::error("Failed to mount DMG");
return false;
}
// Extract mount point from output
std::string mountPoint;
for (const auto &line : wolv::util::splitString(*mountOutput, "\n")) {
if (line.contains("/Volumes/")) {
auto parts = wolv::util::splitString(line, "\t");
mountPoint = parts.back();
break;
}
}
if (mountPoint.empty()) {
hex::log::error("Failed to find mount point");
return false;
}
ON_SCOPE_EXIT {
hex::executeCommand(fmt::format("hdiutil detach \"{}\"", mountPoint));
};
// Find the .app bundle in the mounted DMG
auto findCmd = fmt::format("find \"{}\" -name '*.app' -maxdepth 1", mountPoint);
auto appPath = hex::executeCommandWithOutput(findCmd);
if (!appPath.has_value()) {
hex::log::error("Failed to find .app in DMG");
hex::executeCommand(fmt::format("hdiutil detach \"{}\"", mountPoint));
return false;
}
appPath = wolv::util::trim(*appPath);
if (appPath->empty()) {
hex::log::error("Failed to find .app in DMG");
return false;
}
// Get the app name
auto appName = std::fs::path(*appPath).filename().string();
auto installPath = fmt::format("/Applications/{}", appName);
// Use AppleScript to copy with elevated privileges
auto installScript = fmt::format(
R"(osascript -e 'do shell script "rm -rf \"{}\" && cp -R \"{}\" /Applications/" with administrator privileges')",
installPath, *appPath
);
if (hex::executeCommand(installScript) != 0) {
hex::log::error("Failed to install update");
return false;
}
// Launch the new version
hex::executeCommand(fmt::format("open \"{}\"", installPath));
return true;
}
bool installUpdate(const std::fs::path &updatePath) {
using UpdaterFunction = std::function<bool(const std::fs::path &updatePath)>;
struct UpdateHandler {
std::string ending;
UpdaterFunction func;
};
const static auto UpdateHandlers = {
UpdateHandler { .ending=".msi", .func=updateCommand("msiexec /i \"{}\" /qb") },
UpdateHandler { .ending=".dmg", .func=updateMacOSBundle },
UpdateHandler { .ending=".deb", .func=updateCommand("zenity --password | sudo -S apt install -y --fix-broken \"{}\"") },
UpdateHandler { .ending=".rpm", .func=updateCommand("zenity --password | sudo -S rpm -i \"{}\"") },
UpdateHandler { .ending=".pkg.tar.zst", .func=updateCommand("zenity --password | sudo -S pacman -Syy && sudo pacman -U --noconfirm \"{}\"") },
UpdateHandler { .ending=".AppImage", .func=updateCommand(fmt::format(R"(zenity --password | sudo -S cp "{{}}" "{}")", hex::getEnvironmentVariable("APPIMAGE").value_or(""))) },
UpdateHandler { .ending=".flatpak", .func=updateCommand("zenity --password | sudo -S flatpak install -y --reinstall \"{}\"") },
UpdateHandler { .ending=".snap", .func=updateCommand("zenity --password | sudo -S snap install --dangerous \"{}\"") },
};
const auto updateFileName = wolv::util::toUTF8String(updatePath.filename());
for (const auto &handler : UpdateHandlers) {
if (updateFileName.ends_with(handler.ending)) {
// Install the update using the correct command
return handler.func(updatePath);
}
}
// If the installation type isn't handled here, the detected installation type doesn't support updates through the updater
hex::log::error("Install type cannot be updated");
return false;
}
int main(int argc, char **argv) {
hex::TaskManager::setCurrentThreadName("ImHex Updater");
hex::TaskManager::setMainThreadId(std::this_thread::get_id());
hex::log::impl::enableColorPrinting();
if (argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--help" || std::string_view(argv[1]) == "-h"))) {
fmt::print("ImHex Updater - You should probably not run this on its own\n");
fmt::print("Usage: updater <version-type>\n");
fmt::print(" version-type: The type of version to update to. Can be either 'stable' or 'nightly'.\n");
return EXIT_SUCCESS;
}
// Check we have the correct number of arguments
if (argc != 2) {
hex::log::error("Failed to start updater: Invalid arguments");
return EXIT_FAILURE;
}
hex::log::info("Updating ImHex...");
// Read the version type from the arguments
const std::string_view versionTypeString = argv[1];
hex::log::info("Installing '{}' version of ImHex", versionTypeString);
// Convert the version type string to the enum value
hex::ImHexApi::System::UpdateType updateType;
std::string releaseUrl;
if (versionTypeString == "stable") {
updateType = hex::ImHexApi::System::UpdateType::Stable;
releaseUrl = "https://github.com/WerWolv/ImHex/releases/latest";
} else if (versionTypeString == "nightly") {
updateType = hex::ImHexApi::System::UpdateType::Nightly;
releaseUrl = "https://github.com/WerWolv/ImHex/releases/tag/nightly";
} else {
hex::log::error("Invalid version type: {}", versionTypeString);
// Wait for user input before exiting so logs can be read
std::getchar();
return EXIT_FAILURE;
}
// Get the artifact name ending based on the current platform and architecture
const auto artifactEnding = getUpdateArtifactEnding();
if (artifactEnding.empty()) {
hex::log::error("Updater artifact ending is empty");
// Wait for user input before exiting so logs can be read
std::getchar();
return EXIT_FAILURE;
}
// Get the URL for the correct update artifact
const auto updateArtifactUrl = getArtifactUrl(artifactEnding, updateType);
if (updateArtifactUrl.empty()) {
// If the current artifact cannot be updated, open the latest release page in the browser
hex::log::warn("Failed to get update artifact URL for ending: {}", artifactEnding);
hex::log::info("Opening release page in browser to allow manual update");
hex::openWebpage(releaseUrl);
return EXIT_FAILURE;
}
// Download the update artifact
const auto updatePath = downloadUpdate(updateArtifactUrl);
// Install the update
if (!installUpdate(*updatePath)) {
// Open the latest release page in the default browser to allow the user to manually update
hex::openWebpage(releaseUrl);
// Wait for user input before exiting so logs can be read
std::getchar();
return EXIT_FAILURE;
} else {
return EXIT_SUCCESS;
}
}