1#
2# Copyright (c) 2023 BayLibre, SAS
3# Author: Julien Stepahn <jstephan@baylibre.com>
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8import os
9import re
10import bb.tinfoil
11
12import oeqa.utils.ftools as ftools
13from oeqa.utils.commands import runCmd, get_bb_var, get_bb_vars, bitbake
14
15from oeqa.selftest.case import OESelftestTestCase
16
17
18class BBLock(OESelftestTestCase):
19    @classmethod
20    def setUpClass(cls):
21        super(BBLock, cls).setUpClass()
22        cls.lockfile = cls.builddir + "/conf/bblock.conf"
23
24    def unlock_recipes(self, recipes=None, tasks=None):
25        cmd = "bblock -r "
26        if recipes:
27            cmd += " ".join(recipes)
28        if tasks:
29            cmd += " -t " + ",".join(tasks)
30        result = runCmd(cmd)
31
32        if recipes:
33            # ensure all signatures are removed from lockfile
34            contents = ftools.read_file(self.lockfile)
35            for recipe in recipes:
36                for task in tasks:
37                    find_in_contents = re.search(
38                        'SIGGEN_LOCKEDSIGS_.+\s\+=\s"%s:%s:.*"' % (recipe, task),
39                        contents,
40                    )
41                    self.assertFalse(
42                        find_in_contents,
43                        msg="%s:%s should not be present into bblock.conf anymore"
44                        % (recipe, task),
45                    )
46                self.assertExists(self.lockfile)
47        else:
48            self.assertNotExists(self.lockfile)
49
50    def lock_recipes(self, recipes, tasks=None):
51        cmd = "bblock " + " ".join(recipes)
52        if tasks:
53            cmd += " -t " + ",".join(tasks)
54
55        result = runCmd(cmd)
56
57        self.assertExists(self.lockfile)
58
59        # ensure all signatures are added to lockfile
60        contents = ftools.read_file(self.lockfile)
61        for recipe in recipes:
62            if tasks:
63                for task in tasks:
64                    find_in_contents = re.search(
65                        'SIGGEN_LOCKEDSIGS_.+\s\+=\s"%s:%s:.*"' % (recipe, task),
66                        contents,
67                    )
68                    self.assertTrue(
69                        find_in_contents,
70                        msg="%s:%s was not added into bblock.conf. bblock output: %s"
71                        % (recipe, task, result.output),
72                    )
73
74    def modify_tasks(self, recipes, tasks):
75        task_append = ""
76        for recipe in recipes:
77            bb_vars = get_bb_vars(["PV"], recipe)
78            recipe_pv = bb_vars["PV"]
79            recipe_append_file = recipe + "_" + recipe_pv + ".bbappend"
80
81            os.mkdir(os.path.join(self.testlayer_path, "recipes-test", recipe))
82            recipe_append_path = os.path.join(
83                self.testlayer_path, "recipes-test", recipe, recipe_append_file
84            )
85
86            for task in tasks:
87                task_append += "%s:append() {\n#modify task hash \n}\n" % task
88            ftools.write_file(recipe_append_path, task_append)
89            self.add_command_to_tearDown(
90                "rm -rf %s" % os.path.join(self.testlayer_path, "recipes-test", recipe)
91            )
92
93    def test_lock_single_recipe_single_task(self):
94        recipes = ["quilt"]
95        tasks = ["do_compile"]
96        self._run_test(recipes, tasks)
97
98    def test_lock_single_recipe_multiple_tasks(self):
99        recipes = ["quilt"]
100        tasks = ["do_compile", "do_install"]
101        self._run_test(recipes, tasks)
102
103    def test_lock_single_recipe_all_tasks(self):
104        recipes = ["quilt"]
105        self._run_test(recipes, None)
106
107    def test_lock_multiple_recipe_single_task(self):
108        recipes = ["quilt", "bc"]
109        tasks = ["do_compile"]
110        self._run_test(recipes, tasks)
111
112    def test_lock_architecture_specific(self):
113        # unlock all recipes and ensure no bblock.conf file exist
114        self.unlock_recipes()
115
116        recipes = ["quilt"]
117        tasks = ["do_compile"]
118
119        # lock quilt's do_compile task for another machine
120        if self.td["MACHINE"] == "qemux86-64":
121            machine = "qemuarm"
122        else:
123            machine = "qemux86-64"
124
125        self.write_config('MACHINE = "%s"\n' % machine)
126
127        self.lock_recipes(recipes, tasks)
128
129        self.write_config('MACHINE = "%s"\n' % self.td["MACHINE"])
130        # modify quilt's do_compile task
131        self.modify_tasks(recipes, tasks)
132
133        # build quilt using the default machine
134        # No Note/Warning should be emitted since sig is locked for another machine
135        # (quilt package is architecture dependant)
136        info_message = "NOTE: The following recipes have locked tasks: " + recipes[0]
137        warn_message = "The %s:%s sig is computed to be" % (recipes[0], tasks[0])
138        result = bitbake(recipes[0] + " -n")
139        self.assertNotIn(info_message, result.output)
140        self.assertNotIn(warn_message, result.output)
141
142        # unlock all recipes
143        self.unlock_recipes()
144
145    def _run_test(self, recipes, tasks=None):
146        # unlock all recipes and ensure no bblock.conf file exist
147        self.unlock_recipes()
148
149        self.write_config('BB_SIGNATURE_HANDLER = "OEBasicHash"')
150
151        # lock tasks for recipes
152        result = self.lock_recipes(recipes, tasks)
153
154        if not tasks:
155            tasks = []
156            result = bitbake("-c listtasks " + recipes[0])
157            with bb.tinfoil.Tinfoil() as tinfoil:
158                tinfoil.prepare(config_only=False, quiet=2)
159                d = tinfoil.parse_recipe(recipes[0])
160
161                for line in result.output.splitlines():
162                    if line.startswith("do_"):
163                        task = line.split()[0]
164                        if "setscene" in task:
165                            continue
166                        if d.getVarFlag(task, "nostamp"):
167                            continue
168                        tasks.append(task)
169
170        # build recipes. At this stage we should have a Note about recipes
171        # having locked task's sig, but no warning since sig still match
172        info_message = "NOTE: The following recipes have locked tasks: " + " ".join(
173            recipes
174        )
175        for recipe in recipes:
176            result = bitbake(recipe + " -n")
177            self.assertIn(info_message, result.output)
178            for task in tasks:
179                warn_message = "The %s:%s sig is computed to be" % (recipe, task)
180                self.assertNotIn(warn_message, result.output)
181
182        # modify all tasks that are locked to trigger a sig change then build the recipes
183        # at this stage we should have a Note as before, but also a Warning for all
184        # locked tasks indicating the sig mismatch
185        self.modify_tasks(recipes, tasks)
186        for recipe in recipes:
187            result = bitbake(recipe + " -n")
188            self.assertIn(info_message, result.output)
189            for task in tasks:
190                warn_message = "The %s:%s sig is computed to be" % (recipe, task)
191                self.assertIn(warn_message, result.output)
192
193        # unlock all tasks and rebuild, no more Note/Warning should remain
194        self.unlock_recipes(recipes, tasks)
195        for recipe in recipes:
196            result = bitbake(recipe + " -n")
197            self.assertNotIn(info_message, result.output)
198            for task in tasks:
199                warn_message = "The %s:%s sig is computed to be" % (recipe, task)
200                self.assertNotIn(warn_message, result.output)
201
202        # unlock all recipes
203        self.unlock_recipes()
204