From c98a7feb48bc511f79c7db0881a4dccb3945ba0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A9=98=E6=82=A6?= Date: Thu, 1 Sep 2022 17:17:19 +0800 Subject: [PATCH] =?UTF-8?q?change=20305=20-=20=E9=BE=99=E8=9C=A5=E7=A4=BE?= =?UTF-8?q?=E5=8C=BA=E8=BD=AF=E4=BB=B6=E5=8C=85=E7=AE=A1=E7=90=86=E5=8E=9F?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 17 +- README.md | 7 +- articles/106-contribute-to-docs.md | 4 +- articles/304-maintain-a-package.md | 1 - ...-introduction-and-management-principles.md | 217 +++ articles/305-add-project-to-anolis-os.md | 193 --- articles/305-module-and-checklist-of-spec.md | 279 ++++ articles/306-instruction-manual-of-spec.md | 1235 +++++++++++++++++ ...-project.md => 307-build-a-new-project.md} | 0 ...ts-execution-order-during-rpm-upgrade.jpeg | Bin 0 -> 68297 bytes 10 files changed, 1748 insertions(+), 205 deletions(-) delete mode 100644 articles/304-maintain-a-package.md create mode 100644 articles/304-package-introduction-and-management-principles.md delete mode 100644 articles/305-add-project-to-anolis-os.md create mode 100644 articles/305-module-and-checklist-of-spec.md create mode 100644 articles/306-instruction-manual-of-spec.md rename articles/{306-build-a-new-project.md => 307-build-a-new-project.md} (100%) create mode 100644 images/306-scripts-execution-order-during-rpm-upgrade.jpeg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fd7929..f8a0cec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,14 +94,19 @@ 龙蜥社区的内核研发经验可以追溯到十年前,社区里的许多开发者都是资深的内核开发者,也为上游 Linux 社区贡献了非常多内核补丁。如何在龙蜥社区的框架下加入内核研发工作,可以参考《[303 - 内核研发指南](/articles/303-join-kernel-developing.md)》页面。 -### 3.4 成为软件包的维护者 +### 3.4 为龙蜥操作系统集成更多的开源项目 -成为维护者意味着除了要参与软件包构建工作,还需要承担整个软件包的研发决策和更新发布工作。《[304 - 软件包维护指南](/articles/304-maintain-a-package.md)》中有一些流程和建议,可以参考。 +除了维护现成的软件包,龙蜥社区非常欢迎将更多的开源项目加入到龙蜥操作系统中,构建更繁荣的操作系统软件生态。相关流程和指南可以参考《[304 - 龙蜥社区软件包引入和管理原则](/articles/304-package-introduction-and-management-principles.md)》一文。 -### 3.5 为龙蜥操作系统集成更多的开源项目 +### 3.5 龙蜥社区软件的 spec 模版和自检项 -除了维护现成的软件包,龙蜥社区非常欢迎将更多的开源项目加入到龙蜥操作系统中,构建更繁荣的操作系统软件生态。相关流程和指南可以参考《[305 - 龙蜥社区软件包集成流程](/articles/305-add-project-to-anolis-os.md)》一文。 +龙蜥社区是个更规范、更统一的社区,每款开源项目都需要有个 spec 文件用于构建和生成 rpm。参考 《[305 - spec 模版和自检项](/articles/305-module-and-checklist-of-spec.md)》一文。 -### 3.6 构建原生的龙蜥社区项目 +### 3.6 龙蜥社区软件的 spec 指导手册 -从0 开始构建一个原生的操作系统项目,无疑是让人非常兴奋的。整个过程中的注意事项,可以参考《[306 - 从零构建一个龙蜥开源项目](/articles/306-build-a-new-project.md)》一文。 +龙蜥社区也会帮助你更加了解一款软件的构建和安装过程,特增加指导手册,可以参考《[306 - spec 指导手册](/articles/306-instruction-manual-of-spec.md)》一文。 + + +### 3.7 构建原生的龙蜥社区项目 + +从0 开始构建一个原生的操作系统项目,无疑是让人非常兴奋的。整个过程中的注意事项,可以参考《[307 - 从零构建一个龙蜥开源项目](/articles/307-build-a-new-project.md)》一文。 diff --git a/README.md b/README.md index 9a439f9..23262a4 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,10 @@ + 《[301 - Review 代码指南](/articles/301-join-code-review.md)》 + 《[302 - 发行版构建指南](/articles/302-join-os-package-build.md)》 + 《[303 - 内核研发指南](/articles/303-join-kernel-developing.md)》 - + 《[304 - 软件包维护指南](/articles/304-maintain-a-package.md)》 - + 《[305 - 龙蜥社区软件包集成流程](/articles/305-add-project-to-anolis-os.md)》 - + 《[306 - 从零构建一个龙蜥开源项目](/articles/306-build-a-new-project.md)》 + + 《[304 - 龙蜥社区软件包引入和管理原则](/articles/304-package-introduction-and-management-principles.md)》 + + 《[305 - spec 模版和自检项](/articles/305-module-and-checklist-of-spec.md)》 + + 《[306 - spec 指导手册](/articles/306-instruction-manual-of-spec.md)》 + + 《[307 - 从零构建一个龙蜥开源项目](/articles/307-build-a-new-project.md)》 ## 4. 参与文档贡献 diff --git a/articles/106-contribute-to-docs.md b/articles/106-contribute-to-docs.md index e5e4c88..db17cd6 100644 --- a/articles/106-contribute-to-docs.md +++ b/articles/106-contribute-to-docs.md @@ -26,7 +26,7 @@ │   ├── 102-join-discussion.md │   ├── ... │   ├── ... -│   └── 306-build-a-new-project.md +│   └── 307-build-a-new-project.md ├── assets # Jekyll 样式、素材目录 │   └── css │   ├── style.scss @@ -70,7 +70,7 @@ ![fork 仓库](../images/106-fork-a-repo.jpg) + 如果本次文档任务是基于现有的文章进行完善,或者你发现了已发布文档的 BUG,可以在根目录或者 `articles/` 目录下找到对应的文档,进行修改; -+ 如果你的任务是写一篇新的文档,可以在 `articles/` 目录下创建新的文档,文件名格式为:以三位数字开头(在会议上会提前确定),然后用简单直观的英文表述,中间用'-'连字符,最后以`.md`结尾。例如:`306-build-a-new-project.md`。随后,即可开始相关的文档编写工作。 ++ 如果你的任务是写一篇新的文档,可以在 `articles/` 目录下创建新的文档,文件名格式为:以三位数字开头(在会议上会提前确定),然后用简单直观的英文表述,中间用'-'连字符,最后以`.md`结尾。例如:`307-build-a-new-project.md`。随后,即可开始相关的文档编写工作。 本站所有的文档,都遵循标准的 Markdown 语法,了解 Markdown 基本语法可以点击[这里](https://www.markdown.xyz/basic-syntax/)。基于 Markdown 语法,我们可以将所有的文档工作都纳入到代码流程中去。完成一次文档编写后,需要以 git commit 的形式保存。建议使用 `git commit -s ` 的方式,带上自己的邮箱签名。 diff --git a/articles/304-maintain-a-package.md b/articles/304-maintain-a-package.md deleted file mode 100644 index f835e02..0000000 --- a/articles/304-maintain-a-package.md +++ /dev/null @@ -1 +0,0 @@ -# 帮助维护社区的基础设施 diff --git a/articles/304-package-introduction-and-management-principles.md b/articles/304-package-introduction-and-management-principles.md new file mode 100644 index 0000000..9d9ba3c --- /dev/null +++ b/articles/304-package-introduction-and-management-principles.md @@ -0,0 +1,217 @@ +# 304 龙蜥社区软件包引入和管理原则 + + +## 1. 综述 +### 1.1 文档范围 +本文档涵盖: + +- 龙蜥社区产品发布 SIG 成员在引入或维护 RPM 软件包时需遵循的准入原则及操作流程; +- 龙蜥社区开发者及各 SIG 成员将 RPM 引入 Anolis OS 发行版时需遵循的准入原则及操作流程。 + +本文档不涵盖: + +- 源码引入原则及操作流程。源码引入到龙蜥社区是另外的话题,需要单独向龙蜥社区技术委员会申请; +- 软件包引入规则的原理解析; +- 软件包引入时 RPM SPEC 的编写细节。 + +本文档的维护主体为**龙蜥社区技术委员会**,文档定稿或重大修改均需报请技术委员会审议,通过后施行。 + + +### 1.2 术语定义 + +在本文描述中,软件包代码在仓库中的组织形式分为两种,分别为「source tree」和「rpm tree」。 + +- 「**source tree**」 是指项目源码本身,在 Gitee 中以源码协作开发的方式维护,并按照软件自身研发节奏发布对应的 release,供其他社区或者爱好者进行下载使用,本文档不涉及 source tree 的组织与管理; +- 「**rpm tree**」 是用于龙蜥社区构建发行版和 SIG 组构建产品对应的软件包的代码树,通常包含 rpm spec 文件 + 包含源码的 tarball,可用于后续构建和发布。这部分软件按照软件的成熟度分别放在对应不同的 Gitee 仓库。 + + +### 1.3 修订记录 + +| **时间** | **作者** | **描述** | **审核人** | +| --- | --- | --- | --- | +| 2022/08/31 | happy_orange, caspar | v1.0 - 初始版本 | | + + +## 2. 软件包管理基础设施介绍 + +龙蜥社区使用 Gitee 管理 Anolis OS 发行版及所有 SIG 组的软件包代码,统一放在 [https://gitee.com/openanolis]() 组织下。 + + +### 2.1 软件包在代码仓库的位置 + +此处展示使用较为频繁的几个软件包仓库的信息: + +| **类别** | **仓库名称** | **仓库地址** | **仓库介绍** | +| --- | --- | --- | --- | +| source tree | anolis | [https://gitee.com/anolis](https://gitee.com/anolis) | 存放龙蜥社区所有项目的 source tree,本文不涉及。 | +| rpm tree | src-anolis-os | [https://gitee.com/src-anolis-os](https://gitee.com/src-anolis-os) | 存放 Anolis OS 发行版默认 RPM 打包代码 | +| | src-anolis-sig | [https://gitee.com/src-anolis-sig](https://gitee.com/src-anolis-sig) | 存放龙蜥社区各 SIG RPM 打包代码 | +| | src-anolis-xxx | | 其他特殊 SIG 的 RPM 打包代码 | + + +### 2.2 代码分支管理 +对于 rpm tree,每个软件仓库会通过特定分支进行进一步统一管理,确保不同的 OS 版本都有对应分支可映射;在初始化仓库时,应该建立对应版本的分支,后续将代码提交 PR 通过 review 后合入到对应分支上。 + +| **分支名称** | **介绍** | +| --- | --- | +| **master**, **main** | 默认都为空,用于初始化其他发行版对应的分支,禁止使用该分支管理代码。 | +| **aX** (X 为不带任何后缀的大版本数字),如 a7, a8, a23, a25 等 | Anolis OS X 版本的主线分支。 | +| **aX.Y** (X 为大版本数字,Y 为对应小版本数字),如 a8.2, a8.6 等 | Anolis OS X 以前发布过的 Y 小版本对应的分支,最新的主线分支开出后,之前的主线分支则重命名为形如 aX.Y 的形式。 | +| **aX-{module名}-stream-{版本名称}** 或
**aX.Y-{module名}-stream-{版本名称}** | Anolis OS X 或者 X.Y 版本对应的 module 的模块化版本分支。 | +| | + + +## 3. 软件包引入 + +### 3.1 软件包引入原则 +软件包引入申请人主体可以是产品发布 SIG 组成员,也可以是其他 SIG 组对应软件包的 owner(以下统一称为「软件包负责人」)。当软件包引入前,软件包负责人需明确软件包引入原则,不符合软件包引入原则的组件,不允许引入。 + +**程度** | **要求项** | **补充说明** +--|--|-- +must | 软件包的内容必须遵守国家法律法规、遵守社会公序良俗 | +must | 软件包不允许存在合规、安全问题 | 例如:无 license 或 license 尚存在争议的软件 +must | 软件包能给出明确的 License,并且属于开源软件的白名单范围内 | [license 白名单](https://gitee.com/anolis/anolis-ci-test/blob/master/utils/license_check/white_black.xlsx) +must | 软件包在引入时,应该说明背景和用途 | +must | 软件包在龙蜥社区的 rpm tree 中从未引入过,且软件命名规范,遵循上游社区命名方式 | 同一款软件只能存在 rpm tree 的其中一个仓,如有,后加的仓库应当合并到先有的仓库。例如:src-anolis-os 和 src-anolis-sig 不可以包含同名软件包 +must | source.tag.gz 从上游开源社区下载正式发布版本 | md5 值和上游相同 | +must | 原则不允许引入其他二进制,所有的构建依赖和运行依赖都要满足已经存在 OS 的发行版内 | 针对孵化期内的软件,支持额外讨论决策 +must | 需要从源码构建,生成对应的 rpm | 如果对构建依赖版本多要求的(比如:依赖多个 kernel-devel 版本),则需要构建多次 +must | 软件包对应的上游开源社区仍在运营,并在持续发布 release | 运营是指有代码提交、代码和入、issue解决等,代表上游社区的代码仓并没有被废弃 +must | 软件包所需要的全部运行依赖必须已经存在,如果不存在,则必须按照同等规则先引入 | +must | 软件包 owner 需要评估软件的运行平台,缺省默认全部支持,如果存在不支持的架构则需要额外声明 | 目前龙蜥社区支持列表为:x86_64、aarch64 和 loongarch64 + + +### 3.2 版本选型原则 +龙蜥社区的软件选型主要围绕「分层分类」理论展开,根据软件包对操作系统发行版的重要程度,以及整体应用场景模块化程度,设立不同的选型原则。 + +- 优先级上参照「分层分类」理论中的分层思想,从层级优先级高的软件包开始重点制定和维护策略; +- 场景上参照「分层分类」理论中的分类思想,按应用场景维度分割,模块化地分批引入软件包; + +| **layer 序号** | **layer 名称** | **详细描述** | **选型规则** | +| --- | --- | --- | --- | +| layer-0 | 内核层 | 操作系统服务(内核) |
1. 跟随上游内核社区,选取社区成熟期 LTS 版本;
2. 选型需兼顾龙蜥社区 IHV 生态支持最完整的内核版本,降低硬件补丁回合成本;
3. 选型需兼顾龙蜥理事成员单位向上游贡献重大特性较多的内核版本,降低特性回合成本;
4. 在一个 OS 版本内不发生大版本变更,仅采取 release 更新形式,以补丁形式合入。
| +| layer-1 | 核心层 | 核心工具、核心库、核心服务 |
1. 跟随上游社区,选取社区稳定版本或 LTS 版本;
2. 和硬件相关的组件选型时需兼顾龙蜥社区 IHV 生态支持最完整的版本,降低硬件补丁回合成本;
3. 在一个 OS 版本内主要版本不发生大版本变更,仅采取 release 更新形式,以补丁形式合入;
4. 原则上不允许同时引入多个版本(例如 libssh, libssh2 需要选择一个版本)。
| +| layer-2 | 系统层 | 系统工具、系统库、系统服务 | | +| layer-3 | 应用层 | 应用工具、应用库、应用服务 |
1. 尽量选取正式发布的最新版本;
2. 允许在一个 OS 版本内发生 version 变更,但要对影响范围整体评估清楚,必须通过**兼容性验证**;
3. 原则上不允许同时引入多个版本。
4. 自研 module 包可以采用 module 模式进行管理,保障单一大版本演进过程的应用软件兼容性 | +| layer-4 | 应用场景 | 数据库、云原生、大数据、桌面等应用场景 |
1. 优先选择最新的,其次如果最新的版本带来不兼容(对其他软件产生影响)的问题,则可以选取次新版本;
2. 允许支持某款软件存在多个版本,但需要由需求驱动,比如:tomcat 7、tomcat 8、tomcat 9等。
3. 具体业务涉及的软件版本可以由对应负责人决策,不做过多要求。
| + + +### 3.3 SPEC 规范 +详细规范请参阅 [Anolis OS 23 spec 规范](/articles/305-module-and-checklist-of-spec.md)。
下列情形需严格遵守该规范编写 SPEC 文件: + +- Anolis OS 23 及以后的发行版引入的所有软件包; +- Anolis OS 7, Anolis OS 8 中新引入的软件包。 + +下列情形可参考本规范,并不强制执行: +- Anolis OS 7 和 Anolis OS 8 中现存的软件包。 + + +### 3.4 软件包引入流程 +1. **引入条件审查**:软件包负责人需根据 「[3.1 软件包引入原则](#wpzHw)」 和 「[3.2 版本选型原则](#S6HMX)」所载明的规范要求,明确软件包的代码符合要求、版本选型符合规范,同时明确软件包的演进和维护路线。 +1. **提交软件包引入申请**:软件包引入涉及发行版基线变更,基线数据是通过社区的软件包集成项目 ([ospkg-list](https://gitee.com/anolis/ospkg-list)) 管理的。每一个软件包都需要通过 Pull Request 提交引入意向申请。该申请将由产品发布 SIG maintainer 负责审阅,必要时报请技术委员会成员审阅,如通过后则完成软件包基线数据的变更。 + 1. 如果软件包在 ospkg-list 里不存在,则需要参照「[附录1.1 软件包引入申请模板](#XvmFu)」填写一个新的 `{package}.yaml`文件。申请通过后由产品发布 SIG maintainer 创建新的仓库和对应分支; + 1. 如果软件包在 ospkg-list 里已经存在,则无需提交新的 `{package}.yaml`,在现有软件包数据中添加新的字段即可。申请通过后由产品发布 SIG maintainer在现有仓库创建新的分支。 +1. **提交软件包代码**:根据 「[305 SPEC 规范](../articles/305-module-and-checklist-of-spec.md)」制作符合要求的 rpm tree 代码,并提交 PR 到对应的软件包分支中。 +1. **完成引入:** 产品发布 SIG maintainer,必要时社区技术委员会指派成员,根据 review 规范审核对应的 PR,如通过后则完成软件包整体引入。其他 review 结果包括:打回修改,拒绝等。如拒绝则需给出明确的理由。 + + +## 4. 软件包构建 +软件包构建从构建目的角度可以划分为两种:自验证构建和正式构建。 + + +### 4.1 自验证构建 +该过程为开发者自行在本地或模拟环境中对软件进行构建,以验证软件包本身的构建能力或产出测试包供后续测试。目前推荐的方式有两种: + +- 本地通过 mock 机制构建,参考《[Anolis OS 8 软件包本地构建 · 语雀](https://www.yuque.com/anolis-docs/kbase/cvy9g3?view=doc_embed)》一文; +- 通过 Anolis Build System (ABS) 提供的个人工作空间机制进行自定义构建,可以参考[《使用ABS平台轻松胜任Anolis OS开发工作》](../articles/208-how-to-build-package-via-ABS.md) 一文的内容,进行构建。 + + +### 4.2 正式构建 +正式构建的产物将会发布到对应的 Anolis OS 发行版本中,因此需要慎重操作。正式构建操作需要通过命令行操作,并且需要提前获得相关 token,请联系龙蜥社区发 +布小组 maintainer 获取相关 token。 + + +## 5. 软件包发布与变更 + + +### 5.1 软件包发布原则 +软件包发布指的是将通过正式构建流程生成的二进制推送到镜像 YUM repo 中。发布时会根据 ospkg-list 工具中的软件包基线数据信息,推送到对应的 YUM repo 中。
软件包经过正式构建得到 RPM 产物后,并不能直接发布,需要经过 abs 系统集成的 CI 流程后,由产品发布 SIG maintainer 执行软件包签名,之后推送 YUM repo 并生成发布单(Errata)。 + + +### 5.2 软件包成熟度变更原则 +软件包成熟度与 YUM 位置相关,基本原则如下: + +- Anolis OS 每个大版本初次选型,成熟度自动为「系统包」级别; +- Anolis OS 发布后各个时期的软件包选型,如无特殊说明,成熟度自动为「孵化包」级别。 + +如软件包符合下列条件,可以变更成熟度: + +- 符合软件包孵化成熟标准的软件包,可以从孵化池中结束孵化,变更为「成熟包」级别,条件如下: + - 软件包维护责任人清晰,维护计划清晰; + - 软件包代码质量稳定,通过所有 CI 检测条件,通过功能性测试,稳定维护至少一个软件包迭代周期; +- 符合软件包系统包标准的软件包,可以变更为「系统包」级别,条件如下: + - 软件包维护责任人清晰,维护计划清晰; + - 软件包代码质量稳定,通过所有 CI 检测条件,通过功能性测试,稳定维护至少一个软件包迭代周期; + - 软件包是 Anolis OS 必不可少的基础 OS 组件,或者是社区重点应用生态场景(场景列表见「[附录 1.2 社区重点应用生态场景](#Xwfk2)」; +- 符合软件包删除原则的软件包,需从 YUM 仓库删除,状态变更为「退休」级别。 + + +### 5.3 软件包发布流程 + +1. 经过正式构建得到候选发布(release candidate)包,abs 自动执行集成的 CI 流程,如未自动执行,则可在 abs 系统手工触发; +1. 通过所有 CI 测试项,或手工确认失败项无影响后,则达到发布条件。在 [Bugzilla 平台](https://bugzilla.openanolis.cn/enter_bug.cgi?classification=Anolis%20OS) 提交软件包发布需求,模板参考「[附录 1.3 软件包发布需求模板](#A5FyM)」;同时需填写发布单(Errata),如有多个软件包,则只需填写一个发布单即可,模板参考「附录 1.4 发布单模板」。 +1. 产品发布 SIG maintainer 确认通过发布需求,分别执行软件包签名、推送 YUM repo、生成发布单操作,如软件包达到系统包级别,则还需额外更新镜像 comps 文件。 + + +### 5.4 软件包成熟度变更流程 + +1. 在 ospkg-list 提交 PR,表明需要修改的目标成熟度,并在 commit 或 PR 描述中陈述软件包成熟度变更的必要性; +1. 产品发布 SIG maintainer 审核通过后,如涉及到仓库位置改动,需由产品发布 SIG maintainer 执行仓库位置改动操作。 + + +## 6. 软件包删除 +当某些开源软件不再符合 OpenAnolis 的规范时,需要将其从社区中删除。目前已知条件如下,持续更新: + +| **序号** | **分类** | **条件** | +| --- | --- | --- | +| 1 | 版权 | 软件的版权信息发生变更,如:license 存在争议、转商用等 | +| 2 | 代码风险 | 源码中存在恶意代码或者安全隐患,且无法修复或上游社区反馈不修复 | +| 3 | | 源码中存在争议风险,如:地区命名、民族习俗等 | +| 4 | 软件演进 | 开源社区不再进行维护,持续长时间无任何提交和 bug 讨论,考虑退出 | +| 5 | | 软件落后,存在其他同类软件替代 | +| 6 | | 功能不再被需要,可以无影响删除 | + + +## 附录1 + +### 附录 1.1 软件包引入申请模板 +```yaml +name: my_packages +repository: https://gitee.com/src-anolis-sig/my_package +summary: a short description +rpm_owner: anolis-bot +branches: +- name: a8 + repo: epao + maturity:rawhide +- name: a23 + repo: epao + maturity:rawhide +``` + +### 附录 1.2 社区重点生态应用场景 + +- 数据库 +- 云原生 +- Web Server + + +### 附录 1.3 软件包发布需求模板 + +> 申请发布 + + +### 附录 1.4 发布单模板 +> + + diff --git a/articles/305-add-project-to-anolis-os.md b/articles/305-add-project-to-anolis-os.md deleted file mode 100644 index c5a3ab8..0000000 --- a/articles/305-add-project-to-anolis-os.md +++ /dev/null @@ -1,193 +0,0 @@ -# 305 - 龙蜥社区软件包集成流程 - -> 本文档阅读对象适用于龙蜥社区软件开发人员。阅读者可以参照该文档完成新增软件包的集成流程。 - -## 1. Anolis OS 软件包仓库结构 -OpenAnolis 龙蜥社区的主要发行版产品,包括 Anolis OS 7, 8 以及 23,都有相似的 YUM 仓库结构,这有助于用户通过 YUM 等系统工具获取操作系统内更新时,拥有较为一致的体验。 -龙蜥社区欢迎广大开发者积极贡献软件包到 Anolis OS 中,集成过程需要遵循该软件包仓库结构。仓库结构大致示意图如下: - -![龙蜥软件包仓库](../images/305-anolis-yum-repo.png) - -从来源区分,软件包仓库包含系统包和 SIG 包,往下细分又有不同的子仓库。对于用户来说,每一个子仓库在系统中的表现不同:有的仓库预装在 Anolis OS 默认仓库列表中,有的没有预装;有的默认使能,执行 `yum install` 即可执行安装,有的默认不使能,需要添加 `--enablerepo` 参数;有的仓库里的软件包跟随 ISO 交付,有的不跟随。下面是一个简单的对照例子: - -仓库名|SIG 仓库|阶段|Gitee 仓库|ISO 包含|仓库默认状态 ---|--|--|--|--|-- -[BaseOS](http://mirrors.openanolis.cn/anolis/8/BaseOS/)|否|系统包阶段|[src-anolis-os](https://gitee.com/src-anolis-os)|是|✅ 预装
✅ 使能 -[AppStream](http://mirrors.openanolis.cn/anolis/8/AppStream/)|否|系统包阶段|[src-anolis-os](https://gitee.com/src-anolis-os)|是|✅ 预装
✅ 使能 -[PowerTools](http://mirrors.openanolis.cn/anolis/8/PowerTools/)|否|系统包阶段|[src-anolis-os](https://gitee.com/src-anolis-os)|否|✅ 预装
✅ 使能 -[Extras](http://mirrors.openanolis.cn/anolis/8/Extras/)|否|系统包阶段|[src-anolis-os](https://gitee.com/src-anolis-os)|否|✅ 预装
✅ 使能 -[Plus](http://mirrors.openanolis.cn/anolis/8/Plus/)|是|包成熟阶段|[src-anolis-sig](https://gitee.com/src-anolis-sig)|是|✅ 预装
❌ 使能 -[HighAvaliability](http://mirrors.openanolis.cn/anolis/8/HighAvailability/)|是|包成熟阶段|[src-anolis-sig](https://gitee.com/src-anolis-sig)|否|✅ 预装
❌ 使能 -[DDE](http://mirrors.openanolis.cn/anolis/8/DDE/)|是|包成熟阶段|[src-anolis-dde](https://gitee.com/src-anolis-dde)|否|✅ 预装
❌ 使能 -[Experimental](http://mirrors.openanolis.cn/anolis/8/Experimental/)|是|包孵化阶段|[src-anolis-sig](https://gitee.com/src-anolis-sig)|否|❌ 预装
❌ 使能 -[EPAO](https://mirrors.openanolis.cn/epao/)|是|包孵化阶段|[src-anolis-sig](https://gitee.com/src-anolis-sig)|否|❌ 预装
❌ 使能 - -## 2. 软件包集成的意义 -软件包默认集成到 Anolis OS 这个操作系统中,更多地意味着再分发(re-distribution)的便利。 - -- **对于软件包的拥有者来说**,经过软件包集成流程可以明确自己的**软件包在 Anolis OS 中的构建依赖和运行依赖分别是什么**,这意味着该软件包融入了 Anolis OS 发行版生态中,该软件的能力会作为操作系统整体能力的一部分持续存在。因此在发行版的持续演进升级过程中,必须要考虑到对该软件包的兼容性承诺;同样地,该软件包在演进升级过程中,也需要考虑对操作系统的前向兼容性。 -- **对于用户来说**,软件包集成到 Anolis OS 中,可以通过发行版提供的统一的操作界面(例如 YUM 源)来对软件进行统一管理,包括安装、更新、卸载等;甚至也有可能通过操作系统提供的统一配置接口(如 systemd 服务自启动)来以对用户透明的方式获得软件的服务。 -- **对于 Anolis OS 来说**,集成了某个软件包,意味着发行版获得了一个此前不具备的软件能力,以补充操作系统的软件栈大图。同样,发行版对该软件包自动负有维护和管理的能力,因此软件包的加入、变更、退休都需要正式的流程。 - -## 3. 我是否应该集成我的软件包到 Anolis OS 中? - -对于软件包的拥有者通用的一个指导原则是,是否集成软件包到 Anolis OS 中要看**自己是否需要 Anolis OS 提供的再分发的便利**,即: - -- 是否希望 Anolis OS 帮助解决二进制构建和运行的依赖,同时保持一定程度的兼容性; -- 是否希望通过 Anolis OS 提供的各类工具(YUM, systemd service 等)管理和使用自己的软件包; -- 更进一步,是否希望自己的软件成为 Anolis OS 发行版软件栈的一部分。 - -**如果以上的回答全部为“否”,那么可能没有必要集成自己的软件包到 Anolis OS 中**,你可以有其他的选择,比如: - -- **自己维护独立的第三方 repo**。这种方式会有更好的维护上的灵活性,但是可能会和 Anolis OS 有兼容性问题,比如安装的时候和现有 Anolis OS 里的软件冲突; -- **不提供二进制分发方式,只推荐用户从源码下载到 Anolis OS 后自己构建安装**。这种方式在兼容性上不会有大问题,但是操作起来非常复杂,而且不利于整体解决方案复制到其他场景。 - -反之,如果回答“是”,甚至如果回答中有“我不确定”的答案,我们都建议考虑将自己软件包集成到 Anolis OS 中,当然由于需要遵循集成流程,这个过程可能会比较复杂,但是基本上是一劳永逸的付出。 - -## 4. 我应该分发我的软件包到哪个 YUM 仓库中? - -如前文所述, Anolis OS 软件包仓库包含系统包和 SIG 包两大类。从 SIG 组产出的软件包,都应当首先集成为 SIG 包。 -- **SIG 包**: 参照其他成熟开源社区,集成为 SIG 包后会经历孵化期和成熟期,龙蜥社区技术委员会和发布 SIG (sig-distro) 会从技术侧进行软件包集成和孵化指导。对于操作系统特别重要的软件包,还可以进一步孵化为系统包。 -- **系统包**: 通常来源于操作系统主动选型,包含基础系统不可分割的组件(BaseOS)、应用生态的重要组件(AppStream)、devops 依赖工具(PowerTools)及额外仓库文件(extras)等。对于可以孵化为系统包的 SIG 包,社区也提供了指导流程,满足条件的 SIG 包可以根据流程升级为系统包。 - -作为集成的入口,针对 SIG 包,社区提供了多种不同的集成路径和孵化路径,大致如下: - -![集成步骤](../images/305-intergration-ways.png) - -从流程图可以看到,基于维护策略是否跟随系统更新走,会集成到不同的仓库。此处的“系统更新”特指发行版小版本的更新,如 Anolis OS 8.2 到 8.4 版本更新。 - -- 如果一个小版本更新了,为了专门适配此次升级,该软件包可能会迎来一次专门的升级和更新,那么应当集成到每一个小版本的 SIG 仓库中,例如 DDE(独立的 SIG repo), Experimental(非独立的 SIG repo); -- 如果小版本更新后,该软件包并不需要专门升级,只需要定期独立维护,则可以放到 EPAO 仓库中。 - -集成到每一个小版本的 SIG 仓库也有不同的形式,主要分为独立的 SIG 仓库和非独立的 SIG 仓库。Anolis OS 提供了两个非独立的 SIG 仓库,Experimental 和 Plus. 其中 Experimental 仓库可以接收相对更不稳定的 SIG 包,Plus 仓库只能接收生产级可用的稳定包。 - -上述提到的各种集成方式详细对比如下: - -视角↓ | YUM repo →
参考项↓| EPAO | 非独立 SIG repo
Experimental & Plus | 独立 SIG repo ----------|-----------|------|----------------------------------------|-------------- -对维护者 | 大版本更新后
需要采取的措施 | 跟随更新 | 跟随更新 | 跟随更新 -对维护者 | 小版本更新后
需要采取的措施 | 不主动跟随,不重新构建,除非依赖关系发生冲突 | 跟随更新 | 跟随更新 -对维护者 | 一次推送的软件包数量 | 无限制 | SRPM <= 30 个且单架构下 RPM <= 50 个 | SRPM > 30 个且单架构下 RPM > 50 个 -对用户 | ISO 是否包含该仓库 | 否 | Experimental 不包含; Plus 包含 | 不包含 -对用户 | 系统中是否预装
该仓库的 repo 文件 | 否 | Experimental 否; Plus 是 | 孵化期 repo 否
成熟期 repo 是 -对用户 | 用户是否需要
手动开启 repo | 否,添加 repo 文件自动开启 | 是 | 是 - -## 5. 我如何提交软件包集成申请? - -软件包集成申请都需要通过 SIG 来运作,如果想要引入的软件包未归属到已有 SIG 中,请浏览[社区 SIG 页面](https://openanolis.cn/sig)找到对应的 SIG 组加入;如果找不到对应的 SIG,也欢迎先联系社区(钉钉群“龙蜥OpenAnolis社区交流群”,群号 **33311793**)了解如何找到或创建一个 SIG。 - -当前软件包集成申请是通过社区的软件包集成项目 ([ospkg-list](https://gitee.com/anolis/ospkg-list)) 通过 Pull Request 提交集成意向申请。注意如果需要从 repo 中删除一个软件包,也可以使用该流程。 - -- 软件包新增申请 Pull Request 步骤 - -**步骤一**:增加计划集成包的 ${package}.yaml 文件,并完成按需填写; - -``` -name: packge-name // 软件名称,必填 -repository: https://gitee.com/src-anolis-xx/name // 软件对应的 gitee 地址,必填 -summary: xxxxxxxxxxxxxx // 软件的简单描述,选填 -rpm_owner: owner // 软件的 rpm 维护 onwer 的邮箱,如果存在多个 owner, 请用 “,” 隔开,默认缺省,自研软件和新增软件必填 -branches: // 软件的所有分支,每条分支下支持拓展,分支必填,分支下的扩展选填 -‒ name: a7 -- name: a8 // 分支名称 - repo: baseos // 该分支下对应的 repo 地址,可选项有:baseos、appstream、dde、plus、experimental 等 - maturity: system // 表示该软件在这个分支下的成熟程度,可选项有:system、stable、rawhide -- name: a23 -``` - -下面是 package.yaml 的样例: - -``` -name: libXtst -repository: https://gitee.com/src-anolis-os/libXtst -summary: IPMI (Intelligent Platform Management Interface) library and tools -rpm_owner: xiaolong@openanolis.org -branches: -- name: a7 - repo: os - maturity: system -- name: a8 - repo: appstream - maturity: system -- name: a23 - repo: baseos - maturity: system -``` - -**步骤二**:针对新增 package.yaml 提交 Pull Request,并在 commit log 提供如下信息; -``` -1. 软件包名: -2. 软件包分类(可以大致评估一下软件包属于系统中哪种分类,如 Development/Tools): -3. 软件包功能描述: -4. 我当前所属的 SIG 组: -5. 软件包是否在现有仓库中存在:(如果填写为是,则不可以集成) -6. 是否要额外引入一个或多个依赖包:否/是,清单如下: -7. 是否已经制作完成 SPEC 文件: -8. 是否有需要构建的源码包: -9. 是否有明确的更新策略,是否需要跟随系统小版本定期升级: -10. 是否特别重要的软件包,申请直接跳过孵化期转系统包(一般情况请不要选择此项): -11. 拟推送的 YUM repo 名称: Experimental, Plus, EPAO, 独立 repo -12. 其他可以帮助软件包集成的补充说明: -``` - -- 软件包退休申请 Pull Request 步骤 - -**步骤一**:删除计划集成包对应仓库下的 ${package}.yaml 文件; - -**步骤二**:针对删除 package.yaml 提交 Pull Request,并在 commit log 提供如下信息; - -``` -1. 软件包名: -2. 当前所属 RPM Tree 仓库路径: -3. 申请软件包退休的原因陈述: -``` - -在收到集成申请后,龙蜥发布 SIG 小组会进行评审互动,完成后将会创建对应的 RPM Tree 仓库,并告知后续构建软件包的方法,如果对于软件包集成过程不熟悉,龙蜥发布小组可以进行相关培训。培训的相关内容包含: -- 如何编写 RPM SPEC; -- 如何正确处理 RPM 依赖关系,以免破坏系统现有依赖关系; -- 如何保证系统兼容性,确保操作系统升级之后自己的软件包不受影响,或在升级软件包后不破坏系统本身的兼容性。 - -后续社区也会提供一系列教程和工具,提升整个过程的自动化程度,帮助 SIG 组的成员们在软件包集成和发布中顺利完成 landing。 - -## 6. 我的软件包如何孵化为成熟包及系统包? - -软件包随着成熟度增加,SIG 包可以孵化到成熟包及转为系统包。按照是否缺省安装,是否缺省使能等要素可以区分为层级 1 到层级 5,层级编号从小到大对产品/用户影响面依次递增。其中层级 1 属于包孵化阶段,层级 2 属于包成熟阶段,层级 3 到层级 5 会同时涉及到 Mirror 源和镜像的相应操作改变,属于系统包的不同阶段。 - -下表会介绍不同层级转入的条件。 - -层级|阶段|仓库名|集成收益 ---|--|--|-- -1|包孵化阶段|[Experimental](http://mirrors.openanolis.cn/anolis/8/Experimental/)
[EPAO](https://mirrors.openanolis.cn/epao/)
独立 SIG repo|❌ 包含在 anolis-repos
❌ 安装引导可配
❌ 安装引导默认勾选
❌ (仅系统服务)开机自动启动 -2|包成熟阶段|独立 SIG repo|✅ 包含在 anolis-repos
❌ YUM 仓库使能
❌ 包含在 ISO
❌ 安装引导可配
❌ 安装引导默认勾选
❌ (仅系统服务)开机自动启动 -3|包成熟阶段|[Plus](http://mirrors.openanolis.cn/anolis/8/Plus/)|✅ 包含在 anolis-repos
❌ YUM 仓库使能
✅ 包含在 ISO
❌ 安装引导可配
❌ 安装引导默认勾选
❌ (仅系统服务)开机自动启动 -4|系统包阶段|[BaseOS](http://mirrors.openanolis.cn/anolis/8/BaseOS/)
[AppStream](http://mirrors.openanolis.cn/anolis/8/AppStream/)|✅ 包含在 anolis-repos
✅ YUM 仓库使能
✅ 安装引导可配
✅ 安装引导默认勾选
❌ (仅系统服务)开机自动启动 -5|系统包阶段|[BaseOS](http://mirrors.openanolis.cn/anolis/8/BaseOS/)
[AppStream](http://mirrors.openanolis.cn/anolis/8/AppStream/)|✅ 包含在 anolis-repos
✅ YUM 仓库使能
✅ 安装引导可配
✅ 安装引导可配
✅ 安装引导默认勾选
✅ (仅系统服务)开机自动启动 - -在层级升级转入的过程中,需要提供如下材料: -- 提供典型场景测试自证明数据; -- 提供全面覆盖功能自证明测试; -- 提供兼容性相关测试自证明文档; - -其中层级 1 ~ 3 在准入过程中需要通过产品发布 SIG 需求评审 ( 包含研发、测试负责人 ) ;层级 4 ~ 5 除了产品发布 SIG 需求评审,还需要通过 TC 评审。 - -系统包分阶段集成和层级提升和降低均通过社区 [ospkg-list](https://gitee.com/anolis/ospkg-list) 进行录入和跟踪。当前我们使用 Issue 来跟踪和审核申请,后续社区可能会改为 Pull Request 模式提升审核的自动化能力。 - -- 软件包层级提升(成熟度增加)申请模版: - -``` -1. 软件包名 -2. 软件包产品线(Anolis OS 7 , Anolis OS 8 或者 Anolis OS 23) -3. 软件包当前层级 -4. 软件包期望提升到的层级 -5. 软件包层级准入要求对应的信息 -``` - -- 软件包层级降低(成熟度降低)申请模版: - -``` -1. 软件包名 -2. 软件包产品线(Anolis OS 7 , Anolis OS 8 或者 Anolis OS 23) -3. 软件包当前层级 -4. 软件包期望降低到的层级 -5. 软件包层级层级降低的原因 -``` diff --git a/articles/305-module-and-checklist-of-spec.md b/articles/305-module-and-checklist-of-spec.md new file mode 100644 index 0000000..f7edd51 --- /dev/null +++ b/articles/305-module-and-checklist-of-spec.md @@ -0,0 +1,279 @@ +# 305 SPEC 模版和 checklist + +## 1 背景 +本文档规定龙蜥社区 RPM Tree 组织规范和 SPEC File 写作规范。 +适用范围: + +1. Anolis OS 自研的软件包; +1. Anolis 23 及以后的发行版所有的软件包; + +总体原则: + +1. 简洁、可阅读、易维护 +1. 统一龙蜥标签 + +**修订记录** + +| **时间** | **版本** | **作者** | **备注** | +| --- | --- | --- | --- | +| 2022.2.10 | v1.0 | [@林生](https://gitee.com/forrest_ly) | 初始版本 | +| 2022.3.31 | v1.1 | [@伊和](https://gitee.com/yueeranna) | 添加规范 | +| 2022.4.20 | v1.2 | [@伊和](https://gitee.com/yueeranna) | 添加epoch版本号说明 | +| 2022.8.3 | v1.3 | [@橘悦](https://gitee.com/happy_orange)| 新增模版和 checklist | + +## 2 SPEC File 写作规范 +### 2.1 spec 基础模版 + +spec 基础模版,可以使用 rpmdev-newspec 命令生成。 +``` +%define anolis_release 1 +#Global macro/variable 定义 + +Name: package +Version: 1.0.0 +Release: %{anolis_release}{?dist} +Summary: Library providing xxx +License: LBPLv2+ and MIT +URL: https://github.com/package +Source0: https://github.com/package/archive/%{name}-%{version}.tar.gz +Source1: temple.conf + +Patch0: bugfix-xxx-yyy.patch + +BuildRequires: cmake +BuildRequires: gcc +BuildRequires: gcc-c++ + +Requires: glibc + +%description +Library providing xxx and xxx. + +%package devel +Summary: Development files for %{name} +Requires: %{name} = %{version}-%{release} + +%description devel +The %{name}-devel package contains development files for %{name}. + +%package -n python3-%{name} +%{?python_provide:%python_provide python3-%{name}} +Summary: Python 3 bindings for the %{name} library. +BuildRequires: python3-devel python3-setuptools +Requires: %{name} = %{version}-%{release} + +%description -n python3-%{name} +python 3 bindings for the %{name} library. + +%package doc +Summary: Documentation files for %{name} +Requires: %{name} = %{version}-%{release} +BuildArch: noarch + +%description doc +The %{name}-doc package contains documentation files for %{name}. + + +%prep +%autosetup -n %{name}-%{version} -p1 + + +%build +%configure +%make_build + + +%install +%make_install +# 下面两行按需添加,当前仅为样例 +mkdir -p %{buildroot}/%{prefix}/%{name} +install -m 0644 -p %{SOURCE1} %{buildroot}/%{prefix}/%{name} + + +%check +%make test + +%files +%license COPYTRING +%{_bindir}/%{name} +%{_libdir}/%{name}.so.* + +%files devel +%doc example +%{_libdir}/%{name}.so +%{_libdir}/pkgconfig/%{name}.pc +%{_includedir}/%{name}/ + +%files -n python3-%{name} +%{python3_sitearch}/%{name}/ + +%files doc +%doc README.md AUTHORS ChangLog NEWS TODO + + +%changelog +- Wed Aug 03 2022 happy_orange - 1.0.0-1 +- Init package from upstream +``` + +### 2.2 纯 python 类 spec 模版 + +由于 python 类软件比较多,额外对外提供 python 类软件的 spec 模版。 +``` +%define anolis_release 1 +%global pname package_real_name +%global debug_package %{nil} + +Name: python-%{pname} +Version: 0.1.0 +Release: %{anolis_release}%{?dist} +Summary: xxxx package for Python + +License: MIT +URL: https://sourceforge.net/projects/%{pname} +Source0: https://sourceforge.net/%{pname}/code/%{pname}-%{version}.tar.gz + +# python package only need to build in noarch. +BuildArch: noarch + +%description +xxxx package for Python + +%package -n python3-%{pname} +Summary: YAML 1.2 loader/dumper package for Python +BuildRequires: python3-devel +BuildRequires: python3-setuptools +# For tests +BuildRequires: python3-pytest +Requires: python3-setuptools +%{?python_provide:%python_provide python3-%{pypi_name}} + +%description -n python3-%{pname} +xxxx package for Python + +%package -n python3-%{pname}-doc +Summary: doc files for python3-%{pname} +Requires: python3-%{pname} = %{verison}-%{release} + +%description -n python3-%{pname}-doc +doc files for python3-%{pname} + + +%prep +%autosetup -n %{pname}-code-%{commit} -p1 +rm -rf %{pypi_name}.egg-info + + +%build +%py3_build + + +%install +%py3_install + + +%check +%pytest + + +%files -n python3-%{pname} +%license LICENSE +%{python3_sitelib}/%{pypi_name} +%{python3_sitelib}/%{pypi_name}-%{version}-*.pth +%{python3_sitelib}/%{pypi_name}-%{version}-*.egg-info + +%files -n python3-%{pname}-doc +%doc README.rst + +%changelog +* Mon Jul 25 2022 happy_orange - 0.1.0-1 +- Init pacakge from upstream +``` +## 3 字段介绍和标准 + +| **spec header** | 增加 anolis_release 的定义,从 1 开始递增 | | +| --- | --- | --- | +| **字段** | **定义** | **是否可选** | +| Name | 包的基本名称,应与 SPEC 文件名匹配。 | 必选 | +| Epoch | 超级版本号。
1. 用于修正版本号,比如:0.20.0 版本 更改成 21.1
2. 初始化时, epoch 为 1,每次修正版本可以递增
3. 不可过度使用
4. 其他引用 %{version}-%{relase} 需要修改成:%{epoch}:%{version}-%{relase} |可选| +| Version | 软件的上游版本号。正式发布的 release/tag 版本号。 |必选| +| Release | 此版本软件的发布次数。
1. 初始值为: %{anolis_release}%{?dist}
2. 每次构建递增 anolis_release
3. 构建软件新version 时 anolis_release 需要重置为 1 |必选| +| Summary | 一个简短的、单行的软件包摘要。 |必选| +| License | 被打包的软件的许可证,所有许可证的并集。 |必选| +| URL | 该软件的上游项目网站。 |必选| +| Source0 | 上游源代码压缩存档的路径。这应该指向存档的可访问且可靠的存储,例如,上游页面而不是打包程序的本地存储。
如果需要,可以添加更多 SourceX 指令,每次递增编号,例如:Source1、Source2、Source3 等。 |必选| +| Patch0 | 对源码进行的修改以补丁的形式。
1. 可以添加更多 PatchX 指令,每次递增编号,例如:Patch1、Patch2、Patch3 等。
2. 自研补丁序号从100、1000、10000等编号开始。 |可选| +| BuildArch | 声明该软件的构建体系结构。
1. koji 构建时默认为:x86_64 和 aarch64
2. 本地构建时会自动继承构建它的机器的体系结构
3. 如果不依赖体系结构,可以声明:BuildArch: noarch
4. 如果仅涉及一个架构,则需要将对应的架构声明:BuildArch:x86_64 或 BuildArch:aarch64 |可选| +| ExcludeArch | 声明该软件不需要的架构体系。
1. 默认不需要
2. 指定不进行编译的架构,举例:ExcludeArch: x86_64 | 可选 | +| BuildRequires | 声明该软件构建所需要的全部软件包列表。
1. 有多个条目 BuildRequires每个条目在 SPEC 文件中各占一行
2. 每个条目内不同软件使用空格隔开
3. 直接声明依赖软件的 package name,不要包含: %{_isa}、/usr/bin/xx、pkg-config(xx)、/usr/lib64/xx.so 等 |必选| +| **spec body** | | +| **字段** | **定义** | **是否可选** | +| %description | RPM 中打包的软件的完整描述。该描述可以跨越多行并且可以分成段落。 | 必选 | +| %prep | 用于准备软件包构建所需要的源码。
1. 路径信息:将 tar.gz 从 ~/rpmbuild/SOURCES/ 目录下解压到 ~/rpmbuild/BUILD/ 下
2. 建议使用 %autosetup -n %{name}-%{version} -p1,可以自动按照补丁定义顺序将补丁以 -p1 形式打入
3. 允许在此处拷贝 source 文件,例如:cp %{SOURCE1} ./
4. 也允许去执行一些 shell 脚本 | 必选 | +| %build | 将软件包的源码进行编译阶段。
1. 路径信息:在 ~/rpmbuild/BUILD/%{name}-%{version}/ 下
2. 执行构建可以根据源码语言去选择构建方式
3. 在构建过程中是个 chroot 环境,不允许联网 download 等动作
4. 不允许随意修改 flags 等 | 必选 | +| %install | 将软件包编译生成的文件复制到安装目录,进行预安装动作,即模拟所有 package 安装后的环境。
1. 路径信息: ~/rpmbuild/BUILDROOT/%{name}-%{version}/
2. 复制时,文件从 ~/rpmbuild/BUILD/%{name}-%{version}/ 拷贝到 ~/rpmbuild/BUILDROOT/%{name}-%{version}/
3. 复制文件时,需要保留文件的时间戳,采用 cp -p 或 install -p
4. 操作允许直接将 SourceX 文件拷贝到安装目录允许再该阶段直接生成新文件 | 必选 | +| %check | 用于测试软件的命令或一系列命令。
这通常包括诸如单元测试之类的东西,阶段能开则开,如果不能开启,声明不能开启的原因。 | 可选 | +| %files | 定义每个 package 的文件列表,并生成对应的 .rpm。
1. 文件布局必须布局遵循 [**FHS**](https://yuque.antfin-inc.com/bobac/pm1qpi/xz4m02) ,个别情况额外声明
2. 正确设置文件权限:目录 0755,文件 0644 root root,除非出于安全考虑需要使用特定的用户或组
3. 所有安装在 ~/rpmbuild/BUILDROOT/%{name}-%{version}/ 下的文件必须全部有唯一归属
4. 不允许包含具有攻击性、歧义性、宗教性、色情性、受限制使用的文件等
5. %doc 后面可以直接跟 ~/rpmbuild/BUILD/%{name}-%{version}/ 下的说明类文档:README、ChangeLog等
6. %license 后面可以跟~/rpmbuild/BUILD/%{name}-%{version}/ 下的版权文件:copying、license 等 | 必选 | +| %changelog | Version不同或Release构建之间的包发生的更改的记录。
1. changlog 的格式正确,包括:日期、提交者个人信息、版本信息、描述信息等
2. Message 简洁易懂,清晰明确 | 必选 | + + +## 4 check list +下面给出 spec 的 check list,可以自行检查。 + +| **分类** | **要求程度** | **详细内容** | **自检结果** | +| --- | --- | --- | --- | +| 宏定义 | must | 使用宏变量代替硬编码的目录名称 | | +| | must | 不要出现其他 OS 发行版的宏,比如:fedora、rhel、openEuler、opensuse等 | | +| | should | spec 中的宏统一使用同一种格式 | | +| | should | 可以采用 %bcond_with 和 %bcond_without 来作为开关控制特性状态 | | +| | should | 宏定义时,%global 优先于 %define | | +| | should | 不使用 %if 0 作为判断条件 | | +| | should | 关于 python/perl/rust/golang/ruby/meson 等宏的使用,遵循各个 rpm-macros 包中的定义。 | | +| 基本信息 | must | spec 的第一行增加 anolis_release 的定义:%define anolis_release 1 | | +| | must | Name 和 package 命名匹配 | | +| | must | Epoch 用来修正版本号,但不得过度使用 | | +| | must | Version 为正式发布的 release/tag 版本,如果采用 rc 版本,则 version 需要定义为 xx~rc1,post 版本需要定义为:xx-post1 | | +| | must | Release 使用 %{anolis_release}%{?dist} | | +| | must | License 与实际的许可证相匹配,不存在缺失或者扩大 | | +| | should | 如果存在多个 license,建议给多个 license 加以注释 | | +| | must | Url 真实可用,属于上游源真实地址 | | +| | must | Source0 地址支持 curl 或者 wget 方式进行直接下载 | | +| | must | Source0 对应的源码包的 md5 值与开源社区的 md5 值相同 | | +| 依赖阶段 | must | Buildrequires 中声明所有的第一层构建依赖,且全部使用 package name 进行声明,并要求构建依赖以 rpm 形式存在相同 OS 版本构建源里,不允许其他方式引入 | | +| | must | Requires 中声明所有的第一层运行依赖,且全部使用 package name 进行声明,并要求运行依赖以 rpm 形式存在相同 OS 版本的 yum 源里,不允许其他方式引入 (禁止安装后执行 pip install 等动作) | | +| | must | 声明 devel 或其他子包对于主包或者 libs 的依赖时,需要增加版本限制: Requires: %{name} = %{epoch}:%{verison}-%{release} | | +| | should | 如果对依赖软件的版本有限制,则限制到软件包的 %{version} 即可,不需限制到 %{release} ,例:BuildRequires: glibc >= 2.32 || +| | should | 声明 buildrequires 和 requires 时,不要添加 %{?_isa} | | +| | should | 使用 provides 对外提供功能时,和以前版本保持一致,以前有加版本号,则需要一直加下去,如果以前没有加版本号,则不要增加 | | +| | should | 使用 obsoletes 时,需要增加对应版本号 | | +| 补丁文件 | must | Patch 和 Souce 采用序号区分自研和开源三方 | | +| | should | Patch 序号连续,并保持一种规范 | | +| | should | 每个 patch 或 source 内有详细的修改原因和原链接地址 | | +| 架构阶段 | must | 至少在一种架构上构建成功,如果存在仅在部分架构上构建,可以使用 BuildArch(白名单)或者 ExcludeArch(黑名单) | | +| | must | Anolis OS 23 不支持 32 位,不支持multilib,请去除对其他架构的支持 | | +| | should | 合理使用 ifarch 和 ifnarch 的区别 | | +| 子包划分 | must | 有定义 doc 子包,且定义正确(依赖、架构) | | +| | must | python 类软件仅生成 python3 子包,不再支持 python2 | | +| 准备阶段 | should | 在 %prep 阶段采用 %autospec 进行自动解压并打补丁,保持 spec 简洁,如果存在与架构相关的补丁时,可以不采用 %autospec | | +| 构建阶段|must|在 %build 阶段不允许随意修改 flags,如果要修改,需要在 spec 中备注原因|| +| | must | 在 %build 阶段不允许随意禁用 PIE | | +| | should | 在 %build 阶段尽量采用多线程进行编译,提高构建速度 | | +| | should | 在 %build 和 %install 阶段使用宏变量要保持一致,比如:%{buildroot} 或者 $RPM_BUILD_ROOT | | +| 安装阶段 | must | 在 %install 阶段复制文件时,需要保留文件的时间戳,比如 cp -p 或者 install -p | | +| 脚本阶段| must | 如果存在动态库,则必须在 %post 和 %postun 阶段调用 %ldconfig | || +| | must | 如果存在 service 文件时,需要在 %post、%postun 和 %preun 中对服务作出对应动作(比如:%post %systemd_post xxx.service) | | +| 打包阶段 | must | %files 阶段文件系统布局遵循 FHS,个别特殊情况可以加以说明 | | +| | must | %files 阶段正确设置文件权限:目录 0755,文件 0644 root root,除非出于安全考虑需要使用特定的用户或组 | | +| | must | %files 里不允许有重复文件,一个文件仅能有一个归属 | | +| | must | %files 里不允许包含具有攻击性、歧义性、宗教性、色情性、受限制使用的文件等 | | +| | must | %doc 存放 readme、changelog、authors 等文件 | | +| | must | %license 存放 copying、license 等许可证文件 | | +| | must | 可以使用 %find_lang 宏来处理 locale 文件,进行自动打包相关文件。例:%find_lang %{name} | | +| | must | 如果存在需要保存配置的 conf 文件,需要声明 %config(noreplace) xxx.conf | | +| | must | 如果存在头文件,则需要放置在 devel 包内 | | +| | must | 如果有动态库,则包含后缀的库文件放在主包或者 libs 包,不包含后缀的库文件要放在 devel 中 | | +| | must | 如果有静态库,则静态库需要放置在 static 包内 | | +| | must | 公共的目录名称不可以通过 %dir 被重复定义,比如:%{_libdir}、%{_bindir} | | +| changlog 阶段 | must | changlog 的格式正确,包括:日期、提交者个人信息、版本信息、描述信息等 | | +| | should | changlog 的描述信息简洁易懂 | | + diff --git a/articles/306-instruction-manual-of-spec.md b/articles/306-instruction-manual-of-spec.md new file mode 100644 index 0000000..94ecda8 --- /dev/null +++ b/articles/306-instruction-manual-of-spec.md @@ -0,0 +1,1235 @@ +# 306 SPEC 指导手册 +该文章介绍了 spec 编写和构建过程中的细节和技术原理,相当于指导手册。 +## 1 spec 宏 +在软件包构建的过程中,为了防止过多使用硬编码路径和常用路径引用问题,增加了增加了“宏”的概念。将一些常用路径或者变量值通过宏变量定义出来,并可以在整个 Anolis OS 发行版中使用。 +spec 宏目前来源于三种:系统宏、各种编程语言的宏和自定义宏 +### 1.1 系统宏 +当前社区里已经提供了一些系统宏文件,用于在软件包编译过程中使用,包括:系统路径、系统文件、编译选项、编译方式等,下面以 Anolis OS 23 的环境举例。 +目前由 rpm 软件对外提供了 spec 定义和编译过程中的宏 :`/usr/lib/rpm/macros`: + +``` +// 宏文件路径 +[root@localhost ~]$ ls -l /usr/lib/rpm/macros +-rw-r--r--. 1 root root 42715 3月 23 11:05 /usr/lib/rpm/macros + +// 查询宏文件所属的 rpm +[root@localhost ~]$ rpm -qf /usr/lib/rpm/macros +rpm-4.17.0-4.an23.x86_64 + +// 查看宏文件里定义的宏 +[root@localhost ~]$ cat /usr/lib/rpm/macros +%_sourcedir %{_topdir}/SOURCES +%buildroot %{_buildrootdir}/%{NAME}-%{VERSION}-%{RELEASE}.%{_arch} +%_prefix /usr +%_exec_prefix %{_prefix} +%_bindir %{_exec_prefix}/bin +%_sbindir %{_exec_prefix}/sbin +%_libexecdir %{_exec_prefix}/libexec +%_datadir %{_prefix}/share +%_sysconfdir /etc +%_sharedstatedir %{_prefix}/com +%_localstatedir %{_prefix}/var +%_lib lib +%_libdir %{_exec_prefix}/%{_lib} +%_includedir %{_prefix}/include +%_infodir %{_datadir}/info +%_mandir %{_datadir}/man +....... +``` + +编译选项相关的基础宏: +``` +// 宏文件路径 +[root@localhost ~]$ ls -l /usr/lib/rpm/anolis/macros +-rw-r--r-- 1 root root 16707 Mar 16 05:14 /usr/lib/rpm/anolis/macros + +// 查询宏文件所属的 rpm +[root@localhost ~]$ rpm -qf /usr/lib/rpm/anolis/macros +system-rpm-config-23-4.an23.noarch + +// 查看宏文件里定义的宏 +[root@localhost ~]$ cat /usr/lib/rpm/anolis/macros + GCC toolchain +%__cc_gcc gcc +%__cxx_gcc g++ +%__cpp_gcc gcc -E + +# Clang toolchain +%__cc_clang clang +%__cxx_clang clang++ +%__cpp_clang clang-cpp +....... +``` + +### 1.2 各种编程语言宏 +社区里除了系统软件,对外还提供了很多编程语言的软件,包括不限于:python、perl、go、rust、java 等。 +``` +// 查询 yum 源里提供的 macros rpm +[root@localhost root]$ yum list | grep macros | grep an23 +cmake-rpm-macros.noarch 3.22.3-1.an23 @BaseOS +efi-srpm-macros.noarch 5-1.an23 @BaseOS +go-srpm-macros.noarch 3.0.15-1.an23 @BaseOS +perl-srpm-macros.noarch 23-1.an23 @BaseOS +pyproject-rpm-macros.noarch 1.0.0-1.an23 @BaseOS +python-rpm-macros.noarch 3.10-23.1.an23 @BaseOS +python-srpm-macros.noarch 3.10-23.1.an23 @BaseOS +python3-rpm-macros.noarch 3.10-23.1.an23 @BaseOS +rust-srpm-macros.noarch 23-1.an23 @BaseOS +systemd-rpm-macros.noarch 250.4-1.an23 @BaseOS +fonts-rpm-macros.noarch 1:4.0.2-1.an23 BaseOS +fonts-rpm-macros.noarch 1:4.0.2-1.an23 koji +fonts-srpm-macros.noarch 1:4.0.2-1.an23 BaseOS +fonts-srpm-macros.noarch 1:4.0.2-1.an23 koji +go-rpm-macros.x86_64 3.0.15-1.an23 AppStream +go-rpm-macros.x86_64 3.0.15-1.an23 koji +perl-macros.noarch 4:5.34.0-6.an23 BaseOS +perl-macros.noarch 4:5.34.0-6.an23 koji +python-qt5-rpm-macros.noarch 5.15.6-1.an23 koji +qt5-rpm-macros.noarch 5.15.5-1.an23 koji +qt5-srpm-macros.noarch 5.15.5-1.an23 koji +texlive-bbm-macros.noarch 9:svn17224.0-1.an23 koji +texlive-bbm-macros-doc.noarch 9:svn17224.0-1.an23 koji +texlive-chemmacros.noarch 9:svn56983-1.an23 koji +texlive-chemmacros-doc.noarch 9:svn56983-1.an23 koji +texlive-circuit-macros.noarch 9:svn57308-1.an23 koji +texlive-ling-macros.noarch 9:svn42268-1.an23 koji +texlive-macros2e-doc.noarch 9:svn46026-1.an23 koji +texlive-macroswap.noarch 9:svn31498.1.1-1.an23 koji +texlive-macroswap-doc.noarch 9:svn31498.1.1-1.an23 koji +``` + +### 1.3 自定义宏 +允许在 spec 的头部自定义宏变量,通过 `**%global 宏变量名称 宏变量值** `格式定义宏,在下文通过 `**%{宏变量名称}**`引用,样例: + +``` +// % %global 宏变量名称 宏变量值 +%global pname ruamel-yaml + +// 使用宏变量 +Name: python-%{pname} + +// 拓展: +spec 中允许对基本信息字段进行宏拓展使用,比如上述第 5 行的 Name 字段,可以采用 %{name} 进行引用 +``` + +### 1.4 查询已有宏 + +- 通过 `rpm -E %{xxx}`来查询 xxx 宏的实际内容 + ``` + # 查询 %{_bindir} 的值 + [root@localhost ~]$ rpm -E %{_bindir} + /usr/bin + ``` + +- %bcond_with 和 %bcond_without 介绍 + + 通常在开启或者关闭某个特性时,可以使用这两个变量。使用方式为: + ``` + // 定义 + %bcond_without testsuite + + // 使用 + %check + %if %{with testsuite} + ..... + %endif + ``` + + - %bcond_without xx :是指不需要去除 xx 特性,即等价于开启 xx 特性,在定义该宏之后,会在内部自动生成一个全局变量 with_xx ,并对 with_xx 进行赋值 1,即 with_xx =1。这样在使用该变量时,通过with 方法读取 with_xx 变量的值,从而生成判断结果。 + - %bcond_with xx:即关闭 xx 特性,其生成的全局变量 with_xx = 0,从而 with 的判断为假。 + +## 2 rpmbuild 详解 +### 2.1 代码准备 +在准备将某款软件进行构建时,需要准备的文件至少为:spec + source.tar.gz,spec文件用于指导生成rpm,source.tar.gz 为源码,有时额外还会存在一些其他 source 文件或者 patch 文件。 +下面以 lld 软件作为样例讲解: +``` +[root@localhost ~]$ ll ~/rpmbuild/SOURCES/ +total 1492 +-rw-r--r-- 1 root root 1900 Aug 4 02:10 0001-PATCH-lld-CMake-Check-for-gtest-headers-even-if-lit..patch +-rw-r--r-- 1 root root 20288 Aug 4 02:10 0002-PATCH-lld-Import-compact_unwind_encoding.h-from-libu.patch +-rw-r--r-- 1 root root 699 Aug 4 02:10 lit.lld-test.cfg.py +-rw-r--r-- 1 root root 1473868 Aug 4 02:10 lld-13.0.1.src.tar.xz +-rw-r--r-- 1 root root 566 Aug 4 02:10 lld-13.0.1.src.tar.xz.sig +-rw-r--r-- 1 root root 6129 Aug 4 02:10 lld.spec +-rw-r--r-- 1 root root 488 Aug 4 02:10 README.md +-rw-r--r-- 1 root root 1362 Aug 4 02:10 run-lit-tests +-rw-r--r-- 1 root root 2222 Aug 4 02:10 tstellar-gpg-key.asc +``` +### 2.2 %prep 解压 +#### 2.2.1 文件准备 +%prep 阶段的作用就是准备构建所需要的文件,这里一般包括: + - 解压 source.tag.gz + - 打补丁动作 + - 拷贝 source 文件、或者脚本等 +#### 2.2.2 解压方式 +解压是针对 Souce0 定义的 source.tar.gz 进行解压,解压有两种方式:%setup 和 %autosetup,这两种方式的详细介绍如下,可以根据自己软件需要进行选择使用: + + - `%setup -q -n %{name}-%{version}` + - -q :代表采用安静模式进行解压 + - -n :声明解压之后的源码目录名称,一般为 %{name}-%{version},但是可以手动对 source.tag.gz 进行解压后查看具体目录名称 + - 当存在补丁时,需要将每个补丁进行声明打入,并可以针对不同的补丁指定不同的忽略目录,且可以自己指定补丁打入的顺序 + - 在存在与架构相关的补丁时,比较方便,指定对应的架构才将补丁打入用 + + ``` + ...... + Patch0: xxxxxx.patch // 一层路径 + Patch1: xxxxxx.patch // 两层路径 + Patch2: xxxxxx.patch // 一层路径 + Patch3: xxxxxx.patch // 零层路径 + Patch4: x86_64-xxx.patch // 仅 x86_64 架构打入,一层路径 + ...... + + %prep + %setup -q -n %{name}-%{version} + %patch0 -p1 + %patch1 -p2 + %patch3 -p0 + %patch2 -p1 + %ifarch x86_64 + %patch4 -p1 + %endif + ``` + + - `%autosetup -n %{name}-%{version} -p1` + - -n : 同上 + - -p1:按照 spec 内 Patch 的定义顺序全部打入,并且全部采用 -p1 忽略一层目录的结构打入补丁。 + - 当存在与架构相关的补丁时,不再使用 + - 代码举例: + + ``` + ..... + Patch0: xxxxxx.patch // 一层路径 + Patch1: xxxxxx.patch // 一层路径 + ...... + + %prep + %autosetup -q -n %{name}-%{version} -p1 + ``` + + - 此处也可以执行其他动作,比如解压其他 source.tar.gz 、拷贝其他 source 文件等 + + ``` + %prep + %autosetup -q -n %{name}-%{version} -p1 + tar -xvf %{SOURCE1} ./ + ``` + +#### 2.2.3 路径信息 + +- 本地编译时,源码一般存放在 `~/rpmbuild/SOURCES/`下, spec 文件在 `~/rpmbuild/SOURCES/` 或者 `~/rpmbuild/SPECS/`下都可,但是 koji 编译时,spec 文件在`~/rpmbuild/SPECS/`下 +- 在执行完 %prep 后,源码将被解压到 `~/rpmbuild/BUILD` 下 +- 通常可以使用 `rpmbuild -bp ~/rpmbuild/SOURCES/xxx.spec --nodeps`来查看解压结果,并且可以在日志中看到 patch 是有被打入的。 + + ``` + [root@localhost ~]$ rpmbuild -bp ~/rpmbuild/SOURCES/lld.spec --nodeps + Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.cJtqDn + + umask 022 + + cd /root/rpmbuild/BUILD + + /usr/lib/rpm/redhat/gpgverify --keyring=/root/rpmbuild/SOURCES/tstellar-gpg-key.asc --signature=/root/rpmbuild/SOURCES/lld-13.0.1.src.tar.xz.sig --data=/root/rpmbuild/SOURCES/lld-13.0.1.src.tar.xz + gpgv: Signature made Wed Feb 2 09:58:19 2022 EST + gpgv: using RSA key 474E22316ABF4785A88C6E8EA2C794A986419D8A + gpgv: Good signature from "Tom Stellard " + + cd /root/rpmbuild/BUILD + + rm -rf lld-13.0.1.src + + /usr/bin/xz -dc /root/rpmbuild/SOURCES/lld-13.0.1.src.tar.xz + + /usr/bin/tar -xof - + + STATUS=0 + + '[' 0 -ne 0 ']' + + cd lld-13.0.1.src + + /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w . + + /usr/bin/cat /root/rpmbuild/SOURCES/0001-PATCH-lld-CMake-Check-for-gtest-headers-even-if-lit..patch + + /usr/bin/patch -p2 -s --fuzz=0 --no-backup-if-mismatch + + /usr/bin/cat /root/rpmbuild/SOURCES/0002-PATCH-lld-Import-compact_unwind_encoding.h-from-libu.patch + + /usr/bin/patch -p2 -s --fuzz=0 --no-backup-if-mismatch + + exit 0 + [root@localhost SOURCES]$ ll ~/rpmbuild/BUILD/ + total 8 + drwxr-xr-x 16 root root 4096 Aug 4 02:44 lld-13.0.1.src + ``` + +### 2.3 %build 构建 +#### 2.3.1 编译器 +软件包使用的编译器由软件源码的语言来决定,现在比较流行的编译器种类比较多:gcc、clang 等。但是,软件包应该默认使用 gcc 作为编译器(对于 gcc 支持的所有语言),如果上游不支持使用 gcc 构建,则应该使用clang。 +但是,如果有很好的技术原因,打包者可以选择不使用默认编译器。不使用默认编译器的有效技术原因示例包括但不限于: + +- 默认编译器无法正确构建包。 +- 打包者需要禁用编译器功能(例如 LTO),以便默认编译器正确编译他们的包。 +- 默认编译器需要更长的时间来构建包。 +- 默认编译器缺少一个对包有利于的特性。 +- 选择使用非默认编译器的打包人员应在spec文件的注释中记录做出这一决定的原因。 + +#### 2.3.2 编译 Flags + +用于构建包的编译器必须遵守系统 rpm 默认配置中设置的编译器 Flags。 +对于 C、C++ 和 Fortran 代码, %optflags 宏包含这些 flags。不鼓励为了性能优化而覆盖这些 flags(例如,-O3 代替 -O2)。如果可以提供 benchmark 结果,显示这段代码有明显的性能优化,那可以根据具体情况重新审视。如果有充分的理由,可以添加、重写或过滤这些flags;这样做的理由必须记录在 spec 文件中。 +通常允许使用某些与安全相关的flags,这些flags可能会略微降低性能,但是相对某些程序增加的安全性收益来说是值得的。 +默认情况下,启用PIE,如果要在spec中禁用,可以通过: +``` +%undefine _hardened_build +``` +但是以下场景不允许禁用PIE: + +- 软件包长期运行,意味着它很可能会启动并继续运行,直到机器重新启动,而不是按需启动,并在空闲时退出。 +- 软件包具有 suid 二进制文件或具有功能的二进制文件。 +- 软件包以 root 身份运行。 + +#### 2.3.3 并行构建 + +在编译过程中,可以指定多线程来加快构建速度。一般是在 make 后增加 -jxx,来表示用 xx 线程编译,同时可以直接使用 `%make_build` 进行编译,默认增加多线程。 +``` +[root@localhost SOURCES]$ rpm -E %{make_build} +/usr/bin/make -j96 +``` +其他语言也都有对应的编译方式,比如 %cmake_build,%meson_build、%cargo_build 等,这些宏都自带了多线程。 + +``` +[root@localhost ~]$ rpm -E %{cmake_build} +/usr/bin/cmake --build "anolis-linux-build" -j96 --verbose + +[root@localhost ~]$ rpm -E %{cargo_build} +/usr/bin/env CARGO_HOME=.cargo RUSTC_BOOTSTRAP=1 RUSTFLAGS='-Copt-level=3 -Cdebuginfo=2 -Ccodegen-units=1 -Clink-arg=-Wl,-z,relro -Clink-arg=-Wl,-z,now --cap-lints=warn' /usr/bin/cargo build -j96 -Z avoid-dev-deps --release +``` + +#### 2.3.4 构建目录 +在构建阶段,所有的构建动作都在路径:`~/rpmbuild/BUILD/%{name}-%{version}/`下,并且编译生成的文件也只允许生成在当前目录下,不允许对构建环境的系统目录进行操作,比如:`/tmp`、` /home`。 + +#### 2.3.5 网络访问 +构建系统中的软件包是在一个模拟的 chroot 中构建的,无法访问互联网。包不能依赖或使用不是它们自己创建的任何网络资源。 + +### 2.4 %install 预安装 +在完成构建后,需要对构建结果进行一个安装,这里准备了一个预安装的环境,用于模拟安装环境。 + +#### 2.4.1 安装路径 +预安装的绝对路径为:`~/rpmbuild/BUILDROOT/%{name}-%{version}/`,一般可以使用 `%{buildroot} `进行引用。 + +#### 2.4.2 怎么安装 +一般该阶段执行的动作都是将文件从**构建目录**拷贝到**安装目录**,举例: +``` +install: + @echo "BEGIN INSTALL xxx" + mkdir -p $(BINDIR) + +install -m 0755 $(PKGPATH)/xxx $(BINDIR) +``` +#### 2.4.3 其他动作汇总 +``` +%install +%make_install + +// 创建路径 +mkdir -p %{buildroot}/%{_datadir}/%{name} + +// 复制 source 文件到安装目录 +install -p -m 0644 %{SOURCE1} %{buildroot}/%{_datadir}/%{name}/ + +// 新生成文件到安装目录 +pushd %{buildroot}/%{_datadir}/%{name}/ +touch xxx +popd + +// 删除多余文件 +rm -f %{buildroot}/%{_libdir}/%{name}/*.{a,la,so} +``` + +### 2.5 %files 打包 + +spec 中通过 %files 来规定 package 包含哪些文件,对于 %install 里的文件要求如下: + + - 每个文件只能有一个归属,不可以同时属于两个 package + - 所有在 %install 阶段安装到 %{buildroot} 下的文件,必须全部都有归属 + - 当然如果想忽略某个 %{buildroot} 下的文件,可以使用 %exclude 进行去除,但不推荐这种方式,建议直接在 %install 阶段里删除对应文件 + - 不允许声明不在 %{buildroot} 下的文件 + +#### 2.5.1 文件系统布局 +文件系统层次结构标准(FHS),定义了 Linux 操作系统中的主要目录及目录内容。[FHS](https://refspecs.linuxfoundation.org/fhs.shtml ) 由 Linux 基金会维护。主要介绍了每个结构的含义和应该存放的文件。 + +| / | 第一层次结构的根,整个文件系统层次结构的根目录 | +| --- | --- | +| /bin | 需要在单用户模式可用的必要命令(可执行文件);面向所有用户,例如:cat、ls、cp | +| /sbin | 必要的系统二进制文件,面向管理员用户。例如:init、ip、mount | +| /boot | 引导程序文件,系统引导文件,如内核 vmlinuz、ramfs 文件,initrd,以及 grub(bootloader);通常划分单独的分区 | +| /dev | 必要设备, 例如:/dev/null,/dev/sda | +| /etc | 系统范围内的配置文件 | +| /home | 用户的家目录,包含保存的文件、个人设置等
每一个用户的家目录通常默认为 /home/$USER | +| /lib | /bin/ 和 /sbin/中二进制文件必要的库文件。 | +| /lib64 | /bin/ 和 /sbin/中二进制文件必要的库文件(64位)。 | +| /lost+found | 系统断电时候临时保存的,默认为空 | +| /media | 可移除媒体(如CD-ROM)的挂载点 | +| /mnt | 临时挂载的文件系统 | +| /opt | 备用目录,第三方程序的安装目录,默认为空 | +| /proc | 虚拟文件系统,将内核与进程状态归档为文本文件。例如:uptime、 network。在 Linux 中,对应 Procfs 格式挂载。
/proc/meminfo:查看内存信息
/proc/cpuinfo:cpu信息
/proc/mounts:挂载信息
/proc/loadavg:负载信息
/proc/partitions:磁盘信息 |
+| /root | 超级用户的家目录 | +| /selinux | selinux 相关的安全策略等信息存储的位置,默认为空 | +| /srv | 为服务提供数据存放位置,默认为空 | +| /sys | 虚拟文件系统,用于输出当前系统上硬件设备相关信息的虚拟文件系统 | +| /tmp | 临时文件(参见 /var/tmp),在系统重启时目录中文件不会被保留。 | +| /usr | 用于存储只读用户数据的第二层次; 包含绝大多数的(多)用户工具和应用程序。 | +| /var | 频繁发生变化的文件——在正常运行的系统中其内容不断变化的文件,如日志,脱机文件和临时电子邮件文件。
/var/cache:应用程序缓存数据目录
/var/lib:应用程序状态信息数据
/var/local:专用于/usr/local下的应用程序存储可变数据
/var/log:日志目录文件
/var/log/messages系统日志
/var/log/secure 安全日志 SSH连接日志
/var/lock:锁文件
/var/run:与运行中进程相关的数据;通常存放进程的PID文件
/var/spool:应用程序数据池
/var/tmp:保存系统两次重启之间产生的临时数据 | + +但是也存在一些例外: + +- 不允许在/或者/usr下创建目录 +- FHS 没有定义 libexecdir,rpm 的宏中包含了 %{_libexecdir} 的定义,Libexecdir(也就是系统上的 /usr/libexec)只应该用作可执行程序的目录,这些可执行程序是由其他程序运行的而不是由用户运行的。 +- /run 目录是 tmpfs,系统服务应该存放在该目录,/var/run 是 /run 的软链接。 +- /srv、/usr/local、/home/$USER 目录下禁止存放文件或目录 +- 有限使用 /opt,/etc/opt,/var/opt,如果想要安装文件到上述目录,最好是创建子目录,比如 /opt/anolis/。 +- Anolis OS 23 系统中将 / 下的几个目录与 /usr 下的几个目录合并 + + ``` + /bin + /usr/bin %{_bindir} + /sbin + /usr/sbin %{_sbindir} + /lib64 or /lib + /usr/lib64 or /usr/lib %{_libdir} + /lib + /usr/lib %{_prefix}/lib + ``` + +- 举例:用户发现 /bin/sh 与 /usr/bin/sh 是同一个文件,如果一个rpm指定 + + ``` + %files + /bin/sh + ``` + 可以满足 /bin/sh 的文件依赖,但是无法满足 /usr/bin/sh 的文件依赖。所以 %files 中需要列出历史记录中放置在 /bin,/sbin,/lib 或者 /lib64 的内容;历史记录中放在 /usr/bin,/usr/sbin 目录下的内容以 %{_bindir}、%{_sbindir} 在 %files 中列出。如果对于程序放在哪个目录不清楚,可以通过虚拟的 provides 来列出备用路径,如: + ``` + ....... + Provides: %{_sbindir}/ifconfig + ....... + + %files + /sbin/ifconfig + ``` + +#### 2.5.2 rpath注意事项 + +- 不允许使用 rpath。 + + rpath 是在链接二进制文件时使用硬编码指定特定的库路径(使用 -rpath 或 -R 标志)。动态链接器和加载器 (ld.so) 会解析可执行文件对共享库的依赖关系并加载所需的内容。但是,当使用 -rpath 或 -R 时,位置信息会被硬编码到二进制文件中,并在执行开始时由 ld.so 检查。由于 Linux 动态链接器通常比硬编码路径更智能,因此我们通常不允许使用 rpath。 + rpmdevtools 软件包中包含一个名为 check-rpaths 的工具,并且在 ~/.rpmmacros 中配置 `%__arch_install_post` 宏: + + ``` + %__arch_install_post \ + /usr/lib/rpm/check-rpaths \ + /usr/lib/rpm/check-buildroot + ``` + + 运行 check-rpaths 后,可以得到错误提示: + + ``` + ERROR 0001: file '/usr/bin/xapian-tcpsrv' contains a standard rpath '/usr/lib64' in [/usr/lib64] + ``` + + 必须删除由 check-rpaths 标记的任何 rpath。 + +- 允许内部库文件使用 rpath + + 有些程序在安装内部库时,通常不会安装在系目录,这些内部库仅被包内程序使用,并不会给外部程序使用,这种情况戏下,可以使用 rpath 来查找这些库。 + ``` + # Internal libraries for myapp are present in: + %{_libdir}/myapp/ + %{_libdir}/myapp/libmyapp.so.0.3.4 + %{_libdir}/myapp/libmyapp.so + + # myapp has an rpath to %{_libdir}/myapp/ + readelf -d /usr/bin/myapp | grep RPATH + 0x0000000f (RPATH) Library rpath: [/usr/lib/myapp] + ``` + +- rpath 替代方案 + + 通常,使用 rpath 是因为二进制文件在非标准位置(标准位置是/lib、/usr/lib、/lib64、/usr/lib64)寻找库。如果需要将库存储在非标准位置(例如 /usr/lib/foo/),则应该在 /etc/ld.so.conf.d/ 中包含一个自定义配置文件。 + 例如,如果我将 libfoo 库放在 /usr/lib/foo 中,我就需要在 /etc/ld.so.conf.d/ 中创建一个名为“foo.conf”的文件,文件内容为 '/usr/lib/foo' 。 + ``` + %install + ...... + mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/ld.so.conf.d + echo "%{_libdir}/foo" > %{buildroot}%{_sysconfdir}/ld.so.conf.d/%{name}.conf + ...... + ``` + + - 删除 rpath: + - 如果应用程序包含 configure,configure添加--disable-rpath + - 如果应用程序使用 libtool,请将以下几行添加到 %configure 之后的spec中 + + ``` + %configure + sed -i 's|^hardcode_libdir_flag_spec=.*|hardcode_libdir_flag_spec=""|g' libtool + sed -i 's|^runpath_var=LD_RUN_PATH|runpath_var=DIE_RPATH_DIE|g' libtool + ``` + + - 直接修改代码或者Makefile来删除 -rpath 或者-R + - 作为最后的手段,有一个名为 chrpath 的包。安装此软件包后,对包含 rpath 的文件运行 chrpath --delete。需要在 spec 中增加 `BuildRequires: chrpath` 后,运行: + + ``` + chrpath --delete $RPM_BUILD_ROOT%{_bindir}/xapian-tcpsrv + ``` + +#### 2.5.3 权限和属主 +所有的目录和文件必须设置正确的权限。 + +- 每个文件都应该归 root:root 所有,除非出于安全考虑需要更具体的用户或组。 +- 目录默认权限为 0755 ,文件默认权限为 0644 ,除非有特定需求 +- 所有文件必须可读 +- 文件权限方式有几种方式: + - 在 %install 阶段,通过 chmod 和 chown 等命令修改权限 + - **在 %install 阶段执行拷贝命令时进行赋权限:**`install -m 0644 xxxx`, 建议使用该方式。 + - 在 %files 阶段统一增加 `%defattr(-,root,root)`,统一赋权限,但该项仅对没有默认值的文件生效 + - 在 %files 阶段对单个路径或者配置文件增加权限操作:`%dir %attr(0700, root, root) xxxx` + +#### 2.5.4 文件详情 +##### 2.5.4.1 license 许可证 +因为每款开源软件都是来源于上游社区,并且都含有自己的许可证。所以需要将许可证信息打入 rpm 包中。 +一般存在的形式为:COPYING 、License 等,解压开源码包即可查找到。 +``` +[root@localhost lld-13.0.1.src]$ ll +total 88 +drwxr-xr-x 3 root root 4096 Jan 20 2022 cmake +-rw-r--r-- 1 root root 7232 Aug 4 02:44 CMakeLists.txt +-rw-r--r-- 1 root root 879 Jan 20 2022 CODE_OWNERS.TXT +drwxr-xr-x 2 root root 4096 Jan 20 2022 COFF +drwxr-xr-x 2 root root 4096 Jan 20 2022 Common +drwxr-xr-x 6 root root 4096 Jan 20 2022 docs -> 这里也别忘记查看一下 +drwxr-xr-x 3 root root 4096 Jan 20 2022 ELF +drwxr-xr-x 4 root root 4096 Aug 4 02:44 include +drwxr-xr-x 5 root root 4096 Jan 20 2022 lib +-rw-r--r-- 1 root root 15138 Jan 20 2022 LICENSE.TXT -> 看这里 +drwxr-xr-x 3 root root 4096 Jan 20 2022 MachO +drwxr-xr-x 2 root root 4096 Jan 20 2022 MinGW +-rw-r--r-- 1 root root 678 Jan 20 2022 README.md +drwxr-xr-x 10 root root 4096 Jan 20 2022 test +drwxr-xr-x 3 root root 4096 Jan 20 2022 tools +drwxr-xr-x 4 root root 4096 Jan 20 2022 unittests +drwxr-xr-x 2 root root 4096 Jan 20 2022 utils +drwxr-xr-x 2 root root 4096 Jan 20 2022 wasm +``` + +在找到这个文件之后,可以直接使用 %license 的宏将文件从**源码包**目录直接打包至**rpm**包内,不需要再单独执行从编译目录拷贝到安装目录的动作。 + +``` +// 使用样例: +%files +%license LICENSE.TXT +``` + +##### 2.5.4.2 配置文件 + +- %config 标志 + + 配置文件在打包时,可以使用 %config 对配置文件进行标记,%config 额外还提供 %config(noreplace) 的形式,这样可以在软件包升级时不覆盖本地修改。当然是否选择 noreplace,还要根据具体软件包特点来定,推荐使用 %config(noreplace) 。 + 但 %config 仅能标记配置文件,其他文件不允许。 + + ``` + %files + %config /etc/%{package_name}/%{package_name}.conf + %config(noreplace) /etc/%{package_name}/%{package_name}-noreplace.conf + ``` + +- 配置文件的目录 + + 配置文件建议放到 /etc 下,不允许放在 /usr 下。 + +##### 2.5.4.3 systemd units + +systemd units文件的软件包必须将它们放入 %{_unitdir} 或 %{_userunitdir}。 +大多数 systemd 服务应该使用 %{_unitdir},但是如果服务作为用户运行的话可以使用 %{_userunitdir}。如果想要使用这两个宏,则必须增加其对应的 rpm。 + +``` +[root@localhost ~]$ rpm -E %{_unitdir} +/usr/lib/systemd/system + +[root@localhost ~]$ rpm -E %{_userunitdir} +/usr/lib/systemd/user + +// 查询哪个 rpm 对外提供系统服务路径 +[root@localhost ~]$ cd /usr/lib/rpm +[root@localhost rpm]$ grep -nr '_unitdir' +macros.d/macros.systemd:9:%_unitdir /usr/lib/systemd/system +[root@localhost rpm]$ rpm -qf macros.d/macros.systemd +systemd-rpm-macros-250.4-1.an23.noarch +``` + +每个 service 文件都会有 scripts 伴随,一般用于设定服务启动时间、执行服务准备脚本等。主要分布在 %post +、%preun、%postun 阶段。 + +- 系统服务相关脚本 + + ``` + BuildRequires: systemd-rpm-macros + + [...] + %post + %systemd_post apache-httpd.service + + %preun + %systemd_preun apache-httpd.service + + %postun + %systemd_postun_with_restart apache-httpd.service + ``` + + 某些服务不支持重启(例如 D-Bus 和各种存储守护进程)。 如果升级时不应重新启动您的服务,请使用以下 %postun 脚本代替上述脚本: + + ``` + %postun + %systemd_postun apache-httpd.service + ``` + +- 用户服务相关脚本 + + 安装在 %_userunitdir 下的服务也是需要有对应的脚本设置启动或禁用。 + + ``` + BuildRequires: systemd-rpm-macros + + [...] + %post + %systemd_user_post %{name}.service + + %preun + %systemd_user_preun %{name}.service + ``` + +##### 2.5.4.4 命令行 + +命令行一般都被包含在主包内,存放路径为:%_bindir 或 %_sbindir,存在部分命令行直接放在 /bin 和 /sbin 下下,/bin 和 /sbin 是作为超链接存在。命令行对外是个可执行的二进制文件,用户根据自己的需求,设置对应的 option,并获取执行结果。 +``` +[root@localhost rpm]$ rpm -E %{_bindir} +/usr/bin + +[root@localhost rpm]$ rpm -E %{_sbindir} +/usr/sbin + +[root@localhost rpm]$ ll /bin +lrwxrwxrwx. 1 root root 7 Jun 16 2021 /bin -> usr/bin + +[root@localhost rpm]$ ll /sbin +lrwxrwxrwx. 1 root root 8 Jun 16 2021 /sbin -> usr/sbin +``` + +##### 2.5.4.5 动态库 + +动态库又叫共享库,一般存放在 %{_libdir} 下。如果为 64 位系统,%_libdir 为 /usr/lib64,如果为 32 位系统,_libdir 为 /usr/lib。但是也存在一些 so 文件,直接放在 /usr/lib 下,没有64 位 和 32 位的区分。 +注意:%files 声明 %{_libdir} 下的动态库时,尽量不要使用 * 的统配方式隐藏文件名,比如:libxx.so*。 +一般动态库有三种形态,需要严格按照划分方式规定每个 so 的归属。 + +``` +libxxx.so // 放在 devel 包,开发包,作为超链接直接对外提供,链接到 libxxx.so.1.1 +libxxx.so.1 // 放在主包或者 libs 包,作为超链接直接对外提供,链接到 libxxx.so.1.1 +libxxx.so.1.1 // 放在主包或者 libs 包,实际掉用的 so,当发生版本变更时,不影响对外提供 +``` + +举例: + +``` +[root@localhost rpm]$ ll /usr/lib64 | grep libffi +lrwxrwxrwx. 1 root root 15 3月 14 11:19 libffi.so -> libffi.so.8.1.0 +lrwxrwxrwx. 1 root root 15 3月 14 11:19 libffi.so.8 -> libffi.so.8.1.0 +-rwxr-xr-x. 1 root root 45280 3月 14 11:19 libffi.so.8.1.0 + +[root@localhost rpm]$ rpm -qf /usr/lib64/libffi.so.8 +libffi-3.4.2-1.an23.x86_64 +[root@localhost rpm]$ rpm -qf /usr/lib64/libffi.so.8.1.0 +libffi-3.4.2-1.an23.x86_64 +[root@localhost rpm]$ rpm -qf /usr/lib64/libffi.so +libffi-devel-3.4.2-1.an23.x86_64 +``` + +##### 2.5.4.6 静态库 + +静态库是指 .a 或者 .la 的文件,存放路径也在 %{_libdir} 下。但是,我们的软件应该尽可能排除掉静态库,即静态库默认不对外提供。 +如果想要对外提供静态库,则可以生成 package-static 包,其他依赖静态库的可以在 spec 中增加对 package-static 的构建依赖。 +``` +[root@localhost ~]$ rpm -ql zlib-static +/usr/lib64/libz.a +/usr/share/licenses/zlib-static +/usr/share/licenses/zlib-static/README + +[root@localhost ~]$ ll /usr/lib64/libz.a +-rw-r--r--. 1 root root 383514 3月 15 19:47 /usr/lib64/libz.a +``` + +##### 2.5.4.7 网络应用程序 + +一般 Web 应用程序应该将它们的内容放入 `%{_datadir}/%{name}`(即: /usr/share/%{name} )中,而不是放在 /var/www/ 下,原因如下: + +- /var 应该包含可变数据文件和日志。 /usr/share 更适合这种情况。 +- 许多用户有可能已经在 /var/www 中拥有内容,我们不希望任何软件包覆盖用户信息。 +- FHS没有定义 /var/www + +##### 2.5.4.8 Tmpfiles.d + +tmpfiles.d 是用于管理守护进程的临时文件和运行时目录的服务,一般对应的路径为:/run 和 /run/lock。由于 /run 是一个 tmpfs 文件系统,因此每次重新启动时都必须重新创建它及其内容。 通常需要提前创建目录,最好使用 tmpfiles.d 机制来完成。 + +- tmpfiles.d 机制,对应路径为: %{_tmpfilesdir} +- 在 %{_tmpfilesdir} 下生成 %{name}.conf 文件 +- 配置文件的内容为一行或多行相同类型的配置: + ``` + // d 规定如果目录不存在则创建路径,可以有多种类型 + // /run/NAME 需要创建的文件系统路径 + // PERM 创建目录时的目录权限 + // USER 目录的所有者 + // GROUP 目录所在组的名称 + // - 自动清理在指定时间内未使用的文件 + d /run/NAME PERM USER GROUP - + ``` + +- 举例: + ``` + // 查询 %{_tmpfilesdir} 对应的实际路径 + [root@localhost ~]$ rpm -E %{_tmpfilesdir} + /usr/lib/tmpfiles.d + + // 查看 /usr/lib/tmpfiles.d 下存在哪些文件 + [root@localhost ~]$ ll /usr/lib/tmpfiles.d/x11.conf + -rw-r--r--. 1 root root 617 3月 11 15:33 /usr/lib/tmpfiles.d/x11.conf + + // 案例1: + [root@localhost ~]$ cat /usr/lib/tmpfiles.d/cryptsetup.conf + d /run/cryptsetup 0700 root root - + + // 案例2: + [root@localhost ~]$ cat /usr/lib/tmpfiles.d/dnf.conf + // Unlink the dnf lock files during boot + R /var/tmp/dnf*/locks/* + r /var/cache/dnf/download_lock.pid + r /var/cache/dnf/metadata_lock.pid + r /var/lib/dnf/rpmdb_lock.pid + r /var/log/log_lock.pid + ``` + +##### 2.5.4.9 locale 文件 + +rpm 提供了 %find_lang 宏用于处理 locale 文件,%find_lang 能够按名称扫描软件包中所有的 locale 文件,并将列表放入文件中,然后可以通过该文件来打包所有的locale文件。 +使用方式: 在 %install 阶段将所有的文件都安装到 %buildroot 后,运行 %find_lang ,并在 %files 阶段将文件打入 rpm。 +``` +%install +....... +%find_lang %{name} + +%files -f %{name}.lang +...... +``` + 如果 lang 文件与 %{name} 不同,则需要单独调用 %find_lang 脚本 +``` +%install +....... +%find_lang %{name} +%find_lang test --with-man + +%files -f %{name}.lang -f test.lang +...... +``` + +##### 2.5.4.10 cron 文件 + +cron 文件的存放目录取决于运行时的间隔,可能的目录有: +``` +[root@localhost ~]$ ll /etc/ | grep cron +-rw-r--r--. 1 root root 541 Jan 26 2021 anacrontab +drwxr-xr-x. 2 root root 4096 Jun 27 2021 cron.d +drwxr-xr-x. 2 root root 4096 Jun 27 2021 cron.daily +-rw-r--r--. 1 root root 0 Jan 26 2021 cron.deny +drwxr-xr-x. 2 root root 4096 Jun 27 2021 cron.hourly +drwxr-xr-x. 2 root root 4096 Jun 16 2021 cron.monthly +-rw-r--r--. 1 root root 451 Jun 16 2021 crontab +drwxr-xr-x. 2 root root 4096 Jun 16 2021 cron.weekly +``` +例外情况:如果某个 cron 的任务的运行频率或者时间间隔不满足上述规定的间隔,则需要自定义 crontab 文件添加到 /etc/cron.d(具有 0640 权限)), cron 任务文件(脚本)必须放在适当的系统位置(例如 %{_sbindir}、%{_libexecdir}),而不是 /etc/cron.d。 + +##### 2.5.4.11 头文件 +当我们使用某款软件做开发时,会依赖其对外提供的头文件,这些头文件在系统中的位置一般都是 %{_includedir}( /usr/include ),并且会被封装到 devel 包中,同时 devel 包对于主包是存在运行依赖关系的。 +``` +[root@localhost ~]$ rpm -ql zlib-devel +/usr/include/zconf.h +/usr/include/zlib.h +..... +``` + +##### 2.5.4.12 pkgconfig 文件 + +一般伴随头文件的还有一个 pkgconfig 文件,在系统中的位置为 /usr/lib64/pkgconfig,一般以 %{name}.pc 文件存在,对外提供 pkgconfig(%{name}) 功能。 +``` +[root@localhost ~]$ rpm -ql zlib-devel +...... +/usr/lib64/pkgconfig/zlib.pc +...... + +// 查询对外提供的 pkgconfig 能力 +[root@localhost ~]$ rpm -qP zlib-devel +pkgconfig(zlib) = 1.2.11 +zlib-devel = 1.2.11-1.an23 +zlib-devel(x86-64) = 1.2.11-1.an23 +``` + +##### 2.5.4.13 example 文件 + +一般软件的源码中会包含一些测试文档和样本文件,我们也需要将这些 example 文件打入 devel 包中,存在系统的位置为:/usr/share/doc/%{package}-devel/example*,一般源码中会包含 examples 目录,我们也可以将 examples 目录都打入。 +``` +[root@localhost ~]$ rpm -ql zlib-devel +...... +/usr/share/doc/zlib-devel/example.c +...... + +[root@localhost zlib-1.2.11]$ ll +...... +drwxr-xr-x 2 501 games 4096 Aug 8 22:48 examples +...... +``` + +##### 2.5.4.14 doc 文件 + +一般软件源码中也会包含一些说明性质的文档,来讲述软件的基本功能、修改记录、软件计划节奏等。 +包括不限制:readme、changelog、news、todo 等。 + + +### 2.6 脚本执行 + +在 spec 中除了定义上述中的编译、安装、打包外,还有额外的脚本操作,这些脚本是按照 rpm 安装或者卸载的顺序来执行,一方面是为了准备安装或者卸载所需要的环境条件,另一方面是为了满足软件的运行需求。 +一般将脚本定义在 %install 和 %files 中间的位置。 + +#### 2.6.1 %pre + +%pre 是代表 rpm 安装之前所执行的脚本。 +格式如下:在 pre 之后可以定义在具体 rpm 所执行的内容,如果 rpm 为重命名的,需要同样采用 -n 选项 +``` +%pre rpm_name +xxxx + +%pre -n rpm_name +xxxx +``` +样例:dbus 生成的 dbus-daemon.rpm 需要设置 dbus 的用户群组 +``` +%install +...... + +%pre daemon +// Add the "dbus" user and group +getent group dbus >/dev/null || groupadd -f -g %{dbus_user_uid} -r dbus +if ! getent passwd dbus >/dev/null ; then + if ! getent passwd %{dbus_user_uid} >/dev/null ; then + useradd -r -u %{dbus_user_uid} -g %{dbus_user_uid} -d '/' -s /sbin/nologin -c "System message bus" dbus + else + useradd -r -g %{dbus_user_uid} -d '/' -s /sbin/nologin -c "System message bus" dbus + fi +fi +exit 0 + + +...... +%files +``` + +#### 2.6.2 %post + +%post 用来声明 rpm 安装后执行的脚本。 +样例:在 dbus 的 spec 内,对dbus-comon 安装后需要将对应的服务启动。 +``` +%post common +%systemd_post dbus.socket +%systemd_user_post dbus.socket +``` + +#### 2.6.3 %preun + +%preun 用来声明 rpm 卸载之前执行的脚本。 +样例:在 dbus 的 spec 内,对dbus-comon 安装后需要将对应的服务关闭。 +``` +%preun common +%systemd_preun dbus.socket +%systemd_user_preun dbus.socket +``` + +#### 2.6.4 %postun + +%postun 用来声明 rpm 卸载之后执行的脚本。 +样例: +``` +%postun common +%systemd_postun dbus.socket +%systemd_user_postun dbus.socket +``` + +#### 2.6.5 脚本执行顺序 + +- 单个 rpm 的脚本执行顺序: + + ``` + // 安装动作 + %pre 脚本 -》 安装文件 -》执行 %post 脚本 + + // 卸载动作 + %preun 脚本 -》 删除文件 -》执行 %postun 脚本 + ``` + +- rpm 升级时的脚本执行顺序: + + ![](../images/306-scripts-execution-order-during-rpm-upgrade.jpeg) + + +#### 2.6.6 执行状态判断 + +因为在首次安装或升级时,都要去执行 pre 或 post 操作,为了区分不同的前提状态,脚本里默认增加了变量 "$1" ,在脚本对应阶段通过 "$1" 的判断在安装和升级阶段执行不同的脚本 + +| 软件包动作 | %pre | %post | %preun | %postun | +| --- | --- | --- | --- | --- | +| 首次安装 | "$1" = 1 | 不适用 | "$1" = 1 | 不适用 | +| 升级 | "$1" = 2 | "$1" = 1 | "$1" = 2 | "$1" = 1 | +| 卸载 | 不适用 | "$1" = 0 | 不适用 | "$1" = 0 | + + + +## 3 常见问题汇总 + +### 3.1 rpmbuild 基础知识 + +#### 3.1.1 为什么需要了解 rpmbuild ? + +rpmbuild 提供了本地生成 rpm 的功能,可以实现从 spec + source.tar.gz 生成对应的 rpm 文件。 + +#### 3.1.2 环境初始化 + +准备 rpmbuild 的运行所需要的目录结构,通常 SOURCES 是必须的目录,其他路径在运行过程中会自动生成。 + +- 方案1 - 手动创建目录结构 + ``` + [root@localhost ~]$ mkdir rpmbuild + [root@localhost ~]$ mkdir rpmbuild/SOURCES + ``` + +- 方案2 - 通过 rpmdevtools 中的 rpmdev-setuptree 命令生成 + ``` + [root@localhost ~]$ yum install rpmdevtools + [root@localhost ~]$ rpmdev-setuptree + [root@localhost ~]$ ll rpmbuild/ + total 20 + drwxr-xr-x 2 root root 4096 Aug 15 02:12 BUILD + drwxr-xr-x 2 root root 4096 Aug 15 02:12 RPMS + drwxr-xr-x 2 root root 4096 Aug 15 02:12 SOURCES + drwxr-xr-x 2 root root 4096 Aug 15 02:12 SPECS + drwxr-xr-x 2 root root 4096 Aug 15 02:12 SRPMS + ``` + +#### 3.1.3 目录结构介绍 + +实际使用目录介绍: +``` +[root@localhost ~]$ ll /root/rpmbuild/ +总用量 0 +drwxr-xr-x. 3 root root 28 8月 9 14:54 BUILD ## 源码编译目录 +drwxr-xr-x. 2 root root 6 8月 8 14:51 BUILDROOT ## 源码预安装目录 +drwxr-xr-x. 2 root root 6 7月 28 17:48 RPMS ## rpm 产物目录 +drwxr-xr-x. 4 root root 174 8月 9 19:38 SOURCES ## 源码文件目录 +drwxr-xr-x. 2 root root 6 7月 28 17:48 SPECS ## spec 目录 +drwxr-xr-x. 2 root root 47 8月 8 14:51 SRPMS ## srpm 目录 +``` + +#### 3.1.4 安装 rpmbuild + +初始环境中不会默认安装 `/usr/bin/rpmbuild`,需要开发者自行安装 `rpm-build`软件。 +``` +[root@localhost ~]$ yum install rpm-build +``` + +#### 3.1.5 使用介绍 + +rpmbuild 针对 spec 里的每一个阶段都提供了对应的 option,可以方便的按需执行到对应阶段。 + +- 准备阶段 + +将 source.tar.gz 和 spec 拷贝至 SOURCES 目录后,执行 rpmbuild 命令会生成其他目录结构。 + +- 构建相关的基本命令 + ``` + rpmbuild -bp xxx.spec // 执行完 %prep,解压和打补丁 + rpmbuild -bb xxx.spec // 执行完 %build,执行到编译动作结束 + rpmbuild -bi xxx.spec // 执行完 %install,执行到安装动作结束 + # srpm 是将所有 source 文件打成一个压缩包 + rpmbuild -ba xxx.spec // 全部执行,生成 srpm 和 rpm + rpmbuild -bs xxx.spec // 只生成 srpm + rpmbuild -bl xxx.spec // 检验文件是否齐全 + ``` + +- 拓展功能,以下为比较常用的选项介绍 + ``` + -buildroot=DIRECTORY // 自定义 build root 的目录 + -clean // 完成打包后清除BUILD下的文件目录 + -nobuild // 不执行 build 阶段 + -nodeps // 忽略掉构建依赖 + ``` + +### 3.2 补丁 + +#### 3.2.1 什么是补丁? + +在源码中经常能看到后缀为 .patch 的文件,这些文件即是补丁文件。样例如下: +``` +[root@localhost tbb]$ ll +total 5360 +-rw-r--r-- 1 root root 488 Jul 27 07:14 README.md +-rw-r--r-- 1 root root 2677 Jul 27 07:14 tbb-2019-dont-snip-Wall.patch +-rw-r--r-- 1 root root 670 Jul 27 07:14 tbb-2019-test-task-scheduler-init.patch +-rw-r--r-- 1 root root 1864 Jul 27 07:14 tbb-2019-test-thread-monitor.patch +-rw-r--r-- 1 root root 2639737 Jul 27 07:14 tbb-2020.3.tar.gz +-rw-r--r-- 1 root root 2883 Jul 27 07:14 tbb-2020-attributes.patch +-rw-r--r-- 1 root root 2883 Jul 27 07:14 tbb-mark-empty_task-execute-with-gnu-used.patch +``` + +补丁格式详解: +总包含两大块:补丁头 + 实际修改内容。 +其中补丁头用于描述补丁的基本信息,查看这里就能清楚的获取补丁的制作人、制作时间和实现的功能。 +实际修改内容中包括:修改的文件路径 + 内容 +修改文件这里是默认采用源码包解压后的第一层目录作为相对目录进行,通常在打入补丁时采用 -px 来指定忽略 x 层路径。 +``` +From git log 信息 +From: patch 制作者 +Date: patch 的生成日期 +Subject: 修改的简要描述 + +--- + git diff 生成的文件差异信息 + +diff --git 修改前文件路径 修改后文件路径 +--- 修改前文件路径 ++++ 修改后文件路径 +@@ index @@ index 对应的代码行 +实际代码 +``` +样例: + +``` +[root@localhost tbb]$ cat tbb-mark-empty_task-execute-with-gnu-used.patch +From db2f2116adfb545bb76c92205f91e3e3f0f9e44a Mon Sep 17 00:00:00 2001 +From: Thomas Rodgers +Date: Wed, 2 Jun 2021 15:18:30 -0700 +Subject: [PATCH] Mark tbb::empty_task::execute with [[gnu::used]] + +--- + include/tbb/task.h | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/include/tbb/task.h b/include/tbb/task.h +index 5e137c6..5b60163 100644 +--- a/include/tbb/task.h ++++ b/include/tbb/task.h +@@ -1040,6 +1040,9 @@ inline void task::resume(suspend_point tag) { + //! task that does nothing. Useful for synchronization. + /** @ingroup task_scheduling */ + class __TBB_DEPRECATED_IN_VERBOSE_MODE empty_task: public task { ++#if __has_cpp_attribute(gnu::used) ++ [[gnu::used]] ++#endif + task* execute() __TBB_override { + return NULL; + } +-- +2.31.1 +``` + +#### 3.2.2 为什么要有补丁文件? + +因为 OS 体系是由众多第三方开源软件组成的,这些第三方开源软件都是有自己的维护者的。 +那么 Anolis OS 作为发行者的角色,只能从开源社区取到完整的 source.tar.gz,这些 source.tar.gz 是不允许直接解压开直接修改的,所以针对 source.tar.gz 所有的修改都要通过 patch 的途径进行。 + +#### 3.2.3 如何制作补丁? + +- 首先,将源码包解压开,如果有前缀补丁,需要将其先打入。 + + ``` + // 先将源代码拷贝到 ~/rpmbuild/SOURCES/ + [root@localhost tbb]$ cp * /root/rpmbuild/SOURCES/ + + // 执行解压动作 + [root@localhost tbb]$ rpmbuild -bp tbb.spec --nodeps + Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.W4Pc3T + + umask 022 + + cd /root/rpmbuild/BUILD + + cd /root/rpmbuild/BUILD + + rm -rf oneTBB-2021.5.0 + + /usr/bin/gzip -dc /root/rpmbuild/SOURCES/v2021.5.0.tar.gz + + /usr/bin/tar -xof - + + STATUS=0 + + '[' 0 -ne 0 ']' + + cd oneTBB-2021.5.0 + + /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w . + + sed -i 's/version\s*="0.2"/version = "2021.5"/' python/setup.py + + sed -i '1{/^#!.*env python/ d}' python/TBB.py python/tbb/__init__.py python/tbb/__main__.py python/tbb/pool.py python/tbb/test.py + + exit 0 + ``` + +- 进入解压后的目录下,进行 git 初始化 + + ``` + // 这里 v2021.5.0.tar.gz 解压完后的目录名称为 oneTBB-2021.5.0 + [root@localhost tbb]$ cd /root/rpmbuild/BUILD/oneTBB-2021.5.0/ + + // 执行 git 初始化 + [root@localhost oneTBB-2021.5.0]$ git init + Initialized empty Git repository in /root/rpmbuild/BUILD/oneTBB-2021.5.0/.git/ + [root@localhost oneTBB-2021.5.0]$ git add . + [root@localhost oneTBB-2021.5.0]$ git commit -m "init" + ``` + +- 此时可以按照自己需要修改源码,修改之后通过 `git status` 或 `git diff`查看修改内容,如果想把多处修改合并到一个 patch 里,此处可以将要修改的文件都改了。 + + ``` + // 这里样例就随意修改了~ + [root@localhost oneTBB-2021.5.0]$ git status + On branch master + Changes not staged for commit: + (use "git add ..." to update what will be committed) + (use "git restore ..." to discard changes in working directory) + modified: README.md + + no changes added to commit (use "git add" and/or "git commit -a") + ``` + +- 提交本地修改 + + ``` + [root@localhost oneTBB-2021.5.0]$ git add . + [root@localhost oneTBB-2021.5.0]$ git commit -m "modify the README.md" + [master 171249b] modify the README.md + 1 file changed, 5 insertions(+) + ``` + +- 生成补丁,HEAD^ 代表将最近的 一次生成补丁,-o 后面为补丁输出路径。 + + ``` + [root@localhost oneTBB-2021.5.0]$ git format-patch -n HEAD^ -o /root/rpmbuild/SOURCES/ + /root/rpmbuild/SOURCES/0001-modify-the-README.md.patch + ``` + +- 查看补丁内容 + + ``` + root@localhost oneTBB-2021.5.0]$ cat /root/rpmbuild/SOURCES/0001-modify-the-README.md.patch + From 171249bd0bf1641b52697710d399d832d1b85914 Mon Sep 17 00:00:00 2001 + From: "xxx.test" + Date: Wed, 10 Aug 2022 05:22:25 -0400 + Subject: [PATCH 1/1] modify the README.md + README.md | 5 +++++ + 1 file changed, 5 insertions(+) + diff --git a/README.md b/README.md + index dd5156f..b024027 100644 + --- a/README.md + +++ b/README.md + @@ -4,6 +4,11 @@ + oneAPI Threading Building Blocks (oneTBB) lets you easily write parallel C++ programs that take xxxxxxxx + full advantage of multicore performance, that are portable, composable and have future-proof scalability. + + +############## + +this is test + +############## + + + + + #### Release Information + Here are [Release Notes]( https://software.intel.com/en-us/articles/intel-oneapi-threading-building-blocks-release-notes) and + [System Requirements](https://software.intel.com/en-us/articles/intel-oneapi-threading-building-blocks-system-requirements). + -- + 2.27.0 + ``` + + + +#### 3.2.4 如何打补丁? + +- 源码方式打入补丁 + + 可以通过 `git apply`命令在源码中将补丁打入 + + ``` + // 解压源码,并将以前补丁打入 + [root@localhost SOURCES]$ rpmbuild -bp tbb.spec --nodeps + + // 进入解压后的目录 + [root@localhost SOURCES]$ cd /root/rpmbuild/BUILD/oneTBB-2021.5.0/ + + // 检查补丁 + [root@localhost oneTBB-2021.5.0]$ git apply --check /root/rpmbuild/SOURCES/0001-modify-the-README.md.patch + + // 打入补丁 + [root@localhost oneTBB-2021.5.0]$ git apply /root/rpmbuild/SOURCES/0001-modify-the-README.md.patch + ``` + +- 通过 spec 方式打入补丁 + + 参见 2.2 章节中的 %prep 介绍。 + +### 3.3 构建问题 + +#### 3.3.1 构建环境是什么? + +构建环境是一个相对“干净”的环境,因为是从 chroot 生成的,该环境仅仅用于源码构建,不用于其他用途。 + +#### 3.3.2 构建环境里安装的软件是在哪里指定的? + +构建环境是来源于两处:**默认安装** + **spec 中 BuildRequires 指定**。默认安装是我们针对所有软件包构建的一个基础编译环境,只包含编译工具链的软件;每个软件可以将自己构建需要引入的软件包都写到 BuildRequires 中,则构建环境会去安装这些软件,从而共同组成了构建环境。 + +#### 3.3.3 我如果想在构建环境中增加软件,该如何做? + +在编译的过程中,经常会遇到缺少某个头文件或者某个动态库,再或者某个二进制的情况,这是我们需要将缺少的文件所属的 rpmpackage 添加到 BuildRequires 中,然后重新发起构建即可。 +举例: 假设当前缺少 /usr/include/ncursesw/etip.h,可以通过 `dnf`查询改文件的所属,并将该软件写到 BuildRequires 中。 +``` +$ dnf repoquery --whatprovides '/usr/include/ncursesw/etip.h' +上次元数据过期检查:1:26:21 前,执行于 2022年08月10日 星期三 12时45分43秒。 +ncurses-devel-0:6.3-2.an23.x86_64 +``` + +#### 3.3.4 在构建环境里的软件版本不满足我的需求怎么办? + +软件(A)包在构建的过程中,可能需要指定某个构建依赖包(B)的版本的版本号,这时发现构建环境里安装的这款软件和预期版本不满足,此时,需要我们先把依赖包准备出对应的 rpm 后,再来编译原本的软件。 + +- B 大于 A + +这种场景下优先建议 A 软件进行适配 B 软件。因为高版本的 B 软件已经发布,整个 OS 内的其他软件已经依赖了高版本的 B,降级和多版本共存的场景都不满足 OS 发行版的要求,而且从软件的长远发展路线来看,去适配最新版本也是最好的选择。 + +- B 小于 A + +这种场景下建议先将软件包 B 进行基线升级,升级到满足 A 的要求的版本,但是进行基线升级时,需要进行全面的影响评估,请勿对其他软件产生影响。 + +#### 3.3.5 我需要在构建环境中执行外网操作怎么办? + +结论:不允许出现访问外网操作。 +因为构建环境是一个模拟的chroot中构建的,无法访问互联网,同时也不允许从外网直接下载依赖的操作,这里的外网操作包括但不限制如下:配置境外、国内的任何一个远端源(pip、nodejs、rust、maven 等),wget 下载、curl 下载等等。 + +#### 3.3.6 如果我必须下载很多外部源怎么办? + +对于还在孵化期的软件,我们允许将所有构建依赖下载到本地,然后通过 offline 模式指定本地的路径进行编译。但该动作需要得到 OS 产品接口人的许可,方可执行。 +当前以 go 语言举例,其他语言参照 + +- 先在联网环境下,进入到源代码目录下,下载对应的依赖,并放到本地的 vendor 目录下 +``` +go mod vendor +``` + +- 关闭互联网,然后执行离线构建(这里需要根据不同语言,选择不同的离线方式) +``` +go build -mod=vendor +``` + +- 在上述两步执行成功之后,将 vendor 目录生成压缩包,作为 spec 的第二个source,并将离线构建方式写入 + + ``` + ...... + Source0: package.tar.gz + Source1: package_vendor.tar.gz + ....... + + + %prep + %autosetup -n %{name}-%{release} + tar -xvf %{SOURCE1} ./ + + %build + go build -mod=vendor + ``` + +- rpmbuild 构建或者 koji 构建测试,待成功后提交代码 + +#### 3.3.7 构建过程中提示没有权限怎么办? + +当构建过程中提示权限不足时,第一想法都是怎么去提高构建权限,这种想法是不对的。因为,所有发行版的正式构建都是以 build 用户执行的,是不允许操作系统环境的,只允许在构建目录下操作。 +所以,应该去排查是哪里操作了系统目录,然后将该动作的修改到构建目录下。 diff --git a/articles/306-build-a-new-project.md b/articles/307-build-a-new-project.md similarity index 100% rename from articles/306-build-a-new-project.md rename to articles/307-build-a-new-project.md diff --git a/images/306-scripts-execution-order-during-rpm-upgrade.jpeg b/images/306-scripts-execution-order-during-rpm-upgrade.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cc507d43debf4f430ba28634e34d8241c3d32cfe GIT binary patch literal 68297 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 z