Hi, at BMW Car IT we are working on an experimental feature to improve sstate
cache hits and we are looking for comments on the approach who might have some
insights to the problem and seeing if anyone is interested in the feature for
mainline.

The sstate-cache of a recipe is tied closely to its build dependencies, as the
signature generated for a task includes all parent task's signatures as part of
the signature information. This means that when changes occur in the parent
recipes, even if insignificant, all children recipes that have valid sstate
cache must invalidate their sstate cache objects.

What this patchset does is propose to add another optional variable to recipes,
CDEPENDS, which acts like DEPENDS for all RunQueue purposes but for signature
generation it excludes any parent tasks that come from dependencies listed in
it. This is to break the signature change domino effect.

This patchset also proposes modifying RunQueue to then be able to run a
compatibility checker during task execution phase for recipes and tasks that use
CDEPENDS and allow for deciding to re-execute a task despite being covered by
sstate-cache.

The patchset is based on the jethro branch for the poky repository, as this is
the branch that we are using.  If the general idea sounds good, we can consider
porting it to master.

Included is an patch that adds an example recipe and compatibility checker,
where compatibility is based on the file checksums of the parent recipes
packages. An example recipe, cdepends-test1, generates a compatibility report
containing the file checksums of all files that it packages and which file paths
they are at. Another recipe, cdepends-test2, can then strip this compatibility
report to the minimal files it needs to be consistent and can compare the latest
checksums it used to configure/compile/install with and if they have changed,
trigger a rebuild. If not, the previous version restored from sstate-cache is
used.

We are still experimenting with the usages of this, including the use of having
abi-compliance-checker to compare the ABI of shared libraries as a compatibility
checker during RunQueue and using the results to avoid rebuilding child recipes
when the .so files they depend on are still compatible. Example use cases of
this are allowing recipes which provide multiple shared libraries to change a
single .so file without rebuilding all its children that depend on the other
shared libraries but not the one that changed.

We're aware of the SIGGEN_EXCLUDERECIPES_ABISAFE feature but feel it didn't meet
the feature requirements of what this compatibility checker callback is doing,
although maybe when porting to master we could refactor to make better use of
the work already done there. The current implementation is a bit hacky but
comments would be appreciated in regards to if the concept is feasible and if
people are interested in making use of it and their use cases.

Kind regards,
Michael Ho

--
BMW Car IT GmbH
Michael Ho
Spezialist Entwicklung - Linux Software Integration
Lise-Meitner-Str. 14
89081 Ulm

Tel.: +49 731 3780 4071
Mobil: +49 152 5498 0471
Fax: +49-731-37804-001
Mail: michael...@bmw-carit.de
Web: http://www.bmw-carit.de
--------------------------------------------------------
BMW Car IT GmbH
Gechäftsführer: Kai-Uwe Balszuweit und Alexis Trolin
Sitz und Registergericht: München HRB 134810
--------------------------------------------------------
From 0afcfc5bde251e96a434c345b4e0e4db895feeae Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 06:52:12 +0200
Subject: [PATCH 1/9] cache.py: add support for CDEPENDS
To: yocto@yoctoproject.org

Modifies the bitbake recipe cache handling to now parse also the CDEPENDS
variable, the cdepends flag that can be set for tasks, and also the machine
architecture for the recipe target (since compatibility will be tied to the
machine type also).
---
 bitbake/lib/bb/cache.py | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/bitbake/lib/bb/cache.py b/bitbake/lib/bb/cache.py
index ab09b08..3596242 100644
--- a/bitbake/lib/bb/cache.py
+++ b/bitbake/lib/bb/cache.py
@@ -146,6 +146,9 @@ class CoreRecipeInfo(RecipeInfoCommon):
         self.fakerootenv      = self.getvar('FAKEROOTENV', metadata)
         self.fakerootdirs     = self.getvar('FAKEROOTDIRS', metadata)
         self.fakerootnoenv    = self.getvar('FAKEROOTNOENV', metadata)
+        self.cdepends         = self.depvar('CDEPENDS', metadata)
+        self.task_cdepends    = self.flaglist('cdepends', self.tasks, metadata, True)
+        self.target_sys       = self.getvar('MULTIMACH_TARGET_SYS', metadata)
 
     @classmethod
     def init_cacheData(cls, cachedata):
@@ -172,6 +175,12 @@ class CoreRecipeInfo(RecipeInfoCommon):
         cachedata.rproviders = defaultdict(list)
         cachedata.packages_dynamic = defaultdict(list)
 
+        cachedata.all_cdepends = []
+        cachedata.compatible_deps = defaultdict(list)
+        cachedata.all_compatible_deps = defaultdict(list)
+        cachedata.compatible_task_deps = {}
+        cachedata.target_sys = {}
+
         cachedata.rundeps = defaultdict(lambda: defaultdict(list))
         cachedata.runrecs = defaultdict(lambda: defaultdict(list))
         cachedata.possible_world = []
@@ -197,6 +206,9 @@ class CoreRecipeInfo(RecipeInfoCommon):
         cachedata.stamp_extrainfo[fn] = self.stamp_extrainfo
         cachedata.file_checksums[fn] = self.file_checksums
 
+        cachedata.compatible_task_deps[fn] = self.task_cdepends
+        cachedata.target_sys[fn] = self.target_sys
+
         provides = [self.pn]
         for provide in self.provides:
             if provide not in provides:
@@ -214,6 +226,23 @@ class CoreRecipeInfo(RecipeInfoCommon):
             if dep not in cachedata.all_depends:
                 cachedata.all_depends.append(dep)
 
