Skip to content

emit build-database: a rule's inputs are emitted as translation units, and its generated outputs point at a directory that is never filled #724

Description

@Sunrisepeak

mcpp-language-server reads a project's model from mcpp emit build-database --format json. On a project built with mcpp:plugins' rules-qt, the S1 document describes the rule's inputs and outputs in a way an IDE cannot use, so clangd reports errors on every file that includes a generated header. The emit command is following SPEC-005 R2.1 and R2.5 (it writes nothing into the project and runs no action); this is a request to describe what the rules generate, not to generate it.

Seen with mcpp 2026.9.26.2 and 2026.9.27.1, mcpp:plugins 0.15.2 (features = ["rules-qt"]), gcc 16.1.0, Qt 6.11.1 on x86_64 Linux.

The project

[build]
sources = ["src/*.cpp", "ui/*.ui", "res/*.qrc", "i18n/*.ts"]

build.mcpp calls mcpp::rules::qt::compile(...), and src/main.cpp does #include "ui_mainwindow.h" before import std;.

1. Rule inputs are emitted as C++ translation units

The document has a translation unit for each device source the rule claims, with a compiler command:

{
  "source": "<root>/ui/mainwindow.ui",
  "arguments": ["<gcc>/bin/g++", "...", "-c", "<root>/ui/mainwindow.ui", "-o", ".../obj/mainwindow.ui.o"],
  "ide": { "role": "non-module" }
}

The same happens for res/demo.qrc and i18n/qt_demo_zh_CN.ts. These files are compiled by the rule's actions (uic, rcc, lrelease), never by the C++ driver. clangd treats them as linker inputs ('linker' input unused, Indexing ... failed: Couldn't build compiler invocation), and its module scan logs a failure for each of them.

Ask: a source that a rule claims as an action input is not a translation unit of the set. Leave it out, or at least don't give it a compiler command.

2. Generated outputs point at a directory that is never filled

The rule declares every generation as a structured action (rules/qt.cppm in 0.15.2, around line 549):

a.id   = "qt:uic:mainwindow";          // "qt:uic:" + stem
a.role = mcpp::roles::source;
a.arg(uic).arg(in).arg("-o").arg(out).input(in).output(out).submit();   // out = <gen>/ui_mainwindow.h
...
mcpp::include_dir(generic(gen).c_str());

So the plan knows which file is generated, by which action, from which input, and with which command. The S1 document keeps only the include directory:

  • baseline-arguments has -I$MCPP_HOME/cache/build-database/<key>/target/.build-mcpp/out/qt.
  • That directory holds only moc_counter.cpp and qrc_demo.cpp, both 0 bytes. It has no ui_mainwindow.h. Per R2.5 no action runs under emit, so the files never appear there.
  • The translation units for moc_counter.cpp and qrc_demo.cpp point at those 0-byte files.
  • The project's own target/.build-mcpp/out/qt/ has the real ui_mainwindow.h, moc_counter.cpp and qrc_demo.cpp from the last mcpp build.

Effect: src/main.cpp fails with fatal error: 'ui_mainwindow.h' file not found, so clangd cannot scan it for its imports, and the file has no semantics at all. To check that nothing else was wrong, I rewrote the document by hand: the private out/qt replaced by the project's own target/.build-mcpp/out/qt, and the three entries from §1 removed. With that, clangd --check src/main.cpp exits 0 with no diagnostics.

Ask (any of):

  1. Describe the generated outputs. For each generated file or directory a set uses, say that it is generated, by which action (qt:uic:mainwindow), from which inputs (ui/mainwindow.ui), and with which command, e.g. a set-level ide.generated list. The shape is yours to choose. Then an IDE can say "ui_mainwindow.h is generated by qt:uic:mainwindow; build once", instead of reporting a missing header.
  2. Say where a build put them. When mcpp build has already materialized an output under the project's target/, give that path next to the planned one. A reader can then use it read-only. Without it, a consumer has to hard-code how $MCPP_HOME/cache/build-database/<key>/target/... maps onto <root>/target/..., which is mcpp's implementation detail.
  3. (Optional, a change to R2.5.) A mode that runs only code generators a rule declares side-effect free (uic, moc and rcc read their input and write one output) into the private output directory. That would cover a project that was never built. The rule would opt in per action; everything else stays as R2.5 says today.

Also seen while reproducing

These may deserve their own issues.

  • A build-program failure is reported as unclaimed device sources. On a fresh copy of the project, emit (2026.9.26.2 and 2026.9.27.1, with and without MCPP_OFFLINE) fails with MCPP_BUILD_DATABASE_PLAN_FAILED: device sources that no action compiles: i18n/qt_demo_zh_CN.ts, res/demo.qrc, ui/mainwindow.ui ... import the rule package that claims these extensions. The package does import and call the rule. The private directory has the build program's objects (mcpp.o, mcpp.rules.qt.o, ...) but no build.mcpp.bin. On the same machine mcpp build fails linking the build program (ld: cannot find crt1.o, an environment problem here). So the real failure was the build program not linking, and the message sends the user to fix something that isn't broken. The original project plans fine only because its build program is cached (build.mcpp up to date (cached)).
  • emit writes into the project. On every fresh copy, emit created <root>/.mcpp/.xlings.json (the [xlings.workspace] file: "deps": ["xim:[email protected]"]), while the envelope's effects listed only read-project. This goes against R2.1 and R2.6.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions