1*4882a593Smuzhiyun# Copyright (C) 2016-2018 Wind River Systems, Inc. 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun 6*4882a593Smuzhiyunimport logging 7*4882a593Smuzhiyunimport json 8*4882a593Smuzhiyunimport os 9*4882a593Smuzhiyun 10*4882a593Smuzhiyunfrom urllib.parse import unquote 11*4882a593Smuzhiyunfrom urllib.parse import urlparse 12*4882a593Smuzhiyun 13*4882a593Smuzhiyunimport bb 14*4882a593Smuzhiyun 15*4882a593Smuzhiyunimport layerindexlib 16*4882a593Smuzhiyunimport layerindexlib.plugin 17*4882a593Smuzhiyun 18*4882a593Smuzhiyunlogger = logging.getLogger('BitBake.layerindexlib.restapi') 19*4882a593Smuzhiyun 20*4882a593Smuzhiyundef plugin_init(plugins): 21*4882a593Smuzhiyun return RestApiPlugin() 22*4882a593Smuzhiyun 23*4882a593Smuzhiyunclass RestApiPlugin(layerindexlib.plugin.IndexPlugin): 24*4882a593Smuzhiyun def __init__(self): 25*4882a593Smuzhiyun self.type = "restapi" 26*4882a593Smuzhiyun 27*4882a593Smuzhiyun def load_index(self, url, load): 28*4882a593Smuzhiyun """ 29*4882a593Smuzhiyun Fetches layer information from a local or remote layer index. 30*4882a593Smuzhiyun 31*4882a593Smuzhiyun The return value is a LayerIndexObj. 32*4882a593Smuzhiyun 33*4882a593Smuzhiyun url is the url to the rest api of the layer index, such as: 34*4882a593Smuzhiyun https://layers.openembedded.org/layerindex/api/ 35*4882a593Smuzhiyun 36*4882a593Smuzhiyun Or a local file... 37*4882a593Smuzhiyun """ 38*4882a593Smuzhiyun 39*4882a593Smuzhiyun up = urlparse(url) 40*4882a593Smuzhiyun 41*4882a593Smuzhiyun if up.scheme == 'file': 42*4882a593Smuzhiyun return self.load_index_file(up, url, load) 43*4882a593Smuzhiyun 44*4882a593Smuzhiyun if up.scheme == 'http' or up.scheme == 'https': 45*4882a593Smuzhiyun return self.load_index_web(up, url, load) 46*4882a593Smuzhiyun 47*4882a593Smuzhiyun raise layerindexlib.plugin.LayerIndexPluginUrlError(self.type, url) 48*4882a593Smuzhiyun 49*4882a593Smuzhiyun 50*4882a593Smuzhiyun def load_index_file(self, up, url, load): 51*4882a593Smuzhiyun """ 52*4882a593Smuzhiyun Fetches layer information from a local file or directory. 53*4882a593Smuzhiyun 54*4882a593Smuzhiyun The return value is a LayerIndexObj. 55*4882a593Smuzhiyun 56*4882a593Smuzhiyun ud is the parsed url to the local file or directory. 57*4882a593Smuzhiyun """ 58*4882a593Smuzhiyun if not os.path.exists(up.path): 59*4882a593Smuzhiyun raise FileNotFoundError(up.path) 60*4882a593Smuzhiyun 61*4882a593Smuzhiyun index = layerindexlib.LayerIndexObj() 62*4882a593Smuzhiyun 63*4882a593Smuzhiyun index.config = {} 64*4882a593Smuzhiyun index.config['TYPE'] = self.type 65*4882a593Smuzhiyun index.config['URL'] = url 66*4882a593Smuzhiyun 67*4882a593Smuzhiyun params = self.layerindex._parse_params(up.params) 68*4882a593Smuzhiyun 69*4882a593Smuzhiyun if 'desc' in params: 70*4882a593Smuzhiyun index.config['DESCRIPTION'] = unquote(params['desc']) 71*4882a593Smuzhiyun else: 72*4882a593Smuzhiyun index.config['DESCRIPTION'] = up.path 73*4882a593Smuzhiyun 74*4882a593Smuzhiyun if 'cache' in params: 75*4882a593Smuzhiyun index.config['CACHE'] = params['cache'] 76*4882a593Smuzhiyun 77*4882a593Smuzhiyun if 'branch' in params: 78*4882a593Smuzhiyun branches = params['branch'].split(',') 79*4882a593Smuzhiyun index.config['BRANCH'] = branches 80*4882a593Smuzhiyun else: 81*4882a593Smuzhiyun branches = ['*'] 82*4882a593Smuzhiyun 83*4882a593Smuzhiyun 84*4882a593Smuzhiyun def load_cache(path, index, branches=[]): 85*4882a593Smuzhiyun logger.debug('Loading json file %s' % path) 86*4882a593Smuzhiyun with open(path, 'rt', encoding='utf-8') as f: 87*4882a593Smuzhiyun pindex = json.load(f) 88*4882a593Smuzhiyun 89*4882a593Smuzhiyun # Filter the branches on loaded files... 90*4882a593Smuzhiyun newpBranch = [] 91*4882a593Smuzhiyun for branch in branches: 92*4882a593Smuzhiyun if branch != '*': 93*4882a593Smuzhiyun if 'branches' in pindex: 94*4882a593Smuzhiyun for br in pindex['branches']: 95*4882a593Smuzhiyun if br['name'] == branch: 96*4882a593Smuzhiyun newpBranch.append(br) 97*4882a593Smuzhiyun else: 98*4882a593Smuzhiyun if 'branches' in pindex: 99*4882a593Smuzhiyun for br in pindex['branches']: 100*4882a593Smuzhiyun newpBranch.append(br) 101*4882a593Smuzhiyun 102*4882a593Smuzhiyun if newpBranch: 103*4882a593Smuzhiyun index.add_raw_element('branches', layerindexlib.Branch, newpBranch) 104*4882a593Smuzhiyun else: 105*4882a593Smuzhiyun logger.debug('No matching branches (%s) in index file(s)' % branches) 106*4882a593Smuzhiyun # No matching branches.. return nothing... 107*4882a593Smuzhiyun return 108*4882a593Smuzhiyun 109*4882a593Smuzhiyun for (lName, lType) in [("layerItems", layerindexlib.LayerItem), 110*4882a593Smuzhiyun ("layerBranches", layerindexlib.LayerBranch), 111*4882a593Smuzhiyun ("layerDependencies", layerindexlib.LayerDependency), 112*4882a593Smuzhiyun ("recipes", layerindexlib.Recipe), 113*4882a593Smuzhiyun ("machines", layerindexlib.Machine), 114*4882a593Smuzhiyun ("distros", layerindexlib.Distro)]: 115*4882a593Smuzhiyun if lName in pindex: 116*4882a593Smuzhiyun index.add_raw_element(lName, lType, pindex[lName]) 117*4882a593Smuzhiyun 118*4882a593Smuzhiyun 119*4882a593Smuzhiyun if not os.path.isdir(up.path): 120*4882a593Smuzhiyun load_cache(up.path, index, branches) 121*4882a593Smuzhiyun return index 122*4882a593Smuzhiyun 123*4882a593Smuzhiyun logger.debug('Loading from dir %s...' % (up.path)) 124*4882a593Smuzhiyun for (dirpath, _, filenames) in os.walk(up.path): 125*4882a593Smuzhiyun for filename in filenames: 126*4882a593Smuzhiyun if not filename.endswith('.json'): 127*4882a593Smuzhiyun continue 128*4882a593Smuzhiyun fpath = os.path.join(dirpath, filename) 129*4882a593Smuzhiyun load_cache(fpath, index, branches) 130*4882a593Smuzhiyun 131*4882a593Smuzhiyun return index 132*4882a593Smuzhiyun 133*4882a593Smuzhiyun 134*4882a593Smuzhiyun def load_index_web(self, up, url, load): 135*4882a593Smuzhiyun """ 136*4882a593Smuzhiyun Fetches layer information from a remote layer index. 137*4882a593Smuzhiyun 138*4882a593Smuzhiyun The return value is a LayerIndexObj. 139*4882a593Smuzhiyun 140*4882a593Smuzhiyun ud is the parsed url to the rest api of the layer index, such as: 141*4882a593Smuzhiyun https://layers.openembedded.org/layerindex/api/ 142*4882a593Smuzhiyun """ 143*4882a593Smuzhiyun 144*4882a593Smuzhiyun def _get_json_response(apiurl=None, username=None, password=None, retry=True): 145*4882a593Smuzhiyun assert apiurl is not None 146*4882a593Smuzhiyun 147*4882a593Smuzhiyun logger.debug("fetching %s" % apiurl) 148*4882a593Smuzhiyun 149*4882a593Smuzhiyun up = urlparse(apiurl) 150*4882a593Smuzhiyun 151*4882a593Smuzhiyun username=up.username 152*4882a593Smuzhiyun password=up.password 153*4882a593Smuzhiyun 154*4882a593Smuzhiyun # Strip username/password and params 155*4882a593Smuzhiyun if up.port: 156*4882a593Smuzhiyun up_stripped = up._replace(params="", netloc="%s:%s" % (up.hostname, up.port)) 157*4882a593Smuzhiyun else: 158*4882a593Smuzhiyun up_stripped = up._replace(params="", netloc=up.hostname) 159*4882a593Smuzhiyun 160*4882a593Smuzhiyun res = self.layerindex._fetch_url(up_stripped.geturl(), username=username, password=password) 161*4882a593Smuzhiyun 162*4882a593Smuzhiyun try: 163*4882a593Smuzhiyun parsed = json.loads(res.read().decode('utf-8')) 164*4882a593Smuzhiyun except ConnectionResetError: 165*4882a593Smuzhiyun if retry: 166*4882a593Smuzhiyun logger.debug("%s: Connection reset by peer. Retrying..." % url) 167*4882a593Smuzhiyun parsed = _get_json_response(apiurl=up_stripped.geturl(), username=username, password=password, retry=False) 168*4882a593Smuzhiyun logger.debug("%s: retry successful.") 169*4882a593Smuzhiyun else: 170*4882a593Smuzhiyun raise layerindexlib.LayerIndexFetchError('%s: Connection reset by peer. Is there a firewall blocking your connection?' % apiurl) 171*4882a593Smuzhiyun 172*4882a593Smuzhiyun return parsed 173*4882a593Smuzhiyun 174*4882a593Smuzhiyun index = layerindexlib.LayerIndexObj() 175*4882a593Smuzhiyun 176*4882a593Smuzhiyun index.config = {} 177*4882a593Smuzhiyun index.config['TYPE'] = self.type 178*4882a593Smuzhiyun index.config['URL'] = url 179*4882a593Smuzhiyun 180*4882a593Smuzhiyun params = self.layerindex._parse_params(up.params) 181*4882a593Smuzhiyun 182*4882a593Smuzhiyun if 'desc' in params: 183*4882a593Smuzhiyun index.config['DESCRIPTION'] = unquote(params['desc']) 184*4882a593Smuzhiyun else: 185*4882a593Smuzhiyun index.config['DESCRIPTION'] = up.hostname 186*4882a593Smuzhiyun 187*4882a593Smuzhiyun if 'cache' in params: 188*4882a593Smuzhiyun index.config['CACHE'] = params['cache'] 189*4882a593Smuzhiyun 190*4882a593Smuzhiyun if 'branch' in params: 191*4882a593Smuzhiyun branches = params['branch'].split(',') 192*4882a593Smuzhiyun index.config['BRANCH'] = branches 193*4882a593Smuzhiyun else: 194*4882a593Smuzhiyun branches = ['*'] 195*4882a593Smuzhiyun 196*4882a593Smuzhiyun try: 197*4882a593Smuzhiyun index.apilinks = _get_json_response(apiurl=url, username=up.username, password=up.password) 198*4882a593Smuzhiyun except Exception as e: 199*4882a593Smuzhiyun raise layerindexlib.LayerIndexFetchError(url, e) 200*4882a593Smuzhiyun 201*4882a593Smuzhiyun # Local raw index set... 202*4882a593Smuzhiyun pindex = {} 203*4882a593Smuzhiyun 204*4882a593Smuzhiyun # Load all the requested branches at the same time time, 205*4882a593Smuzhiyun # a special branch of '*' means load all branches 206*4882a593Smuzhiyun filter = "" 207*4882a593Smuzhiyun if "*" not in branches: 208*4882a593Smuzhiyun filter = "?filter=name:%s" % "OR".join(branches) 209*4882a593Smuzhiyun 210*4882a593Smuzhiyun logger.debug("Loading %s from %s" % (branches, index.apilinks['branches'])) 211*4882a593Smuzhiyun 212*4882a593Smuzhiyun # The link won't include username/password, so pull it from the original url 213*4882a593Smuzhiyun pindex['branches'] = _get_json_response(index.apilinks['branches'] + filter, 214*4882a593Smuzhiyun username=up.username, password=up.password) 215*4882a593Smuzhiyun if not pindex['branches']: 216*4882a593Smuzhiyun logger.debug("No valid branches (%s) found at url %s." % (branch, url)) 217*4882a593Smuzhiyun return index 218*4882a593Smuzhiyun index.add_raw_element("branches", layerindexlib.Branch, pindex['branches']) 219*4882a593Smuzhiyun 220*4882a593Smuzhiyun # Load all of the layerItems (these can not be easily filtered) 221*4882a593Smuzhiyun logger.debug("Loading %s from %s" % ('layerItems', index.apilinks['layerItems'])) 222*4882a593Smuzhiyun 223*4882a593Smuzhiyun 224*4882a593Smuzhiyun # The link won't include username/password, so pull it from the original url 225*4882a593Smuzhiyun pindex['layerItems'] = _get_json_response(index.apilinks['layerItems'], 226*4882a593Smuzhiyun username=up.username, password=up.password) 227*4882a593Smuzhiyun if not pindex['layerItems']: 228*4882a593Smuzhiyun logger.debug("No layers were found at url %s." % (url)) 229*4882a593Smuzhiyun return index 230*4882a593Smuzhiyun index.add_raw_element("layerItems", layerindexlib.LayerItem, pindex['layerItems']) 231*4882a593Smuzhiyun 232*4882a593Smuzhiyun 233*4882a593Smuzhiyun # From this point on load the contents for each branch. Otherwise we 234*4882a593Smuzhiyun # could run into a timeout. 235*4882a593Smuzhiyun for branch in index.branches: 236*4882a593Smuzhiyun filter = "?filter=branch__name:%s" % index.branches[branch].name 237*4882a593Smuzhiyun 238*4882a593Smuzhiyun logger.debug("Loading %s from %s" % ('layerBranches', index.apilinks['layerBranches'])) 239*4882a593Smuzhiyun 240*4882a593Smuzhiyun # The link won't include username/password, so pull it from the original url 241*4882a593Smuzhiyun pindex['layerBranches'] = _get_json_response(index.apilinks['layerBranches'] + filter, 242*4882a593Smuzhiyun username=up.username, password=up.password) 243*4882a593Smuzhiyun if not pindex['layerBranches']: 244*4882a593Smuzhiyun logger.debug("No valid layer branches (%s) found at url %s." % (branches or "*", url)) 245*4882a593Smuzhiyun return index 246*4882a593Smuzhiyun index.add_raw_element("layerBranches", layerindexlib.LayerBranch, pindex['layerBranches']) 247*4882a593Smuzhiyun 248*4882a593Smuzhiyun 249*4882a593Smuzhiyun # Load the rest, they all have a similar format 250*4882a593Smuzhiyun # Note: the layer index has a few more items, we can add them if necessary 251*4882a593Smuzhiyun # in the future. 252*4882a593Smuzhiyun filter = "?filter=layerbranch__branch__name:%s" % index.branches[branch].name 253*4882a593Smuzhiyun for (lName, lType) in [("layerDependencies", layerindexlib.LayerDependency), 254*4882a593Smuzhiyun ("recipes", layerindexlib.Recipe), 255*4882a593Smuzhiyun ("machines", layerindexlib.Machine), 256*4882a593Smuzhiyun ("distros", layerindexlib.Distro)]: 257*4882a593Smuzhiyun if lName not in load: 258*4882a593Smuzhiyun continue 259*4882a593Smuzhiyun logger.debug("Loading %s from %s" % (lName, index.apilinks[lName])) 260*4882a593Smuzhiyun 261*4882a593Smuzhiyun # The link won't include username/password, so pull it from the original url 262*4882a593Smuzhiyun pindex[lName] = _get_json_response(index.apilinks[lName] + filter, 263*4882a593Smuzhiyun username=up.username, password=up.password) 264*4882a593Smuzhiyun index.add_raw_element(lName, lType, pindex[lName]) 265*4882a593Smuzhiyun 266*4882a593Smuzhiyun return index 267*4882a593Smuzhiyun 268*4882a593Smuzhiyun def store_index(self, url, index): 269*4882a593Smuzhiyun """ 270*4882a593Smuzhiyun Store layer information into a local file/dir. 271*4882a593Smuzhiyun 272*4882a593Smuzhiyun The return value is a dictionary containing API, 273*4882a593Smuzhiyun layer, branch, dependency, recipe, machine, distro, information. 274*4882a593Smuzhiyun 275*4882a593Smuzhiyun ud is a parsed url to a directory or file. If the path is a 276*4882a593Smuzhiyun directory, we will split the files into one file per layer. 277*4882a593Smuzhiyun If the path is to a file (exists or not) the entire DB will be 278*4882a593Smuzhiyun dumped into that one file. 279*4882a593Smuzhiyun """ 280*4882a593Smuzhiyun 281*4882a593Smuzhiyun up = urlparse(url) 282*4882a593Smuzhiyun 283*4882a593Smuzhiyun if up.scheme != 'file': 284*4882a593Smuzhiyun raise layerindexlib.plugin.LayerIndexPluginUrlError(self.type, url) 285*4882a593Smuzhiyun 286*4882a593Smuzhiyun logger.debug("Storing to %s..." % up.path) 287*4882a593Smuzhiyun 288*4882a593Smuzhiyun try: 289*4882a593Smuzhiyun layerbranches = index.layerBranches 290*4882a593Smuzhiyun except KeyError: 291*4882a593Smuzhiyun logger.error('No layerBranches to write.') 292*4882a593Smuzhiyun return 293*4882a593Smuzhiyun 294*4882a593Smuzhiyun 295*4882a593Smuzhiyun def filter_item(layerbranchid, objects): 296*4882a593Smuzhiyun filtered = [] 297*4882a593Smuzhiyun for obj in getattr(index, objects, None): 298*4882a593Smuzhiyun try: 299*4882a593Smuzhiyun if getattr(index, objects)[obj].layerbranch_id == layerbranchid: 300*4882a593Smuzhiyun filtered.append(getattr(index, objects)[obj]._data) 301*4882a593Smuzhiyun except AttributeError: 302*4882a593Smuzhiyun logger.debug('No obj.layerbranch_id: %s' % objects) 303*4882a593Smuzhiyun # No simple filter method, just include it... 304*4882a593Smuzhiyun try: 305*4882a593Smuzhiyun filtered.append(getattr(index, objects)[obj]._data) 306*4882a593Smuzhiyun except AttributeError: 307*4882a593Smuzhiyun logger.debug('No obj._data: %s %s' % (objects, type(obj))) 308*4882a593Smuzhiyun filtered.append(obj) 309*4882a593Smuzhiyun return filtered 310*4882a593Smuzhiyun 311*4882a593Smuzhiyun 312*4882a593Smuzhiyun # Write out to a single file. 313*4882a593Smuzhiyun # Filter out unnecessary items, then sort as we write for determinism 314*4882a593Smuzhiyun if not os.path.isdir(up.path): 315*4882a593Smuzhiyun pindex = {} 316*4882a593Smuzhiyun 317*4882a593Smuzhiyun pindex['branches'] = [] 318*4882a593Smuzhiyun pindex['layerItems'] = [] 319*4882a593Smuzhiyun pindex['layerBranches'] = [] 320*4882a593Smuzhiyun 321*4882a593Smuzhiyun for layerbranchid in layerbranches: 322*4882a593Smuzhiyun if layerbranches[layerbranchid].branch._data not in pindex['branches']: 323*4882a593Smuzhiyun pindex['branches'].append(layerbranches[layerbranchid].branch._data) 324*4882a593Smuzhiyun 325*4882a593Smuzhiyun if layerbranches[layerbranchid].layer._data not in pindex['layerItems']: 326*4882a593Smuzhiyun pindex['layerItems'].append(layerbranches[layerbranchid].layer._data) 327*4882a593Smuzhiyun 328*4882a593Smuzhiyun if layerbranches[layerbranchid]._data not in pindex['layerBranches']: 329*4882a593Smuzhiyun pindex['layerBranches'].append(layerbranches[layerbranchid]._data) 330*4882a593Smuzhiyun 331*4882a593Smuzhiyun for entry in index._index: 332*4882a593Smuzhiyun # Skip local items, apilinks and items already processed 333*4882a593Smuzhiyun if entry in index.config['local'] or \ 334*4882a593Smuzhiyun entry == 'apilinks' or \ 335*4882a593Smuzhiyun entry == 'branches' or \ 336*4882a593Smuzhiyun entry == 'layerBranches' or \ 337*4882a593Smuzhiyun entry == 'layerItems': 338*4882a593Smuzhiyun continue 339*4882a593Smuzhiyun if entry not in pindex: 340*4882a593Smuzhiyun pindex[entry] = [] 341*4882a593Smuzhiyun pindex[entry].extend(filter_item(layerbranchid, entry)) 342*4882a593Smuzhiyun 343*4882a593Smuzhiyun bb.debug(1, 'Writing index to %s' % up.path) 344*4882a593Smuzhiyun with open(up.path, 'wt') as f: 345*4882a593Smuzhiyun json.dump(layerindexlib.sort_entry(pindex), f, indent=4) 346*4882a593Smuzhiyun return 347*4882a593Smuzhiyun 348*4882a593Smuzhiyun 349*4882a593Smuzhiyun # Write out to a directory one file per layerBranch 350*4882a593Smuzhiyun # Prepare all layer related items, to create a minimal file. 351*4882a593Smuzhiyun # We have to sort the entries as we write so they are deterministic 352*4882a593Smuzhiyun for layerbranchid in layerbranches: 353*4882a593Smuzhiyun pindex = {} 354*4882a593Smuzhiyun 355*4882a593Smuzhiyun for entry in index._index: 356*4882a593Smuzhiyun # Skip local items, apilinks and items already processed 357*4882a593Smuzhiyun if entry in index.config['local'] or \ 358*4882a593Smuzhiyun entry == 'apilinks' or \ 359*4882a593Smuzhiyun entry == 'branches' or \ 360*4882a593Smuzhiyun entry == 'layerBranches' or \ 361*4882a593Smuzhiyun entry == 'layerItems': 362*4882a593Smuzhiyun continue 363*4882a593Smuzhiyun pindex[entry] = filter_item(layerbranchid, entry) 364*4882a593Smuzhiyun 365*4882a593Smuzhiyun # Add the layer we're processing as the first one... 366*4882a593Smuzhiyun pindex['branches'] = [layerbranches[layerbranchid].branch._data] 367*4882a593Smuzhiyun pindex['layerItems'] = [layerbranches[layerbranchid].layer._data] 368*4882a593Smuzhiyun pindex['layerBranches'] = [layerbranches[layerbranchid]._data] 369*4882a593Smuzhiyun 370*4882a593Smuzhiyun # We also need to include the layerbranch for any dependencies... 371*4882a593Smuzhiyun for layerdep in pindex['layerDependencies']: 372*4882a593Smuzhiyun layerdependency = layerindexlib.LayerDependency(index, layerdep) 373*4882a593Smuzhiyun 374*4882a593Smuzhiyun layeritem = layerdependency.dependency 375*4882a593Smuzhiyun layerbranch = layerdependency.dependency_layerBranch 376*4882a593Smuzhiyun 377*4882a593Smuzhiyun # We need to avoid duplicates... 378*4882a593Smuzhiyun if layeritem._data not in pindex['layerItems']: 379*4882a593Smuzhiyun pindex['layerItems'].append(layeritem._data) 380*4882a593Smuzhiyun 381*4882a593Smuzhiyun if layerbranch._data not in pindex['layerBranches']: 382*4882a593Smuzhiyun pindex['layerBranches'].append(layerbranch._data) 383*4882a593Smuzhiyun 384*4882a593Smuzhiyun # apply mirroring adjustments here.... 385*4882a593Smuzhiyun 386*4882a593Smuzhiyun fname = index.config['DESCRIPTION'] + '__' + pindex['branches'][0]['name'] + '__' + pindex['layerItems'][0]['name'] 387*4882a593Smuzhiyun fname = fname.translate(str.maketrans('/ ', '__')) 388*4882a593Smuzhiyun fpath = os.path.join(up.path, fname) 389*4882a593Smuzhiyun 390*4882a593Smuzhiyun bb.debug(1, 'Writing index to %s' % fpath + '.json') 391*4882a593Smuzhiyun with open(fpath + '.json', 'wt') as f: 392*4882a593Smuzhiyun json.dump(layerindexlib.sort_entry(pindex), f, indent=4) 393