豫构下一阶段设计:构建目标、Makefile 与 C 绑定

状态:设计提案,尚未实现。文中的新增字段、命令与内部协议均不是当前功能承诺。范围是本地构建正确性和工具链解耦,不包含远程包获取、版本求解或性能优化。

实施进展:最小的包内 C 注册、直接编译归档及预构建核心运行时链接已落地;当前实际支持范围见包内原生源码。下文仍是完整后续设计,不能把所有字段视为已支持。

一、结论

下一步优先完成“编译器只生成编译产物,豫构负责原生依赖和最终链接”,再把 Makefile 中的组合目标迁入豫构。不要先把 Makefile 的 shell 命令逐条复制进配置文件。

保持已确定的规则:包与库不分两层;包根由包文件位置决定;包身份是用户与包名两个字段;语言模块只允许自身与直接依赖;单段导入固定取包根总集;配置仍只做静态解析,不执行豫言代码。

C 绑定须分为两件事:一是源码、头文件、归档及系统库如何构建和链接;二是豫言值与 C 值怎样转换、保存和释放。第一件由豫构管理;第二件由编译器、运行时 SDK 和绑定代码共同保证,不能用链接成功代替 ABI 正确。

二、当前实现与具体问题

本节依据当前仓库源码,不是假设。

位置 当前职责 下一阶段需要解决的事
根 Makefile 首级自举、后续自举、比较、豫构测试、运行时与发布入口 配置和目标逻辑仍分散在两处
运行时支持库/Makefile 调试/优化、静态/动态、旧桥接、WASI 运行时 原生动作还不能由豫构追踪和共享
编译器的主程序生成。豫 调用 make yy_runtime_lib,再调用 clang 链接 编译器反向驱动构建系统,依赖工作目录
编译器的网页汇编。豫 调用运行时 Makefile,使用固定归档路径 WASI 同样需要显式工具链和原生产物输入
工具/豫构/入口。豫 切至编译器所在目录后运行编译器 工具链与完整源码仓库耦合
原生类型定义.h 与外部调用代码生成 豫言值为 __uint128_t,外部调用生成 i128 参数及返回值 现有 C调用 是豫言运行时调用约定,不是通用 C FFI

运行时 Makefile 还有三处结构性风险:

  1. 调试、优化、动态库共用 构建/原生 中的对象路径,配置未隔离,无法安全允许这些构建并行。
  2. 对象文件名使用 notdir,目录身份丢失;不同目录出现同名 C 文件时可能发生碰撞。
  3. 所有头文件与源码整体挂到归档目标,配方批量编译全部源码;没有按翻译单元记录真实头文件依赖。

此外,包含与链接搜索路径中硬编码了 Homebrew 和 /usr/local。数据库包目前调用 sqlite3psql 子进程,不是已有 SQLite/libpq 原生绑定;后文 SQLite 示例仅示意未来绑定包。

三、职责与依赖图

目标依赖关系:

包配置 + 工具链配置
        ↓
豫构:解析包图、选择目标、构造动作图
        ├─ 豫言模块编译 → 有序对象/位码清单、外调需求
        ├─ C 编译 → 对象文件、头文件依赖
        ├─ 原生归档/动态库
        └─ 最终链接 → 程序 → 测试/打包/安装

编译器保留语法、类型、优化、目标代码和内部模块阶段依赖的知识;豫构不重新实现这些语义。运行时和绑定的构建、工具链选择、输出命名、最终链接和测试调度归豫构。

三种边不要混用:

例如应用依赖数据库绑定,绑定依赖 SQLite:应用不能因此导入未直接声明的其他豫言包,但最终链接必须带上绑定需要的 SQLite。

四、包内目标与原生配置

建议向现有包文件增加三个可选字段:程序测试原生。它们描述同一个包的产物及原生组成,不是嵌套的子包或库。包级依赖继续只有一份,第一版不引入目标专属语言依赖;测试有不同依赖时沿用独立测试包。

以下是未来“数据库”绑定包的完整示意,名称尚可调整:

「依赖」者「列」【『标准库』】也。

「程序」者「典」【
  「演示」者「列」【『示例』,『入口』】也,
】也。

「测试」者「典」【
  「内存数据库」者「典」【
    「入口」者「列」【『测试』,『内存数据库』】也,
    「种类」者『运行』也,
    「退出码」者「零」也,
    「超时秒」者「三零」也,
  】也,
】也。

「原生」者「典」【
  「语言」者『C』也,
  「标准」者『c11』也,
  「源文件」者「列」【『原生/数据库桥接.c』】也,
  「包含目录」者「列」【『原生/包含』】也,
  「公开包含目录」者「列」【】也,
  「宏」者「典」【「数据库检查」者『1』也,】也,
  「系统依赖」者「列」【
    「典」【
      「名称」者『SQLite』也,
      「查询名」者『sqlite3』也,
      「链接」者『动态』也,
    】,
  】也,
  「绑定」者「典」【
    「数据库_打开」者「典」【「参数数」者「一」也,】也,
    「数据库_关闭」者「典」【「参数数」者「一」也,】也,
  】也,
】也。

字段语义与约束:

以目标实际使用的豫言模块所属包为种子,再递归展开其显式原生依赖,确定要构建和链接的原生组成。因此纯原生包不会遗漏,仅被发现而未使用的包也不会被链接。现阶段以“整个被使用的包”为原生粒度,不承诺按单个 C 符号裁剪。编译器报告实际模块闭包;不能为了省链接依赖而恢复未声明的语言导入。

五、编译器与豫构的协议

新增一个版本化的“只生成产物”工作模式,名称暂定 --emit-build-artifacts。它不得调用 make、编译 C 运行时、链接最终程序或执行程序。

请求包含:包上下文、入口文件、主机/目标平台、编译配置、编译器身份、目标输出目录、绑定符号映射。响应包含:

  1. 实际访问的模块及包身份。
  2. 保序的链接输入清单:对象、位码、生成的程序入口,以及必须执行的初始化次序。
  3. 编译器需要的运行时 ABI 和目标描述。
  4. 外部调用需求:所属包、逻辑名、物理符号、参数数、ABI、源码位置。
  5. 每个产物的路径、种类和成功状态;失败不得发布有效清单。

内部协议应独立于配置 parser,用结构化编码和版本号;不要解析人类日志,也不要依靠从缓存目录猜文件名。保序信息尤其重要,不能把现有链接顺序擅自字母排序。

迁移分两步:先保留编译器已有模块编译与并行实现,将整次豫言编译视为复合动作;再考虑向豫构暴露细粒度模块工作。第一步不重写调度器。

统一并发的保守方案:复合编译动作运行期间独占本次构建额度,并把 -j N 传给它;C 动作在其他时段按同一额度调度。不要同时给每个程序各开 N 个工作进程。之后若细分模块动作,再实现更充分的交错并行。这是并发正确性要求,不是本轮性能优化任务。

六、运行时及 C 构建

6.1 工具链

独立的静态工具链描述记录:豫言编译器、clang、归档器、LLVM 工具、目标三元组、sysroot/SDK、运行时源码或归档、运行时 SDK、ABI 版本、支持能力。工具路径只在配置阶段解析一次并固定。

工具链路径属于构建环境,不改变按包名引用的规则。普通包不得指定任意编译器或覆盖运行时。运行时是工具链组件,复用原生动作引擎,但不要求所有语言包重复声明运行时依赖,也不构造“运行时依赖标准库”的语言包环。

区分两组工具:构建机上运行的豫构、编译器和生成器;为目标生成代码的 C 编译器、链接器及目标库。交叉编译时不能运行目标平台的探测程序。平台判断来自目标配置,不从 uname 推断目标。

clang、LLVM IR 工具和 LTO 归档器必须经过组合能力检查;不能仅比较名称或版本大号。编译一个小对象、归档、链接,必要时验证 LLVM 位码可消费。探测失败说明具体组件,不静默关闭 LTO 或改用其他编译器。

初期支持原有 Linux、macOS 原生组合;WASI 保留既有可移植核心与替代接口清单,随后迁入同一引擎。WASI 不允许意外解析到宿主动态库。Windows/通用交叉编译只预留模型,不承诺首批支持。

6.2 一个源码对应一个动作

每个 C 文件独立调用 clang,显式给出 -c、输出、包含路径和参数。对象路径保留源文件相对路径及语言,例如 对象/原生/网络/缓冲.c.o;不再取 basename。

C 编译同时产生依赖文件。默认使用 -MD -MF 记录实际包含的用户与系统头文件,正确解析空格转义和续行;不以正则递归扫描 #include 代替预处理器。首次没有依赖文件必须编译;命中缓存要求旧依赖均存在且一致。已有生成头文件须先声明生成动作再编译,不能等编译失败后猜出生成顺序。Clang 提供这些依赖文件选项,详见命令行参考

只记录上次实际头文件仍不足以发现“更高优先级包含目录新增同名头文件”。第一版还须将用户包含目录中的头文件清单及顺序纳入检查;工具链系统目录由 SDK 内容身份或重新依赖扫描保证。不能把系统头文件永不改变当作默认前提。

归档动作精确使用当前对象列表,写临时归档后原子替换;删除源文件后不能保留旧 archive member。动态库、静态库、调试、优化、PIC、LTO、目标架构均使用不同构建目录。只有实际请求的运行时变体才构建,不再每次链接都执行运行时“全部”。

6.3 系统依赖与参数传播

优先使用显式系统依赖查询,例如 pkg-config/pkgconf;探测是独立且可缓存的构建动作,不在解析配置时执行。查询结果记录工具身份、目标、环境、版本、元数据与最终解析到的文件。交叉场景隔离宿主搜索路径。缺库只报错和给安装提示,不自动安装或联网下载。

静态查询必须请求静态链接所需依赖,动态查询不得无故把静态私有库集合全部加入。查询输出按其引号与转义规则转为参数向量,不执行 shell,不简单按空格切开。不支持的参数形式应明确失败。

内容 作用范围
私有包含路径、宏、编译选项 仅本包 C 编译
公开头文件路径、必要公开宏 直接原生消费者;公开头依赖另需显式传播
归档与系统链接库 按最终链接需求传递
优化级别、诊断开关 不从依赖包传播给消费者
动态库运行时位置 进入运行/安装布局,不冒充编译包含路径

公开与私有编译需求的区别可参考 CMake usage requirements。豫构只采用这一区分,不引入 CMake 的完整属性系统。外部源码随语言包构建并携带链接信息的做法也可参考 Dune foreign stubs

原始参数逃生口放到后续;初版优先结构化包含、宏、库与框架。不允许包通过参数覆盖 -o、目标平台、sysroot、依赖文件位置和动作输出根。第三方确需特殊参数时,应明确分类为编译或链接参数,视为执行代码级别的信任,而非宣称其安全。

6.4 链接

从编译器保序对象清单开始,按依赖关系加入包归档和运行时。静态库的使用者通常应在提供者前面;绝不把所有 -l 项排序或粗暴去重。跨归档环首版拒绝并报告环;不把 GNU 的 group 参数直接套到 macOS。

运行时只选择一个 ABI 兼容实例。同一头文件库的不同用户包可以在语言层共存,但这不意味着链接到同一进程的 C 全局符号不会冲突。对绑定导出重复、混用不兼容 ABI 等情况提前报错;第三方同名 C 符号冲突必须重命名、明确隔离或拒绝共链,不能靠链接顺序碰运气。

开发目录允许可定位的动态库路径;可分发产物使用平台对应的相对运行库布局,记录要随包交付的动态库。不要把开发机 /opt/homebrew 绝对 rpath 写成默认发布契约。静态归档链接不等于整个程序完全静态链接。

七、C 绑定 ABI 与生命周期

7.1 首版沿用豫言值 ABI