+        for cdep in self.cdepends:
+            if cdep not in cachedata.deps[fn]:
+                cachedata.deps[fn].append(cdep)
+                if cdep not in cachedata.compatible_deps[fn]:
+                    cachedata.compatible_deps[fn].append(cdep)
+                    bb.debug(1, "cdepends: Adding %s to compatible_deps for %s", cdep, fn)
+            if cdep not in cachedata.all_compatible_deps[fn]:
+                cachedata.all_compatible_deps[fn].append(cdep)
+                bb.debug(1, "cdepends: Adding %s to all_compatible_deps for %s", cdep, fn)
+            if cdep not in cachedata.all_depends:
+                cachedata.all_depends.append(cdep)
+                cachedata.all_cdepends.append(cdep)
+
+        for cdep in cachedata.all_compatible_deps[fn]:
+            if cdep not in cachedata.compatible_deps[fn]:
+                bb.warn("cdepends: %s removed from CDEPENDS for %s because it is also in DEPENDS" % (cdep, fn))
+
         rprovides = self.rprovides
         for package in self.packages:
             cachedata.packages[package].append(fn)
-- 
2.7.4

From e2c8ba70a2b723e1fc67215b1ff0ccb105341633 Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 06:52:52 +0200
Subject: [PATCH 2/9] siggen.py: add support for CDEPENDS
To: yocto@yoctoproject.org

This change modifies the basic signature generator to not add the task
dependencies sstate hash of a dependers task when that task comes from
a provider that is listed in the CDEPENDS variable.
---
 bitbake/lib/bb/siggen.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/bitbake/lib/bb/siggen.py b/bitbake/lib/bb/siggen.py
index 0352e45..e4b8374 100644
--- a/bitbake/lib/bb/siggen.py
+++ b/bitbake/lib/bb/siggen.py
@@ -86,6 +86,7 @@ class SignatureGeneratorBasic(SignatureGenerator):
         self.pkgnameextract = re.compile("(?P<fn>.*)\..*")
         self.basewhitelist = set((data.getVar("BB_HASHBASE_WHITELIST", True) or "").split())
         self.taskwhitelist = None
+        self.cdepends_disabled = data.getVar("BB_CDEPENDS_DISABLED", True)
         self.init_rundepcheck(data)
 
     def init_rundepcheck(self, data):
@@ -180,10 +181,20 @@ class SignatureGeneratorBasic(SignatureGenerator):
         self.runtaskdeps[k] = []
         self.file_checksum_values[k] = {}
         recipename = dataCache.pkg_fn[fn]
+
+        def is_cdepends_task(fn, dep, dataCache):
+            for cdep in dataCache.compatible_deps[fn]:
+                for cdep_provider_fn in dataCache.providers[cdep]:
+                    if dep.startswith(cdep_provider_fn):
+                        return True
+            return False
+
         for dep in sorted(deps, key=clean_basepath):
             depname = dataCache.pkg_fn[self.pkgnameextract.search(dep).group('fn')]
             if not self.rundep_check(fn, recipename, task, dep, depname, dataCache):
                 continue
+            if not self.cdepends_disabled and is_cdepends_task(fn, dep, dataCache):
+                continue
             if dep not in self.taskhash:
                 bb.fatal("%s is not in taskhash, caller isn't calling in dependency order?", dep)
             data = data + self.taskhash[dep]
-- 
2.7.4

From 229bbc3e3feb0b60a70cb98bc1648ffe09b106ce Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 06:53:31 +0200
Subject: [PATCH 3/9] runqueue.py: add support for CDEPENDS
To: yocto@yoctoproject.org

This change modifies the runqueue execution to inject a check where
tasks with a cdepends flag run an extra section of code that invokes a
custom sstate hashing check and compatibility checker (defined by the
recipe).

The result of this extra section of code is then used to decide
whether to skip the sstate/stamp coverage checks during the runqueue
execution.
---
 bitbake/lib/bb/runqueue.py | 197 ++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 186 insertions(+), 11 deletions(-)

diff --git a/bitbake/lib/bb/runqueue.py b/bitbake/lib/bb/runqueue.py
index 878028a..3962ee1 100644
--- a/bitbake/lib/bb/runqueue.py
+++ b/bitbake/lib/bb/runqueue.py
@@ -35,6 +35,7 @@ import bb
 from bb import msg, data, event
 from bb import monitordisk
 import subprocess
+import imp
 
 try:
     import cPickle as pickle
@@ -237,6 +238,7 @@ class RunQueueData:
         self.runq_depends = []
         self.runq_revdeps = []
         self.runq_hash = []
+        self.runq_arch = []
 
     def runq_depends_names(self, ids):
         import re
@@ -555,6 +557,7 @@ class RunQueueData:
             self.runq_depends.append(depends)
             self.runq_revdeps.append(set())
             self.runq_hash.append("")
+            self.runq_arch.append("")
 
             runq_build.append(0)
 
@@ -668,6 +671,7 @@ class RunQueueData:
                 del runq_build[listid-delcount]
                 del self.runq_revdeps[listid-delcount]
                 del self.runq_hash[listid-delcount]
+                del self.runq_arch[listid-delcount]
                 delcount = delcount + 1
                 maps.append(-1)
 
@@ -806,6 +810,16 @@ class RunQueueData:
         if hasattr(bb.parse.siggen, "tasks_resolved"):
             bb.parse.siggen.tasks_resolved(virtmap, virtpnmap, self.dataCache)
 
+        # Iterate over the fn list and cache the variable MULTIMACH_TARGET_SYS
+        cached_archs = []
+        for i in range(max(self.runq_fnid)+1):
+            cached_archs.append("")
+        dealtwith = set()
+        for i in self.runq_fnid:
+            if i not in dealtwith:
+                dealtwith.add(i)
+                cached_archs[i] = self.dataCache.target_sys[self.taskData.fn_index[i]]
+
         # Iterate over the task list and call into the siggen code
         dealtwith = set()
         todeal = set(range(len(self.runq_fnid)))
@@ -818,6 +832,9 @@ class RunQueueData:
                     for dep in self.runq_depends[task]:
                         procdep.append(self.taskData.fn_index[self.runq_fnid[dep]] + "." + self.runq_task[dep])
                     self.runq_hash[task] = bb.parse.siggen.get_taskhash(self.taskData.fn_index[self.runq_fnid[task]], self.runq_task[task], procdep, self.dataCache)
