1*4882a593Smuzhiyun# 2*4882a593Smuzhiyun# Copyright BitBake Contributors 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 5*4882a593Smuzhiyun# 6*4882a593Smuzhiyun 7*4882a593Smuzhiyunimport fnmatch 8*4882a593Smuzhiyunimport logging 9*4882a593Smuzhiyunimport os 10*4882a593Smuzhiyunimport shutil 11*4882a593Smuzhiyunimport sys 12*4882a593Smuzhiyunimport tempfile 13*4882a593Smuzhiyun 14*4882a593Smuzhiyunimport bb.utils 15*4882a593Smuzhiyun 16*4882a593Smuzhiyunfrom bblayers.common import LayerPlugin 17*4882a593Smuzhiyun 18*4882a593Smuzhiyunlogger = logging.getLogger('bitbake-layers') 19*4882a593Smuzhiyun 20*4882a593Smuzhiyun 21*4882a593Smuzhiyundef plugin_init(plugins): 22*4882a593Smuzhiyun return ActionPlugin() 23*4882a593Smuzhiyun 24*4882a593Smuzhiyun 25*4882a593Smuzhiyunclass ActionPlugin(LayerPlugin): 26*4882a593Smuzhiyun def do_add_layer(self, args): 27*4882a593Smuzhiyun """Add one or more layers to bblayers.conf.""" 28*4882a593Smuzhiyun layerdirs = [os.path.abspath(ldir) for ldir in args.layerdir] 29*4882a593Smuzhiyun 30*4882a593Smuzhiyun for layerdir in layerdirs: 31*4882a593Smuzhiyun if not os.path.exists(layerdir): 32*4882a593Smuzhiyun sys.stderr.write("Specified layer directory %s doesn't exist\n" % layerdir) 33*4882a593Smuzhiyun return 1 34*4882a593Smuzhiyun 35*4882a593Smuzhiyun layer_conf = os.path.join(layerdir, 'conf', 'layer.conf') 36*4882a593Smuzhiyun if not os.path.exists(layer_conf): 37*4882a593Smuzhiyun sys.stderr.write("Specified layer directory %s doesn't contain a conf/layer.conf file\n" % layerdir) 38*4882a593Smuzhiyun return 1 39*4882a593Smuzhiyun 40*4882a593Smuzhiyun bblayers_conf = os.path.join('conf', 'bblayers.conf') 41*4882a593Smuzhiyun if not os.path.exists(bblayers_conf): 42*4882a593Smuzhiyun sys.stderr.write("Unable to find bblayers.conf\n") 43*4882a593Smuzhiyun return 1 44*4882a593Smuzhiyun 45*4882a593Smuzhiyun # Back up bblayers.conf to tempdir before we add layers 46*4882a593Smuzhiyun tempdir = tempfile.mkdtemp() 47*4882a593Smuzhiyun backup = tempdir + "/bblayers.conf.bak" 48*4882a593Smuzhiyun shutil.copy2(bblayers_conf, backup) 49*4882a593Smuzhiyun 50*4882a593Smuzhiyun try: 51*4882a593Smuzhiyun notadded, _ = bb.utils.edit_bblayers_conf(bblayers_conf, layerdirs, None) 52*4882a593Smuzhiyun if not (args.force or notadded): 53*4882a593Smuzhiyun try: 54*4882a593Smuzhiyun self.tinfoil.run_command('parseConfiguration') 55*4882a593Smuzhiyun except (bb.tinfoil.TinfoilUIException, bb.BBHandledException): 56*4882a593Smuzhiyun # Restore the back up copy of bblayers.conf 57*4882a593Smuzhiyun shutil.copy2(backup, bblayers_conf) 58*4882a593Smuzhiyun bb.fatal("Parse failure with the specified layer added, exiting.") 59*4882a593Smuzhiyun else: 60*4882a593Smuzhiyun for item in notadded: 61*4882a593Smuzhiyun sys.stderr.write("Specified layer %s is already in BBLAYERS\n" % item) 62*4882a593Smuzhiyun finally: 63*4882a593Smuzhiyun # Remove the back up copy of bblayers.conf 64*4882a593Smuzhiyun shutil.rmtree(tempdir) 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun def do_remove_layer(self, args): 67*4882a593Smuzhiyun """Remove one or more layers from bblayers.conf.""" 68*4882a593Smuzhiyun bblayers_conf = os.path.join('conf', 'bblayers.conf') 69*4882a593Smuzhiyun if not os.path.exists(bblayers_conf): 70*4882a593Smuzhiyun sys.stderr.write("Unable to find bblayers.conf\n") 71*4882a593Smuzhiyun return 1 72*4882a593Smuzhiyun 73*4882a593Smuzhiyun layerdirs = [] 74*4882a593Smuzhiyun for item in args.layerdir: 75*4882a593Smuzhiyun if item.startswith('*'): 76*4882a593Smuzhiyun layerdir = item 77*4882a593Smuzhiyun elif not '/' in item: 78*4882a593Smuzhiyun layerdir = '*/%s' % item 79*4882a593Smuzhiyun else: 80*4882a593Smuzhiyun layerdir = os.path.abspath(item) 81*4882a593Smuzhiyun layerdirs.append(layerdir) 82*4882a593Smuzhiyun (_, notremoved) = bb.utils.edit_bblayers_conf(bblayers_conf, None, layerdirs) 83*4882a593Smuzhiyun if notremoved: 84*4882a593Smuzhiyun for item in notremoved: 85*4882a593Smuzhiyun sys.stderr.write("No layers matching %s found in BBLAYERS\n" % item) 86*4882a593Smuzhiyun return 1 87*4882a593Smuzhiyun 88*4882a593Smuzhiyun def do_flatten(self, args): 89*4882a593Smuzhiyun """flatten layer configuration into a separate output directory. 90*4882a593Smuzhiyun 91*4882a593SmuzhiyunTakes the specified layers (or all layers in the current layer 92*4882a593Smuzhiyunconfiguration if none are specified) and builds a "flattened" directory 93*4882a593Smuzhiyuncontaining the contents of all layers, with any overlayed recipes removed 94*4882a593Smuzhiyunand bbappends appended to the corresponding recipes. Note that some manual 95*4882a593Smuzhiyuncleanup may still be necessary afterwards, in particular: 96*4882a593Smuzhiyun 97*4882a593Smuzhiyun* where non-recipe files (such as patches) are overwritten (the flatten 98*4882a593Smuzhiyun command will show a warning for these) 99*4882a593Smuzhiyun* where anything beyond the normal layer setup has been added to 100*4882a593Smuzhiyun layer.conf (only the lowest priority number layer's layer.conf is used) 101*4882a593Smuzhiyun* overridden/appended items from bbappends will need to be tidied up 102*4882a593Smuzhiyun* when the flattened layers do not have the same directory structure (the 103*4882a593Smuzhiyun flatten command should show a warning when this will cause a problem) 104*4882a593Smuzhiyun 105*4882a593SmuzhiyunWarning: if you flatten several layers where another layer is intended to 106*4882a593Smuzhiyunbe used "inbetween" them (in layer priority order) such that recipes / 107*4882a593Smuzhiyunbbappends in the layers interact, and then attempt to use the new output 108*4882a593Smuzhiyunlayer together with that other layer, you may no longer get the same 109*4882a593Smuzhiyunbuild results (as the layer priority order has effectively changed). 110*4882a593Smuzhiyun""" 111*4882a593Smuzhiyun if len(args.layer) == 1: 112*4882a593Smuzhiyun logger.error('If you specify layers to flatten you must specify at least two') 113*4882a593Smuzhiyun return 1 114*4882a593Smuzhiyun 115*4882a593Smuzhiyun outputdir = args.outputdir 116*4882a593Smuzhiyun if os.path.exists(outputdir) and os.listdir(outputdir): 117*4882a593Smuzhiyun logger.error('Directory %s exists and is non-empty, please clear it out first' % outputdir) 118*4882a593Smuzhiyun return 1 119*4882a593Smuzhiyun 120*4882a593Smuzhiyun layers = self.bblayers 121*4882a593Smuzhiyun if len(args.layer) > 2: 122*4882a593Smuzhiyun layernames = args.layer 123*4882a593Smuzhiyun found_layernames = [] 124*4882a593Smuzhiyun found_layerdirs = [] 125*4882a593Smuzhiyun for layerdir in layers: 126*4882a593Smuzhiyun layername = self.get_layer_name(layerdir) 127*4882a593Smuzhiyun if layername in layernames: 128*4882a593Smuzhiyun found_layerdirs.append(layerdir) 129*4882a593Smuzhiyun found_layernames.append(layername) 130*4882a593Smuzhiyun 131*4882a593Smuzhiyun for layername in layernames: 132*4882a593Smuzhiyun if not layername in found_layernames: 133*4882a593Smuzhiyun logger.error('Unable to find layer %s in current configuration, please run "%s show-layers" to list configured layers' % (layername, os.path.basename(sys.argv[0]))) 134*4882a593Smuzhiyun return 135*4882a593Smuzhiyun layers = found_layerdirs 136*4882a593Smuzhiyun else: 137*4882a593Smuzhiyun layernames = [] 138*4882a593Smuzhiyun 139*4882a593Smuzhiyun # Ensure a specified path matches our list of layers 140*4882a593Smuzhiyun def layer_path_match(path): 141*4882a593Smuzhiyun for layerdir in layers: 142*4882a593Smuzhiyun if path.startswith(os.path.join(layerdir, '')): 143*4882a593Smuzhiyun return layerdir 144*4882a593Smuzhiyun return None 145*4882a593Smuzhiyun 146*4882a593Smuzhiyun applied_appends = [] 147*4882a593Smuzhiyun for layer in layers: 148*4882a593Smuzhiyun overlayed = set() 149*4882a593Smuzhiyun for mc in self.tinfoil.cooker.multiconfigs: 150*4882a593Smuzhiyun for f in self.tinfoil.cooker.collections[mc].overlayed.keys(): 151*4882a593Smuzhiyun for of in self.tinfoil.cooker.collections[mc].overlayed[f]: 152*4882a593Smuzhiyun if of.startswith(layer): 153*4882a593Smuzhiyun overlayed.add(of) 154*4882a593Smuzhiyun 155*4882a593Smuzhiyun logger.plain('Copying files from %s...' % layer ) 156*4882a593Smuzhiyun for root, dirs, files in os.walk(layer): 157*4882a593Smuzhiyun if '.git' in dirs: 158*4882a593Smuzhiyun dirs.remove('.git') 159*4882a593Smuzhiyun if '.hg' in dirs: 160*4882a593Smuzhiyun dirs.remove('.hg') 161*4882a593Smuzhiyun 162*4882a593Smuzhiyun for f1 in files: 163*4882a593Smuzhiyun f1full = os.sep.join([root, f1]) 164*4882a593Smuzhiyun if f1full in overlayed: 165*4882a593Smuzhiyun logger.plain(' Skipping overlayed file %s' % f1full ) 166*4882a593Smuzhiyun else: 167*4882a593Smuzhiyun ext = os.path.splitext(f1)[1] 168*4882a593Smuzhiyun if ext != '.bbappend': 169*4882a593Smuzhiyun fdest = f1full[len(layer):] 170*4882a593Smuzhiyun fdest = os.path.normpath(os.sep.join([outputdir,fdest])) 171*4882a593Smuzhiyun bb.utils.mkdirhier(os.path.dirname(fdest)) 172*4882a593Smuzhiyun if os.path.exists(fdest): 173*4882a593Smuzhiyun if f1 == 'layer.conf' and root.endswith('/conf'): 174*4882a593Smuzhiyun logger.plain(' Skipping layer config file %s' % f1full ) 175*4882a593Smuzhiyun continue 176*4882a593Smuzhiyun else: 177*4882a593Smuzhiyun logger.warning('Overwriting file %s', fdest) 178*4882a593Smuzhiyun bb.utils.copyfile(f1full, fdest) 179*4882a593Smuzhiyun if ext == '.bb': 180*4882a593Smuzhiyun appends = set() 181*4882a593Smuzhiyun for mc in self.tinfoil.cooker.multiconfigs: 182*4882a593Smuzhiyun appends |= set(self.tinfoil.cooker.collections[mc].get_file_appends(f1full)) 183*4882a593Smuzhiyun for append in appends: 184*4882a593Smuzhiyun if layer_path_match(append): 185*4882a593Smuzhiyun logger.plain(' Applying append %s to %s' % (append, fdest)) 186*4882a593Smuzhiyun self.apply_append(append, fdest) 187*4882a593Smuzhiyun applied_appends.append(append) 188*4882a593Smuzhiyun 189*4882a593Smuzhiyun # Take care of when some layers are excluded and yet we have included bbappends for those recipes 190*4882a593Smuzhiyun bbappends = set() 191*4882a593Smuzhiyun for mc in self.tinfoil.cooker.multiconfigs: 192*4882a593Smuzhiyun bbappends |= set(self.tinfoil.cooker.collections[mc].bbappends) 193*4882a593Smuzhiyun 194*4882a593Smuzhiyun for b in bbappends: 195*4882a593Smuzhiyun (recipename, appendname) = b 196*4882a593Smuzhiyun if appendname not in applied_appends: 197*4882a593Smuzhiyun first_append = None 198*4882a593Smuzhiyun layer = layer_path_match(appendname) 199*4882a593Smuzhiyun if layer: 200*4882a593Smuzhiyun if first_append: 201*4882a593Smuzhiyun self.apply_append(appendname, first_append) 202*4882a593Smuzhiyun else: 203*4882a593Smuzhiyun fdest = appendname[len(layer):] 204*4882a593Smuzhiyun fdest = os.path.normpath(os.sep.join([outputdir,fdest])) 205*4882a593Smuzhiyun bb.utils.mkdirhier(os.path.dirname(fdest)) 206*4882a593Smuzhiyun bb.utils.copyfile(appendname, fdest) 207*4882a593Smuzhiyun first_append = fdest 208*4882a593Smuzhiyun 209*4882a593Smuzhiyun # Get the regex for the first layer in our list (which is where the conf/layer.conf file will 210*4882a593Smuzhiyun # have come from) 211*4882a593Smuzhiyun first_regex = None 212*4882a593Smuzhiyun layerdir = layers[0] 213*4882a593Smuzhiyun for layername, pattern, regex, _ in self.tinfoil.cooker.bbfile_config_priorities: 214*4882a593Smuzhiyun if regex.match(os.path.join(layerdir, 'test')): 215*4882a593Smuzhiyun first_regex = regex 216*4882a593Smuzhiyun break 217*4882a593Smuzhiyun 218*4882a593Smuzhiyun if first_regex: 219*4882a593Smuzhiyun # Find the BBFILES entries that match (which will have come from this conf/layer.conf file) 220*4882a593Smuzhiyun bbfiles = str(self.tinfoil.config_data.getVar('BBFILES')).split() 221*4882a593Smuzhiyun bbfiles_layer = [] 222*4882a593Smuzhiyun for item in bbfiles: 223*4882a593Smuzhiyun if first_regex.match(item): 224*4882a593Smuzhiyun newpath = os.path.join(outputdir, item[len(layerdir)+1:]) 225*4882a593Smuzhiyun bbfiles_layer.append(newpath) 226*4882a593Smuzhiyun 227*4882a593Smuzhiyun if bbfiles_layer: 228*4882a593Smuzhiyun # Check that all important layer files match BBFILES 229*4882a593Smuzhiyun for root, dirs, files in os.walk(outputdir): 230*4882a593Smuzhiyun for f1 in files: 231*4882a593Smuzhiyun ext = os.path.splitext(f1)[1] 232*4882a593Smuzhiyun if ext in ['.bb', '.bbappend']: 233*4882a593Smuzhiyun f1full = os.sep.join([root, f1]) 234*4882a593Smuzhiyun entry_found = False 235*4882a593Smuzhiyun for item in bbfiles_layer: 236*4882a593Smuzhiyun if fnmatch.fnmatch(f1full, item): 237*4882a593Smuzhiyun entry_found = True 238*4882a593Smuzhiyun break 239*4882a593Smuzhiyun if not entry_found: 240*4882a593Smuzhiyun logger.warning("File %s does not match the flattened layer's BBFILES setting, you may need to edit conf/layer.conf or move the file elsewhere" % f1full) 241*4882a593Smuzhiyun 242*4882a593Smuzhiyun def get_file_layer(self, filename): 243*4882a593Smuzhiyun layerdir = self.get_file_layerdir(filename) 244*4882a593Smuzhiyun if layerdir: 245*4882a593Smuzhiyun return self.get_layer_name(layerdir) 246*4882a593Smuzhiyun else: 247*4882a593Smuzhiyun return '?' 248*4882a593Smuzhiyun 249*4882a593Smuzhiyun def get_file_layerdir(self, filename): 250*4882a593Smuzhiyun layer = bb.utils.get_file_layer(filename, self.tinfoil.config_data) 251*4882a593Smuzhiyun return self.bbfile_collections.get(layer, None) 252*4882a593Smuzhiyun 253*4882a593Smuzhiyun def apply_append(self, appendname, recipename): 254*4882a593Smuzhiyun with open(appendname, 'r') as appendfile: 255*4882a593Smuzhiyun with open(recipename, 'a') as recipefile: 256*4882a593Smuzhiyun recipefile.write('\n') 257*4882a593Smuzhiyun recipefile.write('##### bbappended from %s #####\n' % self.get_file_layer(appendname)) 258*4882a593Smuzhiyun recipefile.writelines(appendfile.readlines()) 259*4882a593Smuzhiyun 260*4882a593Smuzhiyun def register_commands(self, sp): 261*4882a593Smuzhiyun parser_add_layer = self.add_command(sp, 'add-layer', self.do_add_layer, parserecipes=False) 262*4882a593Smuzhiyun parser_add_layer.add_argument('layerdir', nargs='+', help='Layer directory/directories to add') 263*4882a593Smuzhiyun 264*4882a593Smuzhiyun parser_remove_layer = self.add_command(sp, 'remove-layer', self.do_remove_layer, parserecipes=False) 265*4882a593Smuzhiyun parser_remove_layer.add_argument('layerdir', nargs='+', help='Layer directory/directories to remove (wildcards allowed, enclose in quotes to avoid shell expansion)') 266*4882a593Smuzhiyun parser_remove_layer.set_defaults(func=self.do_remove_layer) 267*4882a593Smuzhiyun 268*4882a593Smuzhiyun parser_flatten = self.add_command(sp, 'flatten', self.do_flatten) 269*4882a593Smuzhiyun parser_flatten.add_argument('layer', nargs='*', help='Optional layer(s) to flatten (otherwise all are flattened)') 270*4882a593Smuzhiyun parser_flatten.add_argument('outputdir', help='Output directory') 271