hyprpm: move to system directories for storing plugins (#10211)
Some checks are pending
Build Hyprland / Build Hyprland (Arch) (push) Waiting to run
Build Hyprland / Build Hyprland with Meson (Arch) (push) Waiting to run
Build Hyprland / Build Hyprland without precompiled headers (Arch) (push) Waiting to run
Build Hyprland / Build Hyprland in pure Wayland (Arch) (push) Waiting to run
Build Hyprland / Code Style (Arch) (push) Waiting to run
Nix (CI) / update-inputs (push) Waiting to run
Nix (CI) / build (push) Waiting to run
Security Checks / Flawfinder Checks (push) Waiting to run

This commit is contained in:
Vaxry 2025-05-01 18:00:26 +02:00 committed by GitHub
parent b5ef049ea1
commit 858c0e26d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 278 additions and 74 deletions

View file

@ -109,7 +109,7 @@ find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION})
pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=0.8.0)
pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=0.3.2)
pkg_check_modules(hyprcursor_dep REQUIRED IMPORTED_TARGET hyprcursor>=0.1.7)
pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=0.6.0)
pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=0.7.0)
pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=0.1.1)
string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION})

View file

@ -9,7 +9,7 @@ file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
set(CMAKE_CXX_STANDARD 23)
pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.2.4)
pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.7.0)
find_package(glaze QUIET)
if (NOT glaze_FOUND)

View file

