xref: /openbmc/openbmc/poky/meta/lib/oe/package.py (revision 15ae2509)
1#
2# SPDX-License-Identifier: GPL-2.0-only
3#
4
5import stat
6import mmap
7import subprocess
8
9def runstrip(arg):
10    # Function to strip a single file, called from split_and_strip_files below
11    # A working 'file' (one which works on the target architecture)
12    #
13    # The elftype is a bit pattern (explained in is_elf below) to tell
14    # us what type of file we're processing...
15    # 4 - executable
16    # 8 - shared library
17    # 16 - kernel module
18
19    (file, elftype, strip) = arg
20
21    newmode = None
22    if not os.access(file, os.W_OK) or os.access(file, os.R_OK):
23        origmode = os.stat(file)[stat.ST_MODE]
24        newmode = origmode | stat.S_IWRITE | stat.S_IREAD
25        os.chmod(file, newmode)
26
27    stripcmd = [strip]
28    skip_strip = False
29    # kernel module
30    if elftype & 16:
31        if is_kernel_module_signed(file):
32            bb.debug(1, "Skip strip on signed module %s" % file)
33            skip_strip = True
34        else:
35            stripcmd.extend(["--strip-debug", "--remove-section=.comment",
36                "--remove-section=.note", "--preserve-dates"])
37    # .so and shared library
38    elif ".so" in file and elftype & 8:
39        stripcmd.extend(["--remove-section=.comment", "--remove-section=.note", "--strip-unneeded"])
40    # shared or executable:
41    elif elftype & 8 or elftype & 4:
42        stripcmd.extend(["--remove-section=.comment", "--remove-section=.note"])
43
44    stripcmd.append(file)
45    bb.debug(1, "runstrip: %s" % stripcmd)
46
47    if not skip_strip:
48        output = subprocess.check_output(stripcmd, stderr=subprocess.STDOUT)
49
50    if newmode:
51        os.chmod(file, origmode)
52
53# Detect .ko module by searching for "vermagic=" string
54def is_kernel_module(path):
55    with open(path) as f:
56        return mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ).find(b"vermagic=") >= 0
57
58# Detect if .ko module is signed
59def is_kernel_module_signed(path):
60    with open(path, "rb") as f:
61        f.seek(-28, 2)
62        module_tail = f.read()
63        return "Module signature appended" in "".join(chr(c) for c in bytearray(module_tail))
64
65# Return type (bits):
66# 0 - not elf
67# 1 - ELF
68# 2 - stripped
69# 4 - executable
70# 8 - shared library
71# 16 - kernel module
72def is_elf(path):
73    exec_type = 0
74    result = subprocess.check_output(["file", "-b", path], stderr=subprocess.STDOUT).decode("utf-8")
75
76    if "ELF" in result:
77        exec_type |= 1
78        if "not stripped" not in result:
79            exec_type |= 2
80        if "executable" in result:
81            exec_type |= 4
82        if "shared" in result:
83            exec_type |= 8
84        if "relocatable" in result:
85            if path.endswith(".ko") and path.find("/lib/modules/") != -1 and is_kernel_module(path):
86                exec_type |= 16
87    return (path, exec_type)
88
89def is_static_lib(path):
90    if path.endswith('.a') and not os.path.islink(path):
91        with open(path, 'rb') as fh:
92            # The magic must include the first slash to avoid
93            # matching golang static libraries
94            magic = b'!<arch>\x0a/'
95            start = fh.read(len(magic))
96            return start == magic
97    return False
98
99def strip_execs(pn, dstdir, strip_cmd, libdir, base_libdir, d, qa_already_stripped=False):
100    """
101    Strip executable code (like executables, shared libraries) _in_place_
102    - Based on sysroot_strip in staging.bbclass
103    :param dstdir: directory in which to strip files
104    :param strip_cmd: Strip command (usually ${STRIP})
105    :param libdir: ${libdir} - strip .so files in this directory
106    :param base_libdir: ${base_libdir} - strip .so files in this directory
107    :param qa_already_stripped: Set to True if already-stripped' in ${INSANE_SKIP}
108    This is for proper logging and messages only.
109    """
110    import stat, errno, oe.path, oe.utils
111
112    elffiles = {}
113    inodes = {}
114    libdir = os.path.abspath(dstdir + os.sep + libdir)
115    base_libdir = os.path.abspath(dstdir + os.sep + base_libdir)
116    exec_mask = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
117    #
118    # First lets figure out all of the files we may have to process
119    #
120    checkelf = []
121    inodecache = {}
122    for root, dirs, files in os.walk(dstdir):
123        for f in files:
124            file = os.path.join(root, f)
125
126            try:
127                ltarget = oe.path.realpath(file, dstdir, False)
128                s = os.lstat(ltarget)
129            except OSError as e:
130                (err, strerror) = e.args
131                if err != errno.ENOENT:
132                    raise
133                # Skip broken symlinks
134                continue
135            if not s:
136                continue
137            # Check its an excutable
138            if s[stat.ST_MODE] & exec_mask \
139                    or ((file.startswith(libdir) or file.startswith(base_libdir)) and ".so" in f) \
140                    or file.endswith('.ko'):
141                # If it's a symlink, and points to an ELF file, we capture the readlink target
142                if os.path.islink(file):
143                    continue
144
145                # It's a file (or hardlink), not a link
146                # ...but is it ELF, and is it already stripped?
147                checkelf.append(file)
148                inodecache[file] = s.st_ino
149    results = oe.utils.multiprocess_launch(is_elf, checkelf, d)
150    for (file, elf_file) in results:
151                #elf_file = is_elf(file)
152                if elf_file & 1:
153                    if elf_file & 2:
154                        if qa_already_stripped:
155                            bb.note("Skipping file %s from %s for already-stripped QA test" % (file[len(dstdir):], pn))
156                        else:
157                            bb.warn("File '%s' from %s was already stripped, this will prevent future debugging!" % (file[len(dstdir):], pn))
158                        continue
159
160                    if inodecache[file] in inodes:
161                        os.unlink(file)
162                        os.link(inodes[inodecache[file]], file)
163                    else:
164                        # break hardlinks so that we do not strip the original.
165                        inodes[inodecache[file]] = file
166                        bb.utils.break_hardlinks(file)
167                        elffiles[file] = elf_file
168
169    #
170    # Now strip them (in parallel)
171    #
172    sfiles = []
173    for file in elffiles:
174        elf_file = int(elffiles[file])
175        sfiles.append((file, elf_file, strip_cmd))
176
177    oe.utils.multiprocess_launch(runstrip, sfiles, d)
178
179
180def file_translate(file):
181    ft = file.replace("@", "@at@")
182    ft = ft.replace(" ", "@space@")
183    ft = ft.replace("\t", "@tab@")
184    ft = ft.replace("[", "@openbrace@")
185    ft = ft.replace("]", "@closebrace@")
186    ft = ft.replace("_", "@underscore@")
187    return ft
188
189def filedeprunner(arg):
190    import re, subprocess, shlex
191
192    (pkg, pkgfiles, rpmdeps, pkgdest) = arg
193    provides = {}
194    requires = {}
195
196    file_re = re.compile(r'\s+\d+\s(.*)')
197    dep_re = re.compile(r'\s+(\S)\s+(.*)')
198    r = re.compile(r'[<>=]+\s+\S*')
199
200    def process_deps(pipe, pkg, pkgdest, provides, requires):
201        file = None
202        for line in pipe.split("\n"):
203
204            m = file_re.match(line)
205            if m:
206                file = m.group(1)
207                file = file.replace(pkgdest + "/" + pkg, "")
208                file = file_translate(file)
209                continue
210
211            m = dep_re.match(line)
212            if not m or not file:
213                continue
214
215            type, dep = m.groups()
216
217            if type == 'R':
218                i = requires
219            elif type == 'P':
220                i = provides
221            else:
222               continue
223
224            if dep.startswith("python("):
225                continue
226
227            # Ignore all perl(VMS::...) and perl(Mac::...) dependencies. These
228            # are typically used conditionally from the Perl code, but are
229            # generated as unconditional dependencies.
230            if dep.startswith('perl(VMS::') or dep.startswith('perl(Mac::'):
231                continue
232
233            # Ignore perl dependencies on .pl files.
234            if dep.startswith('perl(') and dep.endswith('.pl)'):
235                continue
236
237            # Remove perl versions and perl module versions since they typically
238            # do not make sense when used as package versions.
239            if dep.startswith('perl') and r.search(dep):
240                dep = dep.split()[0]
241
242            # Put parentheses around any version specifications.
243            dep = r.sub(r'(\g<0>)',dep)
244
245            if file not in i:
246                i[file] = []
247            i[file].append(dep)
248
249        return provides, requires
250
251    output = subprocess.check_output(shlex.split(rpmdeps) + pkgfiles, stderr=subprocess.STDOUT).decode("utf-8")
252    provides, requires = process_deps(output, pkg, pkgdest, provides, requires)
253
254    return (pkg, provides, requires)
255
256
257def read_shlib_providers(d):
258    import re
259
260    shlib_provider = {}
261    shlibs_dirs = d.getVar('SHLIBSDIRS').split()
262    list_re = re.compile(r'^(.*)\.list$')
263    # Go from least to most specific since the last one found wins
264    for dir in reversed(shlibs_dirs):
265        bb.debug(2, "Reading shlib providers in %s" % (dir))
266        if not os.path.exists(dir):
267            continue
268        for file in os.listdir(dir):
269            m = list_re.match(file)
270            if m:
271                dep_pkg = m.group(1)
272                try:
273                    fd = open(os.path.join(dir, file))
274                except IOError:
275                    # During a build unrelated shlib files may be deleted, so
276                    # handle files disappearing between the listdirs and open.
277                    continue
278                lines = fd.readlines()
279                fd.close()
280                for l in lines:
281                    s = l.strip().split(":")
282                    if s[0] not in shlib_provider:
283                        shlib_provider[s[0]] = {}
284                    shlib_provider[s[0]][s[1]] = (dep_pkg, s[2])
285    return shlib_provider
286
287
288def npm_split_package_dirs(pkgdir):
289    """
290    Work out the packages fetched and unpacked by BitBake's npm fetcher
291    Returns a dict of packagename -> (relpath, package.json) ordered
292    such that it is suitable for use in PACKAGES and FILES
293    """
294    from collections import OrderedDict
295    import json
296    packages = {}
297    for root, dirs, files in os.walk(pkgdir):
298        if os.path.basename(root) == 'node_modules':
299            for dn in dirs:
300                relpth = os.path.relpath(os.path.join(root, dn), pkgdir)
301                pkgitems = ['${PN}']
302                for pathitem in relpth.split('/'):
303                    if pathitem == 'node_modules':
304                        continue
305                    pkgitems.append(pathitem)
306                pkgname = '-'.join(pkgitems).replace('_', '-')
307                pkgname = pkgname.replace('@', '')
308                pkgfile = os.path.join(root, dn, 'package.json')
309                data = None
310                if os.path.exists(pkgfile):
311                    with open(pkgfile, 'r') as f:
312                        data = json.loads(f.read())
313                    packages[pkgname] = (relpth, data)
314    # We want the main package for a module sorted *after* its subpackages
315    # (so that it doesn't otherwise steal the files for the subpackage), so
316    # this is a cheap way to do that whilst still having an otherwise
317    # alphabetical sort
318    return OrderedDict((key, packages[key]) for key in sorted(packages, key=lambda pkg: pkg + '~'))
319