Skip to content

artifacts dependency rejects duplicate C++ module names across independent executables #732

Description

@julixian

Problem

An executable requested through artifacts is a separate program, but mcpp scans its C++ modules together with the consumer's modules and requires module names to be unique across both programs. This makes otherwise independent executables fail to configure when each has a module with the same name.

Observed with mcpp 2026.9.28.2 while changing a GUI's updater dependency from tools = ["Updater"] to artifacts = ["Updater"]. The GUI uses a core package that compiles boost.ixx, and the updater package also compiled that same boost.ixx for its own executable. mcpp build -p GPPGUI --profile fast-release --configure-only failed with:

error: scanner errors:
  .../Updater/../3rdParty/3rdModule/boost.ixx: module 'boost' is provided by package 'gpp.core' (.../GalTranslPP/../3rdParty/3rdModule/boost.ixx) and by package 'gpp.updater' (.../Updater/../3rdParty/3rdModule/boost.ixx)

Minimal example

app/
  mcpp.toml
  src/common.ixx
  src/main.cpp
updater/
  mcpp.toml
  src/common.ixx
  src/main.cpp

app/mcpp.toml:

[package]
name = "app"
version = "0.1.0"

[dependencies]
updater = { path = "../updater", artifacts = ["updater"] }

[build]
sources = ["src/common.ixx"]
module_extensions = [".ixx"]

[targets.app]
kind = "bin"
main = "src/main.cpp"

updater/mcpp.toml is the same except for the package and target names, and it has no dependency on app:

[package]
name = "updater"
version = "0.1.0"

[build]
sources = ["src/common.ixx"]
module_extensions = [".ixx"]

[targets.updater]
kind = "bin"
main = "src/main.cpp"

Both common.ixx files can contain export module common; export int value() { return 1; }, and both main.cpp files can contain import common; int main() { return value() - 1; }. The two module units belong to different executables and need not import each other. Run mcpp build --configure-only from app/.

The minimal example is extracted from the observed project case; I have not run this reduced directory separately.

Expected behavior

artifacts should build the updater for the consumer's target and profile, while allowing each independent executable to have its own module provider scope (or otherwise isolating the updater's build). The updater's object/BMI should not be linked into the consumer.

Actual behavior / likely cause

scan_packages() collects units from every package and then calls resolve_graph() once. resolve_graph() keys producerOf only by the logical module name and rejects the second provider, without considering which executable owns it. This rejection is understandable for two providers within one executable's import closure, but it also applies across independent executables brought together by an artifacts edge.

The project worked around the conflict by replacing the updater's single import boost; with a Boost header include and removing its duplicate module source. That avoids this particular error but does not address the scope issue.

Related: #711 introduced target-side executable artifacts.

Activity

  1. speak-agent commented on Sep 30, 2026

    @speak-agent
    Member

    Fixed in mcpp 2026.9.30.2 (#745, released with #747). The analysis is section F7 of .agents/docs/2026-09-30-build-wall-time-progress-count-and-hang-plan.md.

    The rule. A module name identifies one module within one program. GCC and clang name a module's entities and its initializer after the module, so a program cannot link two modules of one name (measured: multiple definition of value@common() and of initializer for module common), and two programs may each have one. A build holds several programs, a package and the programs it ships through artifacts or the members of a workspace, and mcpp required a name to be unique across the whole build.

    The change.

    • An import is resolved in the importing package's closure: the package and every package it reaches through code and workspace-member edges, not through artifacts, tools or [build-dependencies]. One resolver answers which provider an import means, and every reader uses it.
    • Two providers of one name are refused only when one closure holds both, and the message names that package; one file reached as two packages is recognised by identity, not spelling (the Updater/../3rdParty/... form of this report).
    • When two packages provide a name, their BMIs lie below their packages' directories and each compile that may import the name is bound to its provider: a module map for GCC, -fmodule-file= for clang, /reference for MSVC; mcpp dyndep --module-map reads the same map. When every name has one provider nothing changes: build.ninja and compile_commands.json of the xlings workspace are byte-identical to 2026.9.30.1's.

    Notes on the report:

    • The reported layout, boost.ixx compiled by gpp.core and by gpp.updater, now builds as it stands, since each program has one boost. A package that provides boost, which both depend on, would compile it once.
    • The minimal example's name common is one of the top-level module names mcpp's naming rule refuses (core, util, common, std, detail, internal, base), independently of this issue; the tests use boost.
    • clangd finds a module by its name in the compilation database, so for a name two packages provide it may show the other program's module; the build is not affected (SPEC-005 R3.8a).

    Criteria: e2e 847 under GCC 16 and clang 22 (an artifacts updater with its own module, an edit rebuilding only its own program, two workspace members, the reported layout, and the two refusals; it fails on 2026.9.30.1 with the error of this issue); e2e 848 under MSVC; unit tests of the resolver. Sandbox verification against the published release: sections 1, 2 and 3 pass against the published 2026.9.30.2 in an xlings sandbox (xlings subos use v9302 --sandbox, CN mirror for xlings and mcpp): an app and its artifacts updater each have their own module boost; two providers in one program are refused, naming the module; two workspace members each have their own boost. The full script: 10 of 10 sections pass.

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