保持现有外部调用机器约定:参数和返回值均为豫言值。不能直接把 int sqlite3_open(const char *, sqlite3 **) 填进 C调用;必须由桥接函数转换参数、调用第三方 API,再把结果编码成豫言值。

绑定包提供普通豫言模块作为类型安全的公开接口,C调用 留在包内封装层。原生源文件接入不意味着立刻提供自动头文件解析、任意 struct 按值传递、可变参数或自动回调生成。

新增公共运行时 SDK 头文件,只公开稳定的值转换、句柄、内存保护和错误接口;绑定不应包含运行时所有内部实现头。ABI 身份包含值表示、调用约定、目标布局及相关 GC 契约,不以语言版本字符串单独代替。

7.2 符号归属

配置中的绑定表将逻辑名映射到本包桥接符号,并声明参数数。编译器在生成外调需求时校验本包声明、参数数和运行时 ABI;不要从符号前缀猜依赖。

物理符号采用由结构化包身份与 ABI 派生的唯一前缀。豫构生成声明头和名称映射,桥接 C 代码与编译器使用同一映射。逻辑名不包含硬编码用户摘要;用户/包名中的“的”“之”等字无需保留。

首版运行时现有 豫言_ 符号走工具链提供表,不要求一次性重命名所有运行时函数。包绑定新增符号走新表;原始符号直连只保留为明确的兼容/不安全模式。语言包之间通过豫言接口调用,不开放未声明的跨包原始 C 符号引用。

生成 C 原型可检查签名和参数数,但不能证明实现正确处理 GC 或所有权。链接后还需目标相关符号检查;nm 等检查不能替代 C 编译器的 ABI 验证。

7.3 最小安全约定

第一批验收从纯整数/字节转换桥接开始,随后才做持有 SQLite 连接等资源型绑定。数据库 CLI 实现不在本轮顺带替换。

八、动作、缓存与可观察性

动作记录包含种类、依赖动作、工具绝对路径、参数向量、环境白名单、工作目录、输入、声明输出、动态依赖和可缓存性。动作类型首批固定为豫言编译、C 编译、归档、链接、运行测试、文件比较、复制、打包,不提供任意 shell 字段。

缓存键至少包含:动作格式版本、规范化配置、工具及运行时 ABI 身份、主机/目标、PIC/LTO/优化模式、参数顺序、环境、输入内容摘要、发现的头文件。时间戳可作为读取摘要的加速信息,但不能成为唯一正确性依据。

系统库升级、删除头文件、更换工具链、调整链接模式、改变绑定映射、移除 C 源文件均必须触发正确失效。改普通豫言源码不应重编无关的 C 对象。

动作临时输出在同文件系统中生成,通过后原子发布;失败不更新成功记录。缓存锁以动作身份为单位,输出路径只允许一个生产者。两个豫构进程同时请求相同动作应等待/复用,而不是写同一对象;不同配置天然隔离。

日志按构建和动作分别保存,不继续共用根目录的一个并行日志。提供计划命令与“为何重建”说明,区分源码变、头文件变、参数变和工具变。不自动缓存运行测试结果,尤其是网络、时间和外部服务测试。

这仍不是完全隔离的可复现构建:系统工具和第三方 C 源码必须可信;编译器可能加载插件或访问环境。第一版承诺显式输入、可诊断失效及受控输出,不声称安全沙箱或跨机器缓存可无条件共享。

九、Makefile 的迁移

9.1 根 Makefile

现有目标 豫构接管后的职责
yy2_bs 由稳定豫构和稳定编译器按包构建,见下节
yy3_bs、yy4_bs、连续自举 豫构的编译器产物依赖链及文件比较动作
豫构 具名程序目标
类型检查 编译器的检查目标
豫构测试 声明测试集合,不再硬编码三段编译运行命令
自举包 显式编译产物清单加打包动作,不猜缓存路径
运行时 原生组件的按需构建
清理 只清豫构拥有的产物和记录,不删任意输出路径

根 Makefile 保留为构建编排入口。包内 C 文件交由豫构管理;平台运行时仍由专门的 Makefile 构建,不再包含旧种子的兼容桥接层。

