rpm 系 linux 系統中 /etc/yum.repo.d/ 目錄下的 .repo 檔案中的 $releasever 到底等於多少?

2022-06-18 12:00:42

rpm 系 linux 系統中 /etc/yum.repo.d/ 目錄下的 .repo 檔案中的 $releasever 到底等於多少?

結論

對於 8 來說,通過以下命令

#/usr/libexec/platform-python -c 'import dnf, json; db = dnf.dnf.Base(); print(json.dumps(db.conf.substitutions, indent=2))'
{
  "arch": "x86_64",
  "basearch": "x86_64",
  "releasever": "8"
}

對於 7 來說,通過以下命令

#python -c 'import yum, json; yb = yum.YumBase(); print json.dumps(yb.conf.yumvar, indent=2)'

或者通過以下命令獲取 releasever 的值,repo 為 @system 代表當前系統安裝的包。

#yum provides system-release\(releasever\)
anolis-release-8.2-15.an8.x86_64 : Anolis OS 8 release file
Repo        : @System
Matched from:
Provide    : system-release(releasever) = 8

anolis-release-8.2-15.an8.x86_64 : Anolis OS 8 release file
Repo        : BaseOS
Matched from:
Provide    : system-release(releasever) = 8

例子:

以下取自 stackover

# CentOS 8:
# ---
[root@0928d3917e32 /]# /usr/libexec/platform-python -c 'import dnf, json; db = dnf.dnf.Base(); print(json.dumps(db.conf.substitutions, indent=2))'
Failed to set locale, defaulting to C
{
  "arch": "x86_64",
  "basearch": "x86_64",
  "releasever": "8"
}
[root@0928d3917e32 /]# 


# CentOS 7:
# ---
[root@c41adb7f40c2 /]# python -c 'import yum, json; yb = yum.YumBase(); print json.dumps(yb.conf.yumvar, indent=2)'
Loaded plugins: fastestmirror, ovl
{
  "uuid": "cb5f5f60-d45c-4270-8c36-a4e64d2dece4", 
  "contentdir": "centos", 
  "basearch": "x86_64", 
  "infra": "container", 
  "releasever": "7", 
  "arch": "ia32e"
}
[root@c41adb7f40c2 /]# 

# CentOS 6:
# ---
[root@bfd11c9a0880 /]# python -c 'import yum, json; yb = yum.YumBase(); print json.dumps(yb.conf.yumvar, indent=2)'
Loaded plugins: fastestmirror, ovl
{
  "releasever": "6", 
  "basearch": "x86_64", 
  "arch": "ia32e", 
  "uuid": "3e0273f1-f5b6-481b-987c-b5f21dde4310", 
  "infra": "container"
}
[root@bfd11c9a0880 /]# 

原理

以 8 為例

/usr/libexec/platform-python -c 'import dnf, json; db = dnf.dnf.Base(); print(json.dumps(db.conf.substitutions, indent=2))'

{
  "arch": "x86_64",
  "basearch": "x86_64",
  "releasever": "8"
}

主要是以下幾行程式碼

import dnf, json
db = dnf.dnf.Base()
print(json.dumps(db.conf.substitutions, indent=2))

然後對原始碼進行粗略的分析

# /usr/lib/python3.6/site-packages/dnf/base.py
class Base(object):

    def __init__(self, conf=None):
        # :api
        self._closed = False
        self._conf = conf or self._setup_default_conf()
        self._goal = None
    # ...
    @staticmethod
    def _setup_default_conf():
        conf = dnf.conf.Conf()
        subst = conf.substitutions
        if 'releasever' not in subst:
            subst['releasever'] = \
                dnf.rpm.detect_releasever(conf.installroot)
        return conf

主要是這行程式碼

subst['releasever'] = \
                dnf.rpm.detect_releasever(conf.installroot)

首先 subst 是從 os.environ.items() varsdir=("/etc/yum/vars/", "/etc/dnf/vars/") 得來的,如果 releasever 這個鍵不在 subset 字典裡面,他就會去執行下面的程式碼 dnf.rpm.detect_releasever(conf.installroot)
這個函數我在下面也貼出來了,它是會遍歷以下元組,作為 distroverpkg 的值,通過該函數 ts.dbMatch('provides', distroverpkg) 去獲取對應值並提取 ,個人感覺這個類似於 yum provides 這個命令,然後他是按照順序搞的,所以如果第一個值能獲得 $releasever ,就選用該值。

