mirror of
https://github.com/logos-co/logos-view-module.git
synced 2026-08-31 06:51:08 +00:00
The `--backend ui` emitter existed twice: live in logos-qt-sdk's
qt-generator, and as an uncalled successor here. They were byte-identical
until logos-qt-sdk#38 gave the live one the module teardown hook, and this
copy has been ~45 lines behind ever since.
That drift is dangerous in a specific way. ui-host reaches aboutToUnload()
BY NAME through the meta-object; a plugin class that does not declare it has
no such meta-method, invokeMethod returns false, and the host moves on —
INDISTINGUISHABLE from a plugin answering "Synchronous, nothing to wait
for". Had the migration completed with this copy, every view would have
silently and permanently lost its chance to finish, and nothing would have
failed: not a build, not a load, not a call.
This repo is the right home. The emitter needs nothing from logos-qt-sdk —
`--backend ui` is the one qt-generator backend that never reads LIDL, and
its only qt-sdk symbol was lidlToPascalCase, which this copy already
inlines. The LogosView*.in templates the generated glue compiles against
already live here. And the backend owning its glue is the same rule that
puts the cdylib glue generator in logos-plugin-qt.
logos_ui_plugin_context.h HAD TO COME TOO, which the plan did not
anticipate. It was duplicated as well, and stale by the same changeset. The
emitter and that header are a matched pair — the emitter writes a call, the
header declares what it calls — and they stayed consistent only because one
pin versioned both. Moving the emitter alone fails concretely:
`no member named 'maybeUiPluginAboutToUnload' in namespace '_logos_codegen_'`.
The emitter is taken VERBATIM: the .cpp differs from the live copy only in
three generator-name banners, and the .h and the context header are
byte-identical to it. Every difference in the successor was it being behind
or vaguer — two were outright regressions (the .h named LogosModuleContext,
the exact confusion the live comment warns against, and the .cpp described
emitted files by names neither main uses).
main.cpp accepts `--backend ui` and refuses anything else, converging with
the in-flight qml-dep branch rather than colliding with it.
TESTS CARRIED ACROSS, because the old home's blind spot is the whole story:
`--backend ui` had NO test at all, which is how it became the one generated
plugin class without the hook. qt-sdk's five text assertions come over, plus
a new behavioural check: tests/ui-plugin-metaobject compiles the emitted
plugin against the REAL context header and REAL templates, loads it with
QPluginLoader, and dumps the meta-object —
[4] SIGNAL unloadFinished() [5] METHOD initLogos [6] METHOD aboutToUnload -> int
Negative control: origin/master's stale emitter compiles and loads fine and
the check still fails with both named diagnostics. The silent gap, made loud.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
5.8 KiB
C++
157 lines
5.8 KiB
C++
// logos-view-generator — the plugin glue for a Logos VIEW module.
|
|
//
|
|
// A view module (metadata `type: ui_qml`, `interface: universal`) is authored
|
|
// as two user-written pieces: the `.rep` that declares the view contract (full
|
|
// QtRO: SLOTs, PROPs, SIGNALs) and a `*Backend` class deriving the repc
|
|
// `<RepClass>SimpleSource` plus `LogosUiPluginContext`. This tool emits the
|
|
// third piece — the Qt plugin around them:
|
|
//
|
|
// <name>_ui_interface.h PluginInterface + the IID
|
|
// <name>_ui_glue.{h,cpp} the *Plugin: Q_PLUGIN_METADATA, name()/version(),
|
|
// ViewPluginBase, and the Q_INVOKABLE initLogos that
|
|
// builds LogosModules, wires it into the backend
|
|
// (firing onContextReady) and registers the backend
|
|
// as the QtRO source.
|
|
//
|
|
// It deliberately does NOT read LIDL. A view's contract is its `.rep`, not a
|
|
// `.lidl` — the class name is scraped out of the .rep and everything else comes
|
|
// from metadata.json — so this binary needs Qt Core and nothing else. That is
|
|
// what keeps this repo a leaf.
|
|
//
|
|
// Usage:
|
|
// logos-view-generator [--backend ui] --metadata <metadata.json> --rep <view.rep>
|
|
// [--backend-class <C>] [--backend-header <h>]
|
|
// [--output-dir <dir>]
|
|
//
|
|
// `--backend ui` is the only backend this binary has, and it is the default, so
|
|
// it may be omitted. It is ACCEPTED rather than ignored because the caller that
|
|
// matters -- logos-module-builder's ui_qml codegen step -- spells it explicitly,
|
|
// having previously invoked `logos-qt-generator --backend ui`. An unrecognised
|
|
// value is REFUSED: silently treating `--backend qml-dep` as ui would emit the
|
|
// wrong artifact set under a green build, which is the failure mode that put
|
|
// this generator in two repos in the first place.
|
|
|
|
#include <QCoreApplication>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QTextStream>
|
|
|
|
#include "lidl_gen_ui.h"
|
|
|
|
namespace {
|
|
|
|
struct Out { QString file; QString content; };
|
|
|
|
int writeAll(const QList<Out>& outs, const QString& dir,
|
|
QTextStream& out, QTextStream& err)
|
|
{
|
|
for (const Out& o : outs) {
|
|
const QString abs = QDir(dir).filePath(o.file);
|
|
QFile f(abs);
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write: " << abs << "\n";
|
|
return 1;
|
|
}
|
|
f.write(o.content.toUtf8());
|
|
f.close();
|
|
out << "Generated: " << abs << "\n";
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
QString argValue(const QStringList& args, const QString& flag)
|
|
{
|
|
const int i = args.indexOf(flag);
|
|
if (i < 0 || i + 1 >= args.size()) return QString();
|
|
return args.at(i + 1);
|
|
}
|
|
|
|
// PascalCase of a module name, for the plugin class stem. Same rule the other
|
|
// Logos generators use; inlined here rather than shared so this binary keeps
|
|
// no dependency on a generator frontend it otherwise has no use for.
|
|
QString toPascalCase(const QString& name)
|
|
{
|
|
QString out;
|
|
bool cap = true;
|
|
for (QChar c : name) {
|
|
if (!c.isLetterOrNumber()) { cap = true; continue; }
|
|
if (cap) { out.append(c.toUpper()); cap = false; }
|
|
else { out.append(c.toLower()); }
|
|
}
|
|
if (out.isEmpty()) return QString("Module");
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
QCoreApplication app(argc, argv);
|
|
QTextStream out(stdout);
|
|
QTextStream err(stderr);
|
|
|
|
const QStringList args = QCoreApplication::arguments();
|
|
|
|
// Backend selection. Defaults to the only backend there is; anything else
|
|
// is an error rather than a silent fallback. See the usage note above.
|
|
const QString backend = argValue(args, "--backend");
|
|
if (!backend.isEmpty() && backend != QStringLiteral("ui")) {
|
|
err << "Unknown --backend: " << backend << " (expected ui)\n";
|
|
return 2;
|
|
}
|
|
|
|
const QString metadata = argValue(args, "--metadata");
|
|
const QString repPath = argValue(args, "--rep");
|
|
QString outputDir = argValue(args, "--output-dir");
|
|
|
|
if (metadata.isEmpty() || repPath.isEmpty()) {
|
|
err << "Usage: logos-view-generator [--backend ui] --metadata <metadata.json> --rep <view.rep>\n"
|
|
<< " [--backend-class <C>] [--backend-header <h>] [--output-dir <dir>]\n";
|
|
return 2;
|
|
}
|
|
|
|
QFile mf(metadata);
|
|
if (!mf.open(QIODevice::ReadOnly)) {
|
|
err << "Failed to read metadata: " << metadata << "\n";
|
|
return 3;
|
|
}
|
|
const QJsonObject meta = QJsonDocument::fromJson(mf.readAll()).object();
|
|
|
|
UiGlueSpec spec;
|
|
spec.moduleName = meta.value(QStringLiteral("name")).toString();
|
|
spec.moduleVersion = meta.value(QStringLiteral("version")).toString(QStringLiteral("1.0.0"));
|
|
if (spec.moduleName.isEmpty()) {
|
|
err << "metadata.json has no name\n";
|
|
return 3;
|
|
}
|
|
spec.pluginBase = toPascalCase(spec.moduleName);
|
|
|
|
QString repErr;
|
|
if (!lidlUiParseRepClass(repPath, &spec.repClass, &repErr)) {
|
|
err << "Error: " << repErr << "\n";
|
|
return 4;
|
|
}
|
|
|
|
spec.backendClass = argValue(args, "--backend-class");
|
|
if (spec.backendClass.isEmpty())
|
|
spec.backendClass = spec.pluginBase + QStringLiteral("Backend");
|
|
spec.backendHeader = argValue(args, "--backend-header");
|
|
if (spec.backendHeader.isEmpty())
|
|
spec.backendHeader = spec.moduleName + QStringLiteral("_backend.h");
|
|
|
|
if (outputDir.isEmpty())
|
|
outputDir = QDir::current().filePath("generated");
|
|
QDir().mkpath(outputDir);
|
|
|
|
QList<Out> outs;
|
|
outs.append({spec.moduleName + "_ui_interface.h", lidlMakeUiInterfaceHeader(spec)});
|
|
outs.append({spec.moduleName + "_ui_glue.h", lidlMakeUiGlueHeader(spec)});
|
|
outs.append({spec.moduleName + "_ui_glue.cpp", lidlMakeUiGlueSource(spec)});
|
|
|
|
const int rc = writeAll(outs, outputDir, out, err);
|
|
out.flush();
|
|
return rc;
|
|
}
|