+                    self.runq_arch[task] = cached_archs[self.runq_fnid[task]]
+                    if self.runq_task[task] == "do_compatibility_report":
+                        bb.debug(1, "cdepends: hash for %s:%s is %s" % (self.taskData.fn_index[self.runq_fnid[task]], self.runq_task[task], self.runq_hash[task]))
 
         return len(self.runq_fnid)
 
@@ -1281,6 +1298,8 @@ class RunQueueExecute:
         self.runq_running = []
         self.runq_complete = []
 
+        self.tainted = []
+
         self.build_stamps = {}
         self.build_stamps2 = []
         self.failed_fnids = []
@@ -1503,6 +1522,18 @@ class RunQueueExecuteTasks(RunQueueExecute):
         """
         self.runq_complete[task] = 1
         for revdep in self.rqdata.runq_revdeps[task]:
+            # Taint reverse dependency if task is tainted from a cdepends check
+            # in its build tree and if it is not chained by CDEPENDS (we don't
+            # want cdepends tainting to cross the compatibility checking
+            # boundaries)
+            if task in self.tainted:
+                taskfn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[task]]
+                taskpn = self.rqdata.dataCache.pkg_fn[taskfn]
+                revdepfn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[revdep]]
+                revdeppn = self.rqdata.dataCache.pkg_fn[revdepfn]
+                # Try to be smart so that tainting stops between CDEPENDS items
+                if revdepfn == taskfn or not taskpn in self.rqdata.dataCache.compatible_deps[revdep]:
+                    self.tainted.append(revdep)
             if self.runq_running[revdep] == 1:
                 continue
             if self.runq_buildable[revdep] == 1:
@@ -1559,17 +1590,159 @@ class RunQueueExecuteTasks(RunQueueExecute):
             fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[task]]
             taskname = self.rqdata.runq_task[task]
 
-            if task in self.rq.scenequeue_covered:
-                logger.debug(2, "Setscene covered task %s (%s)", task,
-                                self.rqdata.get_user_idstring(task))
-                self.task_skip(task, "covered")
-                return True
+            # Run compatibility check if task recipe has cdepends flag set and if stamp or setscene covered. Skip if
+            # CDEPENDS is disabled by config however.
+            # TODO: Skip if the cache compatibility task is not covered (maybe we crashed halfway through compile of the
+            # recipe, thus we want to continue with what we have in our workdir and not try to compatibility check -
+            # although we need to check the integrity of the workdir by comparing the sstate hash it starts with and
+            # ends with before we can add this check).
+            compatibility_check_ran = False
+            if (taskname in self.rqdata.dataCache.compatible_task_deps[fn]):
+                if (not self.cfgData.getVar("BB_CDEPENDS_DISABLED", True)) and (task in self.rq.scenequeue_covered or self.rq.check_stamp_task(task, taskname, cache=self.stampcache)):
+                    # Parse recipe to extract cdepends variables and run compatibility check for each cdepends
+                    tmp_cachedata = bb.cache.Cache.loadDataFull(fn, self.cooker.collection.get_file_appends(fn), self.cooker.data)
+
+                    # Loop over all tasks that have the cdepends flag set
+                    for cdepend in self.rqdata.dataCache.compatible_task_deps[fn][taskname].split():
+                        compatibility_result = "incompatible"
+
+                        cdepend_tool = tmp_cachedata.getVar(("CDEPENDS_TOOL_%s" % (cdepend,)), True) or ""
+                        cdepend_searchdir = tmp_cachedata.getVar("CDEPENDS_SEARCHDIR", True) or ""
+                        cdepend_workdir = tmp_cachedata.getVar("CDEPENDS_WORKDIR", True) or ""
+
+                        logger.debug(1, "cdepends: cdepend=%s,cdepend_tool=%s,cdepend_searchdir=%s,cdepend_workdir=%s",
+                            cdepend, cdepend_tool, cdepend_searchdir, cdepend_workdir)
+
+                        # Determine the provider of the cdepends and get the architecture/hashes
+
+                        # XXX: Potential room to improve by fixing this to properly look through indices. However it
+                        # may not be possible to do so. We start with PN's and we need to find the actual provider
+                        # and the task do_cache_compatibility_reports to calculate the hashes to build against
+                        # (using all providers hashes if multiple providers are available)
+                        cdepend_providers = self.rqdata.dataCache.providers[cdepend]
+                        cdepend_preferred_provider = tmp_cachedata.getVar("PREFERRED_PROVIDER_%s" % (cdepend,)) or cdepend
+                        cdepend_hashes = ""
+                        cdepend_hashes_provider = []
+                        cdepend_arch = ""
+                        cdepend_arch_provider = "self"
+
+                        if cdepend_preferred_provider in cdepend_providers:
+                            for x in range(len(self.rqdata.runq_task)):
+                                x_name = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[x]] + ":" + self.rqdata.runq_task[x]
+                                if x_name == cdepend_preferred_provider + ":" + "do_compatibility_report":
+                                    cdepend_hashes = self.rqdata.runq_hash[x]
+                                    cdepend_hashes_provider.append(cdepend_preferred_provider)
+                                    cdepend_arch = self.rqdata.dataCache.target_sys[self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[x]]]
+                                    cdepend_arch_provider = cdepend_preferred_provider
+                                    break
+                        else:
+                            # if multiple providers available and no preferred provider, then merge all (sorted) hashes for safety
+                            found_hashes = dict()
+                            for cdepend_provider in cdepend_providers:
+                                for x in range(len(self.rqdata.runq_task)):
+                                    x_name = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[x]] + ":" + self.rqdata.runq_task[x]
+                                    if x_name == cdepend_provider + ":" + "do_compatibility_report":
+                                        found_hashes[x_name] = self.rqdata.runq_hash[x]
+                                        cdepend_hashes_provider.append(cdepend_provider)
+                                        cdepend_arch = self.rqdata.dataCache.target_sys[self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[x]]]
+                                        cdepend_arch_provider = cdepend_provider
+                            sorted_hashes = sorted(found_hashes.values())
+                            for cdepend_provider_hash in sorted_hashes:
+                                cdepend_hashes += cdepend_provider_hash
+
+                        # if cdepends insane skip is set, ignore the sstate hashing checks
+                        # (since we won't be able to fetch the required sstate hashes for
+                        # dependencies without do_compatibility_report)
+                        if not tmp_cachedata.getVar("CDEPENDS_INSANE_SKIP_%s" % (cdepend,), True):
+                            if not cdepend_hashes:
+                                bb.fatal("cdepends: Cannot find provider task for %s:do_compatibility_report (providers: %s)" % (cdepend, cdepend_providers))
+
+                            logger.debug(1, "cdepends: Provider for %s: %s,runq_hash=%s,runq_arch=%s", cdepend, ','.join(cdepend_hashes_provider), cdepend_hashes, cdepend_arch)
+
+                            # First just check if the sstate hashes are okay to try to avoid the actual compatibility check
+                            cached_sstate_hashes_path = os.path.join(cdepend_workdir, "sstate-hashes")
+                            logger.debug(2, "cdepends: Trying to grab sstate hashes from %s.", cached_sstate_hashes_path)
+
+                            current_runq_hash = cdepend_hashes
+                            cached_runq_hash = ""
+                            try:
+                                with open(cached_sstate_hashes_path, "r") as hashes_log:
+                                    hashes_log_lines = hashes_log.readlines()
+                                    for line in hashes_log_lines:
+                                        line = line.strip()
+                                        try:
+                                            target_name, cached_target_hash = line.split(" ", 1)
+                                            if target_name == cdepend:
+                                                cached_runq_hash = cached_target_hash
+                                        except ValueError:
+                                            continue
+                            except (OSError, IOError) as e:
+                                bb.warn("cdepends: Could not open cached sstate hashes for %s:%s: %s" % (fn, taskname, str(e)))
+
+                            if not cached_runq_hash == "":
+                                logger.debug(2, "cdepends: Comparing sstate hashes: cached: \"%s\" to latest: \"%s\".", cached_runq_hash, current_runq_hash)
+                                if current_runq_hash == cached_runq_hash:
+                                    logger.debug(1, "cdepends: \"%s\" sstate hash \"%s\" still valid for %s:%s.", cdepend, cached_runq_hash, fn, taskname)
+                                    continue
+
+                                # if the sstate hash does not hold, then run the cdepend check
+                                logger.debug(1, "cdepends: \"%s\" sstate hash has changed from \"%s\" to \"%s\" for %s:%s.", cdepend, cached_runq_hash, current_runq_hash, fn, taskname)
+                            else:
+                                logger.debug(1, "cdepends: \"%s\" sstate hash was not found in compatibility cache for %s:%s", cdepend, fn, taskname)
+
+                        # Run actual compatibility checkers from here onwards
+                        latest_reports_dir = os.path.join(cdepend_searchdir, cdepend_arch, cdepend)
+                        cached_reports_dir = os.path.join(cdepend_workdir, cdepend)
+
+                        cdepend_module_name = ("cdepends_%s" % (cdepend_tool,))
+                        f = None
+                        try:
+                            logger.debug(2, "cdepends: Searching for python module \"%s\".", cdepend_module_name)
+                            f, filename, description = imp.find_module(cdepend_module_name)
+                            cdepend_module = imp.load_module(cdepend_module_name, f, filename, description)
+                            logger.debug(2, "cdepends: Running compatibility check for %s:%s using python module \"%s\". Passing arguments \"%s\" and \"%s\"",
+                                fn, taskname, cdepend_module_name, cached_reports_dir, latest_reports_dir)
+                            compatibility_result = cdepend_module.compatibility_check(cached_reports_dir, latest_reports_dir, tmp_cachedata)
+                            bb.note("cdepends: Compatibility check for %s:%s using tool (%s) returned \"%s\" for (%s)." % (fn, taskname, cdepend_tool, compatibility_result, cdepend))
+                        except ImportError as e:
+                            bb.warn("cdepends: Could not find and load/run cdepends module %s" % (cdepend_module_name,))
+                        finally:
+                            if f is not None:
+                                f.close()
+
+                        # Set a flag to show we actually ran a compatibility check to help improve our notifications to the UI
+                        compatibility_check_ran = True
+
+                        if compatibility_result != "compatible":
+                            logger.debug(1, "cdepends: Tainting build chain of %s:%s because of %s.", fn, taskname, cdepend)
+                            self.tainted.append(task)
+                            logger.debug(1, "cdepends: Tainted task %s due to compatibility changes", task)
+
+                            if compatibility_result != "incompatible":
+                                bb.fatal("cdepends: Fatal compatibility error for %s." % (task,))
+                else:
+                    bb.note("cdepends: Skipping cdepends checks for %s (%s)" % (task, self.rqdata.get_user_idstring(task)))
 
-            if self.rq.check_stamp_task(task, taskname, cache=self.stampcache):
-                logger.debug(2, "Stamp current task %s (%s)", task,
-                                self.rqdata.get_user_idstring(task))
-                self.task_skip(task, "existing")
-                return True
+            # Don't believe setscene or stamp if tainted flag set for task
+            if task not in self.tainted:
+                if task in self.rq.scenequeue_covered:
+                    logger.debug(2, "Setscene covered task %s (%s)", task,
+                                    self.rqdata.get_user_idstring(task))
+                    self.task_skip(task, "covered")
+                    if compatibility_check_ran:
+                        bb.note("cdepends: Avoided rebuilding %s" % (fn,))
+                    return True
+
+                if self.rq.check_stamp_task(task, taskname, cache=self.stampcache):
+                    logger.debug(2, "Stamp current task %s (%s)", task,
+                                    self.rqdata.get_user_idstring(task))
+                    self.task_skip(task, "existing")
+                    if compatibility_check_ran:
+                        bb.note("cdepends: Avoided rebuilding %s" % (fn,))
+                    return True
+
+            if compatibility_check_ran and task in self.tainted:
+                bb.note("cdepends: Rebuilding %s" % (fn,))
 
             taskdep = self.rqdata.dataCache.task_deps[fn]
             if 'noexec' in taskdep and taskname in taskdep['noexec']:
@@ -1642,7 +1815,9 @@ class RunQueueExecuteTasks(RunQueueExecute):
                 taskname = self.rqdata.runq_task[revdep]
                 deps = self.rqdata.runq_depends[revdep]
                 provides = self.rqdata.dataCache.fn_provides[fn]
-                taskdepdata[revdep] = [pn, taskname, fn, deps, provides]
+                taskhash = self.rqdata.runq_hash[revdep]
+                taskarch = self.rqdata.runq_arch[revdep]
+                taskdepdata[revdep] = [pn, taskname, fn, deps, provides, taskhash, taskarch]
                 for revdep2 in deps:
                     if revdep2 not in taskdepdata:
                         additional.append(revdep2)
-- 
2.7.4

From fe6f61e7d29c64d8ffa4943283784339ff2d7411 Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 06:54:27 +0200
Subject: [PATCH 4/9] taskdata.py: add support for CDEPENDS
To: yocto@yoctoproject.org

This change adds a new variable to the TaskData called cdepids and
contains the task ids of tasks that are CDEPENDS of the task being
called on.
---
 bitbake/lib/bb/taskdata.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/bitbake/lib/bb/taskdata.py b/bitbake/lib/bb/taskdata.py
index 4d12b33..5b9ea8c 100644
--- a/bitbake/lib/bb/taskdata.py
+++ b/bitbake/lib/bb/taskdata.py
@@ -74,6 +74,8 @@ class TaskData:
 
         self.skiplist = skiplist
 
+        self.cdepids = {}
+
     def getbuild_id(self, name):
         """
         Return an ID number for the build target name.
