Skip to content

feat: add fmt 12.2.0 as C++23 module package (import fmt;)#63

Merged
Sunrisepeak merged 4 commits into
mcpplibs:mainfrom
1115040131:add-fmt-module
Jul 8, 2026
Merged

feat: add fmt 12.2.0 as C++23 module package (import fmt;)#63
Sunrisepeak merged 4 commits into
mcpplibs:mainfrom
1115040131:add-fmt-module

Conversation

@1115040131

@1115040131 1115040131 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

概述

新增公开的 fmt 包(空命名空间),将 {fmt} 12.2.0 以 C++23 模块形态收录,
用户可直接 import fmt;。它是 header 版 compat.fmt 的模块孪生包,两者
消费模型不同、可共存。

形态:module wrapper(generated_files)

fmt 的 release 自带官方模块单元 src/fmt.cc(export module fmt;),但不能
逐字喂给 mcpp
:mcpp 的 M1 模块扫描器拒绝任何位于条件预处理块内的 import
(即使是不启用的死分支),而 fmt.cc 把 import std; 包在 #ifdef FMT_IMPORT_STD
里。该限制在 mcpp 0.0.81 / 0.0.82 / main 均存在,非版本问题。

因此描述符用 generated_files 复刻 fmt.cc,仅删除那 3 行(#ifdef /
import std; / #endif),其余逐字保留:

  • 全局模块片段的 #include(std 头挂到全局模块)
  • export module fmt;(mcpp 据此注册 provider)
  • FMT_EXPORT 宏机制(fmt 头文件在 FMT_MODULE 下自行 export)
  • 末尾 #include "format.cc" / "os.cc"(把实现附着到模块 fmt;若作为独立 TU
    编译会留在全局模块,破坏模块链接)

include_dirs 暴露 */include(模块单元的 #include <fmt/*.h> 可解析)与
*/src(末尾 impl include 可解析)。import_std = false(默认走文本 std 头)。

CI

空命名空间包无法作为 workspace 成员(同公开的 imgui 包:空命名空间映射到
mcpp 内建默认索引,[indices] 无法把默认索引重定向到本地 path)。故新增
tests/smoke_fmt_module.sh —— reseed 默认索引后消费 import fmt;,在
validate.yml 中作为 fmt-module job 运行(与 imgui-module 同构)。

镜像

无 mcpp-res 写权限,下载 url 采用纯上游字符串(lint 对纯字符串 url 放行),
CN 镜像待维护者后续补充。

验证

mcpp 0.0.81(与 CI MCPP_VERSION 一致)本地跑 smoke:构建模块 fmt 并通过
行为断言。

Compiling fmt v12.2.0
Finished release [optimized]
Running target/.../bin/fmt-module-smoke
fmt module package ok: 2 + 3 = 5

本地复现 CI lint 全过,validate.yml YAML 校验通过。

Module sibling of the header-based compat.fmt: the public `fmt` package
(empty namespace) exposes the C++23 module `fmt` so users write
`import fmt;`.

fmt's release ships an official module unit at src/fmt.cc, but mcpp's M1
scanner rejects any `import` inside a conditional preprocessor block (even
a dead one), and fmt.cc guards `import std;` behind `#ifdef FMT_IMPORT_STD`.
So the descriptor reproduces fmt.cc via generated_files with exactly those
three lines removed — everything else verbatim, including the tail
`#include "format.cc"/"os.cc"` that attaches the implementation to module
fmt. include_dirs exposes */include and */src so both the module unit and
its impl includes resolve.

Empty-namespace packages can't be workspace members (like the imgui module
package), so a thin driver tests/smoke_fmt_module.sh reseeds the default
index and consumes `import fmt;`, wired into validate.yml as the fmt-module
job.

