pkg-stats 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. #!/usr/bin/env python
  2. # Copyright (C) 2009 by Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. # General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  17. import argparse
  18. import datetime
  19. import fnmatch
  20. import os
  21. from collections import defaultdict
  22. import re
  23. import subprocess
  24. import sys
  25. INFRA_RE = re.compile("\$\(eval \$\(([a-z-]*)-package\)\)")
  26. class Package:
  27. all_licenses = list()
  28. all_license_files = list()
  29. all_versions = dict()
  30. def __init__(self, name, path):
  31. self.name = name
  32. self.path = path
  33. self.infras = None
  34. self.has_license = False
  35. self.has_license_files = False
  36. self.has_hash = False
  37. self.patch_count = 0
  38. self.warnings = 0
  39. self.current_version = None
  40. def pkgvar(self):
  41. return self.name.upper().replace("-", "_")
  42. def set_infra(self):
  43. """
  44. Fills in the .infras field
  45. """
  46. self.infras = list()
  47. with open(self.path, 'r') as f:
  48. lines = f.readlines()
  49. for l in lines:
  50. match = INFRA_RE.match(l)
  51. if not match:
  52. continue
  53. infra = match.group(1)
  54. if infra.startswith("host-"):
  55. self.infras.append(("host", infra[5:]))
  56. else:
  57. self.infras.append(("target", infra))
  58. def set_license(self):
  59. """
  60. Fills in the .has_license and .has_license_files fields
  61. """
  62. var = self.pkgvar()
  63. if var in self.all_licenses:
  64. self.has_license = True
  65. if var in self.all_license_files:
  66. self.has_license_files = True
  67. def set_hash_info(self):
  68. """
  69. Fills in the .has_hash field
  70. """
  71. hashpath = self.path.replace(".mk", ".hash")
  72. self.has_hash = os.path.exists(hashpath)
  73. def set_patch_count(self):
  74. """
  75. Fills in the .patch_count field
  76. """
  77. self.patch_count = 0
  78. pkgdir = os.path.dirname(self.path)
  79. for subdir, _, _ in os.walk(pkgdir):
  80. self.patch_count += len(fnmatch.filter(os.listdir(subdir), '*.patch'))
  81. def set_current_version(self):
  82. """
  83. Fills in the .current_version field
  84. """
  85. var = self.pkgvar()
  86. if var in self.all_versions:
  87. self.current_version = self.all_versions[var]
  88. def set_check_package_warnings(self):
  89. """
  90. Fills in the .warnings field
  91. """
  92. cmd = ["./utils/check-package"]
  93. pkgdir = os.path.dirname(self.path)
  94. for root, dirs, files in os.walk(pkgdir):
  95. for f in files:
  96. if f.endswith(".mk") or f.endswith(".hash") or f == "Config.in" or f == "Config.in.host":
  97. cmd.append(os.path.join(root, f))
  98. o = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[1]
  99. lines = o.splitlines()
  100. for line in lines:
  101. m = re.match("^([0-9]*) warnings generated", line)
  102. if m:
  103. self.warnings = int(m.group(1))
  104. return
  105. def __eq__(self, other):
  106. return self.path == other.path
  107. def __lt__(self, other):
  108. return self.path < other.path
  109. def __str__(self):
  110. return "%s (path='%s', license='%s', license_files='%s', hash='%s', patches=%d)" % \
  111. (self.name, self.path, self.has_license, self.has_license_files, self.has_hash, self.patch_count)
  112. def get_pkglist(npackages, package_list):
  113. """
  114. Builds the list of Buildroot packages, returning a list of Package
  115. objects. Only the .name and .path fields of the Package object are
  116. initialized.
  117. npackages: limit to N packages
  118. package_list: limit to those packages in this list
  119. """
  120. WALK_USEFUL_SUBDIRS = ["boot", "linux", "package", "toolchain"]
  121. WALK_EXCLUDES = ["boot/common.mk",
  122. "linux/linux-ext-.*.mk",
  123. "package/freescale-imx/freescale-imx.mk",
  124. "package/gcc/gcc.mk",
  125. "package/gstreamer/gstreamer.mk",
  126. "package/gstreamer1/gstreamer1.mk",
  127. "package/gtk2-themes/gtk2-themes.mk",
  128. "package/matchbox/matchbox.mk",
  129. "package/opengl/opengl.mk",
  130. "package/qt5/qt5.mk",
  131. "package/x11r7/x11r7.mk",
  132. "package/doc-asciidoc.mk",
  133. "package/pkg-.*.mk",
  134. "package/nvidia-tegra23/nvidia-tegra23.mk",
  135. "toolchain/toolchain-external/pkg-toolchain-external.mk",
  136. "toolchain/toolchain-external/toolchain-external.mk",
  137. "toolchain/toolchain.mk",
  138. "toolchain/helpers.mk",
  139. "toolchain/toolchain-wrapper.mk"]
  140. packages = list()
  141. count = 0
  142. for root, dirs, files in os.walk("."):
  143. rootdir = root.split("/")
  144. if len(rootdir) < 2:
  145. continue
  146. if rootdir[1] not in WALK_USEFUL_SUBDIRS:
  147. continue
  148. for f in files:
  149. if not f.endswith(".mk"):
  150. continue
  151. # Strip ending ".mk"
  152. pkgname = f[:-3]
  153. if package_list and pkgname not in package_list:
  154. continue
  155. pkgpath = os.path.join(root, f)
  156. skip = False
  157. for exclude in WALK_EXCLUDES:
  158. # pkgpath[2:] strips the initial './'
  159. if re.match(exclude, pkgpath[2:]):
  160. skip = True
  161. continue
  162. if skip:
  163. continue
  164. p = Package(pkgname, pkgpath)
  165. packages.append(p)
  166. count += 1
  167. if npackages and count == npackages:
  168. return packages
  169. return packages
  170. def package_init_make_info():
  171. # Licenses
  172. o = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y",
  173. "-s", "printvars", "VARS=%_LICENSE"])
  174. for l in o.splitlines():
  175. # Get variable name and value
  176. pkgvar, value = l.split("=")
  177. # If present, strip HOST_ from variable name
  178. if pkgvar.startswith("HOST_"):
  179. pkgvar = pkgvar[5:]
  180. # Strip _LICENSE
  181. pkgvar = pkgvar[:-8]
  182. # If value is "unknown", no license details available
  183. if value == "unknown":
  184. continue
  185. Package.all_licenses.append(pkgvar)
  186. # License files
  187. o = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y",
  188. "-s", "printvars", "VARS=%_LICENSE_FILES"])
  189. for l in o.splitlines():
  190. # Get variable name and value
  191. pkgvar, value = l.split("=")
  192. # If present, strip HOST_ from variable name
  193. if pkgvar.startswith("HOST_"):
  194. pkgvar = pkgvar[5:]
  195. if pkgvar.endswith("_MANIFEST_LICENSE_FILES"):
  196. continue
  197. # Strip _LICENSE_FILES
  198. pkgvar = pkgvar[:-14]
  199. Package.all_license_files.append(pkgvar)
  200. # Version
  201. o = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y",
  202. "-s", "printvars", "VARS=%_VERSION"])
  203. # We process first the host package VERSION, and then the target
  204. # package VERSION. This means that if a package exists in both
  205. # target and host variants, with different version numbers
  206. # (unlikely), we'll report the target version number.
  207. version_list = o.splitlines()
  208. version_list = [x for x in version_list if x.startswith("HOST_")] + \
  209. [x for x in version_list if not x.startswith("HOST_")]
  210. for l in version_list:
  211. # Get variable name and value
  212. pkgvar, value = l.split("=")
  213. # If present, strip HOST_ from variable name
  214. if pkgvar.startswith("HOST_"):
  215. pkgvar = pkgvar[5:]
  216. if pkgvar.endswith("_DL_VERSION"):
  217. continue
  218. # Strip _VERSION
  219. pkgvar = pkgvar[:-8]
  220. Package.all_versions[pkgvar] = value
  221. def calculate_stats(packages):
  222. stats = defaultdict(int)
  223. for pkg in packages:
  224. # If packages have multiple infra, take the first one. For the
  225. # vast majority of packages, the target and host infra are the
  226. # same. There are very few packages that use a different infra
  227. # for the host and target variants.
  228. if len(pkg.infras) > 0:
  229. infra = pkg.infras[0][1]
  230. stats["infra-%s" % infra] += 1
  231. else:
  232. stats["infra-unknown"] += 1
  233. if pkg.has_license:
  234. stats["license"] += 1
  235. else:
  236. stats["no-license"] += 1
  237. if pkg.has_license_files:
  238. stats["license-files"] += 1
  239. else:
  240. stats["no-license-files"] += 1
  241. if pkg.has_hash:
  242. stats["hash"] += 1
  243. else:
  244. stats["no-hash"] += 1
  245. stats["patches"] += pkg.patch_count
  246. return stats
  247. html_header = """
  248. <head>
  249. <script src=\"https://www.kryogenix.org/code/browser/sorttable/sorttable.js\"></script>
  250. <style type=\"text/css\">
  251. table {
  252. width: 100%;
  253. }
  254. td {
  255. border: 1px solid black;
  256. }
  257. td.centered {
  258. text-align: center;
  259. }
  260. td.wrong {
  261. background: #ff9a69;
  262. }
  263. td.correct {
  264. background: #d2ffc4;
  265. }
  266. td.nopatches {
  267. background: #d2ffc4;
  268. }
  269. td.somepatches {
  270. background: #ffd870;
  271. }
  272. td.lotsofpatches {
  273. background: #ff9a69;
  274. }
  275. </style>
  276. <title>Statistics of Buildroot packages</title>
  277. </head>
  278. <a href=\"#results\">Results</a><br/>
  279. <p id=\"sortable_hint\"></p>
  280. """
  281. html_footer = """
  282. </body>
  283. <script>
  284. if (typeof sorttable === \"object\") {
  285. document.getElementById(\"sortable_hint\").innerHTML =
  286. \"hint: the table can be sorted by clicking the column headers\"
  287. }
  288. </script>
  289. </html>
  290. """
  291. def infra_str(infra_list):
  292. if not infra_list:
  293. return "Unknown"
  294. elif len(infra_list) == 1:
  295. return "<b>%s</b><br/>%s" % (infra_list[0][1], infra_list[0][0])
  296. elif infra_list[0][1] == infra_list[1][1]:
  297. return "<b>%s</b><br/>%s + %s" % \
  298. (infra_list[0][1], infra_list[0][0], infra_list[1][0])
  299. else:
  300. return "<b>%s</b> (%s)<br/><b>%s</b> (%s)" % \
  301. (infra_list[0][1], infra_list[0][0],
  302. infra_list[1][1], infra_list[1][0])
  303. def boolean_str(b):
  304. if b:
  305. return "Yes"
  306. else:
  307. return "No"
  308. def dump_html_pkg(f, pkg):
  309. f.write(" <tr>\n")
  310. f.write(" <td>%s</td>\n" % pkg.path[2:])
  311. # Patch count
  312. td_class = ["centered"]
  313. if pkg.patch_count == 0:
  314. td_class.append("nopatches")
  315. elif pkg.patch_count < 5:
  316. td_class.append("somepatches")
  317. else:
  318. td_class.append("lotsofpatches")
  319. f.write(" <td class=\"%s\">%s</td>\n" %
  320. (" ".join(td_class), str(pkg.patch_count)))
  321. # Infrastructure
  322. infra = infra_str(pkg.infras)
  323. td_class = ["centered"]
  324. if infra == "Unknown":
  325. td_class.append("wrong")
  326. else:
  327. td_class.append("correct")
  328. f.write(" <td class=\"%s\">%s</td>\n" %
  329. (" ".join(td_class), infra_str(pkg.infras)))
  330. # License
  331. td_class = ["centered"]
  332. if pkg.has_license:
  333. td_class.append("correct")
  334. else:
  335. td_class.append("wrong")
  336. f.write(" <td class=\"%s\">%s</td>\n" %
  337. (" ".join(td_class), boolean_str(pkg.has_license)))
  338. # License files
  339. td_class = ["centered"]
  340. if pkg.has_license_files:
  341. td_class.append("correct")
  342. else:
  343. td_class.append("wrong")
  344. f.write(" <td class=\"%s\">%s</td>\n" %
  345. (" ".join(td_class), boolean_str(pkg.has_license_files)))
  346. # Hash
  347. td_class = ["centered"]
  348. if pkg.has_hash:
  349. td_class.append("correct")
  350. else:
  351. td_class.append("wrong")
  352. f.write(" <td class=\"%s\">%s</td>\n" %
  353. (" ".join(td_class), boolean_str(pkg.has_hash)))
  354. # Current version
  355. if len(pkg.current_version) > 20:
  356. current_version = pkg.current_version[:20] + "..."
  357. else:
  358. current_version = pkg.current_version
  359. f.write(" <td class=\"centered\">%s</td>\n" % current_version)
  360. # Warnings
  361. td_class = ["centered"]
  362. if pkg.warnings == 0:
  363. td_class.append("correct")
  364. else:
  365. td_class.append("wrong")
  366. f.write(" <td class=\"%s\">%d</td>\n" %
  367. (" ".join(td_class), pkg.warnings))
  368. f.write(" </tr>\n")
  369. def dump_html_all_pkgs(f, packages):
  370. f.write("""
  371. <table class=\"sortable\">
  372. <tr>
  373. <td>Package</td>
  374. <td class=\"centered\">Patch count</td>
  375. <td class=\"centered\">Infrastructure</td>
  376. <td class=\"centered\">License</td>
  377. <td class=\"centered\">License files</td>
  378. <td class=\"centered\">Hash file</td>
  379. <td class=\"centered\">Current version</td>
  380. <td class=\"centered\">Warnings</td>
  381. </tr>
  382. """)
  383. for pkg in sorted(packages):
  384. dump_html_pkg(f, pkg)
  385. f.write("</table>")
  386. def dump_html_stats(f, stats):
  387. f.write("<a id=\"results\"></a>\n")
  388. f.write("<table>\n")
  389. infras = [infra[6:] for infra in stats.keys() if infra.startswith("infra-")]
  390. for infra in infras:
  391. f.write(" <tr><td>Packages using the <i>%s</i> infrastructure</td><td>%s</td></tr>\n" %
  392. (infra, stats["infra-%s" % infra]))
  393. f.write(" <tr><td>Packages having license information</td><td>%s</td></tr>\n" %
  394. stats["license"])
  395. f.write(" <tr><td>Packages not having license information</td><td>%s</td></tr>\n" %
  396. stats["no-license"])
  397. f.write(" <tr><td>Packages having license files information</td><td>%s</td></tr>\n" %
  398. stats["license-files"])
  399. f.write(" <tr><td>Packages not having license files information</td><td>%s</td></tr>\n" %
  400. stats["no-license-files"])
  401. f.write(" <tr><td>Packages having a hash file</td><td>%s</td></tr>\n" %
  402. stats["hash"])
  403. f.write(" <tr><td>Packages not having a hash file</td><td>%s</td></tr>\n" %
  404. stats["no-hash"])
  405. f.write(" <tr><td>Total number of patches</td><td>%s</td></tr>\n" %
  406. stats["patches"])
  407. f.write("</table>\n")
  408. def dump_gen_info(f):
  409. # Updated on Mon Feb 19 08:12:08 CET 2018, Git commit aa77030b8f5e41f1c53eb1c1ad664b8c814ba032
  410. o = subprocess.check_output(["git", "log", "master", "-n", "1", "--pretty=format:%H"])
  411. git_commit = o.splitlines()[0]
  412. f.write("<p><i>Updated on %s, git commit %s</i></p>\n" %
  413. (str(datetime.datetime.utcnow()), git_commit))
  414. def dump_html(packages, stats, output):
  415. with open(output, 'w') as f:
  416. f.write(html_header)
  417. dump_html_all_pkgs(f, packages)
  418. dump_html_stats(f, stats)
  419. dump_gen_info(f)
  420. f.write(html_footer)
  421. def parse_args():
  422. parser = argparse.ArgumentParser()
  423. parser.add_argument('-o', dest='output', action='store', required=True,
  424. help='HTML output file')
  425. parser.add_argument('-n', dest='npackages', type=int, action='store',
  426. help='Number of packages')
  427. parser.add_argument('-p', dest='packages', action='store',
  428. help='List of packages (comma separated)')
  429. return parser.parse_args()
  430. def __main__():
  431. args = parse_args()
  432. if args.npackages and args.packages:
  433. print("ERROR: -n and -p are mutually exclusive")
  434. sys.exit(1)
  435. if args.packages:
  436. package_list = args.packages.split(",")
  437. else:
  438. package_list = None
  439. print("Build package list ...")
  440. packages = get_pkglist(args.npackages, package_list)
  441. print("Getting package make info ...")
  442. package_init_make_info()
  443. print("Getting package details ...")
  444. for pkg in packages:
  445. pkg.set_infra()
  446. pkg.set_license()
  447. pkg.set_hash_info()
  448. pkg.set_patch_count()
  449. pkg.set_check_package_warnings()
  450. pkg.set_current_version()
  451. print("Calculate stats")
  452. stats = calculate_stats(packages)
  453. print("Write HTML")
  454. dump_html(packages, stats, args.output)
  455. __main__()