1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7import shutil
8import subprocess
9from oe.package_manager import *
10
11class RpmIndexer(Indexer):
12    def write_index(self):
13        self.do_write_index(self.deploy_dir)
14
15    def do_write_index(self, deploy_dir):
16        if self.d.getVar('PACKAGE_FEED_SIGN') == '1':
17            signer = get_signer(self.d, self.d.getVar('PACKAGE_FEED_GPG_BACKEND'))
18        else:
19            signer = None
20
21        createrepo_c = bb.utils.which(os.environ['PATH'], "createrepo_c")
22        result = create_index("%s --update -q %s" % (createrepo_c, deploy_dir))
23        if result:
24            bb.fatal(result)
25
26        # Sign repomd
27        if signer:
28            sig_type = self.d.getVar('PACKAGE_FEED_GPG_SIGNATURE_TYPE')
29            is_ascii_sig = (sig_type.upper() != "BIN")
30            signer.detach_sign(os.path.join(deploy_dir, 'repodata', 'repomd.xml'),
31                               self.d.getVar('PACKAGE_FEED_GPG_NAME'),
32                               self.d.getVar('PACKAGE_FEED_GPG_PASSPHRASE_FILE'),
33                               armor=is_ascii_sig)
34
35class RpmSubdirIndexer(RpmIndexer):
36    def write_index(self):
37        bb.note("Generating package index for %s" %(self.deploy_dir))
38        # Remove the existing repodata to ensure that we re-generate it no matter what
39        bb.utils.remove(os.path.join(self.deploy_dir, "repodata"), recurse=True)
40
41        self.do_write_index(self.deploy_dir)
42        for entry in os.walk(self.deploy_dir):
43            if os.path.samefile(self.deploy_dir, entry[0]):
44                for dir in entry[1]:
45                    if dir != 'repodata':
46                        dir_path = oe.path.join(self.deploy_dir, dir)
47                        bb.note("Generating package index for %s" %(dir_path))
48                        self.do_write_index(dir_path)
49
50
51class PMPkgsList(PkgsList):
52    def list_pkgs(self):
53        return RpmPM(self.d, self.rootfs_dir, self.d.getVar('TARGET_VENDOR'), needfeed=False).list_installed()
54
55class RpmPM(PackageManager):
56    def __init__(self,
57                 d,
58                 target_rootfs,
59                 target_vendor,
60                 task_name='target',
61                 arch_var=None,
62                 os_var=None,
63                 rpm_repo_workdir="oe-rootfs-repo",
64                 filterbydependencies=True,
65                 needfeed=True):
66        super(RpmPM, self).__init__(d, target_rootfs)
67        self.target_vendor = target_vendor
68        self.task_name = task_name
69        if arch_var == None:
70            self.archs = self.d.getVar('ALL_MULTILIB_PACKAGE_ARCHS').replace("-","_")
71        else:
72            self.archs = self.d.getVar(arch_var).replace("-","_")
73        if task_name == "host":
74            self.primary_arch = self.d.getVar('SDK_ARCH')
75        else:
76            self.primary_arch = self.d.getVar('MACHINE_ARCH')
77
78        if needfeed:
79            self.rpm_repo_dir = oe.path.join(self.d.getVar('WORKDIR'), rpm_repo_workdir)
80            create_packages_dir(self.d, oe.path.join(self.rpm_repo_dir, "rpm"), d.getVar("DEPLOY_DIR_RPM"), "package_write_rpm", filterbydependencies)
81
82        self.saved_packaging_data = self.d.expand('${T}/saved_packaging_data/%s' % self.task_name)
83        if not os.path.exists(self.d.expand('${T}/saved_packaging_data')):
84            bb.utils.mkdirhier(self.d.expand('${T}/saved_packaging_data'))
85        self.packaging_data_dirs = ['etc/rpm', 'etc/rpmrc', 'etc/dnf', 'var/lib/rpm', 'var/lib/dnf', 'var/cache/dnf']
86        self.solution_manifest = self.d.expand('${T}/saved/%s_solution' %
87                                               self.task_name)
88        if not os.path.exists(self.d.expand('${T}/saved')):
89            bb.utils.mkdirhier(self.d.expand('${T}/saved'))
90
91    def _configure_dnf(self):
92        # libsolv handles 'noarch' internally, we don't need to specify it explicitly
93        archs = [i for i in reversed(self.archs.split()) if i not in ["any", "all", "noarch"]]
94        # This prevents accidental matching against libsolv's built-in policies
95        if len(archs) <= 1:
96            archs = archs + ["bogusarch"]
97        # This architecture needs to be upfront so that packages using it are properly prioritized
98        archs = ["sdk_provides_dummy_target"] + archs
99        confdir = "%s/%s" %(self.target_rootfs, "etc/dnf/vars/")
100        bb.utils.mkdirhier(confdir)
101        with open(confdir + "arch", 'w') as f:
102            f.write(":".join(archs))
103
104        distro_codename = self.d.getVar('DISTRO_CODENAME')
105        with open(confdir + "releasever", 'w') as f:
106            f.write(distro_codename if distro_codename is not None else '')
107
108        with open(oe.path.join(self.target_rootfs, "etc/dnf/dnf.conf"), 'w') as f:
109            f.write("")
110
111
112    def _configure_rpm(self):
113        # We need to configure rpm to use our primary package architecture as the installation architecture,
114        # and to make it compatible with other package architectures that we use.
115        # Otherwise it will refuse to proceed with packages installation.
116        platformconfdir = "%s/%s" %(self.target_rootfs, "etc/rpm/")
117        rpmrcconfdir = "%s/%s" %(self.target_rootfs, "etc/")
118        bb.utils.mkdirhier(platformconfdir)
119        with open(platformconfdir + "platform", 'w') as f:
120            f.write("%s-pc-linux" % self.primary_arch)
121        with open(rpmrcconfdir + "rpmrc", 'w') as f:
122            f.write("arch_compat: %s: %s\n" % (self.primary_arch, self.archs if len(self.archs) > 0 else self.primary_arch))
123            f.write("buildarch_compat: %s: noarch\n" % self.primary_arch)
124
125        with open(platformconfdir + "macros", 'w') as f:
126            f.write("%_transaction_color 7\n")
127        if self.d.getVar('RPM_PREFER_ELF_ARCH'):
128            with open(platformconfdir + "macros", 'a') as f:
129                f.write("%%_prefer_color %s" % (self.d.getVar('RPM_PREFER_ELF_ARCH')))
130
131        if self.d.getVar('RPM_SIGN_PACKAGES') == '1':
132            signer = get_signer(self.d, self.d.getVar('RPM_GPG_BACKEND'))
133            pubkey_path = oe.path.join(self.d.getVar('B'), 'rpm-key')
134            signer.export_pubkey(pubkey_path, self.d.getVar('RPM_GPG_NAME'))
135            rpm_bin = bb.utils.which(os.getenv('PATH'), "rpmkeys")
136            cmd = [rpm_bin, '--root=%s' % self.target_rootfs, '--import', pubkey_path]
137            try:
138                subprocess.check_output(cmd, stderr=subprocess.STDOUT)
139            except subprocess.CalledProcessError as e:
140                bb.fatal("Importing GPG key failed. Command '%s' "
141                        "returned %d:\n%s" % (' '.join(cmd), e.returncode, e.output.decode("utf-8")))
142
143    def create_configs(self):
144        self._configure_dnf()
145        self._configure_rpm()
146
147    def write_index(self):
148        lockfilename = self.d.getVar('DEPLOY_DIR_RPM') + "/rpm.lock"
149        lf = bb.utils.lockfile(lockfilename, False)
150        RpmIndexer(self.d, self.rpm_repo_dir).write_index()
151        bb.utils.unlockfile(lf)
152
153    def insert_feeds_uris(self, feed_uris, feed_base_paths, feed_archs):
154        from urllib.parse import urlparse
155
156        if feed_uris == "":
157            return
158
159        gpg_opts = ''
160        if self.d.getVar('PACKAGE_FEED_SIGN') == '1':
161            gpg_opts += 'repo_gpgcheck=1\n'
162            gpg_opts += 'gpgkey=file://%s/pki/packagefeed-gpg/PACKAGEFEED-GPG-KEY-%s-%s\n' % (self.d.getVar('sysconfdir'), self.d.getVar('DISTRO'), self.d.getVar('DISTRO_CODENAME'))
163
164        if self.d.getVar('RPM_SIGN_PACKAGES') != '1':
165            gpg_opts += 'gpgcheck=0\n'
166
167        bb.utils.mkdirhier(oe.path.join(self.target_rootfs, "etc", "yum.repos.d"))
168        remote_uris = self.construct_uris(feed_uris.split(), feed_base_paths.split())
169        for uri in remote_uris:
170            repo_base = "oe-remote-repo" + "-".join(urlparse(uri).path.split("/"))
171            if feed_archs is not None:
172                for arch in feed_archs.split():
173                    repo_uri = uri + "/" + arch
174                    repo_id   = "oe-remote-repo"  + "-".join(urlparse(repo_uri).path.split("/"))
175                    repo_name = "OE Remote Repo:" + " ".join(urlparse(repo_uri).path.split("/"))
176                    with open(oe.path.join(self.target_rootfs, "etc", "yum.repos.d", repo_base + ".repo"), 'a') as f:
177                        f.write("[%s]\nname=%s\nbaseurl=%s\n%s\n" % (repo_id, repo_name, repo_uri, gpg_opts))
178            else:
179                repo_name = "OE Remote Repo:" + " ".join(urlparse(uri).path.split("/"))
180                repo_uri = uri
181                with open(oe.path.join(self.target_rootfs, "etc", "yum.repos.d", repo_base + ".repo"), 'w') as f:
182                    f.write("[%s]\nname=%s\nbaseurl=%s\n%s" % (repo_base, repo_name, repo_uri, gpg_opts))
183
184    def _prepare_pkg_transaction(self):
185        os.environ['D'] = self.target_rootfs
186        os.environ['OFFLINE_ROOT'] = self.target_rootfs
187        os.environ['IPKG_OFFLINE_ROOT'] = self.target_rootfs
188        os.environ['OPKG_OFFLINE_ROOT'] = self.target_rootfs
189        os.environ['INTERCEPT_DIR'] = self.intercepts_dir
190        os.environ['NATIVE_ROOT'] = self.d.getVar('STAGING_DIR_NATIVE')
191
192
193    def install(self, pkgs, attempt_only=False, hard_depends_only=False):
194        if len(pkgs) == 0:
195            return
196        self._prepare_pkg_transaction()
197
198        bad_recommendations = self.d.getVar('BAD_RECOMMENDATIONS')
199        package_exclude = self.d.getVar('PACKAGE_EXCLUDE')
200        exclude_pkgs = (bad_recommendations.split() if bad_recommendations else []) + (package_exclude.split() if package_exclude else [])
201
202        output = self._invoke_dnf((["--skip-broken"] if attempt_only else []) +
203                         (["-x", ",".join(exclude_pkgs)] if len(exclude_pkgs) > 0 else []) +
204                         (["--setopt=install_weak_deps=False"] if (hard_depends_only or self.d.getVar('NO_RECOMMENDATIONS') == "1") else []) +
205                         (["--nogpgcheck"] if self.d.getVar('RPM_SIGN_PACKAGES') != '1' else ["--setopt=gpgcheck=True"]) +
206                         ["install"] +
207                         pkgs)
208
209        failed_scriptlets_pkgnames = collections.OrderedDict()
210        for line in output.splitlines():
211            if line.startswith("Error: Systemctl"):
212                bb.error(line)
213
214            if line.startswith("Error in POSTIN scriptlet in rpm package"):
215                failed_scriptlets_pkgnames[line.split()[-1]] = True
216
217        if len(failed_scriptlets_pkgnames) > 0:
218            failed_postinsts_abort(list(failed_scriptlets_pkgnames.keys()), self.d.expand("${T}/log.do_${BB_CURRENTTASK}"))
219
220    def remove(self, pkgs, with_dependencies = True):
221        if not pkgs:
222            return
223
224        self._prepare_pkg_transaction()
225
226        if with_dependencies:
227            self._invoke_dnf(["remove"] + pkgs)
228        else:
229            cmd = bb.utils.which(os.getenv('PATH'), "rpm")
230            args = ["-e", "-v", "--nodeps", "--root=%s" %self.target_rootfs]
231
232            try:
233                bb.note("Running %s" % ' '.join([cmd] + args + pkgs))
234                output = subprocess.check_output([cmd] + args + pkgs, stderr=subprocess.STDOUT).decode("utf-8")
235                bb.note(output)
236            except subprocess.CalledProcessError as e:
237                bb.fatal("Could not invoke rpm. Command "
238                     "'%s' returned %d:\n%s" % (' '.join([cmd] + args + pkgs), e.returncode, e.output.decode("utf-8")))
239
240    def upgrade(self):
241        self._prepare_pkg_transaction()
242        self._invoke_dnf(["upgrade"])
243
244    def autoremove(self):
245        self._prepare_pkg_transaction()
246        self._invoke_dnf(["autoremove"])
247
248    def remove_packaging_data(self):
249        self._invoke_dnf(["clean", "all"])
250        for dir in self.packaging_data_dirs:
251            bb.utils.remove(oe.path.join(self.target_rootfs, dir), True)
252
253    def backup_packaging_data(self):
254        # Save the packaging dirs for increment rpm image generation
255        if os.path.exists(self.saved_packaging_data):
256            bb.utils.remove(self.saved_packaging_data, True)
257        for i in self.packaging_data_dirs:
258            source_dir = oe.path.join(self.target_rootfs, i)
259            target_dir = oe.path.join(self.saved_packaging_data, i)
260            if os.path.isdir(source_dir):
261                shutil.copytree(source_dir, target_dir, symlinks=True)
262            elif os.path.isfile(source_dir):
263                shutil.copy2(source_dir, target_dir)
264
265    def recovery_packaging_data(self):
266        # Move the rpmlib back
267        if os.path.exists(self.saved_packaging_data):
268            for i in self.packaging_data_dirs:
269                target_dir = oe.path.join(self.target_rootfs, i)
270                if os.path.exists(target_dir):
271                    bb.utils.remove(target_dir, True)
272                source_dir = oe.path.join(self.saved_packaging_data, i)
273                if os.path.isdir(source_dir):
274                    shutil.copytree(source_dir, target_dir, symlinks=True)
275                elif os.path.isfile(source_dir):
276                    shutil.copy2(source_dir, target_dir)
277
278    def list_installed(self):
279        output = self._invoke_dnf(["repoquery", "--installed", "--queryformat", "Package: %{name} %{arch} %{version} %{name}-%{version}-%{release}.%{arch}.rpm\nDependencies:\n%{requires}\nRecommendations:\n%{recommends}\nDependenciesEndHere:\n"],
280                                  print_output = False)
281        packages = {}
282        current_package = None
283        current_deps = None
284        current_state = "initial"
285        for line in output.splitlines():
286            if line.startswith("Package:"):
287                package_info = line.split(" ")[1:]
288                current_package = package_info[0]
289                package_arch = package_info[1]
290                package_version = package_info[2]
291                package_rpm = package_info[3]
292                packages[current_package] = {"arch":package_arch, "ver":package_version, "filename":package_rpm}
293                current_deps = []
294            elif line.startswith("Dependencies:"):
295                current_state = "dependencies"
296            elif line.startswith("Recommendations"):
297                current_state = "recommendations"
298            elif line.startswith("DependenciesEndHere:"):
299                current_state = "initial"
300                packages[current_package]["deps"] = current_deps
301            elif len(line) > 0:
302                if current_state == "dependencies":
303                    current_deps.append(line)
304                elif current_state == "recommendations":
305                    current_deps.append("%s [REC]" % line)
306
307        return packages
308
309    def update(self):
310        self._invoke_dnf(["makecache", "--refresh"])
311
312    def _invoke_dnf(self, dnf_args, fatal = True, print_output = True ):
313        os.environ['RPM_ETCCONFIGDIR'] = self.target_rootfs
314
315        dnf_cmd = bb.utils.which(os.getenv('PATH'), "dnf")
316        standard_dnf_args = ["-v", "--rpmverbosity=info", "-y",
317                             "-c", oe.path.join(self.target_rootfs, "etc/dnf/dnf.conf"),
318                             "--setopt=reposdir=%s" %(oe.path.join(self.target_rootfs, "etc/yum.repos.d")),
319                             "--installroot=%s" % (self.target_rootfs),
320                             "--setopt=logdir=%s" % (self.d.getVar('T'))
321                            ]
322        if hasattr(self, "rpm_repo_dir"):
323            standard_dnf_args.append("--repofrompath=oe-repo,%s" % (self.rpm_repo_dir))
324        cmd = [dnf_cmd] + standard_dnf_args + dnf_args
325        bb.note('Running %s' % ' '.join(cmd))
326        try:
327            output = subprocess.check_output(cmd,stderr=subprocess.STDOUT).decode("utf-8")
328            if print_output:
329                bb.debug(1, output)
330            return output
331        except subprocess.CalledProcessError as e:
332            if print_output:
333                (bb.note, bb.fatal)[fatal]("Could not invoke dnf. Command "
334                     "'%s' returned %d:\n%s" % (' '.join(cmd), e.returncode, e.output.decode("utf-8")))
335            else:
336                (bb.note, bb.fatal)[fatal]("Could not invoke dnf. Command "
337                     "'%s' returned %d:" % (' '.join(cmd), e.returncode))
338            return e.output.decode("utf-8")
339
340    def dump_install_solution(self, pkgs):
341        with open(self.solution_manifest, 'w') as f:
342            f.write(" ".join(pkgs))
343        return pkgs
344
345    def load_old_install_solution(self):
346        if not os.path.exists(self.solution_manifest):
347            return []
348        with open(self.solution_manifest, 'r') as fd:
349            return fd.read().split()
350
351    def _script_num_prefix(self, path):
352        files = os.listdir(path)
353        numbers = set()
354        numbers.add(99)
355        for f in files:
356            numbers.add(int(f.split("-")[0]))
357        return max(numbers) + 1
358
359    def save_rpmpostinst(self, pkg):
360        bb.note("Saving postinstall script of %s" % (pkg))
361        cmd = bb.utils.which(os.getenv('PATH'), "rpm")
362        args = ["-q", "--root=%s" % self.target_rootfs, "--queryformat", "%{postin}", pkg]
363
364        try:
365            output = subprocess.check_output([cmd] + args,stderr=subprocess.STDOUT).decode("utf-8")
366        except subprocess.CalledProcessError as e:
367            bb.fatal("Could not invoke rpm. Command "
368                     "'%s' returned %d:\n%s" % (' '.join([cmd] + args), e.returncode, e.output.decode("utf-8")))
369
370        # may need to prepend #!/bin/sh to output
371
372        target_path = oe.path.join(self.target_rootfs, self.d.expand('${sysconfdir}/rpm-postinsts/'))
373        bb.utils.mkdirhier(target_path)
374        num = self._script_num_prefix(target_path)
375        saved_script_name = oe.path.join(target_path, "%d-%s" % (num, pkg))
376        with open(saved_script_name, 'w') as f:
377            f.write(output)
378        os.chmod(saved_script_name, 0o755)
379
380    def _handle_intercept_failure(self, registered_pkgs):
381        rpm_postinsts_dir = self.target_rootfs + self.d.expand('${sysconfdir}/rpm-postinsts/')
382        bb.utils.mkdirhier(rpm_postinsts_dir)
383
384        # Save the package postinstalls in /etc/rpm-postinsts
385        for pkg in registered_pkgs.split():
386            self.save_rpmpostinst(pkg)
387
388    def extract(self, pkg):
389        output = self._invoke_dnf(["repoquery", "--location", pkg])
390        pkg_name = output.splitlines()[-1]
391        if not pkg_name.endswith(".rpm"):
392            bb.fatal("dnf could not find package %s in repository: %s" %(pkg, output))
393        # Strip file: prefix
394        pkg_path = pkg_name[5:]
395
396        cpio_cmd = bb.utils.which(os.getenv("PATH"), "cpio")
397        rpm2cpio_cmd = bb.utils.which(os.getenv("PATH"), "rpm2cpio")
398
399        if not os.path.isfile(pkg_path):
400            bb.fatal("Unable to extract package for '%s'."
401                     "File %s doesn't exists" % (pkg, pkg_path))
402
403        tmp_dir = tempfile.mkdtemp()
404        current_dir = os.getcwd()
405        os.chdir(tmp_dir)
406
407        try:
408            cmd = "%s %s | %s -idmv" % (rpm2cpio_cmd, pkg_path, cpio_cmd)
409            output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
410        except subprocess.CalledProcessError as e:
411            bb.utils.remove(tmp_dir, recurse=True)
412            bb.fatal("Unable to extract %s package. Command '%s' "
413                     "returned %d:\n%s" % (pkg_path, cmd, e.returncode, e.output.decode("utf-8")))
414        except OSError as e:
415            bb.utils.remove(tmp_dir, recurse=True)
416            bb.fatal("Unable to extract %s package. Command '%s' "
417                     "returned %d:\n%s at %s" % (pkg_path, cmd, e.errno, e.strerror, e.filename))
418
419        bb.note("Extracted %s to %s" % (pkg_path, tmp_dir))
420        os.chdir(current_dir)
421
422        return tmp_dir
423