Verified locally on mcpp 0.0.81 (== CI): the smoke builds module fmt and
its behavioral assertions pass. No mcpp-res write access, so the download
url is a plain upstream string (CN mirror to be added by a maintainer).

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread .github/workflows/validate.yml Outdated
Comment thread pkgs/f/fmtlib.fmt.lua Outdated
modules = { "fmt" },
include_dirs = { "*/include", "*/src" },
generated_files = {
["mcpp_generated/fmt_module.cc"] = "// Formatting library for C++ - C++20 module\n//\n// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors\n// All rights reserved.\n//\n// For the license information refer to format.h.\n\nmodule;\n\n#define FMT_MODULE\n\n#ifdef _MSVC_LANG\n# define FMT_CPLUSPLUS _MSVC_LANG\n#else\n# define FMT_CPLUSPLUS __cplusplus\n#endif\n\n// Put all implementation-provided headers into the global module fragment\n// to prevent attachment to this module.\n#ifndef FMT_IMPORT_STD\n# include <algorithm>\n# include <bitset>\n# include <chrono>\n# include <cmath>\n# include <complex>\n# include <cstddef>\n# include <cstdint>\n# include <cstdio>\n# include <cstdlib>\n# include <cstring>\n# include <ctime>\n# include <exception>\n# if FMT_CPLUSPLUS > 202002L\n# include <expected>\n# endif\n# include <filesystem>\n# include <fstream>\n# include <functional>\n# include <iterator>\n# include <limits>\n# include <locale>\n# include <memory>\n# include <optional>\n# include <ostream>\n# include <source_location>\n# include <stdexcept>\n# include <string>\n# include <string_view>\n# include <system_error>\n# include <thread>\n# include <type_traits>\n# include <typeinfo>\n# include <utility>\n# include <variant>\n# include <vector>\n#else\n# include <limits.h>\n# include <stdint.h>\n# include <stdio.h>\n# include <stdlib.h>\n# include <string.h>\n# include <time.h>\n#endif\n#include <cerrno>\n#include <climits>\n#include <version>\n\n#if __has_include(<cxxabi.h>)\n# include <cxxabi.h>\n#endif\n#if defined(_MSC_VER) || defined(__MINGW32__)\n# include <intrin.h>\n#endif\n#if defined __APPLE__ || defined(__FreeBSD__)\n# include <xlocale.h>\n#endif\n#if __has_include(<winapifamily.h>)\n# include <winapifamily.h>\n#endif\n#if (__has_include(<fcntl.h>) || defined(__APPLE__) || \\\n defined(__linux__)) && \\\n (!defined(WINAPI_FAMILY) || (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP))\n# include <fcntl.h>\n# include <sys/stat.h>\n# include <sys/types.h>\n# ifndef _WIN32\n# include <unistd.h>\n# else\n# include <io.h>\n# endif\n#endif\n#ifdef _WIN32\n# if defined(__GLIBCXX__)\n# include <ext/stdio_filebuf.h>\n# include <ext/stdio_sync_filebuf.h>\n# endif\n# define WIN32_LEAN_AND_MEAN\n# include <windows.h>\n#endif\n\nexport module fmt;\n\n\n#define FMT_EXPORT export\n#define FMT_BEGIN_EXPORT export {\n#define FMT_END_EXPORT }\n\n// If you define FMT_ATTACH_TO_GLOBAL_MODULE\n// - all declarations are detached from module 'fmt'\n// - the module behaves like a traditional static library, too\n// - all library symbols are mangled traditionally\n// - you can mix TUs with either importing or #including the {fmt} API\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\nextern \"C++\" {\n#endif\n\n#ifndef FMT_OS\n# define FMT_OS 1\n#endif\n\n// All library-provided declarations and definitions must be in the module\n// purview to be exported.\n#include \"fmt/args.h\"\n#include \"fmt/chrono.h\"\n#include \"fmt/color.h\"\n#include \"fmt/compile.h\"\n#include \"fmt/format.h\"\n#if FMT_OS\n# include \"fmt/os.h\"\n#endif\n#include \"fmt/ostream.h\"\n#include \"fmt/printf.h\"\n#include \"fmt/ranges.h\"\n#include \"fmt/std.h\"\n#include \"fmt/xchar.h\"\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\n}\n#endif\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\nextern \"C++\" {\n#endif\n\n#if FMT_HAS_INCLUDE(\"format.cc\")\n# include \"format.cc\"\n#endif\n#if FMT_OS && FMT_HAS_INCLUDE(\"os.cc\")\n# include \"os.cc\"\n#endif\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\n}\n#endif\n",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个可以使用多行语法展开 不用使用 \n 放到单行。类似下面

        generated_files = {
            ["mcpp_generated/nlohmann.json.cppm"] = [[
// L1
// L2
// ....
            ]],
        },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

尝试用多行语法展开遇到如下报错:
Workspace building member 'tests/examples/fmtlib.fmt'
Resolving toolchain
Resolved gcc@16.1.0 → @mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++
error: dependency 'fmtlib.fmt': xpkg-lua://fmtlib.fmt@12.2.0: error: malformed mcpp segment near key 'Formatting'

是否是因为mcpp不支持lua的多行展开语法

Comment thread tests/smoke_fmt_module.sh Outdated
Comment thread pkgs/f/fmt.lua Outdated
@Sunrisepeak

Copy link
Copy Markdown
Member

这个62PR 很规范, 建议 本PR直接 作为支持模块化的 fmt 库 然后 添加类似 62的 tests 最小测试工程。就可以省略 62 compat 模式了

@Sunrisepeak

Copy link
Copy Markdown
Member

最新版本好像默认就是支持 模块化的 不用额外 使用 generated_files 好像

@Sunrisepeak

Copy link
Copy Markdown
Member

#ifdef FMT_IMPORT_STD 包描述文件直接定义这个宏就可以了

1115040131 and others added 2 commits July 8, 2026 15:06
Replace the shell smoke driver for the {fmt} module package with a native
workspace member under tests/examples/fmtlib.fmt, matching the compat.* /
nlohmann.json members. The empty-namespace `fmt` package resolved through the
builtin default index, which a member can't point at a local path — so the
package is renamed to the `fmtlib` namespace (fmtlib.fmt), letting the member
route the dependency to this repo via `[indices] fmtlib = { path }`. The
exported module is still plain `fmt` (import fmt;). Drops tests/
smoke_fmt_module.sh and its fmt-module CI job; `mcpp test --workspace` now
covers it.

Co-Authored-By: Claude <noreply@anthropic.com>
The generated fmt_module.cc now `#define FMT_IMPORT_STD` (right after
FMT_MODULE) so fmt's own `#ifndef FMT_IMPORT_STD` block takes the import-std
path — dropping the ~40 `#include <algorithm>`…`<vector>` lines — and the
`#ifdef FMT_IMPORT_STD` guard around `import std;` is removed so the import is
unconditional at module top level (which M1's text scanner accepts). Sets
import_std = true so mcpp builds the std module first. Faster to compile than
the header-include variant; behavior unchanged (format_test passes).

Co-Authored-By: Claude <noreply@anthropic.com>
@1115040131

1115040131 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

环境:mcpp v0.0.84 — modern C++23 build tool
尝试用官方的模块化支持文件

    mcpp = {
        schema       = "0.1",
        language     = "c++23",
        import_std   = true,
        modules      = { "fmt" },
        include_dirs = { "*/include", "*/src" },
        sources      = { "*/src/fmt.cc" },
        cflags       = { "-DFMT_IMPORT_STD" },
        targets      = { ["fmt"] = { kind = "lib" } },
        deps         = { },
    },

遇到如下问题

   Workspace building member 'tests/examples/fmtlib.fmt'
   Resolving toolchain
    Resolved gcc@16.1.0 → @mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++
error: scanner errors:
  /home/pyc/dev/mcpp-index/tests/examples/fmtlib.fmt/.mcpp/.xlings/data/xpkgs/fmtlib-x-fmtlib.fmt/12.2.0/fmt-12.2.0/src/fmt.cc:104: import statement inside conditional preprocessor block (forbidden in M1)

agent分析定位

第 104 行的 import 语句位于一个条件预处理块内部——这在 M1 阶段是被禁止的。

  对应源码正是这三行:

  103| #ifdef FMT_IMPORT_STD    ← 条件预处理块开始
  104| import std;             ← import 落在块内 → 报错
  105| #endif                  ← 条件块结束

  为什么 mcpp 要禁止

  关键在于 "M1" 指的是 mcpp 的 module scanner(模块扫描器)第 1 阶段。要理解为什么,得先看 C++20 模块给构建系统带来的根本难题。

  问题:模块必须按依赖顺序编译。 如果 a.cppm 里写了 import b;,那么 b 必须先编译出 BMI(二进制模块接口)文件,a 才能编译。构建系统在真正调用编译器之前,必须先知道每个文件 import 了谁、export 
  了什么模块,才能排出正确的编译顺序(拓扑排序)。这个"预先扫描每个源文件、提取 import/export 关系"的步骤,就是 module scanner 干的活,也就是这里的 M1。

  M1 是纯文本扫描,不是编译器。 为了快,scanner 不会去做完整的 C++ 预处理(宏展开、#ifdef 求值、头文件递归展开代价极大),它只是逐行读文本,用类似正则的方式识别 import X; / export module Y; 这些行。

  这就带来一个死结:

  #ifdef FMT_IMPORT_STD
  import std;
  #endif

  scanner 看到文本里有 import std;,但它不知道 FMT_IMPORT_STD 到底有没有定义——因为求值 #ifdef 需要完整预处理,而那正是 scanner 为了性能刻意跳过的。于是 scanner 面对一个无法回答的问题:

  ▎ 这条 import std; 到底算不算数?要不要把 std 加进依赖图、先去准备 std 模块?

  - 如果当真(假设分支为真)→ 万一实际编译时 FMT_IMPORT_STD 没定义,就凭空多了个 std 依赖,可能拖慢或拖垮构建;
  - 如果忽略(假设分支为假)→ 万一实际编译时宏定义了,import std; 生效,但 scanner 没把 std 排进依赖顺序,编译器就会因为 BMI 没准备好而失败。

  两种猜测都可能错。mcpp 的设计选择是:不猜——直接报错,要求 import 语句必须无条件地出现在顶层,这样扫描结果才是确定的、可靠的。这就是 "forbidden in M1" 的由来。

  为什么定义宏也没用(你刚才试过)

  你的思路是"那我在包描述里 -DFMT_IMPORT_STD 把宏定义了,让分支为真不就行了?"——实测仍报同样的错。

  原因:-D 宏是给编译器的,而报错发生在编译器还没上场的 M1 文本扫描阶段。 scanner 根本不看 cflags、不做宏求值,它只看到源码文本里 import 被 #ifdef ... #endif
  包着这个文本事实。无论宏定义与否、无论分支真假,只要 import 在文本上处于条件块内,就一律拒绝。所以定义宏改变不了文本结构,报错照旧。

基于当前现状,采取的做法是
将官方的模块化支持文件复制到generated_files 内并删去 import std; 的宏包裹,并在文件开头添加 #define FMT_IMPORT_STD

Comment thread pkgs/f/fmtlib.fmt.lua Outdated
modules = { "fmt" },
include_dirs = { "*/include", "*/src" },
generated_files = {
["mcpp_generated/fmt_module.cc"] = "// Formatting library for C++ - C++20 module\n//\n// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors\n// All rights reserved.\n//\n// For the license information refer to format.h.\n\nmodule;\n\n#define FMT_MODULE\n\n#define FMT_IMPORT_STD\n\n#ifdef _MSVC_LANG\n# define FMT_CPLUSPLUS _MSVC_LANG\n#else\n# define FMT_CPLUSPLUS __cplusplus\n#endif\n\n// Put all implementation-provided headers into the global module fragment\n// to prevent attachment to this module.\n#ifndef FMT_IMPORT_STD\n# include <algorithm>\n# include <bitset>\n# include <chrono>\n# include <cmath>\n# include <complex>\n# include <cstddef>\n# include <cstdint>\n# include <cstdio>\n# include <cstdlib>\n# include <cstring>\n# include <ctime>\n# include <exception>\n# if FMT_CPLUSPLUS > 202002L\n# include <expected>\n# endif\n# include <filesystem>\n# include <fstream>\n# include <functional>\n# include <iterator>\n# include <limits>\n# include <locale>\n# include <memory>\n# include <optional>\n# include <ostream>\n# include <source_location>\n# include <stdexcept>\n# include <string>\n# include <string_view>\n# include <system_error>\n# include <thread>\n# include <type_traits>\n# include <typeinfo>\n# include <utility>\n# include <variant>\n# include <vector>\n#else\n# include <limits.h>\n# include <stdint.h>\n# include <stdio.h>\n# include <stdlib.h>\n# include <string.h>\n# include <time.h>\n#endif\n#include <cerrno>\n#include <climits>\n#include <version>\n\n#if __has_include(<cxxabi.h>)\n# include <cxxabi.h>\n#endif\n#if defined(_MSC_VER) || defined(__MINGW32__)\n# include <intrin.h>\n#endif\n#if defined __APPLE__ || defined(__FreeBSD__)\n# include <xlocale.h>\n#endif\n#if __has_include(<winapifamily.h>)\n# include <winapifamily.h>\n#endif\n#if (__has_include(<fcntl.h>) || defined(__APPLE__) || \\\n defined(__linux__)) && \\\n (!defined(WINAPI_FAMILY) || (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP))\n# include <fcntl.h>\n# include <sys/stat.h>\n# include <sys/types.h>\n# ifndef _WIN32\n# include <unistd.h>\n# else\n# include <io.h>\n# endif\n#endif\n#ifdef _WIN32\n# if defined(__GLIBCXX__)\n# include <ext/stdio_filebuf.h>\n# include <ext/stdio_sync_filebuf.h>\n# endif\n# define WIN32_LEAN_AND_MEAN\n# include <windows.h>\n#endif\n\nexport module fmt;\n\nimport std;\n\n#define FMT_EXPORT export\n#define FMT_BEGIN_EXPORT export {\n#define FMT_END_EXPORT }\n\n// If you define FMT_ATTACH_TO_GLOBAL_MODULE\n// - all declarations are detached from module 'fmt'\n// - the module behaves like a traditional static library, too\n// - all library symbols are mangled traditionally\n// - you can mix TUs with either importing or #including the {fmt} API\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\nextern \"C++\" {\n#endif\n\n#ifndef FMT_OS\n# define FMT_OS 1\n#endif\n\n// All library-provided declarations and definitions must be in the module\n// purview to be exported.\n#include \"fmt/args.h\"\n#include \"fmt/chrono.h\"\n#include \"fmt/color.h\"\n#include \"fmt/compile.h\"\n#include \"fmt/format.h\"\n#if FMT_OS\n# include \"fmt/os.h\"\n#endif\n#include \"fmt/ostream.h\"\n#include \"fmt/printf.h\"\n#include \"fmt/ranges.h\"\n#include \"fmt/std.h\"\n#include \"fmt/xchar.h\"\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\n}\n#endif\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\nextern \"C++\" {\n#endif\n\n#if FMT_HAS_INCLUDE(\"format.cc\")\n# include \"format.cc\"\n#endif\n#if FMT_OS && FMT_HAS_INCLUDE(\"os.cc\")\n# include \"os.cc\"\n#endif\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\n}\n#endif\n",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是不是可以省略, 源码里好像已经有对应模块化的代码了 src/fmt.cc 具体见下面

Comment thread pkgs/f/fmtlib.fmt.lua Outdated
generated_files = {
["mcpp_generated/fmt_module.cc"] = "// Formatting library for C++ - C++20 module\n//\n// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors\n// All rights reserved.\n//\n// For the license information refer to format.h.\n\nmodule;\n\n#define FMT_MODULE\n\n#define FMT_IMPORT_STD\n\n#ifdef _MSVC_LANG\n# define FMT_CPLUSPLUS _MSVC_LANG\n#else\n# define FMT_CPLUSPLUS __cplusplus\n#endif\n\n// Put all implementation-provided headers into the global module fragment\n// to prevent attachment to this module.\n#ifndef FMT_IMPORT_STD\n# include <algorithm>\n# include <bitset>\n# include <chrono>\n# include <cmath>\n# include <complex>\n# include <cstddef>\n# include <cstdint>\n# include <cstdio>\n# include <cstdlib>\n# include <cstring>\n# include <ctime>\n# include <exception>\n# if FMT_CPLUSPLUS > 202002L\n# include <expected>\n# endif\n# include <filesystem>\n# include <fstream>\n# include <functional>\n# include <iterator>\n# include <limits>\n# include <locale>\n# include <memory>\n# include <optional>\n# include <ostream>\n# include <source_location>\n# include <stdexcept>\n# include <string>\n# include <string_view>\n# include <system_error>\n# include <thread>\n# include <type_traits>\n# include <typeinfo>\n# include <utility>\n# include <variant>\n# include <vector>\n#else\n# include <limits.h>\n# include <stdint.h>\n# include <stdio.h>\n# include <stdlib.h>\n# include <string.h>\n# include <time.h>\n#endif\n#include <cerrno>\n#include <climits>\n#include <version>\n\n#if __has_include(<cxxabi.h>)\n# include <cxxabi.h>\n#endif\n#if defined(_MSC_VER) || defined(__MINGW32__)\n# include <intrin.h>\n#endif\n#if defined __APPLE__ || defined(__FreeBSD__)\n# include <xlocale.h>\n#endif\n#if __has_include(<winapifamily.h>)\n# include <winapifamily.h>\n#endif\n#if (__has_include(<fcntl.h>) || defined(__APPLE__) || \\\n defined(__linux__)) && \\\n (!defined(WINAPI_FAMILY) || (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP))\n# include <fcntl.h>\n# include <sys/stat.h>\n# include <sys/types.h>\n# ifndef _WIN32\n# include <unistd.h>\n# else\n# include <io.h>\n# endif\n#endif\n#ifdef _WIN32\n# if defined(__GLIBCXX__)\n# include <ext/stdio_filebuf.h>\n# include <ext/stdio_sync_filebuf.h>\n# endif\n# define WIN32_LEAN_AND_MEAN\n# include <windows.h>\n#endif\n\nexport module fmt;\n\nimport std;\n\n#define FMT_EXPORT export\n#define FMT_BEGIN_EXPORT export {\n#define FMT_END_EXPORT }\n\n// If you define FMT_ATTACH_TO_GLOBAL_MODULE\n// - all declarations are detached from module 'fmt'\n// - the module behaves like a traditional static library, too\n// - all library symbols are mangled traditionally\n// - you can mix TUs with either importing or #including the {fmt} API\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\nextern \"C++\" {\n#endif\n\n#ifndef FMT_OS\n# define FMT_OS 1\n#endif\n\n// All library-provided declarations and definitions must be in the module\n// purview to be exported.\n#include \"fmt/args.h\"\n#include \"fmt/chrono.h\"\n#include \"fmt/color.h\"\n#include \"fmt/compile.h\"\n#include \"fmt/format.h\"\n#if FMT_OS\n# include \"fmt/os.h\"\n#endif\n#include \"fmt/ostream.h\"\n#include \"fmt/printf.h\"\n#include \"fmt/ranges.h\"\n#include \"fmt/std.h\"\n#include \"fmt/xchar.h\"\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\n}\n#endif\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\nextern \"C++\" {\n#endif\n\n#if FMT_HAS_INCLUDE(\"format.cc\")\n# include \"format.cc\"\n#endif\n#if FMT_OS && FMT_HAS_INCLUDE(\"os.cc\")\n# include \"os.cc\"\n#endif\n\n#ifdef FMT_ATTACH_TO_GLOBAL_MODULE\n}\n#endif\n",
},
sources = { "mcpp_generated/fmt_module.cc" },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

使用库自带的模块化代码 src/fmt.cc 并且 使用 宏进行开启。应该可以避免 generated_files

mcpp = {
    ...
    cxxflags = { "-DFMT_IMPORT_STD" },
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

还是报同样的错,根因在于mcpp 的 M1 模块扫描器是预处理之前的纯文本扫描,它根本不求值 #ifdef。所以:

  • -DFMT_IMPORT_STD 对扫描器毫无作用;
  • 上游 fmt.cc:104 的 import std; 被包在 #ifdef FMT_IMPORT_STD 里,扫描器只看到「条件块里有 import」就直接报错。

@Sunrisepeak Sunrisepeak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

目前mcpp版本对宏支持待优化, 该版本暂先合入。后面mcpp更新最新版本后 再进行优化包描述

The generated module wrapper was `mcpp_generated/fmt_module.cc`. On
macOS/Windows mcpp drives Clang in gnu mode (`--driver-mode=g++`), where
Clang decides "is this a module interface unit?" purely from the file
extension: `.cppm`/`.ixx` yes, `.cc` no. A `.cc` module unit still
compiles (only the fopen deprecation warning), but Clang silently drops
`-fmodule-output` as an unused argument, so no BMI (`fmt.pcm`) is ever
written — the consumer's `import fmt;` then fails with
`fatal error: module 'fmt' not found`.

GCC (Linux) keys off `export module` rather than the extension, so `.cc`
happened to work there and masked the bug; nlohmann.json already uses a
`.cppm` wrapper, which is why it passed on all three platforms. Rename
both the generated_files key and sources to `fmt_module.cppm` to match.
Verified end-to-end on Windows (clang 20.1.7, MSVC STL): fmt.pcm is now
produced and format_test passes. import std is unrelated and retained.

Co-Authored-By: Claude <noreply@anthropic.com>
@1115040131

Copy link
Copy Markdown
Contributor Author

根据上一轮CI结果报告另一项缺陷

现象

CI(以及本地 Windows)构建 fmtlib.fmt 这个 workspace 成员失败。fmt 模块封装单元本身编译通过(仅有一条 fopen 弃用警告),但消费者环节报错:

tests/examples/fmtlib.fmt/tests/format_test.cpp:6:8: fatal error: module 'fmt' not found
    6 | import fmt;

Linux 通过,macOS 与 Windows(两者都是 Clang)失败。

根因

mcpp 生成的模块封装文件原先命名为 mcpp_generated/fmt_module.cc

在 macOS/Windows 上,mcpp 以 GNU 驱动模式(--driver-mode=g++)调用 Clang。这种模式下,Clang 仅凭文件扩展名判断"这是不是一个模块接口单元":.cppm / .ixx 会被当作模块接口,.cc 则是普通源文件。

因此,对 .cc 封装文件,Clang 静默丢弃了 -fmodule-output,当成未使用参数:

clang++: warning: argument unused during compilation:
  '-fmodule-output=...\fmt.pcm' [-Wunused-command-line-argument]

结果 BMI(fmt.pcm)从未生成。编译仍然 exit 0 并产出 .o,但磁盘上没有 fmt.pcm,消费者的 import fmt; 也就无从解析该模块。

修复

在 descriptor 中把封装文件从 .cc 改名为 .cppm(generated_files 键 + sources),封装内容一字未改。这与既有的 nlohmann.json 约定一致。

-        ["mcpp_generated/fmt_module.cc"]  = "…"
+        ["mcpp_generated/fmt_module.cppm"] = "…"
-        sources = { "mcpp_generated/fmt_module.cc" }
+        sources = { "mcpp_generated/fmt_module.cppm" }

已在 Windows 上端到端验证:fmt.pcm 正常产出,format_test 通过。

建议的上游跟进(mcpp)

这是 mcpp 自身的一个陷阱:当某个包通过 generated_files 产出模块接口封装时,mcpp 应当二选一——要么把封装文件的扩展名默认设为 .cppm,要么显式传 -x c++-module,让 Clang 无论扩展名如何都将其识别为接口单元。建议向 mcpp 提 issue,避免后续的模块包再踩到同样的"BMI 被静默丢弃"问题。

@Sunrisepeak Sunrisepeak merged commit 0c1aa14 into mcpplibs:main Jul 8, 2026
6 checks passed
@Sunrisepeak

Copy link
Copy Markdown
Member

Test on ubuntu - ok

image

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants