Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions .agents/docs/2026-08-04-add-godot-cpp-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# 收录 godot-cpp:compat.godot-cpp(本 PR)与模块层 godotengine.godot-cpp(后续 PR)

日期:2026-08-04
上游:<https://github.com/godotengine/godot-cpp> · 版本 `godot-4.5-stable` → 索引版本 `4.5.0`

## 1. 形态判定

godot-cpp 是 Godot GDExtension 的 C++ 绑定库。表面上属于「C++ 源码 compat」,但它有一个决定性特征:

**上游 tag 归档只是半棵源码树。** 另一半 —— 约 1000 个引擎类与全部 builtin Variant 类型,即
`gen/include/` 与 `gen/src/` —— 由上游自带的 `binding_generator.py` 从
`gdextension/extension_api.json` 在构建时生成,任何上游 tag 归档或 release 都不含它:

```
上游归档: include/ 67 个 .hpp src/ 32 个 .cpp
生成之后: +1010 个 .hpp +990 个 .cpp (共 18 MB)
```

于是形态判定的真正问题不是模板选哪个,而是**这一步 codegen 在哪里跑**。三个选项:

| 方案 | 结论 |
|---|---|
| 消费侧 `install()` 钩子跑 python | 否决。会让 Python 成为每个消费者、每个平台的硬依赖,与本索引既有做法相悖(compat.ffmpeg 的 config 快照、opencv 包的 gen/ 快照都是「消费侧不跑 codegen」) |
| 把 gen/ 塞进 `generated_files` | 否决。18 MB 文本进描述符 |
| **离线跑一次,产物随镜像归档发布** | **采用** |

因此下载地址不是上游归档,而是 `xlings-res/godot-cpp` 的重打包归档 —— 与
[[windows-symlink-archives]] 里 asio 的 repack 惯例同源,只是这次加的是 `gen/` 而不是去符号链接。

`tools/godot-cpp/repack.sh` 是该归档的唯一来源与验证器:

1. 拉上游 tag 归档(`ac78539c…e339c`);
2. 跑**未修改**的上游 `binding_generator.py`(默认配置:64 位、`precision=single`、`template_get_node` 开);
3. 把重新解包的上游树与生成后的树做 `diff -r`,**除 `gen/` 之外任何差异即拒绝打包**;
4. 确定性打包(`--sort=name`、固定 mtime、`gzip -n`)。

连跑两次 sha256 相同:`b0c36e77f02c4181352cdd7547b209b93a833be1ad6197f8c650d92987221a00`。

### 版本号

上游 tag 是 `godot-4.5-stable`,索引版本取裸版本 `4.5.0`(lint 拦前导 `v`,且给 4.5.x 留位)。

## 2. 镜像

| 区域 | 地址 |
|---|---|
| GLOBAL | `https://github.com/xlings-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz` |
| CN | `https://gitcode.com/mcpp-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz` |

两侧 sha256 一致,均等于本地打包结果(下载回来逐一核过)。仓库 README 记录了复现方式与上游归档 sha。

三平台共用同一份 OS 中立归档:godot-cpp 是可移植 C++,没有按平台切换的源码集合(平台差异在 Godot 本体里,
被 `gdextension_interface.h` 这层 C ABI 挡住了)。

## 3. 描述符要点