DISTROVERPKG=('system-release(releasever)', 'system-release',
              'distribution-release(releasever)', 'distribution-release',
              'redhat-release', 'suse-release', 'anolis-release')

在我係統上測試,發現第一個 system-release(releasever) 就可以匹配到該值了,所以此時 $releasever 的值就是該值。

# yum provides system-release\(releasever\)
anolis-release-8.2-15.an8.x86_64 : Anolis OS 8 release file
Repo        : @System
Matched from:
Provide    : system-release(releasever) = 8

anolis-release-8.2-15.an8.x86_64 : Anolis OS 8 release file
Repo        : BaseOS
Matched from:
Provide    : system-release(releasever) = 8



以下是部分原始碼及路徑

#/usr/lib/python3.6/site-packages/dnf/conf/__init__.py
from dnf.conf.config import BaseConfig, MainConf, RepoConf

Conf = MainConf
#/usr/lib/python3.6/site-packages/dnf/conf/config.py
class MainConf(BaseConfig):
    # :api
    """Configuration option definitions for dnf.conf's [main] section."""
    def __init__(self, section='main', parser=None):
        self.substitutions = dnf.conf.substitutions.Substitutions()

#/usr/lib/python3.6/site-packages/dnf/conf/substitutions.py
class Substitutions(dict):
    # :api

    def __init__(self):
        super(Substitutions, self).__init__()
        self._update_from_env()

    def _update_from_env(self):
        numericvars = ['DNF%d' % num for num in range(0, 10)]
        for key, val in os.environ.items():
            if ENVIRONMENT_VARS_RE.match(key):
                self[key[8:]] = val  # remove "DNF_VAR_" prefix
            elif key in numericvars:
                self[key] = val

    def update_from_etc(self, installroot, varsdir=("/etc/yum/vars/", "/etc/dnf/vars/")):
        # :api

        for vars_path in varsdir:
            fsvars = []
            try:
                dir_fsvars = os.path.join(installroot, vars_path.lstrip('/'))
                fsvars = os.listdir(dir_fsvars)
            except OSError:
                continue
            for fsvar in fsvars:
                filepath = os.path.join(dir_fsvars, fsvar)
                if os.path.isfile(filepath):
                    try:
                        with open(filepath) as fp:
                            val = fp.readline()
                        if val and val[-1] == '\n':
                            val = val[:-1]
                    except (OSError, IOError):
                        continue
                self[fsvar] = val
# /usr/lib/python3.6/site-packages/dnf/rpm/__init__.py
def detect_releasever(installroot):
    # :api
    """Calculate the release version for the system."""

    ts = transaction.initReadOnlyTransaction(root=installroot)
    ts.pushVSFlags(~(rpm._RPMVSF_NOSIGNATURES | rpm._RPMVSF_NODIGESTS))
    for distroverpkg in dnf.const.DISTROVERPKG:
        if dnf.pycomp.PY3:
            distroverpkg = bytes(distroverpkg, 'utf-8')
        try:
            idx = ts.dbMatch('provides', distroverpkg)
        except (TypeError, rpm.error) as e:
            raise dnf.exceptions.Error('Error: %s' % str(e))
        if not len(idx):
            continue
        try:
            hdr = next(idx)
        except StopIteration:
            msg = 'Error: rpmdb failed to list provides. Try: rpm --rebuilddb'
            raise dnf.exceptions.Error(msg)
        releasever = hdr['version']
        try:
            try:
                # header returns bytes -> look for bytes
                # it may fail because rpm returns a decoded string since 10 Apr 2019
                off = hdr[rpm.RPMTAG_PROVIDENAME].index(distroverpkg)
            except ValueError:
                # header returns a string -> look for a string
                off = hdr[rpm.RPMTAG_PROVIDENAME].index(distroverpkg.decode("utf8"))
            flag = hdr[rpm.RPMTAG_PROVIDEFLAGS][off]
            ver = hdr[rpm.RPMTAG_PROVIDEVERSION][off]
            if flag == rpm.RPMSENSE_EQUAL and ver:
                if hdr['name'] not in (distroverpkg, distroverpkg.decode("utf8")):
                    # override the package version
                    releasever = ver
        except (ValueError, KeyError, IndexError):
            pass
# /usr/lib/python3.6/site-packages/dnf/const.py
DISTROVERPKG=('system-release(releasever)', 'system-release',
              'distribution-release(releasever)', 'distribution-release',
              'redhat-release', 'suse-release', 'anolis-release')

原理證明

既然經過一番探索,自然得驗證下,以下以 Anolis OS 8 系統為例子。

anolis-release 這個包的程式碼分析。原始碼路徑

anolis-release.spec 檔案中可以看到,這個包會提供 system-release(releasever) = %{base_release_version}

%{base_release_version} 在檔案定義中是 8,從 koji 的構建記錄也可以看出,確實會產出 system-release(releasever) = 8

%define anolis_release 15

%define debug_package %{nil}
%define product_family Anolis OS
%define base_release_version 8
%define full_release_version 8.2
%define compat_release_version 8
%define beta Beta
%define dist .an%{base_release_version}

Name:           anolis-release
Version:        %{full_release_version}
Release:        %{anolis_release}%{?dist}
Summary:        %{product_family} %{base_release_version} release file
Group:          System Environment/Base
License:        MulanPSLv2
Obsoletes:      rawhide-release redhat-release-as redhat-release-es redhat-release-ws redhat-release-de comps rpmdb-redhat fedora-release redhat-release centos-release
Provides:       redhat-release = %{full_release_version}
Provides:       centos-release = %{full_release_version}
Provides:       system-release = %{version}-%{release}
Provides:       system-release(releasever) = %{base_release_version}

該包在 koji 上的構建記錄 http://8.131.87.1/koji/rpminfo?rpmID=161662

Provides	
anolis-release = 8.2-13.an8
anolis-release(x86-64) = 8.2-13.an8
centos-release = 8.2
config(anolis-release) = 8.2-13.an8
redhat-release = 8.2
system-release = 8.2-13.an8
system-release(releasever) = 8

結論

然後,這個 system-release(releasever) 並不會像 os-release 一樣寫進系統檔案裡(/etc/os-release),但是確實會在系統中 provides ,而 $releasever 會按照元組中定義的順序選取值,最終選到了該值。

反推

既然這樣我是不是可以修改這個值,讓系統的 $releasever 顯示別的值,答案是可以的。

我通過修改 spec 檔案中的變數為 %define base_release_version 7

然後通過 mock 重新構建該包,安裝到我的系統上,成功讓 $releasever 這個變數變成了我想要的值 ( 如下 ),當然此時 yum 源可能會有問題,你可能需要修改 .repo檔案中 $releasever 為原來的值,再次下載 yum 源裡的 anolis-release 這個包,就可以恢復原來的值了。

# yum provides system-release\(releasever\)
Failed to set locale, defaulting to C.UTF-8
Repository AppStream is listed more than once in the configuration
Repository BaseOS is listed more than once in the configuration
Repository Plus is listed more than once in the configuration
Repository PowerTools is listed more than once in the configuration
Last metadata expiration check: 0:01:02 ago on Sat Jun 18 10:42:33 2022.
anolis-release-8.2-14.an8.x86_64 : Anolis OS 8 release file
Repo        : @System
Matched from:
Provide    : system-release(releasever) = 7

mock 構建命令如下

rpmbuild -bs ~/rpmbuild/SPECS/*.spec --define "dist ${dist}"
mock -r xx.cfg clean
mock -r xx.cfg init
mock -r xx.cfg rebuild xx.src.rpm

通過 rpm -Uvh 安裝構建出的 rpm 包

結語

想搞懂這個一個是對這方面比較好奇,而網上有很多誤導,比如網上很多說是看 /etc/os-release 的值,所以想自己去找找真實的答案;另一方面工作中遇到了這方面的問題,故某天熬到2點多,仔細探查了一番。

有可能,我的分析也不對,大家參考就行。✌️