- 开发工具
- CLI
- 代码质量
【免费下载链接】cangjie_tools
仓颉编程语言命令行工具,包括仓颉包管理工具、仓颉格式化工具、仓颉多语言桥接工具及仓颉语言服务。
cjcompat(Cangjie Compatible)是 cangjie_tools 仓库中面向仓颉模块演进的兼容性检查工具,它依据仓颉Library Evolution规范对两个版本的.cjo文件做 AST 级差异比对,帮助库作者在发布新版本前验证是否会破坏下游的 API 或 ABI 兼容承诺。读完本文,你将掌握 cjcompat 的构建安装流程、全部命令行选项与依赖解析机制,并能结合源码理解其 “加载 → 差异识别 → 规则检查 → 结果输出” 的完整检查链路。
项目定位与总体架构
根据 cjcompat 开发者指南,cjcompat (Cangjie Compatible)是一款基于仓颉语言Library Evolution规范开发的ABI/API兼容检查工具。其检查对象是编译期产物.cjo(仓颉模块的二进制摘要),而不是源码文本:工具把新旧两个版本的.cjo加载成 AST 包(package),再对两棵 AST 做结构化 Diff,最后由一组规则检查器(Checker)判定每处变更是否违反兼容规则。
仓库内同时附带了该工具的规范依据——仓颉模块演进兼容性规范(cjcompat/doc/library_evolution_spec/source_zh_cn/)。规范在 概述章节 明确了两级兼容性的定义,并声明该规范“同时作为 CJCOMPAT(仓颉工具链中提供的兼容性检查配套工具)的实现规格文档”:
- API 兼容:改动后保证下游代码重新编译能正常完成;
- ABI 兼容:在 API 兼容的基础上,进一步保证下游代码在不重新编译的情况下可以直接重新链接。
规范还明确了两条业界通行的豁免原则:其一,在本模块中新增 API(新增全局变量/函数/类型/扩展、新增父类与父接口、放宽扩展中的泛型约束等)导致下游符号冲突或函数重载决议失败,不属于API 兼容破坏;其二,仓颉支持 FFI,新增对 foreign 函数的依赖导致名称冲突而链接失败,不属于ABI 兼容破坏。这两条豁免直接体现在后文规则表中部分规则“新增即兼容”的判定上。此外,规范指出判定基准是完成宏展开、条件编译处理与自动类型推导之后的代码。
源码目录结构
cjcompat源码目录及各子模块职责如下(与开发者指南一致):
cjcompat/ |-- build # 构建脚本 |-- doc # 介绍文档 |-- library_evolution_spec # 仓颉模块演进兼容性规范 |-- include # 源码相关头文件 |-- src |-- CheckersAndRules # 检查规则源码 |-- CjoLoader # cjo文件解析 |-- Diff # AST差异识别 |-- Dsl # 公共模块 |-- Logger # 检查结果 |-- Option # 命令行选项对照 src 目录 的实际组织:main.cpp是程序入口,Cjcompat.cpp是主流程编排,CjoLoader/、Diff/、Dsl/、Logger/、Option/五个模块与规范中各 Checker 实现(CheckersAndRules/下共 10 个.cpp文件)一一对应,构建入口为 cjcompat/CMakeLists.txt。
构建与安装
构建准备
cjcompat需要clang或gcc编译器,并且构建依赖cjc及其编译产物,构建方式参见 SDK 构建文档(cangjie_build 仓)。因此构建前必须配置环境变量CANGJIE_HOME指向仓颉 SDK 根目录。
从 CMakeLists.txt 可以看到 CMake 配置阶段同样强制要求该变量:优先取-DCANGJIE_HOME,其次取环境变量ENV{CANGJIE_HOME},否则给出提示“Please specify the path to the Cangjie SDK via the environment variable CANGJIE_HOME or by using the -DCANGJIE_HOME option.”。CMake 还会:
- 将版本宏
CJCOMPAT_VERSION设为当前构建版本(当前仓库为1.3.0-alpha.06,见 CMakeLists.txt 第 16 行),并在配置阶段调用cjc -v获取编译器版本写入CJCOMPAT_CANGJIE_VER(见 CMakeLists.txt 第 156-168 行),供运行期-v输出版本信息; - 从
${CANGJIE_HOME}/tools/lib引入cangjie-lsp动态库(Linux 下为libcangjie-lsp.so,Windows Debug 下为静态库libcangjie-lsp.a,见 CMakeLists.txt 第 188-207 行)——这正是文档强调“可执行文件依赖cangjie-lsp动态库”的构建期依据; - 将头文件搜索路径指向
${CANGJIE_HOME}/include(CMakeLists.txt 第 180-183 行)。
构建步骤
本地构建流程如下:
通过
git clone获取cjcompat的最新源码:cd ${WORKDIR} git clone https://gitcode.com/Cangjie/cangjie_tools.git配置环境变量:
export CANGJIE_HOME=${WORKDIR}/cangjie (for Linux/macOS) set CANGJIE_HOME=${WORKDIR}/cangjie (for Windows)cjcompat的编译依赖cangjie仓的编译产物,因此需要配置环境变量CANGJIE_HOME指定 SDK 的位置。上述${WORKDIR}/cangjie仅作为示意,请根据 SDK 的实际位置调整。注意:
Windows的环境变量配置需要正确使用目录分隔符,并确认路径中的中文字符被正确识别和处理。
通过
cjcompat/build目录下的构建脚本编译cjcompat:cd cangjie-tools/cjcompat/build python3 build.py build -t release当前支持
debug、release两种编译类型,开发者需要通过-t或者--build-type指定。安装到指定目录:
python3 build.py install默认安装到
cjcompat/dist目录下,支持通过install命令的参数--prefix指定安装目录:python3 build.py install --prefix ./output编译产物目录结构为:
dist/ |-- bin `-- cjcompat # 可执行文件,Windows 中为 cjcompat.exe验证
cjcompat是否安装成功:./cjcompat -h进入安装路径的
bin目录下执行上述操作,如果输出cjcompat的帮助信息,则表示安装成功。由于可执行文件依赖cangjie-lsp动态库,需要将库路径配置到系统动态库环境变量中。以Linux环境为例:export LD_LIBRARY_PATH=$CANGJIE_HOME/tools/lib:$LD_LIBRARY_PATH ./cjcompat -h清理编译中间产物:
python3 build.py clean
当前同样支持Linux平台交叉编译出Windows下运行的cjcompat产物,构建指令如下:
export CANGJIE_HOME=${WORKDIR}/cangjie python3 build.py build -t release --target windows-x86_64 python3 build.py install执行后构建产物默认位于cjcompat/dist目录下。请注意:从Linux平台交叉编译获取Windows平台产物时,需要的 SDK 为Windows版本。
更多构建选项
python3 build.py build命令也支持其它参数设置,具体可通过如下命令查询:
python3 build.py build -h命令行接口
cjcompat的核心用法是cjcompat --old cjofile --new cjofile。执行cjcompat -h的帮助信息(源码见 Option.cpp 的 PrintHelp)如下:
Usage: cjcompat --old /path/to/old.cjo --new /path/to/new.cjo [--api] [--abi] [--import-path /path] Options: -h Show usage eg: cjcompat -h --old <value> Path to the .cjo file corresponding to the old version of a Cangjie library. --new <value> Path to the .cjo file corresponding to the new version of a Cangjie library. eg: cjcompat --old filePathOld --new filePathNew --import-path <value> Add .cjo search path. --api Enforces rules for API compatibility. --abi Enforces rules for ABI compatibility. eg: cjcompat --old filePathOld --new filePathNew --api各选项与源码中Options结构体(Option.h)字段的对应关系如下:
| 选项 | 含义 | 源码行为 | |-|-|-| |--old <value>| 旧版本.cjo路径 | 写入cjoPathOld,只允许出现一次,重复出现即报Argument: --old, used more than once!| |--new <value>| 新版本.cjo路径 | 写入cjoPathNew,同样只允许出现一次 | |--import-path <value>| 追加.cjo搜索路径 | 追加到importPaths,可多次指定 | |--api| 仅检查 API 兼容规则 | 置位enableAPI| |--abi| 仅检查 ABI 兼容规则 | 置位enableABI| |-h/ 无参数 | 打印帮助信息 | 打印PrintHelp后以END退出 | |-v| 打印 cjcompat 版本及依赖的cjc版本 | 见 Option.cpp 的 PrintVersion |
参数校验逻辑集中在 CJCOMPATProcessFlag 中,它实现了文档中的两条硬性约束:
--old/--new必须提供且以.cjo结尾,否则输出Essential argument --old wrong or missing!/Essential argument --new wrong or missing!并附帮助信息;- 当
--api与--abi都未指定时,自动将两者都置为开启——即默认同时检查 API 和 ABI 规则。
兼容性检查实战
完整规则检查
cjcompat --old cjofile --new cjofile检查不同版本的cjo是否兼容,cjofile支持相对路径和绝对路径:
cjcompat --old old.cjo --new new.cjo说明:
- 规则检查时必须指定
--old和--new选项且仅允许出现一次,否则报错;- 不同版本
cjofile必须以.cjo结尾,否则报错;- 不同版本
cjofile必须是兼容版本内cjc编译生成的,否则可能因版本不兼容而出现cjo文件解析错误;- 不指定检查规则类型时,默认
api和abi规则都检查;- 如果出现
Cannot load file old.cjo报错提示,则可能还依赖其他cjo文件,可以通过使用CANGJIE_PATH环境变量或--import-path来指定依赖的cjo文件路径。
仅检查 API 兼容规则
--api选项让用户指定仅检查api兼容规则:
cjcompat --old old.cjo --new new.cjo --api适用于“下游允许重新编译”的发布场景:只要重新编译后仍能通过,即可发布。
仅检查 ABI 兼容规则
--abi选项让用户指定仅检查abi兼容规则:
cjcompat --old old.cjo --new new.cjo --abiABI 规则集是 API 规则集的超集(每条规则在规则表中都标注了isAPI/isABI归属),因此仅用--abi检查可以对二进制直接重链接的严格场景给出结论。
依赖.cjo的搜索路径
运行期搜索依赖.cjo的机制在 SetCangjieModulesPath 中实现,可以印证文档中关于CANGJIE_PATH的说明:
- 工具要求进程环境中必须存在
CANGJIE_HOME,否则直接报Can not find CANGJIE_HOME, please setup CANGJIE_HOME(CheckCangjieHome)——注意这意味着运行时也依赖该环境变量,而不仅仅是构建期; - 取
$CANGJIE_HOME/modules下的第一个子目录作为标准库.cjo的modulesPath; - 若存在环境变量
CANGJIE_PATH,则将其按路径分隔符拆分后加入搜索路径cangjiePaths;命令行--import-path传入的路径同样汇入搜索路径。
随后这些路径被交给两个CjoLoader(构造时传入modulesPath、cangjiePaths、importPaths),加载失败时主流程输出的正是文档中提到的报错:Cannot load file <path>,并提示 “Check if the .cjo file of the package and all packages it depends on exists in CANGJIE_PATH or CANGJIE_HOME, or use '--import-path' to specify the .cjo file path.”(见 Cjcompat.cpp 第 27-41 行)。
检查结果如何解读
检查结果由Logger统一输出(Logger.cpp):
- 若没有任何规则命中,输出
Compatible!; - 否则输出
Incompatible!,随后按规则逐条列出命中详情:每条先打印规则归属([API]、[ABI]或[API/ABI],具体取决于规则定义与当前启用的检查模式)和规则英文描述,再列出相关声明的[BEFORE]/[AFTER]标记及文件:行:列位置,方便开发者定位新旧版本中的对应声明。
内部工作原理:从 .cjo 到兼容结论
从源码结构看,CJcompat() 主函数 的调用链非常清晰地对应了架构图中的各阶段:
Options(命令行解析) └─> CjoLoader ×2(分别加载 old/new .cjo,还原出 AST Package 与 TypeManager) └─> Diff(结构化差异识别:added / deleted / potentiallyModified) └─> Dsl + AllCheckers(规则检查器集合,按优先级运行) └─> Logger(汇总输出 Compatible! / Incompatible!)各环节要点如下:
- CjoLoader(cjo 文件解析):CjoLoader 内部复用了编译器前端的
CompilerInvocation+CompilerInstance,把.cjo反加载为带类型信息(TypeManager)的AST::Package,从而让规则检查可以基于类型推导后的结果做判定,与规范“以自动类型推导完成后的类型结果为判定基准”的要求一致。 - Diff(AST 差异识别):Diff 对两个 Package 做声明级配对,维护四类结构:
added(仅新版存在)、deleted(仅旧版存在)、potentiallyModified(新旧版本中配对成功的声明)、成员级potentiallyMemberModified。它还负责收集“模块外可见节点”(visibleNodesOld/New),这与规范中“模块外可见”的定义直接对应——只有模块外可见声明的变更才可能触发兼容规则。 - Dsl(规则判定公共模块):Dsl 为每个检查器提供谓词(
IsPublic、IsConst、IsFrozen、CheckDepreAnnotation等)、类型关系判断(SameType、Subtype、IsParentType)和迭代宏。其中的CHECK(rule, expr, symbols...)宏是规则落地的统一入口:先确认该规则属于当前 Checker、且按--api/--abi开关处于启用状态,再执行判定表达式expr,若判定为不兼容则通过logger.LogIfFalse记录命中的规则与相关 AST 节点。 - CheckersAndRules(检查规则):规则与检查器的注册都采用 X-macro 风格的
.inc文件集中管理:- RulesList.inc 以
RULEKIND(ID, isAPI, isABI, description)宏声明了全部规则(百条量级),每条规则标注它属于 API 还是 ABI 检查,以及面向用户的英文描述;Rule类构造时有CJC_ASSERT(isAPI || isABI)断言,保证每条规则至少归属一个检查维度(Rule.h); - CheckersList.inc 以
CHECKER_DECL(NAME, PRECEDENCE, RULE_KIND...)声明了约 50 个 Checker(如PackageDeclChanged、FuncParametersChanged、StructMemberVarModified、ClassInstanceMemberFuncParamsOrRetTypeChanged、EnumConstructorAdded等),并列出每个 Checker 负责实现的规则集合; - 检查器的执行调度在 AllCheckers::RunAll:先按
precedence升序稳定排序(数字越小优先级越高,默认值为DEFAULT_PRECEDENCE 100,而包名变更检查PackageDeclChanged的优先级被设为 50,使其最先执行——因为包名变化会使所有声明的 mangled name 改变,破坏新旧声明的配对基础),且一旦某个高优先级 Checker 判定失败,后续低优先级检查即停止运行。
- RulesList.inc 以
规则表示例:全局变量修饰符变更
规范文档 Chapter_03_Rules.md 给出了细到每一类变更行为的兼容矩阵,cjcompat 的规则实现与之逐条对应。以“修改全局变量修饰符”为例:
| 变更行为 | API 兼容性 | ABI 兼容性 | |-|-|-| | 访问修饰符由 public 改为非 public | 不兼容 | 不兼容 | | 访问修饰符由非 public 改为 public | 兼容 | 兼容 | | 访问修饰符在非 public 的其他三种情况间(protected/internal/private)变化 | 兼容 | 兼容 | | 修改 let 修饰符为 var 修饰符 | 兼容 | 兼容 | | 修改 var 修饰符为 let 修饰符 | 不兼容 | 不兼容 | | 修改 let 修饰符为 const 修饰符 | 兼容 | 不兼容 | | 修改 var 修饰符为 const 修饰符 | 不兼容 | 不兼容 | | 修改 const 修饰符为 let 修饰符 | 不兼容 | 不兼容 | | 修改 const 修饰符为 var 修饰符 | 不兼容 | 不兼容 |
其中let → const是“API 兼容但 ABI 不兼容”的典型:const 变量经过编译期求值后可能从二进制中被优化删除,导致旧下游的二进制找不到该符号;而let → var兼容是因为可变性放宽不破坏既有调用。类似地,RulesList.inc中VAR_ACCESS_MODIFIER_MODIFIED(API+ABI 均不兼容)、FUNC_NON_ACCESS_MODIFIER_UNSAFE_ADDED(把模块外可见函数改为 unsafe,双不兼容)等规则的isAPI/isABI标志位都与规范矩阵保持一致。
批量回归:SDK 标准库的兼容性自查
仓库还内置了一个面向 SDK 发布流程的批量检查脚本 cjcompat_check_libs.sh:传入新旧两个 SDK 版本目录与目标类型(如x86_64),它会在两个 SDK 的modules/linux_<type>_cjnative(或_llvm)目录下按相对路径配对所有同名.cjo,对每一对依次执行cjcompat --old <旧版.cjo> --new <新版.cjo>并写入result_cjcompat_libs.log,最后扫描日志中是否出现Incompatible来给出总体结论。这为团队在 SDK 大版本升级时批量验证标准库模块的兼容承诺提供了现成手段。
小结
cjcompat把仓颉Library Evolution规范中的兼容矩阵工程化为一条可运行的检查流水线:Option模块完成参数与环境校验,CjoLoader借助编译器前端还原带类型信息的 AST,Diff产出声明级差异,Dsl+ 按优先级调度的 Checker 集合逐条套用规则,Logger最终输出Compatible!/Incompatible!及命中详情。对库作者而言,其价值在于把“我的改动是否破坏了下游的 API/ABI 承诺”这一判断从人工评审变成了可重复执行的命令;对维护者而言,RulesList.inc 与 CheckersList.inc 两份集中注册文件加上规范文档 Chapter_03_Rules.md,为后续扩展兼容规则提供了清晰的扩展点。相关仓库(cangjie 仓、SDK 构建仓)的信息可参见 cjcompat/doc/developer_guide.md 的“相关仓”一节。
- 开发工具
- CLI
- 代码质量
【免费下载链接】cangjie_tools
仓颉编程语言命令行工具,包括仓颉包管理工具、仓颉格式化工具、仓颉多语言桥接工具及仓颉语言服务。
相关推荐
TablePro 插件系统与 PluginKit:基于 Library Evolution 的 ABI 兼容工程实践
TablePro 插件系统与 PluginKit:基于 Library Evolution 的 ABI 兼容工程实践 导读 本文以 TablePro 开源仓库中
仓颉语言命令行工具链 cangjie_tools:九大工具组成、目录规范与构建体系解析
仓颉语言命令行工具链 cangjie_tools:九大工具组成、目录规范与构建体系解析 cangjie_tools 是仓颉(Cangjie)编程语言的官方命令行
开发工具CLI代码质量仓颉语言命令行工具链 cangjie_tools 详解:九大工具全景、目录结构与构建依赖
仓颉语言命令行工具链 cangjie_tools 详解:九大工具全景、目录结构与构建依赖 cangjie_tools 是仓颉语言官方命令行工具链仓库,集中提供项
开发工具CLI代码质量
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考