xref: /openbmc/openbmc/poky/meta/lib/oe/rootfs.py (revision 5a43b434)
1#
2# SPDX-License-Identifier: GPL-2.0-only
3#
4from abc import ABCMeta, abstractmethod
5from oe.utils import execute_pre_post_process
6from oe.package_manager import *
7from oe.manifest import *
8import oe.path
9import filecmp
10import shutil
11import os
12import subprocess
13import re
14
15
16class Rootfs(object, metaclass=ABCMeta):
17    """
18    This is an abstract class. Do not instantiate this directly.
19    """
20
21    def __init__(self, d, progress_reporter=None, logcatcher=None):
22        self.d = d
23        self.pm = None
24        self.image_rootfs = self.d.getVar('IMAGE_ROOTFS')
25        self.deploydir = self.d.getVar('IMGDEPLOYDIR')
26        self.progress_reporter = progress_reporter
27        self.logcatcher = logcatcher
28
29        self.install_order = Manifest.INSTALL_ORDER
30
31    @abstractmethod
32    def _create(self):
33        pass
34
35    @abstractmethod
36    def _get_delayed_postinsts(self):
37        pass
38
39    @abstractmethod
40    def _save_postinsts(self):
41        pass
42
43    @abstractmethod
44    def _log_check(self):
45        pass
46
47    def _log_check_common(self, type, match):
48        # Ignore any lines containing log_check to avoid recursion, and ignore
49        # lines beginning with a + since sh -x may emit code which isn't
50        # actually executed, but may contain error messages
51        excludes = [ 'log_check', r'^\+' ]
52        if hasattr(self, 'log_check_expected_regexes'):
53            excludes.extend(self.log_check_expected_regexes)
54        excludes = [re.compile(x) for x in excludes]
55        r = re.compile(match)
56        log_path = self.d.expand("${T}/log.do_rootfs")
57        messages = []
58        with open(log_path, 'r') as log:
59            for line in log:
60                if self.logcatcher and self.logcatcher.contains(line.rstrip()):
61                    continue
62                for ee in excludes:
63                    m = ee.search(line)
64                    if m:
65                        break
66                if m:
67                    continue
68
69                m = r.search(line)
70                if m:
71                    messages.append('[log_check] %s' % line)
72        if messages:
73            if len(messages) == 1:
74                msg = '1 %s message' % type
75            else:
76                msg = '%d %s messages' % (len(messages), type)
77            msg = '[log_check] %s: found %s in the logfile:\n%s' % \
78                (self.d.getVar('PN'), msg, ''.join(messages))
79            if type == 'error':
80                bb.fatal(msg)
81            else:
82                bb.warn(msg)
83
84    def _log_check_warn(self):
85        self._log_check_common('warning', '^(warn|Warn|WARNING:)')
86
87    def _log_check_error(self):
88        self._log_check_common('error', self.log_check_regex)
89
90    def _insert_feed_uris(self):
91        if bb.utils.contains("IMAGE_FEATURES", "package-management",
92                         True, False, self.d):
93            self.pm.insert_feeds_uris(self.d.getVar('PACKAGE_FEED_URIS') or "",
94                self.d.getVar('PACKAGE_FEED_BASE_PATHS') or "",
95                self.d.getVar('PACKAGE_FEED_ARCHS'))
96
97
98    """
99    The _cleanup() method should be used to clean-up stuff that we don't really
100    want to end up on target. For example, in the case of RPM, the DB locks.
101    The method is called, once, at the end of create() method.
102    """
103    @abstractmethod
104    def _cleanup(self):
105        pass
106
107    def _setup_dbg_rootfs(self, dirs):
108        gen_debugfs = self.d.getVar('IMAGE_GEN_DEBUGFS') or '0'
109        if gen_debugfs != '1':
110           return
111
112        bb.note("  Renaming the original rootfs...")
113        try:
114            shutil.rmtree(self.image_rootfs + '-orig')
115        except:
116            pass
117        os.rename(self.image_rootfs, self.image_rootfs + '-orig')
118
119        bb.note("  Creating debug rootfs...")
120        bb.utils.mkdirhier(self.image_rootfs)
121
122        bb.note("  Copying back package database...")
123        for dir in dirs:
124            if not os.path.isdir(self.image_rootfs + '-orig' + dir):
125                continue
126            bb.utils.mkdirhier(self.image_rootfs + os.path.dirname(dir))
127            shutil.copytree(self.image_rootfs + '-orig' + dir, self.image_rootfs + dir, symlinks=True)
128
129        # Copy files located in /usr/lib/debug or /usr/src/debug
130        for dir in ["/usr/lib/debug", "/usr/src/debug"]:
131            src = self.image_rootfs + '-orig' + dir
132            if os.path.exists(src):
133                dst = self.image_rootfs + dir
134                bb.utils.mkdirhier(os.path.dirname(dst))
135                shutil.copytree(src, dst)
136
137        # Copy files with suffix '.debug' or located in '.debug' dir.
138        for root, dirs, files in os.walk(self.image_rootfs + '-orig'):
139            relative_dir = root[len(self.image_rootfs + '-orig'):]
140            for f in files:
141                if f.endswith('.debug') or '/.debug' in relative_dir:
142                    bb.utils.mkdirhier(self.image_rootfs + relative_dir)
143                    shutil.copy(os.path.join(root, f),
144                                self.image_rootfs + relative_dir)
145
146        bb.note("  Install complementary '*-dbg' packages...")
147        self.pm.install_complementary('*-dbg')
148
149        if self.d.getVar('PACKAGE_DEBUG_SPLIT_STYLE') == 'debug-with-srcpkg':
150            bb.note("  Install complementary '*-src' packages...")
151            self.pm.install_complementary('*-src')
152
153        """
154        Install additional debug packages. Possibility to install additional packages,
155        which are not automatically installed as complementary package of
156        standard one, e.g. debug package of static libraries.
157        """
158        extra_debug_pkgs = self.d.getVar('IMAGE_INSTALL_DEBUGFS')
159        if extra_debug_pkgs:
160            bb.note("  Install extra debug packages...")
161            self.pm.install(extra_debug_pkgs.split(), True)
162
163        bb.note("  Rename debug rootfs...")
164        try:
165            shutil.rmtree(self.image_rootfs + '-dbg')
166        except:
167            pass
168        os.rename(self.image_rootfs, self.image_rootfs + '-dbg')
169
170        bb.note("  Restoreing original rootfs...")
171        os.rename(self.image_rootfs + '-orig', self.image_rootfs)
172
173    def _exec_shell_cmd(self, cmd):
174        fakerootcmd = self.d.getVar('FAKEROOT')
175        if fakerootcmd is not None:
176            exec_cmd = [fakerootcmd, cmd]
177        else:
178            exec_cmd = cmd
179
180        try:
181            subprocess.check_output(exec_cmd, stderr=subprocess.STDOUT)
182        except subprocess.CalledProcessError as e:
183            return("Command '%s' returned %d:\n%s" % (e.cmd, e.returncode, e.output))
184
185        return None
186
187    def create(self):
188        bb.note("###### Generate rootfs #######")
189        pre_process_cmds = self.d.getVar("ROOTFS_PREPROCESS_COMMAND")
190        post_process_cmds = self.d.getVar("ROOTFS_POSTPROCESS_COMMAND")
191        rootfs_post_install_cmds = self.d.getVar('ROOTFS_POSTINSTALL_COMMAND')
192
193        bb.utils.mkdirhier(self.image_rootfs)
194
195        bb.utils.mkdirhier(self.deploydir)
196
197        execute_pre_post_process(self.d, pre_process_cmds)
198
199        if self.progress_reporter:
200            self.progress_reporter.next_stage()
201
202        # call the package manager dependent create method
203        self._create()
204
205        sysconfdir = self.image_rootfs + self.d.getVar('sysconfdir')
206        bb.utils.mkdirhier(sysconfdir)
207        with open(sysconfdir + "/version", "w+") as ver:
208            ver.write(self.d.getVar('BUILDNAME') + "\n")
209
210        execute_pre_post_process(self.d, rootfs_post_install_cmds)
211
212        self.pm.run_intercepts()
213
214        execute_pre_post_process(self.d, post_process_cmds)
215
216        if self.progress_reporter:
217            self.progress_reporter.next_stage()
218
219        if bb.utils.contains("IMAGE_FEATURES", "read-only-rootfs",
220                         True, False, self.d):
221            delayed_postinsts = self._get_delayed_postinsts()
222            if delayed_postinsts is not None:
223                bb.fatal("The following packages could not be configured "
224                         "offline and rootfs is read-only: %s" %
225                         delayed_postinsts)
226
227        if self.d.getVar('USE_DEVFS') != "1":
228            self._create_devfs()
229
230        self._uninstall_unneeded()
231
232        if self.progress_reporter:
233            self.progress_reporter.next_stage()
234
235        self._insert_feed_uris()
236
237        self._run_ldconfig()
238
239        if self.d.getVar('USE_DEPMOD') != "0":
240            self._generate_kernel_module_deps()
241
242        self._cleanup()
243        self._log_check()
244
245        if self.progress_reporter:
246            self.progress_reporter.next_stage()
247
248
249    def _uninstall_unneeded(self):
250        # Remove unneeded init script symlinks
251        delayed_postinsts = self._get_delayed_postinsts()
252        if delayed_postinsts is None:
253            if os.path.exists(self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/init.d/run-postinsts")):
254                self._exec_shell_cmd(["update-rc.d", "-f", "-r",
255                                      self.d.getVar('IMAGE_ROOTFS'),
256                                      "run-postinsts", "remove"])
257
258        image_rorfs = bb.utils.contains("IMAGE_FEATURES", "read-only-rootfs",
259                                        True, False, self.d)
260        image_rorfs_force = self.d.getVar('FORCE_RO_REMOVE')
261
262        if image_rorfs or image_rorfs_force == "1":
263            # Remove components that we don't need if it's a read-only rootfs
264            unneeded_pkgs = self.d.getVar("ROOTFS_RO_UNNEEDED").split()
265            pkgs_installed = image_list_installed_packages(self.d)
266            # Make sure update-alternatives is removed last. This is
267            # because its database has to available while uninstalling
268            # other packages, allowing alternative symlinks of packages
269            # to be uninstalled or to be managed correctly otherwise.
270            provider = self.d.getVar("VIRTUAL-RUNTIME_update-alternatives")
271            pkgs_to_remove = sorted([pkg for pkg in pkgs_installed if pkg in unneeded_pkgs], key=lambda x: x == provider)
272
273            # update-alternatives provider is removed in its own remove()
274            # call because all package managers do not guarantee the packages
275            # are removed in the order they given in the list (which is
276            # passed to the command line). The sorting done earlier is
277            # utilized to implement the 2-stage removal.
278            if len(pkgs_to_remove) > 1:
279                self.pm.remove(pkgs_to_remove[:-1], False)
280            if len(pkgs_to_remove) > 0:
281                self.pm.remove([pkgs_to_remove[-1]], False)
282
283        if delayed_postinsts:
284            self._save_postinsts()
285            if image_rorfs:
286                bb.warn("There are post install scripts "
287                        "in a read-only rootfs")
288
289        post_uninstall_cmds = self.d.getVar("ROOTFS_POSTUNINSTALL_COMMAND")
290        execute_pre_post_process(self.d, post_uninstall_cmds)
291
292        runtime_pkgmanage = bb.utils.contains("IMAGE_FEATURES", "package-management",
293                                              True, False, self.d)
294        if not runtime_pkgmanage:
295            # Remove the package manager data files
296            self.pm.remove_packaging_data()
297
298    def _run_ldconfig(self):
299        if self.d.getVar('LDCONFIGDEPEND'):
300            bb.note("Executing: ldconfig -r " + self.image_rootfs + " -c new -v")
301            self._exec_shell_cmd(['ldconfig', '-r', self.image_rootfs, '-c',
302                                  'new', '-v'])
303
304    def _check_for_kernel_modules(self, modules_dir):
305        for root, dirs, files in os.walk(modules_dir, topdown=True):
306            for name in files:
307                found_ko = name.endswith(".ko")
308                if found_ko:
309                    return found_ko
310        return False
311
312    def _generate_kernel_module_deps(self):
313        modules_dir = os.path.join(self.image_rootfs, 'lib', 'modules')
314        # if we don't have any modules don't bother to do the depmod
315        if not self._check_for_kernel_modules(modules_dir):
316            bb.note("No Kernel Modules found, not running depmod")
317            return
318
319        kernel_abi_ver_file = oe.path.join(self.d.getVar('PKGDATA_DIR'), "kernel-depmod",
320                                           'kernel-abiversion')
321        if not os.path.exists(kernel_abi_ver_file):
322            bb.fatal("No kernel-abiversion file found (%s), cannot run depmod, aborting" % kernel_abi_ver_file)
323
324        kernel_ver = open(kernel_abi_ver_file).read().strip(' \n')
325        versioned_modules_dir = os.path.join(self.image_rootfs, modules_dir, kernel_ver)
326
327        bb.utils.mkdirhier(versioned_modules_dir)
328
329        self._exec_shell_cmd(['depmodwrapper', '-a', '-b', self.image_rootfs, kernel_ver])
330
331    """
332    Create devfs:
333    * IMAGE_DEVICE_TABLE is the old name to an absolute path to a device table file
334    * IMAGE_DEVICE_TABLES is a new name for a file, or list of files, seached
335      for in the BBPATH
336    If neither are specified then the default name of files/device_table-minimal.txt
337    is searched for in the BBPATH (same as the old version.)
338    """
339    def _create_devfs(self):
340        devtable_list = []
341        devtable = self.d.getVar('IMAGE_DEVICE_TABLE')
342        if devtable is not None:
343            devtable_list.append(devtable)
344        else:
345            devtables = self.d.getVar('IMAGE_DEVICE_TABLES')
346            if devtables is None:
347                devtables = 'files/device_table-minimal.txt'
348            for devtable in devtables.split():
349                devtable_list.append("%s" % bb.utils.which(self.d.getVar('BBPATH'), devtable))
350
351        for devtable in devtable_list:
352            self._exec_shell_cmd(["makedevs", "-r",
353                                  self.image_rootfs, "-D", devtable])
354
355
356class RpmRootfs(Rootfs):
357    def __init__(self, d, manifest_dir, progress_reporter=None, logcatcher=None):
358        super(RpmRootfs, self).__init__(d, progress_reporter, logcatcher)
359        self.log_check_regex = r'(unpacking of archive failed|Cannot find package'\
360                               r'|exit 1|ERROR: |Error: |Error |ERROR '\
361                               r'|Failed |Failed: |Failed$|Failed\(\d+\):)'
362        self.manifest = RpmManifest(d, manifest_dir)
363
364        self.pm = RpmPM(d,
365                        d.getVar('IMAGE_ROOTFS'),
366                        self.d.getVar('TARGET_VENDOR')
367                        )
368
369        self.inc_rpm_image_gen = self.d.getVar('INC_RPM_IMAGE_GEN')
370        if self.inc_rpm_image_gen != "1":
371            bb.utils.remove(self.image_rootfs, True)
372        else:
373            self.pm.recovery_packaging_data()
374        bb.utils.remove(self.d.getVar('MULTILIB_TEMP_ROOTFS'), True)
375
376        self.pm.create_configs()
377
378    '''
379    While rpm incremental image generation is enabled, it will remove the
380    unneeded pkgs by comparing the new install solution manifest and the
381    old installed manifest.
382    '''
383    def _create_incremental(self, pkgs_initial_install):
384        if self.inc_rpm_image_gen == "1":
385
386            pkgs_to_install = list()
387            for pkg_type in pkgs_initial_install:
388                pkgs_to_install += pkgs_initial_install[pkg_type]
389
390            installed_manifest = self.pm.load_old_install_solution()
391            solution_manifest = self.pm.dump_install_solution(pkgs_to_install)
392
393            pkg_to_remove = list()
394            for pkg in installed_manifest:
395                if pkg not in solution_manifest:
396                    pkg_to_remove.append(pkg)
397
398            self.pm.update()
399
400            bb.note('incremental update -- upgrade packages in place ')
401            self.pm.upgrade()
402            if pkg_to_remove != []:
403                bb.note('incremental removed: %s' % ' '.join(pkg_to_remove))
404                self.pm.remove(pkg_to_remove)
405
406            self.pm.autoremove()
407
408    def _create(self):
409        pkgs_to_install = self.manifest.parse_initial_manifest()
410        rpm_pre_process_cmds = self.d.getVar('RPM_PREPROCESS_COMMANDS')
411        rpm_post_process_cmds = self.d.getVar('RPM_POSTPROCESS_COMMANDS')
412
413        # update PM index files
414        self.pm.write_index()
415
416        execute_pre_post_process(self.d, rpm_pre_process_cmds)
417
418        if self.progress_reporter:
419            self.progress_reporter.next_stage()
420
421        if self.inc_rpm_image_gen == "1":
422            self._create_incremental(pkgs_to_install)
423
424        if self.progress_reporter:
425            self.progress_reporter.next_stage()
426
427        self.pm.update()
428
429        pkgs = []
430        pkgs_attempt = []
431        for pkg_type in pkgs_to_install:
432            if pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY:
433                pkgs_attempt += pkgs_to_install[pkg_type]
434            else:
435                pkgs += pkgs_to_install[pkg_type]
436
437        if self.progress_reporter:
438            self.progress_reporter.next_stage()
439
440        self.pm.install(pkgs)
441
442        if self.progress_reporter:
443            self.progress_reporter.next_stage()
444
445        self.pm.install(pkgs_attempt, True)
446
447        if self.progress_reporter:
448            self.progress_reporter.next_stage()
449
450        self.pm.install_complementary()
451
452        if self.progress_reporter:
453            self.progress_reporter.next_stage()
454
455        self._setup_dbg_rootfs(['/etc', '/var/lib/rpm', '/var/cache/dnf', '/var/lib/dnf'])
456
457        execute_pre_post_process(self.d, rpm_post_process_cmds)
458
459        if self.inc_rpm_image_gen == "1":
460            self.pm.backup_packaging_data()
461
462        if self.progress_reporter:
463            self.progress_reporter.next_stage()
464
465
466    @staticmethod
467    def _depends_list():
468        return ['DEPLOY_DIR_RPM', 'INC_RPM_IMAGE_GEN', 'RPM_PREPROCESS_COMMANDS',
469                'RPM_POSTPROCESS_COMMANDS', 'RPM_PREFER_ELF_ARCH']
470
471    def _get_delayed_postinsts(self):
472        postinst_dir = self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/rpm-postinsts")
473        if os.path.isdir(postinst_dir):
474            files = os.listdir(postinst_dir)
475            for f in files:
476                bb.note('Delayed package scriptlet: %s' % f)
477            return files
478
479        return None
480
481    def _save_postinsts(self):
482        # this is just a stub. For RPM, the failed postinstalls are
483        # already saved in /etc/rpm-postinsts
484        pass
485
486    def _log_check(self):
487        self._log_check_warn()
488        self._log_check_error()
489
490    def _cleanup(self):
491        if bb.utils.contains("IMAGE_FEATURES", "package-management", True, False, self.d):
492            self.pm._invoke_dnf(["clean", "all"])
493
494
495class DpkgOpkgRootfs(Rootfs):
496    def __init__(self, d, progress_reporter=None, logcatcher=None):
497        super(DpkgOpkgRootfs, self).__init__(d, progress_reporter, logcatcher)
498
499    def _get_pkgs_postinsts(self, status_file):
500        def _get_pkg_depends_list(pkg_depends):
501            pkg_depends_list = []
502            # filter version requirements like libc (>= 1.1)
503            for dep in pkg_depends.split(', '):
504                m_dep = re.match(r"^(.*) \(.*\)$", dep)
505                if m_dep:
506                    dep = m_dep.group(1)
507                pkg_depends_list.append(dep)
508
509            return pkg_depends_list
510
511        pkgs = {}
512        pkg_name = ""
513        pkg_status_match = False
514        pkg_depends = ""
515
516        with open(status_file) as status:
517            data = status.read()
518            status.close()
519            for line in data.split('\n'):
520                m_pkg = re.match(r"^Package: (.*)", line)
521                m_status = re.match(r"^Status:.*unpacked", line)
522                m_depends = re.match(r"^Depends: (.*)", line)
523
524                #Only one of m_pkg, m_status or m_depends is not None at time
525                #If m_pkg is not None, we started a new package
526                if m_pkg is not None:
527                    #Get Package name
528                    pkg_name = m_pkg.group(1)
529                    #Make sure we reset other variables
530                    pkg_status_match = False
531                    pkg_depends = ""
532                elif m_status is not None:
533                    #New status matched
534                    pkg_status_match = True
535                elif m_depends is not None:
536                    #New depends macthed
537                    pkg_depends = m_depends.group(1)
538                else:
539                    pass
540
541                #Now check if we can process package depends and postinst
542                if "" != pkg_name and pkg_status_match:
543                    pkgs[pkg_name] = _get_pkg_depends_list(pkg_depends)
544                else:
545                    #Not enough information
546                    pass
547
548        # remove package dependencies not in postinsts
549        pkg_names = list(pkgs.keys())
550        for pkg_name in pkg_names:
551            deps = pkgs[pkg_name][:]
552
553            for d in deps:
554                if d not in pkg_names:
555                    pkgs[pkg_name].remove(d)
556
557        return pkgs
558
559    def _get_delayed_postinsts_common(self, status_file):
560        def _dep_resolve(graph, node, resolved, seen):
561            seen.append(node)
562
563            for edge in graph[node]:
564                if edge not in resolved:
565                    if edge in seen:
566                        raise RuntimeError("Packages %s and %s have " \
567                                "a circular dependency in postinsts scripts." \
568                                % (node, edge))
569                    _dep_resolve(graph, edge, resolved, seen)
570
571            resolved.append(node)
572
573        pkg_list = []
574
575        pkgs = None
576        if not self.d.getVar('PACKAGE_INSTALL').strip():
577            bb.note("Building empty image")
578        else:
579            pkgs = self._get_pkgs_postinsts(status_file)
580        if pkgs:
581            root = "__packagegroup_postinst__"
582            pkgs[root] = list(pkgs.keys())
583            _dep_resolve(pkgs, root, pkg_list, [])
584            pkg_list.remove(root)
585
586        if len(pkg_list) == 0:
587            return None
588
589        return pkg_list
590
591    def _save_postinsts_common(self, dst_postinst_dir, src_postinst_dir):
592        if bb.utils.contains("IMAGE_FEATURES", "package-management",
593                         True, False, self.d):
594            return
595        num = 0
596        for p in self._get_delayed_postinsts():
597            bb.utils.mkdirhier(dst_postinst_dir)
598
599            if os.path.exists(os.path.join(src_postinst_dir, p + ".postinst")):
600                shutil.copy(os.path.join(src_postinst_dir, p + ".postinst"),
601                            os.path.join(dst_postinst_dir, "%03d-%s" % (num, p)))
602
603            num += 1
604
605class DpkgRootfs(DpkgOpkgRootfs):
606    def __init__(self, d, manifest_dir, progress_reporter=None, logcatcher=None):
607        super(DpkgRootfs, self).__init__(d, progress_reporter, logcatcher)
608        self.log_check_regex = '^E:'
609        self.log_check_expected_regexes = \
610        [
611            "^E: Unmet dependencies."
612        ]
613
614        bb.utils.remove(self.image_rootfs, True)
615        bb.utils.remove(self.d.getVar('MULTILIB_TEMP_ROOTFS'), True)
616        self.manifest = DpkgManifest(d, manifest_dir)
617        self.pm = DpkgPM(d, d.getVar('IMAGE_ROOTFS'),
618                         d.getVar('PACKAGE_ARCHS'),
619                         d.getVar('DPKG_ARCH'))
620
621
622    def _create(self):
623        pkgs_to_install = self.manifest.parse_initial_manifest()
624        deb_pre_process_cmds = self.d.getVar('DEB_PREPROCESS_COMMANDS')
625        deb_post_process_cmds = self.d.getVar('DEB_POSTPROCESS_COMMANDS')
626
627        alt_dir = self.d.expand("${IMAGE_ROOTFS}/var/lib/dpkg/alternatives")
628        bb.utils.mkdirhier(alt_dir)
629
630        # update PM index files
631        self.pm.write_index()
632
633        execute_pre_post_process(self.d, deb_pre_process_cmds)
634
635        if self.progress_reporter:
636            self.progress_reporter.next_stage()
637            # Don't support incremental, so skip that
638            self.progress_reporter.next_stage()
639
640        self.pm.update()
641
642        if self.progress_reporter:
643            self.progress_reporter.next_stage()
644
645        for pkg_type in self.install_order:
646            if pkg_type in pkgs_to_install:
647                self.pm.install(pkgs_to_install[pkg_type],
648                                [False, True][pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY])
649                self.pm.fix_broken_dependencies()
650
651        if self.progress_reporter:
652            # Don't support attemptonly, so skip that
653            self.progress_reporter.next_stage()
654            self.progress_reporter.next_stage()
655
656        self.pm.install_complementary()
657
658        if self.progress_reporter:
659            self.progress_reporter.next_stage()
660
661        self._setup_dbg_rootfs(['/var/lib/dpkg'])
662
663        self.pm.fix_broken_dependencies()
664
665        self.pm.mark_packages("installed")
666
667        self.pm.run_pre_post_installs()
668
669        execute_pre_post_process(self.d, deb_post_process_cmds)
670
671        if self.progress_reporter:
672            self.progress_reporter.next_stage()
673
674    @staticmethod
675    def _depends_list():
676        return ['DEPLOY_DIR_DEB', 'DEB_SDK_ARCH', 'APTCONF_TARGET', 'APT_ARGS', 'DPKG_ARCH', 'DEB_PREPROCESS_COMMANDS', 'DEB_POSTPROCESS_COMMANDS']
677
678    def _get_delayed_postinsts(self):
679        status_file = self.image_rootfs + "/var/lib/dpkg/status"
680        return self._get_delayed_postinsts_common(status_file)
681
682    def _save_postinsts(self):
683        dst_postinst_dir = self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/deb-postinsts")
684        src_postinst_dir = self.d.expand("${IMAGE_ROOTFS}/var/lib/dpkg/info")
685        return self._save_postinsts_common(dst_postinst_dir, src_postinst_dir)
686
687    def _log_check(self):
688        self._log_check_warn()
689        self._log_check_error()
690
691    def _cleanup(self):
692        pass
693
694
695class OpkgRootfs(DpkgOpkgRootfs):
696    def __init__(self, d, manifest_dir, progress_reporter=None, logcatcher=None):
697        super(OpkgRootfs, self).__init__(d, progress_reporter, logcatcher)
698        self.log_check_regex = '(exit 1|Collected errors)'
699
700        self.manifest = OpkgManifest(d, manifest_dir)
701        self.opkg_conf = self.d.getVar("IPKGCONF_TARGET")
702        self.pkg_archs = self.d.getVar("ALL_MULTILIB_PACKAGE_ARCHS")
703
704        self.inc_opkg_image_gen = self.d.getVar('INC_IPK_IMAGE_GEN') or ""
705        if self._remove_old_rootfs():
706            bb.utils.remove(self.image_rootfs, True)
707            self.pm = OpkgPM(d,
708                             self.image_rootfs,
709                             self.opkg_conf,
710                             self.pkg_archs)
711        else:
712            self.pm = OpkgPM(d,
713                             self.image_rootfs,
714                             self.opkg_conf,
715                             self.pkg_archs)
716            self.pm.recover_packaging_data()
717
718        bb.utils.remove(self.d.getVar('MULTILIB_TEMP_ROOTFS'), True)
719
720    def _prelink_file(self, root_dir, filename):
721        bb.note('prelink %s in %s' % (filename, root_dir))
722        prelink_cfg = oe.path.join(root_dir,
723                                   self.d.expand('${sysconfdir}/prelink.conf'))
724        if not os.path.exists(prelink_cfg):
725            shutil.copy(self.d.expand('${STAGING_DIR_NATIVE}${sysconfdir_native}/prelink.conf'),
726                        prelink_cfg)
727
728        cmd_prelink = self.d.expand('${STAGING_DIR_NATIVE}${sbindir_native}/prelink')
729        self._exec_shell_cmd([cmd_prelink,
730                              '--root',
731                              root_dir,
732                              '-amR',
733                              '-N',
734                              '-c',
735                              self.d.expand('${sysconfdir}/prelink.conf')])
736
737    '''
738    Compare two files with the same key twice to see if they are equal.
739    If they are not equal, it means they are duplicated and come from
740    different packages.
741    1st: Comapre them directly;
742    2nd: While incremental image creation is enabled, one of the
743         files could be probaly prelinked in the previous image
744         creation and the file has been changed, so we need to
745         prelink the other one and compare them.
746    '''
747    def _file_equal(self, key, f1, f2):
748
749        # Both of them are not prelinked
750        if filecmp.cmp(f1, f2):
751            return True
752
753        if bb.data.inherits_class('image-prelink', self.d):
754            if self.image_rootfs not in f1:
755                self._prelink_file(f1.replace(key, ''), f1)
756
757            if self.image_rootfs not in f2:
758                self._prelink_file(f2.replace(key, ''), f2)
759
760            # Both of them are prelinked
761            if filecmp.cmp(f1, f2):
762                return True
763
764        # Not equal
765        return False
766
767    """
768    This function was reused from the old implementation.
769    See commit: "image.bbclass: Added variables for multilib support." by
770    Lianhao Lu.
771    """
772    def _multilib_sanity_test(self, dirs):
773
774        allow_replace = self.d.getVar("MULTILIBRE_ALLOW_REP")
775        if allow_replace is None:
776            allow_replace = ""
777
778        allow_rep = re.compile(re.sub(r"\|$", r"", allow_replace))
779        error_prompt = "Multilib check error:"
780
781        files = {}
782        for dir in dirs:
783            for root, subfolders, subfiles in os.walk(dir):
784                for file in subfiles:
785                    item = os.path.join(root, file)
786                    key = str(os.path.join("/", os.path.relpath(item, dir)))
787
788                    valid = True
789                    if key in files:
790                        #check whether the file is allow to replace
791                        if allow_rep.match(key):
792                            valid = True
793                        else:
794                            if os.path.exists(files[key]) and \
795                               os.path.exists(item) and \
796                               not self._file_equal(key, files[key], item):
797                                valid = False
798                                bb.fatal("%s duplicate files %s %s is not the same\n" %
799                                         (error_prompt, item, files[key]))
800
801                    #pass the check, add to list
802                    if valid:
803                        files[key] = item
804
805    def _multilib_test_install(self, pkgs):
806        ml_temp = self.d.getVar("MULTILIB_TEMP_ROOTFS")
807        bb.utils.mkdirhier(ml_temp)
808
809        dirs = [self.image_rootfs]
810
811        for variant in self.d.getVar("MULTILIB_VARIANTS").split():
812            ml_target_rootfs = os.path.join(ml_temp, variant)
813
814            bb.utils.remove(ml_target_rootfs, True)
815
816            ml_opkg_conf = os.path.join(ml_temp,
817                                        variant + "-" + os.path.basename(self.opkg_conf))
818
819            ml_pm = OpkgPM(self.d, ml_target_rootfs, ml_opkg_conf, self.pkg_archs, prepare_index=False)
820
821            ml_pm.update()
822            ml_pm.install(pkgs)
823
824            dirs.append(ml_target_rootfs)
825
826        self._multilib_sanity_test(dirs)
827
828    '''
829    While ipk incremental image generation is enabled, it will remove the
830    unneeded pkgs by comparing the old full manifest in previous existing
831    image and the new full manifest in the current image.
832    '''
833    def _remove_extra_packages(self, pkgs_initial_install):
834        if self.inc_opkg_image_gen == "1":
835            # Parse full manifest in previous existing image creation session
836            old_full_manifest = self.manifest.parse_full_manifest()
837
838            # Create full manifest for the current image session, the old one
839            # will be replaced by the new one.
840            self.manifest.create_full(self.pm)
841
842            # Parse full manifest in current image creation session
843            new_full_manifest = self.manifest.parse_full_manifest()
844
845            pkg_to_remove = list()
846            for pkg in old_full_manifest:
847                if pkg not in new_full_manifest:
848                    pkg_to_remove.append(pkg)
849
850            if pkg_to_remove != []:
851                bb.note('decremental removed: %s' % ' '.join(pkg_to_remove))
852                self.pm.remove(pkg_to_remove)
853
854    '''
855    Compare with previous existing image creation, if some conditions
856    triggered, the previous old image should be removed.
857    The conditions include any of 'PACKAGE_EXCLUDE, NO_RECOMMENDATIONS
858    and BAD_RECOMMENDATIONS' has been changed.
859    '''
860    def _remove_old_rootfs(self):
861        if self.inc_opkg_image_gen != "1":
862            return True
863
864        vars_list_file = self.d.expand('${T}/vars_list')
865
866        old_vars_list = ""
867        if os.path.exists(vars_list_file):
868            old_vars_list = open(vars_list_file, 'r+').read()
869
870        new_vars_list = '%s:%s:%s\n' % \
871                ((self.d.getVar('BAD_RECOMMENDATIONS') or '').strip(),
872                 (self.d.getVar('NO_RECOMMENDATIONS') or '').strip(),
873                 (self.d.getVar('PACKAGE_EXCLUDE') or '').strip())
874        open(vars_list_file, 'w+').write(new_vars_list)
875
876        if old_vars_list != new_vars_list:
877            return True
878
879        return False
880
881    def _create(self):
882        pkgs_to_install = self.manifest.parse_initial_manifest()
883        opkg_pre_process_cmds = self.d.getVar('OPKG_PREPROCESS_COMMANDS')
884        opkg_post_process_cmds = self.d.getVar('OPKG_POSTPROCESS_COMMANDS')
885
886        # update PM index files
887        self.pm.write_index()
888
889        execute_pre_post_process(self.d, opkg_pre_process_cmds)
890
891        if self.progress_reporter:
892            self.progress_reporter.next_stage()
893            # Steps are a bit different in order, skip next
894            self.progress_reporter.next_stage()
895
896        self.pm.update()
897
898        if self.progress_reporter:
899            self.progress_reporter.next_stage()
900
901        if self.inc_opkg_image_gen == "1":
902            self._remove_extra_packages(pkgs_to_install)
903
904        if self.progress_reporter:
905            self.progress_reporter.next_stage()
906
907        for pkg_type in self.install_order:
908            if pkg_type in pkgs_to_install:
909                # For multilib, we perform a sanity test before final install
910                # If sanity test fails, it will automatically do a bb.fatal()
911                # and the installation will stop
912                if pkg_type == Manifest.PKG_TYPE_MULTILIB:
913                    self._multilib_test_install(pkgs_to_install[pkg_type])
914
915                self.pm.install(pkgs_to_install[pkg_type],
916                                [False, True][pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY])
917
918        if self.progress_reporter:
919            self.progress_reporter.next_stage()
920
921        self.pm.install_complementary()
922
923        if self.progress_reporter:
924            self.progress_reporter.next_stage()
925
926        opkg_lib_dir = self.d.getVar('OPKGLIBDIR')
927        opkg_dir = os.path.join(opkg_lib_dir, 'opkg')
928        self._setup_dbg_rootfs([opkg_dir])
929
930        execute_pre_post_process(self.d, opkg_post_process_cmds)
931
932        if self.inc_opkg_image_gen == "1":
933            self.pm.backup_packaging_data()
934
935        if self.progress_reporter:
936            self.progress_reporter.next_stage()
937
938    @staticmethod
939    def _depends_list():
940        return ['IPKGCONF_SDK', 'IPK_FEED_URIS', 'DEPLOY_DIR_IPK', 'IPKGCONF_TARGET', 'INC_IPK_IMAGE_GEN', 'OPKG_ARGS', 'OPKGLIBDIR', 'OPKG_PREPROCESS_COMMANDS', 'OPKG_POSTPROCESS_COMMANDS', 'OPKGLIBDIR']
941
942    def _get_delayed_postinsts(self):
943        status_file = os.path.join(self.image_rootfs,
944                                   self.d.getVar('OPKGLIBDIR').strip('/'),
945                                   "opkg", "status")
946        return self._get_delayed_postinsts_common(status_file)
947
948    def _save_postinsts(self):
949        dst_postinst_dir = self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/ipk-postinsts")
950        src_postinst_dir = self.d.expand("${IMAGE_ROOTFS}${OPKGLIBDIR}/opkg/info")
951        return self._save_postinsts_common(dst_postinst_dir, src_postinst_dir)
952
953    def _log_check(self):
954        self._log_check_warn()
955        self._log_check_error()
956
957    def _cleanup(self):
958        self.pm.remove_lists()
959
960def get_class_for_type(imgtype):
961    return {"rpm": RpmRootfs,
962            "ipk": OpkgRootfs,
963            "deb": DpkgRootfs}[imgtype]
964
965def variable_depends(d, manifest_dir=None):
966    img_type = d.getVar('IMAGE_PKGTYPE')
967    cls = get_class_for_type(img_type)
968    return cls._depends_list()
969
970def create_rootfs(d, manifest_dir=None, progress_reporter=None, logcatcher=None):
971    env_bkp = os.environ.copy()
972
973    img_type = d.getVar('IMAGE_PKGTYPE')
974    if img_type == "rpm":
975        RpmRootfs(d, manifest_dir, progress_reporter, logcatcher).create()
976    elif img_type == "ipk":
977        OpkgRootfs(d, manifest_dir, progress_reporter, logcatcher).create()
978    elif img_type == "deb":
979        DpkgRootfs(d, manifest_dir, progress_reporter, logcatcher).create()
980
981    os.environ.clear()
982    os.environ.update(env_bkp)
983
984
985def image_list_installed_packages(d, rootfs_dir=None):
986    if not rootfs_dir:
987        rootfs_dir = d.getVar('IMAGE_ROOTFS')
988
989    img_type = d.getVar('IMAGE_PKGTYPE')
990    if img_type == "rpm":
991        return RpmPkgsList(d, rootfs_dir).list_pkgs()
992    elif img_type == "ipk":
993        return OpkgPkgsList(d, rootfs_dir, d.getVar("IPKGCONF_TARGET")).list_pkgs()
994    elif img_type == "deb":
995        return DpkgPkgsList(d, rootfs_dir).list_pkgs()
996
997if __name__ == "__main__":
998    """
999    We should be able to run this as a standalone script, from outside bitbake
1000    environment.
1001    """
1002    """
1003    TBD
1004    """
1005