This commit is contained in:
6arelyFuture 2024-01-13 22:57:08 +01:00
commit 095e3e8b42
Signed by: Future
GPG Key ID: FA77F074E98D98A5
37 changed files with 2372 additions and 0 deletions

7
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: gitsubmodule
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

162
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,162 @@
name: Build
on:
push:
branches:
- "*"
pull_request:
branches:
- "*"
types: [opened, synchronize, reopened]
env:
PREMAKE_VERSION: "5.0.0-beta2"
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
build-win:
name: Build Windows
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
configuration:
- debug
- release
arch:
- x64
include:
- arch: x64
platform: x64
steps:
- name: Check out files
uses: actions/checkout@main
with:
submodules: true
fetch-depth: 0
# NOTE - If LFS ever starts getting used during builds, switch this to true!
lfs: false
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@main
- name: Install Premake5
uses: abel0b/setup-premake@v2.3
with:
version: ${{ env.PREMAKE_VERSION }}
- name: Generate project files
run: premake5 vs2022
- name: Set up problem matching
uses: ammaraskar/msvc-problem-matcher@master
- name: Build ${{matrix.arch}} ${{matrix.configuration}} binaries
run: msbuild /m /v:minimal /p:Configuration=${{matrix.configuration}} /p:Platform=${{matrix.platform}} build/aw-installer.sln
- name: Upload ${{matrix.arch}} ${{matrix.configuration}} binaries
uses: actions/upload-artifact@main
with:
name: windows-${{matrix.arch}}-${{matrix.configuration}}
path: |
build/bin/${{matrix.arch}}/${{matrix.configuration}}/aw-installer.exe
build/bin/${{matrix.arch}}/${{matrix.configuration}}/aw-installer.pdb
build-linux:
name: Build Linux
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
configuration:
- debug
- release
arch:
- x64
steps:
- name: Check out files
uses: actions/checkout@main
with:
submodules: true
fetch-depth: 0
# NOTE - If LFS ever starts getting used during builds, switch this to true!
lfs: false
- name: Install dependencies (x64)
if: matrix.arch == 'x64'
run: |
sudo apt-get update
sudo apt-get install libcurl4-gnutls-dev -y
- name: Install Premake5
uses: abel0b/setup-premake@v2.3
with:
version: ${{ env.PREMAKE_VERSION }}
- name: Generate project files
run: premake5 --cc=clang gmake2
- name: Set up problem matching
uses: ammaraskar/gcc-problem-matcher@master
- name: Build ${{matrix.arch}} ${{matrix.configuration}} binaries
run: |
pushd build
make config=${{matrix.configuration}}_${{matrix.arch}} -j$(nproc)
env:
CC: clang
CXX: clang++
- name: Upload ${{matrix.arch}} ${{matrix.configuration}} binaries
uses: actions/upload-artifact@main
with:
name: linux-${{matrix.arch}}-${{matrix.configuration}}
path: |
build/bin/${{matrix.arch}}/${{matrix.configuration}}/aw-installer
build-macos:
name: Build macOS
runs-on: macos-13
strategy:
fail-fast: false
matrix:
configuration:
- debug
- release
arch:
- x64
- arm64
steps:
- name: Check out files
uses: actions/checkout@main
with:
submodules: true
fetch-depth: 0
# NOTE - If LFS ever starts getting used during builds, switch this to true!
lfs: false
- name: Install Premake5
uses: abel0b/setup-premake@v2.3
with:
version: ${{ env.PREMAKE_VERSION }}
- name: Generate project files
run: premake5 gmake2
- name: Set up problem matching
uses: ammaraskar/gcc-problem-matcher@master
- name: Build ${{matrix.arch}} ${{matrix.configuration}} binaries
run: |
pushd build
make config=${{matrix.configuration}}_${{matrix.arch}} -j$(sysctl -n hw.logicalcpu)
- name: Upload ${{matrix.arch}} ${{matrix.configuration}} binaries
uses: actions/upload-artifact@v3.1.3
with:
name: macos-${{matrix.arch}}-${{matrix.configuration}}
path: |
build/bin/${{matrix.arch}}/${{matrix.configuration}}/aw-installer

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Build results
build

