ch03:fork 不是复制粘贴——许可证合规与工具链落地

第 4 / 14 章
ch03:fork 不是复制粘贴——许可证合规与工具链落地

"fork 完了,先改代码?"新来的同事小周第一天就问。

"先过法务,再过构建,最后才轮到改代码。"我说。开源改造项目里最贵的返工,不是写错的代码,而是发现许可证用错了、或者构建链两个月都稳不下来。这一章讲我们 fork OpenEMS 后头两周做的三件事:合规基线、工具链落地、CI 第一绿。

一、事故现场:一个许可证头引发的评审风暴

许可证合规评审

技术方案评审会上,法务顾问指着 PPT 问:"你们要闭源交付给业主,AGPL 的部分怎么办?"

我提前准备过这个问题,但还是被追问了三层:Edge 层 EPL-2.0 允许修改后闭源,那"修改"的边界是什么?我们新写的控制器调用了 OpenEMS 的接口,新代码属于衍生作品吗?UI 层 AGPL-3.0,如果我们给业主部署了一套私有化的 Web 界面,是不是必须开放源码?

那天评审的结论,整理成了三条公司红线:

  1. 分层隔离:EPL-2.0 的 Edge/Backend 层允许闭源衍生修改,我们所有自研代码写在这一层,每个文件保留原始版权声明并附修改声明——这是 EPL-2.0 第 3 条的明确要求;
  2. UI 层不碰:AGPL-3.0 的 UI 我们只做"未修改的聚合使用",通过标准 JSON-RPC 接口交互;将来业主要定制大屏,就新建独立的前端项目,与 AGPL 代码之间只走网络协议边界;
  3. 法务留痕:fork 仓库里建 NOTICE.md,逐条记录"哪些文件改了、改了什么、为什么",这个文件后来成了每次审计的第一入口。

顺便说个真问题:很多团队以为开源软件"标了商用友好"就万事大吉,实际上 EPL-2.0 要求专利授权只来自贡献者本人、且触发专利报复条款会失效——这些细节法务比工程师专业,让专业的人早进场,比上线后被发函强一百倍。

二、梳理:bnd 工具链的两周爬坡

合规定了,接下来是让 fork 仓库在本机和 CI 上稳定构建。OpenEMS 用的是 Gradle + bnd 工作区,对没碰过 OSGi 的团队来说有两个爬坡点。

爬坡点一:模块发现机制。 上章说过 settings.gradle.kts 会自动扫描含 bnd.bnd 的目录注册为子项目。这意味着"新建一个模块"不是复制 pom,而是"建目录+写 bnd.bnd+放对包结构"。我们给团队定了一个模块模板,新模块五分钟就能挂进构建。

爬坡点二:bnd.bnd 的元数据。 每个 bnd.bnd 里声明 Bundle 的名称、版本、-buildpath(编译依赖)和 -runrequires(运行时装配)。OSGi 的包导入导出是自动推导的,但推导结果要人工审——我们踩过的坑是某个驱动模块意外引入了传递依赖,导致打包体积膨胀了 40MB,最后靠 bnd 的 -check 指令在 CI 里拦住。

配置好之后,本机构建就一条命令的事:JDK 17+ 装好,./gradlew build 一把过,产物在 io.openems.edge.application 模块下——它是 Edge 层的装配模块,把所有 bundle 打成一个可运行的发行包。Backend 同理是 io.openems.backend.application。

三、底层原理:为什么 fork 策略选"跟随上游"

fork 之后第一个架构决策:本地 develop 分支跟随上游 main,还是彻底分道扬镳?

我们的选择是长期跟随上游,理由写进了仓库的 docs/fork-policy.md:

  1. 上游还在活跃进化:设备驱动、协议桥每个月都有社区提交,彻底分叉等于放弃这些红利;
  2. 我们的改动集中在"新增"而非"修改":ch02 说过,自研控制器、AGC 模块都是新增模块,与上游冲突面天然小;
  3. 修改必须留痕:极少数需要改上游文件的场景(比如给调度引擎加甘肃模式的接口预留),坚持"小步修改+commit 信息注明上游文件+NOTICE.md 登记"三件套。