9.2 引导不能成环

稳定编译器 yy_bs_stable + 稳定构建器 yy豫构_stable
                 ↓ 按软件包构建
              yy2_bs
                 ↓ 同一稳定构建器管理原生依赖与链接
       yy3_bs → yy4_bs → 比较 → 正式 yy豫构

发布包同时携带两套可移植种子源码,构建后得到稳定编译器和稳定豫构。缺少任一种子就报错,不回退到旧单文件构建机制。文件形式的编译器命令也委托豫构建立所属包的上下文。

平台运行时由顶层构建显式准备;包注册的 C 文件始终由豫构编译和链接。旧单种子启动配方、引导豫构和兼容 C 桥接已移除。

Make 的并发额度不能与豫构各算一套。初期薄 Make 入口串行启动一次豫构;若用户请求多个并行 make 目标,保守串行化顶层入口。后续再实现 GNU Make jobserver 协作,不承诺不受控的嵌套并行。

9.3 第三方已有 Makefile

不要求重写上游项目,也不把它们默认为可信构建钩子。第二阶段可提供独立外部构建适配器:声明源码树输入、构建机工具、出树构建目录、参数向量、必须产生的归档/公开头文件、环境和目标平台。

外部构建结果作为导入的原生产物;不直接在依赖源码目录运行 make install,不自动下载或修改系统。首版不理解第三方 Make 内部依赖时,以整个源码树摘要保守失效。上游不支持出树构建时复制到受控暂存目录。此功能晚于内建 C 编译,不成为运行时迁移的前提。

十、测试与验收

必须建立以下自动化用例:

  1. 两个目录下同名 C 文件均正确参与构建。
  2. 修改直接/间接头文件触发正确重编;删除头文件、增加遮蔽头文件不能误用缓存。
  3. 删除一个 C 源文件后,归档中没有残留对象。
  4. 同时构建调试/优化与静态/动态变体,产物互不覆盖。
  5. 两个豫构进程构建同一目标,不出现损坏缓存和半成品。
  6. 包 A → B → 原生库:链接依赖正确传递,而未声明的语言导入仍失败。
  7. 静态与动态系统依赖的参数、运行库布局正确;缺库报错不回退。
  8. 绑定参数数错误、错误返回 ABI、重复导出符号提前报错;GC 压力、关闭两次、错误路径及异步结果清理有专项测试。
  9. 从与工具链目录无关的项目目录构建,且编译器不再调用 make;路径含空格仍正常。
  10. 新运行时与旧配方产物分别链接代表程序,行为一致;按仓库要求完成三级自举及字节一致比较。
  11. Linux 与 macOS 在各自 CI 上验证;WASI 在自己的工具链测试,不以宿主成功替代目标验证。
  12. 无修改不执行 C 编译/归档/链接;失败动作不发布成功状态。冷启动与原 Python 基线对比不退步即可,不追加优化工程。

十一、实施顺序

阶段 交付 退出条件
编译产物协议、工具链身份、原生动作与最终链接;暂不开放通用绑定配置 同一程序经新旧链接路径行为一致,新路径不调用 make
运行时静态构建、头文件追踪、缓存和输出隔离 自举固定点通过;无改动不重编,修改头文件正确失效
包级原生配置、系统依赖和最小绑定 SDK 一个纯值桥接包跨包调用成功,ABI 与失败用例通过
多程序/测试目标、自举及打包动作、薄 Makefile 日常构建和 CI 组合逻辑由豫构拥有
动态库分发、WASI 迁移、外部 Make 适配器 分平台验收;不支持项明确报错

第一阶段仍可暂用旧配方生成一次运行时归档,再把该归档作为显式输入验证新链接接口;第二阶段才切到豫构编译运行时,降低一次变更范围。不要同时更改 FFI 语言语法、值布局和构建引擎。

建议先批准四个边界:编译器退出最终链接;包原生组成首版只有一个;绑定首版只支持豫言值桥接 ABI;Makefile 保留严格限定的启动兼容层。其余字段名可在实现前再精简。