blob: 18ec5c895de9b902be3486b8ca55aab8b90bd701 [file] [log] [blame]
Andrew Geissler635e0e42020-08-21 15:58:33 -05001#
Patrick Williams92b42cb2022-09-03 06:53:57 -05002# Copyright OpenEmbedded Contributors
3#
Andrew Geissler635e0e42020-08-21 15:58:33 -05004# 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))
Andrew Geisslerd1e89492021-02-12 15:35:20 -060038 # 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
Andrew Geissler635e0e42020-08-21 15:58:33 -050041 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
Andrew Geissler6ce62a22020-11-30 19:58:47 -060051class PMPkgsList(PkgsList):
Andrew Geissler635e0e42020-08-21 15:58:33 -050052 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 open(confdir + "arch", 'w').write(":".join(archs))
102 distro_codename = self.d.getVar('DISTRO_CODENAME')
103 open(confdir + "releasever", 'w').write(distro_codename if distro_codename is not None else '')
104
105 open(oe.path.join(self.target_rootfs, "etc/dnf/dnf.conf"), 'w').write("")
106
107
108 def _configure_rpm(self):
109 # We need to configure rpm to use our primary package architecture as the installation architecture,
110 # and to make it compatible with other package architectures that we use.
111 # Otherwise it will refuse to proceed with packages installation.
112 platformconfdir = "%s/%s" %(self.target_rootfs, "etc/rpm/")
113 rpmrcconfdir = "%s/%s" %(self.target_rootfs, "etc/")
114 bb.utils.mkdirhier(platformconfdir)
115 open(platformconfdir + "platform", 'w').write("%s-pc-linux" % self.primary_arch)
116 with open(rpmrcconfdir + "rpmrc", 'w') as f:
117 f.write("arch_compat: %s: %s\n" % (self.primary_arch, self.archs if len(self.archs) > 0 else self.primary_arch))
118 f.write("buildarch_compat: %s: noarch\n" % self.primary_arch)
119
120 open(platformconfdir + "macros", 'w').write("%_transaction_color 7\n")
121 if self.d.getVar('RPM_PREFER_ELF_ARCH'):
122 open(platformconfdir + "macros", 'a').write("%%_prefer_color %s" % (self.d.getVar('RPM_PREFER_ELF_ARCH')))
123
124 if self.d.getVar('RPM_SIGN_PACKAGES') == '1':
125 signer = get_signer(self.d, self.d.getVar('RPM_GPG_BACKEND'))
126 pubkey_path = oe.path.join(self.d.getVar('B'), 'rpm-key')
127 signer.export_pubkey(pubkey_path, self.d.getVar('RPM_GPG_NAME'))
128 rpm_bin = bb.utils.which(os.getenv('PATH'), "rpmkeys")
129 cmd = [rpm_bin, '--root=%s' % self.target_rootfs, '--import', pubkey_path]
130 try:
131 subprocess.check_output(cmd, stderr=subprocess.STDOUT)
132 except subprocess.CalledProcessError as e:
133 bb.fatal("Importing GPG key failed. Command '%s' "
134 "returned %d:\n%s" % (' '.join(cmd), e.returncode, e.output.decode("utf-8")))
135
136 def create_configs(self):
137 self._configure_dnf()
138 self._configure_rpm()
139
140 def write_index(self):
141 lockfilename = self.d.getVar('DEPLOY_DIR_RPM') + "/rpm.lock"
142 lf = bb.utils.lockfile(lockfilename, False)
143 RpmIndexer(self.d, self.rpm_repo_dir).write_index()
144 bb.utils.unlockfile(lf)
145
146 def insert_feeds_uris(self, feed_uris, feed_base_paths, feed_archs):
147 from urllib.parse import urlparse
148
149 if feed_uris == "":
150 return
151
152 gpg_opts = ''
153 if self.d.getVar('PACKAGE_FEED_SIGN') == '1':
154 gpg_opts += 'repo_gpgcheck=1\n'
155 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'))
156
157 if self.d.getVar('RPM_SIGN_PACKAGES') != '1':
158 gpg_opts += 'gpgcheck=0\n'
159
160 bb.utils.mkdirhier(oe.path.join(self.target_rootfs, "etc", "yum.repos.d"))
161 remote_uris = self.construct_uris(feed_uris.split(), feed_base_paths.split())
162 for uri in remote_uris:
163 repo_base = "oe-remote-repo" + "-".join(urlparse(uri).path.split("/"))
164 if feed_archs is not None:
165 for arch in feed_archs.split():
166 repo_uri = uri + "/" + arch
167 repo_id = "oe-remote-repo" + "-".join(urlparse(repo_uri).path.split("/"))
168 repo_name = "OE Remote Repo:" + " ".join(urlparse(repo_uri).path.split("/"))
169 open(oe.path.join(self.target_rootfs, "etc", "yum.repos.d", repo_base + ".repo"), 'a').write(
170 "[%s]\nname=%s\nbaseurl=%s\n%s\n" % (repo_id, repo_name, repo_uri, gpg_opts))
171 else:
172 repo_name = "OE Remote Repo:" + " ".join(urlparse(uri).path.split("/"))
173 repo_uri = uri
174 open(oe.path.join(self.target_rootfs, "etc", "yum.repos.d", repo_base + ".repo"), 'w').write(
175 "[%s]\nname=%s\nbaseurl=%s\n%s" % (repo_base, repo_name, repo_uri, gpg_opts))
176
177 def _prepare_pkg_transaction(self):
178 os.environ['D'] = self.target_rootfs
179 os.environ['OFFLINE_ROOT'] = self.target_rootfs
180 os.environ['IPKG_OFFLINE_ROOT'] = self.target_rootfs
181 os.environ['OPKG_OFFLINE_ROOT'] = self.target_rootfs
182 os.environ['INTERCEPT_DIR'] = self.intercepts_dir
183 os.environ['NATIVE_ROOT'] = self.d.getVar('STAGING_DIR_NATIVE')
184
185
Andrew Geissler615f2f12022-07-15 14:00:58 -0500186 def install(self, pkgs, attempt_only=False, hard_depends_only=False):
Andrew Geissler635e0e42020-08-21 15:58:33 -0500187 if len(pkgs) == 0:
188 return
189 self._prepare_pkg_transaction()
190
191 bad_recommendations = self.d.getVar('BAD_RECOMMENDATIONS')
192 package_exclude = self.d.getVar('PACKAGE_EXCLUDE')
193 exclude_pkgs = (bad_recommendations.split() if bad_recommendations else []) + (package_exclude.split() if package_exclude else [])
194
195 output = self._invoke_dnf((["--skip-broken"] if attempt_only else []) +
196 (["-x", ",".join(exclude_pkgs)] if len(exclude_pkgs) > 0 else []) +
Andrew Geissler615f2f12022-07-15 14:00:58 -0500197 (["--setopt=install_weak_deps=False"] if (hard_depends_only or self.d.getVar('NO_RECOMMENDATIONS') == "1") else []) +
Andrew Geissler635e0e42020-08-21 15:58:33 -0500198 (["--nogpgcheck"] if self.d.getVar('RPM_SIGN_PACKAGES') != '1' else ["--setopt=gpgcheck=True"]) +
199 ["install"] +
200 pkgs)
201
202 failed_scriptlets_pkgnames = collections.OrderedDict()
203 for line in output.splitlines():
William A. Kennington IIIac69b482021-06-02 12:28:27 -0700204 if line.startswith("Error: Systemctl"):
205 bb.error(line)
206
Andrew Geissler635e0e42020-08-21 15:58:33 -0500207 if line.startswith("Error in POSTIN scriptlet in rpm package"):
208 failed_scriptlets_pkgnames[line.split()[-1]] = True
209
210 if len(failed_scriptlets_pkgnames) > 0:
211 failed_postinsts_abort(list(failed_scriptlets_pkgnames.keys()), self.d.expand("${T}/log.do_${BB_CURRENTTASK}"))
212
213 def remove(self, pkgs, with_dependencies = True):
214 if not pkgs:
215 return
216
217 self._prepare_pkg_transaction()
218
219 if with_dependencies:
220 self._invoke_dnf(["remove"] + pkgs)
221 else:
222 cmd = bb.utils.which(os.getenv('PATH'), "rpm")
223 args = ["-e", "-v", "--nodeps", "--root=%s" %self.target_rootfs]
224
225 try:
226 bb.note("Running %s" % ' '.join([cmd] + args + pkgs))
227 output = subprocess.check_output([cmd] + args + pkgs, stderr=subprocess.STDOUT).decode("utf-8")
228 bb.note(output)
229 except subprocess.CalledProcessError as e:
230 bb.fatal("Could not invoke rpm. Command "
231 "'%s' returned %d:\n%s" % (' '.join([cmd] + args + pkgs), e.returncode, e.output.decode("utf-8")))
232
233 def upgrade(self):
234 self._prepare_pkg_transaction()
235 self._invoke_dnf(["upgrade"])
236
237 def autoremove(self):
238 self._prepare_pkg_transaction()
239 self._invoke_dnf(["autoremove"])
240
241 def remove_packaging_data(self):
242 self._invoke_dnf(["clean", "all"])
243 for dir in self.packaging_data_dirs:
244 bb.utils.remove(oe.path.join(self.target_rootfs, dir), True)
245
246 def backup_packaging_data(self):
247 # Save the packaging dirs for increment rpm image generation
248 if os.path.exists(self.saved_packaging_data):
249 bb.utils.remove(self.saved_packaging_data, True)
250 for i in self.packaging_data_dirs:
251 source_dir = oe.path.join(self.target_rootfs, i)
252 target_dir = oe.path.join(self.saved_packaging_data, i)
253 if os.path.isdir(source_dir):
254 shutil.copytree(source_dir, target_dir, symlinks=True)
255 elif os.path.isfile(source_dir):
256 shutil.copy2(source_dir, target_dir)
257
258 def recovery_packaging_data(self):
259 # Move the rpmlib back
260 if os.path.exists(self.saved_packaging_data):
261 for i in self.packaging_data_dirs:
262 target_dir = oe.path.join(self.target_rootfs, i)
263 if os.path.exists(target_dir):
264 bb.utils.remove(target_dir, True)
265 source_dir = oe.path.join(self.saved_packaging_data, i)
266 if os.path.isdir(source_dir):
267 shutil.copytree(source_dir, target_dir, symlinks=True)
268 elif os.path.isfile(source_dir):
269 shutil.copy2(source_dir, target_dir)
270
271 def list_installed(self):
272 output = self._invoke_dnf(["repoquery", "--installed", "--queryformat", "Package: %{name} %{arch} %{version} %{name}-%{version}-%{release}.%{arch}.rpm\nDependencies:\n%{requires}\nRecommendations:\n%{recommends}\nDependenciesEndHere:\n"],
273 print_output = False)
274 packages = {}
275 current_package = None
276 current_deps = None
277 current_state = "initial"
278 for line in output.splitlines():
279 if line.startswith("Package:"):
280 package_info = line.split(" ")[1:]
281 current_package = package_info[0]
282 package_arch = package_info[1]
283 package_version = package_info[2]
284 package_rpm = package_info[3]
285 packages[current_package] = {"arch":package_arch, "ver":package_version, "filename":package_rpm}
286 current_deps = []
287 elif line.startswith("Dependencies:"):
288 current_state = "dependencies"
289 elif line.startswith("Recommendations"):
290 current_state = "recommendations"
291 elif line.startswith("DependenciesEndHere:"):
292 current_state = "initial"
293 packages[current_package]["deps"] = current_deps
294 elif len(line) > 0:
295 if current_state == "dependencies":
296 current_deps.append(line)
297 elif current_state == "recommendations":
298 current_deps.append("%s [REC]" % line)
299
300 return packages
301
302 def update(self):
303 self._invoke_dnf(["makecache", "--refresh"])
304
305 def _invoke_dnf(self, dnf_args, fatal = True, print_output = True ):
306 os.environ['RPM_ETCCONFIGDIR'] = self.target_rootfs
307
308 dnf_cmd = bb.utils.which(os.getenv('PATH'), "dnf")
309 standard_dnf_args = ["-v", "--rpmverbosity=info", "-y",
310 "-c", oe.path.join(self.target_rootfs, "etc/dnf/dnf.conf"),
311 "--setopt=reposdir=%s" %(oe.path.join(self.target_rootfs, "etc/yum.repos.d")),
312 "--installroot=%s" % (self.target_rootfs),
313 "--setopt=logdir=%s" % (self.d.getVar('T'))
314 ]
315 if hasattr(self, "rpm_repo_dir"):
316 standard_dnf_args.append("--repofrompath=oe-repo,%s" % (self.rpm_repo_dir))
317 cmd = [dnf_cmd] + standard_dnf_args + dnf_args
318 bb.note('Running %s' % ' '.join(cmd))
319 try:
320 output = subprocess.check_output(cmd,stderr=subprocess.STDOUT).decode("utf-8")
321 if print_output:
322 bb.debug(1, output)
323 return output
324 except subprocess.CalledProcessError as e:
325 if print_output:
326 (bb.note, bb.fatal)[fatal]("Could not invoke dnf. Command "
327 "'%s' returned %d:\n%s" % (' '.join(cmd), e.returncode, e.output.decode("utf-8")))
328 else:
329 (bb.note, bb.fatal)[fatal]("Could not invoke dnf. Command "
330 "'%s' returned %d:" % (' '.join(cmd), e.returncode))
331 return e.output.decode("utf-8")
332
333 def dump_install_solution(self, pkgs):
334 open(self.solution_manifest, 'w').write(" ".join(pkgs))
335 return pkgs
336
337 def load_old_install_solution(self):
338 if not os.path.exists(self.solution_manifest):
339 return []
340 with open(self.solution_manifest, 'r') as fd:
341 return fd.read().split()
342
343 def _script_num_prefix(self, path):
344 files = os.listdir(path)
345 numbers = set()
346 numbers.add(99)
347 for f in files:
348 numbers.add(int(f.split("-")[0]))
349 return max(numbers) + 1
350
351 def save_rpmpostinst(self, pkg):
352 bb.note("Saving postinstall script of %s" % (pkg))
353 cmd = bb.utils.which(os.getenv('PATH'), "rpm")
354 args = ["-q", "--root=%s" % self.target_rootfs, "--queryformat", "%{postin}", pkg]
355
356 try:
357 output = subprocess.check_output([cmd] + args,stderr=subprocess.STDOUT).decode("utf-8")
358 except subprocess.CalledProcessError as e:
359 bb.fatal("Could not invoke rpm. Command "
360 "'%s' returned %d:\n%s" % (' '.join([cmd] + args), e.returncode, e.output.decode("utf-8")))
361
362 # may need to prepend #!/bin/sh to output
363
364 target_path = oe.path.join(self.target_rootfs, self.d.expand('${sysconfdir}/rpm-postinsts/'))
365 bb.utils.mkdirhier(target_path)
366 num = self._script_num_prefix(target_path)
367 saved_script_name = oe.path.join(target_path, "%d-%s" % (num, pkg))
368 open(saved_script_name, 'w').write(output)
369 os.chmod(saved_script_name, 0o755)
370
371 def _handle_intercept_failure(self, registered_pkgs):
372 rpm_postinsts_dir = self.target_rootfs + self.d.expand('${sysconfdir}/rpm-postinsts/')
373 bb.utils.mkdirhier(rpm_postinsts_dir)
374
375 # Save the package postinstalls in /etc/rpm-postinsts
376 for pkg in registered_pkgs.split():
377 self.save_rpmpostinst(pkg)
378
379 def extract(self, pkg):
380 output = self._invoke_dnf(["repoquery", "--queryformat", "%{location}", pkg])
381 pkg_name = output.splitlines()[-1]
382 if not pkg_name.endswith(".rpm"):
383 bb.fatal("dnf could not find package %s in repository: %s" %(pkg, output))
384 pkg_path = oe.path.join(self.rpm_repo_dir, pkg_name)
385
386 cpio_cmd = bb.utils.which(os.getenv("PATH"), "cpio")
387 rpm2cpio_cmd = bb.utils.which(os.getenv("PATH"), "rpm2cpio")
388
389 if not os.path.isfile(pkg_path):
390 bb.fatal("Unable to extract package for '%s'."
391 "File %s doesn't exists" % (pkg, pkg_path))
392
393 tmp_dir = tempfile.mkdtemp()
394 current_dir = os.getcwd()
395 os.chdir(tmp_dir)
396
397 try:
398 cmd = "%s %s | %s -idmv" % (rpm2cpio_cmd, pkg_path, cpio_cmd)
399 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
400 except subprocess.CalledProcessError as e:
401 bb.utils.remove(tmp_dir, recurse=True)
402 bb.fatal("Unable to extract %s package. Command '%s' "
403 "returned %d:\n%s" % (pkg_path, cmd, e.returncode, e.output.decode("utf-8")))
404 except OSError as e:
405 bb.utils.remove(tmp_dir, recurse=True)
406 bb.fatal("Unable to extract %s package. Command '%s' "
407 "returned %d:\n%s at %s" % (pkg_path, cmd, e.errno, e.strerror, e.filename))
408
409 bb.note("Extracted %s to %s" % (pkg_path, tmp_dir))
410 os.chdir(current_dir)
411
412 return tmp_dir