@@ -212,6 +214,14 @@ class TaskData:
             self.depids[fnid] = dependids.keys()
             logger.debug(2, "Added dependencies %s for %s", str(dataCache.deps[fn]), fn)
 
+        # Work out build compatible dependencies
+        if not fnid in self.cdepids:
+            cdependids = {}
+            for cdepend in dataCache.compatible_deps[fn]:
+                cdependids[self.getbuild_id(cdepend)] = None
+            self.cdepids[fnid] = cdependids.keys()
+            logger.debug(2, "Added compatible dependencies %s for %s", str(dataCache.compatible_deps[fn]), fn)
+
         # Work out runtime dependencies
         if not fnid in self.rdepids:
             rdependids = {}
-- 
2.7.4

From d0149bcd7fba3d432eebd6218d6199878bdd91f8 Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 06:54:39 +0200
Subject: [PATCH 5/9] rm_work.bbclass: add exception for
 do_cache_compatibility_reports
To: yocto@yoctoproject.org

This change is needed to handle the situation of rm_work clearing
compatibility reports that are actually shared to depender recipes.
This is similar to the workarounds in place for do_package.
---
 meta/classes/rm_work.bbclass | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/meta/classes/rm_work.bbclass b/meta/classes/rm_work.bbclass
index 5e9efc1..ce802cd 100644
--- a/meta/classes/rm_work.bbclass
+++ b/meta/classes/rm_work.bbclass
@@ -76,6 +76,11 @@ do_rm_work () {
                 i=dummy
                 break
                 ;;
+            *do_cache_compatibility_reports|*do_cache_compatibility_reports.*|*do_cache_compatibility_reports_setscene.*)
+                rm -f $i;
+                i=dummy
+                break
+                ;;
             *_setscene*)
                 i=dummy
                 break
-- 
2.7.4

From 77070002f7970ccab89b38eb5eb91bc768120052 Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 06:55:24 +0200
Subject: [PATCH 6/9] native.bbclass: add support for CDEPENDS
To: yocto@yoctoproject.org

Remap CDEPENDS in the same way DEPENDS is remapped when extending a
recipe.
---
 meta/classes/native.bbclass | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/meta/classes/native.bbclass b/meta/classes/native.bbclass
index bcbcd61..1357f1f 100644
--- a/meta/classes/native.bbclass
+++ b/meta/classes/native.bbclass
@@ -163,7 +163,18 @@ python native_virtclass_handler () {
             nprovides.append(prov)
     e.data.setVar("PROVIDES", ' '.join(nprovides))
 
-
+    map_dependencies("CDEPENDS", e.data)
+    for cdep in bb.utils.explode_deps(e.data.getVar("CDEPENDS") or ""):
+        cdep_tool = d.getVar("CDEPENDS_TOOL_%s" % (cdep,), True) or ""
+        if cdep == pn:
+            continue
+        elif "-cross-" in cdep:
+            cdep.replace("-cross", "-native")
+        elif not cdep.endswith("-native"):
+            cdep += "-native"
+        else:
+            pass
+        e.data.setVar("CDEPENDS_TOOL_%s" % (cdep,), cdep_tool)
 }
 
 addhandler native_virtclass_handler
-- 
2.7.4

From 2ce44b4ac4ebae7e90e07e95911e8c5be0ff15c6 Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 06:55:45 +0200
Subject: [PATCH 7/9] nativesdk.bbclass: add support for CDEPENDS
To: yocto@yoctoproject.org

Remap CDEPENDS in the same way DEPENDS is remapped when extending a
recipe. Remaps the CDEPENDS_TOOL_* variable also.
---
 meta/classes/nativesdk.bbclass | 3 +++
 meta/lib/oe/classextend.py     | 5 +++++
 2 files changed, 8 insertions(+)

diff --git a/meta/classes/nativesdk.bbclass b/meta/classes/nativesdk.bbclass
index f74da62..6a98732 100644
--- a/meta/classes/nativesdk.bbclass
+++ b/meta/classes/nativesdk.bbclass
@@ -86,6 +86,9 @@ python () {
     clsextend.map_packagevars()
     clsextend.map_variable("PROVIDES")
     clsextend.map_regexp_variable("PACKAGES_DYNAMIC")
+
+    clsextend.rename_cdepends_tools()
+    clsextend.map_depends_variable("CDEPENDS")
 }
 
 addhandler nativesdk_virtclass_handler
