From 3a6d46f4e116fab641cb35c1c99fbac1231bd2b4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 4 Aug 2026 03:52:09 +0800 Subject: [PATCH 1/2] feat: add compat.godot-cpp 4.5.0 (godot-4.5-stable) godot-cpp's tag archives are only half a source tree: the ~1000 GDExtension engine classes and every builtin Variant type (gen/include, gen/src) are emitted by upstream's own binding_generator.py at build time, and ship in no upstream archive or release. Running that generator on the consumer side would make Python a hard dependency of every build on every platform, so it runs once offline instead and the result is published as an immutable mirror archive: upstream's tree byte-for-byte plus gen/. tools/godot-cpp/repack.sh is the only source of that archive and verifies it -- it diffs the regenerated tree against a fresh extraction of the upstream archive and refuses to publish if any upstream file differs, then packs deterministically (sorted, fixed mtime, gzip -n). Two runs hash identically. GLOBAL github.com/xlings-res/godot-cpp 4.5.0 CN gitcode.com/mcpp-res/godot-cpp 4.5.0 sha256 b0c36e77f02c4181352cdd7547b209b93a833be1ad6197f8c650d92987221a00 GDEXTENSION is upstream's PUBLIC compile definition, so it rides on a default feature and reaches consumer TUs. DEBUG_ENABLED / DEV_ENABLED / HOT_RELOAD_ENABLED are deliberately not features (each re-keys the store into a second full 1022-TU build, and HOT_RELOAD_ENABLED changes the Wrapped layout); REAL_T_IS_DOUBLE cannot be one at all, since it needs a gen/ tree generated with precision=double. The test member asserts on both halves and can fail on either: Vector2:: length(), Basis::orthonormalized(), Color::to_rgba32() and AABB::get_volume() are defined in src/variant/*.cpp, so they only resolve if the library really compiled and linked, while Node, Node::PROCESS_MODE_*, godot::OK and Variant::OBJECT exist only in gen/. Everything a running Godot process would be needed for (String, Array, class registration) is out of scope. Verified locally with the CI-pinned mcpp 2026.8.3.3: `mcpp test -p godot-cpp` -> test result ok. 1 passed; 0 failed (106.54s). --- .agents/docs/2026-08-04-add-godot-cpp-plan.md | 123 ++++++++++++++++++ README.md | 1 + README.zh-CN.md | 1 + mcpp.toml | 1 + pkgs/c/compat.godot-cpp.lua | 108 +++++++++++++++ tests/examples/godot-cpp/mcpp.toml | 15 +++ tests/examples/godot-cpp/tests/godot_cpp.cpp | 73 +++++++++++ tools/godot-cpp/repack.sh | 69 ++++++++++ 8 files changed, 391 insertions(+) create mode 100644 .agents/docs/2026-08-04-add-godot-cpp-plan.md create mode 100644 pkgs/c/compat.godot-cpp.lua create mode 100644 tests/examples/godot-cpp/mcpp.toml create mode 100644 tests/examples/godot-cpp/tests/godot_cpp.cpp create mode 100755 tools/godot-cpp/repack.sh diff --git a/.agents/docs/2026-08-04-add-godot-cpp-plan.md b/.agents/docs/2026-08-04-add-godot-cpp-plan.md new file mode 100644 index 0000000..ee1dfae --- /dev/null +++ b/.agents/docs/2026-08-04-add-godot-cpp-plan.md @@ -0,0 +1,123 @@ +# 收录 godot-cpp:compat.godot-cpp(本 PR)与模块层 godotengine.godot-cpp(后续 PR) + +日期:2026-08-04 +上游: · 版本 `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 远高于它。 + +### 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 不等于包被编译)。 +- **生成绑定类**:``、`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 ` 和库名对应;点号在本索引里留给子模块,如 `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 与描述符注释里写明。 diff --git a/README.md b/README.md index 1f9f605..ed08b10 100644 --- a/README.md +++ b/README.md @@ -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_` 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` | diff --git a/README.zh-CN.md b/README.zh-CN.md index daba711..4791a14 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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_`,在强制包含头里做前置判定。另注意 `cflags` 只作用于 C TU,C++ 需 `cxxflags` —— 只写进 `cflags` 的后端 define 到不了任何 `.cpp` | diff --git a/mcpp.toml b/mcpp.toml index 47323c5..102c827 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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", diff --git a/pkgs/c/compat.godot-cpp.lua b/pkgs/c/compat.godot-cpp.lua new file mode 100644 index 0000000..75924c7 --- /dev/null +++ b/pkgs/c/compat.godot-cpp.lua @@ -0,0 +1,108 @@ +-- compat.godot-cpp -- godot-cpp (the C++ bindings for Godot's GDExtension API) +-- as an ordinary mcpp source package: `#include ` 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 = { }, + }, +} diff --git a/tests/examples/godot-cpp/mcpp.toml b/tests/examples/godot-cpp/mcpp.toml new file mode 100644 index 0000000..8b71c3b --- /dev/null +++ b/tests/examples/godot-cpp/mcpp.toml @@ -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" diff --git a/tests/examples/godot-cpp/tests/godot_cpp.cpp b/tests/examples/godot-cpp/tests/godot_cpp.cpp new file mode 100644 index 0000000..afab726 --- /dev/null +++ b/tests/examples/godot-cpp/tests/godot_cpp.cpp @@ -0,0 +1,73 @@ +// Behavioral test for compat.godot-cpp. +// +// Two things are under test, and both can fail: +// +// 1. The PRE-GENERATED bindings are really in the package. gen/include is +// what upstream's binding_generator.py would have produced at build time; +// if it were missing, and the global +// constants below would not compile at all. +// 2. The library links. Vector2::length(), Basis::orthonormalized(), +// Color::to_rgba32() and AABB::get_volume() are declared in the headers +// but DEFINED in src/variant/*.cpp, so these calls only resolve if the +// ~1000 translation units of the package were compiled and linked in. +// +// Everything asserted here is pure math -- no gdextension_interface function +// pointers, so no running Godot process is needed. Engine classes are checked +// by compiling against them (types, enums, sizes), which is all that can be +// done outside a loaded extension. + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +bool close(double a, double b) { + return std::fabs(a - b) < 1e-5; +} + +} // namespace + +int main() { + using namespace godot; + + // --- out-of-line variant math: proves the library linked --- + const bool vec2_ok = close(Vector2(3, 4).length(), 5.0) && + close(Vector2(3, 4).normalized().length(), 1.0); + + const Vector3 cross = Vector3(1, 0, 0).cross(Vector3(0, 1, 0)); + const bool vec3_ok = cross == Vector3(0, 0, 1) && + close(Vector3(2, 3, 6).length(), 7.0); + + const Basis basis = Basis().orthonormalized(); + const bool basis_ok = close(basis.determinant(), 1.0); + + const bool color_ok = Color(1.0f, 0.0f, 0.0f, 1.0f).to_rgba32() == 0xff0000ffu; + + const AABB box(Vector3(0, 0, 0), Vector3(2, 3, 4)); + const AABB other(Vector3(1, 1, 1), Vector3(4, 4, 4)); + const bool aabb_ok = close(box.get_volume(), 24.0) && + box.intersects(other) && + close(box.intersection(other).get_volume(), 1.0 * 2.0 * 3.0); + + // --- generated bindings: engine class + global enums are visible --- + const bool gen_ok = sizeof(Node) > 0 && + Node::PROCESS_MODE_INHERIT == 0 && + Node::PROCESS_MODE_DISABLED == 4 && + godot::OK == 0 && + godot::ERR_FILE_NOT_FOUND == 7 && + godot::SIDE_LEFT == 0 && + Variant::OBJECT != Variant::NIL; + + const bool ok = vec2_ok && vec3_ok && basis_ok && color_ok && aabb_ok && gen_ok; + std::printf("vec2=%d vec3=%d basis=%d color=%d aabb=%d gen=%d\n", + vec2_ok, vec3_ok, basis_ok, color_ok, aabb_ok, gen_ok); + return ok ? 0 : 1; +} diff --git a/tools/godot-cpp/repack.sh b/tools/godot-cpp/repack.sh new file mode 100755 index 0000000..efd0382 --- /dev/null +++ b/tools/godot-cpp/repack.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Build the godot-cpp source archive this index points at. +# +# WHY a repack at all: godot-cpp's GDExtension bindings (~1000 classes, +# gen/include + gen/src) are not in any upstream tag archive -- they are +# produced by upstream's own binding_generator.py from +# gdextension/extension_api.json at build time, which would make every +# consumer of the package need a Python toolchain. So the generator runs +# ONCE, here, and the result is published alongside the untouched upstream +# tree. Same idea as the frozen configure snapshots in the opencv/ffmpeg +# packages: no code generation on the consumer side. +# +# The archive is exactly: +# * the upstream tag tarball, byte-for-byte (verified below), plus +# * gen/, produced by running upstream's unmodified binding_generator.py. +# +# Reproduce: +# tools/godot-cpp/repack.sh godot-4.5-stable 4.5.0 /tmp/out +# then publish /tmp/out/godot-cpp-.tar.gz to +# github.com/xlings-res/godot-cpp (GLOBAL) and gitcode.com/mcpp-res/godot-cpp +# (CN), both under the bare-version tag. +set -euo pipefail + +TAG="${1:?usage: repack.sh }" +VERSION="${2:?}" +OUTDIR="${3:?}" + +PYTHON="${PYTHON:-python3}" +WRAP="godot-cpp-${TAG}" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +mkdir -p "$OUTDIR" + +echo "==> fetching upstream ${TAG}" +curl -L -fsS -o "$WORK/upstream.tar.gz" \ + "https://github.com/godotengine/godot-cpp/archive/refs/tags/${TAG}.tar.gz" +echo " upstream sha256: $(sha256sum "$WORK/upstream.tar.gz" | cut -d' ' -f1)" + +tar -xzf "$WORK/upstream.tar.gz" -C "$WORK" +test -d "$WORK/$WRAP" || { echo "unexpected wrap dir in archive" >&2; exit 1; } + +echo "==> generating bindings with upstream binding_generator.py" +( + cd "$WORK/$WRAP" + "$PYTHON" -c " +import binding_generator +binding_generator.generate_bindings('gdextension/extension_api.json', True, '64', 'single', '.') +" + # generate_bindings() imports the module, which leaves bytecode behind + find . -name '__pycache__' -type d -prune -exec rm -rf {} + +) + +echo "==> verifying the upstream tree is untouched" +tar -xzf "$WORK/upstream.tar.gz" -C "$WORK" --transform "s|^${WRAP}|${WRAP}.orig|" +diff -r --no-dereference "$WORK/${WRAP}.orig" "$WORK/${WRAP}" > "$WORK/diff.txt" || true +if grep -v "^Only in $WORK/${WRAP}: gen\$" "$WORK/diff.txt" | grep -q .; then + echo "upstream files were modified -- refusing:" >&2 + grep -v "^Only in $WORK/${WRAP}: gen\$" "$WORK/diff.txt" >&2 + exit 1 +fi +echo " every upstream file is byte-identical; gen/ is the only addition" +rm -rf "$WORK/${WRAP}.orig" + +echo "==> packing (deterministic: sorted, fixed mtime, gzip -n)" +tar --sort=name --mtime='UTC 2026-01-01' --owner=0 --group=0 --numeric-owner \ + -C "$WORK" -cf - "$WRAP" | gzip -n -9 > "$OUTDIR/godot-cpp-${VERSION}.tar.gz" + +sha256sum "$OUTDIR/godot-cpp-${VERSION}.tar.gz" From 5b4eb4d9007980317d96faa794b3b97c2412f122 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 4 Aug 2026 04:14:32 +0800 Subject: [PATCH 2/2] fix(godot-cpp): compile with -fPIC on linux/macOS A GDExtension is a shared library, so compat.godot-cpp's objects are almost always linked into one -- and without position-independent code that link fails outright: ld: obj/vector4i.o: relocation R_X86_64_32 against `.rodata` can not be used when making a shared object; recompile with -fPIC which leaves the package unable to do the one thing it exists for. Upstream's own SCons and CMake builds pass -fPIC for the same reason. Windows does not need it: the PE toolchain has no such distinction and clang-cl would only report the flag as unused. Caught by building a real GDExtension (kind = "shared") against the package, not by inspection. With it, that shared library links and exports its entry symbol, and the workspace member still passes. --- .agents/docs/2026-08-04-add-godot-cpp-plan.md | 9 +++++++++ pkgs/c/compat.godot-cpp.lua | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/.agents/docs/2026-08-04-add-godot-cpp-plan.md b/.agents/docs/2026-08-04-add-godot-cpp-plan.md index ee1dfae..8f50646 100644 --- a/.agents/docs/2026-08-04-add-godot-cpp-plan.md +++ b/.agents/docs/2026-08-04-add-godot-cpp-plan.md @@ -62,6 +62,15 @@ godot-cpp 是 Godot GDExtension 的 C++ 绑定库。表面上属于「C++ 源码 (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 必须一致, diff --git a/pkgs/c/compat.godot-cpp.lua b/pkgs/c/compat.godot-cpp.lua index 75924c7..187f7b9 100644 --- a/pkgs/c/compat.godot-cpp.lua +++ b/pkgs/c/compat.godot-cpp.lua @@ -104,5 +104,15 @@ package = { ["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" } }, }, }