linter.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import lxml
  4. import os
  5. import os.path
  6. import re
  7. import requests
  8. from AdvancedHTMLParser import AdvancedHTMLParser
  9. from glob import iglob
  10. from urllib.parse import urlparse
  11. from cmsis.PackLint import PackLinter, VersionParser
  12. from cmsis.Pack import Pack, Api, SemanticVersion
  13. def create():
  14. return CmsisPackLinter()
  15. class CmsisPackVersionParser(VersionParser):
  16. def __init__(self, logger = None):
  17. super().__init__(logger)
  18. self._packs = {}
  19. def _file_version_(self, file):
  20. v = self._regex_(file, ".*@version\s+([vV])?([0-9]+[.][0-9]+([.][0-9]+)?).*", 2)
  21. if not v:
  22. v = self._regex_(file, ".*\$Revision:\s+([vV])?([0-9]+[.][0-9]+([.][0-9]+)?).*", 2)
  23. return v
  24. def _cmtable_(self, file, skip = 0):
  25. table = ""
  26. dump = False
  27. with open(file, 'r') as f:
  28. for l in f:
  29. if not dump and l.strip() == "<table class=\"cmtable\" summary=\"Revision History\">":
  30. if skip > 0:
  31. skip -= 1
  32. else:
  33. dump = True
  34. if dump:
  35. table += l.replace("<br>", "\\n").replace("\\<", "&lt;").replace("\\>", "&gt;")
  36. if l.strip() == "</table>":
  37. break
  38. if table:
  39. table = lxml.etree.fromstring(table)
  40. return table
  41. return None
  42. def _revhistory_(self, file, skip = 0):
  43. table = self._cmtable_(file, skip)
  44. if table is not None:
  45. m = re.match("[Vv]?(\d+.\d+(.\d+)?)", table[1][0].text)
  46. if m:
  47. return SemanticVersion(m.group(1))
  48. else:
  49. self._logger.info("Revision History table not found in "+file)
  50. return None
  51. def readme_md(self, file):
  52. """Get the latest release version from README.md"""
  53. return self._regex_(file, ".*repository contains the CMSIS Version ([0-9]+[.][0-9]+([.][0-9]+)?).*")
  54. def _dxy(self, file):
  55. """Get the PROJECT_NUMBER from a Doxygen configuration file."""
  56. return self._regex_(file, "PROJECT_NUMBER\s*=\s*\"(Version\s+)?(\d+.\d+(.\d+)?)\"", 2)
  57. def _pdsc(self, file, component = None):
  58. pack = None
  59. if not file in self._packs:
  60. pack = Pack(file, None)
  61. self._packs[file] = pack
  62. else:
  63. pack = self._packs[file]
  64. if component:
  65. history = pack.history()
  66. for r in sorted(history.keys(), reverse=True):
  67. m = re.search(re.escape(component)+"(:)?\s+[Vv]?(\d+.\d+(.\d+)?)", history[r], re.MULTILINE)
  68. if m:
  69. return SemanticVersion(m.group(2))
  70. else:
  71. return pack.version()
  72. def _h(self, file):
  73. return self._file_version_(file)
  74. def _c(self, file):
  75. return self._file_version_(file)
  76. def _s(self, file):
  77. return self._file_version_(file)
  78. def _xsd(self, file, rev=False, history=False):
  79. if rev:
  80. return self._all_(file)
  81. elif history:
  82. return self._regex_(file, ".*[0-9]+\. [A-Z][a-z]+ [12][0-9]+: (v)?(\d+.\d+(.\d+)?).*", 2)
  83. else:
  84. xsd = lxml.etree.parse(str(file)).getroot()
  85. return SemanticVersion(xsd.get("version", None))
  86. def overview_txt(self, file, skip = 0):
  87. return self._revhistory_(file, skip)
  88. def introduction_txt(self, file, component = None):
  89. table = self._cmtable_(file)
  90. if table is None:
  91. return None
  92. if component:
  93. m = re.search(re.escape(component)+"\s+[Vv]?(\d+.\d+(.\d+)?)", table[1][1].text, re.MULTILINE)
  94. if m:
  95. return SemanticVersion(m.group(1))
  96. else:
  97. return SemanticVersion(table[1][0].text)
  98. def dap_txt(self, file, skip = 0):
  99. return self._revhistory_(file, skip)
  100. def general_txt(self, file, skip = 0):
  101. return self._revhistory_(file, skip)
  102. def history_txt(self, file, skip = 0):
  103. return self._revhistory_(file, skip)
  104. def _all_(self, file):
  105. """Get the version or revision tag from an arbitrary file."""
  106. version = self._regex_(file, ".*@version\s+([vV])?([0-9]+[.][0-9]+([.][0-9]+)?).*", 2)
  107. if not version:
  108. version = self._regex_(file, ".*\$Revision:\s+([vV])?([0-9]+[.][0-9]+([.][0-9]+)?).*", 2)
  109. return version
  110. class CmsisPackLinter(PackLinter):
  111. def __init__(self, pdsc = "ARM.CMSIS.pdsc"):
  112. super().__init__(pdsc)
  113. self._versionParser = CmsisPackVersionParser(self._logger)
  114. def pack_version(self):
  115. return self._pack.version()
  116. def cmsis_corem_component(self):
  117. rte = { 'components' : set(), 'Dcore' : "Cortex-M3", 'Dvendor' : "*", 'Dname' : "*", 'Dtz' : "*", 'Dsecure' : "*", 'Tcompiler' : "*", 'Toptions' : "*" }
  118. cs = self._pack.component_by_name(rte, "CMSIS.CORE")
  119. cvs = { SemanticVersion(c.version()) for c in cs }
  120. if len(cvs) > 1:
  121. self.warning("Not all CMSIS-Core(M) components have same version information: %s", str([ (c.name(), c.version()) for c in cs ]))
  122. return cvs.pop()
  123. def cmsis_corea_component(self):
  124. rte = { 'components' : set(), 'Dcore' : "Cortex-A9", 'Dvendor' : "*", 'Dname' : "*", 'Dtz' : "*", 'Dsecure' : "*", 'Tcompiler' : "*", 'Toptions' : "*" }
  125. cs = self._pack.component_by_name(rte, "CMSIS.CORE")
  126. cvs = { SemanticVersion(c.version()) for c in cs }
  127. if len(cvs) > 1:
  128. self.warning("Not all CMSIS-Core(A) components have same version information: %s", str([ (c.name(), c.version()) for c in cs ]))
  129. return cvs.pop()
  130. def cmsis_rtos2_api(self):
  131. cs = self._pack.components_by_name("CMSIS.RTOS2")
  132. cvs = { SemanticVersion(c.version()) for c in cs }
  133. if len(cvs) > 1:
  134. self.warning("Not all CMSIS-RTOS2 APIs have same version information: %s", str([ (c.name(), c.version()) for c in cs ]))
  135. return cvs.pop()
  136. def cmsis_rtx5_component(self):
  137. cs = self._pack.components_by_name("CMSIS.RTOS2.Keil RTX5*")
  138. cvs = { (SemanticVersion(c.version()), SemanticVersion(c.apiversion())) for c in cs }
  139. if len(cvs) == 1:
  140. return cvs.pop()
  141. elif len(cvs) > 1:
  142. self.warning("Not all RTX5 components have same version information: %s", str([ (c.name(), c.version(), c.apiversion()) for c in cs ]))
  143. return None, None
  144. def check_general(self):
  145. """CMSIS version"""
  146. v = self.pack_version()
  147. self.verify_version("README.md", v)
  148. self.verify_version("CMSIS/DoxyGen/General/general.dxy", v)
  149. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v)
  150. def check_build(self):
  151. """CMSIS-Build version"""
  152. v = self._versionParser.get_version("CMSIS/DoxyGen/Build/Build.dxy")
  153. self.verify_version("CMSIS/DoxyGen/Build/src/General.txt", v)
  154. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v, component="CMSIS-Build")
  155. self.verify_version(self._pack.location(), v, component="CMSIS-Build")
  156. def check_corem(self):
  157. """CMSIS-Core(M) version"""
  158. v = self.cmsis_corem_component()
  159. self.verify_version("CMSIS/DoxyGen/Core/core.dxy", v)
  160. self.verify_version("CMSIS/DoxyGen/Core/src/Overview.txt", v)
  161. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v, component="CMSIS-Core (Cortex-M)")
  162. self.verify_version(self._pack.location(), v, component="CMSIS-Core(M)")
  163. def check_corea(self):
  164. """CMSIS-Core(A) version"""
  165. v = self.cmsis_corea_component()
  166. self.verify_version("CMSIS/DoxyGen/Core_A/core_A.dxy", v)
  167. self.verify_version("CMSIS/DoxyGen/Core_A/src/Overview.txt", v)
  168. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v, component="CMSIS-Core (Cortex-A)")
  169. self.verify_version(self._pack.location(), v, component="CMSIS-Core(A)")
  170. def check_dap(self):
  171. """CMSIS-DAP version"""
  172. v = self._versionParser.get_version("CMSIS/DoxyGen/DAP/dap.dxy")
  173. self.verify_version("CMSIS/DoxyGen/DAP/src/dap.txt", v)
  174. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v, component="CMSIS-DAP")
  175. self.verify_version(self._pack.location(), v, component="CMSIS-DAP")
  176. def check_driver(self):
  177. """CMSIS-Driver version"""
  178. v = self._versionParser.get_version("CMSIS/DoxyGen/Driver/Driver.dxy")
  179. self.verify_version("CMSIS/DoxyGen/Driver/src/General.txt", v)
  180. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v, component="CMSIS-Driver")
  181. self.verify_version(self._pack.location(), v, component="CMSIS-Driver")
  182. def check_dsp(self):
  183. """CMSIS-DSP version"""
  184. v = self._versionParser.get_version("CMSIS/DoxyGen/DSP/dsp.dxy")
  185. self.verify_version("CMSIS/DoxyGen/DSP/src/history.txt", v)
  186. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v, component="CMSIS-DSP")
  187. self.verify_version(self._pack.location(), v, component="CMSIS-DSP")
  188. def check_nn(self):
  189. """CMSIS-NN version"""
  190. v = self._versionParser.get_version("CMSIS/DoxyGen/NN/nn.dxy")
  191. self.verify_version("CMSIS/DoxyGen/NN/src/history.txt", v)
  192. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v, component="CMSIS-NN")
  193. self.verify_version(self._pack.location(), v, component="CMSIS-NN")
  194. def check_pack(self):
  195. """CMSIS-Pack version"""
  196. v = self._versionParser.get_version("CMSIS/Utilities/PACK.xsd")
  197. self.verify_version("CMSIS/Utilities/PACK.xsd:Revision", v, rev=True)
  198. self.verify_version("CMSIS/Utilities/PACK.xsd:History", v, history=True)
  199. self.verify_version("CMSIS/DoxyGen/Pack/Pack.dxy", v)
  200. self.verify_version("CMSIS/DoxyGen/Pack/src/General.txt", v)
  201. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", v, component="CMSIS-Pack")
  202. self.verify_version(self._pack.location(), v, component="CMSIS-Pack")
  203. def check_rtos2(self):
  204. """CMSIS-RTOS2 version"""
  205. api = self.cmsis_rtos2_api()
  206. v, a = self.cmsis_rtx5_component()
  207. self.verify_version("CMSIS/DoxyGen/RTOS2/rtos.dxy", api)
  208. self.verify_version("CMSIS/DoxyGen/RTOS2/src/history.txt", api, skip=0)
  209. self.verify_version("CMSIS/DoxyGen/General/src/introduction.txt", api, component="CMSIS-RTOS")
  210. # self.verify_version(self._pack.location(), v, component="CMSIS-RTOS2")
  211. if a and not api.match(a):
  212. self.warning("RTX5 API version (%s) does not match RTOS2 API version (%s)!", a, api)
  213. self.verify_version("CMSIS/DoxyGen/RTOS2/src/history.txt", v, skip=1)
  214. def check_files(self):
  215. """Files referenced by pack description"""
  216. # Check schema of pack description
  217. self.verify_schema(self._pack.location(), "CMSIS/Utilities/PACK.xsd")
  218. # Check schema of SVD files
  219. svdfiles = { d.svdfile() for d in self._pack.devices() if d.svdfile() }
  220. for svd in svdfiles:
  221. if os.path.exists(svd):
  222. self.verify_schema(svd, "CMSIS/Utilities/CMSIS-SVD.xsd")
  223. else:
  224. self.warning("SVD File does not exist: %s!", svd)
  225. # Check component file version
  226. for c in self._pack.components():
  227. cv = c.version()
  228. for f in c.files():
  229. hv = f.version()
  230. if c is Api:
  231. if f.isHeader():
  232. if not hv:
  233. self.verify_version(f.location(), cv)
  234. if hv:
  235. self.verify_version(f.location(), SemanticVersion(hv))
  236. def check_doc(self, pattern="./CMSIS/Documentation/**/*.html"):
  237. """Documentation"""
  238. self.debug("Using pattern '%s'", pattern)
  239. for html in iglob(pattern, recursive=True):
  240. parser = AdvancedHTMLParser()
  241. parser.parseFile(html)
  242. links = parser.getElementsByTagName("a")
  243. if links:
  244. self.info("%s: Checking links ...", html)
  245. else:
  246. self.debug("%s: No links found...", html)
  247. for l in links:
  248. href = l.getAttribute("href")
  249. if href:
  250. href = urlparse(href)
  251. if href.scheme in ["http", "https", "ftp", "ftps" ]:
  252. try:
  253. self.info("%s: Checking link to %s...", html, href.geturl())
  254. r = requests.head(href.geturl(), headers={'user-agent' : "packlint/1.0"}, timeout=10)
  255. if r.status_code >= 400:
  256. self.debug(f'HEAD method failed with HTTP-{r.status_code}, falling back to GET method.')
  257. r = requests.get(href.geturl(), headers={'user-agent': "packlint/1.0"}, timeout=10)
  258. r.raise_for_status()
  259. except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as e:
  260. exc_info = None
  261. if self.loglevel() == logging.DEBUG:
  262. exc_info = e
  263. self.warning("%s: Broken web-link to %s!", html, href.geturl(), exc_info=exc_info)
  264. except requests.exceptions.Timeout as e:
  265. exc_info = None
  266. if self.loglevel() == logging.DEBUG:
  267. exc_info = e
  268. self.warning("%s: Timeout following web-link to %s.", html, href.geturl(), exc_info=exc_info)
  269. elif href.scheme == "javascript":
  270. pass
  271. elif not os.path.isabs(href.path):
  272. target = os.path.normpath(os.path.join(os.path.dirname(html), href.path))
  273. if not os.path.exists(target):
  274. self.warning("%s: Broken relative-link to %s!", html, href.path)
  275. else:
  276. self.warning("%s: Broken relative-link to %s!", html, href.path)