mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 17:51:07 +00:00
The Std surface — std-typed signatures over a QVariant + LogosAPIClient body — no longer had a caller. `interface: "universal"` modules moved to `lp` (std types over the Qt-free logos-protocol C ABI), and nothing else ever selected it, so every Std branch was dead weight sitting in front of the two live ones. `--api-style=std` is now rejected with a message naming the retirement rather than aliased to `qt`. A stale caller that still passes it wants std signatures; handing it the Qt surface would fail later, further from the cause. The collapse is deliberate about the branches where Std was tested BEFORE Qt, since a naive "delete the block containing ApiStyle::Std" changes Qt output: - makeHeader's include block tested Std first, so its `else` is the Qt include list — the Qt includes are kept and promoted, not deleted. - recordToWireExpr / recordFromWireExpr returned the Qt map form from a guarded `if` and the Std form from the function's trailing `return`. The guard is dropped and the Qt form promoted to the tail; deleting only the trailing return would have left a path falling off the end. - The private-member `else if (!events.isEmpty())` arm reads as an event test but was Std-only; the Qt arm (m_eventReplica + m_eventSource) survives, so setEventSource/trigger still have their storage. - `if (apiStyle == Qt || !events.isEmpty())` is a disjunction, not an Std branch: it unwraps to an unconditional emit, keeping ensureReplica()'s declaration next to its definition. - `isRec || style == Std` loses only the right disjunct — dropping `isRec ||` would double-wrap record fields in QVariant::fromValue. mapParamTypeStd / mapReturnTypeStd / isStdRefType stay: they are the shared std type table that ApiStyle::Lp reaches through the non-Qt arm of paramTypeFor / returnTypeFor / byRefFor and directly from lpPushExpr / lpFromJsonExpr. Verified by output equivalence rather than by the build succeeding: the generator was run over 13 fixture cases (the full_api contract as both a bound interface and a baked dep, three record-bearing contracts incl. map-of-record fields, the chat module's production contract, and a no-events contract) for both qt and lp, before and after. `diff -r` over the 194 resulting files reports no differences, and the experimental --lidl backends are byte-identical too. Test suite: 180/180, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1277 lines
54 KiB
C++
1277 lines
54 KiB
C++
#include <QCoreApplication>
|
|
#include <QPluginLoader>
|
|
#include <QFileInfo>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QJsonDocument>
|
|
#include <QMetaObject>
|
|
#include <QMetaMethod>
|
|
#include <QByteArray>
|
|
#include <QTextStream>
|
|
#include <QDir>
|
|
#include <QByteArrayList>
|
|
#include <QFile>
|
|
#include <QSet>
|
|
#include <QRegularExpression>
|
|
#include <QtGlobal>
|
|
#include "logos_provider_interface.h"
|
|
#include "generator_lib.h"
|
|
#include "../experimental/lidl_compat.h"
|
|
#include "../experimental/impl_header_parser.h"
|
|
#include "../experimental/lidl_emit_common.h" // lidlTypeToQt — the one Qt type mapper
|
|
|
|
// Escape a string for safe embedding inside a generated C++ string literal.
|
|
static QString cppStringEscape(const QString& s)
|
|
{
|
|
QString out = s;
|
|
out.replace('\\', "\\\\");
|
|
out.replace('"', "\\\"");
|
|
out.replace('\n', "\\n");
|
|
return out;
|
|
}
|
|
|
|
// Convert a TypeExpr → Qt-typed string name (same surface the
|
|
// metaobject-introspection path produces for methods, so generator_lib
|
|
// can consume both via one code path).
|
|
//
|
|
// ONE Qt type mapper. This used to be a near-duplicate of `lidlTypeToQt`
|
|
// (experimental/lidl_emit_common.cpp) and the two disagreed: this copy had no
|
|
// `void` case, so a `-> void` method reaching it as Primitive("void") from the
|
|
// impl-header parser fell through to QVariant and generated
|
|
// `QVariant doVoid(...)`. (The .lidl parser spells the same thing
|
|
// Named("void"), which survived only by accident — mapReturnType's
|
|
// `base == "void"` early-out.) The lp/std tables are DERIVED from this name, so
|
|
// the same bug produced `LogosMap doVoid(...)` on the Qt-free surface: not a
|
|
// Qt-only defect, a front-end one. It is now a delegation, so there is one
|
|
// table to disagree with.
|
|
static QString lidlTypeExprToQtTypeName(const TypeExpr& te)
|
|
{
|
|
return lidlTypeToQt(te);
|
|
}
|
|
|
|
static QJsonArray moduleRecordsToJson(const ModuleDecl& mod);
|
|
|
|
// Load events from a `.lidl` sidecar shipped alongside a module's
|
|
// pre-built headers. Returns a JSON array of
|
|
// { name, params: [ { name, type } ] }
|
|
// using Qt-typed type names — same shape generator_lib's makeHeader /
|
|
// makeSource already consume for methods.
|
|
static QJsonArray loadEventsFromLidl(const QString& lidlPath, QTextStream& err,
|
|
QJsonArray* outRecords = nullptr)
|
|
{
|
|
QJsonArray result;
|
|
QFile f(lidlPath);
|
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
err << "Failed to open events sidecar: " << lidlPath << "\n";
|
|
return result;
|
|
}
|
|
QString source = QString::fromUtf8(f.readAll());
|
|
f.close();
|
|
|
|
LidlParseResult pr = lidlParse(source);
|
|
if (pr.hasError()) {
|
|
err << lidlPath << ":" << pr.errorLine << ":" << pr.errorColumn
|
|
<< ": " << pr.error << "\n";
|
|
return result;
|
|
}
|
|
|
|
if (outRecords) *outRecords = moduleRecordsToJson(pr.module);
|
|
for (const EventDecl& ed : pr.module.events) {
|
|
QJsonObject obj;
|
|
obj["name"] = qs(ed.name);
|
|
QJsonArray params;
|
|
for (const ParamDecl& pd : ed.params) {
|
|
QJsonObject p;
|
|
p["name"] = qs(pd.name);
|
|
p["type"] = lidlTypeExprToQtTypeName(pd.type);
|
|
params.append(p);
|
|
}
|
|
obj["params"] = params;
|
|
result.append(obj);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ── Dependency interfaces ───────────────────────────────────────────────────
|
|
//
|
|
// An "interface dependency" is a method/event contract a consumer declares
|
|
// (in `metadata.json#interface_dependencies`) decoupled from any concrete
|
|
// module. The definition file is either a `.lidl` or a pure-C++ `.h` (the
|
|
// module's own language). The generator emits a BOUND wrapper class — the
|
|
// target module name is a runtime ctor argument, not baked in — so one
|
|
// interface can be bound to any module that satisfies it.
|
|
|
|
// A single interface to generate a bound wrapper for. `path` is already
|
|
// resolved (nix resolves local `${src}/file` and remote `${input}/file`
|
|
// store paths and passes them via --interface; the generator never touches
|
|
// flake inputs). `implClass` is required for `.h` files, empty for `.lidl`.
|
|
struct InterfaceSpec {
|
|
QString name; // interface identifier → class/file name + bind_<name>
|
|
QString path; // resolved path to the .lidl / .h definition
|
|
QString implClass; // class inside a .h whose API defines the interface
|
|
};
|
|
|
|
// Parse all `<flag> <name>=<path>[=<impl_class>]` (or `<flag>=<name>=...`)
|
|
// occurrences. Names and store paths contain no '=', so splitting on the
|
|
// first two '=' is unambiguous. Used for both `--interface` (runtime-bound
|
|
// wrappers) and `--dep` (name-baked wrappers generated from a dep's LIDL).
|
|
static QVector<InterfaceSpec> parseSpecFlags(const QStringList& args, const QString& flag)
|
|
{
|
|
const QString flagEq = flag + "=";
|
|
QVector<InterfaceSpec> specs;
|
|
for (int i = 0; i < args.size(); ++i) {
|
|
QString value;
|
|
if (args.at(i) == flag && i + 1 < args.size()) {
|
|
value = args.at(i + 1);
|
|
} else if (args.at(i).startsWith(flagEq)) {
|
|
value = args.at(i).section('=', 1);
|
|
} else {
|
|
continue;
|
|
}
|
|
const int firstEq = value.indexOf('=');
|
|
if (firstEq <= 0) continue; // need at least name=path
|
|
InterfaceSpec spec;
|
|
spec.name = value.left(firstEq);
|
|
const int secondEq = value.indexOf('=', firstEq + 1);
|
|
if (secondEq < 0) {
|
|
spec.path = value.mid(firstEq + 1);
|
|
} else {
|
|
spec.path = value.mid(firstEq + 1, secondEq - firstEq - 1);
|
|
spec.implClass = value.mid(secondEq + 1);
|
|
}
|
|
specs.append(spec);
|
|
}
|
|
return specs;
|
|
}
|
|
|
|
// Build a getMethods()-shaped QJsonArray (the surface makeHeader/makeSource
|
|
// consume) from a parsed ModuleDecl. Every interface method is invokable.
|
|
static QJsonArray moduleMethodsToJson(const ModuleDecl& mod)
|
|
{
|
|
QJsonArray arr;
|
|
for (const MethodDecl& m : mod.methods) {
|
|
QJsonObject o;
|
|
o["name"] = qs(m.name);
|
|
o["returnType"] = lidlTypeExprToQtTypeName(m.returnType);
|
|
o["isInvokable"] = true;
|
|
QJsonArray params;
|
|
for (const ParamDecl& p : m.params) {
|
|
QJsonObject po;
|
|
po["type"] = lidlTypeExprToQtTypeName(p.type);
|
|
po["name"] = qs(p.name);
|
|
params.append(po);
|
|
}
|
|
o["parameters"] = params;
|
|
arr.append(o);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Build the records QJsonArray ({ name, fields:[{name,type}] }) from a parsed
|
|
// ModuleDecl — the contract's `type Foo { ... }` declarations, which
|
|
// generator_lib turns into structs nested in the wrapper class.
|
|
static QJsonArray moduleRecordsToJson(const ModuleDecl& mod)
|
|
{
|
|
QJsonArray arr;
|
|
for (const TypeDecl& td : mod.types) {
|
|
QJsonObject o;
|
|
o["name"] = qs(td.name);
|
|
QJsonArray fields;
|
|
for (const FieldDecl& fd : td.fields) {
|
|
QJsonObject f;
|
|
f["name"] = qs(fd.name);
|
|
f["type"] = lidlTypeExprToQtTypeName(fd.type);
|
|
fields.append(f);
|
|
}
|
|
o["fields"] = fields;
|
|
arr.append(o);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Build the events QJsonArray ({ name, params:[{name,type}] }) — same shape
|
|
// loadEventsFromLidl produces — from a parsed ModuleDecl.
|
|
static QJsonArray moduleEventsToJson(const ModuleDecl& mod)
|
|
{
|
|
QJsonArray arr;
|
|
for (const EventDecl& ed : mod.events) {
|
|
QJsonObject o;
|
|
o["name"] = qs(ed.name);
|
|
QJsonArray params;
|
|
for (const ParamDecl& pd : ed.params) {
|
|
QJsonObject p;
|
|
p["name"] = qs(pd.name);
|
|
p["type"] = lidlTypeExprToQtTypeName(pd.type);
|
|
params.append(p);
|
|
}
|
|
o["params"] = params;
|
|
arr.append(o);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Parse an interface definition file into a ModuleDecl. `.lidl` parses
|
|
// directly; `.h`/`.hpp` go through the impl-header parser, which needs a
|
|
// metadata.json — we feed it a synthetic one carrying only the interface
|
|
// name so the consumer's identity and events are NOT pulled in (the
|
|
// interface's events come solely from the file's own `logos_events:` block).
|
|
static bool parseInterfaceFile(const InterfaceSpec& spec, const QString& genDirPath,
|
|
ModuleDecl& outMod, QTextStream& err)
|
|
{
|
|
QFileInfo fi(spec.path);
|
|
if (!fi.exists()) {
|
|
err << "Interface file not found for '" << spec.name << "': " << spec.path << "\n";
|
|
return false;
|
|
}
|
|
const QString ext = fi.suffix().toLower();
|
|
if (ext == "lidl") {
|
|
QFile f(spec.path);
|
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
err << "Failed to open interface file: " << spec.path << "\n";
|
|
return false;
|
|
}
|
|
const QString src = QString::fromUtf8(f.readAll());
|
|
f.close();
|
|
LidlParseResult pr = lidlParse(src);
|
|
if (pr.hasError()) {
|
|
err << spec.path << ":" << pr.errorLine << ":" << pr.errorColumn
|
|
<< ": " << pr.error << "\n";
|
|
return false;
|
|
}
|
|
outMod = pr.module;
|
|
return true;
|
|
}
|
|
if (ext == "h" || ext == "hpp") {
|
|
if (spec.implClass.isEmpty()) {
|
|
err << "Interface '" << spec.name << "' is a C++ header but no impl_class was given "
|
|
<< "(metadata.json interface_dependencies entry needs \"impl_class\")\n";
|
|
return false;
|
|
}
|
|
// Synthetic minimal metadata: name only, no events — keeps the
|
|
// consumer's identity/events out of the interface.
|
|
const QString synthMeta = QDir(genDirPath).filePath("." + spec.name + "_iface_meta.json");
|
|
{
|
|
QFile mf(synthMeta);
|
|
if (!mf.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write temporary interface metadata: " << synthMeta << "\n";
|
|
return false;
|
|
}
|
|
mf.write(QString("{\"name\":\"%1\"}").arg(spec.name).toUtf8());
|
|
mf.close();
|
|
}
|
|
ImplParseResult pr = parseImplHeader(spec.path, spec.implClass, synthMeta, err);
|
|
QFile::remove(synthMeta);
|
|
if (pr.hasError()) {
|
|
err << "Error parsing interface header " << spec.path << ": " << pr.error << "\n";
|
|
return false;
|
|
}
|
|
outMod = pr.module;
|
|
return true;
|
|
}
|
|
err << "Unsupported interface file type for '" << spec.name << "': " << spec.path
|
|
<< " (expected .lidl or .h)\n";
|
|
return false;
|
|
}
|
|
|
|
// Generate a wrapper (`<name>_api.{h,cpp}`) per spec from its definition file.
|
|
// The wrapper class is named from the spec `name` (PascalCase), NOT the
|
|
// definition file's internal module name, so it matches the `#include` the
|
|
// umbrella header emits. `bindMode` picks the wrapper flavour:
|
|
// Bound — interface dependency: ctor takes a runtime module name; exposed
|
|
// via a `bind_<name>(...)` factory on the umbrella.
|
|
// Static — concrete dependency: the module name is baked in; exposed as a
|
|
// `<name>` member on the umbrella (byte-identical to the wrapper the
|
|
// dep's prebuilt headers used to ship).
|
|
static bool generateInterfaceWrappers(const QVector<InterfaceSpec>& ifaces,
|
|
const QString& genDirPath, ApiStyle apiStyle,
|
|
QTextStream& out, QTextStream& err,
|
|
BindMode bindMode = BindMode::Bound)
|
|
{
|
|
for (const InterfaceSpec& spec : ifaces) {
|
|
ModuleDecl mod;
|
|
if (!parseInterfaceFile(spec, genDirPath, mod, err)) return false;
|
|
|
|
{
|
|
QString recErr;
|
|
if (!lidlCheckRecords(mod, &recErr)) {
|
|
err << spec.path << ": " << recErr << "\n";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const QString className = toPascalCase(spec.name);
|
|
const QJsonArray methods = moduleMethodsToJson(mod);
|
|
const QJsonArray events = moduleEventsToJson(mod);
|
|
const QJsonArray records = moduleRecordsToJson(mod);
|
|
const QString headerRel = spec.name + "_api.h";
|
|
const QString sourceRel = spec.name + "_api.cpp";
|
|
|
|
const QString header = makeHeader(spec.name, className, methods, apiStyle, events, bindMode, records);
|
|
const QString source = makeSource(spec.name, className, headerRel, methods, apiStyle, events, bindMode, records);
|
|
|
|
{
|
|
QFile f(QDir(genDirPath).filePath(headerRel));
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write wrapper header: " << headerRel << "\n";
|
|
return false;
|
|
}
|
|
f.write(header.toUtf8());
|
|
}
|
|
{
|
|
QFile f(QDir(genDirPath).filePath(sourceRel));
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write wrapper source: " << sourceRel << "\n";
|
|
return false;
|
|
}
|
|
f.write(source.toUtf8());
|
|
}
|
|
out << "Generated " << (bindMode == BindMode::Bound ? "bound interface" : "dependency")
|
|
<< " wrapper: " << headerRel << " (class " << className << ", "
|
|
<< methods.size() << " methods, " << events.size() << " events)\n";
|
|
}
|
|
out.flush();
|
|
return true;
|
|
}
|
|
|
|
static QJsonArray enumerateMethods(QObject* moduleInstance)
|
|
{
|
|
QJsonArray methodsArray;
|
|
|
|
if (!moduleInstance) {
|
|
return methodsArray;
|
|
}
|
|
|
|
const QMetaObject* metaObject = moduleInstance->metaObject();
|
|
|
|
for (int i = 0; i < metaObject->methodCount(); ++i) {
|
|
QMetaMethod method = metaObject->method(i);
|
|
|
|
if (method.enclosingMetaObject() != metaObject) {
|
|
continue;
|
|
}
|
|
|
|
QJsonObject methodObj;
|
|
methodObj["signature"] = QString::fromUtf8(method.methodSignature());
|
|
methodObj["name"] = QString::fromUtf8(method.name());
|
|
methodObj["returnType"] = QString::fromUtf8(method.typeName());
|
|
bool isInvokable = method.isValid() && (method.methodType() == QMetaMethod::Method || method.methodType() == QMetaMethod::Slot);
|
|
methodObj["isInvokable"] = isInvokable;
|
|
|
|
if (method.parameterCount() > 0) {
|
|
QJsonArray params;
|
|
for (int p = 0; p < method.parameterCount(); ++p) {
|
|
QJsonObject paramObj;
|
|
paramObj["type"] = QString::fromUtf8(method.parameterTypeName(p));
|
|
QByteArrayList paramNames = method.parameterNames();
|
|
if (p < paramNames.size() && !paramNames.at(p).isEmpty()) {
|
|
paramObj["name"] = QString::fromUtf8(paramNames.at(p));
|
|
} else {
|
|
paramObj["name"] = QString("param%1").arg(p);
|
|
}
|
|
params.append(paramObj);
|
|
}
|
|
methodObj["parameters"] = params;
|
|
}
|
|
|
|
methodsArray.append(methodObj);
|
|
}
|
|
|
|
return methodsArray;
|
|
}
|
|
|
|
// toPascalCase, normalizeType, mapParamType, mapReturnType -> generator_lib.h/cpp
|
|
|
|
// makeHeader -> generator_lib.h/cpp
|
|
|
|
// makeSource -> generator_lib.h/cpp
|
|
|
|
static bool writeUmbrellaHeader(const QString& genDirPath, QTextStream& err)
|
|
{
|
|
// Generate logos_sdk.h: include every per-module wrapper header in
|
|
// the gen dir and aggregate them into a flat `LogosModules` struct.
|
|
// The wrappers may be Qt-typed or std-typed (lp) depending on the
|
|
// --api-style picked for this build; the umbrella shape doesn't
|
|
// change because either flavor produces the same accessor name
|
|
// (`<dep>`) on the same class name (`<Dep>`).
|
|
//
|
|
// `core_manager_api.h` (if present in the gen dir from an older
|
|
// run) is intentionally filtered out — universal modules access
|
|
// only the deps they explicitly declared in `metadata.json#
|
|
// dependencies`. Apps that need to manage the core use the C API
|
|
// in liblogos directly, not the typed `LogosModules` aggregate.
|
|
QDir genDir(genDirPath);
|
|
QStringList headers = genDir.entryList(QStringList() << "*_api.h", QDir::Files | QDir::Readable);
|
|
headers.removeAll(QStringLiteral("core_manager_api.h"));
|
|
|
|
QString content;
|
|
QTextStream s(&content);
|
|
s << "#pragma once\n";
|
|
s << "#include \"logos_api.h\"\n";
|
|
s << "#include \"logos_api_client.h\"\n\n";
|
|
for (const QString& h : headers) s << "#include \"" << h << "\"\n";
|
|
s << "\n";
|
|
|
|
s << "struct LogosModules {\n";
|
|
s << " explicit LogosModules(LogosAPI* api) : api(api)";
|
|
for (const QString& h : headers) {
|
|
QString base = h;
|
|
base.chop(QString("_api.h").size());
|
|
s << ", \n " << base << "(api)";
|
|
}
|
|
s << " {}\n";
|
|
s << " LogosAPI* api;\n";
|
|
for (const QString& h : headers) {
|
|
QString base = h;
|
|
base.chop(QString("_api.h").size());
|
|
QString className = toPascalCase(base);
|
|
s << " " << className << " " << base << ";\n";
|
|
}
|
|
s << "};\n";
|
|
|
|
QFile outFile(genDir.filePath("logos_sdk.h"));
|
|
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write umbrella header: " << outFile.fileName() << "\n";
|
|
return false;
|
|
}
|
|
outFile.write(content.toUtf8());
|
|
outFile.close();
|
|
return true;
|
|
}
|
|
|
|
static bool writeUmbrellaHeaderFromDeps(const QString& genDirPath, const QJsonArray& deps, const QStringList& interfaceNames, QTextStream& err, ApiStyle apiStyle = ApiStyle::Qt, const QString& originName = QString())
|
|
{
|
|
// Lp (Qt-free) umbrella: no LogosAPI. Each dep wrapper self-creates its
|
|
// lp_client on behalf of `originName` (this module), so the struct is
|
|
// default-constructible and the glue just does `new LogosModules()`.
|
|
if (apiStyle == ApiStyle::Lp) {
|
|
QDir genDir(genDirPath);
|
|
QString content;
|
|
QTextStream s(&content);
|
|
s << "#pragma once\n";
|
|
s << "#include <string>\n";
|
|
if (!interfaceNames.isEmpty()) {
|
|
s << "#include <map>\n";
|
|
s << "#include <memory>\n";
|
|
}
|
|
for (const QJsonValue& v : deps) {
|
|
if (!v.isString()) continue;
|
|
s << "#include \"" << v.toString() << "_api.h\"\n";
|
|
}
|
|
for (const QString& ifaceName : interfaceNames)
|
|
s << "#include \"" << ifaceName << "_api.h\"\n";
|
|
s << "\n";
|
|
s << "struct LogosModules {\n";
|
|
s << " LogosModules()";
|
|
bool first = true;
|
|
for (const QJsonValue& v : deps) {
|
|
if (!v.isString()) continue;
|
|
s << (first ? " : " : ",\n ");
|
|
first = false;
|
|
s << v.toString() << "(\"" << originName << "\")";
|
|
}
|
|
s << " {}\n";
|
|
for (const QJsonValue& v : deps) {
|
|
if (!v.isString()) continue;
|
|
const QString depName = v.toString();
|
|
s << " " << toPascalCase(depName) << " " << depName << ";\n";
|
|
}
|
|
// Interface dependencies: bound at runtime. The bound wrapper is a
|
|
// THIN handle over per-provider State the umbrella OWNS for the
|
|
// module's lifetime — so a transient `modules().bind_x(p)` temporary
|
|
// can register an async callback / event subscription that outlives
|
|
// it (the LpClient + RAII subscriptions persist in the map). Keyed by
|
|
// provider so repeated binds to the same provider share one client.
|
|
for (const QString& ifaceName : interfaceNames) {
|
|
const QString className = toPascalCase(ifaceName);
|
|
s << " " << className << " bind_" << ifaceName << "(const std::string& moduleName) {\n";
|
|
s << " auto& _st = m_" << ifaceName << "_bound[moduleName];\n";
|
|
s << " if (!_st) _st = std::make_unique<" << className << "::State>(moduleName, \"" << originName << "\");\n";
|
|
s << " return " << className << "(_st.get());\n";
|
|
s << " }\n";
|
|
}
|
|
for (const QString& ifaceName : interfaceNames) {
|
|
const QString className = toPascalCase(ifaceName);
|
|
s << " std::map<std::string, std::unique_ptr<" << className << "::State>> m_"
|
|
<< ifaceName << "_bound;\n";
|
|
}
|
|
s << "};\n";
|
|
QFile outFile(genDir.filePath("logos_sdk.h"));
|
|
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write umbrella header: " << outFile.fileName() << "\n";
|
|
return false;
|
|
}
|
|
outFile.write(content.toUtf8());
|
|
outFile.close();
|
|
return true;
|
|
}
|
|
|
|
// Generate logos_sdk.h from metadata.json's dependencies list. The
|
|
// shape doesn't depend on apiStyle — each dep emits a single
|
|
// `<name>_api.h` whose class signature shape was already decided
|
|
// at codegen time. The umbrella just `#include`s and aggregates
|
|
// each wrapper into the flat `LogosModules` struct.
|
|
//
|
|
// Only the modules explicitly listed in `metadata.json#
|
|
// dependencies` are exposed. Apps that need to manage the core
|
|
// (basecamp, logoscore) use liblogos' C API directly rather than
|
|
// the typed `LogosModules` aggregate.
|
|
//
|
|
// Interface dependencies (`metadata.json#interface_dependencies`) are
|
|
// NOT fixed members — they bind to a runtime-chosen module — so each
|
|
// gets a `bind_<name>(moduleName)` factory instead, returning a bound
|
|
// wrapper by value.
|
|
QDir genDir(genDirPath);
|
|
QString content;
|
|
QTextStream s(&content);
|
|
s << "#pragma once\n";
|
|
// <string> is only needed for the std::string bind_<iface> overloads;
|
|
// omit it when there are no interfaces so the umbrella stays identical
|
|
// to its historical form for dependency-only modules.
|
|
if (!interfaceNames.isEmpty()) s << "#include <string>\n";
|
|
s << "#include \"logos_api.h\"\n";
|
|
s << "#include \"logos_api_client.h\"\n\n";
|
|
for (const QJsonValue& v : deps) {
|
|
if (!v.isString()) continue;
|
|
QString depName = v.toString();
|
|
s << "#include \"" << depName << "_api.h\"\n";
|
|
}
|
|
for (const QString& ifaceName : interfaceNames) {
|
|
s << "#include \"" << ifaceName << "_api.h\"\n";
|
|
}
|
|
s << "\n";
|
|
|
|
s << "struct LogosModules {\n";
|
|
s << " explicit LogosModules(LogosAPI* api) : api(api)";
|
|
for (const QJsonValue& v : deps) {
|
|
if (!v.isString()) continue;
|
|
QString depName = v.toString();
|
|
s << ", \n " << depName << "(api)";
|
|
}
|
|
s << " {}\n";
|
|
s << " LogosAPI* api;\n";
|
|
for (const QJsonValue& v : deps) {
|
|
if (!v.isString()) continue;
|
|
QString depName = v.toString();
|
|
QString className = toPascalCase(depName);
|
|
s << " " << className << " " << depName << ";\n";
|
|
}
|
|
// Bind factories — one per interface dependency. Two overloads so
|
|
// both Qt-typed (QString) and std-typed (std::string) call sites can
|
|
// pass the runtime module name without converting at the call site.
|
|
for (const QString& ifaceName : interfaceNames) {
|
|
const QString className = toPascalCase(ifaceName);
|
|
s << " " << className << " bind_" << ifaceName << "(const QString& moduleName) {\n";
|
|
s << " return " << className << "(api, moduleName);\n";
|
|
s << " }\n";
|
|
s << " " << className << " bind_" << ifaceName << "(const std::string& moduleName) {\n";
|
|
s << " return " << className << "(api, QString::fromStdString(moduleName));\n";
|
|
s << " }\n";
|
|
}
|
|
s << "};\n";
|
|
|
|
QFile outFile(genDir.filePath("logos_sdk.h"));
|
|
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write umbrella header: " << outFile.fileName() << "\n";
|
|
return false;
|
|
}
|
|
outFile.write(content.toUtf8());
|
|
outFile.close();
|
|
return true;
|
|
}
|
|
|
|
static bool writeUmbrellaSource(const QString& genDirPath, QTextStream& err)
|
|
{
|
|
// Generate logos_sdk.cpp: one #include per per-module wrapper
|
|
// `.cpp` in the gen dir. There's now exactly one wrapper file per
|
|
// module (Qt or std, picked at generation time), so no de-dup or
|
|
// twin-file filtering is needed.
|
|
//
|
|
// `core_manager_api.cpp` (if present from an older run) is
|
|
// filtered out — the umbrella header no longer declares
|
|
// `CoreManager core_manager;` so including its definitions would
|
|
// produce dead code.
|
|
QDir genDir(genDirPath);
|
|
QStringList sources = genDir.entryList(QStringList() << "*_api.cpp", QDir::Files | QDir::Readable);
|
|
sources.removeAll(QStringLiteral("core_manager_api.cpp"));
|
|
|
|
QString content;
|
|
QTextStream s(&content);
|
|
s << "#include \"logos_sdk.h\"\n\n";
|
|
for (const QString& c : sources) s << "#include \"" << c << "\"\n";
|
|
s << "\n";
|
|
|
|
QFile outFile(genDir.filePath("logos_sdk.cpp"));
|
|
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write umbrella source: " << outFile.fileName() << "\n";
|
|
return false;
|
|
}
|
|
outFile.write(content.toUtf8());
|
|
outFile.close();
|
|
return true;
|
|
}
|
|
|
|
static bool writeUmbrellaSourceFromDeps(const QString& genDirPath, const QJsonArray& deps, const QStringList& interfaceNames, QTextStream& err)
|
|
{
|
|
// Generate logos_sdk.cpp from metadata.json's dependencies list.
|
|
// Each dep emits one wrapper `.cpp` (Qt or std — decided at codegen
|
|
// time, file name is the same either way), `#include`'d here.
|
|
// Interface wrappers (`<name>_api.cpp`) are #include'd the same way.
|
|
QDir genDir(genDirPath);
|
|
QString content;
|
|
QTextStream s(&content);
|
|
s << "#include \"logos_sdk.h\"\n\n";
|
|
for (const QJsonValue& v : deps) {
|
|
if (!v.isString()) continue;
|
|
QString depName = v.toString();
|
|
s << "#include \"" << depName << "_api.cpp\"\n";
|
|
}
|
|
for (const QString& ifaceName : interfaceNames) {
|
|
s << "#include \"" << ifaceName << "_api.cpp\"\n";
|
|
}
|
|
s << "\n";
|
|
|
|
QFile outFile(genDir.filePath("logos_sdk.cpp"));
|
|
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write umbrella source: " << outFile.fileName() << "\n";
|
|
return false;
|
|
}
|
|
outFile.write(content.toUtf8());
|
|
outFile.close();
|
|
return true;
|
|
}
|
|
|
|
// ── Provider-header mode: scan LOGOS_METHOD markers and generate dispatch ────
|
|
// ParsedMethod, parseProviderHeader, toQVariantConversion -> generator_lib.h/cpp
|
|
|
|
static int generateProviderDispatch(const QString& headerPath, const QString& outputDir, QTextStream& out, QTextStream& err)
|
|
{
|
|
QFileInfo fi(headerPath);
|
|
if (!fi.exists()) {
|
|
err << "Header file does not exist: " << headerPath << "\n";
|
|
return 2;
|
|
}
|
|
|
|
QVector<ParsedMethod> methods = parseProviderHeader(headerPath, err);
|
|
if (methods.isEmpty()) {
|
|
err << "No LOGOS_METHOD markers found in: " << headerPath << "\n";
|
|
return 3;
|
|
}
|
|
|
|
// Derive the class name from the header: parse for ": public LogosProviderBase"
|
|
QString className;
|
|
{
|
|
QFile f(headerPath);
|
|
f.open(QIODevice::ReadOnly | QIODevice::Text);
|
|
QTextStream ts(&f);
|
|
QRegularExpression classRe(R"(class\s+(\w+)\s*:\s*public\s+LogosProviderBase)");
|
|
while (!ts.atEnd()) {
|
|
QString line = ts.readLine();
|
|
auto m = classRe.match(line);
|
|
if (m.hasMatch()) {
|
|
className = m.captured(1);
|
|
break;
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
|
|
if (className.isEmpty()) {
|
|
err << "Could not find class inheriting LogosProviderBase in: " << headerPath << "\n";
|
|
return 4;
|
|
}
|
|
|
|
QString headerBaseName = fi.fileName();
|
|
|
|
QString genDirPath = outputDir.isEmpty() ? fi.absolutePath() : outputDir;
|
|
QDir().mkpath(genDirPath);
|
|
|
|
// Generate logos_provider_dispatch.cpp
|
|
QString content;
|
|
QTextStream s(&content);
|
|
|
|
s << "// AUTO-GENERATED by logos-cpp-generator -- do not edit\n";
|
|
s << "#include \"" << headerBaseName << "\"\n";
|
|
s << "#include <QJsonArray>\n";
|
|
s << "#include <QJsonObject>\n";
|
|
s << "#include <QVariant>\n";
|
|
s << "#include <QString>\n";
|
|
s << "#include \"logos_types.h\"\n";
|
|
// The canonical argument decoder — the Qt face of logos::fromJson<T>.
|
|
s << "#include \"logos_qt_arg_decode.h\"\n";
|
|
s << "#include <exception>\n\n";
|
|
|
|
// callMethod() — group by name to support overloaded methods
|
|
QMap<QString, QVector<const ParsedMethod*>> methodsByName;
|
|
for (const ParsedMethod& m : methods) {
|
|
methodsByName[m.name].append(&m);
|
|
}
|
|
|
|
// The dispatch body is wrapped in a catch-all: any exception the author's
|
|
// code lets escape becomes an ordinary method failure (invalid QVariant)
|
|
// instead of unwinding through Qt event dispatch and killing the module
|
|
// process.
|
|
s << "QVariant " << className << "::callMethod(const QString& methodName, const QVariantList& args)\n";
|
|
s << "{\n";
|
|
s << " try {\n";
|
|
for (auto it = methodsByName.constBegin(); it != methodsByName.constEnd(); ++it) {
|
|
const QString& name = it.key();
|
|
const QVector<const ParsedMethod*>& overloads = it.value();
|
|
s << " if (methodName == \"" << name << "\") {\n";
|
|
bool needArgsSizeCheck = overloads.size() > 1;
|
|
for (const ParsedMethod* m : overloads) {
|
|
if (needArgsSizeCheck) {
|
|
s << " if (args.size() == " << m->params.size() << ") {\n";
|
|
s << " ";
|
|
}
|
|
if (m->returnType == "void" || m->returnType.isEmpty()) {
|
|
s << " " << m->name << "(";
|
|
for (int i = 0; i < m->params.size(); ++i) {
|
|
s << toProviderArgDecode(m->params[i].first,
|
|
QString("args.at(%1)").arg(i),
|
|
QString("arg%1").arg(i));
|
|
if (i + 1 < m->params.size()) s << ", ";
|
|
}
|
|
s << ");\n";
|
|
if (needArgsSizeCheck) s << " ";
|
|
s << " return QVariant(true);\n";
|
|
} else {
|
|
s << " return QVariant::fromValue(" << m->name << "(";
|
|
for (int i = 0; i < m->params.size(); ++i) {
|
|
s << toProviderArgDecode(m->params[i].first,
|
|
QString("args.at(%1)").arg(i),
|
|
QString("arg%1").arg(i));
|
|
if (i + 1 < m->params.size()) s << ", ";
|
|
}
|
|
s << "));\n";
|
|
}
|
|
if (needArgsSizeCheck) {
|
|
s << " }\n";
|
|
}
|
|
}
|
|
s << " }\n";
|
|
}
|
|
// An argument the declared type cannot represent is a REJECTED call, not a
|
|
// failed one: it answers the canonical {"code":"dispatch_failed", ...}
|
|
// object — byte-identical to what the cdylib dispatch and the Rust provider
|
|
// return — so the caller can tell "you sent me the wrong thing" from "the
|
|
// method threw". Anything else the author lets escape stays an ordinary
|
|
// method failure (invalid QVariant) rather than unwinding through Qt event
|
|
// dispatch and killing the module process.
|
|
s << " } catch (const logos::CodecError& e) {\n";
|
|
s << " qWarning() << \"" << className
|
|
<< "::callMethod:\" << methodName << \"rejected:\" << e.what();\n";
|
|
s << " return logos::dispatchFailedVariant(providerName(), "
|
|
"QString::fromUtf8(e.what()));\n";
|
|
s << " } catch (const std::exception& e) {\n";
|
|
s << " qWarning() << \"" << className
|
|
<< "::callMethod:\" << methodName << \"failed:\" << e.what();\n";
|
|
s << " return QVariant();\n";
|
|
s << " }\n";
|
|
s << " qWarning() << \"" << className << "::callMethod: unknown method:\" << methodName;\n";
|
|
s << " return QVariant();\n";
|
|
s << "}\n\n";
|
|
|
|
// getMethods()
|
|
s << "QJsonArray " << className << "::getMethods()\n";
|
|
s << "{\n";
|
|
s << " QJsonArray methods;\n";
|
|
for (const ParsedMethod& m : methods) {
|
|
s << " {\n";
|
|
s << " QJsonObject obj;\n";
|
|
s << " obj[\"name\"] = QStringLiteral(\"" << m.name << "\");\n";
|
|
s << " obj[\"returnType\"] = QStringLiteral(\"" << m.returnType << "\");\n";
|
|
s << " obj[\"isInvokable\"] = true;\n";
|
|
if (!m.description.isEmpty()) {
|
|
s << " obj[\"description\"] = QStringLiteral(\"" << cppStringEscape(m.description) << "\");\n";
|
|
}
|
|
QString sig = m.name + "(";
|
|
for (int i = 0; i < m.params.size(); ++i) {
|
|
sig += m.params[i].first;
|
|
if (i + 1 < m.params.size()) sig += ",";
|
|
}
|
|
sig += ")";
|
|
s << " obj[\"signature\"] = QStringLiteral(\"" << sig << "\");\n";
|
|
if (!m.params.isEmpty()) {
|
|
s << " QJsonArray params;\n";
|
|
for (int i = 0; i < m.params.size(); ++i) {
|
|
s << " params.append(QJsonObject{{\"type\", QStringLiteral(\"" << m.params[i].first << "\")}, {\"name\", QStringLiteral(\"" << m.params[i].second << "\")}});\n";
|
|
}
|
|
s << " obj[\"parameters\"] = params;\n";
|
|
}
|
|
s << " methods.append(obj);\n";
|
|
s << " }\n";
|
|
}
|
|
s << " return methods;\n";
|
|
s << "}\n";
|
|
|
|
QString outputPath = QDir(genDirPath).filePath("logos_provider_dispatch.cpp");
|
|
QFile outFile(outputPath);
|
|
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write dispatch file: " << outputPath << "\n";
|
|
return 5;
|
|
}
|
|
outFile.write(content.toUtf8());
|
|
outFile.close();
|
|
|
|
out << "Generated provider dispatch: " << outputPath << " (" << methods.size() << " methods from " << className << ")\n";
|
|
out.flush();
|
|
return 0;
|
|
}
|
|
|
|
static int generateFromPlugin(const QString& pluginInputPath, const QString& outputDir, bool moduleOnly, ApiStyle apiStyle, const QJsonArray& events, QTextStream& out, QTextStream& err, const QJsonArray& records = {})
|
|
{
|
|
QFileInfo fi(pluginInputPath);
|
|
if (!fi.exists()) {
|
|
err << "Plugin file does not exist: " << pluginInputPath << "\n";
|
|
return 2;
|
|
}
|
|
|
|
QString resolvedPath = fi.canonicalFilePath();
|
|
if (resolvedPath.isEmpty()) {
|
|
resolvedPath = fi.absoluteFilePath();
|
|
}
|
|
|
|
QString genDirPath = outputDir.isEmpty() ? QDir::current().filePath("logos-cpp-sdk/cpp/generated") : outputDir;
|
|
QDir().mkpath(genDirPath);
|
|
|
|
QPluginLoader loader(resolvedPath);
|
|
if (!loader.load()) {
|
|
err << "Failed to load plugin at " << resolvedPath << ": " << loader.errorString() << "\n";
|
|
return 3;
|
|
}
|
|
QObject* instance = loader.instance();
|
|
if (!instance) {
|
|
err << "Plugin loaded but no instance could be created for " << resolvedPath << "\n";
|
|
loader.unload();
|
|
return 4;
|
|
}
|
|
|
|
QString moduleName;
|
|
{
|
|
QJsonObject md = loader.metaData();
|
|
QJsonObject meta = md.value("MetaData").toObject();
|
|
moduleName = meta.value("name").toString();
|
|
if (moduleName.isEmpty()) {
|
|
moduleName = QFileInfo(resolvedPath).baseName();
|
|
}
|
|
}
|
|
|
|
QJsonArray methods;
|
|
LogosProviderPlugin* providerPlugin = qobject_cast<LogosProviderPlugin*>(instance);
|
|
if (providerPlugin) {
|
|
LogosProviderObject* provider = providerPlugin->createProviderObject();
|
|
if (provider) {
|
|
methods = provider->getMethods();
|
|
out << "Detected new-API plugin (LogosProviderPlugin), using getMethods() — "
|
|
<< methods.size() << " methods\n";
|
|
delete provider;
|
|
} else {
|
|
err << "LogosProviderPlugin::createProviderObject() returned null\n";
|
|
}
|
|
} else {
|
|
methods = enumerateMethods(instance);
|
|
}
|
|
|
|
QString className = toPascalCase(moduleName);
|
|
QString headerRel = QString("%1_api.h").arg(moduleName);
|
|
QString sourceRel = QString("%1_api.cpp").arg(moduleName);
|
|
QString headerAbs = QDir(genDirPath).filePath(headerRel);
|
|
QString sourceAbs = QDir(genDirPath).filePath(sourceRel);
|
|
|
|
// Single per-module wrapper file pair. apiStyle decides the
|
|
// signature shape: Qt-typed for legacy / handcrafted callers
|
|
// (default), std-typed and Qt-free when the consuming module's build
|
|
// passed --api-style=lp (typically because it's `interface:
|
|
// "universal"` or `"cdylib"`).
|
|
// Both produce the same filename and class name, so the umbrella
|
|
// doesn't need to know which style was picked. `events` (loaded
|
|
// from a sibling `.lidl` sidecar via --events-from) adds typed
|
|
// `on<EventName>(callback)` accessors next to the existing methods.
|
|
QString header = makeHeader(moduleName, className, methods, apiStyle, events, BindMode::Static, records);
|
|
QString source = makeSource(moduleName, className, headerRel, methods, apiStyle, events, BindMode::Static, records);
|
|
|
|
{
|
|
QFile f(headerAbs);
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write header: " << headerAbs << "\n";
|
|
loader.unload();
|
|
return 5;
|
|
}
|
|
f.write(header.toUtf8());
|
|
f.close();
|
|
}
|
|
{
|
|
QFile f(sourceAbs);
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
err << "Failed to write source: " << sourceAbs << "\n";
|
|
loader.unload();
|
|
return 6;
|
|
}
|
|
f.write(source.toUtf8());
|
|
f.close();
|
|
}
|
|
|
|
if (!moduleOnly) {
|
|
if (!writeUmbrellaHeader(genDirPath, err)) {
|
|
loader.unload();
|
|
return 7;
|
|
}
|
|
if (!writeUmbrellaSource(genDirPath, err)) {
|
|
loader.unload();
|
|
return 8;
|
|
}
|
|
}
|
|
|
|
QJsonDocument doc(methods);
|
|
// out << doc.toJson(QJsonDocument::Indented) << "\n";
|
|
out << "Generated: " << QDir(genDirPath).filePath(headerRel) << " and " << QDir(genDirPath).filePath(sourceRel) << "\n";
|
|
out.flush();
|
|
|
|
loader.unload();
|
|
return 0;
|
|
}
|
|
|
|
int legacy_main(int argc, char* argv[])
|
|
{
|
|
QCoreApplication app(argc, argv);
|
|
|
|
QTextStream err(stderr);
|
|
QTextStream out(stdout);
|
|
|
|
const QStringList args = app.arguments();
|
|
|
|
// Parse --output-dir option
|
|
QString outputDir;
|
|
const int outDirIdx = args.indexOf("--output-dir");
|
|
if (outDirIdx != -1 && outDirIdx + 1 < args.size()) {
|
|
outputDir = args.at(outDirIdx + 1);
|
|
if (outputDir.startsWith('@')) {
|
|
outputDir.remove(0, 1);
|
|
}
|
|
}
|
|
|
|
// Parse --module-only option
|
|
bool moduleOnly = args.contains("--module-only");
|
|
|
|
// Parse --general-only option
|
|
bool generalOnly = args.contains("--general-only");
|
|
|
|
// Parse --api-style option (qt | lp). Picks which type surface
|
|
// the generated `<Module>` wrapper exposes. Default is qt for
|
|
// backward compatibility — every existing module that doesn't
|
|
// declare `interface: "universal"` in its metadata.json keeps
|
|
// its Qt-typed LogosModules surface. Universal / cdylib modules get
|
|
// -DLOGOS_API_STYLE=lp threaded through by mkLogosModule.nix /
|
|
// LogosModule.cmake, which becomes `--api-style=lp` here.
|
|
// Both forms accepted: `--api-style lp` and `--api-style=lp`.
|
|
//
|
|
// `std` was a third surface (std types over a QVariant/LogosAPIClient
|
|
// body). It is retired, and rejected LOUDLY rather than aliased to qt:
|
|
// a stale caller that still passes it wants std signatures, and silently
|
|
// handing it the Qt surface would only fail later, further from the cause.
|
|
ApiStyle apiStyle = ApiStyle::Qt;
|
|
{
|
|
QString apiVal;
|
|
for (int i = 0; i < args.size(); ++i) {
|
|
const QString& a = args.at(i);
|
|
if (a == "--api-style") {
|
|
if (i + 1 < args.size()) apiVal = args.at(i + 1);
|
|
break;
|
|
}
|
|
if (a.startsWith("--api-style=")) {
|
|
apiVal = a.section('=', 1);
|
|
break;
|
|
}
|
|
}
|
|
if (apiVal == "std") {
|
|
err << "--api-style=std was retired: the Std surface (std types over a "
|
|
<< "QVariant/LogosAPIClient body) no longer exists.\n"
|
|
<< "Use 'lp' for the Qt-free std-typed surface, or 'qt' for the "
|
|
<< "Qt-typed one.\n";
|
|
return 1;
|
|
}
|
|
else if (apiVal == "lp") apiStyle = ApiStyle::Lp;
|
|
else if (!apiVal.isEmpty() && apiVal != "qt") {
|
|
err << "Unknown --api-style value: " << apiVal
|
|
<< " (expected 'qt' or 'lp')\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Support: extract dependencies from a metadata.json file
|
|
{
|
|
const int metaIdx = args.indexOf("--metadata");
|
|
if (metaIdx != -1) {
|
|
if (metaIdx + 1 >= args.size()) {
|
|
err << "Usage: " << QFileInfo(app.applicationFilePath()).fileName() << " --metadata /absolute/path/to/metadata.json [--output-dir /path/to/output] [--module-only] [--general-only]\n";
|
|
return 1;
|
|
}
|
|
QString metaPathArg = args.at(metaIdx + 1);
|
|
if (metaPathArg.startsWith('@')) {
|
|
metaPathArg.remove(0, 1);
|
|
}
|
|
QFileInfo mfi(metaPathArg);
|
|
if (!mfi.exists()) {
|
|
err << "Metadata file does not exist: " << metaPathArg << "\n";
|
|
return 2;
|
|
}
|
|
QString metaResolvedPath = mfi.canonicalFilePath();
|
|
if (metaResolvedPath.isEmpty()) {
|
|
metaResolvedPath = mfi.absoluteFilePath();
|
|
}
|
|
QFile mf(metaResolvedPath);
|
|
if (!mf.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
err << "Failed to open metadata file: " << metaResolvedPath << "\n";
|
|
return 3;
|
|
}
|
|
const QByteArray jsonData = mf.readAll();
|
|
mf.close();
|
|
QJsonParseError parseError;
|
|
const QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
|
|
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
|
err << "Invalid metadata JSON in " << metaResolvedPath << ": " << parseError.errorString() << "\n";
|
|
return 4;
|
|
}
|
|
const QJsonObject obj = doc.object();
|
|
const QJsonArray deps = obj.value("dependencies").toArray();
|
|
|
|
// If --general-only provided, generate only the umbrella files.
|
|
// `LogosModules` exposes ONLY the modules listed in
|
|
// `metadata.json#dependencies` — apps that need to manage the
|
|
// core use liblogos' C API directly.
|
|
if (generalOnly) {
|
|
QString genDirPath = outputDir.isEmpty() ? QDir::current().filePath("logos-cpp-sdk/cpp/generated") : outputDir;
|
|
QDir().mkpath(genDirPath);
|
|
|
|
// Collect interface dependencies. Primary source: --interface
|
|
// flags (nix resolves both local `${src}/file` and remote
|
|
// `${input}/file` store paths and passes them here, so the
|
|
// generator never touches flake inputs). Fallback: self-resolve
|
|
// LOCAL interface_dependencies entries (those without an
|
|
// `input`) from metadata.json, relative to the metadata dir —
|
|
// covers non-nix / source-tree builds. Flags win on collision.
|
|
// Dedup --interface flags by name and drop malformed specs:
|
|
// a repeated interface name would emit duplicate
|
|
// #include "<name>_api.h" / bind_<name>(...) into logos_sdk.h
|
|
// and fail to compile, and an empty name/path can only fail
|
|
// later in a less actionable way.
|
|
QVector<InterfaceSpec> ifaceSpecs;
|
|
QSet<QString> haveIface;
|
|
for (const InterfaceSpec& sp : parseSpecFlags(args, "--interface")) {
|
|
if (sp.name.isEmpty() || sp.path.isEmpty()) {
|
|
err << "Ignoring malformed --interface spec (empty name or path)\n";
|
|
continue;
|
|
}
|
|
if (haveIface.contains(sp.name)) {
|
|
err << "Ignoring duplicate --interface '" << sp.name << "'\n";
|
|
continue;
|
|
}
|
|
haveIface.insert(sp.name);
|
|
ifaceSpecs.append(sp);
|
|
}
|
|
|
|
const QString metaDir = QFileInfo(metaResolvedPath).absolutePath();
|
|
const QJsonArray ifaceDeps = obj.value("interface_dependencies").toArray();
|
|
for (const QJsonValue& v : ifaceDeps) {
|
|
if (!v.isObject()) continue;
|
|
const QJsonObject eo = v.toObject();
|
|
const QString name = eo.value("name").toString();
|
|
if (name.isEmpty() || haveIface.contains(name)) continue;
|
|
// Entries with an `input` reference another repo (flake
|
|
// input); only nix can resolve those, via a --interface
|
|
// flag. If we reach here without a matching flag, skip.
|
|
if (eo.contains("input")) {
|
|
err << "Note: interface '" << name << "' has an 'input' (cross-repo) "
|
|
<< "but no --interface flag was passed; skipping (nix supplies the path).\n";
|
|
continue;
|
|
}
|
|
const QString file = eo.value("file").toString();
|
|
if (file.isEmpty()) continue;
|
|
InterfaceSpec spec;
|
|
spec.name = name;
|
|
spec.path = QDir(metaDir).filePath(file);
|
|
spec.implClass = eo.value("impl_class").toString();
|
|
ifaceSpecs.append(spec);
|
|
haveIface.insert(name);
|
|
}
|
|
|
|
// Generate one bound wrapper (<name>_api.{h,cpp}) per interface.
|
|
if (!ifaceSpecs.isEmpty()) {
|
|
if (!generateInterfaceWrappers(ifaceSpecs, genDirPath, apiStyle, out, err)) {
|
|
return 9;
|
|
}
|
|
}
|
|
|
|
// Concrete dependencies generated from their published LIDL
|
|
// (`--dep <name>=<lidl>`). Same backend as interfaces but
|
|
// BindMode::Static — the module name is baked in and the dep is
|
|
// exposed as a `<dep>` MEMBER (the umbrella already emits it from
|
|
// `dependencies`, so no umbrella change). nix passes `--dep` only
|
|
// for deps that publish a `lidl` output; deps without one fall
|
|
// back to the header-copy path and are NOT passed here. Dedup vs
|
|
// each other and vs interface names.
|
|
QVector<InterfaceSpec> depSpecs;
|
|
QSet<QString> haveDep;
|
|
for (const InterfaceSpec& sp : parseSpecFlags(args, "--dep")) {
|
|
if (sp.name.isEmpty() || sp.path.isEmpty()) {
|
|
err << "Ignoring malformed --dep spec (empty name or path)\n";
|
|
continue;
|
|
}
|
|
if (haveIface.contains(sp.name)) {
|
|
err << "Ignoring --dep '" << sp.name << "' (name already used by an interface)\n";
|
|
continue;
|
|
}
|
|
if (haveDep.contains(sp.name)) {
|
|
err << "Ignoring duplicate --dep '" << sp.name << "'\n";
|
|
continue;
|
|
}
|
|
haveDep.insert(sp.name);
|
|
depSpecs.append(sp);
|
|
}
|
|
if (!depSpecs.isEmpty()) {
|
|
if (!generateInterfaceWrappers(depSpecs, genDirPath, apiStyle, out, err, BindMode::Static)) {
|
|
return 9;
|
|
}
|
|
}
|
|
|
|
QStringList interfaceNames;
|
|
for (const InterfaceSpec& sp : ifaceSpecs) interfaceNames.append(sp.name);
|
|
|
|
// Generate umbrella headers based on dependencies + interfaces.
|
|
// For the Lp (Qt-free) flavor the umbrella bakes this module's
|
|
// name as the lp_client origin.
|
|
const QString originName = obj.value("name").toString();
|
|
if (!writeUmbrellaHeaderFromDeps(genDirPath, deps, interfaceNames, err, apiStyle, originName)) {
|
|
return 7;
|
|
}
|
|
if (!writeUmbrellaSourceFromDeps(genDirPath, deps, interfaceNames, err)) {
|
|
return 8;
|
|
}
|
|
|
|
out << "Generated logos_sdk.h and logos_sdk.cpp\n";
|
|
out.flush();
|
|
return 0;
|
|
}
|
|
|
|
// If --module-dir provided, generate for each dependency; else print deps
|
|
const int modDirIdx = args.indexOf("--module-dir");
|
|
if (modDirIdx != -1) {
|
|
if (modDirIdx + 1 >= args.size()) {
|
|
err << "Usage: " << QFileInfo(app.applicationFilePath()).fileName() << " --metadata /path/to/metadata.json --module-dir /path/to/modules_dir [--output-dir /path/to/output] [--module-only] [--general-only]\n";
|
|
return 1;
|
|
}
|
|
QString moduleDirArg = args.at(modDirIdx + 1);
|
|
if (moduleDirArg.startsWith('@')) {
|
|
moduleDirArg.remove(0, 1);
|
|
}
|
|
QDir moduleDir(moduleDirArg);
|
|
if (!moduleDir.exists()) {
|
|
err << "Module directory does not exist: " << moduleDirArg << "\n";
|
|
return 2;
|
|
}
|
|
|
|
QString genDirPath = outputDir.isEmpty() ? QDir::current().filePath("logos-cpp-sdk/cpp/generated") : outputDir;
|
|
QDir().mkpath(genDirPath);
|
|
|
|
QString suffix;
|
|
#if defined(Q_OS_MACOS)
|
|
suffix = ".dylib";
|
|
#elif defined(Q_OS_LINUX)
|
|
suffix = ".so";
|
|
#elif defined(Q_OS_WIN)
|
|
suffix = ".dll";
|
|
#else
|
|
suffix = "";
|
|
#endif
|
|
|
|
int overallStatus = 0;
|
|
for (const QJsonValue& v : deps) {
|
|
if (!v.isString()) continue;
|
|
const QString depName = v.toString();
|
|
const QString pluginFileName = depName + "_plugin" + suffix;
|
|
const QString pluginPath = moduleDir.filePath(pluginFileName);
|
|
if (!QFileInfo::exists(pluginPath)) {
|
|
err << "Skipping: plugin not found for dependency '" << depName << "' at " << pluginPath << "\n";
|
|
continue;
|
|
}
|
|
out << "Running generator for dependency plugin: " << pluginPath << "\n";
|
|
// No --events-from sidecar in the multi-dep iteration
|
|
// path (each dep would need its own sidecar — out of
|
|
// scope here; --events-from is consumed by the
|
|
// per-plugin path below, invoked from buildHeaders.nix).
|
|
const int st = generateFromPlugin(pluginPath, outputDir, moduleOnly, apiStyle, QJsonArray(), out, err);
|
|
if (st != 0) {
|
|
overallStatus = st; // remember last non-zero
|
|
}
|
|
}
|
|
if (overallStatus == 0 && !moduleOnly) {
|
|
if (!writeUmbrellaHeader(genDirPath, err)) {
|
|
overallStatus = 7;
|
|
} else if (!writeUmbrellaSource(genDirPath, err)) {
|
|
overallStatus = 8;
|
|
}
|
|
}
|
|
return overallStatus;
|
|
} else {
|
|
for (const QJsonValue& v : deps) {
|
|
if (v.isString()) {
|
|
out << v.toString() << "\n";
|
|
}
|
|
}
|
|
out.flush();
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --provider-header mode: scan LOGOS_METHOD markers and generate dispatch code
|
|
{
|
|
const int phIdx = args.indexOf("--provider-header");
|
|
if (phIdx != -1) {
|
|
if (phIdx + 1 >= args.size()) {
|
|
err << "Usage: " << QFileInfo(app.applicationFilePath()).fileName() << " --provider-header /path/to/impl.h [--output-dir /path/to/output]\n";
|
|
return 1;
|
|
}
|
|
QString headerArg = args.at(phIdx + 1);
|
|
if (headerArg.startsWith('@')) headerArg.remove(0, 1);
|
|
return generateProviderDispatch(headerArg, outputDir, out, err);
|
|
}
|
|
}
|
|
|
|
if (args.size() < 2) {
|
|
err << "Usage: " << QFileInfo(app.applicationFilePath()).fileName() << " /absolute/path/to/plugin [--output-dir /path/to/output] [--module-only] [--events-from /path/to/<name>.lidl]\n";
|
|
err << " or: " << QFileInfo(app.applicationFilePath()).fileName() << " --metadata /absolute/path/to/metadata.json [--output-dir /path/to/output] [--module-only] [--general-only]\n";
|
|
err << " or: " << QFileInfo(app.applicationFilePath()).fileName() << " --metadata /absolute/path/to/metadata.json --general-only [--output-dir /path/to/output]\n";
|
|
err << " or: " << QFileInfo(app.applicationFilePath()).fileName() << " --provider-header /path/to/impl.h [--output-dir /path/to/output]\n";
|
|
return 1;
|
|
}
|
|
|
|
// --events-from <path>: load typed event prototypes from a LIDL
|
|
// sidecar shipped alongside a dep's pre-built headers. When set,
|
|
// the consumer wrapper (<name>_api.{h,cpp}) gains typed
|
|
// `on<EventName>(callback)` accessors next to the existing
|
|
// generic `onEvent(name, callback)` channel.
|
|
QJsonArray eventsFromSidecar;
|
|
QJsonArray recordsFromSidecar;
|
|
{
|
|
const int evIdx = args.indexOf("--events-from");
|
|
QString evPath;
|
|
if (evIdx != -1 && evIdx + 1 < args.size()) {
|
|
evPath = args.at(evIdx + 1);
|
|
} else {
|
|
for (const QString& a : args) {
|
|
if (a.startsWith("--events-from=")) {
|
|
evPath = a.section('=', 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!evPath.isEmpty() && QFileInfo(evPath).exists()) {
|
|
eventsFromSidecar = loadEventsFromLidl(evPath, err, &recordsFromSidecar);
|
|
}
|
|
}
|
|
|
|
QString argPath = args.at(1);
|
|
return generateFromPlugin(argPath, outputDir, moduleOnly, apiStyle, eventsFromSidecar, out, err, recordsFromSidecar);
|
|
}
|