diff options
| -rw-r--r-- | meta/classes/packagefeed-stability.bbclass | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/meta/classes/packagefeed-stability.bbclass b/meta/classes/packagefeed-stability.bbclass new file mode 100644 index 0000000000..b5207d9f84 --- /dev/null +++ b/meta/classes/packagefeed-stability.bbclass | |||
| @@ -0,0 +1,270 @@ | |||
| 1 | # Class to avoid copying packages into the feed if they haven't materially changed | ||
| 2 | # | ||
| 3 | # Copyright (C) 2015 Intel Corporation | ||
| 4 | # Released under the MIT license (see COPYING.MIT for details) | ||
| 5 | # | ||
| 6 | # This class effectively intercepts packages as they are written out by | ||
| 7 | # do_package_write_*, causing them to be written into a different | ||
| 8 | # directory where we can compare them to whatever older packages might | ||
| 9 | # be in the "real" package feed directory, and avoid copying the new | ||
| 10 | # package to the feed if it has not materially changed. The idea is to | ||
| 11 | # avoid unnecessary churn in the packages when dependencies trigger task | ||
| 12 | # reexecution (and thus repackaging). Enabling the class is simple: | ||
| 13 | # | ||
| 14 | # INHERIT += "packagefeed-stability" | ||
| 15 | # | ||
| 16 | # Caveats: | ||
| 17 | # 1) Latest PR values in the build system may not match those in packages | ||
| 18 | # seen on the target (naturally) | ||
| 19 | # 2) If you rebuild from sstate without the existing package feed present, | ||
| 20 | # you will lose the "state" of the package feed i.e. the preserved old | ||
| 21 | # package versions. Not the end of the world, but would negate the | ||
| 22 | # entire purpose of this class. | ||
| 23 | # | ||
| 24 | # Note that running -c cleanall on a recipe will purposely delete the old | ||
| 25 | # package files so they will definitely be copied the next time. | ||
| 26 | |||
| 27 | python() { | ||
| 28 | # Package backend agnostic intercept | ||
| 29 | # This assumes that the package_write task is called package_write_<pkgtype> | ||
| 30 | # and that the directory in which packages should be written is | ||
| 31 | # pointed to by the variable DEPLOY_DIR_<PKGTYPE> | ||
| 32 | for pkgclass in (d.getVar('PACKAGE_CLASSES', True) or '').split(): | ||
| 33 | if pkgclass.startswith('package_'): | ||
| 34 | pkgtype = pkgclass.split('_', 1)[1] | ||
| 35 | pkgwritefunc = 'do_package_write_%s' % pkgtype | ||
| 36 | sstate_outputdirs = d.getVarFlag(pkgwritefunc, 'sstate-outputdirs', False) | ||
| 37 | deploydirvar = 'DEPLOY_DIR_%s' % pkgtype.upper() | ||
| 38 | deploydirvarref = '${' + deploydirvar + '}' | ||
| 39 | pkgcomparefunc = 'do_package_compare_%s' % pkgtype | ||
| 40 | |||
| 41 | if bb.data.inherits_class('image', d): | ||
| 42 | d.appendVarFlag('do_rootfs', 'recrdeptask', ' ' + pkgcomparefunc) | ||
| 43 | |||
| 44 | if bb.data.inherits_class('populate_sdk_base', d): | ||
| 45 | d.appendVarFlag('do_populate_sdk', 'recrdeptask', ' ' + pkgcomparefunc) | ||
| 46 | |||
| 47 | if bb.data.inherits_class('populate_sdk_ext', d): | ||
| 48 | d.appendVarFlag('do_populate_sdk_ext', 'recrdeptask', ' ' + pkgcomparefunc) | ||
| 49 | |||
| 50 | d.appendVarFlag('do_build', 'recrdeptask', ' ' + pkgcomparefunc) | ||
| 51 | |||
| 52 | if d.getVarFlag(pkgwritefunc, 'noexec', True) or (not d.getVarFlag(pkgwritefunc, 'task', True)) or pkgwritefunc in (d.getVar('__BBDELTASKS', True) or []): | ||
| 53 | # Packaging is disabled for this recipe, we shouldn't do anything | ||
| 54 | continue | ||
| 55 | |||
| 56 | if deploydirvarref in sstate_outputdirs: | ||
| 57 | # Set intermediate output directory | ||
| 58 | d.setVarFlag(pkgwritefunc, 'sstate-outputdirs', sstate_outputdirs.replace(deploydirvarref, deploydirvarref + '-prediff')) | ||
| 59 | |||
| 60 | d.setVar(pkgcomparefunc, d.getVar('do_package_compare', False)) | ||
| 61 | d.setVarFlags(pkgcomparefunc, d.getVarFlags('do_package_compare', False)) | ||
| 62 | d.appendVarFlag(pkgcomparefunc, 'depends', ' build-compare-native:do_populate_sysroot') | ||
| 63 | bb.build.addtask(pkgcomparefunc, 'do_build', 'do_packagedata ' + pkgwritefunc, d) | ||
| 64 | } | ||
| 65 | |||
| 66 | # This isn't the real task function - it's a template that we use in the | ||
| 67 | # anonymous python code above | ||
| 68 | fakeroot python do_package_compare () { | ||
| 69 | currenttask = d.getVar('BB_CURRENTTASK', True) | ||
| 70 | pkgtype = currenttask.rsplit('_', 1)[1] | ||
| 71 | package_compare_impl(pkgtype, d) | ||
| 72 | } | ||
| 73 | |||
| 74 | def package_compare_impl(pkgtype, d): | ||
| 75 | import errno | ||
| 76 | import fnmatch | ||
| 77 | import glob | ||
| 78 | import subprocess | ||
| 79 | import oe.sstatesig | ||
| 80 | |||
| 81 | pn = d.getVar('PN', True) | ||
| 82 | deploydir = d.getVar('DEPLOY_DIR_%s' % pkgtype.upper(), True) | ||
| 83 | prepath = deploydir + '-prediff/' | ||
| 84 | |||
| 85 | # Find out PKGR values are | ||
| 86 | pkgdatadir = d.getVar('PKGDATA_DIR', True) | ||
| 87 | packages = [] | ||
| 88 | try: | ||
| 89 | with open(os.path.join(pkgdatadir, pn), 'r') as f: | ||
| 90 | for line in f: | ||
| 91 | if line.startswith('PACKAGES:'): | ||
| 92 | packages = line.split(':', 1)[1].split() | ||
| 93 | break | ||
| 94 | except IOError as e: | ||
| 95 | if e.errno == errno.ENOENT: | ||
| 96 | pass | ||
| 97 | |||
| 98 | if not packages: | ||
| 99 | bb.debug(2, '%s: no packages, nothing to do' % pn) | ||
| 100 | return | ||
| 101 | |||
| 102 | pkgrvalues = {} | ||
| 103 | rpkgnames = {} | ||
| 104 | rdepends = {} | ||
| 105 | pkgvvalues = {} | ||
| 106 | for pkg in packages: | ||
| 107 | with open(os.path.join(pkgdatadir, 'runtime', pkg), 'r') as f: | ||
| 108 | for line in f: | ||
| 109 | if line.startswith('PKGR:'): | ||
| 110 | pkgrvalues[pkg] = line.split(':', 1)[1].strip() | ||
| 111 | if line.startswith('PKGV:'): | ||
| 112 | pkgvvalues[pkg] = line.split(':', 1)[1].strip() | ||
| 113 | elif line.startswith('PKG_%s:' % pkg): | ||
| 114 | rpkgnames[pkg] = line.split(':', 1)[1].strip() | ||
| 115 | elif line.startswith('RDEPENDS_%s:' % pkg): | ||
| 116 | rdepends[pkg] = line.split(':', 1)[1].strip() | ||
| 117 | |||
| 118 | # Prepare a list of the runtime package names for packages that were | ||
| 119 | # actually produced | ||
| 120 | rpkglist = [] | ||
| 121 | for pkg, rpkg in rpkgnames.iteritems(): | ||
| 122 | if os.path.exists(os.path.join(pkgdatadir, 'runtime', pkg + '.packaged')): | ||
| 123 | rpkglist.append((rpkg, pkg)) | ||
| 124 | rpkglist.sort(key=lambda x: len(x[0]), reverse=True) | ||
| 125 | |||
| 126 | pvu = d.getVar('PV', False) | ||
| 127 | if '$' + '{SRCPV}' in pvu: | ||
| 128 | pvprefix = pvu.split('$' + '{SRCPV}', 1)[0] | ||
| 129 | else: | ||
| 130 | pvprefix = None | ||
| 131 | |||
| 132 | pkgwritetask = 'package_write_%s' % pkgtype | ||
| 133 | files = [] | ||
| 134 | copypkgs = [] | ||
| 135 | manifest, _ = oe.sstatesig.sstate_get_manifest_filename(pkgwritetask, d) | ||
| 136 | with open(manifest, 'r') as f: | ||
| 137 | for line in f: | ||
| 138 | if line.startswith(prepath): | ||
| 139 | srcpath = line.rstrip() | ||
| 140 | if os.path.isfile(srcpath): | ||
| 141 | destpath = os.path.join(deploydir, os.path.relpath(srcpath, prepath)) | ||
| 142 | |||
| 143 | # This is crude but should work assuming the output | ||
| 144 | # package file name starts with the package name | ||
| 145 | # and rpkglist is sorted by length (descending) | ||
| 146 | pkgbasename = os.path.basename(destpath) | ||
| 147 | pkgname = None | ||
| 148 | for rpkg, pkg in rpkglist: | ||
| 149 | if pkgbasename.startswith(rpkg): | ||
| 150 | pkgr = pkgrvalues[pkg] | ||
| 151 | destpathspec = destpath.replace(pkgr, '*') | ||
| 152 | if pvprefix: | ||
| 153 | pkgv = pkgvvalues[pkg] | ||
| 154 | if pkgv.startswith(pvprefix): | ||
| 155 | pkgvsuffix = pkgv[len(pvprefix):] | ||
| 156 | if '+' in pkgvsuffix: | ||
| 157 | newpkgv = pvprefix + '*+' + pkgvsuffix.split('+', 1)[1] | ||
| 158 | destpathspec = destpathspec.replace(pkgv, newpkgv) | ||
| 159 | pkgname = pkg | ||
| 160 | break | ||
| 161 | else: | ||
| 162 | bb.warn('Unable to map %s back to package' % pkgbasename) | ||
| 163 | destpathspec = destpath | ||
| 164 | |||
| 165 | oldfiles = glob.glob(destpathspec) | ||
| 166 | oldfile = None | ||
| 167 | docopy = True | ||
| 168 | if oldfiles: | ||
| 169 | oldfile = oldfiles[-1] | ||
| 170 | result = subprocess.call(['pkg-diff.sh', oldfile, srcpath]) | ||
| 171 | if result == 0: | ||
| 172 | docopy = False | ||
| 173 | |||
| 174 | files.append((pkgname, pkgbasename, srcpath, oldfile, destpath)) | ||
| 175 | bb.debug(2, '%s: package %s %s' % (pn, files[-1], docopy)) | ||
| 176 | if docopy: | ||
| 177 | copypkgs.append(pkgname) | ||
| 178 | |||
| 179 | # Ensure that dependencies on specific versions (such as -dev on the | ||
| 180 | # main package) are copied in lock-step | ||
| 181 | changed = True | ||
| 182 | while changed: | ||
| 183 | rpkgdict = {x[0]: x[1] for x in rpkglist} | ||
| 184 | changed = False | ||
| 185 | for pkgname, pkgbasename, srcpath, oldfile, destpath in files: | ||
| 186 | rdeps = rdepends.get(pkgname, None) | ||
| 187 | if not rdeps: | ||
| 188 | continue | ||
| 189 | rdepvers = bb.utils.explode_dep_versions2(rdeps) | ||
| 190 | for rdep, versions in rdepvers.iteritems(): | ||
| 191 | dep = rpkgdict.get(rdep, None) | ||
| 192 | for version in versions: | ||
| 193 | if version and version.startswith('= '): | ||
| 194 | if dep in copypkgs and not pkgname in copypkgs: | ||
| 195 | bb.debug(2, '%s: copying %s because it has a fixed version dependency on %s and that package is going to be copied' % (pn, pkgname, dep)) | ||
| 196 | changed = True | ||
| 197 | copypkgs.append(pkgname) | ||
| 198 | elif pkgname in copypkgs and not dep in copypkgs: | ||
| 199 | bb.debug(2, '%s: copying %s because %s has a fixed version dependency on it and that package is going to be copied' % (pn, dep, pkgname)) | ||
| 200 | changed = True | ||
| 201 | copypkgs.append(dep) | ||
| 202 | |||
| 203 | # Read in old manifest so we can delete any packages we aren't going to replace or preserve | ||
| 204 | pcmanifest = os.path.join(prepath, d.expand('pkg-compare-manifest-${MULTIMACH_TARGET_SYS}-${PN}')) | ||
| 205 | try: | ||
| 206 | with open(pcmanifest, 'r') as f: | ||
| 207 | knownfiles = [x[3] for x in files if x[3]] | ||
| 208 | for line in f: | ||
| 209 | fn = line.rstrip() | ||
| 210 | if fn: | ||
| 211 | if fn in knownfiles: | ||
| 212 | knownfiles.remove(fn) | ||
| 213 | else: | ||
| 214 | try: | ||
| 215 | os.remove(fn) | ||
| 216 | bb.warn('Removed old package %s' % fn) | ||
| 217 | except OSError as e: | ||
| 218 | if e.errno == errno.ENOENT: | ||
| 219 | pass | ||
| 220 | except IOError as e: | ||
| 221 | if e.errno == errno.ENOENT: | ||
| 222 | pass | ||
| 223 | |||
| 224 | # Create new manifest | ||
| 225 | with open(pcmanifest, 'w') as f: | ||
| 226 | for pkgname, pkgbasename, srcpath, oldfile, destpath in files: | ||
| 227 | if pkgname in copypkgs: | ||
| 228 | bb.warn('Copying %s' % pkgbasename) | ||
| 229 | destdir = os.path.dirname(destpath) | ||
| 230 | bb.utils.mkdirhier(destdir) | ||
| 231 | if oldfile: | ||
| 232 | try: | ||
| 233 | os.remove(oldfile) | ||
| 234 | except OSError as e: | ||
| 235 | if e.errno == errno.ENOENT: | ||
| 236 | pass | ||
| 237 | if (os.stat(srcpath).st_dev == os.stat(destdir).st_dev): | ||
| 238 | # Use a hard link to save space | ||
| 239 | os.link(srcpath, destpath) | ||
| 240 | else: | ||
| 241 | shutil.copyfile(srcpath, destpath) | ||
| 242 | f.write('%s\n' % destpath) | ||
| 243 | else: | ||
| 244 | bb.warn('Not copying %s' % pkgbasename) | ||
| 245 | f.write('%s\n' % oldfile) | ||
| 246 | |||
| 247 | |||
| 248 | do_cleanall_append() { | ||
| 249 | import errno | ||
| 250 | for pkgclass in (d.getVar('PACKAGE_CLASSES', True) or '').split(): | ||
| 251 | if pkgclass.startswith('package_'): | ||
| 252 | pkgtype = pkgclass.split('_', 1)[1] | ||
| 253 | deploydir = d.getVar('DEPLOY_DIR_%s' % pkgtype.upper(), True) | ||
| 254 | prepath = deploydir + '-prediff' | ||
| 255 | pcmanifest = os.path.join(prepath, d.expand('pkg-compare-manifest-${MULTIMACH_TARGET_SYS}-${PN}')) | ||
| 256 | try: | ||
| 257 | with open(pcmanifest, 'r') as f: | ||
| 258 | for line in f: | ||
| 259 | fn = line.rstrip() | ||
| 260 | if fn: | ||
| 261 | try: | ||
| 262 | os.remove(fn) | ||
| 263 | except OSError as e: | ||
| 264 | if e.errno == errno.ENOENT: | ||
| 265 | pass | ||
| 266 | os.remove(pcmanifest) | ||
| 267 | except IOError as e: | ||
| 268 | if e.errno == errno.ENOENT: | ||
| 269 | pass | ||
| 270 | } | ||