落地成 Git 纪律就是三条:本地 upstream 指向 OpenEMS 官方仓库,origin 指向我们的私有 fork;每章改造代码合入 develop;每逢上游发版,由专人做一次 rebase 评估。这个策略 later 让我们吃到过一次甜头:上游对某个协议桥的线程池缺陷的修复,我们 rebase 时白捡。

四、正确姿势:CI 第一绿的清单

改造型项目的 CI 和常规项目有一个本质区别:它必须证明"你改过的地方没坏,你没改的地方也没坏"。我们的 CI 流水线第一周就立好了四道关卡:

CI 流水线关卡

  1. 编译关:./gradlew compileJava 全模块编译,bnd 的 -check 开到最严,任何包导入异常直接红灯;
  2. 单测关:./gradlew test,上游自带测试全保留,新增模块的测试覆盖率卡 60% 红线——对调度引擎这类核心,我们在 ch12 会展示黄金数据回归测试的方法论;
  3. 合规关:自定义脚本扫描所有新增源文件,检查 EPL/AGPL 许可证头是否按模板齐全,缺失即红灯;同时扫描 NOTICE.md 是否登记了本轮改动;
  4. 构建产物关:出 Edge 发行包,启动、加载全部 bundle、模拟一个最小系统拓扑,冒烟通过才算绿——OSGi 运行期的装配问题只有真启动才能暴露。

这四道关里最花心思的是第四道。OSGi 的依赖问题编译期完全无感,运行期装配失败却是真事故。那次为了把"模拟拓扑"跑进 CI,我们用 OpenEMS 自带的仿真设施拼了一个虚拟场站:虚拟电表+虚拟储能+一个只写日志的控制器,五分钟内完成"启动-装配-循环-退出"闭环。这套冒烟拓扑后来一路进化,成为 ch12 全场联合仿真的种子。

五、数据说话:两周落地了多少

给数据控一个交代,头两周工程化的产出清单:

  • fork 与分支:1 个 fork 仓库,develop 跟随上游 main,fork 政策文档 1 份;
  • 合规:许可证红线 3 条、NOTICE.md 1 份、CI 合规扫描脚本 1 个,扫描规则覆盖 233 个模块的全部源文件;
  • 工具链:模块模板 1 套(新模块 5 分钟入构建)、bnd 构建稳定通过、本机全量构建时长约 6 分钟(后续讲增量构建优化);
  • CI:四道关卡全绿,全流水线约 15 分钟,OSGi 冒烟拓扑 1 套(虚拟电表+虚拟储能+日志控制器);
  • 代码:自研净增 0 行——是的,两周没写一行业务代码,但之后每次提交都踩在硬地基上。

有同事问过"两周零业务代码值不值",我的回答是:改造型项目的风险分布和自研相反——自研的坑在后期的算法和联调,改造的坑在前期的地基和合规。地基的钱不能省。

六、面试怎么答

"你们改造开源项目,怎么管许可证合规?"答题框架:

  1. 先分层:讲 EPL-2.0 与 AGPL-3.0 的边界,修改闭源的合法性与义务(保留声明+修改声明);
  2. 再隔离:AGPL 层不修改只聚合,网络交互边界隔离传染风险;
  3. 后流程:NOTICE.md 留痕+CI 自动扫描,让合规从"法务的事"变成"流水线的事";
  4. 加一句认知:合规的本质不是防御,而是让公司敢在开源之上做商业承诺——这层认知是工程师和架构师的分水岭。

追问"OSGi 工具链学习成本怎么办",答案参考 ch02 第六节:把 OSGi 收敛在骨架层,团队按普通 Java 开发,模板与 CI 兜底。

七、落地清单

  1. fork 先过法务再过构建:许可证分层红线、NOTICE.md、CI 扫描三件套一个不能少;
  2. bnd.bnd 是模块边界的唯一判据,给团队准备模块模板,新模块五分钟入构建;
  3. fork 策略优先"跟随上游",改动尽量新增而非修改,修改必留痕;
  4. CI 必须有 OSGi 冒烟拓扑——编译绿灯不代表能装配、能装配不代表能循环;
  5. 下一章(ch04),我们终于摸到真家伙:把华为 SUN2000 和阳光逆变器接进 OpenEMS 的 Modbus 协议栈,从寄存器表读到通道编排,看看"设备接入"这四个字背后到底有多少工程。