Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 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 | if bb.data.inherits_class('native', d) or bb.data.inherits_class('cross', d): |
| 29 | return |
| 30 | # Package backend agnostic intercept |
| 31 | # This assumes that the package_write task is called package_write_<pkgtype> |
| 32 | # and that the directory in which packages should be written is |
| 33 | # pointed to by the variable DEPLOY_DIR_<PKGTYPE> |
| 34 | for pkgclass in (d.getVar('PACKAGE_CLASSES', True) or '').split(): |
| 35 | if pkgclass.startswith('package_'): |
| 36 | pkgtype = pkgclass.split('_', 1)[1] |
| 37 | pkgwritefunc = 'do_package_write_%s' % pkgtype |
| 38 | sstate_outputdirs = d.getVarFlag(pkgwritefunc, 'sstate-outputdirs', False) |
| 39 | deploydirvar = 'DEPLOY_DIR_%s' % pkgtype.upper() |
| 40 | deploydirvarref = '${' + deploydirvar + '}' |
| 41 | pkgcomparefunc = 'do_package_compare_%s' % pkgtype |
| 42 | |
| 43 | if bb.data.inherits_class('image', d): |
| 44 | d.appendVarFlag('do_rootfs', 'recrdeptask', ' ' + pkgcomparefunc) |
| 45 | |
| 46 | if bb.data.inherits_class('populate_sdk_base', d): |
| 47 | d.appendVarFlag('do_populate_sdk', 'recrdeptask', ' ' + pkgcomparefunc) |
| 48 | |
| 49 | if bb.data.inherits_class('populate_sdk_ext', d): |
| 50 | d.appendVarFlag('do_populate_sdk_ext', 'recrdeptask', ' ' + pkgcomparefunc) |
| 51 | |
| 52 | d.appendVarFlag('do_build', 'recrdeptask', ' ' + pkgcomparefunc) |
| 53 | |
| 54 | if d.getVarFlag(pkgwritefunc, 'noexec', True) or not d.getVarFlag(pkgwritefunc, 'task', True): |
| 55 | # Packaging is disabled for this recipe, we shouldn't do anything |
| 56 | continue |
| 57 | |
| 58 | if deploydirvarref in sstate_outputdirs: |
| 59 | deplor_dir_pkgtype = d.expand(deploydirvarref + '-prediff') |
| 60 | # Set intermediate output directory |
| 61 | d.setVarFlag(pkgwritefunc, 'sstate-outputdirs', sstate_outputdirs.replace(deploydirvarref, deplor_dir_pkgtype)) |
| 62 | # Update SSTATE_DUPWHITELIST to avoid shared location conflicted error |
| 63 | d.appendVar('SSTATE_DUPWHITELIST', ' %s' % deplor_dir_pkgtype) |
| 64 | |
| 65 | d.setVar(pkgcomparefunc, d.getVar('do_package_compare', False)) |
| 66 | d.setVarFlags(pkgcomparefunc, d.getVarFlags('do_package_compare', False)) |
| 67 | d.appendVarFlag(pkgcomparefunc, 'depends', ' build-compare-native:do_populate_sysroot') |
| 68 | bb.build.addtask(pkgcomparefunc, 'do_build', 'do_packagedata ' + pkgwritefunc, d) |
| 69 | } |
| 70 | |
| 71 | # This isn't the real task function - it's a template that we use in the |
| 72 | # anonymous python code above |
| 73 | fakeroot python do_package_compare () { |
| 74 | currenttask = d.getVar('BB_CURRENTTASK', True) |
| 75 | pkgtype = currenttask.rsplit('_', 1)[1] |
| 76 | package_compare_impl(pkgtype, d) |
| 77 | } |
| 78 | |
| 79 | def package_compare_impl(pkgtype, d): |
| 80 | import errno |
| 81 | import fnmatch |
| 82 | import glob |
| 83 | import subprocess |
| 84 | import oe.sstatesig |
| 85 | |
| 86 | pn = d.getVar('PN', True) |
| 87 | deploydir = d.getVar('DEPLOY_DIR_%s' % pkgtype.upper(), True) |
| 88 | prepath = deploydir + '-prediff/' |
| 89 | |
| 90 | # Find out PKGR values are |
| 91 | pkgdatadir = d.getVar('PKGDATA_DIR', True) |
| 92 | packages = [] |
| 93 | try: |
| 94 | with open(os.path.join(pkgdatadir, pn), 'r') as f: |
| 95 | for line in f: |
| 96 | if line.startswith('PACKAGES:'): |
| 97 | packages = line.split(':', 1)[1].split() |
| 98 | break |
| 99 | except IOError as e: |
| 100 | if e.errno == errno.ENOENT: |
| 101 | pass |
| 102 | |
| 103 | if not packages: |
| 104 | bb.debug(2, '%s: no packages, nothing to do' % pn) |
| 105 | return |
| 106 | |
| 107 | pkgrvalues = {} |
| 108 | rpkgnames = {} |
| 109 | rdepends = {} |
| 110 | pkgvvalues = {} |
| 111 | for pkg in packages: |
| 112 | with open(os.path.join(pkgdatadir, 'runtime', pkg), 'r') as f: |
| 113 | for line in f: |
| 114 | if line.startswith('PKGR:'): |
| 115 | pkgrvalues[pkg] = line.split(':', 1)[1].strip() |
| 116 | if line.startswith('PKGV:'): |
| 117 | pkgvvalues[pkg] = line.split(':', 1)[1].strip() |
| 118 | elif line.startswith('PKG_%s:' % pkg): |
| 119 | rpkgnames[pkg] = line.split(':', 1)[1].strip() |
| 120 | elif line.startswith('RDEPENDS_%s:' % pkg): |
| 121 | rdepends[pkg] = line.split(':', 1)[1].strip() |
| 122 | |
| 123 | # Prepare a list of the runtime package names for packages that were |
| 124 | # actually produced |
| 125 | rpkglist = [] |
| 126 | for pkg, rpkg in rpkgnames.items(): |
| 127 | if os.path.exists(os.path.join(pkgdatadir, 'runtime', pkg + '.packaged')): |
| 128 | rpkglist.append((rpkg, pkg)) |
| 129 | rpkglist.sort(key=lambda x: len(x[0]), reverse=True) |
| 130 | |
| 131 | pvu = d.getVar('PV', False) |
| 132 | if '$' + '{SRCPV}' in pvu: |
| 133 | pvprefix = pvu.split('$' + '{SRCPV}', 1)[0] |
| 134 | else: |
| 135 | pvprefix = None |
| 136 | |
| 137 | pkgwritetask = 'package_write_%s' % pkgtype |
| 138 | files = [] |
| 139 | docopy = False |
| 140 | manifest, _ = oe.sstatesig.sstate_get_manifest_filename(pkgwritetask, d) |
| 141 | mlprefix = d.getVar('MLPREFIX', True) |
| 142 | # Copy recipe's all packages if one of the packages are different to make |
| 143 | # they have the same PR. |
| 144 | with open(manifest, 'r') as f: |
| 145 | for line in f: |
| 146 | if line.startswith(prepath): |
| 147 | srcpath = line.rstrip() |
| 148 | if os.path.isfile(srcpath): |
| 149 | destpath = os.path.join(deploydir, os.path.relpath(srcpath, prepath)) |
| 150 | |
| 151 | # This is crude but should work assuming the output |
| 152 | # package file name starts with the package name |
| 153 | # and rpkglist is sorted by length (descending) |
| 154 | pkgbasename = os.path.basename(destpath) |
| 155 | pkgname = None |
| 156 | for rpkg, pkg in rpkglist: |
| 157 | if mlprefix and pkgtype == 'rpm' and rpkg.startswith(mlprefix): |
| 158 | rpkg = rpkg[len(mlprefix):] |
| 159 | if pkgbasename.startswith(rpkg): |
| 160 | pkgr = pkgrvalues[pkg] |
| 161 | destpathspec = destpath.replace(pkgr, '*') |
| 162 | if pvprefix: |
| 163 | pkgv = pkgvvalues[pkg] |
| 164 | if pkgv.startswith(pvprefix): |
| 165 | pkgvsuffix = pkgv[len(pvprefix):] |
| 166 | if '+' in pkgvsuffix: |
| 167 | newpkgv = pvprefix + '*+' + pkgvsuffix.split('+', 1)[1] |
| 168 | destpathspec = destpathspec.replace(pkgv, newpkgv) |
| 169 | pkgname = pkg |
| 170 | break |
| 171 | else: |
| 172 | bb.warn('Unable to map %s back to package' % pkgbasename) |
| 173 | destpathspec = destpath |
| 174 | |
| 175 | oldfile = None |
| 176 | if not docopy: |
| 177 | oldfiles = glob.glob(destpathspec) |
| 178 | if oldfiles: |
| 179 | oldfile = oldfiles[-1] |
| 180 | result = subprocess.call(['pkg-diff.sh', oldfile, srcpath]) |
| 181 | if result != 0: |
| 182 | docopy = True |
| 183 | bb.note("%s and %s are different, will copy packages" % (oldfile, srcpath)) |
| 184 | else: |
| 185 | docopy = True |
| 186 | bb.note("No old packages found for %s, will copy packages" % pkgname) |
| 187 | |
| 188 | files.append((pkgname, pkgbasename, srcpath, destpath)) |
| 189 | |
| 190 | # Remove all the old files and copy again if docopy |
| 191 | if docopy: |
| 192 | bb.plain('Copying packages for recipe %s' % pn) |
| 193 | pcmanifest = os.path.join(prepath, d.expand('pkg-compare-manifest-${MULTIMACH_TARGET_SYS}-${PN}')) |
| 194 | try: |
| 195 | with open(pcmanifest, 'r') as f: |
| 196 | for line in f: |
| 197 | fn = line.rstrip() |
| 198 | if fn: |
| 199 | try: |
| 200 | os.remove(fn) |
| 201 | bb.note('Removed old package %s' % fn) |
| 202 | except OSError as e: |
| 203 | if e.errno == errno.ENOENT: |
| 204 | pass |
| 205 | except IOError as e: |
| 206 | if e.errno == errno.ENOENT: |
| 207 | pass |
| 208 | |
| 209 | # Create new manifest |
| 210 | with open(pcmanifest, 'w') as f: |
| 211 | for pkgname, pkgbasename, srcpath, destpath in files: |
| 212 | destdir = os.path.dirname(destpath) |
| 213 | bb.utils.mkdirhier(destdir) |
| 214 | # Remove allarch rpm pkg if it is already existed (for |
| 215 | # multilib), they're identical in theory, but sstate.bbclass |
| 216 | # copies it again, so keep align with that. |
| 217 | if os.path.exists(destpath) and pkgtype == 'rpm' \ |
| 218 | and d.getVar('PACKAGE_ARCH', True) == 'all': |
| 219 | os.unlink(destpath) |
| 220 | if (os.stat(srcpath).st_dev == os.stat(destdir).st_dev): |
| 221 | # Use a hard link to save space |
| 222 | os.link(srcpath, destpath) |
| 223 | else: |
| 224 | shutil.copyfile(srcpath, destpath) |
| 225 | f.write('%s\n' % destpath) |
| 226 | else: |
| 227 | bb.plain('Not copying packages for recipe %s' % pn) |
| 228 | |
| 229 | do_cleansstate[postfuncs] += "pfs_cleanpkgs" |
| 230 | python pfs_cleanpkgs () { |
| 231 | import errno |
| 232 | for pkgclass in (d.getVar('PACKAGE_CLASSES', True) or '').split(): |
| 233 | if pkgclass.startswith('package_'): |
| 234 | pkgtype = pkgclass.split('_', 1)[1] |
| 235 | deploydir = d.getVar('DEPLOY_DIR_%s' % pkgtype.upper(), True) |
| 236 | prepath = deploydir + '-prediff' |
| 237 | pcmanifest = os.path.join(prepath, d.expand('pkg-compare-manifest-${MULTIMACH_TARGET_SYS}-${PN}')) |
| 238 | try: |
| 239 | with open(pcmanifest, 'r') as f: |
| 240 | for line in f: |
| 241 | fn = line.rstrip() |
| 242 | if fn: |
| 243 | try: |
| 244 | os.remove(fn) |
| 245 | except OSError as e: |
| 246 | if e.errno == errno.ENOENT: |
| 247 | pass |
| 248 | os.remove(pcmanifest) |
| 249 | except IOError as e: |
| 250 | if e.errno == errno.ENOENT: |
| 251 | pass |
| 252 | } |