blob: aa01def74d1b0129ad1472a167ca2f761eaa836d [file] [log] [blame]
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001# 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
27python() {
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
73fakeroot 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
79def 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
229do_cleansstate[postfuncs] += "pfs_cleanpkgs"
230python 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}