diff --git a/meta/lib/oe/classextend.py b/meta/lib/oe/classextend.py
index 5107ecd..a19b213 100644
--- a/meta/lib/oe/classextend.py
+++ b/meta/lib/oe/classextend.py
@@ -108,6 +108,11 @@ class ClassExtender(object):
             for subs in variables:
                 self.d.renameVar("%s_%s" % (subs, pkg_mapping[0]), "%s_%s" % (subs, pkg_mapping[1]))
 
+    def rename_cdepends_tools(self):
+        for cdep in (self.d.getVar("CDEPENDS", True) or "").split():
+            renamed_cdep = self.map_depends(cdep)
+            self.d.renameVar("CDEPENDS_TOOL_%s" % (cdep,), "CDEPENDS_TOOL_%s" % (renamed_cdep,))
+
 class NativesdkClassExtender(ClassExtender):
     def map_depends(self, dep):
         if dep.startswith(self.extname):
-- 
2.7.4

From 7d9d7e67099ef8ed4c6aa32453bf5cbd8ae53957 Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 06:56:36 +0200
Subject: [PATCH 8/9] classes: add bbclasses compatible-depends and
 compatible-depends-report
To: yocto@yoctoproject.org

Adds bbclasses that are used in conjunction with triggering the
CDEPENDS mechanisms in Bitbake.

The compatible-depends bbclass adds the base functionality to cache
compatibility reports from the dependencies.

The compatible-depends-report bbclass adds the base functionality to
generate the compatibility reports for recipes that can be depended
on via CDEPENDS.
---
 .../classes/compatible-depends-report.bbclass      |  32 +++++
 meta-yocto/classes/compatible-depends.bbclass      | 144 +++++++++++++++++++++
 2 files changed, 176 insertions(+)
 create mode 100644 meta-yocto/classes/compatible-depends-report.bbclass
 create mode 100644 meta-yocto/classes/compatible-depends.bbclass

diff --git a/meta-yocto/classes/compatible-depends-report.bbclass b/meta-yocto/classes/compatible-depends-report.bbclass
new file mode 100644
index 0000000..ffb9e4e
--- /dev/null
+++ b/meta-yocto/classes/compatible-depends-report.bbclass
@@ -0,0 +1,32 @@
+# Not to be used directly - The concrete bbclass for a tool should inherit this
+
+# Working directory and output directory for reports
+# This needs to stage somewhere that sstate can recover so that when rebuilding
+# sysroots from sstate this is also recovered.
+COMPATIBILITY_REPORT_WORKDIR = "${WORKDIR}/compatibility-info/"
+COMPATIBILITY_REPORT_DIR = "${TMPDIR}/compatibility-info/${MULTIMACH_TARGET_SYS}/${PN}/"
+
+# Dummy task for generating reports - the concrete formats are expected to
+# append to this task to add their actual code for generating reports
+do_compatibility_report[dirs] += "${COMPATIBILITY_REPORT_WORKDIR}"
+do_compatibility_report () {
+    :
+}
+addtask compatibility_report after do_package before do_build
+
+# Make do_compatibility_report task SSTATE compatible
+SSTATETASKS += "do_compatibility_report"
+
+do_compatibility_report[sstate-inputdirs] = "${COMPATIBILITY_REPORT_WORKDIR}"
+do_compatibility_report[sstate-outputdirs] = "${COMPATIBILITY_REPORT_DIR}"
+
+python do_compatibility_report_setscene () {
+    sstate_setscene(d)
+}
+addtask do_compatibility_report_setscene
+
+# Needed for recipes like native XXX: is this better than just adding do_install by default?
+python __anonymous () {
+    if not d.getVarFlag("do_package", "task", True):
+        d.appendVarFlag('do_compatibility_report', 'depends', ' do_install')
+}
diff --git a/meta-yocto/classes/compatible-depends.bbclass b/meta-yocto/classes/compatible-depends.bbclass
new file mode 100644
index 0000000..3e6823d
--- /dev/null
+++ b/meta-yocto/classes/compatible-depends.bbclass
@@ -0,0 +1,144 @@
+# Recipes that use CDEPENDS inherit this bbclass
+
+# Add default CDEPENDS because this variable must be declared
+CDEPENDS ?= ""
+
+# Add CDEPENDS to the sstate hashing signatures so that changes to this variable
+# trigger sstate invalidations and can enforce rebuilds
+do_unpack[vardeps] += "CDEPENDS"
+do_cache_compatibility_reports[vardeps] += "CDEPENDS"
+
+# Delay the compiling tasks of the recipe to wait for the report data of other
+# recipes it CDEPENDS on (if they have a do_compatibility_report task).
+do_unpack[deptask] += "do_compatibility_report"
+
+# Give do_unpack task a cdepends flag, so runQueueExecuteTask can determine if
+# it should run compatibility checks prior to running do_unpack
+do_unpack[cdepends] = "${CDEPENDS}"
+
+# Working directories to get reports from and store them while working on them
+# Extracted by Bitbake during compatibility checking
+# Potential bug here, what if a dependency doesn't fit into these arch targets, how do we find out his architecture?
+# To fix: we now pass architecture through BBTASKDEPS - we need to use this somehow
+CDEPENDS_SEARCHDIR = "${TMPDIR}/compatibility-info/"
+CDEPENDS_STAGEDIR = "${WORKDIR}/dependency-compatibility-info-staging/"
+CDEPENDS_WORKDIR = "${WORKDIR}/dependency-compatibility-info/"
+
+# Task to copy the compatibility reports of its CDEPENDS to the local working
+# directory. These are then cached by sstate and placed into the expected
+# directory for Bitbake to use for future compatibility checks.
+python copy_compatibility_reports () {
+    import errno
+    import os
+    import shutil
+    import subprocess
+
+    cdepends = d.getVar("CDEPENDS", True).strip() or ""
+    cdepends_searchdir = d.getVar("CDEPENDS_SEARCHDIR", True)
+    cdepends_stagedir = d.getVar("CDEPENDS_STAGEDIR", True)
+
+    taskdepdata = d.getVar("BB_TASKDEPDATA", True)
+
+    if cdepends_stagedir:
+        # Need to make the directory ourselves because prefuncs are too early..
+        bb.utils.mkdirhier(cdepends_stagedir)
+
+        for cdepend in cdepends.split():
+            cdepend = cdepend.strip()
+            cdepend_arch = d.getVar("MULTIMACH_TARGET_SYS", True) or ""
+            cdepend_provider = cdepend
+            for task in taskdepdata:
+                if cdepend in taskdepdata[task][4]:
+                    if taskdepdata[task][1] == "do_compatibility_report":
+                        cdepend_arch = taskdepdata[task][6]
+                        cdepend_provider = taskdepdata[task][0]
+
+            shutil.rmtree(os.path.join(cdepends_stagedir, cdepend), ignore_errors=True)
+            try:
+                shutil.copytree(os.path.join(cdepends_searchdir, cdepend_arch, cdepend_provider), os.path.join(cdepends_stagedir, cdepend), symlinks=True)
+                bb.debug(3, "cdepends: %s.copy_compatibility_reports: copied from %s to %s" % (d.getVar("PN", True), os.path.join(cdepends_searchdir, cdepend_arch, cdepend_provider), os.path.join(cdepends_stagedir, cdepend)))
+            except OSError as e:
+                # Sometimes we CDEPENDS on things with no compatibility report
+                # (ie. binary deliveries) - so avoid throwing errors when
+                # CDEPENDS_INSANE_SKIP is set for this dependency
+                insane_skip = d.getVar("CDEPENDS_INSANE_SKIP_%s" % (cdepend,), True)
+                if e.errno == errno.ENOENT and insane_skip:
+                    pass
+                else:
+                    raise e
+}
+
+do_cache_compatibility_reports () {
+    :
+}
+do_cache_compatibility_reports[prefuncs] += "copy_compatibility_reports"
+addtask cache_compatibility_reports after do_install do_package before do_populate_sysroot do_build
+
+# Make do_cache_compatibility_reports task SSTATE compatible
+SSTATETASKS += "do_cache_compatibility_reports"
+
+do_cache_compatibility_reports[dirs] = "${CDEPENDS_STAGEDIR} ${CDEPENDS_WORKDIR}"
+do_cache_compatibility_reports[sstate-inputdirs] = "${CDEPENDS_STAGEDIR}"
+do_cache_compatibility_reports[sstate-outputdirs] = "${CDEPENDS_WORKDIR}"
+
+python do_cache_compatibility_reports_setscene () {
+    sstate_setscene(d)
+}
+addtask do_cache_compatibility_reports_setscene
+
+python cache_sstate_hashes () {
+    import os
+
+    cdepends = d.getVar("CDEPENDS", True).strip() or ""
+    cdepends_hashes = dict()
+    if cdepends:
+        bb.debug(1, "cdepends: %s:cache_sstate_hashes: CDEPENDS: %s" % (d.getVar("PN", True), cdepends))
+        taskdepdata = d.getVar("BB_TASKDEPDATA", True)
+
+        for cdepend in cdepends.split():
+            cdepend = cdepend.strip()
+            cdepend_preferred_provider = d.getVar("PREFERRED_PROVIDER_%s" % (cdepend), True) or ""
+            cdepend_preferred_provider_hash = ""
+
+            cdepend_providers = ""
+            cdepend_hashes = ""
+            found_hashes = dict()
+            for task in taskdepdata:
+                if cdepend in taskdepdata[task][4]:
+                    if taskdepdata[task][1] == "do_compatibility_report":
+                        found_hashes[taskdepdata[task][0]] = taskdepdata[task][5]
+                        if cdepend_preferred_provider == taskdepdata[task][0]:
+                            cdepend_preferred_provider_hash = taskdepdata[task][5]
+
+            # Sort hashes
+            cdepend_providers = ','.join(found_hashes.keys())
+            sorted_hashes = sorted(found_hashes.values())
+            for cdepend_provider_hash in sorted_hashes:
+                cdepend_hashes += cdepend_provider_hash
+
+            if cdepend_preferred_provider_hash:
+                bb.debug(1, "cdepends: %s:cache_sstate_hashes: CDEPENDS_HASH_%s=%s from single provider: %s" % (d.getVar("PN", True), cdepend, cdepend_preferred_provider_hash, cdepend_preferred_provider))
+                cdepends_hashes[cdepend] = cdepend_preferred_provider_hash
+            else:
+                if len(found_hashes.keys()) == 0:
+                    bb.debug(1, "cdepends: %s:cache_sstate_hashes: No sstate hashes found for provider of: %s" % (d.getVar("PN", True), cdepend))
+                    cdepends_hashes[cdepend] = ""
+                elif len(found_hashes.keys()) == 1:
+                    bb.debug(1, "cdepends: %s:cache_sstate_hashes: CDEPENDS_HASH_%s=%s from single provider: %s" % (d.getVar("PN", True), cdepend, cdepend_hashes, cdepend_providers))
+                    cdepends_hashes[cdepend] = cdepend_hashes
+                else:
+                    bb.debug(1, "cdepends: %s:cache_sstate_hashes: CDEPENDS_HASH_%s=%s from group of providers: %s" % (d.getVar("PN", True), cdepend, cdepend_hashes, cdepend_providers))
+                    cdepends_hashes[cdepend] = cdepend_hashes
+
+    # Write to sstate cached compatibility reports directory
+    try:
+        bb.utils.mkdirhier(d.getVar("CDEPENDS_STAGEDIR", True))
+        sstate_hash_manifest_file_path = os.path.join(d.getVar("CDEPENDS_STAGEDIR", True), "sstate-hashes")
+        with open(sstate_hash_manifest_file_path, "w") as sstate_hash_manifest_file:
+            for cdepend in cdepends_hashes:
+                sstate_hash_manifest_file.write("%s %s\n" % (cdepend, cdepends_hashes[cdepend]))
+            sstate_hash_manifest_file.close()
+    except (OSError, IOError) as e:
+        bb.warn("cdepends: Could not cache sstate hashes for %s: %s" % (d.getVar("PN", True), str(e)))
+}
+do_cache_compatibility_reports[postfuncs] += "cache_sstate_hashes"
-- 
2.7.4

From ebc8216d7439fbf60c98a326382687f212dbbeb4 Mon Sep 17 00:00:00 2001
From: Michael Ho <michael...@bmw.de>
Date: Fri, 30 Jun 2017 07:07:27 +0200
Subject: [PATCH 9/9] recipes-cdepends-test: example recipe framework for using
 CDEPENDS
To: yocto@yoctoproject.org

Adds required classes, python library, and recipes for testing the
CDEPENDS usage of a parent recipe generating text files and another
recipe that depends on the file checksums of a key file from the
parent.

Recipe cdepends-test1 should always rebuild and generate a new text
file. However, it contains a file called motd2.txt that is taken from
the recipe files path. This does not change as often.

Recipe cdepends-test2 depends on the motd2.txt file that is built in
the parent cdepends-test1. It only needs to recompile when this text
file is changed.

What should be found by running these examples is that after the
sstate cache is populated with a first run, the recipe cdepends-test2
should only rebuild when the file motd2.txt is modified in the recipe
cdepends-test1.
---
 .../compatible-depends-report-pkgcontents.bbclass  | 10 ++++++
 meta-yocto/lib/cdepends_pkgcontents.py             | 41 ++++++++++++++++++++++
 meta-yocto/recipes-cdepends-test                   |  1 +
 3 files changed, 52 insertions(+)
 create mode 100644 meta-yocto/classes/compatible-depends-report-pkgcontents.bbclass
 create mode 100644 meta-yocto/lib/cdepends_pkgcontents.py
 create mode 160000 meta-yocto/recipes-cdepends-test

diff --git a/meta-yocto/classes/compatible-depends-report-pkgcontents.bbclass b/meta-yocto/classes/compatible-depends-report-pkgcontents.bbclass
new file mode 100644
index 0000000..a060a51
--- /dev/null
+++ b/meta-yocto/classes/compatible-depends-report-pkgcontents.bbclass
@@ -0,0 +1,10 @@
+inherit compatible-depends-report
+
+pkgcontents_compatibility_report () {
+    REPORT_TOOL="pkgcontents"
+    mkdir -p "${COMPATIBILITY_REPORT_WORKDIR}"/"${REPORT_TOOL}"
+    find "${WORKDIR}/packages-split/" -type f -exec sha1sum "{}" \; | tee "${COMPATIBILITY_REPORT_WORKDIR}"/"${REPORT_TOOL}"/manifest
+    sed -i -e "s?${WORKDIR}/packages-split/??" "${COMPATIBILITY_REPORT_WORKDIR}"/"${REPORT_TOOL}"/manifest
+}
+
+do_compatibility_report[postfuncs] += "pkgcontents_compatibility_report"
diff --git a/meta-yocto/lib/cdepends_pkgcontents.py b/meta-yocto/lib/cdepends_pkgcontents.py
new file mode 100644
index 0000000..6af4c84
--- /dev/null
+++ b/meta-yocto/lib/cdepends_pkgcontents.py
@@ -0,0 +1,41 @@
+import os.path
+
+import bb
+
+def compatibility_check(cached_report_dir, new_report_dir, bb_cachedata):
+    if not os.path.isdir(cached_report_dir):
+        bb.debug(1, "cdepends: cdepends_pkgcontents.compatibility_check cannot find %s" % (cached_report_dir,))
+        return "incompatible"
+
+    cached_manifest_path = os.path.join(cached_report_dir, "pkgcontents", "manifest")
+    if not os.path.isfile(cached_manifest_path):
+        bb.debug(1, "cdepends: cdepends_pkgcontents.compatibility_check cannot find %s" % (cached_manifest_path,))
+        return "incompatible"
+
+    new_manifest_path = os.path.join(new_report_dir, "pkgcontents", "manifest")
+    if not os.path.isfile(new_manifest_path):
+        bb.debug(1, "cdepends: cdepends_pkgcontents.compatibility_check cannot find %s" % (new_manifest_path,))
+        return "incompatible"
+
+    match = 1
+    try:
+        with open(cached_manifest_path) as f1, open(new_manifest_path) as f2:
+            for line1 in f1:
+                line_match = 0
+                for line2 in f2:
+                    if line1 == line2:
+                        line_match = 1
+                        break
+                if line_match == 0:
+                    match = 0
+                    break
+    except (IOError, OSError) as e:
+        bb.debug(1, "cdepends: cdepends_pkgcontents.compatibility_check could not read manifest file/s: %s" % (str(e),))
+        return "incompatible"
+
+    if match == 0:
+        bb.debug(1, "cdepends: cdepends_pkgcontents.compatibility_check returning incompatible")
+        return "incompatible"
+    else:
+        bb.debug(1, "cdepends: cdepends_pkgcontents.compatibility_check returning compatible")
+        return "compatible"
diff --git a/meta-yocto/recipes-cdepends-test b/meta-yocto/recipes-cdepends-test
new file mode 160000
index 0000000..013ad65
--- /dev/null
+++ b/meta-yocto/recipes-cdepends-test
@@ -0,0 +1 @@
+Subproject commit 013ad65d7a884931eab619e9f613760ba209e1cf
-- 
2.7.4

-- 
_______________________________________________
yocto mailing list
yocto@yoctoproject.org
https://lists.yoctoproject.org/listinfo/yocto

Reply via email to