fix: report why each FrameXML file failed, and imply the API fallback

Sixty-seven failures arrived with their reasons scattered across thousands
of log lines, and one broken script takes down every file referencing it,
so what matters is seeing them together. Collect a reason per file and
print them as one block; carry the real cause up through includes and
referenced scripts rather than naming the file that referenced them.

WOWEE_LOAD_FRAMEXML now implies WOWEE_LUA_API_FALLBACK. FrameXML cannot
get through its own load without it, so two switches where one is useless
alone was only a way to be handed a wall of failures for setting the
obvious one.

The compile checker counted a file it could not parse as neither pass nor
fail, reporting a clean run over files that never loaded.
This commit is contained in:
Kelsi
2026-08-01 13:34:52 -07:00
parent d4df731878
commit ab81f0e60b
5 changed files with 71 additions and 9 deletions

View File

@@ -63,6 +63,9 @@ private:
// addonName -> enabled. Absent means enabled (default on).
std::unordered_map<std::string, bool> addonEnabled_;
std::string frameXmlDir_;
/// Why the last loadXmlFile returned false, so a caller loading many files
/// can report the reasons together instead of leaving them scattered.
std::string lastXmlError_;
bool addonsLoaded_ = false;
static std::string enabledStatePath();
void loadEnabledState();

View File

@@ -28,6 +28,11 @@ public:
bool executeFile(const std::string& path);
bool executeString(const std::string& code);
/// Error from the last executeFile/executeString that returned false.
/// Lets a caller loading many files report them together rather than
/// leaving the reasons scattered through the log.
const std::string& lastError() const { return lastError_; }
void setGameHandler(game::GameHandler* handler);
void setLuaServices(const LuaServices& services);
@@ -70,6 +75,7 @@ private:
game::GameHandler* gameHandler_ = nullptr;
LuaServices luaServices_;
LuaErrorCallback luaErrorCallback_;
std::string lastError_;
void callFrameScript(uint32_t wid, const char* script, const char* arg = nullptr);

View File