13
.gitmodules vendored Normal file
View File

@ -0,0 +1,13 @@
[submodule "deps/GSL"]
path = deps/GSL
url = https://github.com/microsoft/GSL.git
[submodule "deps/curl"]
path = deps/curl
url = https://github.com/curl/curl.git
branch = curl-8_5_0
[submodule "deps/rapidjson"]
path = deps/rapidjson
url = https://github.com/Tencent/rapidjson.git
[submodule "deps/zlib"]
path = deps/zlib
url = https://github.com/madler/zlib.git

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2024, AlterWare
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

24
README.md Normal file
View File

@ -0,0 +1,24 @@
[![build](https://github.com/alterware/aw-installer/workflows/Build/badge.svg)](https://github.com/alterware/aw-installer/actions)
# AlterWare: Installer
This is the tool we use to pull changes made from the release page of some of our clients and install it where we need to.
## Build
- Install [Premake5](premake5-link) and add it to your system PATH
- Clone this repository using [Git][git-link]
- Update the submodules using ``git submodule update --init --recursive``
- Run Premake with either of these two options ``premake5 vs2022`` (Windows) or ``premake5 gmake2`` (Linux/macOS)
**IMPORTANT**
Requirements for Unix systems:
- Compilation: Please use Clang as the preferred compiler
- Dependencies: Ensure the LLVM C++ Standard library is installed
- Alternative compilers: If you opt for a different compiler such as GCC, use the [Mold][mold-link] linker
- Customization: Modifications to the Premake5.lua script may be required
- Platform support: Details regarding supported platforms are available in [build.yml][build-link]
[premake5-link]: https://premake.github.io
[git-link]: https://git-scm.com
[mold-link]: https://github.com/rui314/mold
[build-link]: https://github.com/alterware/master-server/blob/master/.github/workflows/build.yml

1
deps/GSL vendored Submodule

@ -0,0 +1 @@
Subproject commit e64c97fc2cfc11992098bb38eda932de275e3f4d

1
deps/curl vendored Submodule

@ -0,0 +1 @@
Subproject commit 7161cb17c01dcff1dc5bf89a18437d9d729f1ecd

75
deps/premake/curl.lua vendored Normal file
View File

@ -0,0 +1,75 @@
curl = {
source = path.join(dependencies.basePath, "curl"),
}
function curl.import()
links { "curl" }
filter "toolset:msc*"
links { "Crypt32.lib" }
filter {}
curl.includes()
end
function curl.includes()
filter "toolset:msc*"
includedirs {
path.join(curl.source, "include"),
}
defines {
"CURL_STRICTER",
"CURL_STATICLIB",
"CURL_DISABLE_LDAP",
}
filter {}
end
function curl.project()
if not os.istarget("windows") then
return
end
project "curl"
language "C"
curl.includes()
includedirs {
path.join(curl.source, "lib"),
}
files {
path.join(curl.source, "lib/**.c"),
path.join(curl.source, "lib/**.h"),
}
defines {
"BUILDING_LIBCURL",
}
filter "toolset:msc*"
defines {
"USE_SCHANNEL",
"USE_WINDOWS_SSPI",
"USE_THREADS_WIN32",
}
filter {}
filter "toolset:not msc*"
defines {
"USE_GNUTLS",
"USE_THREADS_POSIX",
}
filter {}
warnings "Off"
kind "StaticLib"
end
table.insert(dependencies, curl)

19
deps/premake/gsl.lua vendored Normal file
View File

@ -0,0 +1,19 @@
gsl = {
source = path.join(dependencies.basePath, "GSL"),
}
function gsl.import()
gsl.includes()
end
function gsl.includes()
includedirs {
path.join(gsl.source, "include")
}
end
function gsl.project()
end
table.insert(dependencies, gsl)

50
deps/premake/minizip.lua vendored Normal file
View File

@ -0,0 +1,50 @@
minizip = {
source = path.join(dependencies.basePath, "zlib/contrib/minizip"),
}
function minizip.import()
links { "minizip" }
zlib.import()
minizip.includes()
end
function minizip.includes()
includedirs {
minizip.source
}
zlib.includes()
end
function minizip.project()
project "minizip"
language "C"
cdialect "C89"
minizip.includes()
files {
path.join(minizip.source, "*.h"),
path.join(minizip.source, "*.c"),
}
filter "system:not windows"
removefiles {
path.join(minizip.source, "iowin32.c"),
}
filter {}
removefiles {
path.join(minizip.source, "miniunz.c"),
path.join(minizip.source, "minizip.c"),
}
filter { "system:windows" }
defines "_CRT_SECURE_NO_DEPRECATE"
filter {}
warnings "Off"
kind "StaticLib"
end
table.insert(dependencies, minizip)

20
deps/premake/rapidjson.lua vendored Normal file
View File

@ -0,0 +1,20 @@
rapidjson = {
source = path.join(dependencies.basePath, "rapidjson"),
}
function rapidjson.import()
defines{"RAPIDJSON_HAS_STDSTRING"}
rapidjson.includes()
end
function rapidjson.includes()
includedirs {
path.join(rapidjson.source, "include"),
}
end
function rapidjson.project()
end
table.insert(dependencies, rapidjson)

40
deps/premake/zlib.lua vendored Normal file
View File

@ -0,0 +1,40 @@
zlib = {
source = path.join(dependencies.basePath, "zlib"),
}
function zlib.import()
links { "zlib" }
zlib.includes()
end
function zlib.includes()
includedirs {
zlib.source
}
defines {
"ZLIB_CONST",
}
end
function zlib.project()
project "zlib"
language "C"
cdialect "C89"
zlib.includes()
files {
path.join(zlib.source, "*.h"),
path.join(zlib.source, "*.c"),
}
filter { "system:windows" }
defines "_CRT_SECURE_NO_DEPRECATE"
filter {}
warnings "Off"
kind "StaticLib"
end
table.insert(dependencies, zlib)

1
deps/rapidjson vendored Submodule

@ -0,0 +1 @@
Subproject commit 6089180ecb704cb2b136777798fa1be303618975

1
deps/zlib vendored Submodule

@ -0,0 +1 @@
Subproject commit 643e17b7498d12ab8d15565662880579692f769d

142
premake5.lua Normal file
View File

@ -0,0 +1,142 @@
dependencies = {
basePath = "./deps"
}
function dependencies.load()
dir = path.join(dependencies.basePath, "premake/*.lua")
deps = os.matchfiles(dir)
for i, dep in pairs(deps) do
dep = dep:gsub(".lua", "")
require(dep)
end
end
function dependencies.imports()
for i, proj in pairs(dependencies) do
if type(i) == 'number' then
proj.import()
end
end
end
function dependencies.projects()
for i, proj in pairs(dependencies) do
if type(i) == 'number' then
proj.project()
end
end
end
dependencies.load()
workspace "aw-installer"
startproject "aw-installer"
location "./build"
objdir "%{wks.location}/obj"
targetdir "%{wks.location}/bin/%{cfg.platform}/%{cfg.buildcfg}"
configurations {"debug", "release"}
language "C++"
cppdialect "C++20"
if os.istarget("darwin") then
platforms {"x64", "arm64"}
else
platforms {"x86", "x64"}
end
filter "platforms:x86"
architecture "x86"
filter {}
filter "platforms:x64"
architecture "x86_64"
filter {}
filter "platforms:arm64"
architecture "ARM64"
filter {}
symbols "On"
staticruntime "On"
editandcontinue "Off"
warnings "Extra"
characterset "ASCII"
filter { "system:linux", "system:macosx" }
buildoptions "-pthread"
linkoptions "-pthread"
filter {}
if os.istarget("linux") then
filter { "toolset:clang*" }
buildoptions "-stdlib=libc++"
linkoptions "-stdlib=libc++"
-- always try to use lld. LD or Gold will not work
linkoptions "-fuse-ld=lld"
filter {}
end
filter { "system:macosx", "platforms:arm64" }
buildoptions "-arch arm64"
linkoptions "-arch arm64"
filter {}
if _OPTIONS["dev-build"] then
defines {"DEV_BUILD"}
end
if os.getenv("CI") then
defines "CI"
end
flags {"NoIncrementalLink", "NoMinimalRebuild", "MultiProcessorCompile", "No64BitChecks"}
filter "configurations:Release"
optimize "Size"
defines "NDEBUG"
flags "FatalCompileWarnings"
filter {}
filter "configurations:Debug"
optimize "Debug"
defines {"DEBUG", "_DEBUG"}
filter {}
project "aw-installer"
kind "ConsoleApp"
language "C++"
pchheader "std_include.hpp"
pchsource "src/std_include.cpp"
files {"./src/**.rc", "./src/**.hpp", "./src/**.cpp"}
includedirs {"./src", "%{prj.location}/src"}
filter "system:windows"
files {
"./src/**.rc",
}
filter {}
filter { "system:windows", "toolset:not msc*" }
resincludedirs {
"%{_MAIN_SCRIPT_DIR}/src"
}
filter {}
filter { "system:windows", "toolset:msc*" }
resincludedirs {
"$(ProjectDir)src"
}
filter {}
dependencies.imports()
group "Dependencies"
dependencies.projects()

248
src/console.cpp Normal file
View File

@ -0,0 +1,248 @@
#include "std_include.hpp"
#include "console.hpp"
#ifdef _WIN32
#define COLOR_LOG_INFO 11//15
#define COLOR_LOG_WARN 14
#define COLOR_LOG_ERROR 12
#define COLOR_LOG_DEBUG 15//7
#else
#define COLOR_LOG_INFO "\033[0;36m"
#define COLOR_LOG_WARN "\033[0;33m"
#define COLOR_LOG_ERROR "\033[0;31m"
#define COLOR_LOG_DEBUG "\033[0m"
#endif
namespace console
{
namespace
{
std::mutex signal_mutex;
std::function<void()> signal_callback;
#ifdef _WIN32
#define COLOR(win, posix) win
using color_type = WORD;
#else
#define COLOR(win, posix) posix
using color_type = const char*;
#endif
const color_type color_array[] =
{
COLOR(0x8, "\033[0;90m"), // 0 - black
COLOR(0xC, "\033[0;91m"), // 1 - red
COLOR(0xA, "\033[0;92m"), // 2 - green
COLOR(0xE, "\033[0;93m"), // 3 - yellow
COLOR(0x9, "\033[0;94m"), // 4 - blue
COLOR(0xB, "\033[0;96m"), // 5 - cyan
COLOR(0xD, "\033[0;95m"), // 6 - pink
COLOR(0xF, "\033[0;97m"), // 7 - white
};
#ifdef _WIN32
BOOL WINAPI handler(const DWORD signal)
{
if (signal == CTRL_C_EVENT && signal_callback)
{
signal_callback();
}
return TRUE;
}
#else
void handler(int signal)
{
if (signal == SIGINT && signal_callback)
{
signal_callback();
}
}
#endif
std::string format(va_list* ap, const char* message)
{
static thread_local char buffer[0x1000];
#ifdef _WIN32
const int count = vsnprintf_s(buffer, _TRUNCATE, message, *ap);
#else
const int count = vsnprintf(buffer, sizeof(buffer), message, *ap);
#endif
if (count < 0) return {};
return {buffer, static_cast<size_t>(count)};
}
#ifdef _WIN32
HANDLE get_console_handle()
{
return GetStdHandle(STD_OUTPUT_HANDLE);
}
#endif
void set_color(const color_type color)
{
#ifdef _WIN32
SetConsoleTextAttribute(get_console_handle(), color);
#else
printf("%s", color);
#endif
}
bool apply_color(const std::string& data, const size_t index, const color_type base_color)
{
if (data[index] != '^' || (index + 1) >= data.size())
{
return false;
}
auto code = data[index + 1] - '0';
if (code < 0 || code > 11)
{
return false;
}
code = std::min(code, 7); // Everything above white is white
if (code == 7)
{
set_color(base_color);
}
else
{
set_color(color_array[code]);
}
return true;
}
void print_colored(const std::string& line, const color_type base_color)
{
lock _{};
set_color(base_color);
for (size_t i = 0; i < line.size(); ++i)
{
if (apply_color(line, i, base_color))
{
++i;
continue;
}
putchar(line[i]);
}
reset_color();
}
}
lock::lock()
{
#ifdef _WIN32
_lock_file(stdout);
#else
flockfile(stdout);
#endif
}
lock::~lock()
{
#ifdef _WIN32
_unlock_file(stdout);
#else
funlockfile(stdout);
#endif
}
void reset_color()
{
lock _{};
#ifdef _WIN32
SetConsoleTextAttribute(get_console_handle(), 7);
#else
printf("\033[0m");
#endif
fflush(stdout);
}
void info(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[+] " + data + "\n", COLOR_LOG_INFO);
va_end(ap);
}
void warn(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[!] " + data + "\n", COLOR_LOG_WARN);
va_end(ap);
}
void error(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[-] " + data + "\n", COLOR_LOG_ERROR);
va_end(ap);
}
void log(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[*] " + data + "\n", COLOR_LOG_DEBUG);
va_end(ap);
}
void set_title(const std::string& title)
{
lock _{};
#ifdef _WIN32
SetConsoleTitleA(title.c_str());
#else
printf("\033]0;%s\007", title.c_str());
fflush(stdout);
#endif
}
signal_handler::signal_handler(std::function<void()> callback)
: std::lock_guard<std::mutex>(signal_mutex)
{
signal_callback = std::move(callback);
#ifdef _WIN32
SetConsoleCtrlHandler(handler, TRUE);
#else
signal(SIGINT, handler);
#endif
}
signal_handler::~signal_handler()
{
#ifdef _WIN32
SetConsoleCtrlHandler(handler, FALSE);
#else
signal(SIGINT, SIG_DFL);
#endif
signal_callback = {};
}
}

32
src/console.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
namespace console
{
class lock
{
public:
lock();
~lock();
lock(lock&&) = delete;
lock(const lock&) = delete;
lock& operator=(lock&&) = delete;
lock& operator=(const lock&) = delete;
};
void reset_color();
void info(const char* message, ...);
void warn(const char* message, ...);
void error(const char* message, ...);
void log(const char* message, ...);
void set_title(const std::string& title);
class signal_handler : std::lock_guard<std::mutex>
{
public:
signal_handler(std::function<void()> callback);
~signal_handler();
};
}

53
src/main.cpp Normal file
View File

@ -0,0 +1,53 @@
#include <std_include.hpp>
#include "console.hpp"
#include "updater/updater.hpp"
namespace
{
int unsafe_main(std::string&& prog, std::vector<std::string>&& args)
{
// Parse command-line flags (only increment i for matching flags)
for (auto i = args.begin(); i != args.end();)
{
if (*i == "-update-iw4x")
{
return updater::update_iw4x();
}
else
{
console::info("AlterWare Installer\n"
"Usage: %s OPTIONS\n"
" -update-iw4x\n",
prog.data()
);
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
}
int main(const int argc, char* argv[])
{
console::set_title("AlterWare Installer");
console::log("AlterWare Installer");
try
{
std::string prog(argv[0]);
std::vector<std::string> args;
args.reserve(argc - 1);
args.assign(argv + 1, argv + argc);
return unsafe_main(std::move(prog), std::move(args));
}
catch (const std::exception& ex)
{
console::error("Fatal error: %s", ex.what());
return EXIT_FAILURE;
}
}

1
src/std_include.cpp Normal file
View File

@ -0,0 +1 @@
#include <std_include.hpp>

74
src/std_include.hpp Normal file
View File

@ -0,0 +1,74 @@
#ifdef _WIN32
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#define ZeroMemory(x, y) std::memset(x, 0, y)
#endif
// min and max is required by gdi, therefore NOMINMAX won't work
#ifdef max
#undef max
#endif
#ifdef min
#undef min
#endif
#include <cassert>
#include <cctype>
#include <csignal>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <mutex>
#include <optional>
#include <queue>
#include <ranges>
#include <regex>
#include <span>
#include <sstream>
#include <thread>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>
#include <gsl/gsl>
#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
#ifdef _WIN32
#pragma comment(lib, "ws2_32.lib")
#endif
using namespace std::literals;

View File

@ -0,0 +1,258 @@
#include <std_include.hpp>
#include <console.hpp>
#include "file_updater.hpp"
#include <utils/compression.hpp>
#include <utils/http.hpp>
#include <utils/io.hpp>
namespace updater
{
namespace
{
std::optional<std::string> get_release_tag(const std::string& release_url)
{
const auto release_info = utils::http::get_data(release_url);
if (!release_info.has_value())
{
console::warn("Could not reach remote URL \"%s\"", release_url.c_str());
return {};
}
rapidjson::Document release_json{};
const rapidjson::ParseResult result = release_json.Parse(release_info.value());
if (!result || !release_json.IsObject())
{
console::error("Could not parse remote JSON response from \"%s\"", release_url.c_str());
return {};
}
if (release_json.HasMember("tag_name") && release_json["tag_name"].IsString())
{
const auto* tag_name = release_json["tag_name"].GetString();
return tag_name;
}
console::error("Remote JSON response from \"%s\" does not contain the data we expected", release_url.c_str());
return {};
}
}
file_updater::file_updater(std::string name, std::filesystem::path base, std::filesystem::path out_name,
std::filesystem::path version_file,
std::string remote_tag, std::string remote_download)
: name_(std::move(name))
, base_(std::move(base))
, out_name_(std::move(out_name))
, version_file_(std::move(version_file))
, remote_tag_(std::move(remote_tag))
, remote_download_(std::move(remote_download))
{
}
bool file_updater::update_if_necessary() const
{
update_state update_state;
const auto local_version = this->read_local_revision_file();
if (!this->does_require_update(update_state, local_version))
{
console::log("%s does not require an update", this->name_.c_str());
return true;
}
console::info("Updating %s", this->name_.c_str());
if (!this->update_file(this->remote_download_))
{
console::error("Update failed");
return false;
}
this->cleanup_directories();
if (!this->deploy_files())
{
console::error("Unable to deploy files");
return false;
}
// Do this last to make sure we don't ever create a version file when something failed
this->create_version_file(update_state.latest_tag);
return true;
}
void file_updater::add_dir_to_clean(const std::string& dir)
{
this->cleanup_directories_.emplace_back(this->base_ / dir);
}
void file_updater::add_file_to_skip(const std::string& file)
{
this->skip_files_.emplace_back(file);
}
std::string file_updater::read_local_revision_file() const
{
const std::filesystem::path revision_file_path = this->version_file_;
std::string data;
if (!utils::io::read_file(revision_file_path.string(), &data) || data.empty())
{
console::warn("Could not load \"%s\"", revision_file_path.string().c_str());
return {};
}
rapidjson::Document doc{};
const rapidjson::ParseResult result = doc.Parse(data);
if (!result || !doc.IsObject())
{
console::error("Could not parse \"%s\"", revision_file_path.string().c_str());
return {};
}
if (!doc.HasMember("version") || !doc["version"].IsString())
{
console::error("\"%s\" contains invalid data", revision_file_path.string().c_str());
return {};
}
return doc["version"].GetString();
}
bool file_updater::does_require_update(update_state& update_state, const std::string& local_version) const
{
console::info("Fetching tags from GitHub");
const auto raw_files_tag = get_release_tag(this->remote_tag_);
if (!raw_files_tag.has_value())
{
console::warn("Failed to reach GitHub. Aborting the update");
update_state.requires_update = false;
return update_state.requires_update;
}
update_state.requires_update = local_version != raw_files_tag.value();
update_state.latest_tag = raw_files_tag.value();
console::info("Got release tag \"%s\". Requires updating: %s", raw_files_tag.value().c_str(), update_state.requires_update ? "Yes" : "No");
return update_state.requires_update;
}
void file_updater::create_version_file(const std::string& revision_version) const
{
console::info("Creating version file \"%s\". Revision is \"%s\"", this->version_file_.c_str(), revision_version.c_str());
rapidjson::Document doc{};
doc.SetObject();
doc.AddMember("version", revision_version, doc.GetAllocator());
rapidjson::StringBuffer buffer{};
rapidjson::Writer<rapidjson::StringBuffer, rapidjson::Document::EncodingType, rapidjson::ASCII<>>
writer(buffer);
doc.Accept(writer);
const std::string json(buffer.GetString(), buffer.GetLength());
if (utils::io::write_file(this->version_file_.string(), json))
{
console::info("File \"%s\" was created successfully", this->version_file_.string().c_str());
return;
}
console::error("Error while writing file \"%s\"", this->version_file_.string().c_str());
}
bool file_updater::update_file(const std::string& url) const
{
console::info("Downloading %s", url.c_str());
const auto data = utils::http::get_data(url, {});
if (!data)
{
console::error("Failed to download %s", url.c_str());
return false;
}
if (data.value().empty())
{
console::error("The data buffer returned by Curl is empty");
return false;
}
// Download the files in the working directory, move them later.
const auto out_file = std::filesystem::current_path() / this->out_name_;
console::info("Writing file to \"%s\"", out_file.string().c_str());
if (!utils::io::write_file(out_file.string(), data.value(), false))
{
console::error("Error while writing file \"%s\"", out_file.string().c_str());
return false;
}
console::info("Done updating file \"%s\"", out_file.string().c_str());
return true;
}
// Not a fan of using exceptions here. Once C++23 is more widespread I'd like to use <expected>
bool file_updater::deploy_files() const
{
const auto out_dir = std::filesystem::current_path() / ".out";
assert(utils::io::file_exists(this->out_name_.string()));
// Always try to cleanup
const auto _ = gsl::finally([this, &out_dir]() -> void
{
utils::io::remove_file(this->out_name_.string());
std::error_code ec;
std::filesystem::remove_all(out_dir, ec);
});
try
{
utils::io::create_directory(out_dir);
utils::compression::zip::archive::decompress(this->out_name_.string(), out_dir);
}
catch (const std::exception& ex)
{
console::error("Get error \"%s\" while decompressing \"%s\"", ex.what(), this->out_name_.string().c_str());
return false;
}
console::info("\"%s\" was decompressed. Removing files that must be skipped", this->out_name_.string().c_str());
this->skip_files(out_dir);
console::info("Deploying files to \"%s\"", this->base_.string().c_str());
utils::io::copy_folder(out_dir, this->base_);
return true;
}
void file_updater::cleanup_directories() const
{
console::log("Cleaning up directories");
std::for_each(this->cleanup_directories_.begin(), this->cleanup_directories_.end(), [](const auto& dir)
{
std::error_code ec;
std::filesystem::remove_all(dir, ec);
console::log("Removed directory \"%s\"", dir.string().c_str());
});
}
void file_updater::skip_files(const std::filesystem::path& target_dir) const
{
console::log("Skipping files");
std::for_each(this->skip_files_.begin(), this->skip_files_.end(), [&target_dir](const auto& file)
{
const auto target_file = target_dir / file;
utils::io::remove_file(target_file.string());
console::log("Removed file \"%s\"", target_file.string().c_str());
});
}
}

View File

@ -0,0 +1,46 @@
#pragma once
namespace updater
{
class file_updater
{
public:
file_updater(std::string name, std::filesystem::path base, std::filesystem::path out_name, std::filesystem::path version_file, std::string remote_tag, std::string remote_download);
[[nodiscard]] bool update_if_necessary() const;
void add_dir_to_clean(const std::string& dir);
void add_file_to_skip(const std::string& file);
private:
struct update_state
{
bool requires_update = false;
std::string latest_tag;
};
std::string name_;
std::filesystem::path base_;
std::filesystem::path out_name_;
std::filesystem::path version_file_;
std::string remote_tag_;
std::string remote_download_;
// Directories to cleanup
std::vector<std::filesystem::path> cleanup_directories_;
// Files to skip
std::vector<std::string> skip_files_;
[[nodiscard]] std::string read_local_revision_file() const;
[[nodiscard]] bool does_require_update(update_state& update_state, const std::string& local_version) const;
void create_version_file(const std::string& revision_version) const;
[[nodiscard]] bool update_file(const std::string& url) const;
[[nodiscard]] bool deploy_files() const;
void cleanup_directories() const;
void skip_files(const std::filesystem::path& target_dir) const;
};
}

37
src/updater/updater.cpp Normal file
View File

@ -0,0 +1,37 @@
#include <std_include.hpp>
#include <console.hpp>
#include "file_updater.hpp"
#include "updater.hpp"
#include <utils/properties.hpp>
#define IW4X_VERSION_FILE "iw4x-version.json"
#define IW4X_RAW_FILES_UPDATE_FILE "release.zip"
#define IW4X_RAW_FILES_UPDATE_URL "https://github.com/iw4x/iw4x-rawfiles/releases/latest/download/" IW4X_RAW_FILES_UPDATE_FILE
#define IW4X_RAW_FILES_TAGS "https://api.github.com/repos/iw4x/iw4x-rawfiles/releases/latest"
namespace updater
{
int update_iw4x()
{
const auto iw4_install = utils::properties::load("iw4-install");
if (!iw4_install)
{
console::error("Failed to load the properties file");
return false;
}
const auto& base = iw4_install.value();
file_updater file_updater{ "IW4x", base, IW4X_RAW_FILES_UPDATE_FILE, IW4X_VERSION_FILE, IW4X_RAW_FILES_TAGS, IW4X_RAW_FILES_UPDATE_URL };
file_updater.add_dir_to_clean("iw4x");
file_updater.add_dir_to_clean("zone");
file_updater.add_file_to_skip("iw4sp.exe");
return file_updater.update_if_necessary();
}
}

6
src/updater/updater.hpp Normal file
View File

@ -0,0 +1,6 @@
#pragma once
namespace updater
{
int update_iw4x();
}

280
src/utils/compression.cpp Normal file
View File

@ -0,0 +1,280 @@
#include <std_include.hpp>
#include "compression.hpp"
#include <unzip.h>
#include <zlib.h>
#include <zip.h>
#include <gsl/gsl>
#include "io.hpp"
#include "string.hpp"
#ifndef MAX_PATH
#define MAX_PATH 256
#endif
namespace utils::compression
{
namespace zlib
{
namespace
{
class zlib_stream
{
public:
zlib_stream()
{
memset(&stream_, 0, sizeof(stream_));
valid_ = inflateInit(&stream_) == Z_OK;
}
zlib_stream(zlib_stream&&) = delete;
zlib_stream(const zlib_stream&) = delete;
zlib_stream& operator=(zlib_stream&&) = delete;
zlib_stream& operator=(const zlib_stream&) = delete;
~zlib_stream()
{
if (valid_)
{
inflateEnd(&stream_);
}
}
z_stream& get()
{
return stream_; //
}
bool is_valid() const
{
return valid_;
}
private:
bool valid_{false};
z_stream stream_{};
};
}
std::string decompress(const std::string& data)
{
std::string buffer{};
zlib_stream stream_container{};
if (!stream_container.is_valid())
{
return {};
}
int ret{};
size_t offset = 0;
static thread_local uint8_t dest[CHUNK] = {0};
auto& stream = stream_container.get();
do
{
const auto input_size = std::min(sizeof(dest), data.size() - offset);
stream.avail_in = static_cast<uInt>(input_size);
stream.next_in = reinterpret_cast<const Bytef*>(data.data()) + offset;
offset += stream.avail_in;
do
{
stream.avail_out = sizeof(dest);
stream.next_out = dest;
ret = inflate(&stream, Z_NO_FLUSH);
if (ret != Z_OK && ret != Z_STREAM_END)
{
return {};
}
buffer.insert(buffer.end(), dest, dest + sizeof(dest) - stream.avail_out);
}
while (stream.avail_out == 0);
}
while (ret != Z_STREAM_END);
return buffer;
}