#include "PackageManager.h" #include #include #include #include #pragma region Local Path Helpers namespace { fs::path normalizePath(const fs::path& p) { std::error_code ec; fs::path canonical = fs::weakly_canonical(p, ec); if (ec) { canonical = fs::absolute(p, ec); } return canonical.lexically_normal(); } bool containsPath(const std::vector& haystack, const fs::path& needle) { std::string target = normalizePath(needle).string(); for (const auto& entry : haystack) { if (normalizePath(entry).string() == target) { return true; } } return false; } bool isGitRepo(const fs::path& root) { std::error_code ec; fs::path dotGit = root / ".git"; if (fs::exists(dotGit, ec)) { return true; } return false; } fs::path guessIncludeDir(const fs::path& repoRoot, const std::string& includeRel) { if (!includeRel.empty()) { fs::path candidate = normalizePath(repoRoot / includeRel); if (fs::exists(candidate) && fs::is_directory(candidate)) { return candidate; } } const char* defaults[] = {"include", "Include", "includes", "inc", "Inc"}; for (const char* name : defaults) { fs::path candidate = normalizePath(repoRoot / name); if (fs::exists(candidate) && fs::is_directory(candidate)) { return candidate; } } for (const auto& entry : fs::directory_iterator(repoRoot)) { if (entry.is_directory()) { return normalizePath(entry.path()); } } return normalizePath(repoRoot); } } // namespace #pragma endregion #pragma region Lifecycle PackageManager::PackageManager() { buildRegistry(); } void PackageManager::setProjectRoot(const fs::path& root) { buildRegistry(); projectRoot = root; manifestPath = projectRoot / "packages.modu"; loadManifest(); } #pragma endregion #pragma region Install / Remove bool PackageManager::isInstalled(const std::string& id) const { return std::find(installedIds.begin(), installedIds.end(), id) != installedIds.end(); } bool PackageManager::install(const std::string& id) { lastError.clear(); const PackageInfo* pkg = findPackage(id); if (!pkg) { lastError = "Unknown package: " + id; return false; } if (isInstalled(id)) { return true; } installedIds.push_back(id); saveManifest(); return true; } bool PackageManager::remove(const std::string& id) { lastError.clear(); const PackageInfo* pkg = findPackage(id); if (!pkg) { lastError = "Unknown package: " + id; return false; } if (pkg->builtIn) { lastError = "Cannot remove built-in packages."; return false; } if (pkg->external) { std::string log; if (isGitRepo(projectRoot)) { std::error_code ec; fs::path relPath = fs::relative(pkg->localPath, projectRoot, ec); std::string rel = (!ec ? relPath : pkg->localPath).generic_string(); std::string deinitCmd = "git -C \"" + projectRoot.string() + "\" submodule deinit -f \"" + rel + "\""; runCommand(deinitCmd, log); // best-effort std::string rmCmd = "git -C \"" + projectRoot.string() + "\" rm -f \"" + rel + "\""; if (!runCommand(rmCmd, log)) { lastError = "Failed to remove submodule. Git log:\n" + log; return false; } } else { std::error_code ec; fs::remove_all(pkg->localPath, ec); if (ec) { lastError = "Failed to remove package folder: " + ec.message(); return false; } } } auto it = std::remove(installedIds.begin(), installedIds.end(), id); if (it == installedIds.end()) { return true; } installedIds.erase(it, installedIds.end()); registry.erase(std::remove_if(registry.begin(), registry.end(), [&](const PackageInfo& p){ return p.id == id && p.external; }), registry.end()); saveManifest(); return true; } #pragma endregion #pragma region Build Config void PackageManager::applyToBuildConfig(ScriptBuildConfig& config) const { std::unordered_set defineSet(config.defines.begin(), config.defines.end()); std::unordered_set linuxLibSet(config.linuxLinkLibs.begin(), config.linuxLinkLibs.end()); std::unordered_set winLibSet(config.windowsLinkLibs.begin(), config.windowsLinkLibs.end()); for (const auto& id : installedIds) { const PackageInfo* pkg = findPackage(id); if (!pkg) continue; for (const auto& dir : pkg->includeDirs) { if (!containsPath(config.includeDirs, dir)) { config.includeDirs.push_back(normalizePath(dir)); } } for (const auto& def : pkg->defines) { if (defineSet.insert(def).second) { config.defines.push_back(def); } } for (const auto& lib : pkg->linuxLibs) { if (linuxLibSet.insert(lib).second) { config.linuxLinkLibs.push_back(lib); } } for (const auto& lib : pkg->windowsLibs) { if (winLibSet.insert(lib).second) { config.windowsLinkLibs.push_back(lib); } } } } #pragma endregion #pragma region Registry void PackageManager::buildRegistry() { registry.clear(); fs::path engineRoot = fs::current_path(); auto add = [this](PackageInfo info) { for (auto& dir : info.includeDirs) { dir = normalizePath(dir); } registry.push_back(std::move(info)); }; PackageInfo engineCore; engineCore.id = "engine-core"; engineCore.name = "Engine Core"; engineCore.description = "Modularity engine headers and common utilities"; engineCore.builtIn = true; engineCore.includeDirs = { engineRoot / "src", engineRoot / "include", engineRoot / "src/ThirdParty", engineRoot / "src/ThirdParty/glm", engineRoot / "src/ThirdParty/glad" }; engineCore.linuxLibs = {"pthread", "dl"}; engineCore.windowsLibs = {"User32.lib", "Advapi32.lib"}; add(engineCore); PackageInfo glm; glm.id = "glm"; glm.name = "GLM Math"; glm.description = "Header-only GLM math library (bundled)"; glm.builtIn = false; // Count as installed instead of hidden built-in glm.includeDirs = { engineRoot / "src/ThirdParty/glm" }; add(glm); PackageInfo imgui; imgui.id = "imgui"; imgui.name = "Dear ImGui"; imgui.description = "Immediate-mode UI helpers for editor-time tools"; imgui.builtIn = false; imgui.includeDirs = { engineRoot / "src/ThirdParty/imgui", engineRoot / "src/ThirdParty/imgui/backends" }; add(imgui); PackageInfo imguizmo; imguizmo.id = "imguizmo"; imguizmo.name = "ImGuizmo"; imguizmo.description = "Gizmo/transform helpers used by the editor"; imguizmo.builtIn = false; imguizmo.includeDirs = { engineRoot / "src/ThirdParty/ImGuizmo" }; add(imguizmo); PackageInfo miniaudio; miniaudio.id = "miniaudio"; miniaudio.name = "miniaudio"; miniaudio.description = "Single-header audio helpers (bundled)"; miniaudio.builtIn = false; miniaudio.includeDirs = { engineRoot / "include/ThirdParty" }; add(miniaudio); } #pragma endregion #pragma region Manifest IO void PackageManager::loadManifest() { installedIds.clear(); for (const auto& pkg : registry) { if (!isInstalled(pkg.id)) { installedIds.push_back(pkg.id); } } if (manifestPath.empty()) return; if (!fs::exists(manifestPath)) { saveManifest(); return; } std::ifstream file(manifestPath); if (!file.is_open()) { lastError = "Unable to open package manifest."; return; } std::string line; while (std::getline(file, line)) { std::string cleaned = trim(line); if (cleaned.empty() || cleaned[0] == '#') continue; std::string id; if (cleaned.rfind("package=", 0) == 0) { id = cleaned.substr(8); if (!id.empty() && !isInstalled(id) && findPackage(id)) { installedIds.push_back(id); } continue; } if (cleaned.rfind("git=", 0) == 0) { auto payload = cleaned.substr(4); auto parts = split(payload, '|'); if (parts.size() < 4) continue; PackageInfo pkg; pkg.id = parts[0]; pkg.name = parts[1]; pkg.description = "External package from git"; pkg.external = true; pkg.gitUrl = parts[2]; fs::path relPath = parts[3]; pkg.localPath = normalizePath(projectRoot / relPath); std::vector includeTokens; if (parts.size() > 4) includeTokens = split(parts[4], ';'); for (const auto& inc : includeTokens) { if (inc.empty()) continue; pkg.includeDirs.push_back(normalizePath(projectRoot / inc)); } std::vector defTokens; if (parts.size() > 5) defTokens = split(parts[5], ';'); pkg.defines = defTokens; if (parts.size() > 6) pkg.linuxLibs = split(parts[6], ';'); if (parts.size() > 7) pkg.windowsLibs = split(parts[7], ';'); registry.push_back(pkg); if (!isInstalled(pkg.id)) { installedIds.push_back(pkg.id); } } } } void PackageManager::saveManifest() const { if (manifestPath.empty()) return; std::ofstream file(manifestPath); if (!file.is_open()) return; file << "# Modularity package manifest\n"; file << "# Add optional script-time dependencies here\n"; for (const auto& id : installedIds) { const PackageInfo* pkg = findPackage(id); if (!pkg) continue; if (!pkg->external) { if (!pkg->builtIn) { file << "package=" << id << "\n"; } continue; } // Persist external package metadata std::vector relIncludes; for (const auto& inc : pkg->includeDirs) { std::error_code ec; fs::path rel = fs::relative(inc, projectRoot, ec); relIncludes.push_back((!ec ? rel : inc).generic_string()); } file << "git=" << pkg->id << "|" << pkg->name << "|" << pkg->gitUrl << "|"; std::error_code ec; fs::path relPath = fs::relative(pkg->localPath, projectRoot, ec); file << ((!ec ? relPath : pkg->localPath).generic_string()) << "|"; file << join(relIncludes, ';') << "|"; file << join(pkg->defines, ';') << "|"; file << join(pkg->linuxLibs, ';') << "|"; file << join(pkg->windowsLibs, ';') << "\n"; } } #pragma endregion #pragma region Registry Lookup const PackageInfo* PackageManager::findPackage(const std::string& id) const { auto it = std::find_if(registry.begin(), registry.end(), [&](const PackageInfo& p) { return p.id == id; }); return it == registry.end() ? nullptr : &(*it); } bool PackageManager::isBuiltIn(const std::string& id) const { const PackageInfo* pkg = findPackage(id); return pkg && pkg->builtIn; } #pragma endregion #pragma region Utility Helpers std::string PackageManager::trim(const std::string& value) { size_t start = 0; while (start < value.size() && std::isspace(static_cast(value[start]))) start++; size_t end = value.size(); while (end > start && std::isspace(static_cast(value[end - 1]))) end--; return value.substr(start, end - start); } std::string PackageManager::slugify(const std::string& value) { std::string out; out.reserve(value.size()); for (char c : value) { if (std::isalnum(static_cast(c))) { out.push_back(static_cast(std::tolower(static_cast(c)))); } else if (c == '-' || c == '_') { out.push_back(c); } else if (std::isspace(static_cast(c))) { out.push_back('-'); } } if (out.empty()) out = "pkg"; return out; } bool PackageManager::runCommand(const std::string& command, std::string& output) { std::array buffer{}; FILE* pipe = popen(command.c_str(), "r"); if (!pipe) return false; while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { output += buffer.data(); } int rc = pclose(pipe); return rc == 0; } bool PackageManager::ensureProjectRoot() const { return !projectRoot.empty(); } std::vector PackageManager::split(const std::string& input, char delim) const { std::vector out; std::stringstream ss(input); std::string part; while (std::getline(ss, part, delim)) { out.push_back(part); } return out; } std::string PackageManager::join(const std::vector& vals, char delim) const { std::ostringstream oss; for (size_t i = 0; i < vals.size(); ++i) { if (i > 0) oss << delim; oss << vals[i]; } return oss.str(); } #pragma endregion #pragma region External Packages fs::path PackageManager::packagesFolder() const { fs::path newFolder = projectRoot / "Library" / "InstalledPackages"; if (fs::exists(newFolder) || fs::exists(projectRoot / "scripts.modu")) { return newFolder; } return projectRoot / "Packages"; } bool PackageManager::installGitPackage(const std::string& url, const std::string& nameHint, const std::string& includeRel, std::string& outId) { lastError.clear(); if (!ensureProjectRoot()) { lastError = "Project root not set."; return false; } if (url.empty()) { lastError = "Git URL is required."; return false; } std::string repoName = nameHint; size_t slash = url.find_last_of("/\\"); if (repoName.empty() && slash != std::string::npos) { repoName = url.substr(slash + 1); if (repoName.rfind(".git") != std::string::npos) { repoName = repoName.substr(0, repoName.size() - 4); } } std::string id = slugify(repoName); if (isInstalled(id)) { lastError = "Package already installed: " + id; return false; } fs::path dest = normalizePath(packagesFolder() / id); fs::create_directories(dest.parent_path()); std::string cmd; if (isGitRepo(projectRoot)) { std::error_code ec; fs::path relPath = fs::relative(dest, projectRoot, ec); std::string rel = (!ec ? relPath : dest).generic_string(); cmd = "git -C \"" + projectRoot.string() + "\" submodule add --force \"" + url + "\" \"" + rel + "\""; } else { cmd = "git clone \"" + url + "\" \"" + dest.string() + "\""; } std::string log; if (!runCommand(cmd, log)) { if (isGitRepo(projectRoot)) { lastError = "git submodule add failed:\n" + log; } else { lastError = "git clone failed:\n" + log; } return false; } PackageInfo pkg; pkg.id = id; pkg.name = repoName.empty() ? id : repoName; pkg.description = "External package from " + url; pkg.external = true; pkg.gitUrl = url; pkg.localPath = dest; pkg.includeDirs.push_back(guessIncludeDir(dest, includeRel)); registry.push_back(pkg); installedIds.push_back(id); saveManifest(); outId = id; return true; } bool PackageManager::checkGitStatus(const std::string& id, std::string& outStatus) { lastError.clear(); outStatus.clear(); const PackageInfo* pkg = findPackage(id); if (!pkg || !pkg->external) { lastError = "Package is not external or not found."; return false; } std::string fetchCmd = "git -C \"" + pkg->localPath.string() + "\" fetch --quiet"; runCommand(fetchCmd, outStatus); // ignore fetch failures here std::string statusCmd = "git -C \"" + pkg->localPath.string() + "\" status -sb"; if (!runCommand(statusCmd, outStatus)) { lastError = "Failed to read git status."; return false; } return true; } bool PackageManager::updateGitPackage(const std::string& id, std::string& outLog) { lastError.clear(); outLog.clear(); const PackageInfo* pkg = findPackage(id); if (!pkg || !pkg->external) { lastError = "Package is not external or not found."; return false; } std::string cmd = "git -C \"" + pkg->localPath.string() + "\" pull --ff-only"; if (!runCommand(cmd, outLog)) { lastError = "git pull failed."; return false; } return true; } #pragma endregion