@@ -10,6 +10,8 @@
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <utility>
#include <vector>
namespace fs = std::filesystem;
@@ -269,6 +271,11 @@ bool AddonManager::loadFrameXml(const std::string& frameXmlDir) {
toc->files.size(), " files from ", resolvedDir);
int lua = 0, xml = 0, failed = 0;
// Kept and printed together at the end. Spread through the log these are
// unreadable: the reasons land among thousands of other lines, and one
// broken script takes down every file that references it, so what matters
// is seeing them side by side and spotting the cause they share.
std::vector<std::pair<std::string, std::string>> failures;
for (const auto& filename : toc->files) {
std::string lower = filename;
for (char& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
@@ -276,6 +283,7 @@ bool AddonManager::loadFrameXml(const std::string& frameXmlDir) {
if (resolved.empty()) {
LOG_WARNING("FrameXML: ", filename, " is listed but not on disk");
++failed;
failures.emplace_back(filename, "listed in the manifest but not on disk");
continue;
}
const std::string full = resolved.string();
@@ -285,14 +293,29 @@ bool AddonManager::loadFrameXml(const std::string& frameXmlDir) {
// inherit from them. Following it is most of what makes this possible
// at all.
if (lower.size() >= 4 && lower.compare(lower.size() - 4, 4, ".lua") == 0) {
if (luaEngine_.executeFile(full)) ++lua; else { ++failed;
LOG_ERROR("FrameXML: ", filename, " failed"); }
if (luaEngine_.executeFile(full)) {
++lua;
} else {
++failed;
failures.emplace_back(filename, luaEngine_.lastError());
}
} else if (lower.size() >= 4 && lower.compare(lower.size() - 4, 4, ".xml") == 0) {
if (loadXmlFile(full, 0)) ++xml; else ++failed;
lastXmlError_.clear();
if (loadXmlFile(full, 0)) {
++xml;
} else {
++failed;
failures.emplace_back(filename, lastXmlError_.empty()
? "(no reason recorded)"
: lastXmlError_);
}
}
}
LOG_WARNING("FrameXML: ", lua, " Lua files and ", xml, " XML files loaded, ",
failed, " failed");
for (const auto& [file, why] : failures) {
LOG_WARNING("FrameXML: ", file, "", why);
}
return failed == 0;
}
@@ -301,12 +324,14 @@ bool AddonManager::loadXmlFile(const std::string& path, int depth) {
// until the stack gives out.
constexpr int kMaxDepth = 16;
if (depth > kMaxDepth) {
lastXmlError_ = "include nesting too deep";
LOG_ERROR("AddonManager: include nesting too deep at ", path);
return false;
}
std::ifstream in(path, std::ios::binary);
if (!in) {
lastXmlError_ = "not on disk";
LOG_WARNING("AddonManager: XML not found: ", path);
return false;
}
@@ -316,6 +341,7 @@ bool AddonManager::loadXmlFile(const std::string& path, int depth) {
ui::XmlNode root;
std::string error;
if (!ui::parseXml(buffer.str(), root, error)) {
lastXmlError_ = "XML parse: " + error;
LOG_ERROR("AddonManager: ", path, ": ", error);
return false;
}
@@ -340,17 +366,25 @@ bool AddonManager::loadXmlFile(const std::string& path, int depth) {
// Order matters and is not the order the emitter reports things in. Includes
// carry the templates a file inherits from, and scripts define the functions
// its handlers name, so both have to be in place before any frame is built.
// A file is only as loadable as what it pulls in, so the reason kept here is
// the first real one — the include or script that actually broke — rather
// than the name of whichever file happened to reference it.
for (const auto& inc : emitted.includeFiles) {
if (!loadXmlFile(sibling(inc).string(), depth + 1)) ok = false;
if (!loadXmlFile(sibling(inc).string(), depth + 1)) {
if (ok) lastXmlError_ = "include " + inc + ": " + lastXmlError_;
ok = false;
}
}
for (const auto& script : emitted.scriptFiles) {
if (!luaEngine_.executeFile(sibling(script).string())) {
if (ok) lastXmlError_ = "script " + script + ": " + luaEngine_.lastError();
LOG_ERROR("AddonManager: ", path, " referenced ", script, " which failed");
ok = false;
}
}
if (!emitted.lua.empty()) {
if (!luaEngine_.executeString(emitted.lua)) {
if (ok) lastXmlError_ = "frames: " + luaEngine_.lastError();
LOG_ERROR("AddonManager: frames from ", path, " failed to build");
ok = false;
} else {

View File

@@ -2096,8 +2096,16 @@ void LuaEngine::installMissingApiFallback() {
// newer client. That is the right trade for bringing FrameXML up, where the
// point is to get past a missing name and find out what actually matters,
// and the wrong one for everyday addon loading.
const char* env = std::getenv("WOWEE_LUA_API_FALLBACK");
const bool enabled = env && *env && std::string(env) != "0";
auto isSet = [](const char* name) {
const char* v = std::getenv(name);
return v && *v && std::string(v) != "0";
};
// Loading FrameXML implies it. FrameXML cannot get through its own load
// without the fallback, so two separate switches where one is useless
// without the other is only a way to be handed a wall of failures for
// setting the obvious one.
const bool enabled = isSet("WOWEE_LUA_API_FALLBACK") ||
isSet("WOWEE_LOAD_FRAMEXML");
if (!enabled) return;
lua_pushcfunction(L_, lua_RecordMissingApi);
@@ -2459,6 +2467,7 @@ bool LuaEngine::executeFile(const std::string& path) {
if (err != 0) {
const char* errMsg = lua_tostring(L_, -1);
std::string msg = errMsg ? errMsg : "(unknown error)";
lastError_ = msg;
LOG_ERROR("LuaEngine: error loading '", path, "': ", msg);
if (luaErrorCallback_) luaErrorCallback_(msg);
if (gameHandler_) {
@@ -2481,6 +2490,7 @@ bool LuaEngine::executeString(const std::string& code) {
if (err != 0) {
const char* errMsg = lua_tostring(L_, -1);
std::string msg = errMsg ? errMsg : "(unknown error)";
lastError_ = msg;
LOG_ERROR("LuaEngine: script error: ", msg);
if (luaErrorCallback_) luaErrorCallback_(msg);
if (gameHandler_) {

View File

@@ -25,12 +25,20 @@ extern "C" {
#include <sstream>
int main(int argc, char** argv) {
lua_State* L = luaL_newstate();
int ok = 0, bad = 0, shown = 0;
int ok = 0, bad = 0, shown = 0, unparsed = 0;
for (auto& e : std::filesystem::directory_iterator(argv[1])) {
if (e.path().extension() != ".xml") continue;
std::ifstream f(e.path()); std::stringstream ss; ss << f.rdbuf();
wowee::ui::XmlNode root; std::string err;
if (!wowee::ui::parseXml(ss.str(), root, err)) continue;
// Counted, not skipped. A file the reader cannot get through never
// reaches the compiler at all, so passing over it quietly reports a
// clean run on files that in fact never loaded.
if (!wowee::ui::parseXml(ss.str(), root, err)) {
++unparsed;
printf(" UNPARSED %-30s %s\n", e.path().filename().string().c_str(),
err.c_str());
continue;
}
auto r = wowee::ui::emitFrameXml(root);
if (r.lua.empty()) { ++ok; continue; }
std::string chunk = "local __WoweeTemplates={} "
@@ -44,6 +52,7 @@ int main(int argc, char** argv) {
}
lua_settop(L, 0);
}
printf("emitted Lua compiles: %d fails: %d\n", ok, bad);
printf("emitted Lua compiles: %d fails: %d unparsed XML: %d\n",
ok, bad, unparsed);
return 0;
}