对于一个要运行几年几十年的软件产品,当它的软件工程开始为满足少数人的要求添加各种条件、规则、约束时,情况会逐步恶化,最终系统变成小心维护的炸弹,我们常遇到的问_题包括:
- 交付特性时,为满足特定客户、场景打包述求在构建工程添加各种分支逻辑,后来人依样画葫芦,构建脚本随之变得臃肿,同时叠加人员流动,情况会变得更复杂,扩展和维护的成本呈超线性扩张。
- 组件构建时依赖专用工具,组件自身缺少对这个工具的显式管理和引入,完全依赖CI工程为其安装工具,随着产品的长期演进,会产生组件裁剪或丢弃后工具冗余、组件依赖与CI安装的工具版本匹配导致CI无法归一到统一开发镜像、开发者不再具备本地构建能力等问_题。
- 应用为了满足不同满足需求,可能会添加一些特性开关(如用于控制发布到测试环境或正式环境),一般单个组件的特性开关较少,但在大量组件化场景,开关的数量可能突破几十或几百个,而且特性开关之间可能还会存在关联关系,如果这些开关缺少管理,或能会将特性置于错误的状态,引发风险。
"Keep it simple, stupid!"
openubmc版本借鉴了其它产品或公司的经验,认为“保持简单”是一个系统具有扩展性的一种基本原则,以下是在“保持简单”方面的一些实践。
正确高于一切
隐藏错误绝对是一个愚蠢的想法,
快速失败
遇到错误应立即退出。
openubmc构建代码中,在任何地方遇到错误时,会立即停止执行并抛出异常,并输出日志。
机制和策略分离
openubmc构建代码中预置了110+产品条件判断分支以及100+隐形的判断分支,以满足不同产品的安全策略、生产烧片、装配测试、文件权限、组件裁剪、签名打包述求。新增产品需要在现有构建代码新增或适配条件分支实现,带来各种霰弹式修改,情况持续恶化,易改错改漏。
在现代操作系统的结构设计中,经常利用“机制与策略分离”的原理来构造OS结构。所谓机制,是指实现某一功能的具体执行机构。而策略,则是在机制基础上,借助于某些参数和算法来实现该功能的优化,或达到不同的功能目标。通常,机制处于一个系统的下层,而策略则处于系统的上层。
构建系统具有相似的逻辑,我们可以把构建实现的一些基础功能提炼成构建机制,如构建产品包、集成组件、制作emmc烧片镜像、文件赋权裁剪、特性生效等都是机制,机制就如同带入参的函数,产品需要做的就是为机制提供输入,如产品可以决定产品包的签名算法、集成哪些组件、烧片emmc的容量、文件需要什么样的权限、打开什么特性等。
我们希望能够提供一款可扩展的构建系统,这个系统可以满足已知的产品构建,也能满足不可预知的产品灵活定制。同时,我们想构筑的是一个往稳定方向收敛的构建系统,其质量可以媲美产品,其维护成本要足够低。为此,我们决定将机制和策略完全解耦,构建任务只提供机制,而产品侧由一个我们定义的manifest.yml文件描述产品构建策略,两者结合实现产品构建。解耦可以实现如下改进:
- 可测可靠:机制可独立测试,任意正确的预期被破坏时都能被拦_截。并且,我们正在考虑为构建代码增加DT用例。
- 可维护:构建机制的有效代码量控制在小于100行/文件范围内,增加代码量小,可维护性强。
- 可定位:构建日志能区分机制或策略问_题,构建和业务的边界更清晰。
- 可描述:构建制品100%可描述,扩展、检视、测试更简单,产品间差异更清晰。
- 可扩展:结合任务编排框架进一步提升构建可扩展能力。
机制和策略分离最重要的变化是职责划分更清晰,构建工程师以正确执行机制为准,而产品则需要正则描述自己的述求,两者结合可以在各自擅长的领域工作,实现效率最优。
机制的扩展
实际操作和维护过程中我们还是会面对产品的扩展的需求挑战,面对产品的交付压力,软件经理可能会寻求最快速的解决方_案,此时将逻辑耦合到构建代码中可能是最快速实现功能方法,此时应该怎么权衡?
openubmc的生命周期可能会达到20年以上,所以在一个生态周期以十年记算的软件产品中,为了一时的快增加的临_时方_案只会增加技术债务,总有要还的时候,视而不见肯定不是最佳策略。
明确要求
技术债务的偿往往意味着工作的反复,考虑到过程中增加的成本,方_案可能存在的兼容和落地的成本,相比正式的方_案,临-时方_案总体至少有两倍以上的侧务需要偿还。
为此软件工程应该保持机制和策略解耦,并始终作为我们的最基本和最简单的要求持续看护。
扩展机制
为了保持稳定而不扩展往往会失去机会,我们的构建机制需要能够扩展,持续满足开发者、产品的要求,这个过程中我们要做到。
- 机制扩展需要分析,明确需要扩展的机制和产品实施策略。
- 机制本身需要能够配置,明确自己的入参和出参。
TIP
我们已经有很多成熟的机制和策略可以参考,以下是efuse包出包功能示例。
EFUSE包出包机制实现示例:
# 因机制实现跟构建框架有关且代码量较大,有兴趣的可以参考以下文件。
https:// openubmc /manifest/files?ref=master&filePath=build%2Fworks%2Fpacket%2Fwork_build_efuse_hpm.py&isFile=trueEFUSE包出包策略配置示例:
# openubmc/manifest.yml产品构建efuse包策略(节选),描述产品使用的efuse打包文件、hash校验以及最终在0502包中呈现的名称。
manufacture:
05024HFM:
efuse_hpm:
# efuse打包必须文件配置,因各产品存在差异,需要产品自己配置
files:
- file: /usr/share/bmcgo/efuse-packet/beforeaction.sh
- file: /usr/share/bmcgo/efuse-packet/afteraction.sh
- file: /usr/share/bmcgo/efuse-packet/CfgFileList.conf
- file: /usr/share/bmcgo/efuse-packet/efuse_partner_enable.txt
dst: efuse.txt
- file: /usr/share/bmcgo/efuse-packet/firstboot.sh
- file: /usr/share/bmcgo/efuse-packet/hpm_efuse.config
- file: /usr/share/bmcgo/efuse-packet/packetefuse.sh
- file: /usr/share/bmcgo/efuse-packet/update.cfg
# efuse重复构建hash值
sha256: 5b9997e1c8cb9ab655bd7dddc67a047f7748bf058511a0b33ea1dfc13cec1db6
files: # 0502打包机制的策略
- file: # …… 其它配置已省略
- file: ${work_out}/efuse-crypt-image.hpm # 复制efuse打包机制的产物
dst: efuse-crypt-image-partner.hpm配置项自动化管理
openubmc在组件mds、构建策略manifest.yml、csr配置等场景广泛使用json/yaml文件,一般我们会为文件定义明确的格式规范要求,如一个对象需要配置什么属性,这些属性需要什么样的值等要求。但格式规范要求的传递方式较为落后,主要靠文档、wiki、口口相传等方式传递,导致定义和使用之间脱节,不正确的配置持续上库,影响功能,降低了整体效率。
例如:机制和策略分离使我们有机会以yaml格式描述构建策略,但机制实际是一堆业务代码,对于旁人来源难以阅读和掌握,也缺少文档化描述,策略配置人员不能正确配置或者构建系统无法校验配置文件都将会把构建系统引入另一个难以配置的极端,将开发人员自然分为了全懂和全不懂的两种人,影响了机制的持续扩展。
为提供最简单高效的配置,我们采用json schema文件描述和校验配置文件
构建系统主要使用yaml格式描述,下面以yaml示例说明,采用json描述的文件可以复用相同的模式。
为解决配置项传递、约束和管理困境,我们采用schema描述配置文件,主要基于以下动作持续看护配置正确性和效率:
- 建立配置项标准,规范配置项引入,新增机制首先要完成schema设计,描述的内容除配置项名称、值类型、必须项等,如果是复杂字符串还要使用正则表达式限制可选值。
- 在团队推广vscode-yaml插件,该插件实现了格式检查和配置指导,如会提醒:错误的类型、值不符合约定范围、配置项缺失等,最大限度降低学习成本,甚至期望开发者不具备任何知识情况下也能正确配置策略。
- 我们的构建系统在会在启动构建前完成校验配置项的正确性,让配置错误拦_截阶段左移,拒绝错误配置进入仓库。
schema示例
schema使用json格式描述,主要的字段包括type、additionalProperties、required、properties等,示例参考:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "manifest.yaml",
"description": "openubmc Manifest file",
"type": "object",
"additionalProperties": false,
"required": [
"dependencies"
],
"properties": {
"dependencies": {
"$ref": "#/$defs/dependencies"
}
},
"$defs": {
"dependencies": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"conan"
],
"properties": {
"conan": {
"type": "string",
"pattern": "^[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+@[A-Za-z0-9_.\\-]+/(rc|stable|dev)$"
}
}
}
}
}
}配置管理
对于需要使用schema保护的配置文件,需要在文件头部申明yaml-language-server指示配置文件需要满足的schema标准,以下是子系统给把清单配置示例:
# yaml-language-server: $schema=../../schema/subsys.rc.schema.json
dependencies:
- conan: "mdb_interface/1.10.116@openubmc/rc"
- conan: "mdb_mgmt/1.0.1@openubmc/rc"validation校验
我们的python程序使用jsonschema库在启动构建时完成配置文件正确性校验,拦_截配置项的格式错误问_题。
校验代码示例:
import jsonschema
import yaml
import json
def schema_valid_file(schema_file, config_file):
"""使用schema_file校验config_file"""
with open(schema_file, "rb") as fp:
schema = json.load(fp)
with open(config_file, "rb") as fp:
config = yaml.load(fp, yaml.FullLoader)
jsonschema.validate(subsys, config)TIP
json schema已经普遍应用在各种场合,json和yaml的schema文件归一的,schema规范可参考:
依赖自管理
构建工具自维护
构建系统会维护组件构建、测试、发布依赖的公共工具集,如编译器、conan、python3等,但如果工具只是少数几个组件使用的,需要组件自维护,这样做可以让环境更容易管理,具体实施方法请参考《构建工具管理》文档
依赖关系显式管理
我们使用service.json文件描述组件的依赖关系,并将依赖关系分为build依赖和test依赖,两者的区别在于:
- build依赖:组件的编译时依赖,一般是依赖的库或头文件,缺少这些依赖一般会导致头文件找不到、库文件无法链接等。
- test依赖:组件的运行时依赖,缺少这些组件可能会导致无法运行或功能受限,当前只应用在DT/IT场景。test依赖不参与编译,只是在需要IT时部署到本地。
构建系统一般包含编译器、SDK及待编译组件,除编译器和SDK提供的公共头文件、库文件名外,以conan包形式提供的组件,如开源软件、平台软件、第三方芯片软件等,都应该以组件的显式依赖引入到系统,我们的构建工程会自动管理这些组件。