- `include_dirs = { "*/include", "*/gen/include", "*/gdextension" }` —— 与上游 cmake 暴露的三个根一致。
- `sources` 逐层枚举(`*/src/*.cpp`、`*/src/*/*.cpp`、`*/gen/src/*/*.cpp`)而非 `**`:上游自带的
`test/src/*.cpp` 是它自己的示例扩展,不能被扫进库里。
- 目标名 `godot-cpp`,kind = lib,共 1022 个 TU。
- `src/core/object.cpp` 与 `gen/src/classes/object.cpp` **basename 撞名**。mcpp ≥ 0.0.98 的 obj 路径消歧
(mcpp#233/#240)已覆盖这种同包内撞名,本地实测链接正常;这是本包对客户端版本下限的隐含依赖,
index.toml 现有 floor 远高于它。

### -fPIC

**GDExtension 本身就是一个 shared library**,所以这个静态库的 .o 几乎必然要链进 .so/.dylib。
不加 `-fPIC` 时链接直接失败(`relocation R_X86_64_32 against '.rodata' can not be used when
making a shared object`),等于这个包做不了它唯一存在的意义 —— 上游 SCons/CMake 也正是为此传 `-fPIC`。
故 `linux`/`macosx` 的 `cxxflags` 加 `-fPIC`;windows 不需要(PE 无此区分,clang-cl 只会报 flag 未使用)。

这条是写 examples/summator(真 GDExtension,`kind = "shared"`)时被链接器抓出来的,不是推断的。

### define 与 feature 评估

`GDEXTENSION` 是上游 cmake 挂在 target INTERFACE 上的 **PUBLIC** define,库与消费者 TU 必须一致,
所以走 `default = { implies = { "gdextension" } }`(feature 的 `defines` 才到得了消费端,而
`default.implies` 无条件生效 —— 与 compat.curl 的 `CURL_STATICLIB` 同一解法)。

以下 define **有意不做成 feature**:

- `DEBUG_ENABLED` / `DEV_ENABLED`:额外检查,上游 release 默认关。
- `HOT_RELOAD_ENABLED`:会改 `Wrapped` 的布局 —— 属于 ABI,不是开关。
- `REAL_T_IS_DOUBLE`:需要用 `precision=double` 重新生成的另一棵 `gen/` 树,归档里没有,
所以它根本不可能是本包的 feature;真要支持是另一个版本/包。

共同理由:每个都会把 store 重新 key 一次,等于把同一个库的 1000 TU 再全量编一遍。

## 4. 测试成员

`tests/examples/godot-cpp/`(根 `[indices] compat` 继承,成员不再声明)。断言分两类,都能真失败:

- **链接类**:`Vector2::length()`、`Basis::orthonormalized()`、`Color::to_rgba32()`、`AABB::get_volume()`
在头里只有声明,定义在 `src/variant/*.cpp` —— 跑通即证明这 1022 个 TU 真的编了并链进来了
(对照 [[verify-obj-count-not-green-ci]]:绿 CI 不等于包被编译)。
- **生成绑定类**:`<godot_cpp/classes/node.hpp>`、`Node::PROCESS_MODE_*`、`godot::OK`、
`godot::ERR_FILE_NOT_FOUND`、`Variant::OBJECT` —— 这些只存在于 `gen/`,缺了就编不过。

**不能测什么**:`String`/`Array` 等一切要走 `gdextension_interface_*` 函数指针的 API,以及类注册
(`GDREGISTER_CLASS`),都需要一个已加载该扩展的 Godot 进程。纯数学那半边不需要,所以断言全落在那里。

## 5. 本地验证

与 CI 同版本(`.github/workflows/validate.yml` 的 `MCPP_VERSION = 2026.8.3.3`),冷验证:

```
$ mcpp test -p godot-cpp
Compiling compat.godot-cpp v4.5.0
Running bin/godot_cpp
vec2=1 vec3=1 basis=1 color=1 aabb=1 gen=1
godot_cpp ... ok (0.12s)
test result ok. 1 passed; 0 failed; finished in 106.54s (build 55.60s + run 0.02s)
```

另外用 gcc 13 单独全量编过一遍 1022 个 TU(`-std=c++23`,零告警失败),并把全部 .o 与测试 main
直接链接跑通 —— 用来提前确认「依赖 .o 全量入链」下没有未解析符号(不需要 `-ldl`/`-lpthread`)。
CPU 时间约 16 分钟,4 核 runner 上折合 4~5 分钟。

## 6. 后续:模块层 godotengine.godot-cpp

模块包**不**放在本索引里内联(避免索引变重,也避免把 1000 TU 再编一遍):按 Form A 走外部仓
`mcpplibs/godot-cpp-m`,其 `mcpp.toml` 依赖 `compat.godot-cpp`,提供 `import godot_cpp;`
(单段模块名,与 `#include <godot_cpp/...>` 和库名对应;点号在本索引里留给子模块,如 `opencv.cv`)。

**顺序是硬约束**:模块成员的测试工程只能声明一条 `[indices]`,给 `godotengine`;它的传递依赖
`compat.godot-cpp` 于是从**已发布**的远端索引解析(tests/examples/ffmpeg-module 的注释写的就是这件事)。
所以必须先合并本 PR、`publish-artifact` 重新发布 artifact,第二个 PR 的 CI 才可能绿。

模块 wrapper 的生成方式已验证可行:1077 个头在单个 TU 里全部编过只要 3.5 秒 / 728 MB RSS,
按命名空间作用域声明批量产出 `export using ::godot::X;` 即可。**宏不在其中** —— `GDCLASS`、
`GDREGISTER_CLASS`、`memnew`、`ERR_*`、`GDVIRTUAL_*` 是预处理器构造,模块带不走,做类注册的 TU
仍需 `#include` 对应头;这一条要在 godot-cpp-m 的 README 与描述符注释里写明。
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Two kinds of packages live here:
| header-only (with `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
| Runtime loader compat (pure sources, sidestepping upstream codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua) (the Khronos loader: `loader/generated/` is checked in, and the assembly path degrades to plain C through `UNKNOWN_FUNCTIONS_SUPPORTED`, so no CMake/Python/assembler is needed; windows deferred) · [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) |
| Whole-source direct build + generated config (only where a platform lacks one) | [`compat.curl`](pkgs/c/compat.curl.lua) (win32 uses upstream's checked-in config, unix generates one) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua) (win/mac use upstream's checked-in config; linux generates one and enables X11 by hand) |
| Upstream codegen frozen into the mirror archive | [`compat.godot-cpp`](pkgs/c/compat.godot-cpp.lua) (the ~1000 GDExtension classes under `gen/` exist in no upstream tag archive — upstream's `binding_generator.py` emits them at build time. Running it once offline and publishing upstream's tree byte-for-byte **plus** `gen/` keeps Python off the consumer side entirely; `tools/godot-cpp/repack.sh` reproduces the archive deterministically and refuses to publish if any upstream file differs) |
| Header package filling a gap in the index | [`compat.glx-headers`](pkgs/c/compat.glx-headers.lua) (libglvnd's `GL/glx.h`, absent from the Khronos registry and required by SDL's X11 backend) |
| C++ application framework compat (dependencies reuse packages already in the index) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua) (upstream's `3rd/` ships 8 vendored dependencies; none of them is compiled here — all are redirected to the same-version `compat.*` packages in this index) |
| Mutually exclusive backends (one of several inside one package) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua): `vulkan` / `sdl2` each **replace** the default OpenGL / GLFW, and the default backend is expressed by *naming no feature at all* — there is no `opengl`/`glfw` feature. A `default` feature cannot express exclusivity: its own `defines`/`sources`/`deps` have no effect whatsoever, while its `implies` always applies and cannot be overridden by a named feature (which is, conversely, exactly the solution for the "always-on interface define" row below). The workable answer is to read the `-DMCPP_FEATURE_<NAME>` mcpp passes anyway and decide up front in a force-included header. Note also that `cflags` only reaches C TUs — C++ needs `cxxflags`, so a backend define written only into `cflags` never reaches any `.cpp` |
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上
| header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
| 运行时 loader compat(纯源码,绕开上游 codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua)(Khronos loader:`loader/generated/` 已签入,汇编路径经 `UNKNOWN_FUNCTIONS_SUPPORTED` 降级为纯 C,故无需 CMake/Python/汇编器;windows 延后)· [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) |
| 全源码直编 + 生成 config(仅缺口平台) | [`compat.curl`](pkgs/c/compat.curl.lua)(win32 用上游签入 config,unix 生成) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua)(win/mac 用上游签入 config,linux 生成 + 手工开 X11) |
| 上游 codegen 前置冻结进镜像归档 | [`compat.godot-cpp`](pkgs/c/compat.godot-cpp.lua)(`gen/` 下约 1000 个 GDExtension 类不在任何上游 tag 归档里,由上游 `binding_generator.py` 在构建时生成。改为离线跑一次,把上游源码树逐字节原样 **加上** `gen/` 一起发布,消费侧就完全不需要 Python;`tools/godot-cpp/repack.sh` 可确定性复现该归档,且上游文件一旦有出入即拒绝打包) |
| 补索引空缺的头文件包 | [`compat.glx-headers`](pkgs/c/compat.glx-headers.lua)(libglvnd 的 `GL/glx.h`,Khronos registry 不含,SDL 的 X11 后端必需) |
| C++ 应用框架 compat(依赖复用索引内既有包) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua)(上游 `3rd/` 自带 8 个 vendored 依赖,此处一个不编,全部改指索引内同版本 `compat.*`) |
| 互斥后端(同包多后端二选一) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua):`vulkan` / `sdl2` 各自**替换**默认的 OpenGL / GLFW,默认后端由"不点名任何 feature"表达,并不存在 `opengl`/`glfw` feature。`default` feature 表达不了互斥 —— 它自带的 `defines`/`sources`/`deps` 完全不生效,而 `implies` 又恒生效、无法被点名的 feature 覆盖(后者反而正好是本表『恒开的 interface define』一行的解法)。可行解是读 mcpp 本就会传的 `-DMCPP_FEATURE_<NAME>`,在强制包含头里做前置判定。另注意 `cflags` 只作用于 C TU,C++ 需 `cxxflags` —— 只写进 `cflags` 的后端 define 到不了任何 `.cpp` |
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ members = [
"tests/examples/ffmpeg",
"tests/examples/ffmpeg-module",
"tests/examples/fmtlib.fmt",
"tests/examples/godot-cpp",
"tests/examples/gui-stack",
"tests/examples/imgui",
"tests/examples/imgui-module",
Expand Down
118 changes: 118 additions & 0 deletions pkgs/c/compat.godot-cpp.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
-- compat.godot-cpp -- godot-cpp (the C++ bindings for Godot's GDExtension API)
-- as an ordinary mcpp source package: `#include <godot_cpp/...>` works out of
-- the box, no SCons, no CMake and no Python on the consumer side.
--
-- Why the download is NOT the upstream tag archive
-- godot-cpp is only half a source tree. The other half -- ~1000 engine
-- classes and every builtin Variant type, i.e. gen/include + gen/src -- is
-- produced by upstream's own binding_generator.py from
-- gdextension/extension_api.json, and no upstream tag archive or release
-- carries it. A package that ran that generator at install time would make
-- a Python toolchain a hard runtime dependency of every consumer, on every
-- platform, which is exactly what this index avoids elsewhere (the frozen
-- configure snapshots in compat.ffmpeg / the opencv module package).
--
-- So the generator runs ONCE, offline, and the result is published as an
-- immutable mirror archive: upstream's tree byte-for-byte (the repack
-- verifies this and refuses otherwise) plus the gen/ tree that upstream's
-- unmodified binding_generator.py emitted for it. Recipe and verification
-- live in tools/godot-cpp/repack.sh; upstream's own archive for
-- godot-4.5-stable hashes to
-- ac78539c0042554c494ea419549d2de88758d448721aeb0e5d41129aa87e339c.
--
-- Bindings are generated for the default configuration -- 64-bit,
-- precision=single, template_get_node on -- which is what `scons` and
-- `cmake` give you by default. A double-precision build needs a different
-- gen/ tree, so it cannot be a feature over this archive; it would be a
-- second version/package.
--
-- Defines
-- GDEXTENSION is upstream's PUBLIC compile definition (cmake sets it on the
-- godot-cpp target's INTERFACE), so it rides on a default feature: the lib
-- and every consumer TU must agree. The layout-affecting ones are left
-- undefined on both sides, which is upstream's release default:
-- DEBUG_ENABLED / DEV_ENABLED (extra checks), HOT_RELOAD_ENABLED (changes
-- the Wrapped layout) and REAL_T_IS_DOUBLE (needs the double-precision
-- gen/ tree above). They are deliberately NOT features: each would re-key
-- the store into a second full ~1000-TU build of the same library.
--
-- Consuming
-- compat.godot-cpp is the plain-header form. `import godot_cpp;` is the
-- module package godotengine.godot-cpp (mcpplibs/godot-cpp-m), which builds
-- on top of this one.
package = {
spec = "1",
namespace = "compat",
name = "godot-cpp",
description = "C++ bindings for the Godot GDExtension API (pre-generated bindings, no Python/SCons needed)",
licenses = {"MIT"},
repo = "https://github.com/godotengine/godot-cpp",
type = "package",

-- One OS-neutral archive: godot-cpp is portable C++ with no per-platform
-- source selection (the platform split lives in Godot itself, behind the
-- gdextension_interface.h ABI).
xpm = {
linux = {
["4.5.0"] = {
url = {
GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz",
},
sha256 = "b0c36e77f02c4181352cdd7547b209b93a833be1ad6197f8c650d92987221a00",
},
},
macosx = {
["4.5.0"] = {
url = {
GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz",
},
sha256 = "b0c36e77f02c4181352cdd7547b209b93a833be1ad6197f8c650d92987221a00",
},
},
windows = {
["4.5.0"] = {
url = {
GLOBAL = "https://github.com/xlings-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/godot-cpp/releases/download/4.5.0/godot-cpp-4.5.0.tar.gz",
},
sha256 = "b0c36e77f02c4181352cdd7547b209b93a833be1ad6197f8c650d92987221a00",
},
},
},

mcpp = {
schema = "0.1",
language = "c++23",
import_std = false,
-- Three roots, exactly as upstream's build systems expose them:
-- hand-written headers, generated headers, and the GDExtension C ABI
-- header (gdextension_interface.h) that both of them include.
include_dirs = { "*/include", "*/gen/include", "*/gdextension" },
-- Enumerated rather than `**`: upstream's own test project ships a
-- test/src/*.cpp that must not be swept into the library, and the two
-- source roots are only ever one and two levels deep.
sources = {
"*/src/*.cpp",
"*/src/*/*.cpp",
"*/gen/src/*/*.cpp",
},
targets = { ["godot-cpp"] = { kind = "lib" } },
features = {
["default"] = { implies = { "gdextension" } },
["gdextension"] = { defines = { "GDEXTENSION" } },
},
deps = { },
-- A GDExtension IS a shared library, so this static library's objects
-- are almost always linked into one. Without position-independent code
-- that link fails outright ("relocation R_X86_64_32 against `.rodata`
-- can not be used when making a shared object"), which would leave the
-- package unable to do the one thing it exists for -- upstream's own
-- SCons and CMake builds pass -fPIC for exactly this reason. Not
-- needed on windows: the PE toolchain has no such distinction and
-- clang-cl would only report the flag as unused.
linux = { cxxflags = { "-fPIC" } },
macosx = { cxxflags = { "-fPIC" } },
},
}
15 changes: 15 additions & 0 deletions tests/examples/godot-cpp/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# compat.godot-cpp test project: builds the pre-generated GDExtension bindings
# and asserts on the part of the API that runs without a Godot process --
# the Variant math types, whose out-of-line definitions live in the library's
# src/variant/*.cpp, so a passing run proves the ~1000-TU library really did
# compile and link.
#
# `compat` is redirected to this checkout by the workspace-root [indices],
# which every member inherits.

[package]
name = "godot-cpp-tests"
version = "0.1.0"

[dependencies.compat]
godot-cpp = "4.5.0"
Loading
Loading