@ -1,22 +1,15 @@
#include "DataState.hpp"
#include <sys/stat.h>
#include <toml++/toml.hpp>
#include <print>
#include <fstream>
#include "PluginManager.hpp"
#include "../helpers/Die.hpp"
#include "../helpers/Sys.hpp"
#include "../helpers/StringUtils.hpp"
std::filesystem::path DataState::getDataStatePath() {
const auto HOME = getenv("HOME");
if (!HOME) {
std::println(stderr, "DataState: no $HOME");
throw std::runtime_error("no $HOME");
return "";
}
const auto XDG_DATA_HOME = getenv("XDG_DATA_HOME");
if (XDG_DATA_HOME)
return std::filesystem::path{XDG_DATA_HOME} / "hyprpm";
return std::filesystem::path{HOME} / ".local/share/hyprpm";
return std::filesystem::path("/var/cache/hyprpm/" + g_pPluginManager->m_szUsername);
}
std::string DataState::getHeadersPath() {
@ -41,21 +34,25 @@ std::vector<std::filesystem::path> DataState::getPluginStates() {
}
void DataState::ensureStateStoreExists() {
const auto PATH = getDataStatePath();
if (!std::filesystem::exists(PATH))
std::filesystem::create_directories(PATH);
if (!std::filesystem::exists(getHeadersPath()))
std::filesystem::create_directories(getHeadersPath());
std::error_code ec;
if (!std::filesystem::exists(getHeadersPath(), ec) || ec) {
std::println("{}", infoString("The hyprpm state store doesn't exist. Creating now..."));
if (!std::filesystem::exists("/var/cache/hyprpm/", ec) || ec)
NSys::runAsSuperuser("mkdir -p -m 755 '/var/cache/hyprpm/'");
if (!std::filesystem::exists(getDataStatePath(), ec) || ec)
NSys::runAsSuperuser("mkdir -p -m 755 '" + getDataStatePath().string() + "'");
NSys::runAsSuperuser("mkdir -p -m 755 '" + getHeadersPath() + "'");
}
}
void DataState::addNewPluginRepo(const SPluginRepository& repo) {
ensureStateStoreExists();
const auto PATH = getDataStatePath() / repo.name;
const auto PATH = getDataStatePath() / repo.name;
std::filesystem::create_directories(PATH);
std::error_code ec;
if (!std::filesystem::exists(PATH, ec) || ec)
NSys::runAsSuperuser("mkdir -p -m 755 '" + PATH.string() + "'");
// clang-format off
auto DATA = toml::table{
{"repository", toml::table{
@ -68,9 +65,9 @@ void DataState::addNewPluginRepo(const SPluginRepository& repo) {
for (auto const& p : repo.plugins) {
const auto filename = p.name + ".so";
// copy .so to the good place
// copy .so to the good place and chmod 755
if (std::filesystem::exists(p.filename))
std::filesystem::copy_file(p.filename, PATH / filename);
NSys::runAsSuperuser("cp '" + p.filename + "' '" + (PATH / filename).string() + "' && chmod 755 '" + (PATH / filename).string() + "'");
DATA.emplace(p.name, toml::table{
{"filename", filename},
@ -80,16 +77,16 @@ void DataState::addNewPluginRepo(const SPluginRepository& repo) {
}
// clang-format on
std::ofstream ofs(PATH / "state.toml", std::ios::trunc);
ofs << DATA;
ofs.close();
std::stringstream ss;
ss << DATA;
NSys::runAsSuperuser("cat << EOF > " + (PATH / "state.toml").string() + "\n" + ss.str() + "\nEOF");
NSys::runAsSuperuser("chmod 644 '" + (PATH / "state.toml").string() + "'");
}
bool DataState::pluginRepoExists(const std::string& urlOrName) {
ensureStateStoreExists();
const auto PATH = getDataStatePath();
for (const auto& stateFile : getPluginStates()) {
const auto STATE = toml::parse_file(stateFile.c_str());
const auto NAME = STATE["repository"]["name"].value_or("");
@ -105,8 +102,6 @@ bool DataState::pluginRepoExists(const std::string& urlOrName) {
void DataState::removePluginRepo(const std::string& urlOrName) {
ensureStateStoreExists();
const auto PATH = getDataStatePath();
for (const auto& stateFile : getPluginStates()) {
const auto STATE = toml::parse_file(stateFile.c_str());
const auto NAME = STATE["repository"]["name"].value_or("");
@ -122,7 +117,13 @@ void DataState::removePluginRepo(const std::string& urlOrName) {
g_pPluginManager->loadUnloadPlugin(std::filesystem::absolute(file.path()), false);
}
std::filesystem::remove_all(stateFile.parent_path());
const auto PATH = stateFile.parent_path().string();
if (!PATH.starts_with("/var/cache/hyprpm") || PATH.contains('\''))
return; // WTF?
// scary!
NSys::runAsSuperuser("rm -r '" + PATH + "'");
return;
}
}
@ -131,9 +132,11 @@ void DataState::removePluginRepo(const std::string& urlOrName) {
void DataState::updateGlobalState(const SGlobalState& state) {
ensureStateStoreExists();
const auto PATH = getDataStatePath();
const auto PATH = getDataStatePath();
std::filesystem::create_directories(PATH);
std::error_code ec;
if (!std::filesystem::exists(PATH, ec) || ec)
NSys::runAsSuperuser("mkdir -p -m 755 '" + PATH.string() + "'");
// clang-format off
auto DATA = toml::table{
{"state", toml::table{
@ -143,17 +146,20 @@ void DataState::updateGlobalState(const SGlobalState& state) {
};
// clang-format on
std::ofstream ofs(PATH / "state.toml", std::ios::trunc);
ofs << DATA;
ofs.close();
std::stringstream ss;
ss << DATA;
NSys::runAsSuperuser("cat << EOF > " + (PATH / "state.toml").string() + "\n" + ss.str() + "\nEOF");
NSys::runAsSuperuser("chmod 644 '" + (PATH / "state.toml").string() + "'");
}
SGlobalState DataState::getGlobalState() {
ensureStateStoreExists();
const auto stateFile = getDataStatePath() / "state.toml";
const auto stateFile = getDataStatePath() / "state.toml";
if (!std::filesystem::exists(stateFile))
std::error_code ec;
if (!std::filesystem::exists(stateFile, ec) || ec)
return SGlobalState{};
auto DATA = toml::parse_file(stateFile.c_str());
@ -168,8 +174,6 @@ SGlobalState DataState::getGlobalState() {
std::vector<SPluginRepository> DataState::getAllRepositories() {
ensureStateStoreExists();
const auto PATH = getDataStatePath();
std::vector<SPluginRepository> repos;
for (const auto& stateFile : getPluginStates()) {
const auto STATE = toml::parse_file(stateFile.c_str());
@ -205,8 +209,6 @@ std::vector<SPluginRepository> DataState::getAllRepositories() {
bool DataState::setPluginEnabled(const std::string& name, bool enabled) {
ensureStateStoreExists();
const auto PATH = getDataStatePath();
for (const auto& stateFile : getPluginStates()) {
const auto STATE = toml::parse_file(stateFile.c_str());
for (const auto& [key, val] : STATE) {
@ -224,9 +226,11 @@ bool DataState::setPluginEnabled(const std::string& name, bool enabled) {
auto modifiedState = STATE;
(*modifiedState[key].as_table()).insert_or_assign("enabled", enabled);
std::ofstream state(stateFile, std::ios::trunc);
state << modifiedState;
state.close();
std::stringstream ss;
ss << modifiedState;
NSys::runAsSuperuser("cat << EOF > " + stateFile.string() + "\n" + ss.str() + "\nEOF");
NSys::runAsSuperuser("chmod 644 '" + stateFile.string() + "'");
return true;
}
@ -234,3 +238,17 @@ bool DataState::setPluginEnabled(const std::string& name, bool enabled) {
return false;
}
void DataState::purgeAllCache() {
std::error_code ec;
if (!std::filesystem::exists(getDataStatePath()) && !ec) {
std::println("{}", infoString("Nothing to do"));
return;
}
const auto PATH = getDataStatePath().string();
if (PATH.contains('\''))
return;
// scary!
NSys::runAsSuperuser("rm -r '" + PATH + "'");
}

View file

@ -18,6 +18,7 @@ namespace DataState {
void removePluginRepo(const std::string& urlOrName);
bool pluginRepoExists(const std::string& urlOrName);
void updateGlobalState(const SGlobalState& state);
void purgeAllCache();
SGlobalState getGlobalState();
bool setPluginEnabled(const std::string& name, bool enabled);
std::vector<SPluginRepository> getAllRepositories();

View file

@ -5,6 +5,8 @@
#include "Manifest.hpp"
#include "DataState.hpp"
#include "HyprlandSocket.hpp"
#include "../helpers/Sys.hpp"
#include "../helpers/Die.hpp"
#include <cstdio>
#include <iostream>
@ -50,6 +52,13 @@ static std::string getTempRoot() {
return STR;
}
CPluginManager::CPluginManager() {
if (NSys::isSuperuser())
Debug::die("Don't run hyprpm as a superuser.");
m_szUsername = getpwuid(NSys::getUID())->pw_name;
}
SHyprlandVersion CPluginManager::getHyprlandVersion(bool running) {
static bool onceRunning = false;
static bool onceInstalled = false;
@ -275,6 +284,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
if (HEADERSSTATUS != HEADERS_OK) {
std::println("\n{}", headerError(HEADERSSTATUS));
std::println("\n{}", infoString("if the problem persists, try running hyprpm purge-cache."));
return false;
}
@ -550,13 +560,20 @@ bool CPluginManager::updateHeaders(bool force) {
progress.m_szCurrentMessage = "Installing sources";
progress.print();
const std::string& cmd =
std::format("sed -i -e \"s#PREFIX = /usr/local#PREFIX = {}#\" {}/Makefile && cd {} && make installheaders", DataState::getHeadersPath(), WORKINGDIR, WORKINGDIR);
std::string cmd = std::format("sed -i -e \"s#PREFIX = /usr/local#PREFIX = {}#\" {}/Makefile", DataState::getHeadersPath(), WORKINGDIR);
if (m_bVerbose)
progress.printMessageAbove(verboseString("installation will run: {}", cmd));
progress.printMessageAbove(verboseString("prepare install will run: {}", cmd));
ret = execAndGet(cmd);
cmd = std::format("cd {} && make installheaders && chmod -R 644 {} && find {} -type d -exec chmod o+x {{}} \\;", WORKINGDIR, DataState::getHeadersPath(),
DataState::getHeadersPath());
if (m_bVerbose)
progress.printMessageAbove(verboseString("install will run as sudo: {}", cmd));
ret = NSys::runAsSuperuser(cmd);
if (m_bVerbose)
std::println("{}", verboseString("installer returned: {}", ret));
@ -570,9 +587,14 @@ bool CPluginManager::updateHeaders(bool force) {
progress.m_szCurrentMessage = "Done!";
progress.print();
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersHashCompiled = HLVER.hash;
DataState::updateGlobalState(GLOBALSTATE);
std::print("\n");
} else {
progress.printMessageAbove(failureString("failed to install headers with error code {} ({})", (int)HEADERSVALID, headerErrorShort(HEADERSVALID)));
progress.printMessageAbove(infoString("if the problem persists, try running hyprpm purge-cache."));
progress.m_iSteps = 5;
progress.m_szCurrentMessage = "Failed";
progress.print();
@ -884,7 +906,8 @@ bool CPluginManager::loadUnloadPlugin(const std::string& path, bool load) {
auto HLVER = getHyprlandVersion(true);
if (state.headersHashCompiled != HLVER.hash) {
std::println("{}", infoString("Running Hyprland version differs from plugin state, please restart Hyprland."));
if (load)
std::println("{}", infoString("Running Hyprland version ({}) differs from plugin state ({}), please restart Hyprland.", HLVER.hash, state.headersHashCompiled));
return false;
}

View file

@ -40,6 +40,8 @@ struct SHyprlandVersion {
class CPluginManager {
public:
CPluginManager();
bool addNewPluginRepo(const std::string& url, const std::string& rev);
bool removePluginRepo(const std::string& urlOrName);
@ -62,7 +64,7 @@ class CPluginManager {
bool m_bVerbose = false;
bool m_bNoShallow = false;
std::string m_szCustomHlUrl;
std::string m_szCustomHlUrl, m_szUsername;
// will delete recursively if exists!!
bool createSafeDirectory(const std::string& path);

View file

@ -0,0 +1,15 @@
#pragma once
#include <format>
#include <iostream>
// NOLINTNEXTLINE
namespace Debug {
template <typename... Args>
void die(std::format_string<Args...> fmt, Args&&... args) {
const std::string logMsg = std::vformat(fmt.get(), std::make_format_args(args...));
std::cout << "\n[ERR] " << logMsg << "\n";
exit(1);
}
};

110
hyprpm/src/helpers/Sys.cpp Normal file
View file

@ -0,0 +1,110 @@
#include "Sys.hpp"
#include "Die.hpp"
#include "StringUtils.hpp"
#include <pwd.h>
#include <unistd.h>
#include <print>
#include <filesystem>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/string/VarList.hpp>
using namespace Hyprutils::OS;
using namespace Hyprutils::String;
static const std::vector<const char*> SUPERUSER_BINARIES = {
"sudo",
"doas",
"run0",
};
static bool executableExistsInPath(const std::string& exe) {
if (!getenv("PATH"))
return false;
static CVarList paths(getenv("PATH"), 0, ':', true);
for (auto& p : paths) {
std::string path = p + std::string{"/"} + exe;
std::error_code ec;
if (!std::filesystem::exists(path, ec) || ec)
continue;
if (!std::filesystem::is_regular_file(path, ec) || ec)
continue;
auto stat = std::filesystem::status(path, ec);
if (ec)
continue;
auto perms = stat.permissions();
return std::filesystem::perms::none != (perms & std::filesystem::perms::others_exec);
}
return false;
}
static std::pair<std::string, int> execAndGet(std::string cmd, bool noRedirect = false) {
if (!noRedirect)
cmd += " 2>&1";
CProcess proc("/bin/sh", {"-c", cmd});
if (!proc.runSync())
return {"error", 1};
return {proc.stdOut(), proc.exitCode()};
}
int NSys::getUID() {
const auto UID = getuid();
const auto PWUID = getpwuid(UID);
return PWUID ? PWUID->pw_uid : UID;
}
int NSys::getEUID() {
const auto UID = geteuid();
const auto PWUID = getpwuid(UID);
return PWUID ? PWUID->pw_uid : UID;
}
bool NSys::isSuperuser() {
return getuid() != geteuid() || !geteuid();
}
std::string NSys::runAsSuperuser(const std::string& cmd) {
for (const auto& SB : SUPERUSER_BINARIES) {
if (!executableExistsInPath(SB))
continue;
const auto RESULT = execAndGet(std::string{SB} + " /bin/sh -c \"" + cmd + "\"", true);
if (RESULT.second != 0)
Debug::die("Failed to run a command as sudo. This could be due to an invalid password, or a hyprpm bug.");
return RESULT.first;
}
Debug::die("Failed to find a superuser binary. Supported: sudo, doas, run0.");
return "";
}
void NSys::cacheSudo() {
// "caches" the sudo so that the prompt later doesn't pop up in a weird spot
// sudo will not ask us again for a moment
runAsSuperuser("echo e > /dev/null");
}
void NSys::dropSudo() {
for (const auto& SB : SUPERUSER_BINARIES) {
if (!executableExistsInPath(SB))
continue;
if (SB == std::string_view{"sudo"})
execAndGet("sudo -k");
else
std::println("{}", infoString("Don't know how to drop timestamp for {}, ignoring.", SB));
return;
}
}

View file

@ -0,0 +1,12 @@
#pragma once
#include <string>
namespace NSys {
bool isSuperuser();
int getUID();
int getEUID();
std::string runAsSuperuser(const std::string& cmd);
void cacheSudo();
void dropSudo();
};

View file

@ -2,34 +2,37 @@
#include "helpers/StringUtils.hpp"
#include "core/PluginManager.hpp"
#include "core/DataState.hpp"
#include "helpers/Sys.hpp"
#include <cstdio>
#include <vector>
#include <string>
#include <print>
#include <chrono>
#include <thread>
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::Utils;
constexpr std::string_view HELP = R"#(┏ hyprpm, a Hyprland Plugin Manager
add [url] [git rev] Install a new plugin repository from git. Git revision
add [url] [git rev] Install a new plugin repository from git. Git revision.
is optional, when set, commit locks are ignored.
remove [url/name] Remove an installed plugin repository
enable [name] Enable a plugin
disable [name] Disable a plugin
update Check and update all plugins if needed
remove [url/name] Remove an installed plugin repository.
enable [name] Enable a plugin.
disable [name] Disable a plugin.
update Check and update all plugins if needed.
reload Reload hyprpm state. Ensure all enabled plugins are loaded.
list List all installed plugins
list List all installed plugins.
purge-cache Remove the entire hyprpm cache, built plugins, hyprpm settings and headers.
Flags:
--notify | -n Send a hyprland notification for important events (including both successes and fail events)
--notify-fail | -nn Send a hyprland notification for fail events only
--help | -h Show this menu
--verbose | -v Enable too much logging
--force | -f Force an operation ignoring checks (e.g. update -f)
--no-shallow | -s Disable shallow cloning of Hyprland sources
--hl-url | Pass a custom hyprland source url
--notify | -n Send a hyprland notification for important events (including both successes and fail events).
--notify-fail | -nn Send a hyprland notification for fail events only.
--help | -h Show this menu.
--verbose | -v Enable too much logging.
--force | -f Force an operation ignoring checks (e.g. update -f).
--no-shallow | -s Disable shallow cloning of Hyprland sources.
--hl-url | Pass a custom hyprland source url.
)#";
@ -96,9 +99,11 @@ int main(int argc, char** argv, char** envp) {
}
std::string rev = "";
if (command.size() >= 3) {
if (command.size() >= 3)
rev = command[2];
}
NSys::cacheSudo();
CScopeGuard x([] { NSys::dropSudo(); });
return g_pPluginManager->addNewPluginRepo(command[1], rev) ? 0 : 1;
} else if (command[0] == "remove") {
@ -107,10 +112,17 @@ int main(int argc, char** argv, char** envp) {
return 1;
}
NSys::cacheSudo();
CScopeGuard x([] { NSys::dropSudo(); });
return g_pPluginManager->removePluginRepo(command[1]) ? 0 : 1;
} else if (command[0] == "update") {
bool headersValid = g_pPluginManager->headersValid() == HEADERS_OK;
bool headers = g_pPluginManager->updateHeaders(force);
NSys::cacheSudo();
CScopeGuard x([] { NSys::dropSudo(); });
bool headersValid = g_pPluginManager->headersValid() == HEADERS_OK;
bool headers = g_pPluginManager->updateHeaders(force);
if (headers) {
const auto HLVER = g_pPluginManager->getHyprlandVersion(false);
auto GLOBALSTATE = DataState::getGlobalState();
@ -141,7 +153,10 @@ int main(int argc, char** argv, char** envp) {
return 1;
}
auto ret = g_pPluginManager->ensurePluginsLoadState();
NSys::cacheSudo();
CScopeGuard x([] { NSys::dropSudo(); });
auto ret = g_pPluginManager->ensurePluginsLoadState();
if (ret == LOADSTATE_HYPRLAND_UPDATED)
g_pPluginManager->notify(ICON_INFO, 0, 10000, "[hyprpm] Enabled plugin, but Hyprland was updated. Please restart Hyprland.");
@ -159,7 +174,11 @@ int main(int argc, char** argv, char** envp) {
return 1;
}
auto ret = g_pPluginManager->ensurePluginsLoadState();
NSys::cacheSudo();
CScopeGuard x([] { NSys::dropSudo(); });
auto ret = g_pPluginManager->ensurePluginsLoadState();
if (ret != LOADSTATE_OK)
return 1;
} else if (command[0] == "reload") {
@ -181,6 +200,10 @@ int main(int argc, char** argv, char** envp) {
} else if (notify && !notifyFail) {
g_pPluginManager->notify(ICON_OK, 0, 4000, "[hyprpm] Loaded plugins");
}
} else if (command[0] == "purge-cache") {
NSys::cacheSudo();
CScopeGuard x([] { NSys::dropSudo(); });
DataState::purgeAllCache();
} else if (command[0] == "list") {
g_pPluginManager->listAllPlugins();
} else {

View file

@ -35,7 +35,7 @@ aquamarine = dependency('aquamarine', version: '>=0.8.0')
hyprcursor = dependency('hyprcursor', version: '>=0.1.7')
hyprgraphics = dependency('hyprgraphics', version: '>= 0.1.1')
hyprlang = dependency('hyprlang', version: '>= 0.3.2')
hyprutils = dependency('hyprutils', version: '>= 0.6.0')
hyprutils = dependency('hyprutils', version: '>= 0.7.0')
aquamarine_version_list = aquamarine.version().split('.')
add_project_arguments(['-DAQUAMARINE_VERSION="@0@"'.format(aquamarine.version())], language: 'cpp')
add_project_arguments(['-DAQUAMARINE_VERSION_MAJOR=@0@'.format(aquamarine_version_list.get(0))], language: 'cpp')