Add Windows portable auto-update from Forgejo continuous releases.
Some checks failed
Windows Release / windows-x86_64 (push) Failing after 1h12m0s
Some checks failed
Windows Release / windows-x86_64 (push) Failing after 1h12m0s
Poll windows-continuous by git SHA, download the matching zip, and apply it in place so friends stay current without manual installs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7f34283537
commit
cdc4c5cb30
13 changed files with 371 additions and 22 deletions
3
src/BuildCommit.hpp.in
Normal file
3
src/BuildCommit.hpp.in
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
// Generated at configure time from git HEAD (short sha).
|
||||
#define INFINIPAINT_BUILD_COMMIT "@INFINIPAINT_GIT_SHA@"
|
||||
|
|
@ -251,6 +251,6 @@ void GlobalConfig::load_licenses() {
|
|||
std::sort(thirdPartyLicenses.begin(), thirdPartyLicenses.end(), [](const auto& a1, const auto& a2) {
|
||||
return std::lexicographical_compare(a1.first.begin(), a1.first.end(), a2.first.begin(), a2.first.end());
|
||||
});
|
||||
ownLicenseText = "InfiniPaint v" + VersionConstants::CURRENT_VERSION_STRING + "\n";
|
||||
ownLicenseText = "InfiniPaint v" + VersionConstants::full_version_string() + "\n";
|
||||
ownLicenseText += read_file_to_string("data/license");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@
|
|||
#endif
|
||||
|
||||
#include <Helpers/Logger.hpp>
|
||||
#include "VersionConstants.hpp"
|
||||
#include "WindowsPortableUpdater.hpp"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#define UPDATE_NOTIFICATION_URL "https://infinipaint.com/updateNotificationVersionAndroid.txt"
|
||||
|
|
@ -121,18 +123,95 @@ void MainProgram::update() {
|
|||
post_callback();
|
||||
}
|
||||
|
||||
void MainProgram::start_windows_portable_update_download() {
|
||||
#ifdef _WIN32
|
||||
if(updateCheckerData.downloadingZip || updateCheckerData.downloadUrl.empty())
|
||||
return;
|
||||
updateCheckerData.downloadingZip = true;
|
||||
updateCheckerData.downloadStatus = "Downloading update…";
|
||||
updateCheckerData.zipDownload = FileDownloader::download_data_from_url(updateCheckerData.downloadUrl);
|
||||
g.gui.set_to_layout();
|
||||
#else
|
||||
(void)0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainProgram::update_notification_check() {
|
||||
#ifndef __EMSCRIPTEN__
|
||||
#ifdef _WIN32
|
||||
// Poll in-flight zip download / apply.
|
||||
if(updateCheckerData.downloadingZip && updateCheckerData.zipDownload) {
|
||||
switch(updateCheckerData.zipDownload->status) {
|
||||
case FileDownloader::DownloadData::Status::IN_PROGRESS: {
|
||||
float p = updateCheckerData.zipDownload->progress.load();
|
||||
if(p > 0.f)
|
||||
updateCheckerData.downloadStatus = "Downloading update… " + std::to_string(int(p * 100.f)) + "%";
|
||||
break;
|
||||
}
|
||||
case FileDownloader::DownloadData::Status::SUCCESS: {
|
||||
updateCheckerData.downloadStatus = "Installing update…";
|
||||
g.gui.set_to_layout();
|
||||
if(!WindowsPortableUpdater::apply_zip_and_restart(updateCheckerData.zipDownload->str)) {
|
||||
updateCheckerData.downloadStatus = "Update install failed (see log).";
|
||||
updateCheckerData.downloadingZip = false;
|
||||
updateCheckerData.zipDownload = nullptr;
|
||||
g.gui.set_to_layout();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FileDownloader::DownloadData::Status::FAILURE:
|
||||
updateCheckerData.downloadStatus = "Download failed.";
|
||||
updateCheckerData.downloadingZip = false;
|
||||
updateCheckerData.zipDownload = nullptr;
|
||||
g.gui.set_to_layout();
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if(!updateCheckerData.updateCheckDone) {
|
||||
if(conf.checkForUpdates) {
|
||||
if(!updateCheckerData.versionFile)
|
||||
if(!updateCheckerData.versionFile) {
|
||||
#ifdef _WIN32
|
||||
// Fork continuous builds: compare git SHA against Forgejo windows-continuous.
|
||||
updateCheckerData.forgejoContinuous = true;
|
||||
updateCheckerData.versionFile = FileDownloader::download_data_from_url(FORGEJO_WINDOWS_CONTINUOUS_API);
|
||||
#else
|
||||
updateCheckerData.versionFile = FileDownloader::download_data_from_url(UPDATE_NOTIFICATION_URL);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
switch(updateCheckerData.versionFile->status) {
|
||||
case FileDownloader::DownloadData::Status::IN_PROGRESS:
|
||||
break;
|
||||
case FileDownloader::DownloadData::Status::SUCCESS: {
|
||||
updateCheckerData.updateCheckDone = true;
|
||||
#ifdef _WIN32
|
||||
std::string label, commit, zipUrl;
|
||||
if(WindowsPortableUpdater::parse_continuous_release_json(
|
||||
updateCheckerData.versionFile->str, label, commit, zipUrl)) {
|
||||
updateCheckerData.remoteCommit = commit;
|
||||
updateCheckerData.downloadUrl = zipUrl;
|
||||
updateCheckerData.newVersionStr = label.empty()
|
||||
? (VersionConstants::CURRENT_VERSION_STRING + "+" + commit)
|
||||
: label;
|
||||
const std::string& local = VersionConstants::build_commit_short();
|
||||
Logger::get().log(Logger::LogType::INFO,
|
||||
"Forgejo continuous commit=" + commit + " local=" + local);
|
||||
if(local != "unknown" && commit != local.substr(0, commit.size())) {
|
||||
updateCheckerData.showGui = true;
|
||||
Logger::get().log(Logger::LogType::PHONE_USERINFO,
|
||||
"Windows update available: " + updateCheckerData.newVersionStr);
|
||||
g.gui.set_to_layout();
|
||||
}
|
||||
else if(local == "unknown")
|
||||
Logger::get().log(Logger::LogType::INFO, "Local build commit unknown — skip update prompt");
|
||||
else
|
||||
Logger::get().log(Logger::LogType::INFO, "Windows portable build is up to date");
|
||||
}
|
||||
else
|
||||
Logger::get().log(Logger::LogType::INFO, "Could not parse Forgejo windows-continuous release");
|
||||
#else
|
||||
std::optional<VersionNumber> newVersion = version_str_to_version_numbers(updateCheckerData.versionFile->str);
|
||||
std::optional<VersionNumber> currentVersion = VersionConstants::CURRENT_VERSION_NUMBER;
|
||||
if(newVersion.has_value() && currentVersion.has_value()) {
|
||||
|
|
@ -152,6 +231,7 @@ void MainProgram::update_notification_check() {
|
|||
}
|
||||
else
|
||||
Logger::get().log(Logger::LogType::INFO, "Update notification file couldn't be converted to version numbers");
|
||||
#endif
|
||||
updateCheckerData.versionFile = nullptr;
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ class MainProgram {
|
|||
public:
|
||||
static constexpr size_t LOG_SIZE = 30;
|
||||
static constexpr const char* UPDATE_DOWNLOAD_URL = "https://infinipaint.com/download.html";
|
||||
/** Public Forgejo continuous Windows release (fork). */
|
||||
static constexpr const char* FORGEJO_WINDOWS_CONTINUOUS_API =
|
||||
"https://forgejo.fsociety00.cc/api/v1/repos/copilot/infinipaint/releases/tags/windows-continuous";
|
||||
static constexpr const char* FORGEJO_WINDOWS_CONTINUOUS_PAGE =
|
||||
"https://forgejo.fsociety00.cc/copilot/infinipaint/releases/tag/windows-continuous";
|
||||
|
||||
InputManager input;
|
||||
|
||||
|
|
@ -198,11 +203,18 @@ class MainProgram {
|
|||
~MainProgram();
|
||||
|
||||
void update_notification_check();
|
||||
void start_windows_portable_update_download();
|
||||
struct UpdateCheckerData {
|
||||
bool showGui = false;
|
||||
bool updateCheckDone = false;
|
||||
std::string newVersionStr;
|
||||
std::string downloadUrl;
|
||||
std::string remoteCommit;
|
||||
bool forgejoContinuous = false;
|
||||
bool downloadingZip = false;
|
||||
std::string downloadStatus;
|
||||
std::shared_ptr<FileDownloader::DownloadData> versionFile;
|
||||
std::shared_ptr<FileDownloader::DownloadData> zipDownload;
|
||||
} updateCheckerData;
|
||||
|
||||
float calculate_gui_scale();
|
||||
|
|
|
|||
|
|
@ -677,27 +677,57 @@ void Toolbar::update_notification_gui() {
|
|||
|
||||
center_obstructing_window_gui("Update notifications GUI", CLAY_SIZING_FIXED(700), CLAY_SIZING_FIT(0), [&] {
|
||||
gui.new_id("update notification gui", [&] {
|
||||
text_label_centered(gui, "Update v" + main.updateCheckerData.newVersionStr + " available!");
|
||||
text_button(gui, "download", "Open download page in web browser", {
|
||||
.wide = true,
|
||||
.onClick = [&]{
|
||||
SDL_OpenURL(MainProgram::UPDATE_DOWNLOAD_URL);
|
||||
main.updateCheckerData.showGui = false;
|
||||
text_label_centered(gui, "Update available!");
|
||||
text_label_centered(gui, main.updateCheckerData.newVersionStr);
|
||||
#ifdef _WIN32
|
||||
if(main.updateCheckerData.forgejoContinuous) {
|
||||
text_label(gui, "Local: " + VersionConstants::full_version_string());
|
||||
if(main.updateCheckerData.downloadingZip) {
|
||||
text_label_centered(gui, main.updateCheckerData.downloadStatus.empty()
|
||||
? "Downloading…"
|
||||
: main.updateCheckerData.downloadStatus);
|
||||
}
|
||||
});
|
||||
text_button(gui, "ignore forever", "Ignore and don't notify again (can be changed in settings)", {
|
||||
.wide = true,
|
||||
.onClick = [&]{
|
||||
main.conf.checkForUpdates = false;
|
||||
main.updateCheckerData.showGui = false;
|
||||
else {
|
||||
text_button(gui, "download install", "Download and install update", {
|
||||
.wide = true,
|
||||
.onClick = [&]{
|
||||
main.start_windows_portable_update_download();
|
||||
}
|
||||
});
|
||||
text_button(gui, "open page", "Open Forgejo release page", {
|
||||
.wide = true,
|
||||
.onClick = [&]{
|
||||
SDL_OpenURL(MainProgram::FORGEJO_WINDOWS_CONTINUOUS_PAGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
text_button(gui, "ignore for now", "Ignore for now", {
|
||||
.wide = true,
|
||||
.onClick = [&]{
|
||||
main.updateCheckerData.showGui = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
text_button(gui, "download", "Open download page in web browser", {
|
||||
.wide = true,
|
||||
.onClick = [&]{
|
||||
SDL_OpenURL(MainProgram::UPDATE_DOWNLOAD_URL);
|
||||
main.updateCheckerData.showGui = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if(!main.updateCheckerData.downloadingZip) {
|
||||
text_button(gui, "ignore forever", "Ignore and don't notify again (can be changed in settings)", {
|
||||
.wide = true,
|
||||
.onClick = [&]{
|
||||
main.conf.checkForUpdates = false;
|
||||
main.updateCheckerData.showGui = false;
|
||||
}
|
||||
});
|
||||
text_button(gui, "ignore for now", "Ignore for now", {
|
||||
.wide = true,
|
||||
.onClick = [&]{
|
||||
main.updateCheckerData.showGui = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,20 @@
|
|||
*/
|
||||
|
||||
#include "VersionConstants.hpp"
|
||||
#include "BuildCommit.hpp"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace VersionConstants {
|
||||
|
||||
const std::string& build_commit_short() {
|
||||
static const std::string sha = INFINIPAINT_BUILD_COMMIT;
|
||||
return sha;
|
||||
}
|
||||
|
||||
std::string full_version_string() {
|
||||
return CURRENT_VERSION_STRING + "+" + build_commit_short();
|
||||
}
|
||||
|
||||
VersionNumber header_to_version_number(const std::string& header) {
|
||||
static std::unordered_map<std::string, VersionNumber> m;
|
||||
if(m.empty()) {
|
||||
|
|
|
|||
|
|
@ -31,4 +31,8 @@ namespace VersionConstants {
|
|||
const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000009"; // Change whenever the save file is incompatible with the previous version
|
||||
const std::string CURRENT_VERSION_STRING = "0.9.0-fork";
|
||||
constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 9, 0);
|
||||
/** Short git SHA baked at configure time (`BuildCommit.hpp`). */
|
||||
const std::string& build_commit_short();
|
||||
/** e.g. `0.9.0-fork+7f34283` */
|
||||
std::string full_version_string();
|
||||
}
|
||||
|
|
|
|||
156
src/WindowsPortableUpdater.cpp
Normal file
156
src/WindowsPortableUpdater.cpp
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#include "WindowsPortableUpdater.hpp"
|
||||
|
||||
#include <Helpers/Logger.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <regex>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <SDL3/SDL.h>
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace WindowsPortableUpdater {
|
||||
|
||||
bool parse_continuous_release_json(
|
||||
const std::string& jsonText,
|
||||
std::string& outLabel,
|
||||
std::string& outCommit,
|
||||
std::string& outZipUrl)
|
||||
{
|
||||
outLabel.clear();
|
||||
outCommit.clear();
|
||||
outZipUrl.clear();
|
||||
try {
|
||||
auto j = nlohmann::json::parse(jsonText);
|
||||
outLabel = j.value("name", "");
|
||||
std::regex shaRe(R"(\+([0-9a-fA-F]{7,40})\b)");
|
||||
std::smatch m;
|
||||
if(std::regex_search(outLabel, m, shaRe))
|
||||
outCommit = m[1].str().substr(0, 7);
|
||||
if(outCommit.empty()) {
|
||||
std::string body = j.value("body", "");
|
||||
if(std::regex_search(body, m, shaRe))
|
||||
outCommit = m[1].str().substr(0, 7);
|
||||
}
|
||||
if(!j.contains("assets") || !j["assets"].is_array())
|
||||
return false;
|
||||
// Prefer the zip that matches the release commit (rolling tag keeps older assets).
|
||||
auto pick_url = [](const nlohmann::json& a) -> std::string {
|
||||
std::string u = a.value("browser_download_url", "");
|
||||
if(u.empty())
|
||||
u = a.value("url", "");
|
||||
return u;
|
||||
};
|
||||
for(auto& a : j["assets"]) {
|
||||
std::string name = a.value("name", "");
|
||||
if(name.find("win-x86_64-portable.zip") == std::string::npos)
|
||||
continue;
|
||||
if(!outCommit.empty() && name.find(outCommit) == std::string::npos)
|
||||
continue;
|
||||
if(outCommit.empty() && std::regex_search(name, m, shaRe))
|
||||
outCommit = m[1].str().substr(0, 7);
|
||||
outZipUrl = pick_url(a);
|
||||
if(!outZipUrl.empty())
|
||||
break;
|
||||
}
|
||||
// Fallback: newest matching zip by created_at if commit filter found nothing.
|
||||
if(outZipUrl.empty()) {
|
||||
std::string bestCreated;
|
||||
for(auto& a : j["assets"]) {
|
||||
std::string name = a.value("name", "");
|
||||
if(name.find("win-x86_64-portable.zip") == std::string::npos)
|
||||
continue;
|
||||
std::string created = a.value("created_at", "");
|
||||
if(created < bestCreated && !bestCreated.empty())
|
||||
continue;
|
||||
std::string url = pick_url(a);
|
||||
if(url.empty())
|
||||
continue;
|
||||
bestCreated = created;
|
||||
outZipUrl = url;
|
||||
if(outCommit.empty() && std::regex_search(name, m, shaRe))
|
||||
outCommit = m[1].str().substr(0, 7);
|
||||
}
|
||||
}
|
||||
return !outCommit.empty() && !outZipUrl.empty();
|
||||
}
|
||||
catch(const std::exception& e) {
|
||||
Logger::get().log(Logger::LogType::INFO, std::string("Windows update JSON parse failed: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool apply_zip_and_restart(const std::string& zipBytes) {
|
||||
#ifdef _WIN32
|
||||
namespace fs = std::filesystem;
|
||||
try {
|
||||
char* basePath = SDL_GetBasePath();
|
||||
if(!basePath)
|
||||
return false;
|
||||
fs::path appDir = fs::path(basePath);
|
||||
SDL_free(basePath);
|
||||
|
||||
fs::path tempRoot = fs::temp_directory_path() / "infinipaint-update";
|
||||
fs::create_directories(tempRoot);
|
||||
fs::path zipPath = tempRoot / "update.zip";
|
||||
fs::path extractDir = tempRoot / "extracted";
|
||||
fs::path batPath = tempRoot / "apply_update.bat";
|
||||
|
||||
{
|
||||
std::ofstream out(zipPath, std::ios::binary);
|
||||
if(!out)
|
||||
return false;
|
||||
out.write(zipBytes.data(), static_cast<std::streamsize>(zipBytes.size()));
|
||||
}
|
||||
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
std::ofstream bat(batPath);
|
||||
if(!bat)
|
||||
return false;
|
||||
bat << "@echo off\r\n"
|
||||
<< "setlocal\r\n"
|
||||
<< "set \"APPDIR=" << appDir.string() << "\"\r\n"
|
||||
<< "set \"ZIP=" << zipPath.string() << "\"\r\n"
|
||||
<< "set \"OUT=" << extractDir.string() << "\"\r\n"
|
||||
<< "set \"PID=" << pid << "\"\r\n"
|
||||
<< ":wait\r\n"
|
||||
<< "tasklist /FI \"PID eq %PID%\" 2>nul | find \"%PID%\" >nul\r\n"
|
||||
<< "if not errorlevel 1 (\r\n"
|
||||
<< " timeout /t 1 /nobreak >nul\r\n"
|
||||
<< " goto wait\r\n"
|
||||
<< ")\r\n"
|
||||
<< "if exist \"%OUT%\" rmdir /s /q \"%OUT%\"\r\n"
|
||||
<< "mkdir \"%OUT%\" >nul 2>nul\r\n"
|
||||
<< "powershell -NoProfile -ExecutionPolicy Bypass -Command "
|
||||
<< "\"Expand-Archive -Force -Path '%ZIP%' -DestinationPath '%OUT%'\"\r\n"
|
||||
<< "if errorlevel 1 exit /b 1\r\n"
|
||||
<< "xcopy /E /Y /Q \"%OUT%\\*\" \"%APPDIR%\\\" >nul\r\n"
|
||||
<< "start \"\" \"%APPDIR%\\infinipaint.exe\"\r\n"
|
||||
<< "endlocal\r\n";
|
||||
bat.close();
|
||||
|
||||
std::string batStr = batPath.string();
|
||||
HINSTANCE hi = ShellExecuteA(nullptr, "open", batStr.c_str(), nullptr, nullptr, SW_HIDE);
|
||||
if(reinterpret_cast<intptr_t>(hi) <= 32) {
|
||||
Logger::get().log(Logger::LogType::INFO, "ShellExecute apply_update.bat failed");
|
||||
return false;
|
||||
}
|
||||
Logger::get().log(Logger::LogType::INFO, "Windows update script launched; quitting to apply");
|
||||
SDL_Event e{};
|
||||
e.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&e);
|
||||
return true;
|
||||
}
|
||||
catch(const std::exception& e) {
|
||||
Logger::get().log(Logger::LogType::INFO, std::string("Windows update apply failed: ") + e.what());
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
(void)zipBytes;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
16
src/WindowsPortableUpdater.hpp
Normal file
16
src/WindowsPortableUpdater.hpp
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
/** Windows portable self-update helpers (Forgejo windows-continuous). */
|
||||
namespace WindowsPortableUpdater {
|
||||
/** Parse Forgejo release JSON; fill remote label, commit, zip URL. Returns true if usable. */
|
||||
bool parse_continuous_release_json(
|
||||
const std::string& jsonText,
|
||||
std::string& outLabel,
|
||||
std::string& outCommit,
|
||||
std::string& outZipUrl);
|
||||
|
||||
/** Write zip + apply script, then request process exit. Returns false on I/O failure. */
|
||||
bool apply_zip_and_restart(const std::string& zipBytes);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue