1#
2# SPDX-License-Identifier: GPL-2.0-only
3#
4
5import layerindexlib
6
7import argparse
8import logging
9import os
10import subprocess
11
12from bblayers.action import ActionPlugin
13
14logger = logging.getLogger('bitbake-layers')
15
16
17def plugin_init(plugins):
18    return LayerIndexPlugin()
19
20
21class LayerIndexPlugin(ActionPlugin):
22    """Subcommands for interacting with the layer index.
23
24    This class inherits ActionPlugin to get do_add_layer.
25    """
26
27    def get_fetch_layer(self, fetchdir, url, subdir, fetch_layer, branch, shallow=False):
28        layername = self.get_layer_name(url)
29        if os.path.splitext(layername)[1] == '.git':
30            layername = os.path.splitext(layername)[0]
31        repodir = os.path.join(fetchdir, layername)
32        layerdir = os.path.join(repodir, subdir)
33        if not os.path.exists(repodir):
34            if fetch_layer:
35                cmd = ['git', 'clone']
36                if shallow:
37                    cmd.extend(['--depth', '1'])
38                if branch:
39                    cmd.extend(['-b' , branch])
40                cmd.extend([url, repodir])
41                result = subprocess.call(cmd)
42                if result:
43                    logger.error("Failed to download %s (%s)" % (url, branch))
44                    return None, None, None
45                else:
46                    return subdir, layername, layerdir
47            else:
48                logger.plain("Repository %s needs to be fetched" % url)
49                return subdir, layername, layerdir
50        elif os.path.exists(layerdir):
51            return subdir, layername, layerdir
52        else:
53            logger.error("%s is not in %s" % (url, subdir))
54        return None, None, None
55
56    def do_layerindex_fetch(self, args):
57        """Fetches a layer from a layer index along with its dependent layers, and adds them to conf/bblayers.conf.
58"""
59
60        def _construct_url(baseurls, branches):
61            urls = []
62            for baseurl in baseurls:
63                if baseurl[-1] != '/':
64                    baseurl += '/'
65
66                if not baseurl.startswith('cooker'):
67                    baseurl += "api/"
68
69                if branches:
70                    baseurl += ";branch=%s" % ','.join(branches)
71
72                urls.append(baseurl)
73
74            return urls
75
76
77        # Set the default...
78        if args.branch:
79            branches = [args.branch]
80        else:
81            branches = (self.tinfoil.config_data.getVar('LAYERSERIES_CORENAMES') or 'master').split()
82        logger.debug('Trying branches: %s' % branches)
83
84        ignore_layers = []
85        if args.ignore:
86            ignore_layers.extend(args.ignore.split(','))
87
88        # Load the cooker DB
89        cookerIndex = layerindexlib.LayerIndex(self.tinfoil.config_data)
90        cookerIndex.load_layerindex('cooker://', load='layerDependencies')
91
92        # Fast path, check if we already have what has been requested!
93        (dependencies, invalidnames) = cookerIndex.find_dependencies(names=args.layername, ignores=ignore_layers)
94        if not args.show_only and not invalidnames:
95            logger.plain("You already have the requested layer(s): %s" % args.layername)
96            return 0
97
98        # The information to show is already in the cookerIndex
99        if invalidnames:
100            # General URL to use to access the layer index
101            # While there is ONE right now, we're expect users could enter several
102            apiurl = self.tinfoil.config_data.getVar('BBLAYERS_LAYERINDEX_URL').split()
103            if not apiurl:
104                logger.error("Cannot get BBLAYERS_LAYERINDEX_URL")
105                return 1
106
107            remoteIndex = layerindexlib.LayerIndex(self.tinfoil.config_data)
108
109            for remoteurl in _construct_url(apiurl, branches):
110                logger.plain("Loading %s..." % remoteurl)
111                remoteIndex.load_layerindex(remoteurl)
112
113            if remoteIndex.is_empty():
114                logger.error("Remote layer index %s is empty for branches %s" % (apiurl, branches))
115                return 1
116
117            lIndex = cookerIndex + remoteIndex
118
119            (dependencies, invalidnames) = lIndex.find_dependencies(names=args.layername, ignores=ignore_layers)
120
121            if invalidnames:
122                for invaluename in invalidnames:
123                    logger.error('Layer "%s" not found in layer index' % invaluename)
124                return 1
125
126        logger.plain("%s  %s  %s" % ("Layer".ljust(49), "Git repository (branch)".ljust(54), "Subdirectory"))
127        logger.plain('=' * 125)
128
129        for deplayerbranch in dependencies:
130            layerBranch = dependencies[deplayerbranch][0]
131
132            # TODO: Determine display behavior
133            # This is the local content, uncomment to hide local
134            # layers from the display.
135            #if layerBranch.index.config['TYPE'] == 'cooker':
136            #    continue
137
138            layerDeps = dependencies[deplayerbranch][1:]
139
140            requiredby = []
141            recommendedby = []
142            for dep in layerDeps:
143                if dep.required:
144                    requiredby.append(dep.layer.name)
145                else:
146                    recommendedby.append(dep.layer.name)
147
148            logger.plain('%s %s %s' % (("%s:%s:%s" %
149                                  (layerBranch.index.config['DESCRIPTION'],
150                                  layerBranch.branch.name,
151                                  layerBranch.layer.name)).ljust(50),
152                                  ("%s (%s)" % (layerBranch.layer.vcs_url,
153                                  layerBranch.actual_branch)).ljust(55),
154                                  layerBranch.vcs_subdir
155                                               ))
156            if requiredby:
157                logger.plain('  required by: %s' % ' '.join(requiredby))
158            if recommendedby:
159                logger.plain('  recommended by: %s' % ' '.join(recommendedby))
160
161        if dependencies:
162            if args.fetchdir:
163                fetchdir = args.fetchdir
164            else:
165                fetchdir = self.tinfoil.config_data.getVar('BBLAYERS_FETCH_DIR')
166                if not fetchdir:
167                    logger.error("Cannot get BBLAYERS_FETCH_DIR")
168                    return 1
169
170            if not os.path.exists(fetchdir):
171                os.makedirs(fetchdir)
172
173            addlayers = []
174
175            for deplayerbranch in dependencies:
176                layerBranch = dependencies[deplayerbranch][0]
177
178                if layerBranch.index.config['TYPE'] == 'cooker':
179                    # Anything loaded via cooker is already local, skip it
180                    continue
181
182                subdir, name, layerdir = self.get_fetch_layer(fetchdir,
183                                                      layerBranch.layer.vcs_url,
184                                                      layerBranch.vcs_subdir,
185                                                      not args.show_only,
186                                                      layerBranch.actual_branch,
187                                                      args.shallow)
188                if not name:
189                    # Error already shown
190                    return 1
191                addlayers.append((subdir, name, layerdir))
192        if not args.show_only:
193            localargs = argparse.Namespace()
194            localargs.layerdir = []
195            localargs.force = args.force
196            for subdir, name, layerdir in addlayers:
197                if os.path.exists(layerdir):
198                    if subdir:
199                        logger.plain("Adding layer \"%s\" (%s) to conf/bblayers.conf" % (subdir, layerdir))
200                    else:
201                        logger.plain("Adding layer \"%s\" (%s) to conf/bblayers.conf" % (name, layerdir))
202                    localargs.layerdir.append(layerdir)
203                else:
204                    break
205
206            if localargs.layerdir:
207                self.do_add_layer(localargs)
208
209    def do_layerindex_show_depends(self, args):
210        """Find layer dependencies from layer index.
211"""
212        args.show_only = True
213        args.ignore = []
214        args.fetchdir = ""
215        args.shallow = True
216        self.do_layerindex_fetch(args)
217
218    def register_commands(self, sp):
219        parser_layerindex_fetch = self.add_command(sp, 'layerindex-fetch', self.do_layerindex_fetch, parserecipes=False)
220        parser_layerindex_fetch.add_argument('-n', '--show-only', help='show dependencies and do nothing else', action='store_true')
221        parser_layerindex_fetch.add_argument('-b', '--branch', help='branch name to fetch')
222        parser_layerindex_fetch.add_argument('-s', '--shallow', help='do only shallow clones (--depth=1)', action='store_true')
223        parser_layerindex_fetch.add_argument('-i', '--ignore', help='assume the specified layers do not need to be fetched/added (separate multiple layers with commas, no spaces)', metavar='LAYER')
224        parser_layerindex_fetch.add_argument('-f', '--fetchdir', help='directory to fetch the layer(s) into (will be created if it does not exist)')
225        parser_layerindex_fetch.add_argument('layername', nargs='+', help='layer to fetch')
226
227        parser_layerindex_show_depends = self.add_command(sp, 'layerindex-show-depends', self.do_layerindex_show_depends, parserecipes=False)
228        parser_layerindex_show_depends.add_argument('-b', '--branch', help='branch name to fetch')
229        parser_layerindex_show_depends.add_argument('layername', nargs='+', help='layer to query')
230