GLEP 33:Eclass 重构/重新设计

作者 Brian Harring <[email protected]>,John Mylchreest <[email protected]>
类型 标准跟踪
状态 延迟
版本 1
创建日期 2005-01-29
最后修改日期 2014-01-17
发布历史 2005-01-29, 2005-03-06, 2005-09-15, 2006-09-05
GLEP 源代码 glep-0033.rst

状态

经 Gentoo 委员会于 2005 年 9 月 15 日批准。截至 2006 年 9 月,此 GLEP 处于暂停状态,等待未来修订。

摘要

对于任何设计,从理论到应用的过渡都会暴露出原始设计中的不足之处。本文档旨在记录并提出对当前 eclass 设置的修订,以解决当前 eclass 的不足之处。

本文档提出了一些内容——创建 ebuild 库,“elib”,缩小 eclass 的焦点,在树中移动 eclass,添加变更日志,以及一种允许简单 eclass gpg 签名的机制。总的来说,是对 eclass 是什么以及它们如何实现进行的大规模重构。本质上是 eclass 设置的第二个版本。

术语

从现在开始,建议的 eclass 设置将被称为“新 eclass”,现有的版本(截至本文撰写之时)将被称为“旧 eclass”。本文档中将详细阐述这种区别。

动机和基本原理

树中现有的 Eclass 有点混乱——它们被迫保持与所有先前功能的向后兼容性。实际上,它们的 API 是不变的,只能添加,永远不会更改现有功能。这显然非常有限,并导致 eclass 中累积了大量冗余代码,因为 eclass 设计经过改进。在 eclass 代码达到无法管理/脆弱的临界质量之前,需要解决这个问题(最近推动 eclass 版本控制可以被解释为对此的证明)。

除此之外,Eclass 最初的目的是作为一种方法,允许 ebuild 使用预先存在的代码块,而不是必须在每个 ebuild 中复制代码。这是一件好事,但当前设计导致了一些不良影响。Eclass 继承其他 Eclass 以获得单个功能——这样做会修改导出的“模板”(默认的 src_compile、默认的 src_unpack、各种变量等)。所有 eclass 设计师想要的只是重用一个函数,而不是让他们的 eclass 对其继承的 eclass 模板中的更改敏感。Eclass 设计师应该了解他们正在使用的函数的更改,但不应该担心他们的默认 src_* 和 pkg_* 函数被覆盖,更不用说环境更改了。

提前说明为什么将一系列 eclass 改进合并到一组更改中,此提案的某些部分可以拆分为多个阶段。为什么要这样做呢?对于开发人员来说,知道第一个 eclass 规范是什么,第二个规范是什么,比要求他们了解 eclass 更改的哪个阶段正在进行中要简单得多。

通过将所有更改合并到一个大的更改中,有意地在沙滩上划了一条线。旧的 eclass 允许这样做,并以这种方式运行。新的 eclass 允许这样做,并以这种方式运行。这应该减少对 eclass 允许/可能做什么的误解,从而减少由此产生的错误。

关于 elib 的一些说明——可以将它们视为 eclass 的行为功能和库功能之间的明确定义。Eclass 修改模板数据,并且是其他 ebuild 的基础——但是,elib 仅仅是通用的 bash 功能。

考虑大多数 portage bin/* 脚本——这些脚本都可以作为 elib 添加到树中,eutils 的大部分内容也是如此。

规范

此提案的各个部分被分解成一系列更改,并详细说明了为什么建议的更改更好。建议读者按顺序阅读,而不是跳来跳去。

Ebuild 库(简称 elib)

正如在动机和基本原理中简要提到的,最初的 eclass 设计允许 eclass 修改 ebuild 的元数据,元数据是指 DEPENDS、RDEPENDS、SRC_URI、IUSE 等需要保持不变的变量,并由 portage 用于依赖项解析、获取等。使用前面的示例,如果您想要来自 eclass 的单个函数(例如 eutils 中的 epatch),您不希望修改您继承的 eclass 可能进行的元数据。您希望将您从中提取的 eclass 视为一个库,纯属简单。

应在树的顶层添加一个名为 elib 的新目录,用作 ebuild 函数库的存储库。而不是依赖于使用 source 命令,应向 portage 添加一个“elib”函数来导入库的功能。通过函数进行间接调用的原因主要与 portage 内部相关,但它确实作为一个抽象,这样(例如)zsh 兼容性 hack 可以隐藏在 elib 函数中。

Elib 将是 bash 函数的集合——除了函数定义和绝对必要的库的任何最小初始化之外,它们不允许在全局范围内执行任何操作。此外,它们不能修改任何 ebuild 模板函数——src_compile、src_unpack。由于它们需要不修改元数据键,也不以任何方式影响 ebuild,除了提供功能之外,它们可以有条件地被拉入。它们还可以拉入其他 elib,但严格来说只是 elib——没有 eclass,只有其他 elib。一个现实世界的例子是 eutils eclass。

由于 elib 不修改元数据,因此 Portage 不需要像跟踪 eclass 一样跟踪 elib。因此,当 elib 更改时,不会导致树的一半被迫重新生成/标记为陈旧(这更多是基础设施的好处,尽管由于 eclass 更改而导致的生成时间过长会导致由于时间戳丢失而导致 rsync 问题)。

Elib 在 eclass 或 ebuild 的全局范围内不可用——也不在依赖项阶段(基本上是获取 ebuild 以获取其元数据的阶段)。全局范围内的 Elib 调用将被跟踪,但 elib 不会加载到设置阶段(pkg_setup)之前。有两个原因——首先,它确保 elib 完全无法修改元数据。没有混淆的空间,elib 的延迟加载为您提供了所有阶段的功能,除了依赖项——依赖项是唯一能够指定元数据的阶段。其次,作为一个额外的好处,延迟加载减少了为重新生成而获取的 bash 数量——更快的重新生成。但是,这很小,并且是第一个原因的附带好处。

Elib 还有一些进一步的限制——主要是,要加载的 elib 只能在全局范围或设置、解包、编译、测试和安装阶段指定。您不能在 prerm、postrm、preinst 和 postinst 中加载 elib。原因是,对于 *rm 阶段,已安装的包将不得不查找树以获取 elib,这会导致 API 漂移导致中断。对于 *inst 阶段,也是如此,只不过罪魁祸首是 binpkgs。

有一个最终限制——elib 不能根据 API 更改其导出的 API(例如,某些 eclass 那样)。原因主要是 elib 只加载一次——而不是多次,就像 eclass 一样。

澄清一下,例如,这无效。

if [[ -n ${SOME_VAR} ]]; then
        func x() { echo "I'm accessible only via tweaking some var";}
else
        func x() { echo "this is invalid, do not do it."; }
fi

关于 elib 的可维护性,它应该比旧的 eclass 负担更小。旧的 eclass 的主要问题之一是它们的函数非常乱伦——它们与定义它们的 env 密切绑定。这使得 eclass 函数有点脆弱——对 elib 中可以做什么和不能做什么的限制将解决这个问题,使功能更不脆弱(因此更易于维护)。

不需要与 elib 向后兼容——它们只需要针对当前树工作即可。因此,当树不再需要 elib 时,可以删除它们。下面解释了这样做的原因。

elib 目录的结构将与新的 eclass 目录(如下所述)完全相同,只是扩展名不同。

至于为什么有这么多限制,答案很简单——elib 是什么、它们能够做什么以及如何使用它们的定义尽可能地确定下来,以避免与它们相关的任何歧义。目的是使其清晰,以免产生误解,从而导致错误。

Eclass 的作用减少,以及对现有 Eclass 要求的澄清

由于 elib 现在旨在保存通用的 bash 功能,因此 eclass 的重点应该是定义适合 ebuild 的模板。例如,定义通用的 DEPENDS、RDEPENDS、src_compile 函数、src_unpack 等。此外,eclass 应该拉入它们需要的任何 elib 以实现功能。

与元数据或 src_* 和 pkg_* 函数无关的 eclass 功能应转移到 elib 中,以实现最大的代码重用。但这并不是硬性要求,而只是一个措辞强烈的建议。

以前,开发人员“强烈”建议避免在全局范围内执行任何不需要的代码。此建议现在成为一项要求。仅执行全局范围内必须执行的操作。在全局范围内执行的与配置/构建包相关的任何代码都必须放在 pkg_setup 中。元数据键(已经是一条规则,但现在作为绝对要求来澄清它)必须是常量。系统 A 上从 ebuild 导出的元数据键的结果必须与系统 B 上导出的键完全相同。

如果 eclass(或 ebuild)违反了此常量要求,则会导致 portage 对 rsync 用户执行错误的操作——例如,拉入了错误的依赖项,导致编译失败或依赖项失效。

如果现有的元数据对于包所需的灵活性不够,则更改元数据的解析以解决此问题。已知存在违反常量要求的情况,并且允许少数情况——这些是由于 portage 的不足而导致的规则例外。在确定可能需要违反常量要求的任何情况下,开发人员必须让大多数开发人员以及 portage 开发人员了解情况。这应该在提交之前完成。

很可能有一种方法可以允许您尝试的操作——如果您只是去做,rsync 用户(我们的用户群)将遭受编译失败和拉入不需要的依赖项的结果。

在进行了严厉的提醒之后,回到新的 eclass。在 eclass 中不再需要定义 INHERITED 和 ECLASS。如果未定义这些变量,Portage 已经处理了它们。

与elib类似,不再需要无限期地维护向后兼容性 - 必须针对当前的代码树维护兼容性,仅此而已。因此,一旦不再使用新的eclass(下一节将详细说明新旧eclass的真正区别),就可以将其从代码树中删除。

向后兼容性的终结…

对于当前的eclass,一旦eclass被使用,其API就不能再更改,也不能从代码树中删除。这就是为什么我们仍然在代码树中保留着完全未使用的古老的eclass,例如inherit.eclass。毫不奇怪,其原因是portage的一个缺陷:在取消合并已安装的ebuild时,portage使用了当前代码树中的eclass。

举个真实的例子,如果您在两年前合并了glibc,那么它使用的任何eclass都必须仍然兼容,否则您可能无法在升级到较新版本时取消合并旧版本的glibc。因此,glibc维护者要么只能选择让使用旧版本的用户自生自灭,要么在任何使用的eclass中维护不断增长的向后兼容性冗余代码。

Binpkgs也面临类似的命运。合并binpkg会从代码树中提取所需的eclass,因此如果eclass的API发生了更改,您甚至可能无法合并binpkg。如果eclass被删除,则您根本无法合并binpkg。

portage的下一个主要版本将解决这个问题 - ebuild构建的环境已经包含了eclass函数,因此可以重用该环境,而不是依赖于eclass。换句话说,binpkgs和已安装的ebuild将不再从代码树中提取所需的eclass,而是使用它们构建/合并时“保存”的eclass版本。

因此,对于下一个主要portage版本(及以后版本)的用户,不再需要向后兼容性要求。所有冗余代码都可以删除。

问题在于,有些用户使用的是不支持此功能的旧版portage - 这些旧安装无法使用新的eclass,因为它们的portage版本无法正确依赖于环境 - 换句话说,eclass的不同API会导致用户在取消合并期间遇到明显的错误。

因此,我们可以完全清除所有旧的eclass和API冗余代码,但我们需要一种方法来基本上禁止所有无法正确处理环境要求的portage版本访问新的eclass。

不幸的是,我们不能仅仅依赖于旧的eclass目录中的不同分组/命名约定。新的eclass必须不可访问,而portage在此方面设置了一个障碍 - 用于处理现有eclass的现有inherit函数。基本上,无论传递给它什么(inherit kernel或inherit kernel/kernel),它都会引入(kernel.eclass和kernel/kernel.eclass)。因此,即使新的eclass在代码树中eclass目录的子目录中实现,所有当前的portage版本仍然可以访问它们。

换句话说,这些新的eclass实际上将成为旧的eclass,因为旧版的portage仍然可以访问它们。

树结构重构

只有两种方法可以阻止现有的(截至本文撰写之时)inherit功能访问新的eclass - 或者将eclass的扩展名更改为除'eclass'之外的其他名称,或者将它们存储在代码树中eclass目录之外的单独子目录中。

后者更可取,也是建议的解决方案。原因是 - 当前的eclass目录已经过度增长。新的eclass目录的结构(如下所述)将允许更容易地进行签名、更改日志和eclass分组。新的eclass允许类似于干净的断裂并具有新的功能/要求,因此建议从一个干净的目录开始,不包含旧eclass实现的所有冗余代码。

如果不清楚为什么旧的inherit函数无法访问新的eclass,请重新阅读上一节。不幸的是,这是利用portage下一个主要版本将允许的所有功能的必要条件。

建议的目录结构为${PORTDIR}/include/{eclass,elib}。可以使用类似${PORTDIR}/new-eclass或${PORTDIR}/eclass-ng的内容(尽管许多人会对-ng感到畏缩),但这样的名称是不明智的。考虑一下(很可能是一个事实)新的eclass有一天可能会发现存在不足,并需要进一步改进(例如第三版)。或者也许我们想添加更多与源文件相关的功能,然后我们将需要进一步填充${PORTDIR}。

new-eclass目录将至少有两层深度 - 例如

::
kernel/ kernel/linux-info.eclass kernel/linux-mod.eclass kernel/kernel-2.6.eclass kernel/kernel-2.4.eclass kernel/ChangeLog kernel/Manifest

基本目录中不允许存在eclass - 需要对新的eclass进行分组以帮助保持整洁,并出于以下原因。eclass的分组允许添加特定于该组eclass的更改日志,根据需要分组文件/补丁,并允许更合理/更容易地对eclass进行签名 - 您只需在该分组中粘贴一个签名的Manifest文件,从而提供portage确保文件完整且未被篡改所需的信息。

elib目录将以相同的方式构建,原因相同。

Repoman将需要扩展以在新的eclass和elib组中工作,并处理签名和提交。这是有意的,也是一件好事。这使repoman能够对elib/新的eclass进行健全性检查。

请注意,这些检查不会阻止开发人员对eclass进行愚蠢的操作 - 这些检查只能执行基本的健全性检查,例如语法检查。无法阻止人们做愚蠢的事情(除非也许反复使用电击棒) - 这些严格是自动检查,类似于repoman的依赖项检查。

不同阶段向后兼容性的开始

如上所述,新的eclass将存在于一个单独的目录中,该目录将有意无法被inherit函数访问。因此,旧版portage的用户必须升级才能合并任何使用elib/新eclass的ebuild。对下一个主要portage版本的依赖项将为rsync用户透明地处理此问题。

仍然存在尚未升级到所需portage版本的用户的问题。坦率地说,这是一个次要问题 - portage版本包含新功能和错误修复。如果他们不升级,则假定他们有自己的理由并且是成年人,因此能够自己处理复杂情况。

真正的问题是损坏的环境,无论是在binpkgs中还是在已安装的包中。存在两种选择 - 要么旧的eclass无限期地保留在代码树中,要么保留N个月,然后从代码树中移出并放入可以合并的tarball中。

将它们从代码树中移出是可取的,原因有几个 - 代码树中冗余代码减少,但更重要的是它们未签名(因此存在攻击角度)。请注意,建议的eclass签名方法甚至没有尝试解决它们。坦率地说,支持两种eclass签名变化不值得付出努力,因为旧的eclass设置并非旨在易于签名。

如果采用这种方法,则要么必须将旧的eclass合并到覆盖目录的eclass目录(难看),要么合并到portage的inherit函数知道要查找的安全位置(不那么难看)。

对于在旧的eclass位于代码树中的N个月窗口内未升级的用户,如前所述,假定他们知道自己在做什么。如果他们专门阻止了新的portage版本,随着代码树中的ebuild迁移到新的eclass,他们可用的ebuild将越来越少。如果他们尝试注入新的portage版本(本质上是欺骗portage),portage将退出,因为它找不到新的eclass。对于使用新eclass的ebuild,实际上没有任何方法可以规避portage版本要求 - 与其他portage功能一样。

更令人烦恼的是,一旦旧的eclass从代码树中删除,如果用户尚未升级到支持环境处理的portage版本,他们将失去取消合并任何使用旧eclass的已安装ebuild的能力。相同的原因,不同的症状是他们也将失去合并任何使用旧eclass的tbz2的能力。

还有一个很少见的情况,但应该注意 - 如果用户已遭受已安装包数据库(vdb)的严重损坏。这里忽略了vdb此时是否可用,但由于以下两种情况之一,保存的环境可能无法使用:A)丢失,或B)损坏。在这种情况下,即使使用新的portage功能,他们也需要旧的eclass兼容ebuild。

请注意,要发生这种情况,要么需要相当......不明智的root使用方式,要么需要严重的fs损坏。无论原因是什么,这种情况甚至成为问题都很可能,系统的vdb完全不可用。在这一点上,这是一个无关紧要的问题。如果您丢失了vdb或它严重损坏,则类似于对portage进行切除手术 - 它不知道安装了什么,它不知道自己的文件,总的来说,重建系统几乎是唯一明智的行动方案。在这种情况下,丢失的环境确实是用户最不关心的问题。

继续讨论更可能的情况,不愿升级portage的用户不会被抛弃。合并旧的eclass兼容ebuild将提供缺少的eclass,从而提供丢失的功能。

请注意,我们的意图不是强迫他们升级,因此能够恢复丢失的功能。我们的意图是清理现有的混乱,并让我们继续前进。俗话说“为了做煎蛋卷,必须打破几个鸡蛋”与此类似,除了我们提供了一种使鸡蛋完整的方法(国王的侍卫们会喜欢这种选择)。

迁移到新设置

过去,每当代码树中的更改导致ebuild需要特定版本的portage时,都会这样做,随着ebuild迁移到新的eclass,它们应该依赖于支持它的portage版本。从用户的角度来看,这可以透明地处理迁移。

然而,对于开发人员或特定的基础设施服务器来说,这并不那么透明。开发人员由于使用cvs来管理他们的代码树,因此缺乏rsync用户拥有的预生成缓存。开发人员必须成为新portage的早期采用者。旧版本的portage将无法访问新的eclass,因此该ebuild的本地缓存生成将失败,因此对较新portage版本的依赖项将无法为他们透明地处理它。

此外,在代码树中的任何ebuild使用新的eclass之前,为rsync用户生成缓存的基础设施服务器必须升级到支持新eclass的portage版本,或打补丁。对于portage开发人员来说,前者比后者更可取。

除此之外,必须确定旧的eclass在代码树中存在的适当窗口,并且在该窗口过去之前,必须将ebuild添加到代码树中,以便用户在需要时获取旧的eclass。

对于eclass开发人员从旧版本迁移到新版本,他们可以将旧的eclass转移到新eclass目录中适当的分组中,尽管建议他们清除eclass中的所有冗余代码。您可以逐步将ebuild迁移到新的eclass,并且不必担心必须支持X年前的ebuild。

从本质上讲,您有机会完美/干净地完成设计,并有一个窗口来重新设计它。建议eclass开发人员利用它。 :)

向后兼容性

所有向后兼容性问题都已在文中得到解决,但这里提供了一个回顾——建议如果读者对某个特定的兼容性问题有疑问/担心,请阅读相关章节。该章节应该对问题进行更深入的讨论,并对潜在的解决方案以及选择该方案的原因进行更详细的解释。

回顾

New eclasses and elib functionality will be tied to a specific portage
version.  A DEPENDs on said portage version should address this for rsync
users who refuse to upgrade to a portage version that supports the new
eclasses/elibs and will gradually be unable to merge ebuilds that use said
functionality.  It is their choice to upgrade, as such, the gradual
'thinning' of available ebuilds should they block the portage upgrade is
their responsibility.

Old eclasses at some point in the future should be removed from the tree,
and released in a tarball/ebuild.  This will cause installed ebuilds that
rely on the old eclass to be unable to unmerge, with the same applying for
merging of binpkgs dependent on the following paragraph.

The old eclass-compat is only required for users who do not upgrade their
portage installation, and one further exemption- if the user has somehow
corrupted/destroyed their installed pkgs database (/var/db/pkg currently),
in the process, they've lost their saved environments.  The eclass-compat
ebuild would be required for ebuilds that required older eclasses in such a
case.  Note, this case is rare also- as clarified above, it's mentioned
strictly to be complete, it's not much of a real world scenario as elaborated
above.