poky/meta/lib/oe/package.py
Richard Purdie d5d35a27b4 lib/oe: Move vardepexclude entries alongside functions
Now we have decorators that can do this, move the variable dependencies
exclusions alongside the code that needs them for maintainability.

(From OE-Core rev: e522169c5f95de6fc74b43672573700d8eb8e082)

Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
2025-06-16 17:57:29 +01:00

2111 lines
81 KiB
Python

#
# Copyright OpenEmbedded Contributors
#
# SPDX-License-Identifier: GPL-2.0-only
#
import errno
import fnmatch
import itertools
import os
import shlex
import re
import glob
import stat
import mmap
import subprocess
import shutil
import bb.parse
import oe.cachedpath
def runstrip(file, elftype, strip, extra_strip_sections=''):
# Function to strip a single file, called from split_and_strip_files below
# A working 'file' (one which works on the target architecture)
#
# The elftype is a bit pattern (explained in is_elf below) to tell
# us what type of file we're processing...
# 4 - executable
# 8 - shared library
# 16 - kernel module
newmode = None
if not os.access(file, os.W_OK) or os.access(file, os.R_OK):
origmode = os.stat(file)[stat.ST_MODE]
newmode = origmode | stat.S_IWRITE | stat.S_IREAD
os.chmod(file, newmode)
stripcmd = [strip]
skip_strip = False
# kernel module
if elftype & 16:
if is_kernel_module_signed(file):
bb.debug(1, "Skip strip on signed module %s" % file)
skip_strip = True
else:
stripcmd.extend(["--strip-debug", "--remove-section=.comment",
"--remove-section=.note", "--preserve-dates"])
# .so and shared library
elif ".so" in file and elftype & 8:
stripcmd.extend(["--remove-section=.comment", "--remove-section=.note", "--strip-unneeded"])
# shared or executable:
elif elftype & 8 or elftype & 4:
stripcmd.extend(["--remove-section=.comment", "--remove-section=.note"])
if extra_strip_sections != '':
for section in extra_strip_sections.split():
stripcmd.extend(["--remove-section=" + section])
stripcmd.append(file)
bb.debug(1, "runstrip: %s" % stripcmd)
if not skip_strip:
output = subprocess.check_output(stripcmd, stderr=subprocess.STDOUT)
if newmode:
os.chmod(file, origmode)
# Detect .ko module by searching for "vermagic=" string
def is_kernel_module(path):
with open(path) as f:
return mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ).find(b"vermagic=") >= 0
# Detect if .ko module is signed
def is_kernel_module_signed(path):
with open(path, "rb") as f:
f.seek(-28, 2)
module_tail = f.read()
return "Module signature appended" in "".join(chr(c) for c in bytearray(module_tail))
# Return type (bits):
# 0 - not elf
# 1 - ELF
# 2 - stripped
# 4 - executable
# 8 - shared library
# 16 - kernel module
def is_elf(path):
exec_type = 0
result = subprocess.check_output(["file", "-b", path], stderr=subprocess.STDOUT).decode("utf-8")
if "ELF" in result:
exec_type |= 1
if "not stripped" not in result:
exec_type |= 2
if "executable" in result:
exec_type |= 4
if "shared" in result:
exec_type |= 8
if "relocatable" in result:
if path.endswith(".ko") and path.find("/lib/modules/") != -1 and is_kernel_module(path):
exec_type |= 16
return (path, exec_type)
def is_static_lib(path):
if path.endswith('.a') and not os.path.islink(path):
with open(path, 'rb') as fh:
# The magic must include the first slash to avoid
# matching golang static libraries
magic = b'!<arch>\x0a/'
start = fh.read(len(magic))
return start == magic
return False
def strip_execs(pn, dstdir, strip_cmd, libdir, base_libdir, max_process, qa_already_stripped=False):
"""
Strip executable code (like executables, shared libraries) _in_place_
- Based on sysroot_strip in staging.bbclass
:param dstdir: directory in which to strip files
:param strip_cmd: Strip command (usually ${STRIP})
:param libdir: ${libdir} - strip .so files in this directory
:param base_libdir: ${base_libdir} - strip .so files in this directory
:param max_process: number of stripping processes started in parallel
:param qa_already_stripped: Set to True if already-stripped' in ${INSANE_SKIP}
This is for proper logging and messages only.
"""
import stat, errno, oe.path, oe.utils
elffiles = {}
inodes = {}
libdir = os.path.abspath(dstdir + os.sep + libdir)
base_libdir = os.path.abspath(dstdir + os.sep + base_libdir)
exec_mask = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
#
# First lets figure out all of the files we may have to process
#
checkelf = []
inodecache = {}
for root, dirs, files in os.walk(dstdir):
for f in files:
file = os.path.join(root, f)
try:
ltarget = oe.path.realpath(file, dstdir, False)
s = os.lstat(ltarget)
except OSError as e:
(err, strerror) = e.args
if err != errno.ENOENT:
raise
# Skip broken symlinks
continue
if not s:
continue
# Check its an excutable
if s[stat.ST_MODE] & exec_mask \
or ((file.startswith(libdir) or file.startswith(base_libdir)) and ".so" in f) \
or file.endswith('.ko'):
# If it's a symlink, and points to an ELF file, we capture the readlink target
if os.path.islink(file):
continue
# It's a file (or hardlink), not a link
# ...but is it ELF, and is it already stripped?
checkelf.append(file)
inodecache[file] = s.st_ino
results = oe.utils.multiprocess_launch_mp(is_elf, checkelf, max_process)
for (file, elf_file) in results:
#elf_file = is_elf(file)
if elf_file & 1:
if elf_file & 2:
if qa_already_stripped:
bb.note("Skipping file %s from %s for already-stripped QA test" % (file[len(dstdir):], pn))
else:
bb.warn("File '%s' from %s was already stripped, this will prevent future debugging!" % (file[len(dstdir):], pn))
continue
if inodecache[file] in inodes:
os.unlink(file)
os.link(inodes[inodecache[file]], file)
else:
# break hardlinks so that we do not strip the original.
inodes[inodecache[file]] = file
bb.utils.break_hardlinks(file)
elffiles[file] = elf_file
#
# Now strip them (in parallel)
#
sfiles = []
for file in elffiles:
elf_file = int(elffiles[file])
sfiles.append((file, elf_file, strip_cmd))
oe.utils.multiprocess_launch_mp(runstrip, sfiles, max_process)
TRANSLATE = (
("@", "@at@"),
(" ", "@space@"),
("\t", "@tab@"),
("[", "@openbrace@"),
("]", "@closebrace@"),
("_", "@underscore@"),
(":", "@colon@"),
)
def file_translate(file):
ft = file
for s, replace in TRANSLATE:
ft = ft.replace(s, replace)
return ft
def file_reverse_translate(file):
ft = file
for s, replace in reversed(TRANSLATE):
ft = ft.replace(replace, s)
return ft
def filedeprunner(pkg, pkgfiles, rpmdeps, pkgdest):
import re, subprocess, shlex
provides = {}
requires = {}
file_re = re.compile(r'\s+\d+\s(.*)')
dep_re = re.compile(r'\s+(\S)\s+(.*)')
r = re.compile(r'[<>=]+\s+\S*')
def process_deps(pipe, pkg, pkgdest, provides, requires):
file = None
for line in pipe.split("\n"):
m = file_re.match(line)
if m:
file = m.group(1)
file = file.replace(pkgdest + "/" + pkg, "")
file = file_translate(file)
continue
m = dep_re.match(line)
if not m or not file:
continue
type, dep = m.groups()
if type == 'R':
i = requires
elif type == 'P':
i = provides
else:
continue
if dep.startswith("python("):
continue
# Ignore all perl(VMS::...) and perl(Mac::...) dependencies. These
# are typically used conditionally from the Perl code, but are
# generated as unconditional dependencies.
if dep.startswith('perl(VMS::') or dep.startswith('perl(Mac::'):
continue
# Ignore perl dependencies on .pl files.
if dep.startswith('perl(') and dep.endswith('.pl)'):
continue
# Remove perl versions and perl module versions since they typically
# do not make sense when used as package versions.
if dep.startswith('perl') and r.search(dep):
dep = dep.split()[0]
# Put parentheses around any version specifications.
dep = r.sub(r'(\g<0>)',dep)
if file not in i:
i[file] = []
i[file].append(dep)
return provides, requires
output = subprocess.check_output(shlex.split(rpmdeps) + pkgfiles, stderr=subprocess.STDOUT).decode("utf-8")
provides, requires = process_deps(output, pkg, pkgdest, provides, requires)
return (pkg, provides, requires)
def read_shlib_providers(d):
import re
shlib_provider = {}
shlibs_dirs = d.getVar('SHLIBSDIRS').split()
list_re = re.compile(r'^(.*)\.list$')
# Go from least to most specific since the last one found wins
for dir in reversed(shlibs_dirs):
bb.debug(2, "Reading shlib providers in %s" % (dir))
if not os.path.exists(dir):
continue
for file in sorted(os.listdir(dir)):
m = list_re.match(file)
if m:
dep_pkg = m.group(1)
try:
fd = open(os.path.join(dir, file))
except IOError:
# During a build unrelated shlib files may be deleted, so
# handle files disappearing between the listdirs and open.
continue
lines = fd.readlines()
fd.close()
for l in lines:
s = l.strip().split(":")
if s[0] not in shlib_provider:
shlib_provider[s[0]] = {}
shlib_provider[s[0]][s[1]] = (dep_pkg, s[2])
return shlib_provider
# We generate a master list of directories to process, we start by
# seeding this list with reasonable defaults, then load from
# the fs-perms.txt files
def fixup_perms(d):
import pwd, grp
cpath = oe.cachedpath.CachedPath()
dvar = d.getVar('PKGD')
# init using a string with the same format as a line as documented in
# the fs-perms.txt file
# <path> <mode> <uid> <gid> <walk> <fmode> <fuid> <fgid>
# <path> link <link target>
#
# __str__ can be used to print out an entry in the input format
#
# if fs_perms_entry.path is None:
# an error occurred
# if fs_perms_entry.link, you can retrieve:
# fs_perms_entry.path = path
# fs_perms_entry.link = target of link
# if not fs_perms_entry.link, you can retrieve:
# fs_perms_entry.path = path
# fs_perms_entry.mode = expected dir mode or None
# fs_perms_entry.uid = expected uid or -1
# fs_perms_entry.gid = expected gid or -1
# fs_perms_entry.walk = 'true' or something else
# fs_perms_entry.fmode = expected file mode or None
# fs_perms_entry.fuid = expected file uid or -1
# fs_perms_entry_fgid = expected file gid or -1
class fs_perms_entry():
def __init__(self, line):
lsplit = line.split()
if len(lsplit) == 3 and lsplit[1].lower() == "link":
self._setlink(lsplit[0], lsplit[2])
elif len(lsplit) == 8:
self._setdir(lsplit[0], lsplit[1], lsplit[2], lsplit[3], lsplit[4], lsplit[5], lsplit[6], lsplit[7])
else:
msg = "Fixup Perms: invalid config line %s" % line
oe.qa.handle_error("perm-config", msg, d)
self.path = None
self.link = None
def _setdir(self, path, mode, uid, gid, walk, fmode, fuid, fgid):
self.path = os.path.normpath(path)
self.link = None
self.mode = self._procmode(mode)
self.uid = self._procuid(uid)
self.gid = self._procgid(gid)
self.walk = walk.lower()
self.fmode = self._procmode(fmode)
self.fuid = self._procuid(fuid)
self.fgid = self._procgid(fgid)
def _setlink(self, path, link):
self.path = os.path.normpath(path)
self.link = link
def _procmode(self, mode):
if not mode or (mode and mode == "-"):
return None
else:
return int(mode,8)
# Note uid/gid -1 has special significance in os.lchown
def _procuid(self, uid):
if uid is None or uid == "-":
return -1
elif uid.isdigit():
return int(uid)
else:
return pwd.getpwnam(uid).pw_uid
def _procgid(self, gid):
if gid is None or gid == "-":
return -1
elif gid.isdigit():
return int(gid)
else:
return grp.getgrnam(gid).gr_gid
# Use for debugging the entries
def __str__(self):
if self.link:
return "%s link %s" % (self.path, self.link)
else:
mode = "-"
if self.mode:
mode = "0%o" % self.mode
fmode = "-"
if self.fmode:
fmode = "0%o" % self.fmode
uid = self._mapugid(self.uid)
gid = self._mapugid(self.gid)
fuid = self._mapugid(self.fuid)
fgid = self._mapugid(self.fgid)
return "%s %s %s %s %s %s %s %s" % (self.path, mode, uid, gid, self.walk, fmode, fuid, fgid)
def _mapugid(self, id):
if id is None or id == -1:
return "-"
else:
return "%d" % id
# Fix the permission, owner and group of path
def fix_perms(path, mode, uid, gid, dir):
if mode and not os.path.islink(path):
#bb.note("Fixup Perms: chmod 0%o %s" % (mode, dir))
os.chmod(path, mode)
# -1 is a special value that means don't change the uid/gid
# if they are BOTH -1, don't bother to lchown
if not (uid == -1 and gid == -1):
#bb.note("Fixup Perms: lchown %d:%d %s" % (uid, gid, dir))
os.lchown(path, uid, gid)
# Return a list of configuration files based on either the default
# files/fs-perms.txt or the contents of FILESYSTEM_PERMS_TABLES
# paths are resolved via BBPATH
def get_fs_perms_list(d):
str = ""
bbpath = d.getVar('BBPATH')
fs_perms_tables = d.getVar('FILESYSTEM_PERMS_TABLES') or ""
for conf_file in fs_perms_tables.split():
confpath = bb.utils.which(bbpath, conf_file)
if confpath:
str += " %s" % bb.utils.which(bbpath, conf_file)
else:
bb.warn("cannot find %s specified in FILESYSTEM_PERMS_TABLES" % conf_file)
return str
fs_perms_table = {}
fs_link_table = {}
# By default all of the standard directories specified in
# bitbake.conf will get 0755 root:root.
target_path_vars = [ 'base_prefix',
'prefix',
'exec_prefix',
'base_bindir',
'base_sbindir',
'base_libdir',
'datadir',
'sysconfdir',
'servicedir',
'sharedstatedir',
'localstatedir',
'infodir',
'mandir',
'docdir',
'bindir',
'sbindir',
'libexecdir',
'libdir',
'includedir' ]
for path in target_path_vars:
dir = d.getVar(path) or ""
if dir == "":
continue
fs_perms_table[dir] = fs_perms_entry(d.expand("%s 0755 root root false - - -" % (dir)))
# Now we actually load from the configuration files
for conf in get_fs_perms_list(d).split():
if not os.path.exists(conf):
continue
with open(conf) as f:
for line in f:
if line.startswith('#'):
continue
lsplit = line.split()
if len(lsplit) == 0:
continue
if len(lsplit) != 8 and not (len(lsplit) == 3 and lsplit[1].lower() == "link"):
msg = "Fixup perms: %s invalid line: %s" % (conf, line)
oe.qa.handle_error("perm-line", msg, d)
continue
entry = fs_perms_entry(d.expand(line))
if entry and entry.path:
if entry.link:
fs_link_table[entry.path] = entry
if entry.path in fs_perms_table:
fs_perms_table.pop(entry.path)
else:
fs_perms_table[entry.path] = entry
if entry.path in fs_link_table:
fs_link_table.pop(entry.path)
# Debug -- list out in-memory table
#for dir in fs_perms_table:
# bb.note("Fixup Perms: %s: %s" % (dir, str(fs_perms_table[dir])))
#for link in fs_link_table:
# bb.note("Fixup Perms: %s: %s" % (link, str(fs_link_table[link])))
# We process links first, so we can go back and fixup directory ownership
# for any newly created directories
# Process in sorted order so /run gets created before /run/lock, etc.
for entry in sorted(fs_link_table.values(), key=lambda x: x.link):
link = entry.link
dir = entry.path
origin = dvar + dir
if not (cpath.exists(origin) and cpath.isdir(origin) and not cpath.islink(origin)):
continue
if link[0] == "/":
target = dvar + link
ptarget = link
else:
target = os.path.join(os.path.dirname(origin), link)
ptarget = os.path.join(os.path.dirname(dir), link)
if os.path.exists(target):
msg = "Fixup Perms: Unable to correct directory link, target already exists: %s -> %s" % (dir, ptarget)
oe.qa.handle_error("perm-link", msg, d)
continue
# Create path to move directory to, move it, and then setup the symlink
bb.utils.mkdirhier(os.path.dirname(target))
#bb.note("Fixup Perms: Rename %s -> %s" % (dir, ptarget))
bb.utils.rename(origin, target)
#bb.note("Fixup Perms: Link %s -> %s" % (dir, link))
os.symlink(link, origin)
for dir in fs_perms_table:
origin = dvar + dir
if not (cpath.exists(origin) and cpath.isdir(origin)):
continue
fix_perms(origin, fs_perms_table[dir].mode, fs_perms_table[dir].uid, fs_perms_table[dir].gid, dir)
if fs_perms_table[dir].walk == 'true':
for root, dirs, files in os.walk(origin):
for dr in dirs:
each_dir = os.path.join(root, dr)
fix_perms(each_dir, fs_perms_table[dir].mode, fs_perms_table[dir].uid, fs_perms_table[dir].gid, dir)
for f in files:
each_file = os.path.join(root, f)
fix_perms(each_file, fs_perms_table[dir].fmode, fs_perms_table[dir].fuid, fs_perms_table[dir].fgid, dir)
# Get a list of files from file vars by searching files under current working directory
# The list contains symlinks, directories and normal files.
def files_from_filevars(filevars):
cpath = oe.cachedpath.CachedPath()
files = []
for f in filevars:
if os.path.isabs(f):
f = '.' + f
if not f.startswith("./"):
f = './' + f
globbed = glob.glob(f, recursive=True)
if globbed:
if [ f ] != globbed:
files += globbed
continue
files.append(f)
symlink_paths = []
for ind, f in enumerate(files):
# Handle directory symlinks. Truncate path to the lowest level symlink
parent = ''
for dirname in f.split('/')[:-1]:
parent = os.path.join(parent, dirname)
if dirname == '.':
continue
if cpath.islink(parent):
bb.warn("FILES contains file '%s' which resides under a "
"directory symlink. Please fix the recipe and use the "
"real path for the file." % f[1:])
symlink_paths.append(f)
files[ind] = parent
f = parent
break
if not cpath.islink(f):
if cpath.isdir(f):
newfiles = [ os.path.join(f,x) for x in os.listdir(f) ]
if newfiles:
files += newfiles
return files, symlink_paths
# Called in package_<rpm,ipk,deb>.bbclass to get the correct list of configuration files
def get_conffiles(pkg, d):
pkgdest = d.getVar('PKGDEST')
root = os.path.join(pkgdest, pkg)
cwd = os.getcwd()
os.chdir(root)
conffiles = d.getVar('CONFFILES:%s' % pkg);
if conffiles == None:
conffiles = d.getVar('CONFFILES')
if conffiles == None:
conffiles = ""
conffiles = conffiles.split()
conf_orig_list = files_from_filevars(conffiles)[0]
# Remove links and directories from conf_orig_list to get conf_list which only contains normal files
conf_list = []
for f in conf_orig_list:
if os.path.isdir(f):
continue
if os.path.islink(f):
continue
if not os.path.exists(f):
continue
conf_list.append(f)
# Remove the leading './'
for i in range(0, len(conf_list)):
conf_list[i] = conf_list[i][1:]
os.chdir(cwd)
return sorted(conf_list)
def legitimize_package_name(s):
"""
Make sure package names are legitimate strings
"""
def fixutf(m):
cp = m.group(1)
if cp:
return ('\\u%s' % cp).encode('latin-1').decode('unicode_escape')
# Handle unicode codepoints encoded as <U0123>, as in glibc locale files.
s = re.sub(r'<U([0-9A-Fa-f]{1,4})>', fixutf, s)
# Remaining package name validity fixes
return s.lower().replace('_', '-').replace('@', '+').replace(',', '+').replace('/', '-')
def split_locales(d):
cpath = oe.cachedpath.CachedPath()
if (d.getVar('PACKAGE_NO_LOCALE') == '1'):
bb.debug(1, "package requested not splitting locales")
return
packages = (d.getVar('PACKAGES') or "").split()
dvar = d.getVar('PKGD')
pn = d.getVar('LOCALEBASEPN')
try:
locale_index = packages.index(pn + '-locale')
packages.pop(locale_index)
except ValueError:
locale_index = len(packages)
lic = d.getVar("LICENSE:" + pn + "-locale")
localepaths = []
locales = set()
for localepath in (d.getVar('LOCALE_PATHS') or "").split():
localedir = dvar + localepath
if not cpath.isdir(localedir):
bb.debug(1, 'No locale files in %s' % localepath)
continue
localepaths.append(localepath)
with os.scandir(localedir) as it:
for entry in it:
if entry.is_dir():
locales.add(entry.name)
if len(locales) == 0:
bb.debug(1, "No locale files in this package")
return
summary = d.getVar('SUMMARY') or pn
description = d.getVar('DESCRIPTION') or ""
locale_section = d.getVar('LOCALE_SECTION')
mlprefix = d.getVar('MLPREFIX') or ""
for l in sorted(locales):
ln = legitimize_package_name(l)
pkg = pn + '-locale-' + ln
packages.insert(locale_index, pkg)
locale_index += 1
files = []
for localepath in localepaths:
files.append(os.path.join(localepath, l))
d.setVar('FILES:' + pkg, " ".join(files))
d.setVar('RRECOMMENDS:' + pkg, '%svirtual-locale-%s' % (mlprefix, ln))
d.setVar('RPROVIDES:' + pkg, '%s-locale %s%s-translation' % (pn, mlprefix, ln))
d.setVar('SUMMARY:' + pkg, '%s - %s translations' % (summary, l))
d.setVar('DESCRIPTION:' + pkg, '%s This package contains language translation files for the %s locale.' % (description, l))
if lic:
d.setVar('LICENSE:' + pkg, lic)
if locale_section:
d.setVar('SECTION:' + pkg, locale_section)
d.setVar('PACKAGES', ' '.join(packages))
# Disabled by RP 18/06/07
# Wildcards aren't supported in debian
# They break with ipkg since glibc-locale* will mean that
# glibc-localedata-translit* won't install as a dependency
# for some other package which breaks meta-toolchain
# Probably breaks since virtual-locale- isn't provided anywhere
#rdep = (d.getVar('RDEPENDS:%s' % pn) or "").split()
#rdep.append('%s-locale*' % pn)
#d.setVar('RDEPENDS:%s' % pn, ' '.join(rdep))
def package_debug_vars(d):
# We default to '.debug' style
if d.getVar('PACKAGE_DEBUG_SPLIT_STYLE') == 'debug-file-directory':
# Single debug-file-directory style debug info
debug_vars = {
"append": ".debug",
"staticappend": "",
"dir": "",
"staticdir": "",
"libdir": "/usr/lib/debug",
"staticlibdir": "/usr/lib/debug-static",
"srcdir": "/usr/src/debug",
}
elif d.getVar('PACKAGE_DEBUG_SPLIT_STYLE') == 'debug-without-src':
# Original OE-core, a.k.a. ".debug", style debug info, but without sources in /usr/src/debug
debug_vars = {
"append": "",
"staticappend": "",
"dir": "/.debug",
"staticdir": "/.debug-static",
"libdir": "",
"staticlibdir": "",
"srcdir": "",
}
elif d.getVar('PACKAGE_DEBUG_SPLIT_STYLE') == 'debug-with-srcpkg':
debug_vars = {
"append": "",
"staticappend": "",
"dir": "/.debug",
"staticdir": "/.debug-static",
"libdir": "",
"staticlibdir": "",
"srcdir": "/usr/src/debug",
}
else:
# Original OE-core, a.k.a. ".debug", style debug info
debug_vars = {
"append": "",
"staticappend": "",
"dir": "/.debug",
"staticdir": "/.debug-static",
"libdir": "",
"staticlibdir": "",
"srcdir": "/usr/src/debug",
}
return debug_vars
def parse_debugsources_from_dwarfsrcfiles_output(dwarfsrcfiles_output):
debugfiles = {}
for line in dwarfsrcfiles_output.splitlines():
if line.startswith("\t"):
debugfiles[os.path.normpath(line.split()[0])] = ""
return debugfiles.keys()
def source_info(file, d, fatal=True):
cmd = ["dwarfsrcfiles", file]
try:
output = subprocess.check_output(cmd, universal_newlines=True, stderr=subprocess.STDOUT)
retval = 0
except subprocess.CalledProcessError as exc:
output = exc.output
retval = exc.returncode
# 255 means a specific file wasn't fully parsed to get the debug file list, which is not a fatal failure
if retval != 0 and retval != 255:
msg = "dwarfsrcfiles failed with exit code %s (cmd was %s)%s" % (retval, cmd, ":\n%s" % output if output else "")
if fatal:
bb.fatal(msg)
bb.note(msg)
debugsources = parse_debugsources_from_dwarfsrcfiles_output(output)
return list(debugsources)
def splitdebuginfo(file, dvar, dv, d):
# Function to split a single file into two components, one is the stripped
# target system binary, the other contains any debugging information. The
# two files are linked to reference each other.
#
# return a mapping of files:debugsources
src = file[len(dvar):]
dest = dv["libdir"] + os.path.dirname(src) + dv["dir"] + "/" + os.path.basename(src) + dv["append"]
debugfile = dvar + dest
sources = []
if file.endswith(".ko") and file.find("/lib/modules/") != -1:
if oe.package.is_kernel_module_signed(file):
bb.debug(1, "Skip strip on signed module %s" % file)
return (file, sources)
# Split the file...
bb.utils.mkdirhier(os.path.dirname(debugfile))
#bb.note("Split %s -> %s" % (file, debugfile))
# Only store off the hard link reference if we successfully split!
dvar = d.getVar('PKGD')
objcopy = d.getVar("OBJCOPY")
newmode = None
if not os.access(file, os.W_OK) or os.access(file, os.R_OK):
origmode = os.stat(file)[stat.ST_MODE]
newmode = origmode | stat.S_IWRITE | stat.S_IREAD
os.chmod(file, newmode)
# We need to extract the debug src information here...
if dv["srcdir"]:
sources = source_info(file, d)
bb.utils.mkdirhier(os.path.dirname(debugfile))
subprocess.check_output([objcopy, '--only-keep-debug', file, debugfile], stderr=subprocess.STDOUT)
# Set the debuglink to have the view of the file path on the target
subprocess.check_output([objcopy, '--add-gnu-debuglink', debugfile, file], stderr=subprocess.STDOUT)
if newmode:
os.chmod(file, origmode)
return (file, sources)
def splitstaticdebuginfo(file, dvar, dv, d):
# Unlike the function above, there is no way to split a static library
# two components. So to get similar results we will copy the unmodified
# static library (containing the debug symbols) into a new directory.
# We will then strip (preserving symbols) the static library in the
# typical location.
#
# return a mapping of files:debugsources
src = file[len(dvar):]
dest = dv["staticlibdir"] + os.path.dirname(src) + dv["staticdir"] + "/" + os.path.basename(src) + dv["staticappend"]
debugfile = dvar + dest
sources = []
# Copy the file...
bb.utils.mkdirhier(os.path.dirname(debugfile))
#bb.note("Copy %s -> %s" % (file, debugfile))
dvar = d.getVar('PKGD')
newmode = None
if not os.access(file, os.W_OK) or os.access(file, os.R_OK):
origmode = os.stat(file)[stat.ST_MODE]
newmode = origmode | stat.S_IWRITE | stat.S_IREAD
os.chmod(file, newmode)
# We need to extract the debug src information here...
if dv["srcdir"]:
sources = source_info(file, d)
bb.utils.mkdirhier(os.path.dirname(debugfile))
# Copy the unmodified item to the debug directory
shutil.copy2(file, debugfile)
if newmode:
os.chmod(file, origmode)
return (file, sources)
def inject_minidebuginfo(file, dvar, dv, d):
# Extract just the symbols from debuginfo into minidebuginfo,
# compress it with xz and inject it back into the binary in a .gnu_debugdata section.
# https://sourceware.org/gdb/onlinedocs/gdb/MiniDebugInfo.html
readelf = d.getVar('READELF')
nm = d.getVar('NM')
objcopy = d.getVar('OBJCOPY')
minidebuginfodir = d.expand('${WORKDIR}/minidebuginfo')
src = file[len(dvar):]
dest = dv["libdir"] + os.path.dirname(src) + dv["dir"] + "/" + os.path.basename(src) + dv["append"]
debugfile = dvar + dest
minidebugfile = minidebuginfodir + src + '.minidebug'
bb.utils.mkdirhier(os.path.dirname(minidebugfile))
# If we didn't produce debuginfo for any reason, we can't produce minidebuginfo either
# so skip it.
if not os.path.exists(debugfile):
bb.debug(1, 'ELF file {} has no debuginfo, skipping minidebuginfo injection'.format(file))
return
# minidebuginfo does not make sense to apply to ELF objects other than
# executables and shared libraries, skip applying the minidebuginfo
# generation for objects like kernel modules.
for line in subprocess.check_output([readelf, '-h', debugfile], universal_newlines=True).splitlines():
if not line.strip().startswith("Type:"):
continue
elftype = line.split(":")[1].strip()
if not any(elftype.startswith(i) for i in ["EXEC", "DYN"]):
bb.debug(1, 'ELF file {} is not executable/shared, skipping minidebuginfo injection'.format(file))
return
break
# Find non-allocated PROGBITS, NOTE, and NOBITS sections in the debuginfo.
# We will exclude all of these from minidebuginfo to save space.
remove_section_names = []
for line in subprocess.check_output([readelf, '-W', '-S', debugfile], universal_newlines=True).splitlines():
# strip the leading " [ 1]" section index to allow splitting on space
if ']' not in line:
continue
fields = line[line.index(']') + 1:].split()
if len(fields) < 7:
continue
name = fields[0]
type = fields[1]
flags = fields[6]
# .debug_ sections will be removed by objcopy -S so no need to explicitly remove them
if name.startswith('.debug_'):
continue
if 'A' not in flags and type in ['PROGBITS', 'NOTE', 'NOBITS']:
remove_section_names.append(name)
# List dynamic symbols in the binary. We can exclude these from minidebuginfo
# because they are always present in the binary.
dynsyms = set()
for line in subprocess.check_output([nm, '-D', file, '--format=posix', '--defined-only'], universal_newlines=True).splitlines():
dynsyms.add(line.split()[0])
# Find all function symbols from debuginfo which aren't in the dynamic symbols table.
# These are the ones we want to keep in minidebuginfo.
keep_symbols_file = minidebugfile + '.symlist'
found_any_symbols = False
with open(keep_symbols_file, 'w') as f:
for line in subprocess.check_output([nm, debugfile, '--format=sysv', '--defined-only'], universal_newlines=True).splitlines():
fields = line.split('|')
if len(fields) < 7:
continue
name = fields[0].strip()
type = fields[3].strip()
if type == 'FUNC' and name not in dynsyms:
f.write('{}\n'.format(name))
found_any_symbols = True
if not found_any_symbols:
bb.debug(1, 'ELF file {} contains no symbols, skipping minidebuginfo injection'.format(file))
return
bb.utils.remove(minidebugfile)
bb.utils.remove(minidebugfile + '.xz')
subprocess.check_call([objcopy, '-S'] +
['--remove-section={}'.format(s) for s in remove_section_names] +
['--keep-symbols={}'.format(keep_symbols_file), debugfile, minidebugfile])
subprocess.check_call(['xz', '--keep', minidebugfile])
subprocess.check_call([objcopy, '--add-section', '.gnu_debugdata={}.xz'.format(minidebugfile), file])
def copydebugsources(debugsrcdir, sources, d):
# The debug src information written out to sourcefile is further processed
# and copied to the destination here.
cpath = oe.cachedpath.CachedPath()
if debugsrcdir and sources:
sourcefile = d.expand("${WORKDIR}/debugsources.list")
bb.utils.remove(sourcefile)
# filenames are null-separated - this is an artefact of the previous use
# of rpm's debugedit, which was writing them out that way, and the code elsewhere
# is still assuming that.
debuglistoutput = '\0'.join(sources) + '\0'
with open(sourcefile, 'a') as sf:
sf.write(debuglistoutput)
dvar = d.getVar('PKGD')
strip = d.getVar("STRIP")
objcopy = d.getVar("OBJCOPY")
workdir = d.getVar("WORKDIR")
sdir = d.getVar("S")
cflags = d.expand("${CFLAGS}")
prefixmap = {}
for flag in cflags.split():
if not flag.startswith("-ffile-prefix-map"):
continue
if "recipe-sysroot" in flag:
continue
flag = flag.split("=")
prefixmap[flag[1]] = flag[2]
nosuchdir = []
basepath = dvar
for p in debugsrcdir.split("/"):
basepath = basepath + "/" + p
if not cpath.exists(basepath):
nosuchdir.append(basepath)
bb.utils.mkdirhier(basepath)
cpath.updatecache(basepath)
for pmap in prefixmap:
# Ignore files from the recipe sysroots (target and native)
cmd = "LC_ALL=C ; sort -z -u '%s' | egrep -v -z '((<internal>|<built-in>)$|/.*recipe-sysroot.*/)' | " % sourcefile
# We need to ignore files that are not actually ours
# we do this by only paying attention to items from this package
cmd += "fgrep -zw '%s' | " % prefixmap[pmap]
# Remove prefix in the source paths
cmd += "sed 's#%s/##g' | " % (prefixmap[pmap])
cmd += "(cd '%s' ; cpio -pd0mlL --no-preserve-owner '%s%s' 2>/dev/null)" % (pmap, dvar, prefixmap[pmap])
try:
subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
# Can "fail" if internal headers/transient sources are attempted
pass
# cpio seems to have a bug with -lL together and symbolic links are just copied, not dereferenced.
# Work around this by manually finding and copying any symbolic links that made it through.
cmd = "find %s%s -type l -print0 -delete | sed s#%s%s/##g | (cd '%s' ; cpio -pd0mL --no-preserve-owner '%s%s')" % \
(dvar, prefixmap[pmap], dvar, prefixmap[pmap], pmap, dvar, prefixmap[pmap])
subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
# debugsources.list may be polluted from the host if we used externalsrc,
# cpio uses copy-pass and may have just created a directory structure
# matching the one from the host, if thats the case move those files to
# debugsrcdir to avoid host contamination.
# Empty dir structure will be deleted in the next step.
# Same check as above for externalsrc
if workdir not in sdir:
if os.path.exists(dvar + debugsrcdir + sdir):
cmd = "mv %s%s%s/* %s%s" % (dvar, debugsrcdir, sdir, dvar,debugsrcdir)
subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
# The copy by cpio may have resulted in some empty directories! Remove these
cmd = "find %s%s -empty -type d -delete" % (dvar, debugsrcdir)
subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
# Also remove debugsrcdir if its empty
for p in nosuchdir[::-1]:
if os.path.exists(p) and not os.listdir(p):
os.rmdir(p)
@bb.parse.vardepsexclude("BB_NUMBER_THREADS")
def save_debugsources_info(debugsrcdir, sources_raw, d):
import json
import bb.compress.zstd
if debugsrcdir and sources_raw:
debugsources_file = d.expand("${PKGDESTWORK}/debugsources/${PN}-debugsources.json.zstd")
debugsources_dir = os.path.dirname(debugsources_file)
if not os.path.isdir(debugsources_dir):
bb.utils.mkdirhier(debugsources_dir)
bb.utils.remove(debugsources_file)
workdir = d.getVar("WORKDIR")
pn = d.getVar('PN')
# Kernel sources are in a different directory and are special case
# we format the sources as expected by spdx by replacing /usr/src/kernel/
# into BP/
kernel_src = d.getVar('KERNEL_SRC_PATH')
bp = d.getVar('BP')
sources_dict = {}
for file, src_files in sources_raw:
file_clean = file.replace(f"{workdir}/package/","")
sources_clean = [
src.replace(f"{debugsrcdir}/{pn}/", "")
if not kernel_src else src.replace(f"{kernel_src}/", f"{bp}/")
for src in src_files
if not any(keyword in src for keyword in ("<internal>", "<built-in>")) and not src.endswith("/")
]
sources_dict[file_clean] = sorted(sources_clean)
num_threads = int(d.getVar("BB_NUMBER_THREADS"))
with bb.compress.zstd.open(debugsources_file, "wt", encoding="utf-8", num_threads=num_threads) as f:
json.dump(sources_dict, f, sort_keys=True)
@bb.parse.vardepsexclude("BB_NUMBER_THREADS")
def read_debugsources_info(d):
import json
import bb.compress.zstd
try:
fn = d.expand("${PKGDESTWORK}/debugsources/${PN}-debugsources.json.zstd")
num_threads = int(d.getVar("BB_NUMBER_THREADS"))
with bb.compress.zstd.open(fn, "rt", encoding="utf-8", num_threads=num_threads) as f:
return json.load(f)
except FileNotFoundError:
bb.debug(1, f"File not found: {fn}")
return None
def process_split_and_strip_files(d):
cpath = oe.cachedpath.CachedPath()
dvar = d.getVar('PKGD')
pn = d.getVar('PN')
hostos = d.getVar('HOST_OS')
oldcwd = os.getcwd()
os.chdir(dvar)
dv = package_debug_vars(d)
#
# First lets figure out all of the files we may have to process ... do this only once!
#
elffiles = {}
symlinks = {}
staticlibs = []
inodes = {}
libdir = os.path.abspath(dvar + os.sep + d.getVar("libdir"))
baselibdir = os.path.abspath(dvar + os.sep + d.getVar("base_libdir"))
skipfiles = (d.getVar("INHIBIT_PACKAGE_STRIP_FILES") or "").split()
if (d.getVar('INHIBIT_PACKAGE_STRIP') != '1' or \
d.getVar('INHIBIT_PACKAGE_DEBUG_SPLIT') != '1'):
checkelf = {}
checkelflinks = {}
checkstatic = {}
for root, dirs, files in cpath.walk(dvar):
for f in files:
file = os.path.join(root, f)
# Skip debug files
if dv["append"] and file.endswith(dv["append"]):
continue
if dv["dir"] and dv["dir"] in os.path.dirname(file[len(dvar):]):
continue
if file in skipfiles:
continue
try:
ltarget = cpath.realpath(file, dvar, False)
s = cpath.lstat(ltarget)
except OSError as e:
(err, strerror) = e.args
if err != errno.ENOENT:
raise
# Skip broken symlinks
continue
if not s:
continue
if oe.package.is_static_lib(file):
# Use a reference of device ID and inode number to identify files
file_reference = "%d_%d" % (s.st_dev, s.st_ino)
checkstatic[file] = (file, file_reference)
continue
# Check its an executable
if (s[stat.ST_MODE] & stat.S_IXUSR) or (s[stat.ST_MODE] & stat.S_IXGRP) \
or (s[stat.ST_MODE] & stat.S_IXOTH) \
or ((file.startswith(libdir) or file.startswith(baselibdir)) \
and (".so" in f or ".node" in f)) \
or (f.startswith('vmlinux') or ".ko" in f):
if cpath.islink(file):
checkelflinks[file] = ltarget
continue
# Use a reference of device ID and inode number to identify files
file_reference = "%d_%d" % (s.st_dev, s.st_ino)
checkelf[file] = (file, file_reference)
results = oe.utils.multiprocess_launch(oe.package.is_elf, checkelflinks.values(), d)
results_map = {}
for (ltarget, elf_file) in results:
results_map[ltarget] = elf_file
for file in checkelflinks:
ltarget = checkelflinks[file]
# If it's a symlink, and points to an ELF file, we capture the readlink target
if results_map[ltarget]:
target = os.readlink(file)
#bb.note("Sym: %s (%d)" % (ltarget, results_map[ltarget]))
symlinks[file] = target
results = oe.utils.multiprocess_launch(oe.package.is_elf, checkelf.keys(), d)
# Sort results by file path. This ensures that the files are always
# processed in the same order, which is important to make sure builds
# are reproducible when dealing with hardlinks
results.sort(key=lambda x: x[0])
for (file, elf_file) in results:
# It's a file (or hardlink), not a link
# ...but is it ELF, and is it already stripped?
if elf_file & 1:
if elf_file & 2:
if 'already-stripped' in (d.getVar('INSANE_SKIP:' + pn) or "").split():
bb.note("Skipping file %s from %s for already-stripped QA test" % (file[len(dvar):], pn))
else:
msg = "File '%s' from %s was already stripped, this will prevent future debugging!" % (file[len(dvar):], pn)
oe.qa.handle_error("already-stripped", msg, d)
continue
# At this point we have an unstripped elf file. We need to:
# a) Make sure any file we strip is not hardlinked to anything else outside this tree
# b) Only strip any hardlinked file once (no races)
# c) Track any hardlinks between files so that we can reconstruct matching debug file hardlinks
# Use a reference of device ID and inode number to identify files
file_reference = checkelf[file][1]
if file_reference in inodes:
os.unlink(file)
os.link(inodes[file_reference][0], file)
inodes[file_reference].append(file)
else:
inodes[file_reference] = [file]
# break hardlink
bb.utils.break_hardlinks(file)
elffiles[file] = elf_file
# Modified the file so clear the cache
cpath.updatecache(file)
# Do the same hardlink processing as above, but for static libraries
results = list(checkstatic.keys())
# As above, sort the results.
results.sort(key=lambda x: x[0])
for file in results:
# Use a reference of device ID and inode number to identify files
file_reference = checkstatic[file][1]
if file_reference in inodes:
os.unlink(file)
os.link(inodes[file_reference][0], file)
inodes[file_reference].append(file)
else:
inodes[file_reference] = [file]
# break hardlink
bb.utils.break_hardlinks(file)
staticlibs.append(file)
# Modified the file so clear the cache
cpath.updatecache(file)
def strip_pkgd_prefix(f):
nonlocal dvar
if f.startswith(dvar):
return f[len(dvar):]
return f
#
# First lets process debug splitting
#
if (d.getVar('INHIBIT_PACKAGE_DEBUG_SPLIT') != '1'):
results = oe.utils.multiprocess_launch(splitdebuginfo, list(elffiles), d, extraargs=(dvar, dv, d))
if dv["srcdir"] and not hostos.startswith("mingw"):
if (d.getVar('PACKAGE_DEBUG_STATIC_SPLIT') == '1'):
results = oe.utils.multiprocess_launch(splitstaticdebuginfo, staticlibs, d, extraargs=(dvar, dv, d))
else:
for file in staticlibs:
results.append( (file,source_info(file, d)) )
d.setVar("PKGDEBUGSOURCES", {strip_pkgd_prefix(f): sorted(s) for f, s in results})
sources = set()
for r in results:
sources.update(r[1])
# Hardlink our debug symbols to the other hardlink copies
for ref in inodes:
if len(inodes[ref]) == 1:
continue
target = inodes[ref][0][len(dvar):]
for file in inodes[ref][1:]:
src = file[len(dvar):]
dest = dv["libdir"] + os.path.dirname(src) + dv["dir"] + "/" + os.path.basename(target) + dv["append"]
fpath = dvar + dest
ftarget = dvar + dv["libdir"] + os.path.dirname(target) + dv["dir"] + "/" + os.path.basename(target) + dv["append"]
if os.access(ftarget, os.R_OK):
bb.utils.mkdirhier(os.path.dirname(fpath))
# Only one hardlink of separated debug info file in each directory
if not os.access(fpath, os.R_OK):
#bb.note("Link %s -> %s" % (fpath, ftarget))
os.link(ftarget, fpath)
elif (d.getVar('PACKAGE_DEBUG_STATIC_SPLIT') == '1'):
deststatic = dv["staticlibdir"] + os.path.dirname(src) + dv["staticdir"] + "/" + os.path.basename(file) + dv["staticappend"]
fpath = dvar + deststatic
ftarget = dvar + dv["staticlibdir"] + os.path.dirname(target) + dv["staticdir"] + "/" + os.path.basename(target) + dv["staticappend"]
if os.access(ftarget, os.R_OK):
bb.utils.mkdirhier(os.path.dirname(fpath))
# Only one hardlink of separated debug info file in each directory
if not os.access(fpath, os.R_OK):
#bb.note("Link %s -> %s" % (fpath, ftarget))
os.link(ftarget, fpath)
else:
bb.note("Unable to find inode link target %s" % (target))
# Create symlinks for all cases we were able to split symbols
for file in symlinks:
src = file[len(dvar):]
dest = dv["libdir"] + os.path.dirname(src) + dv["dir"] + "/" + os.path.basename(src) + dv["append"]
fpath = dvar + dest
# Skip it if the target doesn't exist
try:
s = os.stat(fpath)
except OSError as e:
(err, strerror) = e.args
if err != errno.ENOENT:
raise
continue
ltarget = symlinks[file]
lpath = os.path.dirname(ltarget)
lbase = os.path.basename(ltarget)
ftarget = ""
if lpath and lpath != ".":
ftarget += lpath + dv["dir"] + "/"
ftarget += lbase + dv["append"]
if lpath.startswith(".."):
ftarget = os.path.join("..", ftarget)
bb.utils.mkdirhier(os.path.dirname(fpath))
#bb.note("Symlink %s -> %s" % (fpath, ftarget))
os.symlink(ftarget, fpath)
# Process the dv["srcdir"] if requested...
# This copies and places the referenced sources for later debugging...
copydebugsources(dv["srcdir"], sources, d)
# Save source info to be accessible to other tasks
save_debugsources_info(dv["srcdir"], results, d)
#
# End of debug splitting
#
#
# Now lets go back over things and strip them
#
if (d.getVar('INHIBIT_PACKAGE_STRIP') != '1'):
strip = d.getVar("STRIP")
sfiles = []
for file in elffiles:
elf_file = int(elffiles[file])
#bb.note("Strip %s" % file)
sfiles.append((file, elf_file, strip))
if (d.getVar('PACKAGE_STRIP_STATIC') == '1' or d.getVar('PACKAGE_DEBUG_STATIC_SPLIT') == '1'):
for f in staticlibs:
sfiles.append((f, 16, strip))
oe.utils.multiprocess_launch(oe.package.runstrip, sfiles, d)
# Build "minidebuginfo" and reinject it back into the stripped binaries
if bb.utils.contains('DISTRO_FEATURES', 'minidebuginfo', True, False, d):
oe.utils.multiprocess_launch(inject_minidebuginfo, list(elffiles), d,
extraargs=(dvar, dv, d))
#
# End of strip
#
os.chdir(oldcwd)
def populate_packages(d):
cpath = oe.cachedpath.CachedPath()
workdir = d.getVar('WORKDIR')
outdir = d.getVar('DEPLOY_DIR')
dvar = d.getVar('PKGD')
packages = d.getVar('PACKAGES').split()
pn = d.getVar('PN')
bb.utils.mkdirhier(outdir)
os.chdir(dvar)
autodebug = not (d.getVar("NOAUTOPACKAGEDEBUG") or False)
split_source_package = (d.getVar('PACKAGE_DEBUG_SPLIT_STYLE') == 'debug-with-srcpkg')
# If debug-with-srcpkg mode is enabled then add the source package if it
# doesn't exist and add the source file contents to the source package.
if split_source_package:
src_package_name = ('%s-src' % d.getVar('PN'))
if not src_package_name in packages:
packages.append(src_package_name)
d.setVar('FILES:%s' % src_package_name, '/usr/src/debug')
# Sanity check PACKAGES for duplicates
# Sanity should be moved to sanity.bbclass once we have the infrastructure
package_dict = {}
for i, pkg in enumerate(packages):
if pkg in package_dict:
msg = "%s is listed in PACKAGES multiple times, this leads to packaging errors." % pkg
oe.qa.handle_error("packages-list", msg, d)
# Ensure the source package gets the chance to pick up the source files
# before the debug package by ordering it first in PACKAGES. Whether it
# actually picks up any source files is controlled by
# PACKAGE_DEBUG_SPLIT_STYLE.
elif pkg.endswith("-src"):
package_dict[pkg] = (10, i)
elif autodebug and pkg.endswith("-dbg"):
package_dict[pkg] = (30, i)
else:
package_dict[pkg] = (50, i)
packages = sorted(package_dict.keys(), key=package_dict.get)
d.setVar('PACKAGES', ' '.join(packages))
pkgdest = d.getVar('PKGDEST')
seen = []
# os.mkdir masks the permissions with umask so we have to unset it first
oldumask = os.umask(0)
debug = []
for root, dirs, files in cpath.walk(dvar):
dir = root[len(dvar):]
if not dir:
dir = os.sep
for f in (files + dirs):
path = "." + os.path.join(dir, f)
if "/.debug/" in path or "/.debug-static/" in path or path.endswith("/.debug"):
debug.append(path)
for pkg in packages:
root = os.path.join(pkgdest, pkg)
bb.utils.mkdirhier(root)
filesvar = d.getVar('FILES:%s' % pkg) or ""
if "//" in filesvar:
msg = "FILES variable for package %s contains '//' which is invalid. Attempting to fix this but you should correct the metadata.\n" % pkg
oe.qa.handle_error("files-invalid", msg, d)
filesvar.replace("//", "/")
origfiles = filesvar.split()
files, symlink_paths = oe.package.files_from_filevars(origfiles)
if autodebug and pkg.endswith("-dbg"):
files.extend(debug)
for file in files:
if (not cpath.islink(file)) and (not cpath.exists(file)):
continue
if file in seen:
continue
seen.append(file)
def mkdir(src, dest, p):
src = os.path.join(src, p)
dest = os.path.join(dest, p)
fstat = cpath.stat(src)
os.mkdir(dest)
os.chmod(dest, fstat.st_mode)
os.chown(dest, fstat.st_uid, fstat.st_gid)
if p not in seen:
seen.append(p)
cpath.updatecache(dest)
def mkdir_recurse(src, dest, paths):
if cpath.exists(dest + '/' + paths):
return
while paths.startswith("./"):
paths = paths[2:]
p = "."
for c in paths.split("/"):
p = os.path.join(p, c)
if not cpath.exists(os.path.join(dest, p)):
mkdir(src, dest, p)
if cpath.isdir(file) and not cpath.islink(file):
mkdir_recurse(dvar, root, file)
continue
mkdir_recurse(dvar, root, os.path.dirname(file))
fpath = os.path.join(root,file)
if not cpath.islink(file):
os.link(file, fpath)
continue
ret = bb.utils.copyfile(file, fpath)
if ret is False or ret == 0:
bb.fatal("File population failed")
# Check if symlink paths exist
for file in symlink_paths:
if not os.path.exists(os.path.join(root,file)):
bb.fatal("File '%s' cannot be packaged into '%s' because its "
"parent directory structure does not exist. One of "
"its parent directories is a symlink whose target "
"directory is not included in the package." %
(file, pkg))
os.umask(oldumask)
os.chdir(workdir)
# Handle excluding packages with incompatible licenses
package_list = []
skipped_pkgs = oe.license.skip_incompatible_package_licenses(d, packages)
for pkg in packages:
if pkg in skipped_pkgs:
msg = "Excluding %s from packaging as it has incompatible license(s): %s" % (pkg, skipped_pkgs[pkg])
oe.qa.handle_error("incompatible-license", msg, d)
else:
package_list.append(pkg)
d.setVar('PACKAGES', ' '.join(package_list))
unshipped = []
for root, dirs, files in cpath.walk(dvar):
dir = root[len(dvar):]
if not dir:
dir = os.sep
for f in (files + dirs):
path = os.path.join(dir, f)
if ('.' + path) not in seen:
unshipped.append(path)
if unshipped != []:
msg = pn + ": Files/directories were installed but not shipped in any package:"
if "installed-vs-shipped" in (d.getVar('INSANE_SKIP:' + pn) or "").split():
bb.note("Package %s skipping QA tests: installed-vs-shipped" % pn)
else:
for f in unshipped:
msg = msg + "\n " + f
msg = msg + "\nPlease set FILES such that these items are packaged. Alternatively if they are unneeded, avoid installing them or delete them within do_install.\n"
msg = msg + "%s: %d installed and not shipped files." % (pn, len(unshipped))
oe.qa.handle_error("installed-vs-shipped", msg, d)
def process_fixsymlinks(pkgfiles, d):
cpath = oe.cachedpath.CachedPath()
pkgdest = d.getVar('PKGDEST')
packages = d.getVar("PACKAGES", False).split()
dangling_links = {}
pkg_files = {}
for pkg in packages:
dangling_links[pkg] = []
pkg_files[pkg] = []
inst_root = os.path.join(pkgdest, pkg)
for path in pkgfiles[pkg]:
rpath = path[len(inst_root):]
pkg_files[pkg].append(rpath)
rtarget = cpath.realpath(path, inst_root, True, assume_dir = True)
if not cpath.lexists(rtarget):
dangling_links[pkg].append(os.path.normpath(rtarget[len(inst_root):]))
newrdepends = {}
for pkg in dangling_links:
for l in dangling_links[pkg]:
found = False
bb.debug(1, "%s contains dangling link %s" % (pkg, l))
for p in packages:
if l in pkg_files[p]:
found = True
bb.debug(1, "target found in %s" % p)
if p == pkg:
break
if pkg not in newrdepends:
newrdepends[pkg] = []
newrdepends[pkg].append(p)
break
if found == False:
bb.note("%s contains dangling symlink to %s" % (pkg, l))
for pkg in newrdepends:
rdepends = bb.utils.explode_dep_versions2(d.getVar('RDEPENDS:' + pkg) or "")
for p in newrdepends[pkg]:
if p not in rdepends:
rdepends[p] = []
d.setVar('RDEPENDS:' + pkg, bb.utils.join_deps(rdepends, commasep=False))
def process_filedeps(pkgfiles, d):
"""
Collect perfile run-time dependency metadata
Output:
FILERPROVIDESFLIST:pkg - list of all files w/ deps
FILERPROVIDES:filepath:pkg - per file dep
FILERDEPENDSFLIST:pkg - list of all files w/ deps
FILERDEPENDS:filepath:pkg - per file dep
"""
if d.getVar('SKIP_FILEDEPS') == '1':
return
pkgdest = d.getVar('PKGDEST')
packages = d.getVar('PACKAGES')
rpmdeps = d.getVar('RPMDEPS')
def chunks(files, n):
return [files[i:i+n] for i in range(0, len(files), n)]
pkglist = []
for pkg in packages.split():
if d.getVar('SKIP_FILEDEPS:' + pkg) == '1':
continue
if pkg.endswith('-dbg') or pkg.endswith('-doc') or pkg.find('-locale-') != -1 or pkg.find('-localedata-') != -1 or pkg.find('-gconv-') != -1 or pkg.find('-charmap-') != -1 or pkg.startswith('kernel-module-') or pkg.endswith('-src'):
continue
for files in chunks(pkgfiles[pkg], 100):
pkglist.append((pkg, files, rpmdeps, pkgdest))
processed = oe.utils.multiprocess_launch(oe.package.filedeprunner, pkglist, d)
provides_files = {}
requires_files = {}
for result in processed:
(pkg, provides, requires) = result
if pkg not in provides_files:
provides_files[pkg] = []
if pkg not in requires_files:
requires_files[pkg] = []
for file in sorted(provides):
provides_files[pkg].append(file)
key = "FILERPROVIDES:" + file + ":" + pkg
d.appendVar(key, " " + " ".join(provides[file]))
for file in sorted(requires):
requires_files[pkg].append(file)
key = "FILERDEPENDS:" + file + ":" + pkg
d.appendVar(key, " " + " ".join(requires[file]))
for pkg in requires_files:
d.setVar("FILERDEPENDSFLIST:" + pkg, " ".join(sorted(requires_files[pkg])))
for pkg in provides_files:
d.setVar("FILERPROVIDESFLIST:" + pkg, " ".join(sorted(provides_files[pkg])))
def process_shlibs(pkgfiles, d):
cpath = oe.cachedpath.CachedPath()
exclude_shlibs = d.getVar('EXCLUDE_FROM_SHLIBS', False)
if exclude_shlibs:
bb.note("not generating shlibs")
return
lib_re = re.compile(r"^.*\.so")
libdir_re = re.compile(r".*/%s$" % d.getVar('baselib'))
packages = d.getVar('PACKAGES')
shlib_pkgs = []
exclusion_list = d.getVar("EXCLUDE_PACKAGES_FROM_SHLIBS")
if exclusion_list:
for pkg in packages.split():
if pkg not in exclusion_list.split():
shlib_pkgs.append(pkg)
else:
bb.note("not generating shlibs for %s" % pkg)
else:
shlib_pkgs = packages.split()
hostos = d.getVar('HOST_OS')
workdir = d.getVar('WORKDIR')
ver = d.getVar('PKGV')
if not ver:
msg = "PKGV not defined"
oe.qa.handle_error("pkgv-undefined", msg, d)
return
pkgdest = d.getVar('PKGDEST')
shlibswork_dir = d.getVar('SHLIBSWORKDIR')
def linux_so(file, pkg, pkgver, d):
needs_ldconfig = False
needed = set()
sonames = set()
ldir = os.path.dirname(file).replace(pkgdest + "/" + pkg, '')
cmd = d.getVar('OBJDUMP') + " -p " + shlex.quote(file) + " 2>/dev/null"
fd = os.popen(cmd)
lines = fd.readlines()
fd.close()
rpath = tuple()
for l in lines:
m = re.match(r"\s+RPATH\s+([^\s]*)", l)
if m:
rpaths = m.group(1).replace("$ORIGIN", ldir).split(":")
rpath = tuple(map(os.path.normpath, rpaths))
for l in lines:
m = re.match(r"\s+NEEDED\s+([^\s]*)", l)
if m:
dep = m.group(1)
if dep not in needed:
needed.add((dep, file, rpath))
m = re.match(r"\s+SONAME\s+([^\s]*)", l)
if m:
this_soname = m.group(1)
prov = (this_soname, ldir, pkgver)
if not prov in sonames:
# if library is private (only used by package) then do not build shlib for it
if not private_libs or len([i for i in private_libs if fnmatch.fnmatch(this_soname, i)]) == 0:
sonames.add(prov)
if libdir_re.match(os.path.dirname(file)):
needs_ldconfig = True
return (needs_ldconfig, needed, sonames)
def darwin_so(file, needed, sonames, pkgver):
if not os.path.exists(file):
return
ldir = os.path.dirname(file).replace(pkgdest + "/" + pkg, '')
def get_combinations(base):
#
# Given a base library name, find all combinations of this split by "." and "-"
#
combos = []
options = base.split(".")
for i in range(1, len(options) + 1):
combos.append(".".join(options[0:i]))
options = base.split("-")
for i in range(1, len(options) + 1):
combos.append("-".join(options[0:i]))
return combos
if (file.endswith('.dylib') or file.endswith('.so')) and not pkg.endswith('-dev') and not pkg.endswith('-dbg') and not pkg.endswith('-src'):
# Drop suffix
name = os.path.basename(file).rsplit(".",1)[0]
# Find all combinations
combos = get_combinations(name)
for combo in combos:
if not combo in sonames:
prov = (combo, ldir, pkgver)
sonames.add(prov)
if file.endswith('.dylib') or file.endswith('.so'):
rpath = []
p = subprocess.Popen([d.expand("${HOST_PREFIX}otool"), '-l', file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
out, err = p.communicate()
# If returned successfully, process stdout for results
if p.returncode == 0:
for l in out.split("\n"):
l = l.strip()
if l.startswith('path '):
rpath.append(l.split()[1])
p = subprocess.Popen([d.expand("${HOST_PREFIX}otool"), '-L', file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
out, err = p.communicate()
# If returned successfully, process stdout for results
if p.returncode == 0:
for l in out.split("\n"):
l = l.strip()
if not l or l.endswith(":"):
continue
if "is not an object file" in l:
continue
name = os.path.basename(l.split()[0]).rsplit(".", 1)[0]
if name and name not in needed[pkg]:
needed[pkg].add((name, file, tuple()))
def mingw_dll(file, needed, sonames, pkgver):
if not os.path.exists(file):
return
if file.endswith(".dll"):
# assume all dlls are shared objects provided by the package
sonames.add((os.path.basename(file), os.path.dirname(file).replace(pkgdest + "/" + pkg, ''), pkgver))
if (file.endswith(".dll") or file.endswith(".exe")):
# use objdump to search for "DLL Name: .*\.dll"
p = subprocess.Popen([d.expand("${OBJDUMP}"), "-p", file], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
# process the output, grabbing all .dll names
if p.returncode == 0:
for m in re.finditer(r"DLL Name: (.*?\.dll)$", out.decode(), re.MULTILINE | re.IGNORECASE):
dllname = m.group(1)
if dllname:
needed[pkg].add((dllname, file, tuple()))
needed = {}
shlib_provider = oe.package.read_shlib_providers(d)
for pkg in shlib_pkgs:
private_libs = d.getVar('PRIVATE_LIBS:' + pkg) or d.getVar('PRIVATE_LIBS') or ""
private_libs = private_libs.split()
needs_ldconfig = False
bb.debug(2, "calculating shlib provides for %s" % pkg)
pkgver = d.getVar('PKGV:' + pkg)
if not pkgver:
pkgver = d.getVar('PV_' + pkg)
if not pkgver:
pkgver = ver
needed[pkg] = set()
sonames = set()
linuxlist = []
for file in pkgfiles[pkg]:
soname = None
if cpath.islink(file):
continue
if hostos.startswith("darwin"):
darwin_so(file, needed, sonames, pkgver)
elif hostos.startswith("mingw"):
mingw_dll(file, needed, sonames, pkgver)
elif os.access(file, os.X_OK) or lib_re.match(file):
linuxlist.append(file)
if linuxlist:
results = oe.utils.multiprocess_launch(linux_so, linuxlist, d, extraargs=(pkg, pkgver, d))
for r in results:
ldconfig = r[0]
needed[pkg] |= r[1]
sonames |= r[2]
needs_ldconfig = needs_ldconfig or ldconfig
shlibs_file = os.path.join(shlibswork_dir, pkg + ".list")
if len(sonames):
with open(shlibs_file, 'w') as fd:
for s in sorted(sonames):
if s[0] in shlib_provider and s[1] in shlib_provider[s[0]]:
(old_pkg, old_pkgver) = shlib_provider[s[0]][s[1]]
if old_pkg != pkg:
bb.warn('%s-%s was registered as shlib provider for %s, changing it to %s-%s because it was built later' % (old_pkg, old_pkgver, s[0], pkg, pkgver))
bb.debug(1, 'registering %s-%s as shlib provider for %s' % (pkg, pkgver, s[0]))
fd.write(s[0] + ':' + s[1] + ':' + s[2] + '\n')
if s[0] not in shlib_provider:
shlib_provider[s[0]] = {}
shlib_provider[s[0]][s[1]] = (pkg, pkgver)
if needs_ldconfig:
bb.debug(1, 'adding ldconfig call to postinst for %s' % pkg)
postinst = d.getVar('pkg_postinst:%s' % pkg)
if not postinst:
postinst = '#!/bin/sh\n'
postinst += d.getVar('ldconfig_postinst_fragment')
d.setVar('pkg_postinst:%s' % pkg, postinst)
bb.debug(1, 'LIBNAMES: pkg %s sonames %s' % (pkg, sonames))
assumed_libs = d.getVar('ASSUME_SHLIBS')
if assumed_libs:
libdir = d.getVar("libdir")
for e in assumed_libs.split():
l, dep_pkg = e.split(":")
lib_ver = None
dep_pkg = dep_pkg.rsplit("_", 1)
if len(dep_pkg) == 2:
lib_ver = dep_pkg[1]
dep_pkg = dep_pkg[0]
if l not in shlib_provider:
shlib_provider[l] = {}
shlib_provider[l][libdir] = (dep_pkg, lib_ver)
libsearchpath = [d.getVar('libdir'), d.getVar('base_libdir')]
for pkg in shlib_pkgs:
bb.debug(2, "calculating shlib requirements for %s" % pkg)
private_libs = d.getVar('PRIVATE_LIBS:' + pkg) or d.getVar('PRIVATE_LIBS') or ""
private_libs = private_libs.split()
deps = list()
for n in needed[pkg]:
# if n is in private libraries, don't try to search provider for it
# this could cause problem in case some abc.bb provides private
# /opt/abc/lib/libfoo.so.1 and contains /usr/bin/abc depending on system library libfoo.so.1
# but skipping it is still better alternative than providing own
# version and then adding runtime dependency for the same system library
if private_libs and len([i for i in private_libs if fnmatch.fnmatch(n[0], i)]) > 0:
bb.debug(2, '%s: Dependency %s covered by PRIVATE_LIBS' % (pkg, n[0]))
continue
if n[0] in shlib_provider.keys():
shlib_provider_map = shlib_provider[n[0]]
matches = set()
for p in itertools.chain(list(n[2]), sorted(shlib_provider_map.keys()), libsearchpath):
if p in shlib_provider_map:
matches.add(p)
if len(matches) > 1:
matchpkgs = ', '.join([shlib_provider_map[match][0] for match in matches])
bb.error("%s: Multiple shlib providers for %s: %s (used by files: %s)" % (pkg, n[0], matchpkgs, n[1]))
elif len(matches) == 1:
(dep_pkg, ver_needed) = shlib_provider_map[matches.pop()]
bb.debug(2, '%s: Dependency %s requires package %s (used by files: %s)' % (pkg, n[0], dep_pkg, n[1]))
if dep_pkg == pkg:
continue
if ver_needed:
dep = "%s (>= %s)" % (dep_pkg, ver_needed)
else:
dep = dep_pkg
if not dep in deps:
deps.append(dep)
continue
bb.note("Couldn't find shared library provider for %s, used by files: %s" % (n[0], n[1]))
deps_file = os.path.join(pkgdest, pkg + ".shlibdeps")
if os.path.exists(deps_file):
os.remove(deps_file)
if deps:
with open(deps_file, 'w') as fd:
for dep in sorted(deps):
fd.write(dep + '\n')
def process_pkgconfig(pkgfiles, d):
packages = d.getVar('PACKAGES')
workdir = d.getVar('WORKDIR')
pkgdest = d.getVar('PKGDEST')
shlibs_dirs = d.getVar('SHLIBSDIRS').split()
shlibswork_dir = d.getVar('SHLIBSWORKDIR')
pc_re = re.compile(r'(.*)\.pc$')
var_re = re.compile(r'(.*)=(.*)')
field_re = re.compile(r'(.*): (.*)')
pkgconfig_provided = {}
pkgconfig_needed = {}
for pkg in packages.split():
pkgconfig_provided[pkg] = []
pkgconfig_needed[pkg] = []
for file in sorted(pkgfiles[pkg]):
m = pc_re.match(file)
if m:
pd = bb.data.init()
name = m.group(1)
pkgconfig_provided[pkg].append(os.path.basename(name))
if not os.access(file, os.R_OK):
continue
with open(file, 'r') as f:
lines = f.readlines()
for l in lines:
m = field_re.match(l)
if m:
hdr = m.group(1)
exp = pd.expand(m.group(2))
if hdr == 'Requires' or hdr == 'Requires.private':
pkgconfig_needed[pkg] += exp.replace(',', ' ').split()
continue
m = var_re.match(l)
if m:
name = m.group(1)
val = m.group(2)
pd.setVar(name, pd.expand(val))
for pkg in packages.split():
pkgs_file = os.path.join(shlibswork_dir, pkg + ".pclist")
if pkgconfig_provided[pkg] != []:
with open(pkgs_file, 'w') as f:
for p in sorted(pkgconfig_provided[pkg]):
f.write('%s\n' % p)
# Go from least to most specific since the last one found wins
for dir in reversed(shlibs_dirs):
if not os.path.exists(dir):
continue
for file in sorted(os.listdir(dir)):
m = re.match(r'^(.*)\.pclist$', file)
if m:
pkg = m.group(1)
with open(os.path.join(dir, file)) as fd:
lines = fd.readlines()
pkgconfig_provided[pkg] = []
for l in lines:
pkgconfig_provided[pkg].append(l.rstrip())
for pkg in packages.split():
deps = []
for n in pkgconfig_needed[pkg]:
found = False
for k in pkgconfig_provided.keys():
if n in pkgconfig_provided[k]:
if k != pkg and not (k in deps):
deps.append(k)
found = True
if found == False:
bb.note("couldn't find pkgconfig module '%s' in any package" % n)
deps_file = os.path.join(pkgdest, pkg + ".pcdeps")
if len(deps):
with open(deps_file, 'w') as fd:
for dep in deps:
fd.write(dep + '\n')
def read_libdep_files(d):
pkglibdeps = {}
packages = d.getVar('PACKAGES').split()
for pkg in packages:
pkglibdeps[pkg] = {}
for extension in ".shlibdeps", ".pcdeps", ".clilibdeps":
depsfile = d.expand("${PKGDEST}/" + pkg + extension)
if os.access(depsfile, os.R_OK):
with open(depsfile) as fd:
lines = fd.readlines()
for l in lines:
l.rstrip()
deps = bb.utils.explode_dep_versions2(l)
for dep in deps:
if not dep in pkglibdeps[pkg]:
pkglibdeps[pkg][dep] = deps[dep]
return pkglibdeps
def process_depchains(pkgfiles, d):
"""
For a given set of prefix and postfix modifiers, make those packages
RRECOMMENDS on the corresponding packages for its RDEPENDS.
Example: If package A depends upon package B, and A's .bb emits an
A-dev package, this would make A-dev Recommends: B-dev.
If only one of a given suffix is specified, it will take the RRECOMMENDS
based on the RDEPENDS of *all* other packages. If more than one of a given
suffix is specified, its will only use the RDEPENDS of the single parent
package.
"""
packages = d.getVar('PACKAGES')
postfixes = (d.getVar('DEPCHAIN_POST') or '').split()
prefixes = (d.getVar('DEPCHAIN_PRE') or '').split()
def pkg_adddeprrecs(pkg, base, suffix, getname, depends, d):
#bb.note('depends for %s is %s' % (base, depends))
rreclist = bb.utils.explode_dep_versions2(d.getVar('RRECOMMENDS:' + pkg) or "")
for depend in sorted(depends):
if depend.find('-native') != -1 or depend.find('-cross') != -1 or depend.startswith('virtual/'):
#bb.note("Skipping %s" % depend)
continue
if depend.endswith('-dev'):
depend = depend[:-4]
if depend.endswith('-dbg'):
depend = depend[:-4]
pkgname = getname(depend, suffix)
#bb.note("Adding %s for %s" % (pkgname, depend))
if pkgname not in rreclist and pkgname != pkg:
rreclist[pkgname] = []
#bb.note('setting: RRECOMMENDS:%s=%s' % (pkg, ' '.join(rreclist)))
d.setVar('RRECOMMENDS:%s' % pkg, bb.utils.join_deps(rreclist, commasep=False))
def pkg_addrrecs(pkg, base, suffix, getname, rdepends, d):
#bb.note('rdepends for %s is %s' % (base, rdepends))
rreclist = bb.utils.explode_dep_versions2(d.getVar('RRECOMMENDS:' + pkg) or "")
for depend in sorted(rdepends):
if depend.find('virtual-locale-') != -1:
#bb.note("Skipping %s" % depend)
continue
if depend.endswith('-dev'):
depend = depend[:-4]
if depend.endswith('-dbg'):
depend = depend[:-4]
pkgname = getname(depend, suffix)
#bb.note("Adding %s for %s" % (pkgname, depend))
if pkgname not in rreclist and pkgname != pkg:
rreclist[pkgname] = []
#bb.note('setting: RRECOMMENDS:%s=%s' % (pkg, ' '.join(rreclist)))
d.setVar('RRECOMMENDS:%s' % pkg, bb.utils.join_deps(rreclist, commasep=False))
def add_dep(list, dep):
if dep not in list:
list.append(dep)
depends = []
for dep in bb.utils.explode_deps(d.getVar('DEPENDS') or ""):
add_dep(depends, dep)
rdepends = []
for pkg in packages.split():
for dep in bb.utils.explode_deps(d.getVar('RDEPENDS:' + pkg) or ""):
add_dep(rdepends, dep)
#bb.note('rdepends is %s' % rdepends)
def post_getname(name, suffix):
return '%s%s' % (name, suffix)
def pre_getname(name, suffix):
return '%s%s' % (suffix, name)
pkgs = {}
for pkg in packages.split():
for postfix in postfixes:
if pkg.endswith(postfix):
if not postfix in pkgs:
pkgs[postfix] = {}
pkgs[postfix][pkg] = (pkg[:-len(postfix)], post_getname)
for prefix in prefixes:
if pkg.startswith(prefix):
if not prefix in pkgs:
pkgs[prefix] = {}
pkgs[prefix][pkg] = (pkg[:-len(prefix)], pre_getname)
if "-dbg" in pkgs:
pkglibdeps = read_libdep_files(d)
pkglibdeplist = []
for pkg in pkglibdeps:
for k in pkglibdeps[pkg]:
add_dep(pkglibdeplist, k)
dbgdefaultdeps = ((d.getVar('DEPCHAIN_DBGDEFAULTDEPS') == '1') or (bb.data.inherits_class('packagegroup', d)))
for suffix in pkgs:
for pkg in pkgs[suffix]:
if d.getVarFlag('RRECOMMENDS:' + pkg, 'nodeprrecs'):
continue
(base, func) = pkgs[suffix][pkg]
if suffix == "-dev":
pkg_adddeprrecs(pkg, base, suffix, func, depends, d)
elif suffix == "-dbg":
if not dbgdefaultdeps:
pkg_addrrecs(pkg, base, suffix, func, pkglibdeplist, d)
continue
if len(pkgs[suffix]) == 1:
pkg_addrrecs(pkg, base, suffix, func, rdepends, d)
else:
rdeps = []
for dep in bb.utils.explode_deps(d.getVar('RDEPENDS:' + base) or ""):
add_dep(rdeps, dep)
pkg_addrrecs(pkg, base, suffix, func, rdeps, d)