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