File indexing completed on 2024-04-21 03:52:27

0001 # -*- coding: utf-8 -*-
0002 #
0003 # SPDX-FileCopyrightText: 2016 Olivier Churlaud <olivier@churlaud.com>
0004 # SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kdemail.net>
0005 # SPDX-FileCopyrightText: 2014 Aurélien Gâteau <agateau@kde.org>
0006 # SPDX-FileCopyrightText: 2014 Alex Turbov <i.zaufi@gmail.com>
0007 #
0008 # SPDX-License-Identifier: BSD-2-Clause
0009 
0010 import logging
0011 import os
0012 import sys
0013 from typing import Any, Dict, Optional
0014 
0015 from urllib.request import Request, urlopen
0016 from urllib.error import HTTPError
0017 
0018 import yaml
0019 
0020 from kapidox import utils
0021 from kapidox.models import Library, Product
0022 
0023 __all__ = (
0024     "create_metainfo",
0025     "parse_tree")
0026 
0027 PLATFORM_ALL = "All"
0028 PLATFORM_UNKNOWN = "UNKNOWN"
0029 
0030 
0031 ## @package kapidox.preprocessing
0032 #
0033 # Preprocessing of the needed information.
0034 #
0035 # The module allow to walk through folders, read metainfo files and create
0036 # products, subgroups and libraries representing the projects.
0037 #
0038 
0039 def expand_platform_all(dct, available_platforms):
0040     """If one of the keys of dct is `PLATFORM_ALL` (or `PLATFORM_UNKNOWN`),
0041     remove it and add entries for all available platforms to dct
0042 
0043     Args:
0044         dct: (dictionary) dictionary to expand
0045         available_platforms: (list of string) name of platforms
0046     """
0047 
0048     add_all_platforms = False
0049     if PLATFORM_ALL in dct:
0050         note = dct[PLATFORM_ALL]
0051         add_all_platforms = True
0052         del dct[PLATFORM_ALL]
0053     if PLATFORM_UNKNOWN in dct:
0054         add_all_platforms = True
0055         note = dct[PLATFORM_UNKNOWN]
0056         del dct[PLATFORM_UNKNOWN]
0057     if add_all_platforms:
0058         for platform in available_platforms:
0059             if platform not in dct:
0060                 dct[platform] = note
0061 
0062 
0063 def create_metainfo(path) -> Optional[Dict[str, Any]]:
0064     """Look for a `metadata.yaml` file and create a dictionary out it.
0065 
0066     Args:
0067         path: (string) the current path to search.
0068     Returns:
0069         A dictionary containing all the parsed information, or `None` if it
0070     did not fulfill some conditions.
0071     """
0072 
0073     metainfo: Optional[Dict[str, Any]]
0074 
0075     if not os.path.isdir(path):
0076         return None
0077 
0078     try:
0079         metainfo_file = os.path.join(path, 'metainfo.yaml')
0080     except UnicodeDecodeError as e:
0081         logging.warning('Unusual base path {!r} for metainfo.yaml'.format(path))
0082         return None
0083     if not os.path.isfile(metainfo_file):
0084         return None
0085 
0086     try:
0087         metainfo = yaml.safe_load(open(metainfo_file))
0088     except Exception as e:
0089         print(e)
0090         logging.warning(f'Could not load metainfo.yaml for {path}, skipping it')
0091         return None
0092 
0093     if metainfo is None:
0094         logging.warning(f'Empty metainfo.yaml for {path}, skipping it')
0095         return None
0096 
0097     if 'subgroup' in metainfo and 'group' not in metainfo:
0098         logging.warning(f'Subgroup but no group in {path}, skipping it')
0099         return None
0100 
0101     # Suppose we get a relative path passed in (e.g. on the command-line,
0102     # path .. because we're building the dox in a subdirectory of a source
0103     # checkout) then we don't want dirname to be "..", but the name that
0104     # that resolves to.
0105     dirname = os.path.basename(os.path.abspath(path))
0106     if 'fancyname' in metainfo:
0107         fancyname = metainfo['fancyname']
0108     else:
0109         fancyname = utils.parse_fancyname(path)
0110 
0111     if not fancyname:
0112         logging.warning(f'Could not find fancy name for {path}, skipping it')
0113         return None
0114     # A fancyname has 1st char capitalized
0115     fancyname = fancyname[0].capitalize() + fancyname[1:]
0116 
0117     if 'repo_id' in metainfo:
0118         repo_id = metainfo['repo_id']
0119     else:
0120         repo_id = dirname
0121 
0122     qdoc: bool = False
0123 
0124     if 'qdoc' in metainfo:
0125         qdoc = metainfo['qdoc']
0126 
0127     metainfo.update({
0128         'fancyname': fancyname,
0129         'name': dirname,
0130         'repo_id': repo_id,
0131         'public_lib': metainfo.get('public_lib', False),
0132         'dependency_diagram': None,
0133         'path': path,
0134         'qdoc': qdoc,
0135     })
0136 
0137     # replace legacy platform names
0138     if 'platforms' in metainfo:
0139         platforms = metainfo['platforms']
0140         for index, x in enumerate(platforms):
0141             if x['name'] == "MacOSX":
0142                 x['name'] = "macOS"
0143                 platforms[index] = x
0144                 logging.warning('{fancyname} uses outdated platform name, please replace "MacOSX" with "macOS".'
0145                                 .format_map(metainfo))
0146         metainfo.update({'platforms': platforms})
0147     if 'group_info' in metainfo:
0148         group_info = metainfo['group_info']
0149         if 'platforms' in group_info:
0150             platforms = group_info['platforms']
0151             for index, x in enumerate(platforms):
0152                 if "MacOSX" in x:
0153                     x = x.replace("MacOSX", "macOS")
0154                     platforms[index] = x
0155                     logging.warning('Group {fancyname} uses outdated platform name, please replace "MacOSX" with "macOS".'
0156                                     .format_map(group_info))
0157             group_info.update({'platforms': platforms})
0158 
0159     return metainfo
0160 
0161 
0162 def parse_tree(rootdir):
0163     """Recursively call create_metainfo() in subdirs of rootdir
0164 
0165     Args:
0166         rootdir: (string)  Top level directory containing the libraries.
0167 
0168     Returns:
0169         A list of metainfo dictionary (see create_metainfo()).
0170 
0171     """
0172     metalist = []
0173     for path, dirs, _ in os.walk(rootdir, topdown=True):
0174         # We don't want to do the recursion in the dotdirs
0175         dirs[:] = [d for d in dirs if not d[0] == '.']
0176         metainfo = create_metainfo(path)
0177         if metainfo is not None:
0178             # There was a metainfo.yaml, which means it was
0179             # the top of a checked-out repository. Stop processing,
0180             # because we do not support having repo B checked out (even
0181             # as a submodule) inside repo A.
0182             #
0183             # There are exceptions: messagelib (KDE PIM) contains
0184             # multiple subdirectories with their own metainfo.yaml,
0185             # which are listed as public sources.
0186             dirs[:] = [d for d in dirs if d in metainfo.get('public_source_dirs', [])]
0187             if metainfo['public_lib'] or 'group_info' in metainfo:
0188                 metalist.append(metainfo)
0189             else:
0190                 logging.warning('{name} has no public libraries'.format_map(metainfo))
0191 
0192     return metalist
0193 
0194 
0195 def sort_metainfo(metalist, all_maintainers):
0196     """Extract the structure (Product/Subproduct/Library) from the metainfo
0197     list.
0198 
0199     Args:
0200         metalist: (list of dict) lists of the metainfo extracted in parse_tree().
0201         all_maintainers: (dict of dict)  all possible maintainers.
0202 
0203     Returns:
0204         A list of Products, a list of groups (which are products containing
0205     several libraries), a list of Libraries and the available platforms.
0206     """
0207     products = dict()
0208     groups = []
0209     libraries = []
0210     available_platforms = {'Windows', 'macOS', 'Linux', 'Android', 'FreeBSD'}
0211 
0212     # First extract the structural info
0213     for metainfo in metalist:
0214         product = extract_product(metainfo, all_maintainers)
0215         if product is not None:
0216             products[product.name] = product
0217 
0218     # Second extract the libraries
0219     for metainfo in metalist:
0220         try:
0221             platforms = metainfo['platforms']
0222             platform_lst = [x['name'] for x in platforms
0223                             if x['name'] not in (PLATFORM_ALL,
0224                                                  PLATFORM_UNKNOWN)]
0225 
0226             available_platforms.update(set(platform_lst))
0227         except (KeyError, TypeError):
0228             logging.warning('{fancyname} library lacks valid platform definitions'
0229                             .format_map(metainfo))
0230             platforms = [dict(name=PLATFORM_UNKNOWN)]
0231 
0232         dct = dict((x['name'], x.get('note', '')) for x in platforms)
0233 
0234         expand_platform_all(dct, available_platforms)
0235         platforms = dct
0236 
0237         if metainfo['public_lib']:
0238             lib = Library(metainfo, products, platforms, all_maintainers)
0239             libraries.append(lib)
0240 
0241     groups = []
0242     for key in products.copy():
0243         if len(products[key].libraries) == 0:
0244             del products[key]
0245         elif products[key].part_of_group:
0246             groups.append(products[key])
0247 
0248     return list(products.values()), groups, libraries, available_platforms
0249 
0250 
0251 def extract_product(metainfo, all_maintainers):
0252     """Extract a product from a metainfo dictionary.
0253 
0254     Args:
0255         metainfo: (dict) metainfo created by the create_metainfo() function.
0256         all_maintainers: (dict of dict) all possible maintainers
0257 
0258     Returns:
0259         A Product or None if the metainfo does not describe a product.
0260     """
0261 
0262     if 'group_info' not in metainfo and 'group' in metainfo:
0263         # This is not a product but a simple lib
0264         return None
0265 
0266     try:
0267         product = Product(metainfo, all_maintainers)
0268         return product
0269     except ValueError as e:
0270         logging.error(